From 11a01d339f1e7e4fadb4f1d0140987f792994edd Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 27 Mar 2023 11:50:11 -0500 Subject: [PATCH] feat: improve tracing (#13) --- .dockerignore | 5 ++ Dockerfile | 2 +- Makefile | 8 +-- cmd/tunneld/main.go | 13 +++- cmd/tunneld/tracing.go | 8 ++- compose/.env.example | 2 + compose/.gitignore | 1 + compose/Makefile | 17 +++++ compose/caddy/Dockerfile | 12 ++++ compose/docker-compose.yml | 47 +++++++++++++ go.mod | 5 +- go.sum | 19 +++++- tunneld/api.go | 133 +++++++++++++++++++------------------ tunneld/api_test.go | 16 +++++ tunneld/tunneld.go | 24 +++++-- tunneld/tunneld_test.go | 47 +++++-------- 16 files changed, 243 insertions(+), 116 deletions(-) create mode 100644 .dockerignore create mode 100644 compose/.env.example create mode 100644 compose/.gitignore create mode 100644 compose/Makefile create mode 100644 compose/caddy/Dockerfile create mode 100644 compose/docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..74386f6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +# Ignore everything +* + +# Allow the tunnel binary +!/build/tunneld diff --git a/Dockerfile b/Dockerfile index 78f3dc0..0ecf6ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,6 @@ LABEL \ RUN adduser -D -u 1000 tunneld USER tunneld -COPY tunneld / +COPY ./build/tunneld / CMD ["/tunneld"] diff --git a/Makefile b/Makefile index 18d37e9..27e4d4f 100644 --- a/Makefile +++ b/Makefile @@ -49,17 +49,11 @@ build/tunneld.tag: build/tunneld version="$(VERSION)" tag="ghcr.io/coder/wgtunnel/tunneld:$${version//+/-}" - # make a temp directory, copy the binary into it, and build the image. - temp_dir=$$(mktemp -d) - cp build/tunneld "$$temp_dir" - docker build \ --file Dockerfile \ --build-arg "WGTUNNEL_VERSION=$(VERSION)" \ - --tag "$$tag" \ - "$$temp_dir" + --tag "$$tag" - rm -rf "$$temp_dir" echo "$$tag" > "$@" test: diff --git a/cmd/tunneld/main.go b/cmd/tunneld/main.go index a1af048..f48dcb1 100644 --- a/cmd/tunneld/main.go +++ b/cmd/tunneld/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "io" "log" "net/http" @@ -121,6 +122,11 @@ func main() { Usage: "The Honeycomb team ID to send tracing data to. If not specified, tracing will not be shipped anywhere.", EnvVars: []string{"TUNNELD_TRACING_HONEYCOMB_TEAM"}, }, + &cli.StringFlag{ + Name: "tracing-service-id", + Usage: "The service ID to annotate all traces with that uniquely identifies this deployment.", + EnvVars: []string{"TUNNELD_TRACING_SERVICE_ID"}, + }, }, Action: runApp, } @@ -146,6 +152,7 @@ func runApp(ctx *cli.Context) error { realIPHeader = ctx.String("real-ip-header") pprofListenAddress = ctx.String("pprof-listen-address") tracingHoneycombTeam = ctx.String("tracing-honeycomb-team") + tracingServiceID = ctx.String("tracing-service-id") ) if baseURL == "" { return xerrors.New("base-url is required. See --help for more information.") @@ -173,12 +180,12 @@ func runApp(ctx *cli.Context) error { if tracingHoneycombTeam != "" { exp, err := newHoneycombExporter(ctx.Context, tracingHoneycombTeam) if err != nil { - return xerrors.Errorf("failed to create honeycomb telemetry exporter: %w", err) + return xerrors.Errorf("create honeycomb telemetry exporter: %w", err) } // Create a new tracer provider with a batch span processor and the otlp // exporter. - tp := newTraceProvider(exp) + tp := newTraceProvider(exp, tracingServiceID) otel.SetTracerProvider(tp) otel.SetTextMapPropagator( propagation.NewCompositeTextMapPropagator( @@ -210,7 +217,7 @@ func runApp(ctx *cli.Context) error { if wireguardKeyFile != "" { _, err = os.Stat(wireguardKeyFile) - if xerrors.Is(err, os.ErrNotExist) { + if errors.Is(err, os.ErrNotExist) { logger.Info(ctx.Context, "generating private key to file", slog.F("path", wireguardKeyFile)) key, err := tunnelsdk.GeneratePrivateKey() if err != nil { diff --git a/cmd/tunneld/tracing.go b/cmd/tunneld/tracing.go index 71fd103..ad1f1df 100644 --- a/cmd/tunneld/tracing.go +++ b/cmd/tunneld/tracing.go @@ -7,8 +7,10 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + semconv "go.opentelemetry.io/otel/semconv/v1.11.0" "google.golang.org/grpc/credentials" + + "github.com/coder/wgtunnel/buildinfo" ) func newHoneycombExporter(ctx context.Context, teamID string) (*otlptrace.Exporter, error) { @@ -24,10 +26,12 @@ func newHoneycombExporter(ctx context.Context, teamID string) (*otlptrace.Export return otlptrace.New(ctx, client) } -func newTraceProvider(exp *otlptrace.Exporter) *sdktrace.TracerProvider { +func newTraceProvider(exp *otlptrace.Exporter, serviceID string) *sdktrace.TracerProvider { rsc := resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String("WireguardTunnel"), + semconv.ServiceInstanceIDKey.String(serviceID), + semconv.ServiceVersionKey.String(buildinfo.Version()), ) return sdktrace.NewTracerProvider( diff --git a/compose/.env.example b/compose/.env.example new file mode 100644 index 0000000..7ca747d --- /dev/null +++ b/compose/.env.example @@ -0,0 +1,2 @@ +CLOUDFLARE_TOKEN= +HONEYCOMB_TEAM= diff --git a/compose/.gitignore b/compose/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/compose/.gitignore @@ -0,0 +1 @@ +.env diff --git a/compose/Makefile b/compose/Makefile new file mode 100644 index 0000000..90caf9c --- /dev/null +++ b/compose/Makefile @@ -0,0 +1,17 @@ +# Use a single bash shell for each job, and immediately exit on failure +SHELL := bash +.SHELLFLAGS := -ceu +.ONESHELL: + +# Don't print the commands in the file unless you specify VERBOSE. This is +# essentially the same as putting "@" at the start of each line. +ifndef VERBOSE +.SILENT: +endif + +up: + pushd .. + make -B build + popd + docker compose -p wgtunnel up --build +.PHONY: up diff --git a/compose/caddy/Dockerfile b/compose/caddy/Dockerfile new file mode 100644 index 0000000..0a5d41e --- /dev/null +++ b/compose/caddy/Dockerfile @@ -0,0 +1,12 @@ +ARG CADDY_VERSION=2.6.4 +FROM caddy:${CADDY_VERSION}-builder AS builder + +RUN xcaddy build \ + --with github.com/lucaslorentz/caddy-docker-proxy/v2 \ + --with github.com/caddy-dns/cloudflare + +FROM caddy:${CADDY_VERSION} + +COPY --from=builder /usr/bin/caddy /usr/bin/caddy + +CMD ["caddy", "docker-proxy"] diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..e8458c8 --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,47 @@ +version: "3.9" +services: + caddy: + build: ./caddy + ports: + - 8080:80 + - 4443:443 + environment: + - CADDY_INGRESS_NETWORKS=caddy + networks: + - caddy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - caddy_data:/data + restart: unless-stopped + + tunnel: + build: .. + restart: always + ports: + - 55551:55551/udp + networks: + - caddy + environment: + TUNNELD_LISTEN_ADDRESS: "0.0.0.0:8080" + TUNNELD_BASE_URL: "https://local.try.coder.app:4443" + TUNNELD_WIREGUARD_ENDPOINT: "local.try.coder.app:55551" + TUNNELD_WIREGUARD_PORT: "55551" + TUNNELD_WIREGUARD_KEY_FILE: "/home/tunneld/wg.key" + TUNNELD_WIREGUARD_MTU: "1280" + TUNNELD_WIREGUARD_SERVER_IP: "fcca::1" + TUNNELD_WIREGUARD_NETWORK_PREFIX: "fcca::/16" + TUNNELD_REAL_IP_HEADER: "X-Forwarded-For" + TUNNELD_PPROF_LISTEN_ADDRESS: "127.0.0.1:6060" + TUNNELD_TRACING_HONEYCOMB_TEAM: "${HONEYCOMB_TEAM}" + TUNNELD_TRACING_INSTANCE_ID: "local" + labels: + caddy: "local.try.coder.app, *.local.try.coder.app" + caddy.reverse_proxy: "{{upstreams 8080}}" + caddy.tls.dns: cloudflare ${CLOUDFLARE_TOKEN} + +networks: + caddy: + external: true + +volumes: + caddy_data: {} diff --git a/go.mod b/go.mod index 99c6e9b..4a252b3 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.20 require ( cdr.dev/slog v1.4.1 - github.com/go-chi/chi v1.5.4 + github.com/go-chi/chi/v5 v5.0.8 + github.com/go-chi/hostrouter v0.2.0 github.com/go-chi/httprate v0.7.1 + github.com/riandyrn/otelchi v0.5.1 github.com/stretchr/testify v1.8.1 github.com/urfave/cli/v2 v2.24.4 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4 @@ -44,6 +46,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib v1.0.0 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.1 // indirect go.opentelemetry.io/otel/metric v0.33.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect diff --git a/go.sum b/go.sum index 53b53b6..4894460 100644 --- a/go.sum +++ b/go.sum @@ -95,19 +95,26 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= -github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUjHM= +github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= github.com/go-chi/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs= github.com/go-chi/httprate v0.7.1/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -211,6 +218,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/riandyrn/otelchi v0.5.1 h1:0/45omeqpP7f/cvdL16GddQBfAEmZvUyl2QzLSE6uYo= +github.com/riandyrn/otelchi v0.5.1/go.mod h1:ZxVxNEl+jQ9uHseRYIxKWRb3OY8YXFEu+EkNiiSNUEA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -248,8 +257,11 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib v1.0.0 h1:khwDCxdSspjOLmFnvMuSHd/5rPzbTx0+l6aURwtQdfE= +go.opentelemetry.io/contrib v1.0.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4 h1:aUEBEdCa6iamGzg6fuYxDA8ThxvOG240mAvWDU+XLio= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4/go.mod h1:l2MdsbKTocpPS5nQZscqTR9jd8u96VYZdcpF8Sye7mA= +go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4= go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.1 h1:X2GndnMCsUPh6CiY2a+frAbNsXaPLbB0soHRYhAZ5Ig= @@ -260,8 +272,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.1 h1:LYyG/ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.1/go.mod h1:QrRRQiY3kzAoYPNLP0W/Ikg0gR6V3LMc+ODSxr7yyvg= go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E= go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI= +go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs= go.opentelemetry.io/otel/sdk v1.11.1/go.mod h1:/l3FE4SupHJ12TduVjUkZtlfFqDCQJlOlithYrdktys= +go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ= go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= @@ -414,6 +428,7 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/tunneld/api.go b/tunneld/api.go index 4efe418..ba2dcd3 100644 --- a/tunneld/api.go +++ b/tunneld/api.go @@ -12,7 +12,9 @@ import ( "strings" "time" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" + "github.com/go-chi/hostrouter" + "github.com/riandyrn/otelchi" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" @@ -23,16 +25,27 @@ import ( "github.com/coder/wgtunnel/tunnelsdk" ) -func (api *API) Router() chi.Router { - r := chi.NewRouter() +func (api *API) Router() http.Handler { + var ( + hr = hostrouter.New() + apiRouter = chi.NewRouter() + proxyRouter = chi.NewRouter() + unknownRouter = chi.NewRouter() + ) + + hr.Map(api.BaseURL.Host, apiRouter) + hr.Map("*."+api.BaseURL.Host, proxyRouter) + hr.Map("*", unknownRouter) - r.Use( + proxyRouter.Use( + otelchi.Middleware("proxy"), httpmw.LimitBody(50*1<<20), // 50MB - api.handleTunnelMW, + ) + proxyRouter.Mount("/", http.HandlerFunc(api.handleTunnel)) - // Post tunnel middleware, this middleware will never execute on - // tunneled connections. - httpmw.LimitBody(1<<20), // change back to 1MB + apiRouter.Use( + otelchi.Middleware("api", otelchi.WithChiRoutes(apiRouter)), + httpmw.LimitBody(1<<20), // 1MB httpmw.RateLimit(httpmw.RateLimitConfig{ Log: api.Log.Named("ratelimier"), Count: 10, @@ -41,20 +54,22 @@ func (api *API) Router() chi.Router { }), ) - r.Get("/", func(w http.ResponseWriter, r *http.Request) { + apiRouter.Get("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("https://coder.com")) }) - r.Post("/tun", api.postTun) - r.Post("/api/v2/clients", api.postClients) + apiRouter.Post("/tun", api.postTun) + apiRouter.Post("/api/v2/clients", api.postClients) - r.NotFound(func(rw http.ResponseWriter, r *http.Request) { + notFound := func(rw http.ResponseWriter, r *http.Request) { httpapi.Write(r.Context(), rw, http.StatusNotFound, tunnelsdk.Response{ Message: "Not found.", }) - }) + } + apiRouter.NotFound(notFound) + unknownRouter.NotFound(notFound) - return r + return hr } type LegacyPostTunRequest struct { @@ -182,63 +197,51 @@ allowed_ip=%s/128`, type ipPortKey struct{} -func (api *API) handleTunnelMW(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() +func (api *API) handleTunnel(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() - // Check if the request looks like a tunnel request. - host := r.Host - if host == "" { - httpapi.Write(ctx, rw, http.StatusBadRequest, tunnelsdk.Response{ - Message: "Missing Host header.", - }) - return - } + host := r.Host + subdomain, _ := splitHostname(host) + subdomainParts := strings.Split(subdomain, "-") + user := subdomainParts[len(subdomainParts)-1] - subdomain, rest := splitHostname(host) - if rest != api.BaseURL.Hostname() { - // Doesn't look like a tunnel request. - next.ServeHTTP(rw, r) - return - } + ip, err := api.HostnameToWireguardIP(user) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, tunnelsdk.Response{ + Message: "Invalid tunnel URL.", + Detail: err.Error(), + }) + return + } - subdomainParts := strings.Split(subdomain, "-") - ip, err := api.HostnameToWireguardIP(subdomainParts[len(subdomainParts)-1]) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, tunnelsdk.Response{ - Message: "Invalid tunnel URL.", + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.Bool("proxy_request", true), + attribute.String("user", user), + ) + + // The transport on the reverse proxy uses this ctx value to know which + // IP to dial. See tunneld.go. + ctx = context.WithValue(ctx, ipPortKey{}, netip.AddrPortFrom(ip, tunnelsdk.TunnelPort)) + r = r.WithContext(ctx) + + rp := httputil.ReverseProxy{ + // This can only happen when it fails to dial. + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + httpapi.Write(ctx, rw, http.StatusBadGateway, tunnelsdk.Response{ + Message: "Failed to dial peer.", Detail: err.Error(), }) - return - } - - span := trace.SpanFromContext(ctx) - span.SetAttributes(attribute.Bool("proxy_request", true)) - - // The transport on the reverse proxy uses this ctx value to know which - // IP to dial. See tunneld.go. - ctx = context.WithValue(ctx, ipPortKey{}, netip.AddrPortFrom(ip, tunnelsdk.TunnelPort)) - r = r.WithContext(ctx) - - rp := httputil.ReverseProxy{ - // This can only happen when it fails to dial. - ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { - httpapi.Write(ctx, rw, http.StatusBadGateway, tunnelsdk.Response{ - Message: "Failed to dial peer.", - Detail: err.Error(), - }) - }, - Director: func(rp *http.Request) { - rp.URL.Scheme = "http" - rp.URL.Host = r.Host - rp.Host = r.Host - }, - Transport: api.transport, - } + }, + Director: func(rp *http.Request) { + rp.URL.Scheme = "http" + rp.URL.Host = r.Host + rp.Host = r.Host + }, + Transport: api.transport, + } - span.End() - rp.ServeHTTP(rw, r) - }) + rp.ServeHTTP(rw, r) } // splitHostname splits a hostname into the subdomain and the rest of the diff --git a/tunneld/api_test.go b/tunneld/api_test.go index f98398c..21a43ee 100644 --- a/tunneld/api_test.go +++ b/tunneld/api_test.go @@ -4,10 +4,12 @@ import ( "context" "encoding/hex" "encoding/json" + "io" "net/http" "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/wgtunnel/tunneld" @@ -120,3 +122,17 @@ func Test_postClients(t *testing.T) { res3.Version = tunnelsdk.TunnelVersion2 require.Equal(t, res, res3) } + +func Test_getRoot(t *testing.T) { + t.Parallel() + + _, client := createTestTunneld(t, nil) + + res, err := client.Request(context.Background(), http.MethodGet, "/", nil) + require.NoError(t, err) + defer res.Body.Close() + + out, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, "https://coder.com", string(out)) +} diff --git a/tunneld/tunneld.go b/tunneld/tunneld.go index 6585ec0..690919b 100644 --- a/tunneld/tunneld.go +++ b/tunneld/tunneld.go @@ -8,6 +8,9 @@ import ( "net/netip" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "golang.org/x/xerrors" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/device" @@ -73,21 +76,34 @@ listen_port=%d`, wgNet: wgNet, wgDevice: dev, transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + DialContext: func(ctx context.Context, network, addr string) (nc net.Conn, err error) { + ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "(http.Transport).DialContext") + defer span.End() + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + ip := ctx.Value(ipPortKey{}) if ip == nil { - return nil, xerrors.New("no ip on context") + err = xerrors.New("no ip on context") + return nil, err } ipp, ok := ip.(netip.AddrPort) if !ok { - return nil, xerrors.Errorf("ip is incorrect type, got %T", ipp) + err = xerrors.Errorf("ip is incorrect type, got %T", ipp) + return nil, err } + span.SetAttributes(attribute.String("wireguard_addr", ipp.Addr().String())) + dialCtx, dialCancel := context.WithTimeout(ctx, options.PeerDialTimeout) defer dialCancel() - nc, err := wgNet.DialContextTCPAddrPort(dialCtx, ipp) + nc, err = wgNet.DialContextTCPAddrPort(dialCtx, ipp) if err != nil { return nil, err } diff --git a/tunneld/tunneld_test.go b/tunneld/tunneld_test.go index a9c7395..cd21d66 100644 --- a/tunneld/tunneld_test.go +++ b/tunneld/tunneld_test.go @@ -52,8 +52,7 @@ func TestEndToEnd(t *testing.T) { require.NotEqual(t, tunnel.URL.String(), tunnel.OtherURLs[0].String()) serveTunnel(t, tunnel) - c := tunnelHTTPClient(client) - waitForTunnelReady(t, c, tunnel) + waitForTunnelReady(t, client, tunnel) // Make a bunch of requests concurrently. var wg sync.WaitGroup @@ -70,7 +69,9 @@ func TestEndToEnd(t *testing.T) { } u, err := u.Parse("/test/" + strconv.Itoa(i)) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return + } // Do a third of the requests with a prefix before the hostname. if i%3 == 0 { @@ -80,10 +81,7 @@ func TestEndToEnd(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - assert.NoError(t, err) - - res, err := c.Do(req) + res, err := client.Request(ctx, http.MethodGet, u.String(), nil) if !assert.NoError(t, err) { return } @@ -181,8 +179,7 @@ func TestCompatibility(t *testing.T) { require.NotNil(t, tunnel) serveTunnel(t, tunnel) - c := tunnelHTTPClient(client) - waitForTunnelReady(t, c, tunnel) + waitForTunnelReady(t, client, tunnel) // Make a request to the tunnel. { @@ -193,10 +190,7 @@ func TestCompatibility(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - assert.NoError(t, err) - - res, err := c.Do(req) + res, err := client.Request(ctx, http.MethodGet, u.String(), nil) if !assert.NoError(t, err) { return } @@ -268,8 +262,7 @@ func TestCompatibility(t *testing.T) { require.NotNil(t, tunnel) serveTunnel(t, tunnel) - c := tunnelHTTPClient(client) - waitForTunnelReady(t, c, tunnel) + waitForTunnelReady(t, client, tunnel) // Make a request to the tunnel. { @@ -280,10 +273,7 @@ func TestCompatibility(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - assert.NoError(t, err) - - res, err := c.Do(req) + res, err := client.Request(ctx, http.MethodGet, u.String(), nil) if !assert.NoError(t, err) { return } @@ -333,18 +323,14 @@ func TestTimeout(t *testing.T) { <-tunnel.Wait() // Requests should fail in roughly 1 second. - c := tunnelHTTPClient(client) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() u := *tunnel.URL u.Path = "/test/1" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - require.NoError(t, err) - now := time.Now() - res, err := c.Do(req) + res, err := client.Request(ctx, http.MethodGet, u.String(), nil) require.NoError(t, err) require.WithinDuration(t, now.Add(time.Second), time.Now(), 2*time.Second) defer res.Body.Close() @@ -425,7 +411,8 @@ func createTestTunneldNoDefaults(t *testing.T, options *tunneld.Options) (*tunne u, err := url.Parse(srv.URL) require.NoError(t, err, "parse server URL") - client := tunnelsdk.New(u) + client := tunnelsdk.New(options.BaseURL) + client.HTTPClient = tunnelHTTPClient(u) return td, client } @@ -459,26 +446,24 @@ func serveTunnel(t *testing.T, tunnel *tunnelsdk.Tunnel) { // tunnelHTTPClient returns a HTTP client that disregards DNS and always // connects to the tunneld server IP. This is useful for testing connections to // generated tunnel URLs with custom hostnames that don't resolve. -func tunnelHTTPClient(client *tunnelsdk.Client) *http.Client { +func tunnelHTTPClient(tunURL *url.URL) *http.Client { return &http.Client{ Transport: &http.Transport{ DisableKeepAlives: true, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return (&net.Dialer{}).DialContext(ctx, network, client.URL.Host) + return (&net.Dialer{}).DialContext(ctx, network, tunURL.Host) }, }, } } -func waitForTunnelReady(t *testing.T, c *http.Client, tunnel *tunnelsdk.Tunnel) { +func waitForTunnelReady(t *testing.T, client *tunnelsdk.Client, tunnel *tunnelsdk.Tunnel) { require.Eventually(t, func() bool { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, tunnel.URL.String(), nil) + res, err := client.Request(ctx, http.MethodGet, tunnel.URL.String(), nil) require.NoError(t, err, "create request") - - res, err := c.Do(req) if err == nil { _ = res.Body.Close() }