Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce7ccd83dc | |||
| 93578d7034 | |||
| 4cfc518301 | |||
| 124df129ec |
14
changelog.md
14
changelog.md
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-17 - 4.8.18 - fix(rust-protocol)
|
||||||
|
switch tunnel frame buffers from Vec<u8> to Bytes to reduce copying and memory overhead
|
||||||
|
|
||||||
|
- Add the bytes crate to core and protocol crates
|
||||||
|
- Update frame encoding, reader payloads, channel queues, and stream backchannels to use Bytes
|
||||||
|
- Adjust edge and hub data/control paths to send framed payloads as Bytes
|
||||||
|
|
||||||
|
## 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)
|
## 2026-03-17 - 4.8.16 - fix(release)
|
||||||
bump package version to 4.8.15
|
bump package version to 4.8.15
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/remoteingress",
|
"name": "@serve.zone/remoteingress",
|
||||||
"version": "4.8.16",
|
"version": "4.8.18",
|
||||||
"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",
|
||||||
|
|||||||
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
@@ -551,6 +551,7 @@ dependencies = [
|
|||||||
name = "remoteingress-core"
|
name = "remoteingress-core"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"log",
|
"log",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"remoteingress-protocol",
|
"remoteingress-protocol",
|
||||||
@@ -568,6 +569,7 @@ dependencies = [
|
|||||||
name = "remoteingress-protocol"
|
name = "remoteingress-protocol"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"log",
|
"log",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ edition = "2021"
|
|||||||
remoteingress-protocol = { path = "../remoteingress-protocol" }
|
remoteingress-protocol = { path = "../remoteingress-protocol" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-rustls = "0.26"
|
tokio-rustls = "0.26"
|
||||||
|
bytes = "1"
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
|
||||||
rcgen = "0.13"
|
rcgen = "0.13"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use tokio_rustls::TlsConnector;
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
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>;
|
||||||
@@ -26,7 +27,7 @@ enum EdgeFrameAction {
|
|||||||
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.
|
||||||
back_tx: mpsc::UnboundedSender<Vec<u8>>,
|
back_tx: mpsc::UnboundedSender<Bytes>,
|
||||||
/// Send window for FRAME_DATA (upload direction).
|
/// Send window for FRAME_DATA (upload direction).
|
||||||
/// Decremented by the client reader, incremented by FRAME_WINDOW_UPDATE_BACK from hub.
|
/// Decremented by the client reader, incremented by FRAME_WINDOW_UPDATE_BACK from hub.
|
||||||
send_window: Arc<AtomicU32>,
|
send_window: Arc<AtomicU32>,
|
||||||
@@ -290,8 +291,8 @@ async fn handle_edge_frame(
|
|||||||
client_writers: &Arc<Mutex<HashMap<u32, EdgeStreamState>>>,
|
client_writers: &Arc<Mutex<HashMap<u32, EdgeStreamState>>>,
|
||||||
listen_ports: &Arc<RwLock<Vec<u16>>>,
|
listen_ports: &Arc<RwLock<Vec<u16>>>,
|
||||||
event_tx: &mpsc::Sender<EdgeEvent>,
|
event_tx: &mpsc::Sender<EdgeEvent>,
|
||||||
tunnel_writer_tx: &mpsc::Sender<Vec<u8>>,
|
tunnel_writer_tx: &mpsc::Sender<Bytes>,
|
||||||
tunnel_data_tx: &mpsc::Sender<Vec<u8>>,
|
tunnel_data_tx: &mpsc::Sender<Bytes>,
|
||||||
port_listeners: &mut HashMap<u16, JoinHandle<()>>,
|
port_listeners: &mut HashMap<u16, JoinHandle<()>>,
|
||||||
active_streams: &Arc<AtomicU32>,
|
active_streams: &Arc<AtomicU32>,
|
||||||
next_stream_id: &Arc<AtomicU32>,
|
next_stream_id: &Arc<AtomicU32>,
|
||||||
@@ -496,8 +497,8 @@ async fn connect_to_hub_and_run(
|
|||||||
|
|
||||||
// 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.
|
||||||
let (tunnel_ctrl_tx, mut tunnel_ctrl_rx) = mpsc::channel::<Vec<u8>>(256);
|
let (tunnel_ctrl_tx, mut tunnel_ctrl_rx) = mpsc::channel::<Bytes>(256);
|
||||||
let (tunnel_data_tx, mut tunnel_data_rx) = mpsc::channel::<Vec<u8>>(4096);
|
let (tunnel_data_tx, mut tunnel_data_rx) = mpsc::channel::<Bytes>(4096);
|
||||||
let tunnel_writer_tx = tunnel_ctrl_tx.clone();
|
let tunnel_writer_tx = tunnel_ctrl_tx.clone();
|
||||||
|
|
||||||
// Start TCP listeners for initial ports
|
// Start TCP listeners for initial ports
|
||||||
@@ -612,8 +613,8 @@ async fn connect_to_hub_and_run(
|
|||||||
fn apply_port_config(
|
fn apply_port_config(
|
||||||
new_ports: &[u16],
|
new_ports: &[u16],
|
||||||
port_listeners: &mut HashMap<u16, JoinHandle<()>>,
|
port_listeners: &mut HashMap<u16, JoinHandle<()>>,
|
||||||
tunnel_ctrl_tx: &mpsc::Sender<Vec<u8>>,
|
tunnel_ctrl_tx: &mpsc::Sender<Bytes>,
|
||||||
tunnel_data_tx: &mpsc::Sender<Vec<u8>>,
|
tunnel_data_tx: &mpsc::Sender<Bytes>,
|
||||||
client_writers: &Arc<Mutex<HashMap<u32, EdgeStreamState>>>,
|
client_writers: &Arc<Mutex<HashMap<u32, EdgeStreamState>>>,
|
||||||
active_streams: &Arc<AtomicU32>,
|
active_streams: &Arc<AtomicU32>,
|
||||||
next_stream_id: &Arc<AtomicU32>,
|
next_stream_id: &Arc<AtomicU32>,
|
||||||
@@ -727,8 +728,8 @@ async fn handle_client_connection(
|
|||||||
stream_id: u32,
|
stream_id: u32,
|
||||||
dest_port: u16,
|
dest_port: u16,
|
||||||
edge_id: &str,
|
edge_id: &str,
|
||||||
tunnel_ctrl_tx: mpsc::Sender<Vec<u8>>,
|
tunnel_ctrl_tx: mpsc::Sender<Bytes>,
|
||||||
tunnel_data_tx: mpsc::Sender<Vec<u8>>,
|
tunnel_data_tx: mpsc::Sender<Bytes>,
|
||||||
client_writers: Arc<Mutex<HashMap<u32, EdgeStreamState>>>,
|
client_writers: Arc<Mutex<HashMap<u32, EdgeStreamState>>>,
|
||||||
client_token: CancellationToken,
|
client_token: CancellationToken,
|
||||||
active_streams: Arc<AtomicU32>,
|
active_streams: Arc<AtomicU32>,
|
||||||
@@ -753,7 +754,7 @@ async fn handle_client_connection(
|
|||||||
// 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.
|
||||||
let (back_tx, mut back_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
let (back_tx, mut back_rx) = mpsc::unbounded_channel::<Bytes>();
|
||||||
// Adaptive initial window: scale with current stream count to keep total in-flight
|
// Adaptive initial window: scale with current stream count to keep total in-flight
|
||||||
// data within the 32MB budget. Prevents burst flooding when many streams open.
|
// data within the 32MB budget. Prevents burst flooding when many streams open.
|
||||||
let initial_window = remoteingress_protocol::compute_window_for_stream_count(
|
let initial_window = remoteingress_protocol::compute_window_for_stream_count(
|
||||||
@@ -862,11 +863,7 @@ async fn handle_client_connection(
|
|||||||
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;
|
||||||
}
|
}
|
||||||
// Adaptive: cap read to current per-stream target window
|
let max_read = w.min(32768);
|
||||||
let adaptive_cap = remoteingress_protocol::compute_window_for_stream_count(
|
|
||||||
active_streams.load(Ordering::Relaxed),
|
|
||||||
) as usize;
|
|
||||||
let max_read = w.min(32768).min(adaptive_cap);
|
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
read_result = client_read.read(&mut buf[FRAME_HEADER_SIZE..FRAME_HEADER_SIZE + max_read]) => {
|
read_result = client_read.read(&mut buf[FRAME_HEADER_SIZE..FRAME_HEADER_SIZE + max_read]) => {
|
||||||
@@ -875,7 +872,7 @@ async fn handle_client_connection(
|
|||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
send_window.fetch_sub(n as u32, Ordering::Release);
|
send_window.fetch_sub(n as u32, Ordering::Release);
|
||||||
encode_frame_header(&mut buf, stream_id, FRAME_DATA, n);
|
encode_frame_header(&mut buf, stream_id, FRAME_DATA, n);
|
||||||
let data_frame = buf[..FRAME_HEADER_SIZE + n].to_vec();
|
let data_frame = Bytes::copy_from_slice(&buf[..FRAME_HEADER_SIZE + n]);
|
||||||
let sent = tokio::select! {
|
let sent = tokio::select! {
|
||||||
result = tunnel_data_tx.send(data_frame) => result.is_ok(),
|
result = tunnel_data_tx.send(data_frame) => result.is_ok(),
|
||||||
_ = client_token.cancelled() => false,
|
_ = client_token.cancelled() => false,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use tokio_rustls::TlsAcceptor;
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
use remoteingress_protocol::*;
|
use remoteingress_protocol::*;
|
||||||
|
|
||||||
type HubTlsStream = tokio_rustls::server::TlsStream<TcpStream>;
|
type HubTlsStream = tokio_rustls::server::TlsStream<TcpStream>;
|
||||||
@@ -26,7 +27,7 @@ struct HubStreamState {
|
|||||||
/// Unbounded channel to deliver FRAME_DATA payloads to the upstream writer task.
|
/// Unbounded channel to deliver FRAME_DATA payloads to the upstream writer task.
|
||||||
/// Unbounded because flow control (WINDOW_UPDATE) already limits bytes-in-flight.
|
/// Unbounded because flow control (WINDOW_UPDATE) already limits bytes-in-flight.
|
||||||
/// A bounded channel would kill streams instead of applying backpressure.
|
/// A bounded channel would kill streams instead of applying backpressure.
|
||||||
data_tx: mpsc::UnboundedSender<Vec<u8>>,
|
data_tx: mpsc::UnboundedSender<Bytes>,
|
||||||
/// Cancellation token for this stream.
|
/// Cancellation token for this stream.
|
||||||
cancel_token: CancellationToken,
|
cancel_token: CancellationToken,
|
||||||
/// Send window for FRAME_DATA_BACK (download direction).
|
/// Send window for FRAME_DATA_BACK (download direction).
|
||||||
@@ -307,8 +308,8 @@ async fn handle_hub_frame(
|
|||||||
edge_stream_count: &Arc<AtomicU32>,
|
edge_stream_count: &Arc<AtomicU32>,
|
||||||
edge_id: &str,
|
edge_id: &str,
|
||||||
event_tx: &mpsc::Sender<HubEvent>,
|
event_tx: &mpsc::Sender<HubEvent>,
|
||||||
ctrl_tx: &mpsc::Sender<Vec<u8>>,
|
ctrl_tx: &mpsc::Sender<Bytes>,
|
||||||
data_tx: &mpsc::Sender<Vec<u8>>,
|
data_tx: &mpsc::Sender<Bytes>,
|
||||||
target_host: &str,
|
target_host: &str,
|
||||||
edge_token: &CancellationToken,
|
edge_token: &CancellationToken,
|
||||||
cleanup_tx: &mpsc::Sender<u32>,
|
cleanup_tx: &mpsc::Sender<u32>,
|
||||||
@@ -346,7 +347,7 @@ async fn handle_hub_frame(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create channel for data from edge to this stream
|
// Create channel for data from edge to this stream
|
||||||
let (stream_data_tx, mut stream_data_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
let (stream_data_tx, mut stream_data_rx) = mpsc::unbounded_channel::<Bytes>();
|
||||||
// Adaptive initial window: scale with current stream count
|
// Adaptive initial window: scale with current stream count
|
||||||
// to keep total in-flight data within the 32MB budget.
|
// to keep total in-flight data within the 32MB budget.
|
||||||
let initial_window = compute_window_for_stream_count(
|
let initial_window = compute_window_for_stream_count(
|
||||||
@@ -487,11 +488,7 @@ async fn handle_hub_frame(
|
|||||||
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;
|
||||||
}
|
}
|
||||||
// Adaptive: cap read to current per-stream target window
|
let max_read = w.min(32768);
|
||||||
let adaptive_cap = remoteingress_protocol::compute_window_for_stream_count(
|
|
||||||
stream_counter.load(Ordering::Relaxed),
|
|
||||||
) as usize;
|
|
||||||
let max_read = w.min(32768).min(adaptive_cap);
|
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
read_result = up_read.read(&mut buf[FRAME_HEADER_SIZE..FRAME_HEADER_SIZE + max_read]) => {
|
read_result = up_read.read(&mut buf[FRAME_HEADER_SIZE..FRAME_HEADER_SIZE + max_read]) => {
|
||||||
@@ -500,7 +497,7 @@ async fn handle_hub_frame(
|
|||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
send_window.fetch_sub(n as u32, Ordering::Release);
|
send_window.fetch_sub(n as u32, Ordering::Release);
|
||||||
encode_frame_header(&mut buf, stream_id, FRAME_DATA_BACK, n);
|
encode_frame_header(&mut buf, stream_id, FRAME_DATA_BACK, n);
|
||||||
let frame = buf[..FRAME_HEADER_SIZE + n].to_vec();
|
let frame = Bytes::copy_from_slice(&buf[..FRAME_HEADER_SIZE + n]);
|
||||||
let sent = tokio::select! {
|
let sent = tokio::select! {
|
||||||
result = data_writer_tx.send(frame) => result.is_ok(),
|
result = data_writer_tx.send(frame) => result.is_ok(),
|
||||||
_ = stream_token.cancelled() => false,
|
_ = stream_token.cancelled() => false,
|
||||||
@@ -711,8 +708,8 @@ async fn handle_edge_connection(
|
|||||||
|
|
||||||
// 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.
|
||||||
let (ctrl_tx, mut ctrl_rx) = mpsc::channel::<Vec<u8>>(256);
|
let (ctrl_tx, mut ctrl_rx) = mpsc::channel::<Bytes>(256);
|
||||||
let (data_tx, mut data_rx) = mpsc::channel::<Vec<u8>>(4096);
|
let (data_tx, mut data_rx) = mpsc::channel::<Bytes>(4096);
|
||||||
|
|
||||||
// Spawn task to forward config updates as FRAME_CONFIG frames
|
// Spawn task to forward config updates as FRAME_CONFIG frames
|
||||||
let config_writer_tx = ctrl_tx.clone();
|
let config_writer_tx = ctrl_tx.clone();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["io-util", "sync", "time"] }
|
tokio = { version = "1", features = ["io-util", "sync", "time"] }
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
|
bytes = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -2,6 +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 tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf};
|
||||||
|
|
||||||
// Frame type constants
|
// Frame type constants
|
||||||
@@ -23,26 +24,24 @@ 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 per-stream window size (4 MB). Sized for full throughput at high RTT:
|
/// Initial (and maximum) per-stream window size (16 MB).
|
||||||
/// at 100ms RTT, this sustains ~40 MB/s per stream.
|
pub const INITIAL_STREAM_WINDOW: u32 = 16 * 1024 * 1024;
|
||||||
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.
|
||||||
pub const MAX_WINDOW_SIZE: u32 = 16 * 1024 * 1024;
|
pub const MAX_WINDOW_SIZE: u32 = 16 * 1024 * 1024;
|
||||||
|
|
||||||
/// Encode a WINDOW_UPDATE frame for a specific stream.
|
/// Encode a WINDOW_UPDATE frame for a specific stream.
|
||||||
pub fn encode_window_update(stream_id: u32, frame_type: u8, increment: u32) -> Vec<u8> {
|
pub fn encode_window_update(stream_id: u32, frame_type: u8, increment: u32) -> Bytes {
|
||||||
encode_frame(stream_id, frame_type, &increment.to_be_bytes())
|
encode_frame(stream_id, frame_type, &increment.to_be_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 ~32MB shared across all streams. As more streams are active,
|
/// Total memory budget is ~800MB shared across all streams. Up to 50 streams get the
|
||||||
/// each gets a smaller window. This adapts to current demand — few streams get high
|
/// full 16MB window; above that the window scales down to a 4MB floor at 200+ streams.
|
||||||
/// 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 = (32 * 1024 * 1024u64) / (active.max(1) as u64);
|
let per_stream = (800 * 1024 * 1024u64) / (active.max(1) as u64);
|
||||||
per_stream.clamp(64 * 1024, INITIAL_STREAM_WINDOW as u64) as u32
|
per_stream.clamp(4 * 1024 * 1024, INITIAL_STREAM_WINDOW as u64) as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -58,18 +57,18 @@ pub fn decode_window_update(payload: &[u8]) -> Option<u32> {
|
|||||||
pub struct Frame {
|
pub struct Frame {
|
||||||
pub stream_id: u32,
|
pub stream_id: u32,
|
||||||
pub frame_type: u8,
|
pub frame_type: u8,
|
||||||
pub payload: Vec<u8>,
|
pub payload: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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]) -> Vec<u8> {
|
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 = Vec::with_capacity(FRAME_HEADER_SIZE + payload.len());
|
let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE + payload.len());
|
||||||
buf.extend_from_slice(&stream_id.to_be_bytes());
|
buf.put_slice(&stream_id.to_be_bytes());
|
||||||
buf.push(frame_type);
|
buf.put_u8(frame_type);
|
||||||
buf.extend_from_slice(&len.to_be_bytes());
|
buf.put_slice(&len.to_be_bytes());
|
||||||
buf.extend_from_slice(payload);
|
buf.put_slice(payload);
|
||||||
buf
|
buf.freeze()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a frame header into `buf[0..FRAME_HEADER_SIZE]`.
|
/// Write a frame header into `buf[0..FRAME_HEADER_SIZE]`.
|
||||||
@@ -144,7 +143,7 @@ impl<R: AsyncRead + Unpin> FrameReader<R> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut payload = vec![0u8; length as usize];
|
let mut payload = BytesMut::zeroed(length as usize);
|
||||||
if length > 0 {
|
if length > 0 {
|
||||||
self.reader.read_exact(&mut payload).await?;
|
self.reader.read_exact(&mut payload).await?;
|
||||||
}
|
}
|
||||||
@@ -152,7 +151,7 @@ impl<R: AsyncRead + Unpin> FrameReader<R> {
|
|||||||
Ok(Some(Frame {
|
Ok(Some(Frame {
|
||||||
stream_id,
|
stream_id,
|
||||||
frame_type,
|
frame_type,
|
||||||
payload,
|
payload: payload.freeze(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +185,8 @@ pub enum TunnelEvent {
|
|||||||
/// Write state extracted into a sub-struct so the borrow checker can see
|
/// Write state extracted into a sub-struct so the borrow checker can see
|
||||||
/// disjoint field access between `self.write` and `self.stream`.
|
/// disjoint field access between `self.write` and `self.stream`.
|
||||||
struct WriteState {
|
struct WriteState {
|
||||||
ctrl_queue: VecDeque<Vec<u8>>, // PONG, WINDOW_UPDATE, CLOSE, OPEN — always first
|
ctrl_queue: VecDeque<Bytes>, // PONG, WINDOW_UPDATE, CLOSE, OPEN — always first
|
||||||
data_queue: VecDeque<Vec<u8>>, // 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,
|
||||||
}
|
}
|
||||||
@@ -236,12 +235,12 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Queue a high-priority control frame (PONG, WINDOW_UPDATE, CLOSE, OPEN).
|
/// Queue a high-priority control frame (PONG, WINDOW_UPDATE, CLOSE, OPEN).
|
||||||
pub fn queue_ctrl(&mut self, frame: Vec<u8>) {
|
pub fn queue_ctrl(&mut self, frame: Bytes) {
|
||||||
self.write.ctrl_queue.push_back(frame);
|
self.write.ctrl_queue.push_back(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue a lower-priority data frame (DATA, DATA_BACK).
|
/// Queue a lower-priority data frame (DATA, DATA_BACK).
|
||||||
pub fn queue_data(&mut self, frame: Vec<u8>) {
|
pub fn queue_data(&mut self, frame: Bytes) {
|
||||||
self.write.data_queue.push_back(frame);
|
self.write.data_queue.push_back(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +286,9 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = self.read_buf[base + FRAME_HEADER_SIZE..base + total_frame_size].to_vec();
|
let payload = Bytes::copy_from_slice(
|
||||||
|
&self.read_buf[base + FRAME_HEADER_SIZE..base + total_frame_size],
|
||||||
|
);
|
||||||
self.parse_pos += total_frame_size;
|
self.parse_pos += total_frame_size;
|
||||||
|
|
||||||
// Compact when parse_pos > half the data to reclaim memory
|
// Compact when parse_pos > half the data to reclaim memory
|
||||||
@@ -302,12 +303,12 @@ impl<S: AsyncRead + AsyncWrite + Unpin> TunnelIo<S> {
|
|||||||
|
|
||||||
/// 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(ctrl->data) -> flush -> read -> channels -> timers
|
||||||
pub fn poll_step(
|
pub fn poll_step(
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
ctrl_rx: &mut tokio::sync::mpsc::Receiver<Vec<u8>>,
|
ctrl_rx: &mut tokio::sync::mpsc::Receiver<Bytes>,
|
||||||
data_rx: &mut tokio::sync::mpsc::Receiver<Vec<u8>>,
|
data_rx: &mut tokio::sync::mpsc::Receiver<Bytes>,
|
||||||
liveness_deadline: &mut Pin<Box<tokio::time::Sleep>>,
|
liveness_deadline: &mut Pin<Box<tokio::time::Sleep>>,
|
||||||
cancel_token: &tokio_util::sync::CancellationToken,
|
cancel_token: &tokio_util::sync::CancellationToken,
|
||||||
) -> Poll<TunnelEvent> {
|
) -> Poll<TunnelEvent> {
|
||||||
@@ -409,7 +410,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) {
|
||||||
@@ -471,14 +472,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]
|
||||||
@@ -646,7 +647,7 @@ mod tests {
|
|||||||
let frame = reader.next_frame().await.unwrap().unwrap();
|
let frame = reader.next_frame().await.unwrap().unwrap();
|
||||||
assert_eq!(frame.stream_id, i as u32);
|
assert_eq!(frame.stream_id, i as u32);
|
||||||
assert_eq!(frame.frame_type, ft);
|
assert_eq!(frame.frame_type, ft);
|
||||||
assert_eq!(frame.payload, format!("payload_{}", i).as_bytes());
|
assert_eq!(&frame.payload[..], format!("payload_{}", i).as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(reader.next_frame().await.unwrap().is_none());
|
assert!(reader.next_frame().await.unwrap().is_none());
|
||||||
@@ -655,7 +656,7 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_frame_reader_zero_length_payload() {
|
async fn test_frame_reader_zero_length_payload() {
|
||||||
let data = encode_frame(42, FRAME_CLOSE, &[]);
|
let data = encode_frame(42, FRAME_CLOSE, &[]);
|
||||||
let cursor = std::io::Cursor::new(data);
|
let cursor = std::io::Cursor::new(data.to_vec());
|
||||||
let mut reader = FrameReader::new(cursor);
|
let mut reader = FrameReader::new(cursor);
|
||||||
|
|
||||||
let frame = reader.next_frame().await.unwrap().unwrap();
|
let frame = reader.next_frame().await.unwrap().unwrap();
|
||||||
@@ -683,90 +684,57 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_adaptive_window_zero_streams() {
|
fn test_adaptive_window_zero_streams() {
|
||||||
// 0 streams treated as 1: 32MB/1 = 32MB → clamped to 4MB max
|
// 0 streams treated as 1: 800MB/1 -> clamped to 16MB 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_at_max_boundary() {
|
fn test_adaptive_window_50_streams_full() {
|
||||||
// 32MB/8 = 4MB = exactly INITIAL_STREAM_WINDOW
|
// 800MB/50 = 16MB = exactly INITIAL_STREAM_WINDOW
|
||||||
assert_eq!(compute_window_for_stream_count(8), INITIAL_STREAM_WINDOW);
|
assert_eq!(compute_window_for_stream_count(50), INITIAL_STREAM_WINDOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_adaptive_window_just_below_max() {
|
fn test_adaptive_window_51_streams_starts_scaling() {
|
||||||
// 32MB/9 = 3,728,270 — first value below INITIAL_STREAM_WINDOW
|
// 800MB/51 < 16MB — first value below max
|
||||||
let w = compute_window_for_stream_count(9);
|
let w = compute_window_for_stream_count(51);
|
||||||
assert!(w < INITIAL_STREAM_WINDOW);
|
assert!(w < INITIAL_STREAM_WINDOW);
|
||||||
assert_eq!(w, (32 * 1024 * 1024u64 / 9) as u32);
|
assert_eq!(w, (800 * 1024 * 1024u64 / 51) 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() {
|
||||||
// 32MB/100 = 335,544 bytes (~327KB)
|
// 800MB/100 = 8MB
|
||||||
let w = compute_window_for_stream_count(100);
|
assert_eq!(compute_window_for_stream_count(100), 8 * 1024 * 1024);
|
||||||
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() {
|
fn test_adaptive_window_200_streams_at_floor() {
|
||||||
// 32MB/200 = 167,772 bytes (~163KB), above 64KB floor
|
// 800MB/200 = 4MB = exactly the floor
|
||||||
let w = compute_window_for_stream_count(200);
|
assert_eq!(compute_window_for_stream_count(200), 4 * 1024 * 1024);
|
||||||
assert_eq!(w, (32 * 1024 * 1024u64 / 200) as u32);
|
|
||||||
assert!(w > 64 * 1024);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_adaptive_window_500_streams() {
|
fn test_adaptive_window_500_streams_clamped() {
|
||||||
// 32MB/500 = 67,108 bytes (~65.5KB), just above 64KB floor
|
// 800MB/500 = 1.6MB -> clamped up to 4MB floor
|
||||||
let w = compute_window_for_stream_count(500);
|
assert_eq!(compute_window_for_stream_count(500), 4 * 1024 * 1024);
|
||||||
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 64KB
|
// Extreme: u32::MAX streams -> tiny value -> clamped to 4MB
|
||||||
assert_eq!(compute_window_for_stream_count(u32::MAX), 64 * 1024);
|
assert_eq!(compute_window_for_stream_count(u32::MAX), 4 * 1024 * 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, 5, 10, 50, 100, 200, 500, 512, 1000] {
|
for n in [2, 10, 50, 51, 100, 200, 500, 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;
|
||||||
@@ -775,11 +743,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_adaptive_window_total_budget_bounded() {
|
fn test_adaptive_window_total_budget_bounded() {
|
||||||
// active × per_stream_window should never exceed 32MB (+ clamp overhead for high N)
|
// active x per_stream_window should never exceed 800MB (+ clamp overhead for high N)
|
||||||
for n in [1, 10, 50, 100, 200, 500] {
|
for n in [1, 10, 50, 100, 200] {
|
||||||
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 <= 32 * 1024 * 1024, "total {}MB exceeds budget at n={}", total / (1024*1024), n);
|
assert!(total <= 800 * 1024 * 1024, "total {}MB exceeds budget at n={}", total / (1024*1024), n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/remoteingress',
|
name: '@serve.zone/remoteingress',
|
||||||
version: '4.8.16',
|
version: '4.8.18',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user