Introduce tundev API

The data structre is held private so that it can be easily extended in
the future.

Change-Id: I6f8324da9ba49b9249682e2ec5b45297f18dd8c2
This commit is contained in:
Pau Espin 2023-01-24 17:34:34 +01:00
parent 9d0321d041
commit 51e9dde95e
5 changed files with 617 additions and 0 deletions

View File

@ -11,3 +11,4 @@ libosmocore new API osmo_sockaddr_is_any(), osmo_sockaddr_netmask_to_prefixlen()
libosmocore ABI breakage OSMO_NUM_DLIB change affecting internal_cat[]
libosmocore new API osmo_netns_*()
libosmocore new API osmo_netdev_*()
libosmocore new API osmo_tundev_*()

View File

@ -55,6 +55,7 @@ osmocore_HEADERS = \
thread.h \
timer.h \
timer_compat.h \
tun.h \
utils.h \
write_queue.h \
sockaddr_str.h \

View File

@ -0,0 +1,43 @@
/*! \file tun.h
* tunnel network device convenience functions. */
#pragma once
#if (!EMBEDDED)
#include <stddef.h>
#include <stdint.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/netdev.h>
struct osmo_tundev;
/* callback user gets ownership of the msgb and is expected to free it. */
typedef int (*osmo_tundev_data_ind_cb_t)(struct osmo_tundev *tun, struct msgb *msg);
struct osmo_tundev *osmo_tundev_alloc(void *ctx, const char *name);
void osmo_tundev_free(struct osmo_tundev *tundev);
int osmo_tundev_open(struct osmo_tundev *tundev);
int osmo_tundev_close(struct osmo_tundev *tundev);
bool osmo_tundev_is_open(struct osmo_tundev *tundev);
void osmo_tundev_set_priv_data(struct osmo_tundev *tundev, void *priv_data);
void *osmo_tundev_get_priv_data(struct osmo_tundev *tundev);
void osmo_tundev_set_data_ind_cb(struct osmo_tundev *tundev, osmo_tundev_data_ind_cb_t data_ind_cb);
const char *osmo_tundev_get_name(const struct osmo_tundev *tundev);
int osmo_tundev_set_dev_name(struct osmo_tundev *tundev, const char *dev_name);
const char *osmo_tundev_get_dev_name(const struct osmo_tundev *tundev);
int osmo_tundev_set_netns_name(struct osmo_tundev *tundev, const char *netns);
const char *osmo_tundev_get_netns_name(const struct osmo_tundev *tundev);
struct osmo_netdev *osmo_tundev_get_netdev(struct osmo_tundev *tundev);
int osmo_tundev_send(struct osmo_tundev *tundev, struct msgb *msg);
#endif /* (!EMBEDDED) */
/*! @} */

View File

@ -71,6 +71,7 @@ libosmocore_la_SOURCES = \
timer.c \
timer_gettimeofday.c \
timer_clockgettime.c \
tun.c \
use_count.c \
utils.c \
write_queue.c \

571
src/core/tun.c Normal file
View File

@ -0,0 +1,571 @@
/* TUN interface functions.
* (C) 2023 by sysmocom - s.m.f.c. GmbH <info@sysmocom.de>
* Author: Pau Espin Pedrol <pespin@sysmocom.de>
*
* All Rights Reserved
*
* SPDX-License-Identifier: GPL-2.0+
*
* 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.
*
*/
#include "config.h"
/*! \addtogroup tun
* @{
* tun network device (interface) convenience functions
*
* \file tundev.c
*
* Example lifecycle use of the API:
*
* struct osmo_sockaddr_str osa_str = {};
* struct osmo_sockaddr osa = {};
*
* // Allocate object:
* struct osmo_tundev *tundev = osmo_tundev_alloc(parent_talloc_ctx, name);
* OSMO_ASSERT(tundev);
*
* // Configure object (before opening):
* osmo_tundev_set_data_ind_cb(tun, tun_data_ind_cb);
* rc = osmo_tundev_set_dev_name(tun, "mytunnel0");
* rc = osmo_tundev_set_netns_name(tun, "some_netns_name_or_null");
*
* // Open the tundev object:
* rc = osmo_tundev_open(tundev);
* // The tunnel device is now created and an associatd netdev object
* // is available to operate the device:
* struct osmo_netdev *netdev = osmo_tundev_get_netdev(tundev);
* OSMO_ASSERT(netdev);
*
* // Add a local IPv4 address:
* rc = osmo_sockaddr_str_from_str2(&osa_str, "192.168.200.1");
* rc = osmo_sockaddr_str_to_sockaddr(&osa_str, &osa.u.sas);
* rc = osmo_netdev_add_addr(netdev, &osa, 24);
*
* // Bring network interface up:
* rc = osmo_netdev_ifupdown(netdev, true);
*
* // Add default route (0.0.0.0/0):
* rc = osmo_sockaddr_str_from_str2(&osa_str, "0.0.0.0");
* rc = osmo_sockaddr_str_to_sockaddr(&osa_str, &osa.u.sas);
* rc = osmo_netdev_add_route(netdev, &osa, 0, NULL);
*
* // Close the tunnel (asssociated netdev object becomes unavailable)
* rc = osmo_tundev_close(tundev);
* // Free the object:
* osmo_tundev_free(tundev);
*/
#if (!EMBEDDED)
#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <ifaddrs.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <net/if.h>
#if defined(__linux__)
#include <linux/if_tun.h>
#else
#error "Unknown platform!"
#endif
#include <osmocom/core/utils.h>
#include <osmocom/core/select.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/write_queue.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/netns.h>
#include <osmocom/core/netdev.h>
#include <osmocom/core/tun.h>
#define TUN_DEV_PATH "/dev/net/tun"
#define TUN_PACKET_MAX 8196
#define LOGTUN(tun, lvl, fmt, args ...) \
LOGP(DLGLOBAL, lvl, "TUN(%s,if=%s/%u,ns=%s): " fmt, \
(tun)->name, (tun)->dev_name ? : "", \
(tun)->ifindex, (tun)->netns_name ? : "", ## args)
struct osmo_tundev {
/* Name used to identify the osmo_tundev */
char *name;
/* netdev managing the tun interface: */
struct osmo_netdev *netdev;
/* ifindiex of the currently opened tunnel interface */
unsigned int ifindex;
/* Network interface name to use when setting up the tun device.
* NULL = let the system pick one. */
char *dev_name;
/* Whether dev_name is set by user or dynamically allocated by system */
bool dev_name_dynamic;
/* Write queue used since tun fd is set non-blocking */
struct osmo_wqueue wqueue;
/* netns name where the tun interface is created (NULL = default netns) */
char *netns_name;
/* API user private data */
void *priv_data;
/* Called by tundev each time a new packet is received on the tun interface. Can be NULL. */
osmo_tundev_data_ind_cb_t data_ind_cb;
/* Whether the tundev is in opened state (managing the tun interface) */
bool opened;
};
/* A new pkt arrived from the tun device, dispatch it to the API user */
static int tundev_decaps(struct osmo_tundev *tundev)
{
struct msgb *msg;
int rc;
msg = msgb_alloc(TUN_PACKET_MAX, "tundev_rx");
if ((rc = read(tundev->wqueue.bfd.fd, msgb_data(msg), TUN_PACKET_MAX)) <= 0) {
LOGTUN(tundev, LOGL_ERROR, "read() failed: %s (%d)\n", strerror(errno), errno);
msgb_free(msg);
return -1;
}
msgb_put(msg, rc);
if (tundev->data_ind_cb)
return tundev->data_ind_cb(tundev, msg);
msgb_free(msg);
return 0;
}
/* callback for tun device osmocom select loop integration */
static int tundev_read_cb(struct osmo_fd *fd)
{
struct osmo_tundev *tundev = fd->data;
return tundev_decaps(tundev);
}
/* callback for tun device osmocom select loop integration */
static int tundev_write_cb(struct osmo_fd *fd, struct msgb *msg)
{
struct osmo_tundev *tundev = fd->data;
size_t pkt_len = msgb_length(msg);
int rc;
rc = write(tundev->wqueue.bfd.fd, msgb_data(msg), pkt_len);
if (rc < 0)
LOGTUN(tundev, LOGL_ERROR, "write() failed: %s (%d)\n", strerror(errno), errno);
else if (rc < pkt_len)
LOGTUN(tundev, LOGL_ERROR, "short write() %d < %zu\n", rc, pkt_len);
return rc;
}
static int tundev_ifupdown_ind_cb(struct osmo_netdev *netdev, bool ifupdown)
{
struct osmo_tundev *tundev = osmo_netdev_get_priv_data(netdev);
LOGTUN(tundev, LOGL_NOTICE, "Physical link state changed: %s\n",
ifupdown ? "UP" : "DOWN");
/* free any backlog, both on IFUP and IFDOWN. Keep the LMI, as it makes
* sense to get one out of the door ASAP. */
osmo_wqueue_clear(&tundev->wqueue);
return 0;
}
static int tundev_dev_name_chg_cb(struct osmo_netdev *netdev, const char *new_dev_name)
{
struct osmo_tundev *tundev = osmo_netdev_get_priv_data(netdev);
LOGTUN(tundev, LOGL_NOTICE, "netdev changed name: %s -> %s\n",
osmo_netdev_get_dev_name(netdev), new_dev_name);
if (tundev->dev_name_dynamic) {
osmo_talloc_replace_string(tundev, &tundev->dev_name, new_dev_name);
} else {
/* TODO: in here we probably want to force the iface name back
* to tundev->dev_name one we have a osmo_netdev_set_ifname() API */
osmo_talloc_replace_string(tundev, &tundev->dev_name, new_dev_name);
}
return 0;
}
static int tundev_mtu_chg_cb(struct osmo_netdev *netdev, uint32_t new_mtu)
{
struct osmo_tundev *tundev = osmo_netdev_get_priv_data(netdev);
LOGTUN(tundev, LOGL_NOTICE, "netdev changed MTU: %u\n", new_mtu);
return 0;
}
/*! Allocate a new tundev object.
* \param[in] ctx talloc context to use as a parent when allocating the tundev object
* \param[in] name A name providen to identify the tundev object
* \returns newly allocated tundev object on success; NULL on error
*/
struct osmo_tundev *osmo_tundev_alloc(void *ctx, const char *name)
{
struct osmo_tundev *tundev;
tundev = talloc_zero(ctx, struct osmo_tundev);
if (!tundev)
return NULL;
tundev->netdev = osmo_netdev_alloc(tundev, name);
if (!tundev->netdev) {
talloc_free(tundev);
return NULL;
}
osmo_netdev_set_priv_data(tundev->netdev, tundev);
osmo_netdev_set_ifupdown_ind_cb(tundev->netdev, tundev_ifupdown_ind_cb);
osmo_netdev_set_dev_name_chg_cb(tundev->netdev, tundev_dev_name_chg_cb);
osmo_netdev_set_mtu_chg_cb(tundev->netdev, tundev_mtu_chg_cb);
tundev->name = talloc_strdup(tundev, name);
osmo_wqueue_init(&tundev->wqueue, 1000);
osmo_fd_setup(&tundev->wqueue.bfd, -1, OSMO_FD_READ, osmo_wqueue_bfd_cb, tundev, 0);
tundev->wqueue.read_cb = tundev_read_cb;
tundev->wqueue.write_cb = tundev_write_cb;
return tundev;
}
/*! Free an allocated tundev object.
* \param[in] tundev The tundev object to free
*/
void osmo_tundev_free(struct osmo_tundev *tundev)
{
if (!tundev)
return;
osmo_tundev_close(tundev);
osmo_netdev_free(tundev->netdev);
talloc_free(tundev);
}
/*! Open and configure fd of the tunnel device.
* \param[in] tundev The tundev object whose tunnel interface to open
* \param[in] flags internal linux flags to pass when creating the device (not used yet)
* \returns 0 on success; negative on error
*/
static int tundev_open_fd(struct osmo_tundev *tundev, int flags)
{
struct ifreq ifr;
int fd, rc;
fd = open(TUN_DEV_PATH, O_RDWR);
if (fd < 0) {
LOGTUN(tundev, LOGL_ERROR, "Cannot open " TUN_DEV_PATH ": %s\n", strerror(errno));
return fd;
}
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI | flags;
if (tundev->dev_name) {
/* if a TUN interface name was specified, put it in the structure; otherwise,
the kernel will try to allocate the "next" device of the specified type */
osmo_strlcpy(ifr.ifr_name, tundev->dev_name, IFNAMSIZ);
}
/* try to create the device */
rc = ioctl(fd, TUNSETIFF, (void *) &ifr);
if (rc < 0)
goto close_ret;
/* Read name back from device */
if (!tundev->dev_name) {
ifr.ifr_name[IFNAMSIZ - 1] = '\0';
tundev->dev_name = talloc_strdup(tundev, ifr.ifr_name);
tundev->dev_name_dynamic = true;
}
/* Store interface index:
* (Note: there's a potential race condition here between creating the
* iface with a given name above and attempting to retrieve its ifindex based
* on that name. Someone (ie udev) could have the iface renamed in
* between here. It's a pity that TUNSETIFF doesn't copy back to us the
* newly allocated ifinidex as it does with ifname)
*/
tundev->ifindex = if_nametoindex(tundev->dev_name);
if (tundev->ifindex == 0) {
LOGTUN(tundev, LOGL_ERROR, "Unable to find ifinidex for dev %s\n",
tundev->dev_name);
rc = -ENODEV;
goto close_ret;
}
LOGTUN(tundev, LOGL_INFO, "TUN device created\n");
/* set non-blocking: */
rc = fcntl(fd, F_GETFL);
if (rc < 0) {
LOGTUN(tundev, LOGL_ERROR, "fcntl(F_GETFL) failed: %s (%d)\n",
strerror(errno), errno);
goto close_ret;
}
rc = fcntl(fd, F_SETFL, rc | O_NONBLOCK);
if (rc < 0) {
LOGTUN(tundev, LOGL_ERROR, "fcntl(F_SETFL, O_NONBLOCK) failed: %s (%d)\n",
strerror(errno), errno);
goto close_ret;
}
return fd;
close_ret:
close(fd);
return rc;
}
/*! Open the tunnel device owned by the tundev object.
* \param[in] tundev The tundev object to open
* \returns 0 on success; negative on error
*/
int osmo_tundev_open(struct osmo_tundev *tundev)
{
struct osmo_netns_switch_state switch_state;
int rc;
int netns_fd = -1;
if (tundev->opened)
return -EALREADY;
/* temporarily switch to specified namespace to create tun device */
if (tundev->netns_name) {
LOGTUN(tundev, LOGL_INFO, "Open tun: Switch to netns '%s'\n",
tundev->netns_name);
netns_fd = osmo_netns_open_fd(tundev->netns_name);
if (netns_fd < 0) {
LOGP(DLGLOBAL, LOGL_ERROR, "Open tun: Cannot switch to netns '%s': %s (%d)\n",
tundev->netns_name, strerror(errno), errno);
return netns_fd;
}
rc = osmo_netns_switch_enter(netns_fd, &switch_state);
if (rc < 0) {
LOGTUN(tundev, LOGL_ERROR, "Open tun: Cannot switch to netns '%s': %s (%d)\n",
tundev->netns_name, strerror(errno), errno);
goto err_close_netns_fd;
}
}
tundev->wqueue.bfd.fd = tundev_open_fd(tundev, 0);
if (tundev->wqueue.bfd.fd < 0) {
LOGTUN(tundev, LOGL_ERROR, "Cannot open TUN device: %s\n", strerror(errno));
rc = -ENODEV;
goto err_restore_ns;
}
/* switch back to default namespace */
if (tundev->netns_name) {
rc = osmo_netns_switch_exit(&switch_state);
if (rc < 0) {
LOGTUN(tundev, LOGL_ERROR, "Open tun: Cannot switch back from netns '%s': %s\n",
tundev->netns_name, strerror(errno));
goto err_close_tun;
}
LOGTUN(tundev, LOGL_INFO, "Open tun: Back from netns '%s'\n",
tundev->netns_name);
}
rc = osmo_netdev_set_netns_name(tundev->netdev, tundev->netns_name);
if (rc < 0)
goto err_close_tun;
rc = osmo_netdev_set_ifindex(tundev->netdev, tundev->ifindex);
if (rc < 0)
goto err_close_tun;
rc = osmo_netdev_register(tundev->netdev);
if (rc < 0)
goto err_close_tun;
osmo_fd_register(&tundev->wqueue.bfd);
tundev->opened = true;
return 0;
err_close_tun:
close(tundev->wqueue.bfd.fd);
tundev->wqueue.bfd.fd = -1;
err_restore_ns:
osmo_netns_switch_exit(&switch_state);
err_close_netns_fd:
if (netns_fd >= 0)
close(netns_fd);
return rc;
}
/*! Close the tunnel device owned by the tundev object.
* \param[in] tundev The tundev object to close
* \returns 0 on success; negative on error
*/
int osmo_tundev_close(struct osmo_tundev *tundev)
{
if (!tundev->opened)
return -EALREADY;
osmo_wqueue_clear(&tundev->wqueue);
if (tundev->wqueue.bfd.fd != -1) {
osmo_fd_unregister(&tundev->wqueue.bfd);
close(tundev->wqueue.bfd.fd);
tundev->wqueue.bfd.fd = -1;
}
osmo_netdev_unregister(tundev->netdev);
if (tundev->dev_name_dynamic) {
TALLOC_FREE(tundev->dev_name);
tundev->dev_name_dynamic = false;
}
tundev->opened = false;
return 0;
}
/*! Retrieve whether the tundev object is in "opened" state.
* \param[in] tundev The tundev object to check
* \returns true if in state "opened"; false otherwise
*/
bool osmo_tundev_is_open(struct osmo_tundev *tundev)
{
return tundev->opened;
}
/*! Set private user data pointer on the tundev object.
* \param[in] tundev The tundev object where the field is set
*/
void osmo_tundev_set_priv_data(struct osmo_tundev *tundev, void *priv_data)
{
tundev->priv_data = priv_data;
}
/*! Get private user data pointer from the tundev object.
* \param[in] tundev The tundev object from where to retrieve the field
* \returns The current value of the priv_data field.
*/
void *osmo_tundev_get_priv_data(struct osmo_tundev *tundev)
{
return tundev->priv_data;
}
/*! Set data_ind_cb callback, called when a new packet is received on the tun interface.
* \param[in] tundev The tundev object where the field is set
* \param[in] data_ind_cb the user provided function to be called when a new packet is received
*/
void osmo_tundev_set_data_ind_cb(struct osmo_tundev *tundev, osmo_tundev_data_ind_cb_t data_ind_cb)
{
tundev->data_ind_cb = data_ind_cb;
}
/*! Get name used to identify the tundev object.
* \param[in] tundev The tundev object from where to retrieve the field
* \returns The current value of the name used to identify the tundev object
*/
const char *osmo_tundev_get_name(const struct osmo_tundev *tundev)
{
return tundev->name;
}
/*! Set name used to name the tunnel interface created by the tundev object.
* \param[in] tundev The tundev object where the field is set
* \param[in] dev_name The tunnel interface name to use
* \returns 0 on success; negative on error
*
* This is used during osmo_tundev_open() time, and hence shouldn't be changed
* when the tundev object is in "opened" state.
* If left as NULL (default), the system will pick a suitable name during
* osmo_tundev_open(), and the field will be updated to the system-selected
* name, which can be retrieved later with osmo_tundev_get_dev_name().
*/
int osmo_tundev_set_dev_name(struct osmo_tundev *tundev, const char *dev_name)
{
if (tundev->opened)
return -EALREADY;
osmo_talloc_replace_string(tundev, &tundev->dev_name, dev_name);
tundev->dev_name_dynamic = false;
return 0;
}
/*! Get name used to name the tunnel interface created by the tundev object
* \param[in] tundev The tundev object from where to retrieve the field
* \returns The current value of the configured tunnel interface name to use
*/
const char *osmo_tundev_get_dev_name(const struct osmo_tundev *tundev)
{
return tundev->dev_name;
}
/*! Set name of the network namespace to use when opening the tunnel interface
* \param[in] tundev The tundev object where the field is set
* \param[in] netns_name The network namespace to use during tunnel interface creation
* \returns 0 on success; negative on error
*
* This is used during osmo_tundev_open() time, and hence shouldn't be changed
* when the tundev object is in "opened" state.
* If left as NULL (default), the system will stay in the current network namespace.
*/
int osmo_tundev_set_netns_name(struct osmo_tundev *tundev, const char *netns_name)
{
if (tundev->opened)
return -EALREADY;
osmo_talloc_replace_string(tundev, &tundev->netns_name, netns_name);
return 0;
}
/*! Get name of network namespace used when opening the tunnel interface
* \param[in] tundev The tundev object from where to retrieve the field
* \returns The current value of the configured network namespace
*/
const char *osmo_tundev_get_netns_name(const struct osmo_tundev *tundev)
{
return tundev->netns_name;
}
/*! Get netdev managing the tunnel interface of tundev
* \param[in] tundev The tundev object from where to retrieve the field
* \returns The netdev objet managing the tun interface
*/
struct osmo_netdev *osmo_tundev_get_netdev(struct osmo_tundev *tundev)
{
return tundev->netdev;
}
/*! Submit a packet to the tunnel device managed by the tundev object
* \param[in] tundev The tundev object owning the tunnel device where to inject the packet
* \param[in] msg The msgb containg the packet to transfer
* \returns The current value of the configured network namespace
*
* This function takes the ownership of msg in all cases.
*/
int osmo_tundev_send(struct osmo_tundev *tundev, struct msgb *msg)
{
int rc = osmo_wqueue_enqueue(&tundev->wqueue, msg);
if (rc < 0) {
LOGTUN(tundev, LOGL_ERROR, "Failed to enqueue the packet\n");
msgb_free(msg);
return rc;
}
return rc;
}
#endif /* (!EMBEDDED) */
/*! @} */