diff --git a/README.md b/README.md index bdd35ff..cd4b517 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,21 +220,31 @@ 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. +Caddy will use [Tailscale's HTTPS support] to issue certificates for your node's hostname. +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`. +This plugin previously used a `tailcale+tls` network listener that required disabling caddy's `auto_https` feature. +That is no longer required nor recommended and will be removed in a future version. + +[Tailscale's HTTPS support]: https://tailscale.com/kb/1153/enabling-https ## Authentication provider @@ -316,7 +326,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/examples/proxyauth.caddyfile b/examples/proxyauth.caddyfile index 8ea09c9..5f53622 100644 --- a/examples/proxyauth.caddyfile +++ b/examples/proxyauth.caddyfile @@ -30,7 +30,10 @@ # This will run an identical site as above, but with TLS enabled. :443 { - bind tailscale+tls/caddytest + bind tailscale/caddytest + tls { + get_certificate tailscale + } tailscale_auth reverse_proxy localhost:3333 { header_up X-Webauth-User {http.auth.user.tailscale_login} diff --git a/go.mod b/go.mod index 8571ea0..6db6dac 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.20240602211424-42cfbf427c67 ) @@ -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 @@ -139,7 +140,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 b88635e..f8af3a2 100644 --- a/module.go +++ b/module.go @@ -17,6 +17,8 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/certmagic" + "github.com/tailscale/tscert" "go.uber.org/zap" "tailscale.com/tsnet" ) @@ -26,6 +28,11 @@ func init() { caddy.RegisterNetwork("tailscale+tls", getTLSListener) caddy.RegisterNetwork("tailscale/udp", getUDPListener) caddyhttp.RegisterNetworkHTTP3("tailscale", "tailscale/udp") + + // Caddy uses tscert to get certificates for Tailscale hostnames. + // Update the tscert dialer to dial the LocalAPI of the correct tsnet node, + // rather than just always dialing the local tailscaled. + tscert.TailscaledDialer = localAPIDialer } func getTCPListener(c context.Context, _ string, addr string, _ net.ListenConfig) (any, error) { @@ -304,3 +311,41 @@ 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 dialer 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) + } + + clientHello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo) + if !ok || clientHello == nil { + return tscert.DialLocalAPI(ctx, network, addr) + } + + var tn *tailscaleNode + nodes.Range(func(key, value any) bool { + if n, ok := value.(*tailscaleNode); ok && n != nil { + for _, d := range n.CertDomains() { + // Tailscale doesn't do wildcard certs, but caddy uses MatchWildcard + // for the built-in Tailscale cert manager, so we do so here as well. + if certmagic.MatchWildcard(clientHello.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) +}