From e4158fd8551e7e09910d98d0c67a75422523f7c3 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Tue, 7 May 2024 17:15:30 -0700 Subject: [PATCH] refactor caddy-tailscale logic This includes: - Move TailscaleAuth logic into auth.go - Move all TSApp logic into app.go (including caddyfile parsing) - Rename "server" to "node" throughout. This aligns better with Tailscale terminology, and is reflective of the fact that nodes can also just be used as proxy transports, in which case they are not acting as servers at all. - Generally prefer referring to a node's "name" than "host". While this name is still used as the default hostname for the node, I would expect that to change with a future iteration of #18. - add godocs throughout Signed-off-by: Will Norris --- app.go | 85 +++++++++++++- caddyfile_test.go => app_test.go | 2 +- auth.go | 127 +++++++++++++++++++++ caddyfile.go | 77 ------------- module.go | 187 ++++++++----------------------- module_test.go | 14 +-- transport.go | 30 ++--- 7 files changed, 281 insertions(+), 241 deletions(-) rename caddyfile_test.go => app_test.go (98%) create mode 100644 auth.go delete mode 100644 caddyfile.go diff --git a/app.go b/app.go index bb408fd..19f9e9d 100644 --- a/app.go +++ b/app.go @@ -1,28 +1,44 @@ package tscaddy +// app.go contains TSApp and TSNode, which provide global configuration for registering Tailscale nodes. + import ( "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "go.uber.org/zap" ) func init() { caddy.RegisterModule(TSApp{}) + httpcaddyfile.RegisterGlobalOption("tailscale", parseTSApp) } +// TSApp is the Tailscale Caddy app used to configure Tailscale nodes. +// Nodes can be used to serve sites privately on a Tailscale network, +// or to connect to other Tailnet nodes as upstream proxy backend. type TSApp struct { // DefaultAuthKey is the default auth key to use for Tailscale if no other auth key is specified. DefaultAuthKey string `json:"auth_key,omitempty" caddy:"namespace=tailscale.auth_key"` + // Ephemeral specifies whether Tailscale nodes should be registered as ephemeral. Ephemeral bool `json:"ephemeral,omitempty" caddy:"namespace=tailscale.ephemeral"` - Servers map[string]TSServer `json:"servers,omitempty" caddy:"namespace=tailscale"` + // Nodes is a map of per-node configuration which overrides global options. + Nodes map[string]TSNode `json:"nodes,omitempty" caddy:"namespace=tailscale"` logger *zap.Logger } -type TSServer struct { +// TSNode is a Tailscale node configuration. +// A single node can be used to serve multiple sites on different domains or ports, +// and/or to connect to other Tailscale nodes. +type TSNode struct { + // AuthKey is the Tailscale auth key used to register the node. AuthKey string `json:"auth_key,omitempty" caddy:"namespace=auth_key"` + // Ephemeral specifies whether the node should be registered as ephemeral. Ephemeral bool `json:"ephemeral,omitempty" caddy:"namespace=tailscale.ephemeral"` name string @@ -48,5 +64,70 @@ func (t *TSApp) Stop() error { return nil } +func parseTSApp(d *caddyfile.Dispenser, _ any) (any, error) { + app := &TSApp{ + Nodes: make(map[string]TSNode), + } + if !d.Next() { + return app, d.ArgErr() + + } + + for d.NextBlock(0) { + val := d.Val() + + switch val { + case "auth_key": + if !d.NextArg() { + return nil, d.ArgErr() + } + app.DefaultAuthKey = d.Val() + case "ephemeral": + app.Ephemeral = true + default: + node, err := parseTSNode(d) + if app.Nodes == nil { + app.Nodes = map[string]TSNode{} + } + if err != nil { + return nil, err + } + app.Nodes[node.name] = node + } + } + + return httpcaddyfile.App{ + Name: "tailscale", + Value: caddyconfig.JSON(app, nil), + }, nil +} + +func parseTSNode(d *caddyfile.Dispenser) (TSNode, error) { + name := d.Val() + segment := d.NewFromNextSegment() + + if !segment.Next() { + return TSNode{}, d.ArgErr() + } + + node := TSNode{name: name} + for nesting := segment.Nesting(); segment.NextBlock(nesting); { + val := segment.Val() + switch val { + case "auth_key": + if !segment.NextArg() { + return node, segment.ArgErr() + } + node.AuthKey = segment.Val() + case "ephemeral": + node.Ephemeral = true + default: + return node, segment.Errf("unrecognized subdirective: %s", segment.Val()) + } + } + + return node, nil +} + var _ caddy.App = (*TSApp)(nil) var _ caddy.Provisioner = (*TSApp)(nil) diff --git a/caddyfile_test.go b/app_test.go similarity index 98% rename from caddyfile_test.go rename to app_test.go index 454f082..a45c1a8 100644 --- a/caddyfile_test.go +++ b/app_test.go @@ -76,7 +76,7 @@ func Test_ParseApp(t *testing.T) { for _, testcase := range tests { t.Run(testcase.name, func(t *testing.T) { - got, err := parseApp(testcase.d, nil) + got, err := parseTSApp(testcase.d, nil) if err != nil { if !testcase.wantErr { t.Errorf("parseApp() error = %v, wantErr %v", err, testcase.wantErr) diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..8ffb50c --- /dev/null +++ b/auth.go @@ -0,0 +1,127 @@ +package tscaddy + +// auth.go contains the TailscaleAuth module and supporting logic. + +import ( + "fmt" + "net/http" + "strings" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth" + "tailscale.com/client/tailscale" + "tailscale.com/tsnet" +) + +func init() { + caddy.RegisterModule(TailscaleAuth{}) + httpcaddyfile.RegisterHandlerDirective("tailscale_auth", parseAuthConfig) +} + +// TailscaleAuth is an HTTP authentication provider that authenticates users based on their Tailscale identity. +// If configured on a caddy site that is listening on a tailscale node, +// that node will be used to identify the user information for inbound requests. +// Otherwise, it will attempt to find and use the local tailscaled daemon running on the system. +type TailscaleAuth struct { + localclient *tailscale.LocalClient +} + +func (TailscaleAuth) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.authentication.providers.tailscale", + New: func() caddy.Module { return new(TailscaleAuth) }, + } +} + +// client returns the tailscale LocalClient for the TailscaleAuth module. +// If the LocalClient has not already been configured, the provided request will be used to +// lookup the tailscale node that serviced the request, and get the associated LocalClient. +func (ta *TailscaleAuth) client(r *http.Request) (*tailscale.LocalClient, error) { + if ta.localclient != nil { + return ta.localclient, nil + } + + // if request was made through a tsnet listener, set up the client for the associated tsnet + // server. + server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) + for _, listener := range server.Listeners() { + if tsl, ok := listener.(tsnetListener); ok { + var err error + ta.localclient, err = tsl.Server().LocalClient() + if err != nil { + return nil, err + } + } + } + + if ta.localclient == nil { + // default to empty client that will talk to local tailscaled + ta.localclient = new(tailscale.LocalClient) + } + + return ta.localclient, nil +} + +// tsnetListener is an interface that is implemented by [tsnet.Listener]. +type tsnetListener interface { + Server() *tsnet.Server +} + +// Authenticate authenticates the request and sets Tailscale user data on the caddy User object. +// +// This method will set the following user metadata: +// - tailscale_login: the user's login name without the domain +// - tailscale_user: the user's full login name +// - tailscale_name: the user's display name +// - tailscale_profile_picture: the user's profile picture URL +// - tailscale_tailnet: the user's tailnet name (if the user is not connecting to a shared node) +func (ta TailscaleAuth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) { + user := caddyauth.User{} + + client, err := ta.client(r) + if err != nil { + return user, false, err + } + + info, err := client.WhoIs(r.Context(), r.RemoteAddr) + if err != nil { + return user, false, err + } + + if len(info.Node.Tags) != 0 { + return user, false, fmt.Errorf("node %s has tags", info.Node.Hostinfo.Hostname()) + } + + var tailnet string + if !info.Node.Hostinfo.ShareeNode() { + if s, found := strings.CutPrefix(info.Node.Name, info.Node.ComputedName+"."); found { + // TODO(will): Update this for current ts.net magicdns hostnames. + if s, found := strings.CutSuffix(s, ".beta.tailscale.net."); found { + tailnet = s + } + } + } + + user.ID = info.UserProfile.LoginName + user.Metadata = map[string]string{ + "tailscale_login": strings.Split(info.UserProfile.LoginName, "@")[0], + "tailscale_user": info.UserProfile.LoginName, + "tailscale_name": info.UserProfile.DisplayName, + "tailscale_profile_picture": info.UserProfile.ProfilePicURL, + "tailscale_tailnet": tailnet, + } + return user, true, nil +} + +func parseAuthConfig(_ httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var ta TailscaleAuth + + return caddyauth.Authentication{ + ProvidersRaw: caddy.ModuleMap{ + "tailscale": caddyconfig.JSON(ta, nil), + }, + }, nil +} diff --git a/caddyfile.go b/caddyfile.go deleted file mode 100644 index 06b1b2b..0000000 --- a/caddyfile.go +++ /dev/null @@ -1,77 +0,0 @@ -package tscaddy - -import ( - "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" -) - -func init() { - httpcaddyfile.RegisterGlobalOption("tailscale", parseApp) -} - -func parseApp(d *caddyfile.Dispenser, _ any) (any, error) { - app := &TSApp{ - Servers: make(map[string]TSServer), - } - if !d.Next() { - return app, d.ArgErr() - - } - - for d.NextBlock(0) { - val := d.Val() - - switch val { - case "auth_key": - if !d.NextArg() { - return nil, d.ArgErr() - } - app.DefaultAuthKey = d.Val() - case "ephemeral": - app.Ephemeral = true - default: - svr, err := parseServer(d) - if app.Servers == nil { - app.Servers = map[string]TSServer{} - } - if err != nil { - return nil, err - } - app.Servers[svr.name] = svr - } - } - - return httpcaddyfile.App{ - Name: "tailscale", - Value: caddyconfig.JSON(app, nil), - }, nil -} - -func parseServer(d *caddyfile.Dispenser) (TSServer, error) { - name := d.Val() - segment := d.NewFromNextSegment() - - if !segment.Next() { - return TSServer{}, d.ArgErr() - } - - svr := TSServer{} - svr.name = name - for nesting := segment.Nesting(); segment.NextBlock(nesting); { - val := segment.Val() - switch val { - case "auth_key": - if !segment.NextArg() { - return svr, segment.ArgErr() - } - svr.AuthKey = segment.Val() - case "ephemeral": - svr.Ephemeral = true - default: - return svr, segment.Errf("unrecognized subdirective: %s", segment.Val()) - } - } - - return svr, nil -} diff --git a/module.go b/module.go index 4feaa6e..4a50165 100644 --- a/module.go +++ b/module.go @@ -1,30 +1,24 @@ +// Package tscaddy provides a set of Caddy modules to integrate Tailscale into Caddy. package tscaddy +// module.go contains the Tailscale network listeners for caddy +// as well as some shared logic for registered Tailscale nodes. + import ( "context" "crypto/tls" "fmt" "net" - "net/http" "os" "path" "strings" "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth" "go.uber.org/zap" - "tailscale.com/client/tailscale" "tailscale.com/tsnet" ) -var servers = caddy.NewUsagePool() - func init() { - caddy.RegisterModule(TailscaleAuth{}) - httpcaddyfile.RegisterHandlerDirective("tailscale_auth", parseCaddyfile) caddy.RegisterNetwork("tailscale", getPlainListener) caddy.RegisterNetwork("tailscale+tls", getTLSListener) caddy.RegisterModule(&TailscaleCaddyTransport{}) @@ -41,7 +35,7 @@ func getPlainListener(c context.Context, _ string, addr string, _ net.ListenConf return nil, err } - s, err := getServer(ctx, host) + s, err := getNode(ctx, host) if err != nil { return nil, err } @@ -50,7 +44,7 @@ func getPlainListener(c context.Context, _ string, addr string, _ net.ListenConf network = "tcp" } - ln := &tsnetServerDestructor{ + ln := &tailscaleNode{ Server: s.Server, } return ln.Listen(network, ":"+port) @@ -67,7 +61,7 @@ func getTLSListener(c context.Context, _ string, addr string, _ net.ListenConfig return nil, err } - s, err := getServer(ctx, host) + s, err := getNode(ctx, host) if err != nil { return nil, err } @@ -89,40 +83,39 @@ func getTLSListener(c context.Context, _ string, addr string, _ net.ListenConfig return ln, nil } -// getServer returns a tailscale tsnet.Server for Caddy apps to listen on. The specified -// address will take the form of "tailscale/host:port" or "tailscale+tls/host:port" with -// host being optional. If specified, host will be provided to tsnet as the desired -// hostname for the tailscale node. Only one tsnet server is created per host, even if -// multiple ports are being listened on for the host. +// nodes are the Tailscale nodes that have been configured and started. +// Node configuration comes from the global Tailscale Caddy app. +// When nodes are no longer in used (e.g. all listeners have been closed), they are shutdown. // -// Auth keys can be provided in environment variables of the form TS_AUTHKEY_. If -// no host is specified in the address, the environment variable TS_AUTHKEY will be used. -func getServer(ctx caddy.Context, addr string) (*tsnetServerDestructor, error) { - _, host, _, err := caddy.SplitNetworkAddress(addr) - if err != nil { - return nil, err - } +// Callers should use getNode() to get a node by name, rather than accessing this pool directly. +var nodes = caddy.NewUsagePool() +// getNode returns a tailscale node for Caddy apps to interface with. +// +// The specified name will be used to lookup the node configuration from the tailscale caddy app, +// used to register the node the first time it is used. +// Only one tailscale node is created per name, even if multiple listeners are created for the node. +func getNode(ctx caddy.Context, name string) (*tailscaleNode, error) { appIface, err := ctx.App("tailscale") if err != nil { return nil, err } app := appIface.(*TSApp) - s, _, err := servers.LoadOrNew(host, func() (caddy.Destructor, error) { + s, _, err := nodes.LoadOrNew(name, func() (caddy.Destructor, error) { s := &tsnet.Server{ - Hostname: host, + Hostname: name, Logf: func(format string, args ...any) { app.logger.Sugar().Debugf(format, args...) }, + Ephemeral: getEphemeral(name, app), } - if host != "" { - if s.AuthKey, err = getAuthKey(host, app); err != nil { - app.logger.Warn("error parsing auth key", zap.Error(err)) - } - s.Ephemeral = getEphemeral(host, app) + if s.AuthKey, err = getAuthKey(name, app); err != nil { + app.logger.Warn("error parsing auth key", zap.Error(err)) + } + if name != "" { // Set config directory for tsnet. By default, tsnet will use the name of the // running program, but we include the hostname as well so that a single // caddy instance can have multiple tsnet servers. @@ -130,13 +123,13 @@ func getServer(ctx caddy.Context, addr string) (*tsnetServerDestructor, error) { if err != nil { return nil, err } - s.Dir = path.Join(configDir, "tsnet-caddy-"+host) + s.Dir = path.Join(configDir, "tsnet-caddy-"+name) if err := os.MkdirAll(s.Dir, 0700); err != nil { return nil, err } } - return &tsnetServerDestructor{ + return &tailscaleNode{ s, }, nil }) @@ -144,19 +137,20 @@ func getServer(ctx caddy.Context, addr string) (*tsnetServerDestructor, error) { return nil, err } - return s.(*tsnetServerDestructor), nil + return s.(*tailscaleNode), nil } var repl = caddy.NewReplacer() -func getAuthKey(host string, app *TSApp) (string, error) { +func getAuthKey(name string, app *TSApp) (string, error) { if app == nil { return "", nil } - svr := app.Servers[host] - if svr.AuthKey != "" { - return repl.ReplaceOrErr(svr.AuthKey, true, true) + if node, ok := app.Nodes[name]; ok { + if node.AuthKey != "" { + return repl.ReplaceOrErr(node.AuthKey, true, true) + } } if app.DefaultAuthKey != "" { @@ -165,139 +159,50 @@ func getAuthKey(host string, app *TSApp) (string, error) { // Set authkey to "TS_AUTHKEY_". // If empty, fall back to "TS_AUTHKEY". - authKey := os.Getenv("TS_AUTHKEY_" + strings.ToUpper(host)) + authKey := os.Getenv("TS_AUTHKEY_" + strings.ToUpper(name)) if authKey != "" { - app.logger.Warn("Relying on TS_AUTHKEY_{HOST} env var is deprecated. Set caddy config instead.", zap.Any("host", host)) + app.logger.Warn("Relying on TS_AUTHKEY_{HOST} env var is deprecated. Set caddy config instead.", zap.Any("host", name)) return authKey, nil } return os.Getenv("TS_AUTHKEY"), nil } -func getEphemeral(host string, app *TSApp) bool { +func getEphemeral(name string, app *TSApp) bool { if app == nil { return false } - if svr, ok := app.Servers[host]; ok { - return svr.Ephemeral + if node, ok := app.Nodes[name]; ok { + return node.Ephemeral } return app.Ephemeral } -type TailscaleAuth struct { - localclient *tailscale.LocalClient -} - -func (TailscaleAuth) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "http.authentication.providers.tailscale", - New: func() caddy.Module { return new(TailscaleAuth) }, - } -} - -// client returns the tailscale LocalClient for the TailscaleAuth module. If the LocalClient -// has not already been configured, the provided request will be used to set it up for the -// appropriate tsnet server. -func (ta *TailscaleAuth) client(r *http.Request) (*tailscale.LocalClient, error) { - if ta.localclient != nil { - return ta.localclient, nil - } - - // if request was made through a tsnet listener, set up the client for the associated tsnet - // server. - server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) - for _, listener := range server.Listeners() { - if tsl, ok := listener.(tsnetListener); ok { - var err error - ta.localclient, err = tsl.Server().LocalClient() - if err != nil { - return nil, err - } - } - } - - if ta.localclient == nil { - // default to empty client that will talk to local tailscaled - ta.localclient = new(tailscale.LocalClient) - } - - return ta.localclient, nil -} - -type tsnetListener interface { - Server() *tsnet.Server -} - -func (ta TailscaleAuth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) { - user := caddyauth.User{} - - client, err := ta.client(r) - if err != nil { - return user, false, err - } - - info, err := client.WhoIs(r.Context(), r.RemoteAddr) - if err != nil { - return user, false, err - } - - if len(info.Node.Tags) != 0 { - return user, false, fmt.Errorf("node %s has tags", info.Node.Hostinfo.Hostname()) - } - - var tailnet string - if !info.Node.Hostinfo.ShareeNode() { - if s, found := strings.CutPrefix(info.Node.Name, info.Node.ComputedName+"."); found { - if s, found := strings.CutSuffix(s, ".beta.tailscale.net."); found { - tailnet = s - } - } - } - - user.ID = info.UserProfile.LoginName - user.Metadata = map[string]string{ - "tailscale_login": strings.Split(info.UserProfile.LoginName, "@")[0], - "tailscale_user": info.UserProfile.LoginName, - "tailscale_name": info.UserProfile.DisplayName, - "tailscale_profile_picture": info.UserProfile.ProfilePicURL, - "tailscale_tailnet": tailnet, - } - return user, true, nil -} - -func parseCaddyfile(_ httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { - var ta TailscaleAuth - - return caddyauth.Authentication{ - ProvidersRaw: caddy.ModuleMap{ - "tailscale": caddyconfig.JSON(ta, nil), - }, - }, nil -} - -type tsnetServerDestructor struct { +// tailscaleNode is a wrapper around a tsnet.Server that provides a fully self-contained Tailscale node. +// This node can listen on the tailscale network interface, or be used to connect to other nodes in the tailnet. +type tailscaleNode struct { *tsnet.Server } -func (t tsnetServerDestructor) Destruct() error { +func (t tailscaleNode) Destruct() error { return t.Close() } -func (t *tsnetServerDestructor) Listen(network string, addr string) (net.Listener, error) { +func (t *tailscaleNode) Listen(network string, addr string) (net.Listener, error) { ln, err := t.Server.Listen(network, addr) if err != nil { return nil, err } serverListener := &tsnetServerListener{ - hostname: t.Hostname, + name: t.Hostname, Listener: ln, } return serverListener, nil } type tsnetServerListener struct { - hostname string + name string net.Listener } @@ -308,6 +213,6 @@ func (t *tsnetServerListener) Close() error { // Decrement usage count of server for this hostname. // If usage reaches zero, then the server is actually shutdown. - _, err := servers.Delete(t.hostname) + _, err := nodes.Delete(t.name) return err } diff --git a/module_test.go b/module_test.go index c16a164..03ba30e 100644 --- a/module_test.go +++ b/module_test.go @@ -61,11 +61,11 @@ func Test_GetAuthKey(t *testing.T) { t.Run(name, func(t *testing.T) { app := &TSApp{ DefaultAuthKey: tt.defaultKey, - Servers: make(map[string]TSServer), + Nodes: make(map[string]TSNode), } app.Provision(caddy.Context{}) if tt.hostKey != "" { - app.Servers[host] = TSServer{ + app.Nodes[host] = TSNode{ AuthKey: tt.hostKey, } } @@ -85,27 +85,27 @@ func Test_Listen(t *testing.T) { must.Do(caddy.Run(new(caddy.Config))) ctx := caddy.ActiveContext() - svr, err := getServer(ctx, "testhost") + node, err := getNode(ctx, "testhost") if err != nil { t.Fatal("failed to get server", err) } - ln, err := svr.Listen("tcp", ":80") + ln, err := node.Listen("tcp", ":80") if err != nil { t.Fatal("failed to listen", err) } - count, exists := servers.References("testhost") + count, exists := nodes.References("testhost") if !exists && count != 1 { t.Fatal("reference doesn't exist") } ln.Close() - count, exists = servers.References("testhost") + count, exists = nodes.References("testhost") if exists && count != 0 { t.Fatal("reference exists when it shouldn't") } - err = svr.Close() + err = node.Close() if !errors.Is(err, net.ErrClosed) { t.Fatal("unexpected error", err) } diff --git a/transport.go b/transport.go index df9af56..b649345 100644 --- a/transport.go +++ b/transport.go @@ -1,5 +1,7 @@ package tscaddy +// transport.go contains the TailscaleCaddyTransport module. + import ( "fmt" "net/http" @@ -9,9 +11,19 @@ import ( "go.uber.org/zap" ) +// TailscaleCaddyTransport is a caddy transport that uses a tailscale node to make requests. type TailscaleCaddyTransport struct { logger *zap.Logger - server *tsnetServerDestructor + node *tailscaleNode +} + +func (t *TailscaleCaddyTransport) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.reverse_proxy.transport.tailscale", + New: func() caddy.Module { + return new(TailscaleCaddyTransport) + }, + } } func (t *TailscaleCaddyTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { @@ -21,7 +33,8 @@ func (t *TailscaleCaddyTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) err func (t *TailscaleCaddyTransport) Provision(ctx caddy.Context) error { t.logger = ctx.Logger() - s, err := getServer(ctx, "caddy-tsnet-client:80") + // TODO(will): allow users to specify a node name used to lookup that node's config in TSApp. + s, err := getNode(ctx, "caddy-tsnet-client") if err != nil { return err } @@ -34,25 +47,16 @@ func (t *TailscaleCaddyTransport) Provision(ctx caddy.Context) error { if err := s.Start(); err != nil { return err } - t.server = s + t.node = s return nil } -func (t *TailscaleCaddyTransport) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "http.reverse_proxy.transport.tailscale", - New: func() caddy.Module { - return new(TailscaleCaddyTransport) - }, - } -} - func (t *TailscaleCaddyTransport) RoundTrip(request *http.Request) (*http.Response, error) { if request.URL.Scheme == "" { request.URL.Scheme = "http" } - return t.server.HTTPClient().Transport.RoundTrip(request) + return t.node.HTTPClient().Transport.RoundTrip(request) } var (