WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
14 changed files with 419 additions and 88 deletions
Showing only changes of commit 3d07b5d6e1 - Show all commits
+2 -1
View File
@@ -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
+14
View File
@@ -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",
+5 -10
View File
@@ -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 {
-17
View File
@@ -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)
}
}
+107
View File
@@ -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
}
+205
View File
@@ -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")
}
+8
View File
@@ -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"
+2
View File
@@ -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",
@@ -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"
@@ -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"
@@ -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"
@@ -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"
+41
View File
@@ -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)]
+35 -56
View File
@@ -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)