summary refs log tree commit diff
path: root/nixos/modules/services/torrent
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/torrent')
-rw-r--r--nixos/modules/services/torrent/deluge.nix279
-rw-r--r--nixos/modules/services/torrent/flexget.nix100
-rw-r--r--nixos/modules/services/torrent/magnetico.nix220
-rw-r--r--nixos/modules/services/torrent/opentracker.nix45
-rw-r--r--nixos/modules/services/torrent/peerflix.nix71
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix211
-rw-r--r--nixos/modules/services/torrent/transmission.nix487
7 files changed, 1413 insertions, 0 deletions
diff --git a/nixos/modules/services/torrent/deluge.nix b/nixos/modules/services/torrent/deluge.nix
new file mode 100644
index 00000000000..cb0da9e83b4
--- /dev/null
+++ b/nixos/modules/services/torrent/deluge.nix
@@ -0,0 +1,279 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.deluge;
+  cfg_web = config.services.deluge.web;
+  isDeluge1 = versionOlder cfg.package.version "2.0.0";
+
+  openFilesLimit = 4096;
+  listenPortsDefault = [ 6881 6889 ];
+
+  listToRange = x: { from = elemAt x 0; to = elemAt x 1; };
+
+  configDir = "${cfg.dataDir}/.config/deluge";
+  configFile = pkgs.writeText "core.conf" (builtins.toJSON cfg.config);
+  declarativeLockFile = "${configDir}/.declarative";
+
+  preStart = if cfg.declarative then ''
+    if [ -e ${declarativeLockFile} ]; then
+      # Was declarative before, no need to back up anything
+      ${if isDeluge1 then "ln -sf" else "cp"} ${configFile} ${configDir}/core.conf
+      ln -sf ${cfg.authFile} ${configDir}/auth
+    else
+      # Declarative for the first time, backup stateful files
+      ${if isDeluge1 then "ln -s" else "cp"} -b --suffix=.stateful ${configFile} ${configDir}/core.conf
+      ln -sb --suffix=.stateful ${cfg.authFile} ${configDir}/auth
+      echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+        > ${declarativeLockFile}
+    fi
+  '' else ''
+    if [ -e ${declarativeLockFile} ]; then
+      rm ${declarativeLockFile}
+    fi
+  '';
+in {
+  options = {
+    services = {
+      deluge = {
+        enable = mkEnableOption "Deluge daemon";
+
+        openFilesLimit = mkOption {
+          default = openFilesLimit;
+          type = types.either types.int types.str;
+          description = ''
+            Number of files to allow deluged to open.
+          '';
+        };
+
+        config = mkOption {
+          type = types.attrs;
+          default = {};
+          example = literalExpression ''
+            {
+              download_location = "/srv/torrents/";
+              max_upload_speed = "1000.0";
+              share_ratio_limit = "2.0";
+              allow_remote = true;
+              daemon_port = 58846;
+              listen_ports = [ ${toString listenPortsDefault} ];
+            }
+          '';
+          description = ''
+            Deluge core configuration for the core.conf file. Only has an effect
+            when <option>services.deluge.declarative</option> is set to
+            <literal>true</literal>. String values must be quoted, integer and
+            boolean values must not. See
+            <link xlink:href="https://git.deluge-torrent.org/deluge/tree/deluge/core/preferencesmanager.py#n41"/>
+            for the availaible options.
+          '';
+        };
+
+        declarative = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to use a declarative deluge configuration.
+            Only if set to <literal>true</literal>, the options
+            <option>services.deluge.config</option>,
+            <option>services.deluge.openFirewall</option> and
+            <option>services.deluge.authFile</option> will be
+            applied.
+          '';
+        };
+
+        openFirewall = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            Whether to open the firewall for the ports in
+            <option>services.deluge.config.listen_ports</option>. It only takes effet if
+            <option>services.deluge.declarative</option> is set to
+            <literal>true</literal>.
+
+            It does NOT apply to the daemon port nor the web UI port. To access those
+            ports secuerly check the documentation
+            <link xlink:href="https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient#CreateSSHTunnel"/>
+            or use a VPN or configure certificates for deluge.
+          '';
+        };
+
+        dataDir = mkOption {
+          type = types.path;
+          default = "/var/lib/deluge";
+          description = ''
+            The directory where deluge will create files.
+          '';
+        };
+
+        authFile = mkOption {
+          type = types.path;
+          example = "/run/keys/deluge-auth";
+          description = ''
+            The file managing the authentication for deluge, the format of this
+            file is straightforward, each line contains a
+            username:password:level tuple in plaintext. It only has an effect
+            when <option>services.deluge.declarative</option> is set to
+            <literal>true</literal>.
+            See <link xlink:href="https://dev.deluge-torrent.org/wiki/UserGuide/Authentication"/> for
+            more informations.
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "deluge";
+          description = ''
+            User account under which deluge runs.
+          '';
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "deluge";
+          description = ''
+            Group under which deluge runs.
+          '';
+        };
+
+        extraPackages = mkOption {
+          type = types.listOf types.package;
+          default = [];
+          description = ''
+            Extra packages available at runtime to enable Deluge's plugins. For example,
+            extraction utilities are required for the built-in "Extractor" plugin.
+            This always contains unzip, gnutar, xz and bzip2.
+          '';
+        };
+
+        package = mkOption {
+          type = types.package;
+          example = literalExpression "pkgs.deluge-2_x";
+          description = ''
+            Deluge package to use.
+          '';
+        };
+      };
+
+      deluge.web = {
+        enable = mkEnableOption "Deluge Web daemon";
+
+        port = mkOption {
+          type = types.port;
+          default = 8112;
+          description = ''
+            Deluge web UI port.
+          '';
+        };
+
+        openFirewall = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Open ports in the firewall for deluge web daemon
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.deluge.package = mkDefault (
+      if versionAtLeast config.system.stateVersion "20.09" then
+        pkgs.deluge-2_x
+      else
+        # deluge-1_x is no longer packaged and this will resolve to an error
+        # thanks to the alias for this name.  This is left here so that anyone
+        # using NixOS older than 20.09 receives that error when they upgrade
+        # and is forced to make an intentional choice to switch to deluge-2_x.
+        # That might be slightly inconvenient but there is no path to
+        # downgrade from 2.x to 1.x so NixOS should not automatically perform
+        # this state migration.
+        pkgs.deluge-1_x
+    );
+
+    # Provide a default set of `extraPackages`.
+    services.deluge.extraPackages = with pkgs; [ unzip gnutar xz bzip2 ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group}"
+      "d '${cfg.dataDir}/.config' 0770 ${cfg.user} ${cfg.group}"
+      "d '${cfg.dataDir}/.config/deluge' 0770 ${cfg.user} ${cfg.group}"
+    ]
+    ++ optional (cfg.config ? download_location)
+      "d '${cfg.config.download_location}' 0770 ${cfg.user} ${cfg.group}"
+    ++ optional (cfg.config ? torrentfiles_location)
+      "d '${cfg.config.torrentfiles_location}' 0770 ${cfg.user} ${cfg.group}"
+    ++ optional (cfg.config ? move_completed_path)
+      "d '${cfg.config.move_completed_path}' 0770 ${cfg.user} ${cfg.group}";
+
+    systemd.services.deluged = {
+      after = [ "network.target" ];
+      description = "Deluge BitTorrent Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package ] ++ cfg.extraPackages;
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/deluged \
+            --do-not-daemonize \
+            --config ${configDir}
+        '';
+        # To prevent "Quit & shutdown daemon" from working; we want systemd to
+        # manage it!
+        Restart = "on-success";
+        User = cfg.user;
+        Group = cfg.group;
+        UMask = "0002";
+        LimitNOFILE = cfg.openFilesLimit;
+      };
+      preStart = preStart;
+    };
+
+    systemd.services.delugeweb = mkIf cfg_web.enable {
+      after = [ "network.target" "deluged.service"];
+      requires = [ "deluged.service" ];
+      description = "Deluge BitTorrent WebUI";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/deluge-web \
+            ${optionalString (!isDeluge1) "--do-not-daemonize"} \
+            --config ${configDir} \
+            --port ${toString cfg.web.port}
+        '';
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+
+    networking.firewall = mkMerge [
+      (mkIf (cfg.declarative && cfg.openFirewall && !(cfg.config.random_port or true)) {
+        allowedTCPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
+        allowedUDPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
+      })
+      (mkIf (cfg.web.openFirewall) {
+        allowedTCPPorts = [ cfg.web.port ];
+      })
+    ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    users.users = mkIf (cfg.user == "deluge") {
+      deluge = {
+        group = cfg.group;
+        uid = config.ids.uids.deluge;
+        home = cfg.dataDir;
+        description = "Deluge Daemon user";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "deluge") {
+      deluge = {
+        gid = config.ids.gids.deluge;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/flexget.nix b/nixos/modules/services/torrent/flexget.nix
new file mode 100644
index 00000000000..e500e02d861
--- /dev/null
+++ b/nixos/modules/services/torrent/flexget.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.flexget;
+  pkg = pkgs.flexget;
+  ymlFile = pkgs.writeText "flexget.yml" ''
+    ${cfg.config}
+
+    ${optionalString cfg.systemScheduler "schedules: no"}
+'';
+  configFile = "${toString cfg.homeDir}/flexget.yml";
+in {
+  options = {
+    services.flexget = {
+      enable = mkEnableOption "Run FlexGet Daemon";
+
+      user = mkOption {
+        default = "deluge";
+        example = "some_user";
+        type = types.str;
+        description = "The user under which to run flexget.";
+      };
+
+      homeDir = mkOption {
+        default = "/var/lib/deluge";
+        example = "/home/flexget";
+        type = types.path;
+        description = "Where files live.";
+      };
+
+      interval = mkOption {
+        default = "10m";
+        example = "1h";
+        type = types.str;
+        description = "When to perform a <command>flexget</command> run. See <command>man 7 systemd.time</command> for the format.";
+      };
+
+      systemScheduler = mkOption {
+        default = true;
+        example = false;
+        type = types.bool;
+        description = "When true, execute the runs via the flexget-runner.timer. If false, you have to specify the settings yourself in the YML file.";
+      };
+
+      config = mkOption {
+        default = "";
+        type = types.lines;
+        description = "The YAML configuration for FlexGet.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkg ];
+
+    systemd.services = {
+      flexget = {
+        description = "FlexGet Daemon";
+        path = [ pkg ];
+        serviceConfig = {
+          User = cfg.user;
+          Environment = "TZ=${config.time.timeZone}";
+          ExecStartPre = "${pkgs.coreutils}/bin/install -m644 ${ymlFile} ${configFile}";
+          ExecStart = "${pkg}/bin/flexget -c ${configFile} daemon start";
+          ExecStop = "${pkg}/bin/flexget -c ${configFile} daemon stop";
+          ExecReload = "${pkg}/bin/flexget -c ${configFile} daemon reload";
+          Restart = "on-failure";
+          PrivateTmp = true;
+          WorkingDirectory = toString cfg.homeDir;
+        };
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      flexget-runner = mkIf cfg.systemScheduler {
+        description = "FlexGet Runner";
+        after = [ "flexget.service" ];
+        wants = [ "flexget.service" ];
+        serviceConfig = {
+          User = cfg.user;
+          ExecStart = "${pkg}/bin/flexget -c ${configFile} execute";
+          PrivateTmp = true;
+          WorkingDirectory = toString cfg.homeDir;
+        };
+      };
+    };
+
+    systemd.timers.flexget-runner = mkIf cfg.systemScheduler {
+      description = "Run FlexGet every ${cfg.interval}";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnBootSec = "5m";
+        OnUnitInactiveSec = cfg.interval;
+        Unit = "flexget-runner.service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/magnetico.nix b/nixos/modules/services/torrent/magnetico.nix
new file mode 100644
index 00000000000..3dd7b1ece76
--- /dev/null
+++ b/nixos/modules/services/torrent/magnetico.nix
@@ -0,0 +1,220 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.magnetico;
+
+  dataDir = "/var/lib/magnetico";
+
+  credFile = with cfg.web;
+    if credentialsFile != null
+      then credentialsFile
+      else pkgs.writeText "magnetico-credentials"
+        (concatStrings (mapAttrsToList
+          (user: hash: "${user}:${hash}\n")
+          cfg.web.credentials));
+
+  # default options in magneticod/main.go
+  dbURI = concatStrings
+    [ "sqlite3://${dataDir}/database.sqlite3"
+      "?_journal_mode=WAL"
+      "&_busy_timeout=3000"
+      "&_foreign_keys=true"
+    ];
+
+  crawlerArgs = with cfg.crawler; escapeShellArgs
+    ([ "--database=${dbURI}"
+       "--indexer-addr=${address}:${toString port}"
+       "--indexer-max-neighbors=${toString maxNeighbors}"
+       "--leech-max-n=${toString maxLeeches}"
+     ] ++ extraOptions);
+
+  webArgs = with cfg.web; escapeShellArgs
+    ([ "--database=${dbURI}"
+       (if (cfg.web.credentialsFile != null || cfg.web.credentials != { })
+         then "--credentials=${toString credFile}"
+         else "--no-auth")
+       "--addr=${address}:${toString port}"
+     ] ++ extraOptions);
+
+in {
+
+  ###### interface
+
+  options.services.magnetico = {
+    enable = mkEnableOption "Magnetico, Bittorrent DHT crawler";
+
+    crawler.address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      example = "1.2.3.4";
+      description = ''
+        Address to be used for indexing DHT nodes.
+      '';
+    };
+
+    crawler.port = mkOption {
+      type = types.port;
+      default = 0;
+      description = ''
+        Port to be used for indexing DHT nodes.
+        This port should be added to
+        <option>networking.firewall.allowedTCPPorts</option>.
+      '';
+    };
+
+    crawler.maxNeighbors = mkOption {
+      type = types.ints.positive;
+      default = 1000;
+      description = ''
+        Maximum number of simultaneous neighbors of an indexer.
+        Be careful changing this number: high values can very
+        easily cause your network to be congested or even crash
+        your router.
+      '';
+    };
+
+    crawler.maxLeeches = mkOption {
+      type = types.ints.positive;
+      default = 200;
+      description = ''
+        Maximum number of simultaneous leeches.
+      '';
+    };
+
+    crawler.extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra command line arguments to pass to magneticod.
+      '';
+    };
+
+    web.address = mkOption {
+      type = types.str;
+      default = "localhost";
+      example = "1.2.3.4";
+      description = ''
+        Address the web interface will listen to.
+      '';
+    };
+
+    web.port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = ''
+        Port the web interface will listen to.
+      '';
+    };
+
+    web.credentials = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = lib.literalExpression ''
+        {
+          myuser = "$2y$12$YE01LZ8jrbQbx6c0s2hdZO71dSjn2p/O9XsYJpz.5968yCysUgiaG";
+        }
+      '';
+      description = ''
+        The credentials to access the web interface, in case authentication is
+        enabled, in the format <literal>username:hash</literal>. If unset no
+        authentication will be required.
+
+        Usernames must start with a lowercase ([a-z]) ASCII character, might
+        contain non-consecutive underscores except at the end, and consists of
+        small-case a-z characters and digits 0-9.  The
+        <command>htpasswd</command> tool from the <package>apacheHttpd
+        </package> package may be used to generate the hash: <command>htpasswd
+        -bnBC 12 username password</command>
+
+        <warning>
+        <para>
+          The hashes will be stored world-readable in the nix store.
+          Consider using the <literal>credentialsFile</literal> option if you
+          don't want this.
+        </para>
+        </warning>
+      '';
+    };
+
+    web.credentialsFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        The path to the file holding the credentials to access the web
+        interface. If unset no authentication will be required.
+
+        The file must constain user names and password hashes in the format
+        <literal>username:hash </literal>, one for each line.  Usernames must
+        start with a lowecase ([a-z]) ASCII character, might contain
+        non-consecutive underscores except at the end, and consists of
+        small-case a-z characters and digits 0-9.
+        The <command>htpasswd</command> tool from the <package>apacheHttpd
+        </package> package may be used to generate the hash:
+        <command>htpasswd -bnBC 12 username password</command>
+      '';
+    };
+
+    web.extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra command line arguments to pass to magneticow.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.magnetico = {
+      description = "Magnetico daemons user";
+      group = "magnetico";
+      isSystemUser = true;
+    };
+    users.groups.magnetico = {};
+
+    systemd.services.magneticod = {
+      description = "Magnetico DHT crawler";
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+
+      serviceConfig = {
+        User      = "magnetico";
+        Restart   = "on-failure";
+        ExecStart = "${pkgs.magnetico}/bin/magneticod ${crawlerArgs}";
+      };
+    };
+
+    systemd.services.magneticow = {
+      description = "Magnetico web interface";
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" "magneticod.service"];
+
+      serviceConfig = {
+        User           = "magnetico";
+        StateDirectory = "magnetico";
+        Restart        = "on-failure";
+        ExecStart      = "${pkgs.magnetico}/bin/magneticow ${webArgs}";
+      };
+    };
+
+    assertions =
+    [
+      {
+        assertion = cfg.web.credentialsFile == null || cfg.web.credentials == { };
+        message = ''
+          The options services.magnetico.web.credentialsFile and
+          services.magnetico.web.credentials are mutually exclusives.
+        '';
+      }
+    ];
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/torrent/opentracker.nix b/nixos/modules/services/torrent/opentracker.nix
new file mode 100644
index 00000000000..d76d61dfe85
--- /dev/null
+++ b/nixos/modules/services/torrent/opentracker.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.opentracker;
+in {
+  options.services.opentracker = {
+    enable = mkEnableOption "opentracker";
+
+    package = mkOption {
+      type = types.package;
+      description = ''
+        opentracker package to use
+      '';
+      default = pkgs.opentracker;
+      defaultText = literalExpression "pkgs.opentracker";
+    };
+
+    extraOptions = mkOption {
+      type = types.separatedString " ";
+      description = ''
+        Configuration Arguments for opentracker
+        See https://erdgeist.org/arts/software/opentracker/ for all params
+      '';
+      default = "";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    systemd.services.opentracker = {
+      description = "opentracker server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartIfChanged = true;
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/opentracker ${cfg.extraOptions}";
+        PrivateTmp = true;
+        WorkingDirectory = "/var/empty";
+        # By default opentracker drops all privileges and runs in chroot after starting up as root.
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/torrent/peerflix.nix b/nixos/modules/services/torrent/peerflix.nix
new file mode 100644
index 00000000000..821c829f6b4
--- /dev/null
+++ b/nixos/modules/services/torrent/peerflix.nix
@@ -0,0 +1,71 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.peerflix;
+  opt = options.services.peerflix;
+
+  configFile = pkgs.writeText "peerflix-config.json" ''
+    {
+      "connections": 50,
+      "tmp": "${cfg.downloadDir}"
+    }
+  '';
+
+in {
+
+  ###### interface
+
+  options.services.peerflix = {
+    enable = mkOption {
+      description = "Whether to enable peerflix service.";
+      default = false;
+      type = types.bool;
+    };
+
+    stateDir = mkOption {
+      description = "Peerflix state directory.";
+      default = "/var/lib/peerflix";
+      type = types.path;
+    };
+
+    downloadDir = mkOption {
+      description = "Peerflix temporary download directory.";
+      default = "${cfg.stateDir}/torrents";
+      defaultText = literalExpression ''"''${config.${opt.stateDir}}/torrents"'';
+      type = types.path;
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - peerflix - - -"
+    ];
+
+    systemd.services.peerflix = {
+      description = "Peerflix Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment.HOME = cfg.stateDir;
+
+      preStart = ''
+        mkdir -p "${cfg.stateDir}"/{torrents,.config/peerflix-server}
+        ln -fs "${configFile}" "${cfg.stateDir}/.config/peerflix-server/config.json"
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkgs.nodePackages.peerflix-server}/bin/peerflix-server";
+        User = "peerflix";
+      };
+    };
+
+    users.users.peerflix = {
+      isSystemUser = true;
+      group = "peerflix";
+    };
+    users.groups.peerflix = {};
+  };
+}
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
new file mode 100644
index 00000000000..759dcfe2e6c
--- /dev/null
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -0,0 +1,211 @@
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rtorrent;
+  opt = options.services.rtorrent;
+
+in {
+  options.services.rtorrent = {
+    enable = mkEnableOption "rtorrent";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/rtorrent";
+      description = ''
+        The directory where rtorrent stores its data files.
+      '';
+    };
+
+    downloadDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/download";
+      defaultText = literalExpression ''"''${config.${opt.dataDir}}/download"'';
+      description = ''
+        Where to put downloaded files.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        User account under which rtorrent runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        Group under which rtorrent runs.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.rtorrent;
+      defaultText = literalExpression "pkgs.rtorrent";
+      description = ''
+        The rtorrent package to use.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 50000;
+      description = ''
+        The rtorrent port.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open the firewall for the port in <option>services.rtorrent.port</option>.
+      '';
+    };
+
+    rpcSocket = mkOption {
+      type = types.str;
+      readOnly = true;
+      default = "/run/rtorrent/rpc.sock";
+      description = ''
+        RPC socket path.
+      '';
+    };
+
+    configText = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        The content of <filename>rtorrent.rc</filename>. The <link xlink:href="https://rtorrent-docs.readthedocs.io/en/latest/cookbook.html#modernized-configuration-template">modernized configuration template</link> with the values specified in this module will be prepended using mkBefore. You can use mkForce to overwrite the config completly.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups = mkIf (cfg.group == "rtorrent") {
+      rtorrent = {};
+    };
+
+    users.users = mkIf (cfg.user == "rtorrent") {
+      rtorrent = {
+        group = cfg.group;
+        shell = pkgs.bashInteractive;
+        home = cfg.dataDir;
+        description = "rtorrent Daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
+
+    services.rtorrent.configText = mkBefore ''
+      # Instance layout (base paths)
+      method.insert = cfg.basedir, private|const|string, (cat,"${cfg.dataDir}/")
+      method.insert = cfg.watch,   private|const|string, (cat,(cfg.basedir),"watch/")
+      method.insert = cfg.logs,    private|const|string, (cat,(cfg.basedir),"log/")
+      method.insert = cfg.logfile, private|const|string, (cat,(cfg.logs),(system.time),".log")
+      method.insert = cfg.rpcsock, private|const|string, (cat,"${cfg.rpcSocket}")
+
+      # Create instance directories
+      execute.throw = sh, -c, (cat, "mkdir -p ", (cfg.basedir), "/session ", (cfg.watch), " ", (cfg.logs))
+
+      # Listening port for incoming peer traffic (fixed; you can also randomize it)
+      network.port_range.set = ${toString cfg.port}-${toString cfg.port}
+      network.port_random.set = no
+
+      # Tracker-less torrent and UDP tracker support
+      # (conservative settings for 'private' trackers, change for 'public')
+      dht.mode.set = disable
+      protocol.pex.set = no
+      trackers.use_udp.set = no
+
+      # Peer settings
+      throttle.max_uploads.set = 100
+      throttle.max_uploads.global.set = 250
+
+      throttle.min_peers.normal.set = 20
+      throttle.max_peers.normal.set = 60
+      throttle.min_peers.seed.set = 30
+      throttle.max_peers.seed.set = 80
+      trackers.numwant.set = 80
+
+      protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
+
+      # Limits for file handle resources, this is optimized for
+      # an `ulimit` of 1024 (a common default). You MUST leave
+      # a ceiling of handles reserved for rTorrent's internal needs!
+      network.http.max_open.set = 50
+      network.max_open_files.set = 600
+      network.max_open_sockets.set = 3000
+
+      # Memory resource usage (increase if you have a large number of items loaded,
+      # and/or the available resources to spend)
+      pieces.memory.max.set = 1800M
+      network.xmlrpc.size_limit.set = 4M
+
+      # Basic operational settings (no need to change these)
+      session.path.set = (cat, (cfg.basedir), "session/")
+      directory.default.set = "${cfg.downloadDir}"
+      log.execute = (cat, (cfg.logs), "execute.log")
+      ##log.xmlrpc = (cat, (cfg.logs), "xmlrpc.log")
+      execute.nothrow = sh, -c, (cat, "echo >", (session.path), "rtorrent.pid", " ", (system.pid))
+
+      # Other operational settings (check & adapt)
+      encoding.add = utf8
+      system.umask.set = 0027
+      system.cwd.set = (cfg.basedir)
+      network.http.dns_cache_timeout.set = 25
+      schedule2 = monitor_diskspace, 15, 60, ((close_low_diskspace, 1000M))
+
+      # Watch directories (add more as you like, but use unique schedule names)
+      #schedule2 = watch_start, 10, 10, ((load.start, (cat, (cfg.watch), "start/*.torrent")))
+      #schedule2 = watch_load, 11, 10, ((load.normal, (cat, (cfg.watch), "load/*.torrent")))
+
+      # Logging:
+      #   Levels = critical error warn notice info debug
+      #   Groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_*
+      print = (cat, "Logging to ", (cfg.logfile))
+      log.open_file = "log", (cfg.logfile)
+      log.add_output = "info", "log"
+      ##log.add_output = "tracker_debug", "log"
+
+      # XMLRPC
+      scgi_local = (cfg.rpcsock)
+      schedule = scgi_group,0,0,"execute.nothrow=chown,\":rtorrent\",(cfg.rpcsock)"
+      schedule = scgi_permission,0,0,"execute.nothrow=chmod,\"g+w,o=\",(cfg.rpcsock)"
+    '';
+
+    systemd = {
+      services = {
+        rtorrent = let
+          rtorrentConfigFile = pkgs.writeText "rtorrent.rc" cfg.configText;
+        in {
+          description = "rTorrent system service";
+          after = [ "network.target" ];
+          path = [ cfg.package pkgs.bash ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            Type = "simple";
+            Restart = "on-failure";
+            WorkingDirectory = cfg.dataDir;
+            ExecStartPre=''${pkgs.bash}/bin/bash -c "if test -e ${cfg.dataDir}/session/rtorrent.lock && test -z $(${pkgs.procps}/bin/pidof rtorrent); then rm -f ${cfg.dataDir}/session/rtorrent.lock; fi"'';
+            ExecStart="${cfg.package}/bin/rtorrent -n -o system.daemon.set=true -o import=${rtorrentConfigFile}";
+            RuntimeDirectory = "rtorrent";
+            RuntimeDirectoryMode = 755;
+          };
+        };
+      };
+
+      tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
new file mode 100644
index 00000000000..d12d8aa2398
--- /dev/null
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -0,0 +1,487 @@
+{ config, lib, pkgs, options, ... }:
+
+with lib;
+
+let
+  cfg = config.services.transmission;
+  opt = options.services.transmission;
+  inherit (config.environment) etc;
+  apparmor = config.security.apparmor;
+  rootDir = "/run/transmission";
+  settingsDir = ".config/transmission-daemon";
+  downloadsDir = "Downloads";
+  incompleteDir = ".incomplete";
+  watchDir = "watchdir";
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "settings.json" cfg.settings;
+in
+{
+  imports = [
+    (mkRenamedOptionModule ["services" "transmission" "port"]
+                           ["services" "transmission" "settings" "rpc-port"])
+    (mkAliasOptionModule ["services" "transmission" "openFirewall"]
+                         ["services" "transmission" "openPeerPorts"])
+  ];
+  options = {
+    services.transmission = {
+      enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
+
+        Transmission daemon can be controlled via the RPC interface using
+        transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
+        or other clients like stig or tremc.
+
+        Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
+        accessible to users in the "transmission" group'';
+
+      settings = mkOption {
+        description = ''
+          Settings whose options overwrite fields in
+          <literal>.config/transmission-daemon/settings.json</literal>
+          (each time the service starts).
+
+          See <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
+          for documentation of settings not explicitely covered by this module.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+          options.download-dir = mkOption {
+            type = types.path;
+            default = "${cfg.home}/${downloadsDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${downloadsDir}"'';
+            description = "Directory where to download torrents.";
+          };
+          options.incomplete-dir = mkOption {
+            type = types.path;
+            default = "${cfg.home}/${incompleteDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${incompleteDir}"'';
+            description = ''
+              When enabled with
+              services.transmission.home
+              <xref linkend="opt-services.transmission.settings.incomplete-dir-enabled"/>,
+              new torrents will download the files to this directory.
+              When complete, the files will be moved to download-dir
+              <xref linkend="opt-services.transmission.settings.download-dir"/>.
+            '';
+          };
+          options.incomplete-dir-enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = "";
+          };
+          options.message-level = mkOption {
+            type = types.ints.between 0 3;
+            default = 2;
+            description = "Set verbosity of transmission messages.";
+          };
+          options.peer-port = mkOption {
+            type = types.port;
+            default = 51413;
+            description = "The peer port to listen for incoming connections.";
+          };
+          options.peer-port-random-high = mkOption {
+            type = types.port;
+            default = 65535;
+            description = ''
+              The maximum peer port to listen to for incoming connections
+              when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
+            '';
+          };
+          options.peer-port-random-low = mkOption {
+            type = types.port;
+            default = 65535;
+            description = ''
+              The minimal peer port to listen to for incoming connections
+              when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
+            '';
+          };
+          options.peer-port-random-on-start = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Randomize the peer port.";
+          };
+          options.rpc-bind-address = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            example = "0.0.0.0";
+            description = ''
+              Where to listen for RPC connections.
+              Use \"0.0.0.0\" to listen on all interfaces.
+            '';
+          };
+          options.rpc-port = mkOption {
+            type = types.port;
+            default = 9091;
+            description = "The RPC port to listen to.";
+          };
+          options.script-torrent-done-enabled = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether to run
+              <xref linkend="opt-services.transmission.settings.script-torrent-done-filename"/>
+              at torrent completion.
+            '';
+          };
+          options.script-torrent-done-filename = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Executable to be run at torrent completion.";
+          };
+          options.umask = mkOption {
+            type = types.int;
+            default = 2;
+            description = ''
+              Sets transmission's file mode creation mask.
+              See the umask(2) manpage for more information.
+              Users who want their saved torrents to be world-writable
+              may want to set this value to 0.
+              Bear in mind that the json markup language only accepts numbers in base 10,
+              so the standard umask(2) octal notation "022" is written in settings.json as 18.
+            '';
+          };
+          options.utp-enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether to enable <link xlink:href="http://en.wikipedia.org/wiki/Micro_Transport_Protocol">Micro Transport Protocol (µTP)</link>.
+            '';
+          };
+          options.watch-dir = mkOption {
+            type = types.path;
+            default = "${cfg.home}/${watchDir}";
+            defaultText = literalExpression ''"''${config.${opt.home}}/${watchDir}"'';
+            description = "Watch a directory for torrent files and add them to transmission.";
+          };
+          options.watch-dir-enabled = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''Whether to enable the
+              <xref linkend="opt-services.transmission.settings.watch-dir"/>.
+            '';
+          };
+          options.trash-original-torrent-files = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''Whether to delete torrents added from the
+              <xref linkend="opt-services.transmission.settings.watch-dir"/>.
+            '';
+          };
+        };
+      };
+
+      downloadDirPermissions = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "770";
+        description = ''
+          If not <code>null</code>, is used as the permissions
+          set by <literal>systemd.activationScripts.transmission-daemon</literal>
+          on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>,
+          <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
+          and <xref linkend="opt-services.transmission.settings.watch-dir"/>.
+          Note that you may also want to change
+          <xref linkend="opt-services.transmission.settings.umask"/>.
+        '';
+      };
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/transmission";
+        description = ''
+          The directory where Transmission will create <literal>${settingsDir}</literal>.
+          as well as <literal>${downloadsDir}/</literal> unless
+          <xref linkend="opt-services.transmission.settings.download-dir"/> is changed,
+          and <literal>${incompleteDir}/</literal> unless
+          <xref linkend="opt-services.transmission.settings.incomplete-dir"/> is changed.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "transmission";
+        description = "User account under which Transmission runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "transmission";
+        description = "Group account under which Transmission runs.";
+      };
+
+      credentialsFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to a JSON file to be merged with the settings.
+          Useful to merge a file which is better kept out of the Nix store
+          to set secret config parameters like <code>rpc-password</code>.
+        '';
+        default = "/dev/null";
+        example = "/var/lib/secrets/transmission/settings.json";
+      };
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--log-debug" ];
+        description = ''
+          Extra flags passed to the transmission command in the service definition.
+        '';
+      };
+
+      openPeerPorts = mkEnableOption "opening of the peer port(s) in the firewall";
+
+      openRPCPort = mkEnableOption "opening of the RPC port in the firewall";
+
+      performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
+        to open many more connections at the same time.
+
+        Note that you may also want to increase
+        <code>peer-limit-global"</code>.
+        And be aware that these settings are quite aggressive
+        and might not suite your regular desktop use.
+        For instance, SSH sessions may time out more easily'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Note that using systemd.tmpfiles would not work here
+    # because it would fail when creating a directory
+    # with a different owner than its parent directory, by saying:
+    # Detected unsafe path transition /home/foo → /home/foo/Downloads during canonicalization of /home/foo/Downloads
+    # when /home/foo is not owned by cfg.user.
+    # Note also that using an ExecStartPre= wouldn't work either
+    # because BindPaths= needs these directories before.
+    system.activationScripts = mkIf (cfg.downloadDirPermissions != null)
+      { transmission-daemon = ''
+        install -d -m 700 '${cfg.home}/${settingsDir}'
+        chown -R '${cfg.user}:${cfg.group}' ${cfg.home}/${settingsDir}
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
+        '' + optionalString cfg.settings.incomplete-dir-enabled ''
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
+        '' + optionalString cfg.settings.watch-dir-enabled ''
+        install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
+        '';
+      };
+
+    systemd.services.transmission = {
+      description = "Transmission BitTorrent Service";
+      after = [ "network.target" ] ++ optional apparmor.enable "apparmor.service";
+      requires = optional apparmor.enable "apparmor.service";
+      wantedBy = [ "multi-user.target" ];
+      environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
+
+      serviceConfig = {
+        # Use "+" because credentialsFile may not be accessible to User= or Group=.
+        ExecStartPre = [("+" + pkgs.writeShellScript "transmission-prestart" ''
+          set -eu${lib.optionalString (cfg.settings.message-level >= 3) "x"}
+          ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' |
+          install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
+           '${cfg.home}/${settingsDir}/settings.json'
+        '')];
+        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f -g ${cfg.home}/${settingsDir} ${escapeShellArgs cfg.extraFlags}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = cfg.user;
+        Group = cfg.group;
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        # This is for BindPaths= and BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        # Using RootDirectory= makes it possible
+        # to use the same paths download-dir/incomplete-dir
+        # (which appear in user's interfaces) without requiring cfg.user
+        # to have access to their parent directories,
+        # by using BindPaths=/BindReadOnlyPaths=.
+        # Note that TemporaryFileSystem= could have been used instead
+        # but not without adding some BindPaths=/BindReadOnlyPaths=
+        # that would only be needed for ExecStartPre=,
+        # because RootDirectoryStartOnly=true would not help.
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        MountAPIVFS = true;
+        BindPaths =
+          [ "${cfg.home}/${settingsDir}"
+            cfg.settings.download-dir
+          ] ++
+          optional cfg.settings.incomplete-dir-enabled
+            cfg.settings.incomplete-dir ++
+          optional (cfg.settings.watch-dir-enabled && cfg.settings.trash-original-torrent-files)
+            cfg.settings.watch-dir;
+        BindReadOnlyPaths = [
+          # No confinement done of /nix/store here like in systemd-confinement.nix,
+          # an AppArmor profile is provided to get a confinement based upon paths and rights.
+          builtins.storeDir
+          "/etc"
+          "/run"
+          ] ++
+          optional (cfg.settings.script-torrent-done-enabled &&
+                    cfg.settings.script-torrent-done-filename != null)
+            cfg.settings.script-torrent-done-filename ++
+          optional (cfg.settings.watch-dir-enabled && !cfg.settings.trash-original-torrent-files)
+            cfg.settings.watch-dir;
+        StateDirectory = [
+          "transmission"
+          "transmission/.config/transmission-daemon"
+          "transmission/.incomplete"
+          "transmission/Downloads"
+          "transmission/watch-dir"
+        ];
+        StateDirectoryMode = mkDefault 750;
+        # The following options are only for optimizing:
+        # systemd-analyze security transmission
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        # ProtectHome=true would not allow BindPaths= to work accross /home,
+        # and ProtectHome=tmpfs would break statfs(),
+        # preventing transmission-daemon to report the available free space.
+        # However, RootDirectory= is used, so this is not a security concern
+        # since there would be nothing in /home but any BindPaths= wanted by the user.
+        ProtectHome = "read-only";
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        # AF_UNIX may become usable one day:
+        # https://github.com/transmission/transmission/issues/441
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall
+          # listed by perf stat -e 'syscalls:sys_enter_*' transmission-daemon -f
+          # in tests, and seem likely not necessary for transmission-daemon.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+          # In the @privileged group, but reached when querying infos through RPC (eg. with stig).
+          "quotactl"
+        ];
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    # It's useful to have transmission in path, e.g. for remote control
+    environment.systemPackages = [ pkgs.transmission ];
+
+    users.users = optionalAttrs (cfg.user == "transmission") ({
+      transmission = {
+        group = cfg.group;
+        uid = config.ids.uids.transmission;
+        description = "Transmission BitTorrent user";
+        home = cfg.home;
+      };
+    });
+
+    users.groups = optionalAttrs (cfg.group == "transmission") ({
+      transmission = {
+        gid = config.ids.gids.transmission;
+      };
+    });
+
+    networking.firewall = mkMerge [
+      (mkIf cfg.openPeerPorts (
+        if cfg.settings.peer-port-random-on-start
+        then
+          { allowedTCPPortRanges =
+              [ { from = cfg.settings.peer-port-random-low;
+                  to   = cfg.settings.peer-port-random-high;
+                }
+              ];
+            allowedUDPPortRanges =
+              [ { from = cfg.settings.peer-port-random-low;
+                  to   = cfg.settings.peer-port-random-high;
+                }
+              ];
+          }
+        else
+          { allowedTCPPorts = [ cfg.settings.peer-port ];
+            allowedUDPPorts = [ cfg.settings.peer-port ];
+          }
+      ))
+      (mkIf cfg.openRPCPort { allowedTCPPorts = [ cfg.settings.rpc-port ]; })
+    ];
+
+    boot.kernel.sysctl = mkMerge [
+      # Transmission uses a single UDP socket in order to implement multiple uTP sockets,
+      # and thus expects large kernel buffers for the UDP socket,
+      # https://trac.transmissionbt.com/browser/trunk/libtransmission/tr-udp.c?rev=11956.
+      # at least up to the values hardcoded here:
+      (mkIf cfg.settings.utp-enabled {
+        "net.core.rmem_max" = mkDefault "4194304"; # 4MB
+        "net.core.wmem_max" = mkDefault "1048576"; # 1MB
+      })
+      (mkIf cfg.performanceNetParameters {
+        # Increase the number of available source (local) TCP and UDP ports to 49151.
+        # Usual default is 32768 60999, ie. 28231 ports.
+        # Find out your current usage with: ss -s
+        "net.ipv4.ip_local_port_range" = mkDefault "16384 65535";
+        # Timeout faster generic TCP states.
+        # Usual default is 600.
+        # Find out your current usage with: watch -n 1 netstat -nptuo
+        "net.netfilter.nf_conntrack_generic_timeout" = mkDefault 60;
+        # Timeout faster established but inactive connections.
+        # Usual default is 432000.
+        "net.netfilter.nf_conntrack_tcp_timeout_established" = mkDefault 600;
+        # Clear immediately TCP states after timeout.
+        # Usual default is 120.
+        "net.netfilter.nf_conntrack_tcp_timeout_time_wait" = mkDefault 1;
+        # Increase the number of trackable connections.
+        # Usual default is 262144.
+        # Find out your current usage with: conntrack -C
+        "net.netfilter.nf_conntrack_max" = mkDefault 1048576;
+      })
+    ];
+
+    security.apparmor.policies."bin.transmission-daemon".profile = ''
+      include "${pkgs.transmission.apparmor}/bin.transmission-daemon"
+    '';
+    security.apparmor.includes."local/bin.transmission-daemon" = ''
+      r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
+
+      owner rw ${cfg.home}/${settingsDir}/**,
+      rw ${cfg.settings.download-dir}/**,
+      ${optionalString cfg.settings.incomplete-dir-enabled ''
+        rw ${cfg.settings.incomplete-dir}/**,
+      ''}
+      ${optionalString cfg.settings.watch-dir-enabled ''
+        r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
+      ''}
+      profile dirs {
+        rw ${cfg.settings.download-dir}/**,
+        ${optionalString cfg.settings.incomplete-dir-enabled ''
+          rw ${cfg.settings.incomplete-dir}/**,
+        ''}
+        ${optionalString cfg.settings.watch-dir-enabled ''
+          r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
+        ''}
+      }
+
+      ${optionalString (cfg.settings.script-torrent-done-enabled &&
+                        cfg.settings.script-torrent-done-filename != null) ''
+        # Stack transmission_directories profile on top of
+        # any existing profile for script-torrent-done-filename
+        # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
+        # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
+        px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
+      ''}
+    '';
+  };
+
+  meta.maintainers = with lib.maintainers; [ julm ];
+}