docs [AI]
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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, здесь только метрики.
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user