net/tstun,wgengine,ipn/ipnlocal: make tstun's peerConfigTable use RouteManager table
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 the last step before 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:
committed by
Brad Fitzpatrick
parent
e4144230f4
commit
f0ce89b715
+114
-55
@@ -18,6 +18,7 @@ import (
|
||||
"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 @@ import (
|
||||
"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 @@ import (
|
||||
"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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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 @@ func TestPeerCfg_NAT(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user