diff --git a/README.md b/README.md index 1b5165c..6e6e821 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ # tailscale-poc -Tailscale reverse proxy proof of concept +Tailscale reverse proxy proof of concept. + +Three different attempts at getting Tailscale to work in Cloudrun / GKE. + + +# Build and Run on CloudRun + +Generate an ephemeral key. + +``` +gcloud builds submit --config=cloudbuild.yaml \ + --substitutions=_TAILSCALE_AUTH="tskey-123456789abcedef",_TAILSCALE_ENDPOINT="http://[fd7a:115c:a1e0:ab12:1234:1234:1234:1234]:5000" +``` \ No newline at end of file diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..6d8fa58 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,98 @@ +substitutions: + _TAILSCALE_AUTH: '' + _TAILSCALE_ENDPOINT: '' + +steps: + # Build the containers + - id: 'build caddy-tailscale' + name: 'gcr.io/kaniko-project/executor:latest' + args: + - --destination=gcr.io/${PROJECT_ID}/caddy-tailscale:latest + - --context=. + - --cache=true + - --dockerfile=package/caddy-tailscale/Dockerfile + - id: 'build nginx-tailscale' + name: 'gcr.io/kaniko-project/executor:latest' + args: + - --destination=gcr.io/${PROJECT_ID}/nginx-tailscale:latest + - --context=. + - --cache=true + - --dockerfile=package/nginx-tailscale/Dockerfile + - id: 'build webapp-tailscale' + name: 'gcr.io/kaniko-project/executor:latest' + args: + - --destination=gcr.io/${PROJECT_ID}/webapp-tailscale:latest + - --context=. + - --cache=true + - --dockerfile=package/webapp-tailscale/Dockerfile + + # Deploy to cloud run + - id: 'deploy caddy-tailscale' + name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:316.0.0-alpine' + waitFor: + - build caddy-tailscale + args: + - 'bash' + - '-eEuo' + - 'pipefail' + - '-c' + - |- + gcloud run deploy "tailscale-poc-caddy" \ + --quiet \ + --platform=managed \ + --region=us-west1 \ + --concurrency=10 \ + --memory=128Mi \ + --max-instances=1 \ + --image="gcr.io/${PROJECT_ID}/caddy-tailscale:latest" \ + --allow-unauthenticated \ + --set-env-vars \ + TAILSCALE_AUTH=${_TAILSCALE_AUTH} \ + --set-env-vars \ + TAILSCALE_ENDPOINT=${_TAILSCALE_ENDPOINT} + - id: 'deploy nginx-tailscale' + name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:316.0.0-alpine' + waitFor: + - build nginx-tailscale + args: + - 'bash' + - '-eEuo' + - 'pipefail' + - '-c' + - |- + gcloud run deploy "tailscale-poc-nginx" \ + --quiet \ + --platform=managed \ + --region=us-west1 \ + --concurrency=10 \ + --memory=128Mi \ + --max-instances=1 \ + --image="gcr.io/${PROJECT_ID}/nginx-tailscale:latest" \ + --allow-unauthenticated \ + --set-env-vars \ + TAILSCALE_AUTH=${_TAILSCALE_AUTH} \ + --set-env-vars \ + TAILSCALE_ENDPOINT=${_TAILSCALE_ENDPOINT} + - id: 'deploy webapp-tailscale' + name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:316.0.0-alpine' + waitFor: + - build webapp-tailscale + args: + - 'bash' + - '-eEuo' + - 'pipefail' + - '-c' + - |- + gcloud run deploy "tailscale-poc-webapp" \ + --quiet \ + --platform=managed \ + --region=us-west1 \ + --concurrency=10 \ + --memory=128Mi \ + --max-instances=1 \ + --image="gcr.io/${PROJECT_ID}/webapp-tailscale:latest" \ + --allow-unauthenticated \ + --set-env-vars \ + TAILSCALE_AUTH=${_TAILSCALE_AUTH} \ + --set-env-vars \ + TAILSCALE_ENDPOINT=${_TAILSCALE_ENDPOINT} diff --git a/cmd/fetch-headers/main.go b/cmd/fetch-headers/main.go new file mode 100644 index 0000000..53a528c --- /dev/null +++ b/cmd/fetch-headers/main.go @@ -0,0 +1,68 @@ +// Fetch Headers attempts to fetch the headers of a service routed through Tailscale. +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" +) + +var client = http.DefaultClient + +func main() { + log.Print("starting server...") + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }) + http.HandleFunc("/", handler) + + port := os.Getenv("PORT") + if port == "" { + port = "80" + log.Printf("defaulting to port %s", port) + } + + log.Printf("listening on port %s", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func handler(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + + endpoint := os.Getenv("TAILSCALE_ENDPOINT") + + fmt.Fprintf(w, "fetching %s...\n\n", endpoint) + hdrFetch(ctx, w, endpoint) + + if err := ctx.Err(); err != nil { + fmt.Fprintf(w, "context error: %v\n", err) + } +} + +func hdrFetch(ctx context.Context, w http.ResponseWriter, addr string) { + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil) + if err != nil { + fmt.Fprintf(w, "unable to create new request: %v\n", err) + return + } + res, err := client.Do(req) + if err != nil { + fmt.Fprintf(w, "unable to issue http request: %v\n", err) + return + } + defer res.Body.Close() + var sb strings.Builder + for k, v := range res.Header { + fmt.Fprintf(&sb, "%s => %q\n", k, v) + } + fmt.Fprintln(w, sb.String()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..be05b1f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/polds/tailscale-poc + +go 1.16 \ No newline at end of file diff --git a/package/caddy-tailscale/Caddyfile b/package/caddy-tailscale/Caddyfile new file mode 100644 index 0000000..34f2b3c --- /dev/null +++ b/package/caddy-tailscale/Caddyfile @@ -0,0 +1,17 @@ +{ + auto_https off +} + +:{$PORT} + +encode zstd gzip + +respond /healthz "ok" 200 +respond /debug/healthz "ok" 200 # Since cloudrun doesn't allow /healthz + +reverse_proxy { + to {$TAILSCALE_ENDPOINT} + + header_up -X-Forwarded-Proto + header_up -X-Forwarded-For +} \ No newline at end of file diff --git a/package/caddy-tailscale/Dockerfile b/package/caddy-tailscale/Dockerfile new file mode 100644 index 0000000..7b9ecbb --- /dev/null +++ b/package/caddy-tailscale/Dockerfile @@ -0,0 +1,23 @@ +# From https://tailscale.com/kb/1108/cloudrun/ +FROM alpine:latest as tailscale +WORKDIR /app +COPY . ./ +ENV TSFILE=tailscale_1.12.3_amd64.tgz +RUN wget https://pkgs.tailscale.com/stable/${TSFILE} && \ + tar xzf ${TSFILE} --strip-components=1 +COPY . ./ + +FROM caddy:2.4.3-alpine +RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* + +# Copy binary to production image +COPY package/caddy-tailscale/start.sh /app/start.sh +COPY package/caddy-tailscale/Caddyfile /etc/caddy/Caddyfile +COPY --from=tailscale /app/tailscaled /app/tailscaled +COPY --from=tailscale /app/tailscale /app/tailscale +RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale + +EXPOSE 80 +# Run on container startup. +RUN chmod +x /app/start.sh +CMD ["/app/start.sh"] \ No newline at end of file diff --git a/package/caddy-tailscale/start.sh b/package/caddy-tailscale/start.sh new file mode 100644 index 0000000..2a1b45f --- /dev/null +++ b/package/caddy-tailscale/start.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +/app/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 & +until /app/tailscale up --authkey=${TAILSCALE_AUTH} --hostname=edge-${HOSTNAME} --accept-routes --advertise-exit-node +do + sleep 0.1 +done +echo Tailscale started +ALL_PROXY=socks5://localhost:1055/ caddy run --config=/etc/caddy/Caddyfile --adapter=caddyfile diff --git a/package/nginx-tailscale/Dockerfile b/package/nginx-tailscale/Dockerfile new file mode 100644 index 0000000..1eb5748 --- /dev/null +++ b/package/nginx-tailscale/Dockerfile @@ -0,0 +1,24 @@ +# From https://tailscale.com/kb/1108/cloudrun/ +FROM alpine:latest as tailscale +WORKDIR /app +COPY . ./ +ENV TSFILE=tailscale_1.12.3_amd64.tgz +RUN wget https://pkgs.tailscale.com/stable/${TSFILE} && \ + tar xzf ${TSFILE} --strip-components=1 +COPY . ./ + +FROM nginx:stable-alpine +RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* + +# Copy binary to production image +COPY --from=tailscale /app/tailscaled /app/tailscaled +COPY --from=tailscale /app/tailscale /app/tailscale +COPY package/nginx-tailscale/start.sh /app/start.sh +COPY package/nginx-tailscale/nginx.conf /etc/nginx/conf.d/test.template + +RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale + +EXPOSE 80 +# Run on container startup. +RUN chmod +x /app/start.sh +CMD ["/app/start.sh"] \ No newline at end of file diff --git a/package/nginx-tailscale/nginx.conf b/package/nginx-tailscale/nginx.conf new file mode 100644 index 0000000..8aa62dd --- /dev/null +++ b/package/nginx-tailscale/nginx.conf @@ -0,0 +1,14 @@ +server { + listen ${PORT}; + location ~ ^/(healthz|debug/healthz) { + return 200 'ok'; + add_header Content-Type text/plain; + } + # Just a quick test to make sure our environment correctly supports forwarding packets. + location /ip { + proxy_pass http://icanhazip.com:80; + } + location / { + proxy_pass ${TAILSCALE_ENDPOINT}; + } +} \ No newline at end of file diff --git a/package/nginx-tailscale/start.sh b/package/nginx-tailscale/start.sh new file mode 100644 index 0000000..19910f8 --- /dev/null +++ b/package/nginx-tailscale/start.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +/app/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 & +until /app/tailscale up --authkey=${TAILSCALE_AUTH} --hostname=edge-${HOSTNAME} --accept-routes --advertise-exit-node +do + sleep 0.1 +done +echo Tailscale started +ALL_PROXY=socks5://localhost:1055/ envsubst '${TAILSCALE_ENDPOINT} ${PORT}' < /etc/nginx/conf.d/test.template > /etc/nginx/conf.d/default.conf && nginx -g "daemon off;" \ No newline at end of file diff --git a/package/webapp-tailscale/Dockerfile b/package/webapp-tailscale/Dockerfile new file mode 100644 index 0000000..bdeecda --- /dev/null +++ b/package/webapp-tailscale/Dockerfile @@ -0,0 +1,36 @@ +# From https://tailscale.com/kb/1108/cloudrun/ +FROM golang:1.16.2-alpine3.13 as builder +WORKDIR /app +COPY . ./ + +RUN go build \ + -a \ + -ldflags "-s -w -extldflags 'static'" \ + -installsuffix cgo \ + -tags netgo \ + -o /bin/app \ + cmd/fetch-headers/*.go + +FROM alpine:latest as tailscale +WORKDIR /app +COPY . ./ +ENV TSFILE=tailscale_1.12.3_amd64.tgz +RUN wget https://pkgs.tailscale.com/stable/${TSFILE} && \ + tar xzf ${TSFILE} --strip-components=1 +COPY . ./ + +FROM alpine +RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* + +# Copy binary to production image +COPY --from=builder /bin/app /bin/app +COPY --from=tailscale /app/tailscaled /app/tailscaled +COPY --from=tailscale /app/tailscale /app/tailscale +COPY package/webapp-tailscale/start.sh /app/start.sh + +RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale + +EXPOSE 80 +# Run on container startup. +RUN chmod +x /app/start.sh +CMD ["/app/start.sh"] \ No newline at end of file diff --git a/package/webapp-tailscale/start.sh b/package/webapp-tailscale/start.sh new file mode 100644 index 0000000..1cec4ea --- /dev/null +++ b/package/webapp-tailscale/start.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +/app/tailscaled --tun=userspace-networking --socks5-server=localhost:1055 & +until /app/tailscale up --authkey=${TAILSCALE_AUTH} --hostname=edge-${HOSTNAME} --accept-routes --advertise-exit-node +do + sleep 0.1 +done +echo Tailscale started +ALL_PROXY=socks5://localhost:1055/ /bin/app