nixos/lxmd: init

This commit is contained in:
Pol Dellaiera
2026-07-24 17:48:55 +02:00
parent 519b09fb67
commit b85d98ec6a
5 changed files with 396 additions and 0 deletions
+18
View File
@@ -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"
],
@@ -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}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
View File
@@ -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
+225
View File
@@ -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 = "<path-to-lxmd-identity-file>";
settings = {
propagation-node = {
autopeer = true;
};
};
rnsd = {
identityFile = "<path-to-rnsd-identity-file>";
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 = "<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;
};
};
};
};
services.lxmd = {
enable = true;
identityFile = "<path-to-lxmd-identity-file>";
settings = {
# LXMD settings, run `lxmd --exampleconfig` for details
};
rnsd = {
identityFile = "<path-to-rnsd-identity-file>";
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 = "<path-to-rnsd-identity-file>";
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 = "<path-to-lxmd-identity-file>";
settings = {
# LXMD settings, run `lxmd --exampleconfig` for details
};
rnsd = {
identityFile = "<path-to-rnsd-identity-file>";
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 <path>`), 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.
+150
View File
@@ -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 ];
};
}