TProxy IPv6 support.

This commit is contained in:
Brian May 2011-07-11 10:59:36 +10:00
parent f41c6b62e5
commit 20254bab57
5 changed files with 126 additions and 28 deletions

View File

@ -99,41 +99,55 @@ def original_dst(sock):
class MultiListener: class MultiListener:
def __init__(self, type=socket.SOCK_STREAM, proto=0): def __init__(self, type=socket.SOCK_STREAM, proto=0):
self.v6 = socket.socket(socket.AF_INET6, type, proto)
self.v4 = socket.socket(socket.AF_INET, type, proto) self.v4 = socket.socket(socket.AF_INET, type, proto)
def setsockopt(self, level, optname, value): def setsockopt(self, level, optname, value):
if self.v6:
self.v6.setsockopt(level, optname, value)
if self.v4: if self.v4:
self.v4.setsockopt(level, optname, value) self.v4.setsockopt(level, optname, value)
def add_handler(self, handlers, callback, method, mux): def add_handler(self, handlers, callback, method, mux):
if self.v6:
handlers.append(Handler([self.v6], lambda: callback(self.v6, method, mux, handlers)))
if self.v4: if self.v4:
handlers.append(Handler([self.v4], lambda: callback(self.v4, method, mux, handlers))) handlers.append(Handler([self.v4], lambda: callback(self.v4, method, mux, handlers)))
def listen(self, backlog): def listen(self, backlog):
if self.v6:
self.v6.listen(backlog)
if self.v4: if self.v4:
self.v4.listen(backlog) self.v4.listen(backlog)
def bind(self, address_v4): def bind(self, address_v6, address_v4):
if address_v6 and self.v6:
self.v6.bind(address_v6)
else:
self.v6 = None
if address_v4 and self.v4: if address_v4 and self.v4:
self.v4.bind(address_v4) self.v4.bind(address_v4)
else: else:
self.v4 = None self.v4 = None
def print_listening(self, what): def print_listening(self, what):
if self.v6:
listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
if self.v4: if self.v4:
listenip = self.v4.getsockname() listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip)) debug1('%s listening on %r.\n' % (what, listenip))
class FirewallClient: class FirewallClient:
def __init__(self, port_v4, subnets_include, subnets_exclude, dnsport_v4, method): def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method):
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
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
['-v'] * (helpers.verbose or 0) + ['-v'] * (helpers.verbose or 0) +
['--firewall', str(port_v4), ['--firewall', str(port_v6), str(port_v4),
str(dnsport_v4), str(dnsport_v6), str(dnsport_v4),
method]) method])
if ssyslog._p: if ssyslog._p:
argvbase += ['--syslog'] argvbase += ['--syslog']
@ -376,7 +390,7 @@ def _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control,
mux.callback() mux.callback()
def main(listenip_v4, def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, ssh_cmd, remotename, python, latency_control, dns,
method, seed_hosts, auto_nets, method, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile): subnets_include, subnets_exclude, syslog, daemon, pidfile):
@ -390,7 +404,7 @@ def main(listenip_v4,
return 5 return 5
debug1('Starting sshuttle proxy.\n') debug1('Starting sshuttle proxy.\n')
if listenip_v4 and listenip_v4[1]: if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
# if both ports given, no need to search for a spare port # if both ports given, no need to search for a spare port
ports = [ 0, ] ports = [ 0, ]
else: else:
@ -399,6 +413,7 @@ def main(listenip_v4,
# search for free ports and try to bind # search for free ports and try to bind
last_e = None last_e = None
redirectport_v6 = 0
redirectport_v4 = 0 redirectport_v4 = 0
bound = False bound = False
debug2('Binding redirector:') debug2('Binding redirector:')
@ -407,6 +422,16 @@ def main(listenip_v4,
tcp_listener = MultiListener() tcp_listener = MultiListener()
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if listenip_v6 and listenip_v6[1]:
lv6 = listenip_v6
redirectport_v6 = lv6[1]
elif listenip_v6:
lv6 = (listenip_v6[0],port)
redirectport_v6 = port
else:
lv6 = None
redirectport_v6 = 0
if listenip_v4 and listenip_v4[1]: if listenip_v4 and listenip_v4[1]:
lv4 = listenip_v4 lv4 = listenip_v4
redirectport_v4 = lv4[1] redirectport_v4 = lv4[1]
@ -418,7 +443,7 @@ def main(listenip_v4,
redirectport_v4 = 0 redirectport_v4 = 0
try: try:
tcp_listener.bind(lv4) tcp_listener.bind(lv6, lv4)
bound = True bound = True
break break
except socket.error, e: except socket.error, e:
@ -443,6 +468,13 @@ def main(listenip_v4,
dns_listener = MultiListener(socket.SOCK_DGRAM) dns_listener = MultiListener(socket.SOCK_DGRAM)
dns_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) dns_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if listenip_v6:
lv6 = (listenip_v6[0],port)
dnsport_v6 = port
else:
lv6 = None
dnsport_v6 = 0
if listenip_v4: if listenip_v4:
lv4 = (listenip_v4[0],port) lv4 = (listenip_v4[0],port)
dnsport_v4 = port dnsport_v4 = port
@ -451,7 +483,7 @@ def main(listenip_v4,
dnsport_v4 = 0 dnsport_v4 = 0
try: try:
dns_listener.bind(lv4) dns_listener.bind(lv6, lv4)
bound = True bound = True
break break
except socket.error, e: except socket.error, e:
@ -465,10 +497,11 @@ def main(listenip_v4,
assert(last_e) assert(last_e)
raise last_e raise last_e
else: else:
dnsport_v6 = 0
dnsport_v4 = 0 dnsport_v4 = 0
dns_listener = None dns_listener = None
fw = FirewallClient(redirectport_v4, subnets_include, subnets_exclude, dnsport_v4, method) fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method)
if fw.method == "tproxy": if fw.method == "tproxy":
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)

View File

@ -15,7 +15,9 @@ def nonfatal(func, *args):
def ipt_chain_exists(family, table, name): def ipt_chain_exists(family, table, name):
if family == socket.AF_INET: if family == socket.AF_INET6:
cmd = 'ip6tables'
elif family == socket.AF_INET:
cmd = 'iptables' cmd = 'iptables'
else: else:
raise Exception('Unsupported family "%s"'%family_to_string(family)) raise Exception('Unsupported family "%s"'%family_to_string(family))
@ -30,7 +32,9 @@ def ipt_chain_exists(family, table, name):
def _ipt(family, table, *args): def _ipt(family, table, *args):
if family == socket.AF_INET: if family == socket.AF_INET6:
argv = ['ip6tables', '-t', table] + list(args)
elif family == socket.AF_INET:
argv = ['iptables', '-t', table] + list(args) argv = ['iptables', '-t', table] + list(args)
else: else:
raise Exception('Unsupported family "%s"'%family_to_string(family)) raise Exception('Unsupported family "%s"'%family_to_string(family))
@ -110,7 +114,7 @@ def do_iptables_nat(port, dnsport, family, subnets):
if dnsport: if dnsport:
nslist = resolvconf_nameservers() nslist = resolvconf_nameservers()
for ip in nslist: for f,ip in filter(lambda i: i[0]==family, nslist):
ipt_ttl('-A', chain, '-j', 'REDIRECT', ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip, '--dest', '%s/32' % ip,
'-p', 'udp', '-p', 'udp',
@ -119,7 +123,7 @@ def do_iptables_nat(port, dnsport, family, subnets):
def do_iptables_tproxy(port, dnsport, family, subnets): def do_iptables_tproxy(port, dnsport, family, subnets):
if family not in [socket.AF_INET]: if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception('Address family "%s" unsupported by tproxy method'%family_to_string(family)) raise Exception('Address family "%s" unsupported by tproxy method'%family_to_string(family))
table = "mangle" table = "mangle"
@ -367,7 +371,7 @@ def do_ipfw(port, dnsport, family, subnets):
divertsock.bind(('0.0.0.0', port)) # IP field is ignored divertsock.bind(('0.0.0.0', port)) # IP field is ignored
nslist = resolvconf_nameservers() nslist = resolvconf_nameservers()
for ip in nslist: for f,ip in filter(lambda i: i[0]==family, nslist):
# relabel and then catch outgoing DNS requests # relabel and then catch outgoing DNS requests
ipfw('add', sport, 'divert', sport, ipfw('add', sport, 'divert', sport,
'log', 'udp', 'log', 'udp',
@ -450,9 +454,13 @@ 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_v4, dnsport_v4, method, syslog): def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, syslog):
assert(port_v4 > 0) assert(port_v6 >= 0)
assert(port_v6 <= 65535)
assert(port_v4 >= 0)
assert(port_v4 <= 65535) assert(port_v4 <= 65535)
assert(dnsport_v6 >= 0)
assert(dnsport_v6 <= 65535)
assert(dnsport_v4 >= 0) assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535) assert(dnsport_v4 <= 65535)
@ -519,6 +527,12 @@ def main(port_v4, dnsport_v4, method, syslog):
if line: if line:
debug1('firewall manager: starting transproxy.\n') debug1('firewall manager: starting transproxy.\n')
subnets_v6 = filter(lambda i: i[0]==socket.AF_INET6, subnets)
if port_v6:
do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets_v6)
elif len(subnets_v6) > 0:
debug1("IPv6 subnets defined but IPv6 disabled\n")
subnets_v4 = filter(lambda i: i[0]==socket.AF_INET, subnets) subnets_v4 = filter(lambda i: i[0]==socket.AF_INET, subnets)
if port_v4: if port_v4:
do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets_v4) do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets_v4)
@ -543,7 +557,7 @@ def main(port_v4, dnsport_v4, method, syslog):
if line.startswith('HOST '): if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1) (name,ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip hostmap[name] = ip
rewrite_etc_hosts(port_v4) rewrite_etc_hosts(port_v6 or port_v4)
elif line: elif line:
raise Fatal('expected EOF, got %r' % line) raise Fatal('expected EOF, got %r' % line)
else: else:
@ -555,4 +569,4 @@ def main(port_v4, dnsport_v4, method, syslog):
pass pass
if port_v4: if port_v4:
do_it(port_v4, 0, socket.AF_INET, []) do_it(port_v4, 0, socket.AF_INET, [])
restore_etc_hosts(port_v4) restore_etc_hosts(port_v6 or port_v4)

View File

@ -42,7 +42,10 @@ def resolvconf_nameservers():
for line in open('/etc/resolv.conf'): for line in open('/etc/resolv.conf'):
words = line.lower().split() words = line.lower().split()
if len(words) >= 2 and words[0] == 'nameserver': if len(words) >= 2 and words[0] == 'nameserver':
l.append(words[1]) if ':' in words[1]:
l.append((socket.AF_INET6,words[1]))
else:
l.append((socket.AF_INET,words[1]))
return l return l
@ -55,7 +58,7 @@ def resolvconf_random_nameserver():
random.shuffle(l) random.shuffle(l)
return l[0] return l[0]
else: else:
return '127.0.0.1' return (socket.AF_INET,'127.0.0.1')
def islocal(ip,family): def islocal(ip,family):

59
main.py
View File

@ -22,12 +22,31 @@ def parse_subnet4(s):
return(socket.AF_INET, '%d.%d.%d.%d' % (a,b,c,d), width) return(socket.AF_INET, '%d.%d.%d.%d' % (a,b,c,d), width)
# 1:2::3/64 or just 1:2::3
def parse_subnet6(s):
m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
if not m:
raise Fatal('%r is not a valid IP subnet format' % s)
(net,width) = m.groups()
if width == None:
width = 128
else:
width = int(width)
if width > 128:
raise Fatal('*/%d is greater than the maximum of 128' % width)
return(socket.AF_INET6, net, width)
# list of: # list of:
# 1.2.3.4/5 or just 1.2.3.4 # 1.2.3.4/5 or just 1.2.3.4
# 1:2::3/64 or just 1:2::3
def parse_subnets(subnets_str): def parse_subnets(subnets_str):
subnets = [] subnets = []
for s in subnets_str: for s in subnets_str:
subnet = parse_subnet4(s) if ':' in s:
subnet = parse_subnet6(s)
else:
subnet = parse_subnet4(s)
subnets.append(subnet) subnets.append(subnet)
return subnets return subnets
@ -50,13 +69,24 @@ def parse_ipport4(s):
return ('%d.%d.%d.%d' % (a,b,c,d), port) return ('%d.%d.%d.%d' % (a,b,c,d), port)
# [1:2::3]:456 or [1:2::3] or 456
def parse_ipport6(s):
s = str(s)
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
if not m:
raise Fatal('%s is not a valid IP:port format' % s)
(ip,port) = m.groups()
(ip,port) = (ip or '::', int(port or 0))
return (ip, port)
optspec = """ optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...> sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --server sshuttle --server
sshuttle --firewall <port> <subnets...> sshuttle --firewall <port> <subnets...>
sshuttle --hostwatch 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
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 dns capture local DNS requests and forward to the remote DNS server
@ -93,10 +123,11 @@ try:
server.latency_control = opt.latency_control server.latency_control = opt.latency_control
sys.exit(server.main()) sys.exit(server.main())
elif opt.firewall: elif opt.firewall:
if len(extra) != 3: if len(extra) != 5:
o.fatal('exactly three arguments expected') o.fatal('exactly five arguments expected')
sys.exit(firewall.main(int(extra[0]), int(extra[1]), sys.exit(firewall.main(int(extra[0]), int(extra[1]),
extra[2], opt.syslog)) int(extra[2]), int(extra[3]),
extra[4], opt.syslog))
elif opt.hostwatch: elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra)) sys.exit(hostwatch.hw_main(extra))
else: else:
@ -124,8 +155,22 @@ try:
method = opt.method method = opt.method
else: else:
o.fatal("method %s not supported"%opt.method) o.fatal("method %s not supported"%opt.method)
ipport_v4 = parse_ipport4(opt.listen or '0.0.0.0:0') if not opt.listen:
sys.exit(client.main(ipport_v4, if opt.method == "tproxy":
ipport_v6 = parse_ipport6('[::1]:0')
else:
ipport_v6 = None
ipport_v4 = parse_ipport4('127.0.0.1:0')
else:
ipport_v6 = None
ipport_v4 = None
list = opt.listen.split(",")
for ip in list:
if '[' in ip and ']' in ip and opt.method == "tproxy":
ipport_v6 = parse_ipport6(ip)
else:
ipport_v4 = parse_ipport4(ip)
sys.exit(client.main(ipport_v6, ipport_v4,
opt.ssh_cmd, opt.ssh_cmd,
remotename, remotename,
opt.python, opt.python,

View File

@ -108,6 +108,7 @@ class Hostwatch:
class DnsProxy(Handler): class DnsProxy(Handler):
def __init__(self, mux, chan, request): def __init__(self, mux, chan, request):
# FIXME! IPv4 specific
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Handler.__init__(self, [sock]) Handler.__init__(self, [sock])
self.timeout = time.time()+30 self.timeout = time.time()+30
@ -117,6 +118,7 @@ class DnsProxy(Handler):
self.peer = None self.peer = None
self.request = request self.request = request
self.sock = sock self.sock = sock
# FIXME! IPv4 specific
self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.try_send() self.try_send()
@ -124,7 +126,8 @@ class DnsProxy(Handler):
if self.tries >= 3: if self.tries >= 3:
return return
self.tries += 1 self.tries += 1
self.peer = resolvconf_random_nameserver() # FIXME! Support IPv6 nameservers
self.peer = resolvconf_random_nameserver()[1]
self.sock.connect((self.peer, 53)) self.sock.connect((self.peer, 53))
debug2('DNS: sending to %r\n' % self.peer) debug2('DNS: sending to %r\n' % self.peer)
try: try: