mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-21 12:28:39 +00:00

cmd/{k8s-operator,containerboot}: reload tailscaled configfile when its contents have changed Instead of restarting the Kubernetes Operator proxies each time tailscaled config has changed, this dynamically reloads the configfile using the new reload endpoint. Older annotation based mechanism will be supported till 1.84 to ensure that proxy versions prior to 1.80 keep working with operator 1.80 and newer. Updates tailscale/tailscale#13032 Updates tailscale/corp#24795 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
239 lines
7.1 KiB
Go
239 lines
7.1 KiB
Go
// 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 <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")
|
|
}
|
|
}
|
|
}
|