allocation optimization and in place crypt
This commit is contained in:
+14
-20
@@ -30,30 +30,24 @@ impl NetworkConfig {
|
||||
pub fn new(system_mtu: usize) -> Self {
|
||||
Self {
|
||||
mtu: system_mtu,
|
||||
// Для сервера 64KB ок, для клиента поднимем до 256KB, чтобы не тормозить чтение с TUN
|
||||
connection_buf_size: 256 * 1024,
|
||||
connection_buf_size: 128 * 1024, // Уменьшили с 256KB
|
||||
tcp_buffer_size: 128 * 1024, // Уменьшили с 256KB
|
||||
udp_buffer_size: 64 * 1024, // Уменьшили со 128KB
|
||||
tcp_chunk_size: 1024 * 1024 * 64,
|
||||
|
||||
// 256KB — это золотая середина. Позволяет держать ~10Мбит на стрим при пинге 200мс.
|
||||
tcp_buffer_size: 256 * 1024,
|
||||
udp_buffer_size: 128 * 1024,
|
||||
tcp_chunk_size: system_mtu - 100,
|
||||
channel_capacity: 512,
|
||||
|
||||
// Емкость каналов: 512 пакетов (~700КБ).
|
||||
// Этого достаточно, чтобы сгладить лаги радио-эфира на телефоне.
|
||||
channel_capacity: 8192,
|
||||
// Окна smoltcp (уменьшаем, чтобы не создавать огромные очереди)
|
||||
tcp_rx_heavy: 128 * 1024,
|
||||
tcp_tx_heavy: 128 * 1024,
|
||||
|
||||
// Окна smoltcp (важно для Download)
|
||||
// Увеличиваем до 512KB для тяжелых профилей
|
||||
tcp_rx_heavy: 256 * 1024,
|
||||
tcp_tx_heavy: 256 * 1024,
|
||||
tcp_rx_light: 32 * 1024, // Уменьшили с 64KB
|
||||
tcp_tx_light: 32 * 1024,
|
||||
|
||||
tcp_rx_light: 64 * 1024,
|
||||
tcp_tx_light: 64 * 1024,
|
||||
|
||||
udp_buf_heavy: 256 * 1024,
|
||||
udp_meta_heavy: 1024, // Больше метаданных для мелких UDP пакетов
|
||||
udp_buf_light: 32 * 1024,
|
||||
udp_meta_light: 64,
|
||||
udp_buf_heavy: 128 * 1024,
|
||||
udp_meta_heavy: 512,
|
||||
udp_buf_light: 16 * 1024,
|
||||
udp_meta_light: 32,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,16 @@ impl TunnelEngine {
|
||||
|| self.rx_codec.is_none()
|
||||
|| self.tx_codec.is_none()
|
||||
{
|
||||
// 💡 ИСПРАВЛЕНИЕ 2: Если это Сервер (remote_addr пуст), он НЕ должен делать реконнект.
|
||||
// Мертвая лега должна просто завершиться и удалиться из памяти.
|
||||
if self.remote_addr.is_empty() {
|
||||
info!(
|
||||
"Server leg {} dropped, shutting down engine task",
|
||||
self.leg_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.leg_status = LegStatus::Reconnecting;
|
||||
match self.attempt_reconnect().await {
|
||||
Ok((new_in, new_out, new_rx, new_tx)) => {
|
||||
@@ -91,7 +101,6 @@ impl TunnelEngine {
|
||||
let outbound = self.outbound.take().unwrap();
|
||||
let read_buf = std::mem::take(&mut self.read_buf);
|
||||
|
||||
// 💡 ИЗМЕНЕНО: Извлекаем кодеки и каналы из структуры во владение
|
||||
let mut rx_codec = self.rx_codec.take().unwrap();
|
||||
let mut tx_codec = self.tx_codec.take().unwrap();
|
||||
let mut control_rx = self.control_rx.take().expect("control_rx is missing");
|
||||
@@ -106,11 +115,10 @@ impl TunnelEngine {
|
||||
let token_reader = token.clone();
|
||||
let token_writer = token.clone();
|
||||
|
||||
// ЧИТАЮЩАЯ ЗАДАЧА
|
||||
// ЧИТАЮЩАЯ ЗАДАЧА (Остается без изменений)
|
||||
let mut reader_handle = tokio::spawn(async move {
|
||||
let mut read_buf = read_buf;
|
||||
let mut inbound = inbound;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 1024 * 1024; // 1 MB
|
||||
|
||||
loop {
|
||||
@@ -119,7 +127,7 @@ impl TunnelEngine {
|
||||
return Err(AppError::new(
|
||||
ERR_INFRA_TIMEOUT,
|
||||
"Переполнение буфера",
|
||||
"Read buffer exceeded MAX_BUFFER_SIZE",
|
||||
"OOM Protection",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -134,29 +142,15 @@ impl TunnelEngine {
|
||||
break;
|
||||
}
|
||||
res = inbound.read_buf(&mut read_buf) => {
|
||||
let n = res.map_err(|e| {
|
||||
AppError::new(
|
||||
ERR_INFRA_TIMEOUT,
|
||||
"Сбой сети",
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let n = res.map_err(|e| AppError::new(ERR_INFRA_TIMEOUT, "Сбой сети", e.to_string()))?;
|
||||
if n == 0 {
|
||||
if read_buf.is_empty() {
|
||||
info!("Connection closed by peer (Clean EOF)");
|
||||
} else {
|
||||
error!("Connection abruptly closed by peer (Incomplete frame: {} bytes left)", read_buf.len());
|
||||
}
|
||||
// Возвращаем rx_codec обратно при EOF
|
||||
info!("Connection closed by peer (Clean EOF)");
|
||||
return Ok::<_, AppError>((true, read_buf, rx_codec));
|
||||
}
|
||||
|
||||
muxer.record_leg_rx(leg_id, n as u64);
|
||||
|
||||
let mut frames = Vec::new();
|
||||
|
||||
// 💡 ИЗМЕНЕНО: Вызов decode_inbound напрямую без блокировок
|
||||
loop {
|
||||
match rx_codec.decode_inbound(&mut read_buf) {
|
||||
Ok(Some(frame)) => frames.push(frame),
|
||||
@@ -164,19 +158,9 @@ impl TunnelEngine {
|
||||
Err(e) => {
|
||||
if e.action == ErrorAction::Wait { break; }
|
||||
if e.action == ErrorAction::Drop {
|
||||
error!("CRITICAL: Crypto tampering or sync lost. Hard dropping tunnel!");
|
||||
return Err(AppError::new(
|
||||
ERR_NET_TLS_TAMPER,
|
||||
"Критическая ошибка шифрования",
|
||||
"Crypto drop",
|
||||
));
|
||||
return Err(AppError::new(ERR_NET_TLS_TAMPER, "Ошибка шифрования", "Crypto drop"));
|
||||
}
|
||||
error!(error = ?e, "Codec inbound failed");
|
||||
return Err(AppError::new(
|
||||
ERR_NET_TLS_TAMPER,
|
||||
"Ошибка декодирования",
|
||||
format!("Codec error: {:?}", e),
|
||||
));
|
||||
return Err(AppError::new(ERR_NET_TLS_TAMPER, "Сбой кодека", format!("{:?}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,9 +168,7 @@ impl TunnelEngine {
|
||||
for frame in frames {
|
||||
if frame.header.frame_type == FrameType::Heartbeat {
|
||||
let m = muxer.clone();
|
||||
tokio::spawn(async move {
|
||||
m.record_pong(leg_id).await;
|
||||
});
|
||||
tokio::spawn(async move { m.record_pong(leg_id).await; });
|
||||
}
|
||||
handler.handle(frame).await;
|
||||
}
|
||||
@@ -201,18 +183,15 @@ impl TunnelEngine {
|
||||
let mut outbound = outbound;
|
||||
let mut heartbeat = tokio::time::interval(HEALTH_CHECK_INTERVAL);
|
||||
|
||||
let mut pending_data: Option<MuxMessage> = None;
|
||||
const INTERLEAVE_CHUNK: usize = 16384;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
_ = token_writer.cancelled() => break,
|
||||
msg_opt = control_rx.recv() => {
|
||||
if let Some(msg) = msg_opt {
|
||||
// 💡 ИЗМЕНЕНО: Передаем &mut tx_codec напрямую, никаких .lock().await!
|
||||
if let Err(e) = Self::handle_outbound(&mut outbound, &mut tx_codec, msg).await {
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
_ = heartbeat.tick() => {
|
||||
muxer_pong.record_ping_sent(leg_id);
|
||||
let msg = MuxMessage { stream_id: 0, frame_type: FrameType::Heartbeat, data: Bytes::new() };
|
||||
@@ -220,16 +199,48 @@ impl TunnelEngine {
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
}
|
||||
msg_opt = data_rx.recv() => {
|
||||
|
||||
msg_opt = control_rx.recv() => {
|
||||
if let Some(msg) = msg_opt {
|
||||
if let Err(e) = Self::handle_outbound(&mut outbound, &mut tx_codec, msg).await {
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
// 💡 ИСПРАВЛЕНИЕ 1: Мгновенно заходим в эту ветку, если есть данные
|
||||
_ = std::future::ready(()), if pending_data.is_some() => {
|
||||
let mut msg = pending_data.take().unwrap();
|
||||
|
||||
let chunk_size = std::cmp::min(msg.data.len(), INTERLEAVE_CHUNK);
|
||||
let chunk_data = msg.data.split_to(chunk_size);
|
||||
|
||||
let chunk_msg = MuxMessage {
|
||||
stream_id: msg.stream_id,
|
||||
frame_type: msg.frame_type.clone(),
|
||||
data: chunk_data,
|
||||
};
|
||||
|
||||
if let Err(e) = Self::handle_outbound(&mut outbound, &mut tx_codec, chunk_msg).await {
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
|
||||
if !msg.data.is_empty() {
|
||||
pending_data = Some(msg);
|
||||
}
|
||||
|
||||
// 💡 ИСПРАВЛЕНИЕ 1.2: Вызываем yield ЗДЕСЬ. Это заставит планировщик
|
||||
// проверить пинги и контрольные пакеты перед отправкой следующего куска.
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
msg_opt = data_rx.recv(), if pending_data.is_none() => {
|
||||
if let Some(msg) = msg_opt {
|
||||
pending_data = Some(msg);
|
||||
} else { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Возвращаем receiver'ы и tx_codec обратно при корректном завершении
|
||||
Ok::<
|
||||
_,
|
||||
(
|
||||
@@ -246,7 +257,7 @@ impl TunnelEngine {
|
||||
match res_reader {
|
||||
Ok(Ok((is_eof, r_buf, returned_rx_codec))) => {
|
||||
self.read_buf = r_buf;
|
||||
self.rx_codec = Some(returned_rx_codec); // Восстанавливаем
|
||||
self.rx_codec = Some(returned_rx_codec);
|
||||
if is_eof {
|
||||
token.cancel();
|
||||
let w_res = writer_handle.await.unwrap();
|
||||
@@ -288,16 +299,23 @@ impl TunnelEngine {
|
||||
};
|
||||
|
||||
token.cancel();
|
||||
reader_handle.abort();
|
||||
writer_handle.abort();
|
||||
|
||||
if let Err(e) = &res {
|
||||
if let Err(e) = res {
|
||||
error!("TunnelEngine critical failure: {}", e);
|
||||
return res;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 💡 ИСПРАВЛЕНИЕ 2.2: И здесь тоже, если сервер словил EOF, он не должен идти на реконнект.
|
||||
if self.remote_addr.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Tunnel iteration finished, preparing to reconnect...");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 ИЗМЕНЕНО: Принимает &mut TxCodec, синхронное и сверхбыстрое шифрование
|
||||
async fn handle_outbound(
|
||||
outbound: &mut OwnedWriteHalf,
|
||||
@@ -342,8 +360,9 @@ impl TunnelEngine {
|
||||
|
||||
for pkt in packets {
|
||||
let write_future = outbound.write_all(&pkt);
|
||||
// 💡 ИЗМЕНЕНО: Увеличен таймаут отправки до 20 секунд для совместимости с агрессивным BBR
|
||||
if let Err(_) =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), write_future).await
|
||||
tokio::time::timeout(std::time::Duration::from_secs(20), write_future).await
|
||||
{
|
||||
error!(stream_id, "🔥 Physical leg STUCK on write. Killing leg.");
|
||||
return Err(AppError::new(
|
||||
|
||||
@@ -150,11 +150,13 @@ impl Muxer {
|
||||
}
|
||||
|
||||
fn select_leg(&self, stream_id: u32) -> Option<MuxLeg> {
|
||||
// 1. Читаем кэш (это Arc, поэтому clone здесь — это просто инкремент счетчика, не копирование данных)
|
||||
let legs = self.active_legs_cache.read().unwrap().clone();
|
||||
if legs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Если поток уже привязан к леге, используем её (Sticky Connection)
|
||||
if let Some(leg_id_ref) = self.stream_bindings.get(&stream_id) {
|
||||
let leg_id = *leg_id_ref;
|
||||
if let Some(leg) = legs.iter().find(|l| l.id == leg_id) {
|
||||
@@ -162,22 +164,31 @@ impl Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
let mut candidates = (*legs).clone();
|
||||
candidates.sort_by(|a, b| {
|
||||
let score_a =
|
||||
a.stats.rtt_ms.load(Ordering::Relaxed) as f64 + (a.congestion_factor() * 2000.0);
|
||||
let score_b =
|
||||
b.stats.rtt_ms.load(Ordering::Relaxed) as f64 + (b.congestion_factor() * 2000.0);
|
||||
score_a
|
||||
.partial_cmp(&score_b)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
// 3. O(N) поиск лучшей леги без сортировки всего вектора
|
||||
// Мы берем подмножество (pool) и сразу ищем в нем минимум
|
||||
let pool_size = std::cmp::min(legs.len(), MUXER_POOL_SIZE);
|
||||
|
||||
let pool_size = std::cmp::min(candidates.len(), MUXER_POOL_SIZE);
|
||||
let selected_leg = candidates[stream_id as usize % pool_size].clone();
|
||||
// Используем min_by, чтобы найти лучший вариант за один проход
|
||||
let selected_leg = legs
|
||||
.iter()
|
||||
.take(pool_size) // Берем только пул
|
||||
.min_by(|a, b| {
|
||||
let score_a = a.stats.rtt_ms.load(Ordering::Relaxed) as f64
|
||||
+ (a.congestion_factor() * 2000.0);
|
||||
let score_b = b.stats.rtt_ms.load(Ordering::Relaxed) as f64
|
||||
+ (b.congestion_factor() * 2000.0);
|
||||
score_a
|
||||
.partial_cmp(&score_b)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.cloned();
|
||||
|
||||
self.stream_bindings.insert(stream_id, selected_leg.id);
|
||||
Some(selected_leg)
|
||||
if let Some(leg) = selected_leg {
|
||||
self.stream_bindings.insert(stream_id, leg.id);
|
||||
return Some(leg);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn record_ping_sent(&self, leg_id: u32) {
|
||||
@@ -215,17 +226,12 @@ impl Muxer {
|
||||
};
|
||||
|
||||
let is_data = matches!(message.frame_type, FrameType::Data | FrameType::UdpData);
|
||||
let target_tx = if is_data {
|
||||
&leg.data_tx
|
||||
} else {
|
||||
&leg.control_tx
|
||||
};
|
||||
|
||||
let stream_id = message.stream_id;
|
||||
let size = message.data.len() as u64;
|
||||
|
||||
if is_data {
|
||||
match target_tx.try_send(message) {
|
||||
// 💡 ДАННЫЕ: Используем .send().await для создания Backpressure
|
||||
match leg.data_tx.send(message).await {
|
||||
Ok(_) => {
|
||||
leg.stats.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(stream_ref) = self.streams.get(&stream_id) {
|
||||
@@ -237,24 +243,37 @@ impl Muxer {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => Ok(()), // HoL Blocking eliminated
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
Err(_) => {
|
||||
self.remove_leg(leg.id, &leg.control_tx);
|
||||
Err(AppError::new(ERR_INFRA_TIMEOUT, "Обрыв", "Leg closed"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(2), target_tx.send(message))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
_ => {
|
||||
// 💡 КОНТРОЛЬ: Используем .try_send() для мгновенной приоритетной отправки (Non-blocking)
|
||||
match leg.control_tx.try_send(message) {
|
||||
Ok(_) => {
|
||||
leg.stats.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(stream_ref) = self.streams.get(&stream_id) {
|
||||
stream_ref
|
||||
.value()
|
||||
.1
|
||||
.tx_bytes
|
||||
.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
|
||||
// Канал контроля не должен забиваться. Если это произошло, пакет сбрасывается,
|
||||
// чтобы предотвратить зависание критических задач.
|
||||
netrunner_logger::warn!(
|
||||
stream_id,
|
||||
"Control queue FULL! Dropping control frame to avoid deadlock."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
self.remove_leg(leg.id, &leg.control_tx);
|
||||
Err(AppError::new(
|
||||
ERR_INFRA_TIMEOUT,
|
||||
"Таймаут",
|
||||
"Control send timeout",
|
||||
))
|
||||
Err(AppError::new(ERR_INFRA_TIMEOUT, "Обрыв", "Leg closed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,16 +330,18 @@ impl Muxer {
|
||||
|
||||
#[inline]
|
||||
pub fn dispatch_to_local(&self, stream_id: u32, data: Bytes) {
|
||||
if let Some(stream_ref) = self.streams.get(&stream_id) {
|
||||
let tx = &stream_ref.value().0;
|
||||
let stats = &stream_ref.value().1;
|
||||
let size = data.len() as u64;
|
||||
// Вычисляем размер здесь, так как переменная size не была определена
|
||||
let size = data.len() as u64;
|
||||
|
||||
match tx.try_send(data) {
|
||||
Ok(_) => {
|
||||
stats.rx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
Err(_) => {} // Silent drop to avoid slowing down Reader Task
|
||||
let tx_and_stats = self.streams.get(&stream_id).map(|s| {
|
||||
let val = s.value();
|
||||
(val.0.clone(), val.1.clone()) // Клонируем Arc и Sender
|
||||
});
|
||||
|
||||
if let Some((tx, stats)) = tx_and_stats {
|
||||
// Используем .try_send() для неблокирующей доставки в локальный поток
|
||||
if tx.try_send(data).is_ok() {
|
||||
stats.rx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub const MAX_SOCKETS: usize = 64;
|
||||
pub const MAX_SOCKETS: usize = 256;
|
||||
pub const MAX_TUNNEL_LEGS: u32 = 4;
|
||||
pub const MUXER_POOL_SIZE: usize = 3;
|
||||
|
||||
pub const TCP_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
pub const UDP_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
pub const UDP_IDLE_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
pub const GLOBAL_IDLE_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
|
||||
// 🔥 ФИКС: Ускоряем обнаружение мертвой сети при переключении Wi-Fi -> LTE
|
||||
|
||||
Reference in New Issue
Block a user