docs [AI]
This commit is contained in:
@@ -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).
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()?;
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user