482 lines
13 KiB
C
482 lines
13 KiB
C
/* SPDX-License-Identifier: GPL-2.0 */
|
|
#include <unistd.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <netdb.h>
|
|
|
|
#include <netinet/ip.h>
|
|
#include <netinet/ip6.h>
|
|
|
|
#include <pthread.h>
|
|
|
|
#include <linux/if.h>
|
|
#include <linux/if_tun.h>
|
|
#include <sys/ioctl.h>
|
|
|
|
#include <linux/netlink.h>
|
|
#include <netlink/socket.h>
|
|
#include <netlink/route/link.h>
|
|
|
|
#include <osmocom/core/linuxlist.h>
|
|
#include <osmocom/core/talloc.h>
|
|
#include <osmocom/core/logging.h>
|
|
#include <osmocom/core/utils.h>
|
|
|
|
#include "gtp.h"
|
|
#include "internal.h"
|
|
#include "netns.h"
|
|
|
|
/***********************************************************************
|
|
* TUN Device
|
|
***********************************************************************/
|
|
|
|
#define LOGTUN(tun, lvl, fmt, args ...) \
|
|
LOGP(DTUN, lvl, "%s: " fmt, (tun)->devname, ## args)
|
|
|
|
/* extracted information from a packet */
|
|
struct pkt_info {
|
|
struct sockaddr_storage saddr;
|
|
struct sockaddr_storage daddr;
|
|
uint8_t proto;
|
|
};
|
|
|
|
static int parse_pkt(struct pkt_info *out, const uint8_t *in, unsigned int in_len)
|
|
{
|
|
const struct iphdr *ip4 = (struct iphdr *) in;
|
|
const uint16_t *l4h = NULL;
|
|
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
if (ip4->version == 4) {
|
|
struct sockaddr_in *saddr4 = (struct sockaddr_in *) &out->saddr;
|
|
struct sockaddr_in *daddr4 = (struct sockaddr_in *) &out->daddr;
|
|
|
|
if (in_len < sizeof(*ip4) || in_len < 4*ip4->ihl)
|
|
return -1;
|
|
|
|
saddr4->sin_family = AF_INET;
|
|
saddr4->sin_addr.s_addr = ip4->saddr;
|
|
|
|
daddr4->sin_family = AF_INET;
|
|
daddr4->sin_addr.s_addr = ip4->daddr;
|
|
|
|
out->proto = ip4->protocol;
|
|
l4h = (const uint16_t *) (in + sizeof(*ip4));
|
|
|
|
switch (out->proto) {
|
|
case IPPROTO_TCP:
|
|
case IPPROTO_UDP:
|
|
case IPPROTO_DCCP:
|
|
case IPPROTO_SCTP:
|
|
case IPPROTO_UDPLITE:
|
|
saddr4->sin_port = ntohs(l4h[0]);
|
|
daddr4->sin_port = ntohs(l4h[1]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else if (ip4->version == 6) {
|
|
const struct ip6_hdr *ip6 = (struct ip6_hdr *) in;
|
|
struct sockaddr_in6 *saddr6 = (struct sockaddr_in6 *) &out->saddr;
|
|
struct sockaddr_in6 *daddr6 = (struct sockaddr_in6 *) &out->daddr;
|
|
|
|
if (in_len < sizeof(*ip6))
|
|
return -1;
|
|
|
|
saddr6->sin6_family = AF_INET6;
|
|
saddr6->sin6_addr = ip6->ip6_src;
|
|
|
|
daddr6->sin6_family = AF_INET6;
|
|
daddr6->sin6_addr = ip6->ip6_dst;
|
|
|
|
/* FIXME: ext hdr */
|
|
out->proto = ip6->ip6_nxt;
|
|
l4h = (const uint16_t *) (in + sizeof(*ip6));
|
|
|
|
switch (out->proto) {
|
|
case IPPROTO_TCP:
|
|
case IPPROTO_UDP:
|
|
case IPPROTO_DCCP:
|
|
case IPPROTO_SCTP:
|
|
case IPPROTO_UDPLITE:
|
|
saddr6->sin6_port = ntohs(l4h[0]);
|
|
daddr6->sin6_port = ntohs(l4h[1]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void tun_device_pthread_cleanup_routine(void *data)
|
|
{
|
|
struct tun_device *tun = data;
|
|
LOGTUN(tun, LOGL_DEBUG, "pthread_cleanup\n");
|
|
int rc = osmo_it_q_enqueue(tun->d->itq, &tun->itq_msg, list);
|
|
OSMO_ASSERT(rc == 0);
|
|
}
|
|
|
|
/* one thread for reading from each TUN device (TUN -> GTP encapsulation) */
|
|
static void *tun_device_thread(void *arg)
|
|
{
|
|
struct tun_device *tun = (struct tun_device *)arg;
|
|
struct gtp_daemon *d = tun->d;
|
|
|
|
uint8_t base_buffer[MAX_UDP_PACKET+sizeof(struct gtp1_header)];
|
|
struct gtp1_header *gtph = (struct gtp1_header *)base_buffer;
|
|
uint8_t *buffer = base_buffer + sizeof(struct gtp1_header);
|
|
|
|
struct sockaddr_storage daddr;
|
|
int old_cancelst_unused;
|
|
|
|
/* initialize the fixed part of the GTP header */
|
|
gtph->flags = 0x30;
|
|
gtph->type = GTP_TPDU;
|
|
|
|
pthread_cleanup_push(tun_device_pthread_cleanup_routine, tun);
|
|
/* IMPORTANT!: All logging functions in this function block must be called with
|
|
* PTHREAD_CANCEL_DISABLE set, otherwise the thread could be cancelled while
|
|
* holding the logging mutex, hence causing deadlock with main (or other)
|
|
* thread. */
|
|
|
|
while (1) {
|
|
struct gtp_tunnel *t;
|
|
struct pkt_info pinfo;
|
|
int rc, nread, outfd;
|
|
|
|
/* 1) read from tun */
|
|
rc = read(tun->fd, buffer, MAX_UDP_PACKET);
|
|
if (rc < 0) {
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelst_unused);
|
|
LOGTUN(tun, LOGL_FATAL, "Error readingfrom tun device: %s\n", strerror(errno));
|
|
exit(1);
|
|
}
|
|
nread = rc;
|
|
gtph->length = htons(nread);
|
|
|
|
rc = parse_pkt(&pinfo, buffer, nread);
|
|
if (rc < 0) {
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelst_unused);
|
|
LOGTUN(tun, LOGL_NOTICE, "Error parsing IP packet: %s\n",
|
|
osmo_hexdump(buffer, nread));
|
|
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelst_unused);
|
|
continue;
|
|
}
|
|
|
|
if (pinfo.saddr.ss_family == AF_INET6 && pinfo.proto == IPPROTO_ICMPV6) {
|
|
/* 2) TODO: magic voodoo for IPv6 neighbor discovery */
|
|
}
|
|
|
|
/* 3) look-up tunnel based on source IP address (+ filter) */
|
|
pthread_rwlock_rdlock(&d->rwlock);
|
|
t = _gtp_tunnel_find_eua(tun, (struct sockaddr *) &pinfo.saddr, pinfo.proto);
|
|
if (!t) {
|
|
char host[128];
|
|
char port[8];
|
|
pthread_rwlock_unlock(&d->rwlock);
|
|
getnameinfo((const struct sockaddr *)&pinfo.saddr,
|
|
sizeof(pinfo.saddr), host, sizeof(host), port, sizeof(port),
|
|
NI_NUMERICHOST | NI_NUMERICSERV);
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelst_unused);
|
|
LOGTUN(tun, LOGL_NOTICE, "No tunnel found for source address %s:%s\n", host, port);
|
|
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelst_unused);
|
|
continue;
|
|
}
|
|
outfd = t->gtp_ep->fd;
|
|
memcpy(&daddr, &t->remote_udp, sizeof(daddr));
|
|
gtph->tid = htonl(t->tx_teid);
|
|
pthread_rwlock_unlock(&d->rwlock);
|
|
|
|
/* 4) write to GTP/UDP socket */
|
|
rc = sendto(outfd, base_buffer, nread+sizeof(*gtph), 0,
|
|
(struct sockaddr *)&daddr, sizeof(daddr));
|
|
if (rc < 0) {
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelst_unused);
|
|
LOGTUN(tun, LOGL_FATAL, "Error Writing to UDP socket: %s\n", strerror(errno));
|
|
exit(1);
|
|
}
|
|
}
|
|
pthread_cleanup_pop(1);
|
|
}
|
|
|
|
static int tun_open(int flags, const char *name)
|
|
{
|
|
struct ifreq ifr;
|
|
int fd, rc;
|
|
|
|
fd = open("/dev/net/tun", O_RDWR);
|
|
if (fd < 0) {
|
|
LOGP(DTUN, LOGL_ERROR, "Cannot open /dev/net/tun: %s\n", strerror(errno));
|
|
return fd;
|
|
}
|
|
|
|
memset(&ifr, 0, sizeof(ifr));
|
|
ifr.ifr_flags = IFF_TUN | IFF_NO_PI | flags;
|
|
if (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, name, IFNAMSIZ);
|
|
}
|
|
|
|
/* try to create the device */
|
|
rc = ioctl(fd, TUNSETIFF, (void *) &ifr);
|
|
if (rc < 0) {
|
|
close(fd);
|
|
return rc;
|
|
}
|
|
|
|
/* FIXME: read name back from device? */
|
|
/* FIXME: SIOCSIFTXQLEN / SIOCSIFFLAGS */
|
|
|
|
return fd;
|
|
}
|
|
|
|
static struct tun_device *
|
|
_tun_device_create(struct gtp_daemon *d, const char *devname, const char *netns_name)
|
|
{
|
|
struct rtnl_link *link;
|
|
struct tun_device *tun;
|
|
sigset_t oldmask;
|
|
int rc;
|
|
|
|
tun = talloc_zero(d, struct tun_device);
|
|
if (!tun)
|
|
return NULL;
|
|
|
|
tun->d = d;
|
|
tun->use_count = 1;
|
|
tun->devname = talloc_strdup(tun, devname);
|
|
|
|
if (netns_name) {
|
|
tun->netns_name = talloc_strdup(tun, netns_name);
|
|
tun->netns_fd = get_nsfd(tun->netns_name);
|
|
if (tun->netns_fd < 0) {
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot obtain netns file descriptor: %s\n",
|
|
strerror(errno));
|
|
goto err_free;
|
|
}
|
|
}
|
|
|
|
/* temporarily switch to specified namespace to create tun device */
|
|
if (tun->netns_name) {
|
|
rc = switch_ns(tun->netns_fd, &oldmask);
|
|
if (rc < 0) {
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot switch to netns '%s': %s\n",
|
|
tun->netns_name, strerror(errno));
|
|
goto err_close_ns;
|
|
}
|
|
}
|
|
|
|
tun->fd = tun_open(0, tun->devname);
|
|
if (tun->fd < 0) {
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot open TUN device: %s\n", strerror(errno));
|
|
goto err_restore_ns;
|
|
}
|
|
|
|
tun->nl = nl_socket_alloc();
|
|
if (!tun->nl || nl_connect(tun->nl, NETLINK_ROUTE) < 0) {
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot create netlink socket in namespace '%s'\n",
|
|
tun->netns_name);
|
|
goto err_close;
|
|
}
|
|
|
|
rc = rtnl_link_get_kernel(tun->nl, 0, tun->devname, &link);
|
|
if (rc < 0) {
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot get ifindex for netif after create?!?\n");
|
|
goto err_free_nl;
|
|
}
|
|
tun->ifindex = rtnl_link_get_ifindex(link);
|
|
rtnl_link_put(link);
|
|
|
|
/* switch back to default namespace before creating new thread */
|
|
if (tun->netns_name)
|
|
OSMO_ASSERT(restore_ns(&oldmask) == 0);
|
|
|
|
/* bring the network device up */
|
|
rc = netdev_set_link(tun->nl, tun->ifindex, true);
|
|
if (rc < 0)
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot set interface to 'up'\n");
|
|
|
|
if (tun->netns_name) {
|
|
rc = netdev_add_defaultroute(tun->nl, tun->ifindex, AF_INET);
|
|
if (rc < 0)
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot add IPv4 default route "
|
|
"(rc=%d): %s\n", rc, nl_geterror(rc));
|
|
else
|
|
LOGTUN(tun, LOGL_INFO, "Added IPv4 default route\n");
|
|
|
|
rc = netdev_add_defaultroute(tun->nl, tun->ifindex, AF_INET6);
|
|
if (rc < 0)
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot add IPv6 default route "
|
|
"(rc=%d): %s\n", rc, nl_geterror(rc));
|
|
else
|
|
LOGTUN(tun, LOGL_INFO, "Added IPv6 default route\n");
|
|
}
|
|
|
|
if (pthread_create(&tun->thread, NULL, tun_device_thread, tun)) {
|
|
LOGTUN(tun, LOGL_ERROR, "Cannot create TUN thread: %s\n", strerror(errno));
|
|
goto err_free_nl;
|
|
}
|
|
|
|
LOGTUN(tun, LOGL_INFO, "Created (in netns '%s')\n", tun->netns_name);
|
|
llist_add_tail(&tun->list, &d->tun_devices);
|
|
|
|
return tun;
|
|
|
|
err_free_nl:
|
|
nl_socket_free(tun->nl);
|
|
err_close:
|
|
close(tun->fd);
|
|
err_restore_ns:
|
|
if (tun->netns_name)
|
|
OSMO_ASSERT(restore_ns(&oldmask) == 0);
|
|
err_close_ns:
|
|
if (tun->netns_name)
|
|
close(tun->netns_fd);
|
|
err_free:
|
|
talloc_free(tun);
|
|
return NULL;
|
|
}
|
|
|
|
struct tun_device *
|
|
_tun_device_find(struct gtp_daemon *d, const char *devname)
|
|
{
|
|
struct tun_device *tun;
|
|
|
|
llist_for_each_entry(tun, &d->tun_devices, list) {
|
|
if (!strcmp(tun->devname, devname))
|
|
return tun;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* find the first tun device within given named netns */
|
|
struct tun_device *
|
|
tun_device_find_netns(struct gtp_daemon *d, const char *netns_name)
|
|
{
|
|
struct tun_device *tun;
|
|
|
|
pthread_rwlock_rdlock(&d->rwlock);
|
|
llist_for_each_entry(tun, &d->tun_devices, list) {
|
|
if (!strcmp(tun->netns_name, netns_name)) {
|
|
pthread_rwlock_unlock(&d->rwlock);
|
|
return tun;
|
|
}
|
|
}
|
|
pthread_rwlock_unlock(&d->rwlock);
|
|
return NULL;
|
|
}
|
|
|
|
struct tun_device *
|
|
tun_device_find_or_create(struct gtp_daemon *d, const char *devname, const char *netns_name)
|
|
{
|
|
struct tun_device *tun;
|
|
|
|
/* talloc is not thread safe, all alloc/free must come from main thread */
|
|
ASSERT_MAIN_THREAD(d);
|
|
|
|
pthread_rwlock_wrlock(&d->rwlock);
|
|
tun = _tun_device_find(d, devname);
|
|
if (tun)
|
|
tun->use_count++;
|
|
else
|
|
tun = _tun_device_create(d, devname, netns_name);
|
|
pthread_rwlock_unlock(&d->rwlock);
|
|
|
|
return tun;
|
|
}
|
|
|
|
/* UNLOCKED hard/forced destroy; caller must make sure references are cleaned
|
|
* up, and tun thread is stopped beforehand by calling
|
|
* _tun_device_{deref_}release */
|
|
void _tun_device_destroy(struct tun_device *tun)
|
|
{
|
|
/* talloc is not thread safe, all alloc/free must come from main thread */
|
|
ASSERT_MAIN_THREAD(tun->d);
|
|
LOGTUN(tun, LOGL_INFO, "Destroying\n");
|
|
|
|
if (tun->netns_name)
|
|
close(tun->netns_fd);
|
|
close(tun->fd);
|
|
nl_socket_free(tun->nl);
|
|
talloc_free(tun);
|
|
}
|
|
|
|
/* UNLOCKED remove all objects referencing this tun and then start async tun release procedure */
|
|
void _tun_device_deref_release(struct tun_device *tun)
|
|
{
|
|
struct gtp_daemon *d = tun->d;
|
|
char *devname = talloc_strdup(d, tun->devname);
|
|
struct gtp_tunnel *t, *t2;
|
|
struct tun_device *tun2;
|
|
|
|
/* talloc is not thread safe, all alloc/free must come from main thread */
|
|
ASSERT_MAIN_THREAD(tun->d);
|
|
|
|
llist_for_each_entry_safe(t, t2, &g_daemon->gtp_tunnels, list) {
|
|
if (t->tun_dev == tun)
|
|
_gtp_tunnel_destroy(t);
|
|
}
|
|
/* _tun_device_destroy may already have been called via
|
|
* _gtp_tunnel_destroy -> _tun_device_release, so we have to
|
|
* check if the tun can still be found in the list */
|
|
tun2 = _tun_device_find(d, devname);
|
|
if (tun2 && tun2 == tun)
|
|
_tun_device_release(tun2);
|
|
|
|
talloc_free(devname);
|
|
}
|
|
|
|
/* UNLOCKED release a reference; start async tun release procedure if refcount drops to 0 */
|
|
bool _tun_device_release(struct tun_device *tun)
|
|
{
|
|
bool released = false;
|
|
|
|
/* talloc is not thread safe, all alloc/free must come from main thread */
|
|
ASSERT_MAIN_THREAD(tun->d);
|
|
|
|
tun->use_count--;
|
|
if (tun->use_count == 0) {
|
|
LOGTUN(tun, LOGL_INFO, "Releasing\n");
|
|
llist_del(&tun->list);
|
|
tun->itq_msg.tun_released.tun = tun;
|
|
tun->d->reset_all_state_tun_remaining++;
|
|
/* We cancel the thread: the pthread_cleanup routing will send a message
|
|
* back to us (main thread) when finally cancelled. */
|
|
pthread_cancel(tun->thread);
|
|
released = true;
|
|
} else {
|
|
LOGTUN(tun, LOGL_DEBUG, "Release; new use_count=%lu\n", tun->use_count);
|
|
}
|
|
|
|
return released;
|
|
}
|
|
|
|
/* release a reference; destroy if refcount drops to 0 */
|
|
bool tun_device_release(struct tun_device *tun)
|
|
{
|
|
struct gtp_daemon *d = tun->d;
|
|
bool released;
|
|
|
|
/* talloc is not thread safe, all alloc/free must come from main thread */
|
|
ASSERT_MAIN_THREAD(tun->d);
|
|
|
|
pthread_rwlock_wrlock(&d->rwlock);
|
|
released = _tun_device_release(tun);
|
|
pthread_rwlock_unlock(&d->rwlock);
|
|
|
|
return released;
|
|
}
|