#!/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}" 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)" 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\":[\"10.20.0.53\"]}" \ "${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}" <> "${WG_GENERATED}" < "${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