mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-04-25 11:48:40 +02:00
Merge branch 'dns'
* dns: dns on MacOS: use divert sockets instead of 'fwd' rules. client.py: do DNS listener on the same port as the TCP listener. Move client._islocal() to helpers.islocal() in preparation for sharing. dns: add support for MacOS (but it doesn't work...) Oops, dns_done() crashed if the request had already been timed out. dns: trim DNS channel handlers after a response, or after a timeout. dns: extract 'nameserver' lines from /etc/resolv.conf Extremely basic, but functional, DNS proxying support (--dns option)
This commit is contained in:
commit
0bf0351d9b
73
client.py
73
client.py
@ -1,4 +1,4 @@
|
|||||||
import struct, socket, select, errno, re, signal
|
import struct, socket, select, errno, re, signal, time
|
||||||
import compat.ssubprocess as ssubprocess
|
import compat.ssubprocess as ssubprocess
|
||||||
import helpers, ssnet, ssh, ssyslog
|
import helpers, ssnet, ssh, ssyslog
|
||||||
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
||||||
@ -6,21 +6,6 @@ from helpers import *
|
|||||||
|
|
||||||
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
_extra_fd = os.open('/dev/null', os.O_RDONLY)
|
||||||
|
|
||||||
def _islocal(ip):
|
|
||||||
sock = socket.socket()
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
sock.bind((ip, 0))
|
|
||||||
except socket.error, e:
|
|
||||||
if e.args[0] == errno.EADDRNOTAVAIL:
|
|
||||||
return False # not a local IP
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
sock.close()
|
|
||||||
return True # it's a local IP, or there would have been an error
|
|
||||||
|
|
||||||
|
|
||||||
def got_signal(signum, frame):
|
def got_signal(signum, frame):
|
||||||
log('exiting on signal %d\n' % signum)
|
log('exiting on signal %d\n' % signum)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -111,14 +96,15 @@ def original_dst(sock):
|
|||||||
|
|
||||||
|
|
||||||
class FirewallClient:
|
class FirewallClient:
|
||||||
def __init__(self, port, subnets_include, subnets_exclude):
|
def __init__(self, port, subnets_include, subnets_exclude, dnsport):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.auto_nets = []
|
self.auto_nets = []
|
||||||
self.subnets_include = subnets_include
|
self.subnets_include = subnets_include
|
||||||
self.subnets_exclude = subnets_exclude
|
self.subnets_exclude = subnets_exclude
|
||||||
|
self.dnsport = dnsport
|
||||||
argvbase = ([sys.argv[0]] +
|
argvbase = ([sys.argv[0]] +
|
||||||
['-v'] * (helpers.verbose or 0) +
|
['-v'] * (helpers.verbose or 0) +
|
||||||
['--firewall', str(port)])
|
['--firewall', str(port), str(dnsport)])
|
||||||
if ssyslog._p:
|
if ssyslog._p:
|
||||||
argvbase += ['--syslog']
|
argvbase += ['--syslog']
|
||||||
argv_tries = [
|
argv_tries = [
|
||||||
@ -190,7 +176,7 @@ class FirewallClient:
|
|||||||
|
|
||||||
|
|
||||||
def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
|
def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
|
||||||
seed_hosts, auto_nets,
|
dnslistener, seed_hosts, auto_nets,
|
||||||
syslog, daemon):
|
syslog, daemon):
|
||||||
handlers = []
|
handlers = []
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
@ -282,7 +268,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
|
|||||||
dstip = original_dst(sock)
|
dstip = original_dst(sock)
|
||||||
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
|
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
|
||||||
dstip[0],dstip[1]))
|
dstip[0],dstip[1]))
|
||||||
if dstip[1] == listener.getsockname()[1] and _islocal(dstip[0]):
|
if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]):
|
||||||
debug1("-- ignored: that's my address!\n")
|
debug1("-- ignored: that's my address!\n")
|
||||||
sock.close()
|
sock.close()
|
||||||
return
|
return
|
||||||
@ -292,6 +278,30 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
|
|||||||
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
||||||
handlers.append(Handler([listener], onaccept))
|
handlers.append(Handler([listener], onaccept))
|
||||||
|
|
||||||
|
dnsreqs = {}
|
||||||
|
def dns_done(chan, data):
|
||||||
|
peer,timeout = dnsreqs.get(chan) or (None,None)
|
||||||
|
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
|
||||||
|
if peer:
|
||||||
|
del dnsreqs[chan]
|
||||||
|
debug3('doing sendto %r\n' % (peer,))
|
||||||
|
dnslistener.sendto(data, peer)
|
||||||
|
def ondns():
|
||||||
|
pkt,peer = dnslistener.recvfrom(4096)
|
||||||
|
now = time.time()
|
||||||
|
if pkt:
|
||||||
|
debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
|
||||||
|
chan = mux.next_channel()
|
||||||
|
dnsreqs[chan] = peer,now+30
|
||||||
|
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
|
||||||
|
mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
|
||||||
|
for chan,(peer,timeout) in dnsreqs.items():
|
||||||
|
if timeout < now:
|
||||||
|
del dnsreqs[chan]
|
||||||
|
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
|
||||||
|
if dnslistener:
|
||||||
|
handlers.append(Handler([dnslistener], ondns))
|
||||||
|
|
||||||
if seed_hosts != None:
|
if seed_hosts != None:
|
||||||
debug1('seed_hosts: %r\n' % seed_hosts)
|
debug1('seed_hosts: %r\n' % seed_hosts)
|
||||||
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
|
||||||
@ -307,7 +317,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
|
|||||||
mux.callback()
|
mux.callback()
|
||||||
|
|
||||||
|
|
||||||
def main(listenip, ssh_cmd, remotename, python, latency_control,
|
def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
|
||||||
seed_hosts, auto_nets,
|
seed_hosts, auto_nets,
|
||||||
subnets_include, subnets_exclude, syslog, daemon, pidfile):
|
subnets_include, subnets_exclude, syslog, daemon, pidfile):
|
||||||
if syslog:
|
if syslog:
|
||||||
@ -319,8 +329,7 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
|
|||||||
log("%s\n" % e)
|
log("%s\n" % e)
|
||||||
return 5
|
return 5
|
||||||
debug1('Starting sshuttle proxy.\n')
|
debug1('Starting sshuttle proxy.\n')
|
||||||
listener = socket.socket()
|
|
||||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
if listenip[1]:
|
if listenip[1]:
|
||||||
ports = [listenip[1]]
|
ports = [listenip[1]]
|
||||||
else:
|
else:
|
||||||
@ -330,8 +339,13 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
|
|||||||
debug2('Binding:')
|
debug2('Binding:')
|
||||||
for port in ports:
|
for port in ports:
|
||||||
debug2(' %d' % port)
|
debug2(' %d' % port)
|
||||||
|
listener = socket.socket()
|
||||||
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
try:
|
try:
|
||||||
listener.bind((listenip[0], port))
|
listener.bind((listenip[0], port))
|
||||||
|
dnslistener.bind((listenip[0], port))
|
||||||
bound = True
|
bound = True
|
||||||
break
|
break
|
||||||
except socket.error, e:
|
except socket.error, e:
|
||||||
@ -344,11 +358,20 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
|
|||||||
listenip = listener.getsockname()
|
listenip = listener.getsockname()
|
||||||
debug1('Listening on %r.\n' % (listenip,))
|
debug1('Listening on %r.\n' % (listenip,))
|
||||||
|
|
||||||
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
|
if dns:
|
||||||
|
dnsip = dnslistener.getsockname()
|
||||||
|
debug1('DNS listening on %r.\n' % (dnsip,))
|
||||||
|
dnsport = dnsip[1]
|
||||||
|
else:
|
||||||
|
dnsport = 0
|
||||||
|
dnslistener = None
|
||||||
|
dnslistener.bind((listenip[0], 0))
|
||||||
|
|
||||||
|
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _main(listener, fw, ssh_cmd, remotename,
|
return _main(listener, fw, ssh_cmd, remotename,
|
||||||
python, latency_control,
|
python, latency_control, dnslistener,
|
||||||
seed_hosts, auto_nets, syslog, daemon)
|
seed_hosts, auto_nets, syslog, daemon)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
130
firewall.py
130
firewall.py
@ -1,8 +1,11 @@
|
|||||||
import re, errno
|
import re, errno, socket, select, struct
|
||||||
import compat.ssubprocess as ssubprocess
|
import compat.ssubprocess as ssubprocess
|
||||||
import helpers, ssyslog
|
import helpers, ssyslog
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
|
# python doesn't have a definition for this
|
||||||
|
IPPROTO_DIVERT = 254
|
||||||
|
|
||||||
|
|
||||||
def ipt_chain_exists(name):
|
def ipt_chain_exists(name):
|
||||||
argv = ['iptables', '-t', 'nat', '-nL']
|
argv = ['iptables', '-t', 'nat', '-nL']
|
||||||
@ -49,7 +52,7 @@ def ipt_ttl(*args):
|
|||||||
# multiple copies shouldn't have overlapping subnets, or only the most-
|
# multiple copies shouldn't have overlapping subnets, or only the most-
|
||||||
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
# recently-started one will win (because we use "-I OUTPUT 1" instead of
|
||||||
# "-A OUTPUT").
|
# "-A OUTPUT").
|
||||||
def do_iptables(port, subnets):
|
def do_iptables(port, dnsport, subnets):
|
||||||
chain = 'sshuttle-%s' % port
|
chain = 'sshuttle-%s' % port
|
||||||
|
|
||||||
# basic cleanup/setup of chains
|
# basic cleanup/setup of chains
|
||||||
@ -59,12 +62,13 @@ def do_iptables(port, subnets):
|
|||||||
ipt('-F', chain)
|
ipt('-F', chain)
|
||||||
ipt('-X', chain)
|
ipt('-X', chain)
|
||||||
|
|
||||||
if subnets:
|
if subnets or dnsport:
|
||||||
ipt('-N', chain)
|
ipt('-N', chain)
|
||||||
ipt('-F', chain)
|
ipt('-F', chain)
|
||||||
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
ipt('-I', 'OUTPUT', '1', '-j', chain)
|
||||||
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
ipt('-I', 'PREROUTING', '1', '-j', chain)
|
||||||
|
|
||||||
|
if subnets:
|
||||||
# create new subnet entries. Note that we're sorting in a very
|
# create new subnet entries. Note that we're sorting in a very
|
||||||
# particular order: we need to go from most-specific (largest swidth)
|
# particular order: we need to go from most-specific (largest swidth)
|
||||||
# to least-specific, and at any given level of specificity, we want
|
# to least-specific, and at any given level of specificity, we want
|
||||||
@ -80,6 +84,15 @@ def do_iptables(port, subnets):
|
|||||||
'--dest', '%s/%s' % (snet,swidth),
|
'--dest', '%s/%s' % (snet,swidth),
|
||||||
'-p', 'tcp',
|
'-p', 'tcp',
|
||||||
'--to-ports', str(port))
|
'--to-ports', str(port))
|
||||||
|
|
||||||
|
if dnsport:
|
||||||
|
nslist = resolvconf_nameservers()
|
||||||
|
for ip in nslist:
|
||||||
|
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
||||||
|
'--dest', '%s/32' % ip,
|
||||||
|
'-p', 'udp',
|
||||||
|
'--dport', '53',
|
||||||
|
'--to-ports', str(dnsport))
|
||||||
|
|
||||||
|
|
||||||
def ipfw_rule_exists(n):
|
def ipfw_rule_exists(n):
|
||||||
@ -88,7 +101,7 @@ def ipfw_rule_exists(n):
|
|||||||
found = False
|
found = False
|
||||||
for line in p.stdout:
|
for line in p.stdout:
|
||||||
if line.startswith('%05d ' % n):
|
if line.startswith('%05d ' % n):
|
||||||
if not ('ipttl 42 setup keep-state' in line
|
if not ('ipttl 42' in line
|
||||||
or ('skipto %d' % (n+1)) in line
|
or ('skipto %d' % (n+1)) in line
|
||||||
or 'check-state' in line):
|
or 'check-state' in line):
|
||||||
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
||||||
@ -135,6 +148,39 @@ def sysctl_set(name, val):
|
|||||||
if val != oldval:
|
if val != oldval:
|
||||||
_changedctls.append(name)
|
_changedctls.append(name)
|
||||||
return _sysctl_set(name, val)
|
return _sysctl_set(name, val)
|
||||||
|
|
||||||
|
|
||||||
|
def _udp_unpack(p):
|
||||||
|
src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0])
|
||||||
|
dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0])
|
||||||
|
return src, dst
|
||||||
|
|
||||||
|
|
||||||
|
def _udp_repack(p, src, dst):
|
||||||
|
addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0])
|
||||||
|
ports = struct.pack('!HH', src[1], dst[1])
|
||||||
|
return p[:12] + addrs + ports + p[24:]
|
||||||
|
|
||||||
|
|
||||||
|
_real_dns_server = [None]
|
||||||
|
def _handle_diversion(divertsock, dnsport):
|
||||||
|
p,tag = divertsock.recvfrom(4096)
|
||||||
|
src,dst = _udp_unpack(p)
|
||||||
|
debug3('got diverted packet from %r to %r\n' % (src, dst))
|
||||||
|
if dst[1] == 53:
|
||||||
|
# outgoing DNS
|
||||||
|
debug3('...packet is a DNS request.\n')
|
||||||
|
_real_dns_server[0] = dst
|
||||||
|
dst = ('127.0.0.1', dnsport)
|
||||||
|
elif src[1] == dnsport:
|
||||||
|
if islocal(src[0]):
|
||||||
|
debug3('...packet is a DNS response.\n')
|
||||||
|
src = _real_dns_server[0]
|
||||||
|
else:
|
||||||
|
log('weird?! unexpected divert from %r to %r\n' % (src, dst))
|
||||||
|
assert(0)
|
||||||
|
newp = _udp_repack(p, src, dst)
|
||||||
|
divertsock.sendto(newp, tag)
|
||||||
|
|
||||||
|
|
||||||
def ipfw(*args):
|
def ipfw(*args):
|
||||||
@ -145,7 +191,7 @@ def ipfw(*args):
|
|||||||
raise Fatal('%r returned %d' % (argv, rv))
|
raise Fatal('%r returned %d' % (argv, rv))
|
||||||
|
|
||||||
|
|
||||||
def do_ipfw(port, subnets):
|
def do_ipfw(port, dnsport, subnets):
|
||||||
sport = str(port)
|
sport = str(port)
|
||||||
xsport = str(port+1)
|
xsport = str(port+1)
|
||||||
|
|
||||||
@ -158,13 +204,14 @@ def do_ipfw(port, subnets):
|
|||||||
oldval = _oldctls[name]
|
oldval = _oldctls[name]
|
||||||
_sysctl_set(name, oldval)
|
_sysctl_set(name, oldval)
|
||||||
|
|
||||||
if subnets:
|
if subnets or dnsport:
|
||||||
sysctl_set('net.inet.ip.fw.enable', 1)
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
||||||
sysctl_set('net.inet.ip.scopedroute', 0)
|
sysctl_set('net.inet.ip.scopedroute', 0)
|
||||||
|
|
||||||
ipfw('add', sport, 'check-state', 'ip',
|
ipfw('add', sport, 'check-state', 'ip',
|
||||||
'from', 'any', 'to', 'any')
|
'from', 'any', 'to', 'any')
|
||||||
|
|
||||||
|
if subnets:
|
||||||
# create new subnet entries
|
# create new subnet entries
|
||||||
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
||||||
if sexclude:
|
if sexclude:
|
||||||
@ -177,6 +224,65 @@ def do_ipfw(port, subnets):
|
|||||||
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
||||||
'not', 'ipttl', '42', 'keep-state', 'setup')
|
'not', 'ipttl', '42', 'keep-state', 'setup')
|
||||||
|
|
||||||
|
# This part is much crazier than it is on Linux, because MacOS (at least
|
||||||
|
# 10.6, and probably other versions, and maybe FreeBSD too) doesn't
|
||||||
|
# correctly fixup the dstip/dstport for UDP packets when it puts them
|
||||||
|
# through a 'fwd' rule. It also doesn't fixup the srcip/srcport in the
|
||||||
|
# response packet. In Linux iptables, all that happens magically for us,
|
||||||
|
# so we just redirect the packets and relax.
|
||||||
|
#
|
||||||
|
# On MacOS, we have to fix the ports ourselves. For that, we use a
|
||||||
|
# 'divert' socket, which receives raw packets and lets us mangle them.
|
||||||
|
#
|
||||||
|
# Here's how it works. Let's say the local DNS server is 1.1.1.1:53,
|
||||||
|
# and the remote DNS server is 2.2.2.2:53, and the local transproxy port
|
||||||
|
# is 10.0.0.1:12300, and a client machine is making a request from
|
||||||
|
# 10.0.0.5:9999. We see a packet like this:
|
||||||
|
# 10.0.0.5:9999 -> 1.1.1.1:53
|
||||||
|
# Since the destip:port matches one of our local nameservers, it will
|
||||||
|
# match a 'fwd' rule, thus grabbing it on the local machine. However,
|
||||||
|
# the local kernel will then see a packet addressed to *:53 and
|
||||||
|
# not know what to do with it; there's nobody listening on port 53. Thus,
|
||||||
|
# we divert it, rewriting it into this:
|
||||||
|
# 10.0.0.5:9999 -> 10.0.0.1:12300
|
||||||
|
# This gets proxied out to the server, which sends it to 2.2.2.2:53,
|
||||||
|
# and the answer comes back, and the proxy sends it back out like this:
|
||||||
|
# 10.0.0.1:12300 -> 10.0.0.5:9999
|
||||||
|
# But that's wrong! The original machine expected an answer from
|
||||||
|
# 1.1.1.1:53, so we have to divert the *answer* and rewrite it:
|
||||||
|
# 1.1.1.1:53 -> 10.0.0.5:9999
|
||||||
|
#
|
||||||
|
# See? Easy stuff.
|
||||||
|
if dnsport:
|
||||||
|
divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
|
||||||
|
IPPROTO_DIVERT)
|
||||||
|
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
|
||||||
|
|
||||||
|
nslist = resolvconf_nameservers()
|
||||||
|
for ip in nslist:
|
||||||
|
# relabel and then catch outgoing DNS requests
|
||||||
|
ipfw('add', sport, 'divert', sport,
|
||||||
|
'log', 'udp',
|
||||||
|
'from', 'any', 'to', '%s/32' % ip, '53',
|
||||||
|
'not', 'ipttl', '42')
|
||||||
|
# relabel DNS responses
|
||||||
|
ipfw('add', sport, 'divert', sport,
|
||||||
|
'log', 'udp',
|
||||||
|
'from', 'any', str(dnsport), 'to', 'any',
|
||||||
|
'not', 'ipttl', '42')
|
||||||
|
|
||||||
|
def do_wait():
|
||||||
|
while 1:
|
||||||
|
r,w,x = select.select([sys.stdin, divertsock], [], [])
|
||||||
|
if divertsock in r:
|
||||||
|
_handle_diversion(divertsock, dnsport)
|
||||||
|
if sys.stdin in r:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
do_wait = None
|
||||||
|
|
||||||
|
return do_wait
|
||||||
|
|
||||||
|
|
||||||
def program_exists(name):
|
def program_exists(name):
|
||||||
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
|
||||||
@ -185,6 +291,7 @@ def program_exists(name):
|
|||||||
if os.path.exists(fn):
|
if os.path.exists(fn):
|
||||||
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
hostmap = {}
|
hostmap = {}
|
||||||
def rewrite_etc_hosts(port):
|
def rewrite_etc_hosts(port):
|
||||||
HOSTSFILE='/etc/hosts'
|
HOSTSFILE='/etc/hosts'
|
||||||
@ -235,9 +342,11 @@ def restore_etc_hosts(port):
|
|||||||
# exit. In case that fails, it's not the end of the world; future runs will
|
# exit. In case that fails, it's not the end of the world; future runs will
|
||||||
# supercede it in the transproxy list, at least, so the leftover rules
|
# supercede it in the transproxy list, at least, so the leftover rules
|
||||||
# are hopefully harmless.
|
# are hopefully harmless.
|
||||||
def main(port, syslog):
|
def main(port, dnsport, syslog):
|
||||||
assert(port > 0)
|
assert(port > 0)
|
||||||
assert(port <= 65535)
|
assert(port <= 65535)
|
||||||
|
assert(dnsport >= 0)
|
||||||
|
assert(dnsport <= 65535)
|
||||||
|
|
||||||
if os.getuid() != 0:
|
if os.getuid() != 0:
|
||||||
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
||||||
@ -291,7 +400,7 @@ def main(port, syslog):
|
|||||||
try:
|
try:
|
||||||
if line:
|
if line:
|
||||||
debug1('firewall manager: starting transproxy.\n')
|
debug1('firewall manager: starting transproxy.\n')
|
||||||
do_it(port, subnets)
|
do_wait = do_it(port, dnsport, subnets)
|
||||||
sys.stdout.write('STARTED\n')
|
sys.stdout.write('STARTED\n')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -305,6 +414,7 @@ def main(port, syslog):
|
|||||||
# to stay running so that we don't need a *second* password
|
# to stay running so that we don't need a *second* password
|
||||||
# authentication at shutdown time - that cleanup is important!
|
# authentication at shutdown time - that cleanup is important!
|
||||||
while 1:
|
while 1:
|
||||||
|
if do_wait: do_wait()
|
||||||
line = sys.stdin.readline(128)
|
line = sys.stdin.readline(128)
|
||||||
if line.startswith('HOST '):
|
if line.startswith('HOST '):
|
||||||
(name,ip) = line[5:].strip().split(',', 1)
|
(name,ip) = line[5:].strip().split(',', 1)
|
||||||
@ -319,5 +429,5 @@ def main(port, syslog):
|
|||||||
debug1('firewall manager: undoing changes.\n')
|
debug1('firewall manager: undoing changes.\n')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
do_it(port, [])
|
do_it(port, 0, [])
|
||||||
restore_etc_hosts(port)
|
restore_etc_hosts(port)
|
||||||
|
40
helpers.py
40
helpers.py
@ -1,4 +1,4 @@
|
|||||||
import sys, os
|
import sys, os, socket
|
||||||
|
|
||||||
logprefix = ''
|
logprefix = ''
|
||||||
verbose = 0
|
verbose = 0
|
||||||
@ -35,3 +35,41 @@ def list_contains_any(l, sub):
|
|||||||
if i in l:
|
if i in l:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def resolvconf_nameservers():
|
||||||
|
l = []
|
||||||
|
for line in open('/etc/resolv.conf'):
|
||||||
|
words = line.lower().split()
|
||||||
|
if len(words) >= 2 and words[0] == 'nameserver':
|
||||||
|
l.append(words[1])
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
def resolvconf_random_nameserver():
|
||||||
|
l = resolvconf_nameservers()
|
||||||
|
if l:
|
||||||
|
if len(l) > 1:
|
||||||
|
# don't import this unless we really need it
|
||||||
|
import random
|
||||||
|
random.shuffle(l)
|
||||||
|
return l[0]
|
||||||
|
else:
|
||||||
|
return '127.0.0.1'
|
||||||
|
|
||||||
|
|
||||||
|
def islocal(ip):
|
||||||
|
sock = socket.socket()
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
sock.bind((ip, 0))
|
||||||
|
except socket.error, e:
|
||||||
|
if e.args[0] == errno.EADDRNOTAVAIL:
|
||||||
|
return False # not a local IP
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
return True # it's a local IP, or there would have been an error
|
||||||
|
|
||||||
|
|
||||||
|
8
main.py
8
main.py
@ -54,6 +54,7 @@ sshuttle --hostwatch
|
|||||||
l,listen= transproxy to this ip address and port number [127.0.0.1:0]
|
l,listen= transproxy to this ip address and port number [127.0.0.1:0]
|
||||||
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
H,auto-hosts scan for remote hostnames and update local /etc/hosts
|
||||||
N,auto-nets automatically determine subnets to route
|
N,auto-nets automatically determine subnets to route
|
||||||
|
dns capture local DNS requests and forward to the remote DNS server
|
||||||
python= path to python interpreter on the remote server [python]
|
python= path to python interpreter on the remote server [python]
|
||||||
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
r,remote= ssh hostname (and optional username) of remote sshuttle server
|
||||||
x,exclude= exclude this subnet (can be used more than once)
|
x,exclude= exclude this subnet (can be used more than once)
|
||||||
@ -82,9 +83,9 @@ try:
|
|||||||
server.latency_control = opt.latency_control
|
server.latency_control = opt.latency_control
|
||||||
sys.exit(server.main())
|
sys.exit(server.main())
|
||||||
elif opt.firewall:
|
elif opt.firewall:
|
||||||
if len(extra) != 1:
|
if len(extra) != 2:
|
||||||
o.fatal('exactly one argument expected')
|
o.fatal('exactly two arguments expected')
|
||||||
sys.exit(firewall.main(int(extra[0]), opt.syslog))
|
sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
|
||||||
elif opt.hostwatch:
|
elif opt.hostwatch:
|
||||||
sys.exit(hostwatch.hw_main(extra))
|
sys.exit(hostwatch.hw_main(extra))
|
||||||
else:
|
else:
|
||||||
@ -111,6 +112,7 @@ try:
|
|||||||
remotename,
|
remotename,
|
||||||
opt.python,
|
opt.python,
|
||||||
opt.latency_control,
|
opt.latency_control,
|
||||||
|
opt.dns,
|
||||||
sh,
|
sh,
|
||||||
opt.auto_nets,
|
opt.auto_nets,
|
||||||
parse_subnets(includes),
|
parse_subnets(includes),
|
||||||
|
36
server.py
36
server.py
@ -1,4 +1,4 @@
|
|||||||
import re, struct, socket, select, traceback
|
import re, struct, socket, select, traceback, time
|
||||||
if not globals().get('skip_imports'):
|
if not globals().get('skip_imports'):
|
||||||
import ssnet, helpers, hostwatch
|
import ssnet, helpers, hostwatch
|
||||||
import compat.ssubprocess as ssubprocess
|
import compat.ssubprocess as ssubprocess
|
||||||
@ -106,6 +106,25 @@ class Hostwatch:
|
|||||||
self.sock = None
|
self.sock = None
|
||||||
|
|
||||||
|
|
||||||
|
class DnsProxy(Handler):
|
||||||
|
def __init__(self, mux, chan, request):
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
Handler.__init__(self, [sock])
|
||||||
|
self.sock = sock
|
||||||
|
self.timeout = time.time()+30
|
||||||
|
self.mux = mux
|
||||||
|
self.chan = chan
|
||||||
|
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
|
||||||
|
self.sock.connect((resolvconf_random_nameserver(), 53))
|
||||||
|
self.sock.send(request)
|
||||||
|
|
||||||
|
def callback(self):
|
||||||
|
data = self.sock.recv(4096)
|
||||||
|
debug2('DNS response: %d bytes\n' % len(data))
|
||||||
|
self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data)
|
||||||
|
self.ok = False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if helpers.verbose >= 1:
|
if helpers.verbose >= 1:
|
||||||
helpers.logprefix = ' s: '
|
helpers.logprefix = ' s: '
|
||||||
@ -165,6 +184,14 @@ def main():
|
|||||||
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
|
||||||
mux.new_channel = new_channel
|
mux.new_channel = new_channel
|
||||||
|
|
||||||
|
dnshandlers = {}
|
||||||
|
def dns_req(channel, data):
|
||||||
|
debug2('Incoming DNS request.\n')
|
||||||
|
h = DnsProxy(mux, channel, data)
|
||||||
|
handlers.append(h)
|
||||||
|
dnshandlers[channel] = h
|
||||||
|
mux.got_dns_req = dns_req
|
||||||
|
|
||||||
while mux.ok:
|
while mux.ok:
|
||||||
if hw.pid:
|
if hw.pid:
|
||||||
assert(hw.pid > 0)
|
assert(hw.pid > 0)
|
||||||
@ -176,3 +203,10 @@ def main():
|
|||||||
if latency_control:
|
if latency_control:
|
||||||
mux.check_fullness()
|
mux.check_fullness()
|
||||||
mux.callback()
|
mux.callback()
|
||||||
|
|
||||||
|
if dnshandlers:
|
||||||
|
now = time.time()
|
||||||
|
for channel,h in dnshandlers.items():
|
||||||
|
if h.timeout < now or not h.ok:
|
||||||
|
del dnshandlers[channel]
|
||||||
|
h.ok = False
|
||||||
|
10
ssnet.py
10
ssnet.py
@ -21,6 +21,8 @@ CMD_DATA = 0x4206
|
|||||||
CMD_ROUTES = 0x4207
|
CMD_ROUTES = 0x4207
|
||||||
CMD_HOST_REQ = 0x4208
|
CMD_HOST_REQ = 0x4208
|
||||||
CMD_HOST_LIST = 0x4209
|
CMD_HOST_LIST = 0x4209
|
||||||
|
CMD_DNS_REQ = 0x420a
|
||||||
|
CMD_DNS_RESPONSE = 0x420b
|
||||||
|
|
||||||
cmd_to_name = {
|
cmd_to_name = {
|
||||||
CMD_EXIT: 'EXIT',
|
CMD_EXIT: 'EXIT',
|
||||||
@ -33,6 +35,8 @@ cmd_to_name = {
|
|||||||
CMD_ROUTES: 'ROUTES',
|
CMD_ROUTES: 'ROUTES',
|
||||||
CMD_HOST_REQ: 'HOST_REQ',
|
CMD_HOST_REQ: 'HOST_REQ',
|
||||||
CMD_HOST_LIST: 'HOST_LIST',
|
CMD_HOST_LIST: 'HOST_LIST',
|
||||||
|
CMD_DNS_REQ: 'DNS_REQ',
|
||||||
|
CMD_DNS_RESPONSE: 'DNS_RESPONSE',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -281,7 +285,7 @@ class Mux(Handler):
|
|||||||
Handler.__init__(self, [rsock, wsock])
|
Handler.__init__(self, [rsock, wsock])
|
||||||
self.rsock = rsock
|
self.rsock = rsock
|
||||||
self.wsock = wsock
|
self.wsock = wsock
|
||||||
self.new_channel = self.got_routes = None
|
self.new_channel = self.got_dns_req = self.got_routes = None
|
||||||
self.got_host_req = self.got_host_list = None
|
self.got_host_req = self.got_host_list = None
|
||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.chani = 0
|
self.chani = 0
|
||||||
@ -343,6 +347,10 @@ class Mux(Handler):
|
|||||||
assert(not self.channels.get(channel))
|
assert(not self.channels.get(channel))
|
||||||
if self.new_channel:
|
if self.new_channel:
|
||||||
self.new_channel(channel, data)
|
self.new_channel(channel, data)
|
||||||
|
elif cmd == CMD_DNS_REQ:
|
||||||
|
assert(not self.channels.get(channel))
|
||||||
|
if self.got_dns_req:
|
||||||
|
self.got_dns_req(channel, data)
|
||||||
elif cmd == CMD_ROUTES:
|
elif cmd == CMD_ROUTES:
|
||||||
if self.got_routes:
|
if self.got_routes:
|
||||||
self.got_routes(data)
|
self.got_routes(data)
|
||||||
|
Loading…
Reference in New Issue
Block a user