diff --git a/README.md b/README.md index a9f720d..4ab66de 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,36 @@ However, having a single Caddy site connect to separate Tailscale nodes doesn't quite work correctly. If this is something you actually need, please open an issue. +#### Alternative ControlURL + +To use with a custom coordination server (Headscale) you can include your ControlURL with the listen network. + +``` +:80 { + bind tailscale/example.de/a +} + +:80 { + bind tailscale/example.de/b +} +``` + +It also supports specifying the used protocol (defaults to tcp): + +``` +:80 { + bind tailscale/example.de/udp/a +} + +:80 { + bind tailscale/udp/b +} +``` + +Current shortcommings: +- no support for path with ControlURL, +- it defaults to HTTPS for connection to custom control server + ### HTTPS support At this time, the Tailscale plugin for Caddy doesn't support using Caddy's @@ -186,3 +216,42 @@ For example: ``` xcaddy tailscale-proxy --from "tailscale/myhost:80" --to localhost:8000 ``` + +## Caddy transport provider + +You can also specifiy tailscale as protocol to be used by caddy to proxy to your application. + +```json +{ + { + "apps": { + "http": { + "servers": { + "status": { + "listen": [ + "tailscale/example.com/status:80" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "upstreams": [ + { + "dial": "node1:3000" + } + ], + "transport": { + "protocol": "tailscale", + "controlURL": "https://example.com" + } + } + ] + } + ] + } + } + } + } +} +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 40f6423..85ebd10 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/caddyserver/caddy/v2 v2.7.3 go.uber.org/zap v1.25.0 - tailscale.com v1.1.1-0.20230810153433-3d56cafd7d23 + tailscale.com v1.58.2 ) require ( diff --git a/module.go b/module.go index 3ad8945..8b9d557 100644 --- a/module.go +++ b/module.go @@ -33,37 +33,30 @@ func init() { } func getPlainListener(_ context.Context, _ string, addr string, _ net.ListenConfig) (any, error) { - network, host, port, err := caddy.SplitNetworkAddress(addr) + controlURL, network, host, port, err := parseAddr(addr) if err != nil { return nil, err } - s, err := getServer("", host) + s, err := getServer("", host, controlURL) if err != nil { return nil, err } - if network == "" { - network = "tcp" - } - return s.Listen(network, ":"+port) } func getTLSListener(_ context.Context, _ string, addr string, _ net.ListenConfig) (any, error) { - network, host, port, err := caddy.SplitNetworkAddress(addr) + controlURL, network, host, port, err := parseAddr(addr) if err != nil { return nil, err } - s, err := getServer("", host) + s, err := getServer("", host, controlURL) if err != nil { return nil, err } - if network == "" { - network = "tcp" - } ln, err := s.Listen(network, ":"+port) if err != nil { return nil, err @@ -86,7 +79,7 @@ func getTLSListener(_ context.Context, _ string, addr string, _ net.ListenConfig // // 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(_, addr string) (*tsnetServerDestructor, error) { +func getServer(_, addr string, controlURL string) (*tsnetServerDestructor, error) { _, host, _, err := caddy.SplitNetworkAddress(addr) if err != nil { return nil, err @@ -104,6 +97,11 @@ func getServer(_, addr string) (*tsnetServerDestructor, error) { }, } + // Setting ControlURL. If empty or not found will default to default of tsnet "https://controlplane.tailscale.com" + if controlURL != "" { + s.ControlURL = controlURL + } + if host != "" { // Set authkey to "TS_AUTHKEY_". If empty, // fall back to "TS_AUTHKEY". @@ -227,6 +225,34 @@ func parseCaddyfile(_ httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) }, nil } +func parseAddr(addr string) (controlURL, network, host, port string, err error) { + controlURL = "" + + network, host, port, err = caddy.SplitNetworkAddress(addr) + if err != nil { + return "", "", "", "", err + } + + switch network { + case "", "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6": + default: + controlURL = fmt.Sprintf("https://%s", network) + network = "tcp" + beforeSlash, afterSlash, slashFound := strings.Cut(host, "/") + if slashFound { + switch beforeSlash { + case "", "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6": + network = beforeSlash + host = afterSlash + default: + network = "tcp" + } + } + } + + return controlURL, network, host, port, nil +} + type tsnetServerDestructor struct { *tsnet.Server } diff --git a/transport.go b/transport.go index 6c8767b..ad5d7b8 100644 --- a/transport.go +++ b/transport.go @@ -10,8 +10,9 @@ import ( ) type TailscaleCaddyTransport struct { - logger *zap.Logger - server *tsnetServerDestructor + logger *zap.Logger + server *tsnetServerDestructor + ControlURL string `json:"controlURL,omitempty"` } func (t *TailscaleCaddyTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { @@ -21,7 +22,7 @@ func (t *TailscaleCaddyTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) err func (t *TailscaleCaddyTransport) Provision(context caddy.Context) error { t.logger = context.Logger() - s, err := getServer("", "caddy-tsnet-client:80") + s, err := getServer("", "caddy-tsnet-client:80", t.ControlURL) if err != nil { return err }