Move default 172.16.10.0/24 destination assignment to after profile resolution and only apply when both selectedDestinations and services are empty. Extract selectedServices calculation before conditional check in applyCurrentPolicy. Add nftables input chain to gateway with per-peer filtering. Accept established connections and non-WireGuard traffic. Allow DNS queries to configured
239 lines
8.9 KiB
Bash
239 lines
8.9 KiB
Bash
#!/usr/bin/env bash
|
|
set -eu
|
|
|
|
echo "NexaVPN gateway helper starting"
|
|
mkdir -p /var/lib/nexavpn
|
|
|
|
IFACE="${NEXAVPN_GATEWAY_INTERFACE:-wg0}"
|
|
UPLINK_IFACE="${NEXAVPN_UPLINK_INTERFACE:-eth0}"
|
|
ENABLE_MASQUERADE="${NEXAVPN_ENABLE_MASQUERADE:-true}"
|
|
GATEWAY_NAME="${NEXAVPN_GATEWAY_NAME:-primary-gateway}"
|
|
GATEWAY_LISTEN_PORT="${NEXAVPN_GATEWAY_LISTEN_PORT:-51900}"
|
|
BOOTSTRAP_URL="${NEXAVPN_GATEWAY_BOOTSTRAP_URL:-http://backend:8080/api/v1/gateway-agent/bootstrap}"
|
|
SYNC_BASE_URL="${NEXAVPN_GATEWAY_SYNC_URL:-http://backend:8080/api/v1/gateway-agent}"
|
|
GATEWAY_ID_FILE="/var/lib/nexavpn/gateway-id"
|
|
BACKEND_HOST="${NEXAVPN_BACKEND_HOST:-backend}"
|
|
CLIENT_DNS_SERVERS="${NEXAVPN_CLIENT_DNS_SERVERS:-10.20.0.53}"
|
|
|
|
if [ -z "${GATEWAY_BOOTSTRAP_TOKEN:-}" ]; then
|
|
echo "GATEWAY_BOOTSTRAP_TOKEN is required."
|
|
tail -f /dev/null
|
|
exit 0
|
|
fi
|
|
|
|
if [ -z "${NEXAVPN_GATEWAY_PRIVATE_KEY:-}" ]; then
|
|
if [ -f /var/lib/nexavpn/gateway-private.key ]; then
|
|
NEXAVPN_GATEWAY_PRIVATE_KEY="$(cat /var/lib/nexavpn/gateway-private.key)"
|
|
else
|
|
wg genkey | tee /var/lib/nexavpn/gateway-private.key >/tmp/nexavpn-gateway-private.key
|
|
NEXAVPN_GATEWAY_PRIVATE_KEY="$(cat /tmp/nexavpn-gateway-private.key)"
|
|
rm -f /tmp/nexavpn-gateway-private.key
|
|
fi
|
|
fi
|
|
|
|
if [ -z "${NEXAVPN_GATEWAY_ID:-}" ] && [ -f "${GATEWAY_ID_FILE}" ]; then
|
|
NEXAVPN_GATEWAY_ID="$(cat "${GATEWAY_ID_FILE}")"
|
|
fi
|
|
|
|
bootstrap_gateway() {
|
|
GATEWAY_PUBLIC_KEY="$(printf '%s' "${NEXAVPN_GATEWAY_PRIVATE_KEY}" | wg pubkey)"
|
|
DNS_JSON="$(printf '%s' "${CLIENT_DNS_SERVERS}" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))')"
|
|
echo "Bootstrapping gateway ${GATEWAY_NAME}"
|
|
BOOTSTRAP_RESPONSE="$(curl -fsSL \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Gateway-Bootstrap-Token: ${GATEWAY_BOOTSTRAP_TOKEN}" \
|
|
-d "{\"name\":\"${GATEWAY_NAME}\",\"endpoint\":\"${DEFAULT_GATEWAY_ENDPOINT:-localhost:${GATEWAY_LISTEN_PORT}}\",\"public_key\":\"${GATEWAY_PUBLIC_KEY}\",\"listen_port\":${GATEWAY_LISTEN_PORT},\"vpn_cidr\":\"${DEFAULT_VPN_CIDR:-100.96.0.0/24}\",\"dns_servers\":${DNS_JSON}}" \
|
|
"${BOOTSTRAP_URL}")"
|
|
NEXAVPN_GATEWAY_ID="$(printf '%s' "${BOOTSTRAP_RESPONSE}" | jq -r '.id')"
|
|
if [ -z "${NEXAVPN_GATEWAY_ID:-}" ] || [ "${NEXAVPN_GATEWAY_ID}" = "null" ]; then
|
|
echo "Gateway bootstrap did not return an id."
|
|
return 1
|
|
fi
|
|
printf '%s' "${NEXAVPN_GATEWAY_ID}" > "${GATEWAY_ID_FILE}"
|
|
}
|
|
|
|
STATE_JSON="/var/lib/nexavpn/sync-bundle.json"
|
|
WG_CONF="/etc/wireguard/${IFACE}.conf"
|
|
WG_GENERATED="/var/lib/nexavpn/${IFACE}.generated.conf"
|
|
NFT_CONF="/var/lib/nexavpn/nftables.generated.conf"
|
|
|
|
mkdir -p /etc/wireguard
|
|
|
|
apply_bundle() {
|
|
if [ -z "${NEXAVPN_GATEWAY_ID:-}" ]; then
|
|
bootstrap_gateway || return 1
|
|
fi
|
|
|
|
if [ -z "${NEXAVPN_GATEWAY_ID:-}" ] || [ -z "${NEXAVPN_GATEWAY_PRIVATE_KEY:-}" ]; then
|
|
echo "Gateway sync is not configured yet."
|
|
return 1
|
|
fi
|
|
|
|
SYNC_URL="${SYNC_BASE_URL}/${NEXAVPN_GATEWAY_ID}/sync"
|
|
echo "Fetching bundle from ${SYNC_URL}"
|
|
TMP_STATE_JSON="${STATE_JSON}.tmp"
|
|
rm -f "${TMP_STATE_JSON}"
|
|
curl -fsSL \
|
|
-H "X-Gateway-Bootstrap-Token: ${GATEWAY_BOOTSTRAP_TOKEN}" \
|
|
"${SYNC_URL}" \
|
|
-o "${TMP_STATE_JSON}" || return 1
|
|
mv "${TMP_STATE_JSON}" "${STATE_JSON}"
|
|
|
|
INTERFACE_ADDRESS=$(jq -r '.interface.address' "${STATE_JSON}")
|
|
NETWORK_CIDR=$(jq -r '.interface.network_cidr' "${STATE_JSON}")
|
|
LISTEN_PORT=$(jq -r '.interface.listen_port' "${STATE_JSON}")
|
|
|
|
cat > "${WG_GENERATED}" <<EOF
|
|
[Interface]
|
|
Address = ${INTERFACE_ADDRESS}
|
|
ListenPort = ${LISTEN_PORT}
|
|
PrivateKey = ${NEXAVPN_GATEWAY_PRIVATE_KEY}
|
|
|
|
EOF
|
|
|
|
jq -c '.peers[]?' "${STATE_JSON}" | while read -r peer; do
|
|
PUBLIC_KEY=$(printf '%s' "${peer}" | jq -r '.public_key')
|
|
ASSIGNED_IP=$(printf '%s' "${peer}" | jq -r '.assigned_ip')
|
|
|
|
cat >> "${WG_GENERATED}" <<EOF
|
|
[Peer]
|
|
PublicKey = ${PUBLIC_KEY}
|
|
AllowedIPs = ${ASSIGNED_IP}
|
|
|
|
EOF
|
|
done
|
|
|
|
cp "${WG_GENERATED}" "${WG_CONF}"
|
|
|
|
{
|
|
echo "table inet nexavpn {"
|
|
echo " chain input {"
|
|
echo " type filter hook input priority 0;"
|
|
echo " policy accept;"
|
|
echo " ct state established,related accept"
|
|
echo " iifname != \"${IFACE}\" accept"
|
|
|
|
jq -c '.peers[]?' "${STATE_JSON}" | while read -r peer; do
|
|
ASSIGNED_IP=$(printf '%s' "${peer}" | jq -r '.assigned_ip')
|
|
printf '%s' "${peer}" | jq -r '.dns_servers[]?' | while read -r dns_server; do
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} udp dport 53 accept"
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} tcp dport 53 accept"
|
|
done
|
|
printf '%s' "${peer}" | jq -c '.allowed_services[]?' | while read -r service; do
|
|
SERVICE_PROXY_IP="$(printf '%s' "${service}" | jq -r '.access_proxy_ip')"
|
|
printf '%s' "${service}" | jq -r '.ports[]?' | while read -r service_port; do
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${SERVICE_PROXY_IP} tcp dport ${service_port} accept"
|
|
done
|
|
done
|
|
done
|
|
|
|
echo " iifname \"${IFACE}\" drop"
|
|
echo " }"
|
|
echo " chain forward {"
|
|
echo " type filter hook forward priority 0;"
|
|
echo " policy accept;"
|
|
echo " ct state established,related accept"
|
|
echo " iifname != \"${IFACE}\" accept"
|
|
echo " iifname \"${IFACE}\" ip saddr ${NETWORK_CIDR} oifname \"${UPLINK_IFACE}\" accept"
|
|
|
|
jq -c '.peers[]?' "${STATE_JSON}" | while read -r peer; do
|
|
ASSIGNED_IP=$(printf '%s' "${peer}" | jq -r '.assigned_ip')
|
|
printf '%s' "${peer}" | jq -r '.dns_servers[]?' | while read -r dns_server; do
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} udp dport 53 accept"
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${dns_server} tcp dport 53 accept"
|
|
done
|
|
printf '%s' "${peer}" | jq -c '.allowed_services[]?' | while read -r service; do
|
|
SERVICE_PROXY_IP="$(printf '%s' "${service}" | jq -r '.access_proxy_ip')"
|
|
printf '%s' "${service}" | jq -r '.ports[]?' | while read -r service_port; do
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${SERVICE_PROXY_IP} tcp dport ${service_port} accept"
|
|
done
|
|
done
|
|
printf '%s' "${peer}" | jq -r '.allowed_destinations[]?' | while read -r destination; do
|
|
echo " iifname \"${IFACE}\" ip saddr ${ASSIGNED_IP} ip daddr ${destination} accept"
|
|
done
|
|
done
|
|
|
|
echo " iifname \"${IFACE}\" drop"
|
|
|
|
echo " }"
|
|
if [ "${ENABLE_MASQUERADE}" = "true" ]; then
|
|
echo " chain postrouting {"
|
|
echo " type nat hook postrouting priority 100;"
|
|
echo " oifname \"${UPLINK_IFACE}\" ip saddr ${NETWORK_CIDR} masquerade"
|
|
echo " }"
|
|
fi
|
|
echo "}"
|
|
} > "${NFT_CONF}"
|
|
|
|
if [ -w /proc/sys/net/ipv4/ip_forward ]; then
|
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null || true
|
|
fi
|
|
|
|
nft delete table inet nexavpn >/dev/null 2>&1 || true
|
|
nft -f "${NFT_CONF}"
|
|
|
|
if ip link show "${IFACE}" >/dev/null 2>&1; then
|
|
wg syncconf "${IFACE}" <(wg-quick strip "${WG_CONF}")
|
|
ip link set "${IFACE}" up
|
|
else
|
|
wg-quick up "${WG_CONF}"
|
|
fi
|
|
|
|
echo "Applied WireGuard config from ${WG_CONF}"
|
|
echo "Applied nftables config from ${NFT_CONF}"
|
|
wg show "${IFACE}" latest-handshakes transfer 2>/dev/null || true
|
|
post_telemetry || true
|
|
}
|
|
|
|
post_telemetry() {
|
|
if [ ! -f "${STATE_JSON}" ]; then
|
|
return 0
|
|
fi
|
|
|
|
TELEMETRY_URL="${SYNC_BASE_URL}/${NEXAVPN_GATEWAY_ID}/telemetry"
|
|
TMP_TELEMETRY_JSON="/tmp/nexavpn-gateway-telemetry.json"
|
|
|
|
{
|
|
printf '{\"collected_at\":\"%s\",\"peers\":[' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
FIRST=1
|
|
|
|
while IFS="$(printf '\t')" read -r PUBLIC_KEY _PRESHARED _ENDPOINT _ALLOWED_IPS LATEST_HANDSHAKE RX_BYTES TX_BYTES _KEEPALIVE; do
|
|
if [ -z "${PUBLIC_KEY:-}" ] || [ "${PUBLIC_KEY}" = "private_key" ]; then
|
|
continue
|
|
fi
|
|
|
|
DEVICE_ID="$(jq -r --arg public_key "${PUBLIC_KEY}" '.peers[]? | select(.public_key == $public_key) | .device_id' "${STATE_JSON}" | head -n1)"
|
|
if [ -z "${DEVICE_ID:-}" ] || [ "${DEVICE_ID}" = "null" ]; then
|
|
continue
|
|
fi
|
|
|
|
if [ "${FIRST}" -eq 0 ]; then
|
|
printf ','
|
|
fi
|
|
FIRST=0
|
|
|
|
if [ "${LATEST_HANDSHAKE:-0}" -gt 0 ] 2>/dev/null; then
|
|
printf '{\"device_id\":\"%s\",\"public_key\":\"%s\",\"rx_bytes\":%s,\"tx_bytes\":%s,\"latest_handshake_at\":%s}' \
|
|
"${DEVICE_ID}" "${PUBLIC_KEY}" "${RX_BYTES:-0}" "${TX_BYTES:-0}" "${LATEST_HANDSHAKE}"
|
|
else
|
|
printf '{\"device_id\":\"%s\",\"public_key\":\"%s\",\"rx_bytes\":%s,\"tx_bytes\":%s}' \
|
|
"${DEVICE_ID}" "${PUBLIC_KEY}" "${RX_BYTES:-0}" "${TX_BYTES:-0}"
|
|
fi
|
|
done < <(wg show "${IFACE}" dump 2>/dev/null | tail -n +2)
|
|
|
|
printf ']}'
|
|
} > "${TMP_TELEMETRY_JSON}"
|
|
|
|
curl -fsSL \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Gateway-Bootstrap-Token: ${GATEWAY_BOOTSTRAP_TOKEN}" \
|
|
-X POST \
|
|
--data @"${TMP_TELEMETRY_JSON}" \
|
|
"${TELEMETRY_URL}" >/dev/null
|
|
}
|
|
|
|
while true; do
|
|
apply_bundle || echo "Gateway apply failed; retrying in 15 seconds"
|
|
sleep 15
|
|
done
|