Compare commits

..

2 Commits

Author SHA1 Message Date
9a9a710363 v4.8.14
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-17 16:37:43 +00:00
156b17135f fix(rust-core,protocol): eliminate edge stream registration races and reduce frame buffering copies 2026-03-17 16:37:43 +00:00
6 changed files with 318 additions and 194 deletions

View File

@@ -1,23 +1,11 @@
# Changelog # Changelog
## 2026-03-17 - 4.8.18 - fix(rust-protocol) ## 2026-03-17 - 4.8.14 - fix(rust-core,protocol)
switch tunnel frame buffers from Vec<u8> to Bytes to reduce copying and memory overhead eliminate edge stream registration races and reduce frame buffering copies
- Add the bytes crate to core and protocol crates - replace Vec<u8> tunnel/frame buffers with bytes::Bytes and BytesMut for lower-copy frame parsing and queueing
- Update frame encoding, reader payloads, channel queues, and stream backchannels to use Bytes - move edge stream ownership into the main I/O loop with explicit register and cleanup channels to ensure streams are registered before OPEN processing
- Adjust edge and hub data/control paths to send framed payloads as Bytes - add proactive send window clamping so active streams converge immediately to adaptive flow-control targets
## 2026-03-17 - 4.8.17 - fix(protocol)
increase per-stream flow control windows and remove adaptive read caps
- Raise the initial per-stream window from 4MB to 16MB and expand the adaptive window budget to 800MB with a 4MB floor
- Stop limiting edge and hub reads by the adaptive per-stream target window, keeping reads capped only by the current window and 32KB chunk size
- Update protocol tests to match the new adaptive window scaling and budget boundaries
## 2026-03-17 - 4.8.16 - fix(release)
bump package version to 4.8.15
- Updates the package.json version field from 4.8.13 to 4.8.15.
## 2026-03-17 - 4.8.13 - fix(remoteingress-protocol) ## 2026-03-17 - 4.8.13 - fix(remoteingress-protocol)
require a flush after each written frame to bound TLS buffer growth require a flush after each written frame to bound TLS buffer growth

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/remoteingress", "name": "@serve.zone/remoteingress",
"version": "4.8.18", "version": "4.8.14",
"private": false, "private": false,
"description": "Edge ingress tunnel for DcRouter - accepts incoming TCP connections at network edge and tunnels them to DcRouter SmartProxy preserving client IP via PROXY protocol v1.", "description": "Edge ingress tunnel for DcRouter - accepts incoming TCP connections at network edge and tunnels them to DcRouter SmartProxy preserving client IP via PROXY protocol v1.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -9,9 +9,9 @@ use tokio::task::JoinHandle;
use tokio::time::{Instant, sleep_until}; use tokio::time::{Instant, sleep_until};
use tokio_rustls::TlsConnector; use tokio_rustls::TlsConnector;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use bytes::Bytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use bytes::Bytes;
use remoteingress_protocol::*; use remoteingress_protocol::*;
type EdgeTlsStream = tokio_rustls::client::TlsStream<TcpStream>; type EdgeTlsStream = tokio_rustls::client::TlsStream<TcpStream>;
@@ -23,7 +23,7 @@ enum EdgeFrameAction {
Disconnect(String), Disconnect(String),
} }
/// Per-stream state tracked in the edge's client_writers map. /// Per-stream state tracked in the edge's stream map.
struct EdgeStreamState { struct EdgeStreamState {
/// Unbounded channel to deliver FRAME_DATA_BACK payloads to the hub_to_client task. /// Unbounded channel to deliver FRAME_DATA_BACK payloads to the hub_to_client task.
/// Unbounded because flow control (WINDOW_UPDATE) already limits bytes-in-flight. /// Unbounded because flow control (WINDOW_UPDATE) already limits bytes-in-flight.
@@ -35,6 +35,12 @@ struct EdgeStreamState {
window_notify: Arc<Notify>, window_notify: Arc<Notify>,
} }
/// Registration message sent from per-stream tasks to the main I/O loop.
struct StreamRegistration {
stream_id: u32,
state: EdgeStreamState,
}
/// Edge configuration (hub-host + credentials only; ports come from hub). /// Edge configuration (hub-host + credentials only; ports come from hub).
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -285,39 +291,29 @@ enum EdgeLoopResult {
/// Process a single frame received from the hub side of the tunnel. /// Process a single frame received from the hub side of the tunnel.
/// Handles FRAME_DATA_BACK, FRAME_WINDOW_UPDATE_BACK, FRAME_CLOSE_BACK, FRAME_CONFIG, FRAME_PING. /// Handles FRAME_DATA_BACK, FRAME_WINDOW_UPDATE_BACK, FRAME_CLOSE_BACK, FRAME_CONFIG, FRAME_PING.
async fn handle_edge_frame( /// No mutex — edge_streams is owned by the main I/O loop (same pattern as hub.rs).
fn handle_edge_frame(
frame: Frame, frame: Frame,
tunnel_io: &mut remoteingress_protocol::TunnelIo<EdgeTlsStream>, tunnel_io: &mut remoteingress_protocol::TunnelIo<EdgeTlsStream>,
client_writers: &Arc<Mutex<HashMap<u32, EdgeStreamState>>>, edge_streams: &mut HashMap<u32, EdgeStreamState>,
listen_ports: &Arc<RwLock<Vec<u16>>>, listen_ports_update: &mut Option<Vec<u16>>,
event_tx: &mpsc::Sender<EdgeEvent>,
tunnel_writer_tx: &mpsc::Sender<Bytes>,
tunnel_data_tx: &mpsc::Sender<Bytes>,
port_listeners: &mut HashMap<u16, JoinHandle<()>>,
active_streams: &Arc<AtomicU32>,
next_stream_id: &Arc<AtomicU32>,
edge_id: &str,
connection_token: &CancellationToken,
bind_address: &str,
) -> EdgeFrameAction { ) -> EdgeFrameAction {
match frame.frame_type { match frame.frame_type {
FRAME_DATA_BACK => { FRAME_DATA_BACK => {
// Dispatch to per-stream unbounded channel. Flow control (WINDOW_UPDATE) // Dispatch to per-stream unbounded channel. Flow control (WINDOW_UPDATE)
// limits bytes-in-flight, so the channel won't grow unbounded. send() only // limits bytes-in-flight, so the channel won't grow unbounded. send() only
// fails if the receiver is dropped (hub_to_client task already exited). // fails if the receiver is dropped (hub_to_client task already exited).
let mut writers = client_writers.lock().await; if let Some(state) = edge_streams.get(&frame.stream_id) {
if let Some(state) = writers.get(&frame.stream_id) {
if state.back_tx.send(frame.payload).is_err() { if state.back_tx.send(frame.payload).is_err() {
// Receiver dropped — hub_to_client task already exited, clean up // Receiver dropped — hub_to_client task already exited, clean up
writers.remove(&frame.stream_id); edge_streams.remove(&frame.stream_id);
} }
} }
} }
FRAME_WINDOW_UPDATE_BACK => { FRAME_WINDOW_UPDATE_BACK => {
if let Some(increment) = decode_window_update(&frame.payload) { if let Some(increment) = decode_window_update(&frame.payload) {
if increment > 0 { if increment > 0 {
let writers = client_writers.lock().await; if let Some(state) = edge_streams.get(&frame.stream_id) {
if let Some(state) = writers.get(&frame.stream_id) {
let prev = state.send_window.fetch_add(increment, Ordering::Release); let prev = state.send_window.fetch_add(increment, Ordering::Release);
if prev + increment > MAX_WINDOW_SIZE { if prev + increment > MAX_WINDOW_SIZE {
state.send_window.store(MAX_WINDOW_SIZE, Ordering::Release); state.send_window.store(MAX_WINDOW_SIZE, Ordering::Release);
@@ -328,28 +324,12 @@ async fn handle_edge_frame(
} }
} }
FRAME_CLOSE_BACK => { FRAME_CLOSE_BACK => {
let mut writers = client_writers.lock().await; edge_streams.remove(&frame.stream_id);
writers.remove(&frame.stream_id);
} }
FRAME_CONFIG => { FRAME_CONFIG => {
if let Ok(update) = serde_json::from_slice::<ConfigUpdate>(&frame.payload) { if let Ok(update) = serde_json::from_slice::<ConfigUpdate>(&frame.payload) {
log::info!("Config update from hub: ports {:?}", update.listen_ports); log::info!("Config update from hub: ports {:?}", update.listen_ports);
*listen_ports.write().await = update.listen_ports.clone(); *listen_ports_update = Some(update.listen_ports);
let _ = event_tx.try_send(EdgeEvent::PortsUpdated {
listen_ports: update.listen_ports.clone(),
});
apply_port_config(
&update.listen_ports,
port_listeners,
tunnel_writer_tx,
tunnel_data_tx,
client_writers,
active_streams,
next_stream_id,
edge_id,
connection_token,
bind_address,
);
} }
} }
FRAME_PING => { FRAME_PING => {
@@ -491,9 +471,12 @@ async fn connect_to_hub_and_run(
} }
}); });
// Client socket map: stream_id -> per-stream state (back channel + flow control) // Stream map owned by the main I/O loop — no mutex, matching hub.rs pattern.
let client_writers: Arc<Mutex<HashMap<u32, EdgeStreamState>>> = let mut edge_streams: HashMap<u32, EdgeStreamState> = HashMap::new();
Arc::new(Mutex::new(HashMap::new())); // Channel for per-stream tasks to register their stream state with the main loop.
let (register_tx, mut register_rx) = mpsc::channel::<StreamRegistration>(256);
// Channel for per-stream tasks to deregister when done.
let (cleanup_tx, mut cleanup_rx) = mpsc::channel::<u32>(256);
// QoS dual-channel: ctrl frames have priority over data frames. // QoS dual-channel: ctrl frames have priority over data frames.
// Stream handlers send through these channels → TunnelIo drains them. // Stream handlers send through these channels → TunnelIo drains them.
@@ -509,7 +492,8 @@ async fn connect_to_hub_and_run(
&mut port_listeners, &mut port_listeners,
&tunnel_writer_tx, &tunnel_writer_tx,
&tunnel_data_tx, &tunnel_data_tx,
&client_writers, &register_tx,
&cleanup_tx,
active_streams, active_streams,
next_stream_id, next_stream_id,
&config.edge_id, &config.edge_id,
@@ -526,7 +510,18 @@ async fn connect_to_hub_and_run(
let mut liveness_deadline = Box::pin(sleep_until(last_activity + liveness_timeout_dur)); let mut liveness_deadline = Box::pin(sleep_until(last_activity + liveness_timeout_dur));
let result = 'io_loop: loop { let result = 'io_loop: loop {
// Drain stream registrations from per-stream tasks (before poll_step so
// registrations are processed before OPEN frames are sent to the hub).
while let Ok(reg) = register_rx.try_recv() {
edge_streams.insert(reg.stream_id, reg.state);
}
// Drain stream cleanups from per-stream tasks
while let Ok(stream_id) = cleanup_rx.try_recv() {
edge_streams.remove(&stream_id);
}
// Drain any buffered frames // Drain any buffered frames
let mut listen_ports_update = None;
loop { loop {
let frame = match tunnel_io.try_parse_frame() { let frame = match tunnel_io.try_parse_frame() {
Some(Ok(f)) => f, Some(Ok(f)) => f,
@@ -539,28 +534,55 @@ async fn connect_to_hub_and_run(
last_activity = Instant::now(); last_activity = Instant::now();
liveness_deadline.as_mut().reset(last_activity + liveness_timeout_dur); liveness_deadline.as_mut().reset(last_activity + liveness_timeout_dur);
if let EdgeFrameAction::Disconnect(reason) = handle_edge_frame( if let EdgeFrameAction::Disconnect(reason) = handle_edge_frame(
frame, &mut tunnel_io, &client_writers, listen_ports, event_tx, frame, &mut tunnel_io, &mut edge_streams, &mut listen_ports_update,
&tunnel_writer_tx, &tunnel_data_tx, &mut port_listeners, ) {
active_streams, next_stream_id, &config.edge_id, connection_token, bind_address,
).await {
break 'io_loop EdgeLoopResult::Reconnect(reason); break 'io_loop EdgeLoopResult::Reconnect(reason);
} }
} }
// Apply port config update if handle_edge_frame signalled one
if let Some(new_ports) = listen_ports_update.take() {
*listen_ports.write().await = new_ports.clone();
let _ = event_tx.try_send(EdgeEvent::PortsUpdated {
listen_ports: new_ports.clone(),
});
apply_port_config(
&new_ports,
&mut port_listeners,
&tunnel_writer_tx,
&tunnel_data_tx,
&register_tx,
&cleanup_tx,
active_streams,
next_stream_id,
&config.edge_id,
connection_token,
bind_address,
);
}
// Poll I/O: write(ctrl→data), flush, read, channels, timers // Poll I/O: write(ctrl→data), flush, read, channels, timers
let event = std::future::poll_fn(|cx| { let event = std::future::poll_fn(|cx| {
tunnel_io.poll_step(cx, &mut tunnel_ctrl_rx, &mut tunnel_data_rx, &mut liveness_deadline, connection_token) tunnel_io.poll_step(cx, &mut tunnel_ctrl_rx, &mut tunnel_data_rx, &mut liveness_deadline, connection_token)
}).await; }).await;
// Drain registrations/cleanups before processing the event — registrations
// may have arrived while poll_step was running (multiple poll cycles inside .await).
while let Ok(reg) = register_rx.try_recv() {
edge_streams.insert(reg.stream_id, reg.state);
}
while let Ok(stream_id) = cleanup_rx.try_recv() {
edge_streams.remove(&stream_id);
}
let mut listen_ports_update = None;
match event { match event {
remoteingress_protocol::TunnelEvent::Frame(frame) => { remoteingress_protocol::TunnelEvent::Frame(frame) => {
last_activity = Instant::now(); last_activity = Instant::now();
liveness_deadline.as_mut().reset(last_activity + liveness_timeout_dur); liveness_deadline.as_mut().reset(last_activity + liveness_timeout_dur);
if let EdgeFrameAction::Disconnect(reason) = handle_edge_frame( if let EdgeFrameAction::Disconnect(reason) = handle_edge_frame(
frame, &mut tunnel_io, &client_writers, listen_ports, event_tx, frame, &mut tunnel_io, &mut edge_streams, &mut listen_ports_update,
&tunnel_writer_tx, &tunnel_data_tx, &mut port_listeners, ) {
active_streams, next_stream_id, &config.edge_id, connection_token, bind_address,
).await {
break EdgeLoopResult::Reconnect(reason); break EdgeLoopResult::Reconnect(reason);
} }
} }
@@ -587,6 +609,27 @@ async fn connect_to_hub_and_run(
break EdgeLoopResult::Shutdown; break EdgeLoopResult::Shutdown;
} }
} }
// Apply port config update if handle_edge_frame signalled one
if let Some(new_ports) = listen_ports_update.take() {
*listen_ports.write().await = new_ports.clone();
let _ = event_tx.try_send(EdgeEvent::PortsUpdated {
listen_ports: new_ports.clone(),
});
apply_port_config(
&new_ports,
&mut port_listeners,
&tunnel_writer_tx,
&tunnel_data_tx,
&register_tx,
&cleanup_tx,
active_streams,
next_stream_id,
&config.edge_id,
connection_token,
bind_address,
);
}
}; };
// Cancel stream tokens FIRST so stream handlers exit immediately. // Cancel stream tokens FIRST so stream handlers exit immediately.
@@ -615,7 +658,8 @@ fn apply_port_config(
port_listeners: &mut HashMap<u16, JoinHandle<()>>, port_listeners: &mut HashMap<u16, JoinHandle<()>>,
tunnel_ctrl_tx: &mpsc::Sender<Bytes>, tunnel_ctrl_tx: &mpsc::Sender<Bytes>,
tunnel_data_tx: &mpsc::Sender<Bytes>, tunnel_data_tx: &mpsc::Sender<Bytes>,
client_writers: &Arc<Mutex<HashMap<u32, EdgeStreamState>>>, register_tx: &mpsc::Sender<StreamRegistration>,
cleanup_tx: &mpsc::Sender<u32>,
active_streams: &Arc<AtomicU32>, active_streams: &Arc<AtomicU32>,
next_stream_id: &Arc<AtomicU32>, next_stream_id: &Arc<AtomicU32>,
edge_id: &str, edge_id: &str,
@@ -637,7 +681,8 @@ fn apply_port_config(
for &port in new_set.difference(&old_set) { for &port in new_set.difference(&old_set) {
let tunnel_ctrl_tx = tunnel_ctrl_tx.clone(); let tunnel_ctrl_tx = tunnel_ctrl_tx.clone();
let tunnel_data_tx = tunnel_data_tx.clone(); let tunnel_data_tx = tunnel_data_tx.clone();
let client_writers = client_writers.clone(); let register_tx = register_tx.clone();
let cleanup_tx = cleanup_tx.clone();
let active_streams = active_streams.clone(); let active_streams = active_streams.clone();
let next_stream_id = next_stream_id.clone(); let next_stream_id = next_stream_id.clone();
let edge_id = edge_id.to_string(); let edge_id = edge_id.to_string();
@@ -671,7 +716,8 @@ fn apply_port_config(
let stream_id = next_stream_id.fetch_add(1, Ordering::Relaxed); let stream_id = next_stream_id.fetch_add(1, Ordering::Relaxed);
let tunnel_ctrl_tx = tunnel_ctrl_tx.clone(); let tunnel_ctrl_tx = tunnel_ctrl_tx.clone();
let tunnel_data_tx = tunnel_data_tx.clone(); let tunnel_data_tx = tunnel_data_tx.clone();
let client_writers = client_writers.clone(); let register_tx = register_tx.clone();
let cleanup_tx = cleanup_tx.clone();
let active_streams = active_streams.clone(); let active_streams = active_streams.clone();
let edge_id = edge_id.clone(); let edge_id = edge_id.clone();
let client_token = port_token.child_token(); let client_token = port_token.child_token();
@@ -687,7 +733,8 @@ fn apply_port_config(
&edge_id, &edge_id,
tunnel_ctrl_tx, tunnel_ctrl_tx,
tunnel_data_tx, tunnel_data_tx,
client_writers, register_tx,
cleanup_tx,
client_token, client_token,
Arc::clone(&active_streams), Arc::clone(&active_streams),
) )
@@ -730,7 +777,8 @@ async fn handle_client_connection(
edge_id: &str, edge_id: &str,
tunnel_ctrl_tx: mpsc::Sender<Bytes>, tunnel_ctrl_tx: mpsc::Sender<Bytes>,
tunnel_data_tx: mpsc::Sender<Bytes>, tunnel_data_tx: mpsc::Sender<Bytes>,
client_writers: Arc<Mutex<HashMap<u32, EdgeStreamState>>>, register_tx: mpsc::Sender<StreamRegistration>,
cleanup_tx: mpsc::Sender<u32>,
client_token: CancellationToken, client_token: CancellationToken,
active_streams: Arc<AtomicU32>, active_streams: Arc<AtomicU32>,
) { ) {
@@ -740,17 +788,6 @@ async fn handle_client_connection(
// Determine edge IP (use 0.0.0.0 as placeholder — hub doesn't use it for routing) // Determine edge IP (use 0.0.0.0 as placeholder — hub doesn't use it for routing)
let edge_ip = "0.0.0.0"; let edge_ip = "0.0.0.0";
// Send OPEN frame with PROXY v1 header via control channel
let proxy_header = build_proxy_v1_header(&client_ip, edge_ip, client_port, dest_port);
let open_frame = encode_frame(stream_id, FRAME_OPEN, proxy_header.as_bytes());
let send_ok = tokio::select! {
result = tunnel_ctrl_tx.send(open_frame) => result.is_ok(),
_ = client_token.cancelled() => false,
};
if !send_ok {
return;
}
// Per-stream unbounded back-channel. Flow control (WINDOW_UPDATE) limits // Per-stream unbounded back-channel. Flow control (WINDOW_UPDATE) limits
// bytes-in-flight, so this won't grow unbounded. Unbounded avoids killing // bytes-in-flight, so this won't grow unbounded. Unbounded avoids killing
// streams due to channel overflow — backpressure slows streams, never kills them. // streams due to channel overflow — backpressure slows streams, never kills them.
@@ -762,13 +799,35 @@ async fn handle_client_connection(
); );
let send_window = Arc::new(AtomicU32::new(initial_window)); let send_window = Arc::new(AtomicU32::new(initial_window));
let window_notify = Arc::new(Notify::new()); let window_notify = Arc::new(Notify::new());
{
let mut writers = client_writers.lock().await; // Register with the main I/O loop BEFORE sending OPEN. The main loop drains
writers.insert(stream_id, EdgeStreamState { // register_rx before poll_step drains ctrl_rx, guaranteeing the stream is
back_tx, // registered before the OPEN frame reaches the hub and DATA_BACK arrives.
send_window: Arc::clone(&send_window), let reg_ok = tokio::select! {
window_notify: Arc::clone(&window_notify), result = register_tx.send(StreamRegistration {
}); stream_id,
state: EdgeStreamState {
back_tx,
send_window: Arc::clone(&send_window),
window_notify: Arc::clone(&window_notify),
},
}) => result.is_ok(),
_ = client_token.cancelled() => false,
};
if !reg_ok {
return;
}
// Send OPEN frame with PROXY v1 header via control channel
let proxy_header = build_proxy_v1_header(&client_ip, edge_ip, client_port, dest_port);
let open_frame = encode_frame(stream_id, FRAME_OPEN, proxy_header.as_bytes());
let send_ok = tokio::select! {
result = tunnel_ctrl_tx.send(open_frame) => result.is_ok(),
_ = client_token.cancelled() => false,
};
if !send_ok {
let _ = cleanup_tx.try_send(stream_id);
return;
} }
let (mut client_read, mut client_write) = client_stream.into_split(); let (mut client_read, mut client_write) = client_stream.into_split();
@@ -854,11 +913,12 @@ async fn handle_client_connection(
} }
if client_token.is_cancelled() { break; } if client_token.is_cancelled() { break; }
// Limit read size to available window. // Proactive QoS: clamp send_window to current adaptive target so existing
// IMPORTANT: if window is 0 (stall timeout fired), we must NOT // streams converge immediately when concurrency increases (no drain cycle).
// read into an empty buffer — read(&mut buf[..0]) returns Ok(0) let adaptive_target = remoteingress_protocol::compute_window_for_stream_count(
// which would be falsely interpreted as EOF. active_streams.load(Ordering::Relaxed),
let w = send_window.load(Ordering::Acquire) as usize; );
let w = remoteingress_protocol::clamp_send_window(&send_window, adaptive_target) as usize;
if w == 0 { if w == 0 {
log::warn!("Stream {} upload: window still 0 after stall timeout, closing", stream_id); log::warn!("Stream {} upload: window still 0 after stall timeout, closing", stream_id);
break; break;
@@ -907,11 +967,8 @@ async fn handle_client_connection(
} }
} }
// Clean up // Clean up — notify main loop to remove stream state
{ let _ = cleanup_tx.try_send(stream_id);
let mut writers = client_writers.lock().await;
writers.remove(&stream_id);
}
hub_to_client.abort(); // No-op if already finished; safety net if timeout fired hub_to_client.abort(); // No-op if already finished; safety net if timeout fired
let _ = edge_id; // used for logging context let _ = edge_id; // used for logging context
} }

View File

@@ -479,11 +479,12 @@ async fn handle_hub_frame(
} }
if stream_token.is_cancelled() { break; } if stream_token.is_cancelled() { break; }
// Limit read size to available window. // Proactive QoS: clamp send_window to current adaptive target so existing
// IMPORTANT: if window is 0 (stall timeout fired), we must NOT // streams converge immediately when concurrency increases (no drain cycle).
// read into an empty buffer — read(&mut buf[..0]) returns Ok(0) let adaptive_target = remoteingress_protocol::compute_window_for_stream_count(
// which would be falsely interpreted as EOF. stream_counter.load(Ordering::Relaxed),
let w = send_window.load(Ordering::Acquire) as usize; );
let w = remoteingress_protocol::clamp_send_window(&send_window, adaptive_target) as usize;
if w == 0 { if w == 0 {
log::warn!("Stream {} download: window still 0 after stall timeout, closing", stream_id); log::warn!("Stream {} download: window still 0 after stall timeout, closing", stream_id);
break; break;

View File

@@ -2,7 +2,7 @@ use std::collections::VecDeque;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use bytes::{Bytes, BytesMut, BufMut}; use bytes::{Bytes, BytesMut};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf};
// Frame type constants // Frame type constants
@@ -24,8 +24,9 @@ pub const FRAME_HEADER_SIZE: usize = 9;
pub const MAX_PAYLOAD_SIZE: u32 = 16 * 1024 * 1024; pub const MAX_PAYLOAD_SIZE: u32 = 16 * 1024 * 1024;
// Per-stream flow control constants // Per-stream flow control constants
/// Initial (and maximum) per-stream window size (16 MB). /// Initial per-stream window size (4 MB). Sized for full throughput at high RTT:
pub const INITIAL_STREAM_WINDOW: u32 = 16 * 1024 * 1024; /// at 100ms RTT, this sustains ~40 MB/s per stream.
pub const INITIAL_STREAM_WINDOW: u32 = 4 * 1024 * 1024;
/// Send WINDOW_UPDATE after consuming this many bytes (half the initial window). /// Send WINDOW_UPDATE after consuming this many bytes (half the initial window).
pub const WINDOW_UPDATE_THRESHOLD: u32 = INITIAL_STREAM_WINDOW / 2; pub const WINDOW_UPDATE_THRESHOLD: u32 = INITIAL_STREAM_WINDOW / 2;
/// Maximum window size to prevent overflow. /// Maximum window size to prevent overflow.
@@ -37,11 +38,36 @@ pub fn encode_window_update(stream_id: u32, frame_type: u8, increment: u32) -> B
} }
/// Compute the target per-stream window size based on the number of active streams. /// Compute the target per-stream window size based on the number of active streams.
/// Total memory budget is ~800MB shared across all streams. Up to 50 streams get the /// Total memory budget is ~32MB shared across all streams. As more streams are active,
/// full 16MB window; above that the window scales down to a 4MB floor at 200+ streams. /// each gets a smaller window. This adapts to current demand — few streams get high
/// throughput, many streams save memory and reduce control frame pressure.
pub fn compute_window_for_stream_count(active: u32) -> u32 { pub fn compute_window_for_stream_count(active: u32) -> u32 {
let per_stream = (800 * 1024 * 1024u64) / (active.max(1) as u64); let per_stream = (32 * 1024 * 1024u64) / (active.max(1) as u64);
per_stream.clamp(4 * 1024 * 1024, INITIAL_STREAM_WINDOW as u64) as u32 per_stream.clamp(64 * 1024, INITIAL_STREAM_WINDOW as u64) as u32
}
/// Proactively clamp a send_window AtomicU32 down to at most `target`.
/// CAS loop so concurrent WINDOW_UPDATE additions are not lost.
/// Returns the value after clamping.
#[inline]
pub fn clamp_send_window(
send_window: &std::sync::atomic::AtomicU32,
target: u32,
) -> u32 {
loop {
let current = send_window.load(std::sync::atomic::Ordering::Acquire);
if current <= target {
return current;
}
match send_window.compare_exchange_weak(
current, target,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Relaxed,
) {
Ok(_) => return target,
Err(_) => continue,
}
}
} }
/// Decode a WINDOW_UPDATE payload into a byte increment. Returns None if payload is malformed. /// Decode a WINDOW_UPDATE payload into a byte increment. Returns None if payload is malformed.
@@ -63,12 +89,12 @@ pub struct Frame {
/// Encode a frame into bytes: [stream_id:4][type:1][length:4][payload] /// Encode a frame into bytes: [stream_id:4][type:1][length:4][payload]
pub fn encode_frame(stream_id: u32, frame_type: u8, payload: &[u8]) -> Bytes { pub fn encode_frame(stream_id: u32, frame_type: u8, payload: &[u8]) -> Bytes {
let len = payload.len() as u32; let len = payload.len() as u32;
let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE + payload.len()); let mut buf = Vec::with_capacity(FRAME_HEADER_SIZE + payload.len());
buf.put_slice(&stream_id.to_be_bytes()); buf.extend_from_slice(&stream_id.to_be_bytes());
buf.put_u8(frame_type); buf.push(frame_type);
buf.put_slice(&len.to_be_bytes()); buf.extend_from_slice(&len.to_be_bytes());
buf.put_slice(payload); buf.extend_from_slice(payload);
buf.freeze() Bytes::from(buf)
} }
/// Write a frame header into `buf[0..FRAME_HEADER_SIZE]`. /// Write a frame header into `buf[0..FRAME_HEADER_SIZE]`.
@@ -143,7 +169,7 @@ impl<R: AsyncRead + Unpin> FrameReader<R> {
)); ));
} }
let mut payload = BytesMut::zeroed(length as usize); let mut payload = vec![0u8; length as usize];
if length > 0 { if length > 0 {
self.reader.read_exact(&mut payload).await?; self.reader.read_exact(&mut payload).await?;
} }
@@ -151,7 +177,7 @@ impl<R: AsyncRead + Unpin> FrameReader<R> {
Ok(Some(Frame { Ok(Some(Frame {
stream_id, stream_id,
frame_type, frame_type,
payload: payload.freeze(), payload: Bytes::from(payload),
})) }))
} }
@@ -187,7 +213,7 @@ pub enum TunnelEvent {
struct WriteState { struct WriteState {
ctrl_queue: VecDeque<Bytes>, // PONG, WINDOW_UPDATE, CLOSE, OPEN — always first ctrl_queue: VecDeque<Bytes>, // PONG, WINDOW_UPDATE, CLOSE, OPEN — always first
data_queue: VecDeque<Bytes>, // DATA, DATA_BACK — only when ctrl is empty data_queue: VecDeque<Bytes>, // DATA, DATA_BACK — only when ctrl is empty
offset: usize, // progress within current frame being written offset: usize, // progress within current frame being written
flush_needed: bool, flush_needed: bool,
} }
@@ -205,26 +231,21 @@ impl WriteState {
/// WINDOW_UPDATE starvation that causes flow control deadlocks. /// WINDOW_UPDATE starvation that causes flow control deadlocks.
pub struct TunnelIo<S> { pub struct TunnelIo<S> {
stream: S, stream: S,
// Read state: accumulate bytes, parse frames incrementally // Read state: BytesMut accumulates bytes; split_to extracts frames zero-copy.
read_buf: Vec<u8>, read_buf: BytesMut,
read_pos: usize,
parse_pos: usize,
// Write state: extracted sub-struct for safe disjoint borrows // Write state: extracted sub-struct for safe disjoint borrows
write: WriteState, write: WriteState,
} }
impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> { impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
pub fn new(stream: S, initial_data: Vec<u8>) -> Self { pub fn new(stream: S, initial_data: Vec<u8>) -> Self {
let read_pos = initial_data.len(); let mut read_buf = BytesMut::from(&initial_data[..]);
let mut read_buf = initial_data;
if read_buf.capacity() < 65536 { if read_buf.capacity() < 65536 {
read_buf.reserve(65536 - read_buf.len()); read_buf.reserve(65536 - read_buf.len());
} }
Self { Self {
stream, stream,
read_buf, read_buf,
read_pos,
parse_pos: 0,
write: WriteState { write: WriteState {
ctrl_queue: VecDeque::new(), ctrl_queue: VecDeque::new(),
data_queue: VecDeque::new(), data_queue: VecDeque::new(),
@@ -245,31 +266,29 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
} }
/// Try to parse a complete frame from the read buffer. /// Try to parse a complete frame from the read buffer.
/// Uses a parse_pos cursor to avoid drain() on every frame. /// Zero-copy: uses BytesMut::split_to to extract frames without allocating.
pub fn try_parse_frame(&mut self) -> Option<Result<Frame, std::io::Error>> { pub fn try_parse_frame(&mut self) -> Option<Result<Frame, std::io::Error>> {
let available = self.read_pos - self.parse_pos; if self.read_buf.len() < FRAME_HEADER_SIZE {
if available < FRAME_HEADER_SIZE {
return None; return None;
} }
let base = self.parse_pos;
let stream_id = u32::from_be_bytes([ let stream_id = u32::from_be_bytes([
self.read_buf[base], self.read_buf[base + 1], self.read_buf[0], self.read_buf[1],
self.read_buf[base + 2], self.read_buf[base + 3], self.read_buf[2], self.read_buf[3],
]); ]);
let frame_type = self.read_buf[base + 4]; let frame_type = self.read_buf[4];
let length = u32::from_be_bytes([ let length = u32::from_be_bytes([
self.read_buf[base + 5], self.read_buf[base + 6], self.read_buf[5], self.read_buf[6],
self.read_buf[base + 7], self.read_buf[base + 8], self.read_buf[7], self.read_buf[8],
]); ]);
if length > MAX_PAYLOAD_SIZE { if length > MAX_PAYLOAD_SIZE {
let header = [ let header = [
self.read_buf[base], self.read_buf[base + 1], self.read_buf[0], self.read_buf[1],
self.read_buf[base + 2], self.read_buf[base + 3], self.read_buf[2], self.read_buf[3],
self.read_buf[base + 4], self.read_buf[base + 5], self.read_buf[4], self.read_buf[5],
self.read_buf[base + 6], self.read_buf[base + 7], self.read_buf[6], self.read_buf[7],
self.read_buf[base + 8], self.read_buf[8],
]; ];
log::error!( log::error!(
"CORRUPT FRAME HEADER: raw={:02x?} stream_id={} type=0x{:02x} length={}", "CORRUPT FRAME HEADER: raw={:02x?} stream_id={} type=0x{:02x} length={}",
@@ -282,28 +301,22 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
} }
let total_frame_size = FRAME_HEADER_SIZE + length as usize; let total_frame_size = FRAME_HEADER_SIZE + length as usize;
if available < total_frame_size { if self.read_buf.len() < total_frame_size {
return None; return None;
} }
let payload = Bytes::copy_from_slice( // Zero-copy extraction: split the frame off the read buffer (O(1) pointer adjustment).
&self.read_buf[base + FRAME_HEADER_SIZE..base + total_frame_size], // split_to removes the first total_frame_size bytes from read_buf.
); let mut frame_data = self.read_buf.split_to(total_frame_size);
self.parse_pos += total_frame_size; // Split off header, keep only payload. freeze() converts BytesMut → Bytes (O(1)).
let payload = frame_data.split_off(FRAME_HEADER_SIZE).freeze();
// Compact when parse_pos > half the data to reclaim memory
if self.parse_pos > self.read_pos / 2 && self.parse_pos > 0 {
self.read_buf.drain(..self.parse_pos);
self.read_pos -= self.parse_pos;
self.parse_pos = 0;
}
Some(Ok(Frame { stream_id, frame_type, payload })) Some(Ok(Frame { stream_id, frame_type, payload }))
} }
/// Poll-based I/O step. Returns Ready on events, Pending when idle. /// Poll-based I/O step. Returns Ready on events, Pending when idle.
/// ///
/// Order: write(ctrl->data) -> flush -> read -> channels -> timers /// Order: write(ctrldata) flush read channels timers
pub fn poll_step( pub fn poll_step(
&mut self, &mut self,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@@ -372,23 +385,18 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
// the waker without re-registering it, causing the task to sleep until a // the waker without re-registering it, causing the task to sleep until a
// timer or channel wakes it (potentially 15+ seconds of lost reads). // timer or channel wakes it (potentially 15+ seconds of lost reads).
loop { loop {
// Compact if needed to make room for reads // Ensure at least 32KB of writable space
if self.parse_pos > 0 && self.read_buf.len() - self.read_pos < 32768 { let len_before = self.read_buf.len();
self.read_buf.drain(..self.parse_pos); self.read_buf.resize(len_before + 32768, 0);
self.read_pos -= self.parse_pos; let mut rbuf = ReadBuf::new(&mut self.read_buf[len_before..]);
self.parse_pos = 0;
}
if self.read_buf.len() < self.read_pos + 32768 {
self.read_buf.resize(self.read_pos + 32768, 0);
}
let mut rbuf = ReadBuf::new(&mut self.read_buf[self.read_pos..]);
match Pin::new(&mut self.stream).poll_read(cx, &mut rbuf) { match Pin::new(&mut self.stream).poll_read(cx, &mut rbuf) {
Poll::Ready(Ok(())) => { Poll::Ready(Ok(())) => {
let n = rbuf.filled().len(); let n = rbuf.filled().len();
// Trim back to actual data length
self.read_buf.truncate(len_before + n);
if n == 0 { if n == 0 {
return Poll::Ready(TunnelEvent::Eof); return Poll::Ready(TunnelEvent::Eof);
} }
self.read_pos += n;
if let Some(result) = self.try_parse_frame() { if let Some(result) = self.try_parse_frame() {
return match result { return match result {
Ok(frame) => Poll::Ready(TunnelEvent::Frame(frame)), Ok(frame) => Poll::Ready(TunnelEvent::Frame(frame)),
@@ -399,10 +407,14 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
// waker is re-registered when it finally returns Pending. // waker is re-registered when it finally returns Pending.
} }
Poll::Ready(Err(e)) => { Poll::Ready(Err(e)) => {
self.read_buf.truncate(len_before);
log::error!("TunnelIo: poll_read error: {}", e); log::error!("TunnelIo: poll_read error: {}", e);
return Poll::Ready(TunnelEvent::ReadError(e)); return Poll::Ready(TunnelEvent::ReadError(e));
} }
Poll::Pending => break, Poll::Pending => {
self.read_buf.truncate(len_before);
break;
}
} }
} }
@@ -410,7 +422,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
// Ctrl frames must never be delayed — always drain fully. // Ctrl frames must never be delayed — always drain fully.
// Data frames are gated: keep data in the bounded channel for proper // Data frames are gated: keep data in the bounded channel for proper
// backpressure when TLS writes are slow. Without this gate, the internal // backpressure when TLS writes are slow. Without this gate, the internal
// data_queue (unbounded VecDeque) grows to hundreds of MB under throttle -> OOM. // data_queue (unbounded VecDeque) grows to hundreds of MB under throttle OOM.
let mut got_new = false; let mut got_new = false;
loop { loop {
match ctrl_rx.poll_recv(cx) { match ctrl_rx.poll_recv(cx) {
@@ -472,14 +484,14 @@ mod tests {
let mut buf = vec![0u8; FRAME_HEADER_SIZE + payload.len()]; let mut buf = vec![0u8; FRAME_HEADER_SIZE + payload.len()];
buf[FRAME_HEADER_SIZE..].copy_from_slice(payload); buf[FRAME_HEADER_SIZE..].copy_from_slice(payload);
encode_frame_header(&mut buf, 42, FRAME_DATA, payload.len()); encode_frame_header(&mut buf, 42, FRAME_DATA, payload.len());
assert_eq!(buf, &encode_frame(42, FRAME_DATA, payload)[..]); assert_eq!(buf[..], encode_frame(42, FRAME_DATA, payload)[..]);
} }
#[test] #[test]
fn test_encode_frame_header_empty_payload() { fn test_encode_frame_header_empty_payload() {
let mut buf = vec![0u8; FRAME_HEADER_SIZE]; let mut buf = vec![0u8; FRAME_HEADER_SIZE];
encode_frame_header(&mut buf, 99, FRAME_CLOSE, 0); encode_frame_header(&mut buf, 99, FRAME_CLOSE, 0);
assert_eq!(buf, &encode_frame(99, FRAME_CLOSE, &[])[..]); assert_eq!(buf[..], encode_frame(99, FRAME_CLOSE, &[])[..]);
} }
#[test] #[test]
@@ -684,57 +696,90 @@ mod tests {
#[test] #[test]
fn test_adaptive_window_zero_streams() { fn test_adaptive_window_zero_streams() {
// 0 streams treated as 1: 800MB/1 -> clamped to 16MB max // 0 streams treated as 1: 32MB/1 = 32MB → clamped to 4MB max
assert_eq!(compute_window_for_stream_count(0), INITIAL_STREAM_WINDOW); assert_eq!(compute_window_for_stream_count(0), INITIAL_STREAM_WINDOW);
} }
#[test] #[test]
fn test_adaptive_window_one_stream() { fn test_adaptive_window_one_stream() {
// 32MB/1 = 32MB → clamped to 4MB max
assert_eq!(compute_window_for_stream_count(1), INITIAL_STREAM_WINDOW); assert_eq!(compute_window_for_stream_count(1), INITIAL_STREAM_WINDOW);
} }
#[test] #[test]
fn test_adaptive_window_50_streams_full() { fn test_adaptive_window_at_max_boundary() {
// 800MB/50 = 16MB = exactly INITIAL_STREAM_WINDOW // 32MB/8 = 4MB = exactly INITIAL_STREAM_WINDOW
assert_eq!(compute_window_for_stream_count(50), INITIAL_STREAM_WINDOW); assert_eq!(compute_window_for_stream_count(8), INITIAL_STREAM_WINDOW);
} }
#[test] #[test]
fn test_adaptive_window_51_streams_starts_scaling() { fn test_adaptive_window_just_below_max() {
// 800MB/51 < 16MB — first value below max // 32MB/9 = 3,728,270 — first value below INITIAL_STREAM_WINDOW
let w = compute_window_for_stream_count(51); let w = compute_window_for_stream_count(9);
assert!(w < INITIAL_STREAM_WINDOW); assert!(w < INITIAL_STREAM_WINDOW);
assert_eq!(w, (800 * 1024 * 1024u64 / 51) as u32); assert_eq!(w, (32 * 1024 * 1024u64 / 9) as u32);
}
#[test]
fn test_adaptive_window_16_streams() {
// 32MB/16 = 2MB
assert_eq!(compute_window_for_stream_count(16), 2 * 1024 * 1024);
} }
#[test] #[test]
fn test_adaptive_window_100_streams() { fn test_adaptive_window_100_streams() {
// 800MB/100 = 8MB // 32MB/100 = 335,544 bytes (~327KB)
assert_eq!(compute_window_for_stream_count(100), 8 * 1024 * 1024); let w = compute_window_for_stream_count(100);
assert_eq!(w, (32 * 1024 * 1024u64 / 100) as u32);
assert!(w > 64 * 1024); // above floor
assert!(w < INITIAL_STREAM_WINDOW as u32); // below ceiling
} }
#[test] #[test]
fn test_adaptive_window_200_streams_at_floor() { fn test_adaptive_window_200_streams() {
// 800MB/200 = 4MB = exactly the floor // 32MB/200 = 167,772 bytes (~163KB), above 64KB floor
assert_eq!(compute_window_for_stream_count(200), 4 * 1024 * 1024); let w = compute_window_for_stream_count(200);
assert_eq!(w, (32 * 1024 * 1024u64 / 200) as u32);
assert!(w > 64 * 1024);
} }
#[test] #[test]
fn test_adaptive_window_500_streams_clamped() { fn test_adaptive_window_500_streams() {
// 800MB/500 = 1.6MB -> clamped up to 4MB floor // 32MB/500 = 67,108 bytes (~65.5KB), just above 64KB floor
assert_eq!(compute_window_for_stream_count(500), 4 * 1024 * 1024); let w = compute_window_for_stream_count(500);
assert_eq!(w, (32 * 1024 * 1024u64 / 500) as u32);
assert!(w > 64 * 1024);
}
#[test]
fn test_adaptive_window_at_min_boundary() {
// 32MB/512 = 65,536 = exactly 64KB floor
assert_eq!(compute_window_for_stream_count(512), 64 * 1024);
}
#[test]
fn test_adaptive_window_below_min_clamped() {
// 32MB/513 = 65,408 → clamped up to 64KB
assert_eq!(compute_window_for_stream_count(513), 64 * 1024);
}
#[test]
fn test_adaptive_window_1000_streams() {
// 32MB/1000 = 33,554 → clamped to 64KB
assert_eq!(compute_window_for_stream_count(1000), 64 * 1024);
} }
#[test] #[test]
fn test_adaptive_window_max_u32() { fn test_adaptive_window_max_u32() {
// Extreme: u32::MAX streams -> tiny value -> clamped to 4MB // Extreme: u32::MAX streams tiny value clamped to 64KB
assert_eq!(compute_window_for_stream_count(u32::MAX), 4 * 1024 * 1024); assert_eq!(compute_window_for_stream_count(u32::MAX), 64 * 1024);
} }
#[test] #[test]
fn test_adaptive_window_monotonically_decreasing() { fn test_adaptive_window_monotonically_decreasing() {
// Window should decrease (or stay same) as stream count increases
let mut prev = compute_window_for_stream_count(1); let mut prev = compute_window_for_stream_count(1);
for n in [2, 10, 50, 51, 100, 200, 500, 1000] { for n in [2, 5, 10, 50, 100, 200, 500, 512, 1000] {
let w = compute_window_for_stream_count(n); let w = compute_window_for_stream_count(n);
assert!(w <= prev, "window increased from {} to {} at n={}", prev, w, n); assert!(w <= prev, "window increased from {} to {} at n={}", prev, w, n);
prev = w; prev = w;
@@ -743,14 +788,47 @@ mod tests {
#[test] #[test]
fn test_adaptive_window_total_budget_bounded() { fn test_adaptive_window_total_budget_bounded() {
// active x per_stream_window should never exceed 800MB (+ clamp overhead for high N) // active × per_stream_window should never exceed 32MB (+ clamp overhead for high N)
for n in [1, 10, 50, 100, 200] { for n in [1, 10, 50, 100, 200, 500] {
let w = compute_window_for_stream_count(n); let w = compute_window_for_stream_count(n);
let total = w as u64 * n as u64; let total = w as u64 * n as u64;
assert!(total <= 800 * 1024 * 1024, "total {}MB exceeds budget at n={}", total / (1024*1024), n); assert!(total <= 32 * 1024 * 1024, "total {}MB exceeds budget at n={}", total / (1024*1024), n);
} }
} }
// --- clamp_send_window tests ---
#[test]
fn test_clamp_send_window_reduces_above_target() {
let w = std::sync::atomic::AtomicU32::new(4 * 1024 * 1024); // 4 MB
let result = clamp_send_window(&w, 512 * 1024); // target 512 KB
assert_eq!(result, 512 * 1024);
assert_eq!(w.load(std::sync::atomic::Ordering::Relaxed), 512 * 1024);
}
#[test]
fn test_clamp_send_window_noop_below_target() {
let w = std::sync::atomic::AtomicU32::new(256 * 1024); // 256 KB
let result = clamp_send_window(&w, 512 * 1024); // target 512 KB
assert_eq!(result, 256 * 1024);
assert_eq!(w.load(std::sync::atomic::Ordering::Relaxed), 256 * 1024);
}
#[test]
fn test_clamp_send_window_noop_at_target() {
let w = std::sync::atomic::AtomicU32::new(512 * 1024);
let result = clamp_send_window(&w, 512 * 1024);
assert_eq!(result, 512 * 1024);
assert_eq!(w.load(std::sync::atomic::Ordering::Relaxed), 512 * 1024);
}
#[test]
fn test_clamp_send_window_zero_value() {
let w = std::sync::atomic::AtomicU32::new(0);
let result = clamp_send_window(&w, 64 * 1024);
assert_eq!(result, 0);
}
// --- encode/decode window_update roundtrip --- // --- encode/decode window_update roundtrip ---
#[test] #[test]

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/remoteingress', name: '@serve.zone/remoteingress',
version: '4.8.18', version: '4.8.14',
description: 'Edge ingress tunnel for DcRouter - accepts incoming TCP connections at network edge and tunnels them to DcRouter SmartProxy preserving client IP via PROXY protocol v1.' description: 'Edge ingress tunnel for DcRouter - accepts incoming TCP connections at network edge and tunnels them to DcRouter SmartProxy preserving client IP via PROXY protocol v1.'
} }