mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
a45c9f982a
The macOS client was forgetting to call netstack.Impl.SetLocalBackend. Change the API so that it can't be started without one, eliminating this class of bug. Then update all the callers. Updates #6764 Change-Id: I2b3a4f31fdfd9fdbbbbfe25a42db0c505373562f Signed-off-by: Claire Wang <claire@tailscale.com> Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com> Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
468 lines
12 KiB
Go
468 lines
12 KiB
Go
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package netstack
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/ipn/store/mem"
|
|
"tailscale.com/net/packet"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/net/tsdial"
|
|
"tailscale.com/net/tstun"
|
|
"tailscale.com/types/ipproto"
|
|
"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...)
|
|
}
|
|
}
|
|
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
|
Tun: tunDev,
|
|
Dialer: dialer,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer eng.Close()
|
|
ig, ok := eng.(wgengine.InternalsGetter)
|
|
if !ok {
|
|
t.Fatal("not an InternalsGetter")
|
|
}
|
|
tunWrap, magicSock, dns, ok := ig.GetInternals()
|
|
if !ok {
|
|
t.Fatal("failed to get internals")
|
|
}
|
|
|
|
lb, err := ipnlocal.NewLocalBackend(logf, "logid", new(mem.Store), "", dialer, eng, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ns, err := Create(logf, tunWrap, eng, magicSock, dialer, dns)
|
|
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 i := 0; i < N; i++ {
|
|
outcome := ns.injectInbound(pkt, tunWrap)
|
|
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(t *testing.T, config func(*Impl)) *Impl {
|
|
tunDev := tstun.NewFake()
|
|
dialer := new(tsdial.Dialer)
|
|
logf := func(format string, args ...any) {
|
|
if !t.Failed() {
|
|
t.Helper()
|
|
t.Logf(format, args...)
|
|
}
|
|
}
|
|
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
|
Tun: tunDev,
|
|
Dialer: dialer,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { eng.Close() })
|
|
ig, ok := eng.(wgengine.InternalsGetter)
|
|
if !ok {
|
|
t.Fatal("not an InternalsGetter")
|
|
}
|
|
tunWrap, magicSock, dns, ok := ig.GetInternals()
|
|
if !ok {
|
|
t.Fatal("failed to get internals")
|
|
}
|
|
|
|
ns, err := Create(logf, tunWrap, eng, magicSock, dialer, dns)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { ns.Close() })
|
|
|
|
lb, err := ipnlocal.NewLocalBackend(logf, "logid", new(mem.Store), "", dialer, eng, 0)
|
|
if err != nil {
|
|
t.Fatalf("NewLocalBackend: %v", err)
|
|
}
|
|
|
|
ns.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return true })
|
|
if config != nil {
|
|
config(ns)
|
|
}
|
|
if err := ns.Start(lb); err != nil {
|
|
t.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{
|
|
LegacyMigrationPrefs: 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{
|
|
LegacyMigrationPrefs: 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{
|
|
LegacyMigrationPrefs: 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{
|
|
LegacyMigrationPrefs: 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{
|
|
LegacyMigrationPrefs: prefs,
|
|
})
|
|
|
|
// Set the PeerAPI port to the Dst port above.
|
|
i.peerapiPort4Atomic.Store(5555)
|
|
i.peerapiPort6Atomic.Store(5555)
|
|
},
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|