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}