diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 28d5b267c..cbe1dc93c 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -9489,6 +9489,7 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) { nm := buildNetmapWithPeers(selfNode, exitA, exitB, subnetBig, subnetSmall) var eng wgengine.Engine + var curLB *LocalBackend var curT *testing.T // active subtest, for test helpers wantPeer := func(ip string, n tailcfg.NodeView) { @@ -9515,19 +9516,19 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) { wantKey := func(ip string, n tailcfg.NodeView) { t := curT t.Helper() - pk, _, ok := eng.PeerKeyForIP(netip.MustParseAddr(ip)) + pr, ok := curLB.currentNode().routeMgr.Outbound().Lookup(netip.MustParseAddr(ip)) if !ok { - t.Fatalf("PeerKeyForIP(%s): ok=false, want true", ip) + t.Fatalf("routeMgr.Outbound().Lookup(%s): ok=false, want true", ip) } - if pk != n.Key() { - t.Fatalf("PeerKeyForIP(%s): key=%v, want %v", ip, pk, n.Key()) + if pr.Key != n.Key() { + t.Fatalf("routeMgr.Outbound().Lookup(%s): key=%v, want %v", ip, pr.Key, n.Key()) } } wantNotKey := func(ip string) { t := curT t.Helper() - if _, _, ok := eng.PeerKeyForIP(netip.MustParseAddr(ip)); ok { - t.Fatalf("PeerKeyForIP(%s): ok=true, want false", ip) + if _, ok := curLB.currentNode().routeMgr.Outbound().Lookup(netip.MustParseAddr(ip)); ok { + t.Fatalf("routeMgr.Outbound().Lookup(%s): ok=true, want false", ip) } } wantSelf := func(ip string) { @@ -9663,6 +9664,7 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) { }) eng = lb.sys.Engine.Get() + curLB = lb curT = t tt.check() }) diff --git a/ipn/ipnlocal/resolve.go b/ipn/ipnlocal/resolve.go index 0b40bfc09..5a07c27e4 100644 --- a/ipn/ipnlocal/resolve.go +++ b/ipn/ipnlocal/resolve.go @@ -143,11 +143,11 @@ func (b *LocalBackend) peerForIP(ip netip.Addr) (_ wgengine.PeerForIP, ok bool) } } - pk, route, ok := b.e.PeerKeyForIP(ip) + route, pr, ok := nb.routeMgr.Outbound().LookupPrefixLPM(netip.PrefixFrom(ip, ip.BitLen())) if !ok { return wgengine.PeerForIP{}, false } - nid, ok := nb.NodeByKey(pk) + nid, ok := nb.NodeByKey(pr.Key) if !ok { return wgengine.PeerForIP{}, false } diff --git a/ipn/ipnlocal/state_test.go b/ipn/ipnlocal/state_test.go index 9b738370a..3ae8e7aa5 100644 --- a/ipn/ipnlocal/state_test.go +++ b/ipn/ipnlocal/state_test.go @@ -2019,9 +2019,6 @@ func (e *mockEngine) SetPeerForIPFunc(func(netip.Addr) (_ wgengine.PeerForIP, ok func (e *mockEngine) SetPeerConfigFunc(func(key.NodePublic) (allowedIPs []netip.Prefix, ok bool)) { } func (e *mockEngine) SyncDevicePeer(key.NodePublic) {} -func (e *mockEngine) PeerKeyForIP(netip.Addr) (_ key.NodePublic, _ netip.Prefix, ok bool) { - return key.NodePublic{}, netip.Prefix{}, false -} func (e *mockEngine) SetPeerSessionStateFunc(func(key.NodePublic, wgengine.PeerWireGuardState)) { } func (e *mockEngine) SetNetLogSource(wgengine.NetLogSource) {} diff --git a/wgengine/bench/wg.go b/wgengine/bench/wg.go index 4264ae448..1e28707e2 100644 --- a/wgengine/bench/wg.go +++ b/wgengine/bench/wg.go @@ -82,6 +82,17 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip. e1.SetFilter(filter.NewAllowAllForTest(l1)) e2.SetFilter(filter.NewAllowAllForTest(l2)) + // There is no LocalBackend in this benchmark, so install trivial + // outbound peer lookups; without one, outbound packets can't + // lazily create their WireGuard peer. + k1pub, k2pub := k1.Public(), k2.Public() + e1.SetPeerByIPPacketFunc(func(dst netip.Addr) (_ key.NodePublic, ok bool) { + return k2pub, a2.Contains(dst) + }) + e2.SetPeerByIPPacketFunc(func(dst netip.Addr) (_ key.NodePublic, ok bool) { + return k1pub, a1.Contains(dst) + }) + var wait sync.WaitGroup wait.Add(2) diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 3200dd434..57c383f50 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -18,7 +18,6 @@ "sync/atomic" "time" - "github.com/gaissmai/bart" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "go4.org/mem" @@ -104,19 +103,6 @@ type userspaceEngine struct { wgLock sync.Mutex // serializes all wgdev operations; see lock order comment below - // peerByIPRoute is a longest-prefix-match table built from - // lastCfgFull.Peers AllowedIPs. It's the slow path for - // SetPeerByIPPacketFunc, used when LocalBackend's exact-IP fast path - // (nodeByAddr) misses — i.e. for subnet routes and exit-node default - // routes. Built from lastCfgFull (the wireguard-filtered peer list) - // rather than the netmap so that exit-node selection is honored: the - // netmap has 0.0.0.0/0 in AllowedIPs for every exit-capable peer, but - // lastCfgFull only has it for the currently-selected exit node. - // - // Replaced (not mutated) by maybeReconfigWireguardLocked. Read by - // the per-packet wgdev callback without locking. - peerByIPRoute atomic.Pointer[bart.Table[key.NodePublic]] - // peerForIP, if non-nil, is the callback installed via // [userspaceEngine.SetPeerForIPFunc]. PeerForIP delegates to it // for the cold-path control lookups (Ping, TSMP, pendopen, etc). @@ -525,16 +511,6 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) e.logf("Creating WireGuard device...") e.wgdev = wgcfg.NewDevice(e.tundev, e.magicConn.Bind(), e.wgLogger.DeviceLogger) closePool.addFunc(e.wgdev.Close) - - // Install a default outbound-packet peer lookup callback. It uses only - // the engine's BART table, which is rebuilt from the wireguard-filtered - // peer list on every Reconfig. Consumers (e.g. LocalBackend) may later - // call SetPeerByIPPacketFunc to additionally install a fast path for - // exact node-address matches; the BART remains the slow-path fallback. - // Without this default, callers that don't run a LocalBackend would - // have no way to route outbound packets to peers, since peers are - // created lazily from inbound packets only via SetPeerLookupFunc. - e.SetPeerByIPPacketFunc(nil) closePool.addFunc(func() { if err := e.magicConn.Close(); err != nil { e.logf("error closing magicconn: %v", err) @@ -721,16 +697,6 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error { // against the current lookup. e.wgLogger.Invalidate() - // Rebuild the prefix-match peer routing table from the current - // (wireguard-filtered) peer list and publish it atomically. - rt := &bart.Table[key.NodePublic]{} - for _, p := range full.Peers { - for _, pfx := range p.AllowedIPs { - rt.Insert(pfx, p.PublicKey) - } - } - e.peerByIPRoute.Store(rt) - e.logf("wgengine: Reconfig: configuring userspace WireGuard config (with %d peers)", len(full.Peers)) if e.peerConfigFn.Load() != nil { // The device has a long-lived PeerLookupFunc backed by the @@ -800,29 +766,23 @@ func (e *userspaceEngine) SyncDevicePeer(k key.NodePublic) { // SetPeerByIPPacketFunc installs a callback used by wireguard-go to look up // which peer should handle an outbound packet by destination IP. // -// If fn is non-nil it is authoritative: LocalBackend's implementation -// consults both the exact node-address fast path and the RouteManager's -// outbound table (covering subnet routes and exit-node default routes), -// and stays correct under incremental netmap deltas. The engine's own -// BART table ([userspaceEngine.peerByIPRoute], rebuilt only on full -// reconfigs) is used only when no fn is installed (e.g. engines running -// without a LocalBackend). +// LocalBackend's implementation consults both the exact node-address fast +// path and the RouteManager's outbound table (covering subnet routes and +// exit-node default routes), and stays correct under incremental netmap +// deltas. // -// [NewUserspaceEngine] installs a BART-only default at engine creation time, -// so callers that don't call SetPeerByIPPacketFunc (e.g. those not running -// a LocalBackend) still get working outbound packet routing. +// A nil fn uninstalls the callback, reverting the device to its standard +// WireGuard AllowedIPs trie, which only contains peers that already exist +// in the device. Callers without a LocalBackend that need outbound packets +// to lazily create peers must install their own callback. func (e *userspaceEngine) SetPeerByIPPacketFunc(fn func(netip.Addr) (_ key.NodePublic, ok bool)) { + if fn == nil { + e.wgdev.SetPeerByIPPacketFunc(nil) + return + } e.wgdev.SetPeerByIPPacketFunc(func(_, dst netip.Addr, _ []byte) (device.NoisePublicKey, bool) { - if fn != nil { - if pk, ok := fn(dst); ok { - return pk.Raw32(), true - } - return device.NoisePublicKey{}, false - } - if rt := e.peerByIPRoute.Load(); rt != nil { - if pk, ok := rt.Lookup(dst); ok { - return pk.Raw32(), true - } + if pk, ok := fn(dst); ok { + return pk.Raw32(), true } return device.NoisePublicKey{}, false }) @@ -1632,20 +1592,6 @@ func (e *userspaceEngine) SetPeerForIPFunc(fn func(netip.Addr) (PeerForIP, bool) e.peerForIP.Store(&fn) } -// PeerKeyForIP looks up ip in the engine's AllowedIPs table -// ([userspaceEngine.peerByIPRoute]). See [Engine.PeerKeyForIP]. -func (e *userspaceEngine) PeerKeyForIP(ip netip.Addr) (pk key.NodePublic, route netip.Prefix, ok bool) { - if !ip.IsValid() { - return pk, route, false - } - rt := e.peerByIPRoute.Load() - if rt == nil { - return pk, route, false - } - route, pk, ok = rt.LookupPrefixLPM(netip.PrefixFrom(ip, ip.BitLen())) - return pk, route, ok -} - // PeerForIP returns the node responsible for handling the given IP. // It delegates to the callback installed via [SetPeerForIPFunc]; engines // without an installed callback return (zero, false). diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 573bc06d4..6b68ccfe4 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -126,18 +126,10 @@ type Engine interface { // If fn is nil, PeerForIP returns (zero, false) for every IP. // // LocalBackend installs a func backed by the live nodeBackend for - // exact-match and self addresses, with [Engine.PeerKeyForIP] - // supplying the subnet-route / exit-node fallback. + // exact-match and self addresses, with the RouteManager's outbound + // table supplying the subnet-route / exit-node fallback. SetPeerForIPFunc(fn func(netip.Addr) (_ PeerForIP, ok bool)) - // PeerKeyForIP returns the peer's NodePublic and the matched prefix - // for the longest-prefix match of ip in the engine's AllowedIPs - // table (the wireguard config most recently installed via - // [Engine.Reconfig]). Exit-node selection is honored: an unselected - // exit node's 0.0.0.0/0 is not matched. It is the same table the - // outbound packet hot path consults via [Engine.SetPeerByIPPacketFunc]. - PeerKeyForIP(netip.Addr) (_ key.NodePublic, _ netip.Prefix, ok bool) - // GetFilter returns the current packet filter, if any. GetFilter() *filter.Filter