net/tstun: invoke conn25 app connector hook on injected reads

The primary purpose is that return packets from the target app get
properly SNATed on connectors with --tun=userspace-networking, matching
the NAT behavior in the kernel tun path.

This is also necessary but not sufficient for clients of connectors in
userspace networking mode. The hook will DNAT MagicIPs, but won't
actually be sent MagicIPs until conn25 app connector DNS works with
userspace networking.

Fixes tailscale/corp#43201

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2026-06-23 16:51:51 -04:00
committed by mzbenami
parent 77d2c87b17
commit 1b2062f3c1
2 changed files with 54 additions and 0 deletions

View File

@@ -1154,6 +1154,26 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
}
invertGSOChecksum(pkt, gso)
// Check if this is a packet for conn25-style app connectors,
// and perform the necessary NAT. The main case that requires
// NAT from netstack toward WireGuard is an SNAT on return traffic
// from the target application on the internet, translating
// the original server's source IP to the TransitIP.
// The hook can also perform DNAT for client-originated traffic,
// translating the destination MagicIP to a TransitIP, and rejects
// MagicIPs that have not been approved for the client.
//
// Normal non-connector traffic is forwarded unmodified.
//
// Cross-tailnet conn25 app connector connections are not supported,
// so at most one of this hook and the following pc.snat should modify the packet.
if t.PreFilterPacketOutboundToWireGuardAppConnectorIntercept != nil {
if r := t.PreFilterPacketOutboundToWireGuardAppConnectorIntercept(p, t); r.IsDrop() {
metricPacketOut.Add(1)
metricPacketOutDrop.Add(1)
return 0, nil
}
}
pc.snat(p)
invertGSOChecksum(pkt, gso)

View File

@@ -28,6 +28,7 @@
"tailscale.com/disco"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/net/packet/checksum"
"tailscale.com/tstest"
"tailscale.com/tstime/mono"
"tailscale.com/types/ipproto"
@@ -1120,3 +1121,36 @@ func TestInterceptOrdering(t *testing.T) {
t.Errorf("got number of intercepts run in Read(): %d; want: %d", seq, numOutboundIntercepts)
}
}
func TestInjectedReadCallsAppConnectorHook(t *testing.T) {
var called bool
hook := func(p *packet.Parsed, _ *Wrapper) filter.Response {
called = true
checksum.UpdateSrcAddr(p, netip.MustParseAddr("169.254.0.1"))
return filter.Accept
}
bus := eventbustest.NewBus(t)
_, tun := newFakeTUN(t.Logf, bus, false)
tun.PreFilterPacketOutboundToWireGuardAppConnectorIntercept = hook
tun.Start()
defer tun.Close()
if err := tun.InjectOutbound(udp4("145.53.32.10", "100.25.63.57", 80, 12345)); err != nil {
t.Fatalf("InjectOutbound error: %v", err)
}
var buf [MaxPacketSize]byte
sizes := make([]int, 1)
tun.Read([][]byte{buf[:]}, sizes, 0)
if !called {
t.Error("app connector hook was not called in InjectOutbound")
}
wantPkt := udp4("169.254.0.1", "100.25.63.57", 80, 12345)
gotPkt := buf[:sizes[0]]
if !bytes.Equal(wantPkt, gotPkt) {
t.Errorf("packet mismatch\nwant:\t% x\ngot:\t% x", wantPkt, gotPkt)
}
}