From c156fc5dbe819ae32e98bae67f0543016b57885b Mon Sep 17 00:00:00 2001 From: Andrea Ciceri Date: Thu, 1 May 2025 15:19:30 +0200 Subject: [PATCH] nixos/amule: improve NixOS module --- nixos/modules/services/networking/amuled.nix | 334 ++++++++++++++++--- 1 file changed, 284 insertions(+), 50 deletions(-) diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix index 0912cfb26abd..32e7dd7c46b2 100644 --- a/nixos/modules/services/networking/amuled.nix +++ b/nixos/modules/services/networking/amuled.nix @@ -1,89 +1,323 @@ { config, lib, - options, + utils, pkgs, ... }: let cfg = config.services.amule; - opt = options.services.amule; - user = if cfg.user != null then cfg.user else "amule"; -in -{ + settingsFormat = pkgs.formats.ini { }; - ###### interface + inherit (lib) + mkOption + mkEnableOption + mkPackageOption + types + optionalAttrs + mkIf + mkMerge + literalExpression + optionalString + optionals + getExe + ; - options = { - - services.amule = { - - enable = lib.mkOption { - type = lib.types.bool; - default = false; + settingsOptions = { + eMule = { + Port = mkOption { + type = types.port; + default = 4662; description = '' - Whether to run the AMule daemon. You need to manually run "amuled --ec-config" to configure the service for the first time. + TCP port for eD2k connections. + Required for connecting to servers and achieving a High ID. ''; }; - - dataDir = lib.mkOption { - type = lib.types.str; - default = "/home/${user}/"; - defaultText = lib.literalExpression '' - "/home/''${config.${opt.user}}/" - ''; + UDPPort = mkOption { + type = types.port; + default = 4672; description = '' - The directory holding configuration, incoming and temporary files. + UDP port for eD2k traffic (searches, source exchange) and all Kad network communication. + Essential for a High ID on both networks and proper Kad functioning. ''; }; - - user = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + TempDir = mkOption { + type = types.path; + default = "${cfg.dataDir}/Temp"; + defaultText = literalExpression "\${config.services.amule.dataDir}/Temp"; description = '' - The user the AMule daemon should run as. + Directory where aMule stores incomplete downloads (.part/.part.met files). ''; }; - + IncomingDir = mkOption { + type = types.path; + default = "${cfg.dataDir}/Incoming"; + defaultText = literalExpression "\${config.services.amule.dataDir}/Incoming"; + description = '' + Directory where aMule moves completed downloads. + Files in this directory are automatically shared. + Ensure the aMule service has write permissions + ''; + }; + OSDirectory = mkOption { + type = types.path; + default = cfg.dataDir; + defaultText = literalExpression "\${config.services.amule.dataDir}"; + description = "On-disk state directory, probably you don't want to change this"; + internal = true; + }; + }; + ExternalConnect = { + AcceptExternalConnections = mkOption { + type = types.enum [ + 0 + 1 + ]; + default = 1; + description = "Whether to accept external connections, if disabled amuled refuses to start"; + internal = true; + }; + ECPort = mkOption { + type = types.port; + default = 4712; + description = "TCP port for external connections, like remote control via amule-gui"; + }; + ECPassword = mkOption { + type = types.str; + default = ""; + description = '' + MD5 hash of the password, obtainaible with `echo "" | md5sum | cut -d ' ' -f 1` + ''; + }; + }; + WebServer = { + Enabled = lib.mkOption { + type = types.enum [ + 0 + 1 + ]; + default = 0; + description = "Set to 1 to enable the web server"; + }; + Password = mkOption { + type = types.str; + default = ""; + description = '' + MD5 hash of the password, obtainaible with `echo "" | md5sum | cut -d ' ' -f 1` + ''; + }; + Port = mkOption { + type = types.port; + default = 4711; + description = "Web server port"; + }; }; - }; - ###### implementation + webServerEnabled = cfg.settings.WebServer.Enabled == 1; +in +{ + options.services.amule = { + enable = mkEnableOption "aMule daemon"; - config = lib.mkIf cfg.enable { + package = mkPackageOption pkgs "amule-daemon" { }; - users.users = lib.mkIf (cfg.user == null) [ + amuleWebPackage = mkPackageOption pkgs "amule-web" { }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Additional passed arguments"; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/amuled"; + description = "Directory holding configuration and by default also incoming and temporary files"; + }; + + user = mkOption { + type = types.str; + default = "amule"; + description = "The user the aMule daemon should run as"; + }; + + group = mkOption { + type = types.str; + default = "amule"; + description = "Group under which amule runs"; + }; + + openPeerPorts = mkEnableOption "open the peer port(s) in the firewall"; + + openExternalConnectPort = mkEnableOption "open the external connect port"; + + openWebServerPort = mkEnableOption "open the web server port"; + + ExternalConnectPasswordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + File containing the password for connecting with amule-gui, + set this only if you didn't set `settings.ExternalConnect.ECPassword` + ''; + }; + + WebServerPasswordFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + File containing the password for connecting to the web server, + set this only if you didn't set `settings.ExternalConnect.ECPassword` + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = settingsOptions; + }; + description = '' + Free form attribute set for aMule settings. + The final configuration file is generated merging the default settings with these options. + ''; + example = literalExpression '' + { + eMule = { + IncomingDir = "/mnt/hd/amule/Incoming"; + TempDir = "/mnt/hd/amule/Temp"; + }; + WebServer.Enabled = 1; + } + ''; + default = { }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ { - name = "amule"; - description = "AMule daemon"; - group = "amule"; + assertion = isNull cfg.ExternalConnectPasswordFile -> cfg.settings.ExternalConnect.ECPassword != ""; + message = "Set only one between `ExternalConnectPasswordFile` and `settings.ExternalConnect.ECPassword`"; + } + ] + ++ optionals webServerEnabled [ + { + assertion = isNull cfg.WebServerPasswordFile -> cfg.settings.WebServer.Password != ""; + message = "Set only one between `ExternalWebServerFile` `settings.WebServer.Password`"; + } + ]; + + users.users = optionalAttrs (cfg.user == "amule") { + amule = { + group = cfg.group; + description = "aMule user"; + isSystemUser = true; uid = config.ids.uids.amule; - } - ]; + }; + }; - users.groups = lib.mkIf (cfg.user == null) [ - { - name = "amule"; - gid = config.ids.gids.amule; - } - ]; + users.groups = optionalAttrs (cfg.group == "amule") { + amule.gid = config.ids.gids.amule; + }; + + systemd.tmpfiles.settings."10-amuled".${cfg.dataDir}.d = { + inherit (cfg) user group; + mode = "0755"; + }; + + services.amule.settings = { + eMule.AppVersion = lib.getVersion cfg.package; + }; systemd.services.amuled = { description = "AMule daemon"; - wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.crudini ]; preStart = '' - mkdir -p ${cfg.dataDir} - chown ${user} ${cfg.dataDir} + AMULE_CONF="${cfg.dataDir}/amule.conf" + + if [ ! -f "$AMULE_CONF" ]; then + echo "First run detected: starting aMule to generate default configuration..." + echo "aMule will fail with an error - this is expected and normal" + rm -f ${cfg.dataDir}/lastversion + set +e + ${getExe cfg.package} --config-dir ${cfg.dataDir} + set -e + fi + '' + + (lib.concatMapAttrsStringSep "" ( + section: + lib.concatMapAttrsStringSep "" ( + param: value: '' + crudini --inplace --set "$AMULE_CONF" "${section}" "${param}" "${builtins.toString value}" + '' + ) + ) cfg.settings) + + optionalString (!isNull cfg.ExternalConnectPasswordFile) '' + EC_PASSWORD=$(cat ${cfg.ExternalConnectPasswordFile} | md5sum | cut -d ' ' -f 1) + crudini --inplace --set "$AMULE_CONF" "ExternalConnect" "ECPassword" "$EC_PASSWORD" + '' + + optionalString (!isNull cfg.WebServerPasswordFile) '' + WEB_PASSWORD=$(cat ${cfg.WebServerPasswordFile} | md5sum | cut -d ' ' -f 1) + crudini --inplace --set "$AMULE_CONF" "WebServer" "Password" "$WEB_PASSWORD" ''; - script = '' - ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${user} \ - -c 'HOME="${cfg.dataDir}" ${pkgs.amule-daemon}/bin/amuled' - ''; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + + ExecStart = utils.escapeSystemdExecArgs ( + [ + (getExe cfg.package) + "--config-dir" + cfg.dataDir + ] + ++ optionals webServerEnabled [ "--use-amuleweb=${getExe cfg.amuleWebPackage}" ] + ++ cfg.extraArgs + ); + + Restart = "on-failure"; + RestartSec = "5s"; + + # Hardening + NoNewPrivileges = true; + PrivateTmp = true; + PrivateDevices = true; + DevicePolicy = "closed"; + ProtectSystem = "strict"; + ProtectControlGroups = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ReadWritePaths = [ + cfg.dataDir + cfg.settings.eMule.TempDir + cfg.settings.eMule.IncomingDir + ]; + RestrictAddressFamilies = "AF_INET"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + }; }; + + networking.firewall = mkMerge [ + (mkIf cfg.openPeerPorts { + allowedTCPPorts = [ cfg.settings.eMule.Port ]; + allowedUDPPorts = [ cfg.settings.eMule.UDPPort ]; + }) + (mkIf cfg.openWebServerPort { + allowedTCPPorts = [ cfg.settings.WebServer.Port ]; + }) + ]; + }; + + meta = { + maintainers = with lib.maintainers; [ aciceri ]; + doc = ./amuled.md; }; }