Nix configurations for my homelab

add modules for protonvpn and qbittorrent

yemou.pink b3d1e6be 5c0bfc42

verified
Changed files
+185 -3
lutea
modules
scripts
secrets
+2
lutea/config.nix
··· 24 24 ../modules/network-info.nix 25 25 ../modules/nix.nix 26 26 ../modules/printing.nix 27 + ../modules/qbittorrent.nix 27 28 ../modules/remote-builder.nix 28 29 ../modules/river.nix 29 30 ../modules/smartd.nix 30 31 ../modules/tools.nix 31 32 ../modules/typst.nix 32 33 ../modules/virtualbox.nix 34 + ../modules/vpn.nix 33 35 ]; 34 36 35 37 sops = {
+69
modules/qbittorrent.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + { 8 + environment.persistence."/data/persistent".directories = [ 9 + { 10 + directory = "/var/lib/qBittorrent"; 11 + mode = "0700"; 12 + user = config.services.qbittorrent.user; 13 + group = config.services.qbittorrent.group; 14 + } 15 + ]; 16 + 17 + # TODO: Make sure that the qbittorrent service only starts if the torrent interface is up 18 + 19 + systemd.services.protonvpn-qbittorrent-natpmp = { 20 + description = "Get a port and provide it to qBittorrent"; 21 + requires = [ 22 + "network-online.target" 23 + "qbittorrent.service" 24 + ]; 25 + wantedBy = [ "multi-user.target" ]; 26 + serviceConfig = { 27 + ExecStart = "${ 28 + pkgs.writeShellApplication { 29 + name = "protonvpn-natpmp"; 30 + runtimeInputs = with pkgs; [ 31 + curl 32 + gnugrep 33 + jq 34 + libnatpmp 35 + ]; 36 + text = builtins.readFile ../scripts/protonvpn-natpmp.sh; 37 + } 38 + }/bin/protonvpn-natpmp"; 39 + Restart = "on-failure"; 40 + }; 41 + }; 42 + 43 + services.qbittorrent = { 44 + enable = true; 45 + webuiPort = 8082; 46 + serverConfig = { 47 + LegalNotice.Accepted = true; 48 + BitTorrent.Session = { 49 + Interface = "vpnt"; 50 + InterfaceName = "vpnt"; 51 + TorrentContentLayout = "Subfolder"; 52 + }; 53 + Network.PortForwardingEnabled = false; 54 + Preferences = { 55 + General.StatusbarExternalIPDisplayed = true; 56 + WebUI = lib.mkMerge [ 57 + (lib.mkIf (config.networking.hostName == "lutea") { LocalHostAuth = false; }) 58 + (lib.mkIf (config.networking.hostName == "lily") { 59 + AuthSubnetWhitelistEnable = true; 60 + AuthSubnetWhitelist = [ 61 + config.garden.lutea.ipv4-local 62 + config.garden.lutea.netbird-ip 63 + ]; 64 + }) 65 + ]; 66 + }; 67 + }; 68 + }; 69 + }
+68
modules/vpn.nix
··· 1 + { config, ... }: 2 + { 3 + sops.secrets = { 4 + "protonvpn-torrent/private-key" = { 5 + sopsFile = ../secrets/lilu.yaml; 6 + owner = "systemd-network"; 7 + group = "systemd-network"; 8 + }; 9 + "protonvpn-torrent/public-key" = { 10 + sopsFile = ../secrets/lilu.yaml; 11 + owner = "systemd-network"; 12 + group = "systemd-network"; 13 + }; 14 + }; 15 + 16 + systemd.network = { 17 + networks."50-vpn-torrent" = { 18 + matchConfig.Name = "vpnt"; 19 + address = [ 20 + "2a07:b944::2:2/128" 21 + "10.2.0.2/32" 22 + ]; 23 + dns = [ 24 + "2a07:b944::2:1" 25 + "10.2.0.1" 26 + ]; 27 + routes = [ 28 + { Destination = "2a07:b944::2:1"; } 29 + { Destination = "10.2.0.1"; } 30 + { 31 + Destination = "::/0"; 32 + Table = 10; 33 + } 34 + { 35 + Destination = "0.0.0.0/0"; 36 + Table = 10; 37 + } 38 + ]; 39 + routingPolicyRules = [ 40 + { 41 + From = "2a07:b944::2:2"; 42 + Table = 10; 43 + } 44 + { 45 + From = "10.2.0.2"; 46 + Table = 10; 47 + } 48 + ]; 49 + }; 50 + netdevs."50-vpn-torrent" = { 51 + netdevConfig = { 52 + Kind = "wireguard"; 53 + Name = "vpnt"; 54 + }; 55 + wireguardConfig.PrivateKeyFile = config.sops.secrets."protonvpn-torrent/private-key".path; 56 + wireguardPeers = [ 57 + { 58 + PublicKeyFile = config.sops.secrets."protonvpn-torrent/public-key".path; 59 + Endpoint = "31.13.189.226:51820"; 60 + AllowedIPs = [ 61 + "0.0.0.0/0" 62 + "::/0" 63 + ]; 64 + } 65 + ]; 66 + }; 67 + }; 68 + }
+40
scripts/protonvpn-natpmp.sh
··· 1 + #!/bin/sh 2 + 3 + QBITTORRENT_URL="http://localhost:8082" 4 + 5 + while true 6 + do 7 + tcp_output=$(natpmpc -g 10.2.0.1 -a 0 1 tcp 60 | grep Mapped) || { 8 + printf '%s\n' "Error: failed to get TCP port" >&2 9 + exit 1 10 + } 11 + 12 + udp_output=$(natpmpc -g 10.2.0.1 -a 0 1 udp 60 | grep Mapped) || { 13 + printf '%s\n' "Error: failed to get UDP port" >&2 14 + exit 1 15 + } 16 + 17 + read -r _ _ _ tcp_port _ <<-EOF 18 + $tcp_output 19 + EOF 20 + 21 + read -r _ _ _ udp_port _ <<-EOF 22 + $udp_output 23 + EOF 24 + 25 + # NOTE: The port numbers should be the same i'm pretty sure but this is a little sanity check 26 + [ "$tcp_port" -eq "$udp_port" ] || { 27 + printf '%s\n' "Warning: TCP and UDP ports aren't the same" >&2 28 + } 29 + 30 + current_port=$(curl -sf ${QBITTORRENT_URL}/api/v2/app/preferences | jq .listen_port) 31 + 32 + [ "$current_port" -eq "$udp_port" ] || { 33 + printf '%s\n' "Port changed from $current_port -> $udp_port" 34 + curl -sfd "json={\"listen_port\":$udp_port}" "${QBITTORRENT_URL}/api/v2/app/setPreferences" 35 + } 36 + 37 + printf '%s\n' "Current Port: $udp_port" 38 + 39 + sleep 30 40 + done
+6 -3
secrets/lilu.yaml
··· 1 1 y6d-smtp: 2 2 user: ENC[AES256_GCM,data:IZK759k1/F6v,iv:Aj92dOU58OU1zCcCsKeaHzsvWePRo6s8sE5mMMwM4DM=,tag:1V12iaPqjroNBQfaJHlP5Q==,type:str] 3 3 pass: ENC[AES256_GCM,data:q6bhty/EUUYIV+VQ9ZLHNjODOqA=,iv:aJ2+ToXQGLmZtO06ZXBwa6OGt7qil/mSbBG4VI6muRU=,tag:zn4mzLC7+qh40lP07ZEzPQ==,type:str] 4 + protonvpn-torrent: 5 + public-key: ENC[AES256_GCM,data:sCLj4u46lr/ImHyFsgwXcw1UxlTfYYT3W6qKcs8NjISW8t9oNwAPQF71VaE=,iv:6edmr6kB0fIXSFlHaujZnE8Ug3M7n9rXIFQVBvYXwRs=,tag:Qovr/4CLdSL78p+E8G2fiw==,type:str] 6 + private-key: ENC[AES256_GCM,data:cNapKRzpeSJ2c972e8tTRAPwNx0RyHCf39YIUDisAIhYSPN9/zLON5iv4EU=,iv:JlDgAt2nM5YS0xGadfPvRb6c4hs0gX5KgZmBYzhMlfI=,tag:66faJtuinyfZsbVKfcUeFA==,type:str] 4 7 sops: 5 8 age: 6 9 - recipient: age1amaa55e7nusv904a9ucfvtnjlw4srtet42suehey6u3yc4t2xc5sdldepj ··· 21 24 cm43OGNYd1ZnbEM0NjVYY3ZOdi94Sk0Kn8jz57CaoCE3ceFv1TNsYdqW83sqxYiy 22 25 4X21omXCeqpRG5DC2QyAJQE/93lBhsHKIMCraNMaOycPlVQYdyTviA== 23 26 -----END AGE ENCRYPTED FILE----- 24 - lastmodified: "2025-08-05T22:10:59Z" 25 - mac: ENC[AES256_GCM,data:Bqymx8fJKDcpr8GfP3fK+PPNivhPCyBx6yiybje0MOwMP4Qrc06YeYyktvV0z0MvPqOf31FMXMYbwYjuWOgUPKCsFC9QGLcshAnD9qG7LxX+5PaonFCk0LAUW5NABluGSkTViM6wCywqoSUB9BC8xnw8kSrMO7yXzJghIr6rusw=,iv:BmOKCn+p14ZKSAsn+nDQYWlPZsJFJEjGMyKuz9d9IY4=,tag:CpVMaMywwLVQYKFMx5l6WA==,type:str] 27 + lastmodified: "2025-10-09T11:59:47Z" 28 + mac: ENC[AES256_GCM,data:utvW8XNWMuBrtKOfxLFFnXnTM8H/hYFO2pgKnNb+UZF6j36HnfcYb25hUiKLdQ791804LE3mmgR+evtcEl2YFV/8TDfwZTvlVKf6LDJtuYpcrlQYIIWrf6z6EjDwReZOW7my+PmgCNGTZ19mdtgXwSYQOGKX/PyUFEPxTwDCY8A=,iv:oGjRrn9f4aVRnlzIPfa1YthUrXXe7xRntcMjxsheOUI=,tag:p0Dira33uF1Tx2Rx+hiUZQ==,type:str] 26 29 unencrypted_suffix: _unencrypted 27 - version: 3.10.2 30 + version: 3.11.0