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
from helpers import *
def original_dst(sock):
try:
SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN)
sockaddr_in = sock.getsockopt(socket.SOL_IP,
SO_ORIGINAL_DST, SOCKADDR_MIN)
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
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:
......@@ -105,7 +111,7 @@ def _main(listener, ipt, use_server, remotename):
dstip = original_dst(sock)
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1]))
if dstip == sock.getsockname():
if dstip == listener.getsockname():
debug1("-- ignored: that's my address!\n")
sock.close()
return
......@@ -150,7 +156,7 @@ def main(listenip, use_server, remotename, subnets):
if listenip[1]:
ports = [listenip[1]]
else:
ports = xrange(12300,65536)
ports = xrange(12300,9000,-1)
last_e = None
bound = False
debug2('Binding:')
......
......@@ -3,7 +3,7 @@ import helpers
from helpers import *
def chain_exists(name):
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout:
......@@ -22,11 +22,16 @@ def ipt(*args):
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
# basic cleanup/setup of chains
if chain_exists(chain):
if ipt_chain_exists(chain):
ipt('-D', 'OUTPUT', '-j', chain)
ipt('-D', 'PREROUTING', '-j', chain)
ipt('-F', chain)
......@@ -48,33 +53,113 @@ def do_it(port, subnets):
)
# This is some iptables voodoo for setting up the Linux kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle chain;
# otherwise we delete it, then make it from scratch.
#
# 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 ipfw_rule_exists(n):
argv = ['ipfw', 'list']
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout:
if line.startswith('%05d ' % n):
if line[5:].find('ipttl 42') < 0:
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
return True
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
# supercede it in the transproxy list, at least, so the leftover iptables
# chains are mostly harmless.
# supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
def main(port, subnets):
assert(port > 0)
assert(port <= 65535)
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
# are both attached to stdout initially. Clone stdout into stdin so we
# can read from it.
os.dup2(1, 0)
debug1('iptables manager ready.\n')
debug1('firewall manager ready.\n')
sys.stdout.write('READY\n')
sys.stdout.flush()
......@@ -89,10 +174,10 @@ def main(port, subnets):
if not line:
return # parent died; nothing to do
if line != 'GO\n':
raise Fatal('iptables: expected GO but got %r' % line)
raise Fatal('firewall: expected GO but got %r' % line)
try:
if line:
debug1('iptables manager: starting transproxy.\n')
debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets)
sys.stdout.write('STARTED\n')
......@@ -111,7 +196,7 @@ def main(port, subnets):
finally:
try:
debug1('iptables manager: undoing changes.\n')
debug1('firewall manager: undoing changes.\n')
except:
pass
do_it(port, [])
......@@ -71,14 +71,17 @@ class SockWrapper:
def try_connect(self):
if not self.connect_to:
return # already connected
self.rsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.rsock.setblocking(False)
try:
self.rsock.connect(self.connect_to)
# connected successfully (Linux)
self.connect_to = None
except socket.error, e:
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
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]:
# a "normal" kind of error
self.connect_to = None
......@@ -387,6 +390,7 @@ class MuxWrapper(SockWrapper):
def connect_dst(ip, port):
debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket()
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock,
connect_to = (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