mirror of
https://github.com/agessaman/meshcore-packet-capture.git
synced 2026-04-20 23:23:37 +00:00
464 lines
17 KiB
Nix
464 lines
17 KiB
Nix
{flake-parts-lib, ...}: {
|
|
flake.nixosModules.default = {config, lib, pkgs, ...}: let
|
|
cfg = config.services.meshcore-packet-capture;
|
|
|
|
# Helper function to create MQTT broker configuration
|
|
mqttBrokerType = num: lib.types.submodule {
|
|
options = {
|
|
enabled = lib.mkEnableOption "Enable MQTT broker ${toString num}";
|
|
server = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "MQTT broker address";
|
|
};
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 1883;
|
|
description = "MQTT broker port";
|
|
};
|
|
username = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "MQTT username";
|
|
};
|
|
password = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "MQTT password";
|
|
};
|
|
transport = lib.mkOption {
|
|
type = lib.types.enum ["tcp" "websockets"];
|
|
default = "tcp";
|
|
description = "Transport type";
|
|
};
|
|
useTLS = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable TLS/SSL encryption";
|
|
};
|
|
tlsVerify = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Verify TLS certificates";
|
|
};
|
|
useAuthToken = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Use auth token authentication";
|
|
};
|
|
tokenAudience = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Token audience for auth token";
|
|
};
|
|
clientIdPrefix = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Client ID prefix";
|
|
};
|
|
qos = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 0;
|
|
description = "Quality of Service level";
|
|
};
|
|
retain = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Retain messages";
|
|
};
|
|
keepalive = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 60;
|
|
description = "Keep-alive interval";
|
|
};
|
|
topicStatus = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Status topic";
|
|
};
|
|
topicPackets = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Packets topic";
|
|
};
|
|
topicRaw = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Raw topic";
|
|
};
|
|
topicDecoded = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Decoded topic";
|
|
};
|
|
topicDebug = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Debug topic";
|
|
};
|
|
};
|
|
};
|
|
|
|
# Build environment variables from configuration
|
|
buildEnvVars = let
|
|
mqttEnvVars = lib.flatten (lib.imap1 (num: broker:
|
|
# Only generate env vars if broker is enabled and has a server configured
|
|
if broker.enabled && broker.server != null then
|
|
[
|
|
"PACKETCAPTURE_MQTT${toString num}_ENABLED=true"
|
|
"PACKETCAPTURE_MQTT${toString num}_SERVER=${broker.server}"
|
|
"PACKETCAPTURE_MQTT${toString num}_PORT=${toString broker.port}"
|
|
] ++ lib.optional (broker.username != null) "PACKETCAPTURE_MQTT${toString num}_USERNAME=${broker.username}"
|
|
++ lib.optional (broker.password != null) "PACKETCAPTURE_MQTT${toString num}_PASSWORD=${broker.password}"
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_TRANSPORT=${broker.transport}"]
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_USE_TLS=${if broker.useTLS then "true" else "false"}"]
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_TLS_VERIFY=${if broker.tlsVerify then "true" else "false"}"]
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_USE_AUTH_TOKEN=${if broker.useAuthToken then "true" else "false"}"]
|
|
++ lib.optional (broker.tokenAudience != null) "PACKETCAPTURE_MQTT${toString num}_TOKEN_AUDIENCE=${broker.tokenAudience}"
|
|
++ lib.optional (broker.clientIdPrefix != null) "PACKETCAPTURE_MQTT${toString num}_CLIENT_ID_PREFIX=${broker.clientIdPrefix}"
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_QOS=${toString broker.qos}"]
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_RETAIN=${if broker.retain then "true" else "false"}"]
|
|
++ ["PACKETCAPTURE_MQTT${toString num}_KEEPALIVE=${toString broker.keepalive}"]
|
|
++ lib.optional (broker.topicStatus != null) "PACKETCAPTURE_MQTT${toString num}_TOPIC_STATUS=${broker.topicStatus}"
|
|
++ lib.optional (broker.topicPackets != null) "PACKETCAPTURE_MQTT${toString num}_TOPIC_PACKETS=${broker.topicPackets}"
|
|
++ lib.optional (broker.topicRaw != null) "PACKETCAPTURE_MQTT${toString num}_TOPIC_RAW=${broker.topicRaw}"
|
|
++ lib.optional (broker.topicDecoded != null) "PACKETCAPTURE_MQTT${toString num}_TOPIC_DECODED=${broker.topicDecoded}"
|
|
++ lib.optional (broker.topicDebug != null) "PACKETCAPTURE_MQTT${toString num}_TOPIC_DEBUG=${broker.topicDebug}"
|
|
else [
|
|
"PACKETCAPTURE_MQTT${toString num}_ENABLED=false"
|
|
]) [
|
|
cfg.mqtt1
|
|
cfg.mqtt2
|
|
cfg.mqtt3
|
|
cfg.mqtt4
|
|
]);
|
|
|
|
connectionEnvVars = [
|
|
"PACKETCAPTURE_CONNECTION_TYPE=${cfg.connectionType}"
|
|
] ++ lib.optional (cfg.bleAddress != null) "PACKETCAPTURE_BLE_ADDRESS=${cfg.bleAddress}"
|
|
++ lib.optional (cfg.bleDeviceName != null) "PACKETCAPTURE_BLE_DEVICE_NAME=${cfg.bleDeviceName}"
|
|
++ lib.optional (cfg.serialPorts != null) "PACKETCAPTURE_SERIAL_PORTS=${lib.concatStringsSep "," cfg.serialPorts}"
|
|
++ lib.optional (cfg.tcpHost != null) "PACKETCAPTURE_TCP_HOST=${cfg.tcpHost}"
|
|
++ lib.optional (cfg.tcpPort != null) "PACKETCAPTURE_TCP_PORT=${toString cfg.tcpPort}"
|
|
++ lib.optional (cfg.timeout != null) "PACKETCAPTURE_TIMEOUT=${toString cfg.timeout}"
|
|
++ lib.optional (cfg.maxConnectionRetries != null) "PACKETCAPTURE_MAX_CONNECTION_RETRIES=${toString cfg.maxConnectionRetries}"
|
|
++ lib.optional (cfg.connectionRetryDelay != null) "PACKETCAPTURE_CONNECTION_RETRY_DELAY=${toString cfg.connectionRetryDelay}"
|
|
++ lib.optional (cfg.healthCheckInterval != null) "PACKETCAPTURE_HEALTH_CHECK_INTERVAL=${toString cfg.healthCheckInterval}";
|
|
|
|
otherEnvVars = [
|
|
"PACKETCAPTURE_LOG_LEVEL=${cfg.logLevel}"
|
|
] ++ lib.optional (cfg.iata != null) "PACKETCAPTURE_IATA=${cfg.iata}"
|
|
++ lib.optional (cfg.origin != null) "PACKETCAPTURE_ORIGIN=${cfg.origin}"
|
|
++ lib.optional (cfg.maxMqttRetries != null) "PACKETCAPTURE_MAX_MQTT_RETRIES=${toString cfg.maxMqttRetries}"
|
|
++ lib.optional (cfg.mqttRetryDelay != null) "PACKETCAPTURE_MQTT_RETRY_DELAY=${toString cfg.mqttRetryDelay}"
|
|
++ lib.optional (cfg.exitOnReconnectFail != null) "PACKETCAPTURE_EXIT_ON_RECONNECT_FAIL=${if cfg.exitOnReconnectFail then "true" else "false"}"
|
|
++ lib.optional (cfg.privateKey != null) "PACKETCAPTURE_PRIVATE_KEY=${cfg.privateKey}"
|
|
++ lib.optional (cfg.privateKeyFile != null) "PACKETCAPTURE_PRIVATE_KEY_FILE=${cfg.privateKeyFile}"
|
|
++ lib.optional (cfg.ownerPublicKey != null) "PACKETCAPTURE_OWNER_PUBLIC_KEY=${cfg.ownerPublicKey}"
|
|
++ lib.optional (cfg.ownerEmail != null) "PACKETCAPTURE_OWNER_EMAIL=${cfg.ownerEmail}"
|
|
++ lib.optional (cfg.advertIntervalHours != null) "PACKETCAPTURE_ADVERT_INTERVAL_HOURS=${toString cfg.advertIntervalHours}"
|
|
++ lib.optional (cfg.uploadPacketTypes != null) "PACKETCAPTURE_UPLOAD_PACKET_TYPES=${lib.concatStringsSep "," (map toString cfg.uploadPacketTypes)}"
|
|
++ lib.optional (cfg.rfDataTimeout != null) "PACKETCAPTURE_RF_DATA_TIMEOUT=${toString cfg.rfDataTimeout}"
|
|
++ lib.optional (cfg.outputFile != null) "PACKETCAPTURE_OUTPUT_FILE=${cfg.outputFile}"
|
|
++ lib.optional cfg.verbose "PACKETCAPTURE_VERBOSE=true"
|
|
++ lib.optional cfg.debug "PACKETCAPTURE_DEBUG=true"
|
|
++ lib.optional (!cfg.enableMqtt) "PACKETCAPTURE_NO_MQTT=true";
|
|
|
|
in connectionEnvVars ++ mqttEnvVars ++ otherEnvVars;
|
|
in {
|
|
options.services.meshcore-packet-capture = {
|
|
enable = lib.mkEnableOption "MeshCore Packet Capture service";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default =
|
|
if pkgs ? meshcore-packet-capture
|
|
then pkgs.meshcore-packet-capture
|
|
else throw ''
|
|
The meshcore-packet-capture package is not available in pkgs.
|
|
|
|
When using this module from a flake, you must set the package option:
|
|
|
|
Example:
|
|
services.meshcore-packet-capture.package = self.packages.x86_64-linux.default;
|
|
|
|
(Replace x86_64-linux with your system architecture if different)
|
|
'';
|
|
defaultText = "pkgs.meshcore-packet-capture";
|
|
description = ''
|
|
The meshcore-packet-capture package to use.
|
|
|
|
When using this module from a flake, you must override this option:
|
|
|
|
Example:
|
|
services.meshcore-packet-capture.package = self.packages.x86_64-linux.default;
|
|
|
|
(Replace x86_64-linux with your system architecture if different)
|
|
'';
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "meshcore";
|
|
description = "User to run the service as";
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "meshcore";
|
|
description = "Group to run the service as";
|
|
};
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/meshcore-packet-capture";
|
|
description = "Directory for data files";
|
|
};
|
|
|
|
# Connection settings
|
|
connectionType = lib.mkOption {
|
|
type = lib.types.enum ["ble" "serial" "tcp"];
|
|
default = "ble";
|
|
description = "Connection type";
|
|
};
|
|
|
|
bleAddress = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Specific BLE device address";
|
|
};
|
|
|
|
bleDeviceName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "BLE device name to scan for";
|
|
};
|
|
|
|
serialPorts = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
description = "Comma-separated list of serial ports to try";
|
|
};
|
|
|
|
tcpHost = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = "localhost";
|
|
description = "TCP host address";
|
|
};
|
|
|
|
tcpPort = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.port;
|
|
default = 5000;
|
|
description = "TCP port number";
|
|
};
|
|
|
|
timeout = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 30;
|
|
description = "Connection timeout in seconds";
|
|
};
|
|
|
|
maxConnectionRetries = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 5;
|
|
description = "Maximum MeshCore connection retry attempts (0 = infinite)";
|
|
};
|
|
|
|
connectionRetryDelay = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 5;
|
|
description = "Delay between MeshCore reconnection attempts (seconds)";
|
|
};
|
|
|
|
healthCheckInterval = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 30;
|
|
description = "How often to check connection health (seconds)";
|
|
};
|
|
|
|
# Logging
|
|
logLevel = lib.mkOption {
|
|
type = lib.types.enum ["DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL"];
|
|
default = "INFO";
|
|
description = "Log level";
|
|
};
|
|
|
|
verbose = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable verbose output";
|
|
};
|
|
|
|
debug = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable debug output";
|
|
};
|
|
|
|
# MQTT brokers
|
|
mqtt1 = lib.mkOption {
|
|
type = mqttBrokerType 1;
|
|
default = {};
|
|
description = "MQTT broker 1 configuration";
|
|
};
|
|
|
|
mqtt2 = lib.mkOption {
|
|
type = mqttBrokerType 2;
|
|
default = {};
|
|
description = "MQTT broker 2 configuration";
|
|
};
|
|
|
|
mqtt3 = lib.mkOption {
|
|
type = mqttBrokerType 3;
|
|
default = {};
|
|
description = "MQTT broker 3 configuration";
|
|
};
|
|
|
|
mqtt4 = lib.mkOption {
|
|
type = mqttBrokerType 4;
|
|
default = {};
|
|
description = "MQTT broker 4 configuration";
|
|
};
|
|
|
|
enableMqtt = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Enable MQTT publishing";
|
|
};
|
|
|
|
maxMqttRetries = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 5;
|
|
description = "Maximum MQTT connection retry attempts (0 = infinite)";
|
|
};
|
|
|
|
mqttRetryDelay = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 5;
|
|
description = "Delay between MQTT reconnection attempts (seconds)";
|
|
};
|
|
|
|
exitOnReconnectFail = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.bool;
|
|
default = true;
|
|
description = "Exit when reconnection attempts fail";
|
|
};
|
|
|
|
# Private key
|
|
privateKey = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Device private key for auth token authentication (hex string)";
|
|
};
|
|
|
|
privateKeyFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = "Path to file containing device private key";
|
|
};
|
|
|
|
ownerPublicKey = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Owner public key for JWT tokens (64 hex characters)";
|
|
};
|
|
|
|
ownerEmail = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Owner email address for Let's Mesh Analyzer";
|
|
};
|
|
|
|
# Other settings
|
|
iata = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = "LOC";
|
|
description = "IATA code for topic templates";
|
|
};
|
|
|
|
origin = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Origin identifier";
|
|
};
|
|
|
|
advertIntervalHours = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = 11;
|
|
description = "Send flood adverts at this interval (0 = disabled)";
|
|
};
|
|
|
|
uploadPacketTypes = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.int);
|
|
default = null;
|
|
description = "List of packet type numbers to upload to MQTT";
|
|
};
|
|
|
|
rfDataTimeout = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.float;
|
|
default = 15.0;
|
|
description = "RF data timeout";
|
|
};
|
|
|
|
outputFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Output file path";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# Create user and group
|
|
users.users.${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
description = "MeshCore Packet Capture service user";
|
|
extraGroups = ["bluetooth" "dialout"]; # For BLE and serial access
|
|
};
|
|
|
|
users.groups.${cfg.group} = {};
|
|
|
|
# Systemd service
|
|
systemd.services.meshcore-packet-capture = {
|
|
description = "MeshCore Packet Capture Service";
|
|
wantedBy = ["multi-user.target"];
|
|
after = ["network.target" "bluetooth.target"];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = cfg.dataDir;
|
|
ExecStart = "${cfg.package}/bin/meshcore-packet-capture";
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
StandardOutput = "journal";
|
|
StandardError = "journal";
|
|
|
|
# Environment variables
|
|
Environment = buildEnvVars;
|
|
|
|
# Security settings
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
ReadWritePaths = [cfg.dataDir];
|
|
|
|
# Device access
|
|
SupplementaryGroups = ["bluetooth" "dialout"];
|
|
};
|
|
|
|
environment = {
|
|
PYTHONUNBUFFERED = "1";
|
|
PYTHONDONTWRITEBYTECODE = "1";
|
|
};
|
|
};
|
|
|
|
# Create data directory
|
|
systemd.tmpfiles.rules = [
|
|
"d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} - -"
|
|
];
|
|
};
|
|
};
|
|
}
|
|
|