diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 08e870505705..06582cf0d305 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -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" diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 829a35682dd5..7b21b7be419f 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -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} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 274965fb7f18..21cc877945da 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/services/networking/rnsd.md b/nixos/modules/services/networking/rnsd.md new file mode 100644 index 000000000000..5c2e466c3164 --- /dev/null +++ b/nixos/modules/services/networking/rnsd.md @@ -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 = ""; + + 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). diff --git a/nixos/modules/services/networking/rnsd.nix b/nixos/modules/services/networking/rnsd.nix new file mode 100644 index 000000000000..113047e79c2e --- /dev/null +++ b/nixos/modules/services/networking/rnsd.nix @@ -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 ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index f892390cefd5..72954d939aba 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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; diff --git a/nixos/tests/networking/rnsd.nix b/nixos/tests/networking/rnsd.nix new file mode 100644 index 000000000000..a2931489221c --- /dev/null +++ b/nixos/tests/networking/rnsd.nix @@ -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}) + ''; +}