diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c4bb293ec..aed1f70fa 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" @@ -2510,6 +2512,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo for k := range 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; @@ -2524,11 +2527,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 { @@ -6177,6 +6178,10 @@ 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. + b.setDataPlanePeerRoutes() err = b.e.Reconfig(cfg, rcfg, dcfg) if err == wgengine.ErrNoChanges { @@ -6190,6 +6195,37 @@ 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 { + for _, pfx := range nm.GetAddresses().All() { + a := pfx.Addr() + switch { + case a.Is4() && !native4.IsValid() && tsaddr.IsTailscaleIP(a): + native4 = a + case a.Is6() && !native6.IsValid() && tsaddr.IsTailscaleIP(a): + native6 = a + } + } + } + } + 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. // @@ -7306,6 +7342,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 47cf595ed..d96ebb959 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" @@ -1973,6 +1975,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/tstun/wrap.go b/net/tstun/wrap.go index dbaabdc0b..7107fa592 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -9,9 +9,7 @@ "io" "net/netip" "os" - "reflect" "runtime" - "slices" "strings" "sync" "sync/atomic" @@ -28,6 +26,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" @@ -587,9 +586,9 @@ func findV6(addrs []netip.Prefix) netip.Addr { 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 +601,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 +634,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 +667,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 +686,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 +697,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 +707,40 @@ 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)) } } - 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. +func (t *Wrapper) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) { + var cfg *peerConfigTable + if routes != nil && (native4.IsValid() || native6.IsValid()) { + cfg = &peerConfigTable{ + nativeAddr4: native4, + nativeAddr6: native6, + byIP: routes, + } + } 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..7fd4f2ce9 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,23 @@ 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) - } else { - p.V6MasqAddr = new(masqIP) + return testPeer{ + pr: pr, + pfxs: append([]netip.Prefix{netip.PrefixFrom(ip, ip.BitLen())}, otherAllowedIPs...), } - p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...) - return p } test := func(addrFam ipproto.Version) { var ( @@ -734,7 +736,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 +750,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 +769,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 +789,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 +807,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 +825,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 +843,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 +861,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 +879,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 +898,20 @@ type dnatTest struct { for _, tc := range tests { t.Run(fmt.Sprintf("%v/%v", addrFam, tc.name), func(t *testing.T) { - pcfg := peerConfigTableFromWGConfig(tc.wcfg) + var pcfg *peerConfigTable + if tc.peers != nil { + pcfg = &peerConfigTable{byIP: &bart.Table[*routemanager.PeerRoute]{}} + if selfNativeIP.Is4() { + pcfg.nativeAddr4 = selfNativeIP + } else { + pcfg.nativeAddr6 = selfNativeIP + } + for _, p := range tc.peers { + for _, pfx := range p.pfxs { + pcfg.byIP.Insert(pfx, p.pr) + } + } + } 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) diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 69d64b43f..c6b187c68 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -33,6 +33,7 @@ "unsafe" qt "github.com/frankban/quicktest" + "github.com/gaissmai/bart" "github.com/google/go-cmp/cmp" wgconn "github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/device" @@ -54,6 +55,7 @@ "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/tstun" @@ -247,6 +249,35 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, ln nettype.PacketListe func (s *magicStack) Reconfig(cfg *wgcfg.Config) error { s.tsTun.SetWGConfig(cfg) + // In production, LocalBackend feeds the tun-layer data plane its + // per-peer route attributes from the route manager. Tests that + // bypass LocalBackend need to build the table from cfg.Peers here + // so that masquerade rewrites and jailed classification work. + tbl := &bart.Table[*routemanager.PeerRoute]{} + for _, p := range cfg.Peers { + pr := &routemanager.PeerRoute{Key: p.PublicKey, Jailed: p.IsJailed} + if p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() { + pr.MasqAddr4 = *p.V4MasqAddr + } + if p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() { + pr.MasqAddr6 = *p.V6MasqAddr + } + for _, pfx := range p.AllowedIPs { + tbl.Insert(pfx, pr) + } + } + var native4, native6 netip.Addr + for _, pfx := range cfg.Addresses { + a := pfx.Addr() + switch { + case a.Is4() && !native4.IsValid(): + native4 = a + case a.Is6() && !native6.IsValid(): + native6 = a + } + } + s.tsTun.SetPeerRoutes(native4, native6, tbl) + // In production, LocalBackend installs a PeerByIPPacketFunc via // Engine.SetPeerByIPPacketFunc. Tests that bypass LocalBackend need // to install one here for outbound packet routing. diff --git a/wgengine/userspace.go b/wgengine/userspace.go index c9b66f07d..6e96caefb 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" @@ -1023,6 +1025,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)