/* xUA Routing Key Management (RKM) as per RFC 4666 */
/* (C) 2017 by Harald Welte <laforge@gnumonks.org>
 * All Rights Reserved
 *
 * 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 <string.h>
#include <arpa/inet.h>

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

#include "xua_msg.h"
#include <osmocom/sigtran/osmo_ss7.h>
#include <osmocom/sigtran/protocol/m3ua.h>

#include "ss7_as.h"
#include "ss7_asp.h"
#include "ss7_internal.h"
#include "ss7_route.h"
#include "ss7_route_table.h"
#include "xua_internal.h"
#include "xua_as_fsm.h"
#include "xua_asp_fsm.h"

const struct value_string m3ua_rkm_reg_status_vals[] = {
	{ M3UA_RKM_REG_SUCCESS,			"SUCCESS" },
	{ M3UA_RKM_REG_ERR_UNKNOWN,		"Unknown Error" },
	{ M3UA_RKM_REG_ERR_INVAL_DPC,		"Invalid Destination Pointcode" },
	{ M3UA_RKM_REG_ERR_INVAL_NET_APPEAR,	"Invalid Network Appearance" },
	{ M3UA_RKM_REG_ERR_INVAL_RKEY,		"Invalid Routing Key" },
	{ M3UA_RKM_REG_ERR_PERM_DENIED,		"Permission Denied" },
	{ M3UA_RKM_REG_ERR_CANT_SUPP_UNQ_RT,	"Cannot Support Unique Routing" },
	{ M3UA_RKM_REG_ERR_RKEY_NOT_PROVD,	"Routing Key Not Provided" },
	{ M3UA_RKM_REG_ERR_INSUFF_RESRC,	"Insufficient Resources" },
	{ M3UA_RKM_REG_ERR_UNSUPP_RK_PARAM,	"Unsupported Routing Key Parameter" },
	{ M3UA_RKM_REG_ERR_UNSUPP_TRAF_MODE,	"Unsupported Traffic Mode Type" },
	{ M3UA_RKM_REG_ERR_RKEY_CHG_REFUSED,	"Routing Key Change Refused" },
	{ M3UA_RKM_REG_ERR_RKEY_ALRDY_REGD,	"Routing Key Already Registered" },
	{ 0, NULL }
};

const struct value_string m3ua_rkm_dereg_status_vals[] = {
	{ M3UA_RKM_DEREG_SUCCESS,		"SUCCSS" },
	{ M3UA_RKM_DEREG_ERR_UNKNOWN,		"Unknown Error" },
	{ M3UA_RKM_DEREG_ERR_INVAL_RCTX,	"Invalid Routing Context" },
	{ M3UA_RKM_DEREG_ERR_PERM_DENIED,	"Permission Denied" },
	{ M3UA_RKM_DEREG_ERR_NOT_REGD,		"Error: Not Registered" },
	{ M3UA_RKM_DEREG_ERR_ASP_ACTIVE,	"Error: ASP Active" },
	{ 0, NULL }
};

/* push a M3UA header to the front of the given message */
static void msgb_push_m3ua_hdr(struct msgb *msg, uint8_t msg_class, uint8_t msg_type)
{
	struct xua_common_hdr *hdr;

	msg->l2h = msgb_push(msg, sizeof(*hdr));
	hdr = (struct xua_common_hdr *) msg->l2h;

	hdr->version = M3UA_VERSION;
	hdr->spare = 0;
	hdr->msg_class = msg_class;
	hdr->msg_type = msg_type;
	hdr->msg_length = htonl(msgb_l2len(msg));
}

/* SG: append a single registration result to given msgb */
static int msgb_append_reg_res(struct msgb *msg, uint32_t local_rk_id,
				uint32_t status, uint32_t rctx)
{
	uint8_t *old_tail = msg->tail;

	/* One individual Registration Result according to Chapter 3.6.2 */
	msgb_put_u16(msg, M3UA_IEI_REG_RESULT); /* outer IEI */
	msgb_put_u16(msg, 24 + 4); /* outer length */
	/* nested IEIs */
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_LOC_RKEY_ID, local_rk_id);
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_REG_STATUS, status);
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_ROUTE_CTX, rctx);

	return msg->tail - old_tail;
}

/* SG: append a single de-registration result to given msgb */
static int msgb_append_dereg_res(struct msgb *msg,
				 uint32_t status, uint32_t rctx)
{
	uint8_t *old_tail = msg->tail;

	/* One individual De-Registration Result according to Chapter 3.6.4 */
	msgb_put_u16(msg, M3UA_IEI_DEREG_RESULT); /* outer IEI */
	msgb_put_u16(msg, 16 + 4); /* outer length */
	/* nested IEIs */
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_ROUTE_CTX, rctx);
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_DEREG_STATUS, status);

	return msg->tail - old_tail;
}

/* ASP: send a RKM Registration Request message for a single routing key */
void xua_rkm_send_reg_req(struct osmo_ss7_asp *asp,
			  const struct osmo_ss7_routing_key *rkey,
			  enum osmo_ss7_as_traffic_mode traf_mode)
{
	struct msgb *msg = m3ua_msgb_alloc(__func__);
	int tmod = osmo_ss7_tmode_to_xua(traf_mode);

	/* One individual Registration Request according to Chapter 3.6.1 */
	msgb_put_u16(msg, M3UA_IEI_ROUT_KEY); /* outer IEI */
	msgb_put_u16(msg, 32 + 4); /* outer length */
	/* nested IEIs */
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_LOC_RKEY_ID, rkey->l_rk_id);
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_ROUTE_CTX, rkey->context);
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_TRAF_MODE_TYP, tmod);
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_DEST_PC, rkey->pc);

	msgb_push_m3ua_hdr(msg, M3UA_MSGC_RKM, M3UA_RKM_REG_REQ);

	osmo_ss7_asp_send(asp, msg);
}

/* ASP: send a RKM De-Registration Request message for a single routing context */
void xua_rkm_send_dereg_req(struct osmo_ss7_asp *asp, uint32_t route_ctx)
{
	struct msgb *msg = m3ua_msgb_alloc(__func__);

	/* One individual De-Registration Request according to Chapter 3.6.3 */
	msgb_t16l16vp_put_u32(msg, M3UA_IEI_ROUTE_CTX, route_ctx);

	msgb_push_m3ua_hdr(msg, M3UA_MSGC_RKM, M3UA_RKM_DEREG_REQ);

	osmo_ss7_asp_send(asp, msg);
}

/* maximum number of newly-assigned Application Servers in one dynamic
 * RKM REG request */
#define MAX_NEW_AS 16

/* Lookup "as" pointer in array "newly_assigned_as" with "num_newly_assigned_as" elements. */
static bool newly_assigned_as_in_array(struct osmo_ss7_as * const *newly_assigned_as,
				       unsigned int num_newly_assigned_as,
				       const struct osmo_ss7_as *as)
{
	unsigned int i;
	for (i = 0; i < num_newly_assigned_as; i++) {
		if (as == newly_assigned_as[i])
			return true;
	}
	return false;
}

/* SG: handle a single registration request IE (nested IEs in 'innner' */
static int handle_rkey_reg(struct osmo_ss7_asp *asp, struct xua_msg *inner,
			   struct msgb *resp, struct osmo_ss7_as **newly_assigned_as,
			   unsigned int max_nas_idx, unsigned int *nas_idx)
{
	uint32_t rk_id, rctx, _tmode, dpc;
	enum osmo_ss7_as_traffic_mode tmode;
	struct osmo_ss7_as *as;
	struct osmo_ss7_route *rt;
	char namebuf[32];
	bool as_already_in_array;

	/* mandatory local routing key ID */
	rk_id = xua_msg_get_u32(inner, M3UA_IEI_LOC_RKEY_ID);
	/* ASP may already include a routing context value here */
	rctx = xua_msg_get_u32(inner, M3UA_IEI_ROUTE_CTX);

	/* traffic mode type (0 = undefined) */
	_tmode = xua_msg_get_u32(inner, M3UA_IEI_TRAF_MODE_TYP);
	if (xua_msg_find_tag(inner, M3UA_IEI_TRAF_MODE_TYP) && _tmode != M3UA_TMOD_OVERRIDE &&
	    _tmode != M3UA_TMOD_LOADSHARE && _tmode != M3UA_TMOD_BCAST) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: Invalid Traffic Mode %u\n", _tmode);
		msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_UNSUPP_TRAF_MODE, 0);
		return -1;
	}

	/* destination point code (mandatory) */
	dpc = xua_msg_get_u32(inner, M3UA_IEI_DEST_PC);

	/* We don't support routing keys with the following criteria, so
	 * we have to reject those */
	/* Network Appearance (optional) */
	if (xua_msg_find_tag(inner, M3UA_IEI_NET_APPEAR)) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: Unsupported 'Network Appearance' IE.\n");
		msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_INVAL_NET_APPEAR, 0);
		return -1;
	}
	/* TODO: service indicators (optional) */
	/* TODO: originating point code list (optional) */
	if (xua_msg_find_tag(inner, M3UA_IEI_SVC_IND) ||
	    xua_msg_find_tag(inner, M3UA_IEI_ORIG_PC)) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: Unsupported Routing Key\n");
		msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_UNSUPP_RK_PARAM, 0);
		return -1;
	}

	/* if the ASP did not include a routing context number, allocate
	 * one locally (will be part of response) */
	if (!rctx)
		rctx = osmo_ss7_find_free_rctx(asp->inst);

	LOGPASP(asp, DLSS7, LOGL_INFO, "RKM: Registering routing key %u for DPC %s\n",
		rctx, osmo_ss7_pointcode_print(asp->inst, dpc));

	/* We have two cases here:
	 * a) pre-configured routing context on both ASP and SG (or IPSP peers):
	 *    We will find the AS based on the RCTX send by the client, check if
	 *    the routing key matches, associated AS with ASP and return success.
	 * b) no routing context set on peer (ASP/IPSP), no pre-existing local AS
	 *    definition (SG/IPSP).  We have to create the AS, set the RK,
	 *    allocate the RCTX and return that RCTX to the client. This
	 *    is a slightly non-standard interpretation of M3UA RKM
	 *    which requires the SG/IPSP to not have a-priori-knowledge of
	 *    all AS/RK in situations where the peers are trusted.
	 */

	/* check if there is already an AS for this routing key */
	as = osmo_ss7_as_find_by_rctx(asp->inst, rctx);

	if (!as && !asp->inst->cfg.permit_dyn_rkm_alloc) {
		/* not permitted to create dynamic RKM entries */
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: RCTX %u not found in configuration, and "
			"dynamic RKM allocation not permitted; permission denied\n", rctx);
		msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_PERM_DENIED, 0);
		return -1;
	}

	/* Early return before allocating stuff if no space left.
	 * If AS didn't exist before, it can't be already in the array:*/
	as_already_in_array = as && newly_assigned_as_in_array(newly_assigned_as, *nas_idx, as);
	if (!as_already_in_array && *nas_idx >= max_nas_idx) {
		LOGPASP(asp, DLSS7, LOGL_ERROR, "RKM: not enough room for newly assigned AS (max %u AS)\n",
			max_nas_idx+1);
		msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_INSUFF_RESRC, 0);
		return -1;
	}

	if (as) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: Found existing AS for RCTX %u\n", rctx);

		/* In IPSP we expect in fact to have the local PC configured in routing-key. */
		if (asp->cfg.role == OSMO_SS7_ASP_ROLE_SG &&
		    as->cfg.routing_key.pc != dpc) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "RKM: DPC doesn't match, rejecting AS (%u != %u)\n",
				as->cfg.routing_key.pc, dpc);
			msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_INVAL_RKEY, 0);
			return -1;
		}
		if (_tmode) {  /* if the peer has specified a traffic mode at all */
			tmode = osmo_ss7_tmode_from_xua(_tmode);
			if (!as->cfg.mode_set_by_peer && !as->cfg.mode_set_by_vty) {
				as->cfg.mode = tmode;
				LOGPAS(as, DLSS7, LOGL_INFO,
					"RKM: Traffic mode set dynamically by peer to %s\n",
					osmo_ss7_as_traffic_mode_name(as->cfg.mode));
			/* verify if existing AS has same traffic-mode as new request (if any) */
			} else if (!osmo_ss7_as_tmode_compatible_xua(as, _tmode)) {
				LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: Non-matching Traffic Mode %s\n",
					osmo_ss7_as_traffic_mode_name(tmode));
				msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_UNSUPP_TRAF_MODE, 0);
				return -1;
			}
			as->cfg.mode_set_by_peer = true;
		}
	} else {
		/* Create an AS for this routing key */
		snprintf(namebuf, sizeof(namebuf), "as-rkm-%u", rctx);
		as = osmo_ss7_as_find_or_create(asp->inst, namebuf, OSMO_SS7_ASP_PROT_M3UA);
		if (!as) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "RKM: Cannot create AS %s\n", namebuf);
			msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_INSUFF_RESRC, 0);
			return -1;
		}

		as->cfg.description = talloc_strdup(as, "Auto-generated by RKM");
		as->rkm_dyn_allocated = true;
		if (!as->cfg.mode_set_by_vty && _tmode) {
			as->cfg.mode = osmo_ss7_tmode_from_xua(_tmode);
			as->cfg.mode_set_by_peer = true;
		}
		/* fill routing key */
		as->cfg.routing_key.pc = dpc;
		as->cfg.routing_key.context = rctx;

		/* add dynamic route for that routing key */
		rt = ss7_route_create(as->inst->rtable_system, dpc, 0xFFFFFF, true, namebuf);
		if (!rt) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "RKM: Cannot insert route for DPC %s / as %s\n",
				osmo_ss7_pointcode_print(asp->inst, dpc), namebuf);
			osmo_ss7_as_destroy(as);
			msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_CANT_SUPP_UNQ_RT, 0);
			return -1;
		}
	}

	/* Make sure ASP is associated to AS:
	 * - If AS was just created
	 * - If we just learned through RKM that this ASP is serving this AS (rctx)
	 */
	if (ss7_as_add_asp(as, asp) < 0) {
		LOGPASP(asp, DLSS7, LOGL_ERROR, "RKM: Cannot associate ASP to AS %s\n", as->cfg.name);
		msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_ERR_INSUFF_RESRC, 0);
		return -1;
	}

	msgb_append_reg_res(resp, rk_id, M3UA_RKM_REG_SUCCESS, rctx);
	/* append to list of newly assigned as */
	if (!as_already_in_array)
		newly_assigned_as[(*nas_idx)++] = as;
	return 0;
}

/* SG: receive a registration request from ASP */
static int m3ua_rx_rkm_reg_req(struct osmo_ss7_asp *asp, struct xua_msg *xua)
{
	struct xua_msg_part *part;
	struct msgb *resp = m3ua_msgb_alloc(__func__);
	struct osmo_ss7_as *newly_assigned_as[MAX_NEW_AS];
	unsigned int i, num_newly_assigned_as = 0;

	memset(newly_assigned_as, 0, sizeof(newly_assigned_as));

	/* iterate over all routing key IEs in message */
	llist_for_each_entry(part, &xua->headers, entry) {
		struct xua_msg *inner;

		if (part->tag != M3UA_IEI_ROUT_KEY)
			continue;

		inner = xua_from_nested(part);
		if (!inner) {
			LOGPASP(asp, DLSS7, LOGL_NOTICE, "RKM: Unable to parse "
				"nested IE for Routing Key\n");
			continue;
		}
		/* handle single registration and append result to
		 * 'resp' */
		handle_rkey_reg(asp, inner, resp, newly_assigned_as,
				ARRAY_SIZE(newly_assigned_as), &num_newly_assigned_as);

		xua_msg_free(inner);
	}
	/* now first send the RKM REG Response */
	msgb_push_m3ua_hdr(resp, M3UA_MSGC_RKM, M3UA_RKM_REG_RSP);
	osmo_ss7_asp_send(asp, resp);

	/* and *after* the RKM REG Response inform the newly assigned
	 * ASs about the fact that there's an INACTIVE ASP for them,
	 * which will cause them to send NOTIFY to the client */
	for (i = 0; i < num_newly_assigned_as; i++) {
		struct osmo_ss7_as *as = newly_assigned_as[i];
		if (!as)
			continue;
		/* Notify AS that it has an INACTIVE ASP */
		/* RFC4666 4.3.4.5: "When an ASP moves from ASP-DOWN to ASP-INACTIVE within a
		* particular AS, a Notify message SHOULD be sent, by the ASP-UP receptor,
		* after sending the ASP-UP-ACK, in order to inform the ASP of the current AS
		* state."
		*/
		struct xua_as_event_asp_inactive_ind_pars pars = {
			.asp = asp,
			.asp_requires_notify = true,
		};
		osmo_fsm_inst_dispatch(as->fi, XUA_ASPAS_ASP_INACTIVE_IND, &pars);
	}

	return 0;
}

/* SG: handle a single routing key de-registration IE */
static int handle_rkey_dereg(struct osmo_ss7_asp *asp, uint32_t rctx,
			     struct msgb *resp)
{
	struct osmo_ss7_instance *inst = asp->inst;
	struct osmo_ss7_as *as;
	struct osmo_ss7_route *rt;

	as = osmo_ss7_as_find_by_rctx(inst, rctx);
	if (!as) {
		msgb_append_dereg_res(resp, M3UA_RKM_DEREG_ERR_INVAL_RCTX, 0);
		return -1;
	}

	/* Reject if not dynamically allocated (OS#4239) */
	if (!as->rkm_dyn_allocated) {
		msgb_append_dereg_res(resp, M3UA_RKM_DEREG_ERR_NOT_REGD, 0);
		return -1;
	}

	/* Reject if ASP is not even part of AS */
	if (!osmo_ss7_as_has_asp(as, asp)) {
		msgb_append_dereg_res(resp, M3UA_RKM_DEREG_ERR_INVAL_RCTX, 0);
		return -1;
	}

	/* Reject if ASP is still active */
	if (asp->fi->state == XUA_ASP_S_ACTIVE) {
		msgb_append_dereg_res(resp, M3UA_RKM_DEREG_ERR_ASP_ACTIVE, 0);
		return -1;
	}

	rt = ss7_route_table_find_route_by_dpc_mask(inst->rtable_system,
						    as->cfg.routing_key.pc, 0xffffff,
						    true);
	if (!rt) {
		msgb_append_dereg_res(resp, M3UA_RKM_DEREG_ERR_UNKNOWN, 0);
		return -1;
	}

	LOGPASP(asp, DLSS7, LOGL_INFO, "RKM: De-Registering rctx %u for DPC %s\n",
		rctx, osmo_ss7_pointcode_print(inst, as->cfg.routing_key.pc));

	/* Release the associated route */
	ss7_route_destroy(rt);
	/* Dissassociate the ASP from the dynamically allocated AS.
	 * The AS may be freed if it is serving no more ASPs. */
	ss7_as_del_asp(as, asp);

	/* report success */
	msgb_append_dereg_res(resp, M3UA_RKM_DEREG_SUCCESS, rctx);

	return 0;
}

/* SG: receive a De-Registration request from ASP */
static int m3ua_rx_rkm_dereg_req(struct osmo_ss7_asp *asp, struct xua_msg *xua)
{
	struct xua_msg_part *part = xua_msg_find_tag(xua, M3UA_IEI_ROUTE_CTX);
	struct msgb *resp = m3ua_msgb_alloc(__func__);
	uint32_t *rctx;

	if (!part)
		return -1;

	for (rctx = (uint32_t *)part->dat; (uint8_t *)rctx < part->dat + part->len; rctx++)
		handle_rkey_dereg(asp, ntohl(*rctx), resp);

	msgb_push_m3ua_hdr(resp, M3UA_MSGC_RKM, M3UA_RKM_DEREG_RSP);
	osmo_ss7_asp_send(asp, resp);

	return 0;
}

/* ASP: handle a single registration response IE (nested IEs in 'inner') */
static int handle_rkey_reg_resp(struct osmo_ss7_asp *asp, struct xua_msg *inner)
{
	struct osmo_xlm_prim *oxp;

	if (!xua_msg_find_tag(inner, M3UA_IEI_LOC_RKEY_ID) ||
	    !xua_msg_find_tag(inner, M3UA_IEI_REG_STATUS) ||
	    !xua_msg_find_tag(inner, M3UA_IEI_ROUTE_CTX)) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "Missing Inner IE in REG RESP\n");
		/* FIXME: ERROR to peer */
		return -1;
	}

	oxp = xua_xlm_prim_alloc(OSMO_XLM_PRIM_M_RK_REG, PRIM_OP_CONFIRM);
	if (!oxp)
		return -1;

	oxp->u.rk_reg.key.l_rk_id = xua_msg_get_u32(inner, M3UA_IEI_LOC_RKEY_ID);
	oxp->u.rk_reg.key.context = xua_msg_get_u32(inner, M3UA_IEI_ROUTE_CTX);
	oxp->u.rk_reg.status = xua_msg_get_u32(inner, M3UA_IEI_REG_STATUS);

	LOGPASP(asp, DLSS7, LOGL_INFO, "Received RKM REG RES rctx=%u status=%s\n",
		oxp->u.rk_reg.key.context,
		get_value_string(m3ua_rkm_reg_status_vals, oxp->u.rk_reg.status));

	/* Send primitive to LM */
	xua_asp_send_xlm_prim(asp, oxp);

	return 0;
}

/* ASP: receive a registration response (ASP role) */
static int m3ua_rx_rkm_reg_rsp(struct osmo_ss7_asp *asp, struct xua_msg *xua)
{
	struct xua_msg_part *part;
	struct xua_msg *inner = NULL;

	llist_for_each_entry(part, &xua->headers, entry) {
		/* skip other IEs and/or short REG_RES IEs */
		if (part->tag != M3UA_IEI_REG_RESULT || part->len < 24)
			continue;

		/* we leave the above loop at the first valid
		 * registration result (we only support one AS per ASP
		 * for now) */
		inner = xua_from_nested(part);
		if (!inner)
			continue;

		handle_rkey_reg_resp(asp, inner);
		xua_msg_free(inner);
	}
	return 0;
}

/* ASP: handle a single De-Registration response IE (nested IEs in 'inner' */
static int handle_rkey_dereg_resp(struct osmo_ss7_asp *asp, struct xua_msg *inner)
{
	struct osmo_xlm_prim *oxp;

	if (!xua_msg_find_tag(inner, M3UA_IEI_DEREG_STATUS) ||
	    !xua_msg_find_tag(inner, M3UA_IEI_ROUTE_CTX)) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "Missing Inner IE in DEREG RESP\n");
		/* FIXME: ERROR to peer */
		return -1;
	}

	oxp = xua_xlm_prim_alloc(OSMO_XLM_PRIM_M_RK_DEREG, PRIM_OP_CONFIRM);
	if (!oxp)
		return -1;

	oxp->u.rk_dereg.route_ctx = xua_msg_get_u32(inner, M3UA_IEI_ROUTE_CTX);
	oxp->u.rk_dereg.status = xua_msg_get_u32(inner, M3UA_IEI_DEREG_STATUS);

	LOGPASP(asp, DLSS7, LOGL_INFO, "Received RKM DEREG RES rctx=%u status=%s\n",
		oxp->u.rk_reg.key.context,
		get_value_string(m3ua_rkm_dereg_status_vals, oxp->u.rk_dereg.status));

	/* Send primitive to LM */
	xua_asp_send_xlm_prim(asp, oxp);

	return 0;
}

/* ASP: receive a De-Registration response */
static int m3ua_rx_rkm_dereg_rsp(struct osmo_ss7_asp *asp, struct xua_msg *xua)
{
	struct xua_msg_part *part;
	struct xua_msg *inner = NULL;

	llist_for_each_entry(part, &xua->headers, entry) {
		/* skip other IEs and/or short REG_RES IEs */
		if (part->tag != M3UA_IEI_DEREG_RESULT || part->len < 16)
			continue;

		/* we leave the above loop at the first valid
		 * registration result (we only support one AS per ASP
		 * for now) */
		inner = xua_from_nested(part);
		if (!inner)
			continue;

		handle_rkey_dereg_resp(asp, inner);
		xua_msg_free(inner);
	}
	return 0;
}

#define ENSURE_ASP_OR_IPSP(asp, xua)								\
	do {											\
		if (asp->cfg.role != OSMO_SS7_ASP_ROLE_ASP &&					\
		    asp->cfg.role != OSMO_SS7_ASP_ROLE_IPSP) {					\
			LOGPASP(asp, DLSS7, LOGL_ERROR, "Rx %s not permitted in role %s\n",	\
				 get_value_string(m3ua_rkm_msgt_names, xua->hdr.msg_type),	\
				 get_value_string(osmo_ss7_asp_role_names, asp->cfg.role));	\
			xua_msg_free(xua);							\
			return -1;								\
		}										\
	} while (0)

#define ENSURE_SG_OR_IPSP(asp, xua)								\
	do {											\
		if (asp->cfg.role != OSMO_SS7_ASP_ROLE_SG &&					\
		    asp->cfg.role != OSMO_SS7_ASP_ROLE_IPSP) {					\
			LOGPASP(asp, DLSS7, LOGL_ERROR, "Rx %s not permitted in role %s\n",	\
				 get_value_string(m3ua_rkm_msgt_names, xua->hdr.msg_type),	\
				 get_value_string(osmo_ss7_asp_role_names, asp->cfg.role));	\
			xua_msg_free(xua);							\
			return -1;								\
		}										\
	} while (0)

/* process an incoming RKM message in xua format
 * This function takes ownership of xua msg passed to it. */
int m3ua_rx_rkm(struct osmo_ss7_asp *asp, struct xua_msg *xua)
{
	int rc;

	switch (xua->hdr.msg_type) {
	/* SG Side */
	case M3UA_RKM_REG_REQ:
		ENSURE_SG_OR_IPSP(asp, xua);
		rc = m3ua_rx_rkm_reg_req(asp, xua);
		break;
	case M3UA_RKM_DEREG_REQ:
		ENSURE_SG_OR_IPSP(asp, xua);
		rc = m3ua_rx_rkm_dereg_req(asp, xua);
		break;
	/* ASP Side */
	case M3UA_RKM_REG_RSP:
		ENSURE_ASP_OR_IPSP(asp, xua);
		rc = m3ua_rx_rkm_reg_rsp(asp, xua);
		break;
	case M3UA_RKM_DEREG_RSP:
		ENSURE_ASP_OR_IPSP(asp, xua);
		rc = m3ua_rx_rkm_dereg_rsp(asp, xua);
		break;
	default:
		LOGPASP(asp, DLSS7, LOGL_ERROR, "Received unknown RKM msg_type %u\n",
			xua->hdr.msg_type);
		rc = -1;
		break;
	}

	xua_msg_free(xua);
	return rc;
}
