pespin has uploaded this change for review. ( https://gerrit.osmocom.org/c/libosmocore/+/31025 )
Change subject: Introduce tundev API ......................................................................
Introduce tundev API
The data structre is held private so that it can be easily extended in the future.
Change-Id: I3463271666df1e85746fb7b06ec45a17024b8c53 --- M TODO-RELEASE M include/osmocom/core/Makefile.am A include/osmocom/core/tun.h M src/core/Makefile.am A src/core/tun.c 5 files changed, 475 insertions(+), 0 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/libosmocore refs/changes/25/31025/1
diff --git a/TODO-RELEASE b/TODO-RELEASE index 535b726..572cba3 100644 --- a/TODO-RELEASE +++ b/TODO-RELEASE @@ -11,3 +11,4 @@ libosmocore ABI breakage OSMO_NUM_DLIB change affecting internal_cat[] libosmocore new API osmo_netdev_*() libosmocore new API osmo_netns_*() +libosmocore new API osmo_tundev_*() diff --git a/include/osmocom/core/Makefile.am b/include/osmocom/core/Makefile.am index 3b2ef30..e1cd92a 100644 --- a/include/osmocom/core/Makefile.am +++ b/include/osmocom/core/Makefile.am @@ -55,6 +55,7 @@ thread.h \ timer.h \ timer_compat.h \ + tun.h \ utils.h \ write_queue.h \ sockaddr_str.h \ diff --git a/include/osmocom/core/tun.h b/include/osmocom/core/tun.h new file mode 100644 index 0000000..8f233c4 --- /dev/null +++ b/include/osmocom/core/tun.h @@ -0,0 +1,38 @@ +/*! \file tun.h + * tunnel network device convenience functions. */ + +#pragma once +#if (!EMBEDDED) + +#include <stddef.h> +#include <stdint.h> + +#include <osmocom/core/msgb.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); + +int osmo_tundev_encaps(struct osmo_tundev *tundev, struct msgb *msg); + +#endif /* (!EMBEDDED) */ +/*! @} */ diff --git a/src/core/Makefile.am b/src/core/Makefile.am index 04201a8..e942a49 100644 --- a/src/core/Makefile.am +++ b/src/core/Makefile.am @@ -71,6 +71,7 @@ timer.c \ timer_gettimeofday.c \ timer_clockgettime.c \ + tun.c \ use_count.c \ utils.c \ write_queue.c \ diff --git a/src/core/tun.c b/src/core/tun.c new file mode 100644 index 0000000..23055ed --- /dev/null +++ b/src/core/tun.c @@ -0,0 +1,434 @@ + +/* TUN interface functions. + * (C) 2023 by sysmocom - s.m.f.c. GmbH info@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 */ + +#include <asm-generic/errno-base.h> +#include <asm-generic/errno.h> +#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> +#include <net/route.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/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): " fmt, (tun)->name, ## args) + +struct osmo_tundev { + /* Name used to identify the osmo_tundev */ + char *name; + + /* Network interface name to use when setting up the tun device. + *NULL = let the system pick one. */ + char *dev_name; + + /* 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; + /* FD to the netns with name "netns_name" above */ + int netns_fd; + + /* API user private data */ + void *priv_data; + + /* Called bu 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 status; + + msg = msgb_alloc(TUN_PACKET_MAX, "tundev_rx"); + + if ((status = 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, status); + + if (tundev->data_ind_cb) + return tundev->data_ind_cb(tundev, msg); + else + 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; +} + +/*! 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->name = talloc_strdup(tundev, name); + tundev->netns_fd = -1; + 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); + talloc_free(tundev); +} + +/*! Open and configure fd of the tunnel device. + * \param[in] tundev The tundev object to free + * \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) { + close(fd); + return rc; + } + + /* Read name back from device */ + if (!tundev->dev_name) { + ifr.ifr_name[IFNAMSIZ - 1] = '\0'; + tundev->dev_name = talloc_strdup(tundev, ifr.ifr_name); + } + + /* 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); + return rc; + } + 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); + return rc; + } + + /* FIXME: SIOCSIFTXQLEN / SIOCSIFFLAGS */ + + return fd; +} + +/*! 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; + + if (tundev->netns_name) { + tundev->netns_fd = osmo_netns_open_fd(tundev->netns_name); + if (tundev->netns_fd < 0) { + LOGTUN(tundev, LOGL_ERROR, "Cannot obtain netns file descriptor: %s (%d)\n", + strerror(errno), errno); + return tundev->netns_fd; + } + } + + /* temporarily switch to specified namespace to create tun device */ + if (tundev->netns_name) { + LOGTUN(tundev, LOGL_INFO, "Switch to netns '%s'\n", + tundev->netns_name); + rc = osmo_netns_switch_enter(tundev->netns_fd, &switch_state); + if (rc < 0) { + LOGTUN(tundev, LOGL_ERROR, "Cannot switch to netns '%s': %s (%d)\n", + tundev->netns_name, strerror(errno), errno); + goto err_close_ns; + } + } + + 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, "Cannot switch back from netns '%s': %s\n", + tundev->netns_name, strerror(errno)); + goto err_close_tun; + } + LOGTUN(tundev, LOGL_INFO, "Back from netns '%s'\n", + tundev->netns_name); + } + + 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_ns: + if (tundev->netns_name) { + close(tundev->netns_fd); + tundev->netns_fd = -1; + } + 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->netns_fd != -1) { + close(tundev->netns_fd); + tundev->netns_fd = -1; + } + if (tundev->wqueue.bfd.fd != -1) { + osmo_fd_unregister(&tundev->wqueue.bfd); + close(tundev->wqueue.bfd.fd); + tundev->wqueue.bfd.fd = -1; + } + 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); + 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 whien 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; +} + +/*! 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_encaps(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; +} + +/*! @} */