Home

tailroute @main - refs - log -
-
https://git.jolheiser.com/tailroute.git
Router for tailnet and funnel across tailscale
tree log patch
merge tailproxy into tailroute and update tailscale Signed-off-by: jolheiser <git@jolheiser.com>
Signature
-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgBTEvCQk6VqUAdN2RuH6bj1dNkY oOpbPWj+jw4ua1B1cAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQApU6PuhN50uLBU1u5yEZnrSkKLvXiaGHvrTzvsHXS9JuiR/bz/ns2CCo1cqow39Mi OlwU1QSASdCtWMkKpRJwU= -----END SSH SIGNATURE-----
jolheiser <git@jolheiser.com>
2 weeks ago
12 changed files, 354 additions(+), 47 deletions(-)
.gitignorecmd/tailproxy/README.mdcmd/tailproxy/main.goflake.lockflake.nixgo.modgo.mod.srigo.sumnix/default.nixnix/module.nixnix/overlay.nixnix/tailproxy.nix
M .gitignore.gitignore
1
2
3
4
5
6
7
diff --git a/.gitignore b/.gitignore
index a004a31d3b0a1789eefdf74491f4a19fb7aecc10..b4e54b9af7a97bd4275b23c1d60117c61cac4250 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 .tailroute
+/tailproxy
I 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
35
36
37
38
39
40
41
42
43
44
45
46
47
diff --git a/cmd/tailproxy/README.md b/cmd/tailproxy/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9c3411f19e23477e6d78389ba772970ea3b818d0
--- /dev/null
+++ b/cmd/tailproxy/README.md
@@ -0,0 +1,41 @@
+# tailproxy
+
+A tiny reverse proxy to expose apps over Tailscale as their own application/service.
+
+## 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
+- `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*
+
+## Nix Usage
+
+As a flake:
+
+```nix
+tailproxy.url = "git+https://git.jolheiser.com/tailroute.git";
+# ...
+imports = [inputs.tailproxy.nixosModules.default];
+```
+
+[Module](../nix/module.nix):
+
+```nix
+{
+  services.tailproxy = {
+    "myapp" = {
+      enable = true;
+      hostname = "myapp";
+      auth-key = "<one-time auth key>"; # https://login.tailscale.com/admin/settings/keys
+      funnel = false;
+      port = 1234;
+    };
+    "otherapp" = {
+      # ...
+    };
+  };
+}
+```
+
I 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
diff --git a/cmd/tailproxy/main.go b/cmd/tailproxy/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd08e895ac2ae318c3c552ac524cd96578671279
--- /dev/null
+++ b/cmd/tailproxy/main.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+	"os"
+
+	"go.jolheiser.com/tailroute"
+)
+
+// Args are commandline arguments
+type Args struct {
+	Hostname string
+	AuthKey  string
+	Funnel   bool
+	DataDir  string
+	Port     int
+}
+
+func main() {
+	args := &Args{
+		DataDir: ".tailproxy",
+	}
+	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.IntVar(&args.Port, "port", args.Port, "Port to proxy")
+	if err := fs.Parse(os.Args[1:]); err != nil {
+		panic(err)
+	}
+	os.Setenv("TS_AUTHKEY", args.AuthKey)
+
+	proxyURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", args.Port))
+	if err != nil {
+		panic(err)
+	}
+
+	proxy := httputil.NewSingleHostReverseProxy(proxyURL)
+	handler := http.HandlerFunc(proxy.ServeHTTP)
+
+	r := tailroute.Router{
+		Tailnet: handler,
+	}
+	if args.Funnel {
+		r.Funnel = handler
+	}
+
+	if err := r.Serve(args.Hostname, args.DataDir); err != nil {
+		panic(err)
+	}
+}
I flake.lock
 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
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000000000000000000000000000000000..43f5d18754de728e604375a24a2e67a1e54960f3
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+  "nodes": {
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1754800730,
+        "narHash": "sha256-HfVZCXic9XLBgybP0318ym3cDnGwBs/+H5MgxFVYF4I=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "641d909c4a7538f1539da9240dedb1755c907e40",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
I flake.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
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000000000000000000000000000000000..78556418abc323af4e68f21488d29ba1403ca00e
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,26 @@
+{
+  inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
+  outputs =
+    {
+      self,
+      nixpkgs,
+      ...
+    }:
+    let
+      systems = [
+        "aarch64-darwin"
+        "aarch64-linux"
+        "x86_64-darwin"
+        "x86_64-linux"
+      ];
+      forAllSystems = f: nixpkgs.lib.genAttrs systems f;
+    in
+    {
+      overlays.default = import ./nix/overlay.nix;
+      nixosModules = {
+        tailproxy = import ./nix/module.nix;
+        default = self.nixosModules.tailproxy;
+      };
+      packages = forAllSystems (system: import ./nix { pkgs = import nixpkgs { inherit system; }; });
+    };
+}
M go.modgo.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
diff --git a/go.mod b/go.mod
index 8793a02db00a8336fa7b8f7a6de6d88efb89aeaf..a9782bf04d9be618668515e152aa7ef9af89ac92 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,10 @@
 module go.jolheiser.com/tailroute
 
-go 1.24.0
+go 1.24.4
 
-toolchain go1.24.1
+toolchain go1.24.6
 
-require tailscale.com v1.84.1
+require tailscale.com v1.86.4
 
 require (
 	filippo.io/edwards25519 v1.1.0 // indirect
@@ -38,8 +38,6 @@ 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/csrf v1.7.3 // indirect
-	github.com/gorilla/securecookie v1.1.2 // indirect
 	github.com/hdevalence/ed25519consensus v0.2.0 // indirect
 	github.com/illarion/gonotify/v3 v3.0.2 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -60,21 +58,21 @@ 	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-20250304000100-91a0587fb251 // indirect
+	github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
 	github.com/vishvananda/netns v0.0.4 // indirect
 	github.com/x448/float16 v0.8.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.37.0 // indirect
+	golang.org/x/crypto v0.38.0 // indirect
 	golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
-	golang.org/x/mod v0.23.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sync v0.13.0 // indirect
-	golang.org/x/sys v0.32.0 // indirect
-	golang.org/x/term v0.31.0 // indirect
-	golang.org/x/text v0.24.0 // indirect
-	golang.org/x/time v0.10.0 // indirect
-	golang.org/x/tools v0.30.0 // indirect
+	golang.org/x/mod v0.24.0 // indirect
+	golang.org/x/net v0.40.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/time v0.11.0 // indirect
+	golang.org/x/tools v0.33.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
 	gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect
I go.mod.sri
1
2
3
4
5
6
7
8
diff --git a/go.mod.sri b/go.mod.sri
new file mode 100644
index 0000000000000000000000000000000000000000..ef1a1a6a8a235a21f743caa74333c44a44998fdb
--- /dev/null
+++ b/go.mod.sri
@@ -0,0 +1 @@
+sha256-FG5sDOfkHMoLkCelT5LzLLgkfJYsYxfDKP6DwUWSLlY=
\ No newline at end of file
M go.sumgo.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
diff --git a/go.sum b/go.sum
index 4eb104925606d9bb4cf9dc85de30ded3e8d3f580..c5d4b18134d29ac77c48dd7b266a0f2d2d4871eb 100644
--- a/go.sum
+++ b/go.sum
@@ -59,8 +59,6 @@ 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/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
-github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
 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=
@@ -85,16 +83,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
 github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
 github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0=
-github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
-github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
-github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
 github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
 github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
@@ -171,8 +163,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
 github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
 github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
 github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
-github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 h1:h/41LFTrwMxB9Xvvug0kRdQCU5TlV1+pAMQw0ZtDE3U=
-github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
+github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
+github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
 github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
 github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
 github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
@@ -190,36 +182,36 @@ 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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
-golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+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/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.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
-golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+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/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
-golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 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-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
-golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
-golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
-golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
-golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+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/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.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=
@@ -238,5 +230,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
 howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
 software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
 software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
-tailscale.com v1.84.1 h1:xtuiYeAIUR+dRztPzzqUsjj+Fv/06vz28zoFaP1k/Os=
-tailscale.com v1.84.1/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=
+tailscale.com v1.86.4 h1:KHAmLyzVn50t2P5877wohBU0UPVeIMHC9XDzRw4Ycz4=
+tailscale.com v1.86.4/go.mod h1:Lm8dnzU2i/Emw15r6sl3FRNp/liSQ/nYw6ZSQvIdZ1M=
I nix/default.nix
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
diff --git a/nix/default.nix b/nix/default.nix
new file mode 100644
index 0000000000000000000000000000000000000000..94b532715b2e994a97092b9eaa2e84be73798e02
--- /dev/null
+++ b/nix/default.nix
@@ -0,0 +1,10 @@
+{
+  pkgs ? import <nixpkgs> { },
+}:
+let
+  tailproxy = pkgs.callPackage ./tailproxy.nix { inherit pkgs; };
+in
+{
+  inherit tailproxy;
+  default = tailproxy;
+}
I 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
124
diff --git a/nix/module.nix b/nix/module.nix
new file mode 100644
index 0000000000000000000000000000000000000000..4c69aba4c646553f538a663dc0f710318378e95f
--- /dev/null
+++ b/nix/module.nix
@@ -0,0 +1,118 @@
+{
+  pkgs,
+  lib,
+  config,
+  ...
+}:
+let
+  cfg = config.services.tailproxy;
+  pkg = pkgs.callPackage ./pkg.nix { inherit pkgs; };
+  instanceOptions =
+    { name, config, ... }:
+    let
+      inherit (lib) mkEnableOption mkOption types;
+    in
+    {
+      options = {
+        enable = mkEnableOption "Enable tailproxy for ${name}";
+
+        package = mkOption {
+          type = types.package;
+          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";
+        };
+
+        dataDir = mkOption {
+          type = types.str;
+          description = "tsnet data directory";
+          default = "/var/lib/tailproxy-${name}";
+        };
+
+        port = mkOption {
+          type = types.int;
+          description = "Port to proxy";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "tailproxy-${name}";
+          description = "User account under which tailproxy runs";
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "tailproxy-${name}";
+          description = "Group account under which tailproxy runs";
+        };
+      };
+    };
+in
+{
+  options = {
+    services.tailproxy = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.submodule instanceOptions);
+      default = { };
+      description = "Attribute set of tailproxy instances";
+    };
+  };
+  config = lib.mkIf (cfg != { }) {
+    systemd.services = lib.mapAttrs' (
+      name: instanceCfg:
+      lib.nameValuePair "tailproxy-${name}" {
+        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}"
+                  "--port=${builtins.toString instanceCfg.port}"
+                  "--data-dir=${instanceCfg.dataDir}"
+                ];
+            in
+            "${instanceCfg.package}/bin/tailproxy ${lib.concatStringsSep " " args}";
+          User = instanceCfg.user;
+          Restart = "on-failure";
+        };
+      }
+    ) (lib.filterAttrs (name: instanceCfg: instanceCfg.enable) cfg);
+
+    users.users = lib.mapAttrs' (
+      name: instanceCfg:
+      lib.nameValuePair instanceCfg.user {
+        isSystemUser = true;
+        group = instanceCfg.user;
+        home = instanceCfg.dataDir;
+        createHome = true;
+      }
+    ) (lib.filterAttrs (name: instanceCfg: instanceCfg.enable) cfg);
+
+    users.groups = lib.mapAttrs' (name: instanceCfg: lib.nameValuePair instanceCfg.user { }) (
+      lib.filterAttrs (name: instanceCfg: instanceCfg.enable) cfg
+    );
+  };
+}
I nix/overlay.nix
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/nix/overlay.nix b/nix/overlay.nix
new file mode 100644
index 0000000000000000000000000000000000000000..d664948148fe4fcb17482ec5340ae088ecadeaf6
--- /dev/null
+++ b/nix/overlay.nix
@@ -0,0 +1,7 @@
+final: prev: {
+  nixosModules = prev.nixosModules or { } // {
+    tailproxy = import ./module.nix;
+  };
+
+  tailproxy = final.callPackage ./tailproxy.nix { };
+}
I nix/tailproxy.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
diff --git a/nix/tailproxy.nix b/nix/tailproxy.nix
new file mode 100644
index 0000000000000000000000000000000000000000..115f84140404382505c4b4ea1c07654cf69e7de1
--- /dev/null
+++ b/nix/tailproxy.nix
@@ -0,0 +1,30 @@
+{
+  pkgs ? import <nixpkgs> { },
+}:
+let
+  name = "tailproxy";
+in
+pkgs.buildGoModule {
+  pname = name;
+  version = "main";
+  src = pkgs.nix-gitignore.gitignoreSource [ ] (
+    builtins.path {
+      inherit name;
+      path = ../.;
+    }
+  );
+  vendorHash = pkgs.lib.fileContents ../go.mod.sri;
+  subPackages = [ "cmd/tailproxy" ];
+  env.CGO_ENABLED = 0;
+  flags = [ "-trimpath" ];
+  ldflags = [
+    "-s"
+    "-w"
+    "-extldflags -static"
+  ];
+  meta = {
+    description = "Tailscale proxy";
+    homepage = "https://git.jolheiser.com/tailroute/tree/main/cmd/tailproxy";
+    mainProgram = "tailproxy";
+  };
+}