summaryrefslogtreecommitdiffstats
path: root/Sshuttle VPN.app/Contents/Resources/sshuttle
diff options
context:
space:
mode:
Diffstat (limited to 'Sshuttle VPN.app/Contents/Resources/sshuttle')
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/client.py81
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/client.pycbin0 -> 13665 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pycbin0 -> 188 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pycbin0 -> 36231 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py161
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pycbin0 -> 12663 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py40
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pycbin0 -> 3118 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pycbin0 -> 9157 bytes
-rwxr-xr-xSshuttle VPN.app/Contents/Resources/sshuttle/main.py13
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/options.py9
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/options.pycbin0 -> 8735 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/server.py40
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/server.pycbin0 -> 9224 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py14
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pycbin0 -> 3963 bytes
-rwxr-xr-xSshuttle VPN.app/Contents/Resources/sshuttle/sshuttle13
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py10
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.pycbin0 -> 21065 bytes
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.pycbin0 -> 965 bytes
20 files changed, 316 insertions, 65 deletions
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py
index dbd11de..1ade5d9 100644
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/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 helpers, ssnet, ssh, ssyslog
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
@@ -6,21 +6,6 @@ from helpers import *
_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):
log('exiting on signal %d\n' % signum)
sys.exit(1)
@@ -111,14 +96,15 @@ def original_dst(sock):
class FirewallClient:
- def __init__(self, port, subnets_include, subnets_exclude):
+ def __init__(self, port, subnets_include, subnets_exclude, dnsport):
self.port = port
self.auto_nets = []
self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
+ self.dnsport = dnsport
argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) +
- ['--firewall', str(port)])
+ ['--firewall', str(port), str(dnsport)])
if ssyslog._p:
argvbase += ['--syslog']
argv_tries = [
@@ -189,7 +175,8 @@ class FirewallClient:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
-def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
+def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
+ dnslistener, seed_hosts, auto_nets,
syslog, daemon):
handlers = []
if helpers.verbose >= 1:
@@ -200,7 +187,8 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
try:
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python,
- stderr=ssyslog._p and ssyslog._p.stdin)
+ stderr=ssyslog._p and ssyslog._p.stdin,
+ options=dict(latency_control=latency_control))
except socket.error, e:
if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)")
@@ -280,7 +268,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
dstip = original_dst(sock)
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[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")
sock.close()
return
@@ -290,6 +278,30 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
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:
debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
@@ -300,11 +312,13 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux)
+ if latency_control:
+ mux.check_fullness()
mux.callback()
- mux.check_fullness()
-def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
+def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
+ seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog:
ssyslog.start_syslog()
@@ -315,8 +329,7 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
log("%s\n" % e)
return 5
debug1('Starting sshuttle proxy.\n')
- listener = socket.socket()
- listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
if listenip[1]:
ports = [listenip[1]]
else:
@@ -326,8 +339,13 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
debug2('Binding:')
for port in ports:
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:
listener.bind((listenip[0], port))
+ dnslistener.bind((listenip[0], port))
bound = True
break
except socket.error, e:
@@ -340,11 +358,20 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
listenip = listener.getsockname()
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
+
+ fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
try:
return _main(listener, fw, ssh_cmd, remotename,
- python, seed_hosts, auto_nets, syslog, daemon)
+ python, latency_control, dnslistener,
+ seed_hosts, auto_nets, syslog, daemon)
finally:
try:
if daemon:
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc
new file mode 100644
index 0000000..64b7efb
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc
new file mode 100644
index 0000000..736e5ec
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc
new file mode 100644
index 0000000..dcb10bd
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py
index 044ac52..c7557ed 100644
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py
@@ -1,8 +1,11 @@
-import re, errno
+import re, errno, socket, select, struct
import compat.ssubprocess as ssubprocess
import helpers, ssyslog
from helpers import *
+# python doesn't have a definition for this
+IPPROTO_DIVERT = 254
+
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
@@ -23,12 +26,33 @@ def ipt(*args):
raise Fatal('%r returned %d' % (argv, rv))
+_no_ttl_module = False
+def ipt_ttl(*args):
+ global _no_ttl_module
+ if not _no_ttl_module:
+ # we avoid infinite loops by generating server-side connections
+ # with ttl 42. This makes the client side not recapture those
+ # connections, in case client == server.
+ try:
+ argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
+ ipt(*argsplus)
+ except Fatal:
+ ipt(*args)
+ # we only get here if the non-ttl attempt succeeds
+ log('sshuttle: warning: your iptables is missing '
+ 'the ttl module.\n')
+ _no_ttl_module = True
+ else:
+ ipt(*args)
+
+
+
# We name the chain based on the transproxy port number so that it's possible
# to run multiple copies of sshuttle at the same time. Of course, the
# multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
-def do_iptables(port, subnets):
+def do_iptables(port, dnsport, subnets):
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
@@ -38,12 +62,13 @@ def do_iptables(port, subnets):
ipt('-F', chain)
ipt('-X', chain)
- if subnets:
+ if subnets or dnsport:
ipt('-N', chain)
ipt('-F', chain)
ipt('-I', 'OUTPUT', '1', '-j', chain)
ipt('-I', 'PREROUTING', '1', '-j', chain)
+ if subnets:
# create new subnet entries. Note that we're sorting in a very
# particular order: we need to go from most-specific (largest swidth)
# to least-specific, and at any given level of specificity, we want
@@ -55,12 +80,19 @@ def do_iptables(port, subnets):
'--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp')
else:
- ipt('-A', chain, '-j', 'REDIRECT',
- '--dest', '%s/%s' % (snet,swidth),
- '-p', 'tcp',
- '--to-ports', str(port),
- '-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
- )
+ ipt_ttl('-A', chain, '-j', 'REDIRECT',
+ '--dest', '%s/%s' % (snet,swidth),
+ '-p', 'tcp',
+ '--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):
@@ -69,7 +101,7 @@ def ipfw_rule_exists(n):
found = False
for line in p.stdout:
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 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip())
@@ -116,6 +148,39 @@ def sysctl_set(name, val):
if val != oldval:
_changedctls.append(name)
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):
@@ -126,7 +191,7 @@ def ipfw(*args):
raise Fatal('%r returned %d' % (argv, rv))
-def do_ipfw(port, subnets):
+def do_ipfw(port, dnsport, subnets):
sport = str(port)
xsport = str(port+1)
@@ -139,13 +204,14 @@ def do_ipfw(port, subnets):
oldval = _oldctls[name]
_sysctl_set(name, oldval)
- if subnets:
+ if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1)
sysctl_set('net.inet.ip.scopedroute', 0)
ipfw('add', sport, 'check-state', 'ip',
'from', 'any', 'to', 'any')
-
+
+ if subnets:
# create new subnet entries
for swidth,sexclude,snet in sorted(subnets, reverse=True):
if sexclude:
@@ -158,6 +224,65 @@ def do_ipfw(port, subnets):
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'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):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
@@ -166,6 +291,7 @@ def program_exists(name):
if os.path.exists(fn):
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
+
hostmap = {}
def rewrite_etc_hosts(port):
HOSTSFILE='/etc/hosts'
@@ -216,9 +342,11 @@ def restore_etc_hosts(port):
# 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
# are hopefully harmless.
-def main(port, syslog):
+def main(port, dnsport, syslog):
assert(port > 0)
assert(port <= 65535)
+ assert(dnsport >= 0)
+ assert(dnsport <= 65535)
if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
@@ -272,7 +400,7 @@ def main(port, syslog):
try:
if line:
debug1('firewall manager: starting transproxy.\n')
- do_it(port, subnets)
+ do_wait = do_it(port, dnsport, subnets)
sys.stdout.write('STARTED\n')
try:
@@ -286,6 +414,7 @@ def main(port, syslog):
# to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important!
while 1:
+ if do_wait: do_wait()
line = sys.stdin.readline(128)
if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1)
@@ -300,5 +429,5 @@ def main(port, syslog):
debug1('firewall manager: undoing changes.\n')
except:
pass
- do_it(port, [])
+ do_it(port, 0, [])
restore_etc_hosts(port)
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc
new file mode 100644
index 0000000..7539b6f
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py
index 18871a2..c169d0c 100644
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py
@@ -1,4 +1,4 @@
-import sys, os
+import sys, os, socket
logprefix = ''
verbose = 0
@@ -35,3 +35,41 @@ def list_contains_any(l, sub):
if i in l:
return True
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
+
+
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc
new file mode 100644
index 0000000..ced3de3
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc
new file mode 100644
index 0000000..5dbb1cc
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py
index 66954f8..e76e596 100755
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py
@@ -54,12 +54,14 @@ sshuttle --hostwatch
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
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]
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh]
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
+no-latency-control sacrifice latency to improve bandwidth benchmarks
D,daemon run in the background as a daemon
syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
@@ -67,7 +69,7 @@ server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
"""
-o = options.Options('sshuttle', optspec)
+o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[1:])
if opt.daemon:
@@ -78,11 +80,12 @@ try:
if opt.server:
if len(extra) != 0:
o.fatal('no arguments expected')
+ server.latency_control = opt.latency_control
sys.exit(server.main())
elif opt.firewall:
- if len(extra) != 1:
- o.fatal('exactly one argument expected')
- sys.exit(firewall.main(int(extra[0]), opt.syslog))
+ if len(extra) != 2:
+ o.fatal('exactly two arguments expected')
+ sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
@@ -108,6 +111,8 @@ try:
opt.ssh_cmd,
remotename,
opt.python,
+ opt.latency_control,
+ opt.dns,
sh,
opt.auto_nets,
parse_subnets(includes),
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py
index 25322fb..2f3fff6 100644
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py
@@ -76,9 +76,8 @@ class Options:
By default, the parser function is getopt.gnu_getopt, and the abort
behaviour is to exit the program.
"""
- def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt,
+ def __init__(self, optspec, optfunc=getopt.gnu_getopt,
onabort=_default_onabort):
- self.exe = exe
self.optspec = optspec
self._onabort = onabort
self.optfunc = optfunc
@@ -122,8 +121,8 @@ class Options:
defval = None
flagl = flags.split(',')
flagl_nice = []
- for f in flagl:
- f,dvi = _remove_negative_kv(f, _intify(defval))
+ for _f in flagl:
+ f,dvi = _remove_negative_kv(_f, _intify(defval))
self._aliases[f] = _remove_negative_k(flagl[0])
self._hasparms[f] = has_parm
self._defaults[f] = dvi
@@ -135,7 +134,7 @@ class Options:
self._aliases[f_nice] = _remove_negative_k(flagl[0])
self._longopts.append(f + (has_parm and '=' or ''))
self._longopts.append('no-' + f)
- flagl_nice.append('--' + f)
+ flagl_nice.append('--' + _f)
flags_nice = ', '.join(flagl_nice)
if has_parm:
flags_nice += ' ...'
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc
new file mode 100644
index 0000000..99ebce5
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py
index 24dd462..45bc2fc 100644
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/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'):
import ssnet, helpers, hostwatch
import compat.ssubprocess as ssubprocess
@@ -106,11 +106,31 @@ class Hostwatch:
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():
if helpers.verbose >= 1:
helpers.logprefix = ' s: '
else:
helpers.logprefix = 'server: '
+ debug1('latency control setting = %r\n' % latency_control)
routes = list(list_routes())
debug1('available routes:\n')
@@ -164,6 +184,14 @@ def main():
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
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:
if hw.pid:
assert(hw.pid > 0)
@@ -172,5 +200,13 @@ def main():
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux)
- mux.check_fullness()
+ if latency_control:
+ mux.check_fullness()
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
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc
new file mode 100644
index 0000000..6e4a14b
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py
index ac7f411..9a6270a 100644
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py
@@ -14,14 +14,16 @@ def readfile(name):
raise Exception("can't find file %r in any of %r" % (name, path))
-def empackage(z, filename):
+def empackage(z, filename, data=None):
(path,basename) = os.path.split(filename)
- content = z.compress(readfile(filename))
+ if not data:
+ data = readfile(filename)
+ content = z.compress(data)
content += z.flush(zlib.Z_SYNC_FLUSH)
- return '%s\n%d\n%s' % (basename,len(content), content)
+ return '%s\n%d\n%s' % (basename, len(content), content)
-def connect(ssh_cmd, rhostport, python, stderr):
+def connect(ssh_cmd, rhostport, python, stderr, options):
main_exe = sys.argv[0]
portl = []
@@ -52,7 +54,9 @@ def connect(ssh_cmd, rhostport, python, stderr):
z = zlib.compressobj(1)
content = readfile('assembler.py')
- content2 = (empackage(z, 'helpers.py') +
+ optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items())
+ content2 = (empackage(z, 'cmdline_options.py', optdata) +
+ empackage(z, 'helpers.py') +
empackage(z, 'compat/ssubprocess.py') +
empackage(z, 'ssnet.py') +
empackage(z, 'hostwatch.py') +
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc
new file mode 100644
index 0000000..6118b39
--- /dev/null
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.pyc
Binary files differ
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle b/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle
index 66954f8..e76e596 100755
--- a/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle
+++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle
@@ -54,12 +54,14 @@ sshuttle --hostwatch
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
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]
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh]
seed-hosts= with -H, use these hostnames for initial scan (comma-separated)
+no-latency-control sacrifice latency to improve bandwidth benchmarks
D,daemon run in the background as a daemon
syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
@@ -67,7 +69,7 @@ server (internal use only)
firewall (internal use only)
hostwatch (internal use only)
"""
-o = options.Options('sshuttle', optspec)
+o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[1:])
if opt.daemon:
@@ -78,11 +80,12 @@ try:
if opt.server:
if len(extra) != 0:
o.fatal('no arguments expected')
+ server.latency_control = opt.latency_control
sys.exit(server.main())
elif opt.firewall:
- if len(extra) != 1:
- o.fatal('exactly one argument expected')
- sys.exit(firewall.main(int(extra[0]), opt.syslog))
+ if len(extra) != 2:
+ o.fatal('exactly two arguments expected')
+ sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra))
else:
<