Files
projects/Dockers/pia-wireguard/entrypoint.sh
T
Bram fca4f0aa55
Build and Push Docker Images / build-and-push (push) Successful in 19s
local nets
2026-05-05 21:17:22 +02:00

331 lines
9.6 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
source /usr/local/lib/pia.sh
PIA_IFACE="${PIA_IFACE:-wg0}"
PIA_STATE_DIR="${PIA_STATE_DIR:-/pia}"
PIA_PF_ENABLED="${PIA_PORT_FORWARD:-false}"
PIA_PF_FILE="${PIA_PF_FILE:-$PIA_STATE_DIR/port_forward.json}"
PIA_PF_PORT_FILE="${PIA_PF_PORT_FILE:-$PIA_STATE_DIR/port_forward.port}"
PIA_DNS_ENABLE="${PIA_DNS_ENABLE:-true}"
PIA_DNS_FALLBACK="${PIA_DNS_FALLBACK:-10.0.0.242}"
PIA_DNS_PRE_VPN="${PIA_DNS_PRE_VPN:-1.1.1.1}"
PIA_HEALTH_URL="${PIA_HEALTH_URL:-https://api64.ipify.org}"
PIA_HANDSHAKE_MAX_AGE_SECONDS="${PIA_HANDSHAKE_MAX_AGE_SECONDS:-180}"
PIA_RECONNECT_BACKOFF_SECONDS="${PIA_RECONNECT_BACKOFF_SECONDS:-5}"
PIA_LOCAL_NETS="${PIA_LOCAL_NETS:-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}"
CA_CERT_PATH="/opt/pia/ca.rsa.4096.crt"
pf_pid=""
current_region=""
current_wg_ip=""
current_wg_cn=""
orig_default_route=""
orig_default_gw=""
orig_default_dev=""
cleanup() {
set +e
if [[ -n "$pf_pid" ]]; then
kill "$pf_pid" 2>/dev/null || true
wait "$pf_pid" 2>/dev/null || true
pf_pid=""
fi
iptables -D OUTPUT -j PIA_KILLSWITCH 2>/dev/null || true
iptables -F PIA_KILLSWITCH 2>/dev/null || true
iptables -X PIA_KILLSWITCH 2>/dev/null || true
ip link del "$PIA_IFACE" 2>/dev/null || true
if [[ -n "${orig_default_route:-}" ]]; then
ip route replace $orig_default_route 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
ensure_state_dir() {
mkdir -p "$PIA_STATE_DIR"
chmod 0755 "$PIA_STATE_DIR"
}
write_resolv_conf() {
local dns="$1"
log "Setting /etc/resolv.conf to DNS $dns"
printf "nameserver %s\n" "$dns" > /etc/resolv.conf
}
setup_killswitch() {
local endpoint_ip="$1"
local endpoint_port="$2"
iptables -N PIA_KILLSWITCH 2>/dev/null || true
iptables -F PIA_KILLSWITCH
iptables -A PIA_KILLSWITCH -o lo -j ACCEPT
iptables -A PIA_KILLSWITCH -o "$PIA_IFACE" -j ACCEPT
iptables -A PIA_KILLSWITCH -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow negotiating the tunnel itself (endpoint traffic leaves on eth0)
iptables -A PIA_KILLSWITCH -p udp -d "$endpoint_ip" --dport "$endpoint_port" -j ACCEPT
# Allow local traffic (docker internal + loopback ranges)
iptables -A PIA_KILLSWITCH -d 127.0.0.0/8 -j ACCEPT
iptables -A PIA_KILLSWITCH -d 172.16.0.0/12 -j ACCEPT
iptables -A PIA_KILLSWITCH -d 192.168.0.0/16 -j ACCEPT
iptables -A PIA_KILLSWITCH -d 10.0.0.0/8 -j ACCEPT
iptables -A PIA_KILLSWITCH -j REJECT
iptables -C OUTPUT -j PIA_KILLSWITCH 2>/dev/null || iptables -I OUTPUT 1 -j PIA_KILLSWITCH
}
capture_default_route() {
# Save original default route so we can restore on exit/reconnect.
if [[ -n "${orig_default_route:-}" ]]; then
return
fi
orig_default_route="$(ip -4 route show default | head -n1 || true)"
orig_default_gw="$(awk '{for (i=1;i<=NF;i++) if ($i=="via") print $(i+1)}' <<<"$orig_default_route")"
orig_default_dev="$(awk '{for (i=1;i<=NF;i++) if ($i=="dev") print $(i+1)}' <<<"$orig_default_route")"
}
wg_up_manual() {
local iface="$1"
local addr="$2"
local privkey="$3"
local endpoint_ip="$4"
local endpoint_port="$5"
local server_pubkey="$6"
capture_default_route
if [[ -z "${orig_default_gw:-}" || -z "${orig_default_dev:-}" ]]; then
>&2 echo "Could not determine original default gateway/device."
return 1
fi
ip link del "$iface" 2>/dev/null || true
ip link add "$iface" type wireguard
ip -4 address add "$addr" dev "$iface"
ip link set mtu 1420 up dev "$iface"
wg set "$iface" private-key <(printf '%s' "$privkey")
wg set "$iface" peer "$server_pubkey" persistent-keepalive 25 allowed-ips 0.0.0.0/0 endpoint "${endpoint_ip}:${endpoint_port}"
# Ensure endpoint traffic keeps going out via the original gateway.
ip -4 route replace "${endpoint_ip}/32" via "$orig_default_gw" dev "$orig_default_dev"
# Ensure local (LAN/RFC1918) traffic stays reachable outside the VPN.
# This is needed so published ports remain accessible from your LAN.
IFS=',' read -ra _nets <<<"$PIA_LOCAL_NETS"
for _net in "${_nets[@]}"; do
_net="$(echo "$_net" | xargs)"
[[ -z "$_net" ]] && continue
ip -4 route replace "$_net" via "$orig_default_gw" dev "$orig_default_dev" || true
done
# Route all egress through the tunnel (main table; avoids wg-quick sysctl/policy routing).
ip -4 route replace default dev "$iface"
}
is_tunnel_healthy() {
if ! wg show "$PIA_IFACE" >/dev/null 2>&1; then
return 1
fi
local now latest age
now="$(date +%s)"
latest="$(wg show "$PIA_IFACE" latest-handshakes 2>/dev/null | awk '{print $2}' | sort -nr | head -n1 || true)"
if [[ -z "$latest" || "$latest" == "0" ]]; then
return 1
fi
age="$(( now - latest ))"
if (( age > PIA_HANDSHAKE_MAX_AGE_SECONDS )); then
return 1
fi
curl -fsS --max-time 5 "$PIA_HEALTH_URL" >/dev/null
}
start_port_forwarding_loop() {
local token="$1"
local wg_ip="$2"
local wg_cn="$3"
local region="$4"
(
set -euo pipefail
local sigresp status payload signature port expires_at bindresp
while true; do
sigresp="$(pia_pf_get_signature "$wg_ip" "$wg_cn" "$token" "$CA_CERT_PATH")"
status="$(jq -r '.status' <<<"$sigresp")"
if [[ "$status" != "OK" ]]; then
>&2 echo "PF getSignature did not return OK: $sigresp"
sleep 5
continue
fi
payload="$(jq -r '.payload' <<<"$sigresp")"
signature="$(jq -r '.signature' <<<"$sigresp")"
port="$(echo "$payload" | base64 -d | jq -r '.port')"
expires_at="$(echo "$payload" | base64 -d | jq -r '.expires_at')"
bindresp="$(pia_pf_bind_port "$wg_ip" "$wg_cn" "$payload" "$signature" "$CA_CERT_PATH")"
if [[ "$(jq -r '.status' <<<"$bindresp")" != "OK" ]]; then
>&2 echo "PF bindPort did not return OK: $bindresp"
sleep 10
continue
fi
ensure_state_dir
jq -n \
--arg region "$region" \
--arg server_ip "$wg_ip" \
--arg server_cn "$wg_cn" \
--argjson port "$port" \
--arg expires_at "$expires_at" \
--arg refreshed_at "$(date -Is)" \
'{region:$region,server:{ip:$server_ip,cn:$server_cn},port:$port,expires_at:$expires_at,refreshed_at:$refreshed_at}' \
> "$PIA_PF_FILE.tmp" \
&& mv "$PIA_PF_FILE.tmp" "$PIA_PF_FILE"
echo "$port" > "$PIA_PF_PORT_FILE.tmp" && mv "$PIA_PF_PORT_FILE.tmp" "$PIA_PF_PORT_FILE"
sleep 900
done
) &
pf_pid="$!"
log "Port-forward loop started (pid=$pf_pid)."
}
connect_region() {
local region="$1"
local wg_ip="$2"
local wg_cn="$3"
local token="$4"
log "Connecting region=$region wg=${wg_cn} (${wg_ip})"
local privKey pubKey wgjson status peer_ip server_key server_port dns_server
privKey="$(wg genkey)"
pubKey="$(echo "$privKey" | wg pubkey)"
wgjson="$(pia_wireguard_add_key "$wg_ip" "$wg_cn" "$token" "$pubKey" "$CA_CERT_PATH")"
status="$(jq -r '.status' <<<"$wgjson")"
if [[ "$status" != "OK" ]]; then
>&2 echo "WireGuard addKey failed: $wgjson"
return 1
fi
peer_ip="$(jq -r '.peer_ip' <<<"$wgjson")"
server_key="$(jq -r '.server_key' <<<"$wgjson")"
server_port="$(jq -r '.server_port' <<<"$wgjson")"
dns_server="$(jq -r '.dns_servers[0] // empty' <<<"$wgjson")"
if [[ -z "$dns_server" ]]; then
dns_server="$PIA_DNS_FALLBACK"
fi
# Ensure we can still resolve endpoints before tunnel is up.
# Once the tunnel is up, we can switch to PIA-provided DNS.
if [[ "$PIA_DNS_ENABLE" == "true" ]]; then
write_resolv_conf "$PIA_DNS_PRE_VPN"
fi
wg_up_manual "$PIA_IFACE" "$peer_ip" "$privKey" "$wg_ip" "$server_port" "$server_key"
if [[ "$PIA_DNS_ENABLE" == "true" ]]; then
write_resolv_conf "$dns_server"
fi
setup_killswitch "$wg_ip" "$server_port"
current_region="$region"
current_wg_ip="$wg_ip"
current_wg_cn="$wg_cn"
log "Connected. Verifying egress."
curl -fsS --max-time 10 "$PIA_HEALTH_URL" | tr -d '\n' | sed 's/^/PublicIP=/' || true
echo
if [[ "$PIA_PF_ENABLED" == "true" ]]; then
start_port_forwarding_loop "$token" "$wg_ip" "$wg_cn" "$region"
fi
}
main() {
require_env PIA_USER
require_env PIA_PASS
require_env PIA_REGIONS
ensure_state_dir
log "Authenticating to PIA."
local token
token="$(pia_get_token)"
log "Fetching serverlist."
local serverlist
serverlist="$(pia_serverlist_v4)"
local candidates
candidates="$(pia_pick_region_candidates <<<"$serverlist")"
if [[ -z "$candidates" ]]; then
>&2 echo "No matching WireGuard servers found for PIA_REGIONS=$(printf %q "${PIA_REGIONS}")"
exit 4
fi
log "Starting connect loop."
local line region wg_ip wg_cn connected=0
while IFS= read -r line; do
region="${line%%|*}"
wg_ip="$(cut -d'|' -f2 <<<"$line")"
wg_cn="$(cut -d'|' -f3 <<<"$line")"
if connect_region "$region" "$wg_ip" "$wg_cn" "$token"; then
connected=1
break
fi
done <<<"$candidates"
if (( connected == 0 )); then
>&2 echo "Failed to connect to any region in PIA_REGIONS."
exit 5
fi
# Recovery loop (runs forever)
local idx=0 total
total="$(wc -l <<<"$candidates" | tr -d ' ')"
while true; do
sleep 15
if is_tunnel_healthy; then
continue
fi
log "Tunnel unhealthy. Reconnecting (rotate region)."
cleanup
idx=$(( (idx + 1) % total ))
line="$(sed -n "$((idx + 1))p" <<<"$candidates")"
region="${line%%|*}"
wg_ip="$(cut -d'|' -f2 <<<"$line")"
wg_cn="$(cut -d'|' -f3 <<<"$line")"
until connect_region "$region" "$wg_ip" "$wg_cn" "$token"; do
log "Reconnect failed; sleeping ${PIA_RECONNECT_BACKOFF_SECONDS}s."
sleep "$PIA_RECONNECT_BACKOFF_SECONDS"
idx=$(( (idx + 1) % total ))
line="$(sed -n "$((idx + 1))p" <<<"$candidates")"
region="${line%%|*}"
wg_ip="$(cut -d'|' -f2 <<<"$line")"
wg_cn="$(cut -d'|' -f3 <<<"$line")"
done
done
}
main "$@"