Commit 096bbcc5 authored by Avery Pennarun's avatar Avery Pennarun

Client "almost" works on MacOS and maybe FreeBSD.

Basic forwarding now works on MacOS, assuming you set up ipfw correctly
(ha ha).  I wasted a few hours today trying to figure this out, and I'm *so
very close*, but unfortunately it just didn't work.  Think you can figure it
out?

Related changes:
- don't die if iptables is unavailable
- BSD uses getsockname() instead of SO_ORIGINAL_DST
- non-blocking connect() returns EISCONN once it's connected
- you can't setsockopt IP_TTL more than once
parent 7bd0efd5
...@@ -4,13 +4,19 @@ from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper ...@@ -4,13 +4,19 @@ from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import * from helpers import *
def original_dst(sock): def original_dst(sock):
SO_ORIGINAL_DST = 80 try:
SOCKADDR_MIN = 16 SO_ORIGINAL_DST = 80
sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN) SOCKADDR_MIN = 16
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) sockaddr_in = sock.getsockopt(socket.SOL_IP,
assert(socket.htons(proto) == socket.AF_INET) SO_ORIGINAL_DST, SOCKADDR_MIN)
ip = '%d.%d.%d.%d' % (a,b,c,d) (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
return (ip,port) assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a,b,c,d)
return (ip,port)
except socket.error, e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class IPTables: class IPTables:
...@@ -105,7 +111,7 @@ def _main(listener, ipt, use_server, remotename): ...@@ -105,7 +111,7 @@ def _main(listener, ipt, use_server, remotename):
dstip = original_dst(sock) dstip = original_dst(sock)
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1], debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1])) dstip[0],dstip[1]))
if dstip == sock.getsockname(): if dstip == listener.getsockname():
debug1("-- ignored: that's my address!\n") debug1("-- ignored: that's my address!\n")
sock.close() sock.close()
return return
...@@ -150,7 +156,7 @@ def main(listenip, use_server, remotename, subnets): ...@@ -150,7 +156,7 @@ def main(listenip, use_server, remotename, subnets):
if listenip[1]: if listenip[1]:
ports = [listenip[1]] ports = [listenip[1]]
else: else:
ports = xrange(12300,65536) ports = xrange(12300,9000,-1)
last_e = None last_e = None
bound = False bound = False
debug2('Binding:') debug2('Binding:')
......
...@@ -3,7 +3,7 @@ import helpers ...@@ -3,7 +3,7 @@ import helpers
from helpers import * from helpers import *
def chain_exists(name): def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL'] argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.PIPE) p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout: for line in p.stdout:
...@@ -22,11 +22,16 @@ def ipt(*args): ...@@ -22,11 +22,16 @@ def ipt(*args):
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def do_it(port, subnets): # We name the chain based on the transproxy port number so that it's possible
# to run multiple copies of sshuttle at the same time. Of course, the
# multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def do_iptables(port, subnets):
chain = 'sshuttle-%s' % port chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
if chain_exists(chain): if ipt_chain_exists(chain):
ipt('-D', 'OUTPUT', '-j', chain) ipt('-D', 'OUTPUT', '-j', chain)
ipt('-D', 'PREROUTING', '-j', chain) ipt('-D', 'PREROUTING', '-j', chain)
ipt('-F', chain) ipt('-F', chain)
...@@ -48,33 +53,113 @@ def do_it(port, subnets): ...@@ -48,33 +53,113 @@ def do_it(port, subnets):
) )
# This is some iptables voodoo for setting up the Linux kernel's transparent def ipfw_rule_exists(n):
# proxying stuff. If subnets is empty, we just delete our sshuttle chain; argv = ['ipfw', 'list']
# otherwise we delete it, then make it from scratch. p = subprocess.Popen(argv, stdout = subprocess.PIPE)
# for line in p.stdout:
# We name the chain based on the transproxy port number so that it's possible if line.startswith('%05d ' % n):
# to run multiple copies of sshuttle at the same time. Of course, the if line[5:].find('ipttl 42') < 0:
# multiple copies shouldn't have overlapping subnets, or only the most- raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
# recently-started one will win (because we use "-I OUTPUT 1" instead of return True
# "-A OUTPUT"). rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def sysctl_get(name):
argv = ['sysctl', '-n', name]
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
line = p.stdout.readline()
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
if not line:
raise Fatal('%r returned no data' % (argv,))
assert(line[-1] == '\n')
return line[:-1]
def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv, stdout = open('/dev/null', 'w'))
_oldctls = []
def sysctl_set(name, val):
oldval = sysctl_get(name)
if str(val) != str(oldval):
_oldctls.append((name, oldval))
return _sysctl_set(name, val)
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, subnets):
sport = str(port)
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('del', sport)
while _oldctls:
(name,oldval) = _oldctls.pop()
_sysctl_set(name, oldval)
if subnets:
sysctl_set('net.inet.ip.fw.enable', 1)
sysctl_set('net.inet.ip.forwarding', 1)
# create new subnet entries
for snet,swidth in subnets:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'log', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'not', 'ipttl', '42')
def program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
for p in paths:
fn = '%s/%s' % (p, name)
if os.path.exists(fn):
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
# This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch.
# #
# This code is supposed to clean up after itself by deleting extra chains on # This code is supposed to clean up after itself by deleting its rules on
# 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 iptables # supercede it in the transproxy list, at least, so the leftover rules
# chains are mostly harmless. # are hopefully harmless.
def main(port, subnets): def main(port, subnets):
assert(port > 0) assert(port > 0)
assert(port <= 65535) assert(port <= 65535)
if os.getuid() != 0: if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set up iptables') raise Fatal('you must be root (or enable su/sudo) to set the firewall')
if program_exists('ipfw'):
do_it = do_ipfw
elif program_exists('iptables'):
do_it = do_iptables
else:
raise Fatal("can't find either ipfw or iptables; check your PATH")
# because of limitations of the 'su' command, the *real* stdin/stdout # because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we # are both attached to stdout initially. Clone stdout into stdin so we
# can read from it. # can read from it.
os.dup2(1, 0) os.dup2(1, 0)
debug1('iptables manager ready.\n') debug1('firewall manager ready.\n')
sys.stdout.write('READY\n') sys.stdout.write('READY\n')
sys.stdout.flush() sys.stdout.flush()
...@@ -89,10 +174,10 @@ def main(port, subnets): ...@@ -89,10 +174,10 @@ def main(port, subnets):
if not line: if not line:
return # parent died; nothing to do return # parent died; nothing to do
if line != 'GO\n': if line != 'GO\n':
raise Fatal('iptables: expected GO but got %r' % line) raise Fatal('firewall: expected GO but got %r' % line)
try: try:
if line: if line:
debug1('iptables manager: starting transproxy.\n') debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets) do_it(port, subnets)
sys.stdout.write('STARTED\n') sys.stdout.write('STARTED\n')
...@@ -111,7 +196,7 @@ def main(port, subnets): ...@@ -111,7 +196,7 @@ def main(port, subnets):
finally: finally:
try: try:
debug1('iptables manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except:
pass pass
do_it(port, []) do_it(port, [])
...@@ -71,14 +71,17 @@ class SockWrapper: ...@@ -71,14 +71,17 @@ class SockWrapper:
def try_connect(self): def try_connect(self):
if not self.connect_to: if not self.connect_to:
return # already connected return # already connected
self.rsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.rsock.setblocking(False) self.rsock.setblocking(False)
try: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux)
self.connect_to = None self.connect_to = None
except socket.error, e: except socket.error, e:
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet pass # not connected yet
elif e.args[0] == errno.EISCONN:
# connected successfully (BSD)
self.connect_to = None
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]: elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]:
# a "normal" kind of error # a "normal" kind of error
self.connect_to = None self.connect_to = None
...@@ -387,6 +390,7 @@ class MuxWrapper(SockWrapper): ...@@ -387,6 +390,7 @@ class MuxWrapper(SockWrapper):
def connect_dst(ip, port): def connect_dst(ip, port):
debug2('Connecting to %s:%d\n' % (ip, port)) debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket() outsock = socket.socket()
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock, return SockWrapper(outsock, outsock,
connect_to = (ip,port), connect_to = (ip,port),
peername = '%s:%d' % (ip,port)) peername = '%s:%d' % (ip,port))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment