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
+33
View File
@@ -1,3 +1,23 @@
//! # netrunner-client — клиент VPN и FFI-фасад для приложений
//!
//! Крейт собирается двумя способами: как библиотека (`.so`) для мобильных
//! приложений через **UniFFI** (этот файл) и как самостоятельный бинарь для
//! Linux ([`main.rs`](crate)). Вся реальная логика — в подмодулях:
//!
//! - [`net`] — userspace TCP/IP-стек на smoltcp и мост в туннель ядра;
//! - [`tun`] — TUN-устройство, smoltcp-`Device` и системная маршрутизация.
//!
//! ## FFI-поверхность (что видит Kotlin/Swift)
//!
//! - [`SessionManager`] — фабрика сессий: [`spawn_session`](SessionManager::spawn_session)
//! поднимает движок в фоне и возвращает управляемую [`Session`]; [`get_traffic_stats`](SessionManager::get_traffic_stats)
//! отдаёт счётчики.
//! - [`Session`] — ручка живого VPN; [`stop`](Session::stop) (и `Drop`) гасит
//! задачи и откатывает маршрутизацию.
//! - [`VpnTrafficStats`] — снимок трафика для UI.
//!
//! Токио-рантайм создаётся один раз ([`RUNTIME`]) и переживёт все сессии.
// Workaround for rustc 1.94 ICE in check_mod_deathness (dead-code MIR pass).
#![allow(dead_code)]
@@ -21,8 +41,10 @@ use std::sync::{Arc, OnceLock};
use tokio::runtime::Runtime;
use tokio_util::sync::CancellationToken;
/// Глобальный многопоточный tokio-рантайм, общий для всех сессий.
pub static RUNTIME: OnceLock<Runtime> = OnceLock::new();
/// Ленивая инициализация общего рантайма (создаётся при первом обращении).
fn get_runtime() -> &'static Runtime {
RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
@@ -32,6 +54,7 @@ fn get_runtime() -> &'static Runtime {
})
}
/// Снимок счётчиков трафика для отображения в приложении.
#[derive(uniffi::Record)]
pub struct VpnTrafficStats {
pub rx_bytes: u64,
@@ -40,6 +63,10 @@ pub struct VpnTrafficStats {
pub tx_packets: u64,
}
/// Ручка одной живой VPN-сессии (передаётся в приложение как объект UniFFI).
///
/// Хранит токен отмены и данные для отката маршрутизации. Останавливается явно
/// ([`stop`](Session::stop)) или автоматически при `Drop`.
#[derive(uniffi::Object)]
pub struct Session {
pub(crate) cancel_token: CancellationToken,
@@ -64,6 +91,7 @@ impl Drop for Session {
}
}
/// Фабрика VPN-сессий — главная точка входа FFI.
#[derive(uniffi::Object)]
pub struct SessionManager;
@@ -74,6 +102,11 @@ impl SessionManager {
Arc::new(SessionManager)
}
/// Поднимает VPN-сессию в фоне и возвращает управляющую [`Session`].
///
/// Создаёт TUN (из переданного `_tun_fd` на мобильных или сам на Linux),
/// конфигурирует движок (MTU, kill-switch, исключения) и запускает его в
/// общем рантайме под токеном отмены. Не блокирует вызывающий поток.
pub fn spawn_session(
&self,
remote_address: String,
+8
View File
@@ -1,3 +1,11 @@
//! Бинарь клиента для Linux-десктопа (отладка/локальный запуск).
//!
//! Поднимает TUN-интерфейс `netr0` (10.0.0.1/24), инициализирует движок
//! ([`EngineBuilder`]) на захардкоженный адрес прокси и крутит его до Ctrl+C,
//! после чего восстанавливает системную маршрутизацию. Мобильная сборка идёт не
//! отсюда, а через FFI в [`lib.rs`](crate) (uniffi) — здесь же удобная точка
//! входа для запуска под Linux без Android.
// Workaround for rustc 1.94 ICE in check_mod_deathness (dead-code MIR pass).
#![allow(dead_code)]
+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>,
}
+40
View File
@@ -0,0 +1,40 @@
# Блок `client/tun` — TUN-интерфейс и системная маршрутизация
«Железо» клиента — граница с ОС, через которую в стек попадают сырые L3-пакеты
приложений и куда возвращаются ответы. Платформо-зависимый ввод/вывод.
> Детали — в rustdoc крейта `netrunner-client`, модуль `tun`.
## Файлы
| Файл | Роль |
|--------------|------------------------------------------------------------------|
| `tun.rs` | Обёртка `Tun`: создание/открытие устройства, `split` на reader/writer. |
| `device.rs` | Счётчики трафика (`GLOBAL_*` для FFI) + `TrafficCounter` со скоростью. |
| `routing.rs` | Установка/снятие системных маршрутов и kill-switch. |
## Как создаётся TUN
- **Linux (desktop)** — `Tun::create(...)` с именем `netr0`, адресом 10.0.0.1/24.
- **Android/iOS** — `Tun::from_fd(fd)`: дескриптор приходит из нативного `VpnService`.
- **Split** разводит устройство на пишущую и читающую половины — их забирают
разные задачи движка ([`net`](../net)).
## Маршрутизация (`routing.rs`)
`setup_platform_routing` заворачивает трафик в TUN, сохраняя доступ к прокси; при
`killswitch` режет всё мимо туннеля. Реализация целиком по `cfg`:
| Платформа | Механизм |
|---------------|------------------------------------------------------------------|
| Linux | `nftables` (таблица `netrunner`) + policy-routing `ip rule`/`table 100`; split-tunneling по UID; DNAT DNS на стек. |
| Windows | таблица маршрутов (`0.0.0.0/1`+`128.0.0.0/1` поверх дефолта); kill-switch удалением дефолта; откат через DHCP-renew. |
| Android/iOS | ничего — маршруты ставит нативная сторона. |
`reset_platform_routing` откатывает всё при остановке.
## Связи
Пакеты из `Tun` читает движок [`net`](../net); счётчики `GLOBAL_*` отдаются в
приложение через FFI (`lib.rs`). Имя `device.rs` историческое — сам smoltcp-`Device`
живёт во внешнем форке smoltcp, здесь только метрики.
+18 -1
View File
@@ -1,11 +1,26 @@
//! Учёт трафика TUN-интерфейса.
//!
//! Глобальные атомарные счётчики (`GLOBAL_*`) видны через FFI и отдают
//! приложению суммарную статистику сессии. [`TrafficCounter`] — пер-сессионный
//! учётчик, который вдобавок раз в секунду пересчитывает скользящую оценку
//! скорости (МБ/с) для отображения в UI.
//!
//! Имя файла историческое: собственно реализация smoltcp-`Device` поверх TUN
//! живёт во внешнем форке smoltcp; здесь — только метрики.
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant as StdInstant;
/// Суммарно принято байт за всё время (для FFI-статистики).
pub static GLOBAL_RX_BYTES: AtomicU64 = AtomicU64::new(0);
/// Суммарно отправлено байт за всё время.
pub static GLOBAL_TX_BYTES: AtomicU64 = AtomicU64::new(0);
/// Суммарно принято пакетов.
pub static GLOBAL_RX_PACKETS: AtomicU64 = AtomicU64::new(0);
/// Суммарно отправлено пакетов.
pub static GLOBAL_TX_PACKETS: AtomicU64 = AtomicU64::new(0);
/// Снимок статистики трафика с мгновенной скоростью.
#[derive(Debug, Clone, Copy)]
pub struct TrafficStats {
pub rx_bytes: u64,
@@ -16,7 +31,7 @@ pub struct TrafficStats {
pub tx_speed_mb_s: f64,
}
/// Per-session traffic counter with rolling speed estimate.
/// Пер-сессионный учётчик трафика со скользящей оценкой скорости.
pub struct TrafficCounter {
rx_bytes: u64,
tx_bytes: u64,
@@ -60,6 +75,8 @@ impl TrafficCounter {
GLOBAL_TX_PACKETS.fetch_add(1, Ordering::Relaxed);
}
/// Возвращает текущий снимок статистики; скорость пересчитывается не чаще
/// раза в секунду (между вызовами отдаётся закешированное значение).
pub fn get_stats(&mut self) -> TrafficStats {
let now = StdInstant::now();
let elapsed = now.duration_since(self.last_speed_calc).as_secs_f64();
+10
View File
@@ -1,3 +1,13 @@
//! TUN-интерфейс и системная маршрутизация (платформо-зависимый ввод/вывод).
//!
//! Это «железо» клиента — граница с ОС, через которую в стек попадают сырые
//! L3-пакеты приложений:
//! - [`tun`] — создание/открытие TUN-устройства и асинхронное чтение/запись пакетов.
//! - [`device`] — реализация smoltcp `Device` поверх TUN (RxToken/TxToken) +
//! глобальные счётчики трафика.
//! - [`routing`] — установка/снятие системных маршрутов и kill-switch (nftables/
//! ip-rule и аналоги), чтобы весь трафик заворачивался в туннель.
pub mod device;
pub mod routing;
pub mod tun;
+27
View File
@@ -1,8 +1,29 @@
//! Платформенная маршрутизация и kill-switch.
//!
//! Заворачивает системный трафик в TUN-интерфейс и (опционально) режет утечки
//! мимо туннеля. Реализация целиком платформо-зависимая (`cfg`):
//!
//! - **Linux** — `nftables` (таблица `netrunner`): маркировка трафика в TUN,
//! split-tunneling по UID, DNAT DNS на стек, kill-switch с исключениями для LAN
//! и самого прокси; плюс policy-routing через `ip rule`/`ip route table 100`.
//! - **Windows** — таблица маршрутов (`route add` половинками `0.0.0.0/1`+`128.0.0.0/1`,
//! чтобы перебить дефолт, не удаляя его), kill-switch удалением дефолтного
//! маршрута, восстановление через DHCP-renew.
//! - **Android/iOS** — ничего: маршрутизацию ставит нативная сторона (`VpnService`).
//!
//! [`setup_platform_routing`] ставит правила, [`reset_platform_routing`] —
//! откатывает их при остановке. Все внешние команды идут через [`run_cmd_ext`].
use netrunner_logger::{error, info};
use std::io;
use std::process::Command;
/// Выполняет внешнюю команду (через `shlex`-разбор строки).
///
/// `ignore_errors` — не падать на ненулевом коде возврата (для идемпотентных
/// операций вроде «удалить правило, которого может не быть»). На Windows окно
/// процесса скрывается флагом `CREATE_NO_WINDOW`.
pub fn run_cmd_ext(full_cmd: &str, ignore_errors: bool) -> io::Result<()> {
let parts = shlex::split(full_cmd)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid syntax"))?;
@@ -76,6 +97,9 @@ pub fn get_default_gateway_linux() -> Option<String> {
stdout.split_whitespace().nth(2).map(|s| s.to_string())
}
/// Ставит платформенные правила маршрутизации: весь трафic → TUN, доступ к
/// прокси сохраняется, при `killswitch` всё прочее блокируется. `excluded_apps`
/// (на Linux — UID) проходят мимо туннеля (split-tunneling).
pub fn setup_platform_routing(
remote_address: &str,
killswitch: bool,
@@ -219,6 +243,9 @@ pub fn setup_platform_routing(
Ok(())
}
/// Откатывает всё, что поставил [`setup_platform_routing`]: удаляет TUN-интерфейс/
/// правила/таблицы и восстанавливает обычную маршрутизацию (на Windows — через
/// DHCP-renew, если был включён kill-switch).
pub fn reset_platform_routing(_proxy_ip: Option<&str>, _was_killswitch: bool) -> io::Result<()> {
#[cfg(target_os = "linux")]
{
+13
View File
@@ -1,12 +1,22 @@
//! Обёртка над асинхронным TUN-устройством.
//!
//! [`Tun`] инкапсулирует создание/открытие TUN тремя путями: по конфигурации
//! ([`new`](Tun::new)), через билдер-замыкание ([`create`](Tun::create)) или из
//! готового файлового дескриптора ([`from_fd`](Tun::from_fd) — так его передаёт
//! Android `VpnService`). [`split`](Tun::split) разводит устройство на отдельные
//! reader/writer, чтобы читать и писать пакеты из разных задач.
use netrunner_logger::error;
use std::io;
use tun::{AsyncDevice, Configuration, DeviceReader, DeviceWriter, create_as_async};
/// Асинхронное TUN-устройство.
pub struct Tun {
device: AsyncDevice,
}
impl Tun {
/// Создаёт устройство из готовой конфигурации.
pub fn new(config: &Configuration) -> io::Result<Self> {
match create_as_async(config) {
Ok(device) => Ok(Self { device }),
@@ -17,6 +27,7 @@ impl Tun {
}
}
/// Создаёт устройство, настраивая конфигурацию через замыкание-билдер.
pub fn create<F>(f: F) -> io::Result<Self>
where
F: FnOnce(&mut Configuration),
@@ -26,6 +37,7 @@ impl Tun {
Self::new(&config)
}
/// Открывает устройство из уже существующего fd (передаётся Android `VpnService`).
pub fn from_fd(fd: i32) -> io::Result<Self> {
let mut config = Configuration::default();
config.raw_fd(fd);
@@ -34,6 +46,7 @@ impl Tun {
Self::new(&config)
}
/// Разводит устройство на пишущую и читающую половины (для разных задач).
pub fn split(self) -> io::Result<(DeviceWriter, DeviceReader)> {
let (writer, reader) = self.device.split()?;
Ok((writer, reader))