Skip to main content

detcore_model/
network_trace.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Versioned data model for schedule-independent external network input.
10//!
11//! This module deliberately contains no recorder, replayer, or scheduler hook.
12//! It defines the fail-closed trace envelopes and their framing so the runtime
13//! integration cannot accidentally reuse the schedule-coupled syscall event
14//! stream. [`NetworkTraceV1`] is the original single-channel envelope;
15//! [`NetworkTraceV2`] is the multi-channel form the recorder writes, and its
16//! reader upgrades v1 traces. The v3 and v4 framings extend the v2 payload
17//! with refused sends and with send waits respectively. Merely constructing a [`NetworkTraceConfig`]
18//! does not enable any behavior.
19
20use std::collections::BTreeMap;
21use std::error::Error;
22use std::fmt;
23use std::io;
24use std::io::Read;
25use std::io::Write;
26use std::net::Ipv4Addr;
27use std::net::Ipv6Addr;
28use std::path::PathBuf;
29
30use chrono::DateTime;
31use chrono::Utc;
32use serde::Deserialize;
33use serde::Serialize;
34
35use crate::fd::OpenFileId;
36use crate::time::LogicalTime;
37
38/// The on-disk format magic. The version is stored in the following four bytes.
39pub const NETWORK_TRACE_MAGIC: [u8; 16] = *b"HERMIT-NET-TRACE";
40/// The first network trace format; still accepted and upgraded on read.
41pub const NETWORK_TRACE_VERSION_V1: u32 = 1;
42
43const FRAME_HEADER_LEN: usize = NETWORK_TRACE_MAGIC.len() + 4 + 8;
44
45/// Whether the future runtime integration records or replays external input.
46#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum NetworkTraceMode {
49    /// No network trace behavior. This is the default and has no runtime effect.
50    #[default]
51    Off,
52    /// Capture supported external TCP input in a new trace.
53    Record,
54    /// Replay a trace without consulting the host network.
55    Replay,
56}
57
58/// Configuration reserved for the future network recorder/replayer seam.
59///
60/// `network_perturb_seed` is intentionally optional and has no fallback to the
61/// scheduler or global seed. A future perturbation layer must consume only this
62/// seed so varying `sched_seed` cannot change the external input stream.
63#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct NetworkTraceConfig {
66    pub mode: NetworkTraceMode,
67    pub path: Option<PathBuf>,
68    pub network_perturb_seed: Option<u64>,
69}
70
71impl NetworkTraceConfig {
72    /// Validate configuration independently of any scheduler configuration.
73    pub fn validate(&self) -> Result<(), NetworkTraceConfigError> {
74        match self.mode {
75            NetworkTraceMode::Off => {
76                if self.path.is_some() || self.network_perturb_seed.is_some() {
77                    return Err(NetworkTraceConfigError::OptionsWhileOff);
78                }
79            }
80            NetworkTraceMode::Record => {
81                validate_trace_path(self.path.as_ref())?;
82                if self.network_perturb_seed.is_some() {
83                    return Err(NetworkTraceConfigError::PerturbationDuringRecord);
84                }
85            }
86            NetworkTraceMode::Replay => validate_trace_path(self.path.as_ref())?,
87        }
88        Ok(())
89    }
90}
91
92fn validate_trace_path(path: Option<&PathBuf>) -> Result<(), NetworkTraceConfigError> {
93    let Some(path) = path else {
94        return Err(NetworkTraceConfigError::MissingPath);
95    };
96    if path.as_os_str().is_empty() {
97        return Err(NetworkTraceConfigError::EmptyPath);
98    }
99    Ok(())
100}
101
102/// Invalid combinations in [`NetworkTraceConfig`].
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum NetworkTraceConfigError {
105    OptionsWhileOff,
106    MissingPath,
107    EmptyPath,
108    PerturbationDuringRecord,
109}
110
111impl fmt::Display for NetworkTraceConfigError {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            Self::OptionsWhileOff => {
115                write!(f, "network trace options require record or replay mode")
116            }
117            Self::MissingPath => write!(f, "network trace record/replay requires a path"),
118            Self::EmptyPath => write!(f, "network trace path must not be empty"),
119            Self::PerturbationDuringRecord => {
120                write!(f, "network perturbation is replay-only")
121            }
122        }
123    }
124}
125
126impl Error for NetworkTraceConfigError {}
127
128/// Internet address recorded without host-layout `sockaddr` padding.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case", deny_unknown_fields)]
131pub enum NetworkAddressV1 {
132    Inet4 {
133        address: [u8; 4],
134        port: u16,
135    },
136    Inet6 {
137        address: [u8; 16],
138        port: u16,
139        flowinfo: u32,
140        scope_id: u32,
141    },
142}
143
144impl NetworkAddressV1 {
145    fn is_supported_external_peer(&self) -> bool {
146        match self {
147            Self::Inet4 { address, port } => {
148                *port != 0 && is_supported_external_ipv4(Ipv4Addr::from(*address))
149            }
150            Self::Inet6 { address, port, .. } => {
151                let address = Ipv6Addr::from(*address);
152                *port != 0
153                    && match address.to_ipv4_mapped() {
154                        Some(mapped) => is_supported_external_ipv4(mapped),
155                        None => {
156                            !address.is_loopback()
157                                && !address.is_unspecified()
158                                && !address.is_multicast()
159                        }
160                    }
161            }
162        }
163    }
164
165    /// The port in host byte order.
166    pub fn port(&self) -> u16 {
167        match self {
168            Self::Inet4 { port, .. } | Self::Inet6 { port, .. } => *port,
169        }
170    }
171
172    /// An address no connection can have as its peer.
173    /// Whether a v2 trace can hold a connection to this peer: a nonzero port
174    /// and an address that names one host. The unspecified address, which
175    /// Linux connects to loopback, is excluded.
176    pub fn is_traceable_peer(&self) -> bool {
177        self.port() != 0 && !self.is_unroutable()
178    }
179
180    fn is_unroutable(&self) -> bool {
181        match self {
182            Self::Inet4 { address, .. } => {
183                let address = Ipv4Addr::from(*address);
184                address.is_unspecified() || address.is_multicast() || address.is_broadcast()
185            }
186            Self::Inet6 { address, .. } => {
187                let address = Ipv6Addr::from(*address);
188                // Linux connects an IPv4-mapped address over IPv4.
189                if let Some(mapped) = address.to_ipv4_mapped() {
190                    return mapped.is_unspecified()
191                        || mapped.is_multicast()
192                        || mapped.is_broadcast();
193                }
194                address.is_unspecified() || address.is_multicast()
195            }
196        }
197    }
198
199    fn same_family(&self, other: &Self) -> bool {
200        matches!(
201            (self, other),
202            (Self::Inet4 { .. }, Self::Inet4 { .. }) | (Self::Inet6 { .. }, Self::Inet6 { .. })
203        )
204    }
205}
206
207fn is_supported_external_ipv4(address: Ipv4Addr) -> bool {
208    !address.is_loopback()
209        && !address.is_unspecified()
210        && !address.is_multicast()
211        && !address.is_broadcast()
212}
213
214/// Transport admitted by the v1 trace envelope.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum NetworkTransportV1 {
218    Tcp,
219}
220
221/// Connection role admitted by v1. Server-side `accept` needs a different
222/// identity and arrival model and is intentionally not representable.
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum NetworkEndpointRoleV1 {
226    OutboundClient,
227}
228
229/// The only channel supported by v1: one outbound TCP client connection.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(deny_unknown_fields)]
232pub struct NetworkChannelV1 {
233    /// Stable Detcore open-file-description identity, never a raw fd.
234    pub id: OpenFileId,
235    pub transport: NetworkTransportV1,
236    pub role: NetworkEndpointRoleV1,
237    pub local_address: NetworkAddressV1,
238    pub peer_address: NetworkAddressV1,
239    /// The recorder must prove socket allocation occurred before competing
240    /// guest threads could make this identity schedule-dependent.
241    pub created_before_competing_threads: bool,
242}
243
244/// Conditions that must both hold before an input becomes observable.
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(deny_unknown_fields)]
247pub struct NetworkReleaseV1 {
248    /// Absolute time in exactly the domain returned by `GlobalTime::as_nanos`.
249    /// It includes the trace epoch; it is not a duration since that epoch.
250    pub not_before_global_time: LogicalTime,
251    /// Number of validated outbound bytes required before release.
252    pub after_transmitted_offset: u64,
253}
254
255impl NetworkReleaseV1 {
256    /// Whether both scheduler-owned eligibility conditions have been met.
257    pub fn is_eligible(
258        &self,
259        committed_global_time: LogicalTime,
260        validated_transmitted_offset: u64,
261    ) -> bool {
262        committed_global_time >= self.not_before_global_time
263            && validated_transmitted_offset >= self.after_transmitted_offset
264    }
265}
266
267/// Supported external observations for the initial TCP byte-stream model.
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case", deny_unknown_fields)]
270pub enum NetworkInputKindV1 {
271    InboundBytes {
272        stream_offset: u64,
273        bytes: Vec<u8>,
274    },
275    PeerWriteClosed {
276        stream_offset: u64,
277    },
278    /// A terminal stream error, not a retryable or interrupted receive attempt.
279    SocketError {
280        stream_offset: u64,
281        errno: i32,
282    },
283}
284
285/// One globally ordered external observation.
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub struct NetworkInputEventV1 {
289    pub ordinal: u64,
290    pub channel: OpenFileId,
291    pub release: NetworkReleaseV1,
292    pub event: NetworkInputKindV1,
293}
294
295/// One contiguous recorded fragment of the expected outbound TCP byte stream.
296///
297/// From v3 on, a fragment with no bytes records a nonblocking send that
298/// Linux refused with `EAGAIN` when the stream had reached `stream_offset`.
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(deny_unknown_fields)]
301pub struct NetworkOutputV1 {
302    pub channel: OpenFileId,
303    pub stream_offset: u64,
304    pub bytes: Vec<u8>,
305}
306
307/// How long a blocking send waited for buffer space before Linux accepted
308/// one fragment of it.
309///
310/// Replay hands the fragment to the guest only after the same send has
311/// waited `waits` times and global time has reached `not_before_global_time`,
312/// so the send returns at the same point of the guest's execution as it did
313/// in the recording.
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(deny_unknown_fields)]
316pub struct NetworkSendWaitV4 {
317    /// Waits for buffer space since the send started or since its previous
318    /// accepted fragment, whichever came later; never zero.
319    pub waits: u64,
320    /// The global time at which the recording accepted the fragment.
321    pub not_before_global_time: LogicalTime,
322}
323
324/// One recorded outbound fragment, as a v4 trace holds it: a v1 fragment
325/// and, when a blocking send waited before Linux accepted it, that wait.
326/// Fragments of earlier versions have no wait.
327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct NetworkOutputV4 {
330    pub channel: OpenFileId,
331    pub stream_offset: u64,
332    pub bytes: Vec<u8>,
333    pub wait: Option<NetworkSendWaitV4>,
334}
335
336impl From<NetworkOutputV1> for NetworkOutputV4 {
337    fn from(output: NetworkOutputV1) -> Self {
338        Self {
339            channel: output.channel,
340            stream_offset: output.stream_offset,
341            bytes: output.bytes,
342            wait: None,
343        }
344    }
345}
346
347/// Version-one schedule-independent network input trace.
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(deny_unknown_fields)]
350pub struct NetworkTraceV1 {
351    pub epoch: DateTime<Utc>,
352    pub channels: Vec<NetworkChannelV1>,
353    pub inputs: Vec<NetworkInputEventV1>,
354    pub outputs: Vec<NetworkOutputV1>,
355}
356
357impl NetworkTraceV1 {
358    /// Convert the recorded epoch to the absolute starting time used by
359    /// `GlobalTime`, rejecting Chrono values outside its `u64` nanosecond
360    /// domain. `GlobalTime` truncates the epoch to microseconds, so this does
361    /// the same conversion without an unchecked signed cast or multiplication.
362    pub fn epoch_global_time(&self) -> Result<LogicalTime, NetworkTraceValidationError> {
363        epoch_global_time(&self.epoch)
364    }
365
366    /// Enforce the narrow v1 envelope before either writing or consuming a trace.
367    pub fn validate(&self) -> Result<(), NetworkTraceValidationError> {
368        if self.channels.len() != 1 {
369            return Err(NetworkTraceValidationError::ChannelCount(
370                self.channels.len(),
371            ));
372        }
373        let channel = &self.channels[0];
374        if !channel.id.is_socket() {
375            return Err(NetworkTraceValidationError::NonSocketChannelIdentity);
376        }
377        if !channel.created_before_competing_threads {
378            return Err(NetworkTraceValidationError::ScheduleDependentChannelIdentity);
379        }
380        if !channel.local_address.same_family(&channel.peer_address) {
381            return Err(NetworkTraceValidationError::AddressFamilyMismatch);
382        }
383        if !channel.peer_address.is_supported_external_peer() {
384            return Err(NetworkTraceValidationError::UnsupportedPeerAddress);
385        }
386
387        let mut inbound_offset = 0u64;
388        let epoch_start = self.epoch_global_time()?;
389        let mut previous_time = epoch_start;
390        let mut previous_tx_watermark = 0u64;
391        let mut terminal = false;
392
393        let total_output = validate_outputs(&self.outputs, channel.id)?;
394        for (index, input) in self.inputs.iter().enumerate() {
395            if input.ordinal != index as u64 {
396                return Err(NetworkTraceValidationError::NonCanonicalOrdinal);
397            }
398            if input.channel != channel.id {
399                return Err(NetworkTraceValidationError::UnknownChannel);
400            }
401            if input.release.not_before_global_time < epoch_start {
402                return Err(NetworkTraceValidationError::ReleaseBeforeEpoch);
403            }
404            if input.release.not_before_global_time < previous_time
405                || input.release.after_transmitted_offset < previous_tx_watermark
406            {
407                return Err(NetworkTraceValidationError::NonMonotonicRelease);
408            }
409            if input.release.after_transmitted_offset > total_output {
410                return Err(NetworkTraceValidationError::UnreachableTransmitWatermark);
411            }
412            if terminal {
413                return Err(NetworkTraceValidationError::EventAfterTerminal);
414            }
415
416            match &input.event {
417                NetworkInputKindV1::InboundBytes {
418                    stream_offset,
419                    bytes,
420                } => {
421                    if bytes.is_empty() {
422                        return Err(NetworkTraceValidationError::EmptyByteChunk);
423                    }
424                    if *stream_offset != inbound_offset {
425                        return Err(NetworkTraceValidationError::NonContiguousInput);
426                    }
427                    inbound_offset = inbound_offset
428                        .checked_add(bytes.len() as u64)
429                        .ok_or(NetworkTraceValidationError::StreamOffsetOverflow)?;
430                }
431                NetworkInputKindV1::PeerWriteClosed { stream_offset } => {
432                    if *stream_offset != inbound_offset {
433                        return Err(NetworkTraceValidationError::NonContiguousInput);
434                    }
435                    terminal = true;
436                }
437                NetworkInputKindV1::SocketError {
438                    stream_offset,
439                    errno,
440                } => {
441                    if *stream_offset != inbound_offset {
442                        return Err(NetworkTraceValidationError::NonContiguousInput);
443                    }
444                    if !(1..=4095).contains(errno) {
445                        return Err(NetworkTraceValidationError::InvalidErrno);
446                    }
447                    // EWOULDBLOCK equals EAGAIN on the supported Linux hosts.
448                    // Neither readiness nor signal interruption terminates the
449                    // peer's byte stream, so neither belongs in this variant.
450                    if matches!(*errno, libc::EAGAIN | libc::EINTR) {
451                        return Err(NetworkTraceValidationError::NonTerminalSocketError);
452                    }
453                    terminal = true;
454                }
455            }
456            previous_time = input.release.not_before_global_time;
457            previous_tx_watermark = input.release.after_transmitted_offset;
458        }
459        Ok(())
460    }
461
462    /// Write one complete, length-delimited v1 trace.
463    pub fn write_framed<W: Write>(&self, writer: W) -> Result<(), NetworkTraceCodecError> {
464        self.validate()?;
465        write_frame(writer, NETWORK_TRACE_VERSION_V1, self)
466    }
467
468    /// Read exactly one complete trace, rejecting truncation, trailing bytes,
469    /// unknown versions, malformed payloads, and invalid v1 semantics.
470    pub fn read_framed<R: Read>(reader: R) -> Result<Self, NetworkTraceCodecError> {
471        let (version, payload) = read_frame(reader)?;
472        if version != NETWORK_TRACE_VERSION_V1 {
473            return Err(NetworkTraceCodecError::UnsupportedVersion(version));
474        }
475        let trace: Self = decode_payload(&payload)?;
476        trace.validate()?;
477        Ok(trace)
478    }
479}
480
481/// Read one frame header and its payload, rejecting truncation and trailing bytes.
482fn read_frame<R: Read>(mut reader: R) -> Result<(u32, Vec<u8>), NetworkTraceCodecError> {
483    let mut header = [0u8; FRAME_HEADER_LEN];
484    read_exact_or_truncated(&mut reader, &mut header)?;
485    if header[..NETWORK_TRACE_MAGIC.len()] != NETWORK_TRACE_MAGIC {
486        return Err(NetworkTraceCodecError::BadMagic);
487    }
488    let version_start = NETWORK_TRACE_MAGIC.len();
489    let version = u32::from_le_bytes(
490        header[version_start..version_start + 4]
491            .try_into()
492            .expect("fixed-size version field"),
493    );
494    if ![
495        NETWORK_TRACE_VERSION_V1,
496        NETWORK_TRACE_VERSION_V2,
497        NETWORK_TRACE_VERSION_V3,
498        NETWORK_TRACE_VERSION_V4,
499    ]
500    .contains(&version)
501    {
502        return Err(NetworkTraceCodecError::UnsupportedVersion(version));
503    }
504    let len_start = version_start + 4;
505    let payload_len = u64::from_le_bytes(
506        header[len_start..len_start + 8]
507            .try_into()
508            .expect("fixed-size length field"),
509    );
510    // Grow the payload only as its bytes arrive, so a corrupt or hostile
511    // length header cannot make the reader allocate more than the input holds.
512    let mut payload = Vec::new();
513    let read = reader
514        .by_ref()
515        .take(payload_len)
516        .read_to_end(&mut payload)?;
517    if read as u64 != payload_len {
518        return Err(NetworkTraceCodecError::Truncated);
519    }
520    let mut trailing = [0u8; 1];
521    if reader.read(&mut trailing)? != 0 {
522        return Err(NetworkTraceCodecError::TrailingData);
523    }
524    Ok((version, payload))
525}
526
527fn decode_payload<T: serde::de::DeserializeOwned>(
528    payload: &[u8],
529) -> Result<T, NetworkTraceCodecError> {
530    let (value, consumed): (T, usize) =
531        bincode::serde::decode_from_slice(payload, bincode::config::standard())
532            .map_err(NetworkTraceCodecError::Decode)?;
533    if consumed != payload.len() {
534        return Err(NetworkTraceCodecError::TrailingPayloadData);
535    }
536    Ok(value)
537}
538
539/// Encode one complete frame: the header, then the payload, in one buffer.
540fn encode_frame<T: Serialize>(version: u32, value: &T) -> Result<Vec<u8>, NetworkTraceCodecError> {
541    let mut frame = Vec::with_capacity(FRAME_HEADER_LEN);
542    frame.extend_from_slice(&NETWORK_TRACE_MAGIC);
543    frame.extend_from_slice(&version.to_le_bytes());
544    frame.extend_from_slice(&[0; 8]);
545    bincode::serde::encode_into_std_write(value, &mut frame, bincode::config::standard())
546        .map_err(NetworkTraceCodecError::Encode)?;
547    let payload_len = (frame.len() - FRAME_HEADER_LEN) as u64;
548    frame[FRAME_HEADER_LEN - 8..FRAME_HEADER_LEN].copy_from_slice(&payload_len.to_le_bytes());
549    Ok(frame)
550}
551
552fn write_frame<W: Write, T: Serialize>(
553    mut writer: W,
554    version: u32,
555    value: &T,
556) -> Result<(), NetworkTraceCodecError> {
557    writer.write_all(&encode_frame(version, value)?)?;
558    Ok(())
559}
560
561/// The trace format written by the runtime recorder when no nonblocking send
562/// was refused.
563pub const NETWORK_TRACE_VERSION_V2: u32 = 2;
564/// The v2 format, extended with empty outbound fragments that record a
565/// nonblocking send refused with `EAGAIN`. The recorder writes it only for a
566/// trace that holds one, so a reader that predates it rejects only those.
567pub const NETWORK_TRACE_VERSION_V3: u32 = 3;
568/// The v3 format, extended with the wait of each outbound fragment that a
569/// blocking send waited for. The recorder writes it only for a trace that
570/// holds one.
571pub const NETWORK_TRACE_VERSION_V4: u32 = 4;
572
573/// One outbound TCP client connection in a v2 trace.
574///
575/// Unlike v1, a v2 trace may hold several channels, may name a loopback peer
576/// (the recording ran with explicit host networking, so a host-loopback
577/// server is as external as any other), and records the connect outcome so a
578/// replay can reproduce a refused connection without touching the host.
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(deny_unknown_fields)]
581pub struct NetworkChannelV2 {
582    /// Stable Detcore open-file-description identity, never a raw fd.
583    pub id: OpenFileId,
584    pub transport: NetworkTransportV1,
585    pub role: NetworkEndpointRoleV1,
586    /// The local address the host assigned, or `None` when connect failed.
587    pub local_address: Option<NetworkAddressV1>,
588    pub peer_address: NetworkAddressV1,
589    /// `0` when the connection was established, otherwise the positive errno
590    /// the guest observed for it.
591    pub connect_errno: i32,
592}
593
594/// Version-two schedule-independent network trace: v1's release model over
595/// several channels.
596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
597#[serde(deny_unknown_fields)]
598pub struct NetworkTraceV2 {
599    pub epoch: DateTime<Utc>,
600    pub channels: Vec<NetworkChannelV2>,
601    /// Inputs of every channel, in the global order they were observed.
602    pub inputs: Vec<NetworkInputEventV1>,
603    /// Outbound fragments of every channel, in the order they were sent.
604    pub outputs: Vec<NetworkOutputV4>,
605}
606
607/// The v2 and v3 payload: [`NetworkTraceV2`] before fragments could carry a
608/// wait. Decoding through it is what keeps a wait out of a v2 or v3 trace.
609#[derive(Serialize, Deserialize)]
610#[serde(deny_unknown_fields)]
611struct NetworkTraceV2Wire {
612    epoch: DateTime<Utc>,
613    channels: Vec<NetworkChannelV2>,
614    inputs: Vec<NetworkInputEventV1>,
615    outputs: Vec<NetworkOutputV1>,
616}
617
618impl From<NetworkTraceV2Wire> for NetworkTraceV2 {
619    fn from(trace: NetworkTraceV2Wire) -> Self {
620        Self {
621            epoch: trace.epoch,
622            channels: trace.channels,
623            inputs: trace.inputs,
624            outputs: trace.outputs.into_iter().map(Into::into).collect(),
625        }
626    }
627}
628
629impl From<NetworkTraceV1> for NetworkTraceV2 {
630    fn from(trace: NetworkTraceV1) -> Self {
631        Self {
632            epoch: trace.epoch,
633            channels: trace
634                .channels
635                .into_iter()
636                .map(|channel| NetworkChannelV2 {
637                    id: channel.id,
638                    transport: channel.transport,
639                    role: channel.role,
640                    local_address: Some(channel.local_address),
641                    peer_address: channel.peer_address,
642                    connect_errno: 0,
643                })
644                .collect(),
645            inputs: trace.inputs,
646            outputs: trace.outputs.into_iter().map(Into::into).collect(),
647        }
648    }
649}
650
651impl NetworkTraceV2 {
652    /// Enforce the v2 envelope before either writing or consuming a trace.
653    pub fn validate(&self) -> Result<(), NetworkTraceValidationError> {
654        let epoch_start = epoch_global_time(&self.epoch)?;
655        let mut channels = BTreeMap::new();
656        for channel in &self.channels {
657            if !channel.id.is_socket() {
658                return Err(NetworkTraceValidationError::NonSocketChannelIdentity);
659            }
660            if !channel.peer_address.is_traceable_peer() {
661                return Err(NetworkTraceValidationError::UnsupportedPeerAddress);
662            }
663            if let Some(local) = &channel.local_address
664                && !local.same_family(&channel.peer_address)
665            {
666                return Err(NetworkTraceValidationError::AddressFamilyMismatch);
667            }
668            if !(0..=4095).contains(&channel.connect_errno)
669                || (channel.connect_errno == 0) != channel.local_address.is_some()
670            {
671                return Err(NetworkTraceValidationError::InvalidErrno);
672            }
673            if channels.insert(channel.id, channel).is_some() {
674                return Err(NetworkTraceValidationError::DuplicateChannel);
675            }
676        }
677
678        let mut total_output: BTreeMap<OpenFileId, u64> = BTreeMap::new();
679        let mut last_wait: BTreeMap<OpenFileId, LogicalTime> = BTreeMap::new();
680        for output in &self.outputs {
681            let channel = channels
682                .get(&output.channel)
683                .ok_or(NetworkTraceValidationError::UnknownChannel)?;
684            if channel.connect_errno != 0 {
685                return Err(NetworkTraceValidationError::TrafficOnFailedChannel);
686            }
687            let offset = total_output.entry(output.channel).or_default();
688            if output.stream_offset != *offset {
689                return Err(NetworkTraceValidationError::NonContiguousOutput);
690            }
691            *offset = offset
692                .checked_add(output.bytes.len() as u64)
693                .ok_or(NetworkTraceValidationError::StreamOffsetOverflow)?;
694            if let Some(wait) = &output.wait {
695                // A refused send returned without waiting, so it has no wait.
696                if output.bytes.is_empty() {
697                    return Err(NetworkTraceValidationError::WaitOnRefusedSend);
698                }
699                if wait.waits == 0 {
700                    return Err(NetworkTraceValidationError::ZeroSendWaits);
701                }
702                if wait.not_before_global_time < epoch_start {
703                    return Err(NetworkTraceValidationError::SendWaitBeforeEpoch);
704                }
705                let previous = last_wait.entry(output.channel).or_insert(epoch_start);
706                if wait.not_before_global_time < *previous {
707                    return Err(NetworkTraceValidationError::NonMonotonicSendWait);
708                }
709                *previous = wait.not_before_global_time;
710            }
711        }
712
713        #[derive(Default)]
714        struct InboundCursor {
715            offset: u64,
716            tx_watermark: u64,
717            terminal: bool,
718        }
719        let mut cursors: BTreeMap<OpenFileId, InboundCursor> = BTreeMap::new();
720        let mut previous_time = epoch_start;
721        for (index, input) in self.inputs.iter().enumerate() {
722            if input.ordinal != index as u64 {
723                return Err(NetworkTraceValidationError::NonCanonicalOrdinal);
724            }
725            let channel = channels
726                .get(&input.channel)
727                .ok_or(NetworkTraceValidationError::UnknownChannel)?;
728            if channel.connect_errno != 0 {
729                return Err(NetworkTraceValidationError::TrafficOnFailedChannel);
730            }
731            if input.release.not_before_global_time < epoch_start {
732                return Err(NetworkTraceValidationError::ReleaseBeforeEpoch);
733            }
734            let cursor = cursors.entry(input.channel).or_default();
735            if input.release.not_before_global_time < previous_time
736                || input.release.after_transmitted_offset < cursor.tx_watermark
737            {
738                return Err(NetworkTraceValidationError::NonMonotonicRelease);
739            }
740            if input.release.after_transmitted_offset
741                > total_output.get(&input.channel).copied().unwrap_or(0)
742            {
743                return Err(NetworkTraceValidationError::UnreachableTransmitWatermark);
744            }
745            if cursor.terminal {
746                return Err(NetworkTraceValidationError::EventAfterTerminal);
747            }
748            cursor.offset = validate_input_kind(&input.event, cursor.offset)?;
749            cursor.terminal = !matches!(input.event, NetworkInputKindV1::InboundBytes { .. });
750            cursor.tx_watermark = input.release.after_transmitted_offset;
751            previous_time = input.release.not_before_global_time;
752        }
753        Ok(())
754    }
755
756    /// Whether the trace records a nonblocking send refused with `EAGAIN`,
757    /// which only v3 and v4 can hold.
758    pub fn records_refused_sends(&self) -> bool {
759        self.outputs.iter().any(|output| output.bytes.is_empty())
760    }
761
762    /// Whether any outbound fragment records a wait, which only v4 can hold.
763    pub fn records_send_waits(&self) -> bool {
764        self.outputs.iter().any(|output| output.wait.is_some())
765    }
766
767    /// Write one complete, length-delimited trace: v4 when it records a send
768    /// wait, otherwise v3 when it records a refused send, otherwise v2.
769    pub fn write_framed<W: Write>(&self, mut writer: W) -> Result<(), NetworkTraceCodecError> {
770        writer.write_all(&self.encode_framed()?)?;
771        Ok(())
772    }
773
774    /// The bytes [`Self::write_framed`] writes, encoded without touching any
775    /// output, so a caller can refuse an invalid trace before it truncates a file.
776    pub fn encode_framed(&self) -> Result<Vec<u8>, NetworkTraceCodecError> {
777        self.validate()?;
778        if self.records_send_waits() {
779            return encode_frame(NETWORK_TRACE_VERSION_V4, self);
780        }
781        let version = if self.records_refused_sends() {
782            NETWORK_TRACE_VERSION_V3
783        } else {
784            NETWORK_TRACE_VERSION_V2
785        };
786        let wire = NetworkTraceV2Wire {
787            epoch: self.epoch,
788            channels: self.channels.clone(),
789            inputs: self.inputs.clone(),
790            outputs: self
791                .outputs
792                .iter()
793                .map(|output| NetworkOutputV1 {
794                    channel: output.channel,
795                    stream_offset: output.stream_offset,
796                    bytes: output.bytes.clone(),
797                })
798                .collect(),
799        };
800        encode_frame(version, &wire)
801    }
802
803    /// Read exactly one complete v1, v2, v3 or v4 trace. A v1 trace is
804    /// upgraded to the equivalent v2 trace after passing v1 validation; a v2
805    /// trace may not hold the refused sends that v3 added, and only a v4
806    /// trace can hold a send wait.
807    pub fn read_framed<R: Read>(reader: R) -> Result<Self, NetworkTraceCodecError> {
808        let (version, payload) = read_frame(reader)?;
809        let trace = match version {
810            NETWORK_TRACE_VERSION_V1 => {
811                let trace: NetworkTraceV1 = decode_payload(&payload)?;
812                trace.validate()?;
813                Self::from(trace)
814            }
815            NETWORK_TRACE_VERSION_V4 => decode_payload(&payload)?,
816            _ => Self::from(decode_payload::<NetworkTraceV2Wire>(&payload)?),
817        };
818        if version < NETWORK_TRACE_VERSION_V3 && trace.records_refused_sends() {
819            return Err(NetworkTraceValidationError::EmptyByteChunk.into());
820        }
821        trace.validate()?;
822        Ok(trace)
823    }
824}
825
826/// Check one input against its channel's inbound cursor and return the next offset.
827fn validate_input_kind(
828    event: &NetworkInputKindV1,
829    offset: u64,
830) -> Result<u64, NetworkTraceValidationError> {
831    match event {
832        NetworkInputKindV1::InboundBytes {
833            stream_offset,
834            bytes,
835        } => {
836            if bytes.is_empty() {
837                return Err(NetworkTraceValidationError::EmptyByteChunk);
838            }
839            if *stream_offset != offset {
840                return Err(NetworkTraceValidationError::NonContiguousInput);
841            }
842            offset
843                .checked_add(bytes.len() as u64)
844                .ok_or(NetworkTraceValidationError::StreamOffsetOverflow)
845        }
846        NetworkInputKindV1::PeerWriteClosed { stream_offset } => {
847            if *stream_offset != offset {
848                return Err(NetworkTraceValidationError::NonContiguousInput);
849            }
850            Ok(offset)
851        }
852        NetworkInputKindV1::SocketError {
853            stream_offset,
854            errno,
855        } => {
856            if *stream_offset != offset {
857                return Err(NetworkTraceValidationError::NonContiguousInput);
858            }
859            if !(1..=4095).contains(errno) {
860                return Err(NetworkTraceValidationError::InvalidErrno);
861            }
862            if matches!(*errno, libc::EAGAIN | libc::EINTR) {
863                return Err(NetworkTraceValidationError::NonTerminalSocketError);
864            }
865            Ok(offset)
866        }
867    }
868}
869
870/// The absolute `GlobalTime` starting point for `epoch`; see
871/// [`NetworkTraceV1::epoch_global_time`].
872pub fn epoch_global_time(
873    epoch: &DateTime<Utc>,
874) -> Result<LogicalTime, NetworkTraceValidationError> {
875    let seconds = u64::try_from(epoch.timestamp())
876        .map_err(|_| NetworkTraceValidationError::EpochOutOfRange)?;
877    let whole_seconds = seconds
878        .checked_mul(1_000_000_000)
879        .ok_or(NetworkTraceValidationError::EpochOutOfRange)?;
880    let fractional_micros = u64::from(epoch.timestamp_subsec_micros())
881        .checked_mul(1_000)
882        .ok_or(NetworkTraceValidationError::EpochOutOfRange)?;
883    whole_seconds
884        .checked_add(fractional_micros)
885        .map(LogicalTime::from_nanos)
886        .ok_or(NetworkTraceValidationError::EpochOutOfRange)
887}
888
889fn validate_outputs(
890    outputs: &[NetworkOutputV1],
891    channel: OpenFileId,
892) -> Result<u64, NetworkTraceValidationError> {
893    let mut expected_offset = 0u64;
894    for output in outputs {
895        if output.channel != channel {
896            return Err(NetworkTraceValidationError::UnknownChannel);
897        }
898        if output.bytes.is_empty() {
899            return Err(NetworkTraceValidationError::EmptyByteChunk);
900        }
901        if output.stream_offset != expected_offset {
902            return Err(NetworkTraceValidationError::NonContiguousOutput);
903        }
904        expected_offset = expected_offset
905            .checked_add(output.bytes.len() as u64)
906            .ok_or(NetworkTraceValidationError::StreamOffsetOverflow)?;
907    }
908    Ok(expected_offset)
909}
910
911fn read_exact_or_truncated<R: Read>(
912    reader: &mut R,
913    bytes: &mut [u8],
914) -> Result<(), NetworkTraceCodecError> {
915    reader.read_exact(bytes).map_err(|error| {
916        if error.kind() == io::ErrorKind::UnexpectedEof {
917            NetworkTraceCodecError::Truncated
918        } else {
919            NetworkTraceCodecError::Io(error)
920        }
921    })
922}
923
924/// Semantically invalid trace.
925#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
926pub enum NetworkTraceValidationError {
927    ChannelCount(usize),
928    EpochOutOfRange,
929    NonSocketChannelIdentity,
930    ScheduleDependentChannelIdentity,
931    AddressFamilyMismatch,
932    UnsupportedPeerAddress,
933    UnknownChannel,
934    NonCanonicalOrdinal,
935    ReleaseBeforeEpoch,
936    NonMonotonicRelease,
937    UnreachableTransmitWatermark,
938    EmptyByteChunk,
939    NonContiguousInput,
940    NonContiguousOutput,
941    StreamOffsetOverflow,
942    InvalidErrno,
943    NonTerminalSocketError,
944    EventAfterTerminal,
945    DuplicateChannel,
946    TrafficOnFailedChannel,
947    WaitOnRefusedSend,
948    ZeroSendWaits,
949    SendWaitBeforeEpoch,
950    NonMonotonicSendWait,
951}
952
953impl fmt::Display for NetworkTraceValidationError {
954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
955        write!(f, "invalid network trace: {self:?}")
956    }
957}
958
959impl Error for NetworkTraceValidationError {}
960
961/// Failure to decode or encode the framed v1 representation.
962#[derive(Debug)]
963pub enum NetworkTraceCodecError {
964    Io(io::Error),
965    Truncated,
966    BadMagic,
967    UnsupportedVersion(u32),
968    TrailingData,
969    TrailingPayloadData,
970    Encode(bincode::error::EncodeError),
971    Decode(bincode::error::DecodeError),
972    Validation(NetworkTraceValidationError),
973}
974
975impl fmt::Display for NetworkTraceCodecError {
976    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
977        write!(f, "network trace codec error: {self:?}")
978    }
979}
980
981impl Error for NetworkTraceCodecError {}
982
983impl From<io::Error> for NetworkTraceCodecError {
984    fn from(error: io::Error) -> Self {
985        Self::Io(error)
986    }
987}
988
989impl From<NetworkTraceValidationError> for NetworkTraceCodecError {
990    fn from(error: NetworkTraceValidationError) -> Self {
991        Self::Validation(error)
992    }
993}
994
995#[cfg(test)]
996mod tests {
997    use std::io::Cursor;
998    use std::net::Ipv4Addr;
999    use std::time::Duration;
1000
1001    use chrono::TimeZone;
1002
1003    use super::*;
1004    use crate::config::Config;
1005    use crate::pid::DetTid;
1006    use crate::time::GlobalTime;
1007
1008    fn channel_id() -> OpenFileId {
1009        OpenFileId::new_socket(DetTid::from_raw(1), 0)
1010    }
1011
1012    fn trace_epoch() -> DateTime<Utc> {
1013        Utc.timestamp_opt(1_790_000_000, 0).unwrap()
1014    }
1015
1016    fn global_time_after_epoch(nanos: u64) -> LogicalTime {
1017        let config = Config {
1018            epoch: trace_epoch(),
1019            ..Config::default()
1020        };
1021        GlobalTime::new(&config).as_nanos() + LogicalTime::from_nanos(nanos)
1022    }
1023
1024    fn valid_trace() -> NetworkTraceV1 {
1025        let channel = channel_id();
1026        NetworkTraceV1 {
1027            epoch: trace_epoch(),
1028            channels: vec![NetworkChannelV1 {
1029                id: channel,
1030                transport: NetworkTransportV1::Tcp,
1031                role: NetworkEndpointRoleV1::OutboundClient,
1032                local_address: NetworkAddressV1::Inet4 {
1033                    address: [10, 0, 0, 2],
1034                    port: 40_000,
1035                },
1036                peer_address: NetworkAddressV1::Inet4 {
1037                    address: [192, 0, 2, 10],
1038                    port: 443,
1039                },
1040                created_before_competing_threads: true,
1041            }],
1042            inputs: vec![
1043                NetworkInputEventV1 {
1044                    ordinal: 0,
1045                    channel,
1046                    release: NetworkReleaseV1 {
1047                        not_before_global_time: global_time_after_epoch(10),
1048                        after_transmitted_offset: 3,
1049                    },
1050                    event: NetworkInputKindV1::InboundBytes {
1051                        stream_offset: 0,
1052                        bytes: b"response".to_vec(),
1053                    },
1054                },
1055                NetworkInputEventV1 {
1056                    ordinal: 1,
1057                    channel,
1058                    release: NetworkReleaseV1 {
1059                        not_before_global_time: global_time_after_epoch(20),
1060                        after_transmitted_offset: 3,
1061                    },
1062                    event: NetworkInputKindV1::PeerWriteClosed { stream_offset: 8 },
1063                },
1064            ],
1065            outputs: vec![NetworkOutputV1 {
1066                channel,
1067                stream_offset: 0,
1068                bytes: b"req".to_vec(),
1069            }],
1070        }
1071    }
1072
1073    fn framed(trace: &NetworkTraceV1) -> Vec<u8> {
1074        let mut bytes = Vec::new();
1075        trace.write_framed(&mut bytes).unwrap();
1076        bytes
1077    }
1078
1079    fn framed_without_validation(trace: &NetworkTraceV1) -> Vec<u8> {
1080        let payload = bincode::serde::encode_to_vec(trace, bincode::config::standard()).unwrap();
1081        let mut bytes = Vec::new();
1082        bytes.extend_from_slice(&NETWORK_TRACE_MAGIC);
1083        bytes.extend_from_slice(&NETWORK_TRACE_VERSION_V1.to_le_bytes());
1084        bytes.extend_from_slice(&(payload.len() as u64).to_le_bytes());
1085        bytes.extend_from_slice(&payload);
1086        bytes
1087    }
1088
1089    #[test]
1090    fn traceable_peer_names_one_host_including_ipv4_mapped_addresses() {
1091        let inet6 = |address: Ipv6Addr| NetworkAddressV1::Inet6 {
1092            port: 80,
1093            flowinfo: 0,
1094            address: address.octets(),
1095            scope_id: 0,
1096        };
1097        let mapped = |v4: Ipv4Addr| inet6(v4.to_ipv6_mapped());
1098        assert!(mapped(Ipv4Addr::LOCALHOST).is_traceable_peer());
1099        assert!(inet6(Ipv6Addr::LOCALHOST).is_traceable_peer());
1100        for v4 in [
1101            Ipv4Addr::UNSPECIFIED,
1102            Ipv4Addr::BROADCAST,
1103            Ipv4Addr::new(224, 0, 0, 1),
1104        ] {
1105            assert!(!mapped(v4).is_traceable_peer(), "{v4} names no single host");
1106        }
1107        assert!(!inet6(Ipv6Addr::UNSPECIFIED).is_traceable_peer());
1108    }
1109
1110    #[test]
1111    fn network_trace_v1_round_trips_exactly() {
1112        let trace = valid_trace();
1113        let decoded = NetworkTraceV1::read_framed(Cursor::new(framed(&trace))).unwrap();
1114        assert_eq!(decoded, trace);
1115    }
1116
1117    #[test]
1118    fn transient_socket_errors_are_rejected_by_validation_writer_and_reader() {
1119        assert_eq!(libc::EWOULDBLOCK, libc::EAGAIN);
1120        for errno in [libc::EAGAIN, libc::EINTR] {
1121            let mut trace = valid_trace();
1122            trace.inputs[1].event = NetworkInputKindV1::SocketError {
1123                stream_offset: 8,
1124                errno,
1125            };
1126            assert_eq!(
1127                trace.validate(),
1128                Err(NetworkTraceValidationError::NonTerminalSocketError),
1129                "transient errno {errno} cannot terminate the stream"
1130            );
1131
1132            let mut output = b"existing output".to_vec();
1133            assert!(matches!(
1134                trace.write_framed(&mut output),
1135                Err(NetworkTraceCodecError::Validation(
1136                    NetworkTraceValidationError::NonTerminalSocketError
1137                ))
1138            ));
1139            assert_eq!(output, b"existing output");
1140            // Bypass the writer to prove untrusted serialized input is refused
1141            // independently, rather than relying on the writer's validation.
1142            assert!(matches!(
1143                NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
1144                Err(NetworkTraceCodecError::Validation(
1145                    NetworkTraceValidationError::NonTerminalSocketError
1146                ))
1147            ));
1148        }
1149    }
1150
1151    #[test]
1152    fn terminal_socket_errors_preserve_framing_and_the_stream_boundary() {
1153        for errno in [libc::ECONNRESET, libc::ETIMEDOUT] {
1154            let mut trace = valid_trace();
1155            trace.inputs[1].event = NetworkInputKindV1::SocketError {
1156                stream_offset: 8,
1157                errno,
1158            };
1159            assert_eq!(trace.validate(), Ok(()));
1160            let bytes = framed(&trace);
1161            assert_eq!(bytes, framed_without_validation(&trace));
1162            assert_eq!(
1163                NetworkTraceV1::read_framed(Cursor::new(bytes)).unwrap(),
1164                trace
1165            );
1166
1167            let mut late = trace.inputs[0].clone();
1168            late.ordinal = 2;
1169            late.release = trace.inputs[1].release;
1170            late.event = NetworkInputKindV1::InboundBytes {
1171                stream_offset: 8,
1172                bytes: b"late".to_vec(),
1173            };
1174            trace.inputs.push(late);
1175            assert_eq!(
1176                trace.validate(),
1177                Err(NetworkTraceValidationError::EventAfterTerminal)
1178            );
1179        }
1180    }
1181
1182    #[test]
1183    fn socket_errno_range_is_still_checked_before_framing() {
1184        for errno in [-1, 0, 4096] {
1185            let mut trace = valid_trace();
1186            trace.inputs[1].event = NetworkInputKindV1::SocketError {
1187                stream_offset: 8,
1188                errno,
1189            };
1190            assert_eq!(
1191                trace.validate(),
1192                Err(NetworkTraceValidationError::InvalidErrno)
1193            );
1194            assert!(matches!(
1195                trace.write_framed(Vec::new()),
1196                Err(NetworkTraceCodecError::Validation(
1197                    NetworkTraceValidationError::InvalidErrno
1198                ))
1199            ));
1200            assert!(matches!(
1201                NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
1202                Err(NetworkTraceCodecError::Validation(
1203                    NetworkTraceValidationError::InvalidErrno
1204                ))
1205            ));
1206        }
1207    }
1208
1209    #[test]
1210    fn every_strict_prefix_is_reported_as_truncated() {
1211        let bytes = framed(&valid_trace());
1212        for end in 0..bytes.len() {
1213            assert!(
1214                matches!(
1215                    NetworkTraceV1::read_framed(Cursor::new(&bytes[..end])),
1216                    Err(NetworkTraceCodecError::Truncated)
1217                ),
1218                "prefix of length {end} was not rejected as truncation"
1219            );
1220        }
1221    }
1222
1223    #[test]
1224    fn unknown_versions_are_rejected_before_payload_decode() {
1225        let mut bytes = framed(&valid_trace());
1226        let start = NETWORK_TRACE_MAGIC.len();
1227        bytes[start..start + 4].copy_from_slice(&2u32.to_le_bytes());
1228        assert!(matches!(
1229            NetworkTraceV1::read_framed(Cursor::new(bytes)),
1230            Err(NetworkTraceCodecError::UnsupportedVersion(2))
1231        ));
1232    }
1233
1234    #[test]
1235    fn hostile_length_is_truncation_without_allocating_it() {
1236        // Allocating the claimed length before reading it panics here.
1237        let mut bytes = Vec::new();
1238        bytes.extend_from_slice(&NETWORK_TRACE_MAGIC);
1239        bytes.extend_from_slice(&NETWORK_TRACE_VERSION_V1.to_le_bytes());
1240        bytes.extend_from_slice(&u64::MAX.to_le_bytes());
1241        bytes.extend_from_slice(b"short");
1242        assert!(matches!(
1243            NetworkTraceV1::read_framed(Cursor::new(bytes)),
1244            Err(NetworkTraceCodecError::Truncated)
1245        ));
1246    }
1247
1248    #[test]
1249    fn trailing_file_data_is_rejected() {
1250        let mut bytes = framed(&valid_trace());
1251        bytes.push(0);
1252        assert!(matches!(
1253            NetworkTraceV1::read_framed(Cursor::new(bytes)),
1254            Err(NetworkTraceCodecError::TrailingData)
1255        ));
1256    }
1257
1258    #[test]
1259    fn reader_and_writer_both_refuse_semantically_invalid_traces() {
1260        let mut trace = valid_trace();
1261        trace.inputs[0].ordinal = 7;
1262
1263        assert!(matches!(
1264            trace.write_framed(Vec::new()),
1265            Err(NetworkTraceCodecError::Validation(
1266                NetworkTraceValidationError::NonCanonicalOrdinal
1267            ))
1268        ));
1269        assert!(matches!(
1270            NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
1271            Err(NetworkTraceCodecError::Validation(
1272                NetworkTraceValidationError::NonCanonicalOrdinal
1273            ))
1274        ));
1275    }
1276
1277    #[test]
1278    fn deserialized_pre_unix_epoch_is_a_typed_validation_error() {
1279        let mut trace = valid_trace();
1280        trace.epoch = Utc.timestamp_opt(-1, 0).unwrap();
1281        assert!(matches!(
1282            NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
1283            Err(NetworkTraceCodecError::Validation(
1284                NetworkTraceValidationError::EpochOutOfRange
1285            ))
1286        ));
1287    }
1288
1289    #[test]
1290    fn deserialized_epoch_beyond_global_time_is_a_typed_validation_error() {
1291        let mut trace = valid_trace();
1292        trace.epoch = Utc.with_ymd_and_hms(9999, 1, 1, 0, 0, 0).unwrap();
1293        assert!(matches!(
1294            NetworkTraceV1::read_framed(Cursor::new(framed_without_validation(&trace))),
1295            Err(NetworkTraceCodecError::Validation(
1296                NetworkTraceValidationError::EpochOutOfRange
1297            ))
1298        ));
1299    }
1300
1301    #[test]
1302    fn config_is_off_by_default_and_perturbation_has_no_seed_fallback() {
1303        let config = NetworkTraceConfig::default();
1304        assert_eq!(config.mode, NetworkTraceMode::Off);
1305        assert_eq!(config.network_perturb_seed, None);
1306        assert_eq!(config.validate(), Ok(()));
1307
1308        let replay = NetworkTraceConfig {
1309            mode: NetworkTraceMode::Replay,
1310            path: Some("trace.net".into()),
1311            network_perturb_seed: Some(17),
1312        };
1313        assert_eq!(replay.network_perturb_seed, Some(17));
1314        assert_eq!(replay.validate(), Ok(()));
1315
1316        let encoded = serde_json::to_string(&replay).unwrap();
1317        assert_eq!(
1318            serde_json::from_str::<NetworkTraceConfig>(&encoded).unwrap(),
1319            replay
1320        );
1321        assert!(
1322            serde_json::from_str::<NetworkTraceConfig>(
1323                r#"{"mode":"off","path":null,"network_perturb_seed":null,"unexpected":true}"#
1324            )
1325            .is_err()
1326        );
1327    }
1328
1329    #[test]
1330    fn config_rejects_ambiguous_or_record_perturbation_combinations() {
1331        assert_eq!(
1332            NetworkTraceConfig {
1333                path: Some("trace.net".into()),
1334                ..NetworkTraceConfig::default()
1335            }
1336            .validate(),
1337            Err(NetworkTraceConfigError::OptionsWhileOff)
1338        );
1339        assert_eq!(
1340            NetworkTraceConfig {
1341                mode: NetworkTraceMode::Replay,
1342                ..NetworkTraceConfig::default()
1343            }
1344            .validate(),
1345            Err(NetworkTraceConfigError::MissingPath)
1346        );
1347        assert_eq!(
1348            NetworkTraceConfig {
1349                mode: NetworkTraceMode::Record,
1350                path: Some("trace.net".into()),
1351                network_perturb_seed: Some(1),
1352            }
1353            .validate(),
1354            Err(NetworkTraceConfigError::PerturbationDuringRecord)
1355        );
1356    }
1357
1358    #[test]
1359    fn release_eligibility_uses_the_absolute_global_time_domain() {
1360        let mut config = Config {
1361            epoch: trace_epoch(),
1362            ..Config::default()
1363        };
1364        // A different scheduler seed must not affect the clock-domain conversion.
1365        config.sched_seed = Some(999);
1366        let mut global_time = GlobalTime::new(&config);
1367        let start = global_time.as_nanos();
1368        assert_eq!(valid_trace().epoch_global_time(), Ok(start));
1369        let release = NetworkReleaseV1 {
1370            not_before_global_time: start + LogicalTime::from_nanos(10),
1371            after_transmitted_offset: 3,
1372        };
1373
1374        assert!(!release.is_eligible(global_time.as_nanos(), 3));
1375        global_time.add_extra_time(Duration::from_nanos(10));
1376        assert_eq!(global_time.as_nanos(), release.not_before_global_time);
1377        assert!(!release.is_eligible(global_time.as_nanos(), 2));
1378        assert!(release.is_eligible(global_time.as_nanos(), 3));
1379    }
1380
1381    #[test]
1382    fn v1_validation_rejects_channels_outside_the_supported_envelope() {
1383        let mut trace = valid_trace();
1384        trace.channels.push(trace.channels[0].clone());
1385        assert_eq!(
1386            trace.validate(),
1387            Err(NetworkTraceValidationError::ChannelCount(2))
1388        );
1389
1390        let mut trace = valid_trace();
1391        let non_socket = OpenFileId::new(DetTid::from_raw(1), 0);
1392        trace.channels[0].id = non_socket;
1393        for input in &mut trace.inputs {
1394            input.channel = non_socket;
1395        }
1396        for output in &mut trace.outputs {
1397            output.channel = non_socket;
1398        }
1399        assert_eq!(
1400            trace.validate(),
1401            Err(NetworkTraceValidationError::NonSocketChannelIdentity)
1402        );
1403
1404        let mut trace = valid_trace();
1405        trace.channels[0].peer_address = NetworkAddressV1::Inet4 {
1406            address: [127, 0, 0, 1],
1407            port: 443,
1408        };
1409        assert_eq!(
1410            trace.validate(),
1411            Err(NetworkTraceValidationError::UnsupportedPeerAddress)
1412        );
1413
1414        for mapped in [
1415            Ipv4Addr::LOCALHOST,
1416            Ipv4Addr::UNSPECIFIED,
1417            Ipv4Addr::new(224, 0, 0, 1),
1418        ] {
1419            let mut trace = valid_trace();
1420            trace.channels[0].local_address = NetworkAddressV1::Inet6 {
1421                address: Ipv6Addr::LOCALHOST.octets(),
1422                port: 40_000,
1423                flowinfo: 0,
1424                scope_id: 0,
1425            };
1426            trace.channels[0].peer_address = NetworkAddressV1::Inet6 {
1427                address: mapped.to_ipv6_mapped().octets(),
1428                port: 443,
1429                flowinfo: 0,
1430                scope_id: 0,
1431            };
1432            assert_eq!(
1433                trace.validate(),
1434                Err(NetworkTraceValidationError::UnsupportedPeerAddress),
1435                "IPv4-mapped {mapped} escaped the IPv4 policy"
1436            );
1437        }
1438
1439        let mut trace = valid_trace();
1440        trace.channels[0].created_before_competing_threads = false;
1441        assert_eq!(
1442            trace.validate(),
1443            Err(NetworkTraceValidationError::ScheduleDependentChannelIdentity)
1444        );
1445    }
1446
1447    #[test]
1448    fn v1_validation_rejects_noncanonical_streams_and_release_conditions() {
1449        let mut trace = valid_trace();
1450        trace.inputs[0].release.not_before_global_time = LogicalTime::ZERO;
1451        assert_eq!(
1452            trace.validate(),
1453            Err(NetworkTraceValidationError::ReleaseBeforeEpoch)
1454        );
1455
1456        let mut trace = valid_trace();
1457        trace.inputs[0].ordinal = 1;
1458        assert_eq!(
1459            trace.validate(),
1460            Err(NetworkTraceValidationError::NonCanonicalOrdinal)
1461        );
1462
1463        let mut trace = valid_trace();
1464        trace.inputs[0].release.after_transmitted_offset = 4;
1465        assert_eq!(
1466            trace.validate(),
1467            Err(NetworkTraceValidationError::UnreachableTransmitWatermark)
1468        );
1469
1470        let mut trace = valid_trace();
1471        trace.outputs[0].stream_offset = 1;
1472        assert_eq!(
1473            trace.validate(),
1474            Err(NetworkTraceValidationError::NonContiguousOutput)
1475        );
1476
1477        let mut trace = valid_trace();
1478        trace.inputs.push(NetworkInputEventV1 {
1479            ordinal: 2,
1480            channel: channel_id(),
1481            release: NetworkReleaseV1 {
1482                not_before_global_time: global_time_after_epoch(21),
1483                after_transmitted_offset: 3,
1484            },
1485            event: NetworkInputKindV1::InboundBytes {
1486                stream_offset: 8,
1487                bytes: b"late".to_vec(),
1488            },
1489        });
1490        assert_eq!(
1491            trace.validate(),
1492            Err(NetworkTraceValidationError::EventAfterTerminal)
1493        );
1494    }
1495
1496    fn second_channel_id() -> OpenFileId {
1497        OpenFileId::new_socket(DetTid::from_raw(1), 1)
1498    }
1499
1500    fn loopback(port: u16) -> NetworkAddressV1 {
1501        NetworkAddressV1::Inet4 {
1502            address: [127, 0, 0, 1],
1503            port,
1504        }
1505    }
1506
1507    /// Two loopback channels whose events interleave, plus one refused connect.
1508    fn valid_trace_v2() -> NetworkTraceV2 {
1509        let first = channel_id();
1510        let second = second_channel_id();
1511        let refused = OpenFileId::new_socket(DetTid::from_raw(2), 0);
1512        let channel = |id, local: Option<u16>, errno| NetworkChannelV2 {
1513            id,
1514            transport: NetworkTransportV1::Tcp,
1515            role: NetworkEndpointRoleV1::OutboundClient,
1516            local_address: local.map(loopback),
1517            peer_address: loopback(8080),
1518            connect_errno: errno,
1519        };
1520        let input = |ordinal, channel, time, tx, event| NetworkInputEventV1 {
1521            ordinal,
1522            channel,
1523            release: NetworkReleaseV1 {
1524                not_before_global_time: global_time_after_epoch(time),
1525                after_transmitted_offset: tx,
1526            },
1527            event,
1528        };
1529        NetworkTraceV2 {
1530            epoch: trace_epoch(),
1531            channels: vec![
1532                channel(first, Some(40_000), 0),
1533                channel(second, Some(40_001), 0),
1534                channel(refused, None, libc::ECONNREFUSED),
1535            ],
1536            inputs: vec![
1537                input(
1538                    0,
1539                    second,
1540                    10,
1541                    2,
1542                    NetworkInputKindV1::InboundBytes {
1543                        stream_offset: 0,
1544                        bytes: b"xy".to_vec(),
1545                    },
1546                ),
1547                input(
1548                    1,
1549                    first,
1550                    10,
1551                    3,
1552                    NetworkInputKindV1::InboundBytes {
1553                        stream_offset: 0,
1554                        bytes: b"abc".to_vec(),
1555                    },
1556                ),
1557                input(
1558                    2,
1559                    first,
1560                    30,
1561                    3,
1562                    NetworkInputKindV1::PeerWriteClosed { stream_offset: 3 },
1563                ),
1564            ],
1565            outputs: vec![
1566                NetworkOutputV4 {
1567                    channel: first,
1568                    stream_offset: 0,
1569                    bytes: b"req".to_vec(),
1570                    wait: None,
1571                },
1572                NetworkOutputV4 {
1573                    channel: second,
1574                    stream_offset: 0,
1575                    bytes: b"hi".to_vec(),
1576                    wait: None,
1577                },
1578            ],
1579        }
1580    }
1581
1582    #[test]
1583    fn network_trace_v2_round_trips_exactly() {
1584        let trace = valid_trace_v2();
1585        let mut bytes = Vec::new();
1586        trace.write_framed(&mut bytes).unwrap();
1587        let start = NETWORK_TRACE_MAGIC.len();
1588        assert_eq!(
1589            bytes[start..start + 4],
1590            NETWORK_TRACE_VERSION_V2.to_le_bytes()
1591        );
1592        assert_eq!(
1593            NetworkTraceV2::read_framed(Cursor::new(bytes)).unwrap(),
1594            trace
1595        );
1596    }
1597
1598    #[test]
1599    fn frames_are_the_header_then_the_bincode_payload() {
1600        // The layout every earlier writer produced: magic, version, payload
1601        // length, then the payload encoded on its own.
1602        let v1 = valid_trace();
1603        assert_eq!(framed(&v1), framed_without_validation(&v1));
1604        let trace = valid_trace_v2();
1605        let wire = NetworkTraceV2Wire {
1606            epoch: trace.epoch,
1607            channels: trace.channels.clone(),
1608            inputs: trace.inputs.clone(),
1609            outputs: trace
1610                .outputs
1611                .iter()
1612                .map(|output| NetworkOutputV1 {
1613                    channel: output.channel,
1614                    stream_offset: output.stream_offset,
1615                    bytes: output.bytes.clone(),
1616                })
1617                .collect(),
1618        };
1619        let payload = bincode::serde::encode_to_vec(&wire, bincode::config::standard()).unwrap();
1620        // Literal bytes, so a changed magic or version number fails here too.
1621        let mut expected = Vec::new();
1622        expected.extend_from_slice(b"HERMIT-NET-TRACE");
1623        expected.extend_from_slice(&[2, 0, 0, 0]);
1624        expected.extend_from_slice(&(payload.len() as u64).to_le_bytes());
1625        expected.extend_from_slice(&payload);
1626        assert_eq!(trace.encode_framed().unwrap(), expected);
1627    }
1628
1629    #[test]
1630    fn network_trace_above_64_mib_round_trips() {
1631        // 64 MiB was the old limit on a whole trace; a download of that size
1632        // recorded completely and then lost its trace at exit.
1633        let mut trace = valid_trace_v2();
1634        let len = 64 * 1024 * 1024 + 1;
1635        trace.inputs[1].event = NetworkInputKindV1::InboundBytes {
1636            stream_offset: 0,
1637            bytes: vec![0xa5; len],
1638        };
1639        trace.inputs[2].event = NetworkInputKindV1::PeerWriteClosed {
1640            stream_offset: len as u64,
1641        };
1642        let mut framed = Vec::new();
1643        trace.write_framed(&mut framed).unwrap();
1644        assert!(framed.len() > len);
1645        assert!(NetworkTraceV2::read_framed(Cursor::new(framed)).unwrap() == trace);
1646    }
1647
1648    #[test]
1649    fn network_trace_v2_reader_upgrades_v1() {
1650        let v1 = valid_trace();
1651        let upgraded = NetworkTraceV2::read_framed(Cursor::new(framed(&v1))).unwrap();
1652        assert_eq!(upgraded, NetworkTraceV2::from(v1.clone()));
1653        assert_eq!(upgraded.channels.len(), 1);
1654        assert_eq!(upgraded.channels[0].connect_errno, 0);
1655        assert_eq!(
1656            upgraded.channels[0].local_address,
1657            Some(v1.channels[0].local_address.clone())
1658        );
1659        assert_eq!(upgraded.inputs, v1.inputs);
1660        let v1_outputs: Vec<NetworkOutputV4> = v1.outputs.into_iter().map(Into::into).collect();
1661        assert_eq!(upgraded.outputs, v1_outputs);
1662    }
1663
1664    #[test]
1665    fn network_trace_v2_reader_keeps_v1_validation() {
1666        let mut v1 = valid_trace();
1667        v1.channels[0].created_before_competing_threads = false;
1668        assert!(matches!(
1669            NetworkTraceV2::read_framed(Cursor::new(framed_without_validation(&v1))),
1670            Err(NetworkTraceCodecError::Validation(_))
1671        ));
1672    }
1673
1674    #[test]
1675    fn network_trace_v2_reader_rejects_unknown_versions() {
1676        let mut bytes = Vec::new();
1677        valid_trace_v2().write_framed(&mut bytes).unwrap();
1678        let start = NETWORK_TRACE_MAGIC.len();
1679        bytes[start..start + 4].copy_from_slice(&5u32.to_le_bytes());
1680        assert!(matches!(
1681            NetworkTraceV2::read_framed(Cursor::new(bytes)),
1682            Err(NetworkTraceCodecError::UnsupportedVersion(5))
1683        ));
1684    }
1685
1686    /// `valid_trace_v2` with one more fragment on the first channel, accepted
1687    /// after its send waited twice.
1688    fn waited_trace_v4() -> NetworkTraceV2 {
1689        let mut trace = valid_trace_v2();
1690        trace.outputs.push(NetworkOutputV4 {
1691            channel: trace.channels[0].id,
1692            stream_offset: 3,
1693            bytes: b"more".to_vec(),
1694            wait: Some(NetworkSendWaitV4 {
1695                waits: 2,
1696                not_before_global_time: global_time_after_epoch(20),
1697            }),
1698        });
1699        trace
1700    }
1701
1702    #[test]
1703    fn network_trace_v4_holds_send_waits_that_v2_and_v3_cannot() {
1704        let trace = waited_trace_v4();
1705        let mut bytes = Vec::new();
1706        trace.write_framed(&mut bytes).unwrap();
1707        let start = NETWORK_TRACE_MAGIC.len();
1708        assert_eq!(
1709            bytes[start..start + 4],
1710            NETWORK_TRACE_VERSION_V4.to_le_bytes()
1711        );
1712        assert_eq!(
1713            NetworkTraceV2::read_framed(Cursor::new(bytes.clone())).unwrap(),
1714            trace
1715        );
1716        // The older payloads have no field for a wait, so the same payload
1717        // labelled v2 or v3 does not decode.
1718        for older in [NETWORK_TRACE_VERSION_V2, NETWORK_TRACE_VERSION_V3] {
1719            let mut relabelled = bytes.clone();
1720            relabelled[start..start + 4].copy_from_slice(&older.to_le_bytes());
1721            assert!(
1722                NetworkTraceV2::read_framed(Cursor::new(relabelled)).is_err(),
1723                "a v4 payload labelled v{older} decoded"
1724            );
1725        }
1726        // A trace without a wait keeps its older version byte for byte.
1727        let mut unwaited = Vec::new();
1728        valid_trace_v2().write_framed(&mut unwaited).unwrap();
1729        assert_eq!(
1730            unwaited[start..start + 4],
1731            NETWORK_TRACE_VERSION_V2.to_le_bytes()
1732        );
1733    }
1734
1735    #[test]
1736    fn network_trace_v4_validation_rejects_each_malformed_wait() {
1737        let check = |mutate: &dyn Fn(&mut NetworkTraceV2), expected| {
1738            let mut trace = waited_trace_v4();
1739            mutate(&mut trace);
1740            assert_eq!(trace.validate(), Err(expected));
1741        };
1742        use NetworkTraceValidationError as E;
1743        let wait_at = |nanos| {
1744            Some(NetworkSendWaitV4 {
1745                waits: 1,
1746                not_before_global_time: global_time_after_epoch(nanos),
1747            })
1748        };
1749        check(
1750            &|t| {
1751                t.outputs.push(NetworkOutputV4 {
1752                    channel: t.channels[0].id,
1753                    stream_offset: 7,
1754                    bytes: Vec::new(),
1755                    wait: wait_at(30),
1756                })
1757            },
1758            E::WaitOnRefusedSend,
1759        );
1760        check(
1761            &|t| t.outputs[2].wait.as_mut().unwrap().waits = 0,
1762            E::ZeroSendWaits,
1763        );
1764        check(
1765            &|t| {
1766                let epoch = epoch_global_time(&t.epoch).unwrap().as_nanos();
1767                t.outputs[2].wait.as_mut().unwrap().not_before_global_time =
1768                    LogicalTime::from_nanos(epoch - 1);
1769            },
1770            E::SendWaitBeforeEpoch,
1771        );
1772        check(
1773            &|t| {
1774                t.outputs.push(NetworkOutputV4 {
1775                    channel: t.channels[0].id,
1776                    stream_offset: 7,
1777                    bytes: b"x".to_vec(),
1778                    wait: wait_at(19),
1779                })
1780            },
1781            E::NonMonotonicSendWait,
1782        );
1783        // Monotonicity is per channel: an earlier wait on another channel
1784        // after a later one on the first is valid.
1785        let mut trace = waited_trace_v4();
1786        trace.outputs.push(NetworkOutputV4 {
1787            channel: trace.channels[1].id,
1788            stream_offset: 2,
1789            bytes: b"x".to_vec(),
1790            wait: wait_at(5),
1791        });
1792        assert_eq!(trace.validate(), Ok(()));
1793    }
1794
1795    #[test]
1796    fn network_trace_v3_holds_refused_sends_that_v2_rejects() {
1797        let mut trace = valid_trace_v2();
1798        let first = trace.channels[0].id;
1799        trace.outputs.insert(
1800            0,
1801            NetworkOutputV4 {
1802                channel: first,
1803                stream_offset: 0,
1804                bytes: Vec::new(),
1805                wait: None,
1806            },
1807        );
1808        let mut bytes = Vec::new();
1809        trace.write_framed(&mut bytes).unwrap();
1810        let start = NETWORK_TRACE_MAGIC.len();
1811        assert_eq!(
1812            bytes[start..start + 4],
1813            NETWORK_TRACE_VERSION_V3.to_le_bytes()
1814        );
1815        assert_eq!(
1816            NetworkTraceV2::read_framed(Cursor::new(bytes.clone())).unwrap(),
1817            trace
1818        );
1819        bytes[start..start + 4].copy_from_slice(&NETWORK_TRACE_VERSION_V2.to_le_bytes());
1820        assert!(matches!(
1821            NetworkTraceV2::read_framed(Cursor::new(bytes)),
1822            Err(NetworkTraceCodecError::Validation(
1823                NetworkTraceValidationError::EmptyByteChunk
1824            ))
1825        ));
1826    }
1827
1828    #[test]
1829    fn network_trace_v2_validation_rejects_each_malformation() {
1830        let check = |mutate: &dyn Fn(&mut NetworkTraceV2), expected| {
1831            let mut trace = valid_trace_v2();
1832            mutate(&mut trace);
1833            assert_eq!(trace.validate(), Err(expected));
1834        };
1835        use NetworkTraceValidationError as E;
1836        check(&|t| t.channels[1].id = channel_id(), E::DuplicateChannel);
1837        check(
1838            &|t| t.channels[0].id = OpenFileId::new(DetTid::from_raw(1), 9),
1839            E::NonSocketChannelIdentity,
1840        );
1841        check(
1842            &|t| t.channels[0].peer_address = loopback(0),
1843            E::UnsupportedPeerAddress,
1844        );
1845        check(
1846            &|t| {
1847                t.channels[0].peer_address = NetworkAddressV1::Inet4 {
1848                    address: [0, 0, 0, 0],
1849                    port: 80,
1850                }
1851            },
1852            E::UnsupportedPeerAddress,
1853        );
1854        check(
1855            &|t| {
1856                t.channels[0].peer_address = NetworkAddressV1::Inet6 {
1857                    address: [0; 16],
1858                    port: 80,
1859                    flowinfo: 0,
1860                    scope_id: 0,
1861                }
1862            },
1863            E::UnsupportedPeerAddress,
1864        );
1865        check(
1866            &|t| {
1867                t.channels[0].peer_address = NetworkAddressV1::Inet6 {
1868                    address: Ipv6Addr::LOCALHOST.octets(),
1869                    port: 80,
1870                    flowinfo: 0,
1871                    scope_id: 0,
1872                }
1873            },
1874            E::AddressFamilyMismatch,
1875        );
1876        check(&|t| t.channels[0].connect_errno = -1, E::InvalidErrno);
1877        check(&|t| t.channels[0].local_address = None, E::InvalidErrno);
1878        check(
1879            &|t| t.channels[2].local_address = Some(loopback(1)),
1880            E::InvalidErrno,
1881        );
1882        check(
1883            &|t| t.outputs[1].channel = t.channels[2].id,
1884            E::TrafficOnFailedChannel,
1885        );
1886        check(
1887            &|t| t.inputs[0].channel = t.channels[2].id,
1888            E::TrafficOnFailedChannel,
1889        );
1890        check(
1891            &|t| t.inputs[0].channel = OpenFileId::new_socket(DetTid::from_raw(9), 0),
1892            E::UnknownChannel,
1893        );
1894        check(&|t| t.outputs[1].stream_offset = 1, E::NonContiguousOutput);
1895        check(&|t| t.inputs[2].ordinal = 7, E::NonCanonicalOrdinal);
1896        check(
1897            &|t| t.inputs[1].release.not_before_global_time = global_time_after_epoch(5),
1898            E::NonMonotonicRelease,
1899        );
1900        check(
1901            &|t| t.inputs[2].release.after_transmitted_offset = 2,
1902            E::NonMonotonicRelease,
1903        );
1904        check(
1905            &|t| t.inputs[0].release.after_transmitted_offset = 3,
1906            E::UnreachableTransmitWatermark,
1907        );
1908        check(
1909            &|t| {
1910                t.inputs[2].event = NetworkInputKindV1::PeerWriteClosed { stream_offset: 2 };
1911            },
1912            E::NonContiguousInput,
1913        );
1914        check(
1915            &|t| {
1916                let mut late = t.inputs[2].clone();
1917                late.ordinal = 3;
1918                late.release.not_before_global_time = global_time_after_epoch(40);
1919                late.event = NetworkInputKindV1::InboundBytes {
1920                    stream_offset: 3,
1921                    bytes: b"z".to_vec(),
1922                };
1923                t.inputs.push(late);
1924            },
1925            E::EventAfterTerminal,
1926        );
1927    }
1928
1929    #[test]
1930    fn network_trace_v2_offsets_are_per_channel() {
1931        // The second channel's bytes start at offset 0 even though the first
1932        // channel already delivered bytes: a shared cursor would reject this.
1933        let mut trace = valid_trace_v2();
1934        trace.inputs.push(NetworkInputEventV1 {
1935            ordinal: 3,
1936            channel: second_channel_id(),
1937            release: NetworkReleaseV1 {
1938                not_before_global_time: global_time_after_epoch(30),
1939                after_transmitted_offset: 2,
1940            },
1941            event: NetworkInputKindV1::InboundBytes {
1942                stream_offset: 2,
1943                bytes: b"z".to_vec(),
1944            },
1945        });
1946        assert_eq!(trace.validate(), Ok(()));
1947    }
1948}