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 80% rename from caddyfile_test.go rename to app_test.go index 454f082..fb3a645 100644 --- a/caddyfile_test.go +++ b/app_test.go @@ -29,10 +29,10 @@ func Test_ParseApp(t *testing.T) { name: "auth_key", d: caddyfile.NewTestDispenser(` tailscsale { - auth_key abcdefghijklmnopqrstuvwxyz + auth_key tskey-default }`), - want: `{"auth_key":"abcdefghijklmnopqrstuvwxyz"}`, - authKey: "abcdefghijklmnopqrstuvwxyz", + want: `{"auth_key":"tskey-default"}`, + authKey: "tskey-default", }, { name: "ephemeral", @@ -57,34 +57,33 @@ func Test_ParseApp(t *testing.T) { tailscsale { foo }`), - want: `{"servers":{"foo":{}}}`, + want: `{"nodes":{"foo":{}}}`, }, { name: "tailscale with server", d: caddyfile.NewTestDispenser(` tailscsale { - auth_key 1234567890 + auth_key tskey-default foo { - auth_key abcdefghijklmnopqrstuvwxyz + auth_key tskey-node } }`), - want: `{"auth_key":"1234567890","servers":{"foo":{"auth_key":"abcdefghijklmnopqrstuvwxyz"}}}`, + want: `{"auth_key":"tskey-default","nodes":{"foo":{"auth_key":"tskey-node"}}}`, wantErr: false, - authKey: "abcdefghijklmnopqrstuvwxyz", + authKey: "tskey-node", }, } 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) return } return - } - if testcase.wantErr && err == nil { + } else if testcase.wantErr { t.Errorf("parseApp() err = %v, wantErr %v", err, testcase.wantErr) return } 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 (