// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2025 Christophe Fontaine

#include "config.h"
#include "control_queue.h"
#include "eth.h"
#include "event.h"
#include "iface.h"
#include "log.h"
#include "loopback.h"
#include "mempool.h"
#include "module.h"
#include "netlink.h"
#include "rxtx.h"
#include "vlan.h"
#include "vrf.h"

#include <gr_string.h>

#include <event2/event.h>
#include <rte_errno.h>
#include <rte_malloc.h>
#include <rte_net.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/ethtool.h>
#include <linux/if_tun.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <stdio.h>
#include <sys/ioctl.h>

LOG_TYPE("ctlplane");

#define TUN_TAP_DEV_PATH "/dev/net/tun"

static struct event_base *ev_base;

static void finalize_fd(struct event *ev, void * /*priv*/) {
	int fd = event_get_fd(ev);
	if (fd >= 0)
		close(fd);
}

void iface_cp_tx(void *obj, uintptr_t, const struct control_queue_drain *drain) {
	struct rte_mbuf *m = obj;
	struct mbuf_data *d = mbuf_data(m);
	struct iface_stats *stats;
	char *data = NULL;

	// Check if packet references deleted interface.
	if (drain != NULL && drain->event == GR_EVENT_IFACE_REMOVE && d->iface == drain->obj)
		goto end;

	if (d->iface->cp_fd == 0)
		goto end;

	if (rte_pktmbuf_linearize(m) == 0) {
		data = rte_pktmbuf_mtod(m, char *);
	} else {
		data = rte_malloc(NULL, rte_pktmbuf_pkt_len(m), 0);
		if (data == NULL) {
			LOG(ERR, "rte_malloc failed %s", rte_strerror(rte_errno));
			goto end;
		}
		// with a non-contiguous mbuf, rte_pktmbuf_read returns a pointer
		// to the user provided buffer.
		rte_pktmbuf_read(m, 0, rte_pktmbuf_pkt_len(m), data);
	}

	// Do not retry even in case of  if EAGAIN || EWOULDBLOCK
	// If the tun device queue is full, something really bad is
	// already happening on the management plane side.
	if (write(d->iface->cp_fd, data, rte_pktmbuf_pkt_len(m)) != rte_pktmbuf_pkt_len(m)) {
		// The user messed up and removed the tap interface
		// release resources on our side to try to recover
		if (errno == EBADF) {
			iface_destroy((struct iface *)d->iface);
		}
		LOG(ERR, "write to tap device failed %s", strerror(errno));
	}

	stats = iface_get_stats(rte_lcore_id(), d->iface->id);
	stats->cp_tx_packets += 1;
	stats->cp_tx_bytes += rte_pktmbuf_pkt_len(m);

	if (gr_config.log_packets)
		trace_log_packet(m, "cp tx", d->iface->name);

	if (gr_mbuf_is_traced(m))
		gr_mbuf_trace_finish(m);
end:
	if (!rte_pktmbuf_is_contiguous(m))
		rte_free(data);
	rte_pktmbuf_free(m);
}

static void iface_cp_poll(evutil_socket_t, short reason, void *ev_iface) {
	struct iface *iface = ev_iface;
	struct rte_ether_addr src, dst;
	struct iface_stats *stats;
	struct rte_ether_hdr *eth;
	struct rte_ether_addr iface_mac;
	struct rte_vlan_hdr *vlan;
	struct rte_mbuf *mbuf;
	rte_be16_t ether_type;
	size_t read_len;
	size_t len;
	char *data;

	if (reason & EV_CLOSED) {
		LOG(ERR, "tap device %s deleted", iface->name);
		iface_destroy(iface);
		return;
	}

	mbuf = rte_pktmbuf_alloc(iface->pool);
	if (!mbuf) {
		LOG(ERR, "rte_pktmbuf_alloc: pool exhausted");
		goto err;
	}

	read_len = iface->mtu + RTE_ETHER_HDR_LEN + RTE_VLAN_HLEN;
	if ((data = rte_pktmbuf_append(mbuf, read_len)) == NULL) {
		LOG(ERR, "rte_pktmbuf_append: not enough tailroom");
		goto err;
	}

	if ((len = read(iface->cp_fd, data, read_len)) <= 0) {
		if (errno != EAGAIN && errno != EWOULDBLOCK)
			LOG(ERR, "read from tap device %s failed %s", iface->name, strerror(errno));
		goto err;
	}

	rte_pktmbuf_trim(mbuf, read_len - len);

	// packet sent from linux tun iface, no need to compute checksum;
	mbuf->ol_flags = RTE_MBUF_F_RX_IP_CKSUM_GOOD;
	mbuf->packet_type = rte_net_get_ptype(mbuf, NULL, RTE_PTYPE_ALL_MASK);

	eth = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *);

	// On NOARP control plane TAP interfaces, the kernel sets the
	// device's own MAC as destination for unicast packets (since it
	// cannot resolve neighbors). Detect this case and re-inject the
	// packet into the L3 datapath (like the TUN loopback does) so
	// grout can route it and set the correct destination MAC.
	if (iface_get_eth_addr(iface, &iface_mac) == 0
	    && rte_is_same_ether_addr(&eth->dst_addr, &iface_mac)) {
		struct eth_input_mbuf_data *e;
		rte_be16_t ether_type = eth->ether_type;

		rte_pktmbuf_adj(mbuf, sizeof(*eth));

		if (ether_type == RTE_BE16(RTE_ETHER_TYPE_IPV4))
			mbuf->packet_type = RTE_PTYPE_L3_IPV4;
		else if (ether_type == RTE_BE16(RTE_ETHER_TYPE_IPV6))
			mbuf->packet_type = RTE_PTYPE_L3_IPV6;
		else {
			LOG(ERR,
			    "cp_poll: unexpected ether_type %#x on %s",
			    rte_be_to_cpu_16(ether_type),
			    iface->name);
			goto err;
		}

		e = eth_input_mbuf_data(mbuf);
		// Keep the original iface so the link-local scope (iface->id) is
		// preserved for fib6_lookup. The VRF context comes from
		// iface->vrf_id either way.
		e->iface = iface;
		e->domain = ETH_DOMAIN_LOOPBACK;
		e->nh = NULL;

		if (loopback_input_send(mbuf) < 0) {
			LOG(ERR, "loopback_input_send: %s", strerror(errno));
			goto err;
		}

		stats = iface_get_stats(rte_lcore_id(), iface->id);
		stats->cp_rx_packets += 1;
		stats->cp_rx_bytes += rte_pktmbuf_pkt_len(mbuf);
		return;
	}

	if (iface->type == GR_IFACE_TYPE_VLAN) {
		// For vlan interfaces, we have to insert ourselves the vlan header
		// as Linux has no knowledge of its existence.
		src = eth->src_addr;
		dst = eth->dst_addr;
		ether_type = eth->ether_type;
		rte_pktmbuf_adj(mbuf, sizeof(*eth));

		vlan = gr_mbuf_prepend(mbuf, vlan);
		if (vlan == NULL) {
			LOG(ERR, "ctlplane vlan_hdr insertion: no headroom");
			goto err;
		}

		vlan->vlan_tci = rte_cpu_to_be_16(iface_info_vlan(iface)->vlan_id);
		vlan->eth_proto = ether_type;

		eth = gr_mbuf_prepend(mbuf, eth);
		if (eth == NULL) {
			LOG(ERR, "ctlplane ether_hdr insertion: no headroom");
			goto err;
		}
		eth->src_addr = src;
		eth->dst_addr = dst;
		eth->ether_type = RTE_BE16(RTE_ETHER_TYPE_VLAN);

		stats = iface_get_stats(rte_lcore_id(), iface->id);
		stats->tx_packets += 1;
		stats->tx_bytes += rte_pktmbuf_pkt_len(mbuf);

		const uint32_t parent_id = iface_info_vlan(iface)->parent_id;
		iface = iface_from_id(parent_id);
		if (iface == NULL) {
			LOG(ERR, "iface_from_id: no iface for id %u", parent_id);
			goto err;
		}
	}

	iface_mbuf_data(mbuf)->iface = iface;
	iface_mbuf_data(mbuf)->vlan_id = 0;

	if (iface_output_send(mbuf) < 0) {
		LOG(ERR, "iface_output_send: %s", strerror(errno));
		goto err;
	}

	stats = iface_get_stats(rte_lcore_id(), iface->id);
	stats->cp_rx_packets += 1;
	stats->cp_rx_bytes += rte_pktmbuf_pkt_len(mbuf);

	if (gr_config.log_packets)
		trace_log_packet(mbuf, "cp rx", iface->name);

	return;

err:
	rte_pktmbuf_free(mbuf);
}

static void sysctl_write(const char *dir, struct iface *iface, const char *prop, const char *val) {
	char path[PATH_MAX];
	snprintf(path, sizeof(path), "/proc/sys/%s/%s/%s", dir, iface->name, prop);
	FILE *f = fopen(path, "w");
	if (f != NULL) {
		if (fputs(val, f) < 0)
			LOG(WARNING, "fputs(%s): %s", path, strerror(errno));
		if (fclose(f) < 0)
			LOG(WARNING, "fclose(%s): %s", path, strerror(errno));
	} else {
		LOG(WARNING, "fopen(%s): %s", path, strerror(errno));
	}
}

static void cp_create(struct iface *iface) {
	char ifalias[IFALIASZ];
	struct ifreq ifr;
	int ioctl_sock;
	int flags;

	memset(&ifr, 0, sizeof(struct ifreq));
	gr_strcpy(ifr.ifr_name, IFNAMSIZ, iface->name);
	ifr.ifr_flags = IFF_TAP | IFF_NO_PI | IFF_MULTICAST;

	if ((ioctl_sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		LOG(ERR, "socket(SOCK_DGRAM): %s", strerror(errno));
		goto err;
	}

	if ((iface->cp_fd = open(TUN_TAP_DEV_PATH, O_RDWR)) < 0) {
		LOG(ERR, "open(%s): %s", TUN_TAP_DEV_PATH, strerror(errno));
		goto err;
	}

	if (ioctl(iface->cp_fd, TUNSETIFF, &ifr) < 0) {
		LOG(ERR, "ioctl(TUNSETIFF): %s", strerror(errno));
		goto err;
	}

	flags = fcntl(iface->cp_fd, F_GETFL);
	if (flags == -1) {
		LOG(ERR, "fcntl(F_GETFL): %s", strerror(errno));
		goto err;
	}

	flags |= O_NONBLOCK;
	if (fcntl(iface->cp_fd, F_SETFL, flags) < 0) {
		LOG(ERR, "fcntl(F_SETFL): %s", strerror(errno));
		goto err;
	}

	if (ioctl(ioctl_sock, SIOCGIFFLAGS, &ifr) < 0) {
		LOG(ERR, "ioctl(SIOCGIFFLAGS): %s", strerror(errno));
		goto err;
	}

	ifr.ifr_flags |= IFF_NOARP;
	if (ioctl(ioctl_sock, SIOCSIFFLAGS, &ifr) < 0) {
		LOG(ERR, "ioctl(SIOCSIFFLAGS): %s", strerror(errno));
		goto err;
	}

	if (ioctl(ioctl_sock, SIOCGIFINDEX, &ifr) < 0) {
		LOG(ERR, "ioctl(SIOCGIFINDEX) %s", strerror(errno));
		goto err;
	}
	iface->cp_id = ifr.ifr_ifindex;

	netlink_set_addr_gen_mode_none(iface->cp_id);
	snprintf(ifalias, IFALIASZ, "Grout control plane interface");
	netlink_set_ifalias(iface->cp_id, ifalias);
	netlink_link_set_admin_state(iface->cp_id, false, true);

	// Keep IPv6 addresses assigned to the tap across admin-down events.
	// When the datapath port flaps (e.g. a peer tap being moved to another
	// netns), grout brings this tap down; with the default sysctl value of
	// 0, the kernel would flush all IPv6 addresses, and userspace (FRR)
	// would then fail to bind() sockets on those addresses.
	sysctl_write("net/ipv6/conf", iface, "keep_addr_on_down", "1");

	if (gr_config.override_rp_filter) {
		// Set loose reverse path filtering on the TAP so that packets
		// delivered by grout are not dropped by rp_filter. The effective
		// rp_filter mode is max(conf.all, conf.<iface>), so setting the
		// per-interface value to 2 (loose) overrides a global strict setting.
		sysctl_write("net/ipv4/conf", iface, "rp_filter", "2");
	}

	iface->pool = gr_pktmbuf_pool_get(SOCKET_ID_ANY, RTE_GRAPH_BURST_SIZE);
	if (iface->pool == NULL) {
		LOG(ERR, "gr_pktmbuf_pool_get: %s", strerror(errno));
		goto err;
	}
	iface->pool_size = RTE_GRAPH_BURST_SIZE;

	iface->cp_ev = event_new(
		ev_base,
		iface->cp_fd,
		EV_READ | EV_CLOSED | EV_PERSIST | EV_FINALIZE,
		iface_cp_poll,
		iface
	);

	if (iface->cp_ev == NULL || event_add(iface->cp_ev, NULL) < 0) {
		if (iface->cp_ev)
			event_free(iface->cp_ev);
		iface->cp_ev = NULL;
		goto err;
	}
	close(ioctl_sock);
	return;

err:
	if (iface->pool != NULL) {
		gr_pktmbuf_pool_release(iface->pool, iface->pool_size);
		iface->pool = NULL;
	}
	if (iface->cp_fd > 0)
		close(iface->cp_fd);
	if (ioctl_sock > 0)
		close(ioctl_sock);
}

static void cp_delete(struct iface *iface) {
	if (iface->cp_ev)
		event_free_finalize(0, iface->cp_ev, finalize_fd);
	if (iface->pool != NULL) {
		gr_pktmbuf_pool_release(iface->pool, iface->pool_size);
		iface->pool = NULL;
	}
}

static void cp_set_speed(struct iface *iface) {
	struct ethtool_link_settings *els;
	struct ifreq ifr = {0};
	char buf[512] = {0};
	int ioctl_sock;

	if (iface->cp_id == 0)
		return;

	gr_strcpy(ifr.ifr_name, IFNAMSIZ, iface->name);
	els = (struct ethtool_link_settings *)buf;

	if ((ioctl_sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		LOG(ERR, "socket(SOCK_DGRAM): %s", strerror(errno));
		goto err;
	}

	ifr.ifr_data = (caddr_t)els;
	els->cmd = ETHTOOL_GLINKSETTINGS;
	if (ioctl(ioctl_sock, SIOCETHTOOL, &ifr) < 0) {
		LOG(ERR, "ETHTOOL_GLINKSETTINGS: %s", strerror(errno));
		goto err;
	}
	els->link_mode_masks_nwords = -els->link_mode_masks_nwords;
	if (ioctl(ioctl_sock, SIOCETHTOOL, &ifr) < 0) {
		LOG(ERR, "ETHTOOL_GLINKSETTINGS: %s", strerror(errno));
		goto err;
	}

	els->speed = iface->speed;
	els->duplex = DUPLEX_FULL;
	els->autoneg = AUTONEG_DISABLE;
	els->cmd = ETHTOOL_SLINKSETTINGS;

	if (ioctl(ioctl_sock, SIOCETHTOOL, &ifr) < 0) {
		LOG(ERR, "ETHTOOL_SLINKSETTINGS: %s", strerror(errno));
		goto err;
	}

err:
	if (ioctl_sock > 0)
		close(ioctl_sock);
}

static void cp_set_vrf_master(const struct iface *iface) {
	uint32_t master = 0; // default VRF has no kernel master: leave TAP standalone

	if (iface->mode == GR_IFACE_MODE_VRF && iface->vrf_id != GR_VRF_DEFAULT_ID) {
		const struct iface *vrf = iface_from_id(iface->vrf_id);
		if (vrf == NULL || vrf->type != GR_IFACE_TYPE_VRF) {
			LOG(ERR, "no VRF iface for id %u on %s", iface->vrf_id, iface->name);
			return;
		}
		master = iface_info_vrf(vrf)->vrf_ifindex;
	}

	if (netlink_link_set_master(iface->cp_id, master) < 0)
		LOG(ERR,
		    "netlink_link_set_master(%s, %u): %s",
		    iface->name,
		    master,
		    strerror(errno));
}

static void cp_update(struct iface *iface) {
	struct rte_ether_addr mac;
	char cur_name[IFNAMSIZ];

	if (iface->cp_id == 0)
		return;

	if (if_indextoname(iface->cp_id, cur_name) != NULL && strcmp(cur_name, iface->name) != 0) {
		if (netlink_link_set_name(iface->cp_id, iface->name) < 0)
			LOG(ERR,
			    "netlink_link_set_name(%s, %s): %s",
			    cur_name,
			    iface->name,
			    strerror(errno));
	}

	cp_set_vrf_master(iface);

	if (iface->mtu != 0) {
		if (netlink_link_set_mtu(iface->cp_id, iface->mtu) < 0)
			LOG(ERR,
			    "netlink_link_set_mtu(%s, %u): %s",
			    iface->name,
			    iface->mtu,
			    strerror(errno));
	}

	if (iface_get_eth_addr(iface, &mac) == 0) {
		if (netlink_link_set_mac(iface->cp_id, &mac) < 0)
			LOG(ERR,
			    "netlink_link_set_mac(%s, " ETH_F "): %s",
			    iface->name,
			    &mac,
			    strerror(errno));
	}
}

static void iface_event(uint32_t event, const void *obj) {
	struct iface *iface = (struct iface *)obj;

	switch (iface->type) {
	case GR_IFACE_TYPE_PORT:
	case GR_IFACE_TYPE_VLAN:
	case GR_IFACE_TYPE_BOND:
	case GR_IFACE_TYPE_BRIDGE:
	case GR_IFACE_TYPE_VXLAN:
		break;
	default:
		return;
	}

	switch (event) {
	case GR_EVENT_IFACE_ADD:
		cp_create(iface);
		// fallthrough
	case GR_EVENT_IFACE_POST_RECONFIG:
		cp_update(iface);
		break;
	case GR_EVENT_IFACE_REMOVE:
		netlink_del_cp_route(iface->name, iface->cp_id);
		cp_delete(iface);
		break;
	case GR_EVENT_IFACE_STATUS_UP:
		netlink_link_set_admin_state(iface->cp_id, true, true);
		netlink_add_cp_route(iface->name, iface->cp_id);
		cp_set_speed(iface);
		break;
	case GR_EVENT_IFACE_STATUS_DOWN:
		netlink_link_set_admin_state(iface->cp_id, false, true);
		break;
	}
}

static void cp_module_init(struct event_base *base) {
	if (netlink_flush_cp_route_table() < 0)
		LOG(WARNING, "netlink_flush_cp_route_table: %s", strerror(errno));

	ev_base = base;
}

static struct module cp_module = {
	.name = "controlplane",
	.depends_on = "graph,mempool",
	.init = cp_module_init,
};

RTE_INIT(cp_constructor) {
	module_register(&cp_module);
	event_subscribe(GR_EVENT_IFACE_ADD, iface_event);
	event_subscribe(GR_EVENT_IFACE_REMOVE, iface_event);
	event_subscribe(GR_EVENT_IFACE_STATUS_UP, iface_event);
	event_subscribe(GR_EVENT_IFACE_STATUS_DOWN, iface_event);
	event_subscribe(GR_EVENT_IFACE_POST_RECONFIG, iface_event);
}
