From a9ac73b5567af3909213ab2cb4bb621815f193f4 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 10:30:39 -0400 Subject: [PATCH] Do not capture traffic destined for internal addresses --- netstack2/local_addr_bypass_test.go | 37 +++++++++++++++++++++++++++++ netstack2/proxy.go | 27 +++++++++++++++++++++ netstack2/tun.go | 3 +++ 3 files changed, 67 insertions(+) create mode 100644 netstack2/local_addr_bypass_test.go diff --git a/netstack2/local_addr_bypass_test.go b/netstack2/local_addr_bypass_test.go new file mode 100644 index 0000000..f30a14c --- /dev/null +++ b/netstack2/local_addr_bypass_test.go @@ -0,0 +1,37 @@ +package netstack2 + +import ( + "net/netip" + "testing" +) + +// With an exit-node rule (0.0.0.0/0) installed, traffic addressed to newt's own +// tunnel IP - e.g. olm's connection-status probe to the wgtester - must be left +// for the main stack rather than matched by the catch-all and proxied out. +func TestHandleIncomingPacket_LocalTunnelAddressBypassesCatchAll(t *testing.T) { + ph, err := NewProxyHandler(ProxyHandlerOptions{EnableICMP: true, MTU: 1500}) + if err != nil { + t.Fatalf("NewProxyHandler: %v", err) + } + if err := ph.Initialize(noopNotification{}); err != nil { + t.Fatalf("Initialize: %v", err) + } + defer ph.Close() + + tunnelIP := netip.MustParseAddr("100.90.128.1") + clientIP := netip.MustParseAddr("100.90.128.5") + internetIP := netip.MustParseAddr("203.0.113.50") + + ph.SetLocalAddresses([]netip.Addr{tunnelIP}) + ph.AddSubnetRule(SubnetRule{ + SourcePrefix: netip.MustParsePrefix("100.90.128.0/24"), + DestPrefix: netip.MustParsePrefix("0.0.0.0/0"), + }) + + if ph.HandleIncomingPacket(buildICMPEchoRequest(t, clientIP, tunnelIP)) { + t.Error("packet to the local tunnel IP was proxied; expected it to be left for the main stack") + } + if !ph.HandleIncomingPacket(buildICMPEchoRequest(t, clientIP, internetIP)) { + t.Error("packet to an internet address should still match the exit-node rule") + } +} diff --git a/netstack2/proxy.go b/netstack2/proxy.go index be5f23f..28b6506 100644 --- a/netstack2/proxy.go +++ b/netstack2/proxy.go @@ -136,6 +136,13 @@ type ProxyHandler struct { accessLogger *AccessLogger // Access logger for tracking sessions httpRequestLogger *HTTPRequestLogger // HTTP request logger for proxied HTTP/HTTPS requests blocked atomic.Bool // when true, all new connections are dropped + + // localAddrs are the addresses owned by the main netstack (the tunnel IP). + // Traffic addressed to them terminates on the main stack (wgtester, SSH, + // ...) and must never be proxied out to the host network, even when a + // catch-all rule such as an exit node's 0.0.0.0/0 would otherwise match it. + // Written once during setup, before any packet is processed. + localAddrs map[netip.Addr]struct{} } // ProxyHandlerOptions configures the proxy handler @@ -494,6 +501,19 @@ func (p *ProxyHandler) Initialize(notifiable channel.Notification) error { return nil } +// SetLocalAddresses registers the addresses owned by the main netstack so +// packets destined to them are left for the main stack instead of being proxied. +// Must be called before the device starts processing packets. +func (p *ProxyHandler) SetLocalAddresses(addrs []netip.Addr) { + if p == nil { + return + } + p.localAddrs = make(map[netip.Addr]struct{}, len(addrs)) + for _, addr := range addrs { + p.localAddrs[addr.Unmap()] = struct{}{} + } +} + // HandleIncomingPacket processes incoming packets and determines if they should // be injected into the proxy stack func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool { @@ -522,6 +542,13 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool { dstBytes := dstIP.As4() dstAddr := netip.AddrFrom4(dstBytes) + // Traffic for our own tunnel IP (e.g. the olm connection-status probe to the + // wgtester, or SSH) is served by the main stack. Without this, an exit node's + // 0.0.0.0/0 rule matches it and forwards it out to the host network instead. + if _, isLocal := p.localAddrs[dstAddr]; isLocal { + return false + } + // Parse transport layer to get destination port var dstPort uint16 protocol := ipv4Header.TransportProtocol() diff --git a/netstack2/tun.go b/netstack2/tun.go index 49f74d4..014cbf9 100644 --- a/netstack2/tun.go +++ b/netstack2/tun.go @@ -139,6 +139,9 @@ func CreateNetTUNWithOptions(localAddresses, dnsServers []netip.Addr, mtu int, o dev.hasV6 = true } } + // Packets to our own addresses belong to the main stack, not the proxy. + dev.proxyHandler.SetLocalAddresses(localAddresses) + if dev.hasV4 { dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) }