wgengine/netstack: deliver self-addressed packets via loopback

When a tsnet.Server dials its own Tailscale IP, TCP SYN packets are
silently dropped. In inject(), outbound packets with dst=self fail the
shouldSendToHost check and fall through to WireGuard, which has no peer
for the node's own address.

Fix this by detecting self-addressed packets in inject() using isLocalIP
and delivering them back into gVisor's network stack as inbound packets
via a new DeliverLoopback method on linkEndpoint. The outbound packet
must be re-serialized into a new PacketBuffer because outbound packets
have their headers parsed into separate views, but DeliverNetworkPacket
expects raw unparsed data.

Updates #18829

Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
James Tucker
2026-02-27 13:49:05 -08:00
committed by James Tucker
parent 30e12310f1
commit 0fb207c3d0
4 changed files with 428 additions and 0 deletions
+38
View File
@@ -7,6 +7,7 @@ import (
"context"
"sync"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -198,6 +199,43 @@ func (ep *linkEndpoint) injectInbound(p *packet.Parsed) {
pkt.DecRef()
}
// DeliverLoopback delivers pkt back into gVisor's network stack as if it
// arrived from the network, for self-addressed (loopback) packets. It takes
// ownership of one reference count on pkt. The caller must not use pkt after
// calling this method. It returns false if the dispatcher is not attached.
//
// Outbound packets from gVisor have their headers already parsed into separate
// views (NetworkHeader, TransportHeader, Data). DeliverNetworkPacket expects
// a raw unparsed packet, so we must re-serialize the packet into a new
// PacketBuffer with all bytes in the payload for gVisor to parse on inbound.
func (ep *linkEndpoint) DeliverLoopback(pkt *stack.PacketBuffer) bool {
ep.mu.RLock()
d := ep.dispatcher
ep.mu.RUnlock()
if d == nil {
pkt.DecRef()
return false
}
// Serialize the outbound packet back to raw bytes.
raw := stack.PayloadSince(pkt.NetworkHeader()).AsSlice()
proto := pkt.NetworkProtocolNumber
// We're done with the original outbound packet.
pkt.DecRef()
// Create a new PacketBuffer from the raw bytes for inbound delivery.
newPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(raw),
})
newPkt.NetworkProtocolNumber = proto
newPkt.RXChecksumValidated = true
d.DeliverNetworkPacket(proto, newPkt)
newPkt.DecRef()
return true
}
// Attach saves the stack network-layer dispatcher for use later when packets
// are injected.
func (ep *linkEndpoint) Attach(dispatcher stack.NetworkDispatcher) {