docs [AI]

This commit is contained in:
2026-06-30 18:51:09 +07:00
parent 18cad76e38
commit f7a76353ea
67 changed files with 2104 additions and 1417 deletions
+47
View File
@@ -0,0 +1,47 @@
# Блок `client/net` — userspace TCP/IP-стек и мост в туннель
«Локальная» половина клиента: то, что превращает перехваченные IP-пакеты
приложений в логические соединения и кормит ими ядро ([`netrunner_core`](../../../core)).
Построено поверх форка `smoltcp` (userspace-стек) + `tokio`.
> Детали — в rustdoc крейта `netrunner-client`, модуль `net`.
## Файлы
| Файл | Роль |
|-------------------------|------------------------------------------------------------|
| `engine.rs` | Главный poll-цикл стека + сборка (`EngineBuilder`). |
| `connection_manager.rs` | Перехват L3-пакетов, реестр сокетов, диспетчеризация. |
| `connection.rs` | Виртуальные TCP/UDP-соединения + ICMP-ответчик. |
| `session_tracker.rs` | NAT-таблица сокетов, idle/LRU-уборка. |
| `socket_factory.rs` | Создание smoltcp-сокетов под профиль трафика. |
| `dns.rs` | Локальный DNS: фейковые IP (CGNAT) + блок-лист. |
## Поток данных
```text
TUN ─пакеты→ engine ─push_rx→ smoltcp ─→ connection_manager
─перехват SYN/датаграмм→ TcpConnection/UdpConnection ─RawCastFrame→ туннель
туннель ─RawCastFrame→ engine ─route_download→ smoltcp ─→ TUN ─→ приложение
```
## Ключевые механики
- **Перехват** (`connection_manager`): на TCP-`SYN` и первую UDP-датаграмму
заводится smoltcp-сокет и виртуальное соединение; служебный трафик (DNS:53,
NetBIOS) пропускается.
- **Фейковый DNS** (`dns`): каждому имени выдаётся стабильный IP из 100.64.0.0/10,
по нему потом восстанавливается хост при установке туннельного соединения.
- **Профили трафика** (`socket_factory`): bulk/interactive/dns → разные размеры
буферов; TCP с BBR, без Nagle.
- **Анти-bufferbloat / анти-HOL**: download раздаётся **пер-сокетно**
(`pending_download` в `engine`), upload — с `tx_congested`-паузой
(`connection`); один застрявший сокет не морозит остальных.
- **Уборка** (`session_tracker`): idle-выметание + LRU-эвикт при лимите сокетов,
с защитой системных/слушающих.
## Связи
Кормит ядро через канал `RawCastFrame` ([`rawcast`](../../../core/src/rawcast));
сам туннель поднимает `ClientHandler::connect` из ядра. Ввод/вывод и маршруты — в
соседнем блоке [`tun`](../tun).
+49
View File
@@ -1,3 +1,20 @@
//! Виртуальные соединения клиента: мост между smoltcp-сокетом и туннелем.
//!
//! Каждое перехваченное приложение-соединение представлено одним из типов:
//! [`TcpConnection`], [`UdpConnection`] или ответчиком [`IcmpResponder`]. Они
//! живут в синхронном цикле стека (`tick`), но общаются с асинхронным туннелем
//! через каналы (`ConnectionCore`): локальный сокет ⇄ канал ⇄ задача `spawn` ⇄
//! [`RawCastFrame`] ⇄ туннель.
//!
//! Главное в [`TcpConnection`] — управление потоком без bufferbloat:
//! - **upload** (браузер→туннель): читаем из smoltcp, пока есть место в канале;
//! переполнение канала ставит флаг `tx_congested` → перестаём читать → срабатывает
//! TCP backpressure к приложению;
//! - **download** (туннель→браузер): держим максимум ОДИН `pending_chunk`; если
//! tx-буфер smoltcp полон — придерживаем чанк и поднимаем `is_saturated`;
//! - **RTT-проброс** в smoltcp (`set_tunnel_rtt`/AQM) для BBR — с потолком, чтобы
//! рост RTT не раздувал очередь по положительной обратной связи.
use bytes::Bytes;
use netrunner_core::{
net::{GLOBAL_MIN_RTT, NetworkConfig, UDP_IDLE_TIMEOUT},
@@ -16,10 +33,16 @@ use tokio::sync::{OwnedSemaphorePermit, mpsc, oneshot};
use netrunner_logger::{debug, info, instrument};
/// Общая «обвязка каналов» соединения: хендл сокета + два встречных канала +
/// флаг насыщения tx-буфера. Параметр `T` — тип исходящего сообщения (для TCP
/// это [`Bytes`], для UDP — кортеж с адресом).
pub struct ConnectionCore<T> {
pub handle: SocketHandle,
/// Канал «локальный сокет → туннель».
pub tx: mpsc::Sender<T>,
/// Канал «туннель → локальный сокет».
pub rx: mpsc::Receiver<Bytes>,
/// Полон ли tx-буфер smoltcp (сигнал backpressure для download).
pub is_saturated: Arc<AtomicBool>,
}
@@ -43,14 +66,20 @@ impl<T> ConnectionCore<T> {
}
}
/// Стадия жизненного цикла виртуального TCP-соединения.
#[derive(Debug, PartialEq)]
pub enum ConnectionState {
/// Туннель подтвердил установку — можно переходить к Active.
Established,
/// Ждём подтверждения от туннеля (CONNECT отправлен).
Handshaking,
/// Рабочее состояние: качаем данные в обе стороны.
Active,
/// Закрыто.
Closed,
}
/// Виртуальное TCP-соединение: один smoltcp tcp-сокет ↔ один поток туннеля.
pub struct TcpConnection {
core: ConnectionCore<Bytes>,
state: ConnectionState,
@@ -125,6 +154,11 @@ impl TcpConnection {
moved
}
/// Один шаг конечного автомата соединения внутри poll-цикла стека.
///
/// Прогоняет состояние (Handshaking→Established→Active→Closed) и в активной
/// фазе качает данные через [`poll_and_process`](TcpConnection::poll_and_process).
/// Возвращает `false`, когда соединение закрылось и его пора убирать.
pub fn tick(&mut self, socket: &mut tcp::Socket, timestamp: smoltcp::time::Instant) -> bool {
match self.state {
ConnectionState::Handshaking => {
@@ -221,6 +255,9 @@ impl TcpConnection {
}
}
/// Прокачивает данные в обе стороны за один тик (см. обзор модуля: upload с
/// `tx_congested`-паузой и download с одним `pending_chunk` + `is_saturated`).
/// В конце, если выгрузка завершена и буфер пуст, шлёт FIN приложению.
fn poll_and_process(&mut self, socket: &mut tcp::Socket, timestamp: smoltcp::time::Instant) {
self.maybe_update_tunnel_rtt(socket, timestamp);
@@ -335,6 +372,11 @@ impl TcpConnection {
self.pending_chunk.as_ref().map(|c| c.len()).unwrap_or(0)
}
/// Запускает асинхронную задачу-«насос» соединения.
///
/// Шлёт в туннель `Connect` (с целью в payload), сигналит хендшейк, затем в
/// цикле гонит данные из smoltcp-канала в туннель `Data`-кадрами, а на выходе
/// отправляет `Close`. Связывает синхронный сокет с асинхронным туннелем.
#[instrument(skip(rx_smol, handshake_tx, tx_tunnel), fields(
socket_id = socket_id,
dst = %target
@@ -377,11 +419,15 @@ impl TcpConnection {
// ─── UDP ────────────────────────────────────────────────────────────────────
/// UDP-датаграмма с адресом назначения: `(данные, ip, port)`.
pub type UdpPacketTarget = (Bytes, std::net::Ipv4Addr, u16);
/// Виртуальное UDP-«соединение» (NAT-запись): smoltcp udp-сокет ↔ поток туннеля.
pub struct UdpConnection {
core: ConnectionCore<UdpPacketTarget>,
/// Последний известный endpoint клиента (куда возвращать ответы).
last_client_endpoint: Option<IpEndpoint>,
/// Время последней активности (для idle-таймаута).
last_activity: std::time::Instant,
}
@@ -484,9 +530,12 @@ impl UdpConnection {
use smoltcp::socket::icmp;
/// Отвечает на ICMP Echo (ping) локально, не гоняя его через туннель.
pub struct IcmpResponder;
impl IcmpResponder {
/// Принимает ICMP-пакет; на Echo Request формирует Echo Reply (v4/v6) с
/// пересчётом контрольной суммы и отправляет обратно источнику.
pub fn handle(socket: &mut icmp::Socket, timestamp: smoltcp::time::Instant) {
if !socket.can_recv() {
return;
+44
View File
@@ -1,3 +1,16 @@
//! Перехват L3-пакетов и порождение виртуальных соединений.
//!
//! [`ConnectionManager`] — «диспетчер» клиентского стека. Он разбирает сырые
//! IP-пакеты из TUN, на TCP-`SYN` и первую UDP-датаграмму заводит новый
//! smoltcp-сокет и виртуальное соединение, а в poll-цикле двигает все сокеты
//! ([`process_sockets`](ConnectionManager::process_sockets)) и убирает мёртвые
//! ([`cleanup`](ConnectionManager::cleanup)).
//!
//! Вспомогательные части: [`TargetResolver`] восстанавливает реальную цель по
//! фейковому IP (через [`FakeIpStore`]) и обслуживает DNS на UDP:53;
//! `connection_limiter` ([`Semaphore`]) ограничивает число одновременных
//! установок, а `pending_connects` гасит повторные SYN до завершения хендшейка.
use dashmap::DashMap;
use netrunner_core::{
net::{
@@ -24,8 +37,10 @@ use crate::net::{
socket_factory::SocketProvider,
};
/// Ключ потока: `(src_ip, src_port, dst_ip, dst_port)`.
type FlowKey = (IpAddress, u16, IpAddress, u16);
/// Распарсенные адреса/порты одного перехваченного пакета.
struct Flow {
src: IpAddress,
dst: IpAddress,
@@ -39,6 +54,7 @@ impl Flow {
}
}
/// Восстановление цели соединения и обслуживание DNS.
struct TargetResolver {
dns_handler: DnsHandler,
fake_ip_store: FakeIpStore,
@@ -56,6 +72,8 @@ impl TargetResolver {
self.dns_handler.handle_query(data, &mut self.fake_ip_store)
}
/// Восстанавливает реальную цель `(ip, "host:port")` по адресу из пакета.
/// Для фейкового IPv4 поднимает домен из [`FakeIpStore`]; иначе использует сам IP.
pub fn resolve_destination(&self, addr: IpAddress, port: u16) -> (std::net::Ipv4Addr, String) {
match addr {
IpAddress::Ipv4(ip) => {
@@ -74,13 +92,21 @@ impl TargetResolver {
}
}
/// Диспетчер клиентского стека: перехват пакетов, реестр сокетов, poll-цикл.
pub struct ConnectionManager {
/// Реестр виртуальных соединений и их таймаутов.
pub tracker: SessionTracker,
/// Резолвер целей + DNS.
resolver: TargetResolver,
/// Канал в туннель (исходящие [`RawCastFrame`]).
tx_to_tunnel: mpsc::Sender<RawCastFrame>,
/// Фабрика smoltcp-сокетов.
factory: Arc<dyn SocketProvider>,
/// Незавершённые установки TCP (гасят повторные SYN), с временем старта.
pending_connects: DashMap<FlowKey, Instant>,
/// Ограничитель числа одновременных соединений.
connection_limiter: Arc<Semaphore>,
/// Переиспользуемый буфер хендлов для прохода по сокетам (без аллокаций).
active_handles_cache: Vec<SocketHandle>,
}
@@ -130,6 +156,8 @@ impl ConnectionManager {
}
}
/// Входная точка перехвата: разбирает версию IP из первого байта и направляет
/// пакет в обработчик IPv4/IPv6. Невалидное/прочее — молча игнорируется.
pub fn try_create_socket_from_packet(&mut self, packet: &[u8], socket_set: &mut SocketSet) {
if packet.is_empty() {
return;
@@ -205,6 +233,11 @@ impl ConnectionManager {
}
}
/// Заводит виртуальное TCP-соединение на перехваченный `SYN` (без ACK).
///
/// Дедупликация по `pending_connects` и наличию сокета; при достижении лимита
/// — LRU-эвикт; затем берётся семафор-пермит и создаётся слушающий сокет на
/// адрес назначения (smoltcp сам завершит хендшейк с приложением).
fn intercept_tcp(&mut self, f: Flow, socket_set: &mut SocketSet) {
let key = f.to_key();
if self.pending_connects.contains_key(&key) {
@@ -238,6 +271,11 @@ impl ConnectionManager {
}
}
/// Заводит виртуальное UDP-«соединение» на перехваченную датаграмму.
///
/// Пропускает служебный трафик (порт 0, локальный DNS:53, NetBIOS) и уже
/// известных клиентов; иначе — лимит/эвикт, создание привязанного UDP-сокета,
/// регистрация и запуск задачи-насоса в туннель.
fn intercept_udp(&mut self, f: Flow, socket_set: &mut SocketSet) {
if f.dst_p == 0
|| f.dst_p == DNS_PORT
@@ -275,6 +313,9 @@ impl ConnectionManager {
}
}
/// Двигает все сокеты на один шаг: для каждого вызывает соответствующий
/// обработчик (TCP/UDP/ICMP). Хендлы кешируются заранее, чтобы не одалживать
/// `socket_set` неизменяемо и изменяемо одновременно.
pub fn process_sockets(&mut self, socket_set: &mut SocketSet, now: smoltcp::time::Instant) {
self.active_handles_cache.clear();
for (h, _) in socket_set.iter() {
@@ -381,6 +422,9 @@ impl ConnectionManager {
}
}
/// Периодическая уборка: снимает протухшие `pending_connects`, выметает
/// неактивные сокеты по [`GLOBAL_IDLE_TIMEOUT`] и физически удаляет
/// помеченные к удалению из `SocketSet`.
pub fn cleanup(&mut self, socket_set: &mut SocketSet) {
self.pending_connects
.retain(|_, timestamp| timestamp.elapsed() < TCP_HANDSHAKE_TIMEOUT);
+30 -1
View File
@@ -1,3 +1,18 @@
//! Локальный перехват DNS: фейковые IP + блок-лист.
//!
//! Клиент сам отвечает на DNS-запросы приложений, чтобы (а) не утекал реальный
//! DNS и (б) каждое имя получало стабильный «фейковый» IP из диапазона CGNAT
//! (RFC 6598, 100.64.0.0/10), по которому потом восстанавливается хост.
//!
//! Две части:
//! - [`FakeIpStore`] — двусторонний LRU-маппинг `домен ⇄ фейковый IP`. Выдаёт
//! новый IP по запросу и позволяет обратный поиск (IP → домен) при установке
//! туннельного соединения.
//! - [`DnsHandler`] — обработчик запросов: режет приватные суффиксы и домены из
//! блок-листа (StevenBlack/hosts, фоново подкачивается и кэшируется),
//! пропускает исключённые домены мимо туннеля (ServFail → системный DNS),
//! остальным A-запросам выдаёт фейковый IP.
use anyhow::Result;
use hickory_proto::op::{Message, MessageType, ResponseCode};
use hickory_proto::rr::{RData, Record, RecordType};
@@ -26,9 +41,13 @@ const BLOCKLIST_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
/// TTL advertised in fake DNS A records (seconds).
const FAKE_DNS_TTL: u32 = 60;
/// Двусторонний LRU-маппинг доменов на фейковые IP из CGNAT-диапазона.
pub struct FakeIpStore {
/// Прямой: домен → выданный IP.
cache: LruCache<String, Ipv4Addr>,
/// Обратный: IP → домен (для восстановления цели при connect).
rev_cache: LruCache<Ipv4Addr, String>,
/// Следующий свободный IP (монотонно растёт от `FAKE_IP_START`).
next_ip: u32,
}
@@ -70,11 +89,16 @@ impl FakeIpStore {
// --- DNS Handler & Blocklist Logic ---
/// Обработчик DNS-запросов: фильтрация + выдача фейковых IP.
pub struct DnsHandler {
/// Заблокированные домены (из StevenBlack/hosts).
block_list: HashSet<String>,
/// Приватные суффиксы, которые всегда NXDomain (`.lan`, `.local`, …).
forbidden_suffixes: Vec<String>,
/// Путь к кэшу блок-листа на диске.
cache_path: String,
excluded_domains: HashSet<String>, // Добавлено
/// Домены в обход туннеля: на них отвечаем ServFail → системный DNS.
excluded_domains: HashSet<String>,
}
impl DnsHandler {
@@ -160,6 +184,11 @@ impl DnsHandler {
Ok(())
}
/// Обрабатывает один DNS-запрос и возвращает сериализованный ответ.
///
/// Порядок решений: исключённый домен → ServFail (фолбэк на системный DNS);
/// приватный суффикс/блок-лист → NXDomain; A-запрос → фейковый IP; прочее →
/// пустой NoError. `None` — если запрос не разобрался.
pub fn handle_query(&self, data: &[u8], store: &mut FakeIpStore) -> Option<Vec<u8>> {
let req = Message::from_vec(data).ok()?;
let query = req.queries().first()?;
+48
View File
@@ -1,3 +1,23 @@
//! Главный движок клиента: poll-цикл smoltcp + мост TUN ⇄ туннель.
//!
//! [`Engine`] — это «сердце» клиентской стороны. В одной задаче `tokio` крутится
//! цикл [`run`](Engine::run), который на каждой итерации делает 7 шагов:
//! 1. download: туннель → локальные сокеты (с пер-сокетными бэклогами);
//! 2. upload: пакеты из TUN → устройство smoltcp;
//! 3. прогон стека smoltcp (`poll`);
//! 4. слив TX smoltcp → writer TUN;
//! 5. периодический лог статистики;
//! 6. обработка диагностических событий → снапшоты;
//! 7. адаптивный сон/пробуждение по событию (анти-spin).
//!
//! Принципиальная защита от bufferbloat и head-of-line: download раздаётся
//! **пер-сокетно** (`pending_download`), поэтому один застрявший потребитель не
//! морозит общий канал для остальных; оба направления делят одну задачу и ходят
//! по очереди с лимитом [`MAX_PACKETS_PER_TICK`] за тик.
//!
//! [`EngineBuilder`]/[`EngineConfig`] — сборка движка: DNS, маршрутизация,
//! установка туннеля ([`ClientHandler::connect`]) и параметры интерфейса.
use bytes::Bytes;
use netrunner_core::net::ClientHandler;
use netrunner_core::net::NetworkConfig;
@@ -57,6 +77,8 @@ const MAX_POLL_SLEEP: Duration = Duration::from_millis(2);
/// so it can never stall the shared download pipe for other sockets.
const MAX_PENDING_FRAMES_PER_SOCKET: usize = 64;
/// Движок клиентского стека: интерфейс smoltcp, реестр сокетов, мост в туннель
/// и диагностика. Живёт в одной задаче `tokio` (см. [`run`](Engine::run)).
pub struct Engine {
interface: Interface,
socket_set: SocketSet<'static>,
@@ -131,6 +153,10 @@ impl Engine {
}
}
/// Запускает главный цикл движка (не возвращается, пока туннель/TUN живы).
///
/// Поднимает reader/writer-задачи TUN и крутит 7-шаговый цикл из обзора
/// модуля. `tun` забирается во владение и расщепляется на половины.
pub async fn run(&mut self, tun: Tun) {
info!("Current routes: {:?}", self.interface.routes());
let (writer, reader) = tun.split().expect("Failed to split TUN");
@@ -495,6 +521,8 @@ impl Engine {
.poll(now, &mut self.device, &mut self.socket_set)
}
/// Задача чтения из TUN: читает IP-пакеты и шлёт их в движок. `send().await`
/// блокируется при полном канале → backpressure доходит до TUN-устройства ОС.
fn spawn_tun_reader(mut reader: DeviceReader, to_engine: mpsc::Sender<Vec<u8>>) {
tokio::spawn(async move {
debug!("TUN Reader task started");
@@ -519,6 +547,8 @@ impl Engine {
});
}
/// Задача записи в TUN: принимает готовые пакеты из движка и пишет их в
/// устройство (отдаёт приложению то, что пришло из туннеля).
fn spawn_tun_writer(mut writer: DeviceWriter, mut from_engine: mpsc::Receiver<Vec<u8>>) {
tokio::spawn(async move {
debug!("TUN Writer task started");
@@ -674,17 +704,28 @@ impl Engine {
// ─── EngineConfig & EngineBuilder (unchanged API surface) ──────────────────
/// Параметры запуска движка (билдер-стайл через `with_*`).
#[derive(Clone, Debug)]
pub struct EngineConfig {
/// Адрес прокси-сервера (`host:port`).
pub remote_address: String,
/// Путь к директории кэша (блок-лист DNS и т.п.).
pub cache_path: String,
/// MTU интерфейса.
pub mtu: usize,
/// Настраивать ли системную маршрутизацию (на мобильных — нет, это делает ОС).
pub setup_routing: bool,
/// Принимать пакеты на любой IP (`any_ip` интерфейса smoltcp).
pub any_ip: bool,
/// Прозрачный режим (стек как промежуточный узел, без своего «адреса»).
pub transparent_mode: bool,
/// Шлюз по умолчанию внутри стека.
pub default_gateway: Ipv4Addr,
/// Включён ли kill-switch (резать трафик мимо туннеля).
pub killswitch_enabled: bool,
/// Приложения в обход туннеля (split-tunneling).
pub excluded_apps: Vec<String>,
/// Домены в обход туннеля.
pub excluded_domains: Vec<String>,
}
@@ -735,6 +776,7 @@ impl EngineConfig {
}
}
/// Сборщик [`Engine`]: подготавливает DNS, маршрутизацию, туннель и интерфейс.
pub struct EngineBuilder {
config: EngineConfig,
tun_device: Option<Tun>,
@@ -755,6 +797,12 @@ impl EngineBuilder {
self
}
/// Собирает готовый к запуску движок.
///
/// Инициализирует DNS-блоклист, при необходимости ставит системные маршруты,
/// поднимает диагностику и **устанавливает туннель** ([`ClientHandler::connect`]),
/// затем создаёт [`Engine`], настраивает интерфейс и добавляет маршруты-исключения
/// для split-tunneling доменов. Возвращает движок и TUN для последующего `run`.
pub async fn build(self) -> Result<(Engine, Tun), String> {
let tun = self.tun_device.ok_or("TUN device is required")?;
+19
View File
@@ -1,3 +1,22 @@
//! Сетевой слой клиента: userspace TCP/IP-стек на smoltcp + мост в туннель.
//!
//! Здесь живёт «локальная» половина клиента — то, что превращает перехваченные
//! IP-пакеты приложений в логические соединения и кормит ими ядро
//! ([`netrunner_core`]). Поток данных:
//!
//! ```text
//! TUN ─пакеты→ engine ─push_rx→ smoltcp ─→ connection_manager
//! ─перехват SYN/датаграмм→ TcpConnection/UdpConnection ─RawCastFrame→ туннель
//! ```
//!
//! Состав:
//! - [`engine`] — главный poll-цикл стека и сборка ([`EngineBuilder`](engine::EngineBuilder)).
//! - [`connection_manager`] — перехват L3-пакетов, реестр сокетов, диспетчеризация.
//! - [`connection`] — виртуальные TCP/UDP-соединения и ICMP-ответчик.
//! - [`session_tracker`] — NAT-таблица сокетов, idle/LRU-уборка.
//! - [`socket_factory`] — создание smoltcp-сокетов под профиль трафика.
//! - [`dns`] — локальный DNS с фейковыми IP и блок-листом.
mod connection;
pub mod connection_manager;
mod dns;
+24
View File
@@ -1,3 +1,19 @@
//! Реестр виртуальных соединений userspace-стека smoltcp.
//!
//! [`SessionTracker`] — это «NAT-таблица» клиента: он связывает хендлы сокетов
//! smoltcp ([`SocketHandle`]) с логическими id, держит активные TCP/UDP-соединения
//! и каналы доставки входящих данных, следит за активностью и убирает «призраков».
//!
//! Ключевые обязанности:
//! - **Реестр** TCP/UDP-соединений и двусторонний маппинг `handle ⇄ id`.
//! - **Pending TCP** — полуоткрытые соединения с удерживаемым семафор-пермитом
//! (ограничение числа одновременных установок).
//! - **Idle-выметание** ([`enforce_idle_timeouts`](SessionTracker::enforce_idle_timeouts))
//! и **LRU-эвикт** ([`evict_oldest_socket`](SessionTracker::evict_oldest_socket))
//! при достижении лимита сокетов — с защитой системных/слушающих сокетов.
//! - **Отложенное удаление**: `queue_removal` + `cleanup` (нельзя трогать
//! `SocketSet` во время итерации по нему).
use std::{
collections::HashMap,
time::{Duration, Instant as StdInstant},
@@ -15,6 +31,7 @@ use tokio::sync::{OwnedSemaphorePermit, mpsc};
use crate::net::connection::{TcpConnection, UdpConnection};
/// Состояние всех виртуальных соединений и их маппингов на хендлы smoltcp.
pub struct SessionTracker {
last_activity: HashMap<SocketHandle, StdInstant>,
active_tcp: HashMap<SocketHandle, TcpConnection>,
@@ -146,6 +163,11 @@ impl SessionTracker {
}
}
/// LRU-эвикт для освобождения слота при достижении лимита сокетов.
///
/// Сначала ищет уже «мёртвый» TCP-сокет (Closed/TimeWait/CloseWait/FinWait);
/// если таких нет — закрывает самый давно неактивный пользовательский сокет,
/// **не трогая** системные/слушающие. Возвращает `true`, если кого-то закрыл.
pub fn evict_oldest_socket(&mut self, socket_set: &mut SocketSet) -> bool {
let mut victim = None;
@@ -193,6 +215,8 @@ impl SessionTracker {
}
}
/// Фактически удаляет все сокеты из очереди `to_remove` из `SocketSet` и всех
/// внутренних таблиц. Вызывается вне итерации по сокетам (см. отложенность).
pub fn cleanup(&mut self, socket_set: &mut SocketSet) {
for handle in self.to_remove.drain(..) {
socket_set.remove(handle);
+28 -1
View File
@@ -1,3 +1,14 @@
//! Фабрика smoltcp-сокетов с профилями трафика.
//!
//! Разные виды трафика хотят разные сокеты: «толстым» закачкам (HTTP/HTTPS) нужны
//! большие буферы ради throughput, интерактиву (SSH/RDP/VNC) — маленькие ради
//! низкой задержки, DNS — совсем маленькие. [`TrafficProfile`] классифицирует
//! трафик по порту, а [`SmolSocketFactory`] (за трейтом [`SocketProvider`])
//! создаёт TCP/UDP/ICMP-сокеты с буферами под профиль из [`NetworkConfig`].
//!
//! TCP-сокеты настраиваются под низкую задержку: Nagle off, без ack-delay,
//! congestion control = BBR.
use netrunner_core::net::{
BUFFERBLOAT_WARN_THRESHOLD, HTTPS_PORT, HTTP_ALT_PORT, HTTP_PORT, ICMP_BUFFER_SIZE,
ICMP_META_SLOTS, MAX_SOCKETS, NTP_PORT, RDP_PORT, RTMP_PORT, SSH_PORT, VNC_PORT, NetworkConfig,
@@ -16,11 +27,16 @@ use smoltcp::{
};
use std::sync::Arc;
/// Класс трафика, определяющий размеры буферов сокета.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrafficProfile {
/// Интерактив (SSH/RDP/VNC): маленькие буферы, минимум задержки.
Interactive,
/// Объёмные потоки (HTTP/HTTPS/RTMP): большие буферы, максимум throughput.
Bulk,
/// DNS/NTP: совсем маленькие буферы.
Dns,
/// Всё остальное: умеренные буферы.
Default,
}
@@ -28,6 +44,8 @@ pub const TCP_SOCKET_KEEP_ALIVE: Duration = Duration::from_secs(15);
pub const TCP_SOCKET_ACTIVE_TIMEOUT: Duration = Duration::from_secs(60);
impl TrafficProfile {
/// Угадывает профиль по (порту назначения, протоколу). Эвристика на основе
/// известных портов; неизвестные → [`TrafficProfile::Default`].
pub fn guess_from_port(port: u16, is_tcp: bool) -> Self {
match (port, is_tcp) {
(SSH_PORT, true) | (RDP_PORT, true) | (VNC_PORT, true) => Self::Interactive,
@@ -40,17 +58,25 @@ impl TrafficProfile {
}
}
/// Абстракция создания сокетов стека (позволяет подменять в тестах).
pub trait SocketProvider: Send + Sync {
/// Создаёт исходящий TCP-сокет под профиль.
fn create_tcp(&self, profile: TrafficProfile) -> tcp::Socket;
/// Создаёт UDP-сокет под профиль.
fn create_udp(&self, profile: TrafficProfile) -> udp::Socket<'static>;
/// Создаёт ICMP-сокет (для ответов на ping).
fn create_icmp(&self, profile: TrafficProfile) -> icmp::Socket<'static>;
/// Создаёт слушающий TCP-сокет (профиль угадывается по порту).
fn create_listening_tcp(&self, addr: Option<IpAddress>, port: u16) -> tcp::Socket;
/// Создаёт привязанный UDP-сокет (профиль угадывается по порту).
fn create_bound_udp(&self, addr: Option<IpAddress>, port: u16) -> udp::Socket<'static>;
/// Создаёт базовый набор сокетов (слушающий UDP:53 + `n_icmp` ICMP).
fn create_base_set(&self, n_icmp: usize) -> SocketSet<'static>;
/// Перенастраивает уже существующий TCP-сокет под профиль (Nagle/BBR и т.п.).
fn reconfigure_tcp(&self, socket: &mut tcp::Socket, profile: TrafficProfile);
// 🔥 НОВЫЙ МЕТОД: Логирование статистики всех сокетов в сете
/// Логирует статистику всех активных сокетов (диагностика bufferbloat).
fn log_stats(
&self,
sockets: &SocketSet,
@@ -58,6 +84,7 @@ pub trait SocketProvider: Send + Sync {
);
}
/// Реализация [`SocketProvider`] поверх [`NetworkConfig`].
pub struct SmolSocketFactory {
config: Arc<NetworkConfig>,
}