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:
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)
def setsockopt(self, level, optname, value):
if self.v6:
self.v6.setsockopt(level, optname, value)
if self.v4:
self.v4.setsockopt(level, optname, value)
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:
handlers.append(Handler([self.v4], lambda: callback(self.v4, method, mux, handlers)))
def listen(self, backlog):
if self.v6:
self.v6.listen(backlog)
if self.v4:
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:
self.v4.bind(address_v4)
else:
self.v4 = None
def print_listening(self, what):
if self.v6:
listenip = self.v6.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
if self.v4:
listenip = self.v4.getsockname()
debug1('%s listening on %r.\n' % (what, listenip))
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.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude
argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] +
['-v'] * (helpers.verbose or 0) +
['--firewall', str(port_v4),
str(dnsport_v4),
['--firewall', str(port_v6), str(port_v4),
str(dnsport_v6), str(dnsport_v4),
method])
if ssyslog._p:
argvbase += ['--syslog']
@ -376,7 +390,7 @@ def _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control,
mux.callback()
def main(listenip_v4,
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns,
method, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile):
@ -390,7 +404,7 @@ def main(listenip_v4,
return 5
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
ports = [ 0, ]
else:
@ -399,6 +413,7 @@ def main(listenip_v4,
# search for free ports and try to bind
last_e = None
redirectport_v6 = 0
redirectport_v4 = 0
bound = False
debug2('Binding redirector:')
@ -407,6 +422,16 @@ def main(listenip_v4,
tcp_listener = MultiListener()
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]:
lv4 = listenip_v4
redirectport_v4 = lv4[1]
@ -418,7 +443,7 @@ def main(listenip_v4,
redirectport_v4 = 0
try:
tcp_listener.bind(lv4)
tcp_listener.bind(lv6, lv4)
bound = True
break
except socket.error, e:
@ -443,6 +468,13 @@ def main(listenip_v4,
dns_listener = MultiListener(socket.SOCK_DGRAM)
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:
lv4 = (listenip_v4[0],port)
dnsport_v4 = port
@ -451,7 +483,7 @@ def main(listenip_v4,
dnsport_v4 = 0
try:
dns_listener.bind(lv4)
dns_listener.bind(lv6, lv4)
bound = True
break
except socket.error, e:
@ -465,10 +497,11 @@ def main(listenip_v4,
assert(last_e)
raise last_e
else:
dnsport_v6 = 0
dnsport_v4 = 0
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":
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):
if family == socket.AF_INET:
if family == socket.AF_INET6:
cmd = 'ip6tables'
elif family == socket.AF_INET:
cmd = 'iptables'
else:
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):
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)
else:
raise Exception('Unsupported family "%s"'%family_to_string(family))
@ -110,7 +114,7 @@ def do_iptables_nat(port, dnsport, family, subnets):
if dnsport:
nslist = resolvconf_nameservers()
for ip in nslist:
for f,ip in filter(lambda i: i[0]==family, nslist):
ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'-p', 'udp',
@ -119,7 +123,7 @@ def do_iptables_nat(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))
table = "mangle"
@ -367,7 +371,7 @@ def do_ipfw(port, dnsport, family, subnets):
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
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
ipfw('add', sport, 'divert', sport,
'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
# supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless.
def main(port_v4, dnsport_v4, method, syslog):
assert(port_v4 > 0)
def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, syslog):
assert(port_v6 >= 0)
assert(port_v6 <= 65535)
assert(port_v4 >= 0)
assert(port_v4 <= 65535)
assert(dnsport_v6 >= 0)
assert(dnsport_v6 <= 65535)
assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535)
@ -519,6 +527,12 @@ def main(port_v4, dnsport_v4, method, syslog):
if line:
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)
if port_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 '):
(name,ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip
rewrite_etc_hosts(port_v4)
rewrite_etc_hosts(port_v6 or port_v4)
elif line:
raise Fatal('expected EOF, got %r' % line)
else:
@ -555,4 +569,4 @@ def main(port_v4, dnsport_v4, method, syslog):
pass
if port_v4:
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'):
words = line.lower().split()
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
@ -55,7 +58,7 @@ def resolvconf_random_nameserver():
random.shuffle(l)
return l[0]
else:
return '127.0.0.1'
return (socket.AF_INET,'127.0.0.1')
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)
# 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:
# 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):
subnets = []
for s in subnets_str:
subnet = parse_subnet4(s)
if ':' in s:
subnet = parse_subnet6(s)
else:
subnet = parse_subnet4(s)
subnets.append(subnet)
return subnets
@ -50,13 +69,24 @@ def parse_ipport4(s):
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 = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --server
sshuttle --firewall <port> <subnets...>
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
N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server
@ -93,10 +123,11 @@ try:
server.latency_control = opt.latency_control
sys.exit(server.main())
elif opt.firewall:
if len(extra) != 3:
o.fatal('exactly three arguments expected')
if len(extra) != 5:
o.fatal('exactly five arguments expected')
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:
sys.exit(hostwatch.hw_main(extra))
else:
@ -124,8 +155,22 @@ try:
method = opt.method
else:
o.fatal("method %s not supported"%opt.method)
ipport_v4 = parse_ipport4(opt.listen or '0.0.0.0:0')
sys.exit(client.main(ipport_v4,
if not opt.listen:
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,
remotename,
opt.python,

View File

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