Skip to content

Commit

Permalink
Merge pull request #108 from vieira/pf-ipv6
Browse files Browse the repository at this point in the history
IPv6 support for OSX and BSDs
  • Loading branch information
brianmay committed Jul 28, 2016
2 parents 22b1b54 + 8520ea2 commit 1ffc3f5
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 83 deletions.
9 changes: 9 additions & 0 deletions sshuttle/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ def main(listenip_v6, listenip_v4,
listenip_v6 = None

required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
required.udp = avail.udp
required.dns = len(nslist) > 0

Expand All @@ -571,6 +572,14 @@ def main(listenip_v6, listenip_v4,
if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0)

if required.ipv4 and \
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32))

if required.ipv6 and \
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128))

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, ]
Expand Down
88 changes: 51 additions & 37 deletions sshuttle/methods/pf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from sshuttle.methods import BaseMethod


_pf_context = {'started_by_sshuttle': False, 'Xtoken': None}
_pf_context = {'started_by_sshuttle': False, 'Xtoken': []}
_pf_fd = None


Expand Down Expand Up @@ -121,18 +121,19 @@ def _add_anchor_rule(self, type, name, pr=None):
'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr)

def _inet_version(self, family):
return b'inet' if family == socket.AF_INET else b'inet6'

def _lo_addr(self, family):
return b'127.0.0.1' if family == socket.AF_INET else b'::1'

def add_rules(self, anchor, rules):
assert isinstance(rules, bytes)
debug3("rules:\n" + rules.decode("ASCII"))
pfctl('-a %s -f /dev/stdin' % anchor, rules)

def has_running_instances(self):
# This should cover most scenarios.
p = ssubprocess.Popen(['pgrep', '-f', 'python.*sshuttle'],
stdout=ssubprocess.PIPE,
stderr=ssubprocess.PIPE)
o, e = p.communicate()
return len(o.splitlines()) > 0
def has_skip_loopback(self):
return b'skip' in pfctl('-s Interfaces -i lo -v')[0]



Expand Down Expand Up @@ -178,29 +179,32 @@ def _add_anchor_rule(self, type, name):
memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4)
super(FreeBsd, self)._add_anchor_rule(type, name, pr=pr)

def add_rules(self, anchor, includes, port, dnsport, nslist):
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family)

tables = [
b'table <forward_subnets> {%s}' % b','.join(includes)
]
translating_rules = [
b'rdr pass on lo0 proto tcp '
b'to <forward_subnets> -> 127.0.0.1 port %r' % port
b'rdr pass on lo0 %s proto tcp to <forward_subnets> '
b'-> %s port %r' % (inet_version, lo_addr, port)
]
filtering_rules = [
b'pass out route-to lo0 inet proto tcp '
b'to <forward_subnets> keep state'
b'pass out route-to lo0 %s proto tcp '
b'to <forward_subnets> keep state' % inet_version
]

if len(nslist) > 0:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append(
b'rdr pass on lo0 proto udp to '
b'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
b'rdr pass on lo0 %s proto udp to <dns_servers> '
b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport))
filtering_rules.append(
b'pass out route-to lo0 inet proto udp to '
b'<dns_servers> port 53 keep state')
b'pass out route-to lo0 %s proto udp to '
b'<dns_servers> port 53 keep state' % inet_version)

rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n'
Expand Down Expand Up @@ -239,33 +243,36 @@ def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo
# that comes by default in openbsd pf.conf so the rules we will add,
# which rely on translating/filtering packets on lo, can work
if not self.has_running_instances():
if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'match on lo\n')
super(OpenBsd, self).add_anchors(anchor)

def add_rules(self, anchor, includes, port, dnsport, nslist):
def add_rules(self, anchor, includes, port, dnsport, nslist, family):
inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family)

tables = [
b'table <forward_subnets> {%s}' % b','.join(includes)
]
translating_rules = [
b'pass in on lo0 inet proto tcp '
b'to <forward_subnets> divert-to 127.0.0.1 port %r' % port
b'pass in on lo0 %s proto tcp to <forward_subnets> '
b'divert-to %s port %r' % (inet_version, lo_addr, port)
]
filtering_rules = [
b'pass out inet proto tcp '
b'to <forward_subnets> route-to lo0 keep state'
b'pass out %s proto tcp to <forward_subnets> '
b'route-to lo0 keep state' % inet_version
]

if len(nslist) > 0:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append(
b'pass in on lo0 inet proto udp to <dns_servers>'
b'port 53 rdr-to 127.0.0.1 port %r' % dnsport)
b'pass in on lo0 %s proto udp to <dns_servers> port 53 '
b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport))
filtering_rules.append(
b'pass out inet proto udp to '
b'<dns_servers> port 53 route-to lo0 keep state')
b'pass out %s proto udp to <dns_servers> port 53 '
b'route-to lo0 keep state' % inet_version)

rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n'
Expand Down Expand Up @@ -303,19 +310,18 @@ class pfioc_natlook(Structure):

def enable(self):
o = pfctl('-E')
_pf_context['Xtoken'] = \
re.search(b'Token : (.+)', o[1]).group(1)
_pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1))

def disable(self, anchor):
pfctl('-a %s -F all' % anchor)
if _pf_context['Xtoken'] is not None:
pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII"))
if _pf_context['Xtoken']:
pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII"))

def add_anchors(self, anchor):
# before adding anchors and rules we must override the skip lo
# that in some cases ends up in the chain so the rules we will add,
# which rely on translating/filtering packets on lo, can work
if not self.has_running_instances():
if self.has_skip_loopback():
pfctl('-f /dev/stdin', b'pass on lo\n')
super(Darwin, self).add_anchors(anchor)

Expand Down Expand Up @@ -362,9 +368,17 @@ def pf_get_dev():
return _pf_fd


def pf_get_anchor(family, port):
return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port)


class Method(BaseMethod):

def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
return result

def get_tcp_dstip(self, sock):
pfile = self.firewall.pfile

Expand All @@ -390,7 +404,7 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
translating_rules = []
filtering_rules = []

if family != socket.AF_INET:
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by pf method_name'
% family_to_string(family))
Expand All @@ -409,20 +423,20 @@ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
snet.encode("ASCII"),
swidth))

anchor = 'sshuttle-%d' % port
anchor = pf_get_anchor(family, port)
pf.add_anchors(anchor)
pf.add_rules(anchor, includes, port, dnsport, nslist)
pf.add_rules(anchor, includes, port, dnsport, nslist, family)
pf.enable()

def restore_firewall(self, port, family, udp):
if family != socket.AF_INET:
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by pf method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by pf method_name")

pf.disable('sshuttle-%d' % port)
pf.disable(pf_get_anchor(family, port))

def firewall_command(self, line):
if line.startswith('QUERY_PF_NAT '):
Expand Down
2 changes: 1 addition & 1 deletion sshuttle/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def __call__(self, parser, namespace, values, option_string=None):
"-x", "--exclude",
metavar="IP/MASK",
action="append",
default=[parse_subnet('127.0.0.1/8')],
default=[],
type=parse_subnet,
help="""
exclude this subnet (can be used more than once)
Expand Down
6 changes: 6 additions & 0 deletions sshuttle/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ def got_host_req(data):
def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family)
# AF_INET is the same constant on Linux and BSD but AF_INET6
# is different. As the client and server can be running on
# different platforms we can not just set the socket family
# to what comes in the wire.
if family != socket.AF_INET:
family = socket.AF_INET6
dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport)
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
Expand Down

0 comments on commit 1ffc3f5

Please sign in to comment.