Setting up tetra and mogma

This commit is contained in:
Lyes Saadi 2026-01-15 03:01:34 +01:00
parent 0812b82c46
commit 34a686c562
Signed by: lyes
GPG key ID: 55A1D803917CF39A
12 changed files with 454 additions and 8 deletions

View file

@ -15,6 +15,6 @@ This repo uses flakes, which can construct the following targets:
- `zora` : For my servers hosted at the [Crans](https://crans.org/) - `zora` : For my servers hosted at the [Crans](https://crans.org/)
- `triforce` : For my custom ISO creation - `triforce` : For my custom ISO creation
Unless indicated otherwise by a comment (when code is taken from elsewhere), Unless indicated otherwise by a comment or file (when the code is taken from
everything in this repo is under the MIT-0 LICENSE. Meaning, you can copy elsewhere), everything in this repo is under the MIT-0 LICENSE. Meaning, you
everything here without any restriction (not even attribution). can copy everything here without any restriction (not even attribution).

View file

@ -1,6 +1,10 @@
{ ... }: { config, ... }:
{ {
imports = [
../../modules/server/mogma
];
# Networking # Networking
networking = { networking = {
hostName = "zora"; hostName = "zora";
@ -51,6 +55,51 @@
}; };
}; };
# VPN
networking.vpn-netns = {
wireguardInterface = "mogma";
nameserver = "10.2.0.1";
interfaceNamespace = "netns-mogma";
vethInterfaceName = "veth-mogma";
vethIP = "192.168.2.2";
vethOuterIP = "192.168.2.1";
wireguardOptions = {
privateKeyFile = config.age.secrets.mogma-privatekey.path;
ips = [ "10.2.0.2/32" ];
peers = [
{
publicKey = "W4XqVNXMdnhtiRxWNzWThy3f7hRoT9NTx/HYu/jTaRU=";
allowedIPs = [
"0.0.0.0/0"
"::/0"
];
endpoint = "79.127.169.89:51820";
persistentKeepalive = 25;
}
];
};
restrictedServices = [
"qbittorrent"
# "suwayomi-server"
];
portForwarding = {
enable = true;
};
};
age.secrets = {
mogma-privatekey = {
file = ../../secrets/zora/services/mogma-privatekey.age;
mode = "755";
};
};
# Imposing a bandwidth limit to avoid Aurore/Crans disruptions # Imposing a bandwidth limit to avoid Aurore/Crans disruptions
# networking.nftables = { # networking.nftables = {
# tables.rate_limit = { # tables.rate_limit = {

View file

@ -83,7 +83,15 @@
"torrent.lyes.eu" = { "torrent.lyes.eu" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
locations."/".proxyPass = "http://localhost:${toString config.services.qbittorrent.webuiPort}"; locations."/" = {
proxyPass = "http://${config.networking.vpn-netns.vethIP}:${toString config.services.qbittorrent.webuiPort}";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
}; };
# 9980 # 9980

View file

@ -6,6 +6,7 @@
- `link` : Kanidm (`auth.lyes.eu`) - `link` : Kanidm (`auth.lyes.eu`)
- `maistro` : Incus - `maistro` : Incus
- `mikau` : Jellyfin (`media.lyes.eu`) - `mikau` : Jellyfin (`media.lyes.eu`)
- `mogma` : VPN NetNS Configuration
- `nayru` : Komga/Manga (`manga.lyes.eu`) - `nayru` : Komga/Manga (`manga.lyes.eu`)
- `taf` : Mail (`taf.lyes.eu`/`mail.lyes.eu`) - `taf` : Mail (`taf.lyes.eu`/`mail.lyes.eu`)
- `tetra` : Torrent (`torrent.lyes.eu`) - `tetra` : Torrent (`torrent.lyes.eu`)

View file

@ -0,0 +1,26 @@
= VPN NetNS
== Credit
The modules here come from https://codeberg.org/loutr/dotfiles, and is thus
licensed under the AGPLv3.
== License
A collection of NixOS modules for various apps and services,
as well as their uses in host configurations.
Copyright (C) 2016-2021 Henrik Lissner
Copyright (C) 2021-2024 Lucas Tabary-Maujean
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View file

@ -0,0 +1,128 @@
{
lib,
config,
options,
...
}:
let
cfg = config.networking.vpn-netns;
in
{
imports = [
./encapsulation.nix
./forwarding.nix
];
options.networking.vpn-netns = with lib; {
restrictedServices = mkOption {
type = types.listOf types.str;
default = [ ];
description = "A list of valid systemd service names to be encapsulated in the vpn netns.";
};
nameserver = mkOption {
type = types.singleLineStr;
description = "The DNS server associated with the wireguard connection.";
};
wireguardInterface = mkOption {
type = types.str;
description = "The name of the wireguard interface.";
};
wireguardOptions = mkOption {
type = with types; submodule { freeformType = attrsOf anything; };
description = "Regular wireguard settings used to setup interface ${wgInterface}.";
};
interfaceNamespace = mkOption {
type = types.singleLineStr;
default = "vpn";
description = "The name of the encapsulating netns.";
};
vethInterfaceName = mkOption {
type = types.singleLineStr;
default = "vethvpn";
description = "The name of the veth interface accross netns.";
};
vethIP = mkOption {
type = types.singleLineStr;
default = "10.0.0.1";
description = "The veth IP address of encapsulated services";
};
vethOuterIP = mkOption {
type = types.singleLineStr;
default = "10.0.0.2";
description = "The veth IP address of non-encapsulated services.";
};
portForwarding = {
enable = mkEnableOption "a port forwarding service.";
leaseDuration = mkOption {
type = types.int;
default = 60;
description = "The NATPMP lease duration in seconds.";
};
updateDuration = mkOption {
type = types.int;
default = 2;
description = ''
How long the update script takes (in seconds).
This is substracted from the timer, so that leases do not get
interrupted. Tweak this based on your hardware performance etc.
'';
};
endpoint = mkOption {
type = types.singleLineStr;
default = cfg.nameserver;
description = "The VPN endpoint (with which to negotiate the lease).";
};
temporaryPortRange = mkOption {
type = options.networking.firewall.allowedUDPPortRanges.type.nestedTypes.elemType;
default = {
from = 30000;
to = 30010;
};
description = ''
The port range used for local port redirection. Make sure it doesn't
interfere with other services, including the assignable port from your
VPN provider.
'';
};
};
encapsulatedServices = mkOption {
default = { };
type =
with types;
attrsOf (submodule {
options.enable = mkEnableOption "network namespace encapsulation for this service.";
options.portForwarding = {
enable = mkEnableOption "port forwarding for this service.";
updateScript = mkOption {
type = str;
example = ''
echo listenPort=$PORT > /var/lib/service/config.conf
'';
description = ''
The script to apply everytime the forwarded port changes.
The shell has access to the `$PORT` variable with the corresponding
port. Be cautious, this script can perform arbitrary commands.
'';
};
};
});
};
};
}

View file

@ -0,0 +1,77 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.networking.vpn-netns;
netnsName = cfg.interfaceNamespace;
vethOuterName = cfg.vethInterfaceName;
vethEncapsulatedName = vethOuterName + "0";
outerIP = cfg.vethOuterIP + "/32";
encapsulatedIP = cfg.vethIP + "/32";
in
{
config = lib.mkIf (cfg.restrictedServices != [ ]) {
systemd.services = builtins.listToAttrs (
builtins.map (name: {
inherit name;
value = {
after = [ "wireguard.target" ];
# preStart = "sleep 3";
serviceConfig.NetworkNamespacePath = "/var/run/netns/${netnsName}";
};
}) cfg.restrictedServices
);
networking = {
wireguard.interfaces.${cfg.wireguardInterface} =
let
ip = "${pkgs.iproute2}/bin/ip";
in
{
preSetup = ''
# clean interfaces
${ip} netns delete "${netnsName}" 2> /dev/null || true
${ip} link delete ${vethOuterName} 2> /dev/null || true
rm -rf /etc/netns/${netnsName}
# add a namespace-specific resolv.conf
mkdir -p "/etc/netns/${netnsName}"
sed '/nameserver /d' /etc/resolv.conf > "/etc/netns/${netnsName}/resolv.conf"
echo "nameserver ${cfg.nameserver}" >> "/etc/netns/${netnsName}/resolv.conf"
# create the network namespace
${ip} netns add "${netnsName}"
${ip} -n "${netnsName}" link set lo up
# Add a custom link between netns
${ip} link add ${vethOuterName} type veth peer name ${vethEncapsulatedName}
${ip} link set ${vethEncapsulatedName} netns ${netnsName}
${ip} addr add ${outerIP} dev ${vethOuterName}
${ip} link set ${vethOuterName} up
${ip} route add ${encapsulatedIP} dev ${vethOuterName}
${ip} -n ${netnsName} addr add ${encapsulatedIP} dev ${vethEncapsulatedName}
${ip} -n ${netnsName} link set ${vethEncapsulatedName} up
${ip} -n ${netnsName} route add ${outerIP} dev ${vethEncapsulatedName}
'';
postShutdown = ''
${ip} netns delete "${netnsName}" || true
${ip} link delete ${vethOuterName} || true
rm -rf /etc/netns/${netnsName}
'';
inherit (cfg) interfaceNamespace;
}
// cfg.wireguardOptions;
firewall.trustedInterfaces = [ cfg.vethInterfaceName ];
};
};
}

View file

@ -0,0 +1,103 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.networking.vpn-netns;
inherit (cfg.portForwarding) leaseDuration updateDuration endpoint;
forwardedServices = lib.filterAttrs (
_: value: value.enable && value.portForwarding.enable
) cfg.encapsulatedServices;
assignCount =
{ acc, temporaryPort }:
name: value: {
acc = acc ++ [
{
inherit name temporaryPort;
inherit (value.portForwarding) updateScript;
}
];
temporaryPort = temporaryPort + 1;
};
forwardedServicesWithTmpPort = lib.foldlAttrs assignCount {
acc = [ ];
temporaryPort = cfg.portForwarding.temporaryPortRange.from;
} forwardedServices;
serviceList = lib.mapAttrsToList (name: _: name + ".service") forwardedServices;
in
lib.mkIf (forwardedServices != { } && cfg.portForwarding.enable) {
assertions = [
{
assertion =
forwardedServicesWithTmpPort.temporaryPort <= cfg.portForwarding.temporaryPortRange.to + 1;
message = ''
vpn forwarding: not enough temporary ports.
Increase the range of vpn.endpoint.temporaryPortRange.
'';
}
];
systemd = {
services.natpmpc-lease = {
description = "Request VPN port forwarding leases.";
wantedBy = serviceList;
after = [ "wireguard.target" ];
wants = [ "wireguard.target" ];
# preStart = "sleep 3";
path = with pkgs; [
libnatpmp
iptables
];
script = lib.concatMapStrings (
{
temporaryPort,
name,
updateScript,
}:
let
temporaryPort' = toString temporaryPort;
leaseDuration' = toString leaseDuration;
in
''
natpmpc -g ${endpoint} -a 1 ${temporaryPort'} udp ${leaseDuration'} > /dev/null
PORT=$(natpmpc -g ${endpoint} -a 1 ${temporaryPort'} tcp ${leaseDuration'} |
grep -oP "Mapped public port \K\d+")
iptables -t nat -C PREROUTING -p udp --dport ${temporaryPort'} -j REDIRECT --to-port "$PORT" 2>/dev/null || (
iptables -t nat -A PREROUTING -p tcp --dport ${temporaryPort'} -j REDIRECT --to-port "$PORT";
iptables -t nat -A PREROUTING -p udp --dport ${temporaryPort'} -j REDIRECT --to-port "$PORT"
)
echo "Changing settings for service ${name} with port $PORT, if needed"
${updateScript}
''
) forwardedServicesWithTmpPort.acc;
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = 60; # toString (2 * updateDuration);
NetworkNamespacePath = "/var/run/netns/${cfg.interfaceNamespace}";
};
};
timers.natpmpc-lease = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnUnitActiveSec = toString (leaseDuration - updateDuration);
Persistent = "yes";
};
};
};
}

View file

@ -1,8 +1,12 @@
{ ... }: { config, lib, pkgs, ... }:
{ {
environment.systemPackages = with pkgs; [
libnatpmp
];
services.qbittorrent = { services.qbittorrent = {
enable = false; enable = true;
user = "qbittorrent"; user = "qbittorrent";
group = "media"; group = "media";
@ -33,13 +37,47 @@
Username = "lyes"; Username = "lyes";
Password_PBKDF2 = "@ByteArray(5UU0KdjkWdtIdml1aQVDOQ==:qs0cVTkuQzbHA3EmF9++MK9eJstbx95hIR52amh2PSSgmQxrXavu0oxUZdUMWnaIRKkUuq18o9GV+DMb7T99NA==)"; Password_PBKDF2 = "@ByteArray(5UU0KdjkWdtIdml1aQVDOQ==:qs0cVTkuQzbHA3EmF9++MK9eJstbx95hIR52amh2PSSgmQxrXavu0oxUZdUMWnaIRKkUuq18o9GV+DMb7T99NA==)";
AuthSubnetWhitelistEnabled = true; AuthSubnetWhitelistEnabled = true;
# AuthSubnetWhitelist = "192.168.2.2/32"; AuthSubnetWhitelist = "192.168.2.2/32";
StatusbarExternalIPDisplayed = true; StatusbarExternalIPDisplayed = true;
}; };
}; };
}; };
}; };
networking.vpn-netns.encapsulatedServices.qbittorrent = {
enable = true;
portForwarding = {
enable = true;
updateScript =
let
configFile = "/var/lib/qbittorrent/qBittorrent/config/qBittorrent.conf";
passwordFile = config.age.secrets.tetra-pass.path;
apiSetPreferenceUrl = "http://${config.networking.vpn-netns.vethIP}:${toString config.services.qbittorrent.webuiPort}/api/v2/app/setPreferences";
curl = lib.getExe pkgs.curl;
ip = "${pkgs.iproute2}/bin/ip";
in
''
CURRENT_PORT=$(cat ${configFile} | grep 'Session\\Port' | cut -d '=' -f 2)
PASS=$(cat ${passwordFile})
test "$PORT" -eq "$CURRENT_PORT" || (
${ip} netns exec netns-mogma ${curl} -i -X POST -d "json={\"random_port\": false}" "${apiSetPreferenceUrl}"
${ip} netns exec netns-mogma ${curl} -i -X POST -d "json={\"listen_port\": $PORT}" "${apiSetPreferenceUrl}"
)
'';
};
};
age.secrets = {
tetra-pass = {
file = ../../../secrets/zora/services/tetra-pass.age;
mode = "770";
owner = "qbittorrent";
group = "media";
};
};
# users.users.qbittorrent.extraGroups = [ "media" ]; # users.users.qbittorrent.extraGroups = [ "media" ];
users.users.qbittorrent.isSystemUser = true; users.users.qbittorrent.isSystemUser = true;
users.users.qbittorrent.group = "media"; users.users.qbittorrent.group = "media";

View file

@ -22,4 +22,6 @@ in
"secrets/zora/services/biggoron-db-pass.age".publicKeys = all; "secrets/zora/services/biggoron-db-pass.age".publicKeys = all;
"secrets/zora/services/biggoron-admin-pass.age".publicKeys = all; "secrets/zora/services/biggoron-admin-pass.age".publicKeys = all;
"secrets/zora/services/ptigoron-token.age".publicKeys = all; "secrets/zora/services/ptigoron-token.age".publicKeys = all;
"secrets/zora/services/mogma-privatekey.age".publicKeys = all;
"secrets/zora/services/tetra-pass.age".publicKeys = all;
} }

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 whuRpQ UsWzTO1dXbNc3QQiQ3ucPSE+1tBSdYyLmxKH/wqOXUU
l1iyFn+HmgihWdv6/4pwnR/7tpRRZE2WyD9NOBYpqX8
-> ssh-ed25519 TFqgIg i7JnezH1VCIoZ689NVYTHsmFZHM6At3OADAg0a7hq0A
w8hxox6Ti+wY6mniY/sjjoOLTuI61TFXWsrHOB/vlkE
--- ckQG+2v4dN1rrpaNPci9mYjHl3Z7Boz4JAPB7c6EeBA
äõ%ld§ÞC»ûÎB¬OÎÝx¢—ÓVöqhÙ™rîîTÓRÁ±/9<!7¨á…7 A„« ÑÑ3ÙÉÜ>køîºà,k{É¿

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 whuRpQ 3poc5ONc1KrUFEJ5b+JJOl5DCJuKIqsCDgXsWJkTtU8
WRr4d9BwyPiY8iVZORHRhcwVPbBqASRMRDvxNfPewiY
-> ssh-ed25519 TFqgIg pb5iRBHs9Sru1cPtmZCAbZiRQFF+EnHdJKF3bnjNmwM
Ap7yRwNQDoY2XDGdruH08qTDU3iGCa+Dk71+d8nnPE8
--- srqiR5By2Q7OgTr9ciw10FFoD5pgISNc67O8wiNfTLc
ûq§!è?ÛË%$G<>ϨØ b>wGÉÕÜ*ÂuÛZ§Aƒ²yÿ-Ú>t|<7C>ª«<C2AA>/C<>Ù Ú»œ