summaryrefslogtreecommitdiffstats
path: root/sshuttle/methods/pf.py
diff options
context:
space:
mode:
authorBrian May <brian@linuxpenguins.xyz>2015-11-16 18:55:56 +1100
committerBrian May <brian@linuxpenguins.xyz>2015-11-16 18:55:56 +1100
commitac723694bf6e1c8604d653bd9ba7c5cc6cd1c95a (patch)
treeb92c2dacc137a96b678035f674aac736d80bec55 /sshuttle/methods/pf.py
parentbcd3205db13fefbea9106ad5f3d49dae31eb467d (diff)
Restructure code
Pull out firewall methods code into seperate files. Fix problems starting with method=='auto'; we were making decisions based on the method, before the method had been finalized by the firewall. Only very basic testing so far. What could go wrong?
Diffstat (limited to 'sshuttle/methods/pf.py')
-rw-r--r--sshuttle/methods/pf.py236
1 files changed, 236 insertions, 0 deletions
diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py
new file mode 100644
index 0000000..81a8e91
--- /dev/null
+++ b/sshuttle/methods/pf.py
@@ -0,0 +1,236 @@
+import os
+import sys
+import re
+import socket
+import struct
+import subprocess as ssubprocess
+from fcntl import ioctl
+from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
+ sizeof, addressof, memmove
+from sshuttle.helpers import debug1, debug2, Fatal
+from sshuttle.methods import BaseMethod
+
+
+def pfctl(args, stdin=None):
+ argv = ['pfctl'] + list(args.split(" "))
+ debug1('>> %s\n' % ' '.join(argv))
+
+ p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
+ stdout=ssubprocess.PIPE,
+ stderr=ssubprocess.PIPE)
+ o = p.communicate(stdin)
+ if p.returncode:
+ raise Fatal('%r returned %d' % (argv, p.returncode))
+
+ return o
+
+_pf_context = {'started_by_sshuttle': False, 'Xtoken': ''}
+
+
+# This are some classes and functions used to support pf in yosemite.
+class pf_state_xport(Union):
+ _fields_ = [("port", c_uint16),
+ ("call_id", c_uint16),
+ ("spi", c_uint32)]
+
+
+class pf_addr(Structure):
+
+ class _pfa(Union):
+ _fields_ = [("v4", c_uint32), # struct in_addr
+ ("v6", c_uint32 * 4), # struct in6_addr
+ ("addr8", c_uint8 * 16),
+ ("addr16", c_uint16 * 8),
+ ("addr32", c_uint32 * 4)]
+
+ _fields_ = [("pfa", _pfa)]
+ _anonymous_ = ("pfa",)
+
+
+class pfioc_natlook(Structure):
+ _fields_ = [("saddr", pf_addr),
+ ("daddr", pf_addr),
+ ("rsaddr", pf_addr),
+ ("rdaddr", pf_addr),
+ ("sxport", pf_state_xport),
+ ("dxport", pf_state_xport),
+ ("rsxport", pf_state_xport),
+ ("rdxport", pf_state_xport),
+ ("af", c_uint8), # sa_family_t
+ ("proto", c_uint8),
+ ("proto_variant", c_uint8),
+ ("direction", c_uint8)]
+
+pfioc_rule = c_char * 3104 # sizeof(struct pfioc_rule)
+
+pfioc_pooladdr = c_char * 1136 # sizeof(struct pfioc_pooladdr)
+
+MAXPATHLEN = 1024
+
+DIOCNATLOOK = ((0x40000000 | 0x80000000) | (
+ (sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23))
+DIOCCHANGERULE = ((0x40000000 | 0x80000000) | (
+ (sizeof(pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26))
+DIOCBEGINADDRS = ((0x40000000 | 0x80000000) | (
+ (sizeof(pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51))
+
+PF_CHANGE_ADD_TAIL = 2
+PF_CHANGE_GET_TICKET = 6
+
+PF_PASS = 0
+PF_RDR = 8
+
+PF_OUT = 2
+
+_pf_fd = None
+
+
+def pf_get_dev():
+ global _pf_fd
+ if _pf_fd is None:
+ _pf_fd = os.open('/dev/pf', os.O_RDWR)
+
+ return _pf_fd
+
+
+def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
+ [proto, family, src_port, dst_port] = [
+ int(v) for v in [proto, family, src_port, dst_port]]
+
+ length = 4 if family == socket.AF_INET else 16
+
+ pnl = pfioc_natlook()
+ pnl.proto = proto
+ pnl.direction = PF_OUT
+ pnl.af = family
+ memmove(addressof(pnl.saddr), socket.inet_pton(pnl.af, src_ip), length)
+ pnl.sxport.port = socket.htons(src_port)
+ memmove(addressof(pnl.daddr), socket.inet_pton(pnl.af, dst_ip), length)
+ pnl.dxport.port = socket.htons(dst_port)
+
+ ioctl(pf_get_dev(), DIOCNATLOOK, (
+ c_char * sizeof(pnl)).from_address(addressof(pnl)))
+
+ ip = socket.inet_ntop(
+ pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)))
+ port = socket.ntohs(pnl.rdxport.port)
+ return (ip, port)
+
+
+def pf_add_anchor_rule(type, name):
+ ACTION_OFFSET = 0
+ POOL_TICKET_OFFSET = 8
+ ANCHOR_CALL_OFFSET = 1040
+ RULE_ACTION_OFFSET = 3068
+
+ pr = pfioc_rule()
+ ppa = pfioc_pooladdr()
+
+ ioctl(pf_get_dev(), DIOCBEGINADDRS, ppa)
+
+ memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket
+ memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name,
+ min(MAXPATHLEN, len(name))) # anchor_call = name
+ memmove(addressof(pr) + RULE_ACTION_OFFSET,
+ struct.pack('I', type), 4) # rule.action = type
+
+ memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
+ 'I', PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
+ ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
+
+ memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
+ 'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
+ ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
+
+
+class Method(BaseMethod):
+
+ def get_tcp_dstip(self, sock):
+ # yuck
+ from sshuttle.client import firewall
+
+ peer = sock.getpeername()
+ proxy = sock.getsockname()
+
+ argv = (sock.family, socket.IPPROTO_TCP,
+ peer[0], peer[1], proxy[0], proxy[1])
+ firewall.pfile.write("QUERY_PF_NAT %r,%r,%s,%r,%s,%r\n" % argv)
+ firewall.pfile.flush()
+ line = firewall.pfile.readline()
+ debug2("QUERY_PF_NAT %r,%r,%s,%r,%s,%r" % argv + ' > ' + line)
+ if line.startswith('QUERY_PF_NAT_SUCCESS '):
+ (ip, port) = line[21:].split(',')
+ return (ip, int(port))
+
+ return sock.getsockname()
+
+ def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
+ global _pf_started_by_sshuttle
+ tables = []
+ translating_rules = []
+ filtering_rules = []
+
+ if subnets:
+ includes = []
+ # If a given subnet is both included and excluded, list the
+ # exclusion first; the table will ignore the second, opposite
+ # definition
+ for f, swidth, sexclude, snet in sorted(
+ subnets, key=lambda s: (s[1], s[2]), reverse=True):
+ includes.append("%s%s/%s" %
+ ("!" if sexclude else "", snet, swidth))
+
+ tables.append('table <forward_subnets> {%s}' % ','.join(includes))
+ translating_rules.append(
+ 'rdr pass on lo0 proto tcp '
+ 'to <forward_subnets> -> 127.0.0.1 port %r' % port)
+ filtering_rules.append(
+ 'pass out route-to lo0 inet proto tcp '
+ 'to <forward_subnets> keep state')
+
+ if dnsport:
+ tables.append('table <dns_servers> {%s}' % ','.join(
+ [ns[1] for ns in nslist]))
+ translating_rules.append(
+ 'rdr pass on lo0 proto udp to '
+ '<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
+ filtering_rules.append(
+ 'pass out route-to lo0 inet proto udp to '
+ '<dns_servers> port 53 keep state')
+
+ rules = '\n'.join(tables + translating_rules + filtering_rules) \
+ + '\n'
+
+ pf_status = pfctl('-s all')[0]
+ if '\nrdr-anchor "sshuttle" all\n' not in pf_status:
+ pf_add_anchor_rule(PF_RDR, "sshuttle")
+ if '\nanchor "sshuttle" all\n' not in pf_status:
+ pf_add_anchor_rule(PF_PASS, "sshuttle")
+
+ pfctl('-a sshuttle -f /dev/stdin', rules)
+ if sys.platform == "darwin":
+ o = pfctl('-E')
+ _pf_context['Xtoken'] = \
+ re.search(r'Token : (.+)', o[1]).group(1)
+ elif 'INFO:\nStatus: Disabled' in pf_status:
+ pfctl('-e')
+ _pf_context['started_by_sshuttle'] = True
+ else:
+ pfctl('-a sshuttle -F all')
+ if sys.platform == "darwin":
+ pfctl('-X %s' % _pf_context['Xtoken'])
+ elif _pf_context['started_by_sshuttle']:
+ pfctl('-d')
+
+ def firewall_command(self, line):
+ if line.startswith('QUERY_PF_NAT '):
+ try:
+ dst = pf_query_nat(*(line[13:].split(',')))
+ sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
+ except IOError as e:
+ sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
+
+ sys.stdout.flush()
+ return True
+ else:
+ return False