mirror of
https://github.com/sshuttle/sshuttle.git
synced 2024-11-24 08:53:43 +01:00
better windivert filters
This commit is contained in:
parent
8fa15c3ca8
commit
dadfba488b
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user