// SPDX-License-Identifier: GPL-2.0-or-later
/* strongSwan VICI protocol implementation for NHRP
 * Copyright (c) 2014-2015 Timo Teräs
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>

#include "frrevent.h"
#include "zbuf.h"
#include "log.h"
#include "lib_errors.h"

#include "nhrpd.h"
#include "vici.h"
#include "nhrp_errors.h"

#define ERRNO_IO_RETRY(EN) (((EN) == EAGAIN) || ((EN) == EWOULDBLOCK) || ((EN) == EINTR))

struct blob {
	char *ptr;
	int len;
};

static int blob_equal(const struct blob *b, const char *str)
{
	if (!b || b->len != (int)strlen(str))
		return 0;
	return memcmp(b->ptr, str, b->len) == 0;
}

static int blob2buf(const struct blob *b, char *buf, size_t n)
{
	if (!b || b->len >= (int)n)
		return 0;
	memcpy(buf, b->ptr, b->len);
	buf[b->len] = 0;
	return 1;
}

struct vici_conn {
	struct event *t_reconnect, *t_read, *t_write;
	struct zbuf ibuf;
	struct zbuf_queue obuf;
	int fd;
	uint8_t ibuf_data[VICI_MAX_MSGLEN];
};

struct vici_message_ctx {
	const char *sections[8];
	int nsections;
};

static void vici_reconnect(struct event *t);
static void vici_submit_request(struct vici_conn *vici, const char *name, ...);

static void vici_zbuf_puts(struct zbuf *obuf, const char *str)
{
	size_t len = strlen(str);
	zbuf_put8(obuf, len);
	zbuf_put(obuf, str, len);
}

static void vici_connection_error(struct vici_conn *vici)
{
	nhrp_vc_reset();

	EVENT_OFF(vici->t_read);
	EVENT_OFF(vici->t_write);
	zbuf_reset(&vici->ibuf);
	zbufq_reset(&vici->obuf);

	close(vici->fd);
	vici->fd = -1;
	event_add_timer(master, vici_reconnect, vici, 2, &vici->t_reconnect);
}

static void vici_parse_message(struct vici_conn *vici, struct zbuf *msg,
			       void (*parser)(struct vici_message_ctx *ctx,
					      enum vici_type_t msgtype,
					      const struct blob *key,
					      const struct blob *val),
			       struct vici_message_ctx *ctx)
{
	uint8_t *type;
	struct blob key = {0};
	struct blob val = {0};

	while ((type = zbuf_may_pull(msg, uint8_t)) != NULL) {
		switch (*type) {
		case VICI_SECTION_START:
			key.len = zbuf_get8(msg);
			key.ptr = zbuf_pulln(msg, key.len);
			debugf(NHRP_DEBUG_VICI, "VICI: Section start '%.*s'",
			       key.len, key.ptr);
			parser(ctx, *type, &key, NULL);
			ctx->nsections++;
			break;
		case VICI_SECTION_END:
			debugf(NHRP_DEBUG_VICI, "VICI: Section end");
			parser(ctx, *type, NULL, NULL);
			ctx->nsections--;
			break;
		case VICI_KEY_VALUE:
			key.len = zbuf_get8(msg);
			key.ptr = zbuf_pulln(msg, key.len);
			val.len = zbuf_get_be16(msg);
			val.ptr = zbuf_pulln(msg, val.len);
			debugf(NHRP_DEBUG_VICI, "VICI: Key '%.*s'='%.*s'",
			       key.len, key.ptr, val.len, val.ptr);
			parser(ctx, *type, &key, &val);
			break;
		case VICI_LIST_START:
			key.len = zbuf_get8(msg);
			key.ptr = zbuf_pulln(msg, key.len);
			debugf(NHRP_DEBUG_VICI, "VICI: List start '%.*s'",
			       key.len, key.ptr);
			break;
		case VICI_LIST_ITEM:
			val.len = zbuf_get_be16(msg);
			val.ptr = zbuf_pulln(msg, val.len);
			debugf(NHRP_DEBUG_VICI, "VICI: List item: '%.*s'",
			       val.len, val.ptr);
			parser(ctx, *type, &key, &val);
			break;
		case VICI_LIST_END:
			debugf(NHRP_DEBUG_VICI, "VICI: List end");
			break;
		}
	}
}

struct handle_sa_ctx {
	struct vici_message_ctx msgctx;
	int event;
	int child_ok;
	int kill_ikesa;
	uint32_t child_uniqueid, ike_uniqueid;
	struct {
		union sockunion host;
		struct blob id, cert;
	} local, remote;
};

static void parse_sa_message(struct vici_message_ctx *ctx,
			     enum vici_type_t msgtype, const struct blob *key,
			     const struct blob *val)
{
	struct handle_sa_ctx *sactx =
		container_of(ctx, struct handle_sa_ctx, msgctx);
	struct nhrp_vc *vc;
	char buf[512];

	switch (msgtype) {
	case VICI_SECTION_START:
		if (ctx->nsections == 3) {
			/* Begin of child-sa section, reset child vars */
			sactx->child_uniqueid = 0;
			sactx->child_ok = 0;
		}
		break;
	case VICI_SECTION_END:
		if (ctx->nsections == 3) {
			/* End of child-sa section, update nhrp_vc */
			int up = sactx->child_ok || sactx->event == 1;
			if (up) {
				vc = nhrp_vc_get(&sactx->local.host,
						 &sactx->remote.host, up);
				if (vc) {
					blob2buf(&sactx->local.id, vc->local.id,
						 sizeof(vc->local.id));
					if (blob2buf(&sactx->local.cert,
						     (char *)vc->local.cert,
						     sizeof(vc->local.cert)))
						vc->local.certlen =
							sactx->local.cert.len;
					blob2buf(&sactx->remote.id,
						 vc->remote.id,
						 sizeof(vc->remote.id));
					if (blob2buf(&sactx->remote.cert,
						     (char *)vc->remote.cert,
						     sizeof(vc->remote.cert)))
						vc->remote.certlen =
							sactx->remote.cert.len;
					sactx->kill_ikesa |=
						nhrp_vc_ipsec_updown(
							sactx->child_uniqueid,
							vc);
					vc->ike_uniqueid = sactx->ike_uniqueid;
				}
			} else {
				nhrp_vc_ipsec_updown(sactx->child_uniqueid, 0);
			}
		}
		break;
	case VICI_START:
	case VICI_KEY_VALUE:
	case VICI_LIST_START:
	case VICI_LIST_ITEM:
	case VICI_LIST_END:
	case VICI_END:
		if (!key || !key->ptr)
			break;

		switch (key->ptr[0]) {
		case 'l':
			if (blob_equal(key, "local-host")
			    && ctx->nsections == 1) {
				if (blob2buf(val, buf, sizeof(buf)))
					if (str2sockunion(buf,
							  &sactx->local.host)
					    < 0)
						flog_err(
							EC_NHRP_SWAN,
							"VICI: bad strongSwan local-host: %s",
							buf);
			} else if (blob_equal(key, "local-id")
				   && ctx->nsections == 1) {
				sactx->local.id = *val;
			} else if (blob_equal(key, "local-cert-data")
				   && ctx->nsections == 1) {
				sactx->local.cert = *val;
			}
			break;
		case 'r':
			if (blob_equal(key, "remote-host")
			    && ctx->nsections == 1) {
				if (blob2buf(val, buf, sizeof(buf)))
					if (str2sockunion(buf,
							  &sactx->remote.host)
					    < 0)
						flog_err(
							EC_NHRP_SWAN,
							"VICI: bad strongSwan remote-host: %s",
							buf);
			} else if (blob_equal(key, "remote-id")
				   && ctx->nsections == 1) {
				sactx->remote.id = *val;
			} else if (blob_equal(key, "remote-cert-data")
				   && ctx->nsections == 1) {
				sactx->remote.cert = *val;
			}
			break;
		case 'u':
			if (blob_equal(key, "uniqueid")
			    && blob2buf(val, buf, sizeof(buf))) {
				if (ctx->nsections == 3)
					sactx->child_uniqueid =
						strtoul(buf, NULL, 0);
				else if (ctx->nsections == 1)
					sactx->ike_uniqueid =
						strtoul(buf, NULL, 0);
			}
			break;
		case 's':
			if (blob_equal(key, "state") && ctx->nsections == 3) {
				sactx->child_ok =
					(sactx->event == 0
					 && (blob_equal(val, "INSTALLED")
					     || blob_equal(val, "REKEYED")));
			}
			break;
		}
		break;
	}
}

static void parse_cmd_response(struct vici_message_ctx *ctx,
			       enum vici_type_t msgtype, const struct blob *key,
			       const struct blob *val)
{
	char buf[512];

	switch (msgtype) {
	case VICI_KEY_VALUE:
		if (blob_equal(key, "errmsg")
		    && blob2buf(val, buf, sizeof(buf)))
			flog_err(EC_NHRP_SWAN, "VICI: strongSwan: %s", buf);
		break;
	case VICI_START:
	case VICI_SECTION_START:
	case VICI_SECTION_END:
	case VICI_LIST_START:
	case VICI_LIST_ITEM:
	case VICI_LIST_END:
	case VICI_END:
		break;
	}
}

static void vici_recv_sa(struct vici_conn *vici, struct zbuf *msg, int event)
{
	char buf[32];
	struct handle_sa_ctx ctx = {
		.event = event,
		.msgctx.nsections = 0
	};

	vici_parse_message(vici, msg, parse_sa_message, &ctx.msgctx);

	if (ctx.kill_ikesa && ctx.ike_uniqueid) {
		debugf(NHRP_DEBUG_COMMON, "VICI: Deleting IKE_SA %u",
		       ctx.ike_uniqueid);
		snprintf(buf, sizeof(buf), "%u", ctx.ike_uniqueid);
		vici_submit_request(vici, "terminate", VICI_KEY_VALUE, "ike-id",
				    strlen(buf), buf, VICI_END);
	}
}

static void vici_recv_message(struct vici_conn *vici, struct zbuf *msg)
{
	uint32_t msglen;
	uint8_t msgtype;
	struct blob name;
	struct vici_message_ctx ctx = { .nsections = 0 };

	msglen = zbuf_get_be32(msg);
	msgtype = zbuf_get8(msg);
	debugf(NHRP_DEBUG_VICI, "VICI: Message %d, %d bytes", msgtype, msglen);

	switch (msgtype) {
	case VICI_EVENT:
		name.len = zbuf_get8(msg);
		name.ptr = zbuf_pulln(msg, name.len);

		debugf(NHRP_DEBUG_VICI, "VICI: Event '%.*s'", name.len,
		       name.ptr);
		if (blob_equal(&name, "list-sa")
		    || blob_equal(&name, "child-updown")
		    || blob_equal(&name, "child-rekey"))
			vici_recv_sa(vici, msg, 0);
		else if (blob_equal(&name, "child-state-installed")
			 || blob_equal(&name, "child-state-rekeyed"))
			vici_recv_sa(vici, msg, 1);
		else if (blob_equal(&name, "child-state-destroying"))
			vici_recv_sa(vici, msg, 2);
		break;
	case VICI_CMD_RESPONSE:
		vici_parse_message(vici, msg, parse_cmd_response, &ctx);
		break;
	case VICI_EVENT_UNKNOWN:
	case VICI_CMD_UNKNOWN:
		flog_err(
			EC_NHRP_SWAN,
			"VICI: StrongSwan does not support mandatory events (unpatched?)");
		break;
	case VICI_EVENT_CONFIRM:
		break;
	default:
		zlog_notice("VICI: Unrecognized message type %d", msgtype);
		break;
	}
}

static void vici_read(struct event *t)
{
	struct vici_conn *vici = EVENT_ARG(t);
	struct zbuf *ibuf = &vici->ibuf;
	struct zbuf pktbuf;

	if (zbuf_read(ibuf, vici->fd, (size_t)-1) < 0) {
		vici_connection_error(vici);
		return;
	}

	/* Process all messages in buffer */
	do {
		uint32_t *hdrlen = zbuf_may_pull(ibuf, uint32_t);
		if (!hdrlen)
			break;
		if (!zbuf_may_pulln(ibuf, ntohl(*hdrlen))) {
			zbuf_reset_head(ibuf, hdrlen);
			break;
		}

		/* Handle packet */
		zbuf_init(&pktbuf, hdrlen, htonl(*hdrlen) + 4,
			  htonl(*hdrlen) + 4);
		vici_recv_message(vici, &pktbuf);
	} while (1);

	event_add_read(master, vici_read, vici, vici->fd, &vici->t_read);
}

static void vici_write(struct event *t)
{
	struct vici_conn *vici = EVENT_ARG(t);
	int r;

	r = zbufq_write(&vici->obuf, vici->fd);
	if (r > 0) {
		event_add_write(master, vici_write, vici, vici->fd,
				&vici->t_write);
	} else if (r < 0) {
		vici_connection_error(vici);
	}
}

static void vici_submit(struct vici_conn *vici, struct zbuf *obuf)
{
	if (vici->fd < 0) {
		zbuf_free(obuf);
		return;
	}

	zbufq_queue(&vici->obuf, obuf);
	event_add_write(master, vici_write, vici, vici->fd, &vici->t_write);
}

static void vici_submit_request(struct vici_conn *vici, const char *name, ...)
{
	struct zbuf *obuf;
	uint32_t *hdrlen;
	va_list va;
	size_t len;
	int type;

	obuf = zbuf_alloc(256);
	if (!obuf)
		return;

	hdrlen = zbuf_push(obuf, uint32_t);
	zbuf_put8(obuf, VICI_CMD_REQUEST);
	vici_zbuf_puts(obuf, name);

	va_start(va, name);
	for (type = va_arg(va, int); type != VICI_END; type = va_arg(va, int)) {
		zbuf_put8(obuf, type);
		switch (type) {
		case VICI_KEY_VALUE:
			vici_zbuf_puts(obuf, va_arg(va, const char *));
			len = va_arg(va, size_t);
			zbuf_put_be16(obuf, len);
			zbuf_put(obuf, va_arg(va, void *), len);
			break;
		default:
			break;
		}
	}
	va_end(va);
	*hdrlen = htonl(zbuf_used(obuf) - 4);
	vici_submit(vici, obuf);
}

static void vici_register_event(struct vici_conn *vici, const char *name)
{
	struct zbuf *obuf;
	uint32_t *hdrlen;
	uint8_t namelen;

	namelen = strlen(name);
	obuf = zbuf_alloc(4 + 1 + 1 + namelen);
	if (!obuf)
		return;

	hdrlen = zbuf_push(obuf, uint32_t);
	zbuf_put8(obuf, VICI_EVENT_REGISTER);
	zbuf_put8(obuf, namelen);
	zbuf_put(obuf, name, namelen);
	*hdrlen = htonl(zbuf_used(obuf) - 4);

	vici_submit(vici, obuf);
}

static bool vici_charon_filepath_done;
static bool vici_charon_not_found;

static char *vici_get_charon_filepath(void)
{
	static char buff[1200];
	FILE *fp;
	char *ptr;
	char line[1024];

	if (vici_charon_filepath_done)
		return (char *)buff;
	fp = popen("ipsec --piddir", "r");
	if (!fp) {
		if (!vici_charon_not_found) {
			flog_err(EC_NHRP_SWAN,
				 "VICI: Failed to retrieve charon file path");
			vici_charon_not_found = true;
		}
		return NULL;
	}
	/* last line of output is used to get vici path */
	while (fgets(line, sizeof(line), fp) != NULL) {
		ptr = strchr(line, '\n');
		if (ptr)
			*ptr = '\0';
		snprintf(buff, sizeof(buff), "%s/charon.vici", line);
	}
	pclose(fp);
	vici_charon_filepath_done = true;
	return buff;
}

static void vici_reconnect(struct event *t)
{
	struct vici_conn *vici = EVENT_ARG(t);
	int fd;
	char *file_path;

	if (vici->fd >= 0)
		return;

	fd = sock_open_unix(VICI_SOCKET);
	if (fd < 0) {
		file_path = vici_get_charon_filepath();
		if (file_path)
			fd = sock_open_unix(file_path);
	}
	if (fd < 0) {
		debugf(NHRP_DEBUG_VICI,
		       "%s: failure connecting VICI socket: %s", __func__,
		       strerror(errno));
		event_add_timer(master, vici_reconnect, vici, 2,
				&vici->t_reconnect);
		return;
	}

	debugf(NHRP_DEBUG_COMMON, "VICI: Connected");
	vici->fd = fd;
	event_add_read(master, vici_read, vici, vici->fd, &vici->t_read);

	/* Send event subscribtions */
	// vici_register_event(vici, "child-updown");
	// vici_register_event(vici, "child-rekey");
	vici_register_event(vici, "child-state-installed");
	vici_register_event(vici, "child-state-rekeyed");
	vici_register_event(vici, "child-state-destroying");
	vici_register_event(vici, "list-sa");
	vici_submit_request(vici, "list-sas", VICI_END);
}

static struct vici_conn vici_connection;

void vici_init(void)
{
	struct vici_conn *vici = &vici_connection;

	vici->fd = -1;
	zbuf_init(&vici->ibuf, vici->ibuf_data, sizeof(vici->ibuf_data), 0);
	zbufq_init(&vici->obuf);
	event_add_timer_msec(master, vici_reconnect, vici, 10,
			     &vici->t_reconnect);
}

void vici_terminate(void)
{
}

void vici_terminate_vc_by_profile_name(char *profile_name)
{
	struct vici_conn *vici = &vici_connection;

	debugf(NHRP_DEBUG_VICI, "Terminate profile = %s", profile_name);
	vici_submit_request(vici, "terminate", VICI_KEY_VALUE, "ike",
		    strlen(profile_name), profile_name, VICI_END);
}

void vici_terminate_vc_by_ike_id(unsigned int ike_id)
{
	struct vici_conn *vici = &vici_connection;
	char ike_id_str[10];

	snprintf(ike_id_str, sizeof(ike_id_str), "%d", ike_id);
	debugf(NHRP_DEBUG_VICI, "Terminate ike_id_str = %s", ike_id_str);
	vici_submit_request(vici, "terminate", VICI_KEY_VALUE, "ike-id",
		    strlen(ike_id_str), ike_id_str, VICI_END);
}

void vici_request_vc(const char *profile, union sockunion *src,
		     union sockunion *dst, int prio)
{
	struct vici_conn *vici = &vici_connection;
	char buf[2][SU_ADDRSTRLEN];

	sockunion2str(src, buf[0], sizeof(buf[0]));
	sockunion2str(dst, buf[1], sizeof(buf[1]));

	vici_submit_request(vici, "initiate", VICI_KEY_VALUE, "child",
			    strlen(profile), profile, VICI_KEY_VALUE, "timeout",
			    (size_t)2, "-1", VICI_KEY_VALUE, "async", (size_t)1,
			    "1", VICI_KEY_VALUE, "init-limits", (size_t)1,
			    prio ? "0" : "1", VICI_KEY_VALUE, "my-host",
			    strlen(buf[0]), buf[0], VICI_KEY_VALUE,
			    "other-host", strlen(buf[1]), buf[1], VICI_END);
}

int sock_open_unix(const char *path)
{
	int ret, fd;
	struct sockaddr_un addr;

	fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (fd < 0)
		return -1;

	memset(&addr, 0, sizeof(addr));
	addr.sun_family = AF_UNIX;
	strlcpy(addr.sun_path, path, sizeof(addr.sun_path));

	ret = connect(fd, (struct sockaddr *)&addr,
		      sizeof(addr.sun_family) + strlen(addr.sun_path));
	if (ret < 0) {
		close(fd);
		return -1;
	}

	ret = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
	if (ret < 0) {
		close(fd);
		return -1;
	}

	return fd;
}
