From bc546c54ead0ec06defa706ca3b299b1e253e28b Mon Sep 17 00:00:00 2001 From: Will Norris Date: Fri, 17 May 2024 15:13:01 -0700 Subject: [PATCH] certs: update tscert dialer to connect to tsnet server This provides an alternate implementation for tscert.TailscaledDialer that tries to find a tsnet server for the requested certificate. This allows caddy-tailscale to be used with caddy's auto_https support. Fixes #19 Signed-off-by: Will Norris --- README.md | 36 ++++++++++++++++++++---------------- go.mod | 4 ++-- module.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bdd35ff..ca7c8d3 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ In those cases, this plugin may be helpful. Use [xcaddy](https://github.com/caddyserver/xcaddy) to build Caddy with the Tailscale plugin included. -``` +```sh xcaddy build v2.8.4 --with github.com/tailscale/caddy-tailscale ``` @@ -149,7 +149,7 @@ For Caddy [JSON config], add the `tailscale` app with fields from [tscaddy.App]: The provided network listener allows privately serving sites on your tailnet. Configure a site block as usual, and use the [bind] directive to specify a tailscale network address: -``` +```caddyfile :80 { bind tailscale/ } @@ -158,7 +158,7 @@ Configure a site block as usual, and use the [bind] directive to specify a tails The trailing slash is required. You can also specify a named node configuration to use for the Tailscale node: -``` +```caddyfile :80 { bind tailscale/myapp } @@ -186,7 +186,7 @@ If using the Caddy JSON configuration, specify a "tailscale/" network in your li Caddy will join your Tailscale network and listen only on that network interface. Multiple addresses can be specified if you want to listen on different Tailscale nodes as well as a local address: -``` +```caddyfile :80 { bind tailscale/myhost tailscale/my-other-host localhost } @@ -194,7 +194,7 @@ Multiple addresses can be specified if you want to listen on different Tailscale Different sites can be configured to join the network as different nodes: -``` +```caddyfile :80 { bind tailscale/myhost } @@ -206,7 +206,7 @@ Different sites can be configured to join the network as different nodes: Or they can be served on different ports of the same Tailscale node: -``` +```caddyfile :80 { bind tailscale/myhost } @@ -220,22 +220,26 @@ Or they can be served on different ports of the same Tailscale node: ### HTTPS support -At this time, the Tailscale plugin for Caddy doesn't support using Caddy's native HTTPS resolvers. -You will need to use the `tailscale+tls` bind protocol with a configuration like this: +Caddy's automatic HTTPS support can be used with the Tailscale network listener like any other site. +If the site address includes the full `ts.net` hostname, no additional configuration is necessary: -``` -{ - auto_https off +```caddyfile +https://myhost.tail1234.ts.net { + bind tailscale/myhost } +``` + +If the site address does not include the full hostname, specify the tailscale cert manager: +```caddyfile :443 { - bind tailscale+tls/myhost + bind tailscale/myhost + tls { + get_certificate tailscale + } } ``` -Please note that because you currently need to turn `auto_https` support off, -you may want to run a separate Caddy instance for sites that do need `auto_https`. - ## Authentication provider Setup the Tailscale authentication provider with `tailscale_auth` directive. @@ -316,7 +320,7 @@ and will enforce Tailscale authentication and map user values to HTTP headers. For example: -``` +```sh xcaddy tailscale-proxy --from "tailscale/myhost:80" --to localhost:8000 ``` diff --git a/go.mod b/go.mod index c3266c7..24ae649 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.22.0 require ( github.com/caddyserver/caddy/v2 v2.8.4 + github.com/caddyserver/certmagic v0.21.3 github.com/google/go-cmp v0.6.0 + github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 go.uber.org/zap v1.27.0 tailscale.com v1.67.0-pre.0.20240510224123-fc1ae97e1037 ) @@ -36,7 +38,6 @@ require ( github.com/aws/smithy-go v1.20.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect - github.com/caddyserver/certmagic v0.21.3 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -138,7 +139,6 @@ require ( github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect - github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect diff --git a/module.go b/module.go index d66c5bf..2ee9ea0 100644 --- a/module.go +++ b/module.go @@ -14,6 +14,8 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" + "github.com/tailscale/tscert" "go.uber.org/zap" "tailscale.com/tsnet" ) @@ -21,6 +23,8 @@ import ( func init() { caddy.RegisterNetwork("tailscale", getPlainListener) caddy.RegisterNetwork("tailscale+tls", getTLSListener) + + tscert.TailscaledDialer = localAPIDialer } func getPlainListener(c context.Context, _ string, addr string, _ net.ListenConfig) (any, error) { @@ -262,3 +266,45 @@ func (t *tsnetServerListener) Close() error { _, err := nodes.Delete(t.name) return err } + +// localAPIDialer finds the node that matches the requested certificate in ctx +// and dials that node's local API. +// If no matching node is found, the default dailer is used, +// which tries to connect to a local tailscaled on the machine. +func localAPIDialer(ctx context.Context, network, addr string) (net.Conn, error) { + if addr != "local-tailscaled.sock:80" { + return nil, fmt.Errorf("unexpected URL address %q", addr) + } + + chi, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo) + if !ok || chi == nil { + return tscert.DialLocalAPI(ctx, network, addr) + } + serverName := chi.ServerName + + var tn *tailscaleNode + nodes.Range(func(key, value any) bool { + n, ok := value.(*tailscaleNode) + if !ok || n == nil { + return true + } + for _, d := range n.CertDomains() { + // MatchWildcard is not really necessary since Tailscale doesn't do wildcard certs, + // but caddy uses it for the built-in Tailscale CertGetter, so we do so here as well. + if certmagic.MatchWildcard(serverName, d) { + tn = n + return false + } + } + + return true + }) + + if tn != nil { + if lc, err := tn.LocalClient(); err == nil { + return lc.Dial(ctx, network, addr) + } + } + + return tscert.DialLocalAPI(ctx, network, addr) +}