diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 27be44d30b25..08e870505705 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -65,6 +65,24 @@ "module-services-keycloak-unix-socket": [ "index.html#module-services-keycloak-unix-socket" ], + "module-services-lxmd": [ + "index.html#module-services-lxmd" + ], + "module-services-lxmd-communication-backbone": [ + "index.html#module-services-lxmd-communication-backbone" + ], + "module-services-lxmd-communication-rpc": [ + "index.html#module-services-lxmd-communication-rpc" + ], + "module-services-lxmd-communication-with-rnsd": [ + "index.html#module-services-lxmd-communication-with-rnsd" + ], + "module-services-lxmd-health-check": [ + "index.html#module-services-lxmd-health-check" + ], + "module-services-lxmd-quickstart": [ + "index.html#module-services-lxmd-quickstart" + ], "module-services-mautrix-discord": [ "index.html#module-services-mautrix-discord" ], diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 2b7750602776..829a35682dd5 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). +- [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 de6172266d5e..274965fb7f18 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1290,6 +1290,7 @@ ./services/networking/logmein-hamachi.nix ./services/networking/lokinet.nix ./services/networking/lxd-image-server.nix + ./services/networking/lxmd.nix ./services/networking/magic-wormhole-mailbox-server.nix ./services/networking/matterbridge.nix ./services/networking/meshtasticd.nix diff --git a/nixos/modules/services/networking/lxmd.md b/nixos/modules/services/networking/lxmd.md new file mode 100644 index 000000000000..08e038fbfdd6 --- /dev/null +++ b/nixos/modules/services/networking/lxmd.md @@ -0,0 +1,225 @@ +# LXMD {#module-services-lxmd} + +[LXMF](https://github.com/markqvist/lxmf) daemon (`lxmd`). + +This module runs `lxmd` as a systemd service with `DynamicUser = true` and +`StateDirectory = "lxmd"` directives. + +## Quickstart {#module-services-lxmd-quickstart} + +A minimal setup: + +```nix +{ + services.lxmd.enable = true; +} +``` + +With custom LXMD and RNSD settings: + +```nix +{ + services.lxmd = { + enable = true; + + identityFile = ""; + + settings = { + propagation-node = { + autopeer = true; + }; + }; + + rnsd = { + identityFile = ""; + + settings = { + reticulum = { + require_shared_instance = true; + is_shared_instance = true; + enable_transport = true; + instance_name = "default"; + shared_instance_type = "unix"; + }; + }; + }; + }; +} +``` + +At startup, settings provided through +[`services.lxmd.settings`](#opt-services.lxmd.settings), +[`services.lxmd.identityFile`](#opt-services.lxmd.identityFile), and +[`services.lxmd.rnsd.settings`](#opt-services.lxmd.rnsd.settings) are copied +into the service state directory. + +## Health Check {#module-services-lxmd-health-check} + +You can optionally wait for `lxmd` to become responsive during startup using +`lxmd --status`: + +```nix +{ + services.lxmd = { + enable = true; + + healthCheck = { + enable = true; + intervalSeconds = 2; + timeoutSeconds = 120; + }; + }; +} +``` + +When enabled, startup fails if `lxmd --status` does not succeed before +[`services.lxmd.healthCheck.timeoutSeconds`](#opt-services.lxmd.healthCheck.timeoutSeconds). + +## Communicating With RNSD {#module-services-lxmd-communication-with-rnsd} + +Even though both services are hardened (via Systemd's `DynamicUser = true` and +`StateDirectory = "lxmd"` directives), `lxmd` can still communicate with `rnsd` +in several ways. + +### Option 1: Shared Instance Over Unix RPC {#module-services-lxmd-communication-rpc} + +Use a shared Reticulum instance exposed through a local Unix RPC socket: + +```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; + }; + }; + }; + }; + + services.lxmd = { + enable = true; + + identityFile = ""; + + settings = { + # LXMD settings, run `lxmd --exampleconfig` for details + }; + + rnsd = { + identityFile = ""; + + settings = { + reticulum = { + require_shared_instance = true; + is_shared_instance = true; + enable_transport = true; + instance_name = "default"; + shared_instance_type = "unix"; + }; + }; + }; + }; +} +``` + +In this model, `lxmd` points to an RNS configuration that joins the shared +instance. Both RNS transport identity files in `rnsd` and `lxmd` must be the +same. If you use different transport identity files, then set the same `rpc_key` +in both `rnsd` configurations to allow them to communicate securely. + +It is also possible to use a shared instance over TCP, please refer to the +[Reticulum documentation](https://reticulum.network/manual/) for details. + +### Option 2: Isolated Instances Connected Through BackboneInterface {#module-services-lxmd-communication-backbone} + +Reticulum applications (`rnsd`, `lxmd`, and others) can be isolated by running: + +- one primary `rnsd` service instance as a non-privileged service account, +- one instance per application, each with its own user and configuration, +- a `BackboneInterface` between each isolated application instance and the + primary instance. + +On the primary `rnsd` instance, the `BackboneInterface` is configured to accept +connections from the isolated instances, as such: + +```nix +{ + services.rnsd = { + enable = true; + + identityFile = ""; + + settings = { + reticulum = { + # RNSD settings, run `rnsd --exampleconfig` for details + }; + interfaces = { + root = { + type = "BackboneInterface"; + enabled = true; + mode = "gateway"; + discoverable = true; + listen_ip = "127.0.0.1"; + listen_port = 4242; + }; + }; + }; + }; +} +``` + +Then, on each isolated application instance, the `BackboneInterface` is +configured to connect to the primary instance: + +```nix +{ + services.lxmd = { + enable = true; + + identityFile = ""; + + settings = { + # LXMD settings, run `lxmd --exampleconfig` for details + }; + + rnsd = { + identityFile = ""; + + settings = { + reticulum = { + # RNSD settings, run `rnsd --exampleconfig` for details + }; + interfaces = { + local = { + type = "BackboneInterface"; + enabled = true; + target_host = "127.0.0.1"; + target_port = 4242; + }; + }; + }; + }; + }; +} +``` + +The primary `rnsd` instance serves system-local `BackboneInterface` links for +applications and external interfaces for the wider network. Each isolated app +uses its own RNS configuration directory (for example with +`--rnsconfig `), and that local instance connects to the root instance +through `BackboneInterface`. + +Running multiple instances increases memory usage and adds one extra hop, but +the performance impact is generally negligible. diff --git a/nixos/modules/services/networking/lxmd.nix b/nixos/modules/services/networking/lxmd.nix new file mode 100644 index 000000000000..017bba146c20 --- /dev/null +++ b/nixos/modules/services/networking/lxmd.nix @@ -0,0 +1,150 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.lxmd; + settingsFormat = pkgs.formats.configobj { }; +in +{ + options = { + services.lxmd = { + enable = lib.mkEnableOption "Enable Reticulum Network Lightweight Extensible Message Format Daemon (lxmd)"; + + package = lib.mkPackageOption pkgs "lxmf" { }; + packageBinaryName = lib.mkOption { + type = lib.types.str; + default = "lxmd"; + 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 lxmd configuration. The generated file is copied to the state directory on service start. Use `lxmd --exampleconfig` to get an example config file."; + }; + + identityFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Path to lxmd identity file. This file will be copied to the state directory on service start."; + }; + + rnsd = { + settings = lib.mkOption { + type = lib.types.nullOr settingsFormat.type; + default = null; + description = "Structured rnsd configuration. The generated file is copied to the state directory 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 state directory on service start."; + }; + + identities = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Map of identity names to paths of identity files."; + }; + }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Additional groups for the lxmd service user."; + }; + + healthCheck = { + enable = lib.mkEnableOption "wait for lxmd to become healthy after startup"; + + intervalSeconds = lib.mkOption { + type = lib.types.ints.positive; + default = 2; + description = "Delay in seconds between each `lxmd --status` health check attempt."; + }; + + timeoutSeconds = lib.mkOption { + type = lib.types.ints.positive; + default = 120; + description = "Maximum time in seconds to wait for `lxmd --status` to succeed during startup."; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.lxmd = { + description = "Reticulum Network Lightweight Extensible Message Format Daemon"; + wantedBy = [ "multi-user.target" ]; + after = lib.optionals config.services.rnsd.enable [ "rnsd.service" ]; + wants = lib.optionals config.services.rnsd.enable [ "rnsd.service" ]; + + preStart = + let + copySettings = lib.optionalString (cfg.settings != null) '' + install -Dm600 ${settingsFormat.generate "lxmd.conf" cfg.settings} "$STATE_DIRECTORY"/lxmd/config + ''; + copyIdentity = lib.optionalString (cfg.identityFile != null) '' + install -Dm600 ${cfg.identityFile} "$STATE_DIRECTORY"/lxmd/identity + ''; + copyRnsdSettings = lib.optionalString (cfg.rnsd.settings != null) '' + install -Dm600 ${settingsFormat.generate "rnsd.conf" cfg.rnsd.settings} "$STATE_DIRECTORY"/rnsd/config + ''; + copyRnsdIdentity = lib.optionalString (cfg.rnsd.identityFile != null) '' + install -Dm600 ${cfg.rnsd.identityFile} "$STATE_DIRECTORY"/rnsd/storage/transport_identity + ''; + copyRnsdIdentities = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: file: '' + install -Dm600 ${file} "$STATE_DIRECTORY"/rnsd/storage/identities/${name} + '') cfg.rnsd.identities + ); + in + copyRnsdSettings + copyRnsdIdentity + copySettings + copyIdentity + copyRnsdIdentities; + + serviceConfig = + let + waitForHealthy = pkgs.writeShellApplication { + name = "lxmd-wait-for-health"; + runtimeInputs = [ pkgs.lxmf ]; + text = '' + deadline=$((SECONDS + ${toString cfg.healthCheck.timeoutSeconds})) + + until lxmd --status --config "$STATE_DIRECTORY"/lxmd --rnsconfig "$STATE_DIRECTORY"/rnsd >/dev/null 2>&1; do + if [ "$SECONDS" -ge "$deadline" ]; then + echo "lxmd 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 = "lxmd"; + SupplementaryGroups = cfg.extraGroups; + RuntimeDirectory = "lxmd"; + CacheDirectory = "lxmd"; + ProtectSystem = "strict"; + + ExecStart = '' + ${lib.getExe' cfg.package cfg.packageBinaryName} --config ''${STATE_DIRECTORY}/lxmd --rnsconfig ''${STATE_DIRECTORY}/rnsd + ''; + } + // lib.optionalAttrs cfg.healthCheck.enable { + ExecStartPost = lib.getExe waitForHealthy; + TimeoutStartSec = cfg.healthCheck.timeoutSeconds + 5; + }; + }; + }; + + meta = { + doc = ./lxmd.md; + maintainers = with lib.maintainers; [ drupol ]; + }; +}