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.
This commit is contained in:
Sergei Zimmerman 2024-07-28 01:25:01 +03:00
parent 918a1f795e
commit e3e6e94010
No known key found for this signature in database
GPG Key ID: A9B0B557CA632325
5 changed files with 258 additions and 0 deletions

View File

@ -68,6 +68,8 @@
- [OpenGFW](https://github.com/apernet/OpenGFW), an implementation of the Great Firewall on Linux. Available as [services.opengfw](#opt-services.opengfw.enable).
- [Rathole](https://github.com/rapiz1/rathole), a lightweight and high-performance reverse proxy for NAT traversal. Available as [services.rathole](#opt-services.rathole.enable).
## Backward Incompatibilities {#sec-release-24.11-incompatibilities}
- `transmission` package has been aliased with a `trace` warning to `transmission_3`. Since [Transmission 4 has been released last year](https://github.com/transmission/transmission/releases/tag/4.0.0), and Transmission 3 will eventually go away, it was decided perform this warning alias to make people aware of the new version. The `services.transmission.package` defaults to `transmission_3` as well because the upgrade can cause data loss in certain specific usage patterns (examples: [#5153](https://github.com/transmission/transmission/issues/5153), [#6796](https://github.com/transmission/transmission/issues/6796)). Please make sure to back up to your data directory per your usage:

View File

@ -1160,6 +1160,7 @@
./services/networking/r53-ddns.nix
./services/networking/radicale.nix
./services/networking/radvd.nix
./services/networking/rathole.nix
./services/networking/rdnssd.nix
./services/networking/realm.nix
./services/networking/redsocks.nix

View File

@ -0,0 +1,165 @@
{
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 ];
}

View File

@ -823,6 +823,7 @@ in {
radicle = runTest ./radicle.nix;
ragnarwm = handleTest ./ragnarwm.nix {};
rasdaemon = handleTest ./rasdaemon.nix {};
rathole = handleTest ./rathole.nix {};
readarr = handleTest ./readarr.nix {};
realm = handleTest ./realm.nix {};
redis = handleTest ./redis.nix {};

89
nixos/tests/rathole.nix Normal file
View File

@ -0,0 +1,89 @@
import ./make-test-python.nix (
{ pkgs, lib, ... }:
let
successMessage = "Success 3333115147933743662";
in
{
name = "rathole";
meta.maintainers = with lib.maintainers; [ xokdvium ];
nodes = {
server = {
networking = {
useNetworkd = true;
useDHCP = false;
firewall.enable = false;
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.1/24";
};
services.rathole = {
enable = true;
role = "server";
settings = {
server = {
bind_addr = "0.0.0.0:2333";
services = {
success-message = {
bind_addr = "0.0.0.0:80";
token = "hunter2";
};
};
};
};
};
};
client = {
networking = {
useNetworkd = true;
useDHCP = false;
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.2/24";
};
services.nginx = {
enable = true;
virtualHosts."127.0.0.1" = {
root = pkgs.writeTextDir "success-message.txt" successMessage;
};
};
services.rathole = {
enable = true;
role = "client";
credentialsFile = pkgs.writeText "rathole-credentials.toml" ''
[client.services.success-message]
token = "hunter2"
'';
settings = {
client = {
remote_addr = "10.0.0.1:2333";
services.success-message = {
local_addr = "127.0.0.1:80";
};
};
};
};
};
};
testScript = ''
start_all()
server.wait_for_unit("rathole.service")
server.wait_for_open_port(2333)
client.wait_for_unit("rathole.service")
server.wait_for_open_port(80)
response = server.succeed("curl http://127.0.0.1/success-message.txt")
assert "${successMessage}" in response, "Got invalid response"
response = client.succeed("curl http://10.0.0.1/success-message.txt")
assert "${successMessage}" in response, "Got invalid response"
'';
}
)