This commit is contained in:
@@ -51,6 +51,9 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
# Give the container more time to boot (pia-vpn-setup retries can delay readiness)
|
||||||
|
systemd.services."container@pia-vpn".serviceConfig.TimeoutStartSec = "180s";
|
||||||
|
|
||||||
containers.pia-vpn = {
|
containers.pia-vpn = {
|
||||||
autoStart = true;
|
autoStart = true;
|
||||||
ephemeral = true;
|
ephemeral = true;
|
||||||
@@ -64,169 +67,169 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
config = { config, pkgs, lib, ... }:
|
config = { config, pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
scriptPkgs = with pkgs; [ wireguard-tools iproute2 curl jq iptables coreutils ];
|
scriptPkgs = with pkgs; [ wireguard-tools iproute2 curl jq iptables coreutils ];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = allModules;
|
imports = allModules;
|
||||||
|
|
||||||
# Static IP on bridge — no gateway (VPN container routes via WG only)
|
# Static IP on bridge — no gateway (VPN container routes via WG only)
|
||||||
networking.useNetworkd = true;
|
networking.useNetworkd = true;
|
||||||
systemd.network.enable = true;
|
systemd.network.enable = true;
|
||||||
networking.useDHCP = false;
|
networking.useDHCP = false;
|
||||||
|
|
||||||
systemd.network.networks."20-eth0" = {
|
systemd.network.networks."20-eth0" = {
|
||||||
matchConfig.Name = "eth0";
|
matchConfig.Name = "eth0";
|
||||||
networkConfig = {
|
networkConfig = {
|
||||||
Address = "${cfg.vpnAddress}/${cfg.subnetPrefixLen}";
|
Address = "${cfg.vpnAddress}/${cfg.subnetPrefixLen}";
|
||||||
DHCPServer = false;
|
DHCPServer = false;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
# Ignore WG interface for wait-online (it's configured manually, not by networkd)
|
|
||||||
systemd.network.wait-online.ignoredInterfaces = [ cfg.interfaceName ];
|
|
||||||
|
|
||||||
# Enable forwarding so bridge traffic can go through WG
|
|
||||||
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
|
||||||
|
|
||||||
# Trust bridge interface
|
|
||||||
networking.firewall.trustedInterfaces = [ "eth0" ];
|
|
||||||
|
|
||||||
# DNS: use systemd-resolved listening on bridge IP so service containers
|
|
||||||
# can use VPN container as DNS server (queries go through WG tunnel = no DNS leak)
|
|
||||||
services.resolved = {
|
|
||||||
enable = true;
|
|
||||||
settings.Resolve.DNSStubListenerExtra = cfg.vpnAddress;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Don't use host resolv.conf — resolved manages DNS
|
|
||||||
networking.useHostResolvConf = false;
|
|
||||||
|
|
||||||
# State directory for PIA config files
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d /var/lib/pia-vpn 0700 root root -"
|
|
||||||
];
|
|
||||||
|
|
||||||
# PIA VPN setup service — does all the PIA auth, WG config, and NAT setup
|
|
||||||
systemd.services.pia-vpn-setup = {
|
|
||||||
description = "PIA VPN WireGuard Setup";
|
|
||||||
|
|
||||||
wants = [ "network-online.target" ];
|
|
||||||
after = [ "network.target" "network-online.target" "systemd-networkd.service" ];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
path = scriptPkgs;
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "simple";
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = "10s";
|
|
||||||
RuntimeMaxSec = "30d";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
script = ''
|
# Ignore WG interface for wait-online (it's configured manually, not by networkd)
|
||||||
set -euo pipefail
|
systemd.network.wait-online.ignoredInterfaces = [ cfg.interfaceName ];
|
||||||
${scripts.scriptCommon}
|
|
||||||
|
|
||||||
# Clean up stale state from previous attempts
|
# Enable forwarding so bridge traffic can go through WG
|
||||||
ip -4 address flush dev ${cfg.interfaceName} 2>/dev/null || true
|
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
||||||
ip route del default dev ${cfg.interfaceName} 2>/dev/null || true
|
|
||||||
iptables -t nat -F 2>/dev/null || true
|
|
||||||
iptables -F FORWARD 2>/dev/null || true
|
|
||||||
|
|
||||||
proxy="${proxy}"
|
# Trust bridge interface
|
||||||
|
networking.firewall.trustedInterfaces = [ "eth0" ];
|
||||||
|
|
||||||
# 1. Authenticate with PIA via proxy (VPN container has no internet yet)
|
# DNS: use systemd-resolved listening on bridge IP so service containers
|
||||||
echo "Choosing PIA server in region '${cfg.serverLocation}'..."
|
# can use VPN container as DNS server (queries go through WG tunnel = no DNS leak)
|
||||||
choosePIAServer '${cfg.serverLocation}'
|
services.resolved = {
|
||||||
|
enable = true;
|
||||||
|
settings.Resolve.DNSStubListenerExtra = cfg.vpnAddress;
|
||||||
|
};
|
||||||
|
|
||||||
echo "Fetching PIA authentication token..."
|
# Don't use host resolv.conf — resolved manages DNS
|
||||||
fetchPIAToken
|
networking.useHostResolvConf = false;
|
||||||
|
|
||||||
# 2. Generate WG keys and authorize with PIA server
|
# State directory for PIA config files
|
||||||
echo "Generating WireGuard keypair..."
|
systemd.tmpfiles.rules = [
|
||||||
generateWireguardKey
|
"d /var/lib/pia-vpn 0700 root root -"
|
||||||
|
];
|
||||||
|
|
||||||
echo "Authorizing key with PIA server $WG_HOSTNAME..."
|
# PIA VPN setup service — does all the PIA auth, WG config, and NAT setup
|
||||||
authorizeKeyWithPIAServer
|
systemd.services.pia-vpn-setup = {
|
||||||
|
description = "PIA VPN WireGuard Setup";
|
||||||
|
|
||||||
# 3. Configure WG interface (already created by host and moved into our namespace)
|
wants = [ "network-online.target" ];
|
||||||
echo "Configuring WireGuard interface ${cfg.interfaceName}..."
|
after = [ "network.target" "network-online.target" "systemd-networkd.service" ];
|
||||||
writeWireguardQuickFile '${wgFile}' ${toString cfg.wireguardListenPort}
|
wantedBy = [ "multi-user.target" ];
|
||||||
writeChosenServerToFile '${serverFile}'
|
|
||||||
connectToServer '${wgFile}' '${cfg.interfaceName}'
|
|
||||||
|
|
||||||
# 4. Default route through WG
|
path = scriptPkgs;
|
||||||
ip route add default dev ${cfg.interfaceName}
|
|
||||||
echo "Default route set through ${cfg.interfaceName}"
|
|
||||||
|
|
||||||
# 5. NAT: masquerade bridge → WG (so service containers' traffic appears to come from VPN IP)
|
serviceConfig = {
|
||||||
echo "Setting up NAT masquerade..."
|
Type = "simple";
|
||||||
iptables -t nat -A POSTROUTING -o ${cfg.interfaceName} -j MASQUERADE
|
Restart = "always";
|
||||||
iptables -A FORWARD -i eth0 -o ${cfg.interfaceName} -j ACCEPT
|
RestartSec = "10s";
|
||||||
iptables -A FORWARD -i ${cfg.interfaceName} -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
RuntimeMaxSec = "30d";
|
||||||
|
};
|
||||||
|
|
||||||
${optionalString portForwarding ''
|
script = ''
|
||||||
# 6. Port forwarding setup
|
set -euo pipefail
|
||||||
echo "Reserving port forward..."
|
${scripts.scriptCommon}
|
||||||
reservePortForward
|
|
||||||
writePortRenewalFile '${portRenewalFile}'
|
|
||||||
|
|
||||||
# First bindPort triggers actual port allocation
|
# Clean up stale state from previous attempts
|
||||||
echo "Binding port $PORT..."
|
ip -4 address flush dev ${cfg.interfaceName} 2>/dev/null || true
|
||||||
|
ip route del default dev ${cfg.interfaceName} 2>/dev/null || true
|
||||||
|
iptables -t nat -F 2>/dev/null || true
|
||||||
|
iptables -F FORWARD 2>/dev/null || true
|
||||||
|
|
||||||
|
proxy="${proxy}"
|
||||||
|
|
||||||
|
# 1. Authenticate with PIA via proxy (VPN container has no internet yet)
|
||||||
|
echo "Choosing PIA server in region '${cfg.serverLocation}'..."
|
||||||
|
choosePIAServer '${cfg.serverLocation}'
|
||||||
|
|
||||||
|
echo "Fetching PIA authentication token..."
|
||||||
|
fetchPIAToken
|
||||||
|
|
||||||
|
# 2. Generate WG keys and authorize with PIA server
|
||||||
|
echo "Generating WireGuard keypair..."
|
||||||
|
generateWireguardKey
|
||||||
|
|
||||||
|
echo "Authorizing key with PIA server $WG_HOSTNAME..."
|
||||||
|
authorizeKeyWithPIAServer
|
||||||
|
|
||||||
|
# 3. Configure WG interface (already created by host and moved into our namespace)
|
||||||
|
echo "Configuring WireGuard interface ${cfg.interfaceName}..."
|
||||||
|
writeWireguardQuickFile '${wgFile}' ${toString cfg.wireguardListenPort}
|
||||||
|
writeChosenServerToFile '${serverFile}'
|
||||||
|
connectToServer '${wgFile}' '${cfg.interfaceName}'
|
||||||
|
|
||||||
|
# 4. Default route through WG
|
||||||
|
ip route replace default dev ${cfg.interfaceName}
|
||||||
|
echo "Default route set through ${cfg.interfaceName}"
|
||||||
|
|
||||||
|
# 5. NAT: masquerade bridge → WG (so service containers' traffic appears to come from VPN IP)
|
||||||
|
echo "Setting up NAT masquerade..."
|
||||||
|
iptables -t nat -A POSTROUTING -o ${cfg.interfaceName} -j MASQUERADE
|
||||||
|
iptables -A FORWARD -i eth0 -o ${cfg.interfaceName} -j ACCEPT
|
||||||
|
iptables -A FORWARD -i ${cfg.interfaceName} -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||||
|
|
||||||
|
${optionalString portForwarding ''
|
||||||
|
# 6. Port forwarding setup
|
||||||
|
echo "Reserving port forward..."
|
||||||
|
reservePortForward
|
||||||
|
writePortRenewalFile '${portRenewalFile}'
|
||||||
|
|
||||||
|
# First bindPort triggers actual port allocation
|
||||||
|
echo "Binding port $PORT..."
|
||||||
|
refreshPIAPort
|
||||||
|
|
||||||
|
echo "PIA assigned port: $PORT"
|
||||||
|
|
||||||
|
# DNAT rules to forward PIA port to target container
|
||||||
|
${dnatSetupScript}
|
||||||
|
''}
|
||||||
|
|
||||||
|
echo "PIA VPN setup complete"
|
||||||
|
exec sleep infinity
|
||||||
|
'';
|
||||||
|
|
||||||
|
preStop = ''
|
||||||
|
echo "Tearing down PIA VPN..."
|
||||||
|
ip -4 address flush dev ${cfg.interfaceName} 2>/dev/null || true
|
||||||
|
ip route del default dev ${cfg.interfaceName} 2>/dev/null || true
|
||||||
|
iptables -t nat -F POSTROUTING 2>/dev/null || true
|
||||||
|
iptables -F FORWARD 2>/dev/null || true
|
||||||
|
${optionalString portForwarding ''
|
||||||
|
iptables -t nat -F PREROUTING 2>/dev/null || true
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Port refresh timer (every 10 min) — keeps PIA port forwarding alive
|
||||||
|
systemd.services.pia-vpn-port-refresh = mkIf portForwarding {
|
||||||
|
description = "PIA VPN Port Forward Refresh";
|
||||||
|
after = [ "pia-vpn-setup.service" ];
|
||||||
|
requires = [ "pia-vpn-setup.service" ];
|
||||||
|
|
||||||
|
path = scriptPkgs;
|
||||||
|
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
${scripts.scriptCommon}
|
||||||
|
loadChosenServerFromFile '${serverFile}'
|
||||||
|
readPortRenewalFile '${portRenewalFile}'
|
||||||
|
echo "Refreshing PIA port forward..."
|
||||||
refreshPIAPort
|
refreshPIAPort
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
echo "PIA assigned port: $PORT"
|
systemd.timers.pia-vpn-port-refresh = mkIf portForwarding {
|
||||||
|
partOf = [ "pia-vpn-port-refresh.service" ];
|
||||||
# DNAT rules to forward PIA port to target container
|
wantedBy = [ "timers.target" ];
|
||||||
${dnatSetupScript}
|
timerConfig = {
|
||||||
''}
|
OnCalendar = "*:0/10";
|
||||||
|
RandomizedDelaySec = "1m";
|
||||||
echo "PIA VPN setup complete"
|
};
|
||||||
exec sleep infinity
|
|
||||||
'';
|
|
||||||
|
|
||||||
preStop = ''
|
|
||||||
echo "Tearing down PIA VPN..."
|
|
||||||
ip -4 address flush dev ${cfg.interfaceName} 2>/dev/null || true
|
|
||||||
ip route del default dev ${cfg.interfaceName} 2>/dev/null || true
|
|
||||||
iptables -t nat -F POSTROUTING 2>/dev/null || true
|
|
||||||
iptables -F FORWARD 2>/dev/null || true
|
|
||||||
${optionalString portForwarding ''
|
|
||||||
iptables -t nat -F PREROUTING 2>/dev/null || true
|
|
||||||
''}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Port refresh timer (every 10 min) — keeps PIA port forwarding alive
|
|
||||||
systemd.services.pia-vpn-port-refresh = mkIf portForwarding {
|
|
||||||
description = "PIA VPN Port Forward Refresh";
|
|
||||||
after = [ "pia-vpn-setup.service" ];
|
|
||||||
requires = [ "pia-vpn-setup.service" ];
|
|
||||||
|
|
||||||
path = scriptPkgs;
|
|
||||||
|
|
||||||
serviceConfig.Type = "oneshot";
|
|
||||||
|
|
||||||
script = ''
|
|
||||||
set -euo pipefail
|
|
||||||
${scripts.scriptCommon}
|
|
||||||
loadChosenServerFromFile '${serverFile}'
|
|
||||||
readPortRenewalFile '${portRenewalFile}'
|
|
||||||
echo "Refreshing PIA port forward..."
|
|
||||||
refreshPIAPort
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.timers.pia-vpn-port-refresh = mkIf portForwarding {
|
|
||||||
partOf = [ "pia-vpn-port-refresh.service" ];
|
|
||||||
wantedBy = [ "timers.target" ];
|
|
||||||
timerConfig = {
|
|
||||||
OnCalendar = "*:0/10";
|
|
||||||
RandomizedDelaySec = "1m";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user