networking: add Linux-specific TAP mode to network interface

This addition was motivated by my lack of knowledge of VDE and my
familiarity with the Linux networking stack. The driver automatically
manages the creation of TAP devices and their association with bridges,
such that configurations which specify the same bridge will be connected
together. It also automatically handles the creation of bridge devices for
convenience's sake.
This commit is contained in:
Doug Johnson
2023-08-06 20:11:36 -06:00
parent 80f5c47221
commit 45bcbc75fd
8 changed files with 463 additions and 18 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) {
@@ -2309,7 +2312,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 */
@@ -125,6 +126,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 {
@@ -154,10 +156,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

@@ -67,5 +67,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

@@ -480,6 +480,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

@@ -1061,6 +1061,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

@@ -38,12 +38,13 @@ SettingsNetwork::enableElements(Ui::SettingsNetwork *ui)
auto *intf_cbox = findChild<QComboBox *>(QString("comboBoxIntf%1").arg(i + 1));
auto *conf_btn = findChild<QPushButton *>(QString("pushButtonConf%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));
int netType = net_type_cbox->currentData().toInt();
bool adaptersEnabled = netType == NET_TYPE_NONE
|| netType == NET_TYPE_SLIRP
|| netType == NET_TYPE_VDE
|| (netType == NET_TYPE_PCAP && intf_cbox->currentData().toInt() > 0);
|| (netType == NET_TYPE_PCAP && intf_cbox->currentData().toInt() > 0)
|| netType == NET_TYPE_TAP;
intf_cbox->setEnabled(net_type_cbox->currentData().toInt() == NET_TYPE_PCAP);
nic_cbox->setEnabled(adaptersEnabled);
@@ -54,6 +55,7 @@ SettingsNetwork::enableElements(Ui::SettingsNetwork *ui)
else
conf_btn->setEnabled(adaptersEnabled && network_card_has_config(nic_cbox->currentData().toInt()));
socket_line->setEnabled(net_type_cbox->currentData().toInt() == NET_TYPE_VDE);
bridge_line->setEnabled(net_type_cbox->currentData().toInt() == NET_TYPE_TAP);
}
}
@@ -86,6 +88,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();
@@ -95,6 +98,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));
}
}
}
@@ -152,7 +157,7 @@ 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(net_cards_conf[i].net_type);
@@ -171,12 +176,17 @@ 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

@@ -144,7 +144,21 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="4" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC1">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC1">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -259,6 +273,13 @@
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC2">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QLineEdit" name="socketVDENIC2">
<property name="maxLength">
@@ -266,7 +287,14 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="4" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC2">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -394,7 +422,21 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="4" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC3">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC3">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -508,13 +550,6 @@
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QLineEdit" name="socketVDENIC4">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
@@ -522,7 +557,28 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1" colspan="2">
<widget class="QLineEdit" name="socketVDENIC4">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelBridgeTAPNIC4">
<property name="text">
<string>TAP Bridge Device</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="bridgeTAPNIC4">
<property name="maxLength">
<number>127</number>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>