Initial commit: abyssVpn project

- Tauri desktop app (apps/desktop)
- Python Django API service (services/api)
- Deployment scripts and docs
This commit is contained in:
arthur_chen
2026-05-15 09:06:10 +08:00
commit f69efb67ad
146 changed files with 18039 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${REMOTE_APP_DIR:-/opt/abyssinfo-home-vpn}"
API_DIR="${APP_DIR}/api"
VENV_DIR="${APP_DIR}/.venv-api"
ZIP_FILE="${API_ZIP_FILE:-/tmp/abyssinfo-api.zip}"
API_DOMAIN="${API_DOMAIN:-vpn-api.nm.101-96-214-81.sslip.io}"
API_LEGACY_DOMAIN="${API_LEGACY_DOMAIN:-vpn-api.nm.101-96-214-81.nip.io}"
API_PORT="${API_PORT:-8000}"
NETMAKER_DOMAIN="${NETMAKER_BASE_DOMAIN:-nm.101-96-214-81.nip.io}"
echo "[AbyssInfo] Deploying Django API"
echo "[AbyssInfo] API domain: ${API_DOMAIN}"
if [[ "$(id -u)" != "0" ]]; then
echo "[AbyssInfo] Please run as root." >&2
exit 1
fi
if [[ ! -f "${ZIP_FILE}" ]]; then
echo "[AbyssInfo] Missing ${ZIP_FILE}" >&2
exit 1
fi
echo "[AbyssInfo] Installing Python runtime ..."
dnf install -y python3.12 python3.12-pip unzip
mkdir -p "${API_DIR}"
PREVIOUS_ENV="/tmp/abyssinfo-api.previous.env"
if [[ -f "${API_DIR}/.env" ]]; then
cp "${API_DIR}/.env" "${PREVIOUS_ENV}"
else
rm -f "${PREVIOUS_ENV}"
fi
find "${API_DIR}" -mindepth 1 \
! -name db.sqlite3 \
! -path "${API_DIR}/db.sqlite3" \
-exec rm -rf {} +
echo "[AbyssInfo] Extracting API package ..."
set +e
unzip -q "${ZIP_FILE}" -d "${API_DIR}"
UNZIP_EXIT=$?
set -e
if [[ "${UNZIP_EXIT}" -gt 1 ]]; then
echo "[AbyssInfo] Failed to unzip API package. Exit code ${UNZIP_EXIT}" >&2
exit "${UNZIP_EXIT}"
fi
if [[ ! -f "${API_DIR}/.env" ]]; then
echo "[AbyssInfo] Missing ${API_DIR}/.env" >&2
exit 1
fi
echo "[AbyssInfo] Normalizing production env ..."
python3.12 - <<PY
from pathlib import Path
import secrets
env_path = Path("${API_DIR}/.env")
previous_env_path = Path("${PREVIOUS_ENV}")
values = {}
for candidate in [previous_env_path, env_path]:
if not candidate.exists():
continue
for line in candidate.read_text(encoding="utf-8").splitlines():
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.lstrip("\\ufeff")
if value or key not in values:
values[key] = value
if not values.get("DJANGO_SECRET_KEY"):
values["DJANGO_SECRET_KEY"] = secrets.token_urlsafe(48)
if not values.get("JWT_SECRET"):
values["JWT_SECRET"] = secrets.token_urlsafe(48)
values["DJANGO_DEBUG"] = "false"
values["DJANGO_ALLOWED_HOSTS"] = "127.0.0.1,localhost,${API_DOMAIN},${API_LEGACY_DOMAIN}"
values["CORS_ALLOWED_ORIGINS"] = "http://localhost:5173,http://127.0.0.1:5173,http://tauri.localhost,https://tauri.localhost,tauri://localhost,https://${API_DOMAIN}"
values["FEISHU_REDIRECT_URI"] = "https://${API_DOMAIN}/auth/feishu/callback"
ordered = [
"DJANGO_SECRET_KEY", "DJANGO_DEBUG", "DJANGO_ALLOWED_HOSTS", "CORS_ALLOWED_ORIGINS",
"FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_REDIRECT_URI", "FEISHU_EVENT_VERIFICATION_TOKEN", "TAURI_DEEP_LINK_SCHEME",
"JWT_SECRET", "JWT_EXPIRES_SECONDS", "OAUTH_STATE_EXPIRES_SECONDS", "LOGIN_CODE_EXPIRES_SECONDS",
"NETMAKER_BASE_URL", "NETMAKER_API_TOKEN", "NETMAKER_NETWORK_ID",
"NETMAKER_ENROLLMENT_TAGS", "NETMAKER_ENROLLMENT_TTL_SECONDS",
]
env_path.write_text("\\n".join(f"{key}={values.get(key, '')}" for key in ordered) + "\\n", encoding="utf-8")
PY
echo "[AbyssInfo] Creating virtual environment ..."
if [[ ! -x "${VENV_DIR}/bin/python" ]]; then
python3.12 -m venv "${VENV_DIR}"
fi
"${VENV_DIR}/bin/python" -m pip install --upgrade pip
"${VENV_DIR}/bin/pip" install -r "${API_DIR}/requirements.txt"
echo "[AbyssInfo] Running Django checks and migrations ..."
cd "${API_DIR}"
"${VENV_DIR}/bin/python" manage.py check
"${VENV_DIR}/bin/python" manage.py migrate --noinput
"${VENV_DIR}/bin/python" manage.py collectstatic --noinput
echo "[AbyssInfo] Installing systemd service ..."
cat >/etc/systemd/system/abyssinfo-api.service <<SERVICE
[Unit]
Description=AbyssInfo Home VPN Django API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=${API_DIR}
Environment=PYTHONUNBUFFERED=1
ExecStart=${VENV_DIR}/bin/gunicorn config.wsgi:application --bind 0.0.0.0:${API_PORT} --workers 2 --timeout 60
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload
systemctl enable --now abyssinfo-api
systemctl restart abyssinfo-api
echo "[AbyssInfo] Updating Netmaker Caddy reverse proxy ..."
CADDYFILE="/root/netmaker/Caddyfile"
if [[ ! -f "${CADDYFILE}" ]]; then
echo "[AbyssInfo] Missing ${CADDYFILE}" >&2
exit 1
fi
if ! grep -q "https://${API_DOMAIN}" "${CADDYFILE}"; then
cat >>"${CADDYFILE}" <<CADDY
# AbyssInfo Django API
https://${API_DOMAIN} {
reverse_proxy http://host.docker.internal:${API_PORT}
}
CADDY
fi
(cd /root/netmaker && docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile)
echo "[AbyssInfo] Verifying API health ..."
curl -fsS "http://127.0.0.1:${API_PORT}/health/" >/dev/null
curl -kfsS "https://${API_DOMAIN}/health/" >/dev/null
echo "[AbyssInfo] Django API deployed:"
echo " https://${API_DOMAIN}/health/"
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DOMAIN="${NETMAKER_BASE_DOMAIN:-nm.101-96-214-81.nip.io}"
ADMIN_EMAIL="${NETMAKER_ADMIN_EMAIL:-admin@abyssinfo.local}"
ASSET_PORT="${NETMAKER_ASSET_PORT:-18080}"
ASSET_DIR="/tmp/netmaker-assets"
QUICK_SCRIPT="/tmp/nm-quick.sh"
echo "[AbyssInfo] Deploying Netmaker CE"
echo "[AbyssInfo] Base domain: ${BASE_DOMAIN}"
if [[ "$(id -u)" != "0" ]]; then
echo "[AbyssInfo] Please run as root." >&2
exit 1
fi
if [[ ! -f "${QUICK_SCRIPT}" ]]; then
echo "[AbyssInfo] Missing ${QUICK_SCRIPT}" >&2
exit 1
fi
if [[ ! -d "${ASSET_DIR}" ]]; then
echo "[AbyssInfo] Missing ${ASSET_DIR}" >&2
exit 1
fi
echo "[AbyssInfo] Installing Netmaker quick-install dependencies ..."
dnf install -y wget jq bind-utils gawk wireguard-tools python3
echo "[AbyssInfo] Ensuring Docker is running ..."
mkdir -p /etc/docker
cat >/etc/docker/daemon.json <<'JSON'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.m.daocloud.io",
"https://hub-mirror.c.163.com"
]
}
JSON
systemctl daemon-reload
systemctl restart docker
systemctl enable --now docker
echo "[AbyssInfo] Stopping Nginx to free ports 80/443 for Netmaker Caddy ..."
systemctl disable --now nginx 2>/dev/null || true
health_check() {
local dashboard_url="https://dashboard.${BASE_DOMAIN}"
if [[ ! -f /root/netmaker/docker-compose.yml ]]; then
echo "[AbyssInfo] Missing /root/netmaker/docker-compose.yml" >&2
return 1
fi
echo "[AbyssInfo] Netmaker containers:"
(cd /root/netmaker && docker compose ps)
echo "[AbyssInfo] Checking dashboard endpoint: ${dashboard_url}"
curl -kfsSI --connect-timeout 15 "${dashboard_url}" >/dev/null
}
apply_internal_only_defaults() {
local env_file="/root/netmaker/netmaker.env"
if [[ ! -f "${env_file}" ]]; then
return 0
fi
echo "[AbyssInfo] Applying internal-only Netmaker client defaults ..."
python3 - <<'PY'
from pathlib import Path
env_path = Path("/root/netmaker/netmaker.env")
values = {}
order = []
for line in env_path.read_text(encoding="utf-8").splitlines():
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key not in values:
order.append(key)
values[key] = value
for key, value in {
"DNS_MODE": "off",
"MANAGE_DNS": "false",
}.items():
if key not in values:
order.append(key)
values[key] = value
env_path.write_text("\n".join(f"{key}={values[key]}" for key in order) + "\n", encoding="utf-8")
PY
}
if [[ "${NETMAKER_FORCE_REINSTALL:-0}" == "1" ]]; then
echo "[AbyssInfo] Force reinstall requested. Removing previous Netmaker deployment ..."
if [[ -f /root/netmaker/docker-compose.yml ]]; then
(cd /root/netmaker && docker compose down --volumes || true)
fi
rm -rf /root/netmaker /etc/netmaker /etc/netclient
elif [[ -f /root/netmaker/docker-compose.yml ]]; then
echo "[AbyssInfo] Existing Netmaker deployment found. Starting services and running health check ..."
apply_internal_only_defaults
(cd /root/netmaker && docker compose up -d)
if health_check; then
echo "[AbyssInfo] Existing Netmaker deployment is healthy."
echo "[AbyssInfo] URLs:"
echo " Dashboard: https://dashboard.${BASE_DOMAIN}"
echo " API: https://api.${BASE_DOMAIN}"
echo " Broker: wss://broker.${BASE_DOMAIN}"
exit 0
fi
echo "[AbyssInfo] Existing deployment is not healthy. Set NETMAKER_FORCE_REINSTALL=1 to rebuild it." >&2
exit 1
fi
echo "[AbyssInfo] Preparing quick installer ..."
mkdir -p /root
cp "${QUICK_SCRIPT}" /root/nm-quick.sh
chmod +x /root/nm-quick.sh
# Use uploaded cached assets instead of pulling compose files from GitHub on the server.
sed -i "s|local BASE_URL=\"https://raw.githubusercontent.com/gravitl/netmaker/\$BRANCH\"|local BASE_URL=\"http://127.0.0.1:${ASSET_PORT}\"|g" /root/nm-quick.sh
echo "[AbyssInfo] Starting temporary local asset server ..."
pkill -f "python3 -m http.server ${ASSET_PORT}" 2>/dev/null || true
cd "${ASSET_DIR}"
python3 -m http.server "${ASSET_PORT}" --bind 127.0.0.1 >/tmp/abyssinfo-netmaker-assets.log 2>&1 &
ASSET_PID=$!
trap 'kill ${ASSET_PID} 2>/dev/null || true' EXIT
sleep 2
curl -fsS "http://127.0.0.1:${ASSET_PORT}/compose/docker-compose.yml" >/dev/null
echo "[AbyssInfo] Running Netmaker quick installer ..."
cd /root
export NM_DOMAIN="${BASE_DOMAIN}"
export NM_EMAIL="${ADMIN_EMAIL}"
export NM_SKIP_DEPS=1
# Input sequence for nm-quick CE:
# 1 = use auto/generated domain, email, y = confirm settings.
set +e
printf "1\n%s\ny\n" "${ADMIN_EMAIL}" | /root/nm-quick.sh -c
QUICK_EXIT=$?
set -e
echo "[AbyssInfo] Netmaker files:"
ls -la /root/netmaker
if ! health_check; then
echo "[AbyssInfo] Netmaker health check failed after quick installer exit code ${QUICK_EXIT}." >&2
exit 1
fi
if [[ "${QUICK_EXIT}" != "0" ]]; then
echo "[AbyssInfo] Quick installer exited with ${QUICK_EXIT}, but Netmaker services are healthy. Continuing."
fi
apply_internal_only_defaults
(cd /root/netmaker && docker compose up -d)
echo "[AbyssInfo] URLs:"
echo " Dashboard: https://dashboard.${BASE_DOMAIN}"
echo " API: https://api.${BASE_DOMAIN}"
echo " Broker: wss://broker.${BASE_DOMAIN}"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DOMAIN="${NETMAKER_BASE_DOMAIN:-nm.101-96-214-81.nip.io}"
ENV_FILE="${NETMAKER_ENV_FILE:-/root/netmaker/netmaker.env}"
NETWORK_ID="${NETMAKER_NETWORK_ID:-homevpn}"
NETWORK_RANGE="${NETMAKER_NETWORK_RANGE:-100.88.0.0/24}"
if [[ ! -f "${ENV_FILE}" ]]; then
echo "[AbyssInfo] Missing ${ENV_FILE}" >&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}"
echo "[AbyssInfo] Checking Netmaker network: ${NETWORK_ID}"
networks_json="$(curl -ksS -H "${auth_header}" "${api_url}/api/networks")"
if echo "${networks_json}" | jq -e --arg id "${NETWORK_ID}" '.[]? | select(.netid == $id)' >/dev/null; then
echo "[AbyssInfo] Network already exists: ${NETWORK_ID}"
else
echo "[AbyssInfo] Creating Netmaker network: ${NETWORK_ID} (${NETWORK_RANGE})"
payload="$(jq -n \
--arg netid "${NETWORK_ID}" \
--arg addressrange "${NETWORK_RANGE}" \
'{
netid: $netid,
addressrange: $addressrange,
defaultacl: "yes",
defaultmtu: 1420,
defaultlistenport: 51821,
defaultkeepalive: 20,
auto_join: true
}')"
response_file="$(mktemp)"
code="$(curl -ksS -o "${response_file}" -w "%{http_code}" \
-X POST "${api_url}/api/networks" \
-H "${auth_header}" \
-H "Content-Type: application/json" \
--data "${payload}" || true)"
if [[ "${code}" -lt 200 || "${code}" -ge 300 ]]; then
echo "[AbyssInfo] Failed to create network. HTTP ${code}" >&2
head -c 500 "${response_file}" >&2 || true
echo >&2
rm -f "${response_file}"
exit 1
fi
rm -f "${response_file}"
fi
echo "[AbyssInfo] Current networks:"
curl -ksS -H "${auth_header}" "${api_url}/api/networks" \
| jq -r '.[] | "- \(.netid) \(.addressrange // "")"'
echo "[AbyssInfo] Backend config:"
echo "NETMAKER_BASE_URL=${api_url}"
echo "NETMAKER_NETWORK_ID=${NETWORK_ID}"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[AbyssInfo] Server environment installer"
echo "[AbyssInfo] User: $(whoami)"
echo "[AbyssInfo] OS:"
cat /etc/os-release
if [[ "$(id -u)" != "0" ]]; then
echo "[AbyssInfo] Please run as root." >&2
exit 1
fi
echo "[AbyssInfo] Installing base packages ..."
dnf install -y dnf-plugins-core ca-certificates curl git tar gzip unzip policycoreutils-python-utils
if ! command -v docker >/dev/null 2>&1; then
echo "[AbyssInfo] Adding Docker CE repository ..."
rm -f /etc/yum.repos.d/docker-ce.repo
dnf clean all
if ! dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo; then
echo "[AbyssInfo] Aliyun Docker repository failed, using official repository ..."
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
dnf clean all
dnf makecache --disablerepo='*' --enablerepo='docker-ce-stable' || true
echo "[AbyssInfo] Installing Docker Engine and Compose plugin ..."
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else
echo "[AbyssInfo] Docker already installed."
fi
systemctl enable --now docker
if ! command -v nginx >/dev/null 2>&1; then
echo "[AbyssInfo] Installing Nginx ..."
dnf install -y nginx
else
echo "[AbyssInfo] Nginx already installed."
fi
mkdir -p /opt/abyssinfo-home-vpn
if systemctl is-active --quiet firewalld; then
echo "[AbyssInfo] Configuring running firewalld ..."
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
else
echo "[AbyssInfo] firewalld is not running; skipping OS firewall changes."
fi
echo "[AbyssInfo] Versions:"
docker --version
docker compose version
git --version
nginx -v
echo "[AbyssInfo] Environment ready."
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DOMAIN="${NETMAKER_BASE_DOMAIN:-nm.101-96-214-81.nip.io}"
ENV_FILE="${NETMAKER_ENV_FILE:-/root/netmaker/netmaker.env}"
if [[ ! -f "${ENV_FILE}" ]]; then
echo "[AbyssInfo] Missing ${ENV_FILE}" >&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
echo "[AbyssInfo] Probing Netmaker API routes"
for path in \
/api/networks \
/api/v1/networks \
/api/v1/enrollment-keys \
/api/server/getserverinfo \
/api/users/adm/hasadmin \
/api/v1/server/getserverinfo
do
response_file="$(mktemp)"
code="$(curl -ksS -o "${response_file}" -w "%{http_code}" \
-H "Authorization: Bearer ${MASTER_KEY}" \
"https://api.${BASE_DOMAIN}${path}" || true)"
printf "%s %s " "${code}" "${path}"
head -c 180 "${response_file}" | tr '\n' ' '
echo
rm -f "${response_file}"
done
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
GITEA_DOMAIN="${GITEA_DOMAIN:-gitea.abyssinfo.com}"
GITEA_WEB_UPSTREAM="${GITEA_WEB_UPSTREAM:-100.88.0.1:3001}"
GITEA_OIDC_UPSTREAM="${GITEA_OIDC_UPSTREAM:-100.88.0.1:3080}"
NETMAKER_DIR="${NETMAKER_DIR:-/root/netmaker}"
CADDYFILE="${CADDYFILE:-${NETMAKER_DIR}/Caddyfile}"
echo "[AbyssInfo] Configuring public Gitea reverse proxy"
echo "[AbyssInfo] Domain: ${GITEA_DOMAIN}"
echo "[AbyssInfo] Web upstream: http://${GITEA_WEB_UPSTREAM}"
echo "[AbyssInfo] OIDC upstream: http://${GITEA_OIDC_UPSTREAM}"
if [[ "$(id -u)" != "0" ]]; then
echo "[AbyssInfo] Please run as root." >&2
exit 1
fi
if [[ ! -f "${CADDYFILE}" ]]; then
echo "[AbyssInfo] Missing ${CADDYFILE}" >&2
exit 1
fi
echo "[AbyssInfo] Checking upstreams from server ..."
curl -fsS --connect-timeout 3 --max-time 8 "http://${GITEA_OIDC_UPSTREAM}/oidc/.well-known/openid-configuration" >/dev/null \
&& echo "[AbyssInfo] OIDC upstream OK" \
|| echo "[AbyssInfo] WARN: OIDC upstream is not reachable from server"
curl -fsSI --connect-timeout 3 --max-time 8 "http://${GITEA_WEB_UPSTREAM}/user/login" >/dev/null \
&& echo "[AbyssInfo] Gitea web upstream OK" \
|| echo "[AbyssInfo] WARN: Gitea web upstream is not reachable from server"
python3 - <<PY
from pathlib import Path
caddyfile = Path("${CADDYFILE}")
domain = "${GITEA_DOMAIN}"
web = "${GITEA_WEB_UPSTREAM}"
oidc = "${GITEA_OIDC_UPSTREAM}"
start = "# AbyssInfo Gitea public access begin"
end = "# AbyssInfo Gitea public access end"
block = f"""
{start}
https://{domain} {{
\tencode gzip zstd
\thandle /oidc/* {{
\t\treverse_proxy http://{oidc} {{
\t\t\theader_up Host {{host}}
\t\t\theader_up X-Forwarded-Host {{host}}
\t\t\theader_up X-Forwarded-Proto https
\t\t\theader_up X-Forwarded-Ssl on
\t\t}}
\t}}
\thandle /auth/* {{
\t\treverse_proxy http://{oidc} {{
\t\t\theader_up Host {{host}}
\t\t\theader_up X-Forwarded-Host {{host}}
\t\t\theader_up X-Forwarded-Proto https
\t\t\theader_up X-Forwarded-Ssl on
\t\t}}
\t}}
\thandle /interaction/* {{
\t\treverse_proxy http://{oidc} {{
\t\t\theader_up Host {{host}}
\t\t\theader_up X-Forwarded-Host {{host}}
\t\t\theader_up X-Forwarded-Proto https
\t\t\theader_up X-Forwarded-Ssl on
\t\t}}
\t}}
\thandle {{
\t\treverse_proxy http://{web} {{
\t\t\theader_up Host {{host}}
\t\t\theader_up X-Forwarded-Host {{host}}
\t\t\theader_up X-Forwarded-Proto https
\t\t\theader_up X-Forwarded-Ssl on
\t\t}}
\t}}
}}
{end}
""".strip()
text = caddyfile.read_text(encoding="utf-8")
if start in text and end in text:
before, rest = text.split(start, 1)
_old, after = rest.split(end, 1)
text = before.rstrip() + "\n\n" + block + "\n" + after.lstrip()
else:
text = text.rstrip() + "\n\n" + block + "\n"
caddyfile.write_text(text, encoding="utf-8")
PY
echo "[AbyssInfo] Reloading Caddy ..."
(cd "${NETMAKER_DIR}" && docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile)
echo "[AbyssInfo] Checking public endpoints ..."
curl -kfsS --connect-timeout 5 --max-time 12 "https://${GITEA_DOMAIN}/oidc/.well-known/openid-configuration" >/dev/null \
&& echo "[AbyssInfo] Public OIDC endpoint OK" \
|| echo "[AbyssInfo] WARN: Public OIDC endpoint is not ready. Check DNS and upstream reachability."
curl -kfsSI --connect-timeout 5 --max-time 12 "https://${GITEA_DOMAIN}/user/login" >/dev/null \
&& echo "[AbyssInfo] Public Gitea login endpoint OK" \
|| echo "[AbyssInfo] WARN: Public Gitea login endpoint is not ready. Check DNS and upstream reachability."
echo "[AbyssInfo] Gitea public access configured:"
echo " https://${GITEA_DOMAIN}/oidc/.well-known/openid-configuration -> http://${GITEA_OIDC_UPSTREAM}"
echo " https://${GITEA_DOMAIN}/user/login -> http://${GITEA_WEB_UPSTREAM}"
+227
View File
@@ -0,0 +1,227 @@
#!/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 "$@"