| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- //go:build linux
- package main
- import (
- "context"
- "errors"
- "fmt"
- "io/fs"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "reflect"
- "strings"
- "syscall"
- "time"
- "github.com/fsnotify/fsnotify"
- "tailscale.com/client/tailscale"
- )
- func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, *os.Process, error) {
- args := tailscaledArgs(cfg)
- // tailscaled runs without context, since it needs to persist
- // beyond the startup timeout in ctx.
- cmd := exec.Command("tailscaled", args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Setpgid: true,
- }
- log.Printf("Starting tailscaled")
- if err := cmd.Start(); err != nil {
- return nil, nil, fmt.Errorf("starting tailscaled failed: %v", err)
- }
- // Wait for the socket file to appear, otherwise API ops will racily fail.
- log.Printf("Waiting for tailscaled socket")
- for {
- if ctx.Err() != nil {
- return nil, nil, errors.New("timed out waiting for tailscaled socket")
- }
- _, err := os.Stat(cfg.Socket)
- if errors.Is(err, fs.ErrNotExist) {
- time.Sleep(100 * time.Millisecond)
- continue
- } else if err != nil {
- return nil, nil, fmt.Errorf("error waiting for tailscaled socket: %w", err)
- }
- break
- }
- tsClient := &tailscale.LocalClient{
- Socket: cfg.Socket,
- UseSocketOnly: true,
- }
- return tsClient, cmd.Process, nil
- }
- // tailscaledArgs uses cfg to construct the argv for tailscaled.
- func tailscaledArgs(cfg *settings) []string {
- args := []string{"--socket=" + cfg.Socket}
- switch {
- case cfg.InKubernetes && cfg.KubeSecret != "":
- args = append(args, "--state=kube:"+cfg.KubeSecret)
- if cfg.StateDir == "" {
- cfg.StateDir = "/tmp"
- }
- fallthrough
- case cfg.StateDir != "":
- args = append(args, "--statedir="+cfg.StateDir)
- default:
- args = append(args, "--state=mem:", "--statedir=/tmp")
- }
- if cfg.UserspaceMode {
- args = append(args, "--tun=userspace-networking")
- } else if err := ensureTunFile(cfg.Root); err != nil {
- log.Fatalf("ensuring that /dev/net/tun exists: %v", err)
- }
- if cfg.SOCKSProxyAddr != "" {
- args = append(args, "--socks5-server="+cfg.SOCKSProxyAddr)
- }
- if cfg.HTTPProxyAddr != "" {
- args = append(args, "--outbound-http-proxy-listen="+cfg.HTTPProxyAddr)
- }
- if cfg.TailscaledConfigFilePath != "" {
- args = append(args, "--config="+cfg.TailscaledConfigFilePath)
- }
- // Once enough proxy versions have been released for all the supported
- // versions to understand this cfg setting, the operator can stop
- // setting TS_TAILSCALED_EXTRA_ARGS for the debug flag.
- if cfg.DebugAddrPort != "" && !strings.Contains(cfg.DaemonExtraArgs, cfg.DebugAddrPort) {
- args = append(args, "--debug="+cfg.DebugAddrPort)
- }
- if cfg.DaemonExtraArgs != "" {
- args = append(args, strings.Fields(cfg.DaemonExtraArgs)...)
- }
- return args
- }
- // tailscaleUp uses cfg to run 'tailscale up' everytime containerboot starts, or
- // if TS_AUTH_ONCE is set, only the first time containerboot starts.
- func tailscaleUp(ctx context.Context, cfg *settings) error {
- args := []string{"--socket=" + cfg.Socket, "up"}
- if cfg.AcceptDNS != nil && *cfg.AcceptDNS {
- args = append(args, "--accept-dns=true")
- } else {
- args = append(args, "--accept-dns=false")
- }
- if cfg.AuthKey != "" {
- args = append(args, "--authkey="+cfg.AuthKey)
- }
- // --advertise-routes can be passed an empty string to configure a
- // device (that might have previously advertised subnet routes) to not
- // advertise any routes. Respect an empty string passed by a user and
- // use it to explicitly unset the routes.
- if cfg.Routes != nil {
- args = append(args, "--advertise-routes="+*cfg.Routes)
- }
- if cfg.Hostname != "" {
- args = append(args, "--hostname="+cfg.Hostname)
- }
- if cfg.ExtraArgs != "" {
- args = append(args, strings.Fields(cfg.ExtraArgs)...)
- }
- log.Printf("Running 'tailscale up'")
- cmd := exec.CommandContext(ctx, "tailscale", args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("tailscale up failed: %v", err)
- }
- return nil
- }
- // tailscaleSet uses cfg to run 'tailscale set' to set any known configuration
- // options that are passed in via environment variables. This is run after the
- // node is in Running state and only if TS_AUTH_ONCE is set.
- func tailscaleSet(ctx context.Context, cfg *settings) error {
- args := []string{"--socket=" + cfg.Socket, "set"}
- if cfg.AcceptDNS != nil && *cfg.AcceptDNS {
- args = append(args, "--accept-dns=true")
- } else {
- args = append(args, "--accept-dns=false")
- }
- // --advertise-routes can be passed an empty string to configure a
- // device (that might have previously advertised subnet routes) to not
- // advertise any routes. Respect an empty string passed by a user and
- // use it to explicitly unset the routes.
- if cfg.Routes != nil {
- args = append(args, "--advertise-routes="+*cfg.Routes)
- }
- if cfg.Hostname != "" {
- args = append(args, "--hostname="+cfg.Hostname)
- }
- log.Printf("Running 'tailscale set'")
- cmd := exec.CommandContext(ctx, "tailscale", args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("tailscale set failed: %v", err)
- }
- return nil
- }
- func watchTailscaledConfigChanges(ctx context.Context, path string, lc *tailscale.LocalClient, errCh chan<- error) {
- var (
- tickChan <-chan time.Time
- tailscaledCfgDir = filepath.Dir(path)
- prevTailscaledCfg []byte
- )
- w, err := fsnotify.NewWatcher()
- if err != nil {
- log.Printf("tailscaled config watch: failed to create fsnotify watcher, timer-only mode: %v", err)
- ticker := time.NewTicker(5 * time.Second)
- defer ticker.Stop()
- tickChan = ticker.C
- } else {
- defer w.Close()
- if err := w.Add(tailscaledCfgDir); err != nil {
- errCh <- fmt.Errorf("failed to add fsnotify watch: %w", err)
- return
- }
- }
- b, err := os.ReadFile(path)
- if err != nil {
- errCh <- fmt.Errorf("error reading configfile: %w", err)
- return
- }
- prevTailscaledCfg = b
- // kubelet mounts Secrets to Pods using a series of symlinks, one of
- // which is <mount-dir>/..data that Kubernetes recommends consumers to
- // use if they need to monitor changes
- // https://github.com/kubernetes/kubernetes/blob/v1.28.1/pkg/volume/util/atomic_writer.go#L39-L61
- const kubeletMountedCfg = "..data"
- toWatch := filepath.Join(tailscaledCfgDir, kubeletMountedCfg)
- for {
- select {
- case <-ctx.Done():
- return
- case err := <-w.Errors:
- errCh <- fmt.Errorf("watcher error: %w", err)
- return
- case <-tickChan:
- case event := <-w.Events:
- if event.Name != toWatch {
- continue
- }
- }
- b, err := os.ReadFile(path)
- if err != nil {
- errCh <- fmt.Errorf("error reading configfile: %w", err)
- return
- }
- // For some proxy types the mounted volume also contains tailscaled state and other files. We
- // don't want to reload config unnecessarily on unrelated changes to these files.
- if reflect.DeepEqual(b, prevTailscaledCfg) {
- continue
- }
- prevTailscaledCfg = b
- log.Printf("tailscaled config watch: ensuring that config is up to date")
- ok, err := lc.ReloadConfig(ctx)
- if err != nil {
- errCh <- fmt.Errorf("error reloading tailscaled config: %w", err)
- return
- }
- if ok {
- log.Printf("tailscaled config watch: config was reloaded")
- }
- }
- }
|