diff --git a/noq-proto/proptest-regressions/tests/proptests.txt b/noq-proto/proptest-regressions/tests/proptests.txt index 7bc939ff5..b1090d98d 100644 --- a/noq-proto/proptest-regressions/tests/proptests.txt +++ b/noq-proto/proptest-regressions/tests/proptests.txt @@ -32,3 +32,5 @@ cc 91184c7b6b718961d2dc03365f02098a18ac0035ca85b95654fbafa430d93664 # shrinks to cc ec5baef3027436b012a332a97d46814ed157f259337c3f651bbb7a3233bd9c7f # shrinks to input = _RandomInteractionWithMultipathSimpleRoutingArgs { seed: [121, 74, 209, 215, 123, 149, 7, 227, 67, 200, 91, 12, 216, 81, 208, 77, 83, 181, 39, 2, 207, 186, 233, 211, 254, 178, 230, 22, 100, 197, 215, 43], interactions: [Drive { side: Client }, ClosePath { side: Client, path_idx: 0, error_code: 0 }] } cc b1429b84bf576bb9000e8d0d6d53cff4c93efacf033c9df898aeb9856d1b03fe # shrinks to input = _RandomInteractionWithMultipathSimpleRoutingArgs { seed: [159, 14, 107, 252, 130, 4, 190, 131, 86, 208, 127, 29, 140, 30, 55, 65, 242, 192, 2, 158, 40, 51, 110, 116, 46, 139, 156, 165, 64, 109, 33, 62], interactions: [PassiveMigration { side: Server, addr_idx: 0 }, OpenPath { side: Client, status: Available, addr_idx: 0 }] } cc 4f717acb71d562f33601dfc8c7fbcca89b13c8259676a20ffc764a92e3ea07a1 # shrinks to input = _RandomInteractionWithMultipathComplexRoutingArgs { seed: [84, 97, 201, 172, 244, 139, 252, 60, 222, 107, 135, 245, 103, 45, 188, 138, 26, 198, 1, 97, 144, 22, 42, 228, 19, 154, 45, 135, 222, 137, 231, 16], interactions: [PassiveMigration { side: Server, addr_idx: 0 }, OpenPath { side: Client, status: Available, addr_idx: 0 }, ClosePath { side: Client, path_idx: 0, error_code: 0 }, PathSetStatus { side: Server, path_idx: 0, status: Backup }], routes: RoutingTable { client_routes: [([::ffff:1.1.1.0]:44433, 0)], server_routes: [([::ffff:2.2.2.0]:4433, 0)] } } +cc 829f66ceb9a2b64e08c089d76137a622a3209cb11a97a8c834b1f41b095b2a63 # shrinks to input = _MonkeyInteractionArgs { setup: MultipathSimpleRouting(Zeroes), side: Client, interactions: [Monkey([Frame(PathAck(PathAck { path_id: PathId(1), largest: 1, delay: 0, ranges: [1..2], ecn: None }))]), Normal(OpenPath { side: Client, status: Available, addr_idx: 0 })] } +cc 2f43de3c3cd460004fac95bd65ee18ec93bac6fca6f15bb7218cc044b0c5ff26 # shrinks to input = _MonkeyInteractionArgs { setup: MultipathSimpleRouting(Zeroes), side: Client, interactions: [Normal(PathSetStatus { side: Client, path_idx: 0, status: Backup }), Normal(Drive { side: Client }), Normal(Drive { side: Client }), Normal(OpenPath { side: Client, status: Available, addr_idx: 0 }), Normal(AdvanceTime), Monkey([Bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), Bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])])] } diff --git a/noq-proto/src/connection/mod.rs b/noq-proto/src/connection/mod.rs index f22291b88..ab96a5743 100644 --- a/noq-proto/src/connection/mod.rs +++ b/noq-proto/src/connection/mod.rs @@ -312,6 +312,13 @@ pub struct Connection { /// State for n0's () nat traversal protocol. n0_nat_traversal: n0_nat_traversal::State, qlog: QlogSink, + + /// A test-only frame queue for injecting valid, but potentially malicious packets. + /// + /// This is used in proptests to ensure that if another side behaves in a protocol- + /// incompliant way, we avoid panicking. + #[cfg(test)] + pending_data_to_inject: VecDeque, } impl Connection { @@ -432,6 +439,9 @@ impl Connection { n0_nat_traversal: Default::default(), qlog, + + #[cfg(test)] + pending_data_to_inject: Default::default(), }; if path_validated { this.on_path_validated(PathId::ZERO); @@ -1681,6 +1691,12 @@ impl Connection { }; } + #[cfg(test)] + if !self.populate_injected_packet_data(path_id, &mut builder) { + self.populate_packet(now, space_id, path_id, scheduling_info, &mut builder); + } + + #[cfg(not(test))] self.populate_packet(now, space_id, path_id, scheduling_info, &mut builder); // ACK-only packets should only be sent when explicitly allowed. If we write them due to @@ -2166,9 +2182,29 @@ impl Connection { can_send.close = connection_close_pending && space_has_crypto; + #[cfg(test)] + { + can_send |= self.can_send_data_to_inject(path_id); + } + can_send } + #[cfg(test)] + fn can_send_data_to_inject(&self, _path_id: PathId) -> SendableFrames { + SendableFrames { + acks: self.pending_data_to_inject.iter().any(|frame| { + matches!( + frame, + DataToInject::Frame(Frame::Ack(_)) | DataToInject::Frame(Frame::PathAck(_)) + ) + }), + other: !self.pending_data_to_inject.is_empty(), + close: false, + space_specific: false, + } + } + /// Process `ConnectionEvent`s generated by the associated `Endpoint` /// /// Will execute protocol logic upon receipt of a connection event, in turn preparing signals @@ -6359,6 +6395,53 @@ impl Connection { } } + #[cfg(test)] + fn populate_injected_packet_data<'a, 'b>( + &mut self, + path_id: PathId, + builder: &mut PacketBuilder<'a, 'b>, + ) -> bool { + let stats = &mut self.path_stats.for_path(path_id).frame_tx; + + let mut injected_test_packet = false; + + while let Some(inject) = self.pending_data_to_inject.pop_front() { + use crate::coding::Encodable; + + match inject { + DataToInject::Frame(frame) => { + let Some(enc_frame) = frame.to_encodable_frame() else { + continue; + }; + let mut encode_for_size = Vec::new(); + enc_frame.encode(&mut encode_for_size); + let size = encode_for_size.len(); + + if builder.frame_space_remaining() < size { + self.pending_data_to_inject + .push_front(DataToInject::Frame(frame)); + break; + } else { + injected_test_packet = true; + builder.inject_test_frame(enc_frame, stats); + } + } + DataToInject::Bytes(bytes) => { + if builder.frame_space_remaining() < bytes.len() { + self.pending_data_to_inject + .push_front(DataToInject::Bytes(bytes)); + break; + } else { + injected_test_packet = true; + builder.inject_test_bytes(bytes); + } + } + } + } + + injected_test_packet + } + /// Write pending ACKs into a buffer fn populate_acks<'a, 'b>( now: Instant, @@ -6745,6 +6828,14 @@ impl Connection { } } + /// Injects arbitrary data into the next packet for test purposes. + /// + /// This data can be structured frames or arbitrary bytes. + #[cfg(test)] + pub(crate) fn test_inject_data(&mut self, data: impl IntoIterator) { + self.pending_data_to_inject.extend(data); + } + /// Whether we have on-path 1-RTT data to send. /// /// This checks for frames that can only be sent in the data space (1-RTT): @@ -7657,6 +7748,14 @@ fn negotiate_max_idle_timeout(x: Option, y: Option) -> Option(), 1..1500))] Vec), +} + #[cfg(test)] mod tests { use super::*; diff --git a/noq-proto/src/connection/packet_builder.rs b/noq-proto/src/connection/packet_builder.rs index 9024abf20..46f17d3df 100644 --- a/noq-proto/src/connection/packet_builder.rs +++ b/noq-proto/src/connection/packet_builder.rs @@ -250,6 +250,34 @@ impl<'a, 'b> PacketBuilder<'a, 'b> { self.sent_frames.record_sent_frame(frame); } + /// Writes/injects a test frame into this packet. + /// + /// Exactly like [`Self::write_frame`], but + /// - logs with "(injected frame)" as the log message + /// - doesn't record the frame in `sent_frames` to avoid triggering retransmission and + /// unwanted processing of ACKs that were intentionally invalid. + #[cfg(test)] + pub(super) fn inject_test_frame<'c>( + &mut self, + frame: EncodableFrame<'c>, + stats: &mut FrameStats, + ) { + frame.encode(&mut self.frame_space_mut()); + stats.record(frame.get_type()); + self.qlog.record(&frame); + trace!(%frame, "(test-injected frame)"); + } + + /// Inserts arbitrary bytes into this packet for testing purposes. + #[cfg(test)] + pub(super) fn inject_test_bytes(&mut self, bytes: Vec) { + use std::io::Write; + + let len = bytes.len(); + let res = self.frame_space_mut().writer().write_all(&bytes); + trace!(len, ?res, "(test-injected data)"); + } + /// Returns a writable buffer limited to the remaining frame space /// /// The [`BufMut::remaining_mut`] call on the returned buffer indicates the amount of diff --git a/noq-proto/src/frame.rs b/noq-proto/src/frame.rs index 29339a882..14791b6b5 100644 --- a/noq-proto/src/frame.rs +++ b/noq-proto/src/frame.rs @@ -556,6 +556,70 @@ impl Frame { }) ) } + + #[cfg(test)] + pub(crate) fn to_encodable_frame(&self) -> Option> { + match self { + Frame::Padding => None, + Frame::Ping => Some(EncodableFrame::Ping(Ping)), + Frame::Ack(ack) => Some(EncodableFrame::Ack(ack.as_encoder())), + Frame::PathAck(path_ack) => Some(EncodableFrame::PathAck(path_ack.as_encoder())), + Frame::ResetStream(reset_stream) => Some(EncodableFrame::ResetStream(*reset_stream)), + Frame::StopSending(stop_sending) => Some(EncodableFrame::StopSending(*stop_sending)), + Frame::Crypto(crypto) => Some(EncodableFrame::Crypto(crypto.clone())), + Frame::NewToken(new_token) => Some(EncodableFrame::NewToken(new_token.clone())), + Frame::Stream(_stream) => None, + Frame::MaxData(max_data) => Some(EncodableFrame::MaxData(*max_data)), + Frame::MaxStreamData(max_stream_data) => { + Some(EncodableFrame::MaxStreamData(*max_stream_data)) + } + Frame::MaxStreams(max_streams) => Some(EncodableFrame::MaxStreams(*max_streams)), + Frame::DataBlocked(_data_blocked) => None, + Frame::StreamDataBlocked(_stream_data_blocked) => None, + Frame::StreamsBlocked(_streams_blocked) => None, + Frame::NewConnectionId(new_connection_id) => { + Some(EncodableFrame::NewConnectionId(*new_connection_id)) + } + Frame::RetireConnectionId(retire_connection_id) => { + Some(EncodableFrame::RetireConnectionId(*retire_connection_id)) + } + Frame::PathChallenge(path_challenge) => { + Some(EncodableFrame::PathChallenge(*path_challenge)) + } + Frame::PathResponse(path_response) => { + Some(EncodableFrame::PathResponse(*path_response)) + } + Frame::Close(close) => Some(EncodableFrame::Close(close.encoder(1000))), + Frame::Datagram(datagram) => Some(EncodableFrame::Datagram(datagram.clone())), + Frame::AckFrequency(ack_frequency) => { + Some(EncodableFrame::AckFrequency(*ack_frequency)) + } + Frame::ImmediateAck => Some(EncodableFrame::ImmediateAck(ImmediateAck)), + Frame::HandshakeDone => Some(EncodableFrame::HandshakeDone(HandshakeDone)), + Frame::ObservedAddr(observed_addr) => { + Some(EncodableFrame::ObservedAddr(*observed_addr)) + } + Frame::PathAbandon(path_abandon) => Some(EncodableFrame::PathAbandon(*path_abandon)), + Frame::PathStatusAvailable(path_status_available) => { + Some(EncodableFrame::PathStatusAvailable(*path_status_available)) + } + Frame::PathStatusBackup(path_status_backup) => { + Some(EncodableFrame::PathStatusBackup(*path_status_backup)) + } + Frame::MaxPathId(max_path_id) => Some(EncodableFrame::MaxPathId(*max_path_id)), + Frame::PathsBlocked(paths_blocked) => { + Some(EncodableFrame::PathsBlocked(*paths_blocked)) + } + Frame::PathCidsBlocked(path_cids_blocked) => { + Some(EncodableFrame::PathCidsBlocked(*path_cids_blocked)) + } + Frame::AddAddress(add_address) => Some(EncodableFrame::AddAddress(*add_address)), + Frame::ReachOut(reach_out) => Some(EncodableFrame::ReachOut(*reach_out)), + Frame::RemoveAddress(remove_address) => { + Some(EncodableFrame::RemoveAddress(*remove_address)) + } + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] @@ -751,7 +815,7 @@ impl Encodable for MaxStreams { } } -#[derive(Debug, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary))] #[display("{} {} seq: {sequence}", self.get_type(), DisplayOption::new("path_id", path_id.as_ref()))] pub(crate) struct RetireConnectionId { @@ -1052,6 +1116,16 @@ impl PathAck { ecn, } } + + #[cfg(test)] + fn as_encoder(&self) -> PathAckEncoder<'_> { + PathAckEncoder { + path_id: self.path_id, + delay: self.delay, + ranges: &self.ranges, + ecn: self.ecn.as_ref(), + } + } } #[derive(derive_more::Display)] @@ -1155,6 +1229,15 @@ impl Ack { AckEncoder { delay, ranges, ecn } } + #[cfg(test)] + fn as_encoder(&self) -> AckEncoder<'_> { + AckEncoder { + delay: self.delay, + ranges: &self.ranges, + ecn: self.ecn.as_ref(), + } + } + pub(crate) fn iter(&self) -> impl DoubleEndedIterator> + '_ { self.ranges.iter() } @@ -1402,7 +1485,7 @@ impl NewToken { } } -#[derive(Debug, Clone, derive_more::Display)] +#[derive(Debug, Clone, Copy, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary, PartialEq, Eq))] #[display("MAX_PATH_ID path_id: {_0}")] pub(crate) struct MaxPathId(pub(crate) PathId); @@ -1429,7 +1512,7 @@ impl Encodable for MaxPathId { } } -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary))] #[display("PATHS_BLOCKED remote_max_path_id: {_0}")] pub(crate) struct PathsBlocked(pub(crate) PathId); @@ -1457,7 +1540,7 @@ impl Decodable for PathsBlocked { } } -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary))] #[display("PATH_CIDS_BLOCKED path_id: {path_id} next_seq: {next_seq}")] pub(crate) struct PathCidsBlocked { @@ -2099,7 +2182,7 @@ impl Encodable for AckFrequency { /// Conjunction of the information contained in the address discovery frames /// ([`FrameType::ObservedIpv4Addr`], [`FrameType::ObservedIpv6Addr`]). -#[derive(Debug, PartialEq, Eq, Clone, derive_more::Display)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, derive_more::Display)] #[display("{} seq_no: {seq_no} addr: {}", self.get_type(), self.socket_addr())] #[cfg_attr(test, derive(Arbitrary))] pub(crate) struct ObservedAddr { @@ -2177,7 +2260,7 @@ impl Encodable for ObservedAddr { /* Multipath */ -#[derive(Debug, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary))] #[display("PATH_ABANDON path_id: {path_id}")] pub(crate) struct PathAbandon { @@ -2210,7 +2293,7 @@ impl Decodable for PathAbandon { } } -#[derive(Debug, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary))] #[display("PATH_STATUS_AVAILABLE path_id: {path_id} seq_no: {status_seq_no}")] pub(crate) struct PathStatusAvailable { @@ -2244,7 +2327,7 @@ impl Decodable for PathStatusAvailable { } } -#[derive(Debug, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)] #[cfg_attr(test, derive(Arbitrary))] #[display("PATH_STATUS_BACKUP path_id: {path_id} seq_no: {status_seq_no}")] pub(crate) struct PathStatusBackup { @@ -2370,7 +2453,7 @@ impl Encodable for AddAddress { /// Conjunction of the information contained in the reach out frames /// ([`FrameType::ReachOutAtIpv4`], [`FrameType::ReachOutAtIpv6`]) -#[derive(Debug, PartialEq, Eq, Clone, derive_more::Display)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, derive_more::Display)] #[display("REACH_OUT round: {round} local_addr: {}", self.socket_addr())] #[cfg_attr(test, derive(Arbitrary))] pub(crate) struct ReachOut { diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 3baee3dc2..3875c3b6f 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -10,14 +10,15 @@ use proptest::{ prop_assert, }; use test_strategy::proptest; -use tracing::error; +use tracing::{error, info}; use crate::{ ClientConfig, Connection, ConnectionClose, ConnectionError, Event, PathStatus, Side, TransportConfig, TransportErrorCode, + connection::DataToInject, tests::{ Pair, RoutingTable, client_config, - random_interaction::{TestOp, run_random_interaction}, + random_interaction::{State, TestOp, run_random_interaction}, server_config, subscribe, }, }; @@ -215,6 +216,45 @@ fn random_interaction( ))); } +#[derive(Debug, test_strategy::Arbitrary)] +enum TestOpOrMonkey { + Normal(TestOp), + Monkey(#[strategy(vec(any::(), 1..50))] Vec), +} + +#[proptest(cases = 256)] +fn monkey_interaction( + side: Side, + setup: PairSetup, + #[strategy(vec(any::(), 0..100))] interactions: Vec, +) { + let (mut pair, client_config) = setup.run("monkey_client_interaction"); + let (client_ch, server_ch) = pair.connect_with(client_config); + pair.drive(); // finish establishing the connection; + info!("INTERACTION SETUP FINISHED"); + let mut client = State::new(Side::Client, client_ch); + let mut server = State::new(Side::Server, server_ch); + + for interaction in interactions { + match interaction { + TestOpOrMonkey::Normal(interaction) => { + info!(?interaction, "INTERACTION STEP"); + interaction.run(&mut pair, &mut client, &mut server); + } + TestOpOrMonkey::Monkey(data) => { + info!(?data, ?side, "MONKEY INJECTION STEP"); + match side { + Side::Client => pair.client_conn_mut(client_ch), + Side::Server => pair.server_conn_mut(server_ch), + } + .test_inject_data(data) + } + } + } + + prop_assert!(!pair.drive_bounded(1000), "connection never became idle"); +} + fn routing_table() -> impl Strategy { (vec(0..=5usize, 0..=4), vec(0..=5usize, 0..=4)).prop_map(|(client_offsets, server_offsets)| { let mut client_addr = SocketAddr::new( diff --git a/noq-proto/src/tests/random_interaction.rs b/noq-proto/src/tests/random_interaction.rs index b87036811..7fdb741a0 100644 --- a/noq-proto/src/tests/random_interaction.rs +++ b/noq-proto/src/tests/random_interaction.rs @@ -102,7 +102,7 @@ pub(super) struct State { } impl TestOp { - fn run(self, pair: &mut Pair, client: &mut State, server: &mut State) -> Option<()> { + pub(crate) fn run(self, pair: &mut Pair, client: &mut State, server: &mut State) -> Option<()> { let now = pair.time; match self { Self::Drive { side: Side::Client } => pair.drive_client(), @@ -283,7 +283,7 @@ impl StreamOp { } impl State { - fn new(side: Side, handle: ConnectionHandle) -> Self { + pub(crate) fn new(side: Side, handle: ConnectionHandle) -> Self { Self { send_streams: Vec::new(), recv_streams: Vec::new(),