Implement addKey API call and tunnel configuration
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ca.rsa.4096.crt
|
||||||
|
.ca.rsa.4096.crt
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# piawg
|
# piawg
|
||||||
|
|
||||||
POSIX shell script for OPNsense that manages Private Internet Access next-gen
|
Shell script for OPNsense that manages Private Internet Access next-gen
|
||||||
WireGuard tunnels using credentials and configuration stored in OpenBao.
|
WireGuard tunnels using credentials and configuration stored in OpenBao.
|
||||||
Licensed under 0BSD.
|
Licensed under 0BSD.
|
||||||
|
|||||||
274
piawg.sh
274
piawg.sh
@@ -2,6 +2,9 @@
|
|||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
# SPDX-FileCopyrightText: 2026 Kris Lamoureux <kris@lamoureux.io>
|
# SPDX-FileCopyrightText: 2026 Kris Lamoureux <kris@lamoureux.io>
|
||||||
|
|
||||||
|
# Allow local variable scoping, therefore not strictly POSIX
|
||||||
|
# shellcheck disable=SC3043
|
||||||
|
|
||||||
err() {
|
err() {
|
||||||
printf '[ERROR]: %s\n' "$1" >&2
|
printf '[ERROR]: %s\n' "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -14,63 +17,159 @@ check_http() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check for plausible looking PIA token
|
||||||
|
check_token() {
|
||||||
|
printf '%s\n' "$1" | grep -q '^[0-9A-Fa-f]\{128\}$'
|
||||||
|
}
|
||||||
|
|
||||||
bao_curl() {
|
bao_curl() {
|
||||||
curl -sS --connect-timeout 5 --max-time 20 --retry 5 --retry-delay 2 \
|
local path
|
||||||
|
local response
|
||||||
|
local error
|
||||||
|
local http_code
|
||||||
|
local notfound
|
||||||
|
notfound=0
|
||||||
|
while getopts "n" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
n) notfound=1 ;;
|
||||||
|
*) err "bao_curl option '$opt' not found" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND - 1))
|
||||||
|
path="$1"
|
||||||
|
shift
|
||||||
|
error="Failed request to '$BAO_ADDR/v1/$path'"
|
||||||
|
if ! response=$(curl -sS --connect-timeout 5 \
|
||||||
|
--max-time 20 --retry 5 --retry-delay 2 \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H "X-Vault-Token: $bao_token" \
|
-H "X-Vault-Token: $bao_token" \
|
||||||
-w '\n%{http_code}' \
|
-w '\n%{http_code}' \
|
||||||
"$@"
|
"$@" "$BAO_ADDR/v1/$path"); then
|
||||||
}
|
err "$error"
|
||||||
|
|
||||||
# Fetch latest token in OpenBao
|
|
||||||
get_token() {
|
|
||||||
if ! pia_token_reply=$(bao_curl \
|
|
||||||
"$BAO_ADDR/v1/$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN"); then
|
|
||||||
err "Failed to fetch PIA token from '$BAO_ADDR'"
|
|
||||||
fi
|
fi
|
||||||
printf '%s' "$pia_token_reply"
|
http_code="$(printf '%s' "$response" | tail -1)"
|
||||||
unset pia_token_reply
|
if [ "$http_code" = "404" ]; then
|
||||||
|
[ "$notfound" -eq 1 ] && return 4
|
||||||
|
err "$error (404)"
|
||||||
|
fi
|
||||||
|
check_http "$http_code" || err "$error"
|
||||||
|
printf '%s' "$response" | sed '$d'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get a new PIA token and store
|
opn_curl() {
|
||||||
|
local path
|
||||||
|
local error
|
||||||
|
local response
|
||||||
|
local http_code
|
||||||
|
path="$1"
|
||||||
|
shift
|
||||||
|
error="Failed request to '$opn_endpoint/$path'"
|
||||||
|
if ! response="$(curl -sS --connect-timeout 5 \
|
||||||
|
--max-time 20 --retry 5 --retry-delay 2 \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-u "$opn_key:$opn_secret" \
|
||||||
|
-w '\n%{http_code}' \
|
||||||
|
"$@" "$opn_endpoint/$path")"; then
|
||||||
|
err "$error"
|
||||||
|
fi
|
||||||
|
http_code="$(printf '%s' "$response" | tail -1)"
|
||||||
|
check_http "$http_code" || err "$error"
|
||||||
|
printf '%s' "$response" | sed '$d'
|
||||||
|
}
|
||||||
|
|
||||||
|
pia_addkey() {
|
||||||
|
local response
|
||||||
|
local key
|
||||||
|
local port
|
||||||
|
local peer_ip
|
||||||
|
local updates
|
||||||
|
# Add pubkey via PIA API and get connection details
|
||||||
|
if ! response=$(
|
||||||
|
curl -sS -G --connect-timeout 5 \
|
||||||
|
--max-time 20 --retry 5 --retry-delay 2 \
|
||||||
|
--resolve "$server_cn:$server_port:$server_ip" \
|
||||||
|
--cacert ./ca.rsa.4096.crt \
|
||||||
|
--data-urlencode "pt=$pia_token" \
|
||||||
|
--data-urlencode "pubkey=$piawg_pubkey" \
|
||||||
|
"https://$server_cn:1337/addKey"
|
||||||
|
); then
|
||||||
|
err "Failed connect to $server_cn to addKey"
|
||||||
|
fi
|
||||||
|
[ "$(printf '%s' "$response" | jq -r '.status')" != "OK" ] &&
|
||||||
|
err "Failed to addKey to $server_cn"
|
||||||
|
|
||||||
|
# Update Wireguard config on OPNsense
|
||||||
|
updates='{}'
|
||||||
|
if [ "$server_ip" != "$piawgsrv_srvaddr" ]; then
|
||||||
|
updates="$(printf '%s' "$updates" |
|
||||||
|
jq --arg a "$server_ip" '.serveraddress = $a')"
|
||||||
|
fi
|
||||||
|
key="$(printf '%s' "$response" | jq -r '.server_key')"
|
||||||
|
if [ "$key" != "$piawgsrv_pubkey" ]; then
|
||||||
|
updates="$(printf '%s' "$updates" | jq --arg k "$key" '.pubkey = $k')"
|
||||||
|
fi
|
||||||
|
port="$(printf '%s' "$response" | jq -r '.server_port')"
|
||||||
|
if [ "$port" != "$piawgsrv_srvport" ]; then
|
||||||
|
updates="$(printf '%s' "$updates" |
|
||||||
|
jq --arg p "$port" '.serverport = $p')"
|
||||||
|
fi
|
||||||
|
if [ "$updates" != '{}' ]; then
|
||||||
|
updates="$(printf '%s' "$updates" |
|
||||||
|
jq --arg s "$piawg_uuid" '.servers = $s')"
|
||||||
|
if [ "$(opn_curl "wireguard/client/setClient/$piawgsrv_uuid" \
|
||||||
|
-d "$(printf '%s' "$updates" | jq '{client: .}')" |
|
||||||
|
jq -r '.result')" != "saved" ]; then
|
||||||
|
err "Failed to update $OPN_PEER pubkey/port"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
peer_ip="$(printf '%s' "$response" | jq -r '.peer_ip')"
|
||||||
|
if [ "$peer_ip" != "$piawg_tunaddr" ]; then
|
||||||
|
if [ "$(opn_curl "wireguard/server/setServer/$piawg_uuid" \
|
||||||
|
-d "$(jq -n --arg ip "$peer_ip/32" \
|
||||||
|
'{server: {tunneladdress: $ip}}')" |
|
||||||
|
jq -r '.result')" != "saved" ]; then
|
||||||
|
err "Failed to update $OPN_WG tunnel address to $peer_ip"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
opn_curl 'wireguard/service/reconfigure' -d '{}'
|
||||||
|
|
||||||
|
# Update firewall rule alias
|
||||||
|
piawg_ip_alias="$(opn_curl 'firewall/alias/searchItem' -d '{}' |
|
||||||
|
jq -r '.rows[] | select(.name == "PIAwg_IP") | .uuid')"
|
||||||
|
opn_curl "firewall/alias/setItem/$piawg_ip_alias" \
|
||||||
|
-d "$(jq -n --arg ip "$peer_ip" '{alias: {content: $ip}}')"
|
||||||
|
opn_curl 'firewall/alias/reconfigure' -d '{}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get a new PIA API token and store
|
||||||
renew_token() {
|
renew_token() {
|
||||||
login_response="$(bao_curl "$BAO_ADDR/v1/$BAO_KV_MOUNT/data/$BAO_PATH_LOGIN")"
|
local login_response
|
||||||
|
local token_response
|
||||||
|
local pia_user
|
||||||
|
local pia_pass
|
||||||
|
local pia_token
|
||||||
|
|
||||||
http_code="$(printf '%s' "$login_response" | tail -1)"
|
login_response="$(bao_curl "$BAO_KV_MOUNT/data/$BAO_PATH_PIA")"
|
||||||
if ! check_http "$http_code"; then
|
|
||||||
err "Failed to get PIA login details (HTTP $http_code)"
|
|
||||||
fi
|
|
||||||
unset http_code
|
|
||||||
|
|
||||||
login_response="$(printf '%s' "$login_response" | sed '$d')"
|
|
||||||
pia_user="$(printf '%s' "$login_response" | jq -r '.data.data.username')"
|
pia_user="$(printf '%s' "$login_response" | jq -r '.data.data.username')"
|
||||||
pia_pass="$(printf '%s' "$login_response" | jq -r '.data.data.password')"
|
pia_pass="$(printf '%s' "$login_response" | jq -r '.data.data.password')"
|
||||||
unset login_response
|
unset login_response
|
||||||
if ! token_reply="$(curl -s -X POST "$PIA_API" \
|
if ! token_response="$(curl -s -X POST "$PIA_API" \
|
||||||
-F "username=$pia_user" \
|
-F "username=$pia_user" \
|
||||||
-F "password=$pia_pass")"; then
|
-F "password=$pia_pass")"; then
|
||||||
err "Failed to get a new PIA token"
|
err "Failed to get a new PIA token"
|
||||||
fi
|
fi
|
||||||
unset pia_pass
|
unset pia_pass
|
||||||
unset pia_user
|
unset pia_user
|
||||||
pia_token="$(echo "$token_reply" | jq -r .token)"
|
pia_token="$(echo "$token_response" | jq -r .token)"
|
||||||
unset token_reply
|
unset token_response
|
||||||
if ! printf '%s' "$pia_token" | grep -Eq '^[0-9A-Fa-f]{128}$'; then
|
check_token "$pia_token" || err "Invalid PIA token during renewal attempt"
|
||||||
err "Invalid token found during renewal attempt"
|
bao_curl "$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN" -X POST \
|
||||||
fi
|
-d "$(jq -n --arg t "$pia_token" '{data:{token:$t}}')"
|
||||||
if ! update_response="$(bao_curl -X POST -d "$(jq -n --arg t "$pia_token" '{data:{token:$t}}')" \
|
|
||||||
"$BAO_ADDR/v1/$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN")"; then
|
|
||||||
err "Failed to save PIA token to '$BAO_ADDR'"
|
|
||||||
fi
|
|
||||||
unset pia_token
|
unset pia_token
|
||||||
http_code="$(printf '%s' "$update_response" | tail -1)"
|
|
||||||
update_response="$(printf '%s' "$update_response" | sed '$d')"
|
|
||||||
check_http "$http_code" ||
|
|
||||||
err "Failed to write PIA token to OpenBao (HTTP $http_code)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check for required external commands
|
# Check for required external commands
|
||||||
for rbin in curl jq; do
|
for rbin in curl jq openssl; do
|
||||||
command -v "$rbin" >/dev/null 2>&1 ||
|
command -v "$rbin" >/dev/null 2>&1 ||
|
||||||
err "Required binary '$rbin' not found"
|
err "Required binary '$rbin' not found"
|
||||||
done
|
done
|
||||||
@@ -98,18 +197,25 @@ else
|
|||||||
err "Can't find config at '$PIAWG_CONF'"
|
err "Can't find config at '$PIAWG_CONF'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Overridable defaults
|
|
||||||
: "${PIA_API:=https://www.privateinternetaccess.com/api/client/v2/token}"
|
|
||||||
: "${BAO_AUTH_PATH:=approle}"
|
|
||||||
: "${BAO_KV_MOUNT:=kv}"
|
|
||||||
: "${BAO_PATH_LOGIN:=piawg/creds/login}"
|
|
||||||
: "${BAO_PATH_TOKEN:=piawg/session/token}"
|
|
||||||
|
|
||||||
# Must set these in PIAWG_CONF
|
# Must set these in PIAWG_CONF
|
||||||
: "${BAO_ADDR:?[ERROR]: BAO_ADDR is not set}"
|
: "${BAO_ADDR:?[ERROR]: BAO_ADDR is not set}"
|
||||||
: "${BAO_ROLE:?[ERROR]: BAO_ROLE is not set}"
|
: "${BAO_ROLE:?[ERROR]: BAO_ROLE is not set}"
|
||||||
: "${BAO_SECRET:?[ERROR]: BAO_SECRET is not set}"
|
: "${BAO_SECRET:?[ERROR]: BAO_SECRET is not set}"
|
||||||
|
|
||||||
|
# Overridable defaults
|
||||||
|
_fingerprint=1fd25658456eab3041fba77ccd398ab8124edcc1b8b2fc1d55fdf6b1bbfc9d70
|
||||||
|
: "${PIA_API:=https://www.privateinternetaccess.com/api/client/v2/token}"
|
||||||
|
: "${PIA_CRT:=https://www.privateinternetaccess.com/openvpn/ca.rsa.4096.crt}"
|
||||||
|
: "${PIA_HASH:=$_fingerprint}"
|
||||||
|
: "${BAO_AUTH_PATH:=approle}"
|
||||||
|
: "${BAO_KV_MOUNT:=kv}"
|
||||||
|
: "${BAO_PATH_CONFIG:=piawg/config/wireguard}"
|
||||||
|
: "${BAO_PATH_OPNSENSE:=piawg/creds/opnsense}"
|
||||||
|
: "${BAO_PATH_PIA:=piawg/creds/pia}"
|
||||||
|
: "${BAO_PATH_TOKEN:=piawg/session/token}"
|
||||||
|
: "${OPN_IF:=PIAwg}"
|
||||||
|
: "${OPN_PEER:=PIAwg_srv}"
|
||||||
|
|
||||||
# Get ephemeral session token from AppRole login
|
# Get ephemeral session token from AppRole login
|
||||||
if ! bao_token_reply=$(curl -sS \
|
if ! bao_token_reply=$(curl -sS \
|
||||||
--connect-timeout 5 \
|
--connect-timeout 5 \
|
||||||
@@ -126,22 +232,74 @@ unset bao_token_reply
|
|||||||
[ -n "$bao_token" ] || err "Failed to get token from '$BAO_ADDR'"
|
[ -n "$bao_token" ] || err "Failed to get token from '$BAO_ADDR'"
|
||||||
|
|
||||||
# Get latest PIA token
|
# Get latest PIA token
|
||||||
get_token_reply="$(get_token)"
|
get_token_reply="$(
|
||||||
http_code="$(printf '%s' "$get_token_reply" | tail -1)"
|
bao_curl -n "$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN"
|
||||||
get_token_reply="$(printf '%s' "$get_token_reply" | sed '$d')"
|
)" && rc=0 || rc=$?
|
||||||
|
if [ "$rc" -eq 4 ]; then
|
||||||
# Renew token if path doesn't exist yet
|
|
||||||
if [ "$http_code" -eq 404 ]; then
|
|
||||||
renew_token
|
renew_token
|
||||||
get_token_reply="$(get_token)"
|
get_token_reply="$(bao_curl "$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN")"
|
||||||
http_code="$(printf '%s' "$get_token_reply" | tail -1)"
|
fi
|
||||||
get_token_reply="$(printf '%s' "$get_token_reply" | sed '$d')"
|
pia_token="$(printf '%s' "$get_token_reply" | jq -r .data.data.token)"
|
||||||
if ! check_http "$http_code"; then
|
unset get_token_reply
|
||||||
err "Failed to get PIA token after renewal"
|
check_token "$pia_token" || err "Failed to get valid PIA token"
|
||||||
fi
|
|
||||||
elif ! check_http "$http_code"; then
|
# Download PIA RSA CA certificate
|
||||||
err "Failed to get PIA token from '$BAO_ADDR' (HTTP $http_code)"
|
if [ ! -f ./ca.rsa.4096.crt ]; then
|
||||||
|
[ -f ./.ca.rsa.4096.crt ] && rm ./.ca.rsa.4096.crt
|
||||||
|
curl -sS --connect-timeout 5 --max-time 20 --retry 5 --retry-delay 2 \
|
||||||
|
-o ./.ca.rsa.4096.crt "$PIA_CRT"
|
||||||
|
pia_file_hash="$(openssl x509 -in ./.ca.rsa.4096.crt -outform DER |
|
||||||
|
openssl dgst -sha256 -r | awk '{print $1}')"
|
||||||
|
[ "$pia_file_hash" != "$PIA_HASH" ] && err "PIA CA fingerprint mismatch"
|
||||||
|
mv ./.ca.rsa.4096.crt ./ca.rsa.4096.crt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s\n' "$get_token_reply"
|
# Get OPNsense login details
|
||||||
exit 0
|
bao_opn_login="$(bao_curl "$BAO_KV_MOUNT/data/$BAO_PATH_OPNSENSE")"
|
||||||
|
opn_key="$(printf '%s' "$bao_opn_login" | jq -r .data.data.key)"
|
||||||
|
opn_secret="$(printf '%s' "$bao_opn_login" | jq -r .data.data.secret)"
|
||||||
|
opn_endpoint="$(printf '%s' "$bao_opn_login" | jq -r .data.data.endpoint)"
|
||||||
|
opn_endpoint="${opn_endpoint%/}"
|
||||||
|
|
||||||
|
# Get OPNsense Wireguard config
|
||||||
|
opn_if_reply="$(opn_curl 'wireguard/server/searchServer' -d '{}' |
|
||||||
|
jq ".rows[] | select(.name == \"$OPN_IF\")")"
|
||||||
|
piawg_uuid="$(printf '%s' "$opn_if_reply" | jq -r .uuid)"
|
||||||
|
piawg_pubkey="$(printf '%s' "$opn_if_reply" | jq -r .pubkey)"
|
||||||
|
piawg_tunaddr="$(printf '%s' "$opn_if_reply" | jq -r .tunneladdress)"
|
||||||
|
echo "$opn_if_reply" | jq .
|
||||||
|
unset opn_if_reply
|
||||||
|
|
||||||
|
opn_peer_reply="$(opn_curl 'wireguard/client/searchClient' -d '{}' |
|
||||||
|
jq ".rows[] | select(.name == \"$OPN_PEER\")")"
|
||||||
|
piawgsrv_uuid="$(printf '%s' "$opn_peer_reply" | jq -r .uuid)"
|
||||||
|
piawgsrv_pubkey="$(printf '%s' "$opn_peer_reply" | jq -r .pubkey)"
|
||||||
|
piawgsrv_srvaddr="$(printf '%s' "$opn_peer_reply" | jq -r .serveraddress)"
|
||||||
|
piawgsrv_srvport="$(printf '%s' "$opn_peer_reply" | jq -r .serverport)"
|
||||||
|
echo "$opn_peer_reply" | jq .
|
||||||
|
unset opn_peer_reply
|
||||||
|
|
||||||
|
# Get target server IP, common name, and OPNsense WG pubkey
|
||||||
|
wg_reply="$(bao_curl "$BAO_KV_MOUNT/data/$BAO_PATH_CONFIG")"
|
||||||
|
server_ip="$(printf '%s' "$wg_reply" | jq -r .data.data.server_ip)"
|
||||||
|
server_cn="$(printf '%s' "$wg_reply" | jq -r .data.data.server_cn)"
|
||||||
|
server_port="$(printf '%s' "$wg_reply" | jq -r .data.data.server_port)"
|
||||||
|
echo "$wg_reply" | jq .
|
||||||
|
unset wg_reply
|
||||||
|
|
||||||
|
# Update to reflect desired state
|
||||||
|
[ "$server_ip" != "$piawgsrv_srvaddr" ] && pia_addkey
|
||||||
|
|
||||||
|
# Check tunnel
|
||||||
|
piawg_tunaddr="$(opn_curl 'wireguard/server/searchServer' -d '{}' |
|
||||||
|
jq -r ".rows[] | select(.name == \"$OPN_IF\") | .tunneladdress")"
|
||||||
|
wg_status_reply="$(opn_curl 'wireguard/service/show' -d '{}' |
|
||||||
|
jq ".rows[] | select(.name == \"$OPN_PEER\")")"
|
||||||
|
echo "$wg_status_reply" | jq .
|
||||||
|
wg_status="$(printf '%s' "$wg_status_reply" | jq -r '."peer-status"')"
|
||||||
|
[ ! "$wg_status" = "online" ] && err "$OPN_PEER is offline"
|
||||||
|
if ! ping -c1 -W3 -S "${piawg_tunaddr%/32}" 1.1.1.1 >/dev/null 2>&1; then
|
||||||
|
if ! ping -c1 -W3 -S "${piawg_tunaddr%/32}" 8.8.8.8 >/dev/null 2>&1; then
|
||||||
|
err "$OPN_PEER failed to ping internet"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user