Skip to main content

squib_virtio/devices/
net.rs

1//! virtio-net — network frontend.
2//!
3//! Per [14-virtio-and-devices.md § 4.2](../../../specs/14-virtio-and-devices.md#42-virtio-net):
4//!
5//! > Frontend ported from cloud-hypervisor `virtio-devices/src/net.rs`.
6//! > Host backend lives in [30-networking.md](../../../specs/30-networking.md) — virtio-net
7//! > is parameterised over a [`NetBackend`] trait so the frontend is host-agnostic.
8//!
9//! The `MmdsInterceptor` from [15-mmds.md § 3](../../../specs/15-mmds.md#3-packet-interception)
10//! sits between the frontend's RX/TX queues and the host backend, peeling off
11//! ARP and TCP-to-MMDS-IP frames before they hit the wire.
12//!
13//! ## Queue layout
14//!
15//! - Queue 0 — RX (host → guest).
16//! - Queue 1 — TX (guest → host).
17//!
18//! `VIRTIO_NET_F_CTRL_VQ` (would add a third control queue) is not offered
19//! and not allocated in 1.0.
20
21use std::sync::Arc;
22
23use parking_lot::Mutex;
24use squib_core::GuestMemory;
25
26use crate::{
27    device::{ActivateError, VirtioDevice},
28    device_id::VirtioDeviceType,
29    interrupt::IrqLine,
30    queue::Queue,
31};
32
33/// `VIRTIO_NET_F_MAC` — driver may read MAC from config-space.
34pub const F_MAC: u64 = 1 << 5;
35/// `VIRTIO_NET_F_STATUS` — driver may read link status.
36pub const F_STATUS: u64 = 1 << 16;
37/// `VIRTIO_NET_F_MTU` — driver may read MTU.
38pub const F_MTU: u64 = 1 << 3;
39
40/// RX queue index (host → guest).
41pub const RX_QUEUE: usize = 0;
42/// TX queue index (guest → host).
43pub const TX_QUEUE: usize = 1;
44
45const QUEUE_MAX_SIZE: u16 = 256;
46const VIRTIO_NET_HDR_LEN: u32 = 12;
47
48/// IPv4 / TCP / UDP frame as it crosses the virtio-net boundary. Pure
49/// payload — the virtio-net header is consumed by the frontend before frames
50/// reach the backend (and prepended on RX).
51///
52/// Storage is `bytes::Bytes` (immutable, shared, refcounted) per I-NET-4 in
53/// [30-networking.md § 7](../../../specs/30-networking.md#7-invariants):
54/// the hot path is allocation-free for clones (`Bytes::clone` is just a
55/// refcount bump). New frames are typically built via [`FramePool::acquire`]
56/// → fill `BytesMut` → [`Frame::from_buf`].
57#[derive(Debug, Clone)]
58pub struct Frame {
59    /// Raw bytes (Ethernet header + IP + payload).
60    pub bytes: bytes::Bytes,
61}
62
63impl Frame {
64    /// Build a frame from a slice. Allocates a fresh `Bytes` — call sites
65    /// that own the buffer should prefer [`Frame::from_buf`] which avoids the
66    /// copy.
67    #[must_use]
68    pub fn from_slice(slice: &[u8]) -> Self {
69        Self {
70            bytes: bytes::Bytes::copy_from_slice(slice),
71        }
72    }
73
74    /// Build a frame by freezing a [`bytes::BytesMut`] into the Bytes
75    /// shape. The caller's exclusive write access ends here; subsequent
76    /// readers go through the immutable `Bytes`.
77    #[must_use]
78    pub fn from_buf(buf: bytes::BytesMut) -> Self {
79        Self {
80            bytes: buf.freeze(),
81        }
82    }
83
84    /// Build a frame directly from a [`bytes::Bytes`] handle. Useful when
85    /// the caller already has a refcounted buffer (e.g. a parser that hands
86    /// out a slice of a larger pre-allocated buffer).
87    #[must_use]
88    pub const fn from_bytes(bytes: bytes::Bytes) -> Self {
89        Self { bytes }
90    }
91
92    /// Wire-form length in bytes.
93    #[must_use]
94    pub fn len(&self) -> usize {
95        self.bytes.len()
96    }
97
98    /// `true` if the frame has no bytes.
99    #[must_use]
100    pub fn is_empty(&self) -> bool {
101        self.bytes.is_empty()
102    }
103}
104
105/// MTU-sized buffer pool for the virtio-net hot path.
106///
107/// Pins I-NET-4 ([30-networking.md § 7](../../../specs/30-networking.md#7-invariants)):
108/// frame allocation goes through a pool of pre-allocated `BytesMut` buffers
109/// rather than `vec![0u8; mtu]` per-call. The pool is sized to MTU × 256 per
110/// direction (the 71 § 5 bench-harness gating value); bursts beyond the pool
111/// allocate fresh and are dropped on release rather than returned, so memory
112/// pressure spikes are bounded.
113///
114/// The pool is `Send + Sync`; per-backend instances share their RX/TX pools
115/// across the device thread and the host-side I/O thread without contention
116/// (a `parking_lot::Mutex<Vec<BytesMut>>` is the boot-time-cheap, hot-path-
117/// uncontended shape — RX or TX rarely interleave).
118#[derive(Debug)]
119pub struct FramePool {
120    free: Mutex<Vec<bytes::BytesMut>>,
121    mtu: usize,
122    pool_capacity: usize,
123}
124
125impl FramePool {
126    /// Build a pool with `pool_capacity` slots, each `mtu` bytes wide.
127    /// Slots are allocated lazily on first acquire.
128    #[must_use]
129    pub fn new(mtu: usize, pool_capacity: usize) -> Self {
130        Self {
131            free: Mutex::new(Vec::with_capacity(pool_capacity)),
132            mtu,
133            pool_capacity,
134        }
135    }
136
137    /// Acquire a buffer of at least `mtu` capacity. The buffer is empty
138    /// (`len() == 0`) on return; the caller fills it via `BytesMut::extend_from_slice`
139    /// or `unsafe { set_len(_) }` after a `read` writes into the spare capacity.
140    #[must_use]
141    pub fn acquire(&self) -> bytes::BytesMut {
142        let mut g = self.free.lock();
143        match g.pop() {
144            Some(mut buf) => {
145                buf.clear();
146                buf
147            }
148            None => bytes::BytesMut::with_capacity(self.mtu),
149        }
150    }
151
152    /// Return a buffer to the pool. Buffers beyond `pool_capacity`
153    /// are dropped, bounding memory pressure under burst.
154    pub fn release(&self, buf: bytes::BytesMut) {
155        let mut g = self.free.lock();
156        if g.len() < self.pool_capacity {
157            g.push(buf);
158        }
159        // else: drop the buffer; capacity rebuilds on next acquire.
160    }
161
162    /// Pool capacity (slots).
163    #[must_use]
164    pub const fn capacity(&self) -> usize {
165        self.pool_capacity
166    }
167
168    /// Configured MTU (per-buffer minimum capacity).
169    #[must_use]
170    pub const fn mtu(&self) -> usize {
171        self.mtu
172    }
173
174    /// Number of buffers currently in the free list (test/diagnostic).
175    #[must_use]
176    pub fn free_count(&self) -> usize {
177        self.free.lock().len()
178    }
179}
180
181/// Host-side network backend. Production implementations live in
182/// `squib-net` (vmnet, gvproxy); test code uses [`LoopbackBackend`].
183pub trait NetBackend: Send + Sync + std::fmt::Debug {
184    /// Send a frame from the guest to the host network.
185    fn send(&self, frame: &Frame);
186
187    /// Take any frames the host wants to deliver to the guest. The frontend
188    /// drains this on every RX queue notification.
189    fn recv(&self) -> Vec<Frame>;
190}
191
192/// Loopback backend — frames sent by the guest become frames the guest
193/// receives on the next RX notification. Useful for tests and the
194/// "shared/userspace network not yet wired" placeholder.
195#[derive(Debug, Default)]
196pub struct LoopbackBackend {
197    pending: Mutex<Vec<Frame>>,
198}
199
200impl NetBackend for LoopbackBackend {
201    fn send(&self, frame: &Frame) {
202        self.pending.lock().push(frame.clone());
203    }
204    fn recv(&self) -> Vec<Frame> {
205        std::mem::take(&mut *self.pending.lock())
206    }
207}
208
209/// Frame interceptor — peels MMDS-bound frames out of the TX path and
210/// injects MMDS responses into the RX path. The wire shape is documented in
211/// [15-mmds.md § 3](../../../specs/15-mmds.md#3-packet-interception).
212pub trait FrameInterceptor: Send + Sync + std::fmt::Debug {
213    /// Inspect a TX frame from the guest. Return `true` if the interceptor
214    /// consumed the frame (it MUST NOT reach the host backend).
215    fn intercept_tx(&self, frame: &Frame) -> bool;
216
217    /// Drain any RX frames the interceptor wants to inject. Called on every
218    /// RX notification before the host backend's frames.
219    fn drain_rx(&self) -> Vec<Frame>;
220}
221
222/// No-op interceptor — every TX frame goes to the wire, no RX is injected.
223/// The default for net configs without `mmds_networks` set.
224#[derive(Debug, Default)]
225pub struct NoopInterceptor;
226
227impl FrameInterceptor for NoopInterceptor {
228    fn intercept_tx(&self, _frame: &Frame) -> bool {
229        false
230    }
231    fn drain_rx(&self) -> Vec<Frame> {
232        Vec::new()
233    }
234}
235
236/// Adapter so [`squib_mmds::MmdsInterceptor`] can plug straight into the
237/// virtio-net frontend's [`FrameInterceptor`] seam. The interceptor's
238/// own API works on raw byte slices; we wrap that as `Frame`-shaped
239/// inputs / outputs.
240impl FrameInterceptor for squib_mmds::MmdsInterceptor {
241    fn intercept_tx(&self, frame: &Frame) -> bool {
242        // The MMDS interceptor expects a complete Ethernet frame; the
243        // virtio-net frontend hands us the same shape, so this is a
244        // direct passthrough.
245        squib_mmds::MmdsInterceptor::intercept(self, &frame.bytes)
246    }
247    fn drain_rx(&self) -> Vec<Frame> {
248        squib_mmds::MmdsInterceptor::drain_rx(self)
249            .into_iter()
250            .map(Frame::from_bytes)
251            .collect()
252    }
253}
254
255/// virtio-net configuration.
256#[derive(Debug, Clone)]
257pub struct NetConfig {
258    /// Operator-supplied identifier.
259    pub iface_id: String,
260    /// Host-side device name (`eth0`, `tap0`, etc.).
261    pub host_dev_name: String,
262    /// Guest-side MAC; if `None` the driver picks one (we don't offer `F_MAC`).
263    pub guest_mac: Option<[u8; 6]>,
264    /// MTU; if `None` we don't offer `VIRTIO_NET_F_MTU`.
265    pub mtu: Option<u16>,
266}
267
268/// virtio-net frontend.
269#[derive(Debug)]
270pub struct NetDevice {
271    avail: u64,
272    acked: u64,
273    queues: Vec<Queue>,
274    config: NetConfig,
275    backend: Arc<dyn NetBackend>,
276    interceptor: Arc<dyn FrameInterceptor>,
277    state: Arc<Mutex<ActiveState>>,
278}
279
280#[derive(Debug, Default)]
281struct ActiveState {
282    mem: Option<Arc<dyn GuestMemory>>,
283    irq: Option<IrqLine>,
284    activated: bool,
285}
286
287impl NetDevice {
288    /// Build a virtio-net.
289    #[must_use]
290    pub fn new(
291        config: NetConfig,
292        backend: Arc<dyn NetBackend>,
293        interceptor: Arc<dyn FrameInterceptor>,
294    ) -> Self {
295        let mut avail = 0;
296        if config.guest_mac.is_some() {
297            avail |= F_MAC;
298        }
299        if config.mtu.is_some() {
300            avail |= F_MTU;
301        }
302        Self {
303            avail,
304            acked: 0,
305            queues: vec![Queue::new(QUEUE_MAX_SIZE), Queue::new(QUEUE_MAX_SIZE)],
306            config,
307            backend,
308            interceptor,
309            state: Arc::new(Mutex::new(ActiveState::default())),
310        }
311    }
312
313    fn drain_tx(&mut self) {
314        let (mem, irq) = {
315            let state = self.state.lock();
316            match (state.mem.clone(), state.irq.clone()) {
317                (Some(m), Some(i)) => (m, i),
318                _ => return,
319            }
320        };
321        let backend = Arc::clone(&self.backend);
322        let interceptor = Arc::clone(&self.interceptor);
323        let queue = &mut self.queues[TX_QUEUE];
324        let mut completed = false;
325        loop {
326            let chain = match queue.pop_avail(mem.as_ref()) {
327                Ok(Some(c)) => c,
328                Ok(None) => break,
329                Err(err) => {
330                    tracing::warn!(error = %err, "net: tx walk failed");
331                    break;
332                }
333            };
334            let head = chain.head_index();
335            let descs = match chain.collect(mem.as_ref()) {
336                Ok(d) => d,
337                Err(err) => {
338                    tracing::warn!(error = %err, "net: tx chain collect failed");
339                    break;
340                }
341            };
342            // Concatenate the device-read descriptors into a single frame
343            // buffer; strip the 12-byte virtio-net header per spec § 5.1.6.
344            let mut frame_bytes = Vec::new();
345            for desc in &descs {
346                if desc.is_write_only() {
347                    continue;
348                }
349                let mut buf = vec![0u8; desc.len as usize];
350                if let Err(err) = mem.read(desc.addr, &mut buf) {
351                    tracing::warn!(error = %err, "net: tx frame read failed");
352                    continue;
353                }
354                frame_bytes.extend_from_slice(&buf);
355            }
356            // Strip header.
357            let payload = if frame_bytes.len() > VIRTIO_NET_HDR_LEN as usize {
358                Frame::from_slice(&frame_bytes[VIRTIO_NET_HDR_LEN as usize..])
359            } else {
360                continue;
361            };
362            if !interceptor.intercept_tx(&payload) {
363                backend.send(&payload);
364            }
365            if let Err(err) = queue.push_used(mem.as_ref(), head, 0) {
366                tracing::warn!(error = %err, "net: tx push_used failed");
367                break;
368            }
369            completed = true;
370        }
371        if completed {
372            let _ = irq.trigger_queue();
373        }
374    }
375
376    fn drain_rx(&mut self) {
377        let (mem, irq) = {
378            let state = self.state.lock();
379            match (state.mem.clone(), state.irq.clone()) {
380                (Some(m), Some(i)) => (m, i),
381                _ => return,
382            }
383        };
384        let backend = Arc::clone(&self.backend);
385        let interceptor = Arc::clone(&self.interceptor);
386        // Combine interceptor + backend frames; interceptor first so MMDS
387        // responses precede any host-network traffic the guest may consume.
388        let mut frames = interceptor.drain_rx();
389        frames.extend(backend.recv());
390        if frames.is_empty() {
391            return;
392        }
393        let queue = &mut self.queues[RX_QUEUE];
394        let mut completed = false;
395        for frame in frames {
396            let chain = match queue.pop_avail(mem.as_ref()) {
397                Ok(Some(c)) => c,
398                Ok(None) => break,
399                Err(err) => {
400                    tracing::warn!(error = %err, "net: rx walk failed");
401                    break;
402                }
403            };
404            let head = chain.head_index();
405            let descs = match chain.collect(mem.as_ref()) {
406                Ok(d) => d,
407                Err(err) => {
408                    tracing::warn!(error = %err, "net: rx chain collect failed");
409                    break;
410                }
411            };
412            // Build the wire payload: 12-byte virtio-net header + frame.
413            let mut wire = vec![0u8; VIRTIO_NET_HDR_LEN as usize];
414            wire.extend_from_slice(&frame.bytes);
415            let mut written: u32 = 0;
416            let mut wire_off: usize = 0;
417            for desc in descs {
418                if !desc.is_write_only() {
419                    continue;
420                }
421                let len = (desc.len as usize).min(wire.len() - wire_off);
422                if len == 0 {
423                    continue;
424                }
425                if mem
426                    .write(desc.addr, &wire[wire_off..wire_off + len])
427                    .is_err()
428                {
429                    break;
430                }
431                wire_off += len;
432                written = written.saturating_add(len as u32);
433                if wire_off >= wire.len() {
434                    break;
435                }
436            }
437            if let Err(err) = queue.push_used(mem.as_ref(), head, written) {
438                tracing::warn!(error = %err, "net: rx push_used failed");
439                break;
440            }
441            completed = true;
442        }
443        if completed && let Err(e) = irq.trigger_queue() {
444            tracing::warn!(error = ?e, "net: rx irq trigger failed");
445        }
446    }
447}
448
449impl VirtioDevice for NetDevice {
450    fn device_type(&self) -> VirtioDeviceType {
451        VirtioDeviceType::Net
452    }
453    fn avail_features(&self) -> u64 {
454        self.avail
455    }
456    fn acked_features(&self) -> u64 {
457        self.acked
458    }
459    fn set_acked_features(&mut self, value: u64) {
460        self.acked = value;
461    }
462    fn queue_max_sizes(&self) -> &[u16] {
463        const SIZES: &[u16] = &[QUEUE_MAX_SIZE, QUEUE_MAX_SIZE];
464        SIZES
465    }
466    fn queues(&self) -> &[Queue] {
467        &self.queues
468    }
469    fn queues_mut(&mut self) -> &mut [Queue] {
470        &mut self.queues
471    }
472    fn read_config(&self, offset: u64, data: &mut [u8]) {
473        // Config layout (virtio v1.2 § 5.1.4):
474        //   0x00 u8[6] mac      (only valid if F_MAC negotiated)
475        //   0x06 u16   status   (only valid if F_STATUS — we don't offer)
476        //   0x0A u16   max_virtqueue_pairs (only valid if F_MQ — we don't offer)
477        //   0x0C u16   mtu      (only valid if F_MTU)
478        let mut full = [0u8; 16];
479        if let Some(mac) = self.config.guest_mac {
480            full[0..6].copy_from_slice(&mac);
481        }
482        if let Some(mtu) = self.config.mtu {
483            full[12..14].copy_from_slice(&mtu.to_le_bytes());
484        }
485        let off = offset as usize;
486        for (i, b) in data.iter_mut().enumerate() {
487            *b = full.get(off + i).copied().unwrap_or(0);
488        }
489    }
490    fn write_config(&mut self, _offset: u64, _data: &[u8]) {}
491    fn activate(&mut self, mem: Arc<dyn GuestMemory>, irq: IrqLine) -> Result<(), ActivateError> {
492        let mut state = self.state.lock();
493        state.mem = Some(mem);
494        state.irq = Some(irq);
495        state.activated = true;
496        Ok(())
497    }
498    fn is_activated(&self) -> bool {
499        self.state.lock().activated
500    }
501    fn process_queue(&mut self, queue_index: u16) {
502        match queue_index as usize {
503            TX_QUEUE => {
504                self.drain_tx();
505                // After every TX flush, also drain RX so MMDS responses
506                // injected by the interceptor reach the guest immediately
507                // (rather than waiting for the next host packet to wake the
508                // RX queue).
509                self.drain_rx();
510            }
511            RX_QUEUE => self.drain_rx(),
512            _ => {}
513        }
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use squib_arch::IntId;
520    use squib_core::{GuestAddress, SliceGuestMemory};
521    use squib_gic::Gic;
522
523    use super::*;
524    use crate::queue::VIRTQ_DESC_F_WRITE;
525
526    #[derive(Debug, Default)]
527    struct StubGic;
528    impl Gic for StubGic {
529        fn pulse_spi(&self, _: IntId) -> Result<(), squib_gic::GicError> {
530            Ok(())
531        }
532        fn set_spi_level(&self, _: IntId, _: bool) -> Result<(), squib_gic::GicError> {
533            Ok(())
534        }
535        fn save_state(&self) -> Result<Vec<u8>, squib_gic::GicError> {
536            Ok(Vec::new())
537        }
538        fn restore_state(&self, _data: &[u8]) -> Result<(), squib_gic::GicError> {
539            Ok(())
540        }
541    }
542
543    fn line() -> IrqLine {
544        let gic: Arc<dyn Gic + Send + Sync> = Arc::new(StubGic);
545        IrqLine::new(gic, IntId::from_spi_cell(17).unwrap())
546    }
547
548    fn config() -> NetConfig {
549        NetConfig {
550            iface_id: "eth0".into(),
551            host_dev_name: "tap0".into(),
552            guest_mac: Some([0x06, 0x00, 0xAC, 0x10, 0x00, 0x02]),
553            mtu: Some(1500),
554        }
555    }
556
557    /// Interceptor that records every TX frame it intercepts and drains
558    /// from a pre-loaded RX queue.
559    #[derive(Debug, Default)]
560    struct ReplayInterceptor {
561        intercepted: Mutex<Vec<Frame>>,
562        rx_queue: Mutex<Vec<Frame>>,
563    }
564    impl FrameInterceptor for ReplayInterceptor {
565        fn intercept_tx(&self, frame: &Frame) -> bool {
566            // Anything addressed to MAC 06:01:23:45:67:01 (MMDS synthetic).
567            if frame.bytes.len() >= 6
568                && &frame.bytes[..6] == [0x06, 0x01, 0x23, 0x45, 0x67, 0x01].as_slice()
569            {
570                self.intercepted.lock().push(frame.clone());
571                true
572            } else {
573                false
574            }
575        }
576        fn drain_rx(&self) -> Vec<Frame> {
577            std::mem::take(&mut *self.rx_queue.lock())
578        }
579    }
580
581    #[test]
582    fn test_should_acquire_and_release_buffers_via_frame_pool() {
583        let pool = FramePool::new(1500, 4);
584        assert_eq!(pool.free_count(), 0);
585        let buf1 = pool.acquire();
586        assert_eq!(buf1.len(), 0);
587        assert!(buf1.capacity() >= 1500);
588        let buf2 = pool.acquire();
589        assert_eq!(pool.free_count(), 0);
590        pool.release(buf1);
591        pool.release(buf2);
592        assert_eq!(pool.free_count(), 2);
593        // The next acquire reuses one of the pooled buffers, no fresh alloc.
594        let _ = pool.acquire();
595        assert_eq!(pool.free_count(), 1);
596    }
597
598    #[test]
599    fn test_should_drop_releases_beyond_pool_capacity() {
600        let pool = FramePool::new(1500, 2);
601        pool.release(bytes::BytesMut::with_capacity(1500));
602        pool.release(bytes::BytesMut::with_capacity(1500));
603        // Third release exceeds capacity → dropped, free_count stays at 2.
604        pool.release(bytes::BytesMut::with_capacity(1500));
605        assert_eq!(pool.free_count(), 2);
606    }
607
608    #[test]
609    fn test_should_clear_acquired_buffer_so_caller_writes_into_empty() {
610        let pool = FramePool::new(1500, 2);
611        let mut b = pool.acquire();
612        b.extend_from_slice(b"hello");
613        assert_eq!(b.len(), 5);
614        pool.release(b);
615        let b = pool.acquire();
616        assert_eq!(
617            b.len(),
618            0,
619            "pool must clear on acquire so the caller writes into an empty buffer"
620        );
621    }
622
623    #[test]
624    fn test_should_freeze_bytesmut_into_frame_via_from_buf() {
625        let mut buf = bytes::BytesMut::with_capacity(8);
626        buf.extend_from_slice(b"abcdef");
627        let frame = Frame::from_buf(buf);
628        assert_eq!(frame.bytes.as_ref(), b"abcdef");
629        assert_eq!(frame.len(), 6);
630    }
631
632    #[test]
633    fn test_should_offer_mac_feature_when_config_supplies_one() {
634        let dev = NetDevice::new(
635            config(),
636            Arc::new(LoopbackBackend::default()),
637            Arc::new(NoopInterceptor),
638        );
639        assert_ne!(dev.avail_features() & F_MAC, 0);
640    }
641
642    #[test]
643    fn test_should_publish_mac_in_config_space() {
644        let dev = NetDevice::new(
645            config(),
646            Arc::new(LoopbackBackend::default()),
647            Arc::new(NoopInterceptor),
648        );
649        let mut got = [0u8; 6];
650        dev.read_config(0, &mut got);
651        assert_eq!(got, [0x06, 0x00, 0xAC, 0x10, 0x00, 0x02]);
652    }
653
654    /// One-way sink backend: records every `send()` for assertions. `recv()`
655    /// always returns empty so the tx-then-rx drain in `process_queue` does
656    /// not recycle frames back into the test.
657    #[derive(Debug, Default)]
658    struct CapturedBackend {
659        sent: Mutex<Vec<Frame>>,
660    }
661    impl NetBackend for CapturedBackend {
662        fn send(&self, frame: &Frame) {
663            self.sent.lock().push(frame.clone());
664        }
665        fn recv(&self) -> Vec<Frame> {
666            Vec::new()
667        }
668    }
669
670    #[test]
671    fn test_should_send_tx_frames_to_backend_when_no_interception() {
672        let backend = Arc::new(CapturedBackend::default());
673        let mut dev = NetDevice::new(config(), backend.clone(), Arc::new(NoopInterceptor));
674        let mem = Arc::new(SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x4000));
675        let q = &mut dev.queues_mut()[TX_QUEUE];
676        q.size = 8;
677        q.desc_table_addr = GuestAddress(0x4000_0000);
678        q.avail_ring_addr = GuestAddress(0x4000_0800);
679        q.used_ring_addr = GuestAddress(0x4000_1000);
680        q.ready = true;
681        // Frame buffer at 0x4000_2000: 12-byte virtio header + 8-byte payload.
682        let mut payload = vec![0u8; 12];
683        payload.extend_from_slice(b"helloeth");
684        mem.write(GuestAddress(0x4000_2000), &payload).unwrap();
685        let base = 0x4000_0000u64;
686        mem.write_u32_le(GuestAddress(base), 0x4000_2000).unwrap();
687        mem.write_u32_le(GuestAddress(base + 4), 0).unwrap();
688        mem.write_u32_le(GuestAddress(base + 8), payload.len() as u32)
689            .unwrap();
690        mem.write_u16_le(GuestAddress(base + 12), 0).unwrap();
691        mem.write_u16_le(GuestAddress(base + 14), 0).unwrap();
692        mem.write_u16_le(GuestAddress(0x4000_0804), 0).unwrap();
693        mem.write_u16_le(GuestAddress(0x4000_0802), 1).unwrap();
694        dev.activate(mem.clone(), line()).unwrap();
695        dev.process_queue(TX_QUEUE as u16);
696        let sent = backend.sent.lock().clone();
697        assert_eq!(sent.len(), 1);
698        assert_eq!(sent[0].bytes.as_ref(), b"helloeth");
699    }
700
701    #[test]
702    fn test_should_intercept_tx_frames_when_interceptor_claims_them() {
703        let backend = Arc::new(CapturedBackend::default());
704        let interceptor = Arc::new(ReplayInterceptor::default());
705        let mut dev = NetDevice::new(config(), backend.clone(), interceptor.clone());
706        let mem = Arc::new(SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x4000));
707        let q = &mut dev.queues_mut()[TX_QUEUE];
708        q.size = 8;
709        q.desc_table_addr = GuestAddress(0x4000_0000);
710        q.avail_ring_addr = GuestAddress(0x4000_0800);
711        q.used_ring_addr = GuestAddress(0x4000_1000);
712        q.ready = true;
713        // Header + frame whose first 6 bytes are the MMDS synthetic MAC.
714        let mut payload = vec![0u8; 12];
715        payload.extend_from_slice(&[0x06, 0x01, 0x23, 0x45, 0x67, 0x01]);
716        payload.extend_from_slice(b"rest");
717        mem.write(GuestAddress(0x4000_2000), &payload).unwrap();
718        let base = 0x4000_0000u64;
719        mem.write_u32_le(GuestAddress(base), 0x4000_2000).unwrap();
720        mem.write_u32_le(GuestAddress(base + 4), 0).unwrap();
721        mem.write_u32_le(GuestAddress(base + 8), payload.len() as u32)
722            .unwrap();
723        mem.write_u16_le(GuestAddress(base + 12), 0).unwrap();
724        mem.write_u16_le(GuestAddress(base + 14), 0).unwrap();
725        mem.write_u16_le(GuestAddress(0x4000_0804), 0).unwrap();
726        mem.write_u16_le(GuestAddress(0x4000_0802), 1).unwrap();
727        dev.activate(mem.clone(), line()).unwrap();
728        dev.process_queue(TX_QUEUE as u16);
729        // I-MMDS-1: intercepted frames MUST NOT reach the host backend.
730        assert!(backend.sent.lock().is_empty());
731        assert_eq!(interceptor.intercepted.lock().len(), 1);
732    }
733
734    #[test]
735    fn test_should_inject_rx_frames_from_interceptor_with_virtio_header_prepended() {
736        let backend = Arc::new(LoopbackBackend::default());
737        let interceptor = Arc::new(ReplayInterceptor::default());
738        interceptor
739            .rx_queue
740            .lock()
741            .push(Frame::from_slice(b"hello-rx"));
742        let mut dev = NetDevice::new(config(), backend.clone(), interceptor.clone());
743        let mem = Arc::new(SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x4000));
744        let q = &mut dev.queues_mut()[RX_QUEUE];
745        q.size = 8;
746        q.desc_table_addr = GuestAddress(0x4000_0000);
747        q.avail_ring_addr = GuestAddress(0x4000_0800);
748        q.used_ring_addr = GuestAddress(0x4000_1000);
749        q.ready = true;
750        // Single device-write descriptor of 32 bytes at 0x4000_2000.
751        let base = 0x4000_0000u64;
752        mem.write_u32_le(GuestAddress(base), 0x4000_2000).unwrap();
753        mem.write_u32_le(GuestAddress(base + 4), 0).unwrap();
754        mem.write_u32_le(GuestAddress(base + 8), 32).unwrap();
755        mem.write_u16_le(GuestAddress(base + 12), VIRTQ_DESC_F_WRITE)
756            .unwrap();
757        mem.write_u16_le(GuestAddress(base + 14), 0).unwrap();
758        mem.write_u16_le(GuestAddress(0x4000_0804), 0).unwrap();
759        mem.write_u16_le(GuestAddress(0x4000_0802), 1).unwrap();
760        dev.activate(mem.clone(), line()).unwrap();
761        dev.process_queue(RX_QUEUE as u16);
762        // First 12 bytes are the (zeroed) virtio-net header; payload follows.
763        let mut got = [0u8; 20];
764        mem.read(GuestAddress(0x4000_2000), &mut got).unwrap();
765        assert_eq!(&got[0..12], &[0u8; 12]); // header zeroed
766        assert_eq!(&got[12..20], b"hello-rx");
767    }
768}