diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index da6262032..f4f8985b0 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -258,7 +258,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com from tailscale.com/version tailscale.com/appc from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/atomicfile from tailscale.com/ipn+ - LD tailscale.com/chirp from tailscale.com/cmd/tailscaled + LD tailscale.com/chirp from tailscale.com/feature/bird tailscale.com/client/local from tailscale.com/client/web+ tailscale.com/client/tailscale/apitype from tailscale.com/client/local+ tailscale.com/client/web from tailscale.com/ipn/ipnlocal @@ -292,6 +292,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/feature/ace from tailscale.com/feature/condregister tailscale.com/feature/acme from tailscale.com/feature/condregister tailscale.com/feature/appconnectors from tailscale.com/feature/condregister + LD tailscale.com/feature/bird from tailscale.com/feature/condregister tailscale.com/feature/buildfeatures from tailscale.com/wgengine/magicsock+ tailscale.com/feature/c2n from tailscale.com/feature/condregister tailscale.com/feature/capture from tailscale.com/feature/condregister diff --git a/cmd/tailscaled/deps_test.go b/cmd/tailscaled/deps_test.go index 9f17ea5d6..ed8a83aad 100644 --- a/cmd/tailscaled/deps_test.go +++ b/cmd/tailscaled/deps_test.go @@ -137,6 +137,20 @@ func TestOmitCaptivePortal(t *testing.T) { }.Check(t) } +func TestOmitBird(t *testing.T) { + deptest.DepChecker{ + GOOS: "linux", + GOARCH: "amd64", + Tags: "ts_omit_bird,ts_include_cli", + OnDep: func(dep string) { + switch dep { + case "tailscale.com/chirp", "tailscale.com/feature/bird": + t.Errorf("unexpected dep with ts_omit_bird: %q", dep) + } + }, + }.Check(t) +} + func TestOmitAuth(t *testing.T) { deptest.DepChecker{ GOOS: "linux", diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index c1408bc6a..41128564a 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -142,9 +142,8 @@ var args struct { } var ( - installSystemDaemon func([]string) error // non-nil on some platforms - uninstallSystemDaemon func([]string) error // non-nil on some platforms - createBIRDClient func(string) (wgengine.BIRDClient, error) // non-nil on some platforms + installSystemDaemon func([]string) error // non-nil on some platforms + uninstallSystemDaemon func([]string) error // non-nil on some platforms ) // Note - we use function pointers for subcommands so that subcommands like @@ -281,7 +280,7 @@ store state on filesystem.`) log.Fatalf("--socket is required") } - if buildfeatures.HasBird && args.birdSocketPath != "" && createBIRDClient == nil { + if buildfeatures.HasBird && args.birdSocketPath != "" && !wgengine.HookNewBird.IsSet() { log.SetFlags(0) log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS) } @@ -804,12 +803,8 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo netstackSubnetRouter := onlyNetstack // but mutated later on some platforms netns.SetEnabled(!onlyNetstack) - if args.birdSocketPath != "" && createBIRDClient != nil { - log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath) - conf.BIRDClient, err = createBIRDClient(args.birdSocketPath) - if err != nil { - return false, fmt.Errorf("createBIRDClient: %w", err) - } + if buildfeatures.HasBird && args.birdSocketPath != "" { + conf.BIRDSocket = args.birdSocketPath } if onlyNetstack { if runtime.GOOS == "linux" && distro.Get() == distro.Synology { diff --git a/cmd/tailscaled/tailscaled_bird.go b/cmd/tailscaled/tailscaled_bird.go deleted file mode 100644 index c1c32d2bb..000000000 --- a/cmd/tailscaled/tailscaled_bird.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Tailscale Inc & contributors -// SPDX-License-Identifier: BSD-3-Clause - -//go:build go1.19 && (linux || darwin || freebsd || openbsd) && !ts_omit_bird - -package main - -import ( - "tailscale.com/chirp" - "tailscale.com/wgengine" -) - -func init() { - createBIRDClient = func(ctlSocket string) (wgengine.BIRDClient, error) { - return chirp.New(ctlSocket) - } -} diff --git a/feature/bird/bird.go b/feature/bird/bird.go new file mode 100644 index 000000000..13b5fb065 --- /dev/null +++ b/feature/bird/bird.go @@ -0,0 +1,107 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Package bird integrates Tailscale with the BIRD Internet Routing +// Daemon: it enables the "tailscale" protocol in BIRD while this node +// is a primary subnet router and disables it otherwise. The BIRD +// client implementation lives in tailscale.com/chirp. +package bird + +import ( + "net/netip" + + "tailscale.com/chirp" + "tailscale.com/feature" + "tailscale.com/tailcfg" + "tailscale.com/types/logger" + "tailscale.com/types/views" + "tailscale.com/wgengine" +) + +func init() { + feature.Register("bird") + wgengine.HookNewBird.Set(newBird) +} + +// protocolName is the name of the BIRD protocol that Tailscale enables +// while this node is a primary subnet router. +const protocolName = "tailscale" + +// bird implements [wgengine.Bird] on top of [chirp.BIRDClient], +// tracking the primary subnet router state across engine +// reconfigurations. One bird exists per engine. +type bird struct { + logf logger.Logf + client *chirp.BIRDClient + + // The fields below are only accessed from Reconfig and + // ReconfigDone, which the engine serializes under its internal + // lock. + + // isSubnetRouter is whether the last Reconfig found this node to + // be a primary subnet router. + isSubnetRouter bool + // lastIsSubnetRouter is the primary subnet router state last + // successfully applied to BIRD. ReconfigDone compares it against + // isSubnetRouter to decide whether to toggle the protocol. + lastIsSubnetRouter bool +} + +func newBird(logf logger.Logf, socketPath string) (wgengine.Bird, error) { + logf("wgengine: connecting to BIRD at %s ...", socketPath) + client, err := chirp.New(socketPath) + if err != nil { + return nil, err + } + // Disable the protocol at start time; ReconfigDone enables it only + // once this node becomes a primary subnet router. + if err := client.DisableProtocol(protocolName); err != nil { + return nil, err + } + return &bird{logf: logf, client: client}, nil +} + +func (b *bird) Reconfig(self tailcfg.NodeView) (changed bool) { + b.isSubnetRouter = false + if self.Valid() { + b.isSubnetRouter = hasOverlap(self.PrimaryRoutes(), self.Hostinfo().RoutableIPs()) + b.logf("[v1] Reconfig: hasOverlap(%v, %v) = %v; isSubnetRouter=%v lastIsSubnetRouter=%v", + self.PrimaryRoutes(), self.Hostinfo().RoutableIPs(), + b.isSubnetRouter, b.isSubnetRouter, b.lastIsSubnetRouter) + } + return b.isSubnetRouter != b.lastIsSubnetRouter +} + +func (b *bird) ReconfigDone() { + if b.isSubnetRouter == b.lastIsSubnetRouter { + return + } + b.logf("wgengine: Reconfig: configuring BIRD") + var err error + if b.isSubnetRouter { + err = b.client.EnableProtocol(protocolName) + } else { + err = b.client.DisableProtocol(protocolName) + } + if err != nil { + // Log but don't fail here; a later Reconfig will retry. + b.logf("wgengine: error configuring BIRD: %v", err) + return + } + b.lastIsSubnetRouter = b.isSubnetRouter +} + +func (b *bird) Close() { + b.client.DisableProtocol(protocolName) + b.client.Close() +} + +// hasOverlap reports whether any prefix in rips is also present in aips. +func hasOverlap(aips, rips views.Slice[netip.Prefix]) bool { + for _, aip := range aips.All() { + if views.SliceContains(rips, aip) { + return true + } + } + return false +} diff --git a/feature/bird/bird_test.go b/feature/bird/bird_test.go new file mode 100644 index 000000000..a5fe66e57 --- /dev/null +++ b/feature/bird/bird_test.go @@ -0,0 +1,205 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package bird + +import ( + "bufio" + "errors" + "fmt" + "net" + "net/netip" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "tailscale.com/feature" + "tailscale.com/tailcfg" + "tailscale.com/wgengine" +) + +// fakeBIRD is a fake BIRD server listening on a unix socket. It speaks +// enough of the BIRD CLI wire protocol to satisfy chirp and records the +// commands it receives so tests can assert on them. +type fakeBIRD struct { + ln net.Listener + sock string + + mu sync.Mutex + calls []string // commands received, e.g. "enable tailscale" + enabled bool // whether the "tailscale" protocol is enabled + failNext bool // whether to reply to the next command with a runtime error +} + +func newFakeBIRD(t *testing.T) *fakeBIRD { + t.Helper() + sock := filepath.Join(t.TempDir(), "bird.sock") + ln, err := net.Listen("unix", sock) + if err != nil { + t.Fatal(err) + } + fb := &fakeBIRD{ln: ln, sock: sock} + t.Cleanup(func() { ln.Close() }) + go fb.listen() + return fb +} + +func (fb *fakeBIRD) listen() { + for { + c, err := fb.ln.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + return + } + panic(err) + } + go fb.handle(c) + } +} + +func (fb *fakeBIRD) handle(c net.Conn) { + fmt.Fprintln(c, "0001 BIRD 2.0.8 ready.") + sc := bufio.NewScanner(c) + for sc.Scan() { + cmd := sc.Text() + var proto string + var wantEnabled bool + switch { + case strings.HasPrefix(cmd, "enable "): + proto, wantEnabled = strings.TrimPrefix(cmd, "enable "), true + case strings.HasPrefix(cmd, "disable "): + proto, wantEnabled = strings.TrimPrefix(cmd, "disable "), false + } + + fb.mu.Lock() + fb.calls = append(fb.calls, cmd) + fail := fb.failNext + fb.failNext = false + switch { + case proto != protocolName: + fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL") + case fail: + fmt.Fprintln(c, "8001 fake runtime error") + case wantEnabled == fb.enabled: + fmt.Fprintf(c, "0010-%s: already %s\n0000 \n", proto, verb(wantEnabled)) + default: + fb.enabled = wantEnabled + fmt.Fprintf(c, "0011-%s: %s\n0000 \n", proto, verb(wantEnabled)) + } + fb.mu.Unlock() + } + if err := sc.Err(); err != nil && !errors.Is(err, net.ErrClosed) { + panic(err) + } +} + +func verb(enabled bool) string { + if enabled { + return "enabled" + } + return "disabled" +} + +// takeCalls returns the commands received since the last call and +// resets the record. It is safe to call once the chirp request that +// caused them has returned, as chirp waits for each response. +func (fb *fakeBIRD) takeCalls() []string { + fb.mu.Lock() + defer fb.mu.Unlock() + calls := fb.calls + fb.calls = nil + return calls +} + +func (fb *fakeBIRD) checkCalls(t *testing.T, want ...string) { + t.Helper() + if diff := cmp.Diff(fb.takeCalls(), want, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("BIRD calls mismatch (-got +want):\n%s", diff) + } +} + +func node(primaryRoutes, routableIPs []netip.Prefix) tailcfg.NodeView { + return (&tailcfg.Node{ + PrimaryRoutes: primaryRoutes, + Hostinfo: (&tailcfg.Hostinfo{RoutableIPs: routableIPs}).View(), + }).View() +} + +func TestBird(t *testing.T) { + if !feature.IsRegistered("bird") { + t.Fatal("bird feature not registered") + } + + fb := newFakeBIRD(t) + newBird, ok := wgengine.HookNewBird.GetOk() + if !ok { + t.Fatal("HookNewBird not set") + } + b, err := newBird(t.Logf, fb.sock) + if err != nil { + t.Fatal(err) + } + // Construction disables the protocol. + fb.checkCalls(t, "disable tailscale") + + pfx := netip.MustParsePrefix + subnetRouter := node( + []netip.Prefix{pfx("192.168.1.0/24")}, + []netip.Prefix{pfx("192.168.1.0/24"), pfx("10.0.0.0/8")}, + ) + notSubnetRouter := node( + []netip.Prefix{pfx("192.168.2.0/24")}, + []netip.Prefix{pfx("192.168.1.0/24")}, + ) + + // An invalid self node is not a subnet router; no state change, no calls. + if changed := b.Reconfig(tailcfg.NodeView{}); changed { + t.Error("Reconfig(invalid) reported change") + } + b.ReconfigDone() + fb.checkCalls(t) + + // Becoming a primary subnet router enables the protocol. + if changed := b.Reconfig(subnetRouter); !changed { + t.Error("Reconfig(subnetRouter) reported no change") + } + b.ReconfigDone() + fb.checkCalls(t, "enable tailscale") + + // Reconfig with the same state is a no-op. + if changed := b.Reconfig(subnetRouter); changed { + t.Error("Reconfig(subnetRouter) again reported change") + } + b.ReconfigDone() + fb.checkCalls(t) + + // A BIRD failure leaves the state unapplied, so the next Reconfig + // still reports a change and retries. + fb.mu.Lock() + fb.failNext = true + fb.mu.Unlock() + if changed := b.Reconfig(notSubnetRouter); !changed { + t.Error("Reconfig(notSubnetRouter) reported no change") + } + b.ReconfigDone() + fb.checkCalls(t, "disable tailscale") + if changed := b.Reconfig(notSubnetRouter); !changed { + t.Error("Reconfig(notSubnetRouter) retry reported no change") + } + b.ReconfigDone() + fb.checkCalls(t, "disable tailscale") + + // And becoming a subnet router again enables it again. + if changed := b.Reconfig(subnetRouter); !changed { + t.Error("Reconfig(subnetRouter) reported no change") + } + b.ReconfigDone() + fb.checkCalls(t, "enable tailscale") + + // Close disables the protocol on the way out. + b.Close() + fb.checkCalls(t, "disable tailscale") +} diff --git a/feature/condregister/maybe_bird.go b/feature/condregister/maybe_bird.go new file mode 100644 index 000000000..b2cae98b0 --- /dev/null +++ b/feature/condregister/maybe_bird.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !ts_omit_bird && (linux || darwin || freebsd || openbsd) + +package condregister + +import _ "tailscale.com/feature/bird" diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index 6767430f1..906008b06 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -3409,6 +3409,8 @@ func TestDeps(t *testing.T) { BadDeps: map[string]string{ "golang.org/x/crypto/ssh": "tsnet should not depend on SSH", "golang.org/x/crypto/ssh/internal/bcrypt_pbkdf": "tsnet should not depend on SSH", + "tailscale.com/chirp": "tsnet should not depend on BIRD integration", + "tailscale.com/feature/bird": "tsnet should not depend on BIRD integration", "tailscale.com/feature/clientupdate": "tsnet should not depend on feature/clientupdate", "tailscale.com/feature/remoteconfig": "tsnet should not depend on feature/remoteconfig", "tailscale.com/feature/syspolicy": "tsnet should not depend on syspolicy", diff --git a/tstest/integration/tailscaled_deps_test_darwin.go b/tstest/integration/tailscaled_deps_test_darwin.go index 3132739f6..9a19b3997 100644 --- a/tstest/integration/tailscaled_deps_test_darwin.go +++ b/tstest/integration/tailscaled_deps_test_darwin.go @@ -10,7 +10,6 @@ import ( // Otherwise cmd/go never sees that we depend on these packages' // transitive deps when we run "go install tailscaled" in a child // process and can cache a prior success when a dependency changes. - _ "tailscale.com/chirp" _ "tailscale.com/client/local" _ "tailscale.com/cmd/tailscaled/childproc" _ "tailscale.com/control/controlclient" diff --git a/tstest/integration/tailscaled_deps_test_freebsd.go b/tstest/integration/tailscaled_deps_test_freebsd.go index 3132739f6..9a19b3997 100644 --- a/tstest/integration/tailscaled_deps_test_freebsd.go +++ b/tstest/integration/tailscaled_deps_test_freebsd.go @@ -10,7 +10,6 @@ import ( // Otherwise cmd/go never sees that we depend on these packages' // transitive deps when we run "go install tailscaled" in a child // process and can cache a prior success when a dependency changes. - _ "tailscale.com/chirp" _ "tailscale.com/client/local" _ "tailscale.com/cmd/tailscaled/childproc" _ "tailscale.com/control/controlclient" diff --git a/tstest/integration/tailscaled_deps_test_linux.go b/tstest/integration/tailscaled_deps_test_linux.go index 3132739f6..9a19b3997 100644 --- a/tstest/integration/tailscaled_deps_test_linux.go +++ b/tstest/integration/tailscaled_deps_test_linux.go @@ -10,7 +10,6 @@ import ( // Otherwise cmd/go never sees that we depend on these packages' // transitive deps when we run "go install tailscaled" in a child // process and can cache a prior success when a dependency changes. - _ "tailscale.com/chirp" _ "tailscale.com/client/local" _ "tailscale.com/cmd/tailscaled/childproc" _ "tailscale.com/control/controlclient" diff --git a/tstest/integration/tailscaled_deps_test_openbsd.go b/tstest/integration/tailscaled_deps_test_openbsd.go index 3132739f6..9a19b3997 100644 --- a/tstest/integration/tailscaled_deps_test_openbsd.go +++ b/tstest/integration/tailscaled_deps_test_openbsd.go @@ -10,7 +10,6 @@ import ( // Otherwise cmd/go never sees that we depend on these packages' // transitive deps when we run "go install tailscaled" in a child // process and can cache a prior success when a dependency changes. - _ "tailscale.com/chirp" _ "tailscale.com/client/local" _ "tailscale.com/cmd/tailscaled/childproc" _ "tailscale.com/control/controlclient" diff --git a/wgengine/bird_hooks.go b/wgengine/bird_hooks.go new file mode 100644 index 000000000..f388ee792 --- /dev/null +++ b/wgengine/bird_hooks.go @@ -0,0 +1,41 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package wgengine + +import ( + "tailscale.com/feature" + "tailscale.com/tailcfg" + "tailscale.com/types/logger" +) + +// Bird is the engine's handle on the BIRD Internet Routing Daemon +// integration, implemented by the feature/bird package. It enables the +// "tailscale" protocol in BIRD while this node is a primary subnet +// router and disables it otherwise. +// +// Reconfig and ReconfigDone are only called from [Engine.Reconfig], +// serialized by the engine's internal lock. Close is called from +// [Engine.Close]. +type Bird interface { + // Reconfig recomputes from the given self node whether this node is + // a primary subnet router. It is called near the top of every + // [Engine.Reconfig], before its ErrNoChanges early return. It + // reports whether the primary subnet router state changed, which + // suppresses the engine's ErrNoChanges early return. + Reconfig(self tailcfg.NodeView) (changed bool) + + // ReconfigDone is called at the end of [Engine.Reconfig], after the + // router is configured, and applies any protocol state change + // computed by the preceding Reconfig call. + ReconfigDone() + + // Close disables the protocol and closes the connection to BIRD. + Close() +} + +// HookNewBird is set by the feature/bird package to construct each +// engine's [Bird], connecting to the BIRD unix socket at socketPath. +// If unset, BIRD integration is not linked into the binary and +// [Config.BIRDSocket] must not be set. +var HookNewBird feature.Hook[func(logf logger.Logf, socketPath string) (Bird, error)] diff --git a/wgengine/userspace.go b/wgengine/userspace.go index f5da3dcea..258f79c40 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -85,9 +85,13 @@ type userspaceEngine struct { netMon *netmon.Monitor health *health.Tracker netMonOwned bool // whether we created netMon (and thus need to close it) - birdClient BIRDClient // or nil controlKnobs *controlknobs.Knobs // or nil + // bird is the BIRD integration handle constructed via + // [HookNewBird], or nil if [Config.BIRDSocket] was empty or the + // feature/bird package is not linked into the binary. + bird Bird + testMaybeReconfigHook func() // for tests; if non-nil, fires if maybeReconfigWireguardLocked called // isLocalAddr reports the whether an IP is assigned to the local @@ -112,11 +116,10 @@ type userspaceEngine struct { // no longer install per-config lookup closures. peerConfigFn atomic.Pointer[func(key.NodePublic) (allowedIPs []netip.Prefix, ok bool)] - lastCfgFull wgcfg.Config - lastRouter *router.Config - lastDNSConfig dns.ConfigView // or invalid if none - lastIsSubnetRouter bool // was the node a primary subnet router in the last run. - reconfigureVPN func() error // or nil + lastCfgFull wgcfg.Config + lastRouter *router.Config + lastDNSConfig dns.ConfigView // or invalid if none + reconfigureVPN func() error // or nil // lastAppliedDisableTUNUDPGRO and lastAppliedDisableTUNTCPGRO cache the // controlknobs values that were last applied to the TUN device. They are @@ -162,13 +165,6 @@ type userspaceEngine struct { // Lock ordering: magicsock.Conn.mu, wgLock, then mu. } -// BIRDClient handles communication with the BIRD Internet Routing Daemon. -type BIRDClient interface { - EnableProtocol(proto string) error - DisableProtocol(proto string) error - Close() error -} - // Config is the engine configuration. type Config struct { // Tun is the device used by the Engine to exchange packets with @@ -228,9 +224,10 @@ type Config struct { // Used in "fake" mode for development. RespondToPing bool - // BIRDClient, if non-nil, will be used to configure BIRD whenever - // this node is a primary subnet router. - BIRDClient BIRDClient + // BIRDSocket, if non-empty, is the path of the BIRD unix socket to + // configure whenever this node is a primary subnet router. It + // requires the feature/bird package to be linked in. + BIRDSocket string // SetSubsystem, if non-nil, is called for each new subsystem created, just before a successful return. SetSubsystem func(any) @@ -369,17 +366,21 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) router: rtr, dialer: conf.Dialer, confListenPort: conf.ListenPort, - birdClient: conf.BIRDClient, controlKnobs: conf.ControlKnobs, reconfigureVPN: conf.ReconfigureVPN, health: conf.HealthTracker, } - if e.birdClient != nil { - // Disable the protocol at start time. - if err := e.birdClient.DisableProtocol("tailscale"); err != nil { + if buildfeatures.HasBird && conf.BIRDSocket != "" { + newBird, ok := HookNewBird.GetOk() + if !ok { + return nil, errors.New("wgengine: Config.BIRDSocket set but the feature/bird package is not linked in") + } + bird, err := newBird(logf, conf.BIRDSocket) + if err != nil { return nil, err } + e.bird = bird } e.isLocalAddr.Store(ipset.FalseContainsIPFunc()) e.isDNSIPOverTailscale.Store(ipset.FalseContainsIPFunc()) @@ -827,17 +828,6 @@ func peerWireGuardStateFromDevice(state device.PeerSessionState) PeerWireGuardSt } } -// hasOverlap checks if there is a IPPrefix which is common amongst the two -// provided slices. -func hasOverlap(aips, rips views.Slice[netip.Prefix]) bool { - for _, aip := range aips.All() { - if views.SliceContains(rips, aip) { - return true - } - } - return false -} - // ResetAndStop resets the engine to a clean state (like calling Reconfig // with all pointers to zero values) and returns the resulting status. // @@ -877,14 +867,14 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, peerMTUEnable := e.magicConn.ShouldPMTUD() - isSubnetRouter := false - if buildfeatures.HasBird && e.birdClient != nil && self.Valid() { - isSubnetRouter = hasOverlap(self.PrimaryRoutes(), self.Hostinfo().RoutableIPs()) - e.logf("[v1] Reconfig: hasOverlap(%v, %v) = %v; isSubnetRouter=%v lastIsSubnetRouter=%v", - self.PrimaryRoutes(), self.Hostinfo().RoutableIPs(), - isSubnetRouter, isSubnetRouter, e.lastIsSubnetRouter) + // Let the BIRD integration recompute whether this node is a + // primary subnet router, before the early return below so that a + // change in that state alone still reaches the protocol toggle in + // ReconfigDone at the end. + birdChanged := false + if e.bird != nil { + birdChanged = e.bird.Reconfig(self) } - isSubnetRouterChanged := buildfeatures.HasAdvertiseRoutes && isSubnetRouter != e.lastIsSubnetRouter engineChanged := !e.lastCfgFull.Equal(cfg) routerChanged := checkchange.Update(&e.lastRouter, routerCfg) @@ -905,7 +895,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, netlogChanged = e.netlogger.Reconfig(routerCfg, routerChanged) } - if !engineChanged && !routerChanged && !dnsChanged && !listenPortChanged && !isSubnetRouterChanged && !peerMTUChanged && !netlogChanged { + if !engineChanged && !routerChanged && !dnsChanged && !listenPortChanged && !birdChanged && !peerMTUChanged && !netlogChanged { return ErrNoChanges } @@ -990,20 +980,10 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, e.netlogger.ReconfigDone() } - if buildfeatures.HasBird && isSubnetRouterChanged && e.birdClient != nil { - e.logf("wgengine: Reconfig: configuring BIRD") - var err error - if isSubnetRouter { - err = e.birdClient.EnableProtocol("tailscale") - } else { - err = e.birdClient.DisableProtocol("tailscale") - } - if err != nil { - // Log but don't fail here. - e.logf("wgengine: error configuring BIRD: %v", err) - } else { - e.lastIsSubnetRouter = isSubnetRouter - } + // Let the BIRD integration apply any protocol state change now, + // after the router is configured. + if e.bird != nil { + e.bird.ReconfigDone() } e.logf("[v1] wgengine: Reconfig done") @@ -1174,9 +1154,8 @@ func (e *userspaceEngine) Close() { e.router.Close() e.wgdev.Close() e.tundev.Close() - if e.birdClient != nil { - e.birdClient.DisableProtocol("tailscale") - e.birdClient.Close() + if e.bird != nil { + e.bird.Close() } close(e.waitCh)