/*
 * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * All Rights Reserved.
 *
 * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
 *
 * 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <net/if.h>
#include <linux/gtp.h>

#include <libgtpnl/gtpnl.h>
#include <libgtpnl/gtp.h>

#include <osmocom/core/socket.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/sockaddr_str.h>

#include <osmocom/upf/upf.h>
#include <osmocom/upf/upf_gtp.h>
#include <osmocom/upf/upf_gtpu_echo.h>

#define LOG_GTP_TUN(TUN, LEVEL, FMT, ARGS...) \
	LOGP(DGTP, LEVEL, "%s: " FMT, upf_gtp_tun_to_str_c(OTC_SELECT, (TUN)), ##ARGS)

int upf_gtp_dev_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_dev *dev)
{
	uint16_t v0_port;
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
	OSMO_STRBUF_PRINTF(sb, "%s", dev->name ? : "null");
	if (dev->kind == UPF_GTP_DEV_MOCKUP) {
		OSMO_STRBUF_PRINTF(sb, " [mockup]");
		return sb.chars_needed;
	}
	if (dev->name && dev->ifidx)
		OSMO_STRBUF_PRINTF(sb, " [%u]", dev->ifidx);
	if (dev->sgsn_mode)
		OSMO_STRBUF_PRINTF(sb, " (SGSN)");
	OSMO_STRBUF_PRINTF(sb, " ");
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &dev->gtpv1.local_addr);
	v0_port = osmo_sockaddr_port(&dev->gtpv0.local_addr.u.sa);
	if (dev->gtpv0.enabled && v0_port)
		OSMO_STRBUF_PRINTF(sb, "/%u", v0_port);
	return sb.chars_needed;
}

char *upf_gtp_dev_to_str_c(void *ctx, const struct upf_gtp_dev *dev)
{
	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_gtp_dev_to_str_buf, dev)
}

struct upf_gtp_dev *upf_gtp_dev_find_by_name(const char *name)
{
	struct upf_gtp_dev *dev;
	llist_for_each_entry(dev, &g_upf->gtp.devs, entry) {
		if (!strcmp(name, dev->name))
			return dev;
	}
	return NULL;
}

struct upf_gtp_dev *upf_gtp_dev_first()
{
	return llist_first_entry_or_null(&g_upf->gtp.devs, struct upf_gtp_dev, entry);
}

/* Tell the kernel to remove the GTP device. Called implicitly by talloc_free() (see upf_gtp_dev_destruct()). */
static int upf_gtp_dev_delete(struct upf_gtp_dev *dev)
{
	int rc;
	if (!dev->name)
		return 0;
	rc = gtp_dev_destroy(dev->name);
	if (rc < 0) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Error while deleting device: %s\n", strerror(errno));
		return rc;
	}
	LOG_GTP_DEV(dev, LOGL_NOTICE, "Deleted GTP device\n");
	dev->name = NULL;
	return 0;
}

static int upf_gtp_dev_destruct(struct upf_gtp_dev *dev);

/* Allocate state for one GTP device, add to g_upf->gtp.devs and return the created device. If state for the device of
 * that name already exists, do nothing and return NULL. */
static struct upf_gtp_dev *upf_gtp_dev_alloc(const char *name, enum upf_gtp_dev_kind kind, const char *local_addr)
{
	struct upf_gtp_dev *dev = upf_gtp_dev_find_by_name(name);
	struct osmo_sockaddr_str addr_conv;
	local_addr = local_addr ? : "0.0.0.0";
	if (dev) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Device already exists. Cannot create %s %s\n", name, local_addr);
		return NULL;
	}
	dev = talloc(g_upf, struct upf_gtp_dev);
	*dev = (struct upf_gtp_dev){
		.kind = kind,
		.name = talloc_strdup(dev, name),
		.gtpv0.ofd.fd = -1,
		.gtpv1.ofd.fd = -1,
	};
	INIT_LLIST_HEAD(&dev->tunnels);

	osmo_sockaddr_str_from_str(&addr_conv, local_addr, PORT_GTP0_U);

	osmo_sockaddr_str_to_sockaddr(&addr_conv, &dev->gtpv0.local_addr.u.sas);

	addr_conv.port = PORT_GTP1_U;
	osmo_sockaddr_str_to_sockaddr(&addr_conv, &dev->gtpv1.local_addr.u.sas);

	/* Need to add to list before setting up the destructor. A talloc_free() does automagically remove from the
	 * list. */
	llist_add(&dev->entry, &g_upf->gtp.devs);

	talloc_set_destructor(dev, upf_gtp_dev_destruct);

	return dev;
}

static int dev_resolve_ifidx(struct upf_gtp_dev *dev)
{
	int rc;

	dev->ifidx = if_nametoindex(dev->name);
	if (dev->ifidx == 0) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "No such device: '%s'\n", dev->name);
		talloc_free(dev);
		return -EIO;
	}
	/* Let's try something to see if talking to the device works. */
	errno = 0;
	rc = gtp_list_tunnel(g_upf->gtp.genl_id, g_upf->gtp.nl);
	if (errno)
		rc = -errno;
	else if (rc)
		rc = -EINVAL;
	if (rc) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Failed to open GTP device: %s\n", strerror(-rc));
		talloc_free(dev);
		return rc;
	}

	LOG_GTP_DEV(dev, LOGL_NOTICE, "GTP device ready (ifidx=%u)\n", dev->ifidx);
	return 0;
}

int upf_gtp_dev_open(const char *name, enum upf_gtp_dev_kind kind, const char *local_addr, bool listen_for_gtpv0,
		     bool sgsn_mode)
{
	const struct osmo_sockaddr any = {
		.u.sin = {
			.sin_family = AF_INET,
			.sin_port = 0,
			.sin_addr = {
				.s_addr = INADDR_ANY,
			},
		},
	};
	int rc;
	struct upf_gtp_dev *dev;

	dev = upf_gtp_dev_alloc(name, kind, local_addr);
	if (!dev)
		return -EIO;

	dev->sgsn_mode = sgsn_mode;

	if (kind == UPF_GTP_DEV_MOCKUP) {
		LOG_GTP_DEV(dev, LOGL_NOTICE,
			    "Created mockup GTP device: not opening kernel GTP device. FOR TESTING PURPOSES ONLY.\n");
		return 0;
	}

	rc = upf_gtp_genl_ensure_open();
	if (rc) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot set up GTP device, failed to open mnl_socket\n");
		return rc;
	}

	if (listen_for_gtpv0) {
		rc = osmo_sock_init_osa_ofd(&dev->gtpv0.ofd, SOCK_DGRAM, 0, &dev->gtpv0.local_addr, &any,
					    OSMO_SOCK_F_BIND);
		if (rc < 0) {
			LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot bind GTPv0 on %s (rc=%d)\n",
				    osmo_sockaddr_to_str_c(OTC_SELECT, &dev->gtpv0.local_addr), rc);
			talloc_free(dev);
			return -EIO;
		}
		dev->gtpv0.enabled = true;
		LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv0 bound\n");
	}

	/* GTPv1 */
	rc = osmo_sock_init_osa_ofd(&dev->gtpv1.ofd, SOCK_DGRAM, 0, &dev->gtpv1.local_addr, &any,
				    OSMO_SOCK_F_BIND);
	if (rc < 0) {
		LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot bind GTPv1 (rc=%d)\n", rc);
		talloc_free(dev);
		return -EIO;
	}
	LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv1 bound\n");

	if (kind == UPF_GTP_DEV_CREATE) {
		int gtp0_fd = listen_for_gtpv0 ? dev->gtpv0.ofd.fd : -1;
		int gtp1_fd = dev->gtpv1.ofd.fd;
		if (dev->sgsn_mode)
			rc = gtp_dev_create_sgsn(-1, dev->name, gtp0_fd, gtp1_fd);
		else
			rc = gtp_dev_create(-1, dev->name, gtp0_fd, gtp1_fd);
		if (rc < 0) {
			LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot create GTP device: rc=%d\n", rc);
			/* name = NULL: signal to the destructor that it does not need to delete the device */
			dev->name = NULL;
			talloc_free(dev);
			return -EIO;
		}

		LOG_GTP_DEV(dev, LOGL_NOTICE, "created GTP device\n");
	}

	rc = dev_resolve_ifidx(dev);
	if (rc) {
		talloc_free(dev);
		return -EIO;
	}

	upf_gtpu_echo_setup(dev);

	return 0;
}

void upf_gtp_devs_close()
{
	struct upf_gtp_dev *dev;
	while ((dev = llist_first_entry_or_null(&g_upf->gtp.devs, struct upf_gtp_dev, entry)))
		talloc_free(dev);
}

void upf_gtp_genl_close()
{
	if (!g_upf->gtp.nl)
		return;
	genl_socket_close(g_upf->gtp.nl);
	g_upf->gtp.nl = NULL;
	g_upf->gtp.genl_id = -1;

	LOGP(DGTP, LOGL_NOTICE, "Closed mnl_socket\n");
}

/* Open an MNL socket which allows to create and remove GTP devices (requires CAP_NET_ADMIN). */
int upf_gtp_genl_ensure_open()
{
	/* Already open? */
	if (g_upf->gtp.nl && g_upf->gtp.genl_id >= 0)
		return 0;

	/* sanity / paranoia: if re-opening, make sure the previous socket is closed */
	if (g_upf->gtp.nl)
		upf_gtp_genl_close();

	g_upf->gtp.nl = genl_socket_open();
	if (!g_upf->gtp.nl) {
		LOGP(DGTP, LOGL_ERROR, "Cannot open mnl_socket: %s\n", strerror(errno));
		return -EIO;
	}

	g_upf->gtp.genl_id = genl_lookup_family(g_upf->gtp.nl, "gtp");
	if (g_upf->gtp.genl_id < 0) {
		LOGP(DGTP, LOGL_ERROR, "genl family 'gtp' not found\n");
		return -ENOTSUP;
	}

	LOGP(DGTP, LOGL_NOTICE, "Opened mnl_socket\n");
	return 0;
}

struct upf_gtp_tun {
	struct llist_head entry;

	struct upf_gtp_dev *dev;
	struct upf_gtp_tun_desc desc;
	bool active;
};

static int upf_gtp_tun_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_tun *tun)
{
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
	OSMO_STRBUF_PRINTF(sb, "%s:tun{TEID=l:0x%x,r:0x%x UE=", tun->dev->name, tun->desc.local_teid,
			   tun->desc.remote_teid);
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &tun->desc.ue_addr);
	OSMO_STRBUF_PRINTF(sb, " GTP-dst=");
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &tun->desc.gtp_remote_addr);
	OSMO_STRBUF_PRINTF(sb, "}");
	return sb.chars_needed;
}

static char *upf_gtp_tun_to_str_c(void *ctx, const struct upf_gtp_tun *tun)
{
	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_gtp_tun_to_str_buf, tun)
}

static int upf_gtp_tun_deactivate(struct upf_gtp_tun *tun);

static int upf_gtp_tun_destruct(struct upf_gtp_tun *tun)
{
	if (tun->active)
		upf_gtp_tun_deactivate(tun);
	llist_del(&tun->entry);
	return 0;
}

static struct upf_gtp_tun *upf_gtp_tun_alloc(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *desc)
{
	struct upf_gtp_tun *tun = talloc(dev, struct upf_gtp_tun);
	OSMO_ASSERT(tun);
	*tun = (struct upf_gtp_tun){
		.dev = dev,
		.desc = *desc,
	};
	llist_add(&tun->entry, &dev->tunnels);
	talloc_set_destructor(tun, upf_gtp_tun_destruct);
	return tun;
}

static struct gtp_tunnel *upf_gtp_tun_to_gtp_tunnel(struct upf_gtp_tun *tun)
{
	struct gtp_tunnel *t;

	if (tun->desc.ue_addr.u.sas.ss_family != AF_INET || tun->desc.gtp_remote_addr.u.sas.ss_family != AF_INET) {
		LOG_GTP_TUN(tun, LOGL_ERROR, "Only capabale of IPv4\n");
		return NULL;
	}

	t = gtp_tunnel_alloc();
	OSMO_ASSERT(t);
	gtp_tunnel_set_ifidx(t, tun->dev->ifidx);
	gtp_tunnel_set_version(t, GTP_V1);
	gtp_tunnel_set_i_tei(t, tun->desc.local_teid);
	gtp_tunnel_set_o_tei(t, tun->desc.remote_teid);
	gtp_tunnel_set_ms_ip4(t, &tun->desc.ue_addr.u.sin.sin_addr);
	gtp_tunnel_set_sgsn_ip4(t, &tun->desc.gtp_remote_addr.u.sin.sin_addr);
	return t;
}

int upf_gtp_tun_activate(struct upf_gtp_tun *tun)
{
	int rc;
	struct gtp_tunnel *t;

	if (tun->active)
		return -EALREADY;

	if (tun->dev->kind == UPF_GTP_DEV_MOCKUP) {
		tun->active = true;
		return 0;
	}

	t = upf_gtp_tun_to_gtp_tunnel(tun);
	if (!t)
		return -ENOTSUP;

	errno = 0;
	rc = gtp_add_tunnel(g_upf->gtp.genl_id, g_upf->gtp.nl, t);
	if (errno) {
		rc = -errno;
	} else if (rc) {
		rc = -EINVAL;
	} else {
		tun->active = true;
	}

	gtp_tunnel_free(t);
	return rc;
}

static struct upf_gtp_tun *upf_gtp_dev_tunnel_find(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *tun_desc)
{
	struct upf_gtp_tun *tun;
	llist_for_each_entry(tun, &dev->tunnels, entry) {
		if (upf_gtp_tun_desc_cmp(tun_desc, &tun->desc))
			continue;
		return tun;
	}
	return NULL;
}

int upf_gtp_dev_tunnel_add(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *tun_desc)
{
	struct upf_gtp_tun *tun;
	tun = upf_gtp_dev_tunnel_find(dev, tun_desc);
	if (!tun)
		tun = upf_gtp_tun_alloc(dev, tun_desc);
	if (tun->active)
		return 0;
	return upf_gtp_tun_activate(tun);
}

int upf_gtp_dev_tunnel_del(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *tun_desc)
{
	struct upf_gtp_tun *tun;
	int rc;
	tun = upf_gtp_dev_tunnel_find(dev, tun_desc);
	if (!tun)
		return 0;
	if (tun->active) {
		rc = upf_gtp_tun_deactivate(tun);
		if (rc)
			return rc;
	}
	talloc_free(tun);
	return 0;
}

static int upf_gtp_tun_deactivate(struct upf_gtp_tun *tun)
{
	int rc;
	struct gtp_tunnel *t;

	if (!tun->active) {
		LOG_GTP_TUN(tun, LOGL_ERROR, "Cannot deactivate, not active\n");
		return -EINVAL;
	}

	if (tun->dev->kind == UPF_GTP_DEV_MOCKUP) {
		tun->active = false;
		return 0;
	}

	t = upf_gtp_tun_to_gtp_tunnel(tun);
	if (!t)
		return -EINVAL;

	rc = gtp_del_tunnel(g_upf->gtp.genl_id, g_upf->gtp.nl, t);
	if (rc)
		LOG_GTP_TUN(tun, LOGL_ERROR, "Failed to delete tunnel: %d %s\n", rc, strerror(rc));
	else
		tun->active = false;

	gtp_tunnel_free(t);
	return rc;
}

static int upf_gtp_dev_destruct(struct upf_gtp_dev *dev)
{
	struct upf_gtp_tun *t;
	/* Destruct and clean up all active tunnels before deleting the device */
	while ((t = llist_first_entry_or_null(&dev->tunnels, struct upf_gtp_tun, entry)))
		talloc_free(t);
	llist_del(&dev->entry);
	/* osmo_fd_close() is a noop if ofd.fd == -1 */
	osmo_fd_close(&dev->gtpv0.ofd);
	osmo_fd_close(&dev->gtpv1.ofd);
	if (dev->kind == UPF_GTP_DEV_CREATE)
		upf_gtp_dev_delete(dev);
	return 0;
}

int upf_gtp_tun_desc_cmp(const struct upf_gtp_tun_desc *a, const struct upf_gtp_tun_desc *b)
{
	int r;

	if (a == b)
		return 0;
	if (!a)
		return -1;
	if (!b)
		return 1;

#define CMP_MEMB(MEMB) OSMO_CMP(a->MEMB, b->MEMB)
	if ((r = CMP_MEMB(local_teid)))
		return r;
	if ((r = CMP_MEMB(remote_teid)))
		return r;
	return osmo_sockaddr_cmp(&a->gtp_remote_addr, &b->gtp_remote_addr);
}
