nixos/transmission: use freeformType on settings

This commit is contained in:
Julien Moutinho 2020-12-03 18:08:55 +01:00
parent ab8489cc40
commit 63e87892ff

View File

@ -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 <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 = 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
<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 <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
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
<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 {
@ -74,31 +172,22 @@ in
example = "775";
description = ''
The permissions set by <literal>systemd.activationScripts.transmission-daemon</literal>
on the directories <link linkend="opt-services.transmission.settings">settings.download-dir</link>
and <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link>.
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
<link linkend="opt-services.transmission.settings">settings.umask</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>.
<xref linkend="opt-services.transmission.settings.umask"/>.
'';
};
home = mkOption {
type = types.path;
default = homeDir;
default = "/var/lib/transmission";
description = ''
The directory where Transmission will create <literal>${settingsDir}</literal>.
as well as <literal>${downloadsDir}/</literal> unless <link linkend="opt-services.transmission.settings">settings.download-dir</link> is changed,
and <literal>${incompleteDir}/</literal> unless <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link> is changed.
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.
'';
};
@ -119,19 +208,21 @@ 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>.
to set secret config parameters like <code>rpc-password</code>.
'';
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
<link linkend="opt-services.transmission.settings">settings.peer-limit-global</link>.
<code>peer-limit-global"</code>.
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'';
@ -156,34 +247,6 @@ in
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";
@ -228,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.
@ -241,8 +302,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 = "";
@ -309,25 +372,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,
@ -342,21 +408,21 @@ 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;
})
];
@ -372,7 +438,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}/**,
@ -380,12 +446,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=