initial check-in of the UECUPS daemon and the related TTCN3 code

Change-Id: I5acf7059722b58e6f57dbf31e18abcc385533970
diff --git a/daemon/Makefile b/daemon/Makefile
new file mode 100644
index 0000000..6cbd59c
--- /dev/null
+++ b/daemon/Makefile
@@ -0,0 +1,16 @@
+LIBS=-lpthread -lsctp $(shell pkg-config --libs libosmocore libosmovty libosmo-netif jansson libnl-route-3.0)
+INC=$(shell pkg-config --cflags libosmocore libosmovty libosmo-netif jansson libnl-route-3.0)
+VER=$(shell git describe --tags)
+CFLAGS=-Wall -g -DPACKAGE_VERSION='"$(VER)"' $(INC)
+
+all: osmo-uecups-daemon
+
+%.o: %.c
+	$(CC) $(CFLAGS) -o $@ -c $^
+
+osmo-uecups-daemon: 	utility.o netdev.o netns.o tun_device.o \
+			gtp_endpoint.o gtp_tunnel.o daemon_vty.o main.o
+	$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+
+clean:
+	@rm -f *.o osmo-uecups-daemon
diff --git a/daemon/daemon_vty.c b/daemon/daemon_vty.c
new file mode 100644
index 0000000..efd9c0f
--- /dev/null
+++ b/daemon/daemon_vty.c
@@ -0,0 +1,290 @@
+#include <string.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/rate_ctr.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/misc.h>
+
+#include "internal.h"
+#include "gtp.h"
+
+#define TUN_STR	"tun device commands\n"
+#define GTP_EP_STR "GTP endpoint commands\n"
+#define TUNNEL_STR "GTP tunnel commands\n"
+
+static void show_tun_hdr(struct vty *vty)
+{
+	vty_out(vty,
+		" tun device name | netwk  namespace | use count%s", VTY_NEWLINE);
+	vty_out(vty,
+		"---------------- | ---------------- | ---------%s", VTY_NEWLINE);
+}
+
+static void show_one_tun(struct vty *vty, const struct tun_device *tun)
+{
+	vty_out(vty, "%16s | %16s | %lu%s",
+		tun->devname, tun->netns_name, tun->use_count, VTY_NEWLINE);
+}
+
+DEFUN(show_tun, show_tun_cmd,
+	"show tun-device [IFNAME]",
+	SHOW_STR TUN_STR
+	"Name of TUN network device\n")
+{
+	struct tun_device *tun;
+
+	show_tun_hdr(vty);
+	pthread_rwlock_rdlock(&g_daemon->rwlock);
+	if (argc) {
+		tun = _tun_device_find(g_daemon, argv[0]);
+		if (!tun) {
+			pthread_rwlock_unlock(&g_daemon->rwlock);
+			vty_out(vty, "Cannot find TUN device '%s'%s", argv[0], VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		show_one_tun(vty, tun);
+	} else {
+		llist_for_each_entry(tun, &g_daemon->tun_devices, list)
+			show_one_tun(vty, tun);
+	}
+	pthread_rwlock_unlock(&g_daemon->rwlock);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(tun_create, tun_create_cmd,
+	"tun-device create IFNAME [NETNS]",
+	TUN_STR "Create a new TUN interface\n"
+	"Name of TUN network device\n"
+	"Name of network namespace for tun device\n"
+	)
+{
+	struct tun_device *tun;
+	const char *ifname = argv[0];
+	const char *netns_name = NULL;
+
+	if (argc > 1)
+		netns_name = argv[1];
+
+	tun = tun_device_find_or_create(g_daemon, ifname, netns_name);
+	if (!tun) {
+		vty_out(vty, "Error creating TUN%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(tun_destroy, tun_destroy_cmd,
+	"tun-device destroy IFNAME",
+	TUN_STR "Destroy a TUN interface\n"
+	"Name of TUN network device\n"
+	)
+{
+	struct tun_device *tun;
+	const char *ifname = argv[0];
+
+	pthread_rwlock_wrlock(&g_daemon->rwlock);
+	tun = _tun_device_find(g_daemon, ifname);
+	if (!tun) {
+		pthread_rwlock_unlock(&g_daemon->rwlock);
+		vty_out(vty, "Cannot destrory non-existant TUN%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	_tun_device_deref_destroy(tun);
+	pthread_rwlock_unlock(&g_daemon->rwlock);
+
+	return CMD_SUCCESS;
+}
+
+
+static void show_ep_hdr(struct vty *vty)
+{
+	vty_out(vty,
+		"                    address port | use count%s", VTY_NEWLINE);
+	vty_out(vty,
+		" ------------------------------- | ---------%s", VTY_NEWLINE);
+}
+
+static void show_one_ep(struct vty *vty, const struct gtp_endpoint *ep)
+{
+	vty_out(vty, "%32s | %lu%s",
+		ep->name, ep->use_count, VTY_NEWLINE);
+
+}
+
+DEFUN(show_gtp, show_gtp_cmd,
+	"show gtp-endpoint [(A.B.C.D|X:X::X:X) [<0-65535>]]",
+	SHOW_STR GTP_EP_STR
+	"Local IP address\n" "Local UDP Port\n")
+{
+	struct gtp_endpoint *ep;
+	struct addrinfo *ai;
+	const char *ipstr;
+	uint16_t port = GTP1U_PORT;
+
+	if (argc > 0) {
+		ipstr = argv[0];
+		if (argc > 1)
+			port = atoi(argv[1]);
+
+		ai = addrinfo_helper(AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, ipstr, port, true);
+		if (!ai) {
+			vty_out(vty, "Error parsing IP/Port%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+	}
+
+	show_ep_hdr(vty);
+	pthread_rwlock_rdlock(&g_daemon->rwlock);
+	if (argc) {
+		ep = _gtp_endpoint_find(g_daemon, (const struct sockaddr_storage *) ai->ai_addr);
+		if (!ep) {
+			pthread_rwlock_unlock(&g_daemon->rwlock);
+			vty_out(vty, "Cannot find GTP endpoint %s:%s%s", argv[0], argv[1], VTY_NEWLINE);
+			freeaddrinfo(ai);
+			return CMD_WARNING;
+		}
+		show_one_ep(vty, ep);
+	} else {
+		llist_for_each_entry(ep, &g_daemon->gtp_endpoints, list)
+			show_one_ep(vty, ep);
+	}
+	pthread_rwlock_unlock(&g_daemon->rwlock);
+
+	freeaddrinfo(ai);
+	return CMD_SUCCESS;
+}
+
+DEFUN(gtp_create, gtp_create_cmd,
+	"gtp-endpoint create (A.B.C.D|X:X::X:X) [<0-65535>]",
+	GTP_EP_STR "Create a new GTP endpoint (UDP socket)\n"
+	"Local IP address\n" "Local UDP Port\n")
+{
+	struct addrinfo *ai;
+	struct gtp_endpoint *ep;
+	const char *ipstr = argv[0];
+	uint16_t port = GTP1U_PORT;
+
+	if (argc > 1)
+		port = atoi(argv[1]);
+
+	ai = addrinfo_helper(AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, ipstr, port, true);
+	if (!ai) {
+		vty_out(vty, "Error parsing IP/Port%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	ep = gtp_endpoint_find_or_create(g_daemon, (struct sockaddr_storage *) ai->ai_addr);
+	if (!ep) {
+		vty_out(vty, "Error creating endpoint%s", VTY_NEWLINE);
+		freeaddrinfo(ai);
+		return CMD_WARNING;
+	}
+
+	freeaddrinfo(ai);
+	return CMD_SUCCESS;
+}
+
+DEFUN(gtp_destroy, gtp_destroy_cmd,
+	"gtp-endpoint destroy (A.B.C.D|X:X::X:X) [<0-65535>]",
+	GTP_EP_STR "Destroy a GTP endpoint\n"
+	"Local IP address\n" "Local UDP Port\n")
+{
+	struct addrinfo *ai;
+	struct gtp_endpoint *ep;
+	const char *ipstr = argv[0];
+	uint16_t port = GTP1U_PORT;
+
+	if (argc > 1)
+		port = atoi(argv[1]);
+
+	ai = addrinfo_helper(AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, ipstr, port, true);
+	if (!ai) {
+		vty_out(vty, "Error parsing IP/Port%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	pthread_rwlock_wrlock(&g_daemon->rwlock);
+	ep = _gtp_endpoint_find(g_daemon, (struct sockaddr_storage *) ai->ai_addr);
+	if (!ep) {
+		pthread_rwlock_unlock(&g_daemon->rwlock);
+		vty_out(vty, "Cannot find to-be-destoryed endpoint%s", VTY_NEWLINE);
+		freeaddrinfo(ai);
+		return CMD_WARNING;
+	}
+	_gtp_endpoint_deref_destroy(ep);
+	pthread_rwlock_unlock(&g_daemon->rwlock);
+
+	freeaddrinfo(ai);
+	return CMD_SUCCESS;
+}
+
+static void show_one_tunnel(struct vty *vty, const struct gtp_tunnel *t)
+{
+	char remote_ip[64], remote_port[16], user_addr[64];
+
+	getnameinfo((struct sockaddr *) &t->remote_udp, sizeof(t->remote_udp),
+		    remote_ip, sizeof(remote_ip), remote_port, sizeof(remote_port),
+		    NI_NUMERICHOST|NI_NUMERICSERV);
+
+	getnameinfo((struct sockaddr *) &t->user_addr, sizeof(t->user_addr),
+		    user_addr, sizeof(user_addr), NULL, 0,
+		    NI_NUMERICHOST|NI_NUMERICSERV);
+
+
+	vty_out(vty, "%s/%08X - %s:%s/%08X %s(%s) %s%s",
+		t->gtp_ep->name, t->rx_teid, remote_ip, remote_port, t->tx_teid,
+		t->tun_dev->devname, t->tun_dev->netns_name, user_addr, VTY_NEWLINE);
+}
+
+DEFUN(show_tunnel, show_tunnel_cmd,
+	"show gtp-tunnel",
+	SHOW_STR TUNNEL_STR)
+{
+	struct gtp_tunnel *t;
+
+	pthread_rwlock_rdlock(&g_daemon->rwlock);
+	llist_for_each_entry(t, &g_daemon->gtp_tunnels, list) {
+		show_one_tunnel(vty, t);
+	}
+	pthread_rwlock_unlock(&g_daemon->rwlock);
+	return CMD_SUCCESS;
+}
+
+
+int gtpud_vty_init(void)
+{
+	install_element_ve(&show_tun_cmd);
+	install_element(ENABLE_NODE, &tun_create_cmd);
+	install_element(ENABLE_NODE, &tun_destroy_cmd);
+
+	install_element_ve(&show_gtp_cmd);
+	install_element(ENABLE_NODE, &gtp_create_cmd);
+	install_element(ENABLE_NODE, &gtp_destroy_cmd);
+
+	install_element_ve(&show_tunnel_cmd);
+
+	return 0;
+}
+
+
+static const char copyright[] =
+	"Copyright (C) 2020 Harald Welte <laforge@gnumonks.org>\r\n"
+	"License GPLv2: GNU GPL version 2 <http://gnu.org/licenses/gpl-2.0.html>\r\n"
+	"This is free software: you are free to change and redistribute it.\r\n"
+	"There is NO WARRANTY, to the extent permitted by law.\r\n";
+
+struct vty_app_info g_vty_info = {
+	.name		= "osmo-gtpud",
+	.version	= PACKAGE_VERSION,
+	.copyright	= copyright,
+};
diff --git a/daemon/gtp.h b/daemon/gtp.h
new file mode 100644
index 0000000..f2acd36
--- /dev/null
+++ b/daemon/gtp.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#pragma once
+#include <stdint.h>
+
+/* General GTP protocol related definitions. */
+
+#define GTP0_PORT	3386
+#define GTP1U_PORT	2152
+
+#define GTP_TPDU	255
+
+struct gtp0_header {	/* According to GSM TS 09.60. */
+	uint8_t	flags;
+	uint8_t	type;
+	uint16_t length;
+	uint16_t seq;
+	uint16_t flow;
+	uint8_t	number;
+	uint8_t	spare[3];
+	uint64_t tid;
+} __attribute__ ((packed));
+
+struct gtp1_header {	/* According to 3GPP TS 29.060. */
+	uint8_t	flags;
+	uint8_t	type;
+	uint16_t length;
+	uint32_t tid;
+} __attribute__ ((packed));
+
+#define GTP1_F_NPDU	0x01
+#define GTP1_F_SEQ	0x02
+#define GTP1_F_EXTHDR	0x04
+#define GTP1_F_MASK	0x07
diff --git a/daemon/gtp_endpoint.c b/daemon/gtp_endpoint.c
new file mode 100644
index 0000000..4221e14
--- /dev/null
+++ b/daemon/gtp_endpoint.c
@@ -0,0 +1,249 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#include <pthread.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/logging.h>
+
+#include "gtp.h"
+#include "internal.h"
+
+#define LOGEP(ep, lvl, fmt, args ...) \
+	LOGP(DEP, lvl, "%s: " fmt, (ep)->name, ## args)
+
+/***********************************************************************
+ * GTP Endpoint (UDP socket)
+ ***********************************************************************/
+
+/* one thread for reading from each GTP/UDP socket (GTP decapsulation -> tun) */
+static void *gtp_endpoint_thread(void *arg)
+{
+	struct gtp_endpoint *ep = (struct gtp_endpoint *)arg;
+	struct gtp_daemon *d = ep->d;
+
+	uint8_t buffer[MAX_UDP_PACKET+sizeof(struct gtp1_header)];
+
+	while (1) {
+		struct gtp_tunnel *t;
+		const struct gtp1_header *gtph;
+		int rc, nread, outfd;
+		uint32_t teid;
+
+		/* 1) read GTP packet from UDP socket */
+		rc = recvfrom(ep->fd, buffer, sizeof(buffer), 0, (struct sockaddr *)NULL, 0);
+		if (rc < 0) {
+			LOGEP(ep, LOGL_FATAL, "Error reading from UDP socket: %s\n", strerror(errno));
+			exit(1);
+		}
+		nread = rc;
+		if (nread < sizeof(*gtph)) {
+			LOGEP(ep, LOGL_NOTICE, "Short read: %d < %lu\n", nread, sizeof(*gtph));
+			continue;
+		}
+		gtph = (struct gtp1_header *)buffer;
+
+		/* check GTP heaader contents */
+		if (gtph->flags != 0x30) {
+			LOGEP(ep, LOGL_NOTICE, "Unexpected GTP Flags: 0x%02x\n", gtph->flags);
+			continue;
+		}
+		if (gtph->type != GTP_TPDU) {
+			LOGEP(ep, LOGL_NOTICE, "Unexpected GTP Message Type: 0x%02x\n", gtph->type);
+			continue;
+		}
+		if (sizeof(*gtph)+ntohs(gtph->length) > nread) {
+			LOGEP(ep, LOGL_NOTICE, "Shotr GTP Message: %lu < len=%d\n",
+				sizeof(*gtph)+ntohs(gtph->length), nread);
+			continue;
+		}
+		teid = ntohl(gtph->tid);
+
+		/* 2) look-up tunnel based on TEID */
+		pthread_rwlock_rdlock(&d->rwlock);
+		t = _gtp_tunnel_find_r(d, teid, ep);
+		if (!t) {
+			pthread_rwlock_unlock(&d->rwlock);
+			LOGEP(ep, LOGL_NOTICE, "Unable to find tunnel for TEID=0x%08x\n", teid);
+			continue;
+		}
+		outfd = t->tun_dev->fd;
+		pthread_rwlock_unlock(&d->rwlock);
+
+		/* 3) write to TUN device */
+		rc = write(outfd, buffer+sizeof(*gtph), ntohs(gtph->length));
+		if (rc < nread-sizeof(struct gtp1_header)) {
+			LOGEP(ep, LOGL_FATAL, "Error writing to tun device %s\n", strerror(errno));
+			exit(1);
+		}
+	}
+}
+
+static struct gtp_endpoint *
+_gtp_endpoint_create(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr)
+{
+	struct gtp_endpoint *ep = talloc_zero(d, struct gtp_endpoint);
+	char ipstr[INET6_ADDRSTRLEN];
+	char portstr[8];
+	int rc;
+
+	if (!ep)
+		return NULL;
+
+	rc = getnameinfo((struct sockaddr *)bind_addr, sizeof(*bind_addr),
+			 ipstr, sizeof(ipstr), portstr, sizeof(portstr), NI_NUMERICHOST|NI_NUMERICSERV);
+	if (rc != 0)
+		goto out_free;
+	ep->name = talloc_asprintf(ep, "%s:%s", ipstr, portstr);
+
+	ep->d = d;
+	ep->use_count = 1;
+	ep->bind_addr = *bind_addr;
+	ep->fd = socket(ep->bind_addr.ss_family, SOCK_DGRAM, IPPROTO_UDP);
+	if (ep->fd < 0) {
+		LOGEP(ep, LOGL_ERROR, "Cannot create UDP socket: %s\n", strerror(errno));
+		goto out_free;
+	}
+	rc = bind(ep->fd, (struct sockaddr *) &ep->bind_addr, sizeof(ep->bind_addr));
+	if (rc < 0) {
+		LOGEP(ep, LOGL_ERROR, "Cannot bind UDP socket: %s\n", strerror(errno));
+		goto out_close;
+	}
+
+	if (pthread_create(&ep->thread, NULL, gtp_endpoint_thread, ep)) {
+		LOGEP(ep, LOGL_ERROR, "Cannot start GTP thread: %s\n", strerror(errno));
+		goto out_close;
+	}
+
+	llist_add_tail(&ep->list, &d->gtp_endpoints);
+	LOGEP(ep, LOGL_INFO, "Created\n");
+
+	return ep;
+
+out_close:
+	close(ep->fd);
+out_free:
+	talloc_free(ep);
+	return NULL;
+}
+
+struct gtp_endpoint *
+_gtp_endpoint_find(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr)
+{
+	struct gtp_endpoint *ep;
+
+	llist_for_each_entry(ep, &d->gtp_endpoints, list) {
+		if (sockaddr_equals((const struct sockaddr *) &ep->bind_addr,
+				    (const struct sockaddr *) bind_addr)) {
+			return ep;
+		}
+	}
+	return NULL;
+}
+
+struct gtp_endpoint *
+gtp_endpoint_find_or_create(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr)
+{
+	struct gtp_endpoint *ep;
+
+	/* talloc is not thread safe, all alloc/free must come from main thread */
+	ASSERT_MAIN_THREAD(d);
+
+	pthread_rwlock_wrlock(&d->rwlock);
+	ep = _gtp_endpoint_find(d, bind_addr);
+	if (ep)
+		ep->use_count++;
+	else
+		ep = _gtp_endpoint_create(d, bind_addr);
+	pthread_rwlock_unlock(&d->rwlock);
+
+	return ep;
+}
+
+/* UNLOCKED hard/forced destroy; caller must make sure references are cleaned up */
+static void _gtp_endpoint_destroy(struct gtp_endpoint *ep)
+{
+	/* talloc is not thread safe, all alloc/free must come from main thread */
+	ASSERT_MAIN_THREAD(ep->d);
+
+	if (ep->use_count)
+		LOGEP(ep, LOGL_ERROR, "Destroying despite use_count %lu != 0\n", ep->use_count);
+	else
+		LOGEP(ep, LOGL_INFO, "Destroying\n");
+
+	pthread_cancel(ep->thread);
+	llist_del(&ep->list);
+	close(ep->fd);
+	talloc_free(ep);
+}
+
+/* UNLOCKED remove all objects referencing this ep and then destroy */
+void _gtp_endpoint_deref_destroy(struct gtp_endpoint *ep)
+{
+	struct gtp_daemon *d = ep->d;
+	struct sockaddr_storage ss = ep->bind_addr;
+	struct gtp_tunnel *t, *t2;
+	struct gtp_endpoint *ep2;
+
+	/* talloc is not thread safe, all alloc/free must come from main thread */
+	ASSERT_MAIN_THREAD(ep->d);
+
+	/* iterate over all tunnels; delete all references to ep */
+	llist_for_each_entry_safe(t, t2, &d->gtp_tunnels, list) {
+		if (t->gtp_ep == ep)
+			_gtp_tunnel_destroy(t);
+	}
+
+	/* _gtp_endpoint_destroy may already have been called via
+	 * _gtp_tunnel_destroy -> gtp_endpoint_release, so we have to
+	 * check if the ep can still be found in the list */
+	ep2 = _gtp_endpoint_find(d, &ss);
+	if (ep2 && ep2 == ep)
+		_gtp_endpoint_destroy(ep2);
+}
+
+/* UNLOCKED release a reference; destroy if refcount drops to 0 */
+bool _gtp_endpoint_release(struct gtp_endpoint *ep)
+{
+	bool released = false;
+
+	/* talloc is not thread safe, all alloc/free must come from main thread */
+	ASSERT_MAIN_THREAD(ep->d);
+
+	ep->use_count--;
+	if (ep->use_count == 0) {
+		_gtp_endpoint_destroy(ep);
+		released = true;
+	} else
+		LOGEP(ep, LOGL_DEBUG, "Release; new use_count=%lu\n", ep->use_count);
+
+	return released;
+}
+
+
+/* release a reference; destroy if refcount drops to 0 */
+bool gtp_endpoint_release(struct gtp_endpoint *ep)
+{
+	struct gtp_daemon *d = ep->d;
+	bool released;
+
+	/* talloc is not thread safe, all alloc/free must come from main thread */
+	ASSERT_MAIN_THREAD(ep->d);
+
+	pthread_rwlock_wrlock(&d->rwlock);
+	released = _gtp_endpoint_release(ep);
+	pthread_rwlock_unlock(&d->rwlock);
+
+	return released;
+}
diff --git a/daemon/gtp_tunnel.c b/daemon/gtp_tunnel.c
new file mode 100644
index 0000000..0d049c7
--- /dev/null
+++ b/daemon/gtp_tunnel.c
@@ -0,0 +1,169 @@
+/* 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 <assert.h>
+#include <errno.h>
+
+#include <pthread.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/logging.h>
+
+#include "internal.h"
+
+#define LOGT(t, lvl, fmt, args ...) \
+	LOGP(DGT, lvl, "%s: " fmt, (t)->name, ## args)
+
+/***********************************************************************
+ * GTP Tunnel
+ ***********************************************************************/
+struct gtp_tunnel *gtp_tunnel_alloc(struct gtp_daemon *d, const struct gtp_tunnel_params *cpars)
+{
+	struct gtp_tunnel *t;
+
+	t = talloc_zero(d, struct gtp_tunnel);
+	if (!t)
+		goto out_unlock;
+	t->d = d;
+	t->name = talloc_asprintf(t, "%s-R%08x-T%08x", cpars->tun_name, cpars->rx_teid, cpars->tx_teid);
+	t->tun_dev = tun_device_find_or_create(d, cpars->tun_name, cpars->tun_netns_name);
+	if (!t->tun_dev) {
+		LOGT(t, LOGL_ERROR, "Cannot find or create tun device %s\n", cpars->tun_name);
+		goto out_free;
+	}
+
+	t->gtp_ep = gtp_endpoint_find_or_create(d, &cpars->local_udp);
+	if (!t->gtp_ep) {
+		LOGT(t, LOGL_ERROR, "Cannot find or create GTP endpoint\n");
+		goto out_tun;
+	}
+
+	pthread_rwlock_wrlock(&d->rwlock);
+	/* check if we already have a tunnel with same Rx-TEID + endpoint */
+	if (_gtp_tunnel_find_r(d, cpars->rx_teid, t->gtp_ep)) {
+		LOGT(t, LOGL_ERROR, "Error: We already have a tunnel for RxTEID 0x%08x "
+			"on this endpoint (%s)\n", cpars->rx_teid, t->gtp_ep->name);
+		goto out_ep;
+	}
+
+	/* FIXME: check if we already have a tunnel with same Tx-TEID + peer */
+	/* FIXME: check if we already have a tunnel with same tun + EUA + filter */
+
+	t->rx_teid = cpars->rx_teid;
+	t->tx_teid = cpars->tx_teid;
+	memcpy(&t->user_addr, &cpars->user_addr, sizeof(t->user_addr));
+	memcpy(&t->remote_udp, &cpars->remote_udp, sizeof(t->remote_udp));
+
+	if (netdev_add_addr(t->tun_dev->nl, t->tun_dev->ifindex, &t->user_addr) < 0) {
+		LOGT(t, LOGL_ERROR, "Cannot add user addr to tun device: %s\n",
+			strerror(errno));
+	}
+
+	/* TODO: hash table? */
+	llist_add_tail(&t->list, &d->gtp_tunnels);
+	pthread_rwlock_unlock(&d->rwlock);
+	LOGT(t, LOGL_NOTICE, "Created\n");
+
+	return t;
+
+out_ep:
+	_gtp_endpoint_release(t->gtp_ep);
+out_tun:
+	_tun_device_release(t->tun_dev);
+out_free:
+	talloc_free(t);
+out_unlock:
+	pthread_rwlock_unlock(&d->rwlock);
+
+	return NULL;
+}
+
+/* find tunnel by R(x_teid), T(x_teid) + A(ddr) */
+static struct gtp_tunnel *
+_gtp_tunnel_find_rta(struct gtp_daemon *d, uint32_t rx_teid, uint32_t tx_teid,
+		     const struct sockaddr_storage *user_addr)
+{
+	struct gtp_tunnel *t;
+	llist_for_each_entry(t, &d->gtp_tunnels, list) {
+		if (t->rx_teid == rx_teid && t->tx_teid == tx_teid &&
+		    sockaddr_equals((struct sockaddr *) &t->user_addr, (struct sockaddr *)user_addr))
+			return t;
+	}
+	return NULL;
+}
+
+/* find tunnel by R(x_teid) + optionally local endpoint */
+struct gtp_tunnel *
+_gtp_tunnel_find_r(struct gtp_daemon *d, uint32_t rx_teid, struct gtp_endpoint *ep)
+{
+	struct gtp_tunnel *t;
+	llist_for_each_entry(t, &d->gtp_tunnels, list) {
+		if (t->rx_teid == rx_teid) {
+			if (!ep)
+				return t;
+			if (t->gtp_ep == ep)
+				return t;
+		}
+	}
+	return NULL;
+}
+
+/* UNLOCKED find tunnel by tun + EUA ip (+proto/port) */
+struct gtp_tunnel *
+_gtp_tunnel_find_eua(struct tun_device *tun, const struct sockaddr *sa, uint8_t proto)
+{
+	struct gtp_daemon *d = tun->d;
+	struct gtp_tunnel *t;
+
+	llist_for_each_entry(t, &d->gtp_tunnels, list) {
+		/* TODO: Find best matching filter */
+		if (t->tun_dev == tun && sockaddr_equals(sa, (struct sockaddr *) &t->user_addr))
+			return t;
+	}
+	return NULL;
+}
+
+/* UNLOCKED destroy of tunnel; drops references to EP + TUN */
+void _gtp_tunnel_destroy(struct gtp_tunnel *t)
+{
+	LOGT(t, LOGL_NOTICE, "Destroying\n");
+	/* talloc is not thread safe, all alloc/free must come from main thread */
+	ASSERT_MAIN_THREAD(t->d);
+
+	if (netdev_del_addr(t->tun_dev->nl, t->tun_dev->ifindex, &t->user_addr) < 0)
+		LOGT(t, LOGL_ERROR, "Cannot remove user address: %s\n", strerror(errno));
+
+	llist_del(&t->list);
+
+	/* drop reference to endpoint + tun */
+	_gtp_endpoint_release(t->gtp_ep);
+	_tun_device_release(t->tun_dev);
+
+	talloc_free(t);
+}
+
+bool gtp_tunnel_destroy(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr, uint32_t rx_teid)
+{
+	struct gtp_endpoint *ep;
+	bool rc = false;
+
+	pthread_rwlock_wrlock(&d->rwlock);
+	/* find endpoint for bind_addr */
+	ep = _gtp_endpoint_find(d, bind_addr);
+	if (ep) {
+		/* find tunnel for rx TEID within endpoint */
+		struct gtp_tunnel *t = _gtp_tunnel_find_r(d, rx_teid, ep);
+		if (t) {
+			_gtp_tunnel_destroy(t);
+			rc = true;
+		}
+	}
+	pthread_rwlock_unlock(&d->rwlock);
+
+	return rc;
+}
diff --git a/daemon/internal.h b/daemon/internal.h
new file mode 100644
index 0000000..cbccbfc
--- /dev/null
+++ b/daemon/internal.h
@@ -0,0 +1,223 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <pthread.h>
+#include <sys/socket.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/write_queue.h>
+#include <osmocom/core/utils.h>
+
+struct nl_sock;
+struct osmo_stream_srv_link;
+
+/***********************************************************************
+ * Utility
+ ***********************************************************************/
+/* ensure we are called from main thread context */
+#define ASSERT_MAIN_THREAD(d) OSMO_ASSERT(pthread_self() == (d)->main_thread)
+
+#define MAX_UDP_PACKET 65535
+
+bool sockaddr_equals(const struct sockaddr *a, const struct sockaddr *b);
+
+struct addrinfo *addrinfo_helper(uint16_t family, uint16_t type, uint8_t proto,
+				 const char *host, uint16_t port, bool passive);
+enum {
+	DTUN,
+	DEP,
+	DGT,
+	DUECUPS,
+};
+
+/***********************************************************************
+ * netdev / netlink
+ ***********************************************************************/
+
+int netdev_add_addr(struct nl_sock *nlsk, int ifindex, const struct sockaddr_storage *ss);
+int netdev_del_addr(struct nl_sock *nlsk, int ifindex, const struct sockaddr_storage *ss);
+int netdev_set_link(struct nl_sock *nlsk, int ifindex, bool up);
+int netdev_add_defaultroute(struct nl_sock *nlsk, int ifindex, uint8_t family);
+
+
+/***********************************************************************
+ * GTP Endpoint (UDP socket)
+ ***********************************************************************/
+
+struct gtp_daemon;
+
+/* local UDP socket for GTP communication */
+struct gtp_endpoint {
+	/* entry in global list */
+	struct llist_head list;
+	/* back-pointer to daemon */
+	struct gtp_daemon *d;
+	unsigned long use_count;
+
+	/* file descriptor */
+	int fd;
+
+	/* local IP:port */
+	struct sockaddr_storage bind_addr;
+	char *name;
+
+	/* the thread handling Rx from the fd/socket */
+	pthread_t thread;
+};
+
+
+struct gtp_endpoint *
+gtp_endpoint_find_or_create(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr);
+
+struct gtp_endpoint *
+_gtp_endpoint_find(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr);
+
+void _gtp_endpoint_deref_destroy(struct gtp_endpoint *ep);
+
+bool _gtp_endpoint_release(struct gtp_endpoint *ep);
+
+bool gtp_endpoint_release(struct gtp_endpoint *ep);
+
+
+
+/***********************************************************************
+ * TUN Device
+ ***********************************************************************/
+
+struct tun_device {
+	/* entry in global list */
+	struct llist_head list;
+	/* back-pointer to daemon */
+	struct gtp_daemon *d;
+	unsigned long use_count;
+
+	/* which device we refer to */
+	const char *devname;
+	int ifindex;
+
+	/* file descriptor */
+	int fd;
+
+	/* network namespace */
+	const char *netns_name;
+	int netns_fd;
+
+	/* netlink socket in the namespace of the tun device */
+	struct nl_sock *nl;
+
+	/* list of local addresses? or simply only have the kernel know thses? */
+
+	/* the thread handling Rx from the tun fd */
+	pthread_t thread;
+};
+
+struct tun_device *
+tun_device_find_or_create(struct gtp_daemon *d, const char *devname, const char *netns_name);
+
+struct tun_device *
+_tun_device_find(struct gtp_daemon *d, const char *devname);
+
+void _tun_device_deref_destroy(struct tun_device *tun);
+
+bool _tun_device_release(struct tun_device *tun);
+
+bool tun_device_release(struct tun_device *tun);
+
+
+
+/***********************************************************************
+ * GTP Tunnel
+ ***********************************************************************/
+
+/* Every tunnel is identified uniquely by the following tuples:
+ *
+ * a) local endpoint + TEID
+ *    this is what happens on incoming GTP messages
+ *
+ * b) tun device + end-user-address (+ filter, if any)
+ *    this is what happens when IP arrives on the tun device
+ */
+
+struct gtp_tunnel {
+	/* entry in global list / hash table */
+	struct llist_head list;
+	/* back-pointer to daemon */
+	struct gtp_daemon *d;
+
+	const char *name;
+
+	/* the TUN device associated with this tunnel */
+	struct tun_device *tun_dev;
+	/* the GTP endpoint (UDP socket) associated with this tunnel */
+	struct gtp_endpoint *gtp_ep;
+
+	/* TEID on transmit (host byte order) */
+	uint32_t tx_teid;
+	/* TEID one receive (host byte order) */
+	uint32_t rx_teid;
+
+	/* End user Address (inner IP) */
+	struct sockaddr_storage	user_addr;
+
+	/* Remote UDP IP/Port*/
+	struct sockaddr_storage remote_udp;
+
+	/* TODO: Filter */
+};
+
+struct gtp_tunnel *
+_gtp_tunnel_find_r(struct gtp_daemon *d, uint32_t rx_teid, struct gtp_endpoint *ep);
+
+struct gtp_tunnel *
+_gtp_tunnel_find_eua(struct tun_device *tun, const struct sockaddr *sa, uint8_t proto);
+
+struct gtp_tunnel_params {
+	/* TEID in receive and transmit direction */
+	uint32_t rx_teid;
+	uint32_t tx_teid;
+
+	/* end user address */
+	struct sockaddr_storage user_addr;
+
+	/* remote GTP/UDP IP+Port */
+	struct sockaddr_storage remote_udp;
+
+	/* local GTP/UDP IP+Port (used to lookup/create local EP) */
+	struct sockaddr_storage local_udp;
+
+	/* local TUN device name (used to lookup/create local tun) */
+	const char *tun_name;
+        const char *tun_netns_name;
+};
+struct gtp_tunnel *gtp_tunnel_alloc(struct gtp_daemon *d, const struct gtp_tunnel_params *cpars);
+
+void _gtp_tunnel_destroy(struct gtp_tunnel *t);
+bool gtp_tunnel_destroy(struct gtp_daemon *d, const struct sockaddr_storage *bind_addr, uint32_t rx_teid);
+
+
+/***********************************************************************
+ * GTP Daemon
+ ***********************************************************************/
+
+struct gtp_daemon {
+	/* global lists of various objects */
+	struct llist_head gtp_endpoints;
+	struct llist_head tun_devices;
+	struct llist_head gtp_tunnels;
+	/* lock protecting all of the above lists */
+	pthread_rwlock_t rwlock;
+	/* main thread ID */
+	pthread_t main_thread;
+	/* client CUPS interface */
+	struct llist_head cups_clients;
+	struct osmo_stream_srv_link *cups_link;
+
+	struct {
+		char *cups_local_ip;
+		uint16_t cups_local_port;
+	} cfg;
+};
+extern struct gtp_daemon *g_daemon;
+
+int gtpud_vty_init(void);
diff --git a/daemon/main.c b/daemon/main.c
new file mode 100644
index 0000000..2375877
--- /dev/null
+++ b/daemon/main.c
@@ -0,0 +1,538 @@
+/* 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 <assert.h>
+#include <sys/types.h>
+#include <signal.h>
+#include <errno.h>
+
+#include <pthread.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/application.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/stats.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/vty/telnet_interface.h>
+#include <osmocom/vty/logging.h>
+#include <osmocom/vty/stats.h>
+#include <osmocom/vty/ports.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/misc.h>
+
+#include <osmocom/netif/stream.h>
+#include <netinet/sctp.h>
+
+#include <jansson.h>
+
+#include "internal.h"
+#include "netns.h"
+#include "gtp.h"
+
+/***********************************************************************
+ * Client (Contol/User Plane Separation) Socket
+ ***********************************************************************/
+
+#define CUPS_MSGB_SIZE	1024
+
+#define LOGCC(cc, lvl, fmt, args ...)	\
+	LOGP(DUECUPS, lvl, "%s: " fmt, (cc)->sockname, ## args)
+
+struct cups_client {
+	/* member in daemon->cups_clients */
+	struct llist_head list;
+	/* back-pointer to daemon */
+	struct gtp_daemon *d;
+	/* client socket */
+	struct osmo_stream_srv *srv;
+	char sockname[OSMO_SOCK_NAME_MAXLEN];
+};
+
+/* Send JSON to a given client/connection */
+static int cups_client_tx_json(struct cups_client *cc, json_t *jtx)
+{
+	struct msgb *msg = msgb_alloc(CUPS_MSGB_SIZE, "Tx JSON");
+	char *json_str = json_dumps(jtx, JSON_SORT_KEYS);
+	char *out;
+	int json_strlen;
+
+	json_decref(jtx);
+	if (!json_str) {
+		LOGCC(cc, LOGL_ERROR, "Error encoding JSON\n");
+		return 0;
+	}
+	json_strlen = strlen(json_str);
+
+	LOGCC(cc, LOGL_DEBUG, "JSON Tx '%s'\n", json_str);
+
+	if (json_strlen > msgb_tailroom(msg)) {
+		LOGCC(cc, LOGL_ERROR, "Not enough room for JSON in msgb\n");
+		free(json_str);
+		return 0;
+	}
+
+	out = (char *)msgb_put(msg, json_strlen);
+	memcpy(out, json_str, json_strlen);
+	free(json_str);
+	osmo_stream_srv_send(cc->srv, msg);
+
+	return 0;
+}
+
+static json_t *gen_uecups_result(const char *name, const char *res)
+{
+	json_t *jres = json_object();
+	json_t *jret = json_object();
+
+	json_object_set_new(jres, "result", json_string(res));
+	json_object_set_new(jret, name, jres);
+
+	return jret;
+}
+
+static int parse_ep(struct sockaddr_storage *out, json_t *in)
+{
+	json_t *jaddr_type, *jport, *jip;
+	const char *addr_type, *ip;
+	uint8_t buf[16];
+
+	/* {"addr_type":"IPV4","ip":"31323334","Port":2152} */
+
+	if (!json_is_object(in))
+		return -EINVAL;
+
+	jaddr_type = json_object_get(in, "addr_type");
+	jport = json_object_get(in, "Port");
+	jip = json_object_get(in, "ip");
+
+	if (!jaddr_type || !jport || !jip)
+		return -EINVAL;
+
+	if (!json_is_string(jaddr_type) || !json_is_integer(jport) || !json_is_string(jip))
+		return -EINVAL;
+
+	addr_type = json_string_value(jaddr_type);
+	ip = json_string_value(jip);
+
+	memset(out, 0, sizeof(*out));
+
+	if (!strcmp(addr_type, "IPV4")) {
+		struct sockaddr_in *sin = (struct sockaddr_in *) out;
+		if (osmo_hexparse(ip, buf, sizeof(buf)) != 4)
+			return -EINVAL;
+		memcpy(&sin->sin_addr, buf, 4);
+		sin->sin_family = AF_INET;
+		sin->sin_port = htons(json_integer_value(jport));
+	} else if (!strcmp(addr_type, "IPV6")) {
+		struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) out;
+		if (osmo_hexparse(ip, buf, sizeof(buf)) != 16)
+			return -EINVAL;
+		memcpy(&sin6->sin6_addr, buf, 16);
+		sin6->sin6_family = AF_INET6;
+		sin6->sin6_port = htons(json_integer_value(jport));
+	} else
+		return -EINVAL;
+
+	return 0;
+}
+
+static int parse_eua(struct sockaddr_storage *out, json_t *jip, json_t *jaddr_type)
+{
+	const char *addr_type, *ip;
+	uint8_t buf[16];
+
+	if (!json_is_string(jip) || !json_is_string(jaddr_type))
+		return -EINVAL;
+
+	addr_type = json_string_value(jaddr_type);
+	ip = json_string_value(jip);
+
+	memset(out, 0, sizeof(*out));
+
+	if (!strcmp(addr_type, "IPV4")) {
+		struct sockaddr_in *sin = (struct sockaddr_in *) out;
+		if (osmo_hexparse(ip, buf, sizeof(buf)) != 4)
+			return -EINVAL;
+		memcpy(&sin->sin_addr, buf, 4);
+		sin->sin_family = AF_INET;
+	} else if (!strcmp(addr_type, "IPV6")) {
+		struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) out;
+		if (osmo_hexparse(ip, buf, sizeof(buf)) != 16)
+			return -EINVAL;
+		memcpy(&sin6->sin6_addr, buf, 16);
+		sin6->sin6_family = AF_INET6;
+	} else
+		return -EINVAL;
+
+	return 0;
+}
+
+
+static int parse_create_tun(struct gtp_tunnel_params *out, json_t *ctun)
+{
+	json_t *jlocal_gtp_ep, *jremote_gtp_ep;
+	json_t *jrx_teid, *jtx_teid;
+	json_t *jtun_dev_name, *jtun_netns_name;
+	json_t *juser_addr, *juser_addr_type;
+	int rc;
+
+	/* '{"create_tun":{"tx_teid":1234,"rx_teid":5678,"user_addr_type":"IPV4","user_addr":"21222324","local_gtp_ep":{"addr_type":"IPV4","ip":"31323334","Port":2152},"remote_gtp_ep":{"addr_type":"IPV4","ip":"41424344","Port":2152},"tun_dev_name":"tun23","tun_netns_name":"foo"}}' */
+
+	if (!json_is_object(ctun))
+		return -EINVAL;
+
+	/* mandatory IEs */
+	jlocal_gtp_ep = json_object_get(ctun, "local_gtp_ep");
+	jremote_gtp_ep = json_object_get(ctun, "remote_gtp_ep");
+	jrx_teid = json_object_get(ctun, "rx_teid");
+	jtx_teid = json_object_get(ctun, "tx_teid");
+	jtun_dev_name = json_object_get(ctun, "tun_dev_name");
+	juser_addr = json_object_get(ctun, "user_addr");
+	juser_addr_type = json_object_get(ctun, "user_addr_type");
+
+	if (!jlocal_gtp_ep || !jremote_gtp_ep || !jrx_teid || !jtx_teid || !jtun_dev_name ||
+	    !juser_addr || !juser_addr_type)
+		return -EINVAL;
+	if (!json_is_object(jlocal_gtp_ep) || !json_is_object(jremote_gtp_ep) ||
+	    !json_is_integer(jrx_teid) || !json_is_integer(jtx_teid) ||
+	    !json_is_string(jtun_dev_name) ||
+	    !json_is_string(juser_addr) || !json_is_string(juser_addr_type))
+		return -EINVAL;
+
+	memset(out, 0, sizeof(*out));
+
+	rc = parse_ep(&out->local_udp, jlocal_gtp_ep);
+	if (rc < 0)
+		return rc;
+	rc = parse_ep(&out->remote_udp, jremote_gtp_ep);
+	if (rc < 0)
+		return rc;
+	rc = parse_eua(&out->user_addr, juser_addr, juser_addr_type);
+	if (rc < 0)
+		return rc;
+	out->rx_teid = json_integer_value(jrx_teid);
+	out->tx_teid = json_integer_value(jtx_teid);
+	out->tun_name = talloc_strdup(out, json_string_value(jtun_dev_name));
+
+	/* optional IEs */
+	jtun_netns_name = json_object_get(ctun, "tun_netns_name");
+	if (jtun_netns_name) {
+		if (!json_is_string(jtun_netns_name))
+			return -EINVAL;
+		out->tun_netns_name = talloc_strdup(out, json_string_value(jtun_netns_name));
+	}
+
+	return 0;
+}
+
+
+static int cups_client_handle_create_tun(struct cups_client *cc, json_t *ctun)
+{
+	int rc;
+	struct gtp_tunnel_params *tpars = talloc_zero(cc, struct gtp_tunnel_params);
+	struct gtp_tunnel *t;
+
+	rc = parse_create_tun(tpars, ctun);
+	if (rc < 0) {
+		talloc_free(tpars);
+		return rc;
+	}
+
+	t = gtp_tunnel_alloc(g_daemon, tpars);
+	if (!t) {
+		LOGCC(cc, LOGL_NOTICE, "Failed to allocate tunnel\n");
+		cups_client_tx_json(cc, gen_uecups_result("create_tun_res", "ERR_NOT_FOUND"));
+	} else {
+		cups_client_tx_json(cc, gen_uecups_result("create_tun_res", "OK"));
+	}
+
+	talloc_free(tpars);
+	return 0;
+}
+
+static int cups_client_handle_destroy_tun(struct cups_client *cc, json_t *dtun)
+{
+	struct sockaddr_storage local_ep_addr;
+	json_t *jlocal_gtp_ep, *jrx_teid;
+	uint32_t rx_teid;
+	int rc;
+
+	jlocal_gtp_ep = json_object_get(dtun, "local_gtp_ep");
+	jrx_teid = json_object_get(dtun, "rx_teid");
+
+	if (!jlocal_gtp_ep || !jrx_teid)
+		return -EINVAL;
+
+	if (!json_is_object(jlocal_gtp_ep) || !json_is_integer(jrx_teid))
+		return -EINVAL;
+
+	rc = parse_ep(&local_ep_addr, jlocal_gtp_ep);
+	if (rc < 0)
+		return rc;
+	rx_teid = json_integer_value(jrx_teid);
+
+	rc = gtp_tunnel_destroy(g_daemon, &local_ep_addr, rx_teid);
+	if (rc < 0) {
+		LOGCC(cc, LOGL_NOTICE, "Failed to destroy tunnel\n");
+		cups_client_tx_json(cc, gen_uecups_result("destroy_tun_res", "ERR_NOT_FOUND"));
+	} else {
+		cups_client_tx_json(cc, gen_uecups_result("destroy_tun_res", "OK"));
+	}
+
+	return 0;
+}
+
+static int cups_client_handle_json(struct cups_client *cc, json_t *jroot)
+{
+	void *iter;
+	const char *key;
+	json_t *cmd;
+	int rc;
+
+	if (!json_is_object(jroot))
+		return -EINVAL;
+
+	iter = json_object_iter(jroot);
+	key = json_object_iter_key(iter);
+	cmd = json_object_iter_value(iter);
+	if (!iter || !key || !cmd)
+		return -EINVAL;
+
+	if (!strcmp(key, "create_tun")) {
+		rc = cups_client_handle_create_tun(cc, cmd);
+	} else if (!strcmp(key, "destroy_tun")) {
+		rc = cups_client_handle_destroy_tun(cc, cmd);
+	} else {
+		LOGCC(cc, LOGL_NOTICE, "Unknown command '%s' received\n", key);
+		return -EINVAL;
+	}
+
+	if (rc < 0) {
+		LOGCC(cc, LOGL_NOTICE, "Error %d handling '%s' command\n", rc, key);
+		char buf[64];
+		snprintf(buf, sizeof(buf), "%s_res", key);
+		cups_client_tx_json(cc, gen_uecups_result(buf, "ERR_INVALID_DATA"));
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/* control/user plane separation per-client read cb */
+static int cups_client_read_cb(struct osmo_stream_srv *conn)
+{
+	struct osmo_fd *ofd = osmo_stream_srv_get_ofd(conn);
+	struct cups_client *cc = osmo_stream_srv_get_data(conn);
+	struct msgb *msg = msgb_alloc(CUPS_MSGB_SIZE, "Rx JSON");
+	struct sctp_sndrcvinfo sinfo;
+	json_error_t jerr;
+	json_t *jroot;
+	int flags = 0;
+	int rc = 0;
+
+	/* Read message from socket */
+	/* we cannot use osmo_stream_srv_recv() here, as we might get some out-of-band info from
+	 * SCTP. FIXME: add something like osmo_stream_srv_recv_sctp() to libosmo-netif and use
+	 * it here as well as in libosmo-sigtran and osmo-msc */
+	rc = sctp_recvmsg(ofd->fd, msg->tail, msgb_tailroom(msg), NULL, NULL, &sinfo,&flags);
+	if (rc <= 0) {
+		osmo_stream_srv_destroy(conn);
+		rc = -1;
+		goto out;
+	} else
+		msgb_put(msg, rc);
+
+	if (flags & MSG_NOTIFICATION) {
+		union sctp_notification *notif = (union sctp_notification *) msgb_data(msg);
+		switch (notif->sn_header.sn_type) {
+		case SCTP_SHUTDOWN_EVENT:
+			osmo_stream_srv_destroy(conn);
+			rc = -EBADF;
+			goto out;
+		default:
+			break;
+		}
+		goto out;
+	}
+
+	LOGCC(cc, LOGL_DEBUG, "Rx '%s'\n", msgb_data(msg));
+
+	/* Parse the JSON */
+	jroot = json_loadb((const char *) msgb_data(msg), msgb_length(msg), 0, &jerr);
+	if (!jroot) {
+		LOGCC(cc, LOGL_ERROR, "Error decoding JSON (%s)", jerr.text);
+		rc = -1;
+		goto out;
+	}
+
+	/* Dispatch */
+	rc = cups_client_handle_json(cc, jroot);
+
+	json_decref(jroot);
+	msgb_free(msg);
+
+	return 0;
+out:
+	msgb_free(msg);
+	return rc;
+}
+
+static int cups_client_closed_cb(struct osmo_stream_srv *conn)
+{
+	struct cups_client *cc = osmo_stream_srv_get_data(conn);
+
+	LOGCC(cc, LOGL_INFO, "UECUPS connection lost\n");
+	llist_del(&cc->list);
+	return 0;
+}
+
+
+/* the control/user plane separation server bind/accept fd */
+static int cups_accept_cb(struct osmo_stream_srv_link *link, int fd)
+{
+	struct gtp_daemon *d = osmo_stream_srv_link_get_data(link);
+	struct cups_client *cc;
+
+	cc = talloc_zero(d, struct cups_client);
+	if (!cc)
+		return -1;
+
+	osmo_sock_get_name_buf(cc->sockname, sizeof(cc->sockname), fd);
+	cc->srv = osmo_stream_srv_create(cc, link, fd, cups_client_read_cb, cups_client_closed_cb, cc);
+	if (!cc->srv) {
+		talloc_free(cc);
+		return -1;
+	}
+	LOGCC(cc, LOGL_INFO, "Accepted new UECUPS connection\n");
+
+	llist_add_tail(&cc->list, &d->cups_clients);
+
+	return 0;
+}
+
+/***********************************************************************
+ * GTP Daemon
+ ***********************************************************************/
+
+struct gtp_daemon *g_daemon;
+static int g_daemonize;
+static char *g_config_file = "osmo-gtpu-daemon.cfg";
+extern struct vty_app_info g_vty_info;
+
+static struct gtp_daemon *gtp_daemon_alloc(void *ctx)
+{
+	struct gtp_daemon *d = talloc_zero(ctx, struct gtp_daemon);
+	if (!d)
+		return NULL;
+
+	INIT_LLIST_HEAD(&d->gtp_endpoints);
+	INIT_LLIST_HEAD(&d->tun_devices);
+	INIT_LLIST_HEAD(&d->gtp_tunnels);
+	pthread_rwlock_init(&d->rwlock, NULL);
+	d->main_thread = pthread_self();
+
+	INIT_LLIST_HEAD(&d->cups_clients);
+
+	d->cfg.cups_local_ip = talloc_strdup(d, "localhost");
+	d->cfg.cups_local_port = 4300;
+
+	return d;
+}
+
+static const struct log_info_cat log_categories[] = {
+	[DTUN] = {
+		.name ="DTUN",
+		.description = "Tunnel interface (tun device)",
+		.enabled = 1, .loglevel = LOGL_INFO,
+	},
+	[DEP] = {
+		.name = "DEP",
+		.description = "GTP endpoint (UDP socket)",
+		.enabled = 1, .loglevel = LOGL_INFO,
+	},
+	[DGT] = {
+		.name = "DGT",
+		.description = "GTP tunnel (session)",
+		.enabled = 1, .loglevel = LOGL_INFO,
+	},
+	[DUECUPS] = {
+		.name = "DUECUPS",
+		.description = "UE Control User Plane Separation",
+		.enabled = 1, .loglevel = LOGL_DEBUG,
+	},
+
+};
+
+static const struct log_info log_info = {
+	.cat = log_categories,
+	.num_cat = ARRAY_SIZE(log_categories),
+};
+
+int main(int argc, char **argv)
+{
+	void *ctx = talloc_named_const(NULL, 0, "root");
+	int rc;
+
+	g_vty_info.tall_ctx = ctx;
+
+	osmo_init_ignore_signals();
+	osmo_init_logging2(ctx,  &log_info);
+
+	g_daemon = gtp_daemon_alloc(ctx);
+	OSMO_ASSERT(g_daemon);
+
+	osmo_stats_init(ctx);
+	vty_init(&g_vty_info);
+	logging_vty_add_cmds();
+	osmo_talloc_vty_add_cmds();
+	osmo_stats_vty_add_cmds();
+	rate_ctr_init(ctx);
+	gtpud_vty_init();
+
+	init_netns();
+
+	rc = vty_read_config_file(g_config_file, NULL);
+	if (rc < 0) {
+		fprintf(stderr, "Failed to open config file: '%s'\n", g_config_file);
+		exit(2);
+	}
+
+	rc = telnet_init_dynif(ctx, NULL, vty_get_bind_addr(), OSMO_VTY_PORT_GGSN);
+	if (rc < 0)
+		exit(1);
+
+	g_daemon->cups_link = osmo_stream_srv_link_create(g_daemon);
+	if (!g_daemon->cups_link) {
+		fprintf(stderr, "Failed to create CUPS socket %s:%u (%s)\n",
+			g_daemon->cfg.cups_local_ip, g_daemon->cfg.cups_local_port, strerror(errno));
+		exit(1);
+	}
+
+	/* UECUPS socket for control from control plane side */
+	osmo_stream_srv_link_set_nodelay(g_daemon->cups_link, true);
+	osmo_stream_srv_link_set_addr(g_daemon->cups_link, g_daemon->cfg.cups_local_ip);
+	osmo_stream_srv_link_set_port(g_daemon->cups_link, g_daemon->cfg.cups_local_port);
+	osmo_stream_srv_link_set_proto(g_daemon->cups_link, IPPROTO_SCTP);
+	osmo_stream_srv_link_set_data(g_daemon->cups_link, g_daemon);
+	osmo_stream_srv_link_set_accept_cb(g_daemon->cups_link, cups_accept_cb);
+	osmo_stream_srv_link_open(g_daemon->cups_link);
+
+	if (g_daemonize) {
+		rc = osmo_daemonize();
+		if (rc < 0) {
+			perror("Error during daemonize");
+			exit(1);
+		}
+	}
+
+	while (1) {
+		osmo_select_main(0);
+	}
+}
diff --git a/daemon/netdev.c b/daemon/netdev.c
new file mode 100644
index 0000000..2022d24
--- /dev/null
+++ b/daemon/netdev.c
@@ -0,0 +1,139 @@
+/* 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 <netinet/ip.h>
+#include <netinet/ip6.h>
+
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <sys/ioctl.h>
+
+#include <netlink/socket.h>
+#include <netlink/route/link.h>
+#include <netlink/route/addr.h>
+#include <netlink/route/route.h>
+#include <netlink/route/nexthop.h>
+
+#include <osmocom/core/utils.h>
+
+/***********************************************************************
+ * netlink helper functions
+ ***********************************************************************/
+
+static int _netdev_addr(struct nl_sock *nlsk, int ifindex, const struct sockaddr_storage *ss, bool add)
+{
+	const struct sockaddr_in6 *sin6;
+	const struct sockaddr_in *sin;
+	struct nl_addr *local = NULL;
+	struct rtnl_addr *addr;
+	int rc;
+
+	switch (ss->ss_family) {
+	case AF_INET:
+		sin = (struct sockaddr_in *) ss;
+		local = nl_addr_build(AF_INET, &sin->sin_addr, 4);
+		break;
+	case AF_INET6:
+		sin6 = (struct sockaddr_in6 *) ss;
+		local = nl_addr_build(AF_INET6, &sin6->sin6_addr, 16);
+		break;
+	}
+	OSMO_ASSERT(local);
+
+	addr = rtnl_addr_alloc();
+	OSMO_ASSERT(addr);
+	rtnl_addr_set_ifindex(addr, ifindex);
+	OSMO_ASSERT(rtnl_addr_set_local(addr, local) == 0);
+
+	if (add)
+		rc = rtnl_addr_add(nlsk, addr, 0);
+	else
+		rc = rtnl_addr_delete(nlsk, addr, 0);
+
+	rtnl_addr_put(addr);
+
+	return rc;
+}
+
+int netdev_add_addr(struct nl_sock *nlsk, int ifindex, const struct sockaddr_storage *ss)
+{
+	return _netdev_addr(nlsk, ifindex, ss, true);
+}
+
+int netdev_del_addr(struct nl_sock *nlsk, int ifindex, const struct sockaddr_storage *ss)
+{
+	return _netdev_addr(nlsk, ifindex, ss, false);
+}
+
+int netdev_set_link(struct nl_sock *nlsk, int ifindex, bool up)
+{
+	struct rtnl_link *link, *change;
+	int rc;
+
+	rc = rtnl_link_get_kernel(nlsk, ifindex, NULL, &link);
+	if (rc < 0)
+		return rc;
+
+	change = rtnl_link_alloc();
+	OSMO_ASSERT(change);
+
+	if (up)
+		rtnl_link_set_flags(change, IFF_UP);
+	else
+		rtnl_link_unset_flags(change, IFF_UP);
+
+	rc = rtnl_link_change(nlsk, link, change, 0);
+
+	rtnl_link_put(change);
+	rtnl_link_put(link);
+
+	return rc;
+}
+
+int netdev_add_defaultroute(struct nl_sock *nlsk, int ifindex, uint8_t family)
+{
+	struct rtnl_route *route = rtnl_route_alloc();
+	struct rtnl_nexthop *nhop = rtnl_route_nh_alloc();
+	struct nl_addr *dst, *gw;
+	uint8_t buf[16];
+	int rc;
+
+	OSMO_ASSERT(route);
+	OSMO_ASSERT(nhop);
+
+	/* destination address of route: all-zero */
+	memset(buf, 0, sizeof(buf));
+	dst = nl_addr_build(family, buf, family == AF_INET ? 4 : 16);
+	OSMO_ASSERT(dst);
+	nl_addr_set_prefixlen(dst, 0);
+
+	/* gateway address of route: also all-zero */
+	gw = nl_addr_clone(dst);
+	OSMO_ASSERT(gw);
+
+	/* nexthop for route */
+	rtnl_route_nh_set_ifindex(nhop, ifindex);
+	rtnl_route_nh_set_gateway(nhop, gw);
+
+	/* tie everything together in the route */
+	rtnl_route_set_dst(route, dst);
+	rtnl_route_set_family(route, family);
+	rtnl_route_add_nexthop(route, nhop);
+
+	rc = rtnl_route_add(nlsk, route, NLM_F_CREATE);
+
+	//rtnl_route_nh_free(nhop);
+	nl_addr_put(gw);
+	nl_addr_put(dst);
+	rtnl_route_put(route);
+
+	return rc;
+}
diff --git a/daemon/netns.c b/daemon/netns.c
new file mode 100644
index 0000000..b0ec254
--- /dev/null
+++ b/daemon/netns.c
@@ -0,0 +1,273 @@
+#warning "Merge netns.c from osmo-ggsn and osmo-gtpu-daemon"
+/*
+ * Copyright (C) 2014-2017, Travelping GmbH <info@travelping.com>
+ * Copyright (C) 2020, Harald Welte <laforge@gnumonks.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if defined(__linux__)
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sched.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/mount.h>
+#include <sys/param.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <osmocom/core/utils.h>
+
+#include "netns.h"
+
+#define NETNS_PATH "/var/run/netns"
+
+/*! default namespace of the GGSN process */
+static int default_nsfd = -1;
+
+/*! switch to a (non-default) namespace, store existing signal mask in oldmask.
+ *  \param[in] nsfd file descriptor representing the namespace to whch we shall switch
+ *  \param[out] oldmaks caller-provided memory location to which old signal mask is stored
+ *  \ returns 0 on success or negative (errno) in case of error */
+int switch_ns(int nsfd, sigset_t *oldmask)
+{
+	sigset_t intmask;
+	int rc;
+
+	OSMO_ASSERT(default_nsfd >= 0);
+
+	if (sigfillset(&intmask) < 0)
+		return -errno;
+	if ((rc = sigprocmask(SIG_BLOCK, &intmask, oldmask)) != 0)
+		return -rc;
+
+	if (setns(nsfd, CLONE_NEWNET) < 0) {
+		/* restore old mask if we couldn't switch the netns */
+		sigprocmask(SIG_SETMASK, oldmask, NULL);
+		return -errno;
+	}
+	return 0;
+}
+
+/*! switch back to the default namespace, restoring signal mask.
+ *  \param[in] oldmask signal mask to restore after returning to default namespace
+ *  \returns 0 on successs; negative errno value in case of error */
+int restore_ns(sigset_t *oldmask)
+{
+	OSMO_ASSERT(default_nsfd >= 0);
+
+	int rc;
+	if (setns(default_nsfd, CLONE_NEWNET) < 0)
+		return -errno;
+
+	if ((rc = sigprocmask(SIG_SETMASK, oldmask, NULL)) != 0)
+		return -rc;
+	return 0;
+}
+
+/*! open a file from within specified network namespace */
+int open_ns(int nsfd, const char *pathname, int flags)
+{
+	sigset_t intmask, oldmask;
+	int ret;
+	int fd = -1;
+	int rc;
+
+	OSMO_ASSERT(default_nsfd >= 0);
+
+	/* mask off all signals, store old signal mask */
+	if (sigfillset(&intmask) < 0)
+		return -errno;
+	if ((rc = sigprocmask(SIG_BLOCK, &intmask, &oldmask)) != 0)
+		return -rc;
+
+	/* associate the calling thread with namespace file descriptor */
+	if (setns(nsfd, CLONE_NEWNET) < 0) {
+		ret = -errno;
+		goto restore_sigmask;
+	}
+	/* open the requested file/path */
+	if ((fd = open(pathname, flags)) < 0) {
+		ret = -errno;
+		goto restore_defaultns;
+	}
+	ret = fd;
+
+restore_defaultns:
+	/* return back to default namespace */
+	if (setns(default_nsfd, CLONE_NEWNET) < 0) {
+		if (fd >= 0)
+			close(fd);
+		return -errno;
+	}
+
+restore_sigmask:
+	/* restore process mask */
+	if ((rc = sigprocmask(SIG_SETMASK, &oldmask, NULL)) != 0) {
+		if (fd >= 0)
+			close(fd);
+		return -rc;
+	}
+
+	return ret;
+}
+
+/*! create a socket in another namespace.
+ *  Switches temporarily to namespace indicated by nsfd, creates a socket in
+ *  that namespace and then returns to the default namespace.
+ *  \param[in] nsfd File descriptor of the namspace in which to create socket
+ *  \param[in] domain Domain of the socket (AF_INET, ...)
+ *  \param[in] type Type of the socket (SOCK_STREAM, ...)
+ *  \param[in] protocol Protocol of the socket (IPPROTO_TCP, ...)
+ *  \returns 0 on success; negative errno in case of error */
+int socket_ns(int nsfd, int domain, int type, int protocol)
+{
+	sigset_t intmask, oldmask;
+	int ret;
+	int sk = -1;
+	int rc;
+
+	OSMO_ASSERT(default_nsfd >= 0);
+
+	/* mask off all signals, store old signal mask */
+	if (sigfillset(&intmask) < 0)
+		return -errno;
+	if ((rc = sigprocmask(SIG_BLOCK, &intmask, &oldmask)) != 0)
+		return -rc;
+
+	/* associate the calling thread with namespace file descriptor */
+	if (setns(nsfd, CLONE_NEWNET) < 0) {
+		ret = -errno;
+		goto restore_sigmask;
+	}
+
+	/* create socket of requested domain/type/proto */
+	if ((sk = socket(domain, type, protocol)) < 0) {
+		ret = -errno;
+		goto restore_defaultns;
+	}
+	ret = sk;
+
+restore_defaultns:
+	/* return back to default namespace */
+	if (setns(default_nsfd, CLONE_NEWNET) < 0) {
+		if (sk >= 0)
+			close(sk);
+		return -errno;
+	}
+
+restore_sigmask:
+	/* restore process mask */
+	if ((rc = sigprocmask(SIG_SETMASK, &oldmask, NULL)) != 0) {
+		if (sk >= 0)
+			close(sk);
+		return -rc;
+	}
+	return ret;
+}
+
+/*! initialize this network namespace helper module.
+ *  Must be called before using any other functions of this file.
+ *  \returns 0 on success; negative errno in case of error */
+int init_netns()
+{
+	/* store the default namespace for later reference */
+	if ((default_nsfd = open("/proc/self/ns/net", O_RDONLY)) < 0)
+		return -errno;
+	return 0;
+}
+
+/*! create obtain file descriptor for network namespace of give name.
+ *  Creates /var/run/netns  if it doesn't exist already.
+ *  \param[in] name Name of the network namespace (in /var/run/netns/)
+ *  \returns File descriptor of network namespace; negative errno in case of error */
+int get_nsfd(const char *name)
+{
+	int ret = 0;
+	int rc;
+	int fd;
+	sigset_t intmask, oldmask;
+	char path[MAXPATHLEN] = NETNS_PATH;
+
+	OSMO_ASSERT(default_nsfd >= 0);
+
+	/* create /var/run/netns, if it doesn't exist already */
+	rc = mkdir(path, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH);
+	if (rc < 0 && errno != EEXIST)
+		return rc;
+
+	/* create /var/run/netns/[name], if it doesn't exist already */
+	snprintf(path, sizeof(path), "%s/%s", NETNS_PATH, name);
+	fd = open(path, O_RDONLY|O_CREAT|O_EXCL, 0);
+	if (fd < 0) {
+		if (errno == EEXIST) {
+			if ((fd = open(path, O_RDONLY)) < 0)
+				return -errno;
+			return fd;
+		}
+		return -errno;
+	}
+	if (close(fd) < 0)
+		return -errno;
+
+	/* mask off all signals, store old signal mask */
+	if (sigfillset(&intmask) < 0)
+		return -errno;
+	if ((rc = sigprocmask(SIG_BLOCK, &intmask, &oldmask)) != 0)
+		return -rc;
+
+	/* create a new network namespace */
+	if (unshare(CLONE_NEWNET) < 0) {
+		ret = -errno;
+		goto restore_sigmask;
+	}
+	if (mount("/proc/self/ns/net", path, "none", MS_BIND, NULL) < 0)
+		ret = -errno;
+
+	/* switch back to default namespace */
+	if (setns(default_nsfd, CLONE_NEWNET) < 0)
+		return -errno;
+
+restore_sigmask:
+	/* restore process mask */
+	if ((rc = sigprocmask(SIG_SETMASK, &oldmask, NULL)) != 0)
+		return -rc;
+
+	/* might have been set above in case mount fails */
+	if (ret < 0)
+		return ret;
+
+	/* finally, open the created namespace file descriptor from default ns */
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return -errno;
+
+	return fd;
+}
+
+#endif
diff --git a/daemon/netns.h b/daemon/netns.h
new file mode 100644
index 0000000..3b91ba3
--- /dev/null
+++ b/daemon/netns.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014-2017, Travelping GmbH <info@travelping.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __NETNS_H
+#define __NETNS_H
+
+#if defined(__linux__)
+
+int init_netns(void);
+
+int switch_ns(int nsfd, sigset_t *oldmask);
+int restore_ns(sigset_t *oldmask);
+
+int open_ns(int nsfd, const char *pathname, int flags);
+int socket_ns(int nsfd, int domain, int type, int protocol);
+int get_nsfd(const char *name);
+
+#endif
+
+#endif
diff --git a/daemon/osmo-gtpu-daemon.cfg b/daemon/osmo-gtpu-daemon.cfg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/daemon/osmo-gtpu-daemon.cfg
diff --git a/daemon/tun_device.c b/daemon/tun_device.c
new file mode 100644
index 0000000..f6553ca
--- /dev/null
+++ b/daemon/tun_device.c
@@ -0,0 +1,432 @@
+/* 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 <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;
+}
+
+/* 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;
+
+	/* initialize the fixed part of the GTP header */
+	gtph->flags = 0x30;
+	gtph->type = GTP_TPDU;
+
+	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) {
+			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) {
+			LOGTUN(tun, LOGL_NOTICE, "Error parsing IP packet: %s\n",
+				osmo_hexdump(buffer, nread));
+			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);
+			LOGTUN(tun, LOGL_NOTICE, "No tunnel found for source address %s:%s\n", host, port);
+			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) {
+			LOGTUN(tun, LOGL_FATAL, "Error Writing to UDP socket: %s\n", strerror(errno));
+			exit(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 */
+		strncpy(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\n");
+		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\n");
+		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;
+}
+
+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 */
+static 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);
+
+	pthread_cancel(tun->thread);
+	llist_del(&tun->list);
+	if (tun->netns_name)
+		close(tun->netns_fd);
+	close(tun->fd);
+	nl_socket_free(tun->nl);
+	LOGTUN(tun, LOGL_INFO, "Destroying\n");
+	talloc_free(tun);
+}
+
+/* UNLOCKED remove all objects referencing this tun and then destroy */
+void _tun_device_deref_destroy(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_destroy(tun2);
+
+	talloc_free(devname);
+}
+
+/* UNLOCKED release a reference; destroy 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) {
+		_tun_device_destroy(tun);
+		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;
+}
diff --git a/daemon/utility.c b/daemon/utility.c
new file mode 100644
index 0000000..1b36a24
--- /dev/null
+++ b/daemon/utility.c
@@ -0,0 +1,72 @@
+/* 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 <assert.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <netdb.h>
+
+#include "internal.h"
+
+/***********************************************************************
+ * Utility
+ ***********************************************************************/
+
+bool sockaddr_equals(const struct sockaddr *a, const struct sockaddr *b)
+{
+	const struct sockaddr_in *a4, *b4;
+	const struct sockaddr_in6 *a6, *b6;
+
+	if (a->sa_family != b->sa_family)
+		return false;
+
+	switch (a->sa_family) {
+	case AF_INET:
+		a4 = (struct sockaddr_in *) a;
+		b4 = (struct sockaddr_in *) b;
+		if (a4->sin_port != b4->sin_port)
+			return false;
+		if (a4->sin_addr.s_addr != b4->sin_addr.s_addr)
+			return false;
+		break;
+	case AF_INET6:
+		a6 = (struct sockaddr_in6 *) a;
+		b6 = (struct sockaddr_in6 *) b;
+		if (a6->sin6_port != b6->sin6_port)
+			return false;
+		if (memcmp(a6->sin6_addr.s6_addr, b6->sin6_addr.s6_addr, sizeof(b6->sin6_addr.s6_addr)))
+			return false;
+		break;
+	default:
+		assert(false);
+	}
+
+	return true;
+}
+
+struct addrinfo *addrinfo_helper(uint16_t family, uint16_t type, uint8_t proto,
+				 const char *host, uint16_t port, bool passive)
+{
+	struct addrinfo hints, *result;
+	char portbuf[6];
+	int rc;
+
+	snprintf(portbuf, sizeof(portbuf), "%u", port);
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = family;
+	hints.ai_socktype = type;
+	hints.ai_protocol = proto;
+	if (passive)
+		hints.ai_flags |= AI_PASSIVE;
+
+	rc = getaddrinfo(host, portbuf, &hints, &result);
+	if (rc != 0)
+		return NULL;
+
+	return result;
+}