diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix index 014a22bb5a8..6f6d5a05a2a 100644 --- a/nixos/modules/services/torrent/transmission.nix +++ b/nixos/modules/services/torrent/transmission.nix @@ -12,10 +12,15 @@ let downloadsDir = "Downloads"; incompleteDir = ".incomplete"; watchDir = "watchdir"; - # TODO: switch to configGen.json once RFC0042 is implemented settingsFile = pkgs.writeText "settings.json" (builtins.toJSON cfg.settings); in { + imports = [ + (mkRenamedOptionModule ["services" "transmission" "port"] + ["services" "transmission" "settings" "rpc-port"]) + (mkRenamedOptionModule ["services" "transmission" "openFirewall"] + ["services" "transmission" "openPeerPorts"]) + ]; options = { services.transmission = { enable = mkEnableOption ''the headless Transmission BitTorrent daemon. @@ -27,45 +32,140 @@ in Torrents are downloaded to ${homeDir}/${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 = recursiveUpdate default; - default = - { - download-dir = "${cfg.home}/${downloadsDir}"; - incomplete-dir = "${cfg.home}/${incompleteDir}"; - incomplete-dir-enabled = true; - watch-dir = "${cfg.home}/${watchDir}"; - watch-dir-enabled = false; - message-level = 1; - 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; - script-torrent-done-enabled = false; - script-torrent-done-filename = ""; - umask = 2; # 0o002 in decimal as expected by Transmission - utp-enabled = true; - }; - 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 fields overwrites fields in .config/transmission-daemon/settings.json - (each time the service starts). String values must be quoted, integer and - boolean values must not. + (each time the service starts). See Transmission's Wiki - for documentation. + for documentation of settings not explicitely covered by this module. ''; + default = {}; + type = types.submodule { + freeformType = with types; + (attrsOf (nullOr (oneOf [str int bool]))) // { + description = "setting option"; + }; + 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 + incomplete-dir-enabled, + new torrents will download the files to this directory. + When complete, the files will be moved to download-dir + 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 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 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 + 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 Micro Transport Protocol (µTP). + ''; + }; + 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 + watch-dir. + ''; + }; + options.trash-original-torrent-files = mkOption { + type = types.bool; + default = false; + description = ''Whether to delete torrents added from the + watch-dir. + ''; + }; + }; }; downloadDirPermissions = mkOption { @@ -81,17 +181,6 @@ in ''; }; - 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 settings.peer-port - or settings.peer-port-random-on-start. - ''; - }; - home = mkOption { type = types.path; default = homeDir; @@ -125,7 +214,9 @@ in example = "/var/lib/secrets/transmission/settings.json"; }; - openFirewall = mkEnableOption "opening of 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. @@ -152,36 +243,10 @@ in 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}' ''; - assertions = [ - { assertion = builtins.match "^/.*" cfg.home != null; - message = "`services.transmission.home' must be an absolute path."; - } - { assertion = types.path.check cfg.settings.download-dir; - message = "`services.transmission.settings.download-dir' must be an absolute path."; - } - { assertion = types.path.check cfg.settings.incomplete-dir; - message = "`services.transmission.settings.incomplete-dir' must be an absolute path."; - } - { assertion = types.path.check cfg.settings.watch-dir; - message = "`services.transmission.settings.watch-dir' must be an absolute path."; - } - { assertion = cfg.settings.script-torrent-done-filename == "" || types.path.check cfg.settings.script-torrent-done-filename; - message = "`services.transmission.settings.script-torrent-done-filename' 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"; @@ -226,11 +291,9 @@ in cfg.settings.download-dir ] ++ optional cfg.settings.incomplete-dir-enabled - cfg.settings.incomplete-dir - ++ - optional cfg.settings.watch-dir-enabled - cfg.settings.watch-dir - ; + 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. @@ -238,8 +301,10 @@ in "/etc" ] ++ optional (cfg.settings.script-torrent-done-enabled && - cfg.settings.script-torrent-done-filename != "") - cfg.settings.script-torrent-done-filename; + 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 AmbientCapabilities = ""; @@ -306,25 +371,28 @@ 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 ]; - } - ); + 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, @@ -419,7 +487,7 @@ in rw ${cfg.settings.incomplete-dir}/**, ''} ${optionalString cfg.settings.watch-dir-enabled '' - rw ${cfg.settings.watch-dir}/**, + r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**, ''} profile dirs { rw ${cfg.settings.download-dir}/**, @@ -427,12 +495,12 @@ in rw ${cfg.settings.incomplete-dir}/**, ''} ${optionalString cfg.settings.watch-dir-enabled '' - rw ${cfg.settings.watch-dir}/**, + 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 != "") '' + 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=