Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 73 additions & 14 deletions module.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"go.uber.org/zap"
"tailscale.com/client/local"
"tailscale.com/hostinfo"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
)

Expand All @@ -45,12 +46,26 @@ func init() {
hostinfo.SetApp("caddy")
}

// parseServiceBind splits a bind host into a node name and optional service name.
// The format is "nodename+servicelabel", where the "+..." suffix is optional.
// For example, "app+plex" returns ("app", "svc:plex") and "app" returns ("app", "").
func parseServiceBind(host string) (nodeName string, serviceName tailcfg.ServiceName) {
nodeName, svcLabel, ok := strings.Cut(host, "+")
if ok && svcLabel != "" {
return nodeName, tailcfg.ServiceName("svc:" + svcLabel)
}
return host, ""
}

func getTCPListener(c context.Context, network string, host string, portRange string, portOffset uint, _ net.ListenConfig) (any, error) {
ctx, ok := c.(caddy.Context)
if !ok {
return nil, fmt.Errorf("context is not a caddy.Context: %T", c)
}

nodeName, svcName := parseServiceBind(host)
host = nodeName

na, err := caddy.ParseNetworkAddress(caddy.JoinNetworkAddress(network, host, portRange))
if err != nil {
return nil, err
Expand All @@ -73,12 +88,30 @@ func getTCPListener(c context.Context, network string, host string, portRange st
}

// Follow Caddy's standard listener pooling mechanism
lnKey := fmt.Sprintf("tailscale/%s:%s:%s", host, network, port)
lnKey := "tailscale/" + host
if svcName != "" {
lnKey += "+" + string(svcName)
}
lnKey += ":" + network + ":" + port

sharedLn, _, err := tailscaleListeners.LoadOrNew(lnKey, func() (caddy.Destructor, error) {
ln, err := node.Listen(network, ":"+port)
if err != nil {
return nil, err
var ln net.Listener
if svcName != "" {
portNum, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid port %q: %w", port, err)
}
sl, err := node.ListenService(string(svcName), tsnet.ServiceModeTCP{Port: uint16(portNum)})
if err != nil {
return nil, err
}
ln = sl
} else {
var err error
ln, err = node.Listen(network, ":"+port)
if err != nil {
return nil, err
}
}

return &tailscaleSharedListener{
Expand All @@ -102,6 +135,9 @@ func getTLSListener(c context.Context, network string, host string, portRange st
return nil, fmt.Errorf("context is not a caddy.Context: %T", c)
}

nodeName, svcName := parseServiceBind(host)
host = nodeName

na, err := caddy.ParseNetworkAddress(caddy.JoinNetworkAddress(network, host, portRange))
if err != nil {
return nil, err
Expand All @@ -124,21 +160,39 @@ func getTLSListener(c context.Context, network string, host string, portRange st
}

// Follow Caddy's standard listener pooling mechanism
lnKey := fmt.Sprintf("tailscale+tls/%s:%s:%s", host, network, port)
lnKey := "tailscale+tls/" + host
if svcName != "" {
lnKey += "+" + string(svcName)
}
lnKey += ":" + network + ":" + port

sharedLn, _, err := tailscaleListeners.LoadOrNew(lnKey, func() (caddy.Destructor, error) {
ln, err := node.Listen(network, ":"+port)
if err != nil {
return nil, err
}
var ln net.Listener
if svcName != "" {
portNum, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid port %q: %w", port, err)
}
sl, err := node.ListenService(string(svcName), tsnet.ServiceModeTCP{Port: uint16(portNum)})
if err != nil {
return nil, err
}
ln = sl
} else {
var err error
ln, err = node.Listen(network, ":"+port)
if err != nil {
return nil, err
}

localClient, _ := node.LocalClient()
tlsLn := tls.NewListener(ln, &tls.Config{
GetCertificate: localClient.GetCertificate,
})
localClient, _ := node.LocalClient()
ln = tls.NewListener(ln, &tls.Config{
GetCertificate: localClient.GetCertificate,
})
}

return &tailscaleSharedListener{
Listener: tlsLn,
Listener: ln,
key: lnKey,
}, nil
})
Expand All @@ -158,6 +212,11 @@ func getUDPListener(c context.Context, network string, host string, portRange st
return nil, fmt.Errorf("context is not a caddy.Context: %T", c)
}

// Strip any service suffix from the host (e.g. "app+plex" -> "app")
// so we can find the correct node. UDP service listeners are not yet
// supported, so we always use a plain ListenPacket on the node.
host, _, _ = strings.Cut(host, "+")

na, err := caddy.ParseNetworkAddress(caddy.JoinNetworkAddress(network, host, portRange))
if err != nil {
return nil, err
Expand Down