diff --git a/Dockerfile b/Dockerfile index 5ff271233..92c8bae6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,10 +66,12 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\ -X tailscale.com/version.Long=$VERSION_LONG \ -X tailscale.com/version.Short=$VERSION_SHORT \ -X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \ - -v ./cmd/tailscale ./cmd/tailscaled + -v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot FROM alpine:3.16 RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables COPY --from=build-env /go/bin/* /usr/local/bin/ -COPY --from=build-env /go/src/tailscale/docs/k8s/run.sh /usr/local/bin/ +# For compat with the previous run.sh, although ideally you should be +# using build_docker.sh which sets an entrypoint for the image. +RUN ln -s /usr/local/bin/containerboot /tailscale/run.sh diff --git a/build_docker.sh b/build_docker.sh index 9b470304b..734f618f5 100755 --- a/build_docker.sh +++ b/build_docker.sh @@ -35,14 +35,14 @@ BASE="${BASE:-${DEFAULT_BASE}}" go run github.com/tailscale/mkctr \ --gopaths="\ tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \ - tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \ + tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled, \ + tailscale.com/cmd/containerboot:/usr/local/bin/containerboot" \ --ldflags="\ -X tailscale.com/version.Long=${VERSION_LONG} \ -X tailscale.com/version.Short=${VERSION_SHORT} \ -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \ - --files="docs/k8s/run.sh:/tailscale/run.sh" \ --base="${BASE}" \ --tags="${TAGS}" \ --repos="${REPOS}" \ --push="${PUSH}" \ - /bin/sh /tailscale/run.sh + /usr/local/bin/containerboot diff --git a/cmd/containerboot/kube.go b/cmd/containerboot/kube.go new file mode 100644 index 000000000..562bdb62b --- /dev/null +++ b/cmd/containerboot/kube.go @@ -0,0 +1,190 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux +// +build linux + +package main + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "sync" + "time" +) + +// findKeyInKubeSecret inspects the kube secret secretName for a data +// field called "authkey", and returns its value if present. +func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) { + kubeOnce.Do(initKube) + req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil) + if err != nil { + return "", err + } + resp, err := doKubeRequest(ctx, req) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + // Kube secret doesn't exist yet, can't have an authkey. + return "", nil + } + return "", err + } + defer resp.Body.Close() + + bs, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + // We use a map[string]any here rather than import corev1.Secret, + // because we only do very limited things to the secret, and + // importing corev1 adds 12MiB to the compiled binary. + var s map[string]any + if err := json.Unmarshal(bs, &s); err != nil { + return "", err + } + if d, ok := s["data"].(map[string]any); ok { + if v, ok := d["authkey"].(string); ok { + bs, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return "", err + } + return string(bs), nil + } + } + return "", nil +} + +// storeDeviceID writes deviceID into the "device_id" data field of +// the kube secret secretName. +func storeDeviceID(ctx context.Context, secretName, deviceID string) error { + kubeOnce.Do(initKube) + m := map[string]map[string]string{ + "stringData": map[string]string{ + "device_id": deviceID, + }, + } + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(m); err != nil { + return err + } + req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/strategic-merge-patch+json") + if _, err := doKubeRequest(ctx, req); err != nil { + return err + } + return nil +} + +// deleteAuthKey deletes the 'authkey' field of the given kube +// secret. No-op if there is no authkey in the secret. +func deleteAuthKey(ctx context.Context, secretName string) error { + kubeOnce.Do(initKube) + // m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902. + m := []struct { + Op string `json:"op"` + Path string `json:"path"` + }{ + { + Op: "remove", + Path: "/data/authkey", + }, + } + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(m); err != nil { + return err + } + req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json-patch+json") + if resp, err := doKubeRequest(ctx, req); err != nil { + if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity { + // This is kubernetes-ese for "the field you asked to + // delete already doesn't exist", aka no-op. + return nil + } + return err + } + return nil +} + +var ( + kubeOnce sync.Once + kubeHost string + kubeNamespace string + kubeToken string + kubeHTTP *http.Transport +) + +func initKube() { + // If running in Kubernetes, set things up so that doKubeRequest + // can talk successfully to the kube apiserver. + if os.Getenv("KUBERNETES_SERVICE_HOST") == "" { + return + } + + kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS") + + bs, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + log.Fatalf("Error reading kube namespace: %v", err) + } + kubeNamespace = strings.TrimSpace(string(bs)) + + bs, err = os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + log.Fatalf("Error reading kube token: %v", err) + } + kubeToken = strings.TrimSpace(string(bs)) + + bs, err = os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + if err != nil { + log.Fatalf("Error reading kube CA cert: %v", err) + } + cp := x509.NewCertPool() + cp.AppendCertsFromPEM(bs) + kubeHTTP = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: cp, + }, + IdleConnTimeout: time.Second, + } +} + +// doKubeRequest sends r to the kube apiserver. +func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) { + kubeOnce.Do(initKube) + if kubeHTTP == nil { + panic("not in kubernetes") + } + + r.URL.Scheme = "https" + r.URL.Host = kubeHost + r.Header.Set("Authorization", "Bearer "+kubeToken) + r.Header.Set("Accept", "application/json") + + resp, err := kubeHTTP.RoundTrip(r) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return resp, fmt.Errorf("got non-200 status code %d", resp.StatusCode) + } + return resp, nil +} diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go new file mode 100644 index 000000000..232e3671c --- /dev/null +++ b/cmd/containerboot/main.go @@ -0,0 +1,424 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux +// +build linux + +// The containerboot binary is a wrapper for starting tailscaled in a +// container. It handles reading the desired mode of operation out of +// environment variables, bringing up and authenticating Tailscale, +// and any other kubernetes-specific side jobs. +// +// As with most container things, configuration is passed through +// environment variables. All configuration is optional. +// +// - TS_AUTH_KEY: the authkey to use for login. +// - TS_ROUTES: subnet routes to advertise. +// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given +// destination. +// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'. +// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'. +// - TS_USERSPACE: run with userspace networking (the default) +// instead of kernel networking. +// - TS_STATE_DIR: the directory in which to store tailscaled +// state. The data should persist across container +// restarts. +// - TS_ACCEPT_DNS: whether to use the tailnet's DNS configuration. +// - TS_KUBE_SECRET: the name of the Kubernetes secret in which to +// store tailscaled state. +// - TS_SOCKS5_SERVER: the address on which to listen for SOCKS5 +// proxying into the tailnet. +// - TS_OUTBOUND_HTTP_PROXY_LISTEN: the address on which to listen +// for HTTP proxying into the tailnet. +// - TS_SOCKET: the path where the tailscaled local API socket should +// be created. +// - TS_AUTH_ONCE: if true, only attempt to log in if not already +// logged in. If false (the default, for backwards +// compatibility), forcibly log in every time the +// container starts. +// +// When running on Kubernetes, TS_KUBE_SECRET takes precedence over +// TS_STATE_DIR. Additionally, if TS_AUTH_KEY is not provided and the +// TS_KUBE_SECRET contains an "authkey" field, that key is used. +package main + +import ( + "context" + "errors" + "fmt" + "io/fs" + "log" + "net/netip" + "os" + "os/exec" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "golang.org/x/sys/unix" + "tailscale.com/client/tailscale" + "tailscale.com/ipn/ipnstate" +) + +func main() { + log.SetPrefix("boot: ") + tailscale.I_Acknowledge_This_API_Is_Unstable = true + + cfg := &settings{ + AuthKey: defaultEnv("TS_AUTH_KEY", ""), + Routes: defaultEnv("TS_ROUTES", ""), + ProxyTo: defaultEnv("TS_DEST_IP", ""), + 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: defaultBool("TS_ACCEPT_DNS", false), + 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), + } + + if cfg.ProxyTo != "" && cfg.UserspaceMode { + log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE") + } + + if !cfg.UserspaceMode { + if err := ensureTunFile(); err != nil { + log.Fatalf("Unable to create tuntap device file: %v", err) + } + } + if cfg.ProxyTo != "" || cfg.Routes != "" { + if err := ensureIPForwarding(); err != nil { + log.Printf("Failed to enable IP forwarding: %v", err) + log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.") + if cfg.InKubernetes { + log.Fatalf("You can either set the sysctls as a privileged initContainer, or run the tailscale container with privileged=true.") + } else { + log.Fatalf("You can fix this by running the container with privileged=true, or the equivalent in your container runtime that permits access to sysctls.") + } + } + } + + // Context is used for all setup stuff until we're in steady + // state, so that if something is hanging we eventually time out + // and crashloop the container. + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.AuthKey == "" { + key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret) + if err != nil { + log.Fatalf("Getting authkey from kube secret: %v", err) + } + if key != "" { + log.Print("Using authkey found in kube secret") + cfg.AuthKey = key + } else { + log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.") + } + } + + st, daemonPid, err := startAndAuthTailscaled(ctx, cfg) + if err != nil { + log.Fatalf("failed to bring up tailscale: %v", err) + } + + if cfg.ProxyTo != "" { + if err := installIPTablesRule(ctx, cfg.ProxyTo, st.TailscaleIPs); err != nil { + log.Fatalf("installing proxy rules: %v", err) + } + } + if cfg.KubeSecret != "" { + if err := storeDeviceID(ctx, cfg.KubeSecret, string(st.Self.ID)); err != nil { + log.Fatalf("storing device ID in kube secret: %v", err) + } + if cfg.AuthOnce { + // 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. + log.Printf("Deleting authkey from kube secret") + if err := deleteAuthKey(ctx, cfg.KubeSecret); err != nil { + log.Fatalf("deleting authkey from kube secret: %v", err) + } + } + } + + log.Println("Startup complete, waiting for shutdown signal") + // Reap all processes, since we are PID1 and need to collect + // zombies. + for { + var status unix.WaitStatus + pid, err := unix.Wait4(-1, &status, 0, nil) + if errors.Is(err, unix.EINTR) { + continue + } + if err != nil { + log.Fatalf("Waiting for exited processes: %v", err) + } + if pid == daemonPid { + log.Printf("Tailscaled exited") + os.Exit(0) + } + } +} + +// startAndAuthTailscaled starts the tailscale daemon and attempts to +// auth it, according to the settings in cfg. If successful, returns +// tailscaled's Status and pid. +func startAndAuthTailscaled(ctx context.Context, cfg *settings) (*ipnstate.Status, int, error) { + args := tailscaledArgs(cfg) + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, unix.SIGTERM, unix.SIGINT) + // 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, 0, fmt.Errorf("starting tailscaled failed: %v", err) + } + go func() { + <-sigCh + log.Printf("Received SIGTERM from container runtime, shutting down tailscaled") + cmd.Process.Signal(unix.SIGTERM) + }() + + // Wait for the socket file to appear, otherwise 'tailscale up' + // can 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 + } + + if !cfg.AuthOnce { + if err := tailscaleUp(ctx, cfg); err != nil { + return nil, 0, fmt.Errorf("couldn't log in: %v", err) + } + } + + tsClient := tailscale.LocalClient{ + Socket: cfg.Socket, + UseSocketOnly: true, + } + + // Poll for daemon state until it goes to either Running or + // NeedsLogin. The latter only happens if cfg.AuthOnce is true, + // because in that case we only try to auth when it's necessary to + // reach the running state. + for { + if ctx.Err() != nil { + return nil, 0, ctx.Err() + } + + loopCtx, cancel := context.WithTimeout(ctx, time.Second) + st, err := tsClient.Status(loopCtx) + cancel() + if err != nil { + return nil, 0, fmt.Errorf("Getting tailscaled state: %w", err) + } + + switch st.BackendState { + case "Running": + if len(st.TailscaleIPs) > 0 { + return st, cmd.Process.Pid, nil + } + log.Printf("No Tailscale IPs assigned yet") + case "NeedsLogin": + // Alas, we cannot currently trigger an authkey login from + // LocalAPI, so we still have to shell out to the + // tailscale CLI for this bit. + if err := tailscaleUp(ctx, cfg); err != nil { + return nil, 0, fmt.Errorf("couldn't log in: %v", err) + } + default: + log.Printf("tailscaled in state %q, waiting", st.BackendState) + } + + time.Sleep(500 * time.Millisecond) + } +} + +// 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, "--statedir=/tmp") + case cfg.StateDir != "": + args = append(args, "--state="+cfg.StateDir) + default: + args = append(args, "--state=mem:", "--statedir=/tmp") + } + + if cfg.UserspaceMode { + args = append(args, "--tun=userspace-networking") + } else if err := ensureTunFile(); 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.DaemonExtraArgs != "" { + args = append(args, strings.Fields(cfg.DaemonExtraArgs)...) + } + return args +} + +// tailscaleUp uses cfg to run 'tailscale up'. +func tailscaleUp(ctx context.Context, cfg *settings) error { + args := []string{"--socket=" + cfg.Socket, "up"} + if cfg.AcceptDNS { + args = append(args, "--accept-dns=true") + } else { + args = append(args, "--accept-dns=false") + } + if cfg.AuthKey != "" { + args = append(args, "--authkey="+cfg.AuthKey) + } + if cfg.Routes != "" { + args = append(args, "--advertise-routes="+cfg.Routes) + } + 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 +} + +// ensureTunFile checks that /dev/net/tun exists, creating it if +// missing. +func ensureTunFile() error { + // Verify that /dev/net/tun exists, in some container envs it + // needs to be mknod-ed. + if _, err := os.Stat("/dev/net"); errors.Is(err, fs.ErrNotExist) { + if err := os.MkdirAll("/dev/net", 0755); err != nil { + return err + } + } + if _, err := os.Stat("/dev/net/tun"); errors.Is(err, fs.ErrNotExist) { + dev := unix.Mkdev(10, 200) // tuntap major and minor + if err := unix.Mknod("/dev/net/tun", 0600|unix.S_IFCHR, int(dev)); err != nil { + return err + } + } + return nil +} + +// ensureIPForwarding enables IPv4/IPv6 forwarding for the container. +func ensureIPForwarding() error { + // In some common configurations (e.g. default docker, + // kubernetes), the container environment denies write access to + // most sysctls, including IP forwarding controls. Check the + // sysctl values before trying to change them, so that we + // gracefully do nothing if the container's already been set up + // properly by e.g. a k8s initContainer. + for _, path := range []string{"/proc/sys/net/ipv4/ip_forward", "/proc/sys/net/ipv6/conf/all/forwarding"} { + bs, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("reading %q: %w", path, err) + } + if v := strings.TrimSpace(string(bs)); v != "1" { + if err := os.WriteFile(path, []byte("1"), 0644); err != nil { + return fmt.Errorf("enabling %q: %w", path, err) + } + } + } + return nil +} + +func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Addr) error { + dst, err := netip.ParseAddr(dstStr) + if err != nil { + return err + } + argv0 := "iptables" + if dst.Is6() { + argv0 = "ip6tables" + } + var local string + for _, ip := range tsIPs { + if ip.Is4() != dst.Is4() { + continue + } + local = ip.String() + break + } + if local == "" { + return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs) + } + cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("executing iptables failed: %w", err) + } + return nil +} + +// settings is all the configuration for containerboot. +type settings struct { + AuthKey string + Routes string + ProxyTo string + DaemonExtraArgs string + ExtraArgs string + InKubernetes bool + UserspaceMode bool + StateDir string + AcceptDNS bool + KubeSecret string + SOCKSProxyAddr string + HTTPProxyAddr string + Socket string + AuthOnce bool +} + +// defaultEnv returns the value of the given envvar name, or defVal if +// unset. +func defaultEnv(name, defVal string) string { + if v := os.Getenv(name); v != "" { + 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 +} diff --git a/docs/k8s/proxy.yaml b/docs/k8s/proxy.yaml index 344f89c30..b52bdc8b3 100644 --- a/docs/k8s/proxy.yaml +++ b/docs/k8s/proxy.yaml @@ -41,6 +41,8 @@ spec: optional: true - name: TS_DEST_IP value: "{{TS_DEST_IP}}" + - name: TS_AUTH_ONCE + value: "true" securityContext: capabilities: add: diff --git a/docs/k8s/run.sh b/docs/k8s/run.sh deleted file mode 100755 index 765f536ab..000000000 --- a/docs/k8s/run.sh +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -#! /bin/sh - -export PATH=$PATH:/tailscale/bin - -TS_AUTH_KEY="${TS_AUTH_KEY:-}" -TS_ROUTES="${TS_ROUTES:-}" -TS_DEST_IP="${TS_DEST_IP:-}" -TS_EXTRA_ARGS="${TS_EXTRA_ARGS:-}" -TS_USERSPACE="${TS_USERSPACE:-true}" -TS_STATE_DIR="${TS_STATE_DIR:-}" -TS_ACCEPT_DNS="${TS_ACCEPT_DNS:-false}" -TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}" -TS_SOCKS5_SERVER="${TS_SOCKS5_SERVER:-}" -TS_OUTBOUND_HTTP_PROXY_LISTEN="${TS_OUTBOUND_HTTP_PROXY_LISTEN:-}" -TS_TAILSCALED_EXTRA_ARGS="${TS_TAILSCALED_EXTRA_ARGS:-}" -TS_SOCKET="${TS_SOCKET:-/tmp/tailscaled.sock}" - -set -e - -TAILSCALED_ARGS="--socket=${TS_SOCKET}" - -if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then - TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET} --statedir=${TS_STATE_DIR:-/tmp}" -elif [[ ! -z "${TS_STATE_DIR}" ]]; then - TAILSCALED_ARGS="${TAILSCALED_ARGS} --statedir=${TS_STATE_DIR}" -else - TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=mem: --statedir=/tmp" -fi - -if [[ "${TS_USERSPACE}" == "true" ]]; then - if [[ ! -z "${TS_DEST_IP}" ]]; then - echo "IP forwarding is not supported in userspace mode" - exit 1 - fi - TAILSCALED_ARGS="${TAILSCALED_ARGS} --tun=userspace-networking" -else - if [[ ! -d /dev/net ]]; then - mkdir -p /dev/net - fi - - if [[ ! -c /dev/net/tun ]]; then - mknod /dev/net/tun c 10 200 - fi -fi - -if [[ ! -z "${TS_SOCKS5_SERVER}" ]]; then - TAILSCALED_ARGS="${TAILSCALED_ARGS} --socks5-server ${TS_SOCKS5_SERVER}" -fi - -if [[ ! -z "${TS_OUTBOUND_HTTP_PROXY_LISTEN}" ]]; then - TAILSCALED_ARGS="${TAILSCALED_ARGS} --outbound-http-proxy-listen ${TS_OUTBOUND_HTTP_PROXY_LISTEN}" -fi - -if [[ ! -z "${TS_TAILSCALED_EXTRA_ARGS}" ]]; then - TAILSCALED_ARGS="${TAILSCALED_ARGS} ${TS_TAILSCALED_EXTRA_ARGS}" -fi - -handler() { - echo "Caught SIGINT/SIGTERM, shutting down tailscaled" - kill -s SIGINT $PID - wait ${PID} -} - -echo "Starting tailscaled" -tailscaled ${TAILSCALED_ARGS} & -PID=$! -trap handler SIGINT SIGTERM - -UP_ARGS="--accept-dns=${TS_ACCEPT_DNS}" -if [[ ! -z "${TS_ROUTES}" ]]; then - UP_ARGS="--advertise-routes=${TS_ROUTES} ${UP_ARGS}" -fi -if [[ ! -z "${TS_AUTH_KEY}" ]]; then - UP_ARGS="--authkey=${TS_AUTH_KEY} ${UP_ARGS}" -fi -if [[ ! -z "${TS_EXTRA_ARGS}" ]]; then - UP_ARGS="${UP_ARGS} ${TS_EXTRA_ARGS:-}" -fi - -echo "Running tailscale up" -tailscale --socket="${TS_SOCKET}" up ${UP_ARGS} - -if [[ ! -z "${TS_DEST_IP}" ]]; then - echo "Adding iptables rule for DNAT" - iptables -t nat -I PREROUTING -d "$(tailscale --socket=${TS_SOCKET} ip -4)" -j DNAT --to-destination "${TS_DEST_IP}" -fi - -echo "Waiting for tailscaled to exit" -wait ${PID} \ No newline at end of file diff --git a/go.mod b/go.mod index d73181fe1..7f2fff580 100644 --- a/go.mod +++ b/go.mod @@ -180,6 +180,7 @@ require ( github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/native v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/kisielk/errcheck v1.6.0 // indirect @@ -213,6 +214,7 @@ require ( github.com/nishanths/exhaustive v0.7.11 // indirect github.com/nishanths/predeclared v0.2.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/onsi/gomega v1.20.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect github.com/pelletier/go-toml v1.9.4 // indirect @@ -246,8 +248,8 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.9.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stretchr/objx v0.3.0 // indirect - github.com/stretchr/testify v1.7.0 // indirect + github.com/stretchr/objx v0.4.0 // indirect + github.com/stretchr/testify v1.8.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/sylvia7788/contextcheck v1.0.4 // indirect github.com/tdakkota/asciicheck v0.1.1 // indirect @@ -273,7 +275,7 @@ require ( gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect howett.net/plist v1.0.0 // indirect mvdan.cc/gofumpt v0.2.0 // indirect mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect diff --git a/go.sum b/go.sum index f7a5d7480..1118b83cf 100644 --- a/go.sum +++ b/go.sum @@ -661,8 +661,9 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -832,8 +833,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= @@ -878,8 +880,9 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -1065,8 +1068,9 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -1074,8 +1078,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/sylvia7788/contextcheck v1.0.4 h1:MsiVqROAdr0efZc/fOCt0c235qm9XJqHtWwM+2h2B04= @@ -1819,8 +1825,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 h1:cv/zaNV0nr1mJzaeo4S5mHIm5va1W0/9J3/5prlsuRM= gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=