Initial commit: abyssVpn project
- Tauri desktop app (apps/desktop) - Python Django API service (services/api) - Deployment scripts and docs
This commit is contained in:
@@ -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/"
|
||||
@@ -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}"
|
||||
@@ -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}"
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user