Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211// 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/3762func 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) })
// The dialer runs on netstack-owned goroutines that can outlive the // test, so it must not log via tb directly. (tailscale/tailscale#21052) logf := tstest.WhileTestRunningLogger(tb)
gotConn := make(chan struct{}, 1) fn := func(ctx context.Context, network, address string) (net.Conn, error) { // Signal that we have a new connection 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(): logf("context done") case <-done: 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. #11304func 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) } }) }}
func alwaysOutboundToWireGuard(*stack.PacketBuffer) outboundQueue { return outboundToWireGuard}
// 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, alwaysOutboundToWireGuard)
// 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, alwaysOutboundToWireGuard) // 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, alwaysOutboundToWireGuard) 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 [linkEndpoint] routes self-addressed packets// to the loopback queue and that [Impl.injectLoopback] delivers them back into// gVisor. 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), }) defer pkt.DecRef() 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
var pkts stack.PacketBufferList pkts.PushBack(pkt) n, tcpipErr := ns.linkEP.WritePackets(pkts) if tcpipErr != nil { t.Fatalf("WritePackets: %v", tcpipErr) } if n != 1 { t.Fatalf("WritePackets wrote %d packets, want 1", n) }
// [linkEndpoint.WritePackets] should detect the self-addressed packet via // [Impl.outboundQueueForPacket], place it on the [outboundLoopback] queue, // which [Impl.injectLoopback] should deliver back into gVisor via // [linkEndpoint.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") }}