diff --git a/machines/fuuko/configuration.nix b/machines/fuuko/configuration.nix index 5c6203f..98961b6 100644 --- a/machines/fuuko/configuration.nix +++ b/machines/fuuko/configuration.nix @@ -9,6 +9,7 @@ ./services/media.nix ./services/prometheus.nix ./services/scan.nix + ./services/torrent.nix ]; sbruder = { diff --git a/machines/fuuko/secrets/aria2-wireguard.nix b/machines/fuuko/secrets/aria2-wireguard.nix new file mode 100644 index 0000000..3fab7c1 Binary files /dev/null and b/machines/fuuko/secrets/aria2-wireguard.nix differ diff --git a/machines/fuuko/services/prometheus.nix b/machines/fuuko/services/prometheus.nix index 91acbcb..55e8387 100644 --- a/machines/fuuko/services/prometheus.nix +++ b/machines/fuuko/services/prometheus.nix @@ -66,6 +66,14 @@ in "vueko.vpn.sbruder.de:9100" ]; } + { + job_name = "aria2"; + static_configs = mkStaticTarget "127.0.0.1:9578"; + relabel_configs = lib.singleton { + target_label = "instance"; + replacement = "torrent.sbruder.de"; + }; + } { job_name = "fritzbox"; static_configs = mkStaticTarget "127.0.0.1:9133"; @@ -113,6 +121,11 @@ in for = "10m"; description = "Thinkpad T440’s ACPI temperature is broken. Its reported temperature is 48 °C for the last 10 minutes. That doesn’t seem right. Try suspending"; } + { + name = "TorrentNoPeers"; + expr = "sum by (instance) (aria2_torrent_peers) == 0"; + description = "Aria2 instance {{ $labels.instance }} has no peers. There might be a network connectivity problem"; + } ]; }; }) diff --git a/machines/fuuko/services/torrent.nix b/machines/fuuko/services/torrent.nix new file mode 100644 index 0000000..94b06fe --- /dev/null +++ b/machines/fuuko/services/torrent.nix @@ -0,0 +1,209 @@ +{ config, lib, pkgs, ... }: +let + homeDir = "/var/lib/aria2"; + downloadDir = "/data/torrent"; + sessionFile = "${homeDir}/session"; + + settings = { + # locations + dir = downloadDir; + + # logging + show-console-readout = false; + summary-interval = 0; + + # rpc + enable-rpc = true; + + # permanent queue + bt-load-saved-metadata = true; + bt-save-metadata = true; + force-save = true; + input-file = sessionFile; + save-session = sessionFile; + save-session-interval = 900; # automatic saving + + # network + async-dns-server = "193.138.218.74"; # aria2 does not respect netns resolv.conf + dht-listen-port = 54971; + listen-port = 54931; + interface = "wg-aria"; + + # limits + max-concurrent-downloads = 65536; + max-overall-download-limit = "6M"; + max-overall-upload-limit = "4M"; + seed-ratio = 0; # do not stop seeding after reaching ratio + }; + + toString' = value: + if lib.isBool value + then (if value then "true" else "false") + else (toString value); + + configFile = pkgs.writeText "aria2.conf" (lib.concatStringsSep + "\n" + (lib.mapAttrsToList + (k: v: "${k}=${toString' v}") + settings)); + + # Without this patch, the download and upload lengths are wrong after pausing + # and unpausing. I don’t patch this globally (in the overlay), since aria2 is + # in the user environment and would have to be rebuilt on slow machines. + aria2 = pkgs.aria2.overrideAttrs (o: o // { + patches = [ + (pkgs.fetchpatch { + url = "https://github.com/aria2/aria2/commit/6ebdddb9f159e87923736f25900897b3602305f9.diff"; + sha256 = "1j5mnafiv92xnzghh03hfdvpn9647vfdcipamnvi8hnzk3ak4mj7"; + }) + ]; + }); + + mkProxyService = socket: port: { + wantedBy = [ "multi-user.target" ]; + after = [ "wireguard-wg-aria.service" ]; + partOf = [ "wireguard-wg-aria.service" ]; + + serviceConfig = { + PrivateNetwork = true; + NetworkNamespacePath = "/run/netns/aria2"; + + Restart = "always"; + ExecStart = "${pkgs.socat}/bin/socat UNIX-LISTEN:${socket},fork,reuseaddr,mode=660 TCP:127.0.0.1:${toString port}"; + User = "aria2"; + Group = "nginx"; + + # systemd-analyze --no-pager security aria2-rpc-proxy.service + CapabilityBoundingSet = null; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectHome = true; + RestrictNamespaces = true; + SystemCallFilter = "@system-service"; + }; + }; +in +{ + users.users.aria2 = { + group = "aria2"; + uid = config.ids.uids.aria2; + home = homeDir; + }; + + users.groups.aria2.gid = config.ids.gids.aria2; + + systemd.tmpfiles.rules = [ + "d '${downloadDir}' 0775 aria2 users - -" + "d '${homeDir}' 0771 aria2 aria2 - -" + ]; + + krops.secrets.wg-aria-private-key = { }; + + networking.wireguard.interfaces.wg-aria = { + interfaceNamespace = "aria2"; + preSetup = "ip netns add aria2 && ip -n aria2 link set lo up"; + postShutdown = "ip netns del aria2"; + + privateKeyFile = config.krops.secrets.wg-aria-private-key.path; + } // (import ../secrets/aria2-wireguard.nix); # potentially sensitive data + + systemd.services.aria2 = { + description = "aria2 Service"; + after = [ "wireguard-wg-aria.service" ]; + requires = [ "wireguard-wg-aria.service" ]; + wantedBy = [ "multi-user.target" ]; + preStart = '' + if [[ ! -e "${sessionFile}" ]]; then + touch "${sessionFile}" + fi + ''; + + serviceConfig = { + PrivateNetwork = true; + NetworkNamespacePath = "/run/netns/aria2"; + + Restart = "always"; + ExecStart = "${aria2}/bin/aria2c --conf-path=${configFile}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + User = "aria2"; + Group = "aria2"; + + # systemd-analyze --no-pager security aria2.service + CapabilityBoundingSet = null; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectHome = true; + RestrictNamespaces = true; + SystemCallFilter = "@system-service"; + }; + }; + + systemd.services.aria2-rpc-proxy = mkProxyService "${homeDir}/rpc.sock" 6800; + + services.aria2_exporter = { + enable = true; + listenAddress = "localhost:9578"; + }; + + systemd.services.aria2_exporter = { + after = [ "wireguard-wg-aria.service" ]; + partOf = [ "wireguard-wg-aria.service" ]; + + serviceConfig = { + PrivateNetwork = true; + NetworkNamespacePath = "/run/netns/aria2"; + }; + }; + + systemd.services.aria2_exporter-proxy = mkProxyService "${homeDir}/metrics.sock" 9578; + + services.nginx.virtualHosts."torrent.sbruder.de" = { + enableACME = true; + forceSSL = true; + + # treated as state + basicAuthFile = "${homeDir}/htpasswd"; + + locations = { + "/" = { + root = toString (import + (pkgs.fetchzip { + url = "https://git.sbruder.de/simon/AriaNg/archive/51c53f298091a6a6b6077febc2bc9370acb47271.tar.gz"; + sha256 = "0f8j86l3fw71q1m00radlv2bg33l9jad0razyalvspzam3p3bsk1"; + }) + { inherit pkgs; }); + }; + "/jsonrpc" = { + proxyPass = "http://unix:${homeDir}/rpc.sock"; + proxyWebsockets = true; + }; + "/download/" = { + alias = "${downloadDir}/"; + extraConfig = '' + autoindex on; + ''; + }; + "=/metrics" = { + proxyPass = "http://unix:${homeDir}/metrics.sock"; + }; + }; + }; + + services.nginx.virtualHosts."aria2-metrics" = { + listen = lib.singleton { + addr = "127.0.0.1"; + port = 9578; + }; + + locations."=/metrics" = { + proxyPass = "http://unix:${homeDir}/metrics.sock"; + }; + }; + + environment.systemPackages = with pkgs; [ + aria2 + mktorrent + ]; +}