summaryrefslogtreecommitdiffhomepage
path: root/wgengine/wgcfg
diff options
context:
space:
mode:
Diffstat (limited to 'wgengine/wgcfg')
-rw-r--r--wgengine/wgcfg/config.go8
-rw-r--r--wgengine/wgcfg/config_test.go2
-rw-r--r--wgengine/wgcfg/device.go96
-rw-r--r--wgengine/wgcfg/device_test.go196
-rw-r--r--wgengine/wgcfg/parser.go186
-rw-r--r--wgengine/wgcfg/parser_test.go95
-rw-r--r--wgengine/wgcfg/wgcfg_clone.go1
-rw-r--r--wgengine/wgcfg/writer.go154
8 files changed, 112 insertions, 626 deletions
diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go
index 782812139..5510b65b2 100644
--- a/wgengine/wgcfg/config.go
+++ b/wgengine/wgcfg/config.go
@@ -53,11 +53,6 @@ type Peer struct {
V6MasqAddr *netip.Addr // if non-nil, masquerade IPv6 traffic to this peer using this address
IsJailed bool // if true, this peer is jailed and cannot initiate connections
PersistentKeepalive uint16 // in seconds between keep-alives; 0 to disable
- // wireguard-go's endpoint for this peer. It should always equal Peer.PublicKey.
- // We represent it explicitly so that we can detect if they diverge and recover.
- // There is no need to set WGEndpoint explicitly when constructing a Peer by hand.
- // It is only populated when reading Peers from wireguard-go.
- WGEndpoint key.NodePublic
}
func addrPtrEq(a, b *netip.Addr) bool {
@@ -74,8 +69,7 @@ func (p Peer) Equal(o Peer) bool {
p.IsJailed == o.IsJailed &&
p.PersistentKeepalive == o.PersistentKeepalive &&
addrPtrEq(p.V4MasqAddr, o.V4MasqAddr) &&
- addrPtrEq(p.V6MasqAddr, o.V6MasqAddr) &&
- p.WGEndpoint == o.WGEndpoint
+ addrPtrEq(p.V6MasqAddr, o.V6MasqAddr)
}
// PeerWithKey returns the Peer with key k and reports whether it was found.
diff --git a/wgengine/wgcfg/config_test.go b/wgengine/wgcfg/config_test.go
index 7059b17b2..013d3a4b4 100644
--- a/wgengine/wgcfg/config_test.go
+++ b/wgengine/wgcfg/config_test.go
@@ -30,7 +30,7 @@ func TestPeerEqual(t *testing.T) {
for sf := range rt.Fields() {
switch sf.Name {
case "PublicKey", "DiscoKey", "AllowedIPs", "IsJailed",
- "PersistentKeepalive", "V4MasqAddr", "V6MasqAddr", "WGEndpoint":
+ "PersistentKeepalive", "V4MasqAddr", "V6MasqAddr":
// These are compared in [Peer.Equal].
default:
t.Errorf("Have you added field %q to Peer.Equal? Do so if not, and then update TestPeerEqual", sf.Name)
diff --git a/wgengine/wgcfg/device.go b/wgengine/wgcfg/device.go
index ba29cfbdc..02e1e36d1 100644
--- a/wgengine/wgcfg/device.go
+++ b/wgengine/wgcfg/device.go
@@ -4,9 +4,8 @@
package wgcfg
import (
- "errors"
- "io"
- "sort"
+ "fmt"
+ "net/netip"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
@@ -21,27 +20,28 @@ func NewDevice(tunDev tun.Device, bind conn.Bind, logger *device.Logger) *device
return ret
}
-func DeviceConfig(d *device.Device) (*Config, error) {
- r, w := io.Pipe()
- errc := make(chan error, 1)
- go func() {
- errc <- d.IpcGetOperation(w)
- w.Close()
- }()
- cfg, fromErr := FromUAPI(r)
- r.Close()
- getErr := <-errc
- err := errors.Join(getErr, fromErr)
- if err != nil {
- return nil, err
- }
- sort.Slice(cfg.Peers, func(i, j int) bool {
- return cfg.Peers[i].PublicKey.Less(cfg.Peers[j].PublicKey)
- })
- return cfg, nil
-}
-
// ReconfigDevice replaces the existing device configuration with cfg.
+//
+// Instead of using the UAPI text protocol, it uses the wireguard-go direct API
+// to install a PeerLookupFunc callback that creates peers on demand.
+//
+// The caller is responsible for:
+// - calling Device.SetPrivateKey when the key changes
+// - installing a PeerByIPPacketFunc on the device for outbound packet routing
+// (e.g. via Engine.SetPeerByIPPacketFunc)
+//
+// Race note: there's a small TOCTOU window between RemoveMatchingPeers and
+// SetPeerLookupFunc where the previously-installed PeerLookupFunc (with a
+// stale peer set) is still active. A concurrent handshake for a peer that's
+// being removed could lazily recreate it via the old callback. Additionally,
+// wireguard-go's LookupPeer snapshots the lookupFunc reference inside its
+// RLock and then invokes it without the lock, so even reordering these calls
+// can't fully close the window. We accept this: lazily-created peers have
+// deleteOnIdle=true and self-clean after the rekey timeout (~9 min idle), so
+// the worst case is a brief excess of memory. Closing the race fully would
+// require either holding wireguard-go's peers lock across the lookupFunc
+// call or more elaborate locking, neither of which seems worth the
+// complexity for a transient memory blip.
func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) {
defer func() {
if err != nil {
@@ -49,20 +49,44 @@ func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error)
}
}()
- prev, err := DeviceConfig(d)
- if err != nil {
- return err
+ // Build peer map: public key → allowed IPs.
+ peers := make(map[device.NoisePublicKey][]netip.Prefix, len(cfg.Peers))
+ for _, p := range cfg.Peers {
+ peers[p.PublicKey.Raw32()] = p.AllowedIPs
}
- r, w := io.Pipe()
- errc := make(chan error, 1)
- go func() {
- errc <- d.IpcSetOperation(r)
- r.Close()
- }()
+ // Remove peers not in the new config.
+ d.RemoveMatchingPeers(func(pk device.NoisePublicKey) bool {
+ _, exists := peers[pk]
+ return !exists
+ })
+
+ // Update AllowedIPs on any already-active peers whose config may have
+ // changed. Peers that don't exist yet will get the correct AllowedIPs
+ // from PeerLookupFunc when they are lazily created.
+ for pk, allowedIPs := range peers {
+ if peer, ok := d.LookupActivePeer(pk); ok {
+ peer.SetAllowedIPs(allowedIPs)
+ }
+ }
+
+ // Install callback for lazy peer creation (incoming packets).
+ bind := d.Bind()
+ d.SetPeerLookupFunc(func(pubk device.NoisePublicKey) (_ *device.NewPeerConfig, ok bool) {
+ allowedIPs, ok := peers[pubk]
+ if !ok {
+ return nil, false
+ }
+ ep, err := bind.ParseEndpoint(fmt.Sprintf("%x", pubk[:]))
+ if err != nil {
+ logf("wgcfg: failed to parse endpoint for peer %x: %v", pubk[:8], err)
+ return nil, false
+ }
+ return &device.NewPeerConfig{
+ AllowedIPs: allowedIPs,
+ Endpoint: ep,
+ }, true
+ })
- toErr := cfg.ToUAPI(logf, w, prev)
- w.Close()
- setErr := <-errc
- return errors.Join(setErr, toErr)
+ return nil
}
diff --git a/wgengine/wgcfg/device_test.go b/wgengine/wgcfg/device_test.go
index 507f22311..07eb41adb 100644
--- a/wgengine/wgcfg/device_test.go
+++ b/wgengine/wgcfg/device_test.go
@@ -4,33 +4,22 @@
package wgcfg
import (
- "bufio"
- "bytes"
"io"
"net/netip"
"os"
- "sort"
- "strings"
- "sync"
"testing"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
- "go4.org/mem"
"tailscale.com/types/key"
)
-func TestDeviceConfig(t *testing.T) {
- newK := func() (key.NodePublic, key.NodePrivate) {
- t.Helper()
- k := key.NewNode()
- return k.Public(), k
- }
+func TestReconfigDevice(t *testing.T) {
k1, pk1 := newK()
ip1 := netip.MustParsePrefix("10.0.0.1/32")
- k2, pk2 := newK()
+ k2, _ := newK()
ip2 := netip.MustParsePrefix("10.0.0.2/32")
k3, _ := newK()
@@ -38,165 +27,80 @@ func TestDeviceConfig(t *testing.T) {
cfg1 := &Config{
PrivateKey: pk1,
- Peers: []Peer{{
- PublicKey: k2,
- AllowedIPs: []netip.Prefix{ip2},
- }},
- }
-
- cfg2 := &Config{
- PrivateKey: pk2,
- Peers: []Peer{{
- PublicKey: k1,
- AllowedIPs: []netip.Prefix{ip1},
- PersistentKeepalive: 5,
- }},
+ Peers: []Peer{
+ {PublicKey: k2, AllowedIPs: []netip.Prefix{ip2}},
+ },
}
- device1 := NewDevice(newNilTun(), new(noopBind), device.NewLogger(device.LogLevelError, "device1"))
- device2 := NewDevice(newNilTun(), new(noopBind), device.NewLogger(device.LogLevelError, "device2"))
- defer device1.Close()
- defer device2.Close()
+ dev := NewDevice(newNilTun(), new(noopBind), device.NewLogger(device.LogLevelError, "test"))
+ defer dev.Close()
- cmp := func(t *testing.T, d *device.Device, want *Config) {
- t.Helper()
- got, err := DeviceConfig(d)
- if err != nil {
+ t.Run("initial-config", func(t *testing.T) {
+ if err := ReconfigDevice(dev, cfg1, t.Logf); err != nil {
t.Fatal(err)
}
- prev := new(Config)
- gotbuf := new(strings.Builder)
- err = got.ToUAPI(t.Logf, gotbuf, prev)
- gotStr := gotbuf.String()
- if err != nil {
- t.Errorf("got.ToUAPI(): error: %v", err)
- return
- }
- wantbuf := new(strings.Builder)
- err = want.ToUAPI(t.Logf, wantbuf, prev)
- wantStr := wantbuf.String()
- if err != nil {
- t.Errorf("want.ToUAPI(): error: %v", err)
- return
- }
- if gotStr != wantStr {
- buf := new(bytes.Buffer)
- w := bufio.NewWriter(buf)
- if err := d.IpcGetOperation(w); err != nil {
- t.Errorf("on error, could not IpcGetOperation: %v", err)
- }
- w.Flush()
- t.Errorf("config mismatch:\n---- got:\n%s\n---- want:\n%s\n---- uapi:\n%s", gotStr, wantStr, buf.String())
- }
- }
-
- t.Run("device1-config", func(t *testing.T) {
- if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
- t.Fatal(err)
- }
- cmp(t, device1, cfg1)
- })
-
- t.Run("device2-config", func(t *testing.T) {
- if err := ReconfigDevice(device2, cfg2, t.Logf); err != nil {
- t.Fatal(err)
- }
- cmp(t, device2, cfg2)
- })
-
- // This is only to test that Config and Reconfig are properly synchronized.
- t.Run("device2-config-reconfig", func(t *testing.T) {
- var wg sync.WaitGroup
- wg.Add(2)
-
- go func() {
- ReconfigDevice(device2, cfg2, t.Logf)
- wg.Done()
- }()
-
- go func() {
- DeviceConfig(device2)
- wg.Done()
- }()
-
- wg.Wait()
- })
-
- t.Run("device1-modify-peer", func(t *testing.T) {
- cfg1.Peers[0].DiscoKey = key.DiscoPublicFromRaw32(mem.B([]byte{0: 1, 31: 0}))
- if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
- t.Fatal(err)
+ // Peer should be creatable on demand via LookupPeer.
+ peer := dev.LookupPeer(k2.Raw32())
+ if peer == nil {
+ t.Fatal("expected peer k2 to exist via LookupPeer")
}
- cmp(t, device1, cfg1)
- })
-
- t.Run("device1-replace-endpoint", func(t *testing.T) {
- cfg1.Peers[0].DiscoKey = key.DiscoPublicFromRaw32(mem.B([]byte{0: 2, 31: 0}))
- if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
- t.Fatal(err)
+ // Unknown peer should not be found.
+ peer = dev.LookupPeer(k3.Raw32())
+ if peer != nil {
+ t.Fatal("expected unknown peer k3 to not exist")
}
- cmp(t, device1, cfg1)
})
- t.Run("device1-add-new-peer", func(t *testing.T) {
+ t.Run("add-peer", func(t *testing.T) {
cfg1.Peers = append(cfg1.Peers, Peer{
PublicKey: k3,
AllowedIPs: []netip.Prefix{ip3},
})
- sort.Slice(cfg1.Peers, func(i, j int) bool {
- return cfg1.Peers[i].PublicKey.Less(cfg1.Peers[j].PublicKey)
- })
-
- origCfg, err := DeviceConfig(device1)
- if err != nil {
+ if err := ReconfigDevice(dev, cfg1, t.Logf); err != nil {
t.Fatal(err)
}
-
- if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
- t.Fatal(err)
+ // Both peers should now be discoverable.
+ if p := dev.LookupPeer(k2.Raw32()); p == nil {
+ t.Fatal("expected peer k2 to exist")
}
- cmp(t, device1, cfg1)
-
- newCfg, err := DeviceConfig(device1)
- if err != nil {
- t.Fatal(err)
+ if p := dev.LookupPeer(k3.Raw32()); p == nil {
+ t.Fatal("expected peer k3 to exist")
}
+ })
- peer0 := func(cfg *Config) Peer {
- p, ok := cfg.PeerWithKey(k2)
- if !ok {
- t.Helper()
- t.Fatal("failed to look up peer 2")
- }
- return p
+ t.Run("remove-peer", func(t *testing.T) {
+ cfg2 := &Config{
+ PrivateKey: pk1,
+ Peers: []Peer{
+ {PublicKey: k2, AllowedIPs: []netip.Prefix{ip2}},
+ },
}
- peersEqual := func(p, q Peer) bool {
- return p.PublicKey == q.PublicKey && p.DiscoKey == q.DiscoKey && p.PersistentKeepalive == q.PersistentKeepalive && cidrsEqual(p.AllowedIPs, q.AllowedIPs)
+ if err := ReconfigDevice(dev, cfg2, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ // k2 should still be discoverable.
+ if p := dev.LookupPeer(k2.Raw32()); p == nil {
+ t.Fatal("expected peer k2 to exist")
}
- if !peersEqual(peer0(origCfg), peer0(newCfg)) {
- t.Error("reconfig modified old peer")
+ // k3 should no longer be discoverable.
+ if p := dev.LookupPeer(k3.Raw32()); p != nil {
+ t.Fatal("expected peer k3 to not exist after removal")
}
})
- t.Run("device1-remove-peer", func(t *testing.T) {
- removeKey := cfg1.Peers[len(cfg1.Peers)-1].PublicKey
- cfg1.Peers = cfg1.Peers[:len(cfg1.Peers)-1]
-
- if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
- t.Fatal(err)
+ t.Run("self-key-not-peer", func(t *testing.T) {
+ // The device's own key should not be a peer.
+ if p := dev.LookupPeer(k1.Raw32()); p != nil {
+ t.Fatal("expected own key to not be a peer")
}
- cmp(t, device1, cfg1)
+ })
- newCfg, err := DeviceConfig(device1)
- if err != nil {
- t.Fatal(err)
- }
+ _ = ip1 // suppress unused
+}
- _, ok := newCfg.PeerWithKey(removeKey)
- if ok {
- t.Error("reconfig failed to remove peer")
- }
- })
+func newK() (key.NodePublic, key.NodePrivate) {
+ k := key.NewNode()
+ return k.Public(), k
}
// TODO: replace with a loopback tunnel
diff --git a/wgengine/wgcfg/parser.go b/wgengine/wgcfg/parser.go
deleted file mode 100644
index 8fb921409..000000000
--- a/wgengine/wgcfg/parser.go
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright (c) Tailscale Inc & contributors
-// SPDX-License-Identifier: BSD-3-Clause
-
-package wgcfg
-
-import (
- "bufio"
- "fmt"
- "io"
- "net"
- "net/netip"
- "strconv"
- "strings"
-
- "go4.org/mem"
- "tailscale.com/types/key"
-)
-
-type ParseError struct {
- why string
- offender string
-}
-
-func (e *ParseError) Error() string {
- return fmt.Sprintf("%s: %q", e.why, e.offender)
-}
-
-func parseEndpoint(s string) (host string, port uint16, err error) {
- i := strings.LastIndexByte(s, ':')
- if i < 0 {
- return "", 0, &ParseError{"Missing port from endpoint", s}
- }
- host, portStr := s[:i], s[i+1:]
- if len(host) < 1 {
- return "", 0, &ParseError{"Invalid endpoint host", host}
- }
- uport, err := strconv.ParseUint(portStr, 10, 16)
- if err != nil {
- return "", 0, err
- }
- hostColon := strings.IndexByte(host, ':')
- if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
- err := &ParseError{"Brackets must contain an IPv6 address", host}
- if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
- maybeV6 := net.ParseIP(host[1 : len(host)-1])
- if maybeV6 == nil || len(maybeV6) != net.IPv6len {
- return "", 0, err
- }
- } else {
- return "", 0, err
- }
- host = host[1 : len(host)-1]
- }
- return host, uint16(uport), nil
-}
-
-// memROCut separates a mem.RO at the separator if it exists, otherwise
-// it returns two empty ROs and reports that it was not found.
-func memROCut(s mem.RO, sep byte) (before, after mem.RO, found bool) {
- if i := mem.IndexByte(s, sep); i >= 0 {
- return s.SliceTo(i), s.SliceFrom(i + 1), true
- }
- found = false
- return
-}
-
-// FromUAPI generates a Config from r.
-// r should be generated by calling device.IpcGetOperation;
-// it is not compatible with other uapi streams.
-func FromUAPI(r io.Reader) (*Config, error) {
- cfg := new(Config)
- var peer *Peer // current peer being operated on
- deviceConfig := true
-
- scanner := bufio.NewScanner(r)
- for scanner.Scan() {
- line := mem.B(scanner.Bytes())
- if line.Len() == 0 {
- continue
- }
- key, value, ok := memROCut(line, '=')
- if !ok {
- return nil, fmt.Errorf("failed to cut line %q on =", line.StringCopy())
- }
- valueBytes := scanner.Bytes()[key.Len()+1:]
-
- if key.EqualString("public_key") {
- if deviceConfig {
- deviceConfig = false
- }
- // Load/create the peer we are now configuring.
- var err error
- peer, err = cfg.handlePublicKeyLine(valueBytes)
- if err != nil {
- return nil, err
- }
- continue
- }
-
- var err error
- if deviceConfig {
- err = cfg.handleDeviceLine(key, value, valueBytes)
- } else {
- err = cfg.handlePeerLine(peer, key, value, valueBytes)
- }
- if err != nil {
- return nil, err
- }
- }
-
- if err := scanner.Err(); err != nil {
- return nil, err
- }
-
- return cfg, nil
-}
-
-func (cfg *Config) handleDeviceLine(k, value mem.RO, valueBytes []byte) error {
- switch {
- case k.EqualString("private_key"):
- // wireguard-go guarantees not to send zero value; private keys are already clamped.
- var err error
- cfg.PrivateKey, err = key.ParseNodePrivateUntyped(value)
- if err != nil {
- return err
- }
- case k.EqualString("listen_port") || k.EqualString("fwmark"):
- // ignore
- default:
- return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())
- }
- return nil
-}
-
-func (cfg *Config) handlePublicKeyLine(valueBytes []byte) (*Peer, error) {
- p := Peer{}
- var err error
- p.PublicKey, err = key.ParseNodePublicUntyped(mem.B(valueBytes))
- if err != nil {
- return nil, err
- }
- cfg.Peers = append(cfg.Peers, p)
- return &cfg.Peers[len(cfg.Peers)-1], nil
-}
-
-func (cfg *Config) handlePeerLine(peer *Peer, k, value mem.RO, valueBytes []byte) error {
- switch {
- case k.EqualString("endpoint"):
- nk, err := key.ParseNodePublicUntyped(value)
- if err != nil {
- return fmt.Errorf("invalid endpoint %q for peer %q, expected a hex public key", value.StringCopy(), peer.PublicKey.ShortString())
- }
- // nk ought to equal peer.PublicKey.
- // Under some rare circumstances, it might not. See corp issue #3016.
- // Even if that happens, don't stop early, so that we can recover from it.
- // Instead, note the value of nk so we can fix as needed.
- peer.WGEndpoint = nk
- case k.EqualString("persistent_keepalive_interval"):
- n, err := mem.ParseUint(value, 10, 16)
- if err != nil {
- return err
- }
- peer.PersistentKeepalive = uint16(n)
- case k.EqualString("allowed_ip"):
- ipp := netip.Prefix{}
- err := ipp.UnmarshalText(valueBytes)
- if err != nil {
- return err
- }
- peer.AllowedIPs = append(peer.AllowedIPs, ipp)
- case k.EqualString("protocol_version"):
- if !value.EqualString("1") {
- return fmt.Errorf("invalid protocol version: %q", value.StringCopy())
- }
- case k.EqualString("replace_allowed_ips") ||
- k.EqualString("preshared_key") ||
- k.EqualString("last_handshake_time_sec") ||
- k.EqualString("last_handshake_time_nsec") ||
- k.EqualString("tx_bytes") ||
- k.EqualString("rx_bytes"):
- // ignore
- default:
- return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())
- }
- return nil
-}
diff --git a/wgengine/wgcfg/parser_test.go b/wgengine/wgcfg/parser_test.go
deleted file mode 100644
index 8c38ec025..000000000
--- a/wgengine/wgcfg/parser_test.go
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (c) Tailscale Inc & contributors
-// SPDX-License-Identifier: BSD-3-Clause
-
-package wgcfg
-
-import (
- "bufio"
- "bytes"
- "io"
- "net/netip"
- "reflect"
- "runtime"
- "testing"
-
- "tailscale.com/types/key"
-)
-
-func noError(t *testing.T, err error) bool {
- if err == nil {
- return true
- }
- _, fn, line, _ := runtime.Caller(1)
- t.Errorf("Error at %s:%d: %#v", fn, line, err)
- return false
-}
-
-func equal(t *testing.T, expected, actual any) bool {
- if reflect.DeepEqual(expected, actual) {
- return true
- }
- _, fn, line, _ := runtime.Caller(1)
- t.Errorf("Failed equals at %s:%d\nactual %#v\nexpected %#v", fn, line, actual, expected)
- return false
-}
-
-func TestParseEndpoint(t *testing.T) {
- _, _, err := parseEndpoint("[192.168.42.0:]:51880")
- if err == nil {
- t.Error("Error was expected")
- }
- host, port, err := parseEndpoint("192.168.42.0:51880")
- if noError(t, err) {
- equal(t, "192.168.42.0", host)
- equal(t, uint16(51880), port)
- }
- host, port, err = parseEndpoint("test.wireguard.com:18981")
- if noError(t, err) {
- equal(t, "test.wireguard.com", host)
- equal(t, uint16(18981), port)
- }
- host, port, err = parseEndpoint("[2607:5300:60:6b0::c05f:543]:2468")
- if noError(t, err) {
- equal(t, "2607:5300:60:6b0::c05f:543", host)
- equal(t, uint16(2468), port)
- }
- _, _, err = parseEndpoint("[::::::invalid:18981")
- if err == nil {
- t.Error("Error was expected")
- }
-}
-
-func BenchmarkFromUAPI(b *testing.B) {
- newK := func() (key.NodePublic, key.NodePrivate) {
- b.Helper()
- k := key.NewNode()
- return k.Public(), k
- }
- k1, pk1 := newK()
- ip1 := netip.MustParsePrefix("10.0.0.1/32")
-
- peer := Peer{
- PublicKey: k1,
- AllowedIPs: []netip.Prefix{ip1},
- }
- cfg1 := &Config{
- PrivateKey: pk1,
- Peers: []Peer{peer, peer, peer, peer},
- }
-
- buf := new(bytes.Buffer)
- w := bufio.NewWriter(buf)
- if err := cfg1.ToUAPI(b.Logf, w, &Config{}); err != nil {
- b.Fatal(err)
- }
- w.Flush()
- r := bytes.NewReader(buf.Bytes())
- b.ReportAllocs()
- for range b.N {
- r.Seek(0, io.SeekStart)
- _, err := FromUAPI(r)
- if err != nil {
- b.Errorf("failed from UAPI: %v", err)
- }
- }
-}
diff --git a/wgengine/wgcfg/wgcfg_clone.go b/wgengine/wgcfg/wgcfg_clone.go
index 9e8de7b6f..a8a212267 100644
--- a/wgengine/wgcfg/wgcfg_clone.go
+++ b/wgengine/wgcfg/wgcfg_clone.go
@@ -72,5 +72,4 @@ var _PeerCloneNeedsRegeneration = Peer(struct {
V6MasqAddr *netip.Addr
IsJailed bool
PersistentKeepalive uint16
- WGEndpoint key.NodePublic
}{})
diff --git a/wgengine/wgcfg/writer.go b/wgengine/wgcfg/writer.go
deleted file mode 100644
index f4981e3e9..000000000
--- a/wgengine/wgcfg/writer.go
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright (c) Tailscale Inc & contributors
-// SPDX-License-Identifier: BSD-3-Clause
-
-package wgcfg
-
-import (
- "fmt"
- "io"
- "net/netip"
- "strconv"
-
- "tailscale.com/types/key"
- "tailscale.com/types/logger"
-)
-
-// ToUAPI writes cfg in UAPI format to w.
-// Prev is the previous device Config.
-//
-// Prev is required so that we can remove now-defunct peers without having to
-// remove and re-add all peers, and so that we can avoid writing information
-// about peers that have not changed since the previous time we wrote our
-// Config.
-func (cfg *Config) ToUAPI(logf logger.Logf, w io.Writer, prev *Config) error {
- var stickyErr error
- set := func(key, value string) {
- if stickyErr != nil {
- return
- }
- _, err := fmt.Fprintf(w, "%s=%s\n", key, value)
- if err != nil {
- stickyErr = err
- }
- }
- setUint16 := func(key string, value uint16) {
- set(key, strconv.FormatUint(uint64(value), 10))
- }
- setPeer := func(peer Peer) {
- set("public_key", peer.PublicKey.UntypedHexString())
- }
-
- // Device config.
- if !prev.PrivateKey.Equal(cfg.PrivateKey) {
- set("private_key", cfg.PrivateKey.UntypedHexString())
- }
-
- old := make(map[key.NodePublic]Peer)
- for _, p := range prev.Peers {
- old[p.PublicKey] = p
- }
-
- // Add/configure all new peers.
- for _, p := range cfg.Peers {
- oldPeer, wasPresent := old[p.PublicKey]
-
- // We only want to write the peer header/version if we're about
- // to change something about that peer, or if it's a new peer.
- // Figure out up-front whether we'll need to do anything for
- // this peer, and skip doing anything if not.
- //
- // If the peer was not present in the previous config, this
- // implies that this is a new peer; set all of these to 'true'
- // to ensure that we're writing the full peer configuration.
- willSetEndpoint := oldPeer.WGEndpoint != p.PublicKey || !wasPresent
- willChangeIPs := !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) || !wasPresent
- willChangeKeepalive := oldPeer.PersistentKeepalive != p.PersistentKeepalive // if not wasPresent, no need to redundantly set zero (default)
-
- if !willSetEndpoint && !willChangeIPs && !willChangeKeepalive {
- // It's safe to skip doing anything here; wireguard-go
- // will not remove a peer if it's unspecified unless we
- // tell it to (which we do below if necessary).
- continue
- }
-
- setPeer(p)
- set("protocol_version", "1")
-
- // Avoid setting endpoints if the correct one is already known
- // to WireGuard, because doing so generates a bit more work in
- // calling magicsock's ParseEndpoint for effectively a no-op.
- if willSetEndpoint {
- if wasPresent {
- // We had an endpoint, and it was wrong.
- // By construction, this should not happen.
- // If it does, keep going so that we can recover from it,
- // but log so that we know about it,
- // because it is an indicator of other failed invariants.
- // See corp issue 3016.
- logf("[unexpected] endpoint changed from %s to %s", oldPeer.WGEndpoint, p.PublicKey)
- }
- set("endpoint", p.PublicKey.UntypedHexString())
- }
-
- // TODO: replace_allowed_ips is expensive.
- // If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs,
- // then skip replace_allowed_ips and instead add only
- // the new ipps with allowed_ip.
- if willChangeIPs {
- set("replace_allowed_ips", "true")
- for _, ipp := range p.AllowedIPs {
- set("allowed_ip", ipp.String())
- }
- }
-
- // Set PersistentKeepalive after the peer is otherwise configured,
- // because it can trigger handshake packets.
- if willChangeKeepalive {
- setUint16("persistent_keepalive_interval", p.PersistentKeepalive)
- }
- }
-
- // Remove peers that were present but should no longer be.
- for _, p := range cfg.Peers {
- delete(old, p.PublicKey)
- }
- for _, p := range old {
- setPeer(p)
- set("remove", "true")
- }
-
- if stickyErr != nil {
- stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr)
- }
- return stickyErr
-}
-
-func cidrsEqual(x, y []netip.Prefix) bool {
- // TODO: re-implement using netaddr.IPSet.Equal.
- if len(x) != len(y) {
- return false
- }
- // First see if they're equal in order, without allocating.
- exact := true
- for i := range x {
- if x[i] != y[i] {
- exact = false
- break
- }
- }
- if exact {
- return true
- }
-
- // Otherwise, see if they're the same, but out of order.
- m := make(map[netip.Prefix]bool)
- for _, v := range x {
- m[v] = true
- }
- for _, v := range y {
- if !m[v] {
- return false
- }
- }
- return true
-}