Compare commits
11 Commits
e44bc4f660
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4767730119 | |||
| 521d77f0cc | |||
| 87f9f865ae | |||
| bf5b914ddc | |||
| 84d53c801b | |||
| cfcb1afcec | |||
| fafd573a28 | |||
| c4be0174bf | |||
| 10d357b8ec | |||
| d8363667e0 | |||
| c835ac983a |
@@ -0,0 +1,24 @@
|
|||||||
|
# Локальный dev/деплой-конфиг для Makefile (rsync/ssh-деплой на прод/dev-ноду,
|
||||||
|
# ADB для тестов на Android, --require-auth для debug-server). Скопировать в
|
||||||
|
# .env и заполнить реальными значениями — .env уже в .gitignore, коммитить
|
||||||
|
# нельзя.
|
||||||
|
|
||||||
|
# --- Прод-нода (make deploy / make restart и т.п.) ---
|
||||||
|
SERVER_IP=CHANGE_ME_PROD_NODE_IP
|
||||||
|
REMOTE_USER=root
|
||||||
|
REMOTE_PATH=/root/netr-core
|
||||||
|
SERVICE_NAME=netrunner-server
|
||||||
|
|
||||||
|
# --- Dev-нода (make deploy-dev и т.п.) ---
|
||||||
|
DEV_IP=CHANGE_ME_DEV_NODE_IP
|
||||||
|
|
||||||
|
# --- Android (ADB по Wi-Fi + сборка нативной либы для vpn-plugin) ---
|
||||||
|
ANDROID_ADB_HOST=CHANGE_ME_PHONE_LOCAL_IP
|
||||||
|
ANDROID_BUILD_SRC=/home/CHANGE_ME/netrunner/netrunner-proxy/gen
|
||||||
|
ANDROID_PROJECT_LIBS=/home/CHANGE_ME/netrunner/netrunner-app/src-tauri/src/plugins/vpn-plugin/android/src/main/jniLibs
|
||||||
|
|
||||||
|
# --- Дев-авторизация debug-server (make debug-server и т.п.) ---
|
||||||
|
# Тот же секрет, что и PROXY_INTERNAL_SECRET у netrunner-backend (.env.dev) —
|
||||||
|
# без совпадения бэкенд отклонит запросы валидации токена/отчёта о трафике.
|
||||||
|
PROXY_INTERNAL_SECRET=CHANGE_ME_SAME_AS_BACKEND_PROXY_INTERNAL_SECRET
|
||||||
|
DEV_BACKEND_URL=http://localhost:8080
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Собирает netrunner-server (release-бинарник) и пушит Docker-образ во
|
||||||
|
# встроенный Container Registry самой Gitea (gitea.netrunner-vpn.com), не
|
||||||
|
# ghcr.io. Dockerfile сам не компилирует Rust — он просто копирует уже
|
||||||
|
# готовый target/release/netrunner-server (см. Dockerfile), поэтому cargo
|
||||||
|
# build идёт отдельным шагом перед docker build, не внутри него.
|
||||||
|
#
|
||||||
|
# Нужен секрет GT_REGISTRY_TOKEN — токен Gitea с правом write:package,
|
||||||
|
# тот же, что и в netrunner-backend/netrunner-landing (пакеты в Gitea
|
||||||
|
# принадлежат аккаунту, не репозиторию, но значение нужно продублировать в
|
||||||
|
# Settings каждого из репозиториев).
|
||||||
|
name: Build & Push Proxy Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ["v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release -p netrunner-server
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Gitea Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.netrunner-vpn.com
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GT_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: gitea.netrunner-vpn.com/nineap/netrunner-proxy:latest
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Деплой на прокси-ноды из nodes.json. Список серверов — сам файл в корне
|
||||||
|
# репозитория (name/host/proxy_port/decoy_host на каждую ноду), поддерживать
|
||||||
|
# его руками при добавлении/выводе серверов.
|
||||||
|
#
|
||||||
|
# Запуск только вручную (workflow_dispatch) — параметр target: имя ноды из
|
||||||
|
# nodes.json (поле "name") или "all" для обновления сразу всех. Job "resolve"
|
||||||
|
# фильтрует список по этому параметру и отдаёт job'у "deploy" как matrix —
|
||||||
|
# каждая нода тогда видна в прогоне отдельной строкой, с отдельным логом.
|
||||||
|
# GH/Gitea Actions не поддерживают выпадающий список (type: choice) с
|
||||||
|
# вариантами из файла — только статичные в самом yaml, а список нод меняется
|
||||||
|
# независимо от workflow, поэтому просто текстовое поле, значение — точное
|
||||||
|
# имя из nodes.json.
|
||||||
|
#
|
||||||
|
# Каждый деплой пересоздаёт контейнер (pull нового образа + rm + run) — не
|
||||||
|
# просто restart, иначе новый образ не подхватится, раз контейнер уже создан
|
||||||
|
# со старым слоем.
|
||||||
|
#
|
||||||
|
# Секреты (общие на все ноды — один master-ключ, как и при первичном
|
||||||
|
# провижининге через netrunner-backend, см. NodeService::provision_node_via_ssh):
|
||||||
|
# - PROXY_NODES_SSH_KEY — приватный SSH-ключ (root на все ноды)
|
||||||
|
# - PROXY_INTERNAL_SECRET — тот же секрет, что и PROXY_INTERNAL_SECRET у бэкенда
|
||||||
|
# - GT_REGISTRY_TOKEN — тот же, что и в build.yml (Container Registry Gitea,
|
||||||
|
# не ghcr.io — каждая нода тянет образ напрямую с gitea.netrunner-vpn.com)
|
||||||
|
#
|
||||||
|
# Repo Variable (vars.*, не secret — публичный URL, не даёт доступа сам по себе):
|
||||||
|
# - BACKEND_URL — публичный адрес control plane (WEB_APP_BASE_URL у бэкенда)
|
||||||
|
#
|
||||||
|
# Job update-observability-targets (см. ниже) обновляет список scrape-таргетов
|
||||||
|
# для центрального Prometheus (netrunner-data/docker-compose.observability.yml,
|
||||||
|
# job "netrunner-proxy-nodes") при КАЖДОМ прогоне (независимо от input target,
|
||||||
|
# т.к. использует полный nodes.json) — ему нужны СВОИ копии секретов/vars
|
||||||
|
# VPS с данными, продублировать в Settings этого репозитория (те же значения,
|
||||||
|
# что уже есть в netrunner-data):
|
||||||
|
# - vars.DATA_VPS_IP
|
||||||
|
# - secrets.DATA_SSH_PRIVATE_KEY
|
||||||
|
name: Deploy Proxy Nodes
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target:
|
||||||
|
description: 'Имя ноды из nodes.json ("name"), или "all" — обновить сразу все'
|
||||||
|
required: true
|
||||||
|
default: "all"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
resolve:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
targets: ${{ steps.filter.outputs.targets }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Filter nodes.json by input
|
||||||
|
id: filter
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
command -v jq >/dev/null || (apt-get update && apt-get install -y jq)
|
||||||
|
|
||||||
|
target="${{ github.event.inputs.target }}"
|
||||||
|
if [ "$target" = "all" ]; then
|
||||||
|
filtered=$(jq -c '.' nodes.json)
|
||||||
|
else
|
||||||
|
filtered=$(jq -c --arg n "$target" '[.[] | select(.name == $n)]' nodes.json)
|
||||||
|
fi
|
||||||
|
|
||||||
|
count=$(echo "$filtered" | jq 'length')
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
echo "Нода '$target' не найдена в nodes.json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "targets=$filtered" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: resolve
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include: ${{ fromJson(needs.resolve.outputs.targets) }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Deploy to ${{ matrix.name }} (${{ matrix.host }})
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ matrix.host }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.PROXY_NODES_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
echo "🚀 Деплой на ${{ matrix.name }} (${{ matrix.host }}:${{ matrix.proxy_port }})"
|
||||||
|
|
||||||
|
echo "${{ secrets.GT_REGISTRY_TOKEN }}" | docker login gitea.netrunner-vpn.com -u ${{ github.actor }} --password-stdin
|
||||||
|
docker pull gitea.netrunner-vpn.com/nineap/netrunner-proxy:latest
|
||||||
|
|
||||||
|
docker stop netrunner-proxy 2>/dev/null || true
|
||||||
|
docker rm netrunner-proxy 2>/dev/null || true
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name netrunner-proxy \
|
||||||
|
--restart always \
|
||||||
|
--network host \
|
||||||
|
--cap-add NET_ADMIN --cap-add NET_RAW --cap-add DAC_OVERRIDE \
|
||||||
|
--device /dev/net/tun:/dev/net/tun \
|
||||||
|
-e PROXY_INTERNAL_SECRET="${{ secrets.PROXY_INTERNAL_SECRET }}" \
|
||||||
|
gitea.netrunner-vpn.com/nineap/netrunner-proxy:latest \
|
||||||
|
./netrunner-proxy \
|
||||||
|
--port "${{ matrix.proxy_port }}" \
|
||||||
|
--decoy-host "${{ matrix.decoy_host }}" \
|
||||||
|
--require-auth \
|
||||||
|
--backend-url "${{ vars.BACKEND_URL }}" \
|
||||||
|
--health-port 9091 \
|
||||||
|
--metrics-port 9093
|
||||||
|
|
||||||
|
docker image prune -f
|
||||||
|
echo "✅ ${{ matrix.name }} обновлена."
|
||||||
|
|
||||||
|
# Независимо от того, какая конкретно нода деплоилась (target может быть
|
||||||
|
# одной нодой, не "all") — список scrape-таргетов всегда генерится из ПОЛНОГО
|
||||||
|
# nodes.json, чтобы центральный Prometheus не терял из виду остальные ноды.
|
||||||
|
update-observability-targets:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate targets/nodes.json from nodes.json
|
||||||
|
run: node scripts/generate-prometheus-targets.mjs nodes.json > nodes.targets.json
|
||||||
|
|
||||||
|
- name: Upload to observability VPS
|
||||||
|
uses: appleboy/scp-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DATA_VPS_IP }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.DATA_SSH_PRIVATE_KEY }}
|
||||||
|
source: "nodes.targets.json"
|
||||||
|
target: "/root/netrunner-data/observability/targets"
|
||||||
|
strip_components: 0
|
||||||
|
|
||||||
|
- name: Rename to nodes.json (file_sd_configs ждёт именно это имя)
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ vars.DATA_VPS_IP }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.DATA_SSH_PRIVATE_KEY }}
|
||||||
|
script: mv /root/netrunner-data/observability/targets/nodes.targets.json /root/netrunner-data/observability/targets/nodes.json
|
||||||
Generated
+186
-1
@@ -572,6 +572,15 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@@ -771,6 +780,17 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evmap"
|
||||||
|
version = "11.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
|
||||||
|
dependencies = [
|
||||||
|
"hashbag",
|
||||||
|
"left-right",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -929,6 +949,21 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generator"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"rustversion",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -1025,6 +1060,12 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbag"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -1459,6 +1500,17 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "left-right"
|
||||||
|
version = "0.11.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"loom",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.184"
|
version = "0.2.184"
|
||||||
@@ -1512,6 +1564,19 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loom"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"generator",
|
||||||
|
"scoped-tls",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
version = "0.16.3"
|
version = "0.16.3"
|
||||||
@@ -1548,6 +1613,48 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metrics"
|
||||||
|
version = "0.24.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
"rapidhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metrics-exporter-prometheus"
|
||||||
|
version = "0.18.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"evmap",
|
||||||
|
"indexmap",
|
||||||
|
"metrics",
|
||||||
|
"metrics-util",
|
||||||
|
"quanta",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metrics-util"
|
||||||
|
version = "0.20.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"metrics",
|
||||||
|
"quanta",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"rand_xoshiro",
|
||||||
|
"rapidhash",
|
||||||
|
"sketches-ddsketch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@@ -1654,6 +1761,8 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"metrics",
|
||||||
|
"metrics-exporter-prometheus",
|
||||||
"netrunner-core",
|
"netrunner-core",
|
||||||
"netrunner-logger",
|
"netrunner-logger",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -1869,6 +1978,21 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quanta"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"raw-cpuid",
|
||||||
|
"wasi",
|
||||||
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@@ -2001,6 +2125,33 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xoshiro"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rapidhash"
|
||||||
|
version = "4.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "raw-cpuid"
|
||||||
|
version = "11.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -2220,6 +2371,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2373,6 +2530,12 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sketches-ddsketch"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -2394,7 +2557,7 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "smoltcp"
|
name = "smoltcp"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+ssh://git@github.com/nineAp/nxrp-smoltcp.git#e9ba23b47ab789eb480d2eef2ffc4d7cdfedb381"
|
source = "git+https://github.com/nineAp/nxrp-smoltcp.git#c8093df6ec24f75c13bedf0e74e53cefe26997e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -3274,6 +3437,22 @@ dependencies = [
|
|||||||
"nom",
|
"nom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@@ -3283,6 +3462,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib", "staticlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
netrunner-core = { path = "../core" }
|
netrunner-core = { path = "../core" }
|
||||||
smoltcp = { git = "ssh://git@github.com/nineAp/nxrp-smoltcp.git", default-features = false, features = [
|
smoltcp = { git = "https://github.com/nineAp/nxrp-smoltcp.git", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"log",
|
"log",
|
||||||
"medium-ip",
|
"medium-ip",
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "nl-1",
|
||||||
|
"host": "147.45.43.70",
|
||||||
|
"proxy_port": 443,
|
||||||
|
"decoy_host": "ubuntu.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fr-1",
|
||||||
|
"host": "77.110.106.113",
|
||||||
|
"proxy_port": 8443,
|
||||||
|
"decoy_host": "www.debian.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Генерит targets-файл для Prometheus file_sd_configs (см.
|
||||||
|
// netrunner-data/observability/prometheus.yml, job "netrunner-proxy-nodes")
|
||||||
|
// из nodes.json этого репозитория — список нод меняется независимо от
|
||||||
|
// прод-конфига Prometheus, руками синхронизировать его на каждый деплой
|
||||||
|
// ноды неудобно и хрупко.
|
||||||
|
//
|
||||||
|
// Порт метрик у всех нод одинаковый (9093 — см. server/netrunner-server.service,
|
||||||
|
// templates/init_node.sh в netrunner-backend, .gitea/workflows/deploy.yml
|
||||||
|
// здесь же), меняется только host — поэтому в самом nodes.json отдельного
|
||||||
|
// поля под него нет.
|
||||||
|
//
|
||||||
|
// Использование: node scripts/generate-prometheus-targets.mjs nodes.json > targets.json
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const METRICS_PORT = 9093;
|
||||||
|
|
||||||
|
const [, , nodesPath] = process.argv;
|
||||||
|
if (!nodesPath) {
|
||||||
|
console.error("Использование: generate-prometheus-targets.mjs <nodes.json>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = JSON.parse(readFileSync(nodesPath, "utf-8"));
|
||||||
|
|
||||||
|
const targets = nodes
|
||||||
|
.filter((n) => !n.host?.startsWith("CHANGE_ME"))
|
||||||
|
.map((n) => ({
|
||||||
|
targets: [`${n.host}:${METRICS_PORT}`],
|
||||||
|
labels: { node: n.name },
|
||||||
|
}));
|
||||||
|
|
||||||
|
process.stdout.write(JSON.stringify(targets, null, 2) + "\n");
|
||||||
@@ -15,3 +15,5 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
reqwest = { version = "0.13.2", features = ["json"] }
|
reqwest = { version = "0.13.2", features = ["json"] }
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
|
metrics = "0.24.3"
|
||||||
|
metrics-exporter-prometheus = { version = "0.18.1", default-features = false }
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/root/netr-core
|
WorkingDirectory=/root/netr-core
|
||||||
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=8443
|
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=8443 --health-port=9092 --metrics-port=9094
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
LimitNOFILE=65535
|
LimitNOFILE=65535
|
||||||
|
# systemctl stop/restart шлёт SIGTERM — теперь сервер ловит его сам и
|
||||||
|
# дожидается отключения активных клиентов вместо мгновенного обрыва.
|
||||||
|
TimeoutStopSec=35
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/root/netr-core
|
WorkingDirectory=/root/netr-core
|
||||||
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=443
|
ExecStart=/root/netr-core/netrunner-server --host=0.0.0.0 --port=443 --health-port=9091 --metrics-port=9093
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
LimitNOFILE=65535
|
LimitNOFILE=65535
|
||||||
|
# systemctl stop/restart шлёт SIGTERM — теперь сервер ловит его сам и
|
||||||
|
# дожидается отключения активных клиентов (см. server/src/main.rs,
|
||||||
|
# server/src/network.rs) вместо мгновенного обрыва по TimeoutStopSec.
|
||||||
|
TimeoutStopSec=35
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -9,28 +9,108 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use netrunner_core::net::{AuthValidator, UsageReport, UserQuota};
|
use netrunner_core::net::{AuthValidator, UsageReport, UserQuota};
|
||||||
use netrunner_logger::AppError;
|
use netrunner_logger::{warn, AppError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const VALIDATE_CACHE_TTL: Duration = Duration::from_secs(60);
|
const VALIDATE_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
/// Таймаут одного HTTP-запроса к бэкенду. Без него `reqwest::Client` ждёт
|
||||||
|
/// ответ неограниченно долго на зависшем (не упавшем — именно зависшем)
|
||||||
|
/// бэкенде: каждая проверка токена на КАЖДОМ новом клиенте висела бы, блокируя
|
||||||
|
/// подключение новых пользователей, а не только тех, чей токен уже в кеше.
|
||||||
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// После скольких подряд неудач (сетевая ошибка/таймаут/5xx — НЕ 4xx, отказ
|
||||||
|
/// по конкретному токену не значит, что бэкенд нездоров) размыкаем цепь.
|
||||||
|
const FAILURE_THRESHOLD: u32 = 5;
|
||||||
|
/// Сколько цепь остаётся разомкнутой (запросы отклоняются мгновенно, без
|
||||||
|
/// попытки реального HTTP-вызова и его таймаута) перед следующей пробой.
|
||||||
|
const CIRCUIT_OPEN_COOLDOWN: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct CircuitState {
|
||||||
|
consecutive_failures: u32,
|
||||||
|
open_until: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Простейший circuit breaker: без внешней библиотеки, состояние — один
|
||||||
|
/// `Mutex` с редкими короткими блокировками (проверка/запись пары полей,
|
||||||
|
/// не сам HTTP-вызов). Цель — не ждать `REQUEST_TIMEOUT` на каждом клиенте
|
||||||
|
/// подряд, если бэкенд уже несколько раз подряд не ответил, а быстро
|
||||||
|
/// отказывать, пока не пройдёт cooldown.
|
||||||
|
struct Circuit {
|
||||||
|
state: Mutex<CircuitState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Circuit {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: Mutex::new(CircuitState::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true`, если цепь разомкнута прямо сейчас — вызывающий код должен
|
||||||
|
/// отказать быстро, не делая реальный HTTP-запрос.
|
||||||
|
fn is_open(&self) -> bool {
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
matches!(state.open_until, Some(until) if Instant::now() < until)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_success(&self) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.consecutive_failures = 0;
|
||||||
|
state.open_until = None;
|
||||||
|
metrics::gauge!("netrunner_circuit_breaker_open").set(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Считать неудачей только сетевые ошибки/таймауты/5xx — HTTP 401/403 на
|
||||||
|
/// конкретный невалидный токен НЕ признак нездоровья бэкенда.
|
||||||
|
fn record_failure(&self) {
|
||||||
|
let mut state = self.state.lock().unwrap();
|
||||||
|
state.consecutive_failures += 1;
|
||||||
|
if state.consecutive_failures >= FAILURE_THRESHOLD {
|
||||||
|
state.open_until = Some(Instant::now() + CIRCUIT_OPEN_COOLDOWN);
|
||||||
|
metrics::gauge!("netrunner_circuit_breaker_open").set(1.0);
|
||||||
|
warn!(
|
||||||
|
failures = state.consecutive_failures,
|
||||||
|
"Circuit breaker разомкнут: бэкенд не отвечает {} раз подряд",
|
||||||
|
state.consecutive_failures
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct BackendClient {
|
pub struct BackendClient {
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
internal_secret: String,
|
internal_secret: String,
|
||||||
validate_cache: DashMap<String, (UserQuota, Instant)>,
|
validate_cache: DashMap<String, (UserQuota, Instant)>,
|
||||||
|
circuit: Circuit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackendClient {
|
impl BackendClient {
|
||||||
pub fn new(base_url: String, internal_secret: String) -> Self {
|
pub fn new(base_url: String, internal_secret: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
http: reqwest::Client::new(),
|
http: reqwest::Client::builder()
|
||||||
|
.timeout(REQUEST_TIMEOUT)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build reqwest client"),
|
||||||
base_url: base_url.trim_end_matches('/').to_string(),
|
base_url: base_url.trim_end_matches('/').to_string(),
|
||||||
internal_secret,
|
internal_secret,
|
||||||
validate_cache: DashMap::new(),
|
validate_cache: DashMap::new(),
|
||||||
|
circuit: Circuit::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn circuit_open_error() -> AppError {
|
||||||
|
AppError::new(
|
||||||
|
netrunner_logger::ERR_INFRA_TIMEOUT,
|
||||||
|
"Бэкенд недоступен",
|
||||||
|
"Circuit breaker open: backend недавно не отвечал несколько раз подряд, короткий отказ без повторной попытки",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -62,6 +142,7 @@ struct UsageResponse {
|
|||||||
impl AuthValidator for BackendClient {
|
impl AuthValidator for BackendClient {
|
||||||
async fn validate(&self, token: &str) -> Result<UserQuota, AppError> {
|
async fn validate(&self, token: &str) -> Result<UserQuota, AppError> {
|
||||||
if token.is_empty() {
|
if token.is_empty() {
|
||||||
|
metrics::counter!("netrunner_auth_failures_total").increment(1);
|
||||||
return Err(AppError::new(
|
return Err(AppError::new(
|
||||||
netrunner_logger::ERR_AUTH_FAILED,
|
netrunner_logger::ERR_AUTH_FAILED,
|
||||||
"Доступ запрещен",
|
"Доступ запрещен",
|
||||||
@@ -76,6 +157,11 @@ impl AuthValidator for BackendClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.circuit.is_open() {
|
||||||
|
return Err(Self::circuit_open_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_start = Instant::now();
|
||||||
let resp = self
|
let resp = self
|
||||||
.http
|
.http
|
||||||
.post(format!("{}/api/v1/internal/validate", self.base_url))
|
.post(format!("{}/api/v1/internal/validate", self.base_url))
|
||||||
@@ -84,14 +170,24 @@ impl AuthValidator for BackendClient {
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
self.circuit.record_failure();
|
||||||
AppError::new(
|
AppError::new(
|
||||||
netrunner_logger::ERR_INFRA_TIMEOUT,
|
netrunner_logger::ERR_INFRA_TIMEOUT,
|
||||||
"Бэкенд недоступен",
|
"Бэкенд недоступен",
|
||||||
e.to_string(),
|
e.to_string(),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
metrics::histogram!("netrunner_auth_validate_duration_seconds")
|
||||||
|
.record(request_start.elapsed().as_secs_f64());
|
||||||
|
|
||||||
|
// 5xx — признак нездоровья самого бэкенда, считается неудачей для
|
||||||
|
// circuit breaker'а. 4xx (например 401 на невалидный токен) — это
|
||||||
|
// ожидаемый легитимный ответ на конкретный запрос, не поломка бэкенда.
|
||||||
|
if resp.status().is_server_error() {
|
||||||
|
self.circuit.record_failure();
|
||||||
|
}
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
|
metrics::counter!("netrunner_auth_failures_total").increment(1);
|
||||||
return Err(AppError::new(
|
return Err(AppError::new(
|
||||||
netrunner_logger::ERR_AUTH_FAILED,
|
netrunner_logger::ERR_AUTH_FAILED,
|
||||||
"Доступ запрещен",
|
"Доступ запрещен",
|
||||||
@@ -107,6 +203,7 @@ impl AuthValidator for BackendClient {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
self.circuit.record_success();
|
||||||
let quota = UserQuota {
|
let quota = UserQuota {
|
||||||
user_id: body.user_id,
|
user_id: body.user_id,
|
||||||
limit_bytes: body.limit_bytes,
|
limit_bytes: body.limit_bytes,
|
||||||
@@ -118,6 +215,10 @@ impl AuthValidator for BackendClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn report_usage(&self, user_id: &str, delta_bytes: u64) -> Result<UsageReport, AppError> {
|
async fn report_usage(&self, user_id: &str, delta_bytes: u64) -> Result<UsageReport, AppError> {
|
||||||
|
if self.circuit.is_open() {
|
||||||
|
return Err(Self::circuit_open_error());
|
||||||
|
}
|
||||||
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.http
|
.http
|
||||||
.post(format!("{}/api/v1/internal/usage", self.base_url))
|
.post(format!("{}/api/v1/internal/usage", self.base_url))
|
||||||
@@ -129,6 +230,7 @@ impl AuthValidator for BackendClient {
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
self.circuit.record_failure();
|
||||||
AppError::new(
|
AppError::new(
|
||||||
netrunner_logger::ERR_INFRA_TIMEOUT,
|
netrunner_logger::ERR_INFRA_TIMEOUT,
|
||||||
"Бэкенд недоступен",
|
"Бэкенд недоступен",
|
||||||
@@ -136,6 +238,9 @@ impl AuthValidator for BackendClient {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if resp.status().is_server_error() {
|
||||||
|
self.circuit.record_failure();
|
||||||
|
}
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(AppError::new(
|
return Err(AppError::new(
|
||||||
netrunner_logger::ERR_INFRA_TIMEOUT,
|
netrunner_logger::ERR_INFRA_TIMEOUT,
|
||||||
@@ -152,6 +257,7 @@ impl AuthValidator for BackendClient {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
self.circuit.record_success();
|
||||||
Ok(UsageReport {
|
Ok(UsageReport {
|
||||||
used_bytes: body.used_bytes,
|
used_bytes: body.used_bytes,
|
||||||
limit_bytes: body.limit_bytes,
|
limit_bytes: body.limit_bytes,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
//! [`ServerDiagnosticsLogger::snapshot_all_sessions`]).
|
//! [`ServerDiagnosticsLogger::snapshot_all_sessions`]).
|
||||||
|
|
||||||
use netrunner_core::net::diagnostics::{
|
use netrunner_core::net::diagnostics::{
|
||||||
self, DiagnosticsEvent, DiagnosticsSnapshot, DiagnosticsStore, TunnelMetrics,
|
self, current_timestamp_ms, DiagnosticsEvent, DiagnosticsSnapshot, DiagnosticsStore,
|
||||||
current_timestamp_ms,
|
TunnelMetrics,
|
||||||
};
|
};
|
||||||
use netrunner_core::net::SessionManager;
|
use netrunner_core::net::SessionManager;
|
||||||
use netrunner_logger::{error, info, warn};
|
use netrunner_logger::{error, info, warn};
|
||||||
@@ -192,10 +192,7 @@ impl ServerDiagnosticsLogger {
|
|||||||
let mut rotated = self.log_path.clone();
|
let mut rotated = self.log_path.clone();
|
||||||
rotated.set_extension("jsonl.1");
|
rotated.set_extension("jsonl.1");
|
||||||
tokio::fs::rename(&self.log_path, &rotated).await?;
|
tokio::fs::rename(&self.log_path, &rotated).await?;
|
||||||
info!(
|
info!("Diagnostics log rotated → {}", rotated.display());
|
||||||
"Diagnostics log rotated → {}",
|
|
||||||
rotated.display()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//! Минимальный HTTP `/health` для supervisor'а (systemd/docker healthcheck) —
|
||||||
|
//! отдельный порт, не тот, где слушается замаскированный туннельный протокол
|
||||||
|
//! (смешивать их нельзя: health-эндпоинт — обычный читаемый HTTP, это выдало
|
||||||
|
//! бы DPI ровно то, что декой должен скрывать). По умолчанию биндится только
|
||||||
|
//! на 127.0.0.1 — наружу торчать не должен, это внутренняя проверка для
|
||||||
|
//! supervisor'а/docker healthcheck на этой же машине.
|
||||||
|
//!
|
||||||
|
//! Не использует HTTP-фреймворк (лишняя зависимость ради одного эндпоинта) —
|
||||||
|
//! отвечает одним и тем же 200+JSON на любое подключение, не разбирая запрос.
|
||||||
|
|
||||||
|
use netrunner_logger::{error, info, warn};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
/// Отдаёт реальное состояние — не просто "процесс жив", а сколько сейчас
|
||||||
|
/// физических соединений реально обслуживается (см. `Network::run`,
|
||||||
|
/// `active_connections`).
|
||||||
|
pub async fn run(
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
active_connections: Arc<AtomicU64>,
|
||||||
|
token: CancellationToken,
|
||||||
|
) {
|
||||||
|
let addr = format!("{host}:{port}");
|
||||||
|
let listener = match TcpListener::bind(&addr).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, addr = %addr, "Не удалось поднять health-listener");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("🩺 Health-эндпоинт слушает на {}", addr);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = token.cancelled() => {
|
||||||
|
info!("Health-listener остановлен по сигналу отмены.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
res = listener.accept() => {
|
||||||
|
let Ok((mut stream, _)) = res else { continue };
|
||||||
|
let active = active_connections.load(Ordering::Relaxed);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let body = format!(
|
||||||
|
r#"{{"status":"ok","active_connections":{active}}}"#
|
||||||
|
);
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
body.len(),
|
||||||
|
body
|
||||||
|
);
|
||||||
|
if let Err(e) = stream.write_all(response.as_bytes()).await {
|
||||||
|
warn!(error = %e, "Health-listener: ошибка записи ответа");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
-3
@@ -12,16 +12,47 @@
|
|||||||
|
|
||||||
mod backend_client;
|
mod backend_client;
|
||||||
mod diagnostics;
|
mod diagnostics;
|
||||||
|
mod health;
|
||||||
|
mod metrics_server;
|
||||||
mod network;
|
mod network;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use netrunner_core::net::AuthValidator;
|
use netrunner_core::net::AuthValidator;
|
||||||
use netrunner_logger::Logger;
|
use netrunner_logger::{error, info, Logger};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::backend_client::BackendClient;
|
use crate::backend_client::BackendClient;
|
||||||
use crate::network::Network;
|
use crate::network::Network;
|
||||||
|
|
||||||
|
/// Ждёт SIGTERM (docker stop/systemctl stop) или SIGINT (Ctrl+C) и отменяет
|
||||||
|
/// токен — раньше этой функции не было вообще: сервер получал сигнал прямо
|
||||||
|
/// от ОС мимо CancellationToken'а, и вся drain-логика в `Network::run` была
|
||||||
|
/// мертва, ни разу не срабатывая на реальном шатдауне.
|
||||||
|
async fn shutdown_signal(token: CancellationToken) {
|
||||||
|
let ctrl_c = async {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("Не удалось установить обработчик Ctrl+C");
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let terminate = async {
|
||||||
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||||
|
.expect("Не удалось установить обработчик SIGTERM")
|
||||||
|
.recv()
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let terminate = std::future::pending::<()>();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = ctrl_c => info!("🛑 Получен SIGINT (Ctrl+C). Останавливаемся..."),
|
||||||
|
_ = terminate => info!("🛑 Получен SIGTERM. Останавливаемся..."),
|
||||||
|
}
|
||||||
|
token.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
/// Аргументы командной строки сервера.
|
/// Аргументы командной строки сервера.
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about = "Netrunner Proxy Server")]
|
#[command(author, version, about = "Netrunner Proxy Server")]
|
||||||
@@ -52,6 +83,19 @@ struct Args {
|
|||||||
/// Обязателен, только если передан `--require-auth`.
|
/// Обязателен, только если передан `--require-auth`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
backend_url: Option<String>,
|
backend_url: Option<String>,
|
||||||
|
|
||||||
|
/// Порт для внутреннего HTTP `/health` (биндится только на 127.0.0.1 —
|
||||||
|
/// не для публичного доступа, только supervisor/docker healthcheck на
|
||||||
|
/// этой же машине). Не задан — health-эндпоинт выключен.
|
||||||
|
#[arg(long)]
|
||||||
|
health_port: Option<u16>,
|
||||||
|
|
||||||
|
/// Порт для `/metrics` (Prometheus text exposition) — в отличие от
|
||||||
|
/// `--health-port`, биндится на 0.0.0.0 (нужен для скрейпа удалённым
|
||||||
|
/// центральным Prometheus), ОБЯЗАТЕЛЬНО зафайрволить на IP
|
||||||
|
/// observability-VPS. Не задан — метрики выключены.
|
||||||
|
#[arg(long)]
|
||||||
|
metrics_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -70,11 +114,48 @@ fn main() {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let net = Network::new(args.host, args.port, args.decoy_host, auth);
|
// Регистрируется один раз, до первого metrics::counter!/gauge!/histogram! —
|
||||||
|
// если --metrics-port не задан, вызовы макросов молча уходят в
|
||||||
|
// no-op recorder по умолчанию (штатное поведение крейта metrics).
|
||||||
|
let metrics_handle = args
|
||||||
|
.metrics_port
|
||||||
|
.map(|_| metrics_server::install_recorder());
|
||||||
|
|
||||||
|
let net = Network::new(
|
||||||
|
args.host.clone(),
|
||||||
|
args.port,
|
||||||
|
args.decoy_host,
|
||||||
|
auth,
|
||||||
|
args.health_port,
|
||||||
|
);
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
net.run(CancellationToken::new()).await;
|
let token = CancellationToken::new();
|
||||||
|
let run_token = token.clone();
|
||||||
|
// net.run() спавним отдельной задачей и дожидаемся её ПОСЛЕ сигнала —
|
||||||
|
// tokio::select! между сигналом и run() тут не подходит: select
|
||||||
|
// дропает недовершившуюся ветку целиком, оборвав drain-фазу в
|
||||||
|
// Network::run в момент получения самого сигнала, вместо того чтобы
|
||||||
|
// дать ей отработать.
|
||||||
|
let run_handle = tokio::spawn(async move {
|
||||||
|
net.run(run_token).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
if let (Some(port), Some(handle)) = (args.metrics_port, metrics_handle) {
|
||||||
|
let metrics_token = token.clone();
|
||||||
|
tokio::spawn(metrics_server::run(
|
||||||
|
"0.0.0.0".to_string(),
|
||||||
|
port,
|
||||||
|
handle,
|
||||||
|
metrics_token,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown_signal(token).await;
|
||||||
|
if let Err(e) = run_handle.await {
|
||||||
|
error!(error = ?e, "Задача сервера завершилась с паникой при остановке");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//! `/metrics` для центрального Prometheus (см.
|
||||||
|
//! `netrunner-data/docker-compose.observability.yml`) — в отличие от
|
||||||
|
//! `/health` (127.0.0.1-only, см. `health.rs`), этот порт публикуется наружу
|
||||||
|
//! и ОБЯЗАТЕЛЬНО должен быть зафайрволен на IP observability-VPS: любой,
|
||||||
|
//! кто до него дотянется, увидит число активных соединений/трафик этой
|
||||||
|
//! ноды (не секрет пользователей, но разведка для DPI/блокировщика).
|
||||||
|
//!
|
||||||
|
//! Тот же hand-rolled HTTP-паттерн, что и в `health.rs` (не тянуть HTTP-
|
||||||
|
//! фреймворк ради одного эндпоинта на бинарнике, для которого важен размер
|
||||||
|
//! и минимальные зависимости) — просто рендерит текущий снапшот метрик из
|
||||||
|
//! уже установленного `PrometheusHandle` на каждый коннект.
|
||||||
|
|
||||||
|
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
|
||||||
|
use netrunner_logger::{error, info, warn};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
/// Регистрирует глобальный recorder `metrics`-крейта — вызывать ровно один
|
||||||
|
/// раз при старте, до первого `metrics::counter!`/`gauge!`/`histogram!`.
|
||||||
|
pub fn install_recorder() -> PrometheusHandle {
|
||||||
|
PrometheusBuilder::new()
|
||||||
|
.install_recorder()
|
||||||
|
.expect("Failed to install Prometheus recorder")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(host: String, port: u16, handle: PrometheusHandle, token: CancellationToken) {
|
||||||
|
let addr = format!("{host}:{port}");
|
||||||
|
let listener = match TcpListener::bind(&addr).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, addr = %addr, "Не удалось поднять metrics-листенер");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("📊 Metrics-эндпоинт слушает на {}", addr);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = token.cancelled() => {
|
||||||
|
info!("Metrics-listener остановлен по сигналу отмены.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
res = listener.accept() => {
|
||||||
|
let Ok((mut stream, _)) = res else { continue };
|
||||||
|
let body = handle.render();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
body.len(),
|
||||||
|
body
|
||||||
|
);
|
||||||
|
if let Err(e) = stream.write_all(response.as_bytes()).await {
|
||||||
|
warn!(error = %e, "Metrics-listener: ошибка записи ответа");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,19 @@ use netrunner_core::net::{
|
|||||||
TOPOLOGY_PRINT_INTERVAL,
|
TOPOLOGY_PRINT_INTERVAL,
|
||||||
};
|
};
|
||||||
use netrunner_logger::{error, info, warn};
|
use netrunner_logger::{error, info, warn};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::diagnostics::{ClientDiagnosticsLogger, ServerDiagnosticsLogger};
|
use crate::diagnostics::{ClientDiagnosticsLogger, ServerDiagnosticsLogger};
|
||||||
|
use crate::health;
|
||||||
|
|
||||||
|
/// Сколько ждём при остановке, пока уже принятые соединения сами закроются,
|
||||||
|
/// прежде чем отпустить рантайм (который при Drop абортит все задачи разом,
|
||||||
|
/// без предупреждения клиентам).
|
||||||
|
const SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
/// Параметры прослушивания сервера.
|
/// Параметры прослушивания сервера.
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
@@ -27,6 +35,9 @@ pub struct Network {
|
|||||||
/// `None` — `--require-auth` не передан, авторизация и лимиты трафика
|
/// `None` — `--require-auth` не передан, авторизация и лимиты трафика
|
||||||
/// выключены на этом инстансе целиком (поведение как до этой фичи).
|
/// выключены на этом инстансе целиком (поведение как до этой фичи).
|
||||||
auth: Option<Arc<dyn AuthValidator>>,
|
auth: Option<Arc<dyn AuthValidator>>,
|
||||||
|
/// `None` — health-эндпоинт выключен (по умолчанию для обратной
|
||||||
|
/// совместимости с уже развёрнутыми нодами без этого флага).
|
||||||
|
health_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Network {
|
impl Network {
|
||||||
@@ -35,12 +46,14 @@ impl Network {
|
|||||||
port: u16,
|
port: u16,
|
||||||
decoy_host: impl Into<Arc<str>>,
|
decoy_host: impl Into<Arc<str>>,
|
||||||
auth: Option<Arc<dyn AuthValidator>>,
|
auth: Option<Arc<dyn AuthValidator>>,
|
||||||
|
health_port: Option<u16>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
decoy_host: decoy_host.into(),
|
decoy_host: decoy_host.into(),
|
||||||
auth,
|
auth,
|
||||||
|
health_port,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +138,23 @@ impl Network {
|
|||||||
info!("🌐 Netrunner Server: Listening on {}", addr);
|
info!("🌐 Netrunner Server: Listening on {}", addr);
|
||||||
let listener = TcpListener::bind(&addr).await.expect("Server bind failed");
|
let listener = TcpListener::bind(&addr).await.expect("Server bind failed");
|
||||||
|
|
||||||
|
// Число реально обслуживаемых физических соединений прямо сейчас — не
|
||||||
|
// "процесс жив", а "сколько клиентов на нём висит". Отдаётся в /health
|
||||||
|
// и используется ниже, чтобы дождаться отключения клиентов при
|
||||||
|
// остановке вместо мгновенного разрыва при drop рантайма.
|
||||||
|
let active_connections = Arc::new(AtomicU64::new(0));
|
||||||
|
|
||||||
|
if let Some(health_port) = self.health_port {
|
||||||
|
let health_token = token.clone();
|
||||||
|
let health_connections = active_connections.clone();
|
||||||
|
tokio::spawn(health::run(
|
||||||
|
"127.0.0.1".to_string(),
|
||||||
|
health_port,
|
||||||
|
health_connections,
|
||||||
|
health_token,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = token.cancelled() => {
|
_ = token.cancelled() => {
|
||||||
@@ -145,6 +175,10 @@ impl Network {
|
|||||||
self.auth.clone(),
|
self.auth.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let active_now = active_connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
|
metrics::gauge!("netrunner_connections_active").set(active_now as f64);
|
||||||
|
metrics::counter!("netrunner_connections_total").increment(1);
|
||||||
|
let conn_counter = active_connections.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// "Входим" в этот Span. Все логи внутри handler.run() привяжутся к этому IP.
|
// "Входим" в этот Span. Все логи внутри handler.run() привяжутся к этому IP.
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
@@ -153,10 +187,32 @@ impl Network {
|
|||||||
if let Err(e) = handler.run().await {
|
if let Err(e) = handler.run().await {
|
||||||
error!(error = %e, "⚠️ Server handler terminated with error");
|
error!(error = %e, "⚠️ Server handler terminated with error");
|
||||||
}
|
}
|
||||||
|
let active_now = conn_counter.fetch_sub(1, Ordering::Relaxed) - 1;
|
||||||
|
metrics::gauge!("netrunner_connections_active").set(active_now as f64);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Graceful drain: приём новых соединений уже остановлен (цикл выше
|
||||||
|
// прерван), но уже принятые клиенты продолжают жить как detached-задачи
|
||||||
|
// рантайма. Без этого ожидания следующий за `run()` выход из
|
||||||
|
// `rt.block_on` уронит рантайм и оборвёт их все разом без предупреждения.
|
||||||
|
let drain_start = tokio::time::Instant::now();
|
||||||
|
while active_connections.load(Ordering::Relaxed) > 0 {
|
||||||
|
if drain_start.elapsed() > SHUTDOWN_DRAIN_TIMEOUT {
|
||||||
|
warn!(
|
||||||
|
remaining = active_connections.load(Ordering::Relaxed),
|
||||||
|
"Drain timeout истёк, принудительно завершаем оставшиеся соединения"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"✅ Drain завершён, соединений осталось: {}",
|
||||||
|
active_connections.load(Ordering::Relaxed)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user