/*
 * (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 <errno.h>
#include <nftables/libnftables.h>

#include <osmocom/core/talloc.h>
#include <osmocom/core/logging.h>

#include <osmocom/upf/upf.h>
#include <osmocom/upf/upf_nft.h>

static char *upf_nft_ruleset_table_create(void *ctx, const char *table_name)
{
	return talloc_asprintf(ctx, "add table inet %s\n", table_name);
}

static int upf_nft_run(const char *ruleset)
{
	int rc;

	if (g_upf->nft.mockup) {
		LOGP(DNFT, LOGL_NOTICE, "tunmap/mockup active: not running nft ruleset: '%s'\n", ruleset);
		return 0;
	}

	if (!g_upf->nft.nft_ctx) {
		rc = upf_nft_init();
		if (rc)
			return rc;
	}

	rc = nft_run_cmd_from_buffer(g_upf->nft.nft_ctx, ruleset);
	if (rc < 0) {
		LOGP(DNFT, LOGL_ERROR, "error running nft ruleset: rc=%d ruleset=%s\n",
		     rc, osmo_quote_str_c(OTC_SELECT, ruleset, -1));
		return -EIO;
	}

	LOGP(DNFT, LOGL_DEBUG, "set up nft ruleset: %s\n", osmo_quote_str_c(OTC_SELECT, ruleset, -1));
	return 0;
}

int upf_nft_init()
{
	int rc;

	/* Always set up the default settings, also in mockup mode, so that the VTY reflects sane values */
	if (!g_upf->nft.table_name)
		g_upf->nft.table_name = talloc_strdup(g_upf, "osmo-upf");

	/* When in mockup mode, do not set up nft_ctx and netfilter table */
	if (g_upf->nft.mockup) {
		LOGP(DNFT, LOGL_NOTICE,
		     "tunmap/mockup active: not allocating libnftables nft_ctx. FOR TESTING PURPOSES ONLY.\n");
		return 0;
	}

	g_upf->nft.nft_ctx = nft_ctx_new(NFT_CTX_DEFAULT);
	if (!g_upf->nft.nft_ctx) {
		LOGP(DNFT, LOGL_ERROR, "cannot allocate libnftables nft_ctx\n");
		return -EIO;
	}

	rc = upf_nft_run(upf_nft_ruleset_table_create(OTC_SELECT, g_upf->nft.table_name));
	if (rc) {
		LOGP(DNFT, LOGL_ERROR, "Failed to create nft table %s\n",
		     osmo_quote_str_c(OTC_SELECT, g_upf->nft.table_name, -1));
		return rc;
	}
	LOGP(DNFT, LOGL_NOTICE, "Created nft table %s\n", osmo_quote_str_c(OTC_SELECT, g_upf->nft.table_name, -1));
	return 0;
}

int upf_nft_free()
{
	if (!g_upf->nft.nft_ctx)
		return 0;
	nft_ctx_free(g_upf->nft.nft_ctx);
	g_upf->nft.nft_ctx = NULL;
	return 0;
}

struct upf_nft_args_peer {
	/* The source IP address in packets received from this peer */
	const struct osmo_sockaddr *addr_remote;
	/* The TEID that we send to the peer in GTP packets. */
	uint32_t teid_remote;
	/* The local destination IP address in packets received from this peer */
	const struct osmo_sockaddr *addr_local;
	/* The TEID that the peer sends to us in GTP packets. */
	uint32_t teid_local;
};

struct upf_nft_args {
	/* global table name */
	const char *table_name;
	/* chain name for this specific tunnel mapping */
	uint32_t chain_id;
	int priority;

	struct upf_nft_args_peer peer_a;
	struct upf_nft_args_peer peer_b;
};

static int tunmap_single_direction(char *buf, size_t buflen,
				   const struct upf_nft_args *args,
				   const struct upf_nft_args_peer *from_peer,
				   const struct upf_nft_args_peer *to_peer,
				   const struct llist_head *rule_append)
{
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };

	OSMO_STRBUF_PRINTF(sb, "add rule inet %s " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u", args->table_name, args->chain_id);

	/* Match only UDP packets */
	OSMO_STRBUF_PRINTF(sb, " meta l4proto udp");

	/* Match on packets coming in at specific local IP */
	OSMO_STRBUF_PRINTF(sb, " ip daddr ");
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, from_peer->addr_local);

	/* Match on the TEID in the header */
	OSMO_STRBUF_PRINTF(sb, " @ih,32,32 0x%08x", from_peer->teid_local);

	/* Change outgoing address to local IP on outgoing interface */
	OSMO_STRBUF_PRINTF(sb, " ip saddr set ");
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, to_peer->addr_local);

	/* Change destination address to to_peer */
	OSMO_STRBUF_PRINTF(sb, " ip daddr set ");
	OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, to_peer->addr_remote);

	/* Change the TEID in the header to the one to_peer expects */
	OSMO_STRBUF_PRINTF(sb, " @ih,32,32 set 0x%08x", to_peer->teid_remote);

	OSMO_STRBUF_PRINTF(sb, " counter");

	if (rule_append) {
		struct string_listitem *i;
		llist_for_each_entry(i, rule_append, entry)
			OSMO_STRBUF_PRINTF(sb, " %s", i->str);
	}

	OSMO_STRBUF_PRINTF(sb, " accept");
	OSMO_STRBUF_PRINTF(sb, ";\n");

	return sb.chars_needed;
}

static int upf_nft_ruleset_tunmap_create_buf(char *buf, size_t buflen, const struct upf_nft_args *args,
					     const struct llist_head *rule_append)
{
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };

	/* Add a chain for this tunnel mapping */
	OSMO_STRBUF_PRINTF(sb, "add chain inet %s " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u { type filter hook prerouting priority %d; }\n",
			   args->table_name, args->chain_id, args->priority);

	/* Forwarding from peer_a to peer_b */
	OSMO_STRBUF_APPEND(sb, tunmap_single_direction, args, &args->peer_a, &args->peer_b, rule_append);
	/* And from peer_b to peer_a */
	OSMO_STRBUF_APPEND(sb, tunmap_single_direction, args, &args->peer_b, &args->peer_a, rule_append);

	return sb.chars_needed;
}

static char *upf_nft_ruleset_tunmap_create_c(void *ctx, const struct upf_nft_args *args,
					     const struct llist_head *rule_append)
{
	OSMO_NAME_C_IMPL(ctx, 512, "ERROR", upf_nft_ruleset_tunmap_create_buf, args, rule_append)
}

static int upf_nft_ruleset_tunmap_delete_buf(char *buf, size_t buflen, const struct upf_nft_args *args)
{
	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
	OSMO_STRBUF_PRINTF(sb, "delete chain inet %s " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u\n",
			   args->table_name, args->chain_id);
	return sb.chars_needed;
}

static char *upf_nft_ruleset_tunmap_delete_c(void *ctx, const struct upf_nft_args *args)
{
	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_nft_ruleset_tunmap_delete_buf, args)
}

static void upf_nft_args_from_tunmap_desc(struct upf_nft_args *args, const struct upf_nft_tunmap_desc *tunmap)
{
	OSMO_ASSERT(osmo_sockaddr_port(&tunmap->access.gtp_remote_addr.u.sa) == 0);
	OSMO_ASSERT(osmo_sockaddr_port(&tunmap->access.gtp_local_addr.u.sa) == 0);
	OSMO_ASSERT(osmo_sockaddr_port(&tunmap->core.gtp_remote_addr.u.sa) == 0);
	OSMO_ASSERT(osmo_sockaddr_port(&tunmap->core.gtp_local_addr.u.sa) == 0);

	*args = (struct upf_nft_args){
		.table_name = g_upf->nft.table_name,
		.chain_id = tunmap->id,
		.priority = g_upf->nft.priority,
		.peer_a = {
			.addr_remote = &tunmap->access.gtp_remote_addr,
			.teid_remote = tunmap->access.remote_teid,
			.addr_local = &tunmap->access.gtp_local_addr,
			.teid_local = tunmap->access.local_teid,
		},
		.peer_b = {
			.addr_remote = &tunmap->core.gtp_remote_addr,
			.teid_remote = tunmap->core.remote_teid,
			.addr_local = &tunmap->core.gtp_local_addr,
			.teid_local = tunmap->core.local_teid,
		},
	};
}

char *upf_nft_tunmap_get_ruleset_str(void *ctx, struct upf_nft_tunmap_desc *tunmap,
				     const struct llist_head *rule_append)
{
	struct upf_nft_args args;

	/* Give this tunnel mapping a new id, returned to the caller so that the tunnel mapping can be deleted later */
	if (!tunmap->id) {
		g_upf->nft.next_id_state++;
		if (!g_upf->nft.next_id_state)
			g_upf->nft.next_id_state++;
		tunmap->id = g_upf->nft.next_id_state;
	}

	upf_nft_args_from_tunmap_desc(&args, tunmap);
	return upf_nft_ruleset_tunmap_create_c(ctx, &args, rule_append);
}

int upf_nft_tunmap_create(struct upf_nft_tunmap_desc *tunmap, const struct llist_head *rule_append)
{
	return upf_nft_run(upf_nft_tunmap_get_ruleset_str(OTC_SELECT, tunmap, rule_append));
}

int upf_nft_tunmap_delete(struct upf_nft_tunmap_desc *tunmap)
{
	struct upf_nft_args args;
	upf_nft_args_from_tunmap_desc(&args, tunmap);
	return upf_nft_run(upf_nft_ruleset_tunmap_delete_c(OTC_SELECT, &args));
}
