| /* |
| * Copyright (C) 2026 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301, USA. |
| * |
| * You can also choose to distribute this program under the terms of |
| * the Unmodified Binary Distribution Licence (as given in the file |
| * COPYING.UBDL), provided that you have satisfied its requirements. |
| */ |
| |
| FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); |
| FILE_SECBOOT ( PERMITTED ); |
| |
| #include <stdint.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <errno.h> |
| #include <byteswap.h> |
| #include <ipxe/netdevice.h> |
| #include <ipxe/ethernet.h> |
| #include <ipxe/if_ether.h> |
| #include <ipxe/iobuf.h> |
| #include <ipxe/malloc.h> |
| #include <ipxe/pci.h> |
| #include "virtio-net.h" |
| |
| /** @file |
| * |
| * Virtual I/O network device |
| * |
| */ |
| |
| /** Supported features */ |
| const struct virtio_features virtio_net_features = { |
| .word = { |
| ( VIRTIO_FEAT0_ANY_LAYOUT | |
| VIRTIO_FEAT0_NET_MTU | |
| VIRTIO_FEAT0_NET_MAC ), |
| ( VIRTIO_FEAT1_MODERN ), |
| }, |
| }; |
| |
| /****************************************************************************** |
| * |
| * Device-specific registers |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Get MAC address |
| * |
| * @v netdev Network device |
| */ |
| static void virtio_net_mac ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_device *virtio = &vnet->virtio; |
| uint32_t has_mac; |
| unsigned int i; |
| |
| /* Read MAC address from device registers */ |
| for ( i = 0 ; i < ETH_ALEN ; i++ ) { |
| netdev->hw_addr[i] = ioread8 ( virtio->device + |
| VIRTIO_NET_MAC + i ); |
| } |
| |
| /* Use random MAC address if undefined or invalid */ |
| has_mac = ( virtio->features.word[0] & VIRTIO_FEAT0_NET_MAC ); |
| if ( ! ( has_mac && is_valid_ether_addr ( netdev->hw_addr ) ) ) { |
| DBGC ( vnet, "VNET %s has %s MAC address\n", |
| virtio->name, ( has_mac ? "invalid" : "no" ) ); |
| eth_random_addr ( netdev->hw_addr ); |
| } |
| } |
| |
| /** |
| * Get MTU |
| * |
| * @v netdev Network device |
| */ |
| static void virtio_net_mtu ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_device *virtio = &vnet->virtio; |
| uint32_t has_mtu; |
| |
| /* Read MTU from device registers, if available */ |
| has_mtu = ( virtio->features.word[0] & VIRTIO_FEAT0_NET_MTU ); |
| if ( has_mtu ) { |
| netdev->mtu = ioread16 ( virtio->device + VIRTIO_NET_MTU ); |
| netdev->max_pkt_len = ( netdev->mtu + ETH_HLEN ); |
| DBGC ( vnet, "VNET %s has MTU %zd\n", |
| virtio->name, netdev->mtu ); |
| } |
| } |
| |
| /****************************************************************************** |
| * |
| * Queue management |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Enable queue |
| * |
| * @v vnet Virtio network device |
| * @v queue Virtio network queue |
| * @ret rc Return status code |
| */ |
| static int virtio_net_enable ( struct virtio_net *vnet, |
| struct virtio_net_queue *queue ) { |
| struct virtio_device *virtio = &vnet->virtio; |
| struct virtio_desc *desc; |
| unsigned int count; |
| unsigned int max; |
| unsigned int fill; |
| unsigned int slot; |
| unsigned int index; |
| unsigned int write; |
| int rc; |
| |
| /* Map packet header */ |
| if ( ( rc = dma_map ( virtio->dma, &queue->map, &queue->hdr, |
| sizeof ( queue->hdr ), queue->dma ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s Q%d could not map header: %s\n", |
| virtio->name, queue->queue.index, strerror ( rc ) ); |
| goto err_map; |
| } |
| |
| /* Enable queue */ |
| count = ( queue->count * VIRTIO_NET_DESCS ); |
| if ( ( rc = virtio_enable ( virtio, &queue->queue, count ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s Q%d could not initialise: %s\n", |
| virtio->name, queue->queue.index, strerror ( rc ) ); |
| goto err_enable; |
| } |
| |
| /* Calculate mask */ |
| max = ( queue->queue.count / VIRTIO_NET_DESCS ); |
| fill = queue->max; |
| if ( fill > max ) |
| fill = max; |
| queue->fill = fill; |
| queue->mask = ( fill - 1 ); |
| |
| /* Initialise descriptors and slot ring */ |
| write = queue->write; |
| for ( slot = 0 ; slot < fill ; slot++ ) { |
| queue->slots[slot] = slot; |
| queue->iobufs[slot] = NULL; |
| index = ( slot * VIRTIO_NET_DESCS ); |
| desc = &queue->queue.desc[index]; |
| desc[0].addr = cpu_to_le64 ( dma ( &queue->map, &queue->hdr )); |
| desc[0].len = cpu_to_le32 ( vnet->hlen ); |
| desc[0].flags = cpu_to_le16 ( VIRTIO_DESC_FL_NEXT | write ); |
| desc[0].next = cpu_to_le16 ( index + 1 ); |
| desc[1].flags = cpu_to_le16 ( write ); |
| } |
| |
| DBGC ( vnet, "VNET %s Q%d using %d/%d descriptor pairs\n", |
| virtio->name, queue->queue.index, queue->fill, max ); |
| return 0; |
| |
| /* There may be no way to disable individual queues: the |
| * caller must reset the whole device to recover from a |
| * failure. |
| */ |
| err_enable: |
| dma_unmap ( &queue->map, sizeof ( queue->hdr ) ); |
| err_map: |
| return rc; |
| } |
| |
| /** |
| * Submit I/O buffer to queue |
| * |
| * @v vnet Virtio network device |
| * @v queue Virtio network queue |
| * @v iobuf I/O buffer |
| * @v len Submitted length |
| */ |
| static void virtio_net_submit ( struct virtio_net *vnet, |
| struct virtio_net_queue *queue, |
| struct io_buffer *iobuf, size_t len ) { |
| struct virtio_device *virtio = &vnet->virtio; |
| struct virtio_desc *desc; |
| unsigned int prod; |
| unsigned int slot; |
| unsigned int index; |
| |
| /* Get next descriptor pair and consume slot */ |
| prod = queue->queue.prod; |
| slot = queue->slots[ prod & queue->mask ]; |
| index = ( slot * VIRTIO_NET_DESCS ); |
| desc = &queue->queue.desc[index]; |
| |
| /* Populate descriptors */ |
| desc[1].addr = cpu_to_le64 ( iob_dma ( iobuf ) ); |
| desc[1].len = cpu_to_le32 ( len ); |
| DBGC2 ( vnet, "VNET %s Q%d [%02x-%02x] is [%lx,%lx)\n", |
| virtio->name, queue->queue.index, index, ( index + 1 ), |
| virt_to_phys ( iobuf->data ), |
| ( virt_to_phys ( iobuf->data ) + len ) ); |
| |
| /* Record I/O buffer */ |
| assert ( queue->iobufs[slot] == NULL ); |
| queue->iobufs[slot] = iobuf; |
| |
| /* Submit descriptors */ |
| virtio_submit ( &queue->queue, index ); |
| } |
| |
| /** |
| * Complete I/O buffer |
| * |
| * @v vnet Virtio network device |
| * @v queue Virtio network queue |
| * @v len Length to fill in (or NULL to ignore) |
| * @ret iobuf I/O buffer |
| */ |
| static struct io_buffer * virtio_net_complete ( struct virtio_net *vnet, |
| struct virtio_net_queue *queue, |
| size_t *len ) { |
| struct virtio_device *virtio = &vnet->virtio; |
| struct io_buffer *iobuf; |
| unsigned int cons; |
| unsigned int slot; |
| unsigned int index; |
| |
| /* Complete descriptor pair and recycle slot */ |
| cons = queue->queue.cons; |
| index = virtio_complete ( &queue->queue, len ); |
| slot = ( index / VIRTIO_NET_DESCS ); |
| queue->slots[ cons & queue->mask ] = slot; |
| |
| /* Complete I/O buffer */ |
| iobuf = queue->iobufs[slot]; |
| assert ( iobuf != NULL ); |
| queue->iobufs[slot] = NULL; |
| DBGC2 ( vnet, "VNET %s Q%d [%02x-%02x] complete", |
| virtio->name, queue->queue.index, index, ( index + 1 ) ); |
| if ( len ) |
| DBGC2 ( vnet, " len %#zx\n", *len ); |
| DBGC2 ( vnet, "\n" ); |
| |
| return iobuf; |
| } |
| |
| /****************************************************************************** |
| * |
| * Network device interface |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Refill receive queue |
| * |
| * @v vnet Virtio network device |
| */ |
| static void virtio_net_refill_rx ( struct virtio_net *vnet ) { |
| struct virtio_device *virtio = &vnet->virtio; |
| struct virtio_net_queue *queue = &vnet->rx; |
| struct io_buffer *iobuf; |
| size_t len = vnet->mfs; |
| unsigned int refilled = 0; |
| |
| /* Refill queue */ |
| while ( ( queue->queue.prod - queue->queue.cons ) < queue->fill ) { |
| |
| /* Allocate I/O buffer */ |
| iobuf = alloc_rx_iob ( len, virtio->dma ); |
| if ( ! iobuf ) { |
| /* Wait for next refill */ |
| break; |
| } |
| |
| /* Submit I/O buffer */ |
| virtio_net_submit ( vnet, queue, iobuf, len ); |
| refilled++; |
| } |
| |
| /* Notify queue, if applicable */ |
| if ( refilled ) |
| virtio_notify ( &queue->queue ); |
| } |
| |
| /** |
| * Open network device |
| * |
| * @v netdev Network device |
| * @ret rc Return status code |
| */ |
| static int virtio_net_open ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_device *virtio = &vnet->virtio; |
| union virtio_net_header hdr; |
| int rc; |
| |
| /* (Re)initialise device */ |
| if ( ( rc = virtio_init ( virtio, &virtio_net_features ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s could not initialise: %s\n", |
| virtio->name, strerror ( rc ) ); |
| goto err_init; |
| } |
| |
| /* Calculate header length */ |
| vnet->hlen = ( virtio_is_legacy ( virtio ) ? |
| sizeof ( hdr.legacy ) : sizeof ( hdr.modern ) ); |
| |
| /* Calculate maximum frame size */ |
| vnet->mfs = ( ETH_HLEN + 4 /* possible VLAN */ + netdev->mtu ); |
| |
| /* Enable receive queue */ |
| if ( ( rc = virtio_net_enable ( vnet, &vnet->rx ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s could not enable RX: %s\n", |
| virtio->name, strerror ( rc ) ); |
| goto err_rx; |
| } |
| |
| /* Enable transmit queue */ |
| if ( ( rc = virtio_net_enable ( vnet, &vnet->tx ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s could not enable TX: %s\n", |
| virtio->name, strerror ( rc ) ); |
| goto err_tx; |
| } |
| |
| /* Report driver readiness */ |
| virtio_status ( virtio, VIRTIO_STAT_DRIVER_OK ); |
| |
| /* Refill receive queue */ |
| virtio_net_refill_rx ( vnet ); |
| |
| return 0; |
| |
| dma_unmap ( &vnet->tx.map, sizeof ( vnet->tx.hdr ) ); |
| err_tx: |
| dma_unmap ( &vnet->rx.map, sizeof ( vnet->rx.hdr ) ); |
| err_rx: |
| /* There may be no way to disable individual queues: we must |
| * reset the whole device instead and then free the queues. |
| */ |
| virtio_reset ( virtio ); |
| virtio_free ( virtio, &vnet->rx.queue ); |
| virtio_free ( virtio, &vnet->tx.queue ); |
| err_init: |
| return rc; |
| } |
| |
| /** |
| * Close network device |
| * |
| * @v netdev Network device |
| */ |
| static void virtio_net_close ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_device *virtio = &vnet->virtio; |
| unsigned int i; |
| |
| /* Reset device */ |
| virtio_reset ( virtio ); |
| |
| /* Unmap headers (now that device is guaranteed idle) */ |
| dma_unmap ( &vnet->rx.map, sizeof ( vnet->rx.hdr ) ); |
| dma_unmap ( &vnet->tx.map, sizeof ( vnet->tx.hdr ) ); |
| |
| /* Free queues */ |
| virtio_free ( virtio, &vnet->rx.queue ); |
| virtio_free ( virtio, &vnet->tx.queue ); |
| |
| /* Discard any incomplete RX buffers */ |
| for ( i = 0 ; i < VIRTIO_NET_RX_MAX ; i++ ) |
| free_rx_iob ( vnet->rx_iobufs[i] ); |
| } |
| |
| /** |
| * Transmit packet |
| * |
| * @v netdev Network device |
| * @v iobuf I/O buffer |
| * @ret rc Return status code |
| */ |
| static int virtio_net_transmit ( struct net_device *netdev, |
| struct io_buffer *iobuf ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_net_queue *queue = &vnet->tx; |
| |
| /* Defer packet if there are no available transmit descriptors */ |
| if ( ( queue->queue.prod - queue->queue.cons ) >= queue->fill ) { |
| netdev_tx_defer ( netdev, iobuf ); |
| return 0; |
| } |
| |
| /* Submit I/O buffer */ |
| virtio_net_submit ( vnet, queue, iobuf, iob_len ( iobuf ) ); |
| |
| /* Notify queue */ |
| virtio_notify ( &queue->queue ); |
| |
| return 0; |
| } |
| |
| /** |
| * Poll for completed packets |
| * |
| * @v netdev Network device |
| */ |
| static void virtio_net_poll_tx ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_net_queue *queue = &vnet->tx; |
| struct io_buffer *iobuf; |
| |
| /* Poll for completed descriptors */ |
| while ( virtio_completions ( &queue->queue ) ) { |
| |
| /* Complete I/O buffer */ |
| iobuf = virtio_net_complete ( vnet, queue, NULL ); |
| netdev_tx_complete ( netdev, iobuf ); |
| } |
| } |
| |
| /** |
| * Poll for received packets |
| * |
| * @v netdev Network device |
| */ |
| static void virtio_net_poll_rx ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_net_queue *queue = &vnet->rx; |
| struct io_buffer *iobuf; |
| size_t len; |
| |
| /* Poll for completed descriptors */ |
| while ( virtio_completions ( &queue->queue ) > 0 ) { |
| |
| /* Complete I/O buffer */ |
| iobuf = virtio_net_complete ( vnet, queue, &len ); |
| iob_put ( iobuf, ( len - vnet->hlen ) ); |
| netdev_rx ( netdev, iobuf ); |
| } |
| } |
| |
| /** |
| * Poll for completed and received packets |
| * |
| * @v netdev Network device |
| */ |
| static void virtio_net_poll ( struct net_device *netdev ) { |
| struct virtio_net *vnet = netdev->priv; |
| |
| /* Poll for completed packets */ |
| virtio_net_poll_tx ( netdev ); |
| |
| /* Poll for received packets */ |
| virtio_net_poll_rx ( netdev ); |
| |
| /* Refill receive queue */ |
| virtio_net_refill_rx ( vnet ); |
| } |
| |
| /** Virtio network device operations */ |
| static struct net_device_operations virtio_net_operations = { |
| .open = virtio_net_open, |
| .close = virtio_net_close, |
| .transmit = virtio_net_transmit, |
| .poll = virtio_net_poll, |
| }; |
| |
| /****************************************************************************** |
| * |
| * PCI interface |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Probe PCI device |
| * |
| * @v pci PCI device |
| * @ret rc Return status code |
| */ |
| static int virtio_net_probe ( struct pci_device *pci ) { |
| struct net_device *netdev; |
| struct virtio_net *vnet; |
| struct virtio_device *virtio; |
| int rc; |
| |
| /* Allocate and initialise net device */ |
| netdev = alloc_etherdev ( sizeof ( *vnet ) ); |
| if ( ! netdev ) { |
| rc = -ENOMEM; |
| goto err_alloc; |
| } |
| netdev_init ( netdev, &virtio_net_operations ); |
| vnet = netdev->priv; |
| pci_set_drvdata ( pci, netdev ); |
| netdev->dev = &pci->dev; |
| netdev->dma = &pci->dma; |
| memset ( vnet, 0, sizeof ( *vnet ) ); |
| virtio = &vnet->virtio; |
| virtio_net_queue_init ( &vnet->rx, vnet->rx_iobufs, vnet->rx_slots, |
| VIRTIO_NET_RX_INDEX, VIRTIO_NET_RX_COUNT, |
| VIRTIO_NET_RX_MAX, DMA_RX, |
| VIRTIO_DESC_FL_WRITE ); |
| virtio_net_queue_init ( &vnet->tx, vnet->tx_iobufs, vnet->tx_slots, |
| VIRTIO_NET_TX_INDEX, VIRTIO_NET_TX_COUNT, |
| VIRTIO_NET_TX_MAX, DMA_TX, 0 ); |
| |
| /* Map PCI device */ |
| if ( ( rc = virtio_pci_map ( virtio, pci ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s could not map: %s\n", |
| virtio->name, strerror ( rc ) ); |
| goto err_pci_map; |
| } |
| |
| /* Initialise device */ |
| if ( ( rc = virtio_init ( virtio, &virtio_net_features ) ) != 0 ) { |
| DBGC ( vnet, "VNET %s could not initialise: %s\n", |
| virtio->name, strerror ( rc ) ); |
| goto err_init; |
| } |
| |
| /* Get MAC address */ |
| virtio_net_mac ( netdev ); |
| |
| /* Set MTU */ |
| virtio_net_mtu ( netdev ); |
| |
| /* Register network device */ |
| if ( ( rc = register_netdev ( netdev ) ) != 0 ) |
| goto err_register; |
| |
| /* Mark as link up, since we have no way to test link state changes */ |
| netdev_link_up ( netdev ); |
| |
| return 0; |
| |
| unregister_netdev ( netdev ); |
| err_register: |
| virtio_reset ( virtio ); |
| err_init: |
| virtio_unmap ( virtio ); |
| err_pci_map: |
| netdev_nullify ( netdev ); |
| netdev_put ( netdev ); |
| err_alloc: |
| return rc; |
| } |
| |
| /** |
| * Remove PCI device |
| * |
| * @v pci PCI device |
| */ |
| static void virtio_net_remove ( struct pci_device *pci ) { |
| struct net_device *netdev = pci_get_drvdata ( pci ); |
| struct virtio_net *vnet = netdev->priv; |
| struct virtio_device *virtio = &vnet->virtio; |
| |
| /* Unregister network device */ |
| unregister_netdev ( netdev ); |
| |
| /* Reset device */ |
| virtio_reset ( virtio ); |
| |
| /* Free network device */ |
| virtio_unmap ( virtio ); |
| netdev_nullify ( netdev ); |
| netdev_put ( netdev ); |
| } |
| |
| /** Virtio network PCI device IDs */ |
| static struct pci_device_id virtio_net_ids[] = { |
| PCI_ROM ( 0x1af4, 0x1000, "virtio-net", "Virtio (legacy)", 0 ), |
| PCI_ROM ( 0x1af4, 0x1041, "virtio-net", "Virtio (modern)", 0 ), |
| }; |
| |
| /** Virtio network PCI driver */ |
| struct pci_driver virtio_net_driver __pci_driver = { |
| .ids = virtio_net_ids, |
| .id_count = ( sizeof ( virtio_net_ids ) / |
| sizeof ( virtio_net_ids[0] ) ), |
| .probe = virtio_net_probe, |
| .remove = virtio_net_remove, |
| }; |