+2
lutea/config.nix
+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
+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
+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
+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
+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