Merge pull request #330522 from xokdvium/dev/init-rathole

nixos/rathole: init module
This commit is contained in:
Sandro
2024-08-05 18:07:25 +02:00
committed by GitHub
5 changed files with 258 additions and 0 deletions

View File

@@ -72,6 +72,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

@@ -1165,6 +1165,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

@@ -830,6 +830,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"
'';
}
)