Compare commits

..

39 Commits

Author SHA1 Message Date
4fde980f46 firewall.py: MacOS: permanently set the net.inet.ip.scopedroute sysctl.
If this sysctl isn't set to 0 at the time your network interface is brought
up, and we later change it, then the MacOS (10.6.6 at least) ARP table gets
totally confused and networking stops working about 15 minutes later, until
you down and re-up the interface.  The symptom is that pings outside your
LAN would give results like this:

    ping: sendto: no route to host

and "arp -a -n" would show *two* entries for your default gateway instead of
just one.

sshuttle was helpfully putting the sysctl back the way it was when it shuts
down, so you would fix your network by downing the interface, so sshuttle
would abort and change the sysctl back, then you would re-up the interface,
then restart sshuttle, and sshuttle would change the sysctl back and restart
the cycle: it would break again a few minutes later.

That's annoying, and it gives sshuttle a bad reputation for being the thing
that breaks your network.  I can't find a *really* good workaround for the
bug, so barring that, let's just permanently set the sysctl to 0 and not
change it back on exit.  That should just leave your computer back how it
worked in MacOS 10.5, as far as I know, which seems harmless.  At least I've
been running my Mac that way for a few days and I haven't seen any
weirdness.

Now, doing *that* would still mean that the first sshuttle session after a
reboot would still break the network, since sysctl changes are lost on
reboot.  Thus, let's be extra hardcore and write it to /etc/sysctl.conf so
that it goes the way we want it after a reboot.  Thus, sshuttle should break
your network at most once.  Which still sucks, but hopefully nobody will
notice.
2011-02-04 21:55:40 -08:00
621997b279 ui-macos: move the noLatencyControl setting to a per-connection setting.
I think some connections you'll want to optimize for latency, and others for
bandwidth.  Probably.

Also, use a dropdown box instead of a checkbox; that way we can make it more
clear what each of the settings means.

While we're here, adjust all the anchor settings for the different display
items so that resizing the dialog box works sensibly.
2011-02-04 21:40:44 -08:00
ca7d38dc1a stresstest.py: a program to create lots and lots of TCP connections.
This version is a bit limited: it always only connects back to itself, which
is always on 127.0.0.1.  It also doesn't really find any problems, other
than odd behaviour when Linux runs out of available port numbers after a
while.
2011-02-04 21:37:22 -08:00
a81972b2b5 Add --wrap option to force channel number wrapping at a lower number.
This makes it easier to actually test what happens when channel numbers wrap
around.  The good news: it works.

However, I did find a bug where sshuttle would die if we completely ran out
of available channel numbers because so many of them were open.  This would
never realistically happen at the default of 65535 channels (we'd run out of
file descriptors first), but it's still a bug, so let's handle it by just
dropping the connection when it happens.
2011-02-02 02:32:46 -08:00
a238f7636c ui-macos: include routing type in each connection title.
This makes it extra clear when a connection is for "all routes" vs. custom
vs. auto.
2011-02-01 03:55:19 -08:00
62e1ac4b46 ui-macos: add checkboxes for --no-latency-control and --dns options. 2011-02-01 03:55:19 -08:00
f2297066e7 Oops, left in a junk option that causes a crash without --dns. 2011-01-26 11:26:35 -08:00
0bf0351d9b 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)
2011-01-26 05:29:51 -08:00
9731680d2e dns on MacOS: use divert sockets instead of 'fwd' rules.
It turns out diverting UDP sockets is pretty easy compared to TCP (which
makes it all the more embarrassing that they screwed up 'fwd' support for
UDP and not TCP, but oh well).  So let's use divert sockets instead of
transproxy for our DNS packets.

This is a little tricky because we have to do it all in firewall.py, since
divert sockets require root access, and only firewall.py has root access.
2011-01-26 05:25:27 -08:00
88937e148e client.py: do DNS listener on the same port as the TCP listener.
UDP and TCP have separate port namespaces, so to make it easier to keep
track of what's going on, just use the same transproxy port number for both.
We still need two sockets, but now tcpdumps are easier to understand.
2011-01-26 05:25:26 -08:00
7f3c522c56 Move client._islocal() to helpers.islocal() in preparation for sharing. 2011-01-26 05:25:26 -08:00
ebfc3703ec dns: add support for MacOS (but it doesn't work...)
...because stupid MacOS ipfw 'fwd' rules don't work quite right with udp.
It can intercept packets bound for remote hosts, but it doesn't correctly
rewrite the port number from its original to the new socket, so it gets
dropped by the local kernel anyway.

That is, a packet to 1.2.3.4:53 should be redirected to, say,
127.0.0.1:9999, the local DNS listener socket.  But instead, it gets sent to
127.0.0.1:53, which nobody is listening on, so it gets eaten.

Sigh.
2011-01-26 05:25:26 -08:00
760740e9aa Oops, dns_done() crashed if the request had already been timed out. 2011-01-26 05:25:26 -08:00
b570778894 dns: trim DNS channel handlers after a response, or after a timeout.
This avoids memory/socket leaks.
2011-01-26 02:34:46 -08:00
4c5185dc55 dns: extract 'nameserver' lines from /etc/resolv.conf 2011-01-26 02:34:46 -08:00
a2fcb08a2d Extremely basic, but functional, DNS proxying support (--dns option)
Limitations:
- uses a hardcoded DNS server IP on both client and server
- never expires request/response objects, so leaks memory and sockets
- works only with iptables, not with ipfw
2011-01-26 02:34:46 -08:00
e7a19890aa Merge branch 'fullness'
Tests with speedtest.net to a linode.com server:

                       Downstream     Upstream

No sshuttle            1.25 Mbit/s    0.55 Mbit/s
Default                0.75 Mbit/s    0.51 Mbit/s
--no-latency-control   1.25 Mbit/s    0.55 Mbit/s

* fullness:
  man page for the --no-latency-control option.
  options: remove unused 'exe' parameter
  options.py: generate usage string correctly for no-* options.
  Implement the optional fullness checking a bit more like I like it.
  new option to disable fullness checking
2011-01-25 22:11:28 -08:00
d9b1bb52e5 man page for the --no-latency-control option. 2011-01-25 21:30:29 -08:00
a30c4d7ccb options: remove unused 'exe' parameter
The 'exe' parameter was added in the hope of using it for additional
contextual information in the help text that Options generates. It was
till then abandoned and was judged as superflous information.

Remove the 'exe' parameter from Options' constructor.

(copied from the 'bup' project)

Signed-off-by: Gabriel Filion <lelutin@gmail.com>
2011-01-25 21:19:28 -08:00
9877a8d6fb options.py: generate usage string correctly for no-* options.
Signed-off-by: Avery Pennarun <apenwarr@gmail.com>
2011-01-25 21:14:51 -08:00
8fde1155da Implement the optional fullness checking a bit more like I like it.
Looks like it worked before, but personal preference is a killer.

The new name is "--no-latency-control".
2011-01-25 21:07:39 -08:00
fdb7c9b995 new option to disable fullness checking
On high latency links, the PING/PONG round trip triggered by fullness
checking could kill the bandwidth. Disabling it could result in >10x
bandwidth increase in some setups where the existing latency is already high
and the available bandwidth is also high.
2011-01-25 21:05:13 -08:00
675f19f57e Don't die if iptables doesn't have 'ttl match' support.
ttl matching is only needed if your server is the same machine as the
client, which is kind of useless anyway (other than for testing), so there's
no reason for it to be fatal if that doesn't work.

Reported by "Alphazo" on the mailing list, who managed to get sshuttle
working on his Nokia N900 by removing the ttl stuff.
2011-01-25 20:42:10 -08:00
049a0c40ac ui-macos: guess we don't need stupid.py anymore.
It was just a test.
2011-01-22 16:55:18 -08:00
668441adb6 Merge branch 'macapp'
A cute little GUI for sshuttle on MacOS, written using pyobjc.

* macapp:
  ui-macos: call the main binary MacOS/Sshuttle.
  ui-macos/git-export.do: write the generated app to a branch.
  ui-macos/default.app.do: get rid of some duplicated files.
  ui-macos: Actually prompt for passwords instead of assuming a default.
  ui-macos: Don't enable connecting for hosts with Custom but zero subnets.
  ui-macos: Smoother log messages in the log window.
  ui-macos: Notice when we've connected; make debug logs optional.
  ui-macos: Much better connection status reporting.
  ui-macos/run.do: a shortcut for running debug.app.
  ui-macos/*: "a series of unfortunate events."
2011-01-22 16:49:21 -08:00
d4ccd30c24 ui-macos: call the main binary MacOS/Sshuttle.
That way we can 'killall Sshuttle' and so on, and it looks right in the
process list.
2011-01-22 16:44:20 -08:00
522bc23854 ui-macos/git-export.do: write the generated app to a branch. 2011-01-22 16:44:20 -08:00
191d97a897 ui-macos/default.app.do: get rid of some duplicated files. 2011-01-22 16:44:15 -08:00
da7a490cd7 ui-macos: Actually prompt for passwords instead of assuming a default. 2011-01-22 16:44:11 -08:00
342ce12840 ui-macos: Don't enable connecting for hosts with Custom but zero subnets.
That would cause the sshuttle client to die.
2011-01-22 16:44:05 -08:00
701f59a5b8 ui-macos: Smoother log messages in the log window. 2011-01-22 16:44:01 -08:00
9f7b2bb4f6 ui-macos: Notice when we've connected; make debug logs optional. 2011-01-22 16:43:48 -08:00
d301184184 ui-macos: Much better connection status reporting. 2011-01-22 16:43:44 -08:00
659a57beb2 ui-macos/run.do: a shortcut for running debug.app. 2011-01-22 16:43:09 -08:00
c70b9937df ui-macos/*: "a series of unfortunate events."
Just kidding.  This is a squash of a whole bunch of unlabeled temporary
commits that I produced over the last couple of weeks while writing a UI
for MacOS while riding on airplanes and sitting in airports.

So long, batch of useless commits!
2011-01-22 16:43:09 -08:00
415be935d4 options.py: don't die if tty width is set to 0.
This sometimes happens if weird people, such as myself, open a pty without
setting the width field correctly.
2011-01-22 16:42:32 -08:00
d4c9d31068 Oops, we were being overzealous about calling nowrite().
We were doing it as soon as the other end was shut_read, but we didn't
confirm first that the other end's read buffer had been totally emptied.
This caused problems on twitter.com, at least, where they create a zillion
short-lived connections.

I don't actually need these lines at all, as it turns out, because we were
already calling nowrite() correctly on the other end after a buffer got
emptied.
2011-01-12 09:33:56 -08:00
38bb7f3c21 If we get EPIPE on uwrite(), don't close, just do nowrite().
EPIPE doesn't mean the whole socket is dead, it just means we can't write to
it.  Maybe there's still data waiting to be read, though.
2011-01-12 09:19:43 -08:00
b7f1530aef Remember which syscall got a particular stream-killing error. 2011-01-12 09:18:46 -08:00
38 changed files with 3867 additions and 77 deletions

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)")
@ -226,6 +214,8 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
raise Fatal('expected server init string %r; got %r' raise Fatal('expected server init string %r; got %r'
% (expected, initstring)) % (expected, initstring))
debug1('connected.\n') debug1('connected.\n')
print 'Connected.'
sys.stdout.flush()
if daemon: if daemon:
daemonize() daemonize()
log('daemonizing (%s).\n' % _pidname) log('daemonizing (%s).\n' % _pidname)
@ -278,16 +268,44 @@ 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
chan = mux.next_channel() chan = mux.next_channel()
if not chan:
log('warning: too many open channels. Discarded connection.\n')
sock.close()
return
mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip) mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip)
outwrap = MuxWrapper(mux, chan) outwrap = MuxWrapper(mux, chan)
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))
@ -298,11 +316,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()
@ -313,8 +333,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:
@ -324,8 +343,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:
@ -338,11 +362,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())
@ -99,11 +131,11 @@ def _fill_oldctls(prefix):
def _sysctl_set(name, val): def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)] argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv)) debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w')) return ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
_changedctls = [] _changedctls = []
def sysctl_set(name, val): def sysctl_set(name, val, permanent=False):
PREFIX = 'net.inet.ip' PREFIX = 'net.inet.ip'
assert(name.startswith(PREFIX + '.')) assert(name.startswith(PREFIX + '.'))
val = str(val) val = str(val)
@ -114,8 +146,49 @@ def sysctl_set(name, val):
return return
oldval = _oldctls[name] oldval = _oldctls[name]
if val != oldval: if val != oldval:
_changedctls.append(name) rv = _sysctl_set(name, val)
return _sysctl_set(name, val) if rv==0 and permanent:
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
f = open('/etc/sysctl.conf', 'a')
f.write('\n'
'# Added by sshuttle\n'
'%s=%s\n' % (name, val))
f.close()
else:
_changedctls.append(name)
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 +199,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 +212,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, permanent=True)
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 +232,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 +299,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 +350,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 +408,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 +422,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 +437,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

17
main.py
View File

@ -54,12 +54,15 @@ 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
wrap= restart counting channel numbers after this number (for testing)
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,22 +70,26 @@ 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:
opt.syslog = 1 opt.syslog = 1
if opt.wrap:
import ssnet
ssnet.MAX_CHANNEL = int(opt.wrap)
helpers.verbose = opt.verbose helpers.verbose = opt.verbose
try: 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 +115,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

@ -60,7 +60,7 @@ def _tty_width():
except (IOError, ImportError): except (IOError, ImportError):
return _atoi(os.environ.get('WIDTH')) or 70 return _atoi(os.environ.get('WIDTH')) or 70
(ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s) (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
return xsize return xsize or 70
class Options: class Options:
@ -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

14
ssh.py
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') +

View File

@ -1,6 +1,6 @@
% sshuttle(8) Sshuttle 0.44 % sshuttle(8) Sshuttle 0.46
% Avery Pennarun <apenwarr@gmail.com> % Avery Pennarun <apenwarr@gmail.com>
% 2010-12-31 % 2011-01-25
# NAME # NAME
@ -109,6 +109,22 @@ entire subnet to the VPN.
if you use this option to give it a few names to start if you use this option to give it a few names to start
from. from.
--no-latency-control
: sacrifice latency to improve bandwidth benchmarks. ssh
uses really big socket buffers, which can overload the
connection if you start doing large file transfers,
thus making all your other sessions inside the same
tunnel go slowly. Normally, sshuttle tries to avoid
this problem using a "fullness check" that allows only
a certain amount of outstanding data to be buffered at
a time. But on high-bandwidth links, this can leave a
lot of your bandwidth underutilized. It also makes
sshuttle seem slow in bandwidth benchmarks (benchmarks
rarely test ping latency, which is what sshuttle is
trying to control). This option disables the latency
control feature, maximizing bandwidth usage. Use at
your own risk.
-D, --daemon -D, --daemon
: automatically fork into the background after connecting : automatically fork into the background after connecting
to the remote server. Implies `--syslog`. to the remote server. Implies `--syslog`.

View File

@ -1,6 +1,8 @@
import struct, socket, errno, select import struct, socket, errno, select
if not globals().get('skip_imports'): if not globals().get('skip_imports'):
from helpers import * from helpers import *
MAX_CHANNEL = 65535
# these don't exist in the socket module in python 2.3! # these don't exist in the socket module in python 2.3!
SHUT_RD = 0 SHUT_RD = 0
@ -21,6 +23,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 +37,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',
} }
@ -151,7 +157,7 @@ class SockWrapper:
try: try:
self.wsock.shutdown(SHUT_WR) self.wsock.shutdown(SHUT_WR)
except socket.error, e: except socket.error, e:
self.seterr(e) self.seterr('nowrite: %s' % e)
def too_full(self): def too_full(self):
return False # fullness is determined by the socket's select() state return False # fullness is determined by the socket's select() state
@ -163,9 +169,14 @@ class SockWrapper:
try: try:
return _nb_clean(os.write, self.wsock.fileno(), buf) return _nb_clean(os.write, self.wsock.fileno(), buf)
except OSError, e: except OSError, e:
# unexpected error... stream is dead if e.errno == errno.EPIPE:
self.seterr(e) debug1('%r: uwrite: got EPIPE\n' % self)
return 0 self.nowrite()
return 0
else:
# unexpected error... stream is dead
self.seterr('uwrite: %s' % e)
return 0
def write(self, buf): def write(self, buf):
assert(buf) assert(buf)
@ -180,7 +191,7 @@ class SockWrapper:
try: try:
return _nb_clean(os.read, self.rsock.fileno(), 65536) return _nb_clean(os.read, self.rsock.fileno(), 65536)
except OSError, e: except OSError, e:
self.seterr(e) self.seterr('uread: %s' % e)
return '' # unexpected error... we'll call it EOF return '' # unexpected error... we'll call it EOF
def fill(self): def fill(self):
@ -232,9 +243,7 @@ class Proxy(Handler):
self.wrap2 = wrap2 self.wrap2 = wrap2
def pre_select(self, r, w, x): def pre_select(self, r, w, x):
if self.wrap1.shut_read: self.wrap2.nowrite()
if self.wrap1.shut_write: self.wrap2.noread() if self.wrap1.shut_write: self.wrap2.noread()
if self.wrap2.shut_read: self.wrap1.nowrite()
if self.wrap2.shut_write: self.wrap1.noread() if self.wrap2.shut_write: self.wrap1.noread()
if self.wrap1.connect_to: if self.wrap1.connect_to:
@ -278,7 +287,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
@ -293,7 +302,7 @@ class Mux(Handler):
# channel 0 is special, so we never allocate it # channel 0 is special, so we never allocate it
for timeout in xrange(1024): for timeout in xrange(1024):
self.chani += 1 self.chani += 1
if self.chani > 65535: if self.chani > MAX_CHANNEL:
self.chani = 1 self.chani = 1
if not self.channels.get(self.chani): if not self.channels.get(self.chani):
return self.chani return self.chani
@ -340,6 +349,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)

86
stresstest.py Executable file
View File

@ -0,0 +1,86 @@
#!/usr/bin/python
import sys, os, socket, select, struct, time
listener = socket.socket()
listener.bind(('127.0.0.1', 0))
listener.listen(500)
servers = []
clients = []
remain = {}
NUMCLIENTS = 50
count = 0
while 1:
if len(clients) < NUMCLIENTS:
c = socket.socket()
c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
c.bind(('0.0.0.0', 0))
c.connect(listener.getsockname())
count += 1
if count >= 16384:
count = 1
print 'cli CREATING %d' % count
b = struct.pack('I', count) + 'x'*count
remain[c] = count
print 'cli >> %r' % len(b)
c.send(b)
c.shutdown(socket.SHUT_WR)
clients.append(c)
r = [listener]
time.sleep(0.1)
else:
r = [listener]+servers+clients
print 'select(%d)' % len(r)
r,w,x = select.select(r, [], [], 5)
assert(r)
for i in r:
if i == listener:
s,addr = listener.accept()
servers.append(s)
elif i in servers:
b = i.recv(4096)
print 'srv << %r' % len(b)
if not i in remain:
assert(len(b) >= 4)
want = struct.unpack('I', b[:4])[0]
b = b[4:]
#i.send('y'*want)
else:
want = remain[i]
if want < len(b):
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
assert(want >= len(b))
want -= len(b)
remain[i] = want
if not b: # EOF
if want:
print 'weird: eof but wanted %d more' % want
assert(want == 0)
i.close()
servers.remove(i)
del remain[i]
else:
print 'srv >> %r' % len(b)
i.send('y'*len(b))
if not want:
i.shutdown(socket.SHUT_WR)
elif i in clients:
b = i.recv(4096)
print 'cli << %r' % len(b)
want = remain[i]
if want < len(b):
print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b)
assert(want >= len(b))
want -= len(b)
remain[i] = want
if not b: # EOF
if want:
print 'weird: eof but wanted %d more' % want
assert(want == 0)
i.close()
clients.remove(i)
del remain[i]
listener.accept()

8
ui-macos/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*.pyc
*~
/*.nib
/debug.app
/sources.list
/Sshuttle VPN.app
/*.tar.gz
/*.zip

40
ui-macos/Info.plist Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>Sshuttle VPN</string>
<key>CFBundleExecutable</key>
<string>Sshuttle</string>
<key>CFBundleIconFile</key>
<string>app.icns</string>
<key>CFBundleIdentifier</key>
<string>ca.apenwarr.Sshuttle</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Sshuttle VPN</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>0.0.0</string>
<key>LSUIElement</key>
<string>1</string>
<key>LSHasLocalizedDisplayName</key>
<false/>
<key>NSAppleScriptEnabled</key>
<false/>
<key>NSHumanReadableCopyright</key>
<string>GNU LGPL Version 2</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

2609
ui-macos/MainMenu.xib Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>startAtLogin</key>
<false/>
<key>autoReconnect</key>
<true/>
</dict>
</plist>

1
ui-macos/all.do Normal file
View File

@ -0,0 +1 @@
redo-ifchange debug.app dist

BIN
ui-macos/app.icns Normal file

Binary file not shown.

28
ui-macos/askpass.py Normal file
View File

@ -0,0 +1,28 @@
import sys, os, re, subprocess
def askpass(prompt):
prompt = prompt.replace('"', "'")
if 'yes/no' in prompt:
return "yes"
script="""
tell application "Finder"
activate
display dialog "%s" \
with title "Sshuttle SSH Connection" \
default answer "" \
with icon caution \
with hidden answer
end tell
""" % prompt
p = subprocess.Popen(['osascript', '-e', script], stdout=subprocess.PIPE)
out = p.stdout.read()
rv = p.wait()
if rv:
return None
g = re.match("text returned:(.*), button returned:.*", out)
if not g:
return None
return g.group(1)

1
ui-macos/bits/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/runpython

1
ui-macos/bits/PkgInfo Normal file
View File

@ -0,0 +1 @@
APPL????

23
ui-macos/bits/runpython.c Normal file
View File

@ -0,0 +1,23 @@
/*
* This rather pointless program acts like the python interpreter, except
* it's intended to sit inside a MacOS .app package, so that its argv[0]
* will point inside the package.
*
* NSApplicationMain() looks for Info.plist using the path in argv[0], which
* goes wrong if your interpreter is /usr/bin/python.
*/
#include <Python.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char *path = strdup(argv[0]), *cptr;
char *args[] = {argv[0], "../Resources/main.py", NULL};
cptr = strrchr(path, '/');
if (cptr)
*cptr = 0;
chdir(path);
free(path);
return Py_Main(2, args);
}

View File

@ -0,0 +1,5 @@
exec >&2
redo-ifchange runpython.c
gcc -Wall -o $3 runpython.c \
-I/usr/include/python2.5 \
-lpython2.5

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

BIN
ui-macos/chicken-tiny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

4
ui-macos/clean.do Normal file
View File

@ -0,0 +1,4 @@
exec >&2
find -name '*~' | xargs rm -f
rm -rf *.app *.zip *.tar.gz
rm -f bits/runpython *.nib sources.list

15
ui-macos/debug.app.do Normal file
View File

@ -0,0 +1,15 @@
redo-ifchange bits/runpython MainMenu.nib
rm -rf debug.app
mkdir debug.app debug.app/Contents
cd debug.app/Contents
ln -s ../.. Resources
ln -s ../.. English.lproj
ln -s ../../Info.plist .
ln -s ../../app.icns .
mkdir MacOS
cd MacOS
ln -s ../../../bits/runpython Sshuttle
cd ../../..
redo-ifchange $(find debug.app -type f)

28
ui-macos/default.app.do Normal file
View File

@ -0,0 +1,28 @@
TOP=$PWD
redo-ifchange sources.list
redo-ifchange Info.plist bits/runpython \
$(while read name newname; do echo "$name"; done <sources.list)
rm -rf "$1.app"
mkdir "$1.app" "$1.app/Contents"
cd "$1.app/Contents"
cp "$TOP/Info.plist" .
mkdir MacOS
cp "$TOP/bits/runpython" MacOS/Sshuttle
mkdir Resources
cd "$TOP"
while read name newname; do
[ -z "$name" ] && continue
: "${newname:=$name}"
outname=$1.app/Contents/Resources/$newname
outdir=$(dirname "$outname")
[ -d "$outdir" ] || mkdir "$outdir"
cp "${name-$newname}" "$outname"
done <sources.list
cd "$1.app"
redo-ifchange $(find . -type f)

View File

@ -0,0 +1,5 @@
exec >&2
IFS="
"
redo-ifchange $1.app
tar -czf $3 $1.app/

View File

@ -0,0 +1,5 @@
exec >&2
IFS="
"
redo-ifchange $1.app
zip -q -r $3 $1.app/

2
ui-macos/default.nib.do Normal file
View File

@ -0,0 +1,2 @@
redo-ifchange $1.xib
ibtool --compile $3 $1.xib

1
ui-macos/dist.do Normal file
View File

@ -0,0 +1 @@
redo-ifchange "Sshuttle VPN.app.zip" "Sshuttle VPN.app.tar.gz"

19
ui-macos/git-export.do Normal file
View File

@ -0,0 +1,19 @@
# update a local branch with pregenerated output files, so people can download
# the completed tarballs from github. Since we don't have any real binaries,
# our final distribution package contains mostly blobs from the source code,
# so this doesn't cost us much extra space in the repo.
BRANCH=dist/macos
redo-ifchange 'Sshuttle VPN.app'
git update-ref refs/heads/$BRANCH origin/$BRANCH '' 2>/dev/null || true
export GIT_INDEX_FILE=$PWD/gitindex.tmp
rm -f "$GIT_INDEX_FILE"
git add -f 'Sshuttle VPN.app'
MSG="MacOS precompiled app package for $(git describe)"
TREE=$(git write-tree --prefix=ui-macos)
git show-ref refs/heads/$BRANCH >/dev/null && PARENT="-p refs/heads/$BRANCH"
COMMITID=$(echo "$MSG" | git commit-tree $TREE $PARENT)
git update-ref refs/heads/$BRANCH $COMMITID
rm -f "$GIT_INDEX_FILE"

367
ui-macos/main.py Normal file
View File

@ -0,0 +1,367 @@
import sys, os, pty
from AppKit import *
import my, models, askpass
def sshuttle_args(host, auto_nets, auto_hosts, dns, nets, debug,
no_latency_control):
argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host]
assert(argv[0])
if debug:
argv.append('-v')
if auto_nets:
argv.append('--auto-nets')
if auto_hosts:
argv.append('--auto-hosts')
if dns:
argv.append('--dns')
if no_latency_control:
argv.append('--no-latency-control')
argv += nets
return argv
class _Callback(NSObject):
def initWithFunc_(self, func):
self = super(_Callback, self).init()
self.func = func
return self
def func_(self, obj):
return self.func(obj)
class Callback:
def __init__(self, func):
self.obj = _Callback.alloc().initWithFunc_(func)
self.sel = self.obj.func_
class Runner:
def __init__(self, argv, logfunc, promptfunc, serverobj):
print 'in __init__'
self.id = argv
self.rv = None
self.pid = None
self.fd = None
self.logfunc = logfunc
self.promptfunc = promptfunc
self.serverobj = serverobj
self.buf = ''
self.logfunc('\nConnecting to %s.\n' % self.serverobj.host())
print 'will run: %r' % argv
self.serverobj.setConnected_(False)
pid,fd = pty.fork()
if pid == 0:
# child
try:
os.execvp(argv[0], argv)
except Exception, e:
sys.stderr.write('failed to start: %r\n' % e)
raise
finally:
os._exit(42)
# parent
self.pid = pid
self.file = NSFileHandle.alloc()\
.initWithFileDescriptor_closeOnDealloc_(fd, True)
self.cb = Callback(self.gotdata)
NSNotificationCenter.defaultCenter()\
.addObserver_selector_name_object_(self.cb.obj, self.cb.sel,
NSFileHandleDataAvailableNotification, self.file)
self.file.waitForDataInBackgroundAndNotify()
def __del__(self):
self.wait()
def _try_wait(self, options):
if self.rv == None and self.pid > 0:
pid,code = os.waitpid(self.pid, options)
if pid == self.pid:
if os.WIFEXITED(code):
self.rv = os.WEXITSTATUS(code)
else:
self.rv = -os.WSTOPSIG(code)
self.serverobj.setConnected_(False)
self.serverobj.setError_('VPN process died')
self.logfunc('Disconnected.\n')
print 'wait_result: %r' % self.rv
return self.rv
def wait(self):
return self._try_wait(0)
def poll(self):
return self._try_wait(os.WNOHANG)
def kill(self):
assert(self.pid > 0)
print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
if self.rv == None:
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
os.kill(self.pid, 15)
self.wait()
def gotdata(self, notification):
print 'gotdata!'
d = str(self.file.availableData())
if d:
self.logfunc(d)
self.buf = self.buf + d
if 'Connected.\r\n' in self.buf:
self.serverobj.setConnected_(True)
self.buf = self.buf[-4096:]
if self.buf.strip().endswith(':'):
lastline = self.buf.rstrip().split('\n')[-1]
resp = self.promptfunc(lastline)
add = ' (response)\n'
self.buf += add
self.logfunc(add)
self.file.writeData_(my.Data(resp + '\n'))
self.file.waitForDataInBackgroundAndNotify()
self.poll()
#print 'gotdata done!'
class SshuttleApp(NSObject):
def initialize(self):
d = my.PList('UserDefaults')
my.Defaults().registerDefaults_(d)
class SshuttleController(NSObject):
# Interface builder outlets
startAtLoginField = objc.IBOutlet()
autoReconnectField = objc.IBOutlet()
debugField = objc.IBOutlet()
routingField = objc.IBOutlet()
prefsWindow = objc.IBOutlet()
serversController = objc.IBOutlet()
logField = objc.IBOutlet()
latencyControlField = objc.IBOutlet()
servers = []
conns = {}
def _connect(self, server):
host = server.host()
print 'connecting %r' % host
self.fill_menu()
def logfunc(msg):
print 'log! (%d bytes)' % len(msg)
self.logField.textStorage()\
.appendAttributedString_(NSAttributedString.alloc()\
.initWithString_(msg))
self.logField.didChangeText()
def promptfunc(prompt):
print 'prompt! %r' % prompt
return askpass.askpass(prompt)
nets_mode = server.autoNets()
if nets_mode == models.NET_MANUAL:
manual_nets = ["%s/%d" % (i.subnet(), i.width())
for i in server.nets()]
elif nets_mode == models.NET_ALL:
manual_nets = ['0/0']
else:
manual_nets = []
noLatencyControl = (server.latencyControl() != models.LAT_INTERACTIVE)
conn = Runner(sshuttle_args(host,
auto_nets = nets_mode == models.NET_AUTO,
auto_hosts = server.autoHosts(),
dns = server.useDns(),
nets = manual_nets,
debug = self.debugField.state(),
no_latency_control = noLatencyControl),
logfunc=logfunc, promptfunc=promptfunc,
serverobj=server)
self.conns[host] = conn
def _disconnect(self, server):
host = server.host()
print 'disconnecting %r' % host
conn = self.conns.get(host)
if conn:
conn.kill()
self.fill_menu()
self.logField.textStorage().setAttributedString_(
NSAttributedString.alloc().initWithString_(''))
@objc.IBAction
def cmd_connect(self, sender):
server = sender.representedObject()
server.setWantConnect_(True)
@objc.IBAction
def cmd_disconnect(self, sender):
server = sender.representedObject()
server.setWantConnect_(False)
@objc.IBAction
def cmd_show(self, sender):
self.prefsWindow.makeKeyAndOrderFront_(self)
NSApp.activateIgnoringOtherApps_(True)
@objc.IBAction
def cmd_quit(self, sender):
NSApp.performSelector_withObject_afterDelay_(NSApp.terminate_,
None, 0.0)
def fill_menu(self):
menu = self.menu
menu.removeAllItems()
def additem(name, func, obj):
it = menu.addItemWithTitle_action_keyEquivalent_(name, None, "")
it.setRepresentedObject_(obj)
it.setTarget_(self)
it.setAction_(func)
def addnote(name):
additem(name, None, None)
any_inprogress = None
any_conn = None
any_err = None
if len(self.servers):
for i in self.servers:
host = i.host()
title = i.title()
want = i.wantConnect()
connected = i.connected()
numnets = len(list(i.nets()))
if not host:
additem('Connect Untitled', None, i)
elif i.autoNets() == models.NET_MANUAL and not numnets:
additem('Connect %s (no routes)' % host, None, i)
elif want:
any_conn = i
additem('Disconnect %s' % title, self.cmd_disconnect, i)
else:
additem('Connect %s' % title, self.cmd_connect, i)
if not want:
msg = 'Off'
elif i.error():
msg = 'ERROR - try reconnecting'
any_err = i
elif connected:
msg = 'Connected'
else:
msg = 'Connecting...'
any_inprogress = i
addnote(' State: %s' % msg)
else:
addnote('No servers defined yet')
menu.addItem_(NSMenuItem.separatorItem())
additem('Preferences...', self.cmd_show, None)
additem('Quit Sshuttle VPN', self.cmd_quit, None)
if any_err:
self.statusitem.setImage_(self.img_err)
self.statusitem.setTitle_('Error!')
elif any_conn:
self.statusitem.setImage_(self.img_running)
if any_inprogress:
self.statusitem.setTitle_('Connecting...')
else:
self.statusitem.setTitle_('')
else:
self.statusitem.setImage_(self.img_idle)
self.statusitem.setTitle_('')
def load_servers(self):
l = my.Defaults().arrayForKey_('servers') or []
sl = []
for s in l:
host = s.get('host', None)
if not host: continue
nets = s.get('nets', [])
nl = []
for n in nets:
subnet = n[0]
width = n[1]
net = models.SshuttleNet.alloc().init()
net.setSubnet_(subnet)
net.setWidth_(width)
nl.append(net)
autoNets = s.get('autoNets', models.NET_AUTO)
autoHosts = s.get('autoHosts', True)
useDns = s.get('useDns', autoNets == models.NET_ALL)
latencyControl = s.get('latencyControl', models.LAT_INTERACTIVE)
srv = models.SshuttleServer.alloc().init()
srv.setHost_(host)
srv.setAutoNets_(autoNets)
srv.setAutoHosts_(autoHosts)
srv.setNets_(nl)
srv.setUseDns_(useDns)
srv.setLatencyControl_(latencyControl)
sl.append(srv)
self.serversController.addObjects_(sl)
self.serversController.setSelectionIndex_(0)
def save_servers(self):
l = []
for s in self.servers:
host = s.host()
if not host: continue
nets = []
for n in s.nets():
subnet = n.subnet()
if not subnet: continue
nets.append((subnet, n.width()))
d = dict(host=s.host(),
nets=nets,
autoNets=s.autoNets(),
autoHosts=s.autoHosts(),
useDns=s.useDns(),
latencyControl=s.latencyControl())
l.append(d)
my.Defaults().setObject_forKey_(l, 'servers')
self.fill_menu()
def awakeFromNib(self):
self.routingField.removeAllItems()
tf = self.routingField.addItemWithTitle_
tf('Send all traffic through this server')
tf('Determine automatically')
tf('Custom...')
self.latencyControlField.removeAllItems()
tf = self.latencyControlField.addItemWithTitle_
tf('Fast transfer')
tf('Low latency')
# Hmm, even when I mark this as !enabled in the .nib, it still comes
# through as enabled. So let's just disable it here (since we don't
# support this feature yet).
self.startAtLoginField.setEnabled_(False)
self.startAtLoginField.setState_(False)
self.autoReconnectField.setEnabled_(False)
self.autoReconnectField.setState_(False)
self.load_servers()
# Initialize our menu item
self.menu = NSMenu.alloc().initWithTitle_('Sshuttle')
bar = NSStatusBar.systemStatusBar()
statusitem = bar.statusItemWithLength_(NSVariableStatusItemLength)
self.statusitem = statusitem
self.img_idle = my.Image('chicken-tiny-bw', 'png')
self.img_running = my.Image('chicken-tiny', 'png')
self.img_err = my.Image('chicken-tiny-err', 'png')
statusitem.setImage_(self.img_idle)
statusitem.setHighlightMode_(True)
statusitem.setMenu_(self.menu)
self.fill_menu()
models.configchange_callback = my.DelayedCallback(self.save_servers)
def sc(server):
if server.wantConnect():
self._connect(server)
else:
self._disconnect(server)
models.setconnect_callback = sc
# Note: NSApplicationMain calls sys.exit(), so this never returns.
NSApplicationMain(sys.argv)

166
ui-macos/models.py Normal file
View File

@ -0,0 +1,166 @@
from AppKit import *
import my
configchange_callback = setconnect_callback = None
def config_changed():
if configchange_callback:
configchange_callback()
def _validate_ip(v):
parts = v.split('.')[:4]
if len(parts) < 4:
parts += ['0'] * (4 - len(parts))
for i in range(4):
n = my.atoi(parts[i])
if n < 0:
n = 0
elif n > 255:
n = 255
parts[i] = str(n)
return '.'.join(parts)
def _validate_width(v):
n = my.atoi(v)
if n < 0:
n = 0
elif n > 32:
n = 32
return n
class SshuttleNet(NSObject):
def subnet(self):
return getattr(self, '_k_subnet', None)
def setSubnet_(self, v):
self._k_subnet = v
config_changed()
@objc.accessor
def validateSubnet_error_(self, value, error):
#print 'validateSubnet!'
return True, _validate_ip(value), error
def width(self):
return getattr(self, '_k_width', 24)
def setWidth_(self, v):
self._k_width = v
config_changed()
@objc.accessor
def validateWidth_error_(self, value, error):
#print 'validateWidth!'
return True, _validate_width(value), error
NET_ALL = 0
NET_AUTO = 1
NET_MANUAL = 2
LAT_BANDWIDTH = 0
LAT_INTERACTIVE = 1
class SshuttleServer(NSObject):
def init(self):
self = super(SshuttleServer, self).init()
config_changed()
return self
def wantConnect(self):
return getattr(self, '_k_wantconnect', False)
def setWantConnect_(self, v):
self._k_wantconnect = v
self.setError_(None)
config_changed()
if setconnect_callback: setconnect_callback(self)
def connected(self):
return getattr(self, '_k_connected', False)
def setConnected_(self, v):
print 'setConnected of %r to %r' % (self, v)
self._k_connected = v
if v: self.setError_(None) # connected ok, so no error
config_changed()
def error(self):
return getattr(self, '_k_error', None)
def setError_(self, v):
self._k_error = v
config_changed()
def isValid(self):
if not self.host():
return False
if self.autoNets() == NET_MANUAL and not len(list(self.nets())):
return False
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):
return getattr(self, '_k_host', None)
def setHost_(self, v):
self._k_host = v
self.setTitle_(None)
config_changed()
@objc.accessor
def validateHost_error_(self, value, error):
#print 'validatehost! %r %r %r' % (self, value, error)
while value.startswith('-'):
value = value[1:]
return True, value, error
def nets(self):
return getattr(self, '_k_nets', [])
def setNets_(self, v):
self._k_nets = v
self.setTitle_(None)
config_changed()
def netsHidden(self):
#print 'checking netsHidden'
return self.autoNets() != NET_MANUAL
def setNetsHidden_(self, v):
config_changed()
#print 'setting netsHidden to %r' % v
def autoNets(self):
return getattr(self, '_k_autoNets', NET_AUTO)
def setAutoNets_(self, v):
self._k_autoNets = v
self.setNetsHidden_(-1)
self.setUseDns_(v == NET_ALL)
self.setTitle_(None)
config_changed()
def autoHosts(self):
return getattr(self, '_k_autoHosts', True)
def setAutoHosts_(self, v):
self._k_autoHosts = v
config_changed()
def useDns(self):
return getattr(self, '_k_useDns', False)
def setUseDns_(self, v):
self._k_useDns = v
config_changed()
def latencyControl(self):
return getattr(self, '_k_latencyControl', LAT_INTERACTIVE)
def setLatencyControl_(self, v):
self._k_latencyControl = v
config_changed()

62
ui-macos/my.py Normal file
View File

@ -0,0 +1,62 @@
import sys, os
from AppKit import *
import PyObjCTools.AppHelper
def bundle_path(name, typ):
if typ:
return NSBundle.mainBundle().pathForResource_ofType_(name, typ)
else:
return os.path.join(NSBundle.mainBundle().resourcePath(), name)
# Load an NSData using a python string
def Data(s):
return NSData.alloc().initWithBytes_length_(s, len(s))
# Load a property list from a file in the application bundle.
def PList(name):
path = bundle_path(name, 'plist')
return NSDictionary.dictionaryWithContentsOfFile_(path)
# Load an NSImage from a file in the application bundle.
def Image(name, ext):
bytes = open(bundle_path(name, ext)).read()
img = NSImage.alloc().initWithData_(Data(bytes))
return img
# Return the NSUserDefaults shared object.
def Defaults():
return NSUserDefaults.standardUserDefaults()
# Usage:
# f = DelayedCallback(func, args...)
# later:
# f()
#
# When you call f(), it will schedule a call to func() next time the
# ObjC event loop iterates. Multiple calls to f() in a single iteration
# will only result in one call to func().
#
def DelayedCallback(func, *args, **kwargs):
flag = [0]
def _go():
if flag[0]:
print 'running %r (flag=%r)' % (func, flag)
flag[0] = 0
func(*args, **kwargs)
def call():
flag[0] += 1
PyObjCTools.AppHelper.callAfter(_go)
return call
def atoi(s):
try:
return int(s)
except ValueError:
return 0

4
ui-macos/run.do Normal file
View File

@ -0,0 +1,4 @@
redo-ifchange debug.app
exec >&2
./debug.app/Contents/MacOS/Sshuttle

14
ui-macos/sources.list.do Normal file
View File

@ -0,0 +1,14 @@
redo-always
exec >$3
cat <<-EOF
app.icns
MainMenu.nib English.lproj/MainMenu.nib
UserDefaults.plist
chicken-tiny.png
chicken-tiny-bw.png
chicken-tiny-err.png
EOF
for d in *.py sshuttle/*.py sshuttle/sshuttle sshuttle/compat/*.py; do
echo $d
done
redo-stamp <$3

1
ui-macos/sshuttle Symbolic link
View File

@ -0,0 +1 @@
..