Commit 0fe48a46 authored by Sean Zeng's avatar Sean Zeng

initial support for pf in yosemite

parent 6121a6dc
...@@ -12,6 +12,8 @@ import ssyslog ...@@ -12,6 +12,8 @@ import ssyslog
import sys import sys
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import log, debug1, debug2, debug3, Fatal, islocal from helpers import log, debug1, debug2, debug3, Fatal, islocal
from fcntl import ioctl
from ctypes import *
recvmsg = None recvmsg = None
try: try:
...@@ -185,6 +187,79 @@ def daemon_cleanup(): ...@@ -185,6 +187,79 @@ def daemon_cleanup():
raise raise
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)]
DIOCNATLOOK = ((0x40000000L | 0x80000000L) | ((sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23))
PF_OUT = 2
_pf_fd = None
def pf_dst(sock):
global _pf_fd
try:
peer = sock.getpeername()
proxy = sock.getsockname()
pnl = pfioc_natlook()
pnl.proto = socket.IPPROTO_TCP
pnl.direction = PF_OUT
if sock.family == socket.AF_INET:
pnl.af = socket.AF_INET
memmove(addressof(pnl.saddr), socket.inet_pton(socket.AF_INET, peer[0]), 4)
pnl.sxport.port = socket.htons(peer[1])
memmove(addressof(pnl.daddr), socket.inet_pton(socket.AF_INET, proxy[0]), 4)
pnl.dxport.port = socket.htons(proxy[1])
elif sock.family == socket.AF_INET6:
pnl.af = socket.AF_INET6
memmove(addressof(pnl.saddr), socket.inet_pton(socket.AF_INET6, peer[0]), 16)
pnl.sxport.port = socket.htons(peer[1])
memmove(addressof(pnl.daddr), socket.inet_pton(socket.AF_INET6, proxy[0]), 16)
pnl.dxport.port = socket.htons(proxy[1])
if _pf_fd == None:
_pf_fd = open('/dev/pf', 'r')
ioctl(_pf_fd, DIOCNATLOOK, (c_char * sizeof(pnl)).from_address(addressof(pnl)))
if pnl.af == socket.AF_INET:
ip = socket.inet_ntop(socket.AF_INET, (c_char * 4).from_address(addressof(pnl.rdaddr)))
elif pnl.af == socket.AF_INET6:
ip = socket.inet_ntop(socket.AF_INET6, (c_char * 16).from_address(addressof(pnl.rdaddr)))
port = socket.ntohs(pnl.rdxport.port)
return (ip, port)
except IOError, e:
return sock.getsockname()
raise
def original_dst(sock): def original_dst(sock):
try: try:
SO_ORIGINAL_DST = 80 SO_ORIGINAL_DST = 80
...@@ -381,6 +456,8 @@ def onaccept_tcp(listener, method, mux, handlers): ...@@ -381,6 +456,8 @@ def onaccept_tcp(listener, method, mux, handlers):
raise raise
if method == "tproxy": if method == "tproxy":
dstip = sock.getsockname() dstip = sock.getsockname()
elif method == "pf":
dstip = pf_dst(sock)
else: else:
dstip = original_dst(sock) dstip = original_dst(sock)
debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1], debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0], srcip[1],
......
...@@ -463,6 +463,47 @@ def do_ipfw(port, dnsport, family, subnets, udp): ...@@ -463,6 +463,47 @@ def do_ipfw(port, dnsport, family, subnets, udp):
return do_wait return do_wait
def pfctl(*args):
argv = ['pfctl'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.Popen(argv, stderr=ssubprocess.PIPE).wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_pf(port, dnsport, family, subnets, udp):
tables = []
translating_rules = []
filtering_rules = []
if subnets:
include_subnets = filter(lambda s:not s[2], sorted(subnets, reverse=True))
if include_subnets:
tables.append('table <include_subnets> {%s}' % ','.join(["%s/%s" % (n[3], n[1]) for n in include_subnets]))
translating_rules.append('rdr pass on lo0 proto tcp to <include_subnets> -> 127.0.0.1 port %r' % port)
filtering_rules.append('pass out route-to lo0 inet proto tcp to <include_subnets> keep state')
exclude_subnets = filter(lambda s:s[2], sorted(subnets, reverse=True))
if exclude_subnets:
tables.append('table <exclude_subnets> {%s}' % ','.join(["%s/%s" % (n[3], n[1]) for n in exclude_subnets]))
filtering_rules.append('pass out route-to lo0 inet proto tcp to <exclude_subnets> keep state')
if dnsport:
nslist = resolvconf_nameservers()
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')
pf_config_file = '/etc/pf-sshuttle.conf'
with open(pf_config_file, 'w+') as f:
f.write('\n'.join(tables + translating_rules + filtering_rules) + '\n')
pfctl('-ef', pf_config_file)
os.remove(pf_config_file)
else:
pfctl('-dF', 'all')
def program_exists(name): def program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
for p in paths: for p in paths:
...@@ -541,6 +582,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): ...@@ -541,6 +582,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
method = "ipfw" method = "ipfw"
elif program_exists('iptables'): elif program_exists('iptables'):
method = "nat" method = "nat"
elif program_exists('pfctl'):
method = "pf"
else: else:
raise Fatal("can't find either ipfw or iptables; check your PATH") raise Fatal("can't find either ipfw or iptables; check your PATH")
...@@ -550,6 +593,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): ...@@ -550,6 +593,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog):
do_it = do_iptables_tproxy do_it = do_iptables_tproxy
elif method == "ipfw": elif method == "ipfw":
do_it = do_ipfw do_it = do_ipfw
elif method == "pf":
do_it = do_pf
else: else:
raise Exception('Unknown method "%s"' % method) raise Exception('Unknown method "%s"' % method)
......
...@@ -116,7 +116,7 @@ l,listen= transproxy to this ip address and port number ...@@ -116,7 +116,7 @@ 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
method= auto, nat, tproxy, or ipfw method= auto, nat, tproxy, pf or ipfw
python= path to python interpreter on the remote server python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once) x,exclude= exclude this subnet (can be used more than once)
...@@ -183,7 +183,7 @@ try: ...@@ -183,7 +183,7 @@ try:
includes = parse_subnet_file(opt.subnets) includes = parse_subnet_file(opt.subnets)
if not opt.method: if not opt.method:
method = "auto" method = "auto"
elif opt.method in ["auto", "nat", "tproxy", "ipfw"]: elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
method = opt.method method = opt.method
else: else:
o.fatal("method %s not supported" % opt.method) o.fatal("method %s not supported" % opt.method)
......
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