/* osmo-pfcp-tool interface to quagga VTY */
/*
 * (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 <stdlib.h>
#include <unistd.h>

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

#include <osmocom/pfcp/pfcp_endpoint.h>
#include <osmocom/pfcp/pfcp_msg.h>

#include <osmocom/vty/vty.h>
#include <osmocom/vty/command.h>

#include "pfcp_tool.h"

enum pfcp_tool_vty_node {
	PEER_NODE = _LAST_OSMOVTY_NODE + 1,
	SESSION_NODE,
};

DEFUN(c_local_addr, c_local_addr_cmd,
      "local-addr IP_ADDR",
      "Set the local IP address to bind on for PFCP; see also 'listen'\n"
      "IP address\n")
{
	if (g_pfcp_tool->ep != NULL) {
		vty_out(vty, "Already listening on %s%s",
			osmo_sockaddr_to_str_c(OTC_SELECT, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep)),
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	osmo_talloc_replace_string(g_pfcp_tool, &g_pfcp_tool->vty_cfg.local_ip, argv[0]);

	return CMD_SUCCESS;
}

DEFUN(c_listen, c_listen_cmd,
      "listen",
      "Bind local PFCP port and listen; see also 'local-addr'\n")
{
	struct osmo_sockaddr_str local_addr;
	struct osmo_pfcp_endpoint_cfg cfg;
	int rc;

	OSMO_ASSERT(g_pfcp_tool);
	if (g_pfcp_tool->ep != NULL) {
		vty_out(vty, "Already listening on %s%s",
			osmo_sockaddr_to_str_c(OTC_SELECT, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep)),
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	cfg = (struct osmo_pfcp_endpoint_cfg){
		.rx_msg_cb = pfcp_tool_rx_msg,
	};

	/* Translate address string from VTY config to osmo_sockaddr: first read into osmo_sockaddr_str, then write to
	 * osmo_sockaddr. */
	osmo_sockaddr_str_from_str(&local_addr, g_pfcp_tool->vty_cfg.local_ip,
				   g_pfcp_tool->vty_cfg.local_port);
	osmo_sockaddr_str_to_sockaddr(&local_addr, &cfg.local_addr.u.sas);

	/* Also use this address as the local PFCP Node Id */
	osmo_pfcp_ie_node_id_from_osmo_sockaddr(&cfg.local_node_id, &cfg.local_addr);

	g_pfcp_tool->ep = osmo_pfcp_endpoint_create(g_pfcp_tool, &cfg);
	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Failed to allocate PFCP endpoint.%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	osmo_pfcp_endpoint_set_seq_nr_state(g_pfcp_tool->ep, rand());

	rc = osmo_pfcp_endpoint_bind(g_pfcp_tool->ep);
	if (rc) {
		vty_out(vty, "Failed to bind PFCP endpoint on %s: %s%s",
			osmo_sockaddr_to_str_c(OTC_SELECT, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep)),
			strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(c_sleep, c_sleep_cmd,
	"sleep <0-999999> [<0-999>]",
	"Let some time pass\n"
	"Seconds to wait\n" "Additional milliseconds to wait\n")
{
	int secs = atoi(argv[0]);
	int msecs = 0;
	struct osmo_timer_list t = {};
	if (argc > 1)
		msecs = atoi(argv[1]);

	vty_out(vty, "zzZ %d.%03ds...%s", secs, msecs, VTY_NEWLINE);
	vty_flush(vty);

	osmo_timer_setup(&t, NULL, NULL);
	osmo_timer_schedule(&t, secs, msecs * 1000);

	/* Still operate the message pump while waiting for time to pass */
	while (t.active && !osmo_select_shutdown_done()) {
		if (pfcp_tool_mainloop())
			break;
	}

	osmo_timer_del(&t);
	vty_out(vty, "...zzZ %d.%03ds%s", secs, msecs, VTY_NEWLINE);
	vty_flush(vty);
	return CMD_SUCCESS;
}

static struct cmd_node peer_node = {
	PEER_NODE,
	"%s(peer)# ",
	1,
};

DEFUN(peer, peer_cmd,
      "pfcp-peer REMOTE_ADDR",
      "Enter the 'peer' node for the given remote address\n"
      "Remote PFCP peer's IP address\n")
{
	struct pfcp_tool_peer *peer;
	struct osmo_sockaddr_str remote_addr_str;
	struct osmo_sockaddr remote_addr;

	osmo_sockaddr_str_from_str(&remote_addr_str, argv[0], OSMO_PFCP_PORT);
	osmo_sockaddr_str_to_sockaddr(&remote_addr_str, (struct sockaddr_storage *)&remote_addr);

	peer = pfcp_tool_peer_find_or_create(&remote_addr);

	vty->index = peer;
	vty->node = PEER_NODE;

	return CMD_SUCCESS;
}

#define TX_STR "Send a PFCP message to a peer\n"

DEFUN(peer_tx_heartbeat, peer_tx_heartbeat_cmd,
      "tx heartbeat",
      TX_STR "Send a Heartbeat Request\n")
{
	struct pfcp_tool_peer *peer = vty->index;
	int rc;

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	vty_out(vty, "Tx Heartbeat Request to %s%s",
		osmo_sockaddr_to_str_c(OTC_SELECT, &peer->remote_addr), VTY_NEWLINE);

	rc = osmo_pfcp_endpoint_tx_heartbeat_req(g_pfcp_tool->ep, &peer->remote_addr);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(peer_tx_assoc_setup_req, peer_tx_assoc_setup_req_cmd,
      "tx assoc-setup-req",
      TX_STR "Send an Association Setup Request\n")
{
	struct pfcp_tool_peer *peer = vty->index;
	int rc;
	struct osmo_pfcp_msg *m;

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_ASSOC_SETUP_REQ);
	m->ies.assoc_setup_req.recovery_time_stamp = osmo_pfcp_endpoint_get_recovery_timestamp(g_pfcp_tool->ep);

	m->ies.assoc_setup_req.cp_function_features_present = true;
	osmo_pfcp_bits_set(m->ies.assoc_setup_req.cp_function_features.bits, OSMO_PFCP_CP_FEAT_BUNDL, true);

	rc = peer_tx(peer, m);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(peer_retrans_req, peer_retrans_req_cmd,
      "retrans (req|resp)",
      "Retransmit the last sent message\n" "Retransmit the last sent PFCP Request\n"
      "Retransmit the last sent PFCP Response\n")
{
	struct pfcp_tool_peer *peer = vty->index;
	int rc;
	struct osmo_pfcp_msg *m;

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	/* Allocate using API function to have the usual talloc destructor set up, then copy the last request or
	 * response over it. */
	m = osmo_pfcp_msg_alloc_rx(OTC_SELECT, &peer->remote_addr);
	if (strcmp(argv[0], "req") == 0)
		*m = peer->last_req;
	else
		*m = peer->last_resp;

	OSMO_LOG_PFCP_MSG(m, LOGL_INFO, "retrans %s\n", argv[0]);

	rc = osmo_pfcp_endpoint_tx_data(g_pfcp_tool->ep, m);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

static struct cmd_node session_node = {
	SESSION_NODE,
	"%s(session)# ",
	1,
};

#define SESSION_STR "Enter the 'session' node for the given SEID\n"
#define TUNEND_STR "Set up GTP tunnel encapsulation/decapsulation (default)\n"
#define TUNMAP_STR "Set up GTP tunnel mapping\n"
#define SEID_STR "local Session Endpoint ID\n"

DEFUN(session, session_cmd,
      "session [(tunend|tunmap)] [<0-18446744073709551615>]",
      SESSION_STR TUNEND_STR TUNMAP_STR SEID_STR)
{
	struct pfcp_tool_peer *peer = vty->index;
	struct pfcp_tool_session *session;
	enum up_gtp_action_kind kind = UP_GTP_U_TUNEND;

	if (argc > 0 && !strcmp(argv[0], "tunmap"))
		kind = UP_GTP_U_TUNMAP;

	if (argc > 1)
		session = pfcp_tool_session_find_or_create(peer, atoll(argv[1]), kind);
	else
		session = pfcp_tool_session_find_or_create(peer, peer_new_seid(peer), kind);

	vty->index = session;
	vty->node = SESSION_NODE;

	return CMD_SUCCESS;
}

/* legacy compat: "tunend" was originally named "endecaps" */
DEFUN_CMD_ELEMENT(session, session_endecaps_cmd,
		  "session (endecaps) [<0-18446744073709551615>]",
		  SESSION_STR TUNEND_STR SEID_STR, CMD_ATTR_HIDDEN, 0);

DEFUN(s_ue, s_ue_cmd,
      "ue ip A.B.C.D",
      "Setup the UE as it appears towards the Core network in plain IP traffic\n"
      "IP address assigned to the UE\n")
{
	struct pfcp_tool_session *session = vty->index;

	if (session->kind != UP_GTP_U_TUNEND) {
		vty_out(vty, "%% Error: 'ue ip' makes no sense in a 'tunmap' session%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (osmo_sockaddr_str_from_str2(&session->tunend.core.ue_local_addr, argv[0])) {
		vty_out(vty, "Error setting UE IP address%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

#define GTP_ACCESS_CORE_STRS \
      "Setup GTP\n" \
      "Setup GTP towards ACCESS (towards the radio network and the actual UE)\n" \
      "Setup GTP towards CORE (towards the internet)\n"
#define GTP_LOCAL_STR "Setup GTP on the local side (UPF's local GTP endpoint)\n"
#define GTP_REMOTE_STR "Setup GTP on the remote side (UPF's remote GTP peer)\n"
#define F_TEID_STR "Set the fully-qualified TEID, i.e. GTP IP address and TEID\n"

DEFUN(s_f_teid, s_f_teid_cmd,
      "gtp (access|core) (local|remote) f-teid A.B.C.D <0-4294967295>",
      GTP_ACCESS_CORE_STRS
      GTP_LOCAL_STR GTP_REMOTE_STR
      F_TEID_STR
      "GTP peer IP address\n"
      "GTP TEID\n")
{
	struct pfcp_tool_session *session = vty->index;
	const char *tun_side = argv[0];
	const char *local_remote = argv[1];
	const char *addr_str = argv[2];
	const char *teid_str = argv[3];
	struct pfcp_tool_gtp_tun_ep *dst;

	switch (session->kind) {
	case UP_GTP_U_TUNEND:
		if (!strcmp(tun_side, "access")) {
			if (!strcmp(local_remote, "local"))
				dst = &session->tunend.access.local;
			else
				dst = &session->tunend.access.remote;
		} else {
			vty_out(vty, "%% Error: 'gtp core (local|remote) f-teid': 'tunend' only has GTP on"
				" the 'access' side%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		break;
	case UP_GTP_U_TUNMAP:
		if (!strcmp(tun_side, "access")) {
			if (!strcmp(local_remote, "local"))
				dst = &session->tunmap.access.local;
			else
				dst = &session->tunmap.access.remote;
		} else {
			if (!strcmp(local_remote, "local"))
				dst = &session->tunmap.core.local;
			else
				dst = &session->tunmap.core.remote;
		}
		break;
	default:
		OSMO_ASSERT(0);
	}

	if (osmo_sockaddr_str_from_str2(&dst->addr, addr_str)) {
		vty_out(vty, "Error setting GTP IP address from %s%s",
			osmo_quote_cstr_c(OTC_SELECT, addr_str, -1), VTY_NEWLINE);
		return CMD_WARNING;
	}
	dst->teid = atoi(teid_str);
	return CMD_SUCCESS;
}

DEFUN(s_f_teid_choose, s_f_teid_choose_cmd,
      "gtp (access|core) local f-teid choose",
      GTP_ACCESS_CORE_STRS
      GTP_LOCAL_STR
      F_TEID_STR
      "Send F-TEID with CHOOSE=1, i.e. the UPF shall return the local F-TEID in a PFCP Created PDR IE\n")
{
	struct pfcp_tool_session *session = vty->index;
	const char *tun_side = argv[0];
	struct pfcp_tool_gtp_tun_ep *dst;

	switch (session->kind) {
	case UP_GTP_U_TUNEND:
		if (!strcmp(tun_side, "access")) {
			dst = &session->tunend.access.local;
		} else {
			vty_out(vty, "%% Error: 'gtp core local choose': 'tunend' only has GTP on"
				" the 'access' side%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		break;
	case UP_GTP_U_TUNMAP:
		if (!strcmp(tun_side, "access"))
			dst = &session->tunmap.access.local;
		else
			dst = &session->tunmap.core.local;
		break;
	default:
		OSMO_ASSERT(0);
	}

	*dst = (struct pfcp_tool_gtp_tun_ep){};
	return CMD_SUCCESS;
}

enum pdr_id_fixed {
	PDR_ID_CORE = 1,
	PDR_ID_ACCESS = 2,
};

int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
{
	struct pfcp_tool_session *session = vty->index;
	struct pfcp_tool_peer *peer = session->peer;
	int rc;
	struct osmo_pfcp_msg *m;
	struct osmo_pfcp_ie_f_teid f_teid_access_local;
	struct osmo_pfcp_ie_outer_header_creation ohc_access;
	struct osmo_pfcp_ie_apply_action aa = {};
	struct osmo_sockaddr ue_addr;
	struct osmo_pfcp_ie_f_seid cp_f_seid;

	OSMO_ASSERT(session->kind == UP_GTP_U_TUNEND);

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (argc > 0 && !strcmp("drop", argv[0]))
		osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
	else
		osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);

#define STR_TO_ADDR(DST, SRC) do { \
			if (osmo_sockaddr_str_to_sockaddr(&SRC, &DST.u.sas)) { \
				vty_out(vty, "Error in " #SRC ": " OSMO_SOCKADDR_STR_FMT "%s", \
					OSMO_SOCKADDR_STR_FMT_ARGS(&SRC), VTY_NEWLINE); \
				return CMD_WARNING; \
			} \
		} while (0)

	STR_TO_ADDR(ue_addr, session->tunend.core.ue_local_addr);

	if (session->tunend.access.local.teid == 0) {
		f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
			.choose_flag = true,
			.choose = {
				.ipv4_addr = true,
			},
		};
	} else {
		f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
			.fixed = {
				.teid = session->tunend.access.local.teid,
				.ip_addr = {
					.v4_present = true,
				},
			},
		};
		STR_TO_ADDR(f_teid_access_local.fixed.ip_addr.v4, session->tunend.access.local.addr);
	}

	ohc_access = (struct osmo_pfcp_ie_outer_header_creation){
		.teid_present = true,
		.teid = session->tunend.access.remote.teid,
		.ip_addr.v4_present = true,
	};
	osmo_pfcp_bits_set(ohc_access.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
	STR_TO_ADDR(ohc_access.ip_addr.v4, session->tunend.access.remote.addr);

	cp_f_seid = (struct osmo_pfcp_ie_f_seid){
		.seid = session->cp_seid,
	};
	osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep));

	m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_EST_REQ);
	m->h.seid_present = true;
	/* the UPF has yet to assign a SEID for itself, no matter what SEID we (the CPF) use for this session */
	m->h.seid = 0;
	/* GTP encapsulation decapsulation: remove header from ACCESS to CORE, add header from CORE towards ACCESS */
	m->ies.session_est_req = (struct osmo_pfcp_msg_session_est_req){
		.node_id = m->ies.session_est_req.node_id,
		.cp_f_seid_present = true,
		.cp_f_seid = cp_f_seid,
		.create_pdr_count = 2,
		.create_pdr = {
			{
				.pdr_id = PDR_ID_CORE,
				.precedence = 255,
				.pdi = {
					.source_iface = OSMO_PFCP_SOURCE_IFACE_CORE,
					.ue_ip_address_present = true,
					.ue_ip_address = {
						.ip_is_destination = true,
						.ip_addr = {
							.v4_present = true,
							.v4 = ue_addr,
						},
					},
				},
				.far_id_present = true,
				.far_id = 1,
			},
			{
				.pdr_id = PDR_ID_ACCESS,
				.precedence = 255,
				.pdi = {
					.source_iface = OSMO_PFCP_SOURCE_IFACE_ACCESS,
					.local_f_teid_present = true,
					.local_f_teid = f_teid_access_local,
				},
				.outer_header_removal_present = true,
				.outer_header_removal = {
					.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
				},
				.far_id_present = true,
				.far_id = 2,
			},
		},
		.create_far_count = 2,
		.create_far = {
			{
				.far_id = 1,
				.forw_params_present = true,
				.forw_params = {
					.destination_iface = OSMO_PFCP_DEST_IFACE_ACCESS,
					.outer_header_creation_present = true,
					.outer_header_creation = ohc_access,
				},
				.apply_action = aa,
			},
			{
				.far_id = 2,
				.forw_params_present = true,
				.forw_params = {
					.destination_iface = OSMO_PFCP_DEST_IFACE_CORE,
				},
				.apply_action = aa,
			},
		},
	};

	rc = peer_tx(peer, m);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
{
	struct pfcp_tool_session *session = vty->index;
	struct pfcp_tool_peer *peer = session->peer;
	int rc;
	struct osmo_pfcp_msg *m;

	struct osmo_pfcp_ie_f_seid cp_f_seid;

	struct osmo_pfcp_ie_f_teid f_teid_access_local;
	struct osmo_pfcp_ie_outer_header_creation ohc_access;

	struct osmo_pfcp_ie_f_teid f_teid_core_local;
	struct osmo_pfcp_ie_outer_header_creation ohc_core;

	struct osmo_pfcp_ie_apply_action aa = {};

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (argc > 0 && !strcmp("drop", argv[0]))
		osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
	else
		osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);

	if (session->tunmap.access.local.teid == 0) {
		f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
			.choose_flag = true,
			.choose = {
				.ipv4_addr = true,
			},
		};
	} else {
		f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
			.fixed = {
				.teid = session->tunmap.access.local.teid,
				.ip_addr = {
					.v4_present = true,
					.v4 = osmo_pfcp_endpoint_get_cfg(g_pfcp_tool->ep)->local_addr,
				},
			},
		};
		STR_TO_ADDR(f_teid_access_local.fixed.ip_addr.v4, session->tunmap.access.local.addr);
	}

	ohc_access = (struct osmo_pfcp_ie_outer_header_creation){
		.teid_present = true,
		.teid = session->tunmap.access.remote.teid,
		.ip_addr.v4_present = true,
	};
	osmo_pfcp_bits_set(ohc_access.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
	STR_TO_ADDR(ohc_access.ip_addr.v4, session->tunmap.access.remote.addr);

	if (session->tunmap.core.local.teid == 0) {
		f_teid_core_local = (struct osmo_pfcp_ie_f_teid){
			.choose_flag = true,
			.choose = {
				.ipv4_addr = true,
			},
		};
	} else {
		f_teid_core_local = (struct osmo_pfcp_ie_f_teid){
			.fixed = {
				.teid = session->tunmap.core.local.teid,
				.ip_addr = {
					.v4_present = true,
				},
			},
		};
		STR_TO_ADDR(f_teid_core_local.fixed.ip_addr.v4, session->tunmap.core.local.addr);
	}
	ohc_core = (struct osmo_pfcp_ie_outer_header_creation){
		.teid_present = true,
		.teid = session->tunmap.core.remote.teid,
		.ip_addr.v4_present = true,
	};
	osmo_pfcp_bits_set(ohc_core.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
	STR_TO_ADDR(ohc_core.ip_addr.v4, session->tunmap.core.remote.addr);

	cp_f_seid = (struct osmo_pfcp_ie_f_seid){
		.seid = session->cp_seid,
	};
	osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep));

	m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_EST_REQ);
	m->h.seid_present = true;
	m->h.seid = 0;
	/* GTP tunmap: remove header from both directions, and add header in both directions */
	m->ies.session_est_req = (struct osmo_pfcp_msg_session_est_req){
		.node_id = m->ies.session_est_req.node_id,
		.cp_f_seid_present = true,
		.cp_f_seid = cp_f_seid,
		.create_pdr_count = 2,
		.create_pdr = {
			{
				.pdr_id = PDR_ID_CORE,
				.precedence = 255,
				.pdi = {
					.source_iface = OSMO_PFCP_SOURCE_IFACE_CORE,
					.local_f_teid_present = true,
					.local_f_teid = f_teid_core_local,
				},
				.outer_header_removal_present = true,
				.outer_header_removal = {
					.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
				},
				.far_id_present = true,
				.far_id = 1,
			},
			{
				.pdr_id = PDR_ID_ACCESS,
				.precedence = 255,
				.pdi = {
					.source_iface = OSMO_PFCP_SOURCE_IFACE_ACCESS,
					.local_f_teid_present = true,
					.local_f_teid = f_teid_access_local,
				},
				.outer_header_removal_present = true,
				.outer_header_removal = {
					.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
				},
				.far_id_present = true,
				.far_id = 2,
			},
		},
		.create_far_count = 2,
		.create_far = {
			{
				.far_id = 1,
				.forw_params_present = true,
				.forw_params = {
					.destination_iface = OSMO_PFCP_DEST_IFACE_ACCESS,
					.outer_header_creation_present = true,
					.outer_header_creation = ohc_access,
				},
				.apply_action = aa,
			},
			{
				.far_id = 2,
				.forw_params_present = true,
				.forw_params = {
					.destination_iface = OSMO_PFCP_DEST_IFACE_CORE,
					.outer_header_creation_present = true,
					.outer_header_creation = ohc_core,
				},
				.apply_action = aa,
			},
		},
	};

	rc = peer_tx(peer, m);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(session_tx_est_req, session_tx_est_req_cmd,
      "tx session-est-req [(forw|drop)]",
      TX_STR "Send a Session Establishment Request\n"
      "Set FAR to FORW = 1 (default)\n"
      "Set FAR to DROP = 1\n")
{
	struct pfcp_tool_session *session = vty->index;
	switch (session->kind) {
	case UP_GTP_U_TUNEND:
		return session_tunend_tx_est_req(vty, argv, argc);
	case UP_GTP_U_TUNMAP:
		return session_tunmap_tx_est_req(vty, argv, argc);
	default:
		vty_out(vty, "unknown gtp action%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
}

DEFUN(session_tx_mod_req, session_tx_mod_req_cmd,
      "tx session-mod-req far [(forw|drop)]",
      TX_STR "Send a Session Modification Request\n"
      "Set FAR to FORW = 1\n"
      "Set FAR to DROP = 1\n")
{
	struct pfcp_tool_session *session = vty->index;
	struct pfcp_tool_peer *peer = session->peer;
	int rc;
	struct osmo_pfcp_msg *m;
	struct osmo_pfcp_ie_apply_action aa = {};
	struct osmo_pfcp_ie_f_seid cp_f_seid;

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (argc > 0 && !strcmp("drop", argv[0]))
		osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
	else
		osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);

	cp_f_seid = (struct osmo_pfcp_ie_f_seid){
		.seid = session->cp_seid,
	};
	osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep));

	m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_MOD_REQ);
	m->h.seid_present = true;
	m->h.seid = session->up_f_seid.seid;
	m->ies.session_mod_req = (struct osmo_pfcp_msg_session_mod_req){
		.cp_f_seid_present = true,
		.cp_f_seid = cp_f_seid,
		.upd_far_count = 2,
		.upd_far = {
			{
				.far_id = 1,
				.apply_action_present = true,
				.apply_action = aa,
			},
			{
				.far_id = 2,
				.apply_action_present = true,
				.apply_action = aa,
			},
		},
	};

	rc = peer_tx(peer, m);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(session_tx_del_req, session_tx_del_req_cmd,
      "tx session-del-req",
      TX_STR "Send a Session Deletion Request\n")
{
	struct pfcp_tool_session *session = vty->index;
	struct pfcp_tool_peer *peer = session->peer;
	int rc;
	struct osmo_pfcp_msg *m;

	if (!g_pfcp_tool->ep) {
		vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_DEL_REQ);
	m->h.seid_present = true;
	m->h.seid = session->up_f_seid.seid;

	rc = peer_tx(peer, m);
	if (rc) {
		vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

static void install_ve_and_config(struct cmd_element *cmd)
{
	install_element_ve(cmd);
	install_element(CONFIG_NODE, cmd);
}

void pfcp_tool_vty_init_cfg()
{
	OSMO_ASSERT(g_pfcp_tool != NULL);

	install_ve_and_config(&c_local_addr_cmd);
	install_ve_and_config(&c_listen_cmd);
}

void pfcp_tool_vty_init_cmds()
{
	OSMO_ASSERT(g_pfcp_tool != NULL);

	install_ve_and_config(&c_sleep_cmd);

	install_ve_and_config(&peer_cmd);
	install_node(&peer_node, NULL);

	install_element(PEER_NODE, &c_sleep_cmd);
	install_element(PEER_NODE, &peer_tx_heartbeat_cmd);
	install_element(PEER_NODE, &peer_tx_assoc_setup_req_cmd);
	install_element(PEER_NODE, &peer_retrans_req_cmd);

	install_element(PEER_NODE, &session_cmd);
	install_element(PEER_NODE, &session_endecaps_cmd);
	install_node(&session_node, NULL);
	install_element(SESSION_NODE, &c_sleep_cmd);
	install_element(SESSION_NODE, &session_tx_est_req_cmd);
	install_element(SESSION_NODE, &session_tx_mod_req_cmd);
	install_element(SESSION_NODE, &session_tx_del_req_cmd);
	install_element(SESSION_NODE, &s_ue_cmd);
	install_element(SESSION_NODE, &s_f_teid_cmd);
	install_element(SESSION_NODE, &s_f_teid_choose_cmd);
}
