fix: resolve fatal connection halt caused by unrecoverable dropped untracked Ack/Nack frames. Control frames are now saved in sent_history without auto-retransmission to allow targeted Nack recovery.

This commit is contained in:
ospab 2026-05-17 02:40:52 +03:00
parent 9c05f130ac
commit 5c33f08a9b
2 changed files with 14 additions and 10 deletions

10
Cargo.lock generated
View File

@ -745,7 +745,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "ostp" name = "ostp"
version = "0.1.56" version = "0.1.57"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -762,7 +762,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.1.56" version = "0.1.57"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -780,7 +780,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.1.56" version = "0.1.57"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -813,7 +813,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-server" name = "ostp-server"
version = "0.1.56" version = "0.1.57"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -828,7 +828,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun-helper" name = "ostp-tun-helper"
version = "0.1.56" version = "0.1.57"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View File

@ -89,6 +89,7 @@ struct SentFrame {
bytes: Bytes, bytes: Bytes,
last_sent: Instant, last_sent: Instant,
retries: u8, retries: u8,
is_retransmittable: bool,
} }
impl ProtocolMachine { impl ProtocolMachine {
@ -356,7 +357,7 @@ impl ProtocolMachine {
self.build_datagram(stream_id, kind, payload, false) self.build_datagram(stream_id, kind, payload, false)
} }
fn build_datagram(&mut self, stream_id: u16, kind: FrameKind, payload: Bytes, track: bool) -> Result<Bytes, ProtocolError> { fn build_datagram(&mut self, stream_id: u16, kind: FrameKind, payload: Bytes, is_retransmittable: bool) -> Result<Bytes, ProtocolError> {
let padding = self.padder.build_padding(payload.len()); let padding = self.padder.build_padding(payload.len());
let header = FrameHeader { let header = FrameHeader {
version: 1, version: 1,
@ -395,9 +396,7 @@ impl ProtocolMachine {
let final_bytes = Bytes::from(out); let final_bytes = Bytes::from(out);
if track { self.push_sent_frame(nonce, final_bytes.clone(), is_retransmittable);
self.push_sent_frame(nonce, final_bytes.clone());
}
Ok(final_bytes) Ok(final_bytes)
} }
@ -417,6 +416,10 @@ impl ProtocolMachine {
let now = Instant::now(); let now = Instant::now();
let base_rto_ms = self.rto.as_millis().max(1) as u64; let base_rto_ms = self.rto.as_millis().max(1) as u64;
for frame in self.sent_history.iter_mut() { for frame in self.sent_history.iter_mut() {
if !frame.is_retransmittable {
continue;
}
if frame.retries == self.max_retries { if frame.retries == self.max_retries {
tracing::warn!( tracing::warn!(
"Frame {} exceeded max retries ({}); continuing with backoff", "Frame {} exceeded max retries ({}); continuing with backoff",
@ -518,12 +521,13 @@ impl ProtocolMachine {
None None
} }
fn push_sent_frame(&mut self, nonce: u64, bytes: Bytes) { fn push_sent_frame(&mut self, nonce: u64, bytes: Bytes, is_retransmittable: bool) {
self.sent_history.push_back(SentFrame { self.sent_history.push_back(SentFrame {
nonce, nonce,
bytes, bytes,
last_sent: Instant::now(), last_sent: Instant::now(),
retries: 0, retries: 0,
is_retransmittable,
}); });
while self.sent_history.len() > self.max_sent_history { while self.sent_history.len() > self.max_sent_history {
self.sent_history.pop_front(); self.sent_history.pop_front();