diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix index 439e1f8ea9b5d..941e3e7366de3 100644 --- a/nixos/modules/services/networking/wstunnel.nix +++ b/nixos/modules/services/networking/wstunnel.nix @@ -1,7 +1,8 @@ -{ config -, lib -, pkgs -, ... +{ + config, + lib, + pkgs, + ... }: let @@ -29,10 +30,9 @@ let package = lib.mkPackageOption pkgs "wstunnel" { }; - autoStart = - lib.mkEnableOption "starting this wstunnel instance automatically" // { - default = true; - }; + autoStart = lib.mkEnableOption "starting this wstunnel instance automatically" // { + default = true; + }; extraArgs = lib.mkOption { description = '' @@ -75,192 +75,198 @@ let }; }; - serverSubmodule = { config, ... }: { - options = commonOptions // { - listen = lib.mkOption { - description = '' - Address and port to listen on. - Setting the port to a value below 1024 will also give the process - the required `CAP_NET_BIND_SERVICE` capability. - ''; - type = lib.types.submodule hostPortSubmodule; - default = { - host = "0.0.0.0"; - port = if config.enableHTTPS then 443 else 80; - }; - defaultText = lib.literalExpression '' - { + serverSubmodule = + { config, ... }: + { + options = commonOptions // { + listen = lib.mkOption { + description = '' + Address and port to listen on. + Setting the port to a value below 1024 will also give the process + the required `CAP_NET_BIND_SERVICE` capability. + ''; + type = lib.types.submodule hostPortSubmodule; + default = { host = "0.0.0.0"; - port = if enableHTTPS then 443 else 80; - } - ''; - }; + port = if config.enableHTTPS then 443 else 80; + }; + defaultText = lib.literalExpression '' + { + host = "0.0.0.0"; + port = if enableHTTPS then 443 else 80; + } + ''; + }; - restrictTo = lib.mkOption { - description = '' - Accepted traffic will be forwarded only to this service. - ''; - type = lib.types.listOf (lib.types.submodule hostPortSubmodule); - default = [ ]; - example = [{ - host = "127.0.0.1"; - port = 51820; - }]; - }; + restrictTo = lib.mkOption { + description = '' + Accepted traffic will be forwarded only to this service. + ''; + type = lib.types.listOf (lib.types.submodule hostPortSubmodule); + default = [ ]; + example = [ + { + host = "127.0.0.1"; + port = 51820; + } + ]; + }; - enableHTTPS = lib.mkOption { - description = "Use HTTPS for the tunnel server."; - type = lib.types.bool; - default = true; - }; + enableHTTPS = lib.mkOption { + description = "Use HTTPS for the tunnel server."; + type = lib.types.bool; + default = true; + }; - tlsCertificate = lib.mkOption { - description = '' - TLS certificate to use instead of the hardcoded one in case of HTTPS connections. - Use together with `tlsKey`. - ''; - type = lib.types.nullOr lib.types.path; - default = null; - example = "/var/lib/secrets/cert.pem"; - }; + tlsCertificate = lib.mkOption { + description = '' + TLS certificate to use instead of the hardcoded one in case of HTTPS connections. + Use together with `tlsKey`. + ''; + type = lib.types.nullOr lib.types.path; + default = null; + example = "/var/lib/secrets/cert.pem"; + }; - tlsKey = lib.mkOption { - description = '' - TLS key to use instead of the hardcoded on in case of HTTPS connections. - Use together with `tlsCertificate`. - ''; - type = lib.types.nullOr lib.types.path; - default = null; - example = "/var/lib/secrets/key.pem"; - }; + tlsKey = lib.mkOption { + description = '' + TLS key to use instead of the hardcoded on in case of HTTPS connections. + Use together with `tlsCertificate`. + ''; + type = lib.types.nullOr lib.types.path; + default = null; + example = "/var/lib/secrets/key.pem"; + }; - useACMEHost = lib.mkOption { - description = '' - Use a certificate generated by the NixOS ACME module for the given host. - Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`. - ''; - type = lib.types.nullOr lib.types.str; - default = null; - example = "example.com"; + useACMEHost = lib.mkOption { + description = '' + Use a certificate generated by the NixOS ACME module for the given host. + Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`. + ''; + type = lib.types.nullOr lib.types.str; + default = null; + example = "example.com"; + }; }; }; - }; - clientSubmodule = { config, ... }: { - options = commonOptions // { - connectTo = lib.mkOption { - description = "Server address and port to connect to."; - type = lib.types.str; - example = "https://wstunnel.server.com:8443"; - }; + clientSubmodule = + { config, ... }: + { + options = commonOptions // { + connectTo = lib.mkOption { + description = "Server address and port to connect to."; + type = lib.types.str; + example = "https://wstunnel.server.com:8443"; + }; - localToRemote = lib.mkOption { - description = ''Listen on local and forwards traffic from remote.''; - type = lib.types.listOf (lib.types.str); - default = [ ]; - example = [ - "tcp://1212:google.com:443" - "unix:///tmp/wstunnel.sock:g.com:443" - ]; - }; + localToRemote = lib.mkOption { + description = ''Listen on local and forwards traffic from remote.''; + type = lib.types.listOf (lib.types.str); + default = [ ]; + example = [ + "tcp://1212:google.com:443" + "unix:///tmp/wstunnel.sock:g.com:443" + ]; + }; - remoteToLocal = lib.mkOption { - description = "Listen on remote and forwards traffic from local. Only tcp is supported"; - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ - "tcp://1212:google.com:443" - "unix://wstunnel.sock:g.com:443" - ]; - }; + remoteToLocal = lib.mkOption { + description = "Listen on remote and forwards traffic from local. Only tcp is supported"; + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ + "tcp://1212:google.com:443" + "unix://wstunnel.sock:g.com:443" + ]; + }; - addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024"; + addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024"; - httpProxy = lib.mkOption { - description = '' - Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`). + httpProxy = lib.mkOption { + description = '' + Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`). - ::: {.warning} - Passwords specified here will be world-readable in the Nix store! - To pass a password to the service, point the `environmentFile` option - to a file containing `PROXY_PASSWORD=` and set - this option to `:$PROXY_PASSWORD@:`. - Note however that this will also locally leak the passwords at - runtime via e.g. /proc//cmdline. - ::: - ''; - type = lib.types.nullOr lib.types.str; - default = null; - }; + ::: {.warning} + Passwords specified here will be world-readable in the Nix store! + To pass a password to the service, point the `environmentFile` option + to a file containing `PROXY_PASSWORD=` and set + this option to `:$PROXY_PASSWORD@:`. + Note however that this will also locally leak the passwords at + runtime via e.g. /proc//cmdline. + ::: + ''; + type = lib.types.nullOr lib.types.str; + default = null; + }; - soMark = lib.mkOption { - description = '' - Mark network packets with the SO_MARK sockoption with the specified value. - Setting this option will also enable the required `CAP_NET_ADMIN` capability - for the systemd service. - ''; - type = lib.types.nullOr lib.types.ints.unsigned; - default = null; - }; + soMark = lib.mkOption { + description = '' + Mark network packets with the SO_MARK sockoption with the specified value. + Setting this option will also enable the required `CAP_NET_ADMIN` capability + for the systemd service. + ''; + type = lib.types.nullOr lib.types.ints.unsigned; + default = null; + }; - upgradePathPrefix = lib.mkOption { - description = '' - Use a specific HTTP path prefix that will show up in the upgrade - request to the `wstunnel` server. - Useful when running `wstunnel` behind a reverse proxy. - ''; - type = lib.types.nullOr lib.types.str; - default = null; - example = "wstunnel"; - }; + upgradePathPrefix = lib.mkOption { + description = '' + Use a specific HTTP path prefix that will show up in the upgrade + request to the `wstunnel` server. + Useful when running `wstunnel` behind a reverse proxy. + ''; + type = lib.types.nullOr lib.types.str; + default = null; + example = "wstunnel"; + }; - tlsSNI = lib.mkOption { - description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls."; - type = lib.types.nullOr lib.types.str; - default = null; - }; + tlsSNI = lib.mkOption { + description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls."; + type = lib.types.nullOr lib.types.str; + default = null; + }; - tlsVerifyCertificate = lib.mkOption { - 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."; - type = lib.types.bool; - default = true; - }; + tlsVerifyCertificate = lib.mkOption { + 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."; + type = lib.types.bool; + default = true; + }; - # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval. - websocketPingInterval = lib.mkOption { - description = "Frequency at which the client will send websocket ping to the server."; - type = lib.types.nullOr lib.types.ints.unsigned; - default = null; - }; + # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval. + websocketPingInterval = lib.mkOption { + description = "Frequency at which the client will send websocket ping to the server."; + type = lib.types.nullOr lib.types.ints.unsigned; + default = null; + }; - upgradeCredentials = lib.mkOption { - description = '' - Use these credentials to authenticate during the HTTP upgrade request - (Basic authorization type, `USER:[PASS]`). - - ::: {.warning} - Passwords specified here will be world-readable in the Nix store! - To pass a password to the service, point the `environmentFile` option - to a file containing `HTTP_PASSWORD=` and set this - option to `:$HTTP_PASSWORD`. - Note however that this will also locally leak the passwords at runtime - via e.g. /proc//cmdline. - ::: - ''; - type = lib.types.nullOr lib.types.str; - default = null; - }; + upgradeCredentials = lib.mkOption { + description = '' + Use these credentials to authenticate during the HTTP upgrade request + (Basic authorization type, `USER:[PASS]`). + + ::: {.warning} + Passwords specified here will be world-readable in the Nix store! + To pass a password to the service, point the `environmentFile` option + to a file containing `HTTP_PASSWORD=` and set this + option to `:$HTTP_PASSWORD`. + Note however that this will also locally leak the passwords at runtime + via e.g. /proc//cmdline. + ::: + ''; + type = lib.types.nullOr lib.types.str; + default = null; + }; - customHeaders = lib.mkOption { - description = "Custom HTTP headers to send during the upgrade request."; - type = lib.types.attrsOf lib.types.str; - default = { }; - example = { - "X-Some-Header" = "some-value"; + customHeaders = lib.mkOption { + description = "Custom HTTP headers to send during the upgrade request."; + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + "X-Some-Header" = "some-value"; + }; }; }; }; - }; generateServerUnit = name: serverCfg: { name = "wstunnel-server-${name}"; @@ -270,22 +276,25 @@ let in { description = "wstunnel server - ${name}"; - requires = [ "network.target" "network-online.target" ]; - after = [ "network.target" "network-online.target" ]; + requires = [ + "network.target" + "network-online.target" + ]; + after = [ + "network.target" + "network-online.target" + ]; wantedBy = lib.optional serverCfg.autoStart "multi-user.target"; environment.RUST_LOG = serverCfg.loggingLevel; serviceConfig = { Type = "exec"; - EnvironmentFile = - lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile; + EnvironmentFile = lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile; DynamicUser = true; - SupplementaryGroups = - lib.optional (serverCfg.useACMEHost != null) certConfig.group; + SupplementaryGroups = lib.optional (serverCfg.useACMEHost != null) certConfig.group; PrivateTmp = true; - AmbientCapabilities = - lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + AmbientCapabilities = lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; @@ -305,19 +314,16 @@ let script = with serverCfg; '' ${lib.getExe package} \ server \ - ${lib.cli.toGNUCommandLineShell { } ( - lib.recursiveUpdate - { - restrict-to = map hostPortToString restrictTo; - tls-certificate = if useACMEHost != null - then "${certConfig.directory}/fullchain.pem" - else "${tlsCertificate}"; - tls-private-key = if useACMEHost != null - then "${certConfig.directory}/key.pem" - else "${tlsKey}"; - } - extraArgs - )} \ + ${ + lib.cli.toGNUCommandLineShell { } ( + lib.recursiveUpdate { + restrict-to = map hostPortToString restrictTo; + tls-certificate = + if useACMEHost != null then "${certConfig.directory}/fullchain.pem" else "${tlsCertificate}"; + tls-private-key = if useACMEHost != null then "${certConfig.directory}/key.pem" else "${tlsKey}"; + } extraArgs + ) + } \ ${lib.escapeShellArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"} ''; }; @@ -327,21 +333,26 @@ let name = "wstunnel-client-${name}"; value = { description = "wstunnel client - ${name}"; - requires = [ "network.target" "network-online.target" ]; - after = [ "network.target" "network-online.target" ]; + requires = [ + "network.target" + "network-online.target" + ]; + after = [ + "network.target" + "network-online.target" + ]; wantedBy = lib.optional clientCfg.autoStart "multi-user.target"; environment.RUST_LOG = clientCfg.loggingLevel; serviceConfig = { Type = "exec"; - EnvironmentFile = - lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile; + EnvironmentFile = lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile; DynamicUser = true; PrivateTmp = true; AmbientCapabilities = - (lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ]) ++ - (lib.optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]); + (lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ]) + ++ (lib.optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]); NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; @@ -361,22 +372,22 @@ let script = with clientCfg; '' ${lib.getExe package} \ client \ - ${lib.cli.toGNUCommandLineShell { } ( - lib.recursiveUpdate - { - local-to-remote = localToRemote; - remote-to-local = remoteToLocal; - http-headers = lib.mapAttrsToList (n: v: "${n}:${v}") customHeaders; - http-proxy = httpProxy; - socket-so-mark = soMark; - http-upgrade-path-prefix = upgradePathPrefix; - tls-sni-override = tlsSNI; - tls-verify-certificate = tlsVerifyCertificate; - websocket-ping-frequency-sec = websocketPingInterval; - http-upgrade-credentials = upgradeCredentials; - } - extraArgs - )} \ + ${ + lib.cli.toGNUCommandLineShell { } ( + lib.recursiveUpdate { + local-to-remote = localToRemote; + remote-to-local = remoteToLocal; + http-headers = lib.mapAttrsToList (n: v: "${n}:${v}") customHeaders; + http-proxy = httpProxy; + socket-so-mark = soMark; + http-upgrade-path-prefix = upgradePathPrefix; + tls-sni-override = tlsSNI; + tls-verify-certificate = tlsVerifyCertificate; + websocket-ping-frequency-sec = websocketPingInterval; + http-upgrade-credentials = upgradeCredentials; + } extraArgs + ) + } \ ${lib.escapeShellArg connectTo} ''; }; @@ -399,10 +410,12 @@ in enableHTTPS = true; tlsCertificate = "/var/lib/secrets/fullchain.pem"; tlsKey = "/var/lib/secrets/key.pem"; - restrictTo = [{ - host = "127.0.0.1"; - port = 51820; - }]; + restrictTo = [ + { + host = "127.0.0.1"; + port = 51820; + } + ]; }; }; }; @@ -429,40 +442,39 @@ in config = lib.mkIf cfg.enable { systemd.services = - (lib.mapAttrs' generateServerUnit (lib.filterAttrs (n: v: v.enable) cfg.servers)) // - (lib.mapAttrs' generateClientUnit (lib.filterAttrs (n: v: v.enable) cfg.clients)); + (lib.mapAttrs' generateServerUnit (lib.filterAttrs (n: v: v.enable) cfg.servers)) + // (lib.mapAttrs' generateClientUnit (lib.filterAttrs (n: v: v.enable) cfg.clients)); assertions = - (lib.mapAttrsToList - (name: serverCfg: { - assertion = - !(serverCfg.useACMEHost != null && serverCfg.tlsCertificate != null); - message = '' - Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive. - ''; - }) - cfg.servers) ++ + (lib.mapAttrsToList (name: serverCfg: { + assertion = !(serverCfg.useACMEHost != null && serverCfg.tlsCertificate != null); + message = '' + Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive. + ''; + }) cfg.servers) + ++ - (lib.mapAttrsToList - (name: serverCfg: { + (lib.mapAttrsToList (name: serverCfg: { assertion = - (serverCfg.tlsCertificate == null && serverCfg.tlsKey == null) || - (serverCfg.tlsCertificate != null && serverCfg.tlsKey != null); + (serverCfg.tlsCertificate == null && serverCfg.tlsKey == null) + || (serverCfg.tlsCertificate != null && serverCfg.tlsKey != null); message = '' services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together. ''; - }) - cfg.servers) ++ + }) cfg.servers) + ++ - (lib.mapAttrsToList - (name: clientCfg: { + (lib.mapAttrsToList (name: clientCfg: { assertion = !(clientCfg.localToRemote == [ ] && clientCfg.remoteToLocal == [ ]); message = '' Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".remoteToLocal must be set. ''; - }) - cfg.clients); + }) cfg.clients); }; - meta.maintainers = with lib.maintainers; [ alyaeanyx rvdp neverbehave ]; + meta.maintainers = with lib.maintainers; [ + alyaeanyx + rvdp + neverbehave + ]; } diff --git a/nixos/tests/wstunnel.nix b/nixos/tests/wstunnel.nix index 3bbc295568fb7..7a0a8ce3496ad 100644 --- a/nixos/tests/wstunnel.nix +++ b/nixos/tests/wstunnel.nix @@ -60,37 +60,34 @@ in clients.my-client = { autoStart = false; connectTo = "wss://${domain}:443"; - localToRemote = [ - "tcp://8080:localhost:2080" - ]; - remoteToLocal = [ - "tcp://2081:localhost:8081" - ]; + localToRemote = [ "tcp://8080:localhost:2080" ]; + remoteToLocal = [ "tcp://2081:localhost:8081" ]; }; }; }; }; - testScript = /* python */ '' - start_all() - server.wait_for_unit("wstunnel-server-my-server.service") - client.wait_for_open_port(443, "10.0.0.1") - - client.systemctl("start wstunnel-client-my-client.service") - client.wait_for_unit("wstunnel-client-my-client.service") - - with subtest("connection from client to server"): - server.succeed("nc -l 2080 >/tmp/msg &") - client.sleep(1) - client.succeed('nc -w1 localhost 8080 <<<"Hello from client"') - server.succeed('grep "Hello from client" /tmp/msg') - - with subtest("connection from server to client"): - client.succeed("nc -l 8081 >/tmp/msg &") - server.sleep(1) - server.succeed('nc -w1 localhost 2081 <<<"Hello from server"') - client.succeed('grep "Hello from server" /tmp/msg') - - client.systemctl("stop wstunnel-client-my-client.service") - ''; + testScript = # python + '' + start_all() + server.wait_for_unit("wstunnel-server-my-server.service") + client.wait_for_open_port(443, "10.0.0.1") + + client.systemctl("start wstunnel-client-my-client.service") + client.wait_for_unit("wstunnel-client-my-client.service") + + with subtest("connection from client to server"): + server.succeed("nc -l 2080 >/tmp/msg &") + client.sleep(1) + client.succeed('nc -w1 localhost 8080 <<<"Hello from client"') + server.succeed('grep "Hello from client" /tmp/msg') + + with subtest("connection from server to client"): + client.succeed("nc -l 8081 >/tmp/msg &") + server.sleep(1) + server.succeed('nc -w1 localhost 2081 <<<"Hello from server"') + client.succeed('grep "Hello from server" /tmp/msg') + + client.systemctl("stop wstunnel-client-my-client.service") + ''; } diff --git a/pkgs/by-name/ws/wstunnel/package.nix b/pkgs/by-name/ws/wstunnel/package.nix index 42b58649493ed..9e918039c14ae 100644 --- a/pkgs/by-name/ws/wstunnel/package.nix +++ b/pkgs/by-name/ws/wstunnel/package.nix @@ -1,13 +1,14 @@ -{ lib -, fetchFromGitHub -, rustPlatform -, testers -, wstunnel -, nixosTests +{ + lib, + fetchFromGitHub, + rustPlatform, + nixosTests, + nix-update-script, + versionCheckHook, }: let - version = "9.7.4"; + version = "10.0.1"; in rustPlatform.buildRustPackage { @@ -18,19 +19,26 @@ rustPlatform.buildRustPackage { owner = "erebe"; repo = "wstunnel"; rev = "v${version}"; - hash = "sha256-OFm0Jk06Mxzr4F7KrMBGFqcDSuTtrMvBSK99bbOgua4="; + hash = "sha256-lahuuVc+fbuWMXaWvt8C6j26Mo/1o5PkBfVH+lemdv4="; }; - cargoHash = "sha256-JMRcXuw6AKfwViOgYAgFdSwUeTo04rEkKj+t+W8wjGI="; + cargoHash = "sha256-gNx01LHIcAbedO2X/OwwnQWd9d0Qc6ahe84IRUyptcY="; + + nativeBuildInputs = [ versionCheckHook ]; + + doInstallCheck = true; checkFlags = [ # Tries to launch a test container "--skip=tcp::tests::test_proxy_connection" + "--skip=protocols::tcp::server::tests::test_proxy_connection" ]; - passthru.tests = { - version = testers.testVersion { package = wstunnel; }; - nixosTest = nixosTests.wstunnel; + passthru = { + updateScript = nix-update-script { }; + tests = { + nixosTest = nixosTests.wstunnel; + }; }; meta = { @@ -38,7 +46,10 @@ rustPlatform.buildRustPackage { homepage = "https://github.com/erebe/wstunnel"; changelog = "https://github.com/erebe/wstunnel/releases/tag/v${version}"; license = lib.licenses.bsd3; - maintainers = with lib.maintainers; [ rvdp neverbehave ]; + maintainers = with lib.maintainers; [ + rvdp + neverbehave + ]; mainProgram = "wstunnel"; }; }