All checks were successful
Check Flake / check-flake (push) Successful in 3m18s
Every PIA API response includes a trailing RSA-SHA256 signature (line 1 = JSON, lines 3+ = base64-encoded signature) which was previously ignored entirely. Add verifyPIAResponse() that checks each response against PIA's public signing key before trusting the data. On verification failure the service aborts and systemd restarts it. Also bump RestartSec to 5m to avoid hammering PIA servers on repeated failures.
236 lines
8.4 KiB
Nix
236 lines
8.4 KiB
Nix
let
|
|
caPath = ./ca.rsa.4096.crt;
|
|
pubKeyPath = ./pubkey.pem;
|
|
in
|
|
|
|
# Bash function library for PIA VPN WireGuard operations.
|
|
# All PIA API calls accept an optional $proxy variable:
|
|
# proxy="http://10.100.0.1:8888" fetchPIAToken
|
|
# When $proxy is set, curl uses --proxy "$proxy"; otherwise direct connection.
|
|
|
|
# Reference materials:
|
|
# https://serverlist.piaservers.net/vpninfo/servers/v6
|
|
# https://github.com/pia-foss/manual-connections
|
|
# https://github.com/thrnz/docker-wireguard-pia/blob/master/extra/wg-gen.sh
|
|
# https://www.wireguard.com/netns/#ordinary-containerization
|
|
|
|
{
|
|
scriptCommon = ''
|
|
proxy_args() {
|
|
if [[ -n "''${proxy:-}" ]]; then
|
|
echo "--proxy $proxy"
|
|
fi
|
|
}
|
|
|
|
# Verify a raw PIA API response (line 1 = JSON, lines 3+ = base64 RSA-SHA256 signature).
|
|
verifyPIAResponse() {
|
|
local raw=$1 label=$2
|
|
local sig_file
|
|
sig_file=$(mktemp)
|
|
echo "$raw" | tail -n +3 | base64 -d > "$sig_file"
|
|
if ! echo -n "$(echo "$raw" | head -n 1 | tr -d '\n')" | \
|
|
openssl dgst -sha256 -verify "${pubKeyPath}" \
|
|
-signature "$sig_file"; then
|
|
echo "ERROR: $label signature verification failed" >&2
|
|
rm -f "$sig_file"
|
|
return 1
|
|
fi
|
|
echo "$label signature verified"
|
|
rm -f "$sig_file"
|
|
}
|
|
|
|
fetchPIAToken() {
|
|
local PIA_USER PIA_PASS raw resp
|
|
echo "Reading PIA credentials..."
|
|
PIA_USER=$(sed '1q;d' /run/agenix/pia-login.conf)
|
|
PIA_PASS=$(sed '2q;d' /run/agenix/pia-login.conf)
|
|
echo "Requesting PIA authentication token..."
|
|
raw=$(curl -s $(proxy_args) -u "$PIA_USER:$PIA_PASS" \
|
|
"https://www.privateinternetaccess.com/gtoken/generateToken")
|
|
verifyPIAResponse "$raw" "generateToken"
|
|
resp=$(echo "$raw" | head -n 1)
|
|
PIA_TOKEN=$(echo "$resp" | jq -r '.token')
|
|
if [[ -z "$PIA_TOKEN" || "$PIA_TOKEN" == "null" ]]; then
|
|
echo "ERROR: Failed to fetch PIA token: $resp" >&2
|
|
return 1
|
|
fi
|
|
echo "PIA token acquired"
|
|
}
|
|
|
|
choosePIAServer() {
|
|
local serverLocation=$1
|
|
local raw servers_json totalservers serverindex
|
|
servers_json=$(mktemp)
|
|
echo "Fetching PIA server list..."
|
|
raw=$(curl -s $(proxy_args) \
|
|
"https://serverlist.piaservers.net/vpninfo/servers/v6")
|
|
verifyPIAResponse "$raw" "serverList"
|
|
echo "$raw" | head -n 1 | tr -d '\n' > "$servers_json"
|
|
|
|
totalservers=$(jq -r \
|
|
'.regions | .[] | select(.id=="'"$serverLocation"'") | .servers.wg | length' \
|
|
"$servers_json")
|
|
if ! [[ "$totalservers" =~ ^[0-9]+$ ]] || [ "$totalservers" -eq 0 ] 2>/dev/null; then
|
|
echo "ERROR: Location \"$serverLocation\" not found." >&2
|
|
rm -f "$servers_json"
|
|
return 1
|
|
fi
|
|
echo "Found $totalservers WireGuard servers in region '$serverLocation'"
|
|
serverindex=$(( RANDOM % totalservers ))
|
|
|
|
WG_HOSTNAME=$(jq -r \
|
|
'.regions | .[] | select(.id=="'"$serverLocation"'") | .servers.wg | .['"$serverindex"'].cn' \
|
|
"$servers_json")
|
|
WG_SERVER_IP=$(jq -r \
|
|
'.regions | .[] | select(.id=="'"$serverLocation"'") | .servers.wg | .['"$serverindex"'].ip' \
|
|
"$servers_json")
|
|
WG_SERVER_PORT=$(jq -r '.groups.wg | .[0] | .ports | .[0]' "$servers_json")
|
|
|
|
rm -f "$servers_json"
|
|
echo "Selected server $serverindex/$totalservers: $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)"
|
|
}
|
|
|
|
generateWireguardKey() {
|
|
PRIVATE_KEY=$(wg genkey)
|
|
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | wg pubkey)
|
|
echo "Generated WireGuard keypair"
|
|
}
|
|
|
|
authorizeKeyWithPIAServer() {
|
|
local raw addKeyResponse
|
|
echo "Sending addKey request to $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)..."
|
|
raw=$(curl -s -G $(proxy_args) \
|
|
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
|
|
--cacert "${caPath}" \
|
|
--data-urlencode "pt=$PIA_TOKEN" \
|
|
--data-urlencode "pubkey=$PUBLIC_KEY" \
|
|
"https://$WG_HOSTNAME:$WG_SERVER_PORT/addKey")
|
|
verifyPIAResponse "$raw" "addKey"
|
|
addKeyResponse=$(echo "$raw" | head -n 1)
|
|
local status
|
|
status=$(echo "$addKeyResponse" | jq -r '.status')
|
|
if [[ "$status" != "OK" ]]; then
|
|
echo "ERROR: addKey failed: $addKeyResponse" >&2
|
|
return 1
|
|
fi
|
|
MY_IP=$(echo "$addKeyResponse" | jq -r '.peer_ip')
|
|
WG_SERVER_PUBLIC_KEY=$(echo "$addKeyResponse" | jq -r '.server_key')
|
|
WG_SERVER_PORT=$(echo "$addKeyResponse" | jq -r '.server_port')
|
|
echo "Key authorized — assigned VPN IP: $MY_IP, server port: $WG_SERVER_PORT"
|
|
}
|
|
|
|
writeWireguardQuickFile() {
|
|
local wgFile=$1
|
|
local listenPort=$2
|
|
rm -f "$wgFile"
|
|
touch "$wgFile"
|
|
chmod 700 "$wgFile"
|
|
cat > "$wgFile" <<WGEOF
|
|
[Interface]
|
|
PrivateKey = $PRIVATE_KEY
|
|
ListenPort = $listenPort
|
|
[Peer]
|
|
PersistentKeepalive = 25
|
|
PublicKey = $WG_SERVER_PUBLIC_KEY
|
|
AllowedIPs = 0.0.0.0/0
|
|
Endpoint = $WG_SERVER_IP:$WG_SERVER_PORT
|
|
WGEOF
|
|
echo "Wrote WireGuard config to $wgFile (listen=$listenPort)"
|
|
}
|
|
|
|
writeChosenServerToFile() {
|
|
local serverFile=$1
|
|
jq -n \
|
|
--arg hostname "$WG_HOSTNAME" \
|
|
--arg ip "$WG_SERVER_IP" \
|
|
--arg port "$WG_SERVER_PORT" \
|
|
'{hostname: $hostname, ip: $ip, port: $port}' > "$serverFile"
|
|
chmod 700 "$serverFile"
|
|
echo "Wrote server info to $serverFile"
|
|
}
|
|
|
|
loadChosenServerFromFile() {
|
|
local serverFile=$1
|
|
WG_HOSTNAME=$(jq -r '.hostname' "$serverFile")
|
|
WG_SERVER_IP=$(jq -r '.ip' "$serverFile")
|
|
WG_SERVER_PORT=$(jq -r '.port' "$serverFile")
|
|
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() {
|
|
local wgFile=$1
|
|
local interfaceName=$2
|
|
|
|
echo "Applying WireGuard config to $interfaceName..."
|
|
wg setconf "$interfaceName" "$wgFile"
|
|
ip -4 address add "$MY_IP" dev "$interfaceName"
|
|
ip link set mtu 1420 up dev "$interfaceName"
|
|
echo "WireGuard interface $interfaceName is up with IP $MY_IP"
|
|
}
|
|
|
|
reservePortForward() {
|
|
local raw payload_and_signature
|
|
echo "Requesting port forward signature from $WG_HOSTNAME..."
|
|
raw=$(curl -s -m 5 \
|
|
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
|
|
--cacert "${caPath}" \
|
|
-G --data-urlencode "token=$PIA_TOKEN" \
|
|
"https://$WG_HOSTNAME:19999/getSignature")
|
|
verifyPIAResponse "$raw" "getSignature"
|
|
payload_and_signature=$(echo "$raw" | head -n 1)
|
|
local status
|
|
status=$(echo "$payload_and_signature" | jq -r '.status')
|
|
if [[ "$status" != "OK" ]]; then
|
|
echo "ERROR: getSignature failed: $payload_and_signature" >&2
|
|
return 1
|
|
fi
|
|
PORT_SIGNATURE=$(echo "$payload_and_signature" | jq -r '.signature')
|
|
PORT_PAYLOAD=$(echo "$payload_and_signature" | jq -r '.payload')
|
|
PORT=$(echo "$PORT_PAYLOAD" | base64 -d | jq -r '.port')
|
|
echo "Port forward reserved: port $PORT"
|
|
}
|
|
|
|
writePortRenewalFile() {
|
|
local portRenewalFile=$1
|
|
jq -n \
|
|
--arg signature "$PORT_SIGNATURE" \
|
|
--arg payload "$PORT_PAYLOAD" \
|
|
'{signature: $signature, payload: $payload}' > "$portRenewalFile"
|
|
chmod 700 "$portRenewalFile"
|
|
echo "Wrote port renewal data to $portRenewalFile"
|
|
}
|
|
|
|
readPortRenewalFile() {
|
|
local portRenewalFile=$1
|
|
PORT_SIGNATURE=$(jq -r '.signature' "$portRenewalFile")
|
|
PORT_PAYLOAD=$(jq -r '.payload' "$portRenewalFile")
|
|
echo "Loaded port renewal data from $portRenewalFile"
|
|
}
|
|
|
|
refreshPIAPort() {
|
|
local raw bindPortResponse
|
|
echo "Refreshing port forward binding with $WG_HOSTNAME..."
|
|
raw=$(curl -Gs -m 5 \
|
|
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
|
|
--cacert "${caPath}" \
|
|
--data-urlencode "payload=$PORT_PAYLOAD" \
|
|
--data-urlencode "signature=$PORT_SIGNATURE" \
|
|
"https://$WG_HOSTNAME:19999/bindPort")
|
|
verifyPIAResponse "$raw" "bindPort"
|
|
bindPortResponse=$(echo "$raw" | head -n 1)
|
|
echo "bindPort response: $bindPortResponse"
|
|
}
|
|
'';
|
|
}
|