Skip to content

Commit

Permalink
override tscert.TailscaledTransport with muxing transport
Browse files Browse the repository at this point in the history
This provides an http.RoundTripper implementation that dynamically
routes requests to the correct tsnet server's LocalAPI based on the
ClientHelloInfo in the context.

Previously, we were just overriding the tscert Dialer. That worked fine
the first time it dialed a LocalAPI, and would correctly dial the right
tsnet server. However, tscert caches the Transport with that Dialer, so
requests that should be routed to different tsnet servers would be
routed incorrectly.

Updates #19
Updates #53
Updates #66

Signed-off-by: Will Norris <will@tailscale.com>
  • Loading branch information
willnorris committed Jun 18, 2024
1 parent 5cc2140 commit 4e8fde6
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 35 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
go.uber.org/zap v1.27.0
tailscale.com v1.67.0-pre.0.20240602211424-42cfbf427c67
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,8 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU=
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
Expand Down
80 changes: 48 additions & 32 deletions module.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
"strconv"
"strings"
"sync"

"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/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/tsnet"
)
Expand All @@ -34,9 +37,9 @@ func init() {
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
// Update the tscert transport to send requests to the correct tsnet server,
// rather than just always connecting to the local machine's tailscaled.
tscert.TailscaledTransport = &tsnetMuxTransport{}
hostinfo.SetApp("caddy")
}

Expand Down Expand Up @@ -317,40 +320,53 @@ func (t *tsnetServerListener) Close() error {
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)
}
// tsnetMuxTransport is an [http.RoundTripper] that sends requests to the LocalAPI
// for the tsnet server that matches the ClientHelloInfo server name.
// If no tsnet server matches, a default Transport is used.
type tsnetMuxTransport struct {
defaultTransport *http.Transport
defaultTransportOnce sync.Once
}

func (t *tsnetMuxTransport) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
var rt http.RoundTripper

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
if ok && clientHello != nil {
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) {
if lc, err := n.LocalClient(); err == nil {
rt = &localAPITransport{lc}
}
return false
}
}
}
}
return true
})
return true
})
}

if tn != nil {
if lc, err := tn.LocalClient(); err == nil {
return lc.Dial(ctx, network, addr)
}
if rt == nil {
t.defaultTransportOnce.Do(func() {
t.defaultTransport = &http.Transport{
DialContext: tscert.TailscaledDialer,
}
})
rt = t.defaultTransport
}
return rt.RoundTrip(req)
}

// localAPITransport is an [http.RoundTripper] that sends requests to a [tailscale.LocalClient]'s LocalAPI.
type localAPITransport struct {
*tailscale.LocalClient
}

return tscert.DialLocalAPI(ctx, network, addr)
func (t *localAPITransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.DoLocalRequest(req)
}

0 comments on commit 4e8fde6

Please sign in to comment.