9 cfg = config.services.wstunnel;
11 hostPortToString = { host, port }: "${host}:${toString port}";
16 description = "The hostname.";
20 description = "The port.";
21 type = lib.types.port;
27 enable = lib.mkEnableOption "this `wstunnel` instance" // {
31 package = lib.mkPackageOption pkgs "wstunnel" { };
33 autoStart = lib.mkEnableOption "starting this wstunnel instance automatically" // {
37 extraArgs = lib.mkOption {
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`.
43 type = with lib.types; attrsOf (either str bool);
46 "someNewOption" = true;
47 "someNewOptionWithValue" = "someValue";
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;
58 loggingLevel = lib.mkOption {
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)
65 type = lib.types.nullOr lib.types.str;
70 environmentFile = lib.mkOption {
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.
79 type = lib.types.nullOr lib.types.path;
81 example = "/var/lib/secrets/wstunnelSecrets";
88 options = commonOptions // {
89 listen = lib.mkOption {
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.
95 type = lib.types.submodule hostPortSubmodule;
98 port = if config.enableHTTPS then 443 else 80;
100 defaultText = lib.literalExpression ''
103 port = if enableHTTPS then 443 else 80;
108 restrictTo = lib.mkOption {
110 Accepted traffic will be forwarded only to this service.
112 type = lib.types.listOf (lib.types.submodule hostPortSubmodule);
122 enableHTTPS = lib.mkOption {
123 description = "Use HTTPS for the tunnel server.";
124 type = lib.types.bool;
128 tlsCertificate = lib.mkOption {
130 TLS certificate to use instead of the hardcoded one in case of HTTPS connections.
131 Use together with `tlsKey`.
133 type = lib.types.nullOr lib.types.path;
135 example = "/var/lib/secrets/cert.pem";
138 tlsKey = lib.mkOption {
140 TLS key to use instead of the hardcoded on in case of HTTPS connections.
141 Use together with `tlsCertificate`.
143 type = lib.types.nullOr lib.types.path;
145 example = "/var/lib/secrets/key.pem";
148 useACMEHost = lib.mkOption {
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`.
153 type = lib.types.nullOr lib.types.str;
155 example = "example.com";
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";
170 localToRemote = lib.mkOption {
171 description = ''Listen on local and forwards traffic from remote.'';
172 type = lib.types.listOf (lib.types.str);
175 "tcp://1212:google.com:443"
176 "unix:///tmp/wstunnel.sock:g.com:443"
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;
185 "tcp://1212:google.com:443"
186 "unix://wstunnel.sock:g.com:443"
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 {
194 Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`).
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.
205 type = lib.types.nullOr lib.types.str;
209 soMark = lib.mkOption {
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.
215 type = lib.types.nullOr lib.types.ints.unsigned;
219 upgradePathPrefix = lib.mkOption {
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.
225 type = lib.types.nullOr lib.types.str;
227 example = "wstunnel";
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;
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;
242 upgradeCredentials = lib.mkOption {
244 Use these credentials to authenticate during the HTTP upgrade request
245 (Basic authorization type, `USER:[PASS]`).
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.
256 type = lib.types.nullOr lib.types.str;
260 customHeaders = lib.mkOption {
261 description = "Custom HTTP headers to send during the upgrade request.";
262 type = lib.types.attrsOf lib.types.str;
265 "X-Some-Header" = "some-value";
271 generateServerUnit = name: serverCfg: {
272 name = "wstunnel-server-${name}";
275 certConfig = config.security.acme.certs.${serverCfg.useACMEHost};
278 description = "wstunnel server - ${name}";
281 "network-online.target"
285 "network-online.target"
287 wantedBy = lib.optional serverCfg.autoStart "multi-user.target";
289 environment.RUST_LOG = serverCfg.loggingLevel;
293 EnvironmentFile = lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
295 SupplementaryGroups = lib.optional (serverCfg.useACMEHost != null) certConfig.group;
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";
302 ProtectKernelTunables = true;
303 ProtectKernelModules = true;
304 ProtectControlGroups = true;
305 PrivateDevices = true;
306 RestrictSUIDSGID = true;
308 Restart = "on-failure";
311 RestartMaxDelaySec = "5min";
314 script = with serverCfg; ''
315 ${lib.getExe package} \
318 lib.cli.toGNUCommandLineShell { } (
319 lib.recursiveUpdate {
320 restrict-to = map hostPortToString restrictTo;
321 websocket-ping-frequency-sec = websocketPingInterval;
325 else if useACMEHost != null then
326 "${certConfig.directory}/fullchain.pem"
332 else if useACMEHost != null then
333 "${certConfig.directory}/key.pem"
339 ${lib.escapeShellArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
344 generateClientUnit = name: clientCfg: {
345 name = "wstunnel-client-${name}";
347 description = "wstunnel client - ${name}";
350 "network-online.target"
354 "network-online.target"
356 wantedBy = lib.optional clientCfg.autoStart "multi-user.target";
358 environment.RUST_LOG = clientCfg.loggingLevel;
362 EnvironmentFile = lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
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";
372 ProtectKernelTunables = true;
373 ProtectKernelModules = true;
374 ProtectControlGroups = true;
375 PrivateDevices = true;
376 RestrictSUIDSGID = true;
378 Restart = "on-failure";
381 RestartMaxDelaySec = "5min";
384 script = with clientCfg; ''
385 ${lib.getExe package} \
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;
403 ${lib.escapeShellArg connectTo}
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);
423 tlsCertificate = "/var/lib/secrets/fullchain.pem";
424 tlsKey = "/var/lib/secrets/key.pem";
435 clients = lib.mkOption {
436 description = "`wstunnel` clients to set up.";
437 type = lib.types.attrsOf (lib.types.submodule clientSubmodule);
441 connectTo = "wss://wstunnel.server.com:8443";
443 "tcp://1212:google.com:443"
444 "tcp://2:n.lan:4?proxy_protocol"
447 "socks5://[::1]:1212"
448 "unix://wstunnel.sock:g.com:443"
455 config = lib.mkIf cfg.enable {
457 (lib.mapAttrs' generateServerUnit (lib.filterAttrs (n: v: v.enable) cfg.servers))
458 // (lib.mapAttrs' generateClientUnit (lib.filterAttrs (n: v: v.enable) cfg.clients));
461 (lib.mapAttrsToList (name: serverCfg: {
462 assertion = !(serverCfg.useACMEHost != null && serverCfg.tlsCertificate != null);
464 Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
469 (lib.mapAttrsToList (name: serverCfg: {
471 (serverCfg.tlsCertificate == null && serverCfg.tlsKey == null)
472 || (serverCfg.tlsCertificate != null && serverCfg.tlsKey != null);
474 services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together.
479 (lib.mapAttrsToList (name: clientCfg: {
480 assertion = !(clientCfg.localToRemote == [ ] && clientCfg.remoteToLocal == [ ]);
482 Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".remoteToLocal must be set.
487 meta.maintainers = with lib.maintainers; [