diff --git a/chirp/chirp.go b/chirp/chirp.go new file mode 100644 index 000000000..fc717be19 --- /dev/null +++ b/chirp/chirp.go @@ -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 +} diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 8762d12d0..44e8d12c4 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -88,6 +88,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de inet.af/peercred from tailscale.com/ipn/ipnserver W 💣 inet.af/wf from tailscale.com/wf 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/apitype from tailscale.com/ipn/ipnlocal+ tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 81a7c7053..8e1e19945 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -73,18 +73,20 @@ var args struct { // or comma-separated list thereof. tunname string - cleanup bool - debug string - port uint16 - statepath string - socketpath string - verbose int - socksAddr string // listen address for SOCKS5 server + cleanup bool + debug string + port uint16 + statepath string + socketpath string + birdSocketPath string + verbose int + socksAddr string // listen address for SOCKS5 server } var ( - installSystemDaemon func([]string) error // non-nil on some platforms - uninstallSystemDaemon 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 + createBIRDClient func(string) (wgengine.BIRDClient, error) // non-nil on some platforms ) 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.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.birdSocketPath, "bird-socket", "", "path of the bird unix socket") flag.BoolVar(&printVersion, "version", false, "print version information and exit") if len(os.Args) > 1 { @@ -152,6 +155,11 @@ func main() { 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() // 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, 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" if !useNetstack { dev, devName, err := tstun.New(logf, name) diff --git a/cmd/tailscaled/tailscaled_bird.go b/cmd/tailscaled/tailscaled_bird.go new file mode 100644 index 000000000..2be2a1263 --- /dev/null +++ b/cmd/tailscaled/tailscaled_bird.go @@ -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) + } +} diff --git a/docs/bird/sample_bird.conf b/docs/bird/sample_bird.conf new file mode 100644 index 000000000..ed38e66c5 --- /dev/null +++ b/docs/bird/sample_bird.conf @@ -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"; diff --git a/docs/bird/tailscale_bird.conf b/docs/bird/tailscale_bird.conf new file mode 100644 index 000000000..8211a50a3 --- /dev/null +++ b/docs/bird/tailscale_bird.conf @@ -0,0 +1,4 @@ +protocol static tailscale { + ipv4; + route 100.64.0.0/10 via "tailscale0"; +} diff --git a/tstest/integration/tailscaled_deps_test_darwin.go b/tstest/integration/tailscaled_deps_test_darwin.go index 0f1a8ff42..41585b78d 100644 --- a/tstest/integration/tailscaled_deps_test_darwin.go +++ b/tstest/integration/tailscaled_deps_test_darwin.go @@ -36,6 +36,7 @@ import ( _ "strconv" _ "strings" _ "syscall" + _ "tailscale.com/chirp" _ "tailscale.com/derp/derphttp" _ "tailscale.com/ipn" _ "tailscale.com/ipn/ipnserver" diff --git a/tstest/integration/tailscaled_deps_test_freebsd.go b/tstest/integration/tailscaled_deps_test_freebsd.go index 0de0939a9..6e7d939d5 100644 --- a/tstest/integration/tailscaled_deps_test_freebsd.go +++ b/tstest/integration/tailscaled_deps_test_freebsd.go @@ -34,6 +34,7 @@ import ( _ "strconv" _ "strings" _ "syscall" + _ "tailscale.com/chirp" _ "tailscale.com/derp/derphttp" _ "tailscale.com/ipn" _ "tailscale.com/ipn/ipnserver" diff --git a/tstest/integration/tailscaled_deps_test_linux.go b/tstest/integration/tailscaled_deps_test_linux.go index 0de0939a9..6e7d939d5 100644 --- a/tstest/integration/tailscaled_deps_test_linux.go +++ b/tstest/integration/tailscaled_deps_test_linux.go @@ -34,6 +34,7 @@ import ( _ "strconv" _ "strings" _ "syscall" + _ "tailscale.com/chirp" _ "tailscale.com/derp/derphttp" _ "tailscale.com/ipn" _ "tailscale.com/ipn/ipnserver" diff --git a/tstest/integration/tailscaled_deps_test_openbsd.go b/tstest/integration/tailscaled_deps_test_openbsd.go index 0de0939a9..6e7d939d5 100644 --- a/tstest/integration/tailscaled_deps_test_openbsd.go +++ b/tstest/integration/tailscaled_deps_test_openbsd.go @@ -34,6 +34,7 @@ import ( _ "strconv" _ "strings" _ "syscall" + _ "tailscale.com/chirp" _ "tailscale.com/derp/derphttp" _ "tailscale.com/ipn" _ "tailscale.com/ipn/ipnserver" diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 159e46b9a..6f37f2cd2 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -93,8 +93,9 @@ type userspaceEngine struct { dns *dns.Manager magicConn *magicsock.Conn linkMon *monitor.Mon - linkMonOwned bool // whether we created linkMon (and thus need to close it) - linkMonUnregister func() // unsubscribes from changes; used regardless of linkMonOwned + linkMonOwned bool // whether we created linkMon (and thus need to close it) + linkMonUnregister func() // unsubscribes from changes; used regardless of linkMonOwned + birdClient BIRDClient // or nil testMaybeReconfigHook func() // for tests; if non-nil, fires if maybeReconfigWireguardLocked called @@ -121,6 +122,8 @@ type userspaceEngine struct { statusBufioReader *bufio.Reader // reusable for UAPI 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 netMap *netmap.NetworkMap // or nil 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 } +// 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 @@ -175,6 +185,10 @@ type Config struct { // reply to ICMP pings, without involving the OS. // 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 } 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, confListenPort: conf.ListenPort, 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.isDNSIPOverTailscale.Store(tsaddr.NewContainsIPFunc(nil)) @@ -759,6 +781,19 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey 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 { if routerCfg == nil { panic("routerCfg must not be nil") @@ -787,9 +822,15 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, 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) routerChanged := deephash.Update(&e.lastRouterSig, routerCfg, dnsCfg) - if !engineChanged && !routerChanged && listenPort == e.magicConn.LocalPort() { + if !engineChanged && !routerChanged && listenPort == e.magicConn.LocalPort() && !isSubnetRouterChanged { 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") return nil } @@ -1077,6 +1134,10 @@ func (e *userspaceEngine) Close() { e.router.Close() e.wgdev.Close() e.tundev.Close() + if e.birdClient != nil { + e.birdClient.DisableProtocol("tailscale") + e.birdClient.Close() + } close(e.waitCh) }