/* 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\n",
			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")
{
	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_DEBUG, "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,
};

DEFUN(session, session_cmd,
      "session [(endecaps|tunmap)] [<0-18446744073709551615>]",
      "Enter the 'session' node for the given SEID\n"
      "Set up GTP tunnel encapsulation/decapsulation (default)\n"
      "Set up GTP tunnel mapping\n"
      "local Session Endpoint ID\n")
{
	struct pfcp_tool_peer *peer = vty->index;
	struct pfcp_tool_session *session;
	enum up_gtp_action_kind gtp_action = UP_GTP_U_ENDECAPS;

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

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

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

	return CMD_SUCCESS;
}

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 (osmo_sockaddr_str_from_str2(&session->core.ue_addr, argv[0])) {
		vty_out(vty, "Error setting UE IP address%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(s_teid, s_teid_cmd,
      "gtp (access|core) teid local <0-4294967295> remote <0-4294967295>",
      "Setup TEID used in GTP\n"
      "Set the TEIDs towards the ACCESS network (towards the radio network and the actual UE)\n"
      "Set the TEIDs towards the CORE network (towards the internet)\n"
      "Local TEID, which the UPF expects to see in incoming GTP packets\n"
      "Local TEID, when 0 tell the UPF to choose (PFCP: FAR F-TEID: CHOOSE=1)\n"
      "Remote TEID, which the UPF sends out in GTP packets\n"
      "Remote TEID, which the GTP peer has assigned for itself\n")
{
	struct pfcp_tool_session *session = vty->index;
	struct pfcp_tool_teid_pair *dst;
	if (!strcmp(argv[0], "access"))
		dst = &session->access.teid;
	else
		dst = &session->core.teid;
	*dst = (struct pfcp_tool_teid_pair){
		.local = atoi(argv[1]),
		.remote = atoi(argv[2]),
	};
	return CMD_SUCCESS;
}

DEFUN(s_gtp, s_gtp_cmd,
      "gtp (access|core) ip A.B.C.D",
      "Setup GTP peer\n"
      "Set the GTP peer towards the ACCESS network (towards the radio network and the actual UE)\n"
      "Set the GTP peer towards the CORE network (towards the internet)\n"
      "Set the GTP peer IP address, where to send GTP packets to / receive GTP packets from\n"
      "GTP peer IP address\n")
{
	struct pfcp_tool_session *session = vty->index;
	struct osmo_sockaddr_str *dst;
	if (!strcmp(argv[0], "access"))
		dst = &session->access.gtp_ip;
	else
		dst = &session->core.gtp_ip;
	if (osmo_sockaddr_str_from_str2(dst, argv[1])) {
		vty_out(vty, "Error setting GTP IP address%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

int session_endecaps_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;

	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 (osmo_sockaddr_str_to_sockaddr(&session->core.ue_addr, &ue_addr.u.sas)) {
		vty_out(vty, "Error in UE IP%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (session->access.teid.local == 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->access.teid.local,
				.ip_addr = {
					.v4_present = true,
					.v4 = osmo_pfcp_endpoint_get_cfg(g_pfcp_tool->ep)->local_addr,
				},
			},
		};
		if (osmo_sockaddr_str_to_sockaddr(&session->access.gtp_ip, &f_teid_access_local.fixed.ip_addr.v4.u.sas)) {
			vty_out(vty, "Error in GTP IP towards Access%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	ohc_access = (struct osmo_pfcp_ie_outer_header_creation){
		.teid_present = true,
		.teid = session->access.teid.remote,
		.ip_addr.v4_present = true,
	};
	osmo_pfcp_bits_set(ohc_access.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
	if (osmo_sockaddr_str_to_sockaddr(&session->access.gtp_ip, &ohc_access.ip_addr.v4.u.sas)) {
		vty_out(vty, "Error in GTP IP towards Access%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	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 = 1,
				.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 = 2,
				.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->access.teid.local == 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->access.teid.local,
				.ip_addr = {
					.v4_present = true,
					.v4 = osmo_pfcp_endpoint_get_cfg(g_pfcp_tool->ep)->local_addr,
				},
			},
		};
		if (osmo_sockaddr_str_to_sockaddr(&session->access.gtp_ip, &f_teid_access_local.fixed.ip_addr.v4.u.sas)) {
			vty_out(vty, "Error in GTP IP towards Access%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	ohc_access = (struct osmo_pfcp_ie_outer_header_creation){
		.teid_present = true,
		.teid = session->access.teid.remote,
		.ip_addr.v4_present = true,
	};
	osmo_pfcp_bits_set(ohc_access.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
	if (osmo_sockaddr_str_to_sockaddr(&session->access.gtp_ip, &ohc_access.ip_addr.v4.u.sas)) {
		vty_out(vty, "Error in GTP IP towards Access%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (session->core.teid.local == 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->core.teid.local,
				.ip_addr = {
					.v4_present = true,
					.v4 = osmo_pfcp_endpoint_get_cfg(g_pfcp_tool->ep)->local_addr,
				},
			},
		};
		if (osmo_sockaddr_str_to_sockaddr(&session->core.gtp_ip, &f_teid_core_local.fixed.ip_addr.v4.u.sas)) {
			vty_out(vty, "Error in GTP IP towards Core%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}
	ohc_core = (struct osmo_pfcp_ie_outer_header_creation){
		.teid_present = true,
		.teid = session->core.teid.remote,
		.ip_addr.v4_present = true,
	};
	osmo_pfcp_bits_set(ohc_core.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
	if (osmo_sockaddr_str_to_sockaddr(&session->core.gtp_ip, &ohc_core.ip_addr.v4.u.sas)) {
		vty_out(vty, "Error in GTP IP towards Core%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	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 = 1,
				.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 = 2,
				.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->gtp_action) {
	case UP_GTP_U_ENDECAPS:
		return session_endecaps_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_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_gtp_cmd);
	install_element(SESSION_NODE, &s_teid_cmd);
}
