ktstr/vmm/
net_config.rs

1//! Network configuration for the virtio-net device.
2//!
3//! [`NetConfig`] is the descriptor — passed by value, copious defaults,
4//! no fd field (the framework owns the in-VMM loopback backend's
5//! lifecycle). Mirrors [`super::disk_config::DiskConfig`] for API
6//! consistency: chainable setters return `Self`, `Default::default()`
7//! produces a working device, and the type lives in the public prelude
8//! so test authors can spell it out.
9//!
10//! v0 backend is in-VMM loopback: TX descriptor bytes are written
11//! directly into RX descriptors and the irqfd fires. There is no host
12//! networking, no `/dev/net/tun`, no AF_PACKET fd. Each attached
13//! virtio-net interface (one per `networks = [..]` element on x86_64,
14//! a single MMIO NIC on aarch64) loops its own TX back to its RX
15//! verbatim — no MAC swap, no ARP synthesis, no IP routing. The byte
16//! flow lives in [`super::virtio_net`] (see `process_tx_loopback` in
17//! the device module). AF_PACKET raw sockets bound to the interface
18//! generate real virtio TX kicks and observe real virtio RX
19//! interrupts — IP-layer self-traffic is forced onto `lo` by the
20//! kernel routing layer (`net/ipv4/route.c::ip_route_output_key_hash_rcu`
21//! resolves any local destination as `RTN_LOCAL` →
22//! `dev_out = net->loopback_dev`) and never reaches virtio-net.
23
24/// Configuration for the virtio-net device attached to the VM.
25///
26/// `Default::default()` produces a working device with a deterministic
27/// locally-administered MAC. Override the MAC with [`Self::mac`] to
28/// pin a value across runs (useful for log correlation against
29/// AF_PACKET captures).
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
31pub struct NetConfig {
32    /// MAC address advertised to the guest via `VIRTIO_NET_F_MAC`.
33    /// The locally-administered bit (0x02) is set in the default to
34    /// avoid collisions with real-hardware OUIs; operators that
35    /// override the MAC are responsible for the bit themselves.
36    pub mac: [u8; 6],
37    /// Number of virtio-net queue-pairs (one RX + one TX virtqueue per
38    /// pair) the device offers. Default `1` — a single queue-pair,
39    /// byte-identical to a device with no multiqueue support (no
40    /// `VIRTIO_NET_F_MQ`, no control virtqueue).
41    ///
42    /// Multiqueue is offered only when `queue_pairs > 1` AND the transport
43    /// carries MSI-X (the x86_64 PCI NIC). On a non-MSI-X transport — the
44    /// aarch64 MMIO NIC, or PCI without MSI-X — the device stays single-pair
45    /// regardless of this value (no `VIRTIO_NET_F_MQ`, no control vq,
46    /// `max_virtqueue_pairs = 0`): per-queue IRQ steering, the point of
47    /// multiqueue, needs the distinct MSI-X vectors that transport lacks.
48    ///
49    /// When multiqueue IS offered, the device advertises `VIRTIO_NET_F_MQ` +
50    /// the control virtqueue (`VIRTIO_NET_F_CTRL_VQ`) and reports
51    /// `max_virtqueue_pairs` in config space; the guest brings up
52    /// `min(num_online_cpus, queue_pairs)` pairs and spreads its RX/TX across
53    /// them.
54    ///
55    /// Clamped to `[1, 256]` at construction: `0` becomes `1` (the spec
56    /// minimum, `VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN`) and values above `256`
57    /// (`MAX_QUEUE_PAIRS`) are clamped down.
58    pub queue_pairs: u16,
59}
60
61impl NetConfig {
62    /// Const default — MAC `02:00:00:00:00:01`. The leading `0x02` sets
63    /// the locally-administered bit per IEEE 802 (bit 1 of the first
64    /// octet), keeping the address out of the IEEE OUI namespace; the
65    /// trailing `0x01` is the conventional first-NIC suffix — multi-NIC
66    /// tests give each element a distinct MAC via [`Self::mac`]. `const`
67    /// so it can seed a `const NetConfig` for the
68    /// `#[ktstr_test(networks = [...])]` attribute, matching
69    /// [`super::disk_config::DiskConfig`]'s `DEFAULT`.
70    pub const DEFAULT: NetConfig = NetConfig {
71        mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
72        queue_pairs: 1,
73    };
74
75    /// Override the advertised MAC. Returns `self` for chained
76    /// configuration. `const fn` so a `const NetConfig` can be built via
77    /// `NetConfig::DEFAULT.mac(...)`, matching `DiskConfig`'s const-fn
78    /// builder style.
79    #[must_use = "builder methods consume self; bind the result"]
80    pub const fn mac(mut self, mac: [u8; 6]) -> Self {
81        self.mac = mac;
82        self
83    }
84
85    /// Set the number of queue-pairs the device offers (see
86    /// [`Self::queue_pairs`]). Returns `self` for chained configuration.
87    /// `const fn` so a `const NetConfig` can be built via
88    /// `NetConfig::DEFAULT.queue_pairs(...)`, matching [`Self::mac`].
89    #[must_use = "builder methods consume self; bind the result"]
90    pub const fn queue_pairs(mut self, pairs: u16) -> Self {
91        self.queue_pairs = pairs;
92        self
93    }
94}
95
96impl Default for NetConfig {
97    /// Delegates to [`Self::DEFAULT`] (MAC `02:00:00:00:00:01`).
98    fn default() -> Self {
99        Self::DEFAULT
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn default_has_locally_administered_bit() {
109        let cfg = NetConfig::default();
110        assert_eq!(
111            cfg.mac[0] & 0x02,
112            0x02,
113            "default MAC must have locally-administered bit (IEEE 802 first-octet bit 1)",
114        );
115    }
116
117    #[test]
118    fn default_is_unicast() {
119        let cfg = NetConfig::default();
120        assert_eq!(
121            cfg.mac[0] & 0x01,
122            0x00,
123            "default MAC must not be multicast (IEEE 802 first-octet bit 0)",
124        );
125    }
126
127    #[test]
128    fn mac_setter_overrides_default() {
129        let cfg = NetConfig::default().mac([0x52, 0x54, 0x00, 0xab, 0xcd, 0xef]);
130        assert_eq!(cfg.mac, [0x52, 0x54, 0x00, 0xab, 0xcd, 0xef]);
131    }
132
133    #[test]
134    fn serde_roundtrip_pins_field_names() {
135        let cfg = NetConfig::default().mac([1, 2, 3, 4, 5, 6]).queue_pairs(4);
136        let json = serde_json::to_string(&cfg).expect("serialize");
137        // Pin the field names so a future rename surfaces here.
138        assert!(json.contains("\"mac\""), "missing key `mac`: {json}");
139        assert!(
140            json.contains("\"queue_pairs\""),
141            "missing key `queue_pairs`: {json}"
142        );
143        let parsed: NetConfig = serde_json::from_str(&json).expect("deserialize");
144        assert_eq!(parsed, cfg);
145    }
146}