// 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 { log.Fatalf("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 { log.Fatalf("Waiting for tailscaled socket: %v", 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 /..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") } } }