nixpkgs/nixos/modules/services/networking/rathole.nix
Sergei Zimmerman e3e6e94010
nixos/rathole: init module
Adds a module for rathole package. The package itself
and this module is very similar to frp, so the options
and tests are not very far off from those for frp.
2024-07-28 01:25:01 +03:00

166 lines
4.9 KiB
Nix

{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.rathole;
settingsFormat = pkgs.formats.toml { };
py-toml-merge =
pkgs.writers.writePython3Bin "py-toml-merge"
{
libraries = with pkgs.python3Packages; [
tomli-w
mergedeep
];
}
''
import argparse
from pathlib import Path
from typing import Any
import tomli_w
import tomllib
from mergedeep import merge
parser = argparse.ArgumentParser(description="Merge multiple TOML files")
parser.add_argument(
"files",
type=Path,
nargs="+",
help="List of TOML files to merge",
)
args = parser.parse_args()
merged: dict[str, Any] = {}
for file in args.files:
with open(file, "rb") as fh:
loaded_toml = tomllib.load(fh)
merged = merge(merged, loaded_toml)
print(tomli_w.dumps(merged))
'';
in
{
options = {
services.rathole = {
enable = lib.mkEnableOption "Rathole";
package = lib.mkPackageOption pkgs "rathole" { };
role = lib.mkOption {
type = lib.types.enum [
"server"
"client"
];
description = ''
Select whether rathole needs to be run as a `client` or a `server`.
Server is a machine with a public IP and client is a device behind NAT,
but running some services that need to be exposed to the Internet.
'';
};
credentialsFile = lib.mkOption {
type = lib.types.path;
default = "/dev/null";
description = ''
Path to a TOML file to be merged with the settings.
Useful to set secret config parameters like tokens, which
should not appear in the Nix Store.
'';
example = "/var/lib/secrets/rathole/config.toml";
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Rathole configuration, for options reference
see the [example](https://github.com/rapiz1/rathole?tab=readme-ov-file#configuration) on GitHub.
Both server and client configurations can be specified at the same time, regardless of the selected role.
'';
example = {
server = {
bind_addr = "0.0.0.0:2333";
services.my_nas_ssh = {
token = "use_a_secret_that_only_you_know";
bind_addr = "0.0.0.0:5202";
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.rathole = {
requires = [ "network.target" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
description = "Rathole ${cfg.role} Service";
serviceConfig =
let
name = "rathole";
configFile = settingsFormat.generate "${name}.toml" cfg.settings;
runtimeDir = "/run/${name}";
ratholePrestart =
"+"
+ (pkgs.writeShellScript "rathole-prestart" ''
DYNUSER_UID=$(stat -c %u ${runtimeDir})
DYNUSER_GID=$(stat -c %g ${runtimeDir})
${lib.getExe py-toml-merge} ${configFile} '${cfg.credentialsFile}' |
install -m 600 -o $DYNUSER_UID -g $DYNUSER_GID /dev/stdin ${runtimeDir}/${mergedConfigName}
'');
mergedConfigName = "merged.toml";
in
{
Type = "simple";
Restart = "on-failure";
RestartSec = 5;
ExecStartPre = ratholePrestart;
ExecStart = "${lib.getExe cfg.package} --${cfg.role} ${runtimeDir}/${mergedConfigName}";
DynamicUser = true;
LimitNOFILE = "1048576";
RuntimeDirectory = name;
RuntimeDirectoryMode = "0700";
# Hardening
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
# PrivateUsers=true breaks AmbientCapabilities=CAP_NET_BIND_SERVICE
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = "0066";
};
};
};
meta.maintainers = with lib.maintainers; [ xokdvium ];
}