mermet: fail2ban: add intranet on the ignoreIP
[sourcephile-nix.git] / nixos / modules / services / torrent / transmission.nix
index e6ef83b659786fe016c4bfb09149ca92da7c1bf1..49c7de2648aacd4972d6198d93230ee1b6634849 100644 (file)
@@ -5,70 +5,165 @@ with lib;
 let
   cfg = config.services.transmission;
   inherit (config.environment) etc;
-  apparmor = config.security.apparmor.enable;
-  stateDir = "/var/lib/transmission";
-  # TODO: switch to configGen.json once RFC0042 is implemented
-  settingsFile = pkgs.writeText "settings.json" (builtins.toJSON (cfg.settings // {
-    download-dir = "${stateDir}/Downloads";
-    incomplete-dir = "${stateDir}/.incomplete";
-  }));
+  apparmor = config.security.apparmor;
+  rootDir = "/run/transmission";
   settingsDir = ".config/transmission-daemon";
-  makeAbsolute = base: path:
-    if builtins.match "^/.*" path == null
-    then base+"/"+path else path;
+  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 ''
-        Whether or not to enable the headless Transmission BitTorrent daemon.
+      enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
 
         Transmission daemon can be controlled via the RPC interface using
-        transmission-remote, the WebUI (http://${cfg.settings.rpc-bind-address}:${toString cfg.settings.rpc-port}/ by default),
+        transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
         or other clients like stig or tremc.
 
-        Torrents are downloaded to ${cfg.settings.download-dir} by default and are
-        accessible to users in the "transmission" group.
-      '';
+        Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
+        accessible to users in the "transmission" group'';
 
-      settings = mkOption rec {
-        # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
-        type = types.attrs;
-        apply = attrs:
-          let super = recursiveUpdate default attrs; in
-          super // {
-            download-dir   = makeAbsolute cfg.home super.download-dir;
-            incomplete-dir = makeAbsolute cfg.home super.incomplete-dir;
-          };
-        default =
-          {
-            download-dir = "${cfg.home}/Downloads";
-            incomplete-dir = "${cfg.home}/.incomplete";
-            incomplete-dir-enabled = true;
-            peer-port = 51413;
-            peer-port-random-high = 65535;
-            peer-port-random-low = 49152;
-            peer-port-random-on-start = false;
-            rpc-bind-address = "127.0.0.1";
-            rpc-port = 9091;
-            umask = 18; # 0o022 in decimal as expected by Transmission, obtained with: echo $((8#022))
-          };
-        example =
-          {
-            download-dir = "/srv/torrents/";
-            incomplete-dir = "/srv/torrents/.incomplete/";
-            incomplete-dir-enabled = true;
-            rpc-whitelist = "127.0.0.1,192.168.*.*";
-          };
+      settings = mkOption {
         description = ''
-          Attribute set whose fields overwrites fields in
+          Settings whose options overwrite fields in
           <literal>.config/transmission-daemon/settings.json</literal>
-          (each time the service starts). String values must be quoted, integer and
-          boolean values must not.
+          (each time the service starts).
 
-          See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
-          for documentation.
+          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}";
+            description = "Directory where to download torrents.";
+          };
+          options.incomplete-dir = mkOption {
+            type = types.path;
+            default = "${cfg.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 2;
+            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}";
+            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 {
@@ -76,30 +171,23 @@ in
         default = "770";
         example = "775";
         description = ''
-          The permissions set by the <literal>systemd-tmpfiles-setup</literal> service
-          on <link linkend="opt-services.transmission.settings">settings.download-dir</link>
-          and <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link>.
-        '';
-      };
-
-      port = mkOption {
-        type = types.port;
-        description = ''
-          TCP port number to run the RPC/web interface.
-
-          If instead you want to change the peer port,
-          use <link linkend="opt-services.transmission.settings">settings.peer-port</link>
-          or <link linkend="opt-services.transmission.settings">settings.peer-port-random-on-start</link>.
+          The permissions set by <literal>systemd.activationScripts.transmission-daemon</literal>
+          on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>
+          and <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
+          Note that you may also want to change
+          <xref linkend="opt-services.transmission.settings.umask"/>.
         '';
       };
 
       home = mkOption {
         type = types.path;
-        default = stateDir;
+        default = "/var/lib/transmission";
         description = ''
-          The directory where Transmission will create <literal>.config/transmission-daemon/</literal>.
-          as well as <literal>Downloads/</literal> unless <link linkend="opt-services.transmission.settings">settings.download-dir</link> is changed,
-          and <literal>.incomplete/</literal> unless <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link> is changed.
+          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.
         '';
       };
 
@@ -120,133 +208,150 @@ in
         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
-          because it contains sensible data like <link linkend="opt-services.transmission.settings">settings.rpc-password</link>.
+          because it contains sensible data like
+          <xref linkend="opt-services.transmission.settings.rpc-password"/>.
         '';
         default = "/dev/null";
         example = "/var/lib/secrets/transmission/settings.json";
       };
 
-      openFirewall = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether to automatically open the peer port(s) in the firewall.
-        '';
-      };
+      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
+        <xref linkend="opt-services.transmission.settings.peer-limit-global"/>.
+        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 {
-    systemd.tmpfiles.rules =
-      optional (cfg.home != stateDir) "d '${cfg.home}/${settingsDir}' 700 '${cfg.user}' '${cfg.group}' - -"
-      ++ [ "d '${cfg.settings.download-dir}' '${cfg.downloadDirPermissions}' '${cfg.user}' '${cfg.group}' - -" ]
-      ++ optional cfg.settings.incomplete-dir-enabled
-        "d '${cfg.settings.incomplete-dir}' '${cfg.downloadDirPermissions}' '${cfg.user}' '${cfg.group}' - -";
-
-    assertions = [
-      { assertion = builtins.match "^/.*" cfg.home != null;
-        message = "`services.transmission.home' must be an absolute path.";
-      }
-      { assertion = types.port.check cfg.settings.rpc-port;
-        message = "${toString cfg.settings.rpc-port} is not a valid port number for `services.transmission.settings.rpc-port`.";
-      }
-      # In case both port and settings.rpc-port are explicitely defined: they must be the same.
-      { assertion = !options.services.transmission.port.isDefined || cfg.port == cfg.settings.rpc-port;
-        message = "`services.transmission.port' is not equal to `services.transmission.settings.rpc-port'";
-      }
-    ];
-
-    services.transmission.settings =
-      optionalAttrs options.services.transmission.port.isDefined { rpc-port = cfg.port; };
+    # 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.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 "apparmor.service";
-      requires = optional apparmor "apparmor.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;
-      preStart = ''
-        set -eux
-        ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' >'${stateDir}/${settingsDir}/settings.json'
-      '';
 
       serviceConfig = {
-        WorkingDirectory = stateDir;
-        ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f";
+        # 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";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = cfg.user;
         Group = cfg.group;
-        StateDirectory = removePrefix "/var/lib/" stateDir + "/" + settingsDir;
-        StateDirectoryMode = "0700";
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
+        InaccessiblePaths = ["-+${rootDir}"];
+        # 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 =
-          optional (cfg.home != stateDir) "${cfg.home}/${settingsDir}:${stateDir}/${settingsDir}"
-          ++ [ "${cfg.settings.download-dir}:${stateDir}/Downloads" ]
-          ++ optional cfg.settings.incomplete-dir-enabled "${cfg.settings.incomplete-dir}:${stateDir}/.incomplete";
-        # The following options give:
+          [ "${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;
+        # The following options are only for optimizing:
         # systemd-analyze security transmission
-        # → Overall exposure level for transmission.service: 1.5 OK
         AmbientCapabilities = "";
         CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
         NoNewPrivileges = true;
         PrivateDevices = true;
         PrivateMounts = true;
-        PrivateNetwork = false;
+        PrivateNetwork = mkDefault false;
         PrivateTmp = true;
-        PrivateUsers = false;
+        PrivateUsers = true;
         ProtectClock = true;
         ProtectControlGroups = true;
-        ProtectHome = mkDefault 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 = mkDefault "strict";
-        ReadWritePaths = [ stateDir ];
+        ProtectSystem = "strict";
         RemoveIPC = true;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        # 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;
-        # In case transmission crashes with status=31/SYS,
-        # having systemd.coredump.enable = true
-        # and environment.enableDebugInfo = true
-        # enables to use coredumpctl debug to find the denied syscall.
         SystemCallFilter = [
-          "@default"
-          "@aio"
-          "@basic-io"
-          #"@chown"
-          #"@clock"
-          #"@cpu-emulation"
-          #"@debug"
-          "@file-system"
-          "@io-event"
-          #"@ipc"
-          #"@keyring"
-          #"@memlock"
-          #"@module"
-          #"@mount"
-          "@network-io"
-          #"@obsolete"
-          #"@pkey"
-          #"@privileged"
-            # Reached when querying infos through RPC (eg. with stig)
-            "quotactl"
-          "@process"
-          #"@raw-io"
-          #"@reboot"
-          #"@resources"
-          #"@setuid"
-          "@signal"
-          #"@swap"
-          "@sync"
           "@system-service"
-          "@timer"
+          # 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";
-        UMask = "0077";
+        SystemCallErrorNumber = "EPERM";
       };
     };
 
@@ -258,8 +363,7 @@ in
         group = cfg.group;
         uid = config.ids.uids.transmission;
         description = "Transmission BitTorrent user";
-        home = stateDir;
-        createHome = false;
+        home = cfg.home;
       };
     });
 
@@ -269,76 +373,106 @@ in
       };
     });
 
-    networking.firewall = mkIf cfg.openFirewall (
-      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 ];
-        }
-    );
-
-    security.apparmor.enforceProfiles = optional apparmor "bin/transmission-daemon";
-    security.apparmor.profiles."bin/transmission-daemon" = ''
-      #include <tunables/global>
-
-      /run/current-system/sw/bin/transmission-daemon {
-        #include <abstractions/base>
-        #include <abstractions/nameservice>
+    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 ]; })
+    ];
 
-        ${getLib pkgs.glibc}/lib/*.so*                   mr,
-        ${getLib pkgs.libevent}/lib/libevent*.so*        mr,
-        ${getLib pkgs.curl}/lib/libcurl*.so*             mr,
-        ${getLib pkgs.openssl}/lib/libssl*.so*           mr,
-        ${getLib pkgs.openssl}/lib/libcrypto*.so*        mr,
-        ${getLib pkgs.zlib}/lib/libz*.so*                mr,
-        ${getLib pkgs.libssh2}/lib/libssh2*.so*          mr,
-        ${getLib pkgs.systemd}/lib/libsystemd*.so*       mr,
-        ${getLib pkgs.xz}/lib/liblzma*.so*               mr,
-        ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so*      mr,
-        ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so* mr,
-        ${getLib pkgs.nghttp2}/lib/libnghttp2*.so*       mr,
-        ${getLib pkgs.c-ares}/lib/libcares*.so*          mr,
-        ${getLib pkgs.libcap}/lib/libcap*.so*            mr,
-        ${getLib pkgs.attr}/lib/libattr*.so*             mr,
-        ${getLib pkgs.lz4}/lib/liblz4*.so*               mr,
-        ${getLib pkgs.libkrb5}/lib/lib*.so*              mr,
-        ${getLib pkgs.keyutils}/lib/libkeyutils*.so*     mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so* mr,
-        ${getLib pkgs.gcc.cc.lib}/lib/libstdc++.so* mr,
-        ${getLib pkgs.gcc.cc.lib}/lib/libgcc_s.so* mr,
+    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;
+      })
+    ];
 
-        @{PROC}/sys/kernel/random/uuid   r,
-        @{PROC}/sys/vm/overcommit_memory r,
-        @{PROC}/@{pid}/environ r,
-        @{PROC}/@{pid}/mounts r,
-        /tmp/tr_session_id_* rwk,
+    security.apparmor.policies."bin.transmission-daemon".profile = ''
+        include <tunables/global>
+        ${pkgs.transmission}/bin/transmission-daemon {
+          include <abstractions/base>
+          include <abstractions/nameservice>
+          include <abstractions/ssl_certs>
+          include "${pkgs.apparmorRulesFromClosure {} [pkgs.transmission]}"
+          include <local/bin.transmission-daemon>
 
-        ${pkgs.openssl.out}/etc/** r,
-        ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE} r,
-        ${pkgs.transmission}/share/transmission/** r,
+          r @{PROC}/sys/kernel/random/uuid,
+          r @{PROC}/sys/vm/overcommit_memory,
+          r @{PROC}/@{pid}/environ,
+          r @{PROC}/@{pid}/mounts,
+          rwk /tmp/tr_session_id_*,
+          r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
+          r /run/systemd/resolve/stub-resolv.conf,
 
-        owner ${stateDir}/${settingsDir}/** rw,
+          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}/**,
+            ''}
+          }
 
-        ${stateDir}/Downloads/** rw,
-        ${optionalString cfg.settings.incomplete-dir-enabled ''
-          ${stateDir}/.incomplete/** rw,
-        ''}
-      }
+          ${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},
+          ''}
+        }
     '';
+    security.apparmor.includes."local/bin.transmission-daemon" = "";
   };
 
   meta.maintainers = with lib.maintainers; [ julm ];