vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / wstunnel.nix
blobbf56858a190fcb0c0de97ac6e696a9e723d5bcf4
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
8 let
9   cfg = config.services.wstunnel;
11   hostPortToString = { host, port }: "${host}:${toString port}";
13   hostPortSubmodule = {
14     options = {
15       host = lib.mkOption {
16         description = "The hostname.";
17         type = lib.types.str;
18       };
19       port = lib.mkOption {
20         description = "The port.";
21         type = lib.types.port;
22       };
23     };
24   };
26   commonOptions = {
27     enable = lib.mkEnableOption "this `wstunnel` instance" // {
28       default = true;
29     };
31     package = lib.mkPackageOption pkgs "wstunnel" { };
33     autoStart = lib.mkEnableOption "starting this wstunnel instance automatically" // {
34       default = true;
35     };
37     extraArgs = lib.mkOption {
38       description = ''
39         Extra command line arguments to pass to `wstunnel`.
40         Attributes of the form `argName = true;` will be translated to `--argName`,
41         and `argName = \"value\"` to `--argName value`.
42       '';
43       type = with lib.types; attrsOf (either str bool);
44       default = { };
45       example = {
46         "someNewOption" = true;
47         "someNewOptionWithValue" = "someValue";
48       };
49     };
51     # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval.
52     websocketPingInterval = lib.mkOption {
53       description = "Frequency at which the client will send websocket ping to the server.";
54       type = lib.types.nullOr lib.types.ints.unsigned;
55       default = null;
56     };
58     loggingLevel = lib.mkOption {
59       description = ''
60         Passed to --log-lvl
62         Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF
63         For more details, checkout [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
64       '';
65       type = lib.types.nullOr lib.types.str;
66       example = "INFO";
67       default = null;
68     };
70     environmentFile = lib.mkOption {
71       description = ''
72         Environment file to be passed to the systemd service.
73         Useful for passing secrets to the service to prevent them from being
74         world-readable in the Nix store.
75         Note however that the secrets are passed to `wstunnel` through
76         the command line, which makes them locally readable for all users of
77         the system at runtime.
78       '';
79       type = lib.types.nullOr lib.types.path;
80       default = null;
81       example = "/var/lib/secrets/wstunnelSecrets";
82     };
83   };
85   serverSubmodule =
86     { config, ... }:
87     {
88       options = commonOptions // {
89         listen = lib.mkOption {
90           description = ''
91             Address and port to listen on.
92             Setting the port to a value below 1024 will also give the process
93             the required `CAP_NET_BIND_SERVICE` capability.
94           '';
95           type = lib.types.submodule hostPortSubmodule;
96           default = {
97             host = "0.0.0.0";
98             port = if config.enableHTTPS then 443 else 80;
99           };
100           defaultText = lib.literalExpression ''
101             {
102               host = "0.0.0.0";
103               port = if enableHTTPS then 443 else 80;
104             }
105           '';
106         };
108         restrictTo = lib.mkOption {
109           description = ''
110             Accepted traffic will be forwarded only to this service.
111           '';
112           type = lib.types.listOf (lib.types.submodule hostPortSubmodule);
113           default = [ ];
114           example = [
115             {
116               host = "127.0.0.1";
117               port = 51820;
118             }
119           ];
120         };
122         enableHTTPS = lib.mkOption {
123           description = "Use HTTPS for the tunnel server.";
124           type = lib.types.bool;
125           default = true;
126         };
128         tlsCertificate = lib.mkOption {
129           description = ''
130             TLS certificate to use instead of the hardcoded one in case of HTTPS connections.
131             Use together with `tlsKey`.
132           '';
133           type = lib.types.nullOr lib.types.path;
134           default = null;
135           example = "/var/lib/secrets/cert.pem";
136         };
138         tlsKey = lib.mkOption {
139           description = ''
140             TLS key to use instead of the hardcoded on in case of HTTPS connections.
141             Use together with `tlsCertificate`.
142           '';
143           type = lib.types.nullOr lib.types.path;
144           default = null;
145           example = "/var/lib/secrets/key.pem";
146         };
148         useACMEHost = lib.mkOption {
149           description = ''
150             Use a certificate generated by the NixOS ACME module for the given host.
151             Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`.
152           '';
153           type = lib.types.nullOr lib.types.str;
154           default = null;
155           example = "example.com";
156         };
157       };
158     };
160   clientSubmodule =
161     { config, ... }:
162     {
163       options = commonOptions // {
164         connectTo = lib.mkOption {
165           description = "Server address and port to connect to.";
166           type = lib.types.str;
167           example = "https://wstunnel.server.com:8443";
168         };
170         localToRemote = lib.mkOption {
171           description = ''Listen on local and forwards traffic from remote.'';
172           type = lib.types.listOf (lib.types.str);
173           default = [ ];
174           example = [
175             "tcp://1212:google.com:443"
176             "unix:///tmp/wstunnel.sock:g.com:443"
177           ];
178         };
180         remoteToLocal = lib.mkOption {
181           description = "Listen on remote and forwards traffic from local. Only tcp is supported";
182           type = lib.types.listOf lib.types.str;
183           default = [ ];
184           example = [
185             "tcp://1212:google.com:443"
186             "unix://wstunnel.sock:g.com:443"
187           ];
188         };
190         addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024";
192         httpProxy = lib.mkOption {
193           description = ''
194             Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`).
196             ::: {.warning}
197             Passwords specified here will be world-readable in the Nix store!
198             To pass a password to the service, point the `environmentFile` option
199             to a file containing `PROXY_PASSWORD=<your-password-here>` and set
200             this option to `<user>:$PROXY_PASSWORD@<host>:<port>`.
201             Note however that this will also locally leak the passwords at
202             runtime via e.g. /proc/<pid>/cmdline.
203             :::
204           '';
205           type = lib.types.nullOr lib.types.str;
206           default = null;
207         };
209         soMark = lib.mkOption {
210           description = ''
211             Mark network packets with the SO_MARK sockoption with the specified value.
212             Setting this option will also enable the required `CAP_NET_ADMIN` capability
213             for the systemd service.
214           '';
215           type = lib.types.nullOr lib.types.ints.unsigned;
216           default = null;
217         };
219         upgradePathPrefix = lib.mkOption {
220           description = ''
221             Use a specific HTTP path prefix that will show up in the upgrade
222             request to the `wstunnel` server.
223             Useful when running `wstunnel` behind a reverse proxy.
224           '';
225           type = lib.types.nullOr lib.types.str;
226           default = null;
227           example = "wstunnel";
228         };
230         tlsSNI = lib.mkOption {
231           description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls.";
232           type = lib.types.nullOr lib.types.str;
233           default = null;
234         };
236         tlsVerifyCertificate = lib.mkOption {
237           description = "Whether to verify the TLS certificate of the server. It might be useful to set this to `false` when working with the `tlsSNI` option.";
238           type = lib.types.bool;
239           default = true;
240         };
242         upgradeCredentials = lib.mkOption {
243           description = ''
244             Use these credentials to authenticate during the HTTP upgrade request
245             (Basic authorization type, `USER:[PASS]`).
247             ::: {.warning}
248             Passwords specified here will be world-readable in the Nix store!
249             To pass a password to the service, point the `environmentFile` option
250             to a file containing `HTTP_PASSWORD=<your-password-here>` and set this
251             option to `<user>:$HTTP_PASSWORD`.
252             Note however that this will also locally leak the passwords at runtime
253             via e.g. /proc/<pid>/cmdline.
254             :::
255           '';
256           type = lib.types.nullOr lib.types.str;
257           default = null;
258         };
260         customHeaders = lib.mkOption {
261           description = "Custom HTTP headers to send during the upgrade request.";
262           type = lib.types.attrsOf lib.types.str;
263           default = { };
264           example = {
265             "X-Some-Header" = "some-value";
266           };
267         };
268       };
269     };
271   generateServerUnit = name: serverCfg: {
272     name = "wstunnel-server-${name}";
273     value =
274       let
275         certConfig = config.security.acme.certs.${serverCfg.useACMEHost};
276       in
277       {
278         description = "wstunnel server - ${name}";
279         requires = [
280           "network.target"
281           "network-online.target"
282         ];
283         after = [
284           "network.target"
285           "network-online.target"
286         ];
287         wantedBy = lib.optional serverCfg.autoStart "multi-user.target";
289         environment.RUST_LOG = serverCfg.loggingLevel;
291         serviceConfig = {
292           Type = "exec";
293           EnvironmentFile = lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
294           DynamicUser = true;
295           SupplementaryGroups = lib.optional (serverCfg.useACMEHost != null) certConfig.group;
296           PrivateTmp = true;
297           AmbientCapabilities = lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
298           NoNewPrivileges = true;
299           RestrictNamespaces = "uts ipc pid user cgroup";
300           ProtectSystem = "strict";
301           ProtectHome = true;
302           ProtectKernelTunables = true;
303           ProtectKernelModules = true;
304           ProtectControlGroups = true;
305           PrivateDevices = true;
306           RestrictSUIDSGID = true;
308           Restart = "on-failure";
309           RestartSec = 2;
310           RestartSteps = 20;
311           RestartMaxDelaySec = "5min";
312         };
314         script = with serverCfg; ''
315           ${lib.getExe package} \
316             server \
317             ${
318               lib.cli.toGNUCommandLineShell { } (
319                 lib.recursiveUpdate {
320                   restrict-to = map hostPortToString restrictTo;
321                   websocket-ping-frequency-sec = websocketPingInterval;
322                   tls-certificate =
323                     if !enableHTTPS then
324                       null
325                     else if useACMEHost != null then
326                       "${certConfig.directory}/fullchain.pem"
327                     else
328                       "${tlsCertificate}";
329                   tls-private-key =
330                     if !enableHTTPS then
331                       null
332                     else if useACMEHost != null then
333                       "${certConfig.directory}/key.pem"
334                     else
335                       "${tlsKey}";
336                 } extraArgs
337               )
338             } \
339             ${lib.escapeShellArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
340         '';
341       };
342   };
344   generateClientUnit = name: clientCfg: {
345     name = "wstunnel-client-${name}";
346     value = {
347       description = "wstunnel client - ${name}";
348       requires = [
349         "network.target"
350         "network-online.target"
351       ];
352       after = [
353         "network.target"
354         "network-online.target"
355       ];
356       wantedBy = lib.optional clientCfg.autoStart "multi-user.target";
358       environment.RUST_LOG = clientCfg.loggingLevel;
360       serviceConfig = {
361         Type = "exec";
362         EnvironmentFile = lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
363         DynamicUser = true;
364         PrivateTmp = true;
365         AmbientCapabilities =
366           (lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ])
367           ++ (lib.optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]);
368         NoNewPrivileges = true;
369         RestrictNamespaces = "uts ipc pid user cgroup";
370         ProtectSystem = "strict";
371         ProtectHome = true;
372         ProtectKernelTunables = true;
373         ProtectKernelModules = true;
374         ProtectControlGroups = true;
375         PrivateDevices = true;
376         RestrictSUIDSGID = true;
378         Restart = "on-failure";
379         RestartSec = 2;
380         RestartSteps = 20;
381         RestartMaxDelaySec = "5min";
382       };
384       script = with clientCfg; ''
385         ${lib.getExe package} \
386           client \
387           ${
388             lib.cli.toGNUCommandLineShell { } (
389               lib.recursiveUpdate {
390                 local-to-remote = localToRemote;
391                 remote-to-local = remoteToLocal;
392                 http-headers = lib.mapAttrsToList (n: v: "${n}:${v}") customHeaders;
393                 http-proxy = httpProxy;
394                 socket-so-mark = soMark;
395                 http-upgrade-path-prefix = upgradePathPrefix;
396                 tls-sni-override = tlsSNI;
397                 tls-verify-certificate = tlsVerifyCertificate;
398                 websocket-ping-frequency-sec = websocketPingInterval;
399                 http-upgrade-credentials = upgradeCredentials;
400               } extraArgs
401             )
402           } \
403           ${lib.escapeShellArg connectTo}
404       '';
405     };
406   };
409   options.services.wstunnel = {
410     enable = lib.mkEnableOption "wstunnel";
412     servers = lib.mkOption {
413       description = "`wstunnel` servers to set up.";
414       type = lib.types.attrsOf (lib.types.submodule serverSubmodule);
415       default = { };
416       example = {
417         "wg-tunnel" = {
418           listen = {
419             host = "0.0.0.0";
420             port = 8080;
421           };
422           enableHTTPS = true;
423           tlsCertificate = "/var/lib/secrets/fullchain.pem";
424           tlsKey = "/var/lib/secrets/key.pem";
425           restrictTo = [
426             {
427               host = "127.0.0.1";
428               port = 51820;
429             }
430           ];
431         };
432       };
433     };
435     clients = lib.mkOption {
436       description = "`wstunnel` clients to set up.";
437       type = lib.types.attrsOf (lib.types.submodule clientSubmodule);
438       default = { };
439       example = {
440         "wg-tunnel" = {
441           connectTo = "wss://wstunnel.server.com:8443";
442           localToRemote = [
443             "tcp://1212:google.com:443"
444             "tcp://2:n.lan:4?proxy_protocol"
445           ];
446           remoteToLocal = [
447             "socks5://[::1]:1212"
448             "unix://wstunnel.sock:g.com:443"
449           ];
450         };
451       };
452     };
453   };
455   config = lib.mkIf cfg.enable {
456     systemd.services =
457       (lib.mapAttrs' generateServerUnit (lib.filterAttrs (n: v: v.enable) cfg.servers))
458       // (lib.mapAttrs' generateClientUnit (lib.filterAttrs (n: v: v.enable) cfg.clients));
460     assertions =
461       (lib.mapAttrsToList (name: serverCfg: {
462         assertion = !(serverCfg.useACMEHost != null && serverCfg.tlsCertificate != null);
463         message = ''
464           Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
465         '';
466       }) cfg.servers)
467       ++
469         (lib.mapAttrsToList (name: serverCfg: {
470           assertion =
471             (serverCfg.tlsCertificate == null && serverCfg.tlsKey == null)
472             || (serverCfg.tlsCertificate != null && serverCfg.tlsKey != null);
473           message = ''
474             services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together.
475           '';
476         }) cfg.servers)
477       ++
479         (lib.mapAttrsToList (name: clientCfg: {
480           assertion = !(clientCfg.localToRemote == [ ] && clientCfg.remoteToLocal == [ ]);
481           message = ''
482             Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".remoteToLocal must be set.
483           '';
484         }) cfg.clients);
485   };
487   meta.maintainers = with lib.maintainers; [
488     alyaeanyx
489     raylas
490     rvdp
491     neverbehave
492   ];