Retire closed transport stream ids

This commit is contained in:
DuProcess
2026-07-09 19:49:56 -04:00
parent 68cf411143
commit 5c8bd68aa6
3 changed files with 303 additions and 38 deletions
+9 -1
View File
@@ -4986,7 +4986,9 @@ async fn run_terminal(
continue; continue;
}; };
last_packet_at = Instant::now(); last_packet_at = Instant::now();
stream_pending_opens.remove(&ok.stream_id); if stream_pending_opens.remove(&ok.stream_id).is_none() {
continue;
}
opened_streams.insert(ok.stream_id); opened_streams.insert(ok.stream_id);
stream_send_credit.entry(ok.stream_id).or_insert(STREAM_INITIAL_WINDOW); stream_send_credit.entry(ok.stream_id).or_insert(STREAM_INITIAL_WINDOW);
stream_next_send_offset.entry(ok.stream_id).or_insert(0); stream_next_send_offset.entry(ok.stream_id).or_insert(0);
@@ -5044,6 +5046,9 @@ async fn run_terminal(
}; };
last_packet_at = Instant::now(); last_packet_at = Instant::now();
let stream_id = data.stream_id; let stream_id = data.stream_id;
if !opened_streams.contains(&stream_id) {
continue;
}
let (writes, consumed, received_offset) = let (writes, consumed, received_offset) =
accept_stream_data(&mut stream_next_recv_offset, &mut stream_recv_pending, data); accept_stream_data(&mut stream_next_recv_offset, &mut stream_recv_pending, data);
if let Some(writer) = stream_writers.get_mut(&stream_id) { if let Some(writer) = stream_writers.get_mut(&stream_id) {
@@ -5078,6 +5083,9 @@ async fn run_terminal(
continue; continue;
}; };
last_packet_at = Instant::now(); last_packet_at = Instant::now();
if !opened_streams.contains(&adjust.stream_id) {
continue;
}
ack_stream_data( ack_stream_data(
&mut stream_sent_data, &mut stream_sent_data,
&mut stream_send_credit, &mut stream_send_credit,
+12 -4
View File
@@ -2053,6 +2053,9 @@ async fn handle_stream_data(
} }
client.endpoint = peer; client.endpoint = peer;
client.last_seen = Instant::now(); client.last_seen = Instant::now();
if !client.opened_streams.contains(&data.stream_id) {
return Ok(());
}
let writer = client.stream_writers.get(&data.stream_id).cloned(); let writer = client.stream_writers.get(&data.stream_id).cloned();
let (writes, consumed, received_offset) = accept_stream_data(client, data); let (writes, consumed, received_offset) = accept_stream_data(client, data);
(writer, writes, consumed, received_offset) (writer, writes, consumed, received_offset)
@@ -2062,9 +2065,9 @@ async fn handle_stream_data(
let _ = writer.send(bytes).await; let _ = writer.send(bytes).await;
} }
} }
// Always return flow-control credit, even when the stream's writer is already // Always return flow-control credit for live streams, even when the stream's
// gone (closed/unknown). The peer debited its send window for these bytes, so // writer is already gone. The peer debited its send window for these bytes,
// skipping the adjust would wedge the stream at a permanent credit deficit. // so skipping the adjust would wedge the stream at a permanent credit deficit.
send_stream_window_adjust_to_client( send_stream_window_adjust_to_client(
state, state,
socket, socket,
@@ -2101,7 +2104,9 @@ async fn handle_stream_open_ok(
} }
client.endpoint = peer; client.endpoint = peer;
client.last_seen = Instant::now(); client.last_seen = Instant::now();
client.stream_pending_opens.remove(&ok.stream_id); if client.stream_pending_opens.remove(&ok.stream_id).is_none() {
return Ok(());
}
client.opened_streams.insert(ok.stream_id); client.opened_streams.insert(ok.stream_id);
client client
.stream_send_credit .stream_send_credit
@@ -2161,6 +2166,9 @@ async fn handle_stream_window_adjust(
} }
client.endpoint = peer; client.endpoint = peer;
client.last_seen = Instant::now(); client.last_seen = Instant::now();
if !client.opened_streams.contains(&adjust.stream_id) {
return Ok(());
}
ack_stream_data(client, adjust.stream_id, adjust.received_offset); ack_stream_data(client, adjust.stream_id, adjust.received_offset);
} }
flush_stream_pending_data_to_client(state, socket, packet.header.conn_id, adjust.stream_id) flush_stream_pending_data_to_client(state, socket, packet.header.conn_id, adjust.stream_id)
+282 -33
View File
@@ -141,6 +141,9 @@ pub enum TransportEvent {
Close { Close {
stream_id: u64, stream_id: u64,
}, },
Ignored {
stream_id: u64,
},
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -170,6 +173,7 @@ pub struct StreamMux {
next_send_offset: HashMap<u64, u64>, next_send_offset: HashMap<u64, u64>,
next_recv_offset: HashMap<u64, u64>, next_recv_offset: HashMap<u64, u64>,
recv_pending: HashMap<u64, BTreeMap<u64, Vec<u8>>>, recv_pending: HashMap<u64, BTreeMap<u64, Vec<u8>>>,
retired_streams: HashSet<u64>,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -225,6 +229,7 @@ impl StreamMux {
next_send_offset: HashMap::new(), next_send_offset: HashMap::new(),
next_recv_offset: HashMap::new(), next_recv_offset: HashMap::new(),
recv_pending: HashMap::new(), recv_pending: HashMap::new(),
retired_streams: HashSet::new(),
} }
} }
@@ -235,7 +240,10 @@ impl StreamMux {
target_port: u16, target_port: u16,
) -> Result<OutgoingStreamPacket> { ) -> Result<OutgoingStreamPacket> {
let target_host = target_host.into(); let target_host = target_host.into();
if self.opened_streams.contains(&stream_id) || self.pending_opens.contains_key(&stream_id) { if self.opened_streams.contains(&stream_id)
|| self.pending_opens.contains_key(&stream_id)
|| self.retired_streams.contains(&stream_id)
{
bail!("stream {stream_id} already exists"); bail!("stream {stream_id} already exists");
} }
self.send_credit self.send_credit
@@ -262,6 +270,9 @@ impl StreamMux {
} }
pub fn accept_open(&mut self, open: StreamOpen) -> Result<IncomingStreamOpen> { pub fn accept_open(&mut self, open: StreamOpen) -> Result<IncomingStreamOpen> {
if self.retired_streams.contains(&open.stream_id) {
bail!("stream {} was already closed", open.stream_id);
}
if self.pending_opens.contains_key(&open.stream_id) { if self.pending_opens.contains_key(&open.stream_id) {
bail!( bail!(
"stream {} collides with a local pending open", "stream {} collides with a local pending open",
@@ -295,19 +306,39 @@ impl StreamMux {
) )
} }
pub fn handle_open_ok(&mut self, ok: StreamOpenOk) -> Result<Vec<OutgoingStreamPacket>> { pub fn reject_open(
self.pending_opens.remove(&ok.stream_id); &mut self,
stream_id: u64,
reason: impl Into<String>,
) -> Result<OutgoingStreamPacket> {
if self.has_stream_state(stream_id) {
self.retire_stream(stream_id);
}
Self::open_reject(stream_id, reason)
}
pub fn handle_open_ok(
&mut self,
ok: StreamOpenOk,
) -> Result<Option<Vec<OutgoingStreamPacket>>> {
if self.pending_opens.remove(&ok.stream_id).is_none() {
return Ok(None);
}
self.opened_streams.insert(ok.stream_id); self.opened_streams.insert(ok.stream_id);
self.send_credit self.send_credit
.entry(ok.stream_id) .entry(ok.stream_id)
.or_insert(self.config.initial_window); .or_insert(self.config.initial_window);
self.next_send_offset.entry(ok.stream_id).or_insert(0); self.next_send_offset.entry(ok.stream_id).or_insert(0);
self.next_recv_offset.entry(ok.stream_id).or_insert(0); self.next_recv_offset.entry(ok.stream_id).or_insert(0);
self.flush_pending_data(ok.stream_id) self.flush_pending_data(ok.stream_id).map(Some)
} }
pub fn handle_open_reject(&mut self, reject: StreamOpenReject) { pub fn handle_open_reject(&mut self, reject: StreamOpenReject) -> bool {
self.cleanup_stream(reject.stream_id); if !self.has_stream_state(reject.stream_id) {
return false;
}
self.retire_stream(reject.stream_id);
true
} }
pub fn send_data( pub fn send_data(
@@ -344,8 +375,11 @@ impl StreamMux {
Ok(vec![self.send_data_now(stream_id, bytes)?]) Ok(vec![self.send_data_now(stream_id, bytes)?])
} }
pub fn handle_data(&mut self, data: StreamData) -> Result<IncomingStreamData> { pub fn handle_data(&mut self, data: StreamData) -> Result<Option<IncomingStreamData>> {
let stream_id = data.stream_id; let stream_id = data.stream_id;
if !self.opened_streams.contains(&stream_id) {
return Ok(None);
}
let (chunks, consumed, received_offset) = self.accept_data(data); let (chunks, consumed, received_offset) = self.accept_data(data);
let window_adjust = encode_packet( let window_adjust = encode_packet(
PacketKind::StreamWindowAdjust, PacketKind::StreamWindowAdjust,
@@ -355,37 +389,52 @@ impl StreamMux {
bytes: consumed.min(u32::MAX as usize) as u32, bytes: consumed.min(u32::MAX as usize) as u32,
}, },
)?; )?;
Ok(IncomingStreamData { Ok(Some(IncomingStreamData {
stream_id, stream_id,
chunks, chunks,
consumed, consumed,
received_offset, received_offset,
window_adjust, window_adjust,
}) }))
} }
pub fn handle_window_adjust( pub fn handle_window_adjust(
&mut self, &mut self,
adjust: StreamWindowAdjust, adjust: StreamWindowAdjust,
) -> Result<Vec<OutgoingStreamPacket>> { ) -> Result<Option<Vec<OutgoingStreamPacket>>> {
if !self.opened_streams.contains(&adjust.stream_id) {
return Ok(None);
}
let acked_bytes = self.ack_data(adjust.stream_id, adjust.received_offset); let acked_bytes = self.ack_data(adjust.stream_id, adjust.received_offset);
self.add_credit(adjust.stream_id, acked_bytes); self.add_credit(adjust.stream_id, acked_bytes);
self.flush_pending_data(adjust.stream_id) self.flush_pending_data(adjust.stream_id).map(Some)
} }
pub fn close_stream(&mut self, stream_id: u64) -> Result<OutgoingStreamPacket> { pub fn close_stream(&mut self, stream_id: u64) -> Result<OutgoingStreamPacket> {
self.cleanup_stream(stream_id); if !self.has_stream_state(stream_id) {
bail!("stream {stream_id} is not open");
}
self.retire_stream(stream_id);
encode_packet(PacketKind::StreamClose, &StreamClose { stream_id }) encode_packet(PacketKind::StreamClose, &StreamClose { stream_id })
} }
pub fn handle_close(&mut self, close: StreamClose) { pub fn handle_close(&mut self, close: StreamClose) -> bool {
self.cleanup_stream(close.stream_id); if !self.has_stream_state(close.stream_id) {
return false;
}
self.retire_stream(close.stream_id);
true
} }
pub fn handle_packet(&mut self, kind: PacketKind, body: &[u8]) -> Result<TransportEvent> { pub fn handle_packet(&mut self, kind: PacketKind, body: &[u8]) -> Result<TransportEvent> {
match kind { match kind {
PacketKind::StreamOpen => { PacketKind::StreamOpen => {
let open: StreamOpen = protocol::from_body(body)?; let open: StreamOpen = protocol::from_body(body)?;
if self.retired_streams.contains(&open.stream_id) {
return Ok(TransportEvent::Ignored {
stream_id: open.stream_id,
});
}
if self.opened_streams.contains(&open.stream_id) { if self.opened_streams.contains(&open.stream_id) {
return Ok(TransportEvent::DuplicateOpen { return Ok(TransportEvent::DuplicateOpen {
stream_id: open.stream_id, stream_id: open.stream_id,
@@ -397,32 +446,47 @@ impl StreamMux {
PacketKind::StreamOpenOk => { PacketKind::StreamOpenOk => {
let ok: StreamOpenOk = protocol::from_body(body)?; let ok: StreamOpenOk = protocol::from_body(body)?;
let stream_id = ok.stream_id; let stream_id = ok.stream_id;
let flushed = self.handle_open_ok(ok)?; match self.handle_open_ok(ok)? {
Ok(TransportEvent::OpenOk { stream_id, flushed }) Some(flushed) => Ok(TransportEvent::OpenOk { stream_id, flushed }),
None => Ok(TransportEvent::Ignored { stream_id }),
}
} }
PacketKind::StreamOpenReject => { PacketKind::StreamOpenReject => {
let reject: StreamOpenReject = protocol::from_body(body)?; let reject: StreamOpenReject = protocol::from_body(body)?;
self.handle_open_reject(reject.clone()); let stream_id = reject.stream_id;
Ok(TransportEvent::OpenReject { if self.handle_open_reject(reject.clone()) {
stream_id: reject.stream_id, Ok(TransportEvent::OpenReject {
reason: reject.reason, stream_id,
}) reason: reject.reason,
})
} else {
Ok(TransportEvent::Ignored { stream_id })
}
} }
PacketKind::StreamData => { PacketKind::StreamData => {
let data: StreamData = protocol::from_body(body)?; let data: StreamData = protocol::from_body(body)?;
self.handle_data(data).map(TransportEvent::Data) let stream_id = data.stream_id;
match self.handle_data(data)? {
Some(data) => Ok(TransportEvent::Data(data)),
None => Ok(TransportEvent::Ignored { stream_id }),
}
} }
PacketKind::StreamWindowAdjust => { PacketKind::StreamWindowAdjust => {
let adjust: StreamWindowAdjust = protocol::from_body(body)?; let adjust: StreamWindowAdjust = protocol::from_body(body)?;
let stream_id = adjust.stream_id; let stream_id = adjust.stream_id;
let flushed = self.handle_window_adjust(adjust)?; match self.handle_window_adjust(adjust)? {
Ok(TransportEvent::WindowAdjust { stream_id, flushed }) Some(flushed) => Ok(TransportEvent::WindowAdjust { stream_id, flushed }),
None => Ok(TransportEvent::Ignored { stream_id }),
}
} }
PacketKind::StreamClose => { PacketKind::StreamClose => {
let close: StreamClose = protocol::from_body(body)?; let close: StreamClose = protocol::from_body(body)?;
let stream_id = close.stream_id; let stream_id = close.stream_id;
self.handle_close(close); if self.handle_close(close) {
Ok(TransportEvent::Close { stream_id }) Ok(TransportEvent::Close { stream_id })
} else {
Ok(TransportEvent::Ignored { stream_id })
}
} }
_ => bail!("packet kind {kind:?} is not a stream transport packet"), _ => bail!("packet kind {kind:?} is not a stream transport packet"),
} }
@@ -610,6 +674,22 @@ impl StreamMux {
*credit = credit.saturating_add(bytes).min(self.config.initial_window); *credit = credit.saturating_add(bytes).min(self.config.initial_window);
} }
fn has_stream_state(&self, stream_id: u64) -> bool {
self.opened_streams.contains(&stream_id)
|| self.pending_opens.contains_key(&stream_id)
|| self.send_credit.contains_key(&stream_id)
|| self.pending_data.contains_key(&stream_id)
|| self.sent_data.contains_key(&stream_id)
|| self.next_send_offset.contains_key(&stream_id)
|| self.next_recv_offset.contains_key(&stream_id)
|| self.recv_pending.contains_key(&stream_id)
}
fn retire_stream(&mut self, stream_id: u64) {
self.cleanup_stream(stream_id);
self.retired_streams.insert(stream_id);
}
fn cleanup_stream(&mut self, stream_id: u64) { fn cleanup_stream(&mut self, stream_id: u64) {
self.opened_streams.remove(&stream_id); self.opened_streams.remove(&stream_id);
self.pending_opens.remove(&stream_id); self.pending_opens.remove(&stream_id);
@@ -727,8 +807,8 @@ impl DoshTransport {
} }
pub async fn reject_stream(&mut self, stream_id: u64, reason: impl Into<String>) -> Result<()> { pub async fn reject_stream(&mut self, stream_id: u64, reason: impl Into<String>) -> Result<()> {
self.send_outgoing(StreamMux::open_reject(stream_id, reason)?) let packet = self.mux.reject_open(stream_id, reason)?;
.await self.send_outgoing(packet).await
} }
pub async fn send(&mut self, stream_id: u64, bytes: impl Into<Vec<u8>>) -> Result<()> { pub async fn send(&mut self, stream_id: u64, bytes: impl Into<Vec<u8>>) -> Result<()> {
@@ -853,7 +933,8 @@ impl DoshTransport {
} }
TransportEvent::Open(_) TransportEvent::Open(_)
| TransportEvent::OpenReject { .. } | TransportEvent::OpenReject { .. }
| TransportEvent::Close { .. } => {} | TransportEvent::Close { .. }
| TransportEvent::Ignored { .. } => {}
} }
Ok(()) Ok(())
} }
@@ -976,7 +1057,10 @@ mod tests {
assert!(mux.send_data(7, b"hello".to_vec()).unwrap().is_empty()); assert!(mux.send_data(7, b"hello".to_vec()).unwrap().is_empty());
assert_eq!(mux.pending_bytes(7), 5); assert_eq!(mux.pending_bytes(7), 5);
let flushed = mux.handle_open_ok(StreamOpenOk { stream_id: 7 }).unwrap(); let flushed = mux
.handle_open_ok(StreamOpenOk { stream_id: 7 })
.unwrap()
.unwrap();
assert_eq!(flushed.len(), 1); assert_eq!(flushed.len(), 1);
assert_eq!(flushed[0].kind, PacketKind::StreamData); assert_eq!(flushed[0].kind, PacketKind::StreamData);
let data: StreamData = decode(&flushed[0]); let data: StreamData = decode(&flushed[0]);
@@ -999,6 +1083,7 @@ mod tests {
offset: 5, offset: 5,
bytes: b"world".to_vec(), bytes: b"world".to_vec(),
}) })
.unwrap()
.unwrap(); .unwrap();
assert!(first.chunks.is_empty()); assert!(first.chunks.is_empty());
assert_eq!(first.received_offset, 0); assert_eq!(first.received_offset, 0);
@@ -1009,6 +1094,7 @@ mod tests {
offset: 0, offset: 0,
bytes: b"hello".to_vec(), bytes: b"hello".to_vec(),
}) })
.unwrap()
.unwrap(); .unwrap();
assert_eq!(second.chunks, vec![b"hello".to_vec(), b"world".to_vec()]); assert_eq!(second.chunks, vec![b"hello".to_vec(), b"world".to_vec()]);
assert_eq!(second.consumed, 10); assert_eq!(second.consumed, 10);
@@ -1023,7 +1109,9 @@ mod tests {
..TransportConfig::default() ..TransportConfig::default()
}); });
mux.open_stream(1, "@dosh-test", 0).unwrap(); mux.open_stream(1, "@dosh-test", 0).unwrap();
mux.handle_open_ok(StreamOpenOk { stream_id: 1 }).unwrap(); mux.handle_open_ok(StreamOpenOk { stream_id: 1 })
.unwrap()
.unwrap();
let sent = mux.send_data(1, b"hello".to_vec()).unwrap(); let sent = mux.send_data(1, b"hello".to_vec()).unwrap();
assert_eq!(sent.len(), 1); assert_eq!(sent.len(), 1);
assert!(mux.send_data(1, b"!".to_vec()).unwrap().is_empty()); assert!(mux.send_data(1, b"!".to_vec()).unwrap().is_empty());
@@ -1034,6 +1122,7 @@ mod tests {
received_offset: 5, received_offset: 5,
bytes: 5, bytes: 5,
}) })
.unwrap()
.unwrap(); .unwrap();
assert_eq!(flushed.len(), 1); assert_eq!(flushed.len(), 1);
let data: StreamData = decode(&flushed[0]); let data: StreamData = decode(&flushed[0]);
@@ -1049,7 +1138,9 @@ mod tests {
..TransportConfig::default() ..TransportConfig::default()
}); });
mux.open_stream(1, "@dosh-test", 0).unwrap(); mux.open_stream(1, "@dosh-test", 0).unwrap();
mux.handle_open_ok(StreamOpenOk { stream_id: 1 }).unwrap(); mux.handle_open_ok(StreamOpenOk { stream_id: 1 })
.unwrap()
.unwrap();
assert_eq!(mux.send_data(1, b"hello".to_vec()).unwrap().len(), 1); assert_eq!(mux.send_data(1, b"hello".to_vec()).unwrap().len(), 1);
assert_eq!(mux.send_credit[&1], 0); assert_eq!(mux.send_credit[&1], 0);
@@ -1059,6 +1150,7 @@ mod tests {
received_offset: 5, received_offset: 5,
bytes: 0, bytes: 0,
}) })
.unwrap()
.unwrap(); .unwrap();
assert!(flushed.is_empty()); assert!(flushed.is_empty());
assert_eq!(mux.send_credit[&1], 5); assert_eq!(mux.send_credit[&1], 5);
@@ -1119,13 +1211,113 @@ mod tests {
let mut mux = StreamMux::new(TransportConfig::default()); let mut mux = StreamMux::new(TransportConfig::default());
mux.open_stream(1, "@dosh-test", 0).unwrap(); mux.open_stream(1, "@dosh-test", 0).unwrap();
assert!(mux.send_data(1, b"queued".to_vec()).unwrap().is_empty()); assert!(mux.send_data(1, b"queued".to_vec()).unwrap().is_empty());
mux.handle_open_ok(StreamOpenOk { stream_id: 1 }).unwrap(); mux.handle_open_ok(StreamOpenOk { stream_id: 1 })
.unwrap()
.unwrap();
mux.close_stream(1).unwrap(); mux.close_stream(1).unwrap();
assert!(mux.send_data(1, b"lost".to_vec()).is_err()); assert!(mux.send_data(1, b"lost".to_vec()).is_err());
assert!(mux.send_data(99, b"lost".to_vec()).is_err()); assert!(mux.send_data(99, b"lost".to_vec()).is_err());
} }
#[test]
fn rejecting_incoming_stream_retires_local_state() {
let mut mux = StreamMux::new(TransportConfig::default());
mux.accept_open(StreamOpen {
stream_id: 7,
target_host: "@dosh-test".to_string(),
target_port: 0,
})
.unwrap();
assert!(mux.is_open(7));
let reject = mux.reject_open(7, "nope").unwrap();
assert_eq!(reject.kind, PacketKind::StreamOpenReject);
assert!(!mux.is_open(7));
assert!(mux.retired_streams.contains(&7));
assert!(mux.send_data(7, b"must-not-send".to_vec()).is_err());
assert!(
mux.accept_open(StreamOpen {
stream_id: 7,
target_host: "@dosh-test".to_string(),
target_port: 0,
})
.is_err()
);
}
#[test]
fn stale_packets_after_close_do_not_recreate_stream_state() {
let mut mux = StreamMux::new(TransportConfig::default());
mux.open_stream(1, "@dosh-test", 0).unwrap();
mux.handle_open_ok(StreamOpenOk { stream_id: 1 })
.unwrap()
.unwrap();
assert_eq!(mux.send_data(1, b"hello".to_vec()).unwrap().len(), 1);
mux.close_stream(1).unwrap();
let stale_data = protocol::to_body(&StreamData {
stream_id: 1,
offset: 0,
bytes: b"stale".to_vec(),
})
.unwrap();
assert_eq!(
mux.handle_packet(PacketKind::StreamData, &stale_data)
.unwrap(),
TransportEvent::Ignored { stream_id: 1 }
);
let stale_open_ok = protocol::to_body(&StreamOpenOk { stream_id: 1 }).unwrap();
assert_eq!(
mux.handle_packet(PacketKind::StreamOpenOk, &stale_open_ok)
.unwrap(),
TransportEvent::Ignored { stream_id: 1 }
);
let stale_adjust = protocol::to_body(&StreamWindowAdjust {
stream_id: 1,
received_offset: 5,
bytes: 5,
})
.unwrap();
assert_eq!(
mux.handle_packet(PacketKind::StreamWindowAdjust, &stale_adjust)
.unwrap(),
TransportEvent::Ignored { stream_id: 1 }
);
let stale_open = protocol::to_body(&StreamOpen {
stream_id: 1,
target_host: "@dosh-test".to_string(),
target_port: 0,
})
.unwrap();
assert_eq!(
mux.handle_packet(PacketKind::StreamOpen, &stale_open)
.unwrap(),
TransportEvent::Ignored { stream_id: 1 }
);
let stale_close = protocol::to_body(&StreamClose { stream_id: 1 }).unwrap();
assert_eq!(
mux.handle_packet(PacketKind::StreamClose, &stale_close)
.unwrap(),
TransportEvent::Ignored { stream_id: 1 }
);
assert!(!mux.is_open(1));
assert!(!mux.pending_opens.contains_key(&1));
assert!(!mux.send_credit.contains_key(&1));
assert!(!mux.pending_data.contains_key(&1));
assert!(!mux.sent_data.contains_key(&1));
assert!(!mux.next_send_offset.contains_key(&1));
assert!(!mux.next_recv_offset.contains_key(&1));
assert!(!mux.recv_pending.contains_key(&1));
assert!(mux.retired_streams.contains(&1));
}
#[tokio::test] #[tokio::test]
async fn session_transport_partitions_client_and_server_stream_ids() { async fn session_transport_partitions_client_and_server_stream_ids() {
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
@@ -1291,6 +1483,63 @@ mod tests {
} }
} }
#[tokio::test]
async fn session_transport_reject_stream_retires_server_state() {
let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let server_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let client_addr = client_socket.local_addr().unwrap();
let server_addr = server_socket.local_addr().unwrap();
let key = [46u8; 32];
let conn_id = [10u8; 16];
let mut client = DoshTransport::new_owned(
client_socket,
SessionTransportConfig {
role: SessionRole::Client,
conn_id,
session_key: key,
peer_addr: server_addr,
initial_send_seq: 1,
initial_ack: 0,
stream: TransportConfig::default(),
},
);
let mut server = DoshTransport::new_owned(
server_socket,
SessionTransportConfig {
role: SessionRole::Server,
conn_id,
session_key: key,
peer_addr: client_addr,
initial_send_seq: 1,
initial_ack: 0,
stream: TransportConfig::default(),
},
);
let stream_id = client.open_service("app").await.unwrap();
match server.recv().await.unwrap() {
SessionEvent::Stream(TransportEvent::Open(open)) => {
assert_eq!(open.stream_id, stream_id);
assert!(server.mux().is_open(stream_id));
server.reject_stream(stream_id, "disabled").await.unwrap();
}
other => panic!("unexpected event {other:?}"),
}
assert!(!server.mux().is_open(stream_id));
assert!(
server
.send(stream_id, b"must-not-send".to_vec())
.await
.is_err()
);
assert!(matches!(
client.recv().await.unwrap(),
SessionEvent::Stream(TransportEvent::OpenReject { stream_id: rejected, reason })
if rejected == stream_id && reason == "disabled"
));
}
#[tokio::test] #[tokio::test]
async fn session_transport_roams_to_authenticated_new_peer_addr() { async fn session_transport_roams_to_authenticated_new_peer_addr() {
let server_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let server_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();