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

View File

@@ -2792,3 +2792,76 @@ func TestResolveAuthKey(t *testing.T) {
})
}
}
// TestSelfDial verifies that a single tsnet.Server can Dial its own Listen
// address. This is a regression test for a bug where self-addressed TCP SYN
// packets were sent to WireGuard (which has no peer for the node's own IP)
// and silently dropped, causing Dial to hang indefinitely.
func TestSelfDial(t *testing.T) {
tstest.Shard(t)
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
ln, err := s1.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
errc := make(chan error, 1)
connc := make(chan net.Conn, 1)
go func() {
c, err := ln.Accept()
if err != nil {
errc <- err
return
}
connc <- c
}()
// Self-dial: the same server dials its own Tailscale IP.
w, err := s1.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
t.Fatalf("self-dial failed: %v", err)
}
defer w.Close()
var accepted net.Conn
select {
case accepted = <-connc:
case err := <-errc:
t.Fatalf("accept failed: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for accept")
}
defer accepted.Close()
// Verify bidirectional data exchange.
want := "hello self"
if _, err := io.WriteString(w, want); err != nil {
t.Fatal(err)
}
got := make([]byte, len(want))
if _, err := io.ReadFull(accepted, got); err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Errorf("client->server: got %q, want %q", got, want)
}
reply := "hello back"
if _, err := io.WriteString(accepted, reply); err != nil {
t.Fatal(err)
}
gotReply := make([]byte, len(reply))
if _, err := io.ReadFull(w, gotReply); err != nil {
t.Fatal(err)
}
if string(gotReply) != reply {
t.Errorf("server->client: got %q, want %q", gotReply, reply)
}
}

View File

@@ -7,6 +7,7 @@
"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) {

View File

@@ -1037,6 +1037,16 @@ func (ns *Impl) inject() {
return
}
} else {
// Self-addressed packet: deliver back into gVisor directly
// via the link endpoint's dispatcher, but only if the packet is not
// earmarked for the host. Neither the inbound path (fakeTUN Write is a
// no-op) nor the outbound path (WireGuard has no peer for our own IP)
// can handle these.
if ns.isSelfDst(pkt) {
ns.linkEP.DeliverLoopback(pkt)
continue
}
if err := ns.tundev.InjectOutboundPacketBuffer(pkt); err != nil {
ns.logf("netstack inject outbound: %v", err)
return
@@ -1116,6 +1126,20 @@ func (ns *Impl) shouldSendToHost(pkt *stack.PacketBuffer) bool {
return false
}
// isSelfDst reports whether pkt's destination IP is a local Tailscale IP
// assigned to this node. This is used by inject() to detect self-addressed
// packets that need loopback delivery.
func (ns *Impl) isSelfDst(pkt *stack.PacketBuffer) bool {
hdr := pkt.Network()
switch v := hdr.(type) {
case header.IPv4:
return ns.isLocalIP(netip.AddrFrom4(v.DestinationAddress().As4()))
case header.IPv6:
return ns.isLocalIP(netip.AddrFrom16(v.DestinationAddress().As16()))
}
return false
}
// isLocalIP reports whether ip is a Tailscale IP assigned to this
// node directly (but not a subnet-routed IP).
func (ns *Impl) isLocalIP(ip netip.Addr) bool {

View File

@@ -15,6 +15,7 @@
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/envknob"
@@ -1073,3 +1074,295 @@ func makeUDP6PacketBuffer(src, dst netip.AddrPort) *stack.PacketBuffer {
return pkt
}
// TestIsSelfDst verifies that isSelfDst correctly identifies packets whose
// destination IP is a local Tailscale IP assigned to this node.
func TestIsSelfDst(t *testing.T) {
var (
selfIP4 = netip.MustParseAddr("100.64.1.2")
selfIP6 = netip.MustParseAddr("fd7a:115c:a1e0::123")
remoteIP4 = netip.MustParseAddr("100.64.99.88")
remoteIP6 = netip.MustParseAddr("fd7a:115c:a1e0::99")
)
ns := makeNetstack(t, func(impl *Impl) {
impl.ProcessLocalIPs = true
impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
return addr == selfIP4 || addr == selfIP6
})
})
testCases := []struct {
name string
src, dst netip.AddrPort
want bool
}{
{
name: "self_to_self_v4",
src: netip.AddrPortFrom(selfIP4, 12345),
dst: netip.AddrPortFrom(selfIP4, 8081),
want: true,
},
{
name: "self_to_self_v6",
src: netip.AddrPortFrom(selfIP6, 12345),
dst: netip.AddrPortFrom(selfIP6, 8081),
want: true,
},
{
name: "remote_to_self_v4",
src: netip.AddrPortFrom(remoteIP4, 12345),
dst: netip.AddrPortFrom(selfIP4, 8081),
want: true,
},
{
name: "remote_to_self_v6",
src: netip.AddrPortFrom(remoteIP6, 12345),
dst: netip.AddrPortFrom(selfIP6, 8081),
want: true,
},
{
name: "self_to_remote_v4",
src: netip.AddrPortFrom(selfIP4, 12345),
dst: netip.AddrPortFrom(remoteIP4, 8081),
want: false,
},
{
name: "self_to_remote_v6",
src: netip.AddrPortFrom(selfIP6, 12345),
dst: netip.AddrPortFrom(remoteIP6, 8081),
want: false,
},
{
name: "remote_to_remote_v4",
src: netip.AddrPortFrom(remoteIP4, 12345),
dst: netip.MustParseAddrPort("100.64.77.66:7777"),
want: false,
},
{
name: "service_ip_to_self_v4",
src: netip.AddrPortFrom(serviceIP, 53),
dst: netip.AddrPortFrom(selfIP4, 9999),
want: true,
},
{
name: "service_ip_to_self_v6",
src: netip.AddrPortFrom(serviceIPv6, 53),
dst: netip.AddrPortFrom(selfIP6, 9999),
want: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
var pkt *stack.PacketBuffer
if tt.src.Addr().Is4() {
pkt = makeUDP4PacketBuffer(tt.src, tt.dst)
} else {
pkt = makeUDP6PacketBuffer(tt.src, tt.dst)
}
defer pkt.DecRef()
if got := ns.isSelfDst(pkt); got != tt.want {
t.Errorf("isSelfDst(%v -> %v) = %v, want %v", tt.src, tt.dst, got, tt.want)
}
})
}
}
// TestDeliverLoopback verifies that DeliverLoopback correctly re-serializes an
// outbound packet and delivers it back into gVisor's inbound path.
func TestDeliverLoopback(t *testing.T) {
ep := newLinkEndpoint(64, 1280, "", groNotSupported)
// Track delivered packets via a mock dispatcher.
type delivered struct {
proto tcpip.NetworkProtocolNumber
data []byte
}
deliveredCh := make(chan delivered, 4)
ep.Attach(&mockDispatcher{
onDeliverNetworkPacket: func(proto tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
// Capture the raw bytes from the delivered packet. At this
// point the packet is unparsed — everything is in the
// payload, no headers have been consumed yet.
buf := pkt.ToBuffer()
raw := buf.Flatten()
deliveredCh <- delivered{proto: proto, data: raw}
},
})
t.Run("ipv4", func(t *testing.T) {
selfAddr := netip.MustParseAddrPort("100.64.1.2:8081")
pkt := makeUDP4PacketBuffer(selfAddr, selfAddr)
// Capture what the outbound bytes look like before loopback.
wantLen := pkt.Size()
wantProto := pkt.NetworkProtocolNumber
if !ep.DeliverLoopback(pkt) {
t.Fatal("DeliverLoopback returned false")
}
select {
case got := <-deliveredCh:
if got.proto != wantProto {
t.Errorf("proto = %d, want %d", got.proto, wantProto)
}
if len(got.data) != wantLen {
t.Errorf("data length = %d, want %d", len(got.data), wantLen)
}
case <-time.After(time.Second):
t.Fatal("timeout waiting for loopback delivery")
}
})
t.Run("ipv6", func(t *testing.T) {
selfAddr := netip.MustParseAddrPort("[fd7a:115c:a1e0::123]:8081")
pkt := makeUDP6PacketBuffer(selfAddr, selfAddr)
wantLen := pkt.Size()
wantProto := pkt.NetworkProtocolNumber
if !ep.DeliverLoopback(pkt) {
t.Fatal("DeliverLoopback returned false")
}
select {
case got := <-deliveredCh:
if got.proto != wantProto {
t.Errorf("proto = %d, want %d", got.proto, wantProto)
}
if len(got.data) != wantLen {
t.Errorf("data length = %d, want %d", len(got.data), wantLen)
}
case <-time.After(time.Second):
t.Fatal("timeout waiting for loopback delivery")
}
})
t.Run("nil_dispatcher", func(t *testing.T) {
ep2 := newLinkEndpoint(64, 1280, "", groNotSupported)
// Don't attach a dispatcher.
selfAddr := netip.MustParseAddrPort("100.64.1.2:8081")
pkt := makeUDP4PacketBuffer(selfAddr, selfAddr)
if ep2.DeliverLoopback(pkt) {
t.Error("DeliverLoopback should return false with nil dispatcher")
}
// pkt refcount was consumed by DeliverLoopback, so we don't DecRef.
})
}
// mockDispatcher implements stack.NetworkDispatcher for testing.
type mockDispatcher struct {
onDeliverNetworkPacket func(tcpip.NetworkProtocolNumber, *stack.PacketBuffer)
}
func (d *mockDispatcher) DeliverNetworkPacket(proto tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
if d.onDeliverNetworkPacket != nil {
d.onDeliverNetworkPacket(proto, pkt)
}
}
func (d *mockDispatcher) DeliverLinkPacket(tcpip.NetworkProtocolNumber, *stack.PacketBuffer) {}
// udp4raw constructs a valid raw IPv4+UDP packet with proper checksums.
func udp4raw(t testing.TB, src, dst netip.Addr, sport, dport uint16, payload []byte) []byte {
t.Helper()
totalLen := header.IPv4MinimumSize + header.UDPMinimumSize + len(payload)
buf := make([]byte, totalLen)
ip := header.IPv4(buf)
ip.Encode(&header.IPv4Fields{
TotalLength: uint16(totalLen),
Protocol: uint8(header.UDPProtocolNumber),
TTL: 64,
SrcAddr: tcpip.AddrFrom4Slice(src.AsSlice()),
DstAddr: tcpip.AddrFrom4Slice(dst.AsSlice()),
})
ip.SetChecksum(^ip.CalculateChecksum())
// Build UDP header + payload.
u := header.UDP(buf[header.IPv4MinimumSize:])
u.Encode(&header.UDPFields{
SrcPort: sport,
DstPort: dport,
Length: uint16(header.UDPMinimumSize + len(payload)),
})
copy(buf[header.IPv4MinimumSize+header.UDPMinimumSize:], payload)
xsum := header.PseudoHeaderChecksum(
header.UDPProtocolNumber,
tcpip.AddrFrom4Slice(src.AsSlice()),
tcpip.AddrFrom4Slice(dst.AsSlice()),
uint16(header.UDPMinimumSize+len(payload)),
)
u.SetChecksum(^header.UDP(buf[header.IPv4MinimumSize:]).CalculateChecksum(xsum))
return buf
}
// TestInjectLoopback verifies that the inject goroutine delivers self-addressed
// packets back into gVisor (via DeliverLoopback) instead of sending them to
// WireGuard outbound. This is a regression test for a bug where self-dial
// packets were sent to WireGuard and silently dropped.
func TestInjectLoopback(t *testing.T) {
selfIP4 := netip.MustParseAddr("100.64.1.2")
ns := makeNetstack(t, func(impl *Impl) {
impl.ProcessLocalIPs = true
impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
return addr == selfIP4
})
})
// Register gVisor's NIC address so the stack accepts and routes
// packets for this IP.
protocolAddr := tcpip.ProtocolAddress{
Protocol: header.IPv4ProtocolNumber,
AddressWithPrefix: tcpip.AddrFrom4(selfIP4.As4()).WithPrefix(),
}
if err := ns.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
t.Fatalf("AddProtocolAddress: %v", err)
}
// Bind a UDP socket on the gVisor stack to receive the loopback packet.
pc, err := gonet.DialUDP(ns.ipstack, &tcpip.FullAddress{
NIC: nicID,
Addr: tcpip.AddrFrom4(selfIP4.As4()),
Port: 8081,
}, nil, header.IPv4ProtocolNumber)
if err != nil {
t.Fatalf("DialUDP: %v", err)
}
defer pc.Close()
// Build a valid self-addressed UDP packet from raw bytes and wrap it
// in a gVisor PacketBuffer with headers already pushed, as gVisor's
// outbound path produces.
payload := []byte("loopback test")
raw := udp4raw(t, selfIP4, selfIP4, 12345, 8081, payload)
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
ReserveHeaderBytes: header.IPv4MinimumSize + header.UDPMinimumSize,
Payload: buffer.MakeWithData(payload),
})
copy(pkt.TransportHeader().Push(header.UDPMinimumSize),
raw[header.IPv4MinimumSize:header.IPv4MinimumSize+header.UDPMinimumSize])
pkt.TransportProtocolNumber = header.UDPProtocolNumber
copy(pkt.NetworkHeader().Push(header.IPv4MinimumSize), raw[:header.IPv4MinimumSize])
pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
if err := ns.linkEP.q.Write(pkt); err != nil {
t.Fatalf("queue.Write: %v", err)
}
// The inject goroutine should detect the self-addressed packet via
// isSelfDst and deliver it back into gVisor via DeliverLoopback.
pc.SetReadDeadline(time.Now().Add(5 * time.Second))
buf := make([]byte, 256)
n, _, err := pc.ReadFrom(buf)
if err != nil {
t.Fatalf("ReadFrom: %v (self-addressed packet was not looped back)", err)
}
if got := string(buf[:n]); got != "loopback test" {
t.Errorf("got %q, want %q", got, "loopback test")
}
}