mirror of
https://github.com/tailscale/tailscale.git
synced 2026-03-28 11:14:06 -04:00
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:
committed by
James Tucker
parent
30e12310f1
commit
0fb207c3d0
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user