Home

tailroute @main - refs - log -
-
https://git.jolheiser.com/tailroute.git
Router for tailnet and funnel across tailscale
tree log patch
tailproxy: init gen Signed-off-by: jolheiser <git@jolheiser.com>
Signature
-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgBTEvCQk6VqUAdN2RuH6bj1dNkY oOpbPWj+jw4ua1B1cAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQDwbwpgQulT8qUVMS6pbmmOtuAo4P4AgRkBW4E2sdr0S5/zfQ14/Rbg1RKQOPeIFuH 5oxU3pmgD5kDPiNSSJegQ= -----END SSH SIGNATURE-----
jolheiser <git@jolheiser.com>
8 hours ago
9 changed files, 264 additions(+), 101 deletions(-)
cmd/tailproxy/README.mdcmd/tailproxy/args.gen.gocmd/tailproxy/main.gocmd/tailproxy/schema.cuego.modgo.mod.srigo.sumnix/module.nixnix/options.gen.nix
M cmd/tailproxy/README.md -> cmd/tailproxy/README.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
diff --git a/cmd/tailproxy/README.md b/cmd/tailproxy/README.md
index 9c3411f19e23477e6d78389ba772970ea3b818d0..23283817ddd99cbf8b264c6c13ab8e6ae7dad5a2 100644
--- a/cmd/tailproxy/README.md
+++ b/cmd/tailproxy/README.md
@@ -6,9 +6,12 @@ ## Config
 
 - `hostname`: The tailscale hostname, i.e. "git" would let you use "http://git" assuming you have MagicDNC enabled
 - `auth-key`: Tailscale/tsnet auth key for authenticating with Tailscale
+- `auth-key-file`: Path to a file containing a Tailscale/tsnet auth key for authenticating with Tailscale
 - `funnel`: Whether to enable as a Tailscale funnel
 - `data-dir`: The tsnet data directory, where things like authentication information/tsnet logs live (default is a `.tailproxy` dir created in the working directory)
 - `port`: The port to *proxy*
+
+[schema.cue](./schema.cue) contains the source of truth for configuration.
 
 ## Nix Usage
 
@@ -27,10 +30,12 @@ {
   services.tailproxy = {
     "myapp" = {
       enable = true;
-      hostname = "myapp";
-      auth-key = "<one-time auth key>"; # https://login.tailscale.com/admin/settings/keys
-      funnel = false;
-      port = 1234;
+      settings = {
+        hostname = "myapp";
+        auth-key = "<one-time auth key>"; # https://login.tailscale.com/admin/settings/keys
+        funnel = false;
+        port = 1234; # The port "myapp" is running on
+      };
     };
     "otherapp" = {
       # ...
I cmd/tailproxy/args.gen.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
diff --git a/cmd/tailproxy/args.gen.go b/cmd/tailproxy/args.gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..c15baafe5bd27436a6b2aa05afaa9b1949b7bf3a
--- /dev/null
+++ b/cmd/tailproxy/args.gen.go
@@ -0,0 +1,61 @@
+// Code generated by gen from schema.cue; DO NOT EDIT.
+
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+)
+
+type cliArgs struct {
+	// Tailscale hostname
+	Hostname string
+	// Tailscale one-time auth key
+	AuthKey string
+	// Path to a file containing a Tailscale auth key (takes precedence over auth-key)
+	AuthKeyFile string
+	// Expose on Tailscale funnel
+	Funnel bool
+	// tsnet data directory
+	DataDir string
+	// Host to proxy
+	Host string
+	// Port to proxy
+	Port int
+}
+
+// defaultArgs returns the defaults from schema.cue.
+func defaultArgs() cliArgs {
+	return cliArgs{
+		DataDir: ".tailproxy",
+		Host:    "localhost",
+	}
+}
+
+// registerFlags defines a flag for every field of c, using c's current
+// values as flag defaults.
+func registerFlags(fs *flag.FlagSet, c *cliArgs) {
+	fs.StringVar(&c.Hostname, "hostname", c.Hostname, "Tailscale hostname")
+	fs.StringVar(&c.AuthKey, "auth-key", c.AuthKey, "Tailscale one-time auth key")
+	fs.StringVar(&c.AuthKeyFile, "auth-key-file", c.AuthKeyFile, "Path to a file containing a Tailscale auth key (takes precedence over auth-key)")
+	fs.BoolVar(&c.Funnel, "funnel", c.Funnel, "Expose on Tailscale funnel")
+	fs.StringVar(&c.DataDir, "data-dir", c.DataDir, "tsnet data directory")
+	fs.StringVar(&c.Host, "host", c.Host, "Host to proxy")
+	fs.IntVar(&c.Port, "port", c.Port, "Port to proxy")
+}
+
+// validate checks c against the constraints in schema.cue.
+func (c *cliArgs) validate() error {
+	var errs []error
+	if c.DataDir == "" {
+		errs = append(errs, errors.New("data-dir: must not be empty"))
+	}
+	if c.Port < 1 {
+		errs = append(errs, fmt.Errorf("port: must be >= 1, got %v", c.Port))
+	}
+	if c.Port > 65535 {
+		errs = append(errs, fmt.Errorf("port: must be <= 65535, got %v", c.Port))
+	}
+	return errors.Join(errs...)
+}
M cmd/tailproxy/main.go -> cmd/tailproxy/main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
diff --git a/cmd/tailproxy/main.go b/cmd/tailproxy/main.go
index 0980a332edeb205a69b83aeb19d4d1b38fa21650..5f2f86e8c4d3488d89e42ff4ee2a4768e2348bef 100644
--- a/cmd/tailproxy/main.go
+++ b/cmd/tailproxy/main.go
@@ -1,5 +1,7 @@
 package main
 
+//go:generate go tool gen --schema schema.cue --go args.gen.go --nix ../../nix/options.gen.nix
+
 import (
 	"flag"
 	"fmt"
@@ -7,35 +9,30 @@ 	"net/http"
 	"net/http/httputil"
 	"net/url"
 	"os"
+	"strings"
 
 	"go.jolheiser.com/tailroute"
 )
 
-// Args are commandline arguments
-type Args struct {
-	Hostname string
-	AuthKey  string
-	Funnel   bool
-	DataDir  string
-	Host     string
-	Port     int
-}
-
 func main() {
-	args := &Args{
-		DataDir: ".tailproxy",
-		Host:    "localhost",
-	}
 	fs := flag.NewFlagSet("tailproxy", flag.ExitOnError)
-	fs.StringVar(&args.Hostname, "hostname", args.Hostname, "Tailscale hostname")
-	fs.StringVar(&args.AuthKey, "auth-key", args.AuthKey, "Tailscale auth key")
-	fs.BoolVar(&args.Funnel, "funnel", args.Funnel, "Expose on Tailscale funnel")
-	fs.StringVar(&args.DataDir, "data-dir", args.DataDir, "tsnet data directory")
-	fs.StringVar(&args.Host, "host", args.Host, "Host to proxy")
-	fs.IntVar(&args.Port, "port", args.Port, "Port to proxy")
+	args := defaultArgs()
+	registerFlags(fs, &args)
 	if err := fs.Parse(os.Args[1:]); err != nil {
 		panic(err)
 	}
+	if err := args.validate(); err != nil {
+		panic(err)
+	}
+
+	if args.AuthKeyFile != "" {
+		key, err := os.ReadFile(args.AuthKeyFile)
+		if err != nil {
+			panic(err)
+		}
+		args.AuthKey = strings.TrimSpace(string(key))
+	}
+
 	os.Setenv("TS_AUTHKEY", args.AuthKey)
 
 	proxyURL, err := url.Parse(fmt.Sprintf("http://%s:%d", args.Host, args.Port))
I cmd/tailproxy/schema.cue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
diff --git a/cmd/tailproxy/schema.cue b/cmd/tailproxy/schema.cue
new file mode 100644
index 0000000000000000000000000000000000000000..63e2e7b8c463e4fce43877036585346c41150376
--- /dev/null
+++ b/cmd/tailproxy/schema.cue
@@ -0,0 +1,25 @@
+package tailproxy
+
+#Path: string & !=""
+#Port: int & >0 & <65536 @nix(type=types.port)
+
+// Tailscale hostname
+hostname: string
+
+// Tailscale one-time auth key
+"auth-key": string | *""
+
+// Path to a file containing a Tailscale auth key (takes precedence over auth-key)
+"auth-key-file": string | *""
+
+// Expose on Tailscale funnel
+funnel: bool | *false
+
+// tsnet data directory
+"data-dir": #Path | *".tailproxy" @nix(default="/var/lib/tailproxy")
+
+// Host to proxy
+host: string | *"localhost"
+
+// Port to proxy
+port: #Port
M go.mod -> go.mod
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
diff --git a/go.mod b/go.mod
index 990f30fce1a83afc24afa03404210afcb0a76358..b32fabff9229a4cd978fe5fd68a4230d04194726 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,15 @@
 require tailscale.com v1.90.6
 
 require (
+	cuelabs.dev/go/oci/ociregistry v0.0.0-20260601085548-328ff8e2c943 // indirect
+	cuelang.org/go v0.17.1 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/akutz/memconn v0.1.0 // indirect
 	github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
-	github.com/coder/websocket v1.8.12 // indirect
+	github.com/cockroachdb/apd/v3 v3.2.3 // indirect
+	github.com/coder/websocket v1.8.14 // indirect
 	github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
+	github.com/emicklei/proto v1.14.3 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/gaissmai/bart v0.18.0 // indirect
 	github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
@@ -24,7 +28,13 @@ 	github.com/klauspost/compress v1.17.11 // indirect
 	github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
 	github.com/mdlayher/socket v0.5.0 // indirect
 	github.com/mitchellh/go-ps v1.0.0 // indirect
+	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+	github.com/opencontainers/go-digest v1.0.0 // indirect
+	github.com/opencontainers/image-spec v1.1.1 // indirect
+	github.com/pelletier/go-toml/v2 v2.3.1 // indirect
 	github.com/prometheus-community/pro-bing v0.4.0 // indirect
+	github.com/protocolbuffers/txtpbfmt v0.0.0-20260420112717-c39628bde8b5 // indirect
+	github.com/rogpeppe/go-internal v1.15.0 // indirect
 	github.com/safchain/ethtool v0.3.0 // indirect
 	github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
 	github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
@@ -34,18 +44,23 @@ 	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/x448/float16 v0.8.4 // indirect
+	go.jolheiser.com/gen v0.0.0-20260924184449-e423ae92176c // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
-	golang.org/x/crypto v0.38.0 // indirect
+	golang.org/x/crypto v0.53.0 // indirect
 	golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
-	golang.org/x/net v0.40.0 // indirect
-	golang.org/x/oauth2 v0.30.0 // indirect
-	golang.org/x/sync v0.14.0 // indirect
-	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/term v0.32.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
+	golang.org/x/net v0.56.0 // indirect
+	golang.org/x/oauth2 v0.36.0 // indirect
+	golang.org/x/sync v0.21.0 // indirect
+	golang.org/x/sys v0.46.0 // indirect
+	golang.org/x/term v0.44.0 // indirect
+	golang.org/x/text v0.38.0 // indirect
 	golang.org/x/time v0.11.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
+	google.golang.org/protobuf v1.36.3 // indirect
 	gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect
 )
+
+tool go.jolheiser.com/gen
M go.mod.sri -> go.mod.sri
1
2
3
4
5
6
7
8
9
diff --git a/go.mod.sri b/go.mod.sri
index f67f6c88f6e9239c89b3a6e3f4239e7a934654db..acc454f4e7dd600771173ae02df242f8f022a53f 100644
--- a/go.mod.sri
+++ b/go.mod.sri
@@ -1 +1 @@
-sha256-QN+L3mZy2N960LSMZH1WhqFAVpYa1lfo0NqR2ZWx50s=
\ No newline at end of file
+sha256-rcidwYILhKnuWYnIEYwzT6zxmbB4gBpSC/Vm4Nqp5so=
\ No newline at end of file
M go.sum -> go.sum
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
diff --git a/go.sum b/go.sum
index e56a81ea9666d65ce7e3a48bd1e6319e3334fed5..839c1195c61e7acbec88ce3095a36eea5974433f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,9 @@
 9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
 9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
+cuelabs.dev/go/oci/ociregistry v0.0.0-20260601085548-328ff8e2c943 h1:XUtzi/yWlmuy8V6kkmVbbmirmUqcFe9Ce3gmEaHXf1Q=
+cuelabs.dev/go/oci/ociregistry v0.0.0-20260601085548-328ff8e2c943/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8=
+cuelang.org/go v0.17.1 h1:liOkxZDqTHrzq0USJX+6bMYOZ5PSf+wzvQr15AHpDCQ=
+cuelang.org/go v0.17.1/go.mod h1:xlly/o1wSLvxOsi5vkQGieU0rLOt7TvUIizOFtnxHRU=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
@@ -42,8 +46,10 @@ github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
 github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
 github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
-github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
-github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
+github.com/cockroachdb/apd/v3 v3.2.3 h1:4Zx+I3R35bFXMnltzmjP79i2cravE4jTRL6ps9Aux80=
+github.com/cockroachdb/apd/v3 v3.2.3/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
+github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
+github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
 github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
 github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
 github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
@@ -56,6 +62,8 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
 github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
 github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
 github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
+github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q=
+github.com/emicklei/proto v1.14.3/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -68,6 +76,8 @@ github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
 github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
 github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
+github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
 github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
 github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
 github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
@@ -106,6 +116,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
+github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
 github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
 github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
@@ -118,10 +132,18 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
 github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
+github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
 github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
@@ -132,8 +154,10 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
 github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/protocolbuffers/txtpbfmt v0.0.0-20260420112717-c39628bde8b5 h1:Mckui8l+Wqz2Ve7XQvsE8SbHNmDWu8NA7Xce5NFJ/kM=
+github.com/protocolbuffers/txtpbfmt v0.0.0-20260420112717-c39628bde8b5/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY=
+github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
+github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
 github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
 github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
 github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
@@ -168,45 +192,52 @@ 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=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+go.jolheiser.com/gen v0.0.0-20260924184449-e423ae92176c h1:gApMH2cOXXjpYYL9Ca8TgxjUrqloexUCFykO4yfH6sg=
+go.jolheiser.com/gen v0.0.0-20260924184449-e423ae92176c/go.mod h1:aAl7GQRWwrUZ2PvefV97hG4fWsQei2c6vzK7ghD0dMQ=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
 go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
 golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
 golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
 golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
+golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
-golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
-golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
-golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
 golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
 golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
 google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
 google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
 gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
 honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
M nix/module.nix -> nix/module.nix
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
diff --git a/nix/module.nix b/nix/module.nix
index 4c8391307d156b2f4c72ee8b714f85cc48151f82..d1038b80194f9da9c0edd2332f984dfeaffb62ac 100644
--- a/nix/module.nix
+++ b/nix/module.nix
@@ -7,10 +7,27 @@ }:
 let
   cfg = config.services.tailproxy;
   pkg = pkgs.callPackage ./tailproxy.nix { inherit pkgs; };
+  toArgs =
+    settings:
+    let
+      fmt = v: if lib.isBool v then lib.boolToString v else toString v;
+    in
+    lib.escapeShellArgs (
+      lib.concatLists (
+        lib.mapAttrsToList (k: v: map (x: "--${k}=${fmt x}") (lib.toList v)) (
+          lib.filterAttrs (_: v: v != null) settings
+        )
+      )
+    );
   instanceOptions =
-    { name, config, ... }:
+    { name, ... }:
     let
-      inherit (lib) mkEnableOption mkOption types;
+      inherit (lib)
+        mkEnableOption
+        mkOption
+        mkDefault
+        types
+        ;
     in
     {
       options = {
@@ -22,41 +39,20 @@           description = "tailproxy package to use";
           default = pkg;
         };
 
-        hostname = mkOption {
-          type = types.str;
-          default = name;
-          description = "Tailscale hostname";
-        };
-
-        authKey = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          description = "Tailscale auth key";
-        };
-
-        funnel = mkOption {
-          type = types.bool;
-          default = false;
-          description = "Expose on Tailscale funnel";
+        settings = lib.mkOption {
+          type = lib.types.submodule {
+            options = import ./options.gen.nix { inherit lib; };
+          };
+          default = { };
+          description = "${name} settings, passed to tailproxy as command-line flags.";
         };
 
         dataDir = mkOption {
           type = types.str;
-          description = "tsnet data directory";
+          description = "tsnet data directory (takes precedence over the settings data-dir)";
           default = "/var/lib/tailproxy-${name}";
         };
 
-        host = mkOption {
-          type = types.str;
-          description = "Host to proxy";
-          default = "localhost";
-        };
-
-        port = mkOption {
-          type = types.int;
-          description = "Port to proxy";
-        };
-
         user = mkOption {
           type = types.str;
           default = "tailproxy-${name}";
@@ -69,6 +65,7 @@           default = "tailproxy-${name}";
           description = "Group account under which tailproxy runs";
         };
       };
+      config.settings.data-dir = mkDefault "/var/lib/tailproxy-${name}";
     };
 in
 {
@@ -87,21 +84,7 @@         description = "tailproxy-${name}";
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ];
         serviceConfig = {
-          ExecStart =
-            let
-              args =
-                lib.optionals (instanceCfg.authKey != null) [
-                  "--auth-key=${instanceCfg.authKey}"
-                ]
-                ++ [
-                  (lib.optionalString instanceCfg.funnel "--funnel")
-                  "--hostname=${instanceCfg.hostname}"
-                  "--host=${instanceCfg.host}"
-                  "--port=${builtins.toString instanceCfg.port}"
-                  "--data-dir=${instanceCfg.dataDir}"
-                ];
-            in
-            "${instanceCfg.package}/bin/tailproxy ${lib.concatStringsSep " " args}";
+          ExecStart = "${instanceCfg.package}/bin/tailproxy ${toArgs instanceCfg.settings}";
           User = instanceCfg.user;
           Restart = "on-failure";
         };
@@ -113,7 +96,7 @@       name: instanceCfg:
       lib.nameValuePair instanceCfg.user {
         isSystemUser = true;
         group = instanceCfg.user;
-        home = instanceCfg.dataDir;
+        home = instanceCfg.settings.data-dir;
         createHome = true;
       }
     ) (lib.filterAttrs (name: instanceCfg: instanceCfg.enable) cfg);
I nix/options.gen.nix
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
diff --git a/nix/options.gen.nix b/nix/options.gen.nix
new file mode 100644
index 0000000000000000000000000000000000000000..9bb401dc12d160e0e70850598a93574c8d5f0d52
--- /dev/null
+++ b/nix/options.gen.nix
@@ -0,0 +1,46 @@
+# Code generated by gen from schema.cue; DO NOT EDIT.
+{ lib }:
+let
+  inherit (lib) mkOption types;
+in
+{
+  hostname = mkOption {
+    type = types.str;
+    description = "Tailscale hostname";
+  };
+
+  auth-key = mkOption {
+    type = types.str;
+    default = "";
+    description = "Tailscale one-time auth key";
+  };
+
+  auth-key-file = mkOption {
+    type = types.str;
+    default = "";
+    description = "Path to a file containing a Tailscale auth key (takes precedence over auth-key)";
+  };
+
+  funnel = mkOption {
+    type = types.bool;
+    default = false;
+    description = "Expose on Tailscale funnel";
+  };
+
+  data-dir = mkOption {
+    type = types.nonEmptyStr;
+    default = "/var/lib/tailproxy";
+    description = "tsnet data directory";
+  };
+
+  host = mkOption {
+    type = types.str;
+    default = "localhost";
+    description = "Host to proxy";
+  };
+
+  port = mkOption {
+    type = types.port;
+    description = "Port to proxy";
+  };
+}