diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 82dda280f..5df162e13 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -811,7 +811,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/ tailscale.com/net/proxymux from tailscale.com/tsnet tailscale.com/net/routecheck from tailscale.com/client/local+ tailscale.com/net/routecheck/peernode from tailscale.com/ipn/ipnlocal+ - tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal + tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock tailscale.com/net/socks5 from tailscale.com/tsnet tailscale.com/net/sockstats from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/depaware-min.txt b/cmd/tailscaled/depaware-min.txt index ab85bdd9b..56eade3f0 100644 --- a/cmd/tailscaled/depaware-min.txt +++ b/cmd/tailscaled/depaware-min.txt @@ -109,7 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/ping from tailscale.com/net/netcheck+ tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+ tailscale.com/net/routecheck/peernode from tailscale.com/ipn/ipnlocal - tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal + tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal+ tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock tailscale.com/net/sockstats from tailscale.com/control/controlclient+ tailscale.com/net/stun from tailscale.com/net/netcheck+ diff --git a/cmd/tailscaled/depaware-minbox.txt b/cmd/tailscaled/depaware-minbox.txt index 1f39aea86..3a8756781 100644 --- a/cmd/tailscaled/depaware-minbox.txt +++ b/cmd/tailscaled/depaware-minbox.txt @@ -126,7 +126,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/ping from tailscale.com/net/netcheck+ tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+ tailscale.com/net/routecheck/peernode from tailscale.com/ipn/ipnlocal - tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal + tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal+ tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock tailscale.com/net/sockstats from tailscale.com/control/controlclient+ tailscale.com/net/stun from tailscale.com/net/netcheck+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index f4f8985b0..488fe57c5 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -385,7 +385,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled tailscale.com/net/routecheck from tailscale.com/feature/routecheck+ tailscale.com/net/routecheck/peernode from tailscale.com/ipn/ipnlocal+ - tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal + tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal+ tailscale.com/net/routetable from tailscale.com/doctor/routetable 💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock+ tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled diff --git a/cmd/tsidp/depaware.txt b/cmd/tsidp/depaware.txt index 07da57287..4b307b6f9 100644 --- a/cmd/tsidp/depaware.txt +++ b/cmd/tsidp/depaware.txt @@ -210,7 +210,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar tailscale.com/net/proxymux from tailscale.com/tsnet tailscale.com/net/routecheck from tailscale.com/client/local+ tailscale.com/net/routecheck/peernode from tailscale.com/ipn/ipnlocal+ - tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal + tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock tailscale.com/net/socks5 from tailscale.com/tsnet tailscale.com/net/sockstats from tailscale.com/control/controlclient+ diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 720f1f9cb..4b60ed2f7 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -31,6 +31,7 @@ "sync/atomic" "time" + "github.com/gaissmai/bart" "go4.org/mem" "go4.org/netipx" "golang.org/x/net/dns/dnsmessage" @@ -62,6 +63,7 @@ "tailscale.com/net/netns" "tailscale.com/net/netutil" "tailscale.com/net/packet" + "tailscale.com/net/routemanager" "tailscale.com/net/traffic" "tailscale.com/net/tsaddr" "tailscale.com/net/tsdial" @@ -2516,6 +2518,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo for k := range deltaRes.ChangedAllowedIPs { b.e.SyncDevicePeer(k) } + b.setDataPlanePeerRoutes() // Reset the WireGuard session for peers whose disco key changed in // a way that indicates a restart, flushing their dead session keys; @@ -2530,11 +2533,9 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo // and its lazy peer creation reads live state via // [wgengine.Engine.SetPeerConfigFunc]. What still rides // authReconfig is everything else derived from the full peer set: - // OS routes (router.Config), the quad-100 resolver's MagicDNS - // hosts map (dnsConfigForNetmap), and tstun's per-peer config - // (masquerade addresses and jailed peers, via SetWGConfig). Once - // those become delta-aware too, this can be gated on the route - // manager's OS-routes changes and the tstun-relevant fields + // OS routes (router.Config) and the quad-100 resolver's MagicDNS + // hosts map (dnsConfigForNetmap). Once those become delta-aware + // too, this can be gated on the route manager's OS-routes changes // instead of firing on every peer change. needsAuthReconfig = needsAuthReconfig || peersUpsertedOrRemoved if needsAuthReconfig { @@ -6183,6 +6184,13 @@ func (b *LocalBackend) authReconfigLocked() { cfg.Peers[i].AllowedIPs = extras.AppendTo(cfg.Peers[i].AllowedIPs) } } + // The prefs and extras commits above can both change the outbound + // table (such as installing the selected exit node's /0 routes), + // so refresh the data plane's view of it. This must stay after + // updateRouteManagerExtras, so the pushed snapshot includes its + // commit, and before the Reconfig below, so packets flowing under + // the new config never see a stale peer table. + b.setDataPlanePeerRoutes() err = b.e.Reconfig(cfg, rcfg, dcfg) if err == wgengine.ErrNoChanges { @@ -6196,6 +6204,29 @@ func (b *LocalBackend) authReconfigLocked() { } } +// setDataPlanePeerRoutes pushes the route manager's outbound table and +// this node's native Tailscale addresses into the engine's tun-layer +// data plane, which uses them for per-packet NAT rewrites and +// jailed-filter selection. It must be called after every route manager +// commit that can change the outbound table or the set of peers with +// data-plane attributes: netmap updates (full or delta), prefs changes, +// and extra-allowed-IP changes. +// +// When no current peer is jailed or masqueraded, it installs nil, which +// keeps the data plane's per-packet fast path to a nil check. +func (b *LocalBackend) setDataPlanePeerRoutes() { + nb := b.currentNode() + var native4, native6 netip.Addr + var routes *bart.Table[*routemanager.PeerRoute] + if nb.routeMgr.HasDataPlaneAttrs() { + routes = nb.routeMgr.Outbound() + if nm := nb.NetMap(); nm != nil { + native4, native6 = tsaddr.FirstTailscaleAddrs(nm.GetAddresses().All()) + } + } + b.e.SetPeerRoutes(native4, native6, routes) +} + // shouldUseOneCGNATRoute reports whether we should prefer to make one big // CGNAT /10 route rather than a /32 per peer. // @@ -7312,6 +7343,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { login = cmp.Or(profileFromView(nm.UserProfiles[nm.User()]).LoginName, "") } discoChanged := b.currentNode().SetNetMap(nm) + b.setDataPlanePeerRoutes() if ms, ok := b.sys.MagicSock.GetOK(); ok { if nm != nil { if nm.Cached { diff --git a/ipn/ipnlocal/state_test.go b/ipn/ipnlocal/state_test.go index 14bc1ebd2..d8b045c00 100644 --- a/ipn/ipnlocal/state_test.go +++ b/ipn/ipnlocal/state_test.go @@ -17,6 +17,7 @@ "time" qt "github.com/frankban/quicktest" + "github.com/gaissmai/bart" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,6 +30,7 @@ "tailscale.com/net/dns" "tailscale.com/net/netmon" "tailscale.com/net/packet" + "tailscale.com/net/routemanager" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" "tailscale.com/tsd" @@ -1961,6 +1963,9 @@ func (e *mockEngine) SetJailedFilter(f *filter.Filter) { e.mu.Unlock() } +func (e *mockEngine) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) { +} + func (e *mockEngine) SetStatusCallback(cb wgengine.StatusCallback) { e.mu.Lock() e.statusCb = cb diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go index 1eac9eb77..e77d8a4f5 100644 --- a/net/tsaddr/tsaddr.go +++ b/net/tsaddr/tsaddr.go @@ -7,6 +7,7 @@ import ( "encoding/binary" "errors" + "iter" "net/netip" "slices" "sync" @@ -317,3 +318,24 @@ func MapVia(siteID uint32, v4 netip.Prefix) (via netip.Prefix, err error) { copy(a[12:], ip4a[:]) return netip.PrefixFrom(netip.AddrFrom16(a), v4.Bits()+64+32), nil } + +// FirstTailscaleAddrs returns the first Tailscale IPv4 address and +// the first Tailscale IPv6 address among the addresses of addrs' +// prefixes, if any. The addrs sequence is typically a node's own +// address list, either a slice (via [slices.All]) or a view (via +// [views.Slice.All]). +func FirstTailscaleAddrs(addrs iter.Seq2[int, netip.Prefix]) (a4, a6 netip.Addr) { + for _, pfx := range addrs { + a := pfx.Addr() + switch { + case a.Is4() && !a4.IsValid() && IsTailscaleIP(a): + a4 = a + case a.Is6() && !a6.IsValid() && IsTailscaleIP(a): + a6 = a + } + if a4.IsValid() && a6.IsValid() { + break + } + } + return a4, a6 +} diff --git a/net/tsaddr/tsaddr_test.go b/net/tsaddr/tsaddr_test.go index ac5a07fff..524d5a65b 100644 --- a/net/tsaddr/tsaddr_test.go +++ b/net/tsaddr/tsaddr_test.go @@ -5,6 +5,7 @@ import ( "net/netip" + "slices" "testing" "github.com/google/go-cmp/cmp" @@ -290,3 +291,27 @@ func TestIsTailscaleIP(t *testing.T) { } } } + +func TestFirstTailscaleAddrs(t *testing.T) { + pfx := func(s string) netip.Prefix { return netip.MustParsePrefix(s) } + addr := func(s string) netip.Addr { return netip.MustParseAddr(s) } + tests := []struct { + name string + in []netip.Prefix + want4 netip.Addr + want6 netip.Addr + }{ + {"empty", nil, netip.Addr{}, netip.Addr{}}, + {"both", []netip.Prefix{pfx("100.64.0.1/32"), pfx("fd7a:115c:a1e0::1/128")}, addr("100.64.0.1"), addr("fd7a:115c:a1e0::1")}, + {"first-of-each", []netip.Prefix{pfx("100.64.0.1/32"), pfx("100.64.0.2/32"), pfx("fd7a:115c:a1e0::1/128"), pfx("fd7a:115c:a1e0::2/128")}, addr("100.64.0.1"), addr("fd7a:115c:a1e0::1")}, + {"non-tailscale-skipped", []netip.Prefix{pfx("192.168.1.1/32"), pfx("2001:db8::1/128"), pfx("100.64.0.9/32")}, addr("100.64.0.9"), netip.Addr{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got4, got6 := FirstTailscaleAddrs(slices.All(tt.in)) + if got4 != tt.want4 || got6 != tt.want6 { + t.Errorf("FirstTailscaleAddrs = %v, %v; want %v, %v", got4, got6, tt.want4, tt.want6) + } + }) + } +} diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index dbaabdc0b..e1d568b22 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -9,7 +9,6 @@ "io" "net/netip" "os" - "reflect" "runtime" "slices" "strings" @@ -28,6 +27,7 @@ "tailscale.com/feature/buildfeatures" "tailscale.com/net/packet" "tailscale.com/net/packet/checksum" + "tailscale.com/net/routemanager" "tailscale.com/net/tsaddr" "tailscale.com/syncs" "tailscale.com/tstime/mono" @@ -565,31 +565,9 @@ func (pc *peerConfigTable) dnat(p *packet.Parsed) { } } -// findV4 returns the first Tailscale IPv4 address in addrs. -func findV4(addrs []netip.Prefix) netip.Addr { - for _, ap := range addrs { - a := ap.Addr() - if a.Is4() && tsaddr.IsTailscaleIP(a) { - return a - } - } - return netip.Addr{} -} - -// findV6 returns the first Tailscale IPv6 address in addrs. -func findV6(addrs []netip.Prefix) netip.Addr { - for _, ap := range addrs { - a := ap.Addr() - if a.Is6() && tsaddr.IsTailscaleIP(a) { - return a - } - } - return netip.Addr{} -} - -// peerConfigTable contains configuration for individual peers and related -// information necessary to perform peer-specific operations. It should be -// treated as immutable. +// peerConfigTable contains the per-peer route attributes and related +// information necessary to perform peer-specific operations. It should +// be treated as immutable. // // The nil value is a valid configuration. type peerConfigTable struct { @@ -602,57 +580,17 @@ type peerConfigTable struct { // inbound packet is IPv6. nativeAddr4, nativeAddr6 netip.Addr - // byIP contains configuration for each peer, indexed by a peer's IP - // address(es). - byIP bart.Table[*peerConfig] - - // masqAddrCounts is a count of peers by MasqueradeAsIP. - // TODO? for logging - masqAddrCounts map[netip.Addr]int -} - -// peerConfig is the configuration for a single peer. -type peerConfig struct { - // dstMasqAddr{4,6} are the addresses that should be used as the - // source address when masquerading packets to this peer (i.e. - // SNAT). If an address is not valid, the packet should not be - // masqueraded for that address family. - dstMasqAddr4 netip.Addr - dstMasqAddr6 netip.Addr - - // jailed is whether this peer is "jailed" (i.e. is restricted from being - // able to initiate connections to this node). This is the case for shared - // nodes. - jailed bool + // byIP maps a peer's IP addresses and routed prefixes to the + // peer's route attributes. It is a shared immutable snapshot + // from [routemanager.RouteManager.Outbound]. + byIP *bart.Table[*routemanager.PeerRoute] } func (c *peerConfigTable) String() string { if c == nil { return "peerConfigTable(nil)" } - var b strings.Builder - b.WriteString("peerConfigTable{") - fmt.Fprintf(&b, "nativeAddr4: %v, ", c.nativeAddr4) - fmt.Fprintf(&b, "nativeAddr6: %v, ", c.nativeAddr6) - - // TODO: figure out how to iterate/debug/print c.byIP - - b.WriteString("}") - - return b.String() -} - -func (c *peerConfig) String() string { - if c == nil { - return "peerConfig(nil)" - } - var b strings.Builder - b.WriteString("peerConfig{") - fmt.Fprintf(&b, "dstMasqAddr4: %v, ", c.dstMasqAddr4) - fmt.Fprintf(&b, "dstMasqAddr6: %v, ", c.dstMasqAddr6) - fmt.Fprintf(&b, "jailed: %v}", c.jailed) - - return b.String() + return fmt.Sprintf("peerConfigTable{nativeAddr4: %v, nativeAddr6: %v}", c.nativeAddr4, c.nativeAddr6) } // mapDstIP returns the destination IP to use for a packet to dst. @@ -675,10 +613,10 @@ func (pc *peerConfigTable) mapDstIP(src, oldDst netip.Addr) netip.Addr { return oldDst } - if oldDst.Is4() && pc.nativeAddr4.IsValid() && c.dstMasqAddr4 == oldDst { + if oldDst.Is4() && pc.nativeAddr4.IsValid() && c.MasqAddr4 == oldDst { return pc.nativeAddr4 } - if oldDst.Is6() && pc.nativeAddr6.IsValid() && c.dstMasqAddr6 == oldDst { + if oldDst.Is6() && pc.nativeAddr6.IsValid() && c.MasqAddr6 == oldDst { return pc.nativeAddr6 } return oldDst @@ -708,105 +646,17 @@ func (pc *peerConfigTable) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr { // Perform SNAT based on the address family and whether we have a valid // addr. - if oldSrc.Is4() && c.dstMasqAddr4.IsValid() { - return c.dstMasqAddr4 + if oldSrc.Is4() && c.MasqAddr4.IsValid() { + return c.MasqAddr4 } - if oldSrc.Is6() && c.dstMasqAddr6.IsValid() { - return c.dstMasqAddr6 + if oldSrc.Is6() && c.MasqAddr6.IsValid() { + return c.MasqAddr6 } // No SNAT; use old src return oldSrc } -// peerConfigTableFromWGConfig generates a peerConfigTable from nm. If NAT is -// not required, and no additional configuration is present, it returns nil. -func peerConfigTableFromWGConfig(wcfg *wgcfg.Config) *peerConfigTable { - if wcfg == nil { - return nil - } - - nativeAddr4 := findV4(wcfg.Addresses) - nativeAddr6 := findV6(wcfg.Addresses) - if !nativeAddr4.IsValid() && !nativeAddr6.IsValid() { - return nil - } - - ret := &peerConfigTable{ - nativeAddr4: nativeAddr4, - nativeAddr6: nativeAddr6, - masqAddrCounts: make(map[netip.Addr]int), - } - - // When using an exit node that requires masquerading, we need to - // fill out the routing table with all peers not just the ones that - // require masquerading. - exitNodeRequiresMasq := false // true if using an exit node and it requires masquerading - for _, p := range wcfg.Peers { - isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6()) - if isExitNode { - hasMasqAddr := false || - (p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) || - (p.V6MasqAddr != nil && p.V6MasqAddr.IsValid()) - if hasMasqAddr { - exitNodeRequiresMasq = true - } - break - } - } - - byIPSize := 0 - for i := range wcfg.Peers { - p := &wcfg.Peers[i] - - // Build a routing table that configures DNAT (i.e. changing - // the V4MasqAddr/V6MasqAddr for a given peer to the current - // peer's v4/v6 IP). - var addrToUse4, addrToUse6 netip.Addr - if p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() { - addrToUse4 = *p.V4MasqAddr - ret.masqAddrCounts[addrToUse4]++ - } - if p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() { - addrToUse6 = *p.V6MasqAddr - ret.masqAddrCounts[addrToUse6]++ - } - - // If the exit node requires masquerading, set the masquerade - // addresses to our native addresses. - if exitNodeRequiresMasq { - if !addrToUse4.IsValid() && nativeAddr4.IsValid() { - addrToUse4 = nativeAddr4 - } - if !addrToUse6.IsValid() && nativeAddr6.IsValid() { - addrToUse6 = nativeAddr6 - } - } - - if !addrToUse4.IsValid() && !addrToUse6.IsValid() && !p.IsJailed { - // NAT not required for this peer. - continue - } - - // Use the same peer configuration for each address of the peer. - pc := &peerConfig{ - dstMasqAddr4: addrToUse4, - dstMasqAddr6: addrToUse6, - jailed: p.IsJailed, - } - - // Insert an entry into our routing table for each allowed IP. - for _, ip := range p.AllowedIPs { - ret.byIP.Insert(ip, pc) - byIPSize++ - } - } - if byIPSize == 0 && len(ret.masqAddrCounts) == 0 { - return nil - } - return ret -} - func (pc *peerConfigTable) inboundPacketIsJailed(p *packet.Parsed) bool { if pc == nil { return false @@ -815,7 +665,7 @@ func (pc *peerConfigTable) inboundPacketIsJailed(p *packet.Parsed) bool { if !ok { return false } - return c.jailed + return c.Jailed } func (pc *peerConfigTable) outboundPacketIsJailed(p *packet.Parsed) bool { @@ -826,7 +676,7 @@ func (pc *peerConfigTable) outboundPacketIsJailed(p *packet.Parsed) bool { if !ok { return false } - return c.jailed + return c.Jailed } // SetIPer is the interface expected to be implemented by the TAP implementation @@ -836,16 +686,53 @@ type SetIPer interface { SetIP(ipV4, ipV6 netip.Addr) error } -// SetWGConfig is called when a new NetworkMap is received. +// SetWGConfig is called when a new NetworkMap is received. Its only +// remaining job is updating the TAP device's IP addresses; the +// per-peer route attributes arrive via [Wrapper.SetPeerRoutes]. func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) { if t.isTAP { if sip, ok := t.tdev.(SetIPer); ok { - sip.SetIP(findV4(wcfg.Addresses), findV6(wcfg.Addresses)) + sip.SetIP(tsaddr.FirstTailscaleAddrs(slices.All(wcfg.Addresses))) } } - cfg := peerConfigTableFromWGConfig(wcfg) +} + +// SetPeerRoutes is called whenever this node's Tailscale addresses or +// the route manager's outbound table change. native4 and native6 are +// this node's own Tailscale addresses, and routes maps each peer's +// addresses and routed prefixes to its route attributes. +// +// A nil routes table disables all per-packet peer processing (NAT +// rewrites and jailed-filter selection); callers pass nil when no +// current peer has any such attributes, which keeps the common +// per-packet path to a nil check. +// +// Unchanged values are a cheap no-op, so callers can call it +// unconditionally whenever the inputs might have changed: the routes +// table is an immutable snapshot whose pointer identity means its +// contents are unchanged. +func (t *Wrapper) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) { + var cfg *peerConfigTable + if routes != nil && (native4.IsValid() || native6.IsValid()) { + if old := t.peerConfig.Load(); old != nil && + old.byIP == routes && old.nativeAddr4 == native4 && old.nativeAddr6 == native6 { + return + } + cfg = &peerConfigTable{ + nativeAddr4: native4, + nativeAddr6: native6, + byIP: routes, + } + } else if t.peerConfig.Load() == nil { + // Uninstalling (cfg stays nil) over an already-nil config; + // skip the Swap and its transition logging below. + return + } old := t.peerConfig.Swap(cfg) - if !reflect.DeepEqual(old, cfg) { + // Log only on nil-ness or native address transitions; the routes + // table changes with every routing update and is too chatty to log. + if (old == nil) != (cfg == nil) || + (old != nil && (old.nativeAddr4 != cfg.nativeAddr4 || old.nativeAddr6 != cfg.nativeAddr6)) { t.logf("peer config: %v", cfg) } } diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go index 631dc083e..d6a5fcd91 100644 --- a/net/tstun/wrap_test.go +++ b/net/tstun/wrap_test.go @@ -18,6 +18,7 @@ "unicode" "unsafe" + "github.com/gaissmai/bart" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/tailscale/wireguard-go/tun/tuntest" @@ -29,6 +30,7 @@ "tailscale.com/net/netaddr" "tailscale.com/net/packet" "tailscale.com/net/packet/checksum" + "tailscale.com/net/routemanager" "tailscale.com/tstest" "tailscale.com/tstime/mono" "tailscale.com/types/ipproto" @@ -42,7 +44,6 @@ "tailscale.com/util/usermetric" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/netstack/gro" - "tailscale.com/wgengine/wgcfg" ) func udp4(src, dst string, sport, dport uint16) []byte { @@ -710,22 +711,43 @@ func TestFilterDiscoLoop(t *testing.T) { } // TODO(andrew-d): refactor this test to no longer use addrFam, after #11945 -// removed it in peerConfigFromWGConfig func TestPeerCfg_NAT(t *testing.T) { - node := func(ip, masqIP netip.Addr, otherAllowedIPs ...netip.Prefix) wgcfg.Peer { - p := wgcfg.Peer{ - PublicKey: key.NewNode().Public(), - AllowedIPs: []netip.Prefix{ - netip.PrefixFrom(ip, ip.BitLen()), - }, + type testPeer struct { + pr *routemanager.PeerRoute + pfxs []netip.Prefix + } + node := func(ip, masqIP netip.Addr, otherAllowedIPs ...netip.Prefix) testPeer { + pr := &routemanager.PeerRoute{Key: key.NewNode().Public()} + switch { + case masqIP.Is4(): + pr.MasqAddr4 = masqIP + case masqIP.Is6(): + pr.MasqAddr6 = masqIP } - if masqIP.Is4() { - p.V4MasqAddr = new(masqIP) + return testPeer{ + pr: pr, + pfxs: append([]netip.Prefix{netip.PrefixFrom(ip, ip.BitLen())}, otherAllowedIPs...), + } + } + // peerCfgTable builds the peerConfigTable under test from peers, + // as SetPeerRoutes would from the route manager's outbound table. + // A nil peers means no table, matching an uninstalled config. + peerCfgTable := func(selfNativeIP netip.Addr, peers []testPeer) *peerConfigTable { + if peers == nil { + return nil + } + pcfg := &peerConfigTable{byIP: &bart.Table[*routemanager.PeerRoute]{}} + if selfNativeIP.Is4() { + pcfg.nativeAddr4 = selfNativeIP } else { - p.V6MasqAddr = new(masqIP) + pcfg.nativeAddr6 = selfNativeIP } - p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...) - return p + for _, p := range peers { + for _, pfx := range p.pfxs { + pcfg.byIP.Insert(pfx, p.pr) + } + } + return pcfg } test := func(addrFam ipproto.Version) { var ( @@ -734,7 +756,6 @@ func TestPeerCfg_NAT(t *testing.T) { selfNativeIP = netip.MustParseAddr("100.64.0.1") selfEIP1 = netip.MustParseAddr("100.64.1.1") selfEIP2 = netip.MustParseAddr("100.64.1.2") - selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())} peer1IP = netip.MustParseAddr("100.64.0.2") peer2IP = netip.MustParseAddr("100.64.0.3") @@ -749,7 +770,6 @@ func TestPeerCfg_NAT(t *testing.T) { selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a") selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a") selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b") - selfAddrs = []netip.Prefix{netip.PrefixFrom(selfNativeIP, selfNativeIP.BitLen())} peer1IP = netip.MustParseAddr("fd7a:115c:a1e0::b") peer2IP = netip.MustParseAddr("fd7a:115c:a1e0::c") @@ -769,13 +789,13 @@ type dnatTest struct { tests := []struct { name string - wcfg *wgcfg.Config + peers []testPeer // nil means no config at all snatMap map[netip.Addr]netip.Addr // dst -> src dnat []dnatTest }{ { - name: "no-cfg", - wcfg: nil, + name: "no-cfg", + peers: nil, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfNativeIP, peer2IP: selfNativeIP, @@ -789,12 +809,9 @@ type dnatTest struct { }, { name: "single-peer-requires-nat", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, noIP), - node(peer2IP, selfEIP2), - }, + peers: []testPeer{ + node(peer1IP, noIP), + node(peer2IP, selfEIP2), }, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfNativeIP, @@ -810,12 +827,9 @@ type dnatTest struct { }, { name: "multiple-peers-require-nat", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, selfEIP1), - node(peer2IP, selfEIP2), - }, + peers: []testPeer{ + node(peer1IP, selfEIP1), + node(peer2IP, selfEIP2), }, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfEIP1, @@ -831,12 +845,9 @@ type dnatTest struct { }, { name: "multiple-peers-require-nat-with-subnet", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, selfEIP1), - node(peer2IP, selfEIP2, subnet), - }, + peers: []testPeer{ + node(peer1IP, selfEIP1), + node(peer2IP, selfEIP2, subnet), }, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfEIP1, @@ -852,12 +863,9 @@ type dnatTest struct { }, { name: "multiple-peers-require-nat-with-default-route", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, selfEIP1), - node(peer2IP, selfEIP2, exitRoute), - }, + peers: []testPeer{ + node(peer1IP, selfEIP1), + node(peer2IP, selfEIP2, exitRoute), }, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfEIP1, @@ -873,12 +881,9 @@ type dnatTest struct { }, { name: "no-nat", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, noIP), - node(peer2IP, noIP), - }, + peers: []testPeer{ + node(peer1IP, noIP), + node(peer2IP, noIP), }, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfNativeIP, @@ -894,12 +899,9 @@ type dnatTest struct { }, { name: "exit-node-require-nat-peer-doesnt", - wcfg: &wgcfg.Config{ - Addresses: selfAddrs, - Peers: []wgcfg.Peer{ - node(peer1IP, noIP), - node(peer2IP, selfEIP2, exitRoute), - }, + peers: []testPeer{ + node(peer1IP, noIP), + node(peer2IP, selfEIP2, exitRoute), }, snatMap: map[netip.Addr]netip.Addr{ peer1IP: selfNativeIP, @@ -916,7 +918,7 @@ type dnatTest struct { for _, tc := range tests { t.Run(fmt.Sprintf("%v/%v", addrFam, tc.name), func(t *testing.T) { - pcfg := peerConfigTableFromWGConfig(tc.wcfg) + pcfg := peerCfgTable(selfNativeIP, tc.peers) for peer, want := range tc.snatMap { if got := pcfg.selectSrcIP(selfNativeIP, peer); got != want { t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want) @@ -1154,3 +1156,60 @@ func TestInjectedReadCallsAppConnectorHook(t *testing.T) { t.Errorf("packet mismatch\nwant:\t% x\ngot:\t% x", wantPkt, gotPkt) } } + +// Tests SetPeerRoutes's fast path: unchanged inputs (including the +// nil-table case) must not replace the stored config, relying on the +// routes table's immutable-snapshot pointer identity, so callers can +// call it unconditionally on every routing update. +func TestSetPeerRoutesFastPath(t *testing.T) { + bus := eventbustest.NewBus(t) + _, tun := newFakeTUN(t.Logf, bus, false) + defer tun.Close() + + // Installing nil over nil is a no-op. + tun.SetPeerRoutes(netip.Addr{}, netip.Addr{}, nil) + if got := tun.peerConfig.Load(); got != nil { + t.Fatalf("peerConfig after nil install = %v; want nil", got) + } + + native4 := netip.MustParseAddr("100.64.0.1") + native6 := netip.MustParseAddr("fd7a:115c:a1e0::1") + routes := &bart.Table[*routemanager.PeerRoute]{} + routes.Insert(netip.MustParsePrefix("100.64.0.2/32"), &routemanager.PeerRoute{Jailed: true}) + + tun.SetPeerRoutes(native4, native6, routes) + installed := tun.peerConfig.Load() + if installed == nil { + t.Fatal("peerConfig = nil; want non-nil") + } + + // Same inputs: the stored config must be untouched. + tun.SetPeerRoutes(native4, native6, routes) + if got := tun.peerConfig.Load(); got != installed { + t.Errorf("peerConfig replaced on unchanged inputs") + } + + // A new snapshot pointer installs a new config. + routes2 := routes.InsertPersist(netip.MustParsePrefix("100.64.0.3/32"), &routemanager.PeerRoute{Jailed: true}) + tun.SetPeerRoutes(native4, native6, routes2) + second := tun.peerConfig.Load() + if second == installed { + t.Error("peerConfig not replaced on changed routes table") + } + + // Changed native address installs a new config too. + tun.SetPeerRoutes(netip.MustParseAddr("100.64.0.9"), native6, routes2) + if got := tun.peerConfig.Load(); got == second { + t.Error("peerConfig not replaced on changed native address") + } + + // Dropping back to nil works and is then a stable no-op. + tun.SetPeerRoutes(netip.Addr{}, netip.Addr{}, nil) + if got := tun.peerConfig.Load(); got != nil { + t.Fatalf("peerConfig after uninstall = %v; want nil", got) + } + tun.SetPeerRoutes(netip.Addr{}, netip.Addr{}, nil) + if got := tun.peerConfig.Load(); got != nil { + t.Fatalf("peerConfig after second uninstall = %v; want nil", got) + } +} diff --git a/tsnet/depaware.txt b/tsnet/depaware.txt index 2b1f8a27f..53304f583 100644 --- a/tsnet/depaware.txt +++ b/tsnet/depaware.txt @@ -206,7 +206,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware) tailscale.com/net/proxymux from tailscale.com/tsnet tailscale.com/net/routecheck from tailscale.com/client/local+ tailscale.com/net/routecheck/peernode from tailscale.com/ipn/ipnlocal+ - tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal + tailscale.com/net/routemanager from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock tailscale.com/net/socks5 from tailscale.com/tsnet tailscale.com/net/sockstats from tailscale.com/control/controlclient+ diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 69d64b43f..4286b6028 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -54,8 +54,10 @@ "tailscale.com/net/netmon" "tailscale.com/net/packet" "tailscale.com/net/ping" + "tailscale.com/net/routemanager" "tailscale.com/net/stun" "tailscale.com/net/stun/stuntest" + "tailscale.com/net/tsaddr" "tailscale.com/net/tstun" "tailscale.com/tailcfg" "tailscale.com/tstest" @@ -244,9 +246,31 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, ln nettype.PacketListe } } -func (s *magicStack) Reconfig(cfg *wgcfg.Config) error { +// Reconfig applies cfg to the stack's device and tun layer. peers, +// if non-nil, are the tailcfg nodes that cfg was derived from; the +// tun layer's per-peer data-plane attributes (masquerade addresses, +// jailed classification) are derived from them with a real +// [routemanager.RouteManager], exactly as LocalBackend does in +// production, so this helper cannot drift from the production +// derivation. Tests whose hand-built configs carry no such +// attributes may pass nil. +func (s *magicStack) Reconfig(cfg *wgcfg.Config, peers []tailcfg.NodeView) error { s.tsTun.SetWGConfig(cfg) + if peers != nil { + rm := routemanager.New(nil) + mut := rm.Begin() + // Mirror the netmap.AllowSubnetRoutes flag the tests pass to + // nmcfg.WGCfg. + mut.SetPrefs(routemanager.Prefs{RouteAll: true}) + for _, n := range peers { + mut.UpsertPeer(n) + } + mut.Commit() + native4, native6 := tsaddr.FirstTailscaleAddrs(slices.All(cfg.Addresses)) + s.tsTun.SetPeerRoutes(native4, native6, rm.Outbound()) + } + // In production, LocalBackend installs a PeerByIPPacketFunc via // Engine.SetPeerByIPPacketFunc. Tests that bypass LocalBackend need // to install one here for outbound packet routing. @@ -373,7 +397,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM // blow up. Shouldn't happen anyway. panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err)) } - if err := m.Reconfig(wg); err != nil { + if err := m.Reconfig(wg, nm.Peers); err != nil { if ctx.Err() != nil || errors.Is(err, errConnClosed) { // shutdown race, don't care. return @@ -1199,10 +1223,10 @@ func testTwoDevicePing(t *testing.T, d *devices) { }, } - if err := m1.Reconfig(m1cfg); err != nil { + if err := m1.Reconfig(m1cfg, nil); err != nil { t.Fatal(err) } - if err := m2.Reconfig(m2cfg); err != nil { + if err := m2.Reconfig(m2cfg, nil); err != nil { t.Fatal(err) } @@ -1326,7 +1350,7 @@ func testTwoDevicePing(t *testing.T, d *devices) { t.Run("no-op-dev1-reconfig", func(t *testing.T) { setT(t) defer setT(outerT) - if err := m1.Reconfig(m1cfg); err != nil { + if err := m1.Reconfig(m1cfg, nil); err != nil { t.Fatal(err) } ping1(t) @@ -2525,7 +2549,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) { if err != nil { t.Fatal(err) } - m.Reconfig(cfg) + m.Reconfig(cfg, nm.Peers) pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr()) m.tun.Outbound <- pbuf @@ -2586,7 +2610,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) { if err != nil { t.Fatal(err) } - m.Reconfig(cfg) + m.Reconfig(cfg, nm.Peers) pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr()) m.tun.Outbound <- pbuf @@ -2627,7 +2651,7 @@ func applyNetworkMap(t *testing.T, m *magicStack, nm *netmap.NetworkMap) { t.Fatal(err) } // Apply the wireguard config to the tailscale internal wireguard device. - if err := m.Reconfig(cfg); err != nil { + if err := m.Reconfig(cfg, nm.Peers); err != nil { t.Fatal(err) } } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 258f79c40..f36d16620 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -18,6 +18,7 @@ "sync/atomic" "time" + "github.com/gaissmai/bart" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "go4.org/mem" @@ -33,6 +34,7 @@ "tailscale.com/net/ipset" "tailscale.com/net/netmon" "tailscale.com/net/packet" + "tailscale.com/net/routemanager" "tailscale.com/net/sockstats" "tailscale.com/net/tsdial" "tailscale.com/net/tstun" @@ -1006,6 +1008,10 @@ func (e *userspaceEngine) SetJailedFilter(filt *filter.Filter) { e.tundev.SetJailedFilter(filt) } +func (e *userspaceEngine) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) { + e.tundev.SetPeerRoutes(native4, native6, routes) +} + func (e *userspaceEngine) SetStatusCallback(cb StatusCallback) { e.mu.Lock() defer e.mu.Unlock() diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 774e1c1aa..652774756 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -9,9 +9,11 @@ "net/netip" "time" + "github.com/gaissmai/bart" "tailscale.com/ipn/ipnstate" "tailscale.com/net/dns" "tailscale.com/net/packet" + "tailscale.com/net/routemanager" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/netmap" @@ -125,6 +127,17 @@ type Engine interface { // SetJailedFilter updates the packet filter for jailed nodes. SetJailedFilter(*filter.Filter) + // SetPeerRoutes updates the per-peer route attributes used by the + // tun-layer data plane for per-packet NAT rewrites and + // jailed-filter selection. native4 and native6 are this node's own + // Tailscale addresses, and routes maps each peer's addresses and + // routed prefixes to its attributes; it is a shared immutable + // snapshot from [routemanager.RouteManager.Outbound]. + // + // A nil routes table disables all per-packet peer processing; + // callers pass nil when no current peer has any such attributes. + SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) + // SetStatusCallback sets the function to call when the // WireGuard status changes. SetStatusCallback(StatusCallback)