better windivert filters

This commit is contained in:
nom3ad 2024-01-02 21:13:30 +05:30 committed by Brian May
parent 8fa15c3ca8
commit dadfba488b

View File

@ -19,6 +19,7 @@ from sshuttle.helpers import debug3, log, debug1, debug2, get_verbose_level, Fat
try: try:
# https://reqrypt.org/windivert-doc.html#divert_iphdr # https://reqrypt.org/windivert-doc.html#divert_iphdr
# https://www.reqrypt.org/windivert-changelog.txt
import pydivert import pydivert
except ImportError: except ImportError:
raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert") raise Exception("Could not import pydivert module. windivert requires https://pypi.org/project/pydivert")
@ -275,12 +276,11 @@ class Method(BaseMethod):
network_config = {} network_config = {}
proxy_port = None proxy_port = None
proxy_addr = {IPFamily.IPv4: None, IPFamily.IPv6: None}
def __init__(self, name): def __init__(self, name):
super().__init__(name) super().__init__(name)
def _get_local_proxy_listen_addr(self, port, family): def _get_bind_addresses_for_port(self, port, family):
proto = "TCPv6" if family.version == 6 else "TCP" proto = "TCPv6" if family.version == 6 else "TCP"
for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines(): for line in subprocess.check_output(["netstat", "-a", "-n", "-p", proto]).decode().splitlines():
try: try:
@ -292,31 +292,28 @@ class Method(BaseMethod):
return ip_address(local_addr[:-len(port_suffix)].strip("[]")) return ip_address(local_addr[:-len(port_suffix)].strip("[]"))
raise Fatal("Could not find listening address for {}/{}".format(port, proto)) raise Fatal("Could not find listening address for {}/{}".format(port, proto))
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, group, tmark): def setup_firewall(self, proxy_port, dnsport, nslist, family, subnets, udp, user, group, tmark):
log(f"{port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}") log(f"{proxy_port=}, {dnsport=}, {nslist=}, {family=}, {subnets=}, {udp=}, {user=}, {tmark=}")
if nslist or user or udp: if nslist or user or udp:
raise NotImplementedError() raise NotImplementedError()
family = IPFamily(family) family = IPFamily(family)
# using loopback proxy address never worked. proxy_ip = None
# >>> self.proxy_addr[family] = family.loopback_addr # using loopback only proxy binding won't work with windivert.
# See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 ,https://github.com/basil00/Divert/issues/82) # See: https://github.com/basil00/Divert/issues/17#issuecomment-341100167 https://github.com/basil00/Divert/issues/82)
# As a workaround we use another interface ip instead. # As a workaround, finding another interface ip instead. (client should not bind proxy to loopback address)
local_addr = self._get_bind_addresses_for_port(proxy_port, family)
local_addr = self._get_local_proxy_listen_addr(port, family)
for addr in (ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)): for addr in (ip_address(info[4][0]) for info in socket.getaddrinfo(socket.gethostname(), None)):
if addr.is_loopback or addr.version != family.version: if addr.is_loopback or addr.version != family.version:
continue continue
if local_addr.is_unspecified or local_addr == addr: if local_addr.is_unspecified or local_addr == addr:
debug2("Found non loopback address to connect to proxy: " + str(addr)) debug2("Found non loopback address to connect to proxy: " + str(addr))
self.proxy_addr[family] = str(addr) proxy_ip = str(addr)
break break
else: else:
raise Fatal("Windivert method requires proxy to listen on non loopback address") raise Fatal("Windivert method requires proxy to listen on a non loopback address")
self.proxy_port = port
subnet_addresses = [] subnet_addresses = []
for (_, mask, exclude, network_addr, fport, lport) in subnets: for (_, mask, exclude, network_addr, fport, lport) in subnets:
@ -329,10 +326,11 @@ class Method(BaseMethod):
self.network_config[family] = { self.network_config[family] = {
"subnets": subnet_addresses, "subnets": subnet_addresses,
"nslist": nslist, "nslist": nslist,
"proxy_addr": (proxy_ip, proxy_port)
} }
def wait_for_firewall_ready(self): def wait_for_firewall_ready(self):
debug2(f"network_config={self.network_config} proxy_addr={self.proxy_addr}") debug2(f"network_config={self.network_config}")
self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS) self.conntrack = ConnTrack(f"sshuttle-windivert-{os.getppid()}", WINDIVERT_MAX_CONNECTIONS)
methods = (self._egress_divert, self._ingress_divert, self._connection_gc) methods = (self._egress_divert, self._ingress_divert, self._connection_gc)
ready_events = [] ready_events = []
@ -380,6 +378,7 @@ class Method(BaseMethod):
return False return False
def _egress_divert(self, ready_cb): def _egress_divert(self, ready_cb):
"""divert outgoing packets to proxy"""
proto = IPProtocol.TCP proto = IPProtocol.TCP
filter = f"outbound and {proto.filter}" filter = f"outbound and {proto.filter}"
@ -391,20 +390,21 @@ class Method(BaseMethod):
ip_net = ip_network(cidr) ip_net = ip_network(cidr)
first_ip = ip_net.network_address first_ip = ip_net.network_address
last_ip = ip_net.broadcast_address last_ip = ip_net.broadcast_address
subnet_filters.append(f"(ip.DstAddr>={first_ip} and ip.DstAddr<={last_ip})") subnet_filters.append(f"({af.filter}.DstAddr>={first_ip} and {af.filter}.DstAddr<={last_ip})")
family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) ") proxy_ip, proxy_port = c["proxy_addr"]
proxy_guard_filter = f'({af.filter}.DstAddr!={proxy_ip} or tcp.DstPort!={proxy_port})'
family_filters.append(f"{af.filter} and ({' or '.join(subnet_filters)}) and {proxy_guard_filter}")
filter = f"{filter} and ({' or '.join(family_filters)})" filter = f"{filter} and ({' or '.join(family_filters)})"
debug1(f"[OUTBOUND] {filter=}") debug1(f"[EGRESS] {filter=}")
with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w:
ready_cb() ready_cb()
proxy_port = self.proxy_port proxy_ipv4 = self.network_config[IPFamily.IPv4]["proxy_addr"] if IPFamily.IPv4 in self.network_config else None
proxy_addr_ipv4 = self.proxy_addr[IPFamily.IPv4] proxy_ipv6 = self.network_config[IPFamily.IPv6]["proxy_addr"] if IPFamily.IPv6 in self.network_config else None
proxy_addr_ipv6 = self.proxy_addr[IPFamily.IPv6]
verbose = get_verbose_level() verbose = get_verbose_level()
for pkt in w: for pkt in w:
verbose >= 3 and debug3(">>> " + repr_pkt(pkt)) verbose >= 3 and debug3("[EGRESS] " + repr_pkt(pkt))
if pkt.tcp.syn and not pkt.tcp.ack: if pkt.tcp.syn and not pkt.tcp.ack:
# SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK) # SYN sent (start of 3-way handshake connection establishment from our side, we wait for SYN+ACK)
self.conntrack.add( self.conntrack.add(
@ -423,11 +423,10 @@ class Method(BaseMethod):
self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port) self.conntrack.remove(IPProtocol.TCP, pkt.src_addr, pkt.src_port)
# DNAT # DNAT
if pkt.ipv4 and proxy_addr_ipv4: if pkt.ipv4 and proxy_ipv4:
pkt.dst_addr = proxy_addr_ipv4 pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv4
if pkt.ipv6 and proxy_addr_ipv6: if pkt.ipv6 and proxy_ipv6:
pkt.dst_addr = proxy_addr_ipv6 pkt.dst_addr, pkt.tcp.dst_port = proxy_ipv6
pkt.tcp.dst_port = proxy_port
# XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well # XXX: If we set loopback proxy address (DNAT), then we should do SNAT as well
# by setting src_addr to loopback address. # by setting src_addr to loopback address.
@ -435,30 +434,28 @@ class Method(BaseMethod):
# as they packet has to cross public to private address space. # as they packet has to cross public to private address space.
# See: https://github.com/basil00/Divert/issues/82 # See: https://github.com/basil00/Divert/issues/82
# Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets. # Managing SNAT is more trickier, as we have to restore the original source IP address for reply packets.
# >>> pkt.dst_addr = proxy_addr_ipv4 # >>> pkt.dst_addr = proxy_ipv4
w.send(pkt, recalculate_checksum=True) w.send(pkt, recalculate_checksum=True)
def _ingress_divert(self, ready_cb): def _ingress_divert(self, ready_cb):
"""handles incoming packets from proxy"""
proto = IPProtocol.TCP proto = IPProtocol.TCP
direction = "inbound" # only when proxy address is not loopback address (Useful for testing) # Windivert treats all local process traffic as outbound, regardless of origin external/loopback iface
ip_filters = [] direction = "outbound"
for addr in (ip_address(a) for a in self.proxy_addr.values() if a): proxy_addr_filters = []
if addr.is_loopback: # Windivert treats all loopback traffic as outbound for af, c in self.network_config.items():
direction = "outbound" proxy_ip, proxy_port = c["proxy_addr"]
if addr.version == 4: # "ip.SrcAddr=={hex(int(proxy_ip))}" # only Windivert >=2 supports this
ip_filters.append(f"ip.SrcAddr=={addr}") proxy_addr_filters.append(f"{af.filter}.SrcAddr=={proxy_ip} and tcp.SrcPort=={proxy_port}")
else: if not proxy_addr_filters:
# ip_checks.append(f"ip.SrcAddr=={hex(int(addr))}") # only Windivert >=2 supports this raise Fatal("At least one ipv4 or ipv6 address is expected")
ip_filters.append(f"ipv6.SrcAddr=={addr}") filter = f"{direction} and {proto.filter} and ({' or '.join(proxy_addr_filters)})"
if not ip_filters:
raise Fatal("At least ipv4 or ipv6 address is expected")
filter = f"{direction} and {proto.filter} and ({' or '.join(ip_filters)}) and tcp.SrcPort=={self.proxy_port}"
debug1(f"[INGRESS] {filter=}") debug1(f"[INGRESS] {filter=}")
with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w: with pydivert.WinDivert(filter, layer=pydivert.Layer.NETWORK, flags=pydivert.Flag.DEFAULT) as w:
ready_cb() ready_cb()
verbose = get_verbose_level() verbose = get_verbose_level()
for pkt in w: for pkt in w:
verbose >= 3 and debug3("<<< " + repr_pkt(pkt)) verbose >= 3 and debug3("[INGRESS] " + repr_pkt(pkt))
if pkt.tcp.syn and pkt.tcp.ack: if pkt.tcp.syn and pkt.tcp.ack:
# SYN+ACK received (connection established) # SYN+ACK received (connection established)
conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED) conn = self.conntrack.update(IPProtocol.TCP, pkt.dst_addr, pkt.dst_port, ConnState.TCP_ESTABLISHED)