mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-13 14:29:49 -04:00
The packet filter only sees a via address's outer ULA, so netstack unconditionally dialed whatever IPv4 was embedded in it. This change refuses TCP, UDP, and ping relays to host-scoped destinations after UnmapVia. Credit to the Anthropic infrastructure security team for finding and reporting. Fixes https://github.com/tailscale/corp/issues/46646 Change-Id: I93d8e27a2eddbce7eb3f8c5fa4677f8de3a8ed9e Signed-off-by: Mike Jensen <mikej@tailscale.com>
2194 lines
69 KiB
Go
2194 lines
69 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package netstack
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"maps"
|
||
"net"
|
||
"net/netip"
|
||
"runtime"
|
||
"testing"
|
||
"time"
|
||
|
||
"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/network/ipv4"
|
||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||
"gvisor.dev/gvisor/pkg/waiter"
|
||
"tailscale.com/envknob"
|
||
"tailscale.com/ipn"
|
||
"tailscale.com/ipn/ipnlocal"
|
||
"tailscale.com/ipn/store/mem"
|
||
"tailscale.com/metrics"
|
||
"tailscale.com/net/netx"
|
||
"tailscale.com/net/packet"
|
||
"tailscale.com/net/tsaddr"
|
||
"tailscale.com/net/tsdial"
|
||
"tailscale.com/net/tstun"
|
||
"tailscale.com/tsd"
|
||
"tailscale.com/tstest"
|
||
"tailscale.com/types/ipproto"
|
||
"tailscale.com/types/logid"
|
||
"tailscale.com/types/netmap"
|
||
"tailscale.com/util/clientmetric"
|
||
"tailscale.com/wgengine"
|
||
"tailscale.com/wgengine/filter"
|
||
)
|
||
|
||
// TestInjectInboundLeak tests that injectInbound doesn't leak memory.
|
||
// See https://github.com/tailscale/tailscale/issues/3762
|
||
func TestInjectInboundLeak(t *testing.T) {
|
||
tunDev := tstun.NewFake()
|
||
dialer := new(tsdial.Dialer)
|
||
logf := func(format string, args ...any) {
|
||
if !t.Failed() {
|
||
t.Logf(format, args...)
|
||
}
|
||
}
|
||
sys := tsd.NewSystem()
|
||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||
Tun: tunDev,
|
||
Dialer: dialer,
|
||
SetSubsystem: sys.Set,
|
||
HealthTracker: sys.HealthTracker.Get(),
|
||
Metrics: sys.UserMetricsRegistry(),
|
||
EventBus: sys.Bus.Get(),
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer eng.Close()
|
||
sys.Set(eng)
|
||
sys.Set(new(mem.Store))
|
||
|
||
tunWrap := sys.Tun.Get()
|
||
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(lb.Shutdown)
|
||
|
||
ns, err := Create(logf, tunWrap, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer ns.Close()
|
||
ns.ProcessLocalIPs = true
|
||
if err := ns.Start(lb); err != nil {
|
||
t.Fatalf("Start: %v", err)
|
||
}
|
||
ns.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return true })
|
||
|
||
pkt := &packet.Parsed{}
|
||
const N = 10_000
|
||
ms0 := getMemStats()
|
||
for range N {
|
||
outcome, _ := ns.injectInbound(pkt, tunWrap, nil)
|
||
if outcome != filter.DropSilently {
|
||
t.Fatalf("got outcome %v; want DropSilently", outcome)
|
||
}
|
||
}
|
||
ms1 := getMemStats()
|
||
if grew := int64(ms1.HeapObjects) - int64(ms0.HeapObjects); grew >= N {
|
||
t.Fatalf("grew by %v (which is too much and >= the %v packets we sent)", grew, N)
|
||
}
|
||
}
|
||
|
||
func getMemStats() (ms runtime.MemStats) {
|
||
runtime.GC()
|
||
runtime.ReadMemStats(&ms)
|
||
return
|
||
}
|
||
|
||
func makeNetstack(tb testing.TB, config func(*Impl)) *Impl {
|
||
tunDev := tstun.NewFake()
|
||
sys := tsd.NewSystem()
|
||
sys.Set(new(mem.Store))
|
||
dialer := new(tsdial.Dialer)
|
||
logf := tstest.WhileTestRunningLogger(tb)
|
||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||
Tun: tunDev,
|
||
Dialer: dialer,
|
||
SetSubsystem: sys.Set,
|
||
HealthTracker: sys.HealthTracker.Get(),
|
||
Metrics: sys.UserMetricsRegistry(),
|
||
EventBus: sys.Bus.Get(),
|
||
})
|
||
if err != nil {
|
||
tb.Fatal(err)
|
||
}
|
||
tb.Cleanup(func() { eng.Close() })
|
||
sys.Set(eng)
|
||
|
||
ns, err := Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
|
||
if err != nil {
|
||
tb.Fatal(err)
|
||
}
|
||
tb.Cleanup(func() { ns.Close() })
|
||
sys.Set(ns)
|
||
|
||
lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
||
if err != nil {
|
||
tb.Fatalf("NewLocalBackend: %v", err)
|
||
}
|
||
tb.Cleanup(lb.Shutdown)
|
||
|
||
ns.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return true })
|
||
if config != nil {
|
||
config(ns)
|
||
}
|
||
if err := ns.Start(lb); err != nil {
|
||
tb.Fatalf("Start: %v", err)
|
||
}
|
||
return ns
|
||
}
|
||
|
||
func TestShouldHandlePing(t *testing.T) {
|
||
srcIP := netip.AddrFrom4([4]byte{1, 2, 3, 4})
|
||
|
||
t.Run("ICMP4", func(t *testing.T) {
|
||
dst := netip.MustParseAddr("5.6.7.8")
|
||
icmph := packet.ICMP4Header{
|
||
IP4Header: packet.IP4Header{
|
||
IPProto: ipproto.ICMPv4,
|
||
Src: srcIP,
|
||
Dst: dst,
|
||
},
|
||
Type: packet.ICMP4EchoRequest,
|
||
Code: packet.ICMP4NoCode,
|
||
}
|
||
_, payload := packet.ICMPEchoPayload(nil)
|
||
icmpPing := packet.Generate(icmph, payload)
|
||
pkt := &packet.Parsed{}
|
||
pkt.Decode(icmpPing)
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
pingDst, ok := impl.shouldHandlePing(pkt)
|
||
if !ok {
|
||
t.Errorf("expected shouldHandlePing==true")
|
||
}
|
||
if pingDst != dst {
|
||
t.Errorf("got dst %s; want %s", pingDst, dst)
|
||
}
|
||
})
|
||
|
||
t.Run("ICMP6-no-via", func(t *testing.T) {
|
||
dst := netip.MustParseAddr("2a09:8280:1::4169")
|
||
icmph := packet.ICMP6Header{
|
||
IP6Header: packet.IP6Header{
|
||
IPProto: ipproto.ICMPv6,
|
||
Src: srcIP,
|
||
Dst: dst,
|
||
},
|
||
Type: packet.ICMP6EchoRequest,
|
||
Code: packet.ICMP6NoCode,
|
||
}
|
||
_, payload := packet.ICMPEchoPayload(nil)
|
||
icmpPing := packet.Generate(icmph, payload)
|
||
pkt := &packet.Parsed{}
|
||
pkt.Decode(icmpPing)
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
pingDst, ok := impl.shouldHandlePing(pkt)
|
||
|
||
// Expect that we handle this since it's going out onto the
|
||
// network.
|
||
if !ok {
|
||
t.Errorf("expected shouldHandlePing==true")
|
||
}
|
||
if pingDst != dst {
|
||
t.Errorf("got dst %s; want %s", pingDst, dst)
|
||
}
|
||
})
|
||
|
||
t.Run("ICMP6-tailscale-addr", func(t *testing.T) {
|
||
dst := netip.MustParseAddr("fd7a:115c:a1e0:ab12::1")
|
||
icmph := packet.ICMP6Header{
|
||
IP6Header: packet.IP6Header{
|
||
IPProto: ipproto.ICMPv6,
|
||
Src: srcIP,
|
||
Dst: dst,
|
||
},
|
||
Type: packet.ICMP6EchoRequest,
|
||
Code: packet.ICMP6NoCode,
|
||
}
|
||
_, payload := packet.ICMPEchoPayload(nil)
|
||
icmpPing := packet.Generate(icmph, payload)
|
||
pkt := &packet.Parsed{}
|
||
pkt.Decode(icmpPing)
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
_, ok := impl.shouldHandlePing(pkt)
|
||
|
||
// We don't handle this because it's a Tailscale IP and not 4via6
|
||
if ok {
|
||
t.Errorf("expected shouldHandlePing==false")
|
||
}
|
||
})
|
||
|
||
// Handle pings for 4via6 addresses regardless of ProcessSubnets
|
||
for _, subnets := range []bool{true, false} {
|
||
t.Run("ICMP6-4via6-ProcessSubnets-"+fmt.Sprint(subnets), func(t *testing.T) {
|
||
// The 4via6 route 10.1.1.0/24 siteid 7, and then the IP
|
||
// 10.1.1.9 within that route.
|
||
dst := netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:7:a01:109")
|
||
expectedPingDst := netip.MustParseAddr("10.1.1.9")
|
||
icmph := packet.ICMP6Header{
|
||
IP6Header: packet.IP6Header{
|
||
IPProto: ipproto.ICMPv6,
|
||
Src: srcIP,
|
||
Dst: dst,
|
||
},
|
||
Type: packet.ICMP6EchoRequest,
|
||
Code: packet.ICMP6NoCode,
|
||
}
|
||
_, payload := packet.ICMPEchoPayload(nil)
|
||
icmpPing := packet.Generate(icmph, payload)
|
||
pkt := &packet.Parsed{}
|
||
pkt.Decode(icmpPing)
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = subnets
|
||
})
|
||
|
||
pingDst, ok := impl.shouldHandlePing(pkt)
|
||
|
||
// Handled due to being 4via6
|
||
if !ok {
|
||
t.Errorf("expected shouldHandlePing==true")
|
||
} else if pingDst != expectedPingDst {
|
||
t.Errorf("got dst %s; want %s", pingDst, expectedPingDst)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// looksLikeATailscaleSelfAddress reports whether addr looks like
|
||
// a Tailscale self address, for tests.
|
||
func looksLikeATailscaleSelfAddress(addr netip.Addr) bool {
|
||
return addr.Is4() && tsaddr.IsTailscaleIP(addr) ||
|
||
addr.Is6() && tsaddr.Tailscale4To6Range().Contains(addr)
|
||
}
|
||
|
||
func TestShouldProcessInbound(t *testing.T) {
|
||
testCases := []struct {
|
||
name string
|
||
pkt *packet.Parsed
|
||
afterStart func(*Impl) // optional; after Impl.Start is called
|
||
beforeStart func(*Impl) // optional; before Impl.Start is called
|
||
want bool
|
||
runOnGOOS string
|
||
}{
|
||
{
|
||
name: "ipv6-via",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 6,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
|
||
// $ tailscale debug via 7 10.1.1.9/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:109/120
|
||
Dst: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:109]:5678"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
// $ tailscale debug via 7 10.1.1.0/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:100/120
|
||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
|
||
}
|
||
i.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
// This should be handled even if we're
|
||
// otherwise not processing local IPs or
|
||
// subnets.
|
||
i.ProcessLocalIPs = false
|
||
i.ProcessSubnets = false
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "ipv6-via-not-advertised",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 6,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
|
||
// $ tailscale debug via 7 10.1.1.9/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:109/120
|
||
Dst: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:109]:5678"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
// tailscale debug via 7 10.1.2.0/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:200/120
|
||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:200/120"),
|
||
}
|
||
i.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "tailscale-ssh-enabled",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("100.101.102.104:22"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
prefs := ipn.NewPrefs()
|
||
prefs.RunSSH = true
|
||
i.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr.String() == "100.101.102.104" // Dst, above
|
||
})
|
||
},
|
||
want: true,
|
||
runOnGOOS: "linux",
|
||
},
|
||
{
|
||
name: "tailscale-ssh-disabled",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("100.101.102.104:22"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
prefs := ipn.NewPrefs()
|
||
prefs.RunSSH = false // default, but to be explicit
|
||
i.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr.String() == "100.101.102.104" // Dst, above
|
||
})
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "process-local-ips",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("100.101.102.104:4567"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
i.ProcessLocalIPs = true
|
||
i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr.String() == "100.101.102.104" // Dst, above
|
||
})
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "process-subnets",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("10.1.2.3:4567"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
i.ProcessSubnets = true
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
// For testing purposes, assume all Tailscale
|
||
// IPs are local; the Dst above is something
|
||
// not in that range.
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "peerapi-port-subnet-router", // see #6235
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("10.0.0.23:5555"),
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
// As if we were running on Linux where netstack isn't used.
|
||
i.ProcessSubnets = false
|
||
i.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return false })
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
netip.MustParsePrefix("10.0.0.1/24"),
|
||
}
|
||
i.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
|
||
// Set the PeerAPI port to the Dst port above.
|
||
i.peerapiPort4Atomic.Store(5555)
|
||
i.peerapiPort6Atomic.Store(5555)
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "udp-on-service-vip-with-listener-ipv4",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.UDP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("100.100.100.100:8080"),
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
i.ProcessLocalIPs = false
|
||
i.ProcessSubnets = false
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
IPServiceMap := netmap.IPServiceMappings{
|
||
serviceIP: "svc:test-service",
|
||
}
|
||
i.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
|
||
i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == serviceIP
|
||
})
|
||
|
||
// Register the service VIP address on the NIC so gVisor can route to it
|
||
protocolAddr := tcpip.ProtocolAddress{
|
||
Protocol: header.IPv4ProtocolNumber,
|
||
AddressWithPrefix: tcpip.AddrFrom4(serviceIP.As4()).WithPrefix(),
|
||
}
|
||
|
||
if err := i.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
|
||
t.Fatalf("AddProtocolAddress: %v", err)
|
||
}
|
||
|
||
// Create a UDP listener on the service VIP
|
||
pc, err := gonet.DialUDP(i.ipstack, &tcpip.FullAddress{
|
||
NIC: nicID,
|
||
Addr: tcpip.AddrFrom4(serviceIP.As4()),
|
||
Port: 8080,
|
||
}, nil, header.IPv4ProtocolNumber)
|
||
if err != nil {
|
||
t.Fatalf("DialUDP: %v", err)
|
||
}
|
||
t.Cleanup(func() { pc.Close() })
|
||
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "udp-on-service-vip-no-listener-ipv4",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.UDP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("100.100.100.100:9999"),
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
i.ProcessLocalIPs = false
|
||
i.ProcessSubnets = false
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
IPServiceMap := netmap.IPServiceMappings{
|
||
serviceIP: "svc:test-service",
|
||
}
|
||
i.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
|
||
i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == serviceIP
|
||
})
|
||
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "udp-on-service-vip-with-listener-ipv6",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 6,
|
||
IPProto: ipproto.UDP,
|
||
Src: netip.MustParseAddrPort("[fd7a:115c:a1e0::1]:1234"),
|
||
Dst: netip.MustParseAddrPort("[fd7a:115c:a1e0::53]:8080"),
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
i.ProcessLocalIPs = false
|
||
i.ProcessSubnets = false
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
IPServiceMap := netmap.IPServiceMappings{
|
||
serviceIPv6: "svc:test-service",
|
||
}
|
||
i.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
|
||
i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == serviceIPv6
|
||
})
|
||
|
||
protocolAddr := tcpip.ProtocolAddress{
|
||
Protocol: header.IPv6ProtocolNumber,
|
||
AddressWithPrefix: tcpip.AddrFrom16(serviceIPv6.As16()).WithPrefix(),
|
||
}
|
||
if err := i.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
|
||
t.Fatalf("AddProtocolAddress: %v", err)
|
||
}
|
||
|
||
pc, err := gonet.DialUDP(i.ipstack, &tcpip.FullAddress{
|
||
NIC: nicID,
|
||
Addr: tcpip.AddrFrom16(serviceIPv6.As16()),
|
||
Port: 8080,
|
||
}, nil, header.IPv6ProtocolNumber)
|
||
if err != nil {
|
||
t.Fatalf("DialUDP: %v", err)
|
||
}
|
||
t.Cleanup(func() { pc.Close() })
|
||
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "udp-on-service-vip-no-listener-ipv6",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 6,
|
||
IPProto: ipproto.UDP,
|
||
Src: netip.MustParseAddrPort("[fd7a:115c:a1e0::1]:1234"),
|
||
Dst: netip.AddrPortFrom(serviceIPv6, 9999),
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
i.ProcessLocalIPs = false
|
||
i.ProcessSubnets = false
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
IPServiceMap := netmap.IPServiceMappings{
|
||
serviceIPv6: "svc:test-service",
|
||
}
|
||
i.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
|
||
i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == serviceIPv6
|
||
})
|
||
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "tcp-on-service-vip-with-udp-listener",
|
||
pkt: &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("100.101.102.103:1234"),
|
||
Dst: netip.MustParseAddrPort("100.100.100.100:8080"), // serviceIP
|
||
TCPFlags: packet.TCPSyn,
|
||
},
|
||
beforeStart: func(i *Impl) {
|
||
i.ProcessLocalIPs = false
|
||
i.ProcessSubnets = false
|
||
},
|
||
afterStart: func(i *Impl) {
|
||
IPServiceMap := netmap.IPServiceMappings{
|
||
serviceIP: "svc:test-service",
|
||
}
|
||
i.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
|
||
i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == serviceIP
|
||
})
|
||
|
||
protocolAddr := tcpip.ProtocolAddress{
|
||
Protocol: header.IPv4ProtocolNumber,
|
||
AddressWithPrefix: tcpip.AddrFrom4(serviceIP.As4()).WithPrefix(),
|
||
}
|
||
if err := i.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
|
||
t.Fatalf("AddProtocolAddress: %v", err)
|
||
}
|
||
|
||
pc, err := gonet.DialUDP(i.ipstack, &tcpip.FullAddress{
|
||
NIC: nicID,
|
||
Addr: tcpip.AddrFrom4(serviceIP.As4()),
|
||
Port: 8080,
|
||
}, nil, header.IPv4ProtocolNumber)
|
||
if err != nil {
|
||
t.Fatalf("DialUDP: %v", err)
|
||
}
|
||
t.Cleanup(func() { pc.Close() })
|
||
|
||
i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
want: false,
|
||
},
|
||
|
||
// TODO(andrew): test PeerAPI
|
||
// TODO(andrew): test TCP packets without the SYN flag set
|
||
}
|
||
|
||
for _, tc := range testCases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if tc.runOnGOOS != "" && runtime.GOOS != tc.runOnGOOS {
|
||
t.Skipf("skipping on GOOS=%v", runtime.GOOS)
|
||
}
|
||
impl := makeNetstack(t, tc.beforeStart)
|
||
if tc.afterStart != nil {
|
||
tc.afterStart(impl)
|
||
}
|
||
|
||
got := impl.shouldProcessInbound(tc.pkt, nil)
|
||
if got != tc.want {
|
||
t.Errorf("got shouldProcessInbound()=%v; want %v", got, tc.want)
|
||
} else {
|
||
t.Logf("OK: shouldProcessInbound() = %v", got)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func tcp4syn(tb testing.TB, src, dst netip.Addr, sport, dport uint16) []byte {
|
||
ip := header.IPv4(make([]byte, header.IPv4MinimumSize+header.TCPMinimumSize))
|
||
ip.Encode(&header.IPv4Fields{
|
||
Protocol: uint8(header.TCPProtocolNumber),
|
||
TotalLength: header.IPv4MinimumSize + header.TCPMinimumSize,
|
||
TTL: 64,
|
||
SrcAddr: tcpip.AddrFrom4Slice(src.AsSlice()),
|
||
DstAddr: tcpip.AddrFrom4Slice(dst.AsSlice()),
|
||
})
|
||
ip.SetChecksum(^ip.CalculateChecksum())
|
||
if !ip.IsChecksumValid() {
|
||
tb.Fatal("test broken; packet has incorrect IP checksum")
|
||
}
|
||
|
||
tcp := header.TCP(ip[header.IPv4MinimumSize:])
|
||
tcp.Encode(&header.TCPFields{
|
||
SrcPort: sport,
|
||
DstPort: dport,
|
||
SeqNum: 0,
|
||
DataOffset: header.TCPMinimumSize,
|
||
Flags: header.TCPFlagSyn,
|
||
WindowSize: 65535,
|
||
Checksum: 0,
|
||
})
|
||
xsum := header.PseudoHeaderChecksum(
|
||
header.TCPProtocolNumber,
|
||
tcpip.AddrFrom4Slice(src.AsSlice()),
|
||
tcpip.AddrFrom4Slice(dst.AsSlice()),
|
||
uint16(header.TCPMinimumSize),
|
||
)
|
||
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
|
||
if !tcp.IsChecksumValid(tcpip.AddrFrom4Slice(src.AsSlice()), tcpip.AddrFrom4Slice(dst.AsSlice()), 0, 0) {
|
||
tb.Fatal("test broken; packet has incorrect TCP checksum")
|
||
}
|
||
|
||
return ip
|
||
}
|
||
|
||
// tcp6syn is tcp4syn for IPv6 packets.
|
||
func tcp6syn(tb testing.TB, src, dst netip.Addr, sport, dport uint16) []byte {
|
||
srcAddr := tcpip.AddrFrom16(src.As16())
|
||
dstAddr := tcpip.AddrFrom16(dst.As16())
|
||
|
||
ip := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
|
||
ip.Encode(&header.IPv6Fields{
|
||
SrcAddr: srcAddr,
|
||
DstAddr: dstAddr,
|
||
PayloadLength: header.TCPMinimumSize,
|
||
TransportProtocol: header.TCPProtocolNumber,
|
||
HopLimit: 64,
|
||
})
|
||
|
||
tcp := header.TCP(ip[header.IPv6MinimumSize:])
|
||
tcp.Encode(&header.TCPFields{
|
||
SrcPort: sport,
|
||
DstPort: dport,
|
||
SeqNum: 0,
|
||
DataOffset: header.TCPMinimumSize,
|
||
Flags: header.TCPFlagSyn,
|
||
WindowSize: 65535,
|
||
Checksum: 0,
|
||
})
|
||
xsum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, srcAddr, dstAddr,
|
||
uint16(header.TCPMinimumSize))
|
||
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
|
||
if !tcp.IsChecksumValid(srcAddr, dstAddr, 0, 0) {
|
||
tb.Fatal("test broken; packet has incorrect TCP checksum")
|
||
}
|
||
|
||
return ip
|
||
}
|
||
|
||
// mustVia99 returns the 4via6 address embedding v4 within site 99's /96.
|
||
func mustVia99(tb testing.TB, v4 string) netip.Addr {
|
||
tb.Helper()
|
||
p, err := tsaddr.MapVia(99, netip.PrefixFrom(netip.MustParseAddr(v4), 32))
|
||
if err != nil {
|
||
tb.Fatal(err)
|
||
}
|
||
return p.Addr()
|
||
}
|
||
|
||
// makeHangDialer returns a dialer that notifies the returned channel when a
|
||
// connection is dialed and then hangs until the test finishes.
|
||
func makeHangDialer(tb testing.TB) (netx.DialFunc, chan struct{}) {
|
||
done := make(chan struct{})
|
||
tb.Cleanup(func() {
|
||
close(done)
|
||
})
|
||
|
||
gotConn := make(chan struct{}, 1)
|
||
fn := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||
// Signal that we have a new connection
|
||
tb.Logf("hangDialer: called with network=%q address=%q", network, address)
|
||
select {
|
||
case gotConn <- struct{}{}:
|
||
default:
|
||
}
|
||
|
||
// Hang until the test is done.
|
||
select {
|
||
case <-ctx.Done():
|
||
tb.Logf("context done")
|
||
case <-done:
|
||
tb.Logf("function completed")
|
||
}
|
||
return nil, fmt.Errorf("canceled")
|
||
}
|
||
return fn, gotConn
|
||
}
|
||
|
||
// TestTCPForwardLimits verifies that the limits on the TCP forwarder work in a
|
||
// success case (i.e. when we don't hit the limit).
|
||
func TestTCPForwardLimits(t *testing.T) {
|
||
envknob.SetenvForTest(t, "TS_DEBUG_NETSTACK", "true")
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
|
||
dialFn, gotConn := makeHangDialer(t)
|
||
impl.forwardDialFunc = dialFn
|
||
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
// This is the TEST-NET-1 IP block for use in documentation,
|
||
// and should never actually be routable.
|
||
netip.MustParsePrefix("192.0.2.0/24"),
|
||
}
|
||
impl.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
|
||
// Inject an "outbound" packet that's going to an IP address that times
|
||
// out. We need to re-parse from a byte slice so that the internal
|
||
// buffer in the packet.Parsed type is filled out.
|
||
client := netip.MustParseAddr("100.101.102.103")
|
||
destAddr := netip.MustParseAddr("192.0.2.1")
|
||
pkt := tcp4syn(t, client, destAddr, 1234, 4567)
|
||
var parsed packet.Parsed
|
||
parsed.Decode(pkt)
|
||
|
||
// When injecting this packet, we want the outcome to be "drop
|
||
// silently", which indicates that netstack is processing the
|
||
// packet and not delivering it to the host system.
|
||
if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
|
||
t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
// Wait until we have an in-flight outgoing connection.
|
||
select {
|
||
case <-ctx.Done():
|
||
t.Fatalf("timed out waiting for connection")
|
||
case <-gotConn:
|
||
t.Logf("got connection in progress")
|
||
}
|
||
|
||
// Inject another packet, which will be deduplicated and thus not
|
||
// increment our counter.
|
||
parsed.Decode(pkt)
|
||
if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
|
||
t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
|
||
}
|
||
|
||
// Verify that we now have a single in-flight address in our map.
|
||
impl.mu.Lock()
|
||
inFlight := maps.Clone(impl.connsInFlightByClient)
|
||
impl.mu.Unlock()
|
||
|
||
if got, ok := inFlight[client]; !ok || got != 1 {
|
||
t.Errorf("expected 1 in-flight connection for %v, got: %v", client, inFlight)
|
||
}
|
||
|
||
// Get the expvar statistics and verify that we're exporting the
|
||
// correct metric.
|
||
metrics := impl.ExpVar().(*metrics.Set)
|
||
|
||
const metricName = "gauge_tcp_forward_in_flight"
|
||
if v := metrics.Get(metricName).String(); v != "1" {
|
||
t.Errorf("got metric %q=%s, want 1", metricName, v)
|
||
}
|
||
}
|
||
|
||
// TestTCPForwardLimits_PerClient verifies that the per-client limit for TCP
|
||
// forwarding works.
|
||
func TestTCPForwardLimits_PerClient(t *testing.T) {
|
||
clientmetric.ResetForTest(t)
|
||
envknob.SetenvForTest(t, "TS_DEBUG_NETSTACK", "true")
|
||
|
||
// Set our test override limits during this test.
|
||
maxInFlightConnectionAttemptsForTest.Store(2)
|
||
t.Cleanup(func() { maxInFlightConnectionAttemptsForTest.Store(0) })
|
||
maxInFlightConnectionAttemptsPerClientForTest.Store(1)
|
||
t.Cleanup(func() { maxInFlightConnectionAttemptsPerClientForTest.Store(0) })
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
|
||
dialFn, gotConn := makeHangDialer(t)
|
||
impl.forwardDialFunc = dialFn
|
||
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
// This is the TEST-NET-1 IP block for use in documentation,
|
||
// and should never actually be routable.
|
||
netip.MustParsePrefix("192.0.2.0/24"),
|
||
}
|
||
impl.lb.Start(ipn.Options{
|
||
UpdatePrefs: prefs,
|
||
})
|
||
impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
|
||
// Inject an "outbound" packet that's going to an IP address that times
|
||
// out. We need to re-parse from a byte slice so that the internal
|
||
// buffer in the packet.Parsed type is filled out.
|
||
client := netip.MustParseAddr("100.101.102.103")
|
||
destAddr := netip.MustParseAddr("192.0.2.1")
|
||
|
||
// Helpers
|
||
var port uint16 = 1234
|
||
mustInjectPacket := func() {
|
||
pkt := tcp4syn(t, client, destAddr, port, 4567)
|
||
port++ // to avoid deduplication based on endpoint
|
||
|
||
var parsed packet.Parsed
|
||
parsed.Decode(pkt)
|
||
|
||
// When injecting this packet, we want the outcome to be "drop
|
||
// silently", which indicates that netstack is processing the
|
||
// packet and not delivering it to the host system.
|
||
if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
|
||
t.Fatalf("got filter outcome %v, want filter.DropSilently", resp)
|
||
}
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
waitPacket := func() {
|
||
select {
|
||
case <-ctx.Done():
|
||
t.Fatalf("timed out waiting for connection")
|
||
case <-gotConn:
|
||
t.Logf("got connection in progress")
|
||
}
|
||
}
|
||
|
||
// Inject the packet to start the TCP forward and wait until we have an
|
||
// in-flight outgoing connection.
|
||
mustInjectPacket()
|
||
waitPacket()
|
||
|
||
// Verify that we now have a single in-flight address in our map.
|
||
impl.mu.Lock()
|
||
inFlight := maps.Clone(impl.connsInFlightByClient)
|
||
impl.mu.Unlock()
|
||
|
||
if got, ok := inFlight[client]; !ok || got != 1 {
|
||
t.Errorf("expected 1 in-flight connection for %v, got: %v", client, inFlight)
|
||
}
|
||
|
||
metrics := impl.ExpVar().(*metrics.Set)
|
||
|
||
// One client should have reached the limit at this point.
|
||
if v := metrics.Get("gauge_tcp_forward_in_flight_per_client_limit_reached").String(); v != "1" {
|
||
t.Errorf("got limit reached expvar metric=%s, want 1", v)
|
||
}
|
||
|
||
// Inject another packet, and verify that we've incremented our
|
||
// "dropped" metrics since this will have been dropped.
|
||
mustInjectPacket()
|
||
|
||
// expvar metric
|
||
const metricName = "counter_tcp_forward_max_in_flight_per_client_drop"
|
||
if v := metrics.Get(metricName).String(); v != "1" {
|
||
t.Errorf("got expvar metric %q=%s, want 1", metricName, v)
|
||
}
|
||
|
||
// client metric
|
||
if v := metricPerClientForwardLimit.Value(); v != 1 {
|
||
t.Errorf("got clientmetric limit metric=%d, want 1", v)
|
||
}
|
||
}
|
||
|
||
// TestHandleLocalPackets tests the handleLocalPackets function, ensuring that
|
||
// we are properly deciding to handle packets that are destined for "local"
|
||
// IPs–addresses that are either for this node, or that it is responsible for.
|
||
//
|
||
// See, e.g. #11304
|
||
func TestHandleLocalPackets(t *testing.T) {
|
||
var (
|
||
selfIP4 = netip.MustParseAddr("100.64.1.2")
|
||
selfIP6 = netip.MustParseAddr("fd7a:115c:a1e0::123")
|
||
)
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = false
|
||
impl.ProcessLocalIPs = false
|
||
impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == selfIP4 || addr == selfIP6
|
||
})
|
||
})
|
||
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
// $ tailscale debug via 7 10.1.1.0/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:100/120
|
||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
|
||
}
|
||
prefs.AdvertiseServices = []string{"svc:test-service"}
|
||
_, err := impl.lb.EditPrefs(&ipn.MaskedPrefs{
|
||
Prefs: *prefs,
|
||
AdvertiseRoutesSet: true,
|
||
AdvertiseServicesSet: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("EditPrefs: %v", err)
|
||
}
|
||
IPServiceMap := netmap.IPServiceMappings{
|
||
netip.MustParseAddr("100.99.55.111"): "svc:test-service",
|
||
netip.MustParseAddr("fd7a:115c:a1e0::abcd"): "svc:test-service",
|
||
}
|
||
impl.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
|
||
t.Run("ShouldHandleServiceIP", func(t *testing.T) {
|
||
t.Parallel()
|
||
pkt := &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("127.0.0.1:9999"),
|
||
Dst: netip.MustParseAddrPort("100.100.100.100:53"),
|
||
TCPFlags: packet.TCPSyn,
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
if resp != filter.DropSilently {
|
||
t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
|
||
}
|
||
})
|
||
// Any port on the quad-100 service IP must be absorbed locally by
|
||
// netstack and never leak out to the WireGuard / peer-routing
|
||
// layers. Historically we only intercepted specific ports (UDP 53
|
||
// and TCP 53/80/8080), causing stray traffic to other ports such
|
||
// as 100.100.100.100:853 (DoT) to time out in wireguard-go and
|
||
// produce "open-conn-track: timeout opening ...; no associated
|
||
// peer node" log spam. See the handleLocalPackets comment.
|
||
quad100LeakCases := []struct {
|
||
name string
|
||
proto ipproto.Proto
|
||
dst string
|
||
}{
|
||
{"TCP-853-DoT-v4", ipproto.TCP, "100.100.100.100:853"},
|
||
{"TCP-443-DoH-v4", ipproto.TCP, "100.100.100.100:443"},
|
||
{"TCP-9000-stray-v4", ipproto.TCP, "100.100.100.100:9000"},
|
||
{"UDP-853-DoQ-v4", ipproto.UDP, "100.100.100.100:853"},
|
||
{"UDP-443-v4", ipproto.UDP, "100.100.100.100:443"},
|
||
{"TCP-853-DoT-v6", ipproto.TCP, "[fd7a:115c:a1e0::53]:853"},
|
||
{"UDP-443-v6", ipproto.UDP, "[fd7a:115c:a1e0::53]:443"},
|
||
}
|
||
for _, tc := range quad100LeakCases {
|
||
t.Run("ShouldNotLeakQuad100_"+tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
dst := netip.MustParseAddrPort(tc.dst)
|
||
ipVersion := uint8(4)
|
||
if dst.Addr().Is6() {
|
||
ipVersion = 6
|
||
}
|
||
src := "127.0.0.1:9999"
|
||
if ipVersion == 6 {
|
||
src = "[::1]:9999"
|
||
}
|
||
pkt := &packet.Parsed{
|
||
IPVersion: ipVersion,
|
||
IPProto: tc.proto,
|
||
Src: netip.MustParseAddrPort(src),
|
||
Dst: dst,
|
||
}
|
||
if tc.proto == ipproto.TCP {
|
||
pkt.TCPFlags = packet.TCPSyn
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
if resp != filter.DropSilently {
|
||
t.Errorf("quad-100 %s packet leaked: got filter outcome %v, want filter.DropSilently", tc.name, resp)
|
||
}
|
||
})
|
||
}
|
||
// Exhaustive sweep of all ports for both transport protocols and
|
||
// both IP versions, confirming no port leaks. The quad-100 branch
|
||
// of handleLocalPackets is port-independent by construction; this
|
||
// test serves as a regression guard against accidental port-based
|
||
// exemptions slipping back in.
|
||
t.Run("ShouldNotLeakQuad100_AllPorts", func(t *testing.T) {
|
||
t.Parallel()
|
||
protos := []ipproto.Proto{ipproto.TCP, ipproto.UDP}
|
||
dsts := []netip.Addr{
|
||
netip.MustParseAddr("100.100.100.100"),
|
||
netip.MustParseAddr("fd7a:115c:a1e0::53"),
|
||
}
|
||
for _, proto := range protos {
|
||
for _, dstAddr := range dsts {
|
||
ipVersion := uint8(4)
|
||
srcStr := "127.0.0.1:9999"
|
||
if dstAddr.Is6() {
|
||
ipVersion = 6
|
||
srcStr = "[::1]:9999"
|
||
}
|
||
src := netip.MustParseAddrPort(srcStr)
|
||
for port := 1; port <= 65535; port++ {
|
||
pkt := &packet.Parsed{
|
||
IPVersion: ipVersion,
|
||
IPProto: proto,
|
||
Src: src,
|
||
Dst: netip.AddrPortFrom(dstAddr, uint16(port)),
|
||
}
|
||
if proto == ipproto.TCP {
|
||
pkt.TCPFlags = packet.TCPSyn
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
if resp != filter.DropSilently {
|
||
t.Fatalf("port=%d proto=%v dst=%v: got %v, want filter.DropSilently", port, proto, dstAddr, resp)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
t.Run("ShouldHandle4via6", func(t *testing.T) {
|
||
t.Parallel()
|
||
pkt := &packet.Parsed{
|
||
IPVersion: 6,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("[::1]:1234"),
|
||
|
||
// This is an IP in the above 4via6 subnet that this node handles.
|
||
// $ tailscale debug via 7 10.1.1.9/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:109/120
|
||
Dst: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:109]:5678"),
|
||
TCPFlags: packet.TCPSyn,
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
|
||
// DropSilently is the outcome we expected, since we actually
|
||
// handled this packet by injecting it into netstack, which
|
||
// will handle creating the TCP forwarder. We drop it so we
|
||
// don't process the packet outside of netstack.
|
||
if resp != filter.DropSilently {
|
||
t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
|
||
}
|
||
})
|
||
t.Run("ShouldHandleLocalTailscaleServices", func(t *testing.T) {
|
||
t.Parallel()
|
||
pkt := &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("127.0.0.1:9999"),
|
||
Dst: netip.MustParseAddrPort("100.99.55.111:80"),
|
||
TCPFlags: packet.TCPSyn,
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
if resp != filter.DropSilently {
|
||
t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
|
||
}
|
||
})
|
||
t.Run("ShouldNotHandleInactiveVIPService", func(t *testing.T) {
|
||
// Tests that packets to Tailscale Services we don't host are accepted.
|
||
inactiveVIP := netip.MustParseAddr("100.99.55.222")
|
||
impl.lb.ForTest().SetIPServiceMappings(netmap.IPServiceMappings{
|
||
netip.MustParseAddr("100.99.55.111"): "svc:test-service", // active (shared fixture)
|
||
netip.MustParseAddr("fd7a:115c:a1e0::abcd"): "svc:test-service",
|
||
inactiveVIP: "svc:inactive-elsewhere",
|
||
})
|
||
t.Cleanup(func() {
|
||
// Restore the shared fixture for any later/parallel subtests.
|
||
impl.lb.ForTest().SetIPServiceMappings(IPServiceMap)
|
||
})
|
||
pkt := &packet.Parsed{
|
||
IPVersion: 4,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("127.0.0.1:9999"),
|
||
Dst: netip.AddrPortFrom(inactiveVIP, 80),
|
||
TCPFlags: packet.TCPSyn,
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
if resp != filter.Accept {
|
||
t.Errorf("inactive VIP service: got filter outcome %v, want filter.Accept (pass through to host)", resp)
|
||
}
|
||
})
|
||
t.Run("OtherNonHandled", func(t *testing.T) {
|
||
t.Parallel()
|
||
pkt := &packet.Parsed{
|
||
IPVersion: 6,
|
||
IPProto: ipproto.TCP,
|
||
Src: netip.MustParseAddrPort("[::1]:1234"),
|
||
|
||
// This IP is *not* in the above 4via6 route
|
||
// $ tailscale debug via 99 10.1.1.9/24
|
||
// fd7a:115c:a1e0:b1a:0:63:a01:109/120
|
||
Dst: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:63:a01:109]:5678"),
|
||
TCPFlags: packet.TCPSyn,
|
||
}
|
||
resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
|
||
|
||
// Accept means that handleLocalPackets does not handle this
|
||
// packet, we "accept" it to continue further processing,
|
||
// instead of dropping because it was already handled.
|
||
if resp != filter.Accept {
|
||
t.Errorf("got filter outcome %v, want filter.Accept", resp)
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestAcceptTCPRoutingTailscaleIPRange tests how acceptTCP behaves for TCP SYN
|
||
// packets destined to IPs in the Tailscale range (100.64.0.0/10).
|
||
//
|
||
// - Packets to the Tailscale IP should be forwarded to loopback if there is
|
||
// no configured handler for the port.
|
||
// - Packets to the service IP (100.100.100.100) on non-served ports should
|
||
// never make it to the local host.
|
||
// - Packets to a Tailscale Service VIP on non-served ports should never make
|
||
// it to the local host.
|
||
func TestAcceptTCPLoopbackForwardVsRST(t *testing.T) {
|
||
serviceVIP := netip.MustParseAddr("100.90.1.2")
|
||
selfIP := netip.MustParseAddr("100.64.1.2")
|
||
const serviceName = "svc:test"
|
||
|
||
cases := []struct {
|
||
name string
|
||
// configure runs inside makeNetstack, before Start.
|
||
configure func(*Impl)
|
||
// afterStart runs after Start, for state that requires the backend to be
|
||
// running (prefs, service IP maps, NIC address registration). Optional.
|
||
afterStart func(t *testing.T, impl *Impl)
|
||
// dst is the SYN's destination. Its port has no registered handler.
|
||
dst netip.AddrPort
|
||
// inbound selects the injection path: true => injectInbound (a packet
|
||
// from a peer), false => handleLocalPackets (host-originated, or the
|
||
// kernel-hairpin re-entry).
|
||
inbound bool
|
||
// wantForward is whether acceptTCP should forward the connection to the
|
||
// host's loopback (true) or reject it with a RST (false).
|
||
wantForward bool
|
||
}{
|
||
{
|
||
name: "Quad-100UnservedPortIsRST",
|
||
configure: func(impl *Impl) {
|
||
impl.ProcessSubnets = false
|
||
impl.ProcessLocalIPs = false
|
||
impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
},
|
||
// 853 is DoT, the specific case called out in the original bug
|
||
// report ("conntrack error no peer found for 100.100.100.100:853").
|
||
// Before the fix, port 853 (and any non-{53,80,8080} port) leaked
|
||
// out to WireGuard; after the fix it is absorbed and must NOT
|
||
// trigger forwardTCP. handleLocalPackets absorbs all quad-100
|
||
// traffic regardless of port to prevent it leaking to WireGuard
|
||
// peers (which produced noisy "open-conn-track: timeout opening ...;
|
||
// no associated peer node" log lines), leaving acceptTCP to reject
|
||
// the unserved port with a RST rather than falling through to the
|
||
// isTailscaleIP loopback rewrite.
|
||
dst: netip.AddrPortFrom(tsaddr.TailscaleServiceIP(), 853),
|
||
wantForward: false,
|
||
},
|
||
{
|
||
name: "VIPServiceUnservedPortIsRST",
|
||
configure: func(impl *Impl) {
|
||
impl.ProcessSubnets = false
|
||
impl.ProcessLocalIPs = false
|
||
impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
impl.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == serviceVIP
|
||
})
|
||
},
|
||
afterStart: func(t *testing.T, impl *Impl) {
|
||
// Mark the service as one this node hosts and is actively
|
||
// serving, so handleLocalPackets absorbs its traffic into
|
||
// netstack (rather than letting a non-hosted VIP route through).
|
||
// The service serves no TCP ports here, so every port is
|
||
// "non-served". AdvertiseServices flows through to
|
||
// UpdateActiveVIPServices, marking the service active.
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseServices = []string{serviceName}
|
||
if _, err := impl.lb.EditPrefs(&ipn.MaskedPrefs{
|
||
Prefs: *prefs,
|
||
AdvertiseServicesSet: true,
|
||
}); err != nil {
|
||
t.Fatalf("EditPrefs: %v", err)
|
||
}
|
||
impl.lb.ForTest().SetIPServiceMappings(netmap.IPServiceMappings{serviceVIP: serviceName})
|
||
},
|
||
dst: netip.AddrPortFrom(serviceVIP, 8001), // 8001: a port the service doesn't serve
|
||
wantForward: false,
|
||
},
|
||
{
|
||
name: "LocalTailscaleIPUnhandledPortForwardsToLoopback",
|
||
configure: func(impl *Impl) {
|
||
impl.ProcessSubnets = false
|
||
// ProcessLocalIPs=true so an inbound packet to a local Tailscale
|
||
// IP is absorbed into netstack and dispatched to acceptTCP.
|
||
impl.ProcessLocalIPs = true
|
||
impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == selfIP
|
||
})
|
||
},
|
||
// 9999 has no SSH/webclient/peerapi/serve handler, so
|
||
// TCPHandlerForDst returns nil and acceptTCP falls to the
|
||
// isTailscaleIP case, which rewrites the dial to 127.0.0.1:9999 and
|
||
// calls forwardTCP. This is how local handlers reach the host, and
|
||
// the RST guards above must not swallow it.
|
||
dst: netip.AddrPortFrom(selfIP, 9999),
|
||
inbound: true,
|
||
wantForward: true,
|
||
},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
impl := makeNetstack(t, tc.configure)
|
||
if tc.afterStart != nil {
|
||
tc.afterStart(t, impl)
|
||
}
|
||
|
||
dialFn, gotConn := makeHangDialer(t)
|
||
impl.forwardDialFunc = dialFn
|
||
|
||
// Use a client IP in the CGNAT range so shouldProcessInbound-adjacent
|
||
// code paths treat this as plausibly-peer-sourced traffic, matching
|
||
// what a real stray probe from a peer or the host OS would look like.
|
||
client := netip.MustParseAddr("100.101.102.103")
|
||
pkt := tcp4syn(t, client, tc.dst.Addr(), 1234, tc.dst.Port())
|
||
var parsed packet.Parsed
|
||
parsed.Decode(pkt)
|
||
|
||
// Both injection paths absorb the packet into netstack, returning
|
||
// filter.DropSilently (i.e. not handing it to the host); acceptTCP
|
||
// is then dispatched with the packet.
|
||
inject := impl.handleLocalPackets
|
||
if tc.inbound {
|
||
inject = impl.injectInbound
|
||
}
|
||
if resp, _ := inject(&parsed, impl.tundev, nil); resp != filter.DropSilently {
|
||
t.Fatalf("inject for %v: got filter outcome %v, want filter.DropSilently", tc.dst, resp)
|
||
}
|
||
|
||
// acceptTCP runs asynchronously in the gVisor TCP dispatcher after
|
||
// handleLocalPackets injects the packet into netstack. Use the
|
||
// in-flight connection counter as a deterministic synchronization
|
||
// point: wrapTCPProtocolHandler increments connsInFlightByClient
|
||
// when the dispatcher hands the connection off to acceptTCP, and
|
||
// acceptTCP's deferred decrementInFlightTCPForward decrements it
|
||
// on return.
|
||
//
|
||
// On the green path (RST guard fires), acceptTCP returns promptly
|
||
// and the counter reaches 0. On the red path (fall-through to
|
||
// forwardTCP), acceptTCP blocks inside the forwardDialFunc call —
|
||
// makeHangDialer signals gotConn on entry (buffered, non-blocking)
|
||
// and then blocks forever — so the counter never reaches 0 but
|
||
// gotConn fires synchronously from the dispatcher goroutine. A
|
||
// select on both races those outcomes without real-time padding.
|
||
//
|
||
// testing/synctest is not usable here: gVisor's sleep package calls
|
||
// the runtime's gopark directly rather than via the standard
|
||
// library, so synctest.Wait() cannot observe those goroutines
|
||
// becoming durably blocked and hangs indefinitely.
|
||
inFlightZero := make(chan struct{})
|
||
go func() {
|
||
for {
|
||
impl.mu.Lock()
|
||
n := impl.connsInFlightByClient[client]
|
||
impl.mu.Unlock()
|
||
if n == 0 {
|
||
close(inFlightZero)
|
||
return
|
||
}
|
||
time.Sleep(time.Millisecond)
|
||
}
|
||
}()
|
||
|
||
select {
|
||
case <-gotConn:
|
||
if !tc.wantForward {
|
||
t.Fatalf("forwardDialFunc was called for %v; acceptTCP forwarded to the host's loopback instead of sending a RST", tc.dst)
|
||
}
|
||
case <-inFlightZero:
|
||
if tc.wantForward {
|
||
t.Fatalf("forwardDialFunc was NOT called for %v; acceptTCP rejected the connection instead of forwarding it to loopback", tc.dst)
|
||
}
|
||
case <-time.After(5 * time.Second):
|
||
// Safety net so a regression in the in-flight counter plumbing
|
||
// doesn't hang the whole test run; both outcomes above should
|
||
// fire within milliseconds in practice.
|
||
t.Fatalf("timed out waiting for acceptTCP to dispatch %v SYN", tc.dst)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestShouldHandlePingViaHostScoped verifies that ping relay for 4via6
|
||
// addresses embedding host-scoped IPv4 destinations is refused.
|
||
func TestShouldHandlePingViaHostScoped(t *testing.T) {
|
||
srcIP := netip.AddrFrom4([4]byte{1, 2, 3, 4})
|
||
pingPkt := func(dst netip.Addr) *packet.Parsed {
|
||
icmph := packet.ICMP6Header{
|
||
IP6Header: packet.IP6Header{
|
||
IPProto: ipproto.ICMPv6,
|
||
Src: srcIP,
|
||
Dst: dst,
|
||
},
|
||
Type: packet.ICMP6EchoRequest,
|
||
Code: packet.ICMP6NoCode,
|
||
}
|
||
_, payload := packet.ICMPEchoPayload(nil)
|
||
pkt := &packet.Parsed{}
|
||
pkt.Decode(packet.Generate(icmph, payload))
|
||
return pkt
|
||
}
|
||
|
||
cases := []struct {
|
||
name string
|
||
v4 string
|
||
wantOK bool
|
||
}{
|
||
{"SiteHost", "10.1.1.9", true},
|
||
{"Metadata", "169.254.169.254", false},
|
||
{"Loopback", "127.0.0.1", false},
|
||
{"TailscaleCGNAT", "100.64.1.2", false},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
dst := mustVia99(t, tc.v4)
|
||
pingDst, ok := impl.shouldHandlePing(pingPkt(dst))
|
||
if ok != tc.wantOK {
|
||
t.Fatalf("shouldHandlePing(%v) ok = %v, want %v", dst, ok, tc.wantOK)
|
||
}
|
||
if ok && pingDst != netip.MustParseAddr(tc.v4) {
|
||
t.Errorf("shouldHandlePing(%v) pingDst = %v, want %v", dst, pingDst, tc.v4)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAcceptTCPViaHostScoped is a regression test for the 4via6 filter
|
||
// bypass: the packet filter only sees the outer via address, so acceptTCP
|
||
// must police the unmapped destination itself. SYNs to via addresses
|
||
// embedding host-scoped IPv4 destinations must be RST without invoking the
|
||
// forward dialer; ordinary site destinations must still be forwarded.
|
||
func TestAcceptTCPViaHostScoped(t *testing.T) {
|
||
viaPrefix := netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:63::/96") // site 99
|
||
|
||
cases := []struct {
|
||
name string
|
||
dst netip.AddrPort
|
||
// wantForward is if acceptTCP should dial the target or reject the connection with a RST
|
||
wantForward bool
|
||
}{
|
||
{"MetadataBlocked", netip.AddrPortFrom(mustVia99(t, "169.254.169.254"), 80), false},
|
||
{"LoopbackBlocked", netip.AddrPortFrom(mustVia99(t, "127.0.0.1"), 22), false},
|
||
{"TailscaleCGNATBlocked", netip.AddrPortFrom(mustVia99(t, "100.64.3.4"), 80), false},
|
||
{"SiteHostAllowed", netip.AddrPortFrom(mustVia99(t, "10.0.0.1"), 80), true},
|
||
{"SiteNetworkAllowed", netip.AddrPortFrom(mustVia99(t, "10.0.0.0"), 80), true},
|
||
{"SiteBroadcastAllowed", netip.AddrPortFrom(mustVia99(t, "10.0.0.255"), 80), true},
|
||
{"SiteLastHostAllowed", netip.AddrPortFrom(mustVia99(t, "10.0.0.254"), 80), true},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.ProcessSubnets = true
|
||
})
|
||
// Advertise the via prefix so ShouldHandleViaIP accepts the dst
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{viaPrefix}
|
||
impl.lb.Start(ipn.Options{UpdatePrefs: prefs})
|
||
impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
|
||
dialFn, gotConn := makeHangDialer(t)
|
||
impl.forwardDialFunc = dialFn
|
||
|
||
client := tsaddr.Tailscale4To6(netip.MustParseAddr("100.101.102.103"))
|
||
pkt := tcp6syn(t, client, tc.dst.Addr(), 1234, tc.dst.Port())
|
||
var parsed packet.Parsed
|
||
parsed.Decode(pkt)
|
||
|
||
if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
|
||
t.Fatalf("inject for %v: got filter outcome %v, want filter.DropSilently", tc.dst, resp)
|
||
}
|
||
|
||
// Same synchronization as TestAcceptTCPLoopbackForwardVsRST:
|
||
// the in-flight counter reaching 0 means acceptTCP returned
|
||
// (RST path); gotConn firing means it called forwardTCP.
|
||
inFlightZero := make(chan struct{})
|
||
go func() {
|
||
for {
|
||
impl.mu.Lock()
|
||
n := impl.connsInFlightByClient[client]
|
||
impl.mu.Unlock()
|
||
if n == 0 {
|
||
close(inFlightZero)
|
||
return
|
||
}
|
||
time.Sleep(time.Millisecond)
|
||
}
|
||
}()
|
||
|
||
select {
|
||
case <-gotConn:
|
||
if !tc.wantForward {
|
||
t.Fatalf("forwardDialFunc was called for %v; acceptTCP forwarded a host-scoped 4via6 target instead of sending a RST", tc.dst)
|
||
}
|
||
case <-inFlightZero:
|
||
if tc.wantForward {
|
||
t.Fatalf("forwardDialFunc was NOT called for %v; acceptTCP rejected a legitimate 4via6 target", tc.dst)
|
||
}
|
||
case <-time.After(5 * time.Second):
|
||
t.Fatalf("timed out waiting for acceptTCP to dispatch %v SYN", tc.dst)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestForwardUDPViaHostScoped is a regression test for the 4via6 filter
|
||
// bypass: the packet filter only sees the outer via address, so forwardUDP
|
||
// must police the unmapped destination itself. UDP flows to via addresses
|
||
// embedding host-scoped IPv4 destinations must be dropped before any backend
|
||
// socket is created, and the drop must close the client endpoint: acceptUDP
|
||
// runs forwardUDP in a goroutine, so nothing else would.
|
||
func TestForwardUDPViaHostScoped(t *testing.T) {
|
||
tstest.AssertNotParallel(t) // calls clientmetric.ResetForTest
|
||
clientmetric.ResetForTest(t)
|
||
|
||
impl := makeNetstack(t, func(impl *Impl) {
|
||
impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
|
||
})
|
||
|
||
client := tsaddr.Tailscale4To6(netip.MustParseAddr("100.101.102.103"))
|
||
blocked := []string{
|
||
"127.0.0.1",
|
||
"169.254.169.254", // port 80 below: metadata
|
||
"100.64.1.2",
|
||
"224.0.0.1",
|
||
"0.0.0.0",
|
||
"255.255.255.255",
|
||
}
|
||
for i, v4 := range blocked {
|
||
dst := netip.AddrPortFrom(mustVia99(t, v4), 80)
|
||
|
||
// A bare endpoint suffices: the drop path only closes the conn; dialing would need real routes
|
||
var wq waiter.Queue
|
||
ep, err := impl.ipstack.NewEndpoint(udp.ProtocolNumber, ipv6.ProtocolNumber, &wq)
|
||
if err != nil {
|
||
t.Fatalf("NewEndpoint: %v", err)
|
||
}
|
||
conn := gonet.NewUDPConn(&wq, ep)
|
||
|
||
// Distinct source port per flow; the guard runs synchronously, so no waiting is needed
|
||
before := metricViaHostScopedDrop.Value()
|
||
impl.forwardUDP(conn, netip.AddrPortFrom(client, uint16(1000+i)), dst)
|
||
|
||
if got := metricViaHostScopedDrop.Value() - before; got != 1 {
|
||
t.Errorf("UDP to %v: drop metric delta = %d, want 1 (guard did not fire)", dst, got)
|
||
}
|
||
// ep.Read never blocks (no gonet retry loop): ErrClosedForReceive iff the guard closed the conn
|
||
w := tcpip.SliceWriter(make([]byte, 1))
|
||
_, rerr := ep.Read(&w, tcpip.ReadOptions{})
|
||
if _, closed := rerr.(*tcpip.ErrClosedForReceive); !closed {
|
||
t.Errorf("after UDP to %v: client endpoint Read = %v, want ErrClosedForReceive", dst, rerr)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestShouldSendToHost(t *testing.T) {
|
||
var (
|
||
selfIP4 = netip.MustParseAddr("100.64.1.2")
|
||
selfIP6 = netip.MustParseAddr("fd7a:115c:a1e0::123")
|
||
tailscaleServiceIP4 = netip.MustParseAddr("100.99.55.111")
|
||
tailscaleServiceIP6 = netip.MustParseAddr("fd7a:115c:a1e0::abcd")
|
||
)
|
||
|
||
makeTestNetstack := func(tb testing.TB) *Impl {
|
||
impl := makeNetstack(tb, func(impl *Impl) {
|
||
impl.ProcessSubnets = false
|
||
impl.ProcessLocalIPs = false
|
||
impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == selfIP4 || addr == selfIP6
|
||
})
|
||
impl.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
|
||
return addr == tailscaleServiceIP4 || addr == tailscaleServiceIP6
|
||
})
|
||
})
|
||
|
||
prefs := ipn.NewPrefs()
|
||
prefs.AdvertiseRoutes = []netip.Prefix{
|
||
// $ tailscale debug via 7 10.1.1.0/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:100/120
|
||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
|
||
}
|
||
_, err := impl.lb.EditPrefs(&ipn.MaskedPrefs{
|
||
Prefs: *prefs,
|
||
AdvertiseRoutesSet: true,
|
||
})
|
||
if err != nil {
|
||
tb.Fatalf("EditPrefs: %v", err)
|
||
}
|
||
return impl
|
||
}
|
||
|
||
testCases := []struct {
|
||
name string
|
||
src, dst netip.AddrPort
|
||
want bool
|
||
}{
|
||
// Reply from service IP to localhost should be sent to host,
|
||
// not over WireGuard.
|
||
{
|
||
name: "from_service_ip_to_localhost",
|
||
src: netip.AddrPortFrom(serviceIP, 53),
|
||
dst: netip.MustParseAddrPort("127.0.0.1:9999"),
|
||
want: true,
|
||
},
|
||
{
|
||
name: "from_service_ip_to_localhost_v6",
|
||
src: netip.AddrPortFrom(serviceIPv6, 53),
|
||
dst: netip.MustParseAddrPort("[::1]:9999"),
|
||
want: true,
|
||
},
|
||
// A reply from the local IP to a remote host isn't sent to the
|
||
// host, but rather over WireGuard.
|
||
{
|
||
name: "local_ip_to_remote",
|
||
src: netip.AddrPortFrom(selfIP4, 12345),
|
||
dst: netip.MustParseAddrPort("100.64.99.88:7777"),
|
||
want: false,
|
||
},
|
||
{
|
||
name: "local_ip_to_remote_v6",
|
||
src: netip.AddrPortFrom(selfIP6, 12345),
|
||
dst: netip.MustParseAddrPort("[fd7a:115:a1e0::99]:7777"),
|
||
want: false,
|
||
},
|
||
// A reply from a 4via6 address to a remote host isn't sent to
|
||
// the local host, but rather over WireGuard. See:
|
||
// https://github.com/tailscale/tailscale/issues/12448
|
||
{
|
||
name: "4via6_to_remote",
|
||
|
||
// $ tailscale debug via 7 10.1.1.99/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:163/120
|
||
src: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:163]:12345"),
|
||
dst: netip.MustParseAddrPort("[fd7a:115:a1e0::99]:7777"),
|
||
want: false,
|
||
},
|
||
// However, a reply from a 4via6 address to the local Tailscale
|
||
// IP for this host *is* sent to the local host. See:
|
||
// https://github.com/tailscale/tailscale/issues/11304
|
||
{
|
||
name: "4via6_to_local",
|
||
|
||
// $ tailscale debug via 7 10.1.1.99/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:163/120
|
||
src: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:163]:12345"),
|
||
dst: netip.AddrPortFrom(selfIP6, 7777),
|
||
want: true,
|
||
},
|
||
// Traffic from a 4via6 address that we're not handling to
|
||
// either the local Tailscale IP or a remote host is sent
|
||
// outbound.
|
||
//
|
||
// In most cases, we won't see this type of traffic in the
|
||
// shouldSendToHost function, but let's confirm.
|
||
{
|
||
name: "other_4via6_to_local",
|
||
|
||
// $ tailscale debug via 4444 10.1.1.88/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:163/120
|
||
src: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:115c:a01:158]:12345"),
|
||
dst: netip.AddrPortFrom(selfIP6, 7777),
|
||
want: false,
|
||
},
|
||
{
|
||
name: "other_4via6_to_remote",
|
||
|
||
// $ tailscale debug via 4444 10.1.1.88/24
|
||
// fd7a:115c:a1e0:b1a:0:7:a01:163/120
|
||
src: netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:115c:a01:158]:12345"),
|
||
dst: netip.MustParseAddrPort("[fd7a:115:a1e0::99]:7777"),
|
||
want: false,
|
||
},
|
||
// After accessing the Tailscale service from host, replies from Tailscale Service IPs
|
||
// to the local Tailscale IPs should be sent to the host.
|
||
{
|
||
name: "from_service_ip_to_local_ip",
|
||
src: netip.AddrPortFrom(tailscaleServiceIP4, 80),
|
||
dst: netip.AddrPortFrom(selfIP4, 12345),
|
||
want: true,
|
||
},
|
||
{
|
||
name: "from_service_ip_to_local_ip_v6",
|
||
src: netip.AddrPortFrom(tailscaleServiceIP6, 80),
|
||
dst: netip.AddrPortFrom(selfIP6, 12345),
|
||
want: true,
|
||
},
|
||
// Traffic from remote IPs to Tailscale Service IPs should be sent over WireGuard.
|
||
{
|
||
name: "from_service_ip_to_remote",
|
||
src: netip.AddrPortFrom(tailscaleServiceIP4, 80),
|
||
dst: netip.MustParseAddrPort("173.201.32.56:54321"),
|
||
want: false,
|
||
},
|
||
{
|
||
name: "from_service_ip_to_remote_v6",
|
||
src: netip.AddrPortFrom(tailscaleServiceIP6, 80),
|
||
dst: netip.MustParseAddrPort("[2001:4860:4860::8888]:54321"),
|
||
want: false,
|
||
},
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
ns := makeTestNetstack(t)
|
||
if got := ns.shouldSendToHost(pkt); got != tt.want {
|
||
t.Errorf("shouldSendToHost returned %v, want %v", got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func makeUDP4PacketBuffer(src, dst netip.AddrPort) *stack.PacketBuffer {
|
||
if !src.Addr().Is4() || !dst.Addr().Is4() {
|
||
panic("src and dst must be IPv4")
|
||
}
|
||
|
||
data := []byte("hello world\n")
|
||
|
||
packetLen := header.IPv4MinimumSize + header.UDPMinimumSize
|
||
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||
ReserveHeaderBytes: packetLen,
|
||
Payload: buffer.MakeWithData(data),
|
||
})
|
||
|
||
// Initialize the UDP header.
|
||
udp := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
|
||
pkt.TransportProtocolNumber = header.UDPProtocolNumber
|
||
|
||
length := uint16(pkt.Size())
|
||
udp.Encode(&header.UDPFields{
|
||
SrcPort: src.Port(),
|
||
DstPort: dst.Port(),
|
||
Length: length,
|
||
})
|
||
|
||
// Add IP header
|
||
ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
|
||
pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
|
||
ipHdr.Encode(&header.IPv4Fields{
|
||
TotalLength: uint16(packetLen),
|
||
Protocol: uint8(header.UDPProtocolNumber),
|
||
SrcAddr: tcpip.AddrFrom4(src.Addr().As4()),
|
||
DstAddr: tcpip.AddrFrom4(dst.Addr().As4()),
|
||
Checksum: 0,
|
||
})
|
||
|
||
return pkt
|
||
}
|
||
|
||
func makeUDP6PacketBuffer(src, dst netip.AddrPort) *stack.PacketBuffer {
|
||
if !src.Addr().Is6() || !dst.Addr().Is6() {
|
||
panic("src and dst must be IPv6")
|
||
}
|
||
data := []byte("hello world\n")
|
||
|
||
packetLen := header.IPv6MinimumSize + header.UDPMinimumSize
|
||
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||
ReserveHeaderBytes: packetLen,
|
||
Payload: buffer.MakeWithData(data),
|
||
})
|
||
|
||
srcAddr := tcpip.AddrFrom16(src.Addr().As16())
|
||
dstAddr := tcpip.AddrFrom16(dst.Addr().As16())
|
||
|
||
// Add IP header
|
||
ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
|
||
pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber
|
||
ipHdr.Encode(&header.IPv6Fields{
|
||
SrcAddr: srcAddr,
|
||
DstAddr: dstAddr,
|
||
PayloadLength: uint16(header.UDPMinimumSize + len(data)),
|
||
TransportProtocol: header.UDPProtocolNumber,
|
||
HopLimit: 64,
|
||
})
|
||
|
||
// Initialize the UDP header.
|
||
udp := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
|
||
pkt.TransportProtocolNumber = header.UDPProtocolNumber
|
||
|
||
length := uint16(pkt.Size())
|
||
udp.Encode(&header.UDPFields{
|
||
SrcPort: src.Port(),
|
||
DstPort: dst.Port(),
|
||
Length: length,
|
||
})
|
||
|
||
// Calculate the UDP pseudo-header checksum.
|
||
xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcAddr, dstAddr, uint16(len(udp)))
|
||
udp.SetChecksum(^udp.CalculateChecksum(xsum))
|
||
|
||
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
|
||
}
|
||
|
||
func fragmentIPv4ForTest(t testing.TB, pkt []byte, firstPayloadLen uint16) (first, second []byte) {
|
||
t.Helper()
|
||
if firstPayloadLen%8 != 0 {
|
||
t.Fatalf("firstPayloadLen %d is not 8-byte aligned", firstPayloadLen)
|
||
}
|
||
if len(pkt) < header.IPv4MinimumSize+int(firstPayloadLen) {
|
||
t.Fatalf("packet length %d too short for firstPayloadLen %d", len(pkt), firstPayloadLen)
|
||
}
|
||
ip := header.IPv4(pkt)
|
||
if ip.HeaderLength() != header.IPv4MinimumSize {
|
||
t.Fatalf("test helper only supports 20-byte IPv4 headers; got %d", ip.HeaderLength())
|
||
}
|
||
ipPayloadLen := len(pkt) - header.IPv4MinimumSize
|
||
if int(firstPayloadLen) >= ipPayloadLen {
|
||
t.Fatalf("firstPayloadLen %d must be smaller than IP payload length %d", firstPayloadLen, ipPayloadLen)
|
||
}
|
||
|
||
first = make([]byte, header.IPv4MinimumSize+int(firstPayloadLen))
|
||
copy(first, pkt[:len(first)])
|
||
firstIP := header.IPv4(first)
|
||
firstIP.SetTotalLength(uint16(len(first)))
|
||
firstIP.SetFlagsFragmentOffset(header.IPv4FlagMoreFragments, 0)
|
||
firstIP.SetChecksum(0)
|
||
firstIP.SetChecksum(^firstIP.CalculateChecksum())
|
||
|
||
secondPayloadLen := ipPayloadLen - int(firstPayloadLen)
|
||
second = make([]byte, header.IPv4MinimumSize+secondPayloadLen)
|
||
copy(second[:header.IPv4MinimumSize], pkt[:header.IPv4MinimumSize])
|
||
copy(second[header.IPv4MinimumSize:], pkt[header.IPv4MinimumSize+int(firstPayloadLen):])
|
||
secondIP := header.IPv4(second)
|
||
secondIP.SetTotalLength(uint16(len(second)))
|
||
secondIP.SetFlagsFragmentOffset(0, firstPayloadLen)
|
||
secondIP.SetChecksum(0)
|
||
secondIP.SetChecksum(^secondIP.CalculateChecksum())
|
||
|
||
return first, second
|
||
}
|
||
|
||
// TestLinkEndpointInjectInboundIPv4Fragments verifies that Tailscale's inbound
|
||
// link endpoint path lets IPv4 fragments reach gVisor for reassembly.
|
||
// Previously (see https://github.com/tailscale/tailscale/issues/20320),
|
||
// gro.RXChecksumOffload validated L4 checksums before reassembly, so the first
|
||
// fragment was dropped and the UDP datagram never reached the socket.
|
||
func TestLinkEndpointInjectInboundIPv4Fragments(t *testing.T) {
|
||
const nicID tcpip.NICID = 1
|
||
localIP := netip.MustParseAddr("100.64.1.2")
|
||
remoteIP := netip.MustParseAddr("100.64.1.3")
|
||
payload := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz")
|
||
raw := udp4raw(t, remoteIP, localIP, 12345, 8081, payload)
|
||
first, second := fragmentIPv4ForTest(t, raw, 80)
|
||
|
||
s := stack.New(stack.Options{
|
||
NetworkProtocols: []stack.NetworkProtocolFactory{
|
||
ipv4.NewProtocol,
|
||
},
|
||
TransportProtocols: []stack.TransportProtocolFactory{
|
||
udp.NewProtocol,
|
||
},
|
||
})
|
||
defer s.Close()
|
||
|
||
ep := newLinkEndpoint(64, 1280, "", groNotSupported)
|
||
if err := s.CreateNIC(nicID, ep); err != nil {
|
||
t.Fatalf("CreateNIC: %v", err)
|
||
}
|
||
if err := s.AddProtocolAddress(nicID, tcpip.ProtocolAddress{
|
||
Protocol: header.IPv4ProtocolNumber,
|
||
AddressWithPrefix: tcpip.AddrFrom4(localIP.As4()).WithPrefix(),
|
||
}, stack.AddressProperties{}); err != nil {
|
||
t.Fatalf("AddProtocolAddress: %v", err)
|
||
}
|
||
|
||
pc, err := gonet.DialUDP(s, &tcpip.FullAddress{
|
||
NIC: nicID,
|
||
Addr: tcpip.AddrFrom4(localIP.As4()),
|
||
Port: 8081,
|
||
}, nil, header.IPv4ProtocolNumber)
|
||
if err != nil {
|
||
t.Fatalf("DialUDP: %v", err)
|
||
}
|
||
defer pc.Close()
|
||
|
||
var parsed packet.Parsed
|
||
parsed.Decode(first)
|
||
ep.injectInbound(&parsed)
|
||
parsed.Decode(second)
|
||
ep.injectInbound(&parsed)
|
||
|
||
if err := pc.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||
t.Fatalf("SetReadDeadline: %v", err)
|
||
}
|
||
buf := make([]byte, 512)
|
||
n, addr, err := pc.ReadFrom(buf)
|
||
if err != nil {
|
||
t.Fatalf("ReadFrom: %v (fragmented packet was not reassembled and delivered)", err)
|
||
}
|
||
if got := string(buf[:n]); got != string(payload) {
|
||
t.Fatalf("payload = %q, want %q", got, payload)
|
||
}
|
||
udpAddr, ok := addr.(*net.UDPAddr)
|
||
if !ok {
|
||
t.Fatalf("remote addr = %T(%v), want *net.UDPAddr", addr, addr)
|
||
}
|
||
if got := udpAddr.AddrPort(); got != netip.MustParseAddrPort("100.64.1.3:12345") {
|
||
t.Fatalf("remote addr = %v, want 100.64.1.3:12345", got)
|
||
}
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
}
|