/* TUN interface functions. * (C) 2023 by sysmocom - s.f.m.c. GmbH * Author: Pau Espin Pedrol * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #else #error "Unknown platform!" #endif #include #include #include #include #include #include #include #include #include #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; rc = osmo_fd_register(&tundev->wqueue.bfd); if (rc < 0) goto err_unregister_netdev; tundev->opened = true; return 0; err_unregister_netdev: osmo_netdev_unregister(tundev->netdev); err_close_tun: close(tundev->wqueue.bfd.fd); tundev->wqueue.bfd.fd = -1; err_restore_ns: if (tundev->netns_name) 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) */ /*! @} */