| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- //go:build linux
- package main
- import (
- "context"
- "errors"
- "fmt"
- "log"
- "net/netip"
- "os"
- "path"
- "strconv"
- "strings"
- "tailscale.com/ipn/conffile"
- "tailscale.com/kube/kubeclient"
- )
- // settings is all the configuration for containerboot.
- type settings struct {
- AuthKey string
- Hostname string
- Routes *string
- // ProxyTargetIP is the destination IP to which all incoming
- // Tailscale traffic should be proxied. If empty, no proxying
- // is done. This is typically a locally reachable IP.
- ProxyTargetIP string
- // ProxyTargetDNSName is a DNS name to whose backing IP addresses all
- // incoming Tailscale traffic should be proxied.
- ProxyTargetDNSName string
- // TailnetTargetIP is the destination IP to which all incoming
- // non-Tailscale traffic should be proxied. This is typically a
- // Tailscale IP.
- TailnetTargetIP string
- // 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
- TailscaledConfigFilePath string
- EnableForwardingOptimizations bool
- // If set to true and, if this containerboot instance is a Kubernetes
- // ingress proxy, set up rules to forward incoming cluster traffic to be
- // forwarded to the ingress target in cluster.
- AllowProxyingClusterTrafficViaIngress bool
- // PodIP is the IP of the Pod if running in Kubernetes. This is used
- // when setting up rules to proxy cluster traffic to cluster ingress
- // target.
- // Deprecated: use PodIPv4, PodIPv6 instead to support dual stack clusters
- PodIP string
- PodIPv4 string
- PodIPv6 string
- HealthCheckAddrPort string
- EgressSvcsCfgPath string
- }
- func configFromEnv() (*settings, error) {
- cfg := &settings{
- AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
- Hostname: defaultEnv("TS_HOSTNAME", ""),
- Routes: defaultEnvStringPointer("TS_ROUTES"),
- ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
- ProxyTargetIP: defaultEnv("TS_DEST_IP", ""),
- ProxyTargetDNSName: defaultEnv("TS_EXPERIMENTAL_DEST_DNS_NAME", ""),
- 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: tailscaledConfigFilePath(),
- AllowProxyingClusterTrafficViaIngress: defaultBool("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS", false),
- PodIP: defaultEnv("POD_IP", ""),
- EnableForwardingOptimizations: defaultBool("TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS", false),
- HealthCheckAddrPort: defaultEnv("TS_HEALTHCHECK_ADDR_PORT", ""),
- EgressSvcsCfgPath: defaultEnv("TS_EGRESS_SERVICES_CONFIG_PATH", ""),
- }
- podIPs, ok := os.LookupEnv("POD_IPS")
- if ok {
- ips := strings.Split(podIPs, ",")
- if len(ips) > 2 {
- return nil, fmt.Errorf("POD_IPs can contain at most 2 IPs, got %d (%v)", len(ips), ips)
- }
- for _, ip := range ips {
- parsed, err := netip.ParseAddr(ip)
- if err != nil {
- return nil, fmt.Errorf("error parsing IP address %s: %w", ip, err)
- }
- if parsed.Is4() {
- cfg.PodIPv4 = parsed.String()
- continue
- }
- cfg.PodIPv6 = parsed.String()
- }
- }
- if err := cfg.validate(); err != nil {
- return nil, fmt.Errorf("invalid configuration: %v", err)
- }
- return cfg, nil
- }
- func (s *settings) validate() error {
- if s.TailscaledConfigFilePath != "" {
- dir, file := path.Split(s.TailscaledConfigFilePath)
- if _, err := os.Stat(dir); err != nil {
- return fmt.Errorf("error validating whether directory with tailscaled config file %s exists: %w", dir, err)
- }
- if _, err := os.Stat(s.TailscaledConfigFilePath); err != nil {
- return fmt.Errorf("error validating whether tailscaled config directory %q contains tailscaled config for current capability version %q: %w. If this is a Tailscale Kubernetes operator proxy, please ensure that the version of the operator is not older than the version of the proxy", dir, file, err)
- }
- if _, err := conffile.Load(s.TailscaledConfigFilePath); err != nil {
- return fmt.Errorf("error validating tailscaled configfile contents: %w", err)
- }
- }
- if s.ProxyTargetIP != "" && s.UserspaceMode {
- return errors.New("TS_DEST_IP is not supported with TS_USERSPACE")
- }
- if s.ProxyTargetDNSName != "" && s.UserspaceMode {
- return errors.New("TS_EXPERIMENTAL_DEST_DNS_NAME is not supported with TS_USERSPACE")
- }
- if s.ProxyTargetDNSName != "" && s.ProxyTargetIP != "" {
- return errors.New("TS_EXPERIMENTAL_DEST_DNS_NAME and TS_DEST_IP cannot both be set")
- }
- 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_VERSIONED_CONFIG_DIR cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS.")
- }
- if s.AllowProxyingClusterTrafficViaIngress && s.UserspaceMode {
- return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is not supported in userspace mode")
- }
- if s.AllowProxyingClusterTrafficViaIngress && s.ServeConfigPath == "" {
- return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is set but this is not a cluster ingress proxy")
- }
- if s.AllowProxyingClusterTrafficViaIngress && s.PodIP == "" {
- return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is set but POD_IP is not set")
- }
- if s.EnableForwardingOptimizations && s.UserspaceMode {
- return errors.New("TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS is not supported in userspace mode")
- }
- if s.HealthCheckAddrPort != "" {
- if _, err := netip.ParseAddrPort(s.HealthCheckAddrPort); err != nil {
- return fmt.Errorf("error parsing TS_HEALTH_CHECK_ADDR_PORT value %q: %w", s.HealthCheckAddrPort, err)
- }
- }
- return nil
- }
- // setupKube is responsible for doing any necessary configuration and checks to
- // ensure that tailscale state storage and authentication mechanism will work on
- // Kubernetes.
- func (cfg *settings) setupKube(ctx context.Context) error {
- if cfg.KubeSecret == "" {
- return nil
- }
- canPatch, canCreate, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
- if err != nil {
- return fmt.Errorf("some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
- }
- cfg.KubernetesCanPatch = canPatch
- s, err := kc.GetSecret(ctx, cfg.KubeSecret)
- if err != nil {
- if !kubeclient.IsNotFoundErr(err) {
- return fmt.Errorf("getting Tailscale state Secret %s: %v", cfg.KubeSecret, err)
- }
- if !canCreate {
- return fmt.Errorf("tailscale state Secret %s does not exist and we don't have permissions to create it. "+
- "If you intend to store tailscale state elsewhere than a Kubernetes Secret, "+
- "you can explicitly set TS_KUBE_SECRET env var to an empty string. "+
- "Else ensure that RBAC is set up that allows the service account associated with this installation to create Secrets.", cfg.KubeSecret)
- }
- }
- // Return early if we already have an auth key.
- if cfg.AuthKey != "" || isOneStepConfig(cfg) {
- return nil
- }
- if s == nil {
- log.Print("TS_AUTHKEY not provided and state Secret does not exist, login will be interactive if needed.")
- return nil
- }
- keyBytes, _ := s.Data["authkey"]
- key := string(keyBytes)
- if key != "" {
- // Enforce that we must be able to patch out the authkey after
- // authenticating if you want to use this feature. This avoids
- // us having to deal with the case where we might leave behind
- // an unnecessary reusable authkey in a secret, like a rake in
- // the grass.
- if !cfg.KubernetesCanPatch {
- return errors.New("authkey found in TS_KUBE_SECRET, but the pod doesn't have patch permissions on the Secret to manage the authkey.")
- }
- cfg.AuthKey = key
- }
- log.Print("No authkey found in state Secret and TS_AUTHKEY not provided, login will be interactive if needed.")
- return nil
- }
- // isTwoStepConfigAuthOnce returns true if the Tailscale node should be configured
- // in two steps and login should only happen once.
- // Step 1: run 'tailscaled'
- // Step 2):
- // A) if this is the first time starting this node run 'tailscale up --authkey <authkey> <config opts>'
- // B) if this is not the first time starting this node run 'tailscale set <config opts>'.
- func isTwoStepConfigAuthOnce(cfg *settings) bool {
- return cfg.AuthOnce && cfg.TailscaledConfigFilePath == ""
- }
- // isTwoStepConfigAlwaysAuth returns true if the Tailscale node should be configured
- // in two steps and we should log in every time it starts.
- // Step 1: run 'tailscaled'
- // Step 2): run 'tailscale up --authkey <authkey> <config opts>'
- func isTwoStepConfigAlwaysAuth(cfg *settings) bool {
- return !cfg.AuthOnce && cfg.TailscaledConfigFilePath == ""
- }
- // isOneStepConfig returns true if the Tailscale node should always be ran and
- // configured in a single step by running 'tailscaled <config opts>'
- func isOneStepConfig(cfg *settings) bool {
- return cfg.TailscaledConfigFilePath != ""
- }
- // isL3Proxy returns true if the Tailscale node needs to be configured to act
- // as an L3 proxy, proxying to an endpoint provided via one of the config env
- // vars.
- func isL3Proxy(cfg *settings) bool {
- return cfg.ProxyTargetIP != "" || cfg.ProxyTargetDNSName != "" || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != "" || cfg.AllowProxyingClusterTrafficViaIngress || cfg.EgressSvcsCfgPath != ""
- }
- // hasKubeStateStore returns true if the state must be stored in a Kubernetes
- // Secret.
- func hasKubeStateStore(cfg *settings) bool {
- return cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != ""
- }
- // defaultEnv returns the value of the given envvar name, or defVal if
- // unset.
- func defaultEnv(name, defVal string) string {
- if v, ok := os.LookupEnv(name); ok {
- return v
- }
- return defVal
- }
- // defaultEnvStringPointer returns a pointer to the given envvar value if set, else
- // returns nil. This is useful in cases where we need to distinguish between a
- // variable being set to empty string vs unset.
- func defaultEnvStringPointer(name string) *string {
- if v, ok := os.LookupEnv(name); ok {
- return &v
- }
- return nil
- }
- // defaultEnvBoolPointer returns a pointer to the given envvar value if set, else
- // returns nil. This is useful in cases where we need to distinguish between a
- // variable being explicitly set to false vs unset.
- func defaultEnvBoolPointer(name string) *bool {
- v := os.Getenv(name)
- ret, err := strconv.ParseBool(v)
- if err != nil {
- return nil
- }
- return &ret
- }
- func defaultEnvs(names []string, defVal string) string {
- for _, name := range names {
- if v, ok := os.LookupEnv(name); ok {
- return v
- }
- }
- return defVal
- }
- // defaultBool returns the boolean value of the given envvar name, or
- // defVal if unset or not a bool.
- func defaultBool(name string, defVal bool) bool {
- v := os.Getenv(name)
- ret, err := strconv.ParseBool(v)
- if err != nil {
- return defVal
- }
- return ret
- }
|