wstunnel: 9.7.4 -> 10.0.1 (#335185)

This commit is contained in:
Florian Klink 2024-08-28 10:56:59 +02:00 committed by GitHub
commit c8912137a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 311 additions and 291 deletions

View File

@ -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";
};
};
};
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"
];
};
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";
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.
:::
'';
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;
};
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;
};
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;
};
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=<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 = 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";
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";
};
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"
];
};
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`).
::: {.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.
:::
'';
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;
};
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;
};
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;
};
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=<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 = 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";
};
};
};
};
};
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
];
}

View File

@ -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")
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")
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 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')
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")
'';
client.systemctl("stop wstunnel-client-my-client.service")
'';
}

View File

@ -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";
};
}