summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cmd/containerboot/main.go151
-rw-r--r--cmd/containerboot/main_test.go18
2 files changed, 116 insertions, 53 deletions
diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go
index 8ce7e731c..f15df47ee 100644
--- a/cmd/containerboot/main.go
+++ b/cmd/containerboot/main.go
@@ -48,6 +48,13 @@
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
// It cannot be used in conjunction with TS_DEST_IP. The file is watched for changes,
// and will be re-applied when it changes.
+// - TS_EXPERIMENTAL_CONFIGFILE_PATH: if specified, a path to tailscaled
+// config. If this is set, TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY,
+// TS_ROUTES, TS_ACCEPT_DNS env vars must not be set. If this is set,
+// containerboot only runs `tailscaled --config <path-to-this-configfile>`
+// and not `tailscale up` or `tailscale set`.
+// The config file contents are currently read once on container start.
+// NB: This env var is currently experimental and the logic will likely change!
//
// When running on Kubernetes, containerboot defaults to storing state in the
// "tailscale" kube secret. To store state on local disk instead, set
@@ -83,6 +90,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
+ "tailscale.com/ipn/conffile"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/ptr"
@@ -102,39 +110,29 @@ func main() {
tailscale.I_Acknowledge_This_API_Is_Unstable = true
cfg := &settings{
- AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
- Hostname: defaultEnv("TS_HOSTNAME", ""),
- Routes: defaultEnvStringPointer("TS_ROUTES"),
- ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
- ProxyTo: defaultEnv("TS_DEST_IP", ""),
- TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
- TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
- DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
- ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
- InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
- UserspaceMode: defaultBool("TS_USERSPACE", true),
- StateDir: defaultEnv("TS_STATE_DIR", ""),
- AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
- KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
- SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
- HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
- Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
- AuthOnce: defaultBool("TS_AUTH_ONCE", false),
- Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
+ AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
+ Hostname: defaultEnv("TS_HOSTNAME", ""),
+ Routes: defaultEnvStringPointer("TS_ROUTES"),
+ ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
+ ProxyTo: defaultEnv("TS_DEST_IP", ""),
+ TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
+ TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
+ DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
+ ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
+ InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
+ UserspaceMode: defaultBool("TS_USERSPACE", true),
+ StateDir: defaultEnv("TS_STATE_DIR", ""),
+ AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
+ KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
+ SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
+ HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
+ Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
+ AuthOnce: defaultBool("TS_AUTH_ONCE", false),
+ Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
+ TailscaledConfigFilePath: defaultEnv("TS_EXPERIMENTAL_CONFIGFILE_PATH", ""),
}
-
- if cfg.ProxyTo != "" && cfg.UserspaceMode {
- log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE")
- }
-
- if cfg.TailnetTargetIP != "" && cfg.UserspaceMode {
- log.Fatal("TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE")
- }
- if cfg.TailnetTargetFQDN != "" && cfg.UserspaceMode {
- log.Fatal("TS_TAILNET_TARGET_FQDN is not supported with TS_USERSPACE")
- }
- if cfg.TailnetTargetFQDN != "" && cfg.TailnetTargetIP != "" {
- log.Fatal("Both TS_TAILNET_TARGET_IP and TS_TAILNET_FQDN cannot be set")
+ if err := cfg.validate(); err != nil {
+ log.Fatalf("invalid containerboot configuration: %v", err)
}
if !cfg.UserspaceMode {
@@ -171,7 +169,7 @@ func main() {
}
cfg.KubernetesCanPatch = canPatch
- if cfg.AuthKey == "" {
+ if runTailscaleSet(cfg) {
key, err := findKeyInKubeSecret(bootCtx, cfg.KubeSecret)
if err != nil {
log.Fatalf("Getting authkey from kube secret: %v", err)
@@ -238,7 +236,7 @@ func main() {
// different points in containerboot's lifecycle, hence the helper function.
didLogin := false
authTailscale := func() error {
- if didLogin {
+ if didLogin || runTailscaledOnly(cfg) {
return nil
}
didLogin = true
@@ -253,7 +251,7 @@ func main() {
return nil
}
- if !cfg.AuthOnce {
+ if !runTailscaleSet(cfg) {
if err := authTailscale(); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
@@ -269,6 +267,13 @@ authLoop:
if n.State != nil {
switch *n.State {
case ipn.NeedsLogin:
+ if runTailscaledOnly(cfg) {
+ // This could happen if this is the
+ // first time tailscaled was run for
+ // this device and the auth key was not
+ // passed via the configfile.
+ log.Fatalf("invalid state: tailscaled daemon started with a config file, but tailscale is not logged in: ensure you pass a valid auth key in the config file.")
+ }
if err := authTailscale(); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
@@ -293,7 +298,7 @@ authLoop:
ctx, cancel := contextWithExitSignalWatch()
defer cancel()
- if cfg.AuthOnce {
+ if runTailscaleSet(cfg) {
// Now that we are authenticated, we can set/reset any of the
// settings that we need to.
if err := tailscaleSet(ctx, cfg); err != nil {
@@ -309,7 +314,7 @@ authLoop:
}
}
- if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {
+ if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && runTailscaleSet(cfg) {
// We were told to only auth once, so any secret-bound
// authkey is no longer needed. We don't strictly need to
// wipe it, but it's good hygiene.
@@ -634,6 +639,9 @@ func tailscaledArgs(cfg *settings) []string {
if cfg.HTTPProxyAddr != "" {
args = append(args, "--outbound-http-proxy-listen="+cfg.HTTPProxyAddr)
}
+ if cfg.TailscaledConfigFilePath != "" {
+ args = append(args, "--config="+cfg.TailscaledConfigFilePath)
+ }
if cfg.DaemonExtraArgs != "" {
args = append(args, strings.Fields(cfg.DaemonExtraArgs)...)
}
@@ -873,21 +881,46 @@ type settings struct {
// TailnetTargetFQDN is an MagicDNS name to which all incoming
// non-Tailscale traffic should be proxied. This must be a full Tailnet
// node FQDN.
- TailnetTargetFQDN string
- ServeConfigPath string
- DaemonExtraArgs string
- ExtraArgs string
- InKubernetes bool
- UserspaceMode bool
- StateDir string
- AcceptDNS *bool
- KubeSecret string
- SOCKSProxyAddr string
- HTTPProxyAddr string
- Socket string
- AuthOnce bool
- Root string
- KubernetesCanPatch bool
+ TailnetTargetFQDN string
+ ServeConfigPath string
+ DaemonExtraArgs string
+ ExtraArgs string
+ InKubernetes bool
+ UserspaceMode bool
+ StateDir string
+ AcceptDNS *bool
+ KubeSecret string
+ SOCKSProxyAddr string
+ HTTPProxyAddr string
+ Socket string
+ AuthOnce bool
+ Root string
+ KubernetesCanPatch bool
+ TailscaledConfigFilePath string
+}
+
+func (s *settings) validate() error {
+ if s.TailscaledConfigFilePath != "" {
+ if _, err := conffile.Load(s.TailscaledConfigFilePath); err != nil {
+ return fmt.Errorf("error validating tailscaled configfile contents: %w", err)
+ }
+ }
+ if s.ProxyTo != "" && s.UserspaceMode {
+ return errors.New("TS_DEST_IP is not supported with TS_USERSPACE")
+ }
+ if s.TailnetTargetIP != "" && s.UserspaceMode {
+ return errors.New("TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE")
+ }
+ if s.TailnetTargetFQDN != "" && s.UserspaceMode {
+ return errors.New("TS_TAILNET_TARGET_FQDN is not supported with TS_USERSPACE")
+ }
+ if s.TailnetTargetFQDN != "" && s.TailnetTargetIP != "" {
+ return errors.New("Both TS_TAILNET_TARGET_IP and TS_TAILNET_FQDN cannot be set")
+ }
+ if s.TailscaledConfigFilePath != "" && (s.AcceptDNS != nil || s.AuthKey != "" || s.Routes != nil || s.ExtraArgs != "" || s.Hostname != "") {
+ return errors.New("TS_EXPERIMENTAL_CONFIGFILE_PATH cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS.")
+ }
+ return nil
}
// defaultEnv returns the value of the given envvar name, or defVal if
@@ -962,3 +995,17 @@ func contextWithExitSignalWatch() (context.Context, func()) {
}
return ctx, f
}
+
+// runTaiscaleSet determines whether `tailscale set` (rather than the default
+// `tailscale up`) should be used to reconfigure tailscaled on every subsequent
+// container restart after the tailnet device has logged in.
+func runTailscaleSet(cfg *settings) bool {
+ return cfg.AuthOnce && cfg.TailscaledConfigFilePath == ""
+}
+
+// runTailscaledOnly determines whether tailscaled only should be ran to start,
+// configure and log in the tailnet device and the `tailscale up`/`tailscale
+// set` steps should be skipped.
+func runTailscaledOnly(cfg *settings) bool {
+ return cfg.TailscaledConfigFilePath != ""
+}
diff --git a/cmd/containerboot/main_test.go b/cmd/containerboot/main_test.go
index 598dba9a5..9435de894 100644
--- a/cmd/containerboot/main_test.go
+++ b/cmd/containerboot/main_test.go
@@ -310,7 +310,7 @@ func TestContainerBoot(t *testing.T) {
},
},
{
- Name: "ingres proxy",
+ Name: "ingress proxy",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_DEST_IP": "1.2.3.4",
@@ -629,6 +629,22 @@ func TestContainerBoot(t *testing.T) {
},
},
},
+ {
+ Name: "experimental tailscaled configfile",
+ Env: map[string]string{
+ // TODO - create this file so we don't fail here
+ "TS_EXPERIMENTAL_CONFIGFILE_PATH": "/conf",
+ },
+ Phases: []phase{
+ {
+ WantCmds: []string{
+ "/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --config=/conf",
+ },
+ }, {
+ Notify: runningNotify,
+ },
+ },
+ },
}
for _, test := range tests {