{ config, lib, pkgs, ... }: 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"; settingsDir = ".config/transmission-daemon"; 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 '' Whether or not to enable 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), or other clients like stig or tremc. Torrents are downloaded to ${stateDir}/${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 type = types.attrs; apply = recursiveUpdate default; default = { download-dir = "Downloads"; incomplete-dir = ".incomplete"; incomplete-dir-enabled = true; rpc-port = 9091; umask = 63; # echo $((8#077)) }; example = { download-dir = "Torrents"; incomplete-dir = ".Torrents"; incomplete-dir-enabled = true; rpc-whitelist = "127.0.0.1,192.168.*.*"; }; description = '' Attribute set whose fields overwrites fields in settings.json (each time the service starts). String values must be quoted, integer and boolean values must not. See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files for documentation. ''; }; 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."; }; }; }; config = mkIf cfg.enable { 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}'"; } ]; systemd.services.transmission = { description = "Transmission BitTorrent Service"; after = [ "network.target" ] ++ optional apparmor "apparmor.service"; requires = mkIf apparmor [ "apparmor.service" ]; wantedBy = [ "multi-user.target" ]; preStart = '' chmod 755 '${stateDir}' chmod 0700 '${stateDir}/${settingsDir}' cp -f ${settingsFile} ${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}"; 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; }; }; # 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 = stateDir; createHome = true; }; }); users.groups = optionalAttrs (cfg.group == "transmission") ({ transmission = { gid = config.ids.gids.transmission; }; }); # AppArmor profile security.apparmor.profiles = mkIf apparmor [ (pkgs.writeText "apparmor-transmission-daemon" '' #include ${pkgs.transmission}/bin/transmission-daemon { #include #include ${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, ${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, @{PROC}/sys/kernel/random/uuid r, @{PROC}/sys/vm/overcommit_memory r, ${pkgs.openssl.out}/etc/** r, ${pkgs.transmission}/share/transmission/** r, owner ${stateDir}/${settingsDir}/** rw, ${stateDir}/${cfg.settings.download-dir}/** rw, ${optionalString cfg.settings.incomplete-dir-enabled '' ${stateDir}/${cfg.settings.incomplete-dir}/** rw, ''} } '') ]; }; }