diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix index e9b5834dab4..779924a65a8 100644 --- a/nixos/modules/services/torrent/transmission.nix +++ b/nixos/modules/services/torrent/transmission.nix @@ -7,15 +7,20 @@ let inherit (config.environment) etc; apparmor = config.security.apparmor; rootDir = "/run/transmission"; - homeDir = "/var/lib/transmission"; settingsDir = ".config/transmission-daemon"; downloadsDir = "Downloads"; incompleteDir = ".incomplete"; watchDir = "watchdir"; - # TODO: switch to configGen.json once RFC0042 is implemented - settingsFile = pkgs.writeText "settings.json" (builtins.toJSON cfg.settings); + 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. @@ -24,48 +29,141 @@ in transmission-remote, the WebUI (http://127.0.0.1:9091/ by default), or other clients like stig or tremc. - Torrents are downloaded to ${homeDir}/${downloadsDir} by default and are + Torrents are downloaded to /${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 options overwrite 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 = 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 + , + new torrents will download the files to this directory. + When complete, the files will be moved to 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 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 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 + + 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 + . + ''; + }; + options.trash-original-torrent-files = mkOption { + type = types.bool; + default = false; + description = ''Whether to delete torrents added from the + . + ''; + }; + }; }; downloadDirPermissions = mkOption { @@ -74,31 +172,22 @@ in example = "775"; description = '' The permissions set by systemd.activationScripts.transmission-daemon - on the directories settings.download-dir - and settings.incomplete-dir. + on the directories + and . Note that you may also want to change - settings.umask. - ''; - }; - - 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; + default = "/var/lib/transmission"; description = '' The directory where Transmission will create ${settingsDir}. - as well as ${downloadsDir}/ unless settings.download-dir is changed, - and ${incompleteDir}/ unless settings.incomplete-dir is changed. + as well as ${downloadsDir}/ unless + is changed, + and ${incompleteDir}/ unless + is changed. ''; }; @@ -119,19 +208,22 @@ 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 settings.rpc-password. + because it contains sensible data like + . ''; default = "/dev/null"; 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. Note that you may also want to increase - 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''; @@ -152,36 +244,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.enable "apparmor.service"; @@ -226,11 +292,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. @@ -239,8 +303,10 @@ in "/run" ] ++ 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 = ""; @@ -307,25 +373,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, @@ -340,74 +409,57 @@ in # 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" = "16384 65535"; + "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" = 60; + "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" = 600; + "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" = 1; + "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" = 1048576; + "net.netfilter.nf_conntrack_max" = mkDefault 1048576; }) ]; security.apparmor.policies."bin.transmission-daemon".profile = '' - include - ${pkgs.transmission}/bin/transmission-daemon { - include - include - include - include "${pkgs.apparmorRulesFromClosure - { name = "transmission-daemon"; } - [ pkgs.transmission ]}" - include - - 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 /run/systemd/resolve/stub-resolv.conf, - - r ${pkgs.openssl.out}/etc/**, - 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 '' - rw ${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 '' - rw ${cfg.settings.watch-dir}/**, - ''} - } - - ${optionalString (cfg.settings.script-torrent-done-enabled && - cfg.settings.script-torrent-done-filename != "") '' - # 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}, - ''} - } + 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}, + ''} ''; - security.apparmor.includes."local/bin.transmission-daemon" = ""; }; meta.maintainers = with lib.maintainers; [ julm ]; diff --git a/pkgs/applications/networking/p2p/transmission/default.nix b/pkgs/applications/networking/p2p/transmission/default.nix index ab4fc0908ba..858b09c9aaa 100644 --- a/pkgs/applications/networking/p2p/transmission/default.nix +++ b/pkgs/applications/networking/p2p/transmission/default.nix @@ -20,6 +20,7 @@ , enableSystemd ? stdenv.isLinux , enableDaemon ? true , enableCli ? true +, apparmorRulesFromClosure }: let @@ -37,6 +38,8 @@ in stdenv.mkDerivation { fetchSubmodules = true; }; + outputs = [ "out" "apparmor" ]; + cmakeFlags = let mkFlag = opt: if opt then "ON" else "OFF"; @@ -72,6 +75,30 @@ in stdenv.mkDerivation { NIX_LDFLAGS = lib.optionalString stdenv.isDarwin "-framework CoreFoundation"; + postInstall = '' + install -D /dev/stdin $apparmor/bin.transmission-daemon < + $out/bin/transmission-daemon { + include + include + include + include "${apparmorRulesFromClosure { name = "transmission-daemon"; } ([ + curl libevent openssl pcre zlib + ] ++ lib.optionals enableSystemd [ systemd ] + ++ lib.optionals stdenv.isLinux [ inotify-tools ] + )}" + 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 /run/systemd/resolve/stub-resolv.conf, + + include + } + EOF + ''; + meta = { description = "A fast, easy and free BitTorrent client"; longDescription = ''