Compare commits
8 Commits
c1030c1dfe
...
pia-vpn-v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b71f4b1fd | |||
| dc3c2194ab | |||
| 39009cbc18 | |||
| 3365a1652c | |||
| 6466406975 | |||
| 4eb0401263 | |||
| f4a4edf478 | |||
| 1ac3f05e3e |
@@ -96,7 +96,7 @@
|
|||||||
{ groups = [ "wheel" ]; persist = true; }
|
{ groups = [ "wheel" ]; persist = true; }
|
||||||
];
|
];
|
||||||
|
|
||||||
nix.gc.automatic = true;
|
nix.gc.automatic = !config.boot.isContainer;
|
||||||
|
|
||||||
security.acme.acceptTerms = true;
|
security.acme.acceptTerms = true;
|
||||||
security.acme.defaults.email = "zuckerberg@neet.dev";
|
security.acme.defaults.email = "zuckerberg@neet.dev";
|
||||||
|
|||||||
@@ -52,8 +52,13 @@ let
|
|||||||
type = types.nullOr (types.submodule {
|
type = types.nullOr (types.submodule {
|
||||||
options = {
|
options = {
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = types.port;
|
type = types.nullOr types.port;
|
||||||
description = "Target port to forward PIA-assigned port to";
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Target port to forward to. If null, forwards to the same PIA-assigned port.
|
||||||
|
PIA-assigned ports below 10000 are rejected to avoid accidentally
|
||||||
|
forwarding traffic to other services.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
protocol = mkOption {
|
protocol = mkOption {
|
||||||
type = types.enum [ "tcp" "udp" "both" ];
|
type = types.enum [ "tcp" "udp" "both" ];
|
||||||
@@ -153,6 +158,13 @@ in
|
|||||||
description = "Prefix length derived from subnet CIDR";
|
description = "Prefix length derived from subnet CIDR";
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Shared host entries for all containers (host + VPN + service containers)
|
||||||
|
containerHosts = mkOption {
|
||||||
|
type = types.attrsOf (types.listOf types.str);
|
||||||
|
internal = true;
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
@@ -252,10 +264,12 @@ in
|
|||||||
# Host entries for container hostnames — NixOS only auto-creates these for
|
# Host entries for container hostnames — NixOS only auto-creates these for
|
||||||
# hostAddress/localAddress containers, not hostBridge. Use the standard
|
# hostAddress/localAddress containers, not hostBridge. Use the standard
|
||||||
# {name}.containers convention.
|
# {name}.containers convention.
|
||||||
networking.hosts =
|
pia-vpn.containerHosts =
|
||||||
{ ${cfg.vpnAddress} = [ "pia-vpn.containers" ]; }
|
{ ${cfg.vpnAddress} = [ "pia-vpn.containers" ]; }
|
||||||
// mapAttrs' (name: ctr: nameValuePair ctr.ip [ "${name}.containers" ]) cfg.containers;
|
// mapAttrs' (name: ctr: nameValuePair ctr.ip [ "${name}.containers" ]) cfg.containers;
|
||||||
|
|
||||||
|
networking.hosts = cfg.containerHosts;
|
||||||
|
|
||||||
# PIA login secret
|
# PIA login secret
|
||||||
age.secrets."pia-login.conf".file = ../../../secrets/pia-login.age;
|
age.secrets."pia-login.conf".file = ../../../secrets/pia-login.age;
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ in
|
|||||||
echo "Loaded server info from $serverFile: $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)"
|
echo "Loaded server info from $serverFile: $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Reset WG interface and tear down NAT/forwarding rules.
|
||||||
|
# Called on startup (clear stale state) and on exit via trap.
|
||||||
|
cleanupVpn() {
|
||||||
|
local interfaceName=$1
|
||||||
|
wg set "$interfaceName" listen-port 0 2>/dev/null || true
|
||||||
|
ip -4 address flush dev "$interfaceName" 2>/dev/null || true
|
||||||
|
ip route del default dev "$interfaceName" 2>/dev/null || true
|
||||||
|
iptables -t nat -F 2>/dev/null || true
|
||||||
|
iptables -F FORWARD 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
connectToServer() {
|
connectToServer() {
|
||||||
local wgFile=$1
|
local wgFile=$1
|
||||||
local interfaceName=$2
|
local interfaceName=$2
|
||||||
|
|||||||
@@ -42,9 +42,29 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
networking.hosts = cfg.containerHosts;
|
||||||
|
|
||||||
# DNS through VPN container (queries go through WG tunnel = no DNS leak)
|
# DNS through VPN container (queries go through WG tunnel = no DNS leak)
|
||||||
networking.nameservers = [ cfg.vpnAddress ];
|
networking.nameservers = [ cfg.vpnAddress ];
|
||||||
|
|
||||||
|
# Wait for actual VPN connectivity before network-online.target.
|
||||||
|
# Without this, services start before the VPN tunnel is ready and failures
|
||||||
|
# can't be reported to ntfy (no outbound connectivity yet).
|
||||||
|
systemd.services.wait-for-vpn = {
|
||||||
|
description = "Wait for VPN connectivity";
|
||||||
|
before = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "network-online.target" ];
|
||||||
|
after = [ "systemd-networkd-wait-online.service" ];
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
path = [ pkgs.iputils ];
|
||||||
|
script = ''
|
||||||
|
until ping -c1 -W2 1.1.1.1 >/dev/null 2>&1; do
|
||||||
|
echo "Waiting for VPN connectivity..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
# Trust the bridge interface (host reaches us directly for nginx)
|
# Trust the bridge interface (host reaches us directly for nginx)
|
||||||
networking.firewall.trustedInterfaces = [ "eth0" ];
|
networking.firewall.trustedInterfaces = [ "eth0" ];
|
||||||
|
|
||||||
|
|||||||
@@ -24,15 +24,16 @@ let
|
|||||||
let
|
let
|
||||||
fwd = forwardingContainer.receiveForwardedPort;
|
fwd = forwardingContainer.receiveForwardedPort;
|
||||||
targetIp = forwardingContainer.ip;
|
targetIp = forwardingContainer.ip;
|
||||||
targetPort = toString fwd.port;
|
dnatTarget = if fwd.port != null then "${targetIp}:${toString fwd.port}" else targetIp;
|
||||||
|
targetPort = if fwd.port != null then toString fwd.port else "$PORT";
|
||||||
tcpRules = optionalString (fwd.protocol == "tcp" || fwd.protocol == "both") ''
|
tcpRules = optionalString (fwd.protocol == "tcp" || fwd.protocol == "both") ''
|
||||||
echo "Setting up TCP DNAT: port $PORT → ${targetIp}:${targetPort}"
|
echo "Setting up TCP DNAT: port $PORT → ${targetIp}:${targetPort}"
|
||||||
iptables -t nat -A PREROUTING -i ${cfg.interfaceName} -p tcp --dport $PORT -j DNAT --to ${targetIp}:${targetPort}
|
iptables -t nat -A PREROUTING -i ${cfg.interfaceName} -p tcp --dport $PORT -j DNAT --to ${dnatTarget}
|
||||||
iptables -A FORWARD -i ${cfg.interfaceName} -d ${targetIp} -p tcp --dport ${targetPort} -j ACCEPT
|
iptables -A FORWARD -i ${cfg.interfaceName} -d ${targetIp} -p tcp --dport ${targetPort} -j ACCEPT
|
||||||
'';
|
'';
|
||||||
udpRules = optionalString (fwd.protocol == "udp" || fwd.protocol == "both") ''
|
udpRules = optionalString (fwd.protocol == "udp" || fwd.protocol == "both") ''
|
||||||
echo "Setting up UDP DNAT: port $PORT → ${targetIp}:${targetPort}"
|
echo "Setting up UDP DNAT: port $PORT → ${targetIp}:${targetPort}"
|
||||||
iptables -t nat -A PREROUTING -i ${cfg.interfaceName} -p udp --dport $PORT -j DNAT --to ${targetIp}:${targetPort}
|
iptables -t nat -A PREROUTING -i ${cfg.interfaceName} -p udp --dport $PORT -j DNAT --to ${dnatTarget}
|
||||||
iptables -A FORWARD -i ${cfg.interfaceName} -d ${targetIp} -p udp --dport ${targetPort} -j ACCEPT
|
iptables -A FORWARD -i ${cfg.interfaceName} -d ${targetIp} -p udp --dport ${targetPort} -j ACCEPT
|
||||||
'';
|
'';
|
||||||
onPortForwarded = optionalString (forwardingContainer.onPortForwarded != null) ''
|
onPortForwarded = optionalString (forwardingContainer.onPortForwarded != null) ''
|
||||||
@@ -43,9 +44,13 @@ let
|
|||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
|
if [ "$PORT" -lt 10000 ]; then
|
||||||
|
echo "ERROR: PIA assigned low port $PORT (< 10000), refusing to set up DNAT" >&2
|
||||||
|
else
|
||||||
${tcpRules}
|
${tcpRules}
|
||||||
${udpRules}
|
${udpRules}
|
||||||
${onPortForwarded}
|
${onPortForwarded}
|
||||||
|
fi
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
@@ -73,6 +78,8 @@ in
|
|||||||
{
|
{
|
||||||
imports = allModules;
|
imports = allModules;
|
||||||
|
|
||||||
|
networking.hosts = cfg.containerHosts;
|
||||||
|
|
||||||
# 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;
|
||||||
@@ -89,6 +96,9 @@ in
|
|||||||
# Ignore WG interface for wait-online (it's configured manually, not by networkd)
|
# Ignore WG interface for wait-online (it's configured manually, not by networkd)
|
||||||
systemd.network.wait-online.ignoredInterfaces = [ cfg.interfaceName ];
|
systemd.network.wait-online.ignoredInterfaces = [ cfg.interfaceName ];
|
||||||
|
|
||||||
|
# Route ntfy alerts through the host proxy (VPN container has no gateway on eth0)
|
||||||
|
ntfy-alerts.curlExtraArgs = "--proxy http://${cfg.hostAddress}:${toString cfg.proxyPort}";
|
||||||
|
|
||||||
# Enable forwarding so bridge traffic can go through WG
|
# Enable forwarding so bridge traffic can go through WG
|
||||||
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
|
||||||
|
|
||||||
@@ -131,12 +141,8 @@ in
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
${scripts.scriptCommon}
|
${scripts.scriptCommon}
|
||||||
|
|
||||||
# Clean up stale state from previous attempts
|
trap 'cleanupVpn ${cfg.interfaceName}' EXIT
|
||||||
wg set ${cfg.interfaceName} listen-port 0 2>/dev/null || true
|
cleanupVpn ${cfg.interfaceName}
|
||||||
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}"
|
proxy="${proxy}"
|
||||||
|
|
||||||
@@ -190,16 +196,6 @@ in
|
|||||||
exec sleep infinity
|
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
|
# Port refresh timer (every 10 min) — keeps PIA port forwarding alive
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ in
|
|||||||
default = "service-failures";
|
default = "service-failures";
|
||||||
description = "ntfy topic to publish alerts to.";
|
description = "ntfy topic to publish alerts to.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
curlExtraArgs = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Extra arguments to pass to curl (e.g. --proxy http://host:port).";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.thisMachine.hasRole."ntfy" {
|
config = lib.mkIf config.thisMachine.hasRole."ntfy" {
|
||||||
@@ -33,6 +39,7 @@ in
|
|||||||
${lib.getExe pkgs.curl} \
|
${lib.getExe pkgs.curl} \
|
||||||
--fail --silent --show-error \
|
--fail --silent --show-error \
|
||||||
--max-time 30 --retry 3 \
|
--max-time 30 --retry 3 \
|
||||||
|
${cfg.curlExtraArgs} \
|
||||||
-H "Authorization: Bearer $NTFY_TOKEN" \
|
-H "Authorization: Bearer $NTFY_TOKEN" \
|
||||||
-H "Title: Service failure on ${config.networking.hostName}" \
|
-H "Title: Service failure on ${config.networking.hostName}" \
|
||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
|
|||||||
@@ -133,8 +133,15 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = mkIf (cfg.enable && vmWorkspaces != { }) {
|
config = mkMerge [
|
||||||
|
(mkIf (cfg.enable && vmWorkspaces != { }) {
|
||||||
# Convert VM workspace configs to microvm.nix format
|
# Convert VM workspace configs to microvm.nix format
|
||||||
microvm.vms = mapAttrs mkVmConfig vmWorkspaces;
|
microvm.vms = mapAttrs mkVmConfig vmWorkspaces;
|
||||||
};
|
})
|
||||||
|
|
||||||
|
# microvm.nixosModules.host enables KSM, but /sys is read-only in containers
|
||||||
|
(mkIf config.boot.isContainer {
|
||||||
|
hardware.ksm.enable = false;
|
||||||
|
})
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,17 +63,17 @@
|
|||||||
ip = "10.100.0.10";
|
ip = "10.100.0.10";
|
||||||
mounts."/var/lib".hostPath = "/var/lib";
|
mounts."/var/lib".hostPath = "/var/lib";
|
||||||
mounts."/data/samba/Public".hostPath = "/data/samba/Public";
|
mounts."/data/samba/Public".hostPath = "/data/samba/Public";
|
||||||
receiveForwardedPort = { port = 51413; protocol = "both"; };
|
receiveForwardedPort = { protocol = "both"; };
|
||||||
onPortForwarded = ''
|
onPortForwarded = ''
|
||||||
# Notify Transmission of the PIA-assigned peer port via RPC
|
# Notify Transmission of the PIA-assigned peer port via RPC
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
curlout=$(curl -s "http://$TARGET_IP:9091/transmission/rpc" 2>/dev/null) && break
|
curlout=$(curl -s "http://transmission.containers:8080/transmission/rpc" 2>/dev/null) && break
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
regex='X-Transmission-Session-Id: (\w*)'
|
regex='X-Transmission-Session-Id: (\w*)'
|
||||||
if [[ $curlout =~ $regex ]]; then
|
if [[ $curlout =~ $regex ]]; then
|
||||||
sessionId=''${BASH_REMATCH[1]}
|
sessionId=''${BASH_REMATCH[1]}
|
||||||
curl -s "http://$TARGET_IP:9091/transmission/rpc" \
|
curl -s "http://transmission.containers:8080/transmission/rpc" \
|
||||||
-d "{\"method\":\"session-set\",\"arguments\":{\"peer-port\":$PORT}}" \
|
-d "{\"method\":\"session-set\",\"arguments\":{\"peer-port\":$PORT}}" \
|
||||||
-H "X-Transmission-Session-Id: $sessionId"
|
-H "X-Transmission-Session-Id: $sessionId"
|
||||||
fi
|
fi
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
"incomplete-dir-enabled" = true;
|
"incomplete-dir-enabled" = true;
|
||||||
|
|
||||||
"rpc-enabled" = true;
|
"rpc-enabled" = true;
|
||||||
|
"rpc-port" = 8080;
|
||||||
"rpc-bind-address" = "0.0.0.0";
|
"rpc-bind-address" = "0.0.0.0";
|
||||||
"rpc-whitelist" = "127.0.0.1,10.100.*.*,192.168.*.*";
|
"rpc-whitelist" = "127.0.0.1,10.100.*.*,192.168.*.*";
|
||||||
"rpc-host-whitelist-enabled" = false;
|
"rpc-host-whitelist-enabled" = false;
|
||||||
@@ -231,7 +232,7 @@
|
|||||||
(mkVirtualHost "lidarr.s0.neet.dev" "http://servarr.containers:8686")
|
(mkVirtualHost "lidarr.s0.neet.dev" "http://servarr.containers:8686")
|
||||||
(mkVirtualHost "sonarr.s0.neet.dev" "http://servarr.containers:8989")
|
(mkVirtualHost "sonarr.s0.neet.dev" "http://servarr.containers:8989")
|
||||||
(mkVirtualHost "prowlarr.s0.neet.dev" "http://servarr.containers:9696")
|
(mkVirtualHost "prowlarr.s0.neet.dev" "http://servarr.containers:9696")
|
||||||
(mkVirtualHost "transmission.s0.neet.dev" "http://transmission.containers:9091")
|
(mkVirtualHost "transmission.s0.neet.dev" "http://transmission.containers:8080")
|
||||||
(mkVirtualHost "unifi.s0.neet.dev" "https://localhost:8443")
|
(mkVirtualHost "unifi.s0.neet.dev" "https://localhost:8443")
|
||||||
(mkVirtualHost "music.s0.neet.dev" "http://localhost:4533")
|
(mkVirtualHost "music.s0.neet.dev" "http://localhost:4533")
|
||||||
(mkVirtualHost "jellyfin.s0.neet.dev" "http://localhost:8096")
|
(mkVirtualHost "jellyfin.s0.neet.dev" "http://localhost:8096")
|
||||||
|
|||||||
Reference in New Issue
Block a user