transmission: improve the service
authorJulien Moutinho <julm@sourcephile.fr>
Wed, 1 Jul 2020 13:40:58 +0000 (15:40 +0200)
committerJulien Moutinho <julm@sourcephile.fr>
Sun, 5 Jul 2020 21:36:46 +0000 (23:36 +0200)
nixos/modules/services/torrent/transmission.nix
servers/losurdo/transmission.nix

index a916380ef1b139547e9f932f188efdf9fa3e4a2a..0085acb168d8c6be8a2a6e4340b88aa6a7f57411 100644 (file)
@@ -1,28 +1,22 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, options, ... }:
 
 with lib;
 
 let
   cfg = config.services.transmission;
   apparmor = config.security.apparmor.enable;
-  # TODO: switch to configGen.json once RFC42 is implemented
-  settingsFile = pkgs.writeText "settings.json" (builtins.toJSON cfg.settings);
   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";
+  }));
   settingsDir = ".config/transmission-daemon";
+  makeAbsolute = base: path:
+    if builtins.match "^/.*" path == null
+    then base+"/"+path else path;
 in
 {
-  imports = [
-    (mkRemovedOptionModule [ "services" "transmission" "port" ]
-      "Instead, use the option `services.transmission.settings.rpc-port'.")
-    (mkRemovedOptionModule [ "services" "transmission" "home" ]
-      "Instead, use systemd's StateDirectory: `${stateDir}'.")
-    (mkRemovedOptionModule [ "services" "transmission" "downloadDirPermissions" ] ''
-      Instead, use the option `services.transmission.settings.umask'
-      (which currently is: ${toString cfg.settings.umask}) (in decimal, as expected by Transmission)
-      and `systemd.services.transmission.serviceConfig.StateDirectoryMode'
-      (which currently is: ${config.systemd.services.transmission.serviceConfig.StateDirectoryMode}).
-    '')
-  ];
   options = {
     services.transmission = {
       enable = mkEnableOption ''
@@ -32,26 +26,32 @@ in
         transmission-remote, the WebUI (http://${cfg.settings.rpc-bind-address}:${toString cfg.settings.rpc-port}/ by default),
         or other clients like stig or tremc.
 
-        Torrents are downloaded to ${stateDir}/${cfg.settings.download-dir} by default and are
+        Torrents are downloaded to ${cfg.settings.download-dir} by default and are
         accessible to users in the "transmission" group.
       '';
 
       settings = mkOption rec {
-        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
+        # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
         type = types.attrs;
-        apply = recursiveUpdate default;
+        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 = "Downloads";
-            incomplete-dir = ".incomplete";
+            download-dir = "${cfg.home}/Downloads";
+            incomplete-dir = "${cfg.home}/.incomplete";
             incomplete-dir-enabled = true;
+            rpc-bind-address = "127.0.0.1";
             rpc-port = 9091;
-            umask = 63; # echo $((8#077))
+            umask = 63; # 0o077 in decimal as expected by Transmission, obtained with: echo $((8#077))
           };
         example =
           {
-            download-dir = "Torrents";
-            incomplete-dir = ".Torrents";
+            download-dir = "/srv/torrents/";
+            incomplete-dir = "/srv/torrents/.incomplete/";
             incomplete-dir-enabled = true;
             rpc-whitelist = "127.0.0.1,192.168.*.*";
           };
@@ -65,6 +65,31 @@ in
         '';
       };
 
+      downloadDirPermissions = mkOption {
+        type = types.str;
+        default = "770";
+        example = "775";
+        description = ''
+          The permissions set by the <literal>systemd-tmpfiles-setup</literal> service
+          on <literal>settings.download-dir</literal> and <literal>settings.incomplete-dir</literal>.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        description = "TCP port number to run the RPC/web interface.";
+      };
+
+      home = mkOption {
+        type = types.path;
+        default = stateDir;
+        description = ''
+          The directory where Transmission will create <literal>.config/transmission-daemon/</literal>.
+          as well as <literal>Downloads/</literal> unless <literal>settings.download-dir</literal> is changed,
+          and <literal>.incomplete/</literal> unless <literal>settings.incomplete-dir</literal> is changed.
+        '';
+      };
+
       user = mkOption {
         type = types.str;
         default = "transmission";
@@ -76,58 +101,95 @@ in
         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
+          because it contains sensible data like <literal>rpc-password</literal>.
+        '';
+        default = "/dev/null";
+        example = "/var/lib/secrets/transmission/settings.json";
+      };
+
+      enableSandbox = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Starting Transmission server with additional sandbox/hardening options.
+        '';
+      };
     };
   };
 
   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.settings.download-dir == null;
-        message = "`services.transmission.settings.download-dir' " +
-                  "can no longer be an absolute path, it must be relative to `${stateDir}'"; }
-      { assertion = builtins.match "^/.*" cfg.settings.incomplete-dir == null;
-        message = "`services.transmission.settings.incomplete-dir' " +
-                  "can no longer be an absolute path, it must be relative to `${stateDir}'"; }
+      { 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; };
+
     systemd.services.transmission = {
       description = "Transmission BitTorrent Service";
       after = [ "network.target" ] ++ optional apparmor "apparmor.service";
-      requires = mkIf apparmor [ "apparmor.service" ];
+      requires = optional apparmor "apparmor.service";
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        chmod 755 '${stateDir}'
-        chmod 0700 '${stateDir}/${settingsDir}'
-        cp -f ${settingsFile} ${settingsDir}/settings.json
+        set -eux
+        ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' >'${stateDir}/${settingsDir}/settings.json'
       '';
 
       serviceConfig = {
         WorkingDirectory = stateDir;
-        StateDirectory = concatMapStringsSep " " (p: "transmission/${p}")
-          ([ settingsDir cfg.settings.download-dir ]
-          ++ optional cfg.settings.incomplete-dir-enabled cfg.settings.incomplete-dir);
-        StateDirectoryMode = mkDefault "770";
-        ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f --config-dir ${settingsDir}";
+        ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = cfg.user;
         Group = cfg.group;
-        UMask = "0007";
-
-        # Hardening options
-        #DevicePolicy = "closed";
-        #LockPersonality = true;
-        #MemoryDenyWriteExecute = true;
-        #NoNewPrivileges = true;
-        #PrivateDevices = true;
-        #PrivateTmp = true;
-        #ProtectControlGroups = true;
-        #ProtectHome = true;
-        #ProtectKernelModules = true;
-        #ProtectKernelTunables = true;
-        #ProtectSystem = "strict";
-        #RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
-        #RestrictNamespaces = true;
-        #RestrictRealtime = true;
-        #RestrictSUIDSGID = true;
+        UMask = "0002";
+        StateDirectory = removePrefix "/var/lib/" stateDir + "/" + settingsDir;
+        StateDirectoryMode = "0700";
+        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";
+        NoNewPrivileges = true;
+        AmbientCapabilities   = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
+      } // optionalAttrs cfg.enableSandbox {
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectHome = mkDefault true;
+        ProtectHostname = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = mkDefault "strict";
+        ReadWritePaths = [ stateDir ];
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
       };
     };
 
@@ -140,7 +202,7 @@ in
         uid = config.ids.uids.transmission;
         description = "Transmission BitTorrent user";
         home = stateDir;
-        createHome = true;
+        createHome = false;
       };
     });
 
@@ -159,7 +221,6 @@ in
           #include <abstractions/base>
           #include <abstractions/nameservice>
 
-          ${getLib pkgs.gcc-unwrapped}/lib/*.so*           mr,
           ${getLib pkgs.glibc}/lib/*.so*                   mr,
           ${getLib pkgs.libevent}/lib/libevent*.so*        mr,
           ${getLib pkgs.curl}/lib/libcurl*.so*             mr,
@@ -181,6 +242,8 @@ in
           ${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,
 
           @{PROC}/sys/kernel/random/uuid   r,
           @{PROC}/sys/vm/overcommit_memory r,
@@ -190,9 +253,9 @@ in
 
           owner ${stateDir}/${settingsDir}/** rw,
 
-          ${stateDir}/${cfg.settings.download-dir}/** rw,
+          ${stateDir}/Downloads/** rw,
           ${optionalString cfg.settings.incomplete-dir-enabled ''
-            ${stateDir}/${cfg.settings.incomplete-dir}/** rw,
+            ${stateDir}/.incomplete/** rw,
           ''}
         }
       '')
index 76a69e59a88f49d1bc6788db755d37710677a817..fc8f70b6b6d66e82be4ba837b8be2ac83caa4718 100644 (file)
@@ -14,22 +14,33 @@ networking.nftables.ruleset = ''
 '';
 services.transmission = {
   enable = true;
+  enableSandbox = true;
   settings = {
     dht-enabled = true;
     download-dir = "Downloads";
     incomplete-dir-enabled = false;
+    preallocation = 0;
+    umask = 63;
+    
     peer-port = 6882;
     peer-port-random-on-start = false;
     port-forwarding-enabled = true;
-    preallocation = 0;
-    rpc-bind-address = "127.0.0.1";
+
+    speed-limit-up = 50;
+    speed-limit-up-enabled = true;
+    alt-speed-enabled = true;
+    alt-speed-time-enabled = true;
+    alt-speed-down = 0;
+    alt-speed-up = 5;
+    alt-speed-time-day = 127; # all days. 65; # weekend only
+    alt-speed-time-begin = 240; # 04h00 UTC
+    alt-speed-time-end = 1260; # 21h00 UTC
+
     rpc-enabled = true;
+    rpc-bind-address = "127.0.0.1";
     rpc-port = 9091;
     rpc-whitelist = "127.0.0.1";
     rpc-whitelist-enabled = true;
-    speed-limit-up = 10;
-    speed-limit-up-enabled = true;
-    umask = 63;
   };
 };
 }