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
Maisem Ali 3 years ago committed by Maisem Ali
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…
Cancel
Save