Merge pull request #3521 from dougvj/net-add-tap-backend

networking: add Linux-specific TAP mode to network devices
This commit is contained in:
Jasmine Iwanek
2025-06-27 19:33:42 -04:00
committed by GitHub
9 changed files with 468 additions and 13 deletions

View File

@@ -660,6 +660,8 @@ load_network(void)
nc->net_type = NET_TYPE_SLIRP;
else if (!strcmp(p, "vde") || !strcmp(p, "2"))
nc->net_type = NET_TYPE_VDE;
else if (!strcmp(p, "tap") || !strcmp(p, "3"))
nc->net_type = NET_TYPE_TAP;
else
nc->net_type = NET_TYPE_NONE;
} else
@@ -706,11 +708,12 @@ load_network(void)
nc->net_type = NET_TYPE_SLIRP;
else if (!strcmp(p, "vde") || !strcmp(p, "2"))
nc->net_type = NET_TYPE_VDE;
else if (!strcmp(p, "tap") || !strcmp(p, "3"))
nc->net_type = NET_TYPE_TAP;
else
nc->net_type = NET_TYPE_NONE;
} else
nc->net_type = NET_TYPE_NONE;
sprintf(temp, "net_%02i_host_device", c + 1);
p = ini_section_get_string(cat, temp, NULL);
if (p != NULL) {
@@ -2433,7 +2436,9 @@ save_network(void)
case NET_TYPE_VDE:
ini_section_set_string(cat, temp, "vde");
break;
case NET_TYPE_TAP:
ini_section_set_string(cat, temp, "tap");
break;
default:
break;
}

View File

@@ -52,6 +52,7 @@
#define NET_TYPE_SLIRP 1 /* use the SLiRP port forwarder */
#define NET_TYPE_PCAP 2 /* use the (Win)Pcap API */
#define NET_TYPE_VDE 3 /* use the VDE plug API */
#define NET_TYPE_TAP 4 /* use a linux TAP device */
#define NET_MAX_FRAME 1518
/* Queue size must be a power of 2 */
@@ -126,6 +127,7 @@ typedef struct netdrv_t {
extern const netdrv_t net_pcap_drv;
extern const netdrv_t net_slirp_drv;
extern const netdrv_t net_vde_drv;
extern const netdrv_t net_tap_drv;
extern const netdrv_t net_null_drv;
struct _netcard_t {
@@ -155,10 +157,11 @@ typedef struct {
int has_slirp;
int has_pcap;
int has_vde;
int has_tap;
} network_devmap_t;
#define HAS_NOSLIRP_NET(x) (x.has_pcap || x.has_vde)
#define HAS_NOSLIRP_NET(x) (x.has_pcap || x.has_vde || x.has_tap)
#ifdef __cplusplus
extern "C" {

View File

@@ -70,5 +70,14 @@ if (UNIX)
endif()
endif()
endif()
if (UNIX AND NOT APPLE) # Support for TAP on Linux and BSD, supposedly.
find_path(HAS_TAP "linux/if_tun.h" PATHS ${TAP_INCLUDE_DIR} "/usr/include /usr/local/include" "/opt/homebrew/include" )
if(HAS_TAP)
add_compile_definitions(HAS_TAP)
list(APPEND net_sources net_tap.c)
else()
message(WARNING "TAP support not available. Are you on some BSD?")
endif()
endif()
add_library(net OBJECT ${net_sources})

353
src/network/net_tap.c Normal file
View File

@@ -0,0 +1,353 @@
/*
* 86Box A hypervisor and IBM PC system emulator that specializes in
* running old operating systems and software designed for IBM
* PC systems and compatibles from 1981 through fairly recent
* system designs based on the PCI bus.
*
* This file is part of the 86Box distribution.
*
* Linux TAP network interface for 86box.
*
* This file was created by looking at the VDE network backend
* as a reference, credit to jguillaumes.
*
* Authors: Doug Johnson <dougvj@gmail.com>
*
*
* Copyright 2023 Doug Johnson
*
* Redistribution and use in source and binary forms, with
* or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the entire
* above notice, this list of conditions and the following
* disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names
* of its contributors may be used to endorse or promote
* products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef _WIN32
# error TAP networking is only supported on Linux
#endif
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>
#include <stdbool.h>
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/if_arp.h>
#include <linux/sockios.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include <86box/device.h>
#include <86box/plat.h>
#include <86box/plat_dynld.h>
#include <86box/thread.h>
#include <86box/timer.h>
#include <86box/network.h>
#include <86box/net_event.h>
typedef struct net_tap_t {
int fd; // tap device file descriptor
netcard_t *card;
thread_t *poll_tid;
net_evt_t tx_event;
net_evt_t stop_event;
netpkt_t pkt_rx;
netpkt_t pkts_tx[NET_QUEUE_LEN];
} net_tap_t;
#ifdef ENABLE_TAP_LOG
int tap_do_log = ENABLE_TAP_LOG;
static void tap_logv(const char *fmt, va_list ap)
{
if (tap_do_log) {
pclog_ex(fmt, ap);
}
}
static void tap_log(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
if (tap_do_log) {
va_start(ap, fmt);
tap_logv(fmt, ap);
va_end(ap);
}
va_end(ap);
}
#else
# define tap_log(...) \
do { \
} while (0)
# define tap_logv(...) \
do { \
} while (0)
#endif
static void net_tap_thread(void *priv) {
enum {
NET_EVENT_STOP = 0,
NET_EVENT_TX,
NET_EVENT_RX,
NET_EVENT_TAP,
NET_EVENT_MAX,
};
net_tap_t *tap = priv;
tap_log("TAP: poll thread started.\n");
struct pollfd pfd[NET_EVENT_MAX];
pfd[NET_EVENT_STOP].fd = net_event_get_fd(&tap->stop_event);
pfd[NET_EVENT_STOP].events = POLLIN | POLLPRI;
pfd[NET_EVENT_TX].fd = net_event_get_fd(&tap->tx_event);
pfd[NET_EVENT_TX].events = POLLIN | POLLPRI;
pfd[NET_EVENT_RX].fd = tap->fd;
pfd[NET_EVENT_RX].events = POLLIN | POLLPRI;
pfd[NET_EVENT_TAP].fd = tap->fd;
pfd[NET_EVENT_TAP].events = POLLERR | POLLHUP | POLLPRI;
fcntl(tap->fd, F_SETFL, O_NONBLOCK);
while(1) {
ssize_t ret = poll(pfd, NET_EVENT_MAX, -1);
if (ret < 0) {
tap_log("TAP: poll error: %s\n", strerror(errno));
net_event_set(&tap->stop_event);
break;
}
if (pfd[NET_EVENT_TAP].revents) {
tap_log("TAP: tap close/error event received.\n");
net_event_set(&tap->stop_event);
}
if (pfd[NET_EVENT_TX].revents & POLLIN) {
net_event_clear(&tap->tx_event);
int packets = network_tx_popv(tap->card, tap->pkts_tx,
NET_QUEUE_LEN);
for(int i = 0; i < packets; i++) {
netpkt_t *pkt = &tap->pkts_tx[i];
ssize_t ret = write(tap->fd, pkt->data, pkt->len);
if (ret < 0) {
tap_log("TAP: write error: %s\n", strerror(errno));
}
}
}
if (pfd[NET_EVENT_RX].revents & POLLIN) {
ssize_t len = read(tap->fd, tap->pkt_rx.data, NET_MAX_FRAME);
if (len < 0) {
tap_log("TAP: read error: %s\n", strerror(errno));
continue;
}
tap->pkt_rx.len = len;
network_rx_put_pkt(tap->card, &tap->pkt_rx);
}
if (pfd[NET_EVENT_STOP].revents & POLLIN) {
net_event_clear(&tap->stop_event);
break;
}
}
}
void net_tap_close(void *priv)
{
if (!priv) {
return;
}
net_tap_t *tap = priv;
tap_log("TAP: closing.\n");
net_event_set(&tap->stop_event);
tap_log("TAP: waiting for poll thread to exit.\n");
thread_wait(tap->poll_tid);
tap_log("TAP: poll thread exited.\n");
for(int i = 0; i < NET_QUEUE_LEN; i++) {
free(tap->pkts_tx[i].data);
}
free(tap->pkt_rx.data);
if (tap->fd >= 0) {
close(tap->fd);
}
free(tap);
}
void net_tap_error(char *errbuf, const char* format, ...)
{
va_list ap;
va_start(ap, format);
vsnprintf(errbuf, NET_DRV_ERRBUF_SIZE, format, ap);
tap_log("TAP: %s", errbuf);
va_end(ap);
}
// Error handling macro for the many ioctl calls we use in net_tap_alloc
#define ioctl_or_fail(fd, request, argp) \
do { \
if ((err = ioctl(fd, request, argp)) < 0) { \
tap_log("TAP: ioctl " #request " error: %s\n", strerror(errno)); \
goto fail; \
} \
} while (0)
// Returns -ERRNO so we can get an idea what's wrong
int net_tap_alloc(const uint8_t *mac_addr, const char* bridge_dev)
{
int fd;
struct ifreq ifr = {0};
if ((fd = open("/dev/net/tun", O_RDWR)) < 0) {
tap_log("TAP: open error: %s\n", strerror(errno));
return -errno;
}
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
int err;
if ((err = ioctl(fd, TUNSETIFF, &ifr)) < 0) {
tap_log("TAP: ioctl TUNSETIFF error: %s\n", strerror(errno));
close(fd);
return -errno;
}
// Create a socket for ioctl operations
int sock;
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
tap_log("TAP: socket error: %s\n", strerror(errno));
close(fd);
return -errno;
}
// Bring the interface up
tap_log("TAP: Bringing interface '%s' up.\n", ifr.ifr_name);
ifr.ifr_flags = IFF_UP;
ioctl_or_fail(sock, SIOCSIFFLAGS, &ifr);
// Add interface to bridge, if specified
if (bridge_dev && bridge_dev[0] != '\0') {
// First see if the bridge exists
struct ifreq ifr_bridge;
//NOTE strncpy does not null terminate if the string is too long, I use
// snprintf or strlcpy instead
//strncpy(ifr_bridge.ifr_name, bridge_dev, IFNAMSIZ);
snprintf(ifr_bridge.ifr_name, IFNAMSIZ, "%s", bridge_dev);
if ((err = ioctl(sock, SIOCGIFINDEX, &ifr_bridge)) < 0) {
if (errno != ENODEV) {
tap_log("TAP: ioctl SIOCGIFINDEX error: %s\n", strerror(errno));
goto fail;
} else {
// Create the bridge
ioctl_or_fail(sock, SIOCBRADDBR, &ifr_bridge);
// Set the bridge up
ifr_bridge.ifr_flags = IFF_UP;
ioctl_or_fail(sock, SIOCSIFFLAGS, &ifr_bridge);
}
}
// Get TAP index
ioctl_or_fail(sock, SIOCGIFINDEX, &ifr);
// Add the tap device to the bridge
ifr_bridge.ifr_ifindex = ifr.ifr_ifindex;
ioctl_or_fail(sock, SIOCBRADDIF, &ifr_bridge);
}
// close the socket we used for ioctl operations
close(sock);
tap_log("Allocated tap device %s\n", ifr.ifr_name);
return fd;
// cleanup point used by ioctl_or_fail macro
fail:
close(sock);
close(fd);
return -errno;
}
void net_tap_in_available(void *priv)
{
net_tap_t *tap = priv;
net_event_set(&tap->tx_event);
}
void *
net_tap_init(
const netcard_t *card,
const uint8_t *mac_addr,
void *priv,
char *netdrv_errbuf)
{
const char *bridge_dev = (void *) priv;
int tap_fd = net_tap_alloc(mac_addr, bridge_dev);
if (tap_fd < 0) {
if (tap_fd == -EPERM) {
net_tap_error(
netdrv_errbuf,
"No permissions to allocate tap device. "
"Try adding NET_CAP_ADMIN,NET_CAP_RAW to 86box ("
"sudo setcap 'CAP_NET_RAW,CAP_NET_ADMIN=eip')");
} else {
net_tap_error(
netdrv_errbuf,
"Unable to allocate TAP device: %s",
strerror(-tap_fd));
}
return NULL;
}
if (bridge_dev && bridge_dev[0] != '\0') {
}
net_tap_t *tap = calloc(1, sizeof(net_tap_t));
if (!tap) {
goto alloc_fail;
}
tap->pkt_rx.data = calloc(1, NET_MAX_FRAME);
if (!tap->pkt_rx.data) {
goto alloc_fail;
}
for(int i = 0; i < NET_QUEUE_LEN; i++) {
tap->pkts_tx[i].data = calloc(1, NET_MAX_FRAME);
if (!tap->pkts_tx[i].data) {
goto alloc_fail;
}
}
tap->fd = tap_fd;
tap->card = (netcard_t *) card;
net_event_init(&tap->tx_event);
net_event_init(&tap->stop_event);
tap->poll_tid = thread_create(net_tap_thread, tap);
return tap;
alloc_fail:
net_tap_error(netdrv_errbuf, "Failed to allocate memory");
close(tap_fd);
free(tap);
return NULL;
}
const netdrv_t net_tap_drv = {
&net_tap_in_available,
&net_tap_init,
&net_tap_close,
NULL
};

View File

@@ -496,6 +496,12 @@ network_attach(void *card_drv, uint8_t *mac, NETRXCB rx, NETSETLINKSTATE set_lin
card->host_drv = net_vde_drv;
card->host_drv.priv = card->host_drv.init(card, mac, net_cards_conf[net_card_current].host_dev_name, net_drv_error);
break;
#endif
#ifdef HAS_TAP
case NET_TYPE_TAP:
card->host_drv = net_tap_drv;
card->host_drv.priv = card->host_drv.init(card, mac, net_cards_conf[net_card_current].host_dev_name, net_drv_error);
break;
#endif
default:
card->host_drv.priv = NULL;

View File

@@ -1081,6 +1081,9 @@ MediaMenu::nicUpdateMenu(int i)
case NET_TYPE_VDE:
netType = "VDE";
break;
case NET_TYPE_TAP:
netType = "TAP";
break;
}
QString devName = DeviceConfig::DeviceName(network_card_getdevice(net_cards_conf[i].device_num), network_card_get_internal_name(net_cards_conf[i].device_num), 1);

View File

@@ -45,6 +45,8 @@ SettingsNetwork::enableElements(Ui::SettingsNetwork *ui)
auto *vde_socket_label = findChild<QLabel *>(QString("labelSocketVDENIC%1").arg(i + 1));
auto *socket_line = findChild<QLineEdit *>(QString("socketVDENIC%1").arg(i + 1));
auto *bridge_line = findChild<QLineEdit *>(QString("bridgeTAPNIC%1").arg(i + 1));
auto *option_list_label = findChild<QLabel *>(QString("labelOptionList%1").arg(i + 1));
auto *option_list_line = findChild<QWidget *>(QString("lineOptionList%1").arg(i + 1));
@@ -60,6 +62,9 @@ SettingsNetwork::enableElements(Ui::SettingsNetwork *ui)
vde_socket_label->setVisible(false);
socket_line->setVisible(false);
// TAP
bridge_line->setEnabled(net_type_cbox->currentData().toInt() == NET_TYPE_TAP);
// PCAP
intf_cbox->setVisible(false);
intf_label->setVisible(false);
@@ -86,6 +91,9 @@ SettingsNetwork::enableElements(Ui::SettingsNetwork *ui)
intf_label->setVisible(true);
break;
case NET_TYPE_TAP:
bridge_line->setVisible(true);
case NET_TYPE_SLIRP:
default:
break;
@@ -123,6 +131,7 @@ SettingsNetwork::save()
for (int i = 0; i < NET_CARD_MAX; ++i) {
auto *cbox = findChild<QComboBox *>(QString("comboBoxNIC%1").arg(i + 1));
auto *socket_line = findChild<QLineEdit *>(QString("socketVDENIC%1").arg(i + 1));
auto *bridge_line = findChild<QLineEdit *>(QString("bridgeTAPNIC%1").arg(i + 1));
net_cards_conf[i].device_num = cbox->currentData().toInt();
cbox = findChild<QComboBox *>(QString("comboBoxNet%1").arg(i + 1));
net_cards_conf[i].net_type = cbox->currentData().toInt();
@@ -132,6 +141,8 @@ SettingsNetwork::save()
strncpy(net_cards_conf[i].host_dev_name, network_devs[cbox->currentData().toInt()].device, sizeof(net_cards_conf[i].host_dev_name) - 1);
else if (net_cards_conf[i].net_type == NET_TYPE_VDE)
strncpy(net_cards_conf[i].host_dev_name, socket_line->text().toUtf8().constData(), sizeof(net_cards_conf[i].host_dev_name));
else if (net_cards_conf[i].net_type == NET_TYPE_TAP)
strncpy(net_cards_conf[i].host_dev_name, bridge_line->text().toUtf8().constData(), sizeof(net_cards_conf[i].host_dev_name));
}
}
@@ -198,6 +209,8 @@ SettingsNetwork::onCurrentMachineChanged(int machineId)
if (network_devmap.has_vde)
Models::AddEntry(model, "VDE", NET_TYPE_VDE);
Models::AddEntry(model, "TAP", NET_TYPE_TAP);
model->removeRows(0, removeRows);
cbox->setCurrentIndex(cbox->findData(net_cards_conf[i].net_type));
@@ -216,13 +229,18 @@ SettingsNetwork::onCurrentMachineChanged(int machineId)
}
model->removeRows(0, removeRows);
cbox->setCurrentIndex(selectedRow);
}
}
if (net_cards_conf[i].net_type == NET_TYPE_VDE) {
QString currentVdeSocket = net_cards_conf[i].host_dev_name;
auto editline = findChild<QLineEdit *>(QString("socketVDENIC%1").arg(i+1));
editline->setText(currentVdeSocket);
}
else if (net_cards_conf[i].net_type == NET_TYPE_TAP) {
QString currentTapDevice = net_cards_conf[i].host_dev_name;
auto editline = findChild<QLineEdit *>(QString("bridgeTAPNIC%1").arg(i+1));
editline->setText(currentTapDevice);
}
}
}

View File

@@ -31,7 +31,6 @@
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabNet1">
<attribute name="title">
<string>Network Card #1</string>
@@ -156,7 +155,21 @@
</property>
</widget>
</item>
<item row="6" column="1">
<item row="6" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC1">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC1">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="7" column="1">
<spacer name="verticalSpacerNIC1">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -171,7 +184,6 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tabNet2">
<attribute name="title">
<string>Network Card #2</string>
@@ -296,7 +308,21 @@
</property>
</widget>
</item>
<item row="6" column="1">
<item row="6" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC2">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC2">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="7" column="1">
<spacer name="verticalSpacerNIC2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -311,7 +337,6 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tabNet3">
<attribute name="title">
<string>Network Card #3</string>
@@ -436,7 +461,21 @@
</property>
</widget>
</item>
<item row="6" column="1">
<item row="6" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC3">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC3">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="7" column="1">
<spacer name="verticalSpacerNIC3">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -451,7 +490,6 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tabNet4">
<attribute name="title">
<string>Network Card #4</string>
@@ -576,7 +614,21 @@
</property>
</widget>
</item>
<item row="6" column="1">
<item row="6" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC4">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC4">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="7" column="1">
<spacer name="verticalSpacerNIC4">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -591,7 +643,6 @@
</item>
</layout>
</widget>
</widget>
</item>
</layout>