1use std::error::Error;
18use std::fmt;
19use std::io;
20use std::io::Read;
21use std::io::Write;
22use std::net::Ipv4Addr;
23use std::net::Ipv6Addr;
24use std::path::PathBuf;
25
26use chrono::DateTime;
27use chrono::Utc;
28use serde::Deserialize;
29use serde::Serialize;
30
31use crate::fd::OpenFileId;
32use crate::time::LogicalTime;
33
34pub const NETWORK_TRACE_MAGIC: [u8; 16] = *b"HERMIT-NET-TRACE";
36pub const NETWORK_TRACE_VERSION_V1: u32 = 1;
38pub const MAX_NETWORK_TRACE_PAYLOAD_BYTES: u64 = 64 * 1024 * 1024;
40
41const FRAME_HEADER_LEN: usize = NETWORK_TRACE_MAGIC.len() + 4 + 8;
42
43#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum NetworkTraceMode {
47 #[default]
49 Off,
50 Record,
52 Replay,
54}
55
56#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct NetworkTraceConfig {
64 pub mode: NetworkTraceMode,
65 pub path: Option<PathBuf>,
66 pub network_perturb_seed: Option<u64>,
67}
68
69impl NetworkTraceConfig {
70 pub fn validate(&self) -> Result<(), NetworkTraceConfigError> {
72 match self.mode {
73 NetworkTraceMode::Off => {
74 if self.path.is_some() || self.network_perturb_seed.is_some() {
75 return Err(NetworkTraceConfigError::OptionsWhileOff);
76 }
77 }
78 NetworkTraceMode::Record => {
79 validate_trace_path(self.path.as_ref())?;
80 if self.network_perturb_seed.is_some() {
81 return Err(NetworkTraceConfigError::PerturbationDuringRecord);
82 }
83 }
84 NetworkTraceMode::Replay => validate_trace_path(self.path.as_ref())?,
85 }
86 Ok(())
87 }
88}
89
90fn validate_trace_path(path: Option<&PathBuf>) -> Result<(), NetworkTraceConfigError> {
91 let Some(path) = path else {
92 return Err(NetworkTraceConfigError::MissingPath);
93 };
94 if path.as_os_str().is_empty() {
95 return Err(NetworkTraceConfigError::EmptyPath);
96 }
97 Ok(())
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum NetworkTraceConfigError {
103 OptionsWhileOff,
104 MissingPath,
105 EmptyPath,
106 PerturbationDuringRecord,
107}
108
109impl fmt::Display for NetworkTraceConfigError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Self::OptionsWhileOff => {
113 write!(f, "network trace options require record or replay mode")
114 }
115 Self::MissingPath => write!(f, "network trace record/replay requires a path"),
116 Self::EmptyPath => write!(f, "network trace path must not be empty"),
117 Self::PerturbationDuringRecord => {
118 write!(f, "network perturbation is replay-only")
119 }
120 }
121 }
122}
123
124impl Error for NetworkTraceConfigError {}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case", deny_unknown_fields)]
129pub enum NetworkAddressV1 {
130 Inet4 {
131 address: [u8; 4],
132 port: u16,
133 },
134 Inet6 {
135 address: [u8; 16],
136 port: u16,
137 flowinfo: u32,
138 scope_id: u32,
139 },
140}
141
142impl NetworkAddressV1 {
143 fn is_supported_external_peer(&self) -> bool {
144 match self {
145 Self::Inet4 { address, port } => {
146 *port != 0 && is_supported_external_ipv4(Ipv4Addr::from(*address))
147 }
148 Self::Inet6 { address, port, .. } => {
149 let address = Ipv6Addr::from(*address);
150 *port != 0
151 && match address.to_ipv4_mapped() {
152 Some(mapped) => is_supported_external_ipv4(mapped),
153 None => {
154 !address.is_loopback()
155 && !address.is_unspecified()
156 && !address.is_multicast()
157 }
158 }
159 }
160 }
161 }
162
163 fn same_family(&self, other: &Self) -> bool {
164 matches!(
165 (self, other),
166 (Self::Inet4 { .. }, Self::Inet4 { .. }) | (Self::Inet6 { .. }, Self::Inet6 { .. })
167 )
168 }
169}
170
171fn is_supported_external_ipv4(address: Ipv4Addr) -> bool {
172 !address.is_loopback()
173 && !address.is_unspecified()
174 && !address.is_multicast()
175 && !address.is_broadcast()
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum NetworkTransportV1 {
182 Tcp,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "snake_case")]
189pub enum NetworkEndpointRoleV1 {
190 OutboundClient,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(deny_unknown_fields)]
196pub struct NetworkChannelV1 {
197 pub id: OpenFileId,
199 pub transport: NetworkTransportV1,
200 pub role: NetworkEndpointRoleV1,
201 pub local_address: NetworkAddressV1,
202 pub peer_address: NetworkAddressV1,
203 pub created_before_competing_threads: bool,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(deny_unknown_fields)]
211pub struct NetworkReleaseV1 {
212 pub not_before_global_time: LogicalTime,
215 pub after_transmitted_offset: u64,
217}
218
219impl NetworkReleaseV1 {
220 pub fn is_eligible(
222 &self,
223 committed_global_time: LogicalTime,
224 validated_transmitted_offset: u64,
225 ) -> bool {
226 committed_global_time >= self.not_before_global_time
227 && validated_transmitted_offset >= self.after_transmitted_offset
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(rename_all = "snake_case", deny_unknown_fields)]
234pub enum NetworkInputKindV1 {
235 InboundBytes {
236 stream_offset: u64,
237 bytes: Vec<u8>,
238 },
239 PeerWriteClosed {
240 stream_offset: u64,
241 },
242 SocketError {
244 stream_offset: u64,
245 errno: i32,
246 },
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(deny_unknown_fields)]
252pub struct NetworkInputEventV1 {
253 pub ordinal: u64,
254 pub channel: OpenFileId,
255 pub release: NetworkReleaseV1,
256 pub event: NetworkInputKindV1,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(deny_unknown_fields)]
262pub struct NetworkOutputV1 {
263 pub channel: OpenFileId,
264 pub stream_offset: u64,
265 pub bytes: Vec<u8>,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct NetworkTraceV1 {
272 pub epoch: DateTime<Utc>,
273 pub channels: Vec<NetworkChannelV1>,
274 pub inputs: Vec<NetworkInputEventV1>,
275 pub outputs: Vec<NetworkOutputV1>,
276}
277
278impl NetworkTraceV1 {
279 pub fn epoch_global_time(&self) -> Result<LogicalTime, NetworkTraceValidationError> {
284 let seconds = u64::try_from(self.epoch.timestamp())
285 .map_err(|_| NetworkTraceValidationError::EpochOutOfRange)?;
286 let whole_seconds = seconds
287 .checked_mul(1_000_000_000)
288 .ok_or(NetworkTraceValidationError::EpochOutOfRange)?;
289 let fractional_micros = u64::from(self.epoch.timestamp_subsec_micros())
290 .checked_mul(1_000)
291 .ok_or(NetworkTraceValidationError::EpochOutOfRange)?;
292 whole_seconds
293 .checked_add(fractional_micros)
294 .map(LogicalTime::from_nanos)
295 .ok_or(NetworkTraceValidationError::EpochOutOfRange)
296 }
297
298 pub fn validate(&self) -> Result<(), NetworkTraceValidationError> {
300 if self.channels.len() != 1 {
301 return Err(NetworkTraceValidationError::ChannelCount(
302 self.channels.len(),
303 ));
304 }
305 let channel = &self.channels[0];
306 if !channel.id.is_socket() {
307 return Err(NetworkTraceValidationError::NonSocketChannelIdentity);
308 }
309 if !channel.created_before_competing_threads {
310 return Err(NetworkTraceValidationError::ScheduleDependentChannelIdentity);
311 }
312 if !channel.local_address.same_family(&channel.peer_address) {
313 return Err(NetworkTraceValidationError::AddressFamilyMismatch);
314 }
315 if !channel.peer_address.is_supported_external_peer() {
316 return Err(NetworkTraceValidationError::UnsupportedPeerAddress);
317 }
318
319 let mut inbound_offset = 0u64;
320 let epoch_start = self.epoch_global_time()?;
321 let mut previous_time = epoch_start;
322 let mut previous_tx_watermark = 0u64;
323 let mut terminal = false;
324
325 let total_output = validate_outputs(&self.outputs, channel.id)?;
326 for (index, input) in self.inputs.iter().enumerate() {
327 if input.ordinal != index as u64 {
328 return Err(NetworkTraceValidationError::NonCanonicalOrdinal);
329 }
330 if input.channel != channel.id {
331 return Err(NetworkTraceValidationError::UnknownChannel);
332 }
333 if input.release.not_before_global_time < epoch_start {
334 return Err(NetworkTraceValidationError::ReleaseBeforeEpoch);
335 }
336 if input.release.not_before_global_time < previous_time
337 || input.release.after_transmitted_offset < previous_tx_watermark
338 {
339 return Err(NetworkTraceValidationError::NonMonotonicRelease);
340 }
341 if input.release.after_transmitted_offset > total_output {
342 return Err(NetworkTraceValidationError::UnreachableTransmitWatermark);
343 }
344 if terminal {
345 return Err(NetworkTraceValidationError::EventAfterTerminal);
346 }
347
348 match &input.event {
349 NetworkInputKindV1::InboundBytes {
350 stream_offset,
351 bytes,
352 } => {
353 if bytes.is_empty() {
354 return Err(NetworkTraceValidationError::EmptyByteChunk);
355 }
356 if *stream_offset != inbound_offset {
357 return Err(NetworkTraceValidationError::NonContiguousInput);
358 }
359 inbound_offset = inbound_offset
360 .checked_add(bytes.len() as u64)
361 .ok_or(NetworkTraceValidationError::StreamOffsetOverflow)?;
362 }
363 NetworkInputKindV1::PeerWriteClosed { stream_offset } => {
364 if *stream_offset != inbound_offset {
365 return Err(NetworkTraceValidationError::NonContiguousInput);
366 }
367 terminal = true;
368 }
369 NetworkInputKindV1::SocketError {
370 stream_offset,
371 errno,
372 } => {
373 if *stream_offset != inbound_offset {
374 return Err(NetworkTraceValidationError::NonContiguousInput);
375 }
376 if !(1..=4095).contains(errno) {
377 return Err(NetworkTraceValidationError::InvalidErrno);
378 }
379 if matches!(*errno, libc::EAGAIN | libc::EINTR) {
383 return Err(NetworkTraceValidationError::NonTerminalSocketError);
384 }
385 terminal = true;
386 }
387 }
388 previous_time = input.release.not_before_global_time;
389 previous_tx_watermark = input.release.after_transmitted_offset;
390 }
391 Ok(())
392 }
393
394 pub fn write_framed<W: Write>(&self, mut writer: W) -> Result<(), NetworkTraceCodecError> {
396 self.validate()?;
397 let payload = bincode::serde::encode_to_vec(self, bincode::config::standard())
398 .map_err(NetworkTraceCodecError::Encode)?;
399 let payload_len =
400 u64::try_from(payload.len()).map_err(|_| NetworkTraceCodecError::TooLarge)?;
401 if payload_len > MAX_NETWORK_TRACE_PAYLOAD_BYTES {
402 return Err(NetworkTraceCodecError::TooLarge);
403 }
404 writer.write_all(&NETWORK_TRACE_MAGIC)?;
405 writer.write_all(&NETWORK_TRACE_VERSION_V1.to_le_bytes())?;
406 writer.write_all(&payload_len.to_le_bytes())?;
407 writer.write_all(&payload)?;
408 Ok(())
409 }
410
411 pub fn read_framed<R: Read>(mut reader: R) -> Result<Self, NetworkTraceCodecError> {
414 let mut header = [0u8; FRAME_HEADER_LEN];
415 read_exact_or_truncated(&mut reader, &mut header)?;
416 if header[..NETWORK_TRACE_MAGIC.len()] != NETWORK_TRACE_MAGIC {
417 return Err(NetworkTraceCodecError::BadMagic);
418 }
419 let version_start = NETWORK_TRACE_MAGIC.len();
420 let version = u32::from_le_bytes(
421 header[version_start..version_start + 4]
422 .try_into()
423 .expect("fixed-size version field"),
424 );
425 if version != NETWORK_TRACE_VERSION_V1 {
426 return Err(NetworkTraceCodecError::UnsupportedVersion(version));
427 }
428 let len_start = version_start + 4;
429 let payload_len = u64::from_le_bytes(
430 header[len_start..len_start + 8]
431 .try_into()
432 .expect("fixed-size length field"),
433 );
434 if payload_len > MAX_NETWORK_TRACE_PAYLOAD_BYTES {
435 return Err(NetworkTraceCodecError::TooLarge);
436 }
437 let payload_len =
438 usize::try_from(payload_len).map_err(|_| NetworkTraceCodecError::TooLarge)?;
439 let mut payload = vec![0; payload_len];
440 read_exact_or_truncated(&mut reader, &mut payload)?;
441 let mut trailing = [0u8; 1];
442 if reader.read(&mut trailing)? != 0 {
443 return Err(NetworkTraceCodecError::TrailingData);
444 }
445 let (trace, consumed): (Self, usize) =
446 bincode::serde::decode_from_slice(&payload, bincode::config::standard())
447 .map_err(NetworkTraceCodecError::Decode)?;
448 if consumed != payload.len() {
449 return Err(NetworkTraceCodecError::TrailingPayloadData);
450 }
451 trace.validate()?;
452 Ok(trace)
453 }
454}
455
456fn validate_outputs(
457 outputs: &[NetworkOutputV1],
458 channel: OpenFileId,
459) -> Result<u64, NetworkTraceValidationError> {
460 let mut expected_offset = 0u64;
461 for output in outputs {
462 if output.channel != channel {
463 return Err(NetworkTraceValidationError::UnknownChannel);
464 }
465 if output.bytes.is_empty() {
466 return Err(NetworkTraceValidationError::EmptyByteChunk);
467 }
468 if output.stream_offset != expected_offset {
469 return Err(NetworkTraceValidationError::NonContiguousOutput);
470 }
471 expected_offset = expected_offset
472 .checked_add(output.bytes.len() as u64)
473 .ok_or(NetworkTraceValidationError::StreamOffsetOverflow)?;
474 }
475 Ok(expected_offset)
476}
477
478fn read_exact_or_truncated<R: Read>(
479 reader: &mut R,
480 bytes: &mut [u8],
481) -> Result<(), NetworkTraceCodecError> {
482 reader.read_exact(bytes).map_err(|error| {
483 if error.kind() == io::ErrorKind::UnexpectedEof {
484 NetworkTraceCodecError::Truncated
485 } else {
486 NetworkTraceCodecError::Io(error)
487 }
488 })
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
493pub enum NetworkTraceValidationError {
494 ChannelCount(usize),
495 EpochOutOfRange,
496 NonSocketChannelIdentity,
497 ScheduleDependentChannelIdentity,
498 AddressFamilyMismatch,
499 UnsupportedPeerAddress,
500 UnknownChannel,
501 NonCanonicalOrdinal,
502 ReleaseBeforeEpoch,
503 NonMonotonicRelease,
504 UnreachableTransmitWatermark,
505 EmptyByteChunk,
506 NonContiguousInput,
507 NonContiguousOutput,
508 StreamOffsetOverflow,
509 InvalidErrno,
510 NonTerminalSocketError,
511 EventAfterTerminal,
512}
513
514impl fmt::Display for NetworkTraceValidationError {
515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516 write!(f, "invalid network trace: {self:?}")
517 }
518}
519
520impl Error for NetworkTraceValidationError {}
521
522#[derive(Debug)]
524pub enum NetworkTraceCodecError {
525 Io(io::Error),
526 Truncated,
527 BadMagic,
528 UnsupportedVersion(u32),
529 TooLarge,
530 TrailingData,
531 TrailingPayloadData,
532 Encode(bincode::error::EncodeError),
533 Decode(bincode::error::DecodeError),
534 Validation(NetworkTraceValidationError),
535}
536
537impl fmt::Display for NetworkTraceCodecError {
538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539 write!(f, "network trace codec error: {self:?}")
540 }
541}
542
543impl Error for NetworkTraceCodecError {}
544
545impl From<io::Error> for NetworkTraceCodecError {
546 fn from(error: io::Error) -> Self {
547 Self::Io(error)
548 }
549}
550
551impl From<NetworkTraceValidationError> for NetworkTraceCodecError {
552 fn from(error: NetworkTraceValidationError) -> Self {
553 Self::Validation(error)
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use std::io::Cursor;
560 use std::net::Ipv4Addr;
561 use std::time::Duration;
562
563 use chrono::TimeZone;
564
565 use super::*;
566 use crate::config::Config;
567 use crate::pid::DetTid;
568 use crate::time::GlobalTime;
569
570 fn channel_id() -> OpenFileId {
571 OpenFileId::new_socket(DetTid::from_raw(1), 0)
572 }
573
574 fn trace_epoch() -> DateTime<Utc> {
575 Utc.timestamp_opt(1_790_000_000, 0).unwrap()
576 }
577
578 fn global_time_after_epoch(nanos: u64) -> LogicalTime {
579 let config = Config {
580 epoch: trace_epoch(),
581 ..Config::default()
582 };
583 GlobalTime::new(&config).as_nanos() + LogicalTime::from_nanos(nanos)
584 }
585
586 fn valid_trace() -> NetworkTraceV1 {
587 let channel = channel_id();
588 NetworkTraceV1 {
589 epoch: trace_epoch(),
590 channels: vec![NetworkChannelV1 {
591 id: channel,
592 transport: NetworkTransportV1::Tcp,
593 role: NetworkEndpointRoleV1::OutboundClient,
594 local_address: NetworkAddressV1::Inet4 {
595 address: [10, 0, 0, 2],
596 port: 40_000,
597 },
598 peer_address: NetworkAddressV1::Inet4 {
599 address: [192, 0, 2, 10],
600 port: 443,
601 },
602 created_before_competing_threads: true,
603 }],
604 inputs: vec![
605 NetworkInputEventV1 {
606 ordinal: 0,
607 channel,
608 release: NetworkReleaseV1 {
609 not_before_global_time: global_time_after_epoch(10),
610 after_transmitted_offset: 3,
611 },
612 event: NetworkInputKindV1::InboundBytes {
613 stream_offset: 0,
614 bytes: b"response".to_vec(),
615 },
616 },
617 NetworkInputEventV1 {
618 ordinal: 1,
619 channel,
620 release: NetworkReleaseV1 {
621 not_before_global_time: global_time_after_epoch(20),
622 after_transmitted_offset: 3,
623 },
624 event: NetworkInputKindV1::PeerWriteClosed { stream_offset: 8 },
625 },
626 ],
627 outputs: vec![NetworkOutputV1 {
628 channel,
629 stream_offset: 0,
630 bytes: b"req".to_vec(),
631 }],
632 }
633 }
634
635 fn framed(trace: &NetworkTraceV1) -> Vec<u8> {
636 let mut bytes = Vec::new();
637 trace.write_framed(&mut bytes).unwrap();
638 bytes
639 }
640
641 fn framed_without_validation(trace: &NetworkTraceV1) -> Vec<u8> {
642 let payload = bincode::serde::encode_to_vec(trace, bincode::config::standard()).unwrap();
643 let mut bytes = Vec::new();
644 bytes.extend_from_slice(&NETWORK_TRACE_MAGIC);
645 bytes.extend_from_slice(&NETWORK_TRACE_VERSION_V1.to_le_bytes());
646 bytes.extend_from_slice(&(payload.len() as u64).to_le_bytes());
647 bytes.extend_from_slice(&payload);
648 bytes
649 }
650
651 #[test]
652 fn network_trace_v1_round_trips_exactly() {
653 let trace = valid_trace();
654 let decoded = NetworkTraceV1::read_framed(Cursor::new(framed(&trace))).unwrap();
655 assert_eq!(decoded, trace);
656 }
657
658 #[test]
659 fn transient_socket_errors_are_rejected_by_validation_writer_and_reader() {
660 assert_eq!(libc::EWOULDBLOCK, libc::EAGAIN);
661 for errno in [libc::EAGAIN, libc::EINTR] {
662 let mut trace = valid_trace();
663 trace.inputs[1].event = NetworkInputKindV1::SocketError {
664 stream_offset: 8,
665 errno,
666 };
667 assert_eq!(
668 trace.validate(),
669 Err(NetworkTraceValidationError::NonTerminalSocketError),
670 "transient errno {errno} cannot terminate the stream"
671 );
672
673 let mut output = b"existing output".to_vec();
674 assert!(matches!(
675 trace.write_framed(&mut output),
676 Err(NetworkTraceCodecError::Validation(
677 NetworkTraceValidationError::NonTerminalSocketError
678 ))
679 ));
680 assert_eq!(output, b"existing output");
681 assert!(matches!(
684 NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
685 Err(NetworkTraceCodecError::Validation(
686 NetworkTraceValidationError::NonTerminalSocketError
687 ))
688 ));
689 }
690 }
691
692 #[test]
693 fn terminal_socket_errors_preserve_framing_and_the_stream_boundary() {
694 for errno in [libc::ECONNRESET, libc::ETIMEDOUT] {
695 let mut trace = valid_trace();
696 trace.inputs[1].event = NetworkInputKindV1::SocketError {
697 stream_offset: 8,
698 errno,
699 };
700 assert_eq!(trace.validate(), Ok(()));
701 let bytes = framed(&trace);
702 assert_eq!(bytes, framed_without_validation(&trace));
703 assert_eq!(
704 NetworkTraceV1::read_framed(Cursor::new(bytes)).unwrap(),
705 trace
706 );
707
708 let mut late = trace.inputs[0].clone();
709 late.ordinal = 2;
710 late.release = trace.inputs[1].release;
711 late.event = NetworkInputKindV1::InboundBytes {
712 stream_offset: 8,
713 bytes: b"late".to_vec(),
714 };
715 trace.inputs.push(late);
716 assert_eq!(
717 trace.validate(),
718 Err(NetworkTraceValidationError::EventAfterTerminal)
719 );
720 }
721 }
722
723 #[test]
724 fn socket_errno_range_is_still_checked_before_framing() {
725 for errno in [-1, 0, 4096] {
726 let mut trace = valid_trace();
727 trace.inputs[1].event = NetworkInputKindV1::SocketError {
728 stream_offset: 8,
729 errno,
730 };
731 assert_eq!(
732 trace.validate(),
733 Err(NetworkTraceValidationError::InvalidErrno)
734 );
735 assert!(matches!(
736 trace.write_framed(Vec::new()),
737 Err(NetworkTraceCodecError::Validation(
738 NetworkTraceValidationError::InvalidErrno
739 ))
740 ));
741 assert!(matches!(
742 NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
743 Err(NetworkTraceCodecError::Validation(
744 NetworkTraceValidationError::InvalidErrno
745 ))
746 ));
747 }
748 }
749
750 #[test]
751 fn every_strict_prefix_is_reported_as_truncated() {
752 let bytes = framed(&valid_trace());
753 for end in 0..bytes.len() {
754 assert!(
755 matches!(
756 NetworkTraceV1::read_framed(Cursor::new(&bytes[..end])),
757 Err(NetworkTraceCodecError::Truncated)
758 ),
759 "prefix of length {end} was not rejected as truncation"
760 );
761 }
762 }
763
764 #[test]
765 fn unknown_versions_are_rejected_before_payload_decode() {
766 let mut bytes = framed(&valid_trace());
767 let start = NETWORK_TRACE_MAGIC.len();
768 bytes[start..start + 4].copy_from_slice(&2u32.to_le_bytes());
769 assert!(matches!(
770 NetworkTraceV1::read_framed(Cursor::new(bytes)),
771 Err(NetworkTraceCodecError::UnsupportedVersion(2))
772 ));
773 }
774
775 #[test]
776 fn oversized_length_is_rejected_before_allocation() {
777 let mut bytes = Vec::new();
778 bytes.extend_from_slice(&NETWORK_TRACE_MAGIC);
779 bytes.extend_from_slice(&NETWORK_TRACE_VERSION_V1.to_le_bytes());
780 bytes.extend_from_slice(&(MAX_NETWORK_TRACE_PAYLOAD_BYTES + 1).to_le_bytes());
781 assert!(matches!(
782 NetworkTraceV1::read_framed(Cursor::new(bytes)),
783 Err(NetworkTraceCodecError::TooLarge)
784 ));
785 }
786
787 #[test]
788 fn trailing_file_data_is_rejected() {
789 let mut bytes = framed(&valid_trace());
790 bytes.push(0);
791 assert!(matches!(
792 NetworkTraceV1::read_framed(Cursor::new(bytes)),
793 Err(NetworkTraceCodecError::TrailingData)
794 ));
795 }
796
797 #[test]
798 fn reader_and_writer_both_refuse_semantically_invalid_traces() {
799 let mut trace = valid_trace();
800 trace.inputs[0].ordinal = 7;
801
802 assert!(matches!(
803 trace.write_framed(Vec::new()),
804 Err(NetworkTraceCodecError::Validation(
805 NetworkTraceValidationError::NonCanonicalOrdinal
806 ))
807 ));
808 assert!(matches!(
809 NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
810 Err(NetworkTraceCodecError::Validation(
811 NetworkTraceValidationError::NonCanonicalOrdinal
812 ))
813 ));
814 }
815
816 #[test]
817 fn deserialized_pre_unix_epoch_is_a_typed_validation_error() {
818 let mut trace = valid_trace();
819 trace.epoch = Utc.timestamp_opt(-1, 0).unwrap();
820 assert!(matches!(
821 NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
822 Err(NetworkTraceCodecError::Validation(
823 NetworkTraceValidationError::EpochOutOfRange
824 ))
825 ));
826 }
827
828 #[test]
829 fn deserialized_epoch_beyond_global_time_is_a_typed_validation_error() {
830 let mut trace = valid_trace();
831 trace.epoch = Utc.with_ymd_and_hms(9999, 1, 1, 0, 0, 0).unwrap();
832 assert!(matches!(
833 NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
834 Err(NetworkTraceCodecError::Validation(
835 NetworkTraceValidationError::EpochOutOfRange
836 ))
837 ));
838 }
839
840 #[test]
841 fn config_is_off_by_default_and_perturbation_has_no_seed_fallback() {
842 let config = NetworkTraceConfig::default();
843 assert_eq!(config.mode, NetworkTraceMode::Off);
844 assert_eq!(config.network_perturb_seed, None);
845 assert_eq!(config.validate(), Ok(()));
846
847 let replay = NetworkTraceConfig {
848 mode: NetworkTraceMode::Replay,
849 path: Some("trace.net".into()),
850 network_perturb_seed: Some(17),
851 };
852 assert_eq!(replay.network_perturb_seed, Some(17));
853 assert_eq!(replay.validate(), Ok(()));
854
855 let encoded = serde_json::to_string(&replay).unwrap();
856 assert_eq!(
857 serde_json::from_str::<NetworkTraceConfig>(&encoded).unwrap(),
858 replay
859 );
860 assert!(
861 serde_json::from_str::<NetworkTraceConfig>(
862 r#"{"mode":"off","path":null,"network_perturb_seed":null,"unexpected":true}"#
863 )
864 .is_err()
865 );
866 }
867
868 #[test]
869 fn config_rejects_ambiguous_or_record_perturbation_combinations() {
870 assert_eq!(
871 NetworkTraceConfig {
872 path: Some("trace.net".into()),
873 ..NetworkTraceConfig::default()
874 }
875 .validate(),
876 Err(NetworkTraceConfigError::OptionsWhileOff)
877 );
878 assert_eq!(
879 NetworkTraceConfig {
880 mode: NetworkTraceMode::Replay,
881 ..NetworkTraceConfig::default()
882 }
883 .validate(),
884 Err(NetworkTraceConfigError::MissingPath)
885 );
886 assert_eq!(
887 NetworkTraceConfig {
888 mode: NetworkTraceMode::Record,
889 path: Some("trace.net".into()),
890 network_perturb_seed: Some(1),
891 }
892 .validate(),
893 Err(NetworkTraceConfigError::PerturbationDuringRecord)
894 );
895 }
896
897 #[test]
898 fn release_eligibility_uses_the_absolute_global_time_domain() {
899 let mut config = Config {
900 epoch: trace_epoch(),
901 ..Config::default()
902 };
903 config.sched_seed = Some(999);
905 let mut global_time = GlobalTime::new(&config);
906 let start = global_time.as_nanos();
907 assert_eq!(valid_trace().epoch_global_time(), Ok(start));
908 let release = NetworkReleaseV1 {
909 not_before_global_time: start + LogicalTime::from_nanos(10),
910 after_transmitted_offset: 3,
911 };
912
913 assert!(!release.is_eligible(global_time.as_nanos(), 3));
914 global_time.add_extra_time(Duration::from_nanos(10));
915 assert_eq!(global_time.as_nanos(), release.not_before_global_time);
916 assert!(!release.is_eligible(global_time.as_nanos(), 2));
917 assert!(release.is_eligible(global_time.as_nanos(), 3));
918 }
919
920 #[test]
921 fn v1_validation_rejects_channels_outside_the_supported_envelope() {
922 let mut trace = valid_trace();
923 trace.channels.push(trace.channels[0].clone());
924 assert_eq!(
925 trace.validate(),
926 Err(NetworkTraceValidationError::ChannelCount(2))
927 );
928
929 let mut trace = valid_trace();
930 let non_socket = OpenFileId::new(DetTid::from_raw(1), 0);
931 trace.channels[0].id = non_socket;
932 for input in &mut trace.inputs {
933 input.channel = non_socket;
934 }
935 for output in &mut trace.outputs {
936 output.channel = non_socket;
937 }
938 assert_eq!(
939 trace.validate(),
940 Err(NetworkTraceValidationError::NonSocketChannelIdentity)
941 );
942
943 let mut trace = valid_trace();
944 trace.channels[0].peer_address = NetworkAddressV1::Inet4 {
945 address: [127, 0, 0, 1],
946 port: 443,
947 };
948 assert_eq!(
949 trace.validate(),
950 Err(NetworkTraceValidationError::UnsupportedPeerAddress)
951 );
952
953 for mapped in [
954 Ipv4Addr::LOCALHOST,
955 Ipv4Addr::UNSPECIFIED,
956 Ipv4Addr::new(224, 0, 0, 1),
957 ] {
958 let mut trace = valid_trace();
959 trace.channels[0].local_address = NetworkAddressV1::Inet6 {
960 address: Ipv6Addr::LOCALHOST.octets(),
961 port: 40_000,
962 flowinfo: 0,
963 scope_id: 0,
964 };
965 trace.channels[0].peer_address = NetworkAddressV1::Inet6 {
966 address: mapped.to_ipv6_mapped().octets(),
967 port: 443,
968 flowinfo: 0,
969 scope_id: 0,
970 };
971 assert_eq!(
972 trace.validate(),
973 Err(NetworkTraceValidationError::UnsupportedPeerAddress),
974 "IPv4-mapped {mapped} escaped the IPv4 policy"
975 );
976 }
977
978 let mut trace = valid_trace();
979 trace.channels[0].created_before_competing_threads = false;
980 assert_eq!(
981 trace.validate(),
982 Err(NetworkTraceValidationError::ScheduleDependentChannelIdentity)
983 );
984 }
985
986 #[test]
987 fn v1_validation_rejects_noncanonical_streams_and_release_conditions() {
988 let mut trace = valid_trace();
989 trace.inputs[0].release.not_before_global_time = LogicalTime::ZERO;
990 assert_eq!(
991 trace.validate(),
992 Err(NetworkTraceValidationError::ReleaseBeforeEpoch)
993 );
994
995 let mut trace = valid_trace();
996 trace.inputs[0].ordinal = 1;
997 assert_eq!(
998 trace.validate(),
999 Err(NetworkTraceValidationError::NonCanonicalOrdinal)
1000 );
1001
1002 let mut trace = valid_trace();
1003 trace.inputs[0].release.after_transmitted_offset = 4;
1004 assert_eq!(
1005 trace.validate(),
1006 Err(NetworkTraceValidationError::UnreachableTransmitWatermark)
1007 );
1008
1009 let mut trace = valid_trace();
1010 trace.outputs[0].stream_offset = 1;
1011 assert_eq!(
1012 trace.validate(),
1013 Err(NetworkTraceValidationError::NonContiguousOutput)
1014 );
1015
1016 let mut trace = valid_trace();
1017 trace.inputs.push(NetworkInputEventV1 {
1018 ordinal: 2,
1019 channel: channel_id(),
1020 release: NetworkReleaseV1 {
1021 not_before_global_time: global_time_after_epoch(21),
1022 after_transmitted_offset: 3,
1023 },
1024 event: NetworkInputKindV1::InboundBytes {
1025 stream_offset: 8,
1026 bytes: b"late".to_vec(),
1027 },
1028 });
1029 assert_eq!(
1030 trace.validate(),
1031 Err(NetworkTraceValidationError::EventAfterTerminal)
1032 );
1033 }
1034}