Do not capture traffic destined for internal addresses

This commit is contained in:
Owen
2026-09-25 10:30:39 -04:00
parent b2efd984f7
commit a9ac73b556
3 changed files with 67 additions and 0 deletions
+37
View File
@@ -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")
}
}
+27
View File
@@ -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()
+3
View File
@@ -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})
}