0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-13 21:50:33 +03:00
nixpkgs/nixos/modules/services/networking/wstunnel.nix

503 lines
15 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.wstunnel;
argsFormat = {
type =
let
inherit (lib.types)
attrsOf
listOf
oneOf
bool
int
str
;
in
attrsOf (oneOf [
bool
int
str
(listOf str)
]);
generate = lib.cli.toGNUCommandLineShell { };
};
hostPortToString = { host, port, ... }: "${host}:${toString port}";
commonOptions = {
enable = lib.mkEnableOption "this `wstunnel` instance" // {
default = true;
};
package = lib.mkPackageOption pkgs "wstunnel" { };
autoStart = lib.mkEnableOption "starting this wstunnel instance automatically" // {
default = true;
};
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 =
let
outerConfig = config;
in
{ config, ... }:
let
certConfig = outerConfig.security.acme.certs.${config.useACMEHost};
in
{
imports =
[
../../misc/assertions.nix
(lib.mkRenamedOptionModule
[
"enableHTTPS"
]
[
"listen"
"enableHTTPS"
]
)
]
++ lib.map
(
option:
lib.mkRemovedOptionModule [ option ] ''
The wstunnel module now uses RFC-42-style settings, please modify your config accordingly
''
)
[
"extraArgs"
"websocketPingInterval"
"loggingLevel"
"restrictTo"
"tlsCertificate"
"tlsKey"
];
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 {
options = {
host = lib.mkOption {
description = "The hostname.";
type = lib.types.str;
};
port = lib.mkOption {
description = "The port.";
type = lib.types.port;
};
enableHTTPS = lib.mkOption {
description = "Use HTTPS for the tunnel server.";
type = lib.types.bool;
default = true;
};
};
};
default =
{ config, ... }:
{
host = "0.0.0.0";
port = if config.enableHTTPS then 443 else 80;
};
defaultText = lib.literalExpression ''
{ config, ... }:
{
host = "0.0.0.0";
port = if config.enableHTTPS then 443 else 80;
}
'';
};
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";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = argsFormat.type;
options = {
restrict-to = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
host = lib.mkOption {
description = "The hostname.";
type = lib.types.str;
};
port = lib.mkOption {
description = "The port.";
type = lib.types.port;
};
};
}
);
default = [ ];
example = [
{
host = "127.0.0.1";
port = 51820;
}
];
description = ''
Restrictions on the connections that the server will accept.
For more flexibility, and the possibility to also allow reverse tunnels,
look into the `restrict-config` option that takes a path to a yaml file.
'';
};
};
};
default = { };
description = ''
Command line arguments to pass to `wstunnel`.
Attributes of the form `argName = true;` will be translated to `--argName`,
and `argName = \"value\"` to `--argName value`.
'';
example = {
"someNewOption" = true;
"someNewOptionWithValue" = "someValue";
};
};
};
config = {
settings = lib.mkIf (config.useACMEHost != null) {
tls-certificate = "${certConfig.directory}/fullchain.pem";
tls-private-key = "${certConfig.directory}/key.pem";
};
};
};
clientSubmodule =
{ config, ... }:
{
imports =
[
../../misc/assertions.nix
]
++ lib.map
(
option:
lib.mkRemovedOptionModule [ option ] ''
The wstunnel module now uses RFC-42-style settings, please modify your config accordingly
''
)
[
"extraArgs"
"websocketPingInterval"
"loggingLevel"
"localToRemote"
"remoteToLocal"
"httpProxy"
"soMark"
"upgradePathPrefix"
"tlsSNI"
"tlsVerifyCertificate"
"upgradeCredentials"
"customHeaders"
];
options = commonOptions // {
connectTo = lib.mkOption {
description = "Server address and port to connect to.";
type = lib.types.str;
example = "https://wstunnel.server.com:8443";
};
addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = argsFormat.type;
options = {
http-headers = lib.mkOption {
type = lib.types.coercedTo (lib.types.attrsOf lib.types.str) (lib.mapAttrsToList (
n: v: "${n}:${v}"
)) (lib.types.listOf lib.types.str);
default = { };
example = {
"X-Some-Header" = "some-value";
};
description = ''
Custom headers to send in the upgrade request
'';
};
};
};
default = { };
description = ''
Command line arguments to pass to `wstunnel`.
Attributes of the form `argName = true;` will be translated to `--argName`,
and `argName = \"value\"` to `--argName value`.
'';
example = {
"someNewOption" = true;
"someNewOptionWithValue" = "someValue";
};
};
};
};
generateServerUnit = name: serverCfg: {
name = "wstunnel-server-${name}";
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 = {
Type = "exec";
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";
ExecStart =
let
convertedSettings = serverCfg.settings // {
restrict-to = lib.map hostPortToString serverCfg.settings.restrict-to;
};
in
''
${lib.getExe serverCfg.package} \
server \
${argsFormat.generate convertedSettings} \
${lib.escapeShellArg "${
if serverCfg.listen.enableHTTPS then "wss" else "ws"
}://${hostPortToString serverCfg.listen}"}
'';
};
};
};
generateClientUnit = name: clientCfg: {
name = "wstunnel-client-${name}";
value = {
description = "wstunnel client - ${name}";
requires = [
"network.target"
"network-online.target"
];
after = [
"network.target"
"network-online.target"
];
wantedBy = lib.optional clientCfg.autoStart "multi-user.target";
serviceConfig = {
Type = "exec";
EnvironmentFile = lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
DynamicUser = true;
PrivateTmp = true;
AmbientCapabilities =
(lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ])
++ (lib.optionals ((clientCfg.settings.socket-so-mark or null) != null) [ "CAP_NET_ADMIN" ]);
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";
ExecStart = ''
${lib.getExe clientCfg.package} \
client \
${argsFormat.generate clientCfg.settings} \
${lib.escapeShellArg clientCfg.connectTo}
'';
};
};
};
in
{
options.services.wstunnel = {
enable = lib.mkEnableOption "wstunnel";
servers = lib.mkOption {
description = "`wstunnel` servers to set up.";
type = lib.types.attrsOf (lib.types.submodule serverSubmodule);
default = { };
example = {
"wg-tunnel" = {
listen = {
host = "0.0.0.0";
port = 8080;
enableHTTPS = true;
};
settings = {
tls-certificate = "/var/lib/secrets/fullchain.pem";
tls-private-key = "/var/lib/secrets/key.pem";
restrict-to = [
{
host = "127.0.0.1";
port = 51820;
}
];
};
};
};
};
clients = lib.mkOption {
description = "`wstunnel` clients to set up.";
type = lib.types.attrsOf (lib.types.submodule clientSubmodule);
default = { };
example = {
"wg-tunnel" = {
connectTo = "wss://wstunnel.server.com:8443";
localToRemote = [
"tcp://1212:google.com:443"
"tcp://2:n.lan:4?proxy_protocol"
];
remoteToLocal = [
"socks5://[::1]:1212"
"unix://wstunnel.sock:g.com:443"
];
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services =
(lib.mapAttrs' generateServerUnit (lib.filterAttrs (_: v: v.enable) cfg.servers))
// (lib.mapAttrs' generateClientUnit (lib.filterAttrs (_: v: v.enable) cfg.clients));
assertions =
(lib.mapAttrsToList (name: serverCfg: {
assertion =
serverCfg.listen.enableHTTPS
->
(serverCfg.useACMEHost != null)
|| (
(serverCfg.settings.tls-certificate or null) != null
&& (serverCfg.settings.tls-private-key or null) != null
);
message = ''
If services.wstunnel.servers."${name}".listen.enableHTTPS is set to true, either services.wstunnel.servers."${name}".useACMEHost or both services.wstunnel.servers."${name}".settings.tls-private-key and services.wstunnel.servers."${name}".settings.tls-certificate need to be set.
'';
}) cfg.servers)
++ (lib.foldlAttrs (
assertions: _: server:
assertions ++ server.assertions
) [ ] cfg.servers)
++ (lib.mapAttrsToList (
name: clientCfg:
let
isListAttrDefined = settings: attr: (settings.${attr} or [ ]) != [ ];
in
{
assertion =
isListAttrDefined clientCfg.settings "local-to-remote"
|| isListAttrDefined clientCfg.settings "remote-to-local";
message = ''
Either one of services.wstunnel.clients."${name}".settings.local-to-remote or services.wstunnel.clients."${name}".settings.remote-to-local must be set.
'';
}
) cfg.clients)
++ (lib.foldlAttrs (
assertions: _: client:
assertions ++ client.assertions
) [ ] cfg.clients);
warnings =
(lib.foldlAttrs (
warnings: _: server:
warnings ++ server.warnings
) [ ] cfg.servers)
++ (lib.foldlAttrs (
warnings: _: client:
warnings ++ client.warnings
) [ ] cfg.clients);
};
meta.maintainers = with lib.maintainers; [
pentane
raylas
rvdp
neverbehave
];
}