#!/usr/bin/env bash set -euo pipefail BASE_DOMAIN="${NETMAKER_BASE_DOMAIN:-vpn.abyssinfo.com}" NETWORK_ID="${NETMAKER_NETWORK_ID:-homevpn}" ENV_FILE="${NETMAKER_ENV_FILE:-/root/netmaker/netmaker.env}" INSTALL_DIR="${NETCLIENT_INSTALL_DIR:-/usr/local/bin}" NETCLIENT_VERSION="${NETCLIENT_VERSION:-v1.5.1}" ADMIN_PORT="${VPN_ADMIN_PORT:-9090}" NETMAKER_DIR="${NETMAKER_DIR:-/root/netmaker}" CADDY_FILE="${NETMAKER_DIR}/Caddyfile" COMPOSE_FILE="${NETMAKER_DIR}/docker-compose.yml" if [[ "$(id -u)" != "0" ]]; then echo "[AbyssInfo] Please run as root." >&2 exit 1 fi if [[ ! -f "${ENV_FILE}" ]]; then echo "[AbyssInfo] Missing ${ENV_FILE}" >&2 exit 1 fi if [[ ! -f "${CADDY_FILE}" || ! -f "${COMPOSE_FILE}" ]]; then echo "[AbyssInfo] Missing Netmaker compose files in ${NETMAKER_DIR}" >&2 exit 1 fi set -a # shellcheck source=/dev/null . "${ENV_FILE}" set +a if [[ -z "${MASTER_KEY:-}" ]]; then echo "[AbyssInfo] MASTER_KEY is missing in ${ENV_FILE}" >&2 exit 1 fi api_url="https://api.${BASE_DOMAIN}" auth_header="Authorization: Bearer ${MASTER_KEY}" install_netclient() { if command -v netclient >/dev/null 2>&1; then return 0 fi echo "[AbyssInfo] Installing netclient ${NETCLIENT_VERSION} ..." dnf install -y wget jq wireguard-tools >/dev/null local arch arch="$(uname -m)" case "${arch}" in x86_64|amd64) arch="amd64" ;; aarch64|arm64) arch="arm64" ;; *) echo "[AbyssInfo] Unsupported architecture: ${arch}" >&2 exit 1 ;; esac mkdir -p "${INSTALL_DIR}" wget -qO "${INSTALL_DIR}/netclient" "https://github.com/gravitl/netclient/releases/download/${NETCLIENT_VERSION}/netclient-linux-${arch}" chmod +x "${INSTALL_DIR}/netclient" "${INSTALL_DIR}/netclient" install } current_vpn_ip() { if [[ -f /etc/netclient/nodes.json ]]; then jq -r --arg network "${NETWORK_ID}" '.[$network].address.IP // empty' /etc/netclient/nodes.json 2>/dev/null | awk 'NF && $1 !~ /^169\.254\./ { print; exit }' fi } create_server_join_token() { local key_name response_file code token key_name="abyssinfo-server-admin-$(date +%Y%m%d%H%M%S)" response_file="$(mktemp)" local payload payload="$(jq -n \ --arg name "${key_name}" \ --arg network "${NETWORK_ID}" \ '{ name: $name, tags: ["abyssinfo-server-admin"], type: 2, uses_remaining: 1, unlimited: false, expiration: 0, networks: [$network], groups: ["homevpn", "server", "admin"], auto_egress: false, auto_assign_gw: false }')" code="$(curl -ksS -o "${response_file}" -w "%{http_code}" \ -X POST "${api_url}/api/v1/enrollment-keys" \ -H "${auth_header}" \ -H "Content-Type: application/json" \ --data "${payload}" || true)" if [[ "${code}" -lt 200 || "${code}" -ge 300 ]]; then echo "[AbyssInfo] Failed to create server enrollment token. HTTP ${code}" >&2 head -c 500 "${response_file}" >&2 || true echo >&2 rm -f "${response_file}" exit 1 fi token="$(jq -r '.token // .data.token // empty' "${response_file}")" rm -f "${response_file}" if [[ -z "${token}" || "${token}" == "null" ]]; then echo "[AbyssInfo] Netmaker did not return an enrollment token." >&2 exit 1 fi printf "%s" "${token}" } join_server_to_vpn() { local ip token ip="$(current_vpn_ip || true)" if [[ -n "${ip}" ]]; then echo "[AbyssInfo] Server is already in ${NETWORK_ID}: ${ip}" systemctl enable --now netclient >/dev/null 2>&1 || true return 0 fi echo "[AbyssInfo] Joining server to Netmaker network ${NETWORK_ID} ..." token="$(create_server_join_token)" netclient join -t "${token}" >/tmp/abyssinfo-server-netclient-join.log 2>&1 || { echo "[AbyssInfo] netclient join failed. Last log lines:" >&2 tail -n 80 /tmp/abyssinfo-server-netclient-join.log >&2 || true exit 1 } systemctl enable --now netclient >/dev/null 2>&1 || true for _ in $(seq 1 90); do ip="$(current_vpn_ip || true)" if [[ -n "${ip}" ]]; then echo "[AbyssInfo] Server VPN IP: ${ip}" return 0 fi sleep 2 done echo "[AbyssInfo] Server joined, but VPN IP was not detected." >&2 exit 1 } configure_caddy_vpn_admin() { local vpn_ip="$1" echo "[AbyssInfo] Configuring Caddy VPN-only admin entry on ${vpn_ip}:${ADMIN_PORT} ..." cp -a "${CADDY_FILE}" "${CADDY_FILE}.bak.$(date +%Y%m%d%H%M%S)" cp -a "${COMPOSE_FILE}" "${COMPOSE_FILE}.bak.$(date +%Y%m%d%H%M%S)" VPN_IP="${vpn_ip}" VPN_ADMIN_PORT="${ADMIN_PORT}" python3 - <<'PY' from pathlib import Path import os import re caddy_path = Path("/root/netmaker/Caddyfile") compose_path = Path("/root/netmaker/docker-compose.yml") vpn_ip = os.environ["VPN_IP"] port = os.environ["VPN_ADMIN_PORT"] block = f""" # AbyssInfo VPN-only Netmaker Dashboard :{port} {{ \theader {{ \t\tX-Robots-Tag "none" \t\t-Server \t}} \treverse_proxy http://netmaker-ui }} """ caddy = caddy_path.read_text(encoding="utf-8") caddy = re.sub( r"\n# AbyssInfo VPN-only Netmaker Dashboard\n:\d+ \{.*?\n\}\n?", "\n", caddy, flags=re.S, ).rstrip() + block caddy_path.write_text(caddy, encoding="utf-8") compose = compose_path.read_text(encoding="utf-8") mapping = f' - "{vpn_ip}:{port}:{port}/tcp"' lines = [] inserted = False for line in compose.splitlines(): if f":{port}:{port}/tcp" in line: if not inserted: lines.append(mapping) inserted = True continue lines.append(line) if '"50051:50051"' in line and not inserted: lines.append(mapping) inserted = True if not inserted: raise SystemExit("failed to insert caddy port mapping after 50051") compose_path.write_text("\n".join(lines) + "\n", encoding="utf-8") PY (cd "${NETMAKER_DIR}" && docker compose up -d caddy) sleep 2 curl -fsSI --connect-timeout 10 "http://${vpn_ip}:${ADMIN_PORT}" >/dev/null } main() { install_netclient join_server_to_vpn local vpn_ip vpn_ip="$(current_vpn_ip)" configure_caddy_vpn_admin "${vpn_ip}" echo "[AbyssInfo] VPN-only Netmaker dashboard:" echo " http://${vpn_ip}:${ADMIN_PORT}" echo "[AbyssInfo] Only VPN clients with route to ${NETWORK_ID} should be able to reach this address." } main "$@"