Skip to content

Commit e1d8506

Browse files
authored
Enforce the TCP receive window to bound per-session memory (#90)
* Enforce the TCP receive window to bound per-session memory The receive window ipstack advertises now holds. A segment beyond it is dropped for the peer to resend, the head-of-line segment always admitted so the stream advances. The handoff channel to the reader is bounded and filled by reserving a slot before consuming, so buffered data leaves the reassembly map only once it has a home; a reader that frees space wakes the loop to flush more and the follow-up ACK carries the reopened window. The window is advertised as zero below one segment, so a stalled reader puts the peer into persist mode until space frees. Consuming trims a stale head entry a re-segmented retransmission left below the ack, and a FIN is accepted only once the data before it has been consumed, so a full channel never strands the tail. * Cover reserve-before-consume with an async test The stream's first tokio test drives extract_data_n_write_upstream against a full handoff channel: buffered data stays in the reassembly map and the ack holds until the reader drains a slot, then the tail flushes and the ack advances.
1 parent 0f95edc commit e1d8506

2 files changed

Lines changed: 157 additions & 13 deletions

File tree

src/stream/tcb.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::{collections::BTreeMap, time::Duration};
44

55
pub(super) const MAX_UNACK: u32 = 1024 * 16; // 16KB
66
pub(super) const READ_BUFFER_SIZE: usize = 1024 * 16; // 16KB
7+
pub(super) const READ_CHUNK: usize = 8192; // 8KB, bytes drained from the reassembly buffer per handoff
78
pub(super) const MAX_COUNT_FOR_DUP_ACK: usize = 3; // Maximum number of duplicate ACKs before retransmission
89

910
/// Retransmission timeout
@@ -121,6 +122,13 @@ impl Tcb {
121122
log::warn!("{:?}: Received packet seq {seq} < self ack {}, len = {}", self.state, self.ack, buf.len());
122123
return;
123124
}
125+
// The head-of-line segment always advances the stream, so it is admitted even at the limit;
126+
// any other segment beyond the receive window is dropped for the peer's RTO to resend.
127+
if seq != self.ack && self.get_unordered_packets_total_len() >= self.read_buffer_size {
128+
#[rustfmt::skip]
129+
log::warn!("{:?}: Receive window full, dropping packet seq {seq}, len = {}", self.state, buf.len());
130+
return;
131+
}
124132
self.unordered_packets.insert(seq, buf);
125133
}
126134
pub(super) fn get_available_read_buffer_size(&self) -> usize {
@@ -137,10 +145,21 @@ impl Tcb {
137145

138146
while remaining_bytes > 0 {
139147
if let Some(seq) = self.unordered_packets.keys().next().copied() {
140-
if seq != self.ack {
148+
if seq > self.ack {
141149
break; // sequence number is not continuous, stop extracting
142150
}
143151

152+
if seq < self.ack {
153+
// A retransmission re-segmented across `ack` left a stale head entry; trim the
154+
// part already delivered so consumption can continue from `ack`.
155+
let payload = self.unordered_packets.remove(&seq).unwrap();
156+
let consumed = self.ack.distance(seq) as usize;
157+
if consumed < payload.len() {
158+
self.unordered_packets.insert(self.ack, payload[consumed..].to_vec());
159+
}
160+
continue;
161+
}
162+
144163
// remove and get the first packet
145164
let mut payload = self.unordered_packets.remove(&seq).unwrap();
146165
let payload_len = payload.len();
@@ -408,6 +427,55 @@ mod tests {
408427
assert!(data.is_none());
409428
}
410429

430+
#[test]
431+
fn test_add_unordered_packet_enforces_read_buffer() {
432+
let mut tcb = Tcb::new(
433+
SeqNum(1000),
434+
1500,
435+
MAX_UNACK,
436+
READ_BUFFER_SIZE,
437+
MAX_COUNT_FOR_DUP_ACK,
438+
RTO,
439+
MAX_RETRANSMIT_COUNT,
440+
);
441+
442+
// fill the receive buffer to its limit with an out-of-order gap held open
443+
tcb.add_unordered_packet(SeqNum(1000 + READ_BUFFER_SIZE as u32), vec![7; READ_BUFFER_SIZE]);
444+
assert_eq!(tcb.get_unordered_packets_total_len(), READ_BUFFER_SIZE);
445+
446+
// a further out-of-order segment is dropped, keeping the buffer bounded
447+
tcb.add_unordered_packet(SeqNum(1000 + 2 * READ_BUFFER_SIZE as u32), vec![8; 500]);
448+
assert_eq!(tcb.get_unordered_packets_total_len(), READ_BUFFER_SIZE);
449+
450+
// the head-of-line segment is admitted even at the limit, so the stream advances
451+
tcb.add_unordered_packet(SeqNum(1000), vec![9; 500]);
452+
assert_eq!(tcb.unordered_packets.get(&SeqNum(1000)).unwrap().len(), 500);
453+
}
454+
455+
#[test]
456+
fn test_consume_trims_overlapping_head_entry() {
457+
let mut tcb = Tcb::new(
458+
SeqNum(1000),
459+
1500,
460+
MAX_UNACK,
461+
READ_BUFFER_SIZE,
462+
MAX_COUNT_FOR_DUP_ACK,
463+
RTO,
464+
MAX_RETRANSMIT_COUNT,
465+
);
466+
467+
// an out-of-order segment stored ahead of ack
468+
tcb.add_unordered_packet(SeqNum(1200), vec![2; 300]);
469+
// the gap-filler that a retransmission re-segmented to overlap the stored one
470+
tcb.add_unordered_packet(SeqNum(1000), vec![1; 400]);
471+
472+
// consuming pulls [1000..1400), advancing ack into the stored entry keyed at 1200
473+
let data = tcb.consume_unordered_packets(10_000).unwrap();
474+
assert_eq!(data.len(), 500); // 400 + the 100 bytes of the stored entry past ack
475+
assert_eq!(tcb.ack, SeqNum(1500));
476+
assert_eq!(tcb.unordered_packets.len(), 0);
477+
}
478+
411479
#[test]
412480
fn test_update_inflight_packet_queue() {
413481
let mut tcb = Tcb::new(

src/stream/tcp.rs

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
tcp_flags::{ACK, FIN, PSH, RST, SYN},
88
tcp_header_flags, tcp_header_fmt,
99
},
10-
stream::tcb::{MAX_COUNT_FOR_DUP_ACK, MAX_RETRANSMIT_COUNT, MAX_UNACK, PacketType, READ_BUFFER_SIZE, RTO, Tcb, TcpState},
10+
stream::tcb::{MAX_COUNT_FOR_DUP_ACK, MAX_RETRANSMIT_COUNT, MAX_UNACK, PacketType, READ_BUFFER_SIZE, READ_CHUNK, RTO, Tcb, TcpState},
1111
};
1212
use etherparse::{IpNumber, Ipv4Header, Ipv6FlowLabel, TcpHeader, TcpOptionElement};
1313
use std::{
@@ -165,9 +165,10 @@ pub struct IpStackTcpStream {
165165
write_notify: std::sync::Arc<std::sync::Mutex<Option<Waker>>>,
166166
destroy_messenger: Option<::tokio::sync::oneshot::Sender<()>>,
167167
timeout: Pin<Box<tokio::time::Sleep>>,
168-
data_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
169-
data_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
168+
data_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
169+
data_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
170170
read_notify: std::sync::Arc<std::sync::Mutex<Option<Waker>>>,
171+
drain_notify: Arc<tokio::sync::Notify>,
171172
task_handle: Option<tokio::task::JoinHandle<std::io::Result<()>>>,
172173
exit_notifier: Option<tokio::sync::mpsc::Sender<()>>,
173174
temp_read_buffer: Vec<u8>,
@@ -205,7 +206,8 @@ impl IpStackTcpStream {
205206
}
206207

207208
let (stream_sender, stream_receiver) = tokio::sync::mpsc::unbounded_channel::<NetworkPacket>();
208-
let (data_tx, data_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
209+
let data_channel_len = config.read_buffer_size.div_ceil(READ_CHUNK).max(1);
210+
let (data_tx, data_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(data_channel_len);
209211
let deadline = tokio::time::Instant::now() + config.timeout;
210212

211213
let mut stream = IpStackTcpStream {
@@ -222,6 +224,7 @@ impl IpStackTcpStream {
222224
data_tx,
223225
data_rx,
224226
read_notify: std::sync::Arc::new(std::sync::Mutex::new(None)),
227+
drain_notify: Arc::new(tokio::sync::Notify::new()),
225228
task_handle: None,
226229
exit_notifier: None,
227230
temp_read_buffer: Vec::new(),
@@ -330,6 +333,8 @@ impl AsyncRead for IpStackTcpStream {
330333
buf.put_slice(&data[..capacity]);
331334
self.temp_read_buffer.extend_from_slice(&data[capacity..]);
332335
}
336+
// A channel slot just freed, so wake the loop to flush more and reopen the window.
337+
self.drain_notify.notify_one();
333338
Poll::Ready(Ok(()))
334339
}
335340
Poll::Ready(None) => Poll::Ready(Ok(())),
@@ -471,6 +476,7 @@ impl IpStackTcpStream {
471476
let write_notify = self.write_notify.clone();
472477
let read_notify = self.read_notify.clone();
473478
let data_tx = self.data_tx.clone();
479+
let drain_notify = self.drain_notify.clone();
474480
let destroy_messenger = self.destroy_messenger.take();
475481

476482
let (exit_task_notifier, exit_monitor) = tokio::sync::mpsc::channel::<()>(10);
@@ -489,6 +495,7 @@ impl IpStackTcpStream {
489495
write_notify,
490496
read_notify,
491497
data_tx,
498+
drain_notify,
492499
exit_monitor,
493500
)
494501
.await;
@@ -516,7 +523,8 @@ async fn tcp_main_logic_loop(
516523
network_tuple: NetworkTuple,
517524
write_notify: std::sync::Arc<std::sync::Mutex<Option<Waker>>>,
518525
read_notify: std::sync::Arc<std::sync::Mutex<Option<Waker>>>,
519-
data_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
526+
data_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
527+
drain_notify: Arc<tokio::sync::Notify>,
520528
mut exit_monitor: tokio::sync::mpsc::Receiver<()>,
521529
) -> std::io::Result<()> {
522530
{
@@ -642,6 +650,13 @@ async fn tcp_main_logic_loop(
642650
log::debug!("{network_tuple} task exited due to exit signal");
643651
break;
644652
}
653+
_ = drain_notify.notified() => {
654+
// The upstream reader freed channel space, so flush whatever is buffered and
655+
// let the follow-up ACK carry the reopened window.
656+
let mut tcb = tcb.lock().unwrap();
657+
extract_data_n_write_upstream(&up_packet_sender, &mut tcb, network_tuple, &data_tx, &read_notify)?;
658+
continue;
659+
}
645660
network_packet = stream_receiver.recv() => network_packet,
646661
};
647662

@@ -743,7 +758,7 @@ async fn tcp_main_logic_loop(
743758
}
744759
PacketType::Invalid => {}
745760
}
746-
} else if flags == (ACK | FIN) {
761+
} else if flags == (ACK | FIN) && tcb.get_ack() == incoming_seq {
747762
// The other side is closing the connection, we need to send an ACK and change state to CloseWait
748763
tcb.increase_ack();
749764
write_packet_to_device(&up_packet_sender, network_tuple, &tcb, None, ACK, None, None)?;
@@ -838,7 +853,7 @@ async fn tcp_main_logic_loop(
838853
log::trace!("{network_tuple} {state:?}: Received final ACK, transitioned to {new_state:?}");
839854
}
840855
TcpState::FinWait1 => {
841-
if flags & (ACK | FIN) == (ACK | FIN) && len == 0 {
856+
if flags & (ACK | FIN) == (ACK | FIN) && len == 0 && tcb.get_ack() == incoming_seq {
842857
// If the received packet is an ACK with FIN, we need to send an ACK and change state to TimeWait directly, not to FinWait2
843858
tcb.increase_ack();
844859
write_packet_to_device(&up_packet_sender, network_tuple, &tcb, None, ACK, None, None)?;
@@ -863,7 +878,7 @@ async fn tcp_main_logic_loop(
863878
}
864879
}
865880
TcpState::FinWait2 => {
866-
if flags & (ACK | FIN) == (ACK | FIN) && len == 0 {
881+
if flags & (ACK | FIN) == (ACK | FIN) && len == 0 && tcb.get_ack() == incoming_seq {
867882
tcb.increase_ack();
868883
write_packet_to_device(&up_packet_sender, network_tuple, &tcb, None, ACK, None, None)?;
869884
tcb.change_state(TcpState::TimeWait);
@@ -914,7 +929,7 @@ fn extract_data_n_write_upstream(
914929
up_packet_sender: &PacketSender,
915930
tcb: &mut Tcb,
916931
network_tuple: NetworkTuple,
917-
data_tx: &tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
932+
data_tx: &tokio::sync::mpsc::Sender<Vec<u8>>,
918933
read_notify: &std::sync::Arc<std::sync::Mutex<Option<Waker>>>,
919934
) -> std::io::Result<()> {
920935
let (state, seq, ack) = (tcb.get_state(), tcb.get_seq(), tcb.get_ack());
@@ -924,10 +939,23 @@ fn extract_data_n_write_upstream(
924939
return Ok(());
925940
}
926941

927-
if let Some(data) = tcb.consume_unordered_packets(8192) {
942+
// Reserve the handoff slot before consuming, so buffered data is removed only once it has a
943+
// guaranteed home; the reserved permit shrinks the advertised window until the reader drains it.
944+
let permit = match data_tx.try_reserve() {
945+
Ok(permit) => permit,
946+
Err(tokio::sync::mpsc::error::TrySendError::Full(())) => {
947+
write_packet_to_device(up_packet_sender, network_tuple, tcb, None, ACK, None, None)?;
948+
return Ok(());
949+
}
950+
Err(tokio::sync::mpsc::error::TrySendError::Closed(())) => {
951+
return Err(std::io::Error::new(BrokenPipe, "data channel closed"));
952+
}
953+
};
954+
955+
if let Some(data) = tcb.consume_unordered_packets(READ_CHUNK) {
928956
let hint = if state == TcpState::Established { "normally" } else { "still" };
929957
log::trace!("{network_tuple} {state:?}: {l_info} {hint} receiving data, len = {}", data.len());
930-
data_tx.send(data).map_err(|e| std::io::Error::new(BrokenPipe, e))?;
958+
permit.send(data);
931959
read_notify.lock().unwrap().take().map(|w| w.wake_by_ref()).unwrap_or(());
932960
write_packet_to_device(up_packet_sender, network_tuple, tcb, None, ACK, None, None)?;
933961
}
@@ -947,7 +975,11 @@ pub(crate) fn write_packet_to_device(
947975
) -> std::io::Result<usize> {
948976
use std::io::Error;
949977
let seq = seq.unwrap_or(tcb.get_seq()).0;
950-
let (ack, window_size) = (tcb.get_ack().0, tcb.get_recv_window().max(tcb.get_mtu()));
978+
// Silly-window-syndrome avoidance: advertise a real window only when a full segment fits,
979+
// otherwise advertise zero so the peer enters persist mode until the reader frees space.
980+
let recv_window = tcb.get_recv_window();
981+
let window_size = if recv_window >= tcb.get_mtu() { recv_window } else { 0 };
982+
let ack = tcb.get_ack().0;
951983
let (src, dst) = (tuple.dst, tuple.src); // Note: The address is reversed here
952984
let calc = |ip_header_len: usize, tcp_header_len: usize| tcb.calculate_payload_max_len(ip_header_len, tcp_header_len);
953985
let packet = create_raw_packet(
@@ -1048,3 +1080,47 @@ pub(crate) fn create_raw_packet(
10481080
payload: Some(payload),
10491081
})
10501082
}
1083+
1084+
#[cfg(test)]
1085+
mod tests {
1086+
use super::*;
1087+
use crate::stream::tcb::{MAX_COUNT_FOR_DUP_ACK, MAX_RETRANSMIT_COUNT, MAX_UNACK, READ_BUFFER_SIZE, RTO};
1088+
1089+
#[tokio::test]
1090+
async fn extract_reserves_before_consuming() {
1091+
let (up_tx, _up_rx) = tokio::sync::mpsc::unbounded_channel::<NetworkPacket>();
1092+
let (data_tx, mut data_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(1);
1093+
let read_notify = std::sync::Arc::new(std::sync::Mutex::new(None));
1094+
let nt = NetworkTuple::new("1.1.1.1:1".parse().unwrap(), "2.2.2.2:2".parse().unwrap(), true);
1095+
1096+
let mut tcb = Tcb::new(
1097+
SeqNum(1000),
1098+
1500,
1099+
MAX_UNACK,
1100+
READ_BUFFER_SIZE,
1101+
MAX_COUNT_FOR_DUP_ACK,
1102+
RTO,
1103+
MAX_RETRANSMIT_COUNT,
1104+
);
1105+
tcb.change_state(TcpState::Established);
1106+
tcb.add_unordered_packet(SeqNum(1000), vec![1; 500]);
1107+
tcb.add_unordered_packet(SeqNum(1500), vec![2; 500]);
1108+
1109+
// first extract fills the single channel slot and advances ack over the first chunk
1110+
extract_data_n_write_upstream(&up_tx, &mut tcb, nt, &data_tx, &read_notify).unwrap();
1111+
assert_eq!(tcb.get_ack(), SeqNum(2000));
1112+
1113+
// channel is full: extract leaves the remaining data in the map and does not advance ack
1114+
tcb.add_unordered_packet(SeqNum(2000), vec![3; 500]);
1115+
extract_data_n_write_upstream(&up_tx, &mut tcb, nt, &data_tx, &read_notify).unwrap();
1116+
assert_eq!(tcb.get_ack(), SeqNum(2000));
1117+
assert_eq!(tcb.get_unordered_packets_total_len(), 500);
1118+
1119+
// draining the reader frees a slot, and the next extract flushes the tail
1120+
let first = data_rx.recv().await.unwrap();
1121+
assert_eq!(first.len(), 1000);
1122+
extract_data_n_write_upstream(&up_tx, &mut tcb, nt, &data_tx, &read_notify).unwrap();
1123+
assert_eq!(tcb.get_ack(), SeqNum(2500));
1124+
assert_eq!(tcb.get_unordered_packets_total_len(), 0);
1125+
}
1126+
}

0 commit comments

Comments
 (0)