@jaspermayone.com's dotfiles
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.atelier.services.frps;
9in
10{
11 options.atelier.services.frps = {
12 enable = lib.mkEnableOption "frp server for tunneling services";
13
14 bindAddr = lib.mkOption {
15 type = lib.types.str;
16 default = "0.0.0.0";
17 description = "Address to bind frp server to";
18 };
19
20 bindPort = lib.mkOption {
21 type = lib.types.port;
22 default = 7000;
23 description = "Port for frp control connection";
24 };
25
26 vhostHTTPPort = lib.mkOption {
27 type = lib.types.port;
28 default = 7080;
29 description = "Port for HTTP virtual host traffic";
30 };
31
32 allowedTCPPorts = lib.mkOption {
33 type = lib.types.listOf lib.types.port;
34 default = lib.lists.range 20000 20099;
35 example = [
36 20000
37 20001
38 20002
39 20003
40 20004
41 ];
42 description = "TCP port range to allow for TCP tunnels (default: 20000-20099)";
43 };
44
45 allowedUDPPorts = lib.mkOption {
46 type = lib.types.listOf lib.types.port;
47 default = lib.lists.range 20000 20099;
48 example = [
49 20000
50 20001
51 20002
52 20003
53 20004
54 ];
55 description = "UDP port range to allow for UDP tunnels (default: 20000-20099)";
56 };
57
58 authToken = lib.mkOption {
59 type = lib.types.nullOr lib.types.str;
60 default = null;
61 description = "Authentication token for clients (deprecated: use authTokenFile)";
62 };
63
64 authTokenFile = lib.mkOption {
65 type = lib.types.nullOr lib.types.path;
66 default = null;
67 description = "Path to file containing authentication token";
68 };
69
70 domain = lib.mkOption {
71 type = lib.types.str;
72 example = "tun.hogwarts.channel";
73 description = "Base domain for subdomains (e.g., *.tun.hogwarts.channel)";
74 };
75
76 enableCaddy = lib.mkOption {
77 type = lib.types.bool;
78 default = true;
79 description = "Automatically configure Caddy reverse proxy for wildcard domain";
80 };
81 };
82
83 config = lib.mkIf cfg.enable {
84 assertions = [
85 {
86 assertion = cfg.authToken != null || cfg.authTokenFile != null;
87 message = "Either authToken or authTokenFile must be set for frps";
88 }
89 ];
90
91 # Open firewall ports for frp control connection and TCP/UDP tunnels
92 networking.firewall.allowedTCPPorts = [ cfg.bindPort ] ++ cfg.allowedTCPPorts;
93 networking.firewall.allowedUDPPorts = cfg.allowedUDPPorts;
94
95 # frp server service
96 systemd.services.frps =
97 let
98 tokenConfig =
99 if cfg.authTokenFile != null then
100 ''
101 auth.tokenSource.type = "file"
102 auth.tokenSource.file.path = "${cfg.authTokenFile}"
103 ''
104 else
105 ''auth.token = "${cfg.authToken}"'';
106
107 configFile = pkgs.writeText "frps.toml" ''
108 bindAddr = "${cfg.bindAddr}"
109 bindPort = ${toString cfg.bindPort}
110 vhostHTTPPort = ${toString cfg.vhostHTTPPort}
111
112 # Dashboard and Prometheus metrics
113 webServer.addr = "127.0.0.1"
114 webServer.port = 7400
115 enablePrometheus = true
116
117 # Authentication token - clients need this to connect
118 auth.method = "token"
119 ${tokenConfig}
120
121 # Subdomain support for *.${cfg.domain}
122 subDomainHost = "${cfg.domain}"
123
124 # Allow port ranges for TCP/UDP tunnels
125 # Format: [[{"start": 20000, "end": 20099}]]
126 allowPorts = [
127 { start = 20000, end = 20099 }
128 ]
129
130 # Custom 404 page
131 custom404Page = "${./404.html}"
132
133 # Logging
134 log.to = "console"
135 log.level = "info"
136 '';
137 in
138 {
139 description = "frp server for ${cfg.domain} tunneling";
140 after = [ "network.target" ];
141 wantedBy = [ "multi-user.target" ];
142 serviceConfig = {
143 Type = "simple";
144 Restart = "on-failure";
145 RestartSec = "5s";
146 ExecStart = "${pkgs.frp}/bin/frps -c ${configFile}";
147 };
148 };
149
150 # Automatically configure Caddy for wildcard domain
151 services.caddy = lib.mkIf cfg.enableCaddy {
152 enable = true;
153
154 # Dashboard for base domain
155 virtualHosts."${cfg.domain}" = {
156 extraConfig = ''
157 tls {
158 dns cloudflare {env.CLOUDFLARE_API_TOKEN}
159 }
160 header {
161 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
162 }
163
164 # Proxy /api/* to frps dashboard
165 handle /api/* {
166 reverse_proxy localhost:7400
167 }
168
169 # Serve dashboard HTML
170 handle {
171 root * ${./.}
172 try_files dashboard.html
173 file_server
174 }
175 '';
176 };
177
178 # Wildcard subdomain proxy to frps
179 virtualHosts."*.${cfg.domain}" = {
180 extraConfig = ''
181 tls {
182 dns cloudflare {env.CLOUDFLARE_API_TOKEN}
183 }
184 header {
185 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
186 }
187 reverse_proxy localhost:${toString cfg.vhostHTTPPort} {
188 header_up X-Forwarded-Proto {scheme}
189 header_up X-Forwarded-For {remote}
190 header_up Host {host}
191 }
192 handle_errors {
193 @404 expression {http.error.status_code} == 404
194 handle @404 {
195 root * ${./.}
196 rewrite * /404.html
197 file_server
198 }
199 }
200 '';
201 };
202 };
203 };
204}