nixos/wstunnel: update the wstunnel module to work with the new rust implementation

Co-authored-by: h7x4 <h7x4@nani.wtf>
This commit is contained in:
r-vdp 2024-06-13 14:52:15 +03:00
parent 8d7a9b3227
commit 53e7bea45c
No known key found for this signature in database
2 changed files with 261 additions and 184 deletions

View File

@ -1,83 +1,94 @@
{ config, lib, options, pkgs, utils, ... }:
with lib;
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.wstunnel;
attrsToArgs = attrs: utils.escapeSystemdExecArgs (
mapAttrsToList
(name: value: if value == true then "--${name}" else "--${name}=${value}")
attrs
);
hostPortToString = { host, port }: "${host}:${builtins.toString port}";
hostPortToString = { host, port }: "${host}:${toString port}";
hostPortSubmodule = {
options = {
host = mkOption {
host = lib.mkOption {
description = "The hostname.";
type = types.str;
type = lib.types.str;
};
port = mkOption {
port = lib.mkOption {
description = "The port.";
type = types.port;
type = lib.types.port;
};
};
};
commonOptions = {
enable = mkOption {
description = "Whether to enable this `wstunnel` instance.";
type = types.bool;
enable = lib.mkEnableOption "this `wstunnel` instance." // {
default = true;
};
package = mkPackageOption pkgs "wstunnel" {};
package = lib.mkPackageOption pkgs "wstunnel" { };
autoStart = mkOption {
description = "Whether this tunnel server should be started automatically.";
type = types.bool;
default = true;
};
autoStart =
lib.mkEnableOption "starting this wstunnel instance automatically." // {
default = true;
};
extraArgs = mkOption {
description = "Extra command line arguments to pass to `wstunnel`. Attributes of the form `argName = true;` will be translated to `--argName`, and `argName = \"value\"` to `--argName=value`.";
type = with types; attrsOf (either str bool);
default = {};
extraArgs = lib.mkOption {
description = ''
Extra command line arguments to pass to `wstunnel`.
Attributes of the form `argName = true;` will be translated to `--argName`,
and `argName = \"value\"` to `--argName value`.
'';
type = with lib.types; attrsOf (either str bool);
default = { };
example = {
"someNewOption" = true;
"someNewOptionWithValue" = "someValue";
};
};
loggingLevel = mkOption {
loggingLevel = lib.mkOption {
description = ''
Passed to --log-lvl
Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF
For more details, checkout [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax)
'';
type = types.nullOr types.str;
type = lib.types.nullOr lib.types.str;
example = "INFO";
default = null;
};
environmentFile = mkOption {
description = "Environment file to be passed to the systemd service. Useful for passing secrets to the service to prevent them from being world-readable in the Nix store. Note however that the secrets are passed to `wstunnel` through the command line, which makes them locally readable for all users of the system at runtime.";
type = types.nullOr types.path;
environmentFile = lib.mkOption {
description = ''
Environment file to be passed to the systemd service.
Useful for passing secrets to the service to prevent them from being
world-readable in the Nix store.
Note however that the secrets are passed to `wstunnel` through
the command line, which makes them locally readable for all users of
the system at runtime.
'';
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/secrets/wstunnelSecrets";
};
};
serverSubmodule = { config, ...}: {
serverSubmodule = { config, ... }: {
options = commonOptions // {
listen = 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 = types.submodule hostPortSubmodule;
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 = literalExpression ''
defaultText = lib.literalExpression ''
{
host = "0.0.0.0";
port = if enableHTTPS then 443 else 80;
@ -85,39 +96,50 @@ let
'';
};
restrictTo = mkOption {
description = "Accepted traffic will be forwarded only to this service. Set to `null` to allow forwarding to arbitrary addresses.";
type = types.listOf (types.submodule hostPortSubmodule);
default = [];
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 = mkOption {
enableHTTPS = lib.mkOption {
description = "Use HTTPS for the tunnel server.";
type = types.bool;
type = lib.types.bool;
default = true;
};
tlsCertificate = mkOption {
description = "TLS certificate to use instead of the hardcoded one in case of HTTPS connections. Use together with `tlsKey`.";
type = types.nullOr types.path;
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 = mkOption {
description = "TLS key to use instead of the hardcoded on in case of HTTPS connections. Use together with `tlsCertificate`.";
type = types.nullOr types.path;
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 = 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 = types.nullOr types.str;
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";
};
@ -126,95 +148,113 @@ let
clientSubmodule = { config, ... }: {
options = commonOptions // {
connectTo = mkOption {
connectTo = lib.mkOption {
description = "Server address and port to connect to.";
type = types.str;
type = lib.types.str;
example = "https://wstunnel.server.com:8443";
};
localToRemote = mkOption {
localToRemote = lib.mkOption {
description = ''Listen on local and forwards traffic from remote.'';
type = types.listOf (types.str);
default = [];
type = lib.types.listOf (lib.types.str);
default = [ ];
example = [
"tcp://1212:google.com:443"
"unix:///tmp/wstunnel.sock:g.com:443"
];
};
remoteToLocal = mkOption {
remoteToLocal = lib.mkOption {
description = "Listen on remote and forwards traffic from local. Only tcp is supported";
type = types.listOf (types.str);
default = [];
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"tcp://1212:google.com:443"
"unix://wstunnel.sock:g.com:443"
];
};
addNetBind = 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 = mkOption {
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=<your-password-here>` and set this option to `<user>:$PROXY_PASSWORD@<host>:<port>`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
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=<your-password-here>` and set
this option to `<user>:$PROXY_PASSWORD@<host>:<port>`.
Note however that this will also locally leak the passwords at
runtime via e.g. /proc/<pid>/cmdline.
:::
'';
type = types.nullOr types.str;
type = lib.types.nullOr lib.types.str;
default = null;
};
soMark = 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 = types.nullOr types.int;
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 = 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 = types.nullOr types.str;
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 = mkOption {
tlsSNI = lib.mkOption {
description = "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls.";
type = types.nullOr types.str;
type = lib.types.nullOr lib.types.str;
default = null;
};
tlsVerifyCertificate = mkOption {
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 = types.bool;
type = lib.types.bool;
default = true;
};
# The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval.
websocketPingInterval = mkOption {
websocketPingInterval = lib.mkOption {
description = "Frequency at which the client will send websocket ping to the server.";
type = types.nullOr types.ints.unsigned;
type = lib.types.nullOr lib.types.ints.unsigned;
default = null;
};
upgradeCredentials = mkOption {
upgradeCredentials = lib.mkOption {
description = ''
Use these credentials to authenticate during the HTTP upgrade request (Basic authorization type, `USER:[PASS]`).
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=<your-password-here>` and set this option to `<user>:$HTTP_PASSWORD`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
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=<your-password-here>` and set this
option to `<user>:$HTTP_PASSWORD`.
Note however that this will also locally leak the passwords at runtime
via e.g. /proc/<pid>/cmdline.
:::
'';
type = types.nullOr types.str;
type = lib.types.nullOr lib.types.str;
default = null;
};
customHeaders = mkOption {
customHeaders = lib.mkOption {
description = "Custom HTTP headers to send during the upgrade request.";
type = types.attrsOf types.str;
default = {};
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
"X-Some-Header" = "some-value";
};
@ -224,49 +264,63 @@ let
generateServerUnit = name: serverCfg: {
name = "wstunnel-server-${name}";
value = {
description = "wstunnel server - ${name}";
requires = [ "network.target" "network-online.target" ];
after = [ "network.target" "network-online.target" ];
wantedBy = optional serverCfg.autoStart "multi-user.target";
value =
let
certConfig = config.security.acme.certs.${serverCfg.useACMEHost};
in
{
description = "wstunnel server - ${name}";
requires = [ "network.target" "network-online.target" ];
after = [ "network.target" "network-online.target" ];
wantedBy = lib.optional serverCfg.autoStart "multi-user.target";
serviceConfig = let
certConfig = config.security.acme.certs."${serverCfg.useACMEHost}";
in {
Type = "simple";
ExecStart = with serverCfg; let
resolvedTlsCertificate = if useACMEHost != null
then "${certConfig.directory}/fullchain.pem"
else tlsCertificate;
resolvedTlsKey = if useACMEHost != null
then "${certConfig.directory}/key.pem"
else tlsKey;
in ''
${package}/bin/wstunnel \
environment.RUST_LOG = serverCfg.loggingLevel;
serviceConfig = {
Type = "simple";
EnvironmentFile =
lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
DynamicUser = true;
SupplementaryGroups =
lib.optional (serverCfg.useACMEHost != null) certConfig.group;
PrivateTmp = true;
AmbientCapabilities =
lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
NoNewPrivileges = true;
RestrictNamespaces = "uts ipc pid user cgroup";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
PrivateDevices = true;
RestrictSUIDSGID = true;
Restart = "on-failure";
RestartSec = 2;
RestartSteps = 20;
RestartMaxDelaySec = "5min";
};
script = with serverCfg; ''
${lib.getExe package} \
server \
${concatStringsSep " " (builtins.map (hostPair: "--restrict-to ${utils.escapeSystemdExecArg (hostPortToString hostPair)}") restrictTo)} \
${optionalString (resolvedTlsCertificate != null) "--tls-certificate ${utils.escapeSystemdExecArg resolvedTlsCertificate}"} \
${optionalString (resolvedTlsKey != null) "--tls-private-key ${utils.escapeSystemdExecArg resolvedTlsKey}"} \
${optionalString (loggingLevel != null) "--log-lvl ${loggingLevel}"} \
${attrsToArgs extraArgs} \
${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
${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}"}
'';
EnvironmentFile = optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
DynamicUser = true;
SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group;
PrivateTmp = true;
AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
NoNewPrivileges = true;
RestrictNamespaces = "uts ipc pid user cgroup";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
PrivateDevices = true;
RestrictSUIDSGID = true;
};
};
};
generateClientUnit = name: clientCfg: {
@ -275,30 +329,19 @@ let
description = "wstunnel client - ${name}";
requires = [ "network.target" "network-online.target" ];
after = [ "network.target" "network-online.target" ];
wantedBy = optional clientCfg.autoStart "multi-user.target";
wantedBy = lib.optional clientCfg.autoStart "multi-user.target";
environment.RUST_LOG = clientCfg.loggingLevel;
serviceConfig = {
Type = "simple";
ExecStart = with clientCfg; ''
${package}/bin/wstunnel client \
${concatStringsSep " " (builtins.map (x: "--local-to-remote ${x}") localToRemote)} \
${concatStringsSep " " (builtins.map (x: "--remote-to-local ${x}") remoteToLocal)} \
${concatStringsSep " " (mapAttrsToList (n: v: "--http-headers \"${n}: ${v}\"") customHeaders)} \
${optionalString (httpProxy != null) "--http-proxy ${httpProxy}"} \
${optionalString (soMark != null) "--socket-so-mark=${toString soMark}"} \
${optionalString (upgradePathPrefix != null) "--http-upgrade-path-prefix ${upgradePathPrefix}"} \
${optionalString (tlsSNI != null) "--tls-sni-override ${tlsSNI}"} \
${optionalString tlsVerifyCertificate "--tls-verify-certificate"} \
${optionalString (websocketPingInterval != null) "--websocket-ping-frequency-sec ${toString websocketPingInterval}"} \
${optionalString (upgradeCredentials != null) "--http-upgrade-credentials ${upgradeCredentials}"} \
${optionalString (loggingLevel != null) "--log-lvl ${loggingLevel}"} \
${attrsToArgs extraArgs} \
${utils.escapeSystemdExecArg connectTo}
'';
EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
EnvironmentFile =
lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
DynamicUser = true;
PrivateTmp = true;
AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals (clientCfg.addNetBind) [ "CAP_NET_BIND_SERVICE" ]);
AmbientCapabilities =
(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";
@ -308,17 +351,45 @@ let
ProtectControlGroups = true;
PrivateDevices = true;
RestrictSUIDSGID = true;
Restart = "on-failure";
RestartSec = 2;
RestartSteps = 20;
RestartMaxDelaySec = "5min";
};
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.escapeShellArg connectTo}
'';
};
};
in {
in
{
options.services.wstunnel = {
enable = mkEnableOption "wstunnel";
enable = lib.mkEnableOption "wstunnel";
servers = mkOption {
servers = lib.mkOption {
description = "`wstunnel` servers to set up.";
type = types.attrsOf (types.submodule serverSubmodule);
default = {};
type = lib.types.attrsOf (lib.types.submodule serverSubmodule);
default = { };
example = {
"wg-tunnel" = {
listen = {
@ -336,13 +407,13 @@ in {
};
};
clients = mkOption {
clients = lib.mkOption {
description = "`wstunnel` clients to set up.";
type = types.attrsOf (types.submodule clientSubmodule);
default = {};
type = lib.types.attrsOf (lib.types.submodule clientSubmodule);
default = { };
example = {
"wg-tunnel" = {
connectTo = "https://wstunnel.server.com:8443";
connectTo = "wss://wstunnel.server.com:8443";
localToRemote = [
"tcp://1212:google.com:443"
"tcp://2:n.lan:4?proxy_protocol"
@ -356,28 +427,42 @@ in {
};
};
config = mkIf cfg.enable {
systemd.services = (mapAttrs' generateServerUnit (filterAttrs (n: v: v.enable) cfg.servers)) // (mapAttrs' generateClientUnit (filterAttrs (n: v: v.enable) cfg.clients));
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));
assertions = (mapAttrsToList (name: serverCfg: {
assertion = !(serverCfg.useACMEHost != null && (serverCfg.tlsCertificate != null || serverCfg.tlsKey != null));
message = ''
Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
'';
}) cfg.servers) ++
(mapAttrsToList (name: serverCfg: {
assertion = !((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) ++
(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);
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.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) ++
(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);
};
meta.maintainers = with maintainers; [ alyaeanyx neverbehave ];
meta.maintainers = with lib.maintainers; [ alyaeanyx rvdp neverbehave ];
}

View File

@ -1,6 +1,8 @@
{ fetchFromGitHub
{ lib
, fetchFromGitHub
, rustPlatform
, lib
, testers
, wstunnel
}:
let
@ -25,22 +27,12 @@ rustPlatform.buildRustPackage {
"--skip=tcp::tests::test_proxy_connection"
];
doInstallCheck = true;
installCheckPhase = ''
runHook preInstallCheck
actual="$($out/bin/wstunnel --version)"
expected="${pname} ${version}"
echo "Check that 'wstunnel --version' returns: $expected"
if [[ "$actual" != "$expected" ]]; then
echo "'wstunnel --version' returned: $actual"
exit 1
fi
runHook postInstallCheck
'';
passthru.tests.version = testers.testVersion { package = wstunnel; };
meta = {
description = "Tunnel all your traffic over Websocket or HTTP2 - Bypass firewalls/DPI";
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 ];
mainProgram = "wstunnel";