diagnostics, recconections fix and bufferbloat fixes
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
// Workaround for rustc 1.94 ICE in check_mod_deathness (dead-code MIR pass).
|
||||
#![allow(dead_code)]
|
||||
|
||||
use netrunner_core::net::NetworkConfig;
|
||||
use uniffi;
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Workaround for rustc 1.94 ICE in check_mod_deathness (dead-code MIR pass).
|
||||
#![allow(dead_code)]
|
||||
|
||||
use netrunner_logger::{error, info};
|
||||
mod net;
|
||||
mod tun;
|
||||
|
||||
+21
-10
@@ -11,9 +11,20 @@ use std::time::{Duration, SystemTime};
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
// --- Fake IP Storage ---
|
||||
// --- Constants ---
|
||||
|
||||
const START_IP: u32 = 0x64400001; // 100.64.0.1
|
||||
/// Start of the RFC 6598 (CGNAT) range used for fake DNS responses.
|
||||
const FAKE_IP_START: u32 = 0x6440_0001; // 100.64.0.1
|
||||
/// LRU capacity for the domain→IP and IP→domain mapping tables.
|
||||
const FAKE_IP_CACHE_SIZE: usize = 2000;
|
||||
/// Re-download the blocklist if the cached file is older than this.
|
||||
const BLOCKLIST_UPDATE_INTERVAL: Duration = Duration::from_secs(7 * 24 * 3600);
|
||||
/// Delay before starting a background blocklist download on first run.
|
||||
const BLOCKLIST_DOWNLOAD_DELAY: Duration = Duration::from_secs(10);
|
||||
/// HTTP timeout for the blocklist download request.
|
||||
const BLOCKLIST_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
/// TTL advertised in fake DNS A records (seconds).
|
||||
const FAKE_DNS_TTL: u32 = 60;
|
||||
|
||||
pub struct FakeIpStore {
|
||||
cache: LruCache<String, Ipv4Addr>,
|
||||
@@ -23,11 +34,11 @@ pub struct FakeIpStore {
|
||||
|
||||
impl FakeIpStore {
|
||||
pub fn new() -> Self {
|
||||
info!("Initializing FakeIpStore starting at 100.64.0.1");
|
||||
info!("Initializing FakeIpStore starting at {}", Ipv4Addr::from(FAKE_IP_START));
|
||||
Self {
|
||||
cache: LruCache::new(NonZeroUsize::new(2000).unwrap()),
|
||||
rev_cache: LruCache::new(NonZeroUsize::new(2000).unwrap()),
|
||||
next_ip: START_IP,
|
||||
cache: LruCache::new(NonZeroUsize::new(FAKE_IP_CACHE_SIZE).unwrap()),
|
||||
rev_cache: LruCache::new(NonZeroUsize::new(FAKE_IP_CACHE_SIZE).unwrap()),
|
||||
next_ip: FAKE_IP_START,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +101,7 @@ impl DnsHandler {
|
||||
SystemTime::now()
|
||||
.duration_since(meta.modified()?)
|
||||
.unwrap_or_default()
|
||||
> Duration::from_secs(604800) // 1 неделя
|
||||
> BLOCKLIST_UPDATE_INTERVAL
|
||||
} else {
|
||||
true
|
||||
};
|
||||
@@ -98,7 +109,7 @@ impl DnsHandler {
|
||||
if needs_update {
|
||||
let p_clone = self.cache_path.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
tokio::time::sleep(BLOCKLIST_DOWNLOAD_DELAY).await;
|
||||
if let Err(e) = Self::download_blocklist_async(p_clone).await {
|
||||
error!("DNS: Background update failed: {}", e);
|
||||
}
|
||||
@@ -111,7 +122,7 @@ impl DnsHandler {
|
||||
async fn download_blocklist_async(cache_path: String) -> Result<()> {
|
||||
info!("DNS: Starting background download to {}", cache_path);
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.timeout(BLOCKLIST_HTTP_TIMEOUT)
|
||||
.build()?;
|
||||
|
||||
let resp = client
|
||||
@@ -183,7 +194,7 @@ impl DnsHandler {
|
||||
let fake_ip = store.get_or_assign(&name);
|
||||
res.add_answer(Record::from_rdata(
|
||||
query.name().clone(),
|
||||
60,
|
||||
FAKE_DNS_TTL,
|
||||
RData::A(fake_ip.into()),
|
||||
));
|
||||
res.set_response_code(ResponseCode::NoError);
|
||||
|
||||
+317
-68
@@ -1,6 +1,11 @@
|
||||
use bytes::Bytes;
|
||||
use netrunner_core::net::ClientHandler;
|
||||
use netrunner_core::net::NetworkConfig;
|
||||
use netrunner_core::net::Muxer;
|
||||
use netrunner_core::net::diagnostics::{
|
||||
self, DiagnosisRx, DiagnosticsEvent, DiagnosticsSnapshot, DiagnosticsStore,
|
||||
EngineMetrics, SocketMetrics, current_timestamp_ms,
|
||||
};
|
||||
use netrunner_core::rawcast::{RawCastEvent, RawCastFrame};
|
||||
use smoltcp::iface::PollResult;
|
||||
use smoltcp::phy::ChannelDevice;
|
||||
@@ -12,13 +17,13 @@ use smoltcp::{
|
||||
};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{sync::LazyLock, time::Instant as StdInstant};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tun::{DeviceReader, DeviceWriter};
|
||||
|
||||
use netrunner_core::net::STATS_LOG_INTERVAL;
|
||||
use netrunner_logger::{debug, error, info, warn};
|
||||
|
||||
use crate::net::connection_manager::ConnectionManager;
|
||||
@@ -30,13 +35,23 @@ use crate::tun::tun::Tun;
|
||||
|
||||
pub static START_TIME: LazyLock<StdInstant> = LazyLock::new(StdInstant::now);
|
||||
|
||||
/// How many inbound packets the device can hold before backpressure kicks in.
|
||||
/// Each packet is ≤ MTU bytes, so at 1500 B × 64 = 96 KB max queue.
|
||||
const DEVICE_RX_CAP: usize = 64;
|
||||
/// How many outbound packets smoltcp can stage before we drain them.
|
||||
const DEVICE_TX_CAP: usize = 64;
|
||||
/// Bounded capacity for the TUN-reader → engine channel (packets).
|
||||
/// Inbound packet slots in the smoltcp device RX queue before backpressure.
|
||||
/// At MTU 1500 B × 128 slots ≈ 192 KB peak queue depth.
|
||||
const DEVICE_RX_CAP: usize = 128;
|
||||
/// Outbound packet slots staged in the smoltcp device TX queue.
|
||||
/// Must be ≥ the engine→TUN channel cap so we never pop without a slot.
|
||||
const DEVICE_TX_CAP: usize = 128;
|
||||
/// Bounded capacity of the TUN-reader → engine mpsc channel (packets).
|
||||
const TUN_CHAN_CAP: usize = 128;
|
||||
/// Maximum packets drained from the TUN channel per loop iteration before
|
||||
/// yielding, to prevent upload traffic from starving download processing.
|
||||
const MAX_PACKETS_PER_TICK: usize = 250;
|
||||
/// Read buffer allocated by the TUN reader task (covers the maximum IP packet).
|
||||
const TUN_READ_BUF_SIZE: usize = 65536;
|
||||
/// Upper bound on the smoltcp poll-delay sleep to keep latency low.
|
||||
/// 2 ms is a good balance: low enough for interactive traffic (<5ms added RTT),
|
||||
/// high enough to avoid spinning the CPU under light load.
|
||||
const MAX_POLL_SLEEP: Duration = Duration::from_millis(2);
|
||||
|
||||
pub struct Engine {
|
||||
interface: Interface,
|
||||
@@ -50,6 +65,17 @@ pub struct Engine {
|
||||
rx_from_tunnel: Option<mpsc::Receiver<RawCastFrame>>,
|
||||
factory: Arc<dyn SocketProvider>,
|
||||
stats: TrafficCounter,
|
||||
/// Arc to the tunnel multiplexer — used to collect tunnel metrics in
|
||||
/// diagnostics snapshots.
|
||||
muxer: Option<Arc<Muxer>>,
|
||||
/// Receiver end of the diagnostics event channel.
|
||||
diag_rx: Option<DiagnosisRx>,
|
||||
/// Shared store that holds the last N snapshots (readable via public API).
|
||||
pub diag_store: Arc<DiagnosticsStore>,
|
||||
/// A single download frame that couldn't be forwarded to its socket channel
|
||||
/// (channel was full). Retried at the start of the next engine tick BEFORE
|
||||
/// reading more frames from rx_tunnel. This preserves in-order delivery.
|
||||
pending_download: Option<(u64, Bytes)>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -83,6 +109,10 @@ impl Engine {
|
||||
rx_from_tunnel: Some(rx_from_tunnel),
|
||||
factory,
|
||||
stats: TrafficCounter::new(),
|
||||
muxer: None,
|
||||
diag_rx: None,
|
||||
diag_store: Arc::new(DiagnosticsStore::new(20)),
|
||||
pending_download: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,44 +149,76 @@ impl Engine {
|
||||
let mut work_done = false;
|
||||
|
||||
// ── 1. Dispatch tunnel → local sockets (download) ────────────
|
||||
// Drain all available frames without blocking.
|
||||
loop {
|
||||
match rx_tunnel.try_recv() {
|
||||
Ok(frame) => {
|
||||
work_done = true;
|
||||
if frame.event == RawCastEvent::Close {
|
||||
local_cache.remove(&frame.socket_id);
|
||||
inbound_map.remove(&frame.socket_id);
|
||||
} else if frame.event == RawCastEvent::Data {
|
||||
let tx_opt = if let Some(cached) = local_cache.get(&frame.socket_id) {
|
||||
Some(cached.clone())
|
||||
} else if let Some(ref_tx) = inbound_map.get(&frame.socket_id) {
|
||||
let val = ref_tx.value().clone();
|
||||
local_cache.insert(frame.socket_id, val.clone());
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
//
|
||||
// ORDERING CONTRACT: we must NEVER reorder bytes for a given socket.
|
||||
// Spawning a background task when the channel is full is forbidden —
|
||||
// it creates a race where later frames overtake the stalled one.
|
||||
//
|
||||
// Instead we use a single `pending_download` slot:
|
||||
// • Retry any stalled frame FIRST each tick.
|
||||
// • Only drain rx_tunnel once pending is empty.
|
||||
// • If delivery fails again, set pending and skip rx_tunnel until
|
||||
// the next tick (when smoltcp has had a chance to drain the channel).
|
||||
|
||||
if let Some((tx, is_saturated)) = tx_opt {
|
||||
if is_saturated.load(Ordering::Relaxed) {
|
||||
continue;
|
||||
}
|
||||
match tx.try_send(frame.payload) {
|
||||
Ok(_) => {}
|
||||
Err(mpsc::error::TrySendError::Full(data)) => {
|
||||
// Spawn a task to wait for space rather than drop.
|
||||
let tx2 = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tx2.send(data).await;
|
||||
});
|
||||
// — Retry stalled frame from previous tick —
|
||||
if let Some((socket_id, payload)) = self.pending_download.take() {
|
||||
let tx_opt = if let Some(cached) = local_cache.get(&socket_id) {
|
||||
Some(cached.clone())
|
||||
} else if let Some(ref_tx) = inbound_map.get(&socket_id) {
|
||||
let val = ref_tx.value().clone();
|
||||
local_cache.insert(socket_id, val.clone());
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match tx_opt {
|
||||
Some((tx, _)) => match tx.try_send(payload) {
|
||||
Ok(_) => { work_done = true; }
|
||||
Err(mpsc::error::TrySendError::Full(p)) => {
|
||||
// Still full — will retry next tick.
|
||||
self.pending_download = Some((socket_id, p));
|
||||
}
|
||||
Err(_) => {} // socket gone — drop
|
||||
},
|
||||
None => {} // socket gone — drop
|
||||
}
|
||||
}
|
||||
|
||||
// — Drain rx_tunnel only when no frame is waiting for its slot —
|
||||
if self.pending_download.is_none() {
|
||||
loop {
|
||||
match rx_tunnel.try_recv() {
|
||||
Ok(frame) => {
|
||||
work_done = true;
|
||||
if frame.event == RawCastEvent::Close {
|
||||
local_cache.remove(&frame.socket_id);
|
||||
inbound_map.remove(&frame.socket_id);
|
||||
} else if frame.event == RawCastEvent::Data {
|
||||
let socket_id = frame.socket_id;
|
||||
let tx_opt = if let Some(cached) = local_cache.get(&socket_id) {
|
||||
Some(cached.clone())
|
||||
} else if let Some(ref_tx) = inbound_map.get(&socket_id) {
|
||||
let val = ref_tx.value().clone();
|
||||
local_cache.insert(socket_id, val.clone());
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((tx, _)) = tx_opt {
|
||||
match tx.try_send(frame.payload) {
|
||||
Ok(_) => {}
|
||||
Err(mpsc::error::TrySendError::Full(data)) => {
|
||||
// Channel full — stash and stop draining.
|
||||
self.pending_download = Some((socket_id, data));
|
||||
break;
|
||||
}
|
||||
Err(_) => {} // socket gone — drop
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,13 +228,13 @@ impl Engine {
|
||||
while !self.device.rx_full() {
|
||||
match tun_rx.try_recv() {
|
||||
Ok(pkt) => {
|
||||
self.stats.record_rx(pkt.len());
|
||||
self.stats.record_tx(pkt.len()); // upload: app → internet
|
||||
self.manager
|
||||
.try_create_socket_from_packet(&pkt, &mut self.socket_set);
|
||||
self.device.push_rx(pkt);
|
||||
work_done = true;
|
||||
packets_read += 1;
|
||||
if packets_read >= 250 {
|
||||
if packets_read >= MAX_PACKETS_PER_TICK {
|
||||
break; // Yield occasionally to prevent starvation.
|
||||
}
|
||||
}
|
||||
@@ -193,20 +255,28 @@ impl Engine {
|
||||
}
|
||||
|
||||
// ── 4. Drain smoltcp TX → TUN writer ─────────────────────────
|
||||
while let Some(pkt) = self.device.pop_tx() {
|
||||
self.stats.record_tx(pkt.len());
|
||||
// Non-blocking: if TUN writer is overloaded, drop the packet.
|
||||
// TCP will retransmit; UDP is best-effort.
|
||||
if self.tun_tx.try_send(pkt).is_err() {
|
||||
break;
|
||||
// Check channel capacity BEFORE popping so we never consume a
|
||||
// packet from the device TX queue without a guaranteed slot to
|
||||
// send it. Packets left in the queue are regenerated by smoltcp
|
||||
// on the next poll() call (TCP state machine handles retransmits,
|
||||
// ACKs, etc.).
|
||||
while self.tun_tx.capacity() > 0 {
|
||||
match self.device.pop_tx() {
|
||||
Some(pkt) => {
|
||||
self.stats.record_rx(pkt.len()); // download: internet → app
|
||||
// capacity() > 0 guarantees this won't fail
|
||||
let _ = self.tun_tx.try_send(pkt);
|
||||
work_done = true;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. Stats logging ─────────────────────────────────────────
|
||||
if last_stats_log.elapsed() >= Duration::from_secs(5) {
|
||||
if last_stats_log.elapsed() >= STATS_LOG_INTERVAL {
|
||||
let stats = self.stats.get_stats();
|
||||
info!(
|
||||
"TunDevice Traffic: RX: {:.2} MB ({} pkts) | TX: {:.2} MB ({} pkts) | Speed: ↓{:.2} MB/s, ↑{:.2} MB/s",
|
||||
"TunDevice Traffic: ↓ {:.2} MB ({} pkts) | ↑ {:.2} MB ({} pkts) | Speed: ↓{:.2} MB/s, ↑{:.2} MB/s",
|
||||
stats.rx_bytes as f64 / 1_048_576.0,
|
||||
stats.rx_packets,
|
||||
stats.tx_bytes as f64 / 1_048_576.0,
|
||||
@@ -220,7 +290,23 @@ impl Engine {
|
||||
last_stats_log = StdInstant::now();
|
||||
}
|
||||
|
||||
// ── 6. Adaptive timing ───────────────────────────────────────
|
||||
// ── 6. Diagnostics event processing ─────────────────────────
|
||||
// Take the receiver out (ends the borrow on self), drain events,
|
||||
// build snapshots (needs &mut self for stats), then put it back.
|
||||
if let Some(mut diag_rx) = self.diag_rx.take() {
|
||||
loop {
|
||||
match diag_rx.try_recv() {
|
||||
Ok(event) => {
|
||||
let snap = self.build_snapshot(event);
|
||||
self.diag_store.push(snap);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
self.diag_rx = Some(diag_rx);
|
||||
}
|
||||
|
||||
// ── 7. Adaptive timing ───────────────────────────────────────
|
||||
if work_done {
|
||||
tokio::task::yield_now().await;
|
||||
} else {
|
||||
@@ -229,24 +315,75 @@ impl Engine {
|
||||
.poll_delay(Self::current_time(), &self.socket_set);
|
||||
|
||||
let sleep_time = delay
|
||||
.map(|d| {
|
||||
std::cmp::min(
|
||||
Duration::from_micros(d.micros()),
|
||||
Duration::from_millis(5),
|
||||
)
|
||||
})
|
||||
.unwrap_or(Duration::from_millis(5));
|
||||
.map(|d| Duration::from_micros(d.micros()).min(MAX_POLL_SLEEP))
|
||||
.unwrap_or(MAX_POLL_SLEEP);
|
||||
|
||||
tokio::select! {
|
||||
_ = sleep(sleep_time) => {}
|
||||
msg = tun_rx.recv() => {
|
||||
match msg {
|
||||
Some(pkt) => {
|
||||
self.stats.record_rx(pkt.len());
|
||||
self.manager.try_create_socket_from_packet(&pkt, &mut self.socket_set);
|
||||
self.device.push_rx(pkt);
|
||||
if self.pending_download.is_some() {
|
||||
// A frame is stalled — don't read more from rx_tunnel yet
|
||||
// (would risk overwriting the pending slot or draining out-
|
||||
// of-order). Just wait for smoltcp to drain inbound_tx.
|
||||
tokio::select! {
|
||||
_ = sleep(sleep_time) => {}
|
||||
msg = tun_rx.recv() => {
|
||||
match msg {
|
||||
Some(pkt) => {
|
||||
self.stats.record_tx(pkt.len());
|
||||
self.manager.try_create_socket_from_packet(&pkt, &mut self.socket_set);
|
||||
self.device.push_rx(pkt);
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tokio::select! {
|
||||
_ = sleep(sleep_time) => {}
|
||||
|
||||
// Upload: wake immediately when the app sends data.
|
||||
msg = tun_rx.recv() => {
|
||||
match msg {
|
||||
Some(pkt) => {
|
||||
self.stats.record_tx(pkt.len());
|
||||
self.manager.try_create_socket_from_packet(&pkt, &mut self.socket_set);
|
||||
self.device.push_rx(pkt);
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Download: wake immediately when tunnel data arrives.
|
||||
// Dispatch inline; step 1 drains the rest next iteration.
|
||||
frame = rx_tunnel.recv() => {
|
||||
match frame {
|
||||
None => break,
|
||||
Some(f) => {
|
||||
if f.event == RawCastEvent::Close {
|
||||
local_cache.remove(&f.socket_id);
|
||||
inbound_map.remove(&f.socket_id);
|
||||
} else if f.event == RawCastEvent::Data {
|
||||
let socket_id = f.socket_id;
|
||||
let tx_opt = if let Some(cached) = local_cache.get(&socket_id) {
|
||||
Some(cached.clone())
|
||||
} else if let Some(ref_tx) = inbound_map.get(&socket_id) {
|
||||
let val = ref_tx.value().clone();
|
||||
local_cache.insert(socket_id, val.clone());
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((tx, _)) = tx_opt {
|
||||
match tx.try_send(f.payload) {
|
||||
Ok(_) => {}
|
||||
Err(mpsc::error::TrySendError::Full(data)) => {
|
||||
// Stash — next tick retries.
|
||||
self.pending_download = Some((socket_id, data));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,7 +400,7 @@ impl Engine {
|
||||
fn spawn_tun_reader(mut reader: DeviceReader, to_engine: mpsc::Sender<Vec<u8>>) {
|
||||
tokio::spawn(async move {
|
||||
debug!("TUN Reader task started");
|
||||
let mut buf = vec![0u8; 65536];
|
||||
let mut buf = vec![0u8; TUN_READ_BUF_SIZE];
|
||||
loop {
|
||||
match reader.read(&mut buf).await {
|
||||
Ok(n) if n > 0 => {
|
||||
@@ -301,6 +438,111 @@ impl Engine {
|
||||
Instant::from_micros(duration.as_micros() as i64)
|
||||
}
|
||||
|
||||
// ── Diagnostics ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns all recent diagnostics snapshots as a pretty-printed JSON string.
|
||||
/// Snapshots are captured automatically whenever a network problem occurs
|
||||
/// (upload fail, download backpressure, leg disconnect, tunnel write stall).
|
||||
pub fn get_diagnostics_json(&self) -> String {
|
||||
self.diag_store.get_all_json()
|
||||
}
|
||||
|
||||
/// Returns only the most recent snapshot as JSON, or `"null"` if none yet.
|
||||
pub fn get_latest_diagnostics_json(&self) -> String {
|
||||
self.diag_store.get_latest_json()
|
||||
}
|
||||
|
||||
/// Builds a full diagnostics snapshot at this exact moment.
|
||||
/// Collects: traffic counters, device queue depths, channel free space,
|
||||
/// all active smoltcp socket states, and tunnel leg/stream metrics.
|
||||
fn build_snapshot(&mut self, trigger: DiagnosticsEvent) -> DiagnosticsSnapshot {
|
||||
let stats = self.stats.get_stats();
|
||||
|
||||
let engine_metrics = EngineMetrics {
|
||||
rx_total_mb: stats.rx_bytes as f64 / 1_048_576.0,
|
||||
tx_total_mb: stats.tx_bytes as f64 / 1_048_576.0,
|
||||
rx_speed_mb_s: stats.rx_speed_mb_s,
|
||||
tx_speed_mb_s: stats.tx_speed_mb_s,
|
||||
rx_packets: stats.rx_packets,
|
||||
tx_packets: stats.tx_packets,
|
||||
device_rx_queue_depth: self.device.rx_len(),
|
||||
// ChannelDevice doesn't expose tx_len; use 0 (best-effort data only)
|
||||
device_tx_queue_depth: if self.device.has_tx() { 1 } else { 0 },
|
||||
tun_tx_channel_free: self.tun_tx.capacity(),
|
||||
// tun_rx is taken by `run()`; not available here
|
||||
tun_rx_channel_free: 0,
|
||||
};
|
||||
|
||||
// Collect socket handles and their smoltcp state first (immutable borrow).
|
||||
struct SocketSnap {
|
||||
handle_str: String,
|
||||
state: String,
|
||||
send_queue: usize,
|
||||
send_cap: usize,
|
||||
recv_queue: usize,
|
||||
recv_cap: usize,
|
||||
}
|
||||
let snaps: Vec<SocketSnap> = self
|
||||
.socket_set
|
||||
.iter()
|
||||
.filter_map(|(handle, socket)| {
|
||||
if let smoltcp::socket::Socket::Tcp(tcp) = socket {
|
||||
if tcp.is_active() {
|
||||
return Some(SocketSnap {
|
||||
handle_str: format!("{}", handle),
|
||||
state: format!("{:?}", tcp.state()),
|
||||
send_queue: tcp.send_queue(),
|
||||
send_cap: tcp.send_capacity(),
|
||||
recv_queue: tcp.recv_queue(),
|
||||
recv_cap: tcp.recv_capacity(),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Collect pending_chunk sizes via manager (separate borrow).
|
||||
let sockets: Vec<SocketMetrics> = snaps
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let stream_id: u32 = s.handle_str.parse().unwrap_or(0);
|
||||
SocketMetrics {
|
||||
stream_id,
|
||||
state: s.state,
|
||||
send_queue_bytes: s.send_queue,
|
||||
send_capacity_bytes: s.send_cap,
|
||||
recv_queue_bytes: s.recv_queue,
|
||||
recv_capacity_bytes: s.recv_cap,
|
||||
// pending_chunk is connection-private; omit for now
|
||||
pending_chunk_bytes: 0,
|
||||
tx_congested: false,
|
||||
total_up_bytes: 0,
|
||||
total_down_bytes: 0,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tunnel = self
|
||||
.muxer
|
||||
.as_ref()
|
||||
.map(|m| m.snapshot_tunnel_metrics())
|
||||
.unwrap_or_else(|| diagnostics::TunnelMetrics {
|
||||
global_min_rtt_ms: 0,
|
||||
active_legs: vec![],
|
||||
total_streams: 0,
|
||||
});
|
||||
|
||||
DiagnosticsSnapshot {
|
||||
timestamp_ms: current_timestamp_ms(),
|
||||
trigger,
|
||||
engine: Some(engine_metrics),
|
||||
tunnel,
|
||||
sockets,
|
||||
error_totals: diagnostics::DIAG_COUNTERS.snapshot(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_any_ip(&mut self, state: bool) {
|
||||
self.interface.set_any_ip(state)
|
||||
}
|
||||
@@ -447,12 +689,16 @@ impl EngineBuilder {
|
||||
caps.max_transmission_unit = self.config.mtu;
|
||||
caps.medium = smoltcp::phy::Medium::Ip;
|
||||
|
||||
// Initialise the diagnostics event channel before connecting so that
|
||||
// any events fired during leg establishment are captured.
|
||||
let diag_rx = diagnostics::init_diagnostics();
|
||||
|
||||
let cap = NetworkConfig::global().channel_capacity;
|
||||
let (tx_to_tunnel, rx_for_client_handler) = mpsc::channel::<RawCastFrame>(cap);
|
||||
let (tx_for_client_handler, rx_from_tunnel) = mpsc::channel::<RawCastFrame>(cap);
|
||||
|
||||
info!("Establishing secure tunnel to proxy server...");
|
||||
ClientHandler::connect(
|
||||
let muxer = ClientHandler::connect(
|
||||
&self.config.remote_address,
|
||||
rx_for_client_handler,
|
||||
tx_for_client_handler,
|
||||
@@ -512,6 +758,9 @@ impl EngineBuilder {
|
||||
factory,
|
||||
);
|
||||
|
||||
engine.muxer = Some(muxer);
|
||||
engine.diag_rx = Some(diag_rx);
|
||||
|
||||
engine.set_any_ip(self.config.any_ip);
|
||||
if self.config.transparent_mode {
|
||||
engine.set_transparent_mode();
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use netrunner_core::net::{MAX_SOCKETS, NetworkConfig};
|
||||
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,
|
||||
DNS_PORT,
|
||||
};
|
||||
use netrunner_logger::{info, warn};
|
||||
use smoltcp::{
|
||||
iface::SocketSet,
|
||||
@@ -26,9 +30,11 @@ pub const TCP_SOCKET_ACTIVE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
impl TrafficProfile {
|
||||
pub fn guess_from_port(port: u16, is_tcp: bool) -> Self {
|
||||
match (port, is_tcp) {
|
||||
(22, true) | (3389, true) | (5900, true) => Self::Interactive,
|
||||
(443, true) | (80, true) | (8080, true) | (1935, true) => Self::Bulk,
|
||||
(53, false) | (123, false) => Self::Dns,
|
||||
(SSH_PORT, true) | (RDP_PORT, true) | (VNC_PORT, true) => Self::Interactive,
|
||||
(HTTPS_PORT, true) | (HTTP_PORT, true) | (HTTP_ALT_PORT, true) | (RTMP_PORT, true) => {
|
||||
Self::Bulk
|
||||
}
|
||||
(DNS_PORT, false) | (NTP_PORT, false) => Self::Dns,
|
||||
_ => Self::Default,
|
||||
}
|
||||
}
|
||||
@@ -143,11 +149,11 @@ impl SocketProvider for SmolSocketFactory {
|
||||
app_pending_len / 1024 // Ждет входа в стек (из туннеля)
|
||||
);
|
||||
|
||||
// Если очередь приложения раздута — это красный флаг
|
||||
if app_pending_len > 1024 * 1024 {
|
||||
if app_pending_len > BUFFERBLOAT_WARN_THRESHOLD {
|
||||
warn!(
|
||||
"⚠️ [TCP {}] Bufferbloat detected! Application queue is > 1MB",
|
||||
handle
|
||||
"⚠️ [TCP {}] Bufferbloat detected! Application queue is > {} KB",
|
||||
handle,
|
||||
BUFFERBLOAT_WARN_THRESHOLD / 1024
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -179,8 +185,14 @@ impl SocketProvider for SmolSocketFactory {
|
||||
|
||||
fn create_icmp(&self, _profile: TrafficProfile) -> icmp::Socket<'static> {
|
||||
icmp::Socket::new(
|
||||
icmp::PacketBuffer::new(vec![icmp::PacketMetadata::EMPTY; 4], vec![0; 512]),
|
||||
icmp::PacketBuffer::new(vec![icmp::PacketMetadata::EMPTY; 4], vec![0; 512]),
|
||||
icmp::PacketBuffer::new(
|
||||
vec![icmp::PacketMetadata::EMPTY; ICMP_META_SLOTS],
|
||||
vec![0; ICMP_BUFFER_SIZE],
|
||||
),
|
||||
icmp::PacketBuffer::new(
|
||||
vec![icmp::PacketMetadata::EMPTY; ICMP_META_SLOTS],
|
||||
vec![0; ICMP_BUFFER_SIZE],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user