Skip to main content

strat9_kernel/hardware/nic/
virtio_net.rs

1//! VirtIO Network Device driver
2//!
3//! Provides network I/O via VirtIO-net protocol for QEMU/KVM environments.
4//! Implements the common [`crate::hardware::nic::NetworkDevice`] trait so
5//! this driver plugs into the unified `/dev/net/` scheme.
6//!
7//! Reference: VirtIO spec v1.2, Section 5.1 (Network Device)
8//! https://docs.oasis-open.org/virtio/virtio/v1.4/cs01/virtio-v1.4-cs01.html#x1-2700001
9
10use crate::{
11    arch::x86_64::pci::{self, PciDevice},
12    hardware::{
13        nic as net,
14        virtio::{
15            common::{VirtioDevice, Virtqueue},
16            status,
17        },
18    },
19    memory::{self, PhysFrame},
20    sync::{FixedQueue, SpinLock},
21};
22use alloc::sync::Arc;
23use core::{mem, ptr, sync::atomic::Ordering};
24use endian_num::Le;
25use net_core::{NetError, NetworkDevice};
26use spin::RwLock as SpinRwLock;
27
28/// VirtIO net header size (12 bytes with MRG_RXBUF, 10 bytes without).
29/// Determined at runtime during feature negotiation.
30static NET_HDR_SIZE: core::sync::atomic::AtomicUsize =
31    core::sync::atomic::AtomicUsize::new(mem::size_of::<VirtioNetHeader>());
32const RX_FRAME_TRACK_CAPACITY: usize = 128;
33
34/// VirtIO net device features
35pub mod features {
36    pub const VIRTIO_NET_F_CSUM: u32 = 1 << 0;
37    pub const VIRTIO_NET_F_GUEST_CSUM: u32 = 1 << 1;
38    pub const VIRTIO_NET_F_MAC: u32 = 1 << 5;
39    pub const VIRTIO_NET_F_GSO: u32 = 1 << 6;
40    pub const VIRTIO_NET_F_GUEST_TSO4: u32 = 1 << 7;
41    pub const VIRTIO_NET_F_GUEST_TSO6: u32 = 1 << 8;
42    pub const VIRTIO_NET_F_GUEST_ECN: u32 = 1 << 9;
43    pub const VIRTIO_NET_F_GUEST_UFO: u32 = 1 << 10;
44    pub const VIRTIO_NET_F_HOST_TSO4: u32 = 1 << 11;
45    pub const VIRTIO_NET_F_HOST_TSO6: u32 = 1 << 12;
46    pub const VIRTIO_NET_F_HOST_ECN: u32 = 1 << 13;
47    pub const VIRTIO_NET_F_HOST_UFO: u32 = 1 << 14;
48    pub const VIRTIO_NET_F_MRG_RXBUF: u32 = 1 << 15;
49    pub const VIRTIO_NET_F_STATUS: u32 = 1 << 16;
50    pub const VIRTIO_NET_F_CTRL_VQ: u32 = 1 << 17;
51    pub const VIRTIO_NET_F_CTRL_RX: u32 = 1 << 18;
52    pub const VIRTIO_NET_F_CTRL_VLAN: u32 = 1 << 19;
53    pub const VIRTIO_NET_F_GUEST_ANNOUNCE: u32 = 1 << 21;
54    pub const VIRTIO_NET_F_MQ: u32 = 1 << 22;
55}
56
57/// VirtIO net status flags
58pub mod net_status {
59    pub const VIRTIO_NET_S_LINK_UP: u16 = 1;
60    pub const VIRTIO_NET_S_ANNOUNCE: u16 = 2;
61}
62
63/// VirtIO net header (prepended to every packet)
64///
65/// Fields are little-endian as mandated by the VirtIO spec
66/// https://docs.oasis-open.org/virtio/virtio/v1.4/virtio-v1.4.html#x1-2810006
67#[repr(C)]
68#[derive(Debug, Clone, Copy, Default)]
69pub struct VirtioNetHeader {
70    pub flags: u8,
71    pub gso_type: u8,
72    pub hdr_len: Le<u16>,
73    pub gso_size: Le<u16>,
74    pub csum_start: Le<u16>,
75    pub csum_offset: Le<u16>,
76    pub num_buffers: Le<u16>,
77}
78
79/// VirtIO Network Device driver
80pub struct VirtioNetDevice {
81    device: VirtioDevice,
82    rx_queue: SpinLock<Virtqueue>,
83    tx_queue: SpinLock<Virtqueue>,
84    mac_address: [u8; 6],
85    pub rx_frames: SpinLock<FixedQueue<(PhysFrame, u8), RX_FRAME_TRACK_CAPACITY>>, // Track allocated RX frames
86}
87
88// Send and Sync are safe because we use SpinLocks
89unsafe impl Send for VirtioNetDevice {}
90unsafe impl Sync for VirtioNetDevice {}
91
92impl VirtioNetDevice {
93    /// Initialize a VirtIO network device from a PCI device
94    pub unsafe fn new(pci_dev: PciDevice) -> Result<Self, &'static str> {
95        log::info!("VirtIO-net: Initializing device at {:?}", pci_dev.address);
96
97        // Create VirtIO device
98        let device = VirtioDevice::new(pci_dev)?;
99
100        // Reset device
101        device.reset();
102
103        // Acknowledge device
104        device.add_status(status::ACKNOWLEDGE as u8);
105
106        // Indicate we know how to drive it
107        device.add_status(status::DRIVER as u8);
108
109        // Read and negotiate features
110        let device_features = device.read_device_features();
111        let needed = features::VIRTIO_NET_F_MAC | features::VIRTIO_NET_F_STATUS;
112        // VIRTIO_NET_F_MRG_RXBUF: requested so the device uses the 12-byte
113        // virtio_net_hdr_v1 layout (with num_buffers) that matches our
114        // VirtioNetHeader struct. Without it the legacy 10-byte header would
115        // shift every packet by 2 bytes, corrupting all data.
116        let desired = needed | features::VIRTIO_NET_F_MRG_RXBUF;
117        if device_features & needed != needed {
118            return Err("Device lacks mandatory MAC/STATUS features");
119        }
120        let guest_features = device_features & desired;
121        device.write_guest_features(guest_features);
122
123        // Features OK
124        device.add_status(status::FEATURES_OK as u8);
125
126        // Double-check that FEATURES_OK stuck
127        if device.get_status() & (status::FEATURES_OK as u8) == 0 {
128            return Err("Device rejected our feature set");
129        }
130
131        // Read back negotiated features to determine actual header size.
132        // With VIRTIO_NET_F_MRG_RXBUF the header is 12 bytes (virtio_net_hdr_v1);
133        // without it the legacy 10-byte header (virtio_net_hdr) is used.
134        let negotiated = device.read_device_features();
135        if negotiated & features::VIRTIO_NET_F_MRG_RXBUF != 0 {
136            NET_HDR_SIZE.store(mem::size_of::<VirtioNetHeader>(), Ordering::Release);
137        } else {
138            // Legacy 10-byte header: num_buffers field is absent.
139            NET_HDR_SIZE.store(10, Ordering::Release);
140        }
141
142        // Create virtqueues
143        // Queue 0: RX (receive)
144        // Queue 1: TX (transmit)
145        let rx_queue = Virtqueue::new(128)?;
146        let tx_queue = Virtqueue::new(128)?;
147
148        // Setup queues with device
149        device.setup_queue(0, &rx_queue);
150        device.setup_queue(1, &tx_queue);
151
152        // Read MAC address from device config space
153        // For legacy devices, MAC is at offset 20 + 0
154        let mut mac_address = [0u8; 6];
155        for i in 0..6 {
156            mac_address[i] = device.read_reg_u8(20 + i as u16);
157        }
158
159        log::info!(
160            "VirtIO-net: MAC address: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
161            mac_address[0],
162            mac_address[1],
163            mac_address[2],
164            mac_address[3],
165            mac_address[4],
166            mac_address[5]
167        );
168
169        // Driver ready
170        device.add_status(status::DRIVER_OK as u8);
171
172        let net_device = Self {
173            device,
174            rx_queue: SpinLock::new(rx_queue),
175            tx_queue: SpinLock::new(tx_queue),
176            mac_address,
177            rx_frames: SpinLock::new(FixedQueue::new()),
178        };
179
180        // Fill RX queue with buffers
181        net_device.refill_rx_queue()?;
182
183        Ok(net_device)
184    }
185
186    /// Fill the RX queue with receive buffers
187    fn refill_rx_queue(&self) -> Result<(), &'static str> {
188        let mut rx_queue = self.rx_queue.lock();
189        let mut rx_frames = self.rx_frames.lock();
190
191        // We want to keep some buffers in the RX queue
192        let current_filled = rx_frames.len();
193        let target_filled = 64;
194        let mut added = 0usize;
195
196        if current_filled >= target_filled {
197            return Ok(());
198        }
199
200        for _ in 0..(target_filled - current_filled) {
201            // Allocate buffer for header + MTU
202            let buf_size = NET_HDR_SIZE.load(Ordering::Relaxed) + net::MTU;
203            let buf_pages = (buf_size + 4095) / 4096;
204            let buf_order = buf_pages.next_power_of_two().trailing_zeros() as u8;
205
206            let buf_frame = match crate::sync::with_irqs_disabled(|token| {
207                memory::allocate_phys_contiguous(token, buf_order)
208            }) {
209                Ok(frame) => frame,
210                Err(_) => break, // No more memory available
211            };
212
213            let buf_addr = buf_frame.start_address.as_u64();
214            let virt_addr = crate::memory::phys_to_virt(buf_addr);
215
216            // Zero the buffer (header needs to be zeroed mostly)
217            unsafe {
218                ptr::write_bytes(virt_addr as *mut u8, 0, buf_size);
219            }
220
221            // Add buffer to RX queue (device Writable)
222            match rx_queue.add_buffer(&[(buf_addr, buf_size as u32, true)]) {
223                Ok(_) => {
224                    if rx_frames.push_back((buf_frame, buf_order)).is_err() {
225                        crate::sync::with_irqs_disabled(|token| {
226                            memory::free_phys_contiguous(token, buf_frame, buf_order);
227                        });
228                        break;
229                    }
230                    added += 1;
231                }
232                Err(_) => {
233                    // Queue full, free the buffer
234                    crate::sync::with_irqs_disabled(|token| {
235                        memory::free_phys_contiguous(token, buf_frame, buf_order);
236                    });
237                    break;
238                }
239            }
240        }
241
242        // Notify device about new RX buffers
243        if rx_queue.should_notify() {
244            self.device.notify_queue(0);
245        }
246
247        if rx_frames.is_empty() && current_filled == 0 && added == 0 {
248            return Err("Failed to allocate RX buffers");
249        }
250
251        Ok(())
252    }
253
254    /// Read link status from device
255    fn read_link_status(&self) -> u16 {
256        // Status is at offset 6 in device-specific config (offset 20 + 6 = 26)
257        self.device.read_reg_u16(26)
258    }
259}
260
261impl NetworkDevice for VirtioNetDevice {
262    /// Performs the name operation.
263    fn name(&self) -> &str {
264        "virtio-net"
265    }
266
267    /// Performs the receive operation.
268    fn receive(&self, buf: &mut [u8]) -> Result<usize, NetError> {
269        let mut rx_queue = self.rx_queue.lock();
270
271        // Check if there's a used buffer
272        if !rx_queue.has_used() {
273            let (dev_idx, drv_idx) = rx_queue.used_indices();
274            log::info!(
275                "[vtnet] rx: no used buf (used.idx={}, last_used={})",
276                dev_idx,
277                drv_idx,
278            );
279            return Err(NetError::NoPacket);
280        }
281
282        let hdr_size = NET_HDR_SIZE.load(Ordering::Relaxed);
283        let (token, len) = rx_queue.get_used().ok_or(NetError::NoPacket)?;
284
285        let _desc_index = token as usize;
286        let _desc_table = rx_queue.desc_area(); // Physical address
287
288        let (frame, order) = self
289            .rx_frames
290            .lock()
291            .pop_front()
292            .ok_or(NetError::NotReady)?;
293
294        let buf_addr = frame.start_address.as_u64();
295        let virt_addr = crate::memory::phys_to_virt(buf_addr);
296
297        let header_ptr = virt_addr as *const VirtioNetHeader;
298        let data_ptr = (virt_addr + hdr_size as u64) as *const u8;
299
300        let header = unsafe { ptr::read(header_ptr) };
301        let packet_len = (len as usize).saturating_sub(hdr_size);
302
303        // Dump first 16 bytes of packet payload for diagnostics
304        let mut dump = [0u8; 16];
305        if packet_len >= 16 {
306            unsafe {
307                ptr::copy_nonoverlapping(data_ptr, dump.as_mut_ptr(), 16);
308            }
309        }
310        log::info!(
311            "[vtnet] rx: token={} len={} hdr={} pkt={} flags={} num_buf={}",
312            token,
313            len,
314            hdr_size,
315            packet_len,
316            header.flags,
317            header.num_buffers,
318        );
319        log::info!(
320            "[vtnet] rx: pkt[0..16] = {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} ...",
321            dump[0],
322            dump[1],
323            dump[2],
324            dump[3],
325            dump[4],
326            dump[5],
327            dump[6],
328            dump[7],
329        );
330
331        if buf.len() < packet_len {
332            // Buffer too small, packet lost
333            crate::sync::with_irqs_disabled(|token| {
334                memory::free_phys_contiguous(token, frame, order);
335            });
336            drop(rx_queue);
337            // We still need to refill.
338            let _ = self.refill_rx_queue();
339            return Err(NetError::BufferTooSmall);
340        }
341
342        // Copy packet data
343        if packet_len > 0 {
344            unsafe {
345                ptr::copy_nonoverlapping(data_ptr, buf.as_mut_ptr(), packet_len);
346            }
347        }
348
349        // Free the frame
350        crate::sync::with_irqs_disabled(|token| {
351            memory::free_phys_contiguous(token, frame, order);
352        });
353        drop(rx_queue);
354
355        // Refill RX queue
356        let _ = self.refill_rx_queue();
357
358        Ok(packet_len)
359    }
360
361    /// Performs the transmit operation.
362    fn transmit(&self, buf: &[u8]) -> Result<(), NetError> {
363        if buf.len() > net::MTU {
364            return Err(NetError::BufferTooSmall);
365        }
366
367        // Allocate TX buffer (header + data)
368        let buf_size = NET_HDR_SIZE.load(Ordering::Relaxed) + buf.len();
369        let buf_pages = (buf_size + 4095) / 4096;
370        let buf_order = buf_pages.next_power_of_two().trailing_zeros() as u8;
371
372        let buf_frame = crate::sync::with_irqs_disabled(|token| {
373            memory::allocate_phys_contiguous(token, buf_order)
374        })
375        .map_err(|_| NetError::NotReady)?;
376
377        let buf_addr = buf_frame.start_address.as_u64();
378        let virt_addr = crate::memory::phys_to_virt(buf_addr);
379
380        let header_ptr = virt_addr as *mut VirtioNetHeader;
381        let data_ptr = (virt_addr + NET_HDR_SIZE.load(Ordering::Relaxed) as u64) as *mut u8;
382
383        // Write header
384        unsafe {
385            ptr::write(header_ptr, VirtioNetHeader::default());
386            ptr::copy_nonoverlapping(buf.as_ptr(), data_ptr, buf.len());
387        }
388
389        // Submit to TX queue
390        let mut tx_queue = self.tx_queue.lock();
391        let _ = tx_queue
392            .add_buffer(&[(buf_addr, buf_size as u32, false)]) // Device Readable
393            .map_err(|_| {
394                // Free buffer if queue is full
395                crate::sync::with_irqs_disabled(|token| {
396                    memory::free_phys_contiguous(token, buf_frame, buf_order);
397                });
398                NetError::TxQueueFull
399            })?;
400
401        log::info!("[vtnet] tx: submit {} bytes @ {:#x}", buf_size, buf_addr,);
402
403        if tx_queue.should_notify() {
404            self.device.notify_queue(1);
405        }
406        drop(tx_queue);
407
408        // Non-blocking: return immediately without waiting for TX completion.
409        // The device will DMA the packet from the buffer and signal completion
410        // via the used ring. The buffer is intentionally leaked for now to keep
411        // the transmit path simple and non-blocking.
412        // TODO: track pending TX buffers and free them after device completion.
413
414        Ok(())
415    }
416
417    /// Performs the mac address operation.
418    fn mac_address(&self) -> [u8; 6] {
419        self.mac_address
420    }
421
422    /// Performs the link up operation.
423    fn link_up(&self) -> bool {
424        let status = self.read_link_status();
425        status & net_status::VIRTIO_NET_S_LINK_UP != 0
426    }
427}
428
429/// Global VirtIO network device
430static VIRTIO_NET: SpinRwLock<Option<Arc<VirtioNetDevice>>> = SpinRwLock::new(None);
431
432/// Initialize VirtIO network device and register it in the global net registry.
433pub fn init() {
434    log::info!("VirtIO-net: Scanning for devices...");
435
436    // Prefer strict class-based probe (network/ethernet), with fallback to
437    // vendor+device for odd firmware/virtual setups.
438    let pci_dev = match pci::probe_first(pci::ProbeCriteria {
439        vendor_id: Some(pci::vendor::VIRTIO),
440        device_id: Some(pci::device::VIRTIO_NET),
441        class_code: Some(pci::class::NETWORK),
442        subclass: Some(pci::net_subclass::ETHERNET),
443        prog_if: None,
444    })
445    .or_else(|| pci::find_virtio_device(pci::device::VIRTIO_NET))
446    {
447        Some(dev) => dev,
448        None => {
449            log::warn!("VirtIO-net: No network device found");
450            return;
451        }
452    };
453
454    match unsafe { VirtioNetDevice::new(pci_dev) } {
455        Ok(device) => {
456            let arc = Arc::new(device);
457            *VIRTIO_NET.write() = Some(arc.clone());
458            net::register_device(arc);
459        }
460        Err(e) => {
461            log::error!("VirtIO-net: Failed to initialize device: {}", e);
462        }
463    }
464}
465
466/// Get the VirtIO network device instance (if present).
467pub fn get_device() -> Option<Arc<VirtioNetDevice>> {
468    VIRTIO_NET.read().clone()
469}