Skip to content
Merged
Show file tree
Hide file tree
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ go build ./cmd/caddy

Multiple example configurations are provided in the [examples directory].
These examples expect an [auth key] to be set in the `TS_AUTHKEY` environment variable.
The [auth key] can optionally be an [oauth client secret] which will generate an [auth key] on demand.
In order for an [oauth client secret] to generate an [auth key] the tag that was registered with the [oauth client secret] must be provided in the caddy config. The ephemeral config option will not have an effect for oauth generated tokens. The linked documentation for [oauth client secret] indicates query parameters that can be appended to the client secret to control these settings.
All nodes registered while running these examples will be ephemeral and removed after disconnect.
See the comments in the individual files for details.

Expand Down Expand Up @@ -114,6 +116,10 @@ Supported options are:
# Default: false
webui true|false

# If set these tags will be included when registering the node
tags tag:test


# Any number of named node configs can be specified to override global options.
<node_name> {
# Tailscale auth key used to register this node.
Expand All @@ -134,6 +140,14 @@ Supported options are:

# If true, run the Tailscale web UI for remotely managing this node.
webui true|false

# If set these tags will be included when registering the node
# Overrides global configuration tags
tags tag:test

# If set this port will be used for tsnet.
# When unset tsnet will pick a random available port
port 4145
}
}
}
Expand Down Expand Up @@ -164,6 +178,7 @@ For Caddy [JSON config], add the `tailscale` app with fields from [tscaddy.App]:
[global option]: https://caddyserver.com/docs/caddyfile/options
[placeholders]: https://caddyserver.com/docs/conventions#placeholders
[auth key]: https://tailscale.com/kb/1085/auth-keys/
[oauth client secret]: https://tailscale.com/kb/1215/oauth-clients#register-new-nodes-using-oauth-credentials
[JSON config]: https://caddyserver.com/docs/json/
[tscaddy.App]: https://pkg.go.dev/github.com/tailscale/caddy-tailscale#App

Expand Down
10 changes: 10 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type App struct {
// WebUI specifies whether Tailscale nodes should run the Web UI for remote management.
WebUI bool `json:"webui,omitempty" caddy:"namespace=tailscale.webui"`

// Tags to apply to all nodes when registered.
Tags []string `json:"tags,omitempty" caddy:"namespace=tailscale.tags"`

// Nodes is a map of per-node configuration which overrides global options.
Nodes map[string]Node `json:"nodes,omitempty" caddy:"namespace=tailscale"`

Expand All @@ -63,6 +66,9 @@ type Node struct {
// WebUI specifies whether the node should run the Web UI for remote management.
WebUI opt.Bool `json:"webui,omitempty" caddy:"namespace=tailscale.webui"`

// Tags to apply to the node when registered. Overrides global tags.
Tags []string `json:"tags,omitempty" caddy:"namespace=tailscale.tags"`

// Hostname is the hostname to use when registering the node.
Hostname string `json:"hostname,omitempty" caddy:"namespace=tailscale.hostname"`

Expand Down Expand Up @@ -142,6 +148,8 @@ func parseAppConfig(d *caddyfile.Dispenser, _ any) (any, error) {
} else {
app.WebUI = true
}
case "tags":
app.Tags = d.RemainingArgs()
default:
node, err := parseNodeConfig(d)
if app.Nodes == nil {
Expand Down Expand Up @@ -223,6 +231,8 @@ func parseNodeConfig(d *caddyfile.Dispenser) (Node, error) {
} else {
node.WebUI = opt.NewBool(true)
}
case "tags":
node.Tags = segment.RemainingArgs()
default:
return node, segment.Errf("unrecognized subdirective: %s", segment.Val())
}
Expand Down
28 changes: 2 additions & 26 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
module github.com/tailscale/caddy-tailscale

go 1.25.1
go 1.25.3

require (
github.com/caddyserver/caddy/v2 v2.10.2
github.com/caddyserver/certmagic v0.24.0
github.com/google/go-cmp v0.7.0
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
go.uber.org/zap v1.27.0
tailscale.com v1.88.4
tailscale.com v1.90.6
)

require (
Expand All @@ -30,20 +30,6 @@ require (
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/aws/aws-sdk-go-v2 v1.36.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.16 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.69 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/ccoveille/go-safecast v1.6.1 // indirect
Expand All @@ -53,15 +39,13 @@ require (
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
Expand All @@ -74,7 +58,6 @@ require (
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
Expand All @@ -85,31 +68,26 @@ require (
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/illarion/gonotify/v3 v3.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/libdns v1.1.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.1.2 // indirect
Expand Down Expand Up @@ -151,12 +129,10 @@ require (
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
Expand Down
12 changes: 2 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,6 @@ github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVK
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
Expand Down Expand Up @@ -539,7 +537,6 @@ github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down Expand Up @@ -692,8 +689,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -702,7 +697,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down Expand Up @@ -799,8 +793,6 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand All @@ -819,5 +811,5 @@ software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
tailscale.com v1.88.4 h1:fXWotRMi9ZARyHRdKQa4ohXj8kqtemvvTzjreWLHVHo=
tailscale.com v1.88.4/go.mod h1:LHaTiwRgzebPDLgZ6RQQVzX+1SR5fbNl51fzm7UtMaw=
tailscale.com v1.90.6 h1:EhYPiZP/xcLeinikaLA0kn4CQT3+z9SZ13IB/kzWhd4=
tailscale.com v1.90.6/go.mod h1:+9EX6pOGCNa6pxCVRhhlJLy/qnkDzOplFYpeZyYlCT0=
16 changes: 13 additions & 3 deletions module.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,10 @@ func getNode(ctx caddy.Context, name string) (*tailscaleNode, error) {
UserLogf: func(format string, args ...any) {
app.logger.Sugar().Infof(format, args...)
},
Ephemeral: getEphemeral(name, app),
RunWebClient: getWebUI(name, app),
Port: getPort(name, app),
Ephemeral: getEphemeral(name, app),
RunWebClient: getWebUI(name, app),
Port: getPort(name, app),
AdvertiseTags: getTags(name, app),
}

if s.AuthKey, err = getAuthKey(name, app); err != nil {
Expand Down Expand Up @@ -401,6 +402,15 @@ func getWebUI(name string, app *App) bool {
return app.WebUI
}

func getTags(name string, app *App) []string {
if node, ok := app.Nodes[name]; ok {
if node.Tags != nil {
return node.Tags
}
}
return app.Tags
}

// 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 {
Expand Down
92 changes: 92 additions & 0 deletions module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,98 @@ func Test_GetWebUI(t *testing.T) {
}
}

func Test_GetTags(t *testing.T) {
tests := map[string]struct {
appTags []string
nodeTags []string
want []string
}{
"no tags": {
appTags: nil,
nodeTags: nil,
want: nil,
},
"app-level tags only": {
appTags: []string{"tag:test1", "tag:test2"},
nodeTags: nil,
want: []string{"tag:test1", "tag:test2"},
},
"node-level tags override app tags": {
appTags: []string{"tag:app1", "tag:app2"},
nodeTags: []string{"tag:node1", "tag:node2"},
want: []string{"tag:node1", "tag:node2"},
},
"empty node tags override app tags": {
appTags: []string{"tag:app1", "tag:app2"},
nodeTags: []string{},
want: []string{},
},
"single tag": {
appTags: []string{"tag:production"},
nodeTags: nil,
want: []string{"tag:production"},
},
"multiple node tags": {
appTags: nil,
nodeTags: []string{"tag:web", "tag:frontend", "tag:production"},
want: []string{"tag:web", "tag:frontend", "tag:production"},
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
app := &App{
Tags: tt.appTags,
Nodes: make(map[string]Node),
}
if tt.nodeTags != nil {
app.Nodes["testnode"] = Node{
Tags: tt.nodeTags,
}
}
if err := app.Provision(caddy.Context{}); err != nil {
t.Fatal(err)
}

got := getTags("testnode", app)
if len(got) != len(tt.want) {
t.Errorf("getTags() = %v, want %v", got, tt.want)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("getTags() = %v, want %v", got, tt.want)
return
}
}
})
}

// Test node without config gets app-level tags
t.Run("node without config uses app tags", func(t *testing.T) {
app := &App{
Tags: []string{"tag:default1", "tag:default2"},
Nodes: make(map[string]Node),
}
if err := app.Provision(caddy.Context{}); err != nil {
t.Fatal(err)
}

got := getTags("unconfigured-node", app)
want := []string{"tag:default1", "tag:default2"}
if len(got) != len(want) {
t.Errorf("getTags() = %v, want %v", got, want)
return
}
for i := range got {
if got[i] != want[i] {
t.Errorf("getTags() = %v, want %v", got, want)
return
}
}
})
}

func Test_Listen(t *testing.T) {
must.Do(caddy.Run(new(caddy.Config)))
ctx := caddy.ActiveContext()
Expand Down
Loading