wgengine, all: remove LazyWG, use wireguard-go callback API for on-demand peers
Replace the UAPI text protocol-based wireguard configuration with wireguard-go's new direct callback API (SetPeerLookupFunc, SetPeerByIPPacketFunc, RemoveMatchingPeers, SetPrivateKey). Instead of computing a trimmed wireguard config ahead of time upon control plane updates and pushing it via UAPI, install callbacks so wireguard-go creates peers on demand when packets arrive. This removes all the LazyWG trimming machinery: idle peer tracking, activity maps, noteRecvActivity callbacks, the KeepFullWGConfig control knob, and the ts_omit_lazywg build tag. For incoming packets, PeerLookupFunc answers wireguard-go's questions about unknown public keys by looking up the peer in the full config. For outgoing packets, PeerByIPPacketFunc (installed from LocalBackend.lookupPeerByIP) maps destination IPs to node public keys using the existing nodeByAddr index. Updates tailscale/corp#12345 Change-Id: I4cba80979ac49a1231d00a01fdba5f0c2af95dd8 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
b313bffbe7
commit
f343b496c3
@@ -8,7 +8,6 @@ import (
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@@ -19,81 +18,16 @@ import (
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
"tailscale.com/util/usermetric"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNoteReceiveActivity(t *testing.T) {
|
||||
now := mono.Time(123456)
|
||||
var logBuf tstest.MemLogger
|
||||
|
||||
confc := make(chan bool, 1)
|
||||
gotConf := func() bool {
|
||||
select {
|
||||
case <-confc:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
e := &userspaceEngine{
|
||||
timeNow: func() mono.Time { return now },
|
||||
recvActivityAt: map[key.NodePublic]mono.Time{},
|
||||
logf: logBuf.Logf,
|
||||
tundev: new(tstun.Wrapper),
|
||||
testMaybeReconfigHook: func() { confc <- true },
|
||||
trimmedNodes: map[key.NodePublic]bool{},
|
||||
}
|
||||
ra := e.recvActivityAt
|
||||
|
||||
nk := key.NewNode().Public()
|
||||
|
||||
// Activity on an untracked key should do nothing.
|
||||
e.noteRecvActivity(nk)
|
||||
if len(ra) != 0 {
|
||||
t.Fatalf("unexpected growth in map: now has %d keys; want 0", len(ra))
|
||||
}
|
||||
if logBuf.Len() != 0 {
|
||||
t.Fatalf("unexpected log write (and thus activity): %s", logBuf.Bytes())
|
||||
}
|
||||
|
||||
// Now track it, but don't mark it trimmed, so shouldn't update.
|
||||
ra[nk] = 0
|
||||
e.noteRecvActivity(nk)
|
||||
if len(ra) != 1 {
|
||||
t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra))
|
||||
}
|
||||
if got := ra[nk]; got != now {
|
||||
t.Fatalf("time in map = %v; want %v", got, now)
|
||||
}
|
||||
if gotConf() {
|
||||
t.Fatalf("unexpected reconfig")
|
||||
}
|
||||
|
||||
// Now mark it trimmed and expect an update.
|
||||
e.trimmedNodes[nk] = true
|
||||
e.noteRecvActivity(nk)
|
||||
if len(ra) != 1 {
|
||||
t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra))
|
||||
}
|
||||
if got := ra[nk]; got != now {
|
||||
t.Fatalf("time in map = %v; want %v", got, now)
|
||||
}
|
||||
if !gotConf() {
|
||||
t.Fatalf("didn't get expected reconfig")
|
||||
}
|
||||
}
|
||||
|
||||
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
|
||||
nv := make([]tailcfg.NodeView, len(v))
|
||||
for i, n := range v {
|
||||
@@ -112,7 +46,6 @@ func TestUserspaceEngineReconfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
ue := e.(*userspaceEngine)
|
||||
|
||||
routerCfg := &router.Config{}
|
||||
|
||||
@@ -148,20 +81,6 @@ func TestUserspaceEngineReconfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantRecvAt := map[key.NodePublic]mono.Time{
|
||||
nkFromHex(nodeHex): 0,
|
||||
}
|
||||
if got := ue.recvActivityAt; !reflect.DeepEqual(got, wantRecvAt) {
|
||||
t.Errorf("wrong recvActivityAt\n got: %v\nwant: %v\n", got, wantRecvAt)
|
||||
}
|
||||
|
||||
wantTrimmedNodes := map[key.NodePublic]bool{
|
||||
nkFromHex(nodeHex): true,
|
||||
}
|
||||
if got := ue.trimmedNodes; !reflect.DeepEqual(got, wantTrimmedNodes) {
|
||||
t.Errorf("wrong wantTrimmedNodes\n got: %v\nwant: %v\n", got, wantTrimmedNodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,121 +476,6 @@ func nkFromHex(hex string) key.NodePublic {
|
||||
return k
|
||||
}
|
||||
|
||||
// makeMaybeReconfigInputs builds a maybeReconfigInputs with n peers,
|
||||
// each with a unique key, disco key, and AllowedIPs entry.
|
||||
func makeMaybeReconfigInputs(n int) *maybeReconfigInputs {
|
||||
peers := make([]wgcfg.Peer, n)
|
||||
trimmed := make(map[key.NodePublic]bool, n)
|
||||
trackNodes := make([]key.NodePublic, n)
|
||||
trackIPs := make([]netip.Addr, n)
|
||||
|
||||
for i := range n {
|
||||
nk := key.NewNode()
|
||||
pub := nk.Public()
|
||||
peers[i] = wgcfg.Peer{
|
||||
PublicKey: pub,
|
||||
DiscoKey: key.NewDisco().Public(),
|
||||
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netip.AddrFrom4([4]byte{100, 64, byte(i >> 8), byte(i)}), 32)},
|
||||
}
|
||||
trimmed[pub] = true
|
||||
trackNodes[i] = pub
|
||||
trackIPs[i] = netip.AddrFrom4([4]byte{100, 64, byte(i >> 8), byte(i)})
|
||||
}
|
||||
|
||||
return &maybeReconfigInputs{
|
||||
WGConfig: &wgcfg.Config{
|
||||
PrivateKey: key.NewNode(),
|
||||
Peers: peers,
|
||||
MTU: 1280,
|
||||
},
|
||||
TrimmedNodes: trimmed,
|
||||
TrackNodes: views.SliceOf(trackNodes),
|
||||
TrackIPs: views.SliceOf(trackIPs),
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeReconfigInputsEqual(t *testing.T) {
|
||||
a := makeMaybeReconfigInputs(100)
|
||||
b := a.Clone()
|
||||
|
||||
// nil cases
|
||||
if !(*maybeReconfigInputs)(nil).Equal(nil) {
|
||||
t.Error("nil.Equal(nil) should be true")
|
||||
}
|
||||
if a.Equal(nil) {
|
||||
t.Error("non-nil.Equal(nil) should be false")
|
||||
}
|
||||
if (*maybeReconfigInputs)(nil).Equal(a) {
|
||||
t.Error("nil.Equal(non-nil) should be false")
|
||||
}
|
||||
|
||||
// same pointer
|
||||
if !a.Equal(a) {
|
||||
t.Error("a.Equal(a) should be true")
|
||||
}
|
||||
|
||||
// cloned equal value
|
||||
if !a.Equal(b) {
|
||||
t.Error("a.Equal(clone) should be true")
|
||||
}
|
||||
|
||||
// Verify that every field in the struct is covered by Equal.
|
||||
// Each entry mutates exactly one field of a clone and expects
|
||||
// Equal to return false. If a new field is added to
|
||||
// maybeReconfigInputs without a corresponding entry here, the
|
||||
// field count check below will fail.
|
||||
type mutator struct {
|
||||
field string
|
||||
fn func(c *maybeReconfigInputs)
|
||||
}
|
||||
mutators := []mutator{
|
||||
{"WGConfig", func(c *maybeReconfigInputs) {
|
||||
c.WGConfig.MTU = 9999
|
||||
}},
|
||||
{"TrimmedNodes", func(c *maybeReconfigInputs) {
|
||||
c.TrimmedNodes[key.NewNode().Public()] = true
|
||||
}},
|
||||
{"TrackNodes", func(c *maybeReconfigInputs) {
|
||||
ns := c.TrackNodes.AsSlice()
|
||||
ns[0] = key.NewNode().Public()
|
||||
c.TrackNodes = views.SliceOf(ns)
|
||||
}},
|
||||
{"TrackIPs", func(c *maybeReconfigInputs) {
|
||||
ips := c.TrackIPs.AsSlice()
|
||||
ips[0] = netip.MustParseAddr("1.2.3.4")
|
||||
c.TrackIPs = views.SliceOf(ips)
|
||||
}},
|
||||
}
|
||||
|
||||
// Ensure we have a mutator for every field.
|
||||
numFields := reflect.TypeOf(maybeReconfigInputs{}).NumField()
|
||||
if len(mutators) != numFields {
|
||||
t.Fatalf("maybeReconfigInputs has %d fields but test covers %d; update the mutators table", numFields, len(mutators))
|
||||
}
|
||||
|
||||
for _, m := range mutators {
|
||||
c := a.Clone()
|
||||
m.fn(c)
|
||||
if a.Equal(c) {
|
||||
t.Errorf("Equal did not detect change in field %s", m.field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaybeReconfigInputsEqual(b *testing.B) {
|
||||
for _, n := range []int{10, 100, 1000, 5000} {
|
||||
b.Run(fmt.Sprintf("peers=%d", n), func(b *testing.B) {
|
||||
a := makeMaybeReconfigInputs(n)
|
||||
o := a.Clone()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
a.Equal(o)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// an experiment to see if genLocalAddrFunc was worth it. As of Go
|
||||
// 1.16, it still very much is. (30-40x faster)
|
||||
func BenchmarkGenLocalAddrFunc(b *testing.B) {
|
||||
|
||||
Reference in New Issue
Block a user