uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -0,0 +1,279 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.deluge;
cfg_web = config.services.deluge.web;
isDeluge1 = versionOlder cfg.package.version "2.0.0";
openFilesLimit = 4096;
listenPortsDefault = [ 6881 6889 ];
listToRange = x: { from = elemAt x 0; to = elemAt x 1; };
configDir = "${cfg.dataDir}/.config/deluge";
configFile = pkgs.writeText "core.conf" (builtins.toJSON cfg.config);
declarativeLockFile = "${configDir}/.declarative";
preStart = if cfg.declarative then ''
if [ -e ${declarativeLockFile} ]; then
# Was declarative before, no need to back up anything
${if isDeluge1 then "ln -sf" else "cp"} ${configFile} ${configDir}/core.conf
ln -sf ${cfg.authFile} ${configDir}/auth
else
# Declarative for the first time, backup stateful files
${if isDeluge1 then "ln -s" else "cp"} -b --suffix=.stateful ${configFile} ${configDir}/core.conf
ln -sb --suffix=.stateful ${cfg.authFile} ${configDir}/auth
echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
> ${declarativeLockFile}
fi
'' else ''
if [ -e ${declarativeLockFile} ]; then
rm ${declarativeLockFile}
fi
'';
in {
options = {
services = {
deluge = {
enable = mkEnableOption "Deluge daemon";
openFilesLimit = mkOption {
default = openFilesLimit;
type = types.either types.int types.str;
description = ''
Number of files to allow deluged to open.
'';
};
config = mkOption {
type = types.attrs;
default = {};
example = literalExpression ''
{
download_location = "/srv/torrents/";
max_upload_speed = "1000.0";
share_ratio_limit = "2.0";
allow_remote = true;
daemon_port = 58846;
listen_ports = [ ${toString listenPortsDefault} ];
}
'';
description = ''
Deluge core configuration for the core.conf file. Only has an effect
when <option>services.deluge.declarative</option> is set to
<literal>true</literal>. String values must be quoted, integer and
boolean values must not. See
<link xlink:href="https://git.deluge-torrent.org/deluge/tree/deluge/core/preferencesmanager.py#n41"/>
for the availaible options.
'';
};
declarative = mkOption {
type = types.bool;
default = false;
description = ''
Whether to use a declarative deluge configuration.
Only if set to <literal>true</literal>, the options
<option>services.deluge.config</option>,
<option>services.deluge.openFirewall</option> and
<option>services.deluge.authFile</option> will be
applied.
'';
};
openFirewall = mkOption {
default = false;
type = types.bool;
description = ''
Whether to open the firewall for the ports in
<option>services.deluge.config.listen_ports</option>. It only takes effet if
<option>services.deluge.declarative</option> is set to
<literal>true</literal>.
It does NOT apply to the daemon port nor the web UI port. To access those
ports secuerly check the documentation
<link xlink:href="https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient#CreateSSHTunnel"/>
or use a VPN or configure certificates for deluge.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/deluge";
description = ''
The directory where deluge will create files.
'';
};
authFile = mkOption {
type = types.path;
example = "/run/keys/deluge-auth";
description = ''
The file managing the authentication for deluge, the format of this
file is straightforward, each line contains a
username:password:level tuple in plaintext. It only has an effect
when <option>services.deluge.declarative</option> is set to
<literal>true</literal>.
See <link xlink:href="https://dev.deluge-torrent.org/wiki/UserGuide/Authentication"/> for
more informations.
'';
};
user = mkOption {
type = types.str;
default = "deluge";
description = ''
User account under which deluge runs.
'';
};
group = mkOption {
type = types.str;
default = "deluge";
description = ''
Group under which deluge runs.
'';
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [];
description = ''
Extra packages available at runtime to enable Deluge's plugins. For example,
extraction utilities are required for the built-in "Extractor" plugin.
This always contains unzip, gnutar, xz and bzip2.
'';
};
package = mkOption {
type = types.package;
example = literalExpression "pkgs.deluge-2_x";
description = ''
Deluge package to use.
'';
};
};
deluge.web = {
enable = mkEnableOption "Deluge Web daemon";
port = mkOption {
type = types.port;
default = 8112;
description = ''
Deluge web UI port.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for deluge web daemon
'';
};
};
};
};
config = mkIf cfg.enable {
services.deluge.package = mkDefault (
if versionAtLeast config.system.stateVersion "20.09" then
pkgs.deluge-2_x
else
# deluge-1_x is no longer packaged and this will resolve to an error
# thanks to the alias for this name. This is left here so that anyone
# using NixOS older than 20.09 receives that error when they upgrade
# and is forced to make an intentional choice to switch to deluge-2_x.
# That might be slightly inconvenient but there is no path to
# downgrade from 2.x to 1.x so NixOS should not automatically perform
# this state migration.
pkgs.deluge-1_x
);
# Provide a default set of `extraPackages`.
services.deluge.extraPackages = with pkgs; [ unzip gnutar xz bzip2 ];
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group}"
"d '${cfg.dataDir}/.config' 0770 ${cfg.user} ${cfg.group}"
"d '${cfg.dataDir}/.config/deluge' 0770 ${cfg.user} ${cfg.group}"
]
++ optional (cfg.config ? download_location)
"d '${cfg.config.download_location}' 0770 ${cfg.user} ${cfg.group}"
++ optional (cfg.config ? torrentfiles_location)
"d '${cfg.config.torrentfiles_location}' 0770 ${cfg.user} ${cfg.group}"
++ optional (cfg.config ? move_completed_path)
"d '${cfg.config.move_completed_path}' 0770 ${cfg.user} ${cfg.group}";
systemd.services.deluged = {
after = [ "network.target" ];
description = "Deluge BitTorrent Daemon";
wantedBy = [ "multi-user.target" ];
path = [ cfg.package ] ++ cfg.extraPackages;
serviceConfig = {
ExecStart = ''
${cfg.package}/bin/deluged \
--do-not-daemonize \
--config ${configDir}
'';
# To prevent "Quit & shutdown daemon" from working; we want systemd to
# manage it!
Restart = "on-success";
User = cfg.user;
Group = cfg.group;
UMask = "0002";
LimitNOFILE = cfg.openFilesLimit;
};
preStart = preStart;
};
systemd.services.delugeweb = mkIf cfg_web.enable {
after = [ "network.target" "deluged.service"];
requires = [ "deluged.service" ];
description = "Deluge BitTorrent WebUI";
wantedBy = [ "multi-user.target" ];
path = [ cfg.package ];
serviceConfig = {
ExecStart = ''
${cfg.package}/bin/deluge-web \
${optionalString (!isDeluge1) "--do-not-daemonize"} \
--config ${configDir} \
--port ${toString cfg.web.port}
'';
User = cfg.user;
Group = cfg.group;
};
};
networking.firewall = mkMerge [
(mkIf (cfg.declarative && cfg.openFirewall && !(cfg.config.random_port or true)) {
allowedTCPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
allowedUDPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
})
(mkIf (cfg.web.openFirewall) {
allowedTCPPorts = [ cfg.web.port ];
})
];
environment.systemPackages = [ cfg.package ];
users.users = mkIf (cfg.user == "deluge") {
deluge = {
group = cfg.group;
uid = config.ids.uids.deluge;
home = cfg.dataDir;
description = "Deluge Daemon user";
};
};
users.groups = mkIf (cfg.group == "deluge") {
deluge = {
gid = config.ids.gids.deluge;
};
};
};
}

View file

@ -0,0 +1,100 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.flexget;
pkg = pkgs.flexget;
ymlFile = pkgs.writeText "flexget.yml" ''
${cfg.config}
${optionalString cfg.systemScheduler "schedules: no"}
'';
configFile = "${toString cfg.homeDir}/flexget.yml";
in {
options = {
services.flexget = {
enable = mkEnableOption "Run FlexGet Daemon";
user = mkOption {
default = "deluge";
example = "some_user";
type = types.str;
description = "The user under which to run flexget.";
};
homeDir = mkOption {
default = "/var/lib/deluge";
example = "/home/flexget";
type = types.path;
description = "Where files live.";
};
interval = mkOption {
default = "10m";
example = "1h";
type = types.str;
description = "When to perform a <command>flexget</command> run. See <command>man 7 systemd.time</command> for the format.";
};
systemScheduler = mkOption {
default = true;
example = false;
type = types.bool;
description = "When true, execute the runs via the flexget-runner.timer. If false, you have to specify the settings yourself in the YML file.";
};
config = mkOption {
default = "";
type = types.lines;
description = "The YAML configuration for FlexGet.";
};
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkg ];
systemd.services = {
flexget = {
description = "FlexGet Daemon";
path = [ pkg ];
serviceConfig = {
User = cfg.user;
Environment = "TZ=${config.time.timeZone}";
ExecStartPre = "${pkgs.coreutils}/bin/install -m644 ${ymlFile} ${configFile}";
ExecStart = "${pkg}/bin/flexget -c ${configFile} daemon start";
ExecStop = "${pkg}/bin/flexget -c ${configFile} daemon stop";
ExecReload = "${pkg}/bin/flexget -c ${configFile} daemon reload";
Restart = "on-failure";
PrivateTmp = true;
WorkingDirectory = toString cfg.homeDir;
};
wantedBy = [ "multi-user.target" ];
};
flexget-runner = mkIf cfg.systemScheduler {
description = "FlexGet Runner";
after = [ "flexget.service" ];
wants = [ "flexget.service" ];
serviceConfig = {
User = cfg.user;
ExecStart = "${pkg}/bin/flexget -c ${configFile} execute";
PrivateTmp = true;
WorkingDirectory = toString cfg.homeDir;
};
};
};
systemd.timers.flexget-runner = mkIf cfg.systemScheduler {
description = "Run FlexGet every ${cfg.interval}";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnUnitInactiveSec = cfg.interval;
Unit = "flexget-runner.service";
};
};
};
}

View file

@ -0,0 +1,220 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.magnetico;
dataDir = "/var/lib/magnetico";
credFile = with cfg.web;
if credentialsFile != null
then credentialsFile
else pkgs.writeText "magnetico-credentials"
(concatStrings (mapAttrsToList
(user: hash: "${user}:${hash}\n")
cfg.web.credentials));
# default options in magneticod/main.go
dbURI = concatStrings
[ "sqlite3://${dataDir}/database.sqlite3"
"?_journal_mode=WAL"
"&_busy_timeout=3000"
"&_foreign_keys=true"
];
crawlerArgs = with cfg.crawler; escapeShellArgs
([ "--database=${dbURI}"
"--indexer-addr=${address}:${toString port}"
"--indexer-max-neighbors=${toString maxNeighbors}"
"--leech-max-n=${toString maxLeeches}"
] ++ extraOptions);
webArgs = with cfg.web; escapeShellArgs
([ "--database=${dbURI}"
(if (cfg.web.credentialsFile != null || cfg.web.credentials != { })
then "--credentials=${toString credFile}"
else "--no-auth")
"--addr=${address}:${toString port}"
] ++ extraOptions);
in {
###### interface
options.services.magnetico = {
enable = mkEnableOption "Magnetico, Bittorrent DHT crawler";
crawler.address = mkOption {
type = types.str;
default = "0.0.0.0";
example = "1.2.3.4";
description = ''
Address to be used for indexing DHT nodes.
'';
};
crawler.port = mkOption {
type = types.port;
default = 0;
description = ''
Port to be used for indexing DHT nodes.
This port should be added to
<option>networking.firewall.allowedTCPPorts</option>.
'';
};
crawler.maxNeighbors = mkOption {
type = types.ints.positive;
default = 1000;
description = ''
Maximum number of simultaneous neighbors of an indexer.
Be careful changing this number: high values can very
easily cause your network to be congested or even crash
your router.
'';
};
crawler.maxLeeches = mkOption {
type = types.ints.positive;
default = 200;
description = ''
Maximum number of simultaneous leeches.
'';
};
crawler.extraOptions = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Extra command line arguments to pass to magneticod.
'';
};
web.address = mkOption {
type = types.str;
default = "localhost";
example = "1.2.3.4";
description = ''
Address the web interface will listen to.
'';
};
web.port = mkOption {
type = types.port;
default = 8080;
description = ''
Port the web interface will listen to.
'';
};
web.credentials = mkOption {
type = types.attrsOf types.str;
default = {};
example = lib.literalExpression ''
{
myuser = "$2y$12$YE01LZ8jrbQbx6c0s2hdZO71dSjn2p/O9XsYJpz.5968yCysUgiaG";
}
'';
description = ''
The credentials to access the web interface, in case authentication is
enabled, in the format <literal>username:hash</literal>. If unset no
authentication will be required.
Usernames must start with a lowercase ([a-z]) ASCII character, might
contain non-consecutive underscores except at the end, and consists of
small-case a-z characters and digits 0-9. The
<command>htpasswd</command> tool from the <package>apacheHttpd
</package> package may be used to generate the hash: <command>htpasswd
-bnBC 12 username password</command>
<warning>
<para>
The hashes will be stored world-readable in the nix store.
Consider using the <literal>credentialsFile</literal> option if you
don't want this.
</para>
</warning>
'';
};
web.credentialsFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
The path to the file holding the credentials to access the web
interface. If unset no authentication will be required.
The file must constain user names and password hashes in the format
<literal>username:hash </literal>, one for each line. Usernames must
start with a lowecase ([a-z]) ASCII character, might contain
non-consecutive underscores except at the end, and consists of
small-case a-z characters and digits 0-9.
The <command>htpasswd</command> tool from the <package>apacheHttpd
</package> package may be used to generate the hash:
<command>htpasswd -bnBC 12 username password</command>
'';
};
web.extraOptions = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Extra command line arguments to pass to magneticow.
'';
};
};
###### implementation
config = mkIf cfg.enable {
users.users.magnetico = {
description = "Magnetico daemons user";
group = "magnetico";
isSystemUser = true;
};
users.groups.magnetico = {};
systemd.services.magneticod = {
description = "Magnetico DHT crawler";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = "magnetico";
Restart = "on-failure";
ExecStart = "${pkgs.magnetico}/bin/magneticod ${crawlerArgs}";
};
};
systemd.services.magneticow = {
description = "Magnetico web interface";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "magneticod.service"];
serviceConfig = {
User = "magnetico";
StateDirectory = "magnetico";
Restart = "on-failure";
ExecStart = "${pkgs.magnetico}/bin/magneticow ${webArgs}";
};
};
assertions =
[
{
assertion = cfg.web.credentialsFile == null || cfg.web.credentials == { };
message = ''
The options services.magnetico.web.credentialsFile and
services.magnetico.web.credentials are mutually exclusives.
'';
}
];
};
meta.maintainers = with lib.maintainers; [ rnhmjoj ];
}

View file

@ -0,0 +1,45 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.opentracker;
in {
options.services.opentracker = {
enable = mkEnableOption "opentracker";
package = mkOption {
type = types.package;
description = ''
opentracker package to use
'';
default = pkgs.opentracker;
defaultText = literalExpression "pkgs.opentracker";
};
extraOptions = mkOption {
type = types.separatedString " ";
description = ''
Configuration Arguments for opentracker
See https://erdgeist.org/arts/software/opentracker/ for all params
'';
default = "";
};
};
config = lib.mkIf cfg.enable {
systemd.services.opentracker = {
description = "opentracker server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartIfChanged = true;
serviceConfig = {
ExecStart = "${cfg.package}/bin/opentracker ${cfg.extraOptions}";
PrivateTmp = true;
WorkingDirectory = "/var/empty";
# By default opentracker drops all privileges and runs in chroot after starting up as root.
};
};
};
}

View file

@ -0,0 +1,71 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.peerflix;
opt = options.services.peerflix;
configFile = pkgs.writeText "peerflix-config.json" ''
{
"connections": 50,
"tmp": "${cfg.downloadDir}"
}
'';
in {
###### interface
options.services.peerflix = {
enable = mkOption {
description = "Whether to enable peerflix service.";
default = false;
type = types.bool;
};
stateDir = mkOption {
description = "Peerflix state directory.";
default = "/var/lib/peerflix";
type = types.path;
};
downloadDir = mkOption {
description = "Peerflix temporary download directory.";
default = "${cfg.stateDir}/torrents";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/torrents"'';
type = types.path;
};
};
###### implementation
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - peerflix - - -"
];
systemd.services.peerflix = {
description = "Peerflix Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment.HOME = cfg.stateDir;
preStart = ''
mkdir -p "${cfg.stateDir}"/{torrents,.config/peerflix-server}
ln -fs "${configFile}" "${cfg.stateDir}/.config/peerflix-server/config.json"
'';
serviceConfig = {
ExecStart = "${pkgs.nodePackages.peerflix-server}/bin/peerflix-server";
User = "peerflix";
};
};
users.users.peerflix = {
isSystemUser = true;
group = "peerflix";
};
users.groups.peerflix = {};
};
}

View file

@ -0,0 +1,211 @@
{ config, options, pkgs, lib, ... }:
with lib;
let
cfg = config.services.rtorrent;
opt = options.services.rtorrent;
in {
options.services.rtorrent = {
enable = mkEnableOption "rtorrent";
dataDir = mkOption {
type = types.str;
default = "/var/lib/rtorrent";
description = ''
The directory where rtorrent stores its data files.
'';
};
downloadDir = mkOption {
type = types.str;
default = "${cfg.dataDir}/download";
defaultText = literalExpression ''"''${config.${opt.dataDir}}/download"'';
description = ''
Where to put downloaded files.
'';
};
user = mkOption {
type = types.str;
default = "rtorrent";
description = ''
User account under which rtorrent runs.
'';
};
group = mkOption {
type = types.str;
default = "rtorrent";
description = ''
Group under which rtorrent runs.
'';
};
package = mkOption {
type = types.package;
default = pkgs.rtorrent;
defaultText = literalExpression "pkgs.rtorrent";
description = ''
The rtorrent package to use.
'';
};
port = mkOption {
type = types.port;
default = 50000;
description = ''
The rtorrent port.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Whether to open the firewall for the port in <option>services.rtorrent.port</option>.
'';
};
rpcSocket = mkOption {
type = types.str;
readOnly = true;
default = "/run/rtorrent/rpc.sock";
description = ''
RPC socket path.
'';
};
configText = mkOption {
type = types.lines;
default = "";
description = ''
The content of <filename>rtorrent.rc</filename>. The <link xlink:href="https://rtorrent-docs.readthedocs.io/en/latest/cookbook.html#modernized-configuration-template">modernized configuration template</link> with the values specified in this module will be prepended using mkBefore. You can use mkForce to overwrite the config completly.
'';
};
};
config = mkIf cfg.enable {
users.groups = mkIf (cfg.group == "rtorrent") {
rtorrent = {};
};
users.users = mkIf (cfg.user == "rtorrent") {
rtorrent = {
group = cfg.group;
shell = pkgs.bashInteractive;
home = cfg.dataDir;
description = "rtorrent Daemon user";
isSystemUser = true;
};
};
networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
services.rtorrent.configText = mkBefore ''
# Instance layout (base paths)
method.insert = cfg.basedir, private|const|string, (cat,"${cfg.dataDir}/")
method.insert = cfg.watch, private|const|string, (cat,(cfg.basedir),"watch/")
method.insert = cfg.logs, private|const|string, (cat,(cfg.basedir),"log/")
method.insert = cfg.logfile, private|const|string, (cat,(cfg.logs),(system.time),".log")
method.insert = cfg.rpcsock, private|const|string, (cat,"${cfg.rpcSocket}")
# Create instance directories
execute.throw = sh, -c, (cat, "mkdir -p ", (cfg.basedir), "/session ", (cfg.watch), " ", (cfg.logs))
# Listening port for incoming peer traffic (fixed; you can also randomize it)
network.port_range.set = ${toString cfg.port}-${toString cfg.port}
network.port_random.set = no
# Tracker-less torrent and UDP tracker support
# (conservative settings for 'private' trackers, change for 'public')
dht.mode.set = disable
protocol.pex.set = no
trackers.use_udp.set = no
# Peer settings
throttle.max_uploads.set = 100
throttle.max_uploads.global.set = 250
throttle.min_peers.normal.set = 20
throttle.max_peers.normal.set = 60
throttle.min_peers.seed.set = 30
throttle.max_peers.seed.set = 80
trackers.numwant.set = 80
protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
# Limits for file handle resources, this is optimized for
# an `ulimit` of 1024 (a common default). You MUST leave
# a ceiling of handles reserved for rTorrent's internal needs!
network.http.max_open.set = 50
network.max_open_files.set = 600
network.max_open_sockets.set = 3000
# Memory resource usage (increase if you have a large number of items loaded,
# and/or the available resources to spend)
pieces.memory.max.set = 1800M
network.xmlrpc.size_limit.set = 4M
# Basic operational settings (no need to change these)
session.path.set = (cat, (cfg.basedir), "session/")
directory.default.set = "${cfg.downloadDir}"
log.execute = (cat, (cfg.logs), "execute.log")
##log.xmlrpc = (cat, (cfg.logs), "xmlrpc.log")
execute.nothrow = sh, -c, (cat, "echo >", (session.path), "rtorrent.pid", " ", (system.pid))
# Other operational settings (check & adapt)
encoding.add = utf8
system.umask.set = 0027
system.cwd.set = (cfg.basedir)
network.http.dns_cache_timeout.set = 25
schedule2 = monitor_diskspace, 15, 60, ((close_low_diskspace, 1000M))
# Watch directories (add more as you like, but use unique schedule names)
#schedule2 = watch_start, 10, 10, ((load.start, (cat, (cfg.watch), "start/*.torrent")))
#schedule2 = watch_load, 11, 10, ((load.normal, (cat, (cfg.watch), "load/*.torrent")))
# Logging:
# Levels = critical error warn notice info debug
# Groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_*
print = (cat, "Logging to ", (cfg.logfile))
log.open_file = "log", (cfg.logfile)
log.add_output = "info", "log"
##log.add_output = "tracker_debug", "log"
# XMLRPC
scgi_local = (cfg.rpcsock)
schedule = scgi_group,0,0,"execute.nothrow=chown,\":rtorrent\",(cfg.rpcsock)"
schedule = scgi_permission,0,0,"execute.nothrow=chmod,\"g+w,o=\",(cfg.rpcsock)"
'';
systemd = {
services = {
rtorrent = let
rtorrentConfigFile = pkgs.writeText "rtorrent.rc" cfg.configText;
in {
description = "rTorrent system service";
after = [ "network.target" ];
path = [ cfg.package pkgs.bash ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Type = "simple";
Restart = "on-failure";
WorkingDirectory = cfg.dataDir;
ExecStartPre=''${pkgs.bash}/bin/bash -c "if test -e ${cfg.dataDir}/session/rtorrent.lock && test -z $(${pkgs.procps}/bin/pidof rtorrent); then rm -f ${cfg.dataDir}/session/rtorrent.lock; fi"'';
ExecStart="${cfg.package}/bin/rtorrent -n -o system.daemon.set=true -o import=${rtorrentConfigFile}";
RuntimeDirectory = "rtorrent";
RuntimeDirectoryMode = 755;
};
};
};
tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
};
};
}

View file

@ -0,0 +1,487 @@
{ config, lib, pkgs, options, ... }:
with lib;
let
cfg = config.services.transmission;
opt = options.services.transmission;
inherit (config.environment) etc;
apparmor = config.security.apparmor;
rootDir = "/run/transmission";
settingsDir = ".config/transmission-daemon";
downloadsDir = "Downloads";
incompleteDir = ".incomplete";
watchDir = "watchdir";
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.
Transmission daemon can be controlled via the RPC interface using
transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
or other clients like stig or tremc.
Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
accessible to users in the "transmission" group'';
settings = mkOption {
description = ''
Settings whose options overwrite fields in
<literal>.config/transmission-daemon/settings.json</literal>
(each time the service starts).
See <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
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}";
defaultText = literalExpression ''"''${config.${opt.home}}/${downloadsDir}"'';
description = "Directory where to download torrents.";
};
options.incomplete-dir = mkOption {
type = types.path;
default = "${cfg.home}/${incompleteDir}";
defaultText = literalExpression ''"''${config.${opt.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 3;
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}";
defaultText = literalExpression ''"''${config.${opt.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 {
type = with types; nullOr str;
default = null;
example = "770";
description = ''
If not <code>null</code>, is used as the permissions
set by <literal>systemd.activationScripts.transmission-daemon</literal>
on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>,
<xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
and <xref linkend="opt-services.transmission.settings.watch-dir"/>.
Note that you may also want to change
<xref linkend="opt-services.transmission.settings.umask"/>.
'';
};
home = mkOption {
type = types.path;
default = "/var/lib/transmission";
description = ''
The directory where Transmission will create <literal>${settingsDir}</literal>.
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.
'';
};
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.";
};
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
to set secret config parameters like <code>rpc-password</code>.
'';
default = "/dev/null";
example = "/var/lib/secrets/transmission/settings.json";
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [];
example = [ "--log-debug" ];
description = ''
Extra flags passed to the transmission command in the service definition.
'';
};
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
<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'';
};
};
config = mkIf cfg.enable {
# Note that using systemd.tmpfiles would not work here
# because it would fail when creating a directory
# with a different owner than its parent directory, by saying:
# Detected unsafe path transition /home/foo → /home/foo/Downloads during canonicalization of /home/foo/Downloads
# when /home/foo is not owned by cfg.user.
# Note also that using an ExecStartPre= wouldn't work either
# because BindPaths= needs these directories before.
system.activationScripts = mkIf (cfg.downloadDirPermissions != null)
{ transmission-daemon = ''
install -d -m 700 '${cfg.home}/${settingsDir}'
chown -R '${cfg.user}:${cfg.group}' ${cfg.home}/${settingsDir}
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}'
'';
};
systemd.services.transmission = {
description = "Transmission BitTorrent Service";
after = [ "network.target" ] ++ optional apparmor.enable "apparmor.service";
requires = optional apparmor.enable "apparmor.service";
wantedBy = [ "multi-user.target" ];
environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
serviceConfig = {
# Use "+" because credentialsFile may not be accessible to User= or Group=.
ExecStartPre = [("+" + pkgs.writeShellScript "transmission-prestart" ''
set -eu${lib.optionalString (cfg.settings.message-level >= 3) "x"}
${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' |
install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
'${cfg.home}/${settingsDir}/settings.json'
'')];
ExecStart="${pkgs.transmission}/bin/transmission-daemon -f -g ${cfg.home}/${settingsDir} ${escapeShellArgs cfg.extraFlags}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = cfg.user;
Group = cfg.group;
# Create rootDir in the host's mount namespace.
RuntimeDirectory = [(baseNameOf rootDir)];
RuntimeDirectoryMode = "755";
# This is for BindPaths= and BindReadOnlyPaths=
# to allow traversal of directories they create in RootDirectory=.
UMask = "0066";
# Using RootDirectory= makes it possible
# to use the same paths download-dir/incomplete-dir
# (which appear in user's interfaces) without requiring cfg.user
# to have access to their parent directories,
# by using BindPaths=/BindReadOnlyPaths=.
# Note that TemporaryFileSystem= could have been used instead
# but not without adding some BindPaths=/BindReadOnlyPaths=
# that would only be needed for ExecStartPre=,
# because RootDirectoryStartOnly=true would not help.
RootDirectory = rootDir;
RootDirectoryStartOnly = true;
MountAPIVFS = true;
BindPaths =
[ "${cfg.home}/${settingsDir}"
cfg.settings.download-dir
] ++
optional cfg.settings.incomplete-dir-enabled
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.
builtins.storeDir
"/etc"
"/run"
] ++
optional (cfg.settings.script-torrent-done-enabled &&
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;
StateDirectory = [
"transmission"
"transmission/.config/transmission-daemon"
"transmission/.incomplete"
"transmission/Downloads"
"transmission/watch-dir"
];
StateDirectoryMode = mkDefault 750;
# The following options are only for optimizing:
# systemd-analyze security transmission
AmbientCapabilities = "";
CapabilityBoundingSet = "";
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = mkDefault false;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
# ProtectHome=true would not allow BindPaths= to work accross /home,
# and ProtectHome=tmpfs would break statfs(),
# preventing transmission-daemon to report the available free space.
# However, RootDirectory= is used, so this is not a security concern
# since there would be nothing in /home but any BindPaths= wanted by the user.
ProtectHome = "read-only";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
# AF_UNIX may become usable one day:
# https://github.com/transmission/transmission/issues/441
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallFilter = [
"@system-service"
# Groups in @system-service which do not contain a syscall
# listed by perf stat -e 'syscalls:sys_enter_*' transmission-daemon -f
# in tests, and seem likely not necessary for transmission-daemon.
"~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
# In the @privileged group, but reached when querying infos through RPC (eg. with stig).
"quotactl"
];
SystemCallArchitectures = "native";
};
};
# 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 = cfg.home;
};
});
users.groups = optionalAttrs (cfg.group == "transmission") ({
transmission = {
gid = config.ids.gids.transmission;
};
});
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,
# and thus expects large kernel buffers for the UDP socket,
# https://trac.transmissionbt.com/browser/trunk/libtransmission/tr-udp.c?rev=11956.
# at least up to the values hardcoded here:
(mkIf cfg.settings.utp-enabled {
"net.core.rmem_max" = mkDefault "4194304"; # 4MB
"net.core.wmem_max" = mkDefault "1048576"; # 1MB
})
(mkIf cfg.performanceNetParameters {
# 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" = 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" = mkDefault 60;
# Timeout faster established but inactive connections.
# Usual default is 432000.
"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" = 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" = mkDefault 1048576;
})
];
security.apparmor.policies."bin.transmission-daemon".profile = ''
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},
''}
'';
};
meta.maintainers = with lib.maintainers; [ julm ];
}