MacOS precompiled app package for sshuttle-0.50a-2-ga238f76

This commit is contained in:
Avery Pennarun 2011-02-01 03:57:29 -08:00
parent 34ea1ed8b7
commit 76359bc71c
27 changed files with 362 additions and 92 deletions

Binary file not shown.

View File

@ -2,7 +2,8 @@ import sys, os, pty
from AppKit import * from AppKit import *
import my, models, askpass import my, models, askpass
def sshuttle_args(host, auto_nets, auto_hosts, nets, debug): def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug,
no_latency_control):
argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host] argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host]
assert(argv[0]) assert(argv[0])
if debug: if debug:
@ -11,6 +12,10 @@ def sshuttle_args(host, auto_nets, auto_hosts, nets, debug):
argv.append('--auto-nets') argv.append('--auto-nets')
if auto_hosts: if auto_hosts:
argv.append('--auto-hosts') argv.append('--auto-hosts')
if dns:
argv.append('--dns')
if no_latency_control:
argv.append('--no-latency-control')
argv += nets argv += nets
return argv return argv
@ -131,6 +136,7 @@ class SshuttleController(NSObject):
prefsWindow = objc.IBOutlet() prefsWindow = objc.IBOutlet()
serversController = objc.IBOutlet() serversController = objc.IBOutlet()
logField = objc.IBOutlet() logField = objc.IBOutlet()
noLatencyControlField = objc.IBOutlet()
servers = [] servers = []
conns = {} conns = {}
@ -159,8 +165,11 @@ class SshuttleController(NSObject):
conn = Runner(sshuttle_args(host, conn = Runner(sshuttle_args(host,
auto_nets = nets_mode == models.NET_AUTO, auto_nets = nets_mode == models.NET_AUTO,
auto_hosts = server.autoHosts(), auto_hosts = server.autoHosts(),
dns = server.useDns(),
nets = manual_nets, nets = manual_nets,
debug = self.debugField.state()), debug = self.debugField.state(),
no_latency_control
= self.noLatencyControlField.state()),
logfunc=logfunc, promptfunc=promptfunc, logfunc=logfunc, promptfunc=promptfunc,
serverobj=server) serverobj=server)
self.conns[host] = conn self.conns[host] = conn
@ -213,6 +222,7 @@ class SshuttleController(NSObject):
if len(self.servers): if len(self.servers):
for i in self.servers: for i in self.servers:
host = i.host() host = i.host()
title = i.title()
want = i.wantConnect() want = i.wantConnect()
connected = i.connected() connected = i.connected()
numnets = len(list(i.nets())) numnets = len(list(i.nets()))
@ -222,9 +232,9 @@ class SshuttleController(NSObject):
additem('Connect %s (no routes)' % host, None, i) additem('Connect %s (no routes)' % host, None, i)
elif want: elif want:
any_conn = i any_conn = i
additem('Disconnect %s' % host, self.cmd_disconnect, i) additem('Disconnect %s' % title, self.cmd_disconnect, i)
else: else:
additem('Connect %s' % host, self.cmd_connect, i) additem('Connect %s' % title, self.cmd_connect, i)
if not want: if not want:
msg = 'Off' msg = 'Off'
elif i.error(): elif i.error():
@ -236,12 +246,6 @@ class SshuttleController(NSObject):
msg = 'Connecting...' msg = 'Connecting...'
any_inprogress = i any_inprogress = i
addnote(' State: %s' % msg) addnote(' State: %s' % msg)
if i.autoNets() == 0:
addnote(' Routes: All')
elif i.autoNets() == 2:
addnote(' Routes: Auto')
else:
addnote(' Routes: Custom')
else: else:
addnote('No servers defined yet') addnote('No servers defined yet')
@ -279,13 +283,15 @@ class SshuttleController(NSObject):
net.setWidth_(width) net.setWidth_(width)
nl.append(net) nl.append(net)
autoNets = s.get('autoNets', 1) autoNets = s.get('autoNets', models.NET_AUTO)
autoHosts = s.get('autoHosts', 1) autoHosts = s.get('autoHosts', True)
useDns = s.get('useDns', autoNets == models.NET_ALL)
srv = models.SshuttleServer.alloc().init() srv = models.SshuttleServer.alloc().init()
srv.setHost_(host) srv.setHost_(host)
srv.setAutoNets_(autoNets) srv.setAutoNets_(autoNets)
srv.setAutoHosts_(autoHosts) srv.setAutoHosts_(autoHosts)
srv.setNets_(nl) srv.setNets_(nl)
srv.setUseDns_(useDns)
sl.append(srv) sl.append(srv)
self.serversController.addObjects_(sl) self.serversController.addObjects_(sl)
self.serversController.setSelectionIndex_(0) self.serversController.setSelectionIndex_(0)
@ -303,7 +309,8 @@ class SshuttleController(NSObject):
d = dict(host=s.host(), d = dict(host=s.host(),
nets=nets, nets=nets,
autoNets=s.autoNets(), autoNets=s.autoNets(),
autoHosts=s.autoHosts()) autoHosts=s.autoHosts(),
useDns=s.useDns())
l.append(d) l.append(d)
my.Defaults().setObject_forKey_(l, 'servers') my.Defaults().setObject_forKey_(l, 'servers')
self.fill_menu() self.fill_menu()

View File

@ -92,11 +92,28 @@ class SshuttleServer(NSObject):
if self.autoNets() == NET_MANUAL and not len(list(self.nets())): if self.autoNets() == NET_MANUAL and not len(list(self.nets())):
return False return False
return True return True
def title(self):
host = self.host()
if not host:
return host
an = self.autoNets()
suffix = ""
if an == NET_ALL:
suffix = " (all traffic)"
elif an == NET_MANUAL:
n = self.nets()
suffix = ' (%d subnet%s)' % (len(n), len(n)!=1 and 's' or '')
return self.host() + suffix
def setTitle_(self, v):
# title is always auto-generated
config_changed()
def host(self): def host(self):
return getattr(self, '_k_host', None) return getattr(self, '_k_host', None)
def setHost_(self, v): def setHost_(self, v):
self._k_host = v self._k_host = v
self.setTitle_(None)
config_changed() config_changed()
@objc.accessor @objc.accessor
def validateHost_error_(self, value, error): def validateHost_error_(self, value, error):
@ -109,6 +126,7 @@ class SshuttleServer(NSObject):
return getattr(self, '_k_nets', []) return getattr(self, '_k_nets', [])
def setNets_(self, v): def setNets_(self, v):
self._k_nets = v self._k_nets = v
self.setTitle_(None)
config_changed() config_changed()
def netsHidden(self): def netsHidden(self):
#print 'checking netsHidden' #print 'checking netsHidden'
@ -122,6 +140,8 @@ class SshuttleServer(NSObject):
def setAutoNets_(self, v): def setAutoNets_(self, v):
self._k_autoNets = v self._k_autoNets = v
self.setNetsHidden_(-1) self.setNetsHidden_(-1)
self.setUseDns_(v == NET_ALL)
self.setTitle_(None)
config_changed() config_changed()
def autoHosts(self): def autoHosts(self):
@ -129,3 +149,9 @@ class SshuttleServer(NSObject):
def setAutoHosts_(self, v): def setAutoHosts_(self, v):
self._k_autoHosts = v self._k_autoHosts = v
config_changed() config_changed()
def useDns(self):
return getattr(self, '_k_useDns', False)
def setUseDns_(self, v):
self._k_useDns = v
config_changed()

Binary file not shown.

Binary file not shown.

View File

@ -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 = [
@ -189,7 +175,8 @@ class FirewallClient:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) 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): syslog, daemon):
handlers = [] handlers = []
if helpers.verbose >= 1: if helpers.verbose >= 1:
@ -200,7 +187,8 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
try: try:
(serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, (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: except socket.error, e:
if e.args[0] == errno.EPIPE: if e.args[0] == errno.EPIPE:
raise Fatal("failed to establish ssh session (1)") 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) 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
@ -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(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))
@ -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) raise Fatal('server died with error code %d' % rv)
ssnet.runonce(handlers, mux) ssnet.runonce(handlers, mux)
if latency_control:
mux.check_fullness()
mux.callback() 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): subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog: if syslog:
ssyslog.start_syslog() ssyslog.start_syslog()
@ -315,8 +329,7 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
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:
@ -326,8 +339,13 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
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:
@ -340,11 +358,20 @@ def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets,
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
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, seed_hosts, auto_nets, syslog, daemon) python, latency_control, dnslistener,
seed_hosts, auto_nets, syslog, daemon)
finally: finally:
try: try:
if daemon: if daemon:

View File

@ -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']
@ -23,12 +26,33 @@ def ipt(*args):
raise Fatal('%r returned %d' % (argv, rv)) 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 # 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 # to run multiple copies of sshuttle at the same time. Of course, the
# 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
@ -38,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
@ -55,12 +80,19 @@ def do_iptables(port, subnets):
'--dest', '%s/%s' % (snet,swidth), '--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp') '-p', 'tcp')
else: else:
ipt('-A', chain, '-j', 'REDIRECT', ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet,swidth), '--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp', '-p', 'tcp',
'--to-ports', str(port), '--to-ports', str(port))
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
) 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):
@ -69,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())
@ -116,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):
@ -126,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)
@ -139,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:
@ -158,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)
@ -166,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'
@ -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 # 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')
@ -272,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:
@ -286,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)
@ -300,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)

View File

@ -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

View File

@ -54,12 +54,14 @@ 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)
v,verbose increase debug message verbosity v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh] 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) 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 D,daemon run in the background as a daemon
syslog send log messages to syslog (default if you use --daemon) syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
@ -67,7 +69,7 @@ server (internal use only)
firewall (internal use only) firewall (internal use only)
hostwatch (internal use only) hostwatch (internal use only)
""" """
o = options.Options('sshuttle', optspec) o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[1:]) (opt, flags, extra) = o.parse(sys.argv[1:])
if opt.daemon: if opt.daemon:
@ -78,11 +80,12 @@ try:
if opt.server: if opt.server:
if len(extra) != 0: if len(extra) != 0:
o.fatal('no arguments expected') o.fatal('no arguments expected')
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:
@ -108,6 +111,8 @@ try:
opt.ssh_cmd, opt.ssh_cmd,
remotename, remotename,
opt.python, opt.python,
opt.latency_control,
opt.dns,
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),

View File

@ -76,9 +76,8 @@ class Options:
By default, the parser function is getopt.gnu_getopt, and the abort By default, the parser function is getopt.gnu_getopt, and the abort
behaviour is to exit the program. 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): onabort=_default_onabort):
self.exe = exe
self.optspec = optspec self.optspec = optspec
self._onabort = onabort self._onabort = onabort
self.optfunc = optfunc self.optfunc = optfunc
@ -122,8 +121,8 @@ class Options:
defval = None defval = None
flagl = flags.split(',') flagl = flags.split(',')
flagl_nice = [] flagl_nice = []
for f in flagl: for _f in flagl:
f,dvi = _remove_negative_kv(f, _intify(defval)) f,dvi = _remove_negative_kv(_f, _intify(defval))
self._aliases[f] = _remove_negative_k(flagl[0]) self._aliases[f] = _remove_negative_k(flagl[0])
self._hasparms[f] = has_parm self._hasparms[f] = has_parm
self._defaults[f] = dvi self._defaults[f] = dvi
@ -135,7 +134,7 @@ class Options:
self._aliases[f_nice] = _remove_negative_k(flagl[0]) self._aliases[f_nice] = _remove_negative_k(flagl[0])
self._longopts.append(f + (has_parm and '=' or '')) self._longopts.append(f + (has_parm and '=' or ''))
self._longopts.append('no-' + f) self._longopts.append('no-' + f)
flagl_nice.append('--' + f) flagl_nice.append('--' + _f)
flags_nice = ', '.join(flagl_nice) flags_nice = ', '.join(flagl_nice)
if has_parm: if has_parm:
flags_nice += ' ...' flags_nice += ' ...'

View File

@ -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,11 +106,31 @@ 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: '
else: else:
helpers.logprefix = 'server: ' helpers.logprefix = 'server: '
debug1('latency control setting = %r\n' % latency_control)
routes = list(list_routes()) routes = list(list_routes())
debug1('available routes:\n') debug1('available routes:\n')
@ -164,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)
@ -172,5 +200,13 @@ def main():
raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv)
ssnet.runonce(handlers, mux) ssnet.runonce(handlers, mux)
mux.check_fullness() if latency_control:
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

View File

@ -14,14 +14,16 @@ def readfile(name):
raise Exception("can't find file %r in any of %r" % (name, path)) 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) (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) 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] main_exe = sys.argv[0]
portl = [] portl = []
@ -52,7 +54,9 @@ def connect(ssh_cmd, rhostport, python, stderr):
z = zlib.compressobj(1) z = zlib.compressobj(1)
content = readfile('assembler.py') 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, 'compat/ssubprocess.py') +
empackage(z, 'ssnet.py') + empackage(z, 'ssnet.py') +
empackage(z, 'hostwatch.py') + empackage(z, 'hostwatch.py') +

Binary file not shown.

View File

@ -54,12 +54,14 @@ 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)
v,verbose increase debug message verbosity v,verbose increase debug message verbosity
e,ssh-cmd= the command to use to connect to the remote [ssh] 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) 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 D,daemon run in the background as a daemon
syslog send log messages to syslog (default if you use --daemon) syslog send log messages to syslog (default if you use --daemon)
pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] pidfile= pidfile name (only if using --daemon) [./sshuttle.pid]
@ -67,7 +69,7 @@ server (internal use only)
firewall (internal use only) firewall (internal use only)
hostwatch (internal use only) hostwatch (internal use only)
""" """
o = options.Options('sshuttle', optspec) o = options.Options(optspec)
(opt, flags, extra) = o.parse(sys.argv[1:]) (opt, flags, extra) = o.parse(sys.argv[1:])
if opt.daemon: if opt.daemon:
@ -78,11 +80,12 @@ try:
if opt.server: if opt.server:
if len(extra) != 0: if len(extra) != 0:
o.fatal('no arguments expected') o.fatal('no arguments expected')
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:
@ -108,6 +111,8 @@ try:
opt.ssh_cmd, opt.ssh_cmd,
remotename, remotename,
opt.python, opt.python,
opt.latency_control,
opt.dns,
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),

View File

@ -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)

Binary file not shown.

View File

@ -1,14 +0,0 @@
import os
pid = os.fork()
if pid == 0:
# child
try:
os.setsid()
#os.execvp('sudo', ['sudo', 'SSH_ASKPASS=%s' % os.path.abspath('askpass.py'), 'ssh', 'afterlife', 'ls'])
os.execvp('ssh', ['ssh', 'afterlife', 'ls'])
finally:
os._exit(44)
else:
# parent
os.wait()