Files
netrunner-proxy/server/src/main.rs
T
nineap cfcb1afcec Метрики /metrics для центрального Prometheus + генерация scrape-таргетов
--metrics-port (0.0.0.0, в отличие от --health-port — тот 127.0.0.1-only)
отдаёт netrunner_connections_active/total, netrunner_auth_validate_duration_seconds,
netrunner_auth_failures_total, netrunner_circuit_breaker_open. Тот же
hand-rolled HTTP-паттерн, что и health.rs — без HTTP-фреймворка ради
одного эндпоинта.

scripts/generate-prometheus-targets.mjs генерит список scrape-таргетов
из nodes.json — job update-observability-targets в deploy.yml гоняет
его при каждом деплое нод и заливает на VPS с данными
(netrunner-data/observability/targets/nodes.json), чтобы центральный
Prometheus не терял из виду ноды без ручной правки конфига.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 23:21:26 +07:00

162 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # netrunner-server — серверная часть прокси
//!
//! Тонкая обвязка вокруг ядра ([`netrunner_core`]): парсит аргументы, поднимает
//! логгер и tokio-рантайм и запускает [`Network`](crate::network::Network),
//! которая слушает TCP, принимает замаскированные TLS-соединения и отдаёт каждое
//! в `ServerHandler` ядра (хендшейк → аутентификация → проксирование или
//! stealth-fallback). Бизнес-логика целиком в ядре; здесь — точка входа и
//! серверная диагностика ([`diagnostics`](crate::diagnostics)).
// Workaround for rustc 1.94 ICE in check_mod_deathness (dead-code MIR pass).
#![allow(dead_code)]
mod backend_client;
mod diagnostics;
mod health;
mod metrics_server;
mod network;
use clap::Parser;
use netrunner_core::net::AuthValidator;
use netrunner_logger::{error, info, Logger};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use crate::backend_client::BackendClient;
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)]
#[command(author, version, about = "Netrunner Proxy Server")]
struct Args {
/// Порт прослушивания.
#[arg(short, long, default_value_t = 8080)]
port: u16,
/// Адрес привязки.
#[arg(long, default_value = "0.0.0.0")]
host: String,
/// Домен-декой: под кого притворяется этот узел для «не наших» подключений
/// (stealth-fallback прозрачно проксирует туда трафик сканеров/чужих
/// клиентов). Раньше было жёстко зашито на `ubuntu.com` в коде ядра — теперь
/// атрибут ноды, можно задавать разный на каждом развёртывании.
#[arg(long, default_value = netrunner_core::net::DEFAULT_DECOY_HOST)]
decoy_host: String,
/// Требовать валидный Bearer-токен (выданный `netrunner-backend`) от
/// каждого клиента и отчитываться о расходе трафика для динамических
/// лимитов. Выключено по умолчанию — включается по инстансу, не меняя
/// поведение уже развёрнутых нод без этого флага.
#[arg(long, default_value_t = false)]
require_auth: bool,
/// URL control-plane бэкенда для проверки токенов/отчётов о трафике.
/// Обязателен, только если передан `--require-auth`.
#[arg(long)]
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() {
Logger::init("./logs".into(), true);
Logger::global().set_level("info");
let args = Args::parse();
let auth: Option<Arc<dyn AuthValidator>> = if args.require_auth {
let backend_url = args
.backend_url
.expect("--require-auth требует --backend-url");
let internal_secret = std::env::var("PROXY_INTERNAL_SECRET")
.expect("--require-auth требует переменную окружения PROXY_INTERNAL_SECRET");
Some(Arc::new(BackendClient::new(backend_url, internal_secret)))
} else {
None
};
// Регистрируется один раз, до первого 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");
rt.block_on(async {
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, "Задача сервера завершилась с паникой при остановке");
}
});
}