net/tstun,wgengine,ipn/ipnlocal: feed per-peer data-plane attrs from the route manager

Previously tstun.Wrapper.SetWGConfig walked wgcfg.Config.Peers on every
netmap to rebuild its own IP-to-peer table for masquerade NAT rewrites
and jailed-peer classification. Now the tun layer instead consumes the
route manager's shared immutable outbound snapshot directly, via a new
Engine.SetPeerRoutes method: LocalBackend pushes the snapshot (plus this
node's native Tailscale addresses) after every route manager commit that
can change it, and per-packet lookups read the interned PeerRoute
attributes from that table.

When no current peer is jailed or masqueraded, LocalBackend installs a
nil table (gated on RouteManager.HasDataPlaneAttrs), preserving the
per-packet nil-check fast path. The exitNodeRequiresMasq machinery is
deleted: its purpose was populating the table with all peers so that
more-specific entries shadow an exit node's /0, and the always-full
route manager table gives that shadowing inherently.

This is another step toward removing the Peers field from wgcfg.Config.

Updates #12542

Change-Id: Ifce09ca929a3f2511303ca1d6efdd583739494ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-09 14:28:53 +00:00
parent 536d9e135e
commit efbdb7bb6a
7 changed files with 192 additions and 210 deletions

View File

@@ -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, "<missing-profile>")
}
discoChanged := b.currentNode().SetNetMap(nm)
b.setDataPlanePeerRoutes()
if ms, ok := b.sys.MagicSock.GetOK(); ok {
if nm != nil {
if nm.Cached {

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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()

View File

@@ -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)