nixos/rnsd: init

This commit is contained in:
Pol Dellaiera
2026-07-24 17:48:55 +02:00
parent b85d98ec6a
commit be36232d8a
7 changed files with 321 additions and 0 deletions
+12
View File
@@ -122,6 +122,18 @@
"module-services-onedrive": [
"index.html#module-services-onedrive"
],
"module-services-rnsd": [
"index.html#module-services-rnsd"
],
"module-services-rnsd-hardware-access": [
"index.html#module-services-rnsd-hardware-access"
],
"module-services-rnsd-health-check": [
"index.html#module-services-rnsd-health-check"
],
"module-services-rnsd-quickstart": [
"index.html#module-services-rnsd-quickstart"
],
"module-services-tandoor-recipes-migrating-media-option-move": [
"index.html#module-services-tandoor-recipes-migrating-media-option-move",
"index.html#module-services-tandoor-recipes-migrating-media-option-1"
@@ -58,6 +58,8 @@
- [NordVPN](https://github.com/NordSecurity/nordvpn-linux), a NordVPN client for linux. Available as [services.nordvpn](options.html#opt-services.nordvpn.enable).
- [RNSD](https://reticulum.network/), the Reticulum Network Stack Daemon. It provides a secure and efficient way to communicate over the Reticulum Network. Available as [services.rnsd](#opt-services.rnsd.enable).
- [LXMD](https://github.com/markqvist/LXMF), a universal, distributed and secure messaging protocol for Reticulum. Available as [services.lxmd](#opt-services.lxmd.enable).
## Backward Incompatibilities {#sec-release-26.11-incompatibilities}
+1
View File
@@ -1388,6 +1388,7 @@
./services/networking/redsocks.nix
./services/networking/reframe.nix
./services/networking/resilio.nix
./services/networking/rnsd.nix
./services/networking/robustirc-bridge.nix
./services/networking/rosenpass.nix
./services/networking/routedns.nix
+87
View File
@@ -0,0 +1,87 @@
# RNSD {#module-services-rnsd}
[Reticulum Network Stack](https://reticulum.network/) daemon (`rnsd`).
This module manages `rnsd` as a systemd service and stores runtime state under
`/var/lib/rnsd`.
## Quickstart {#module-services-rnsd-quickstart}
A minimal setup:
```nix
{
services.rnsd.enable = true;
}
```
With custom settings and a persistent identity:
```nix
{
services.rnsd = {
enable = true;
identityFile = "<path-to-rnsd-identity-file>";
settings = {
reticulum = {
enable_transport = true;
share_instance = true;
instance_name = "default";
shared_instance_type = "unix";
};
interfaces = {
auto = {
type = "AutoInterface";
enabled = true;
};
};
openMulticastPorts = true;
};
};
}
```
At startup, settings provided through
[`services.rnsd.settings`](#opt-services.rnsd.settings) and
[`services.rnsd.identityFile`](#opt-services.rnsd.identityFile)
are copied into the service state directory with restrictive permissions.
## Hardware Access {#module-services-rnsd-hardware-access}
If `rnsd` must access serial devices (for example `/dev/ttyACM0`), add the
service user to additional groups:
```nix
{
services.rnsd = {
enable = true;
extraGroups = [ "dialout" ];
};
}
```
See [`services.rnsd.extraGroups`](#opt-services.rnsd.extraGroups) for details.
## Health Check {#module-services-rnsd-health-check}
You can optionally wait for `rnsd` to become responsive during startup using
`rnstatus`:
```nix
{
services.rnsd = {
enable = true;
healthCheck = {
enable = true;
intervalSeconds = 2;
timeoutSeconds = 120;
};
};
}
```
When enabled, startup fails if `rnstatus` does not succeed before
[`services.rnsd.healthCheck.timeoutSeconds`](#opt-services.rnsd.healthCheck.timeoutSeconds).
+171
View File
@@ -0,0 +1,171 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.rnsd;
settingsFormat = pkgs.formats.configobj { };
in
{
options = {
services.rnsd = {
enable = lib.mkEnableOption "Enable Reticulum Network Stack Daemon (rnsd)";
package = lib.mkPackageOption pkgs "rns" { };
packageBinaryName = lib.mkOption {
type = lib.types.str;
default = "rnsd";
description = "Name of the program to use. Useful in case of using an alternative implementation.";
};
settings = lib.mkOption {
type = lib.types.nullOr settingsFormat.type;
default = null;
description = "Structured rnsd configuration. The generated file is copied to the dataDir on service start. Use `rnsd --exampleconfig` to get an example config file.";
};
identityFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Path to rnsd identity file. This file will be copied to the dataDir on service start.";
};
identities = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Map of identity names to paths of identity files. Each identity file will be copied to $STATE_DIRECTORY/storage/identities/{name}.";
};
extraGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Additional groups for the rnsd service user. E.g., add the `dialout` group to allow rnsd to access serial devices (e.g., `/dev/ttyACM0`).";
};
healthCheck = {
enable = lib.mkEnableOption "wait for rnsd to become healthy after startup";
intervalSeconds = lib.mkOption {
type = lib.types.ints.positive;
default = 2;
description = "Delay in seconds between each `rnstatus` health check attempt.";
};
timeoutSeconds = lib.mkOption {
type = lib.types.ints.positive;
default = 120;
description = "Maximum time in seconds to wait for `rnstatus` to succeed during startup.";
};
};
openMulticastPorts = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open the UDP ports (27916, 42671) used for multicast peer discovery when the AutoInterface is enabled.
'';
};
enableUdevRules = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to install udev rules for automatically symlinking RNode devices (e.g., Heltec HT-n5262, RAK4631) to /dev/rnode.";
};
};
};
config = lib.mkIf cfg.enable {
services.udev.extraRules = lib.mkIf cfg.enableUdevRules (
let
devices = [
{
idVendor = "239a";
idProduct = "8071"; # Heltec HT-n5262
}
{
idVendor = "239a";
idProduct = "8029"; # RAK4631
}
];
makeRule = dev: ''
SUBSYSTEM=="tty", ATTRS{idVendor}=="${dev.idVendor}", ATTRS{idProduct}=="${dev.idProduct}", SYMLINK+="rnode%n", MODE="0660", GROUP="dialout"
'';
in
builtins.concatStringsSep "\n" (map makeRule devices)
);
systemd.services.rnsd = {
description = "Reticulum Network Stack Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart =
let
copySettings = lib.optionalString (cfg.settings != null) ''
install -Dm600 ${settingsFormat.generate "rnsd.conf" cfg.settings} "$STATE_DIRECTORY"/config
'';
copyIdentity = lib.optionalString (cfg.identityFile != null) ''
install -Dm600 ${cfg.identityFile} "$STATE_DIRECTORY"/storage/transport_identity
'';
copyIdentities = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: file: ''
install -Dm600 ${file} "$STATE_DIRECTORY"/storage/identities/${name}
'') cfg.identities
);
in
copySettings + copyIdentity + copyIdentities;
serviceConfig =
let
waitForHealthy = pkgs.writeShellApplication {
name = "rnsd-wait-for-health";
runtimeInputs = [ pkgs.rns ];
text = ''
deadline=$((SECONDS + ${toString cfg.healthCheck.timeoutSeconds}))
until rnstatus --config "$STATE_DIRECTORY" >/dev/null 2>&1; do
if [ "$SECONDS" -ge "$deadline" ]; then
echo "rnsd did not become healthy before timeout (${toString cfg.healthCheck.timeoutSeconds}s)" >&2
exit 1
fi
sleep ${toString cfg.healthCheck.intervalSeconds}
done
'';
};
in
{
DynamicUser = true;
StateDirectory = "rnsd";
SupplementaryGroups = cfg.extraGroups;
RuntimeDirectory = "rnsd";
CacheDirectory = "rnsd";
ProtectSystem = "strict";
ExecStart = "${lib.getExe' cfg.package cfg.packageBinaryName} --config $STATE_DIRECTORY";
}
// lib.optionalAttrs cfg.healthCheck.enable {
ExecStartPost = lib.getExe waitForHealthy;
TimeoutStartSec = cfg.healthCheck.timeoutSeconds + 5;
};
};
networking.firewall = lib.mkIf cfg.openMulticastPorts {
extraCommands = lib.optionalString (!config.networking.nftables.enable) ''
ip46tables -A nixos-fw -p udp -m pkttype --pkt-type multicast -m multiport --dports 27916,42671 -j nixos-fw-accept
'';
extraInputRules = lib.optionalString config.networking.nftables.enable ''
meta l4proto udp pkttype multicast udp dport { 27916, 42671 } accept
'';
};
};
meta = {
doc = ./rnsd.md;
maintainers = with lib.maintainers; [ drupol ];
};
}
+1
View File
@@ -1503,6 +1503,7 @@ in
};
rkvm = handleTest ./rkvm { };
rmfakecloud = runTest ./rmfakecloud.nix;
rnsd = runTest ./networking/rnsd.nix;
robustirc-bridge = runTest ./robustirc-bridge.nix;
rosenpass = runTest ./rosenpass.nix;
roundcube = runTest ./roundcube.nix;
+47
View File
@@ -0,0 +1,47 @@
{
lib,
...
}:
let
mainPort = 4242;
in
{
name = "rnsd";
meta.maintainers = [ lib.maintainers.drupol ];
nodes.machine = {
services.rnsd = {
enable = true;
settings = {
reticulum = {
enable_transport = "Yes";
share_instance = "Yes";
instance_name = "default";
discover_interfaces = "No";
panic_on_interface_error = "No";
};
logging = {
loglevel = 5;
};
interfaces = {
test = {
type = "BackboneInterface";
enabled = "yes";
discoverable = "yes";
listen_ip = "0.0.0.0";
listen_port = mainPort;
discovery_name = "Apollo RNS";
};
};
};
};
};
testScript = ''
start_all()
with subtest("Test rnsd service"):
machine.wait_for_unit("rnsd.service")
machine.wait_for_open_port(${toString mainPort})
'';
}