wgengine/userspace: add support to automatically enable/disable the tailscale
protocol in BIRD, when the node is a primary subnet router as determined by control. Signed-off-by: Maisem Ali <maisem@tailscale.com>pull/2508/merge
parent
7fcf86a14a
commit
fd4838dc57
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package chirp implements a client to communicate with the BIRD Internet
|
||||||
|
// Routing Daemon.
|
||||||
|
package chirp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a BIRDClient.
|
||||||
|
func New(socket string) (*BIRDClient, error) {
|
||||||
|
conn, err := net.Dial("unix", socket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||||
|
}
|
||||||
|
b := &BIRDClient{socket: socket, conn: conn, bs: bufio.NewScanner(conn)}
|
||||||
|
// Read and discard the first line as that is the welcome message.
|
||||||
|
if _, err := b.readLine(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
|
||||||
|
type BIRDClient struct {
|
||||||
|
socket string
|
||||||
|
conn net.Conn
|
||||||
|
bs *bufio.Scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying connection to BIRD.
|
||||||
|
func (b *BIRDClient) Close() error { return b.conn.Close() }
|
||||||
|
|
||||||
|
// DisableProtocol disables the provided protocol.
|
||||||
|
func (b *BIRDClient) DisableProtocol(protocol string) error {
|
||||||
|
out, err := b.exec("disable %s\n", protocol)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.Contains(out, fmt.Sprintf("%s: already disabled", protocol)) {
|
||||||
|
return nil
|
||||||
|
} else if strings.Contains(out, fmt.Sprintf("%s: disabled", protocol)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to disable %s: %v", protocol, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableProtocol enables the provided protocol.
|
||||||
|
func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||||
|
out, err := b.exec("enable %s\n", protocol)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.Contains(out, fmt.Sprintf("%s: already enabled", protocol)) {
|
||||||
|
return nil
|
||||||
|
} else if strings.Contains(out, fmt.Sprintf("%s: enabled", protocol)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to enable %s: %v", protocol, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BIRDClient) exec(cmd string, args ...interface{}) (string, error) {
|
||||||
|
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return b.readLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BIRDClient) readLine() (string, error) {
|
||||||
|
if !b.bs.Scan() {
|
||||||
|
return "", fmt.Errorf("reading response from bird failed")
|
||||||
|
}
|
||||||
|
if err := b.bs.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return b.bs.Text(), nil
|
||||||
|
}
|
|
@ -88,6 +88,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||||
W 💣 inet.af/wf from tailscale.com/wf
|
W 💣 inet.af/wf from tailscale.com/wf
|
||||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||||
|
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
||||||
tailscale.com/client/tailscale from tailscale.com/derp
|
tailscale.com/client/tailscale from tailscale.com/derp
|
||||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||||
|
|
|
@ -73,18 +73,20 @@ var args struct {
|
||||||
// or comma-separated list thereof.
|
// or comma-separated list thereof.
|
||||||
tunname string
|
tunname string
|
||||||
|
|
||||||
cleanup bool
|
cleanup bool
|
||||||
debug string
|
debug string
|
||||||
port uint16
|
port uint16
|
||||||
statepath string
|
statepath string
|
||||||
socketpath string
|
socketpath string
|
||||||
verbose int
|
birdSocketPath string
|
||||||
socksAddr string // listen address for SOCKS5 server
|
verbose int
|
||||||
|
socksAddr string // listen address for SOCKS5 server
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||||
uninstallSystemDaemon 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
|
||||||
)
|
)
|
||||||
|
|
||||||
var subCommands = map[string]*func([]string) error{
|
var subCommands = map[string]*func([]string) error{
|
||||||
|
@ -111,6 +113,7 @@ func main() {
|
||||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||||
|
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||||
|
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
|
@ -152,6 +155,11 @@ func main() {
|
||||||
log.Fatalf("--socket is required")
|
log.Fatalf("--socket is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.birdSocketPath != "" && createBIRDClient == nil {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
err := run()
|
err := run()
|
||||||
|
|
||||||
// Remove file sharing from Windows shell (noop in non-windows)
|
// Remove file sharing from Windows shell (noop in non-windows)
|
||||||
|
@ -379,6 +387,13 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||||
ListenPort: args.port,
|
ListenPort: args.port,
|
||||||
LinkMonitor: linkMon,
|
LinkMonitor: linkMon,
|
||||||
}
|
}
|
||||||
|
if args.birdSocketPath != "" && createBIRDClient != nil {
|
||||||
|
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
||||||
|
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
useNetstack = name == "userspace-networking"
|
useNetstack = name == "userspace-networking"
|
||||||
if !useNetstack {
|
if !useNetstack {
|
||||||
dev, devName, err := tstun.New(logf, name)
|
dev, devName, err := tstun.New(logf, name)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux || darwin || freebsd || openbsd
|
||||||
|
// +build linux darwin freebsd openbsd
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tailscale.com/chirp"
|
||||||
|
"tailscale.com/wgengine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
createBIRDClient = func(ctlSocket string) (wgengine.BIRDClient, error) {
|
||||||
|
return chirp.New(ctlSocket)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
log syslog all;
|
||||||
|
|
||||||
|
protocol device {
|
||||||
|
scan time 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol bgp {
|
||||||
|
local as 64001;
|
||||||
|
neighbor 10.40.2.101 as 64002;
|
||||||
|
ipv4 {
|
||||||
|
import none;
|
||||||
|
export all;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
include "tailscale_bird.conf";
|
|
@ -0,0 +1,4 @@
|
||||||
|
protocol static tailscale {
|
||||||
|
ipv4;
|
||||||
|
route 100.64.0.0/10 via "tailscale0";
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import (
|
||||||
_ "strconv"
|
_ "strconv"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
_ "syscall"
|
_ "syscall"
|
||||||
|
_ "tailscale.com/chirp"
|
||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/ipn"
|
_ "tailscale.com/ipn"
|
||||||
_ "tailscale.com/ipn/ipnserver"
|
_ "tailscale.com/ipn/ipnserver"
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
_ "strconv"
|
_ "strconv"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
_ "syscall"
|
_ "syscall"
|
||||||
|
_ "tailscale.com/chirp"
|
||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/ipn"
|
_ "tailscale.com/ipn"
|
||||||
_ "tailscale.com/ipn/ipnserver"
|
_ "tailscale.com/ipn/ipnserver"
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
_ "strconv"
|
_ "strconv"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
_ "syscall"
|
_ "syscall"
|
||||||
|
_ "tailscale.com/chirp"
|
||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/ipn"
|
_ "tailscale.com/ipn"
|
||||||
_ "tailscale.com/ipn/ipnserver"
|
_ "tailscale.com/ipn/ipnserver"
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
_ "strconv"
|
_ "strconv"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
_ "syscall"
|
_ "syscall"
|
||||||
|
_ "tailscale.com/chirp"
|
||||||
_ "tailscale.com/derp/derphttp"
|
_ "tailscale.com/derp/derphttp"
|
||||||
_ "tailscale.com/ipn"
|
_ "tailscale.com/ipn"
|
||||||
_ "tailscale.com/ipn/ipnserver"
|
_ "tailscale.com/ipn/ipnserver"
|
||||||
|
|
|
@ -93,8 +93,9 @@ type userspaceEngine struct {
|
||||||
dns *dns.Manager
|
dns *dns.Manager
|
||||||
magicConn *magicsock.Conn
|
magicConn *magicsock.Conn
|
||||||
linkMon *monitor.Mon
|
linkMon *monitor.Mon
|
||||||
linkMonOwned bool // whether we created linkMon (and thus need to close it)
|
linkMonOwned bool // whether we created linkMon (and thus need to close it)
|
||||||
linkMonUnregister func() // unsubscribes from changes; used regardless of linkMonOwned
|
linkMonUnregister func() // unsubscribes from changes; used regardless of linkMonOwned
|
||||||
|
birdClient BIRDClient // or nil
|
||||||
|
|
||||||
testMaybeReconfigHook func() // for tests; if non-nil, fires if maybeReconfigWireguardLocked called
|
testMaybeReconfigHook func() // for tests; if non-nil, fires if maybeReconfigWireguardLocked called
|
||||||
|
|
||||||
|
@ -121,6 +122,8 @@ type userspaceEngine struct {
|
||||||
statusBufioReader *bufio.Reader // reusable for UAPI
|
statusBufioReader *bufio.Reader // reusable for UAPI
|
||||||
lastStatusPollTime mono.Time // last time we polled the engine status
|
lastStatusPollTime mono.Time // last time we polled the engine status
|
||||||
|
|
||||||
|
lastIsSubnetRouter bool // was the node a primary subnet router in the last run.
|
||||||
|
|
||||||
mu sync.Mutex // guards following; see lock order comment below
|
mu sync.Mutex // guards following; see lock order comment below
|
||||||
netMap *netmap.NetworkMap // or nil
|
netMap *netmap.NetworkMap // or nil
|
||||||
closing bool // Close was called (even if we're still closing)
|
closing bool // Close was called (even if we're still closing)
|
||||||
|
@ -144,6 +147,13 @@ func (e *userspaceEngine) GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, o
|
||||||
return e.tundev, e.magicConn, true
|
return e.tundev, e.magicConn, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Config is the engine configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Tun is the device used by the Engine to exchange packets with
|
// Tun is the device used by the Engine to exchange packets with
|
||||||
|
@ -175,6 +185,10 @@ type Config struct {
|
||||||
// reply to ICMP pings, without involving the OS.
|
// reply to ICMP pings, without involving the OS.
|
||||||
// Used in "fake" mode for development.
|
// Used in "fake" mode for development.
|
||||||
RespondToPing bool
|
RespondToPing bool
|
||||||
|
|
||||||
|
// BIRDClient, if non-nil, will be used to configure BIRD whenever
|
||||||
|
// this node is a primary subnet router.
|
||||||
|
BIRDClient BIRDClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) {
|
func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) {
|
||||||
|
@ -258,6 +272,14 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||||
router: conf.Router,
|
router: conf.Router,
|
||||||
confListenPort: conf.ListenPort,
|
confListenPort: conf.ListenPort,
|
||||||
magicConnStarted: make(chan struct{}),
|
magicConnStarted: make(chan struct{}),
|
||||||
|
birdClient: conf.BIRDClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.birdClient != nil {
|
||||||
|
// Disable the protocol at start time.
|
||||||
|
if err := e.birdClient.DisableProtocol("tailscale"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(nil))
|
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(nil))
|
||||||
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(nil))
|
e.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(nil))
|
||||||
|
@ -759,6 +781,19 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey
|
||||||
e.tundev.SetDestIPActivityFuncs(e.destIPActivityFuncs)
|
e.tundev.SetDestIPActivityFuncs(e.destIPActivityFuncs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasOverlap checks if there is a IPPrefix which is common amongst the two
|
||||||
|
// provided slices.
|
||||||
|
func hasOverlap(aips, rips []netaddr.IPPrefix) bool {
|
||||||
|
for _, aip := range aips {
|
||||||
|
for _, rip := range rips {
|
||||||
|
if aip == rip {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config, debug *tailcfg.Debug) error {
|
func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config, debug *tailcfg.Debug) error {
|
||||||
if routerCfg == nil {
|
if routerCfg == nil {
|
||||||
panic("routerCfg must not be nil")
|
panic("routerCfg must not be nil")
|
||||||
|
@ -787,9 +822,15 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
||||||
listenPort = 0
|
listenPort = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSubnetRouter := false
|
||||||
|
if e.birdClient != nil {
|
||||||
|
isSubnetRouter = hasOverlap(e.netMap.SelfNode.PrimaryRoutes, e.netMap.Hostinfo.RoutableIPs)
|
||||||
|
}
|
||||||
|
isSubnetRouterChanged := isSubnetRouter != e.lastIsSubnetRouter
|
||||||
|
|
||||||
engineChanged := deephash.Update(&e.lastEngineSigFull, cfg)
|
engineChanged := deephash.Update(&e.lastEngineSigFull, cfg)
|
||||||
routerChanged := deephash.Update(&e.lastRouterSig, routerCfg, dnsCfg)
|
routerChanged := deephash.Update(&e.lastRouterSig, routerCfg, dnsCfg)
|
||||||
if !engineChanged && !routerChanged && listenPort == e.magicConn.LocalPort() {
|
if !engineChanged && !routerChanged && listenPort == e.magicConn.LocalPort() && !isSubnetRouterChanged {
|
||||||
return ErrNoChanges
|
return ErrNoChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -859,6 +900,22 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
e.logf("[v1] wgengine: Reconfig done")
|
e.logf("[v1] wgengine: Reconfig done")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1077,6 +1134,10 @@ func (e *userspaceEngine) Close() {
|
||||||
e.router.Close()
|
e.router.Close()
|
||||||
e.wgdev.Close()
|
e.wgdev.Close()
|
||||||
e.tundev.Close()
|
e.tundev.Close()
|
||||||
|
if e.birdClient != nil {
|
||||||
|
e.birdClient.DisableProtocol("tailscale")
|
||||||
|
e.birdClient.Close()
|
||||||
|
}
|
||||||
close(e.waitCh)
|
close(e.waitCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue