/* SCCP Connection Oriented (SCOC) according to ITU-T Q.713/Q.714 */

/* (C) 2015-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/>.
 *
 */

/* This code is a bit of a hybrid between the ITU-T Q.71x specifications
 * for SCCP (particularly its connection-oriented part), and the IETF
 * RFC 3868 (SUA).  The idea here is to have one shared code base of the
 * state machines for SCCP Connection Oriented, and use those both from
 * SCCP and SUA.
 *
 * To do so, all SCCP messages are translated to SUA messages in the
 * input side, and all generated SUA messages are translated to SCCP on
 * the output side.
 *
 * The Choice of going for SUA messages as the "native" format was based
 * on their easier parseability, and the fact that there are features in
 * SUA which classic SCCP cannot handle (like IP addresses in GT).
 * However, all SCCP features can be expressed in SUA.
 *
 * The code only supports Class 2.  No support for Class 3 is intended,
 * but patches are of course alwys welcome.
 *
 * Missing other features:
 *  * Segmentation/Reassembly support
 *  * T(guard) after (re)start
 *  * freezing of local references
 *  * parsing/encoding of IPv4/IPv6 addresses
 *  * use of multiple Routing Contexts in SUA case
 */

#include <string.h>

#include <osmocom/core/utils.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/timer.h>
#include <osmocom/core/fsm.h>

#include <osmocom/sigtran/sccp_sap.h>
#include <osmocom/sigtran/protocol/sua.h>
#include <osmocom/sccp/sccp_types.h>

#include "xua_internal.h"
#include "sccp_internal.h"

#define S(x)	(1 << (x))
#define SCU_MSGB_SIZE	1024

/***********************************************************************
 * SCCP connection table
 ***********************************************************************/

/* a logical connection within the SCCP instance */
struct sccp_connection {
	/* part of osmo_sccp_instance.list */
	struct llist_head list;
	/* which instance are we part of? */
	struct osmo_sccp_instance *inst;
	/* which user owns us? */
	struct osmo_sccp_user *user;

	/* remote point code */
	uint32_t remote_pc;

	/* local/remote addresses and identiies */
	struct osmo_sccp_addr calling_addr;
	struct osmo_sccp_addr called_addr;
	/* SCCP connection identifier. Only relevant across the SCCP User SAP,
	 * i.e. between the local application using the SCCP stack provided by
	 * libosmo-sccp.  Never transmitted over the wire! */
	uint32_t conn_id;
	/* SCCP Remote Connection Reference.  Allocated by the remote
	 * SCCP stack to uniquely identify a SCCP connection on its end.
	 * We don't interpret it, but simply cache it here so we can use
	 * it whever sending data to the peer. Only relevant over the
	 * wire, not to be used across the SCCP user SAP */
	uint32_t remote_ref;

	uint32_t importance;
	uint32_t sccp_class;
	uint32_t release_cause; /* WAIT_CONN_CONF */

	/* incoming (true) or outgoing (false) */
	bool incoming;

	/* Osmo FSM Instance of sccp_scoc_fsm */
	struct osmo_fsm_inst *fi;

	/* Connect timer */
	struct osmo_timer_list t_conn;

	/* inactivity timers */
	struct osmo_timer_list t_ias;
	struct osmo_timer_list t_iar;

	/* release timers */
	struct osmo_timer_list t_rel;
	struct osmo_timer_list t_int;
	struct osmo_timer_list t_rep_rel;
};

/***********************************************************************
 * various helper functions
 ***********************************************************************/

enum sccp_connection_state {
	S_IDLE,
	S_CONN_PEND_IN,
	S_CONN_PEND_OUT,
	S_ACTIVE,
	S_DISCONN_PEND,
	S_RESET_IN,
	S_RESET_OUT,
	S_BOTHWAY_RESET,
	S_WAIT_CONN_CONF,
};

/* Events that this FSM can process */
enum sccp_scoc_event {
	/* Primitives from SCCP-User */
	SCOC_E_SCU_N_CONN_REQ,
	SCOC_E_SCU_N_CONN_RESP,
	SCOC_E_SCU_N_DISC_REQ,
	SCOC_E_SCU_N_DATA_REQ,
	SCOC_E_SCU_N_EXP_DATA_REQ,

	/* Events from RCOC (Routing for Connection Oriented) */
	SCOC_E_RCOC_CONN_IND,
	SCOC_E_RCOC_ROUT_FAIL_IND,
	SCOC_E_RCOC_RLSD_IND,
	SCOC_E_RCOC_REL_COMPL_IND,
	SCOC_E_RCOC_CREF_IND,
	SCOC_E_RCOC_CC_IND,
	SCOC_E_RCOC_DT1_IND,
	SCOC_E_RCOC_DT2_IND,
	SCOC_E_RCOC_IT_IND,
	SCOC_E_RCOC_OTHER_NPDU,
	SCOC_E_RCOC_ERROR_IND,

	/* Timer Events */
	SCOC_E_T_IAR_EXP,
	SCOC_E_T_IAS_EXP,

	SCOC_E_CONN_TMR_EXP,

	SCOC_E_T_REL_EXP,
	SCOC_E_T_INT_EXP,
	SCOC_E_T_REP_REL_EXP,
};

static const struct value_string scoc_event_names[] = {
	/* Primitives from SCCP-User */
	{ SCOC_E_SCU_N_CONN_REQ,	"N-CONNECT.req" },
	{ SCOC_E_SCU_N_CONN_RESP,	"N-CONNECT.resp" },
	{ SCOC_E_SCU_N_DISC_REQ,	"N-DISCONNECT.req" },
	{ SCOC_E_SCU_N_DATA_REQ,	"N-DATA.req" },
	{ SCOC_E_SCU_N_EXP_DATA_REQ,	"N-EXPEDITED_DATA.req" },

	/* Events from RCOC (Routing for Connection Oriented) */
	{ SCOC_E_RCOC_CONN_IND,		"RCOC-CONNECT.ind" },
	{ SCOC_E_RCOC_ROUT_FAIL_IND,	"RCOC-ROUT_FAIL.ind" },
	{ SCOC_E_RCOC_RLSD_IND,		"RCOC-RELEASED.ind" },
	{ SCOC_E_RCOC_REL_COMPL_IND,	"RCOC-RELEASE_COMPLETE.ind" },
	{ SCOC_E_RCOC_CREF_IND,		"RCOC-CONNECT_REFUSED.ind" },
	{ SCOC_E_RCOC_CC_IND,		"RCOC-CONNECT_CONFIRM.ind" },
	{ SCOC_E_RCOC_DT1_IND,		"RCOC-DT1.ind" },
	{ SCOC_E_RCOC_DT2_IND,		"RCOC-DT2.ind" },
	{ SCOC_E_RCOC_IT_IND,		"RCOC-IT.ind" },
	{ SCOC_E_RCOC_OTHER_NPDU,	"RCOC-OTHER_NPDU.ind" },
	{ SCOC_E_RCOC_ERROR_IND,	"RCOC-ERROR.ind" },

	{ SCOC_E_T_IAR_EXP,		"T(iar)_expired" },
	{ SCOC_E_T_IAS_EXP,		"T(ias)_expired" },
	{ SCOC_E_CONN_TMR_EXP,		"T(conn)_expired" },
	{ SCOC_E_T_REL_EXP,		"T(rel)_expired" },
	{ SCOC_E_T_INT_EXP,		"T(int)_expired" },
	{ SCOC_E_T_REP_REL_EXP,		"T(rep_rel)_expired" },

	{ 0, NULL }
};

/* how to map a SCCP CO message to an event */
static const struct xua_msg_event_map sua_scoc_event_map[] = {
	{ SUA_MSGC_CO, SUA_CO_CORE, SCOC_E_RCOC_CONN_IND },
	{ SUA_MSGC_CO, SUA_CO_RELRE, SCOC_E_RCOC_RLSD_IND },
	{ SUA_MSGC_CO, SUA_CO_RELCO, SCOC_E_RCOC_REL_COMPL_IND },
	{ SUA_MSGC_CO, SUA_CO_COREF, SCOC_E_RCOC_CREF_IND },
	{ SUA_MSGC_CO, SUA_CO_COAK, SCOC_E_RCOC_CC_IND },
	{ SUA_MSGC_CO, SUA_CO_CODT, SCOC_E_RCOC_DT1_IND },
	{ SUA_MSGC_CO, SUA_CO_COIT, SCOC_E_RCOC_IT_IND },
	{ SUA_MSGC_CO, SUA_CO_COERR, SCOC_E_RCOC_ERROR_IND },
};


/* map from SCU-primitives to SCOC FSM events */
static const struct osmo_prim_event_map scu_scoc_event_map[] = {
	{ SCCP_SAP_USER, OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_REQUEST,
		SCOC_E_SCU_N_CONN_REQ },
	{ SCCP_SAP_USER, OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_RESPONSE,
		SCOC_E_SCU_N_CONN_RESP },
	{ SCCP_SAP_USER, OSMO_SCU_PRIM_N_DATA, PRIM_OP_REQUEST,
		SCOC_E_SCU_N_DATA_REQ },
	{ SCCP_SAP_USER, OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_REQUEST,
		SCOC_E_SCU_N_DISC_REQ },
	{ SCCP_SAP_USER, OSMO_SCU_PRIM_N_EXPEDITED_DATA, PRIM_OP_REQUEST,
		SCOC_E_SCU_N_EXP_DATA_REQ },
	{ 0, 0, 0, OSMO_NO_EVENT }
};

/***********************************************************************
 * Timer Handling
 ***********************************************************************/

/* Appendix C.4 of ITU-T Q.714 */
const struct value_string osmo_sccp_timer_names[] = {
	{ OSMO_SCCP_TIMER_CONN_EST, "conn_est" },
	{ OSMO_SCCP_TIMER_IAS, "ias" },
	{ OSMO_SCCP_TIMER_IAR, "iar" },
	{ OSMO_SCCP_TIMER_REL, "rel" },
	{ OSMO_SCCP_TIMER_REPEAT_REL, "repeat_rel" },
	{ OSMO_SCCP_TIMER_INT, "int" },
	{ OSMO_SCCP_TIMER_GUARD, "guard" },
	{ OSMO_SCCP_TIMER_RESET, "reset" },
	{ OSMO_SCCP_TIMER_REASSEMBLY, "reassembly" },
	{}
};

/* Mostly pasted from Appendix C.4 of ITU-T Q.714 (05/2001) -- some of their descriptions are quite
 * unintelligible out of context, for which we have our own description here. */
const struct value_string osmo_sccp_timer_descriptions[] = {
	{ OSMO_SCCP_TIMER_CONN_EST,
	  "Waiting for connection confirm message, 1 to 2 minutes" },
	{ OSMO_SCCP_TIMER_IAS,
	  "Send keep-alive: on an idle connection, delay before sending an Idle Timer message,"
	  " 5 to 10 minutes" },
	{ OSMO_SCCP_TIMER_IAR,
	  "Receive keep-alive: on an idle connection, delay until considering a connection as stale,"
	  " 11 to 21 minutes" },
	{ OSMO_SCCP_TIMER_REL,
	  "Waiting for release complete message, 10 to 20 seconds" },
	{ OSMO_SCCP_TIMER_REPEAT_REL,
	  "Waiting for release complete message; or to repeat sending released message after the initial"
	  " expiry, 10 to 20 seconds" },
	{ OSMO_SCCP_TIMER_INT,
	  "Waiting for release complete message; or to release connection resources, freeze the LRN and"
	  " alert a maintenance function after the initial expiry, extending to 1 minute" },
	{ OSMO_SCCP_TIMER_GUARD,
	  "Waiting to resume normal procedure for temporary connection sections during the restart"
	  " procedure, 23 to 25 minutes" },
	{ OSMO_SCCP_TIMER_RESET,
	  "Waiting to release temporary connection section or alert maintenance function after reset"
	  " request message is sent, 10 to 20 seconds" },
	{ OSMO_SCCP_TIMER_REASSEMBLY,
	  "Waiting to receive all the segments of the remaining segments, single segmented message after"
	  " receiving the first segment, 10 to 20 seconds" },
	{}
};

/* Appendix C.4 of ITU-T Q.714 */
const struct osmo_sccp_timer_val osmo_sccp_timer_defaults[] = {
	[OSMO_SCCP_TIMER_CONN_EST]	= { .s =  1 * 60, },
	[OSMO_SCCP_TIMER_IAS]		= { .s =  7 * 60, }, /* RFC 3868 Ch. 8. */
	[OSMO_SCCP_TIMER_IAR]		= { .s = 15 * 60, }, /* RFC 3868 Ch. 8. */
	[OSMO_SCCP_TIMER_REL]		= { .s =      10, },
	[OSMO_SCCP_TIMER_REPEAT_REL]	= { .s =      10, },
	[OSMO_SCCP_TIMER_INT]		= { .s =  1 * 60, },
	[OSMO_SCCP_TIMER_GUARD]		= { .s = 23 * 60, },
	[OSMO_SCCP_TIMER_RESET]		= { .s =      10, },
	[OSMO_SCCP_TIMER_REASSEMBLY]	= { .s =      10, },
};

osmo_static_assert(ARRAY_SIZE(osmo_sccp_timer_defaults) == OSMO_SCCP_TIMERS_COUNT,
		   assert_osmo_sccp_timers_count);

/* Look up an SCCP timer value as configured in the osmo_ss7_instance.
 * If no user defined value is set, return the global default from osmo_sccp_timer_defaults instead.
 * However, if default_if_unset is passed false, return NULL in case there is no user defined setting, or
 * in case it is identical to the global default. */
const struct osmo_sccp_timer_val *osmo_sccp_timer_get(const struct osmo_sccp_instance *inst,
						      enum osmo_sccp_timer timer,
						      bool default_if_unset)
{
	const struct osmo_sccp_timer_val *val;
	const struct osmo_sccp_timer_val *def;

	OSMO_ASSERT(timer >= 0
		    && timer < ARRAY_SIZE(inst->timers)
		    && timer < ARRAY_SIZE(osmo_sccp_timer_defaults));

	val = &inst->timers[timer];
	def = &osmo_sccp_timer_defaults[timer];

	/* Assert that all timer definitions have a sane global default */
	OSMO_ASSERT(def->s || def->us);

	if (val->s || val->us) {
		if (!default_if_unset && val->s == def->s && val->us == def->us)
			return NULL;
		return val;
	}

	if (!default_if_unset)
		return NULL;

	/* If unset, use the global default. */
	return def;
}

static void sccp_timer_schedule(const struct sccp_connection *conn,
				struct osmo_timer_list *timer,
				enum osmo_sccp_timer timer_name)
{
	const struct osmo_sccp_timer_val *val = osmo_sccp_timer_get(conn->inst, timer_name, true);
	osmo_timer_schedule(timer, val->s, val->us);
}

/* T(ias) has expired, send a COIT message to the peer */
static void tx_inact_tmr_cb(void *data)
{
	struct sccp_connection *conn = data;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_T_IAS_EXP, NULL);
}

/* T(iar) has expired, notify the FSM about it */
static void rx_inact_tmr_cb(void *data)
{
	struct sccp_connection *conn = data;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_T_IAR_EXP, NULL);
}

/* T(rel) has expired, notify the FSM about it */
static void rel_tmr_cb(void *data)
{
	struct sccp_connection *conn = data;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_T_REL_EXP, NULL);
}

/* T(int) has expired, notify the FSM about it */
static void int_tmr_cb(void *data)
{
	struct sccp_connection *conn = data;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_T_INT_EXP, NULL);
}

/* T(repeat_rel) has expired, notify the FSM about it */
static void rep_rel_tmr_cb(void *data)
{
	struct sccp_connection *conn = data;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_T_REP_REL_EXP, NULL);
}

/* T(conn) has expired, notify the FSM about it */
static void conn_tmr_cb(void *data)
{
	struct sccp_connection *conn = data;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_CONN_TMR_EXP, NULL);
}

/* Re-start the Tx inactivity timer */
static void conn_restart_tx_inact_timer(struct sccp_connection *conn)
{
	sccp_timer_schedule(conn, &conn->t_ias, OSMO_SCCP_TIMER_IAS);
}

/* Re-start the Rx inactivity timer */
static void conn_restart_rx_inact_timer(struct sccp_connection *conn)
{
	sccp_timer_schedule(conn, &conn->t_iar, OSMO_SCCP_TIMER_IAR);
}

/* Re-start both Rx and Tx inactivity timers */
static void conn_start_inact_timers(struct sccp_connection *conn)
{
	conn_restart_tx_inact_timer(conn);
	conn_restart_rx_inact_timer(conn);
}

/* Stop both Rx and Tx inactivity timers */
static void conn_stop_inact_timers(struct sccp_connection *conn)
{
	osmo_timer_del(&conn->t_ias);
	osmo_timer_del(&conn->t_iar);
}

/* Start release timer T(rel) */
static void conn_start_rel_timer(struct sccp_connection *conn)
{
	sccp_timer_schedule(conn, &conn->t_rel, OSMO_SCCP_TIMER_REL);
}

/* Start repeat release timer T(rep_rel) */
static void conn_start_rep_rel_timer(struct sccp_connection *conn)
{
	sccp_timer_schedule(conn, &conn->t_rep_rel, OSMO_SCCP_TIMER_REPEAT_REL);
}

/* Start interval timer T(int) */
static void conn_start_int_timer(struct sccp_connection *conn)
{
	sccp_timer_schedule(conn, &conn->t_int, OSMO_SCCP_TIMER_INT);
}

/* Stop all release related timers: T(rel), T(int) and T(rep_rel) */
static void conn_stop_release_timers(struct sccp_connection *conn)
{
	osmo_timer_del(&conn->t_rel);
	osmo_timer_del(&conn->t_int);
	osmo_timer_del(&conn->t_rep_rel);
}

/* Start connect timer T(conn) */
static void conn_start_connect_timer(struct sccp_connection *conn)
{
	sccp_timer_schedule(conn, &conn->t_conn, OSMO_SCCP_TIMER_CONN_EST);
}

/* Stop connect timer T(conn) */
static void conn_stop_connect_timer(struct sccp_connection *conn)
{
	osmo_timer_del(&conn->t_conn);
}


/***********************************************************************
 * SUA Instance and Connection handling
 ***********************************************************************/

static void conn_destroy(struct sccp_connection *conn);

static struct sccp_connection *conn_find_by_id(struct osmo_sccp_instance *inst, uint32_t id)
{
	struct sccp_connection *conn;

	llist_for_each_entry(conn, &inst->connections, list) {
		if (conn->conn_id == id)
			return conn;
	}
	return NULL;
}

#define INIT_TIMER(x, fn, priv)		do { (x)->cb = fn; (x)->data = priv; } while (0)

/* allocate + init a SCCP Connection with given ID */
static struct sccp_connection *conn_create_id(struct osmo_sccp_user *user, uint32_t conn_id)
{
	struct sccp_connection *conn = talloc_zero(user->inst, struct sccp_connection);
	char name[16];

	conn->conn_id = conn_id;
	conn->inst = user->inst;
	conn->user = user;

	llist_add_tail(&conn->list, &user->inst->connections);

	INIT_TIMER(&conn->t_conn, conn_tmr_cb, conn);
	INIT_TIMER(&conn->t_ias, tx_inact_tmr_cb, conn);
	INIT_TIMER(&conn->t_iar, rx_inact_tmr_cb, conn);
	INIT_TIMER(&conn->t_rel, rel_tmr_cb, conn);
	INIT_TIMER(&conn->t_int, int_tmr_cb, conn);
	INIT_TIMER(&conn->t_rep_rel, rep_rel_tmr_cb, conn);

	/* this might change at runtime, as it is not a constant :/ */
	sccp_scoc_fsm.log_subsys = DLSCCP;

	/* we simply use the connection ID as FSM instance name */
	snprintf(name, sizeof(name), "%u", conn->conn_id);
	conn->fi = osmo_fsm_inst_alloc(&sccp_scoc_fsm, conn, conn,
					LOGL_DEBUG, name);
	if (!conn->fi) {
		llist_del(&conn->list);
		talloc_free(conn);
		return NULL;
	}

	return conn;
}

/* Search for next free connection ID and allocate conn */
static struct sccp_connection *conn_create(struct osmo_sccp_user *user)
{
	uint32_t conn_id;

	do {
		/* modulo 2^24 as we currently use the connection ID also as local
		 * reference, and that is limited to 24 bits */
		user->inst->next_id = (user->inst->next_id + 1) %  (1 << 24);
		conn_id = user->inst->next_id;
	} while (conn_find_by_id(user->inst, conn_id));

	return conn_create_id(user, conn_id);
}

/* destroy a SCCP connection state, releasing all timers, terminating
 * FSM and releasing associated memory */
static void conn_destroy(struct sccp_connection *conn)
{
	conn_stop_connect_timer(conn);
	conn_stop_inact_timers(conn);
	conn_stop_release_timers(conn);
	llist_del(&conn->list);

	osmo_fsm_inst_term(conn->fi, OSMO_FSM_TERM_REQUEST, NULL);

	talloc_free(conn);
}

/* allocate a message buffer for an SCCP User Primitive */
static struct msgb *scu_msgb_alloc(void)
{
	return msgb_alloc(SCU_MSGB_SIZE, "SCCP User Primitive");
}

/* generate a RELRE (release request) xua_msg for given conn */
static struct xua_msg *xua_gen_relre(struct sccp_connection *conn,
				     uint32_t cause,
				     struct osmo_scu_prim *prim)
{
	struct xua_msg *xua = xua_msg_alloc();

	if (!xua)
		return NULL;

	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_RELRE);
	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
	xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
	xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
	xua_msg_add_u32(xua, SUA_IEI_CAUSE, SUA_CAUSE_T_RELEASE | cause);
	/* optional: importance */
	if (prim && msgb_l2(prim->oph.msg))
		xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
				 msgb_l2(prim->oph.msg));

	return xua;
}

/* generate xua_msg, encode it and send it to SCRC */
static int xua_gen_relre_and_send(struct sccp_connection *conn, uint32_t cause,
				  struct osmo_scu_prim *prim)
{
	struct xua_msg *xua;

	xua = xua_gen_relre(conn, cause, prim);
	if (!xua)
		return -1;

	/* amend this with point code information; The SUA RELRE
	 * includes neither called nor calling party address! */
	xua->mtp.dpc = conn->remote_pc;
	sccp_scrc_rx_scoc_conn_msg(conn->inst, xua);
	xua_msg_free(xua);
	return 0;
}

/* generate a 'struct xua_msg' of requested type from connection +
 * primitive data */
static struct xua_msg *xua_gen_msg_co(struct sccp_connection *conn, uint32_t event,
				      struct osmo_scu_prim *prim, int msg_type)
{
	struct xua_msg *xua = xua_msg_alloc();

	if (!xua)
		return NULL;

	switch (msg_type) {
	case SUA_CO_CORE: /* Connect Request == SCCP CR */
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_CORE);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, conn->sccp_class);
		xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
		xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &conn->called_addr);
		xua_msg_add_u32(xua, SUA_IEI_SEQ_CTRL, 0); /* TODO */
		/* optional: sequence number (class 3 only) */
		if (conn->calling_addr.presence)
			xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &conn->calling_addr);
		/* optional: hop count; importance; priority; credit */
		if (prim && msgb_l2(prim->oph.msg) && msgb_l2len(prim->oph.msg))
			xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
					 msgb_l2(prim->oph.msg));
		break;
	case SUA_CO_COAK: /* Connect Acknowledge == SCCP CC */
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COAK);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, conn->sccp_class);
		xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
		xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
		xua_msg_add_u32(xua, SUA_IEI_SEQ_CTRL, 0); /* TODO */
		/* optional: sequence number (class 3 only) */
		if (conn->called_addr.presence)
			xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &conn->called_addr);
		/* optional: hop count; importance; priority */
		/* FIXME: destination address will [only] be present in
		 * case the CORE message conveys the source address
		 * parameter */
		if (conn->calling_addr.presence)
			xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &conn->calling_addr);
		if (prim && msgb_l2(prim->oph.msg) && msgb_l2len(prim->oph.msg))
			xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
					 msgb_l2(prim->oph.msg));
		break;
	case SUA_CO_RELRE: /* Release Request == SCCP REL */
		if (!prim)
			goto prim_needed;
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_RELRE);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
		xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
		xua_msg_add_u32(xua, SUA_IEI_CAUSE, SUA_CAUSE_T_RELEASE | prim->u.disconnect.cause);
		/* optional: importance */
		if (prim && msgb_l2(prim->oph.msg) && msgb_l2len(prim->oph.msg))
			xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
					 msgb_l2(prim->oph.msg));
		break;
	case SUA_CO_RELCO: /* Release Confirm == SCCP RLSD */
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_RELCO);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
		xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
		/* optional: importance */
		break;
	case SUA_CO_CODT: /* Connection Oriented Data Transfer == SCCP DT1 */
		if (!prim)
			goto prim_needed;
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_CODT);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		/* Sequence number only in expedited data */
		xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
		/* optional: priority; correlation id */
		xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
				 msgb_l2(prim->oph.msg));
		break;
	case SUA_CO_COIT: /* Connection Oriented Interval Timer == SCCP IT */
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COIT);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, conn->sccp_class);
		xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
		xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
		/* optional: sequence number; credit (both class 3 only) */
		break;
	case SUA_CO_COREF: /* Connect Refuse == SCCP CREF */
		xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COREF);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, conn->inst->route_ctx);
		xua_msg_add_u32(xua, SUA_IEI_DEST_REF, conn->remote_ref);
		//xua_msg_add_u32(xua, SUA_IEI_CAUSE, SUA_CAUSE_T_REFUSAL | prim->u.disconnect.cause);
		xua_msg_add_u32(xua, SUA_IEI_CAUSE, SUA_CAUSE_T_REFUSAL | SCCP_REFUSAL_UNEQUIPPED_USER);
		/* optional: source addr */
		if (conn->called_addr.presence)
			xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &conn->called_addr);
		/* conditional: dest addr */
		if (conn->calling_addr.presence)
			xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &conn->calling_addr);
		/* optional: importance */
		/* optional: data */
		if (prim && msgb_l2(prim->oph.msg) && msgb_l2len(prim->oph.msg))
			xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
					 msgb_l2(prim->oph.msg));
		break;
	/* FIXME */
	default:
		LOGP(DLSCCP, LOGL_ERROR, "Don't know how to encode msg_type %u\n", msg_type);
		xua_msg_free(xua);
		return NULL;
	}
	return xua;

prim_needed:
	xua_msg_free(xua);
	LOGP(DLSCCP, LOGL_ERROR, "%s must be called with valid 'prim' "
	     "pointer for msg_type=%u\n", __func__, msg_type);
	return NULL;
}

/* generate xua_msg, encode it and send it to SCRC */
static int xua_gen_encode_and_send(struct sccp_connection *conn, uint32_t event,
				   struct osmo_scu_prim *prim, int msg_type)
{
	struct xua_msg *xua;

	xua = xua_gen_msg_co(conn, event, prim, msg_type);
	if (!xua)
		return -1;

	/* amend this with point code information; Many CO msgs
	 * includes neither called nor calling party address! */
	xua->mtp.dpc = conn->remote_pc;
	sccp_scrc_rx_scoc_conn_msg(conn->inst, xua);
	xua_msg_free(xua);
	return 0;
}

/* allocate a SCU primitive to be sent to the user */
static struct osmo_scu_prim *scu_prim_alloc(unsigned int primitive, enum osmo_prim_operation operation)
{
	struct msgb *upmsg = scu_msgb_alloc();
	struct osmo_scu_prim *prim;

	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
			primitive, operation, upmsg);
	upmsg->l2h = upmsg->tail;
	return prim;
}

/* high-level function to generate a SCCP User primitive of requested
 * type based on the connection and currently processed XUA message */
static void scu_gen_encode_and_send(struct sccp_connection *conn, uint32_t event,
				    struct xua_msg *xua, unsigned int primitive,
				    enum osmo_prim_operation operation)
{
	struct osmo_scu_prim *scu_prim;
	struct osmo_scu_disconn_param *udisp;
	struct osmo_scu_connect_param *uconp;
	struct osmo_scu_data_param *udatp;
	struct xua_msg_part *data_ie;

	scu_prim = scu_prim_alloc(primitive, operation);

	switch (OSMO_PRIM_HDR(&scu_prim->oph)) {
	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION):
		udisp = &scu_prim->u.disconnect;
		udisp->conn_id = conn->conn_id;
		udisp->responding_addr = conn->called_addr;
		udisp->importance = conn->importance;
		udisp->originator = OSMO_SCCP_ORIG_UNDEFINED;
		//udisp->in_sequence_control;
		if (xua) {
			udisp->cause = xua_msg_get_u32(xua, SUA_IEI_CAUSE);
			if (xua_msg_find_tag(xua, SUA_IEI_SRC_ADDR))
				sua_addr_parse(&udisp->responding_addr, xua, SUA_IEI_SRC_ADDR);
			data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
			udisp->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
			if (data_ie) {
				struct msgb *upmsg = scu_prim->oph.msg;
				upmsg->l2h = msgb_put(upmsg, data_ie->len);
				memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
			}
		}
		break;
	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION):
		uconp = &scu_prim->u.connect;
		uconp->conn_id = conn->conn_id;
		uconp->called_addr = conn->called_addr;
		uconp->calling_addr = conn->calling_addr;
		uconp->sccp_class = conn->sccp_class;
		uconp->importance = conn->importance;
		if (xua) {
			data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
			if (data_ie) {
				struct msgb *upmsg = scu_prim->oph.msg;
				upmsg->l2h = msgb_put(upmsg, data_ie->len);
				memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
			}
		}
		break;
	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM):
		uconp = &scu_prim->u.connect;
		uconp->conn_id = conn->conn_id;
		uconp->called_addr = conn->called_addr;
		uconp->calling_addr = conn->calling_addr;
		//scu_prim->u.connect.in_sequence_control
		uconp->sccp_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS) & 3;
		uconp->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
		data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
		if (data_ie) {
			struct msgb *upmsg = scu_prim->oph.msg;
			upmsg->l2h = msgb_put(upmsg, data_ie->len);
			memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
		}
		break;
	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION):
		udatp = &scu_prim->u.data;
		udatp->conn_id = conn->conn_id;
		udatp->importance = conn->importance;
		data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
		if (data_ie) {
			struct msgb *upmsg = scu_prim->oph.msg;
			upmsg->l2h = msgb_put(upmsg, data_ie->len);
			memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
		}
		break;
	default:
		LOGPFSML(conn->fi, LOGL_ERROR, "Unsupported primitive %u:%u\n",
			 scu_prim->oph.primitive, scu_prim->oph.operation);
		talloc_free(scu_prim->oph.msg);
		return;
	}

	sccp_user_prim_up(conn->user, scu_prim);
}


/***********************************************************************
 * Actual SCCP Connection Oriented Control (SCOC) Finite Stte Machine
 ***********************************************************************/

/* Figure C.2/Q.714 (sheet 1 of 7) and C.3/Q.714 (sheet 1 of 6) */
static void scoc_fsm_idle(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct sccp_connection *conn = fi->priv;
	struct osmo_scu_prim *prim = NULL;
	struct osmo_scu_connect_param *uconp;
	struct xua_msg *xua = NULL;

	switch (event) {
	case SCOC_E_SCU_N_CONN_REQ:
		prim = data;
		uconp = &prim->u.connect;
		/* copy relevant parameters from prim to conn */
		conn->called_addr = uconp->called_addr;
		conn->calling_addr = uconp->calling_addr;
		conn->sccp_class = uconp->sccp_class;
		/* generate + send CR PDU to SCRC */
		xua_gen_encode_and_send(conn, event, prim, SUA_CO_CORE);
		/* start connection timer */
		conn_start_connect_timer(conn);
		osmo_fsm_inst_state_chg(fi, S_CONN_PEND_OUT, 0, 0);
		break;
#if 0
	case SCOC_E_SCU_N_TYPE1_REQ:
		/* ?!? */
		break;
#endif
	case SCOC_E_RCOC_RLSD_IND:
		/* send release complete to SCRC */
		xua_gen_encode_and_send(conn, event, NULL, SUA_CO_RELCO);
		break;
	case SCOC_E_RCOC_REL_COMPL_IND:
		/* do nothing */
		break;
	case SCOC_E_RCOC_OTHER_NPDU:
#if 0
		if (src_ref) {
			/* FIXME: send ERROR to SCRC */
		}
#endif
		break;
	/* destination node / incoming connection */
	/* Figure C.3 / Q.714 (sheet 1 of 6) */
	case SCOC_E_RCOC_CONN_IND:
		xua = data;
		/* copy relevant parameters from xua to conn */
		sua_addr_parse(&conn->calling_addr, xua, SUA_IEI_SRC_ADDR);
		sua_addr_parse(&conn->called_addr, xua, SUA_IEI_DEST_ADDR);
		conn->remote_ref = xua_msg_get_u32(xua, SUA_IEI_SRC_REF);
		conn->sccp_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS) & 3;
		conn->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
		/* 3.1.6.1 The originating node of the CR message
		 * (identified by the OPC in the calling party address
		 * or by default by the OPC in the MTP label, [and the
		 * MTP-SAP instance]) is associated with the incoming
		 * connection section. */
		if (conn->calling_addr.presence & OSMO_SCCP_ADDR_T_PC)
			conn->remote_pc = conn->calling_addr.pc;
		else {
			/* Hack to get the MTP label here ?!? */
			conn->remote_pc = xua->mtp.opc;
		}

		osmo_fsm_inst_state_chg(fi, S_CONN_PEND_IN, 0, 0);
		/* N-CONNECT.ind to User */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_CONNECT,
					PRIM_OP_INDICATION);
		break;
	}
}

static void scoc_fsm_idle_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
{
	conn_destroy(fi->priv);
}

/* Figure C.3 / Q.714 (sheet 2 of 6) */
static void scoc_fsm_conn_pend_in(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct sccp_connection *conn = fi->priv;
	struct osmo_scu_prim *prim = NULL;

	switch (event) {
	case SCOC_E_SCU_N_CONN_RESP:
		prim = data;
		/* FIXME: assign local reference (only now?) */
		/* FIXME: assign sls, protocol class and credit */
		xua_gen_encode_and_send(conn, event, prim, SUA_CO_COAK);
		/* start inactivity timers */
		conn_start_inact_timers(conn);
		osmo_fsm_inst_state_chg(fi, S_ACTIVE, 0, 0);
		break;
	case SCOC_E_SCU_N_DISC_REQ:
		prim = data;
		/* release resources: implicit */
		xua_gen_encode_and_send(conn, event, prim, SUA_CO_COREF);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	}
}

/* Figure C.2/Q.714 (sheet 2 of 7) */
static void scoc_fsm_conn_pend_out(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct sccp_connection *conn = fi->priv;
	struct osmo_scu_prim *prim = NULL;
	struct xua_msg *xua = NULL;

	switch (event) {
	case SCOC_E_SCU_N_DISC_REQ:
		prim = data;
		conn->release_cause = prim->u.disconnect.cause;
		osmo_fsm_inst_state_chg(fi, S_WAIT_CONN_CONF, 0, 0);
		/* keep conn timer running(!) */
		break;
	case SCOC_E_CONN_TMR_EXP:
		/* N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, NULL, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		/* below implicitly releases resources + local ref */
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_ROUT_FAIL_IND:
	case SCOC_E_RCOC_CREF_IND:
		xua = data;
		/* stop conn timer */
		conn_stop_connect_timer(conn);
		/* release local res + ref (implicit by going to idle) */
		/* N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		/* below implicitly releases resources + local ref */
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_RLSD_IND:
		xua = data;
		/* RLC to SCRC */
		xua_gen_encode_and_send(conn, event, NULL, SUA_CO_RELCO);
		/* stop conn timer */
		conn_stop_connect_timer(conn);
		/* release local res + ref (implicit) */
		/* N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_OTHER_NPDU:
		xua = data;
		conn_start_connect_timer(conn);
		/* release local res + ref (implicit) */
		/* N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_CC_IND:
		xua = data;
		/* stop conn timer */
		conn_stop_connect_timer(conn);
		/* start inactivity timers */
		conn_start_inact_timers(conn);
		/* TODO: assign PCU and credit */
		/* associate remote ref to conn */
		conn->remote_ref = xua_msg_get_u32(xua, SUA_IEI_SRC_REF);
		/* 3.1.4.2 The node sending the CC message (identified
		 * by the parameter OPC contained in the
		 * MTP-TRANSFER.indication primitive which conveyed the
		 * CC message [plus the MTP-SAP instance]) is associated
		 * with the connection section. */
		conn->remote_pc = xua->mtp.opc;

		osmo_fsm_inst_state_chg(fi, S_ACTIVE, 0, 0);
		/* N-CONNECT.conf to user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_CONNECT,
					PRIM_OP_CONFIRM);
		break;
	}
}

/* Figure C.2/Q.714 (sheet 3 of 7) */
static void scoc_fsm_wait_conn_conf(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct sccp_connection *conn = fi->priv;
	struct xua_msg *xua = NULL;

	switch (event) {
	case SCOC_E_RCOC_RLSD_IND:
		xua = data;
		/* release complete to SCRC */
		xua_gen_encode_and_send(conn, event, NULL, SUA_CO_RELCO);
		/* stop conn timer */
		conn_stop_connect_timer(conn);
		/* release local res + ref (implicit) */
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_CC_IND:
		xua = data;
		/* stop conn timer */
		conn_stop_connect_timer(conn);
		/* associate rem ref to conn */
		conn->remote_ref = xua_msg_get_u32(xua, SUA_IEI_SRC_REF);
		/* released to SCRC */
		xua_gen_relre_and_send(conn, conn->release_cause, NULL);
		/* start rel timer */
		conn_start_rel_timer(conn);
		osmo_fsm_inst_state_chg(fi, S_DISCONN_PEND, 0, 0);
		break;
	case SCOC_E_RCOC_OTHER_NPDU:
	case SCOC_E_RCOC_CREF_IND:
	case SCOC_E_RCOC_ROUT_FAIL_IND:
		xua = data;
		/* stop conn timer */
		conn_stop_connect_timer(conn);
		/* release local res + ref */
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_CONN_TMR_EXP:
		/* release local res + ref */
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	}
}

/* C.2/Q.714 (sheet 4+5 of 7) and C.3/Q714 (sheet 3+4 of 6) */
static void scoc_fsm_active(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct xua_msg *xua = data;
	struct sccp_connection *conn = fi->priv;
	struct osmo_scu_prim *prim = NULL;

	switch (event) {
#pragma message ("TODO: internal disco: send N-DISCONNECT.ind to user")
		/* send N-DISCONNECT.ind to user */
		/*scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);*/
		/* fall-through */
	case SCOC_E_SCU_N_DISC_REQ:
		prim = data;
		/* stop inact timers */
		conn_stop_inact_timers(conn);
		/* send RLSD to SCRC */
		xua_gen_encode_and_send(conn, event, prim, SUA_CO_RELRE);
		/* start rel timer */
		conn_start_rel_timer(conn);
		osmo_fsm_inst_state_chg(fi, S_DISCONN_PEND, 0, 0);
		break;
	case SCOC_E_RCOC_CREF_IND:
	case SCOC_E_RCOC_CC_IND:
	case SCOC_E_RCOC_REL_COMPL_IND:
		/* do nothing */
		break;
	case SCOC_E_RCOC_RLSD_IND:
		/* send N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		/* release res + local ref (implicit) */
		/* stop inact timers */
		conn_stop_inact_timers(conn);
		/* RLC to SCRC */
		xua_gen_encode_and_send(conn, event, NULL, SUA_CO_RELCO);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_ERROR_IND:
		xua = data;
		/* FIXME: check for cause service_class_mismatch */
		/* release res + local ref (implicit) */
		/* send N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		/* stop inact timers */
		conn_stop_inact_timers(conn);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_T_IAR_EXP:
		/* stop inact timers */
		conn_stop_inact_timers(conn);
		xua = xua_msg_alloc();
		xua_msg_add_u32(xua, SUA_IEI_CAUSE,
				SUA_CAUSE_T_RELEASE | SCCP_RELEASE_CAUSE_EXPIRATION_INACTIVE);
		xua_msg_add_u32(xua, SUA_IEI_IMPORTANCE, conn->importance);
		/* Send N-DISCONNECT.ind to local user */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		talloc_free(xua);
		/* Send RLSD to peer */
		xua_gen_relre_and_send(conn, SCCP_RELEASE_CAUSE_EXPIRATION_INACTIVE, NULL);
		/* start release timer */
		conn_start_rel_timer(conn);
		osmo_fsm_inst_state_chg(fi, S_DISCONN_PEND, 0, 0);
		break;
	case SCOC_E_RCOC_ROUT_FAIL_IND:
		/* send N-DISCONNECT.ind to user */
		scu_gen_encode_and_send(conn, event, NULL, OSMO_SCU_PRIM_N_DISCONNECT,
					PRIM_OP_INDICATION);
		/* stop inact timers */
		conn_stop_inact_timers(conn);
		/* start release timer */
		conn_start_rel_timer(conn);
		osmo_fsm_inst_state_chg(fi, S_DISCONN_PEND, 0, 0);
		break;
	/* Figure C.4/Q.714 */
	case SCOC_E_SCU_N_DATA_REQ:
	case SCOC_E_SCU_N_EXP_DATA_REQ:
		prim = data;
		xua_gen_encode_and_send(conn, event, prim, SUA_CO_CODT);
		conn_restart_tx_inact_timer(conn);
		break;
	case SCOC_E_RCOC_DT1_IND:
		/* restart receive inactivity timer */
		conn_restart_rx_inact_timer(conn);
		/* TODO: M-bit */
		scu_gen_encode_and_send(conn, event, xua, OSMO_SCU_PRIM_N_DATA,
					PRIM_OP_INDICATION);
		break;
	/* Figure C.4/Q.714 (sheet 4 of 4) */
	case SCOC_E_RCOC_IT_IND:
		xua = data;
		/* check if remote reference is what we expect */
		/* check class is what we expect */
		if (xua_msg_get_u32(xua, SUA_IEI_SRC_REF) != conn->remote_ref ||
		    xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS) != conn->sccp_class) {
			/* Release connection */
			/* Stop inactivity Timers */
			conn_stop_inact_timers(conn);
			xua = xua_msg_alloc();
			xua_msg_add_u32(xua, SUA_IEI_CAUSE,
					SUA_CAUSE_T_RELEASE | SCCP_RELEASE_CAUSE_INCONSISTENT_CONN_DATA);
			xua_msg_add_u32(xua, SUA_IEI_IMPORTANCE, conn->importance);
			/* send N-DISCONNECT.ind to user */
			scu_gen_encode_and_send(conn, event, xua,
						OSMO_SCU_PRIM_N_DISCONNECT,
						PRIM_OP_INDICATION);
			talloc_free(xua);
			/* Send RLSD to SCRC */
			xua_gen_relre_and_send(conn, SCCP_RELEASE_CAUSE_INCONSISTENT_CONN_DATA, NULL);
			talloc_free(xua);
			/* Start release timer */
			conn_start_rel_timer(conn);
			osmo_fsm_inst_state_chg(fi, S_DISCONN_PEND, 0, 0);
		}
		conn_restart_rx_inact_timer(conn);
		break;
	case SCOC_E_T_IAS_EXP:
		/* Send IT to peer */
		xua_gen_encode_and_send(conn, event, NULL, SUA_CO_COIT);
		conn_restart_tx_inact_timer(conn);
		break;
	}
}

/* C.2/Q.714 (sheet 6+7 of 7) and C.3/Q.714 (sheet 5+6 of 6) */
static void scoc_fsm_disconn_pend(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct sccp_connection *conn = fi->priv;

	switch (event) {
	case SCOC_E_RCOC_REL_COMPL_IND:
	case SCOC_E_RCOC_RLSD_IND:
		/* release res + local ref (implicit) */
		/* freeze local ref */
		/* stop release + interval timers */
		conn_stop_release_timers(conn);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_RCOC_ROUT_FAIL_IND:
	case SCOC_E_RCOC_OTHER_NPDU:
		/* do nothing */
		break;
	case SCOC_E_T_REL_EXP: /* release timer exp */
		/* send RLSD */
		xua_gen_relre_and_send(conn, SCCP_RELEASE_CAUSE_UNQUALIFIED, NULL);
		/* start interval timer */
		conn_start_int_timer(conn);
		/* start repeat release timer */
		conn_start_rep_rel_timer(conn);
		break;
	case SCOC_E_T_INT_EXP: /* interval timer exp */
		/* TODO: Inform maintenance */
		/* stop release and interval timers */
		conn_stop_release_timers(conn);
		osmo_fsm_inst_state_chg(fi, S_IDLE, 0, 0);
		break;
	case SCOC_E_T_REP_REL_EXP: /* repeat release timer exp */
		/* send RLSD */
		xua_gen_relre_and_send(conn, SCCP_RELEASE_CAUSE_UNQUALIFIED, NULL);
		/* re-start repeat release timer */
		conn_start_rep_rel_timer(conn);
		break;
	}
}

static const struct osmo_fsm_state sccp_scoc_states[] = {
	[S_IDLE] = {
		.name = "IDLE",
		.action = scoc_fsm_idle,
		.onenter= scoc_fsm_idle_onenter,
		.in_event_mask = S(SCOC_E_SCU_N_CONN_REQ) |
				 //S(SCOC_E_SCU_N_TYPE1_REQ) |
				 S(SCOC_E_RCOC_CONN_IND) |
				 S(SCOC_E_RCOC_RLSD_IND) |
				 S(SCOC_E_RCOC_REL_COMPL_IND) |
				 S(SCOC_E_RCOC_OTHER_NPDU),
		.out_state_mask = S(S_CONN_PEND_OUT) |
				  S(S_CONN_PEND_IN),
	},
	[S_CONN_PEND_IN] = {
		.name = "CONN_PEND_IN",
		.action = scoc_fsm_conn_pend_in,
		.in_event_mask = S(SCOC_E_SCU_N_CONN_RESP) |
				 S(SCOC_E_SCU_N_DISC_REQ),
		.out_state_mask = S(S_IDLE) |
				  S(S_ACTIVE),
	},
	[S_CONN_PEND_OUT] = {
		.name = "CONN_PEND_OUT",
		.action = scoc_fsm_conn_pend_out,
		.in_event_mask = S(SCOC_E_SCU_N_DISC_REQ) |
				 S(SCOC_E_CONN_TMR_EXP) |
				 S(SCOC_E_RCOC_ROUT_FAIL_IND) |
				 S(SCOC_E_RCOC_RLSD_IND) |
				 S(SCOC_E_RCOC_OTHER_NPDU) |
				 S(SCOC_E_RCOC_CREF_IND) |
				 S(SCOC_E_RCOC_CC_IND),
		.out_state_mask = S(S_IDLE) |
				  S(S_ACTIVE) |
				  S(S_WAIT_CONN_CONF),
	},
	[S_ACTIVE] = {
		.name = "ACTIVE",
		.action = scoc_fsm_active,
		.in_event_mask = S(SCOC_E_SCU_N_DISC_REQ) |
				/* internal disconnect */
				 S(SCOC_E_RCOC_CREF_IND) |
				 S(SCOC_E_RCOC_REL_COMPL_IND) |
				 S(SCOC_E_RCOC_RLSD_IND) |
				 S(SCOC_E_RCOC_ERROR_IND) |
				 S(SCOC_E_T_IAR_EXP) |
				 S(SCOC_E_T_IAS_EXP) |
				 S(SCOC_E_RCOC_ROUT_FAIL_IND) |
				 S(SCOC_E_SCU_N_DATA_REQ) |
				 S(SCOC_E_SCU_N_EXP_DATA_REQ) |
				 S(SCOC_E_RCOC_DT1_IND) |
				 S(SCOC_E_RCOC_IT_IND),
		.out_state_mask = S(S_IDLE) |
				  S(S_DISCONN_PEND),
	},
	[S_DISCONN_PEND] = {
		.name = "DISCONN_PEND",
		.action = scoc_fsm_disconn_pend,
		.in_event_mask = S(SCOC_E_RCOC_REL_COMPL_IND) |
				 S(SCOC_E_RCOC_RLSD_IND) |
				 S(SCOC_E_RCOC_ROUT_FAIL_IND) |
				 S(SCOC_E_RCOC_OTHER_NPDU) |
				 S(SCOC_E_T_REL_EXP) |
				 S(SCOC_E_T_INT_EXP) |
				 S(SCOC_E_T_REP_REL_EXP),
		.out_state_mask = S(S_IDLE),
	},
	[S_RESET_IN] = {
		.name = "RESET_IN",
	},
	[S_RESET_OUT] = {
		.name = "RESET_OUT",
	},
	[S_BOTHWAY_RESET] = {
		.name = "BOTHWAY_RESET",
	},
	[S_WAIT_CONN_CONF] = {
		.name = "WAIT_CONN_CONF",
		.action = scoc_fsm_wait_conn_conf,
		.in_event_mask = S(SCOC_E_RCOC_RLSD_IND) |
				 S(SCOC_E_RCOC_CC_IND) |
				 S(SCOC_E_RCOC_OTHER_NPDU) |
				 S(SCOC_E_CONN_TMR_EXP) |
				 S(SCOC_E_RCOC_CREF_IND) |
				 S(SCOC_E_RCOC_ROUT_FAIL_IND),
	},
};

struct osmo_fsm sccp_scoc_fsm = {
	.name = "SCCP-SCOC",
	.states = sccp_scoc_states,
	.num_states = ARRAY_SIZE(sccp_scoc_states),
	/* ".log_subsys = DLSCCP" doesn't work as DLSCCP is not a constant */
	.event_names = scoc_event_names,
};

/* map from SCCP return cause to SCCP Refusal cause */
static const uint8_t cause_map_cref[] = {
	[SCCP_RETURN_CAUSE_SUBSYSTEM_CONGESTION] =
				SCCP_REFUSAL_SUBSYTEM_CONGESTION,
	[SCCP_RETURN_CAUSE_SUBSYSTEM_FAILURE] =
				SCCP_REFUSAL_SUBSYSTEM_FAILURE,
	[SCCP_RETURN_CAUSE_UNEQUIPPED_USER] =
				SCCP_REFUSAL_UNEQUIPPED_USER,
	[SCCP_RETURN_CAUSE_UNQUALIFIED] =
				SCCP_REFUSAL_UNQUALIFIED,
	[SCCP_RETURN_CAUSE_SCCP_FAILURE] =
				SCCP_REFUSAL_SCCP_FAILURE,
	[SCCP_RETURN_CAUSE_HOP_COUNTER_VIOLATION] =
				SCCP_REFUSAL_HOP_COUNTER_VIOLATION,
};

static uint8_t get_cref_cause_for_ret(uint8_t ret_cause)
{
	if (ret_cause < ARRAY_SIZE(cause_map_cref))
		return cause_map_cref[ret_cause];
	else
		return SCCP_REFUSAL_UNQUALIFIED;
}

/* Generate a COREF message purely based on an incoming SUA message,
 * without the use of any local connection state */
static struct xua_msg *gen_coref_without_conn(struct osmo_sccp_instance *inst,
					      struct xua_msg *xua_in,
					      uint32_t ref_cause)
{
	struct xua_msg *xua;

	xua = xua_msg_alloc();
	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COREF);
	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, inst->route_ctx);

	xua_msg_copy_part(xua, SUA_IEI_DEST_REF, xua_in, SUA_IEI_SRC_REF);
	xua_msg_add_u32(xua, SUA_IEI_CAUSE, SUA_CAUSE_T_REFUSAL | ref_cause);
	/* optional: source addr */
	xua_msg_copy_part(xua, SUA_IEI_SRC_ADDR, xua_in, SUA_IEI_DEST_ADDR);
	/* conditional: dest addr */
	xua_msg_copy_part(xua, SUA_IEI_DEST_ADDR, xua_in, SUA_IEI_SRC_ADDR);
	/* optional: importance */
	xua_msg_copy_part(xua, SUA_IEI_IMPORTANCE, xua_in, SUA_IEI_IMPORTANCE);
	/* optional: data */
	xua_msg_copy_part(xua, SUA_IEI_DATA, xua_in, SUA_IEI_DATA);

	return xua;
}

/* Find a SCCP user for given SUA message (based on SUA_IEI_DEST_ADDR */
static struct osmo_sccp_user *sccp_find_user(struct osmo_sccp_instance *inst,
					     struct xua_msg *xua)
{
	int rc;
	struct osmo_sccp_addr called_addr;

	rc = sua_addr_parse(&called_addr, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		LOGP(DLSCCP, LOGL_ERROR, "Cannot find SCCP User for XUA "
			"Message %s without valid DEST_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return NULL;
	}

	if (!(called_addr.presence & OSMO_SCCP_ADDR_T_SSN)) {
		LOGP(DLSCCP, LOGL_ERROR, "Cannot resolve SCCP User for "
			"XUA Message %s without SSN in CalledAddr\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		return NULL;
	}

	return sccp_user_find(inst, called_addr.ssn, called_addr.pc);
}

/*! \brief SCOC: Receive SCRC Routing Failure
 *  \param[in] inst SCCP Instance on which we operate
 *  \param[in] xua SUA message that was failed to route
 *  \param[in] return_cause Reason (cause) for routing failure */
void sccp_scoc_rx_scrc_rout_fail(struct osmo_sccp_instance *inst,
				struct xua_msg *xua, uint32_t return_cause)
{
	uint32_t conn_id;
	struct sccp_connection *conn;

	LOGP(DLSCCP, LOGL_NOTICE, "SCRC Routing Failure for message %s\n",
	     xua_hdr_dump(xua, &xua_dialect_sua));

	/* try to dispatch to connection FSM (if any) */
	conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
	conn = conn_find_by_id(inst, conn_id);
	if (conn) {
		osmo_fsm_inst_dispatch(conn->fi,
					SCOC_E_RCOC_ROUT_FAIL_IND, xua);
	} else {
		/* generate + send CREF directly */
		struct xua_msg *cref;
		uint8_t cref_cause = get_cref_cause_for_ret(return_cause);
		cref = gen_coref_without_conn(inst, xua, cref_cause);
		sccp_scrc_rx_scoc_conn_msg(inst, cref);
		xua_msg_free(cref);
	}
}

/* Generate a COERR based in input arguments */
static struct xua_msg *gen_coerr(uint32_t route_ctx, uint32_t dest_ref,
				uint32_t err_cause)
{
	struct xua_msg *xua = xua_msg_alloc();

	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COERR);
	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, route_ctx);
	xua_msg_add_u32(xua, SUA_IEI_DEST_REF, dest_ref);
	xua_msg_add_u32(xua, SUA_IEI_CAUSE, SUA_CAUSE_T_ERROR | err_cause);

	return xua;
}

/* generate COERR from incoming XUA and send it */
static void tx_coerr_from_xua(struct osmo_sccp_instance *inst,
				struct xua_msg *in, uint32_t err_cause)
{
	struct xua_msg *xua;
	uint32_t route_ctx, dest_ref;

	route_ctx = xua_msg_get_u32(in, SUA_IEI_ROUTE_CTX);
	/* get *source* reference and use as destination ref */
	dest_ref = xua_msg_get_u32(in, SUA_IEI_SRC_REF);

	xua = gen_coerr(route_ctx, dest_ref, err_cause);
	/* copy over the MTP parameters */
	xua->mtp.dpc = in->mtp.opc;
	xua->mtp.opc = in->mtp.dpc;
	xua->mtp.sio = in->mtp.sio;

	/* sent to SCRC for transmission */
	sccp_scrc_rx_scoc_conn_msg(inst, xua);
	xua_msg_free(xua);
}

/* Generate a RELCO based in input arguments */
static struct xua_msg *gen_relco(uint32_t route_ctx, uint32_t dest_ref,
				uint32_t src_ref)
{
	struct xua_msg *xua = xua_msg_alloc();

	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_RELCO);
	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, route_ctx);
	xua_msg_add_u32(xua, SUA_IEI_DEST_REF, dest_ref);
	xua_msg_add_u32(xua, SUA_IEI_SRC_REF, src_ref);

	return xua;
}

/* generate RELCO from incoming XUA and send it */
static void tx_relco_from_xua(struct osmo_sccp_instance *inst,
				struct xua_msg *in)
{
	struct xua_msg *xua;
	uint32_t route_ctx, dest_ref, src_ref;

	route_ctx = xua_msg_get_u32(in, SUA_IEI_ROUTE_CTX);
	/* get *source* reference and use as destination ref */
	dest_ref = xua_msg_get_u32(in, SUA_IEI_SRC_REF);
	/* get *dest* reference and use as source ref */
	src_ref = xua_msg_get_u32(in, SUA_IEI_DEST_REF);

	xua = gen_relco(route_ctx, dest_ref, src_ref);
	/* copy over the MTP parameters */
	xua->mtp.dpc = in->mtp.opc;
	xua->mtp.opc = in->mtp.dpc;
	xua->mtp.sio = in->mtp.sio;

	/* send to SCRC for transmission */
	sccp_scrc_rx_scoc_conn_msg(inst, xua);
	xua_msg_free(xua);
}

/* Generate a RLSD based in input arguments */
static struct xua_msg *gen_rlsd(uint32_t route_ctx, uint32_t dest_ref,
				uint32_t src_ref)
{
	struct xua_msg *xua = xua_msg_alloc();

	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_RELRE);
	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, route_ctx);
	xua_msg_add_u32(xua, SUA_IEI_DEST_REF, dest_ref);
	xua_msg_add_u32(xua, SUA_IEI_SRC_REF, src_ref);

	return xua;
}

/* Generate a RLSD to both the remote side and the local conn */
static void tx_rlsd_from_xua_twoway(struct sccp_connection *conn,
				    struct xua_msg *in)
{
	struct xua_msg *xua;
	uint32_t route_ctx, dest_ref, src_ref;

	route_ctx = xua_msg_get_u32(in, SUA_IEI_ROUTE_CTX);
	/* get *source* reference and use as destination ref */
	dest_ref = xua_msg_get_u32(in, SUA_IEI_SRC_REF);
	/* get *source* reference and use as destination ref */
	src_ref = xua_msg_get_u32(in, SUA_IEI_DEST_REF);

	/* Generate RLSD towards remote peer */
	xua = gen_rlsd(route_ctx, dest_ref, src_ref);
	/* copy over the MTP parameters */
	xua->mtp.dpc = in->mtp.opc;
	xua->mtp.opc = in->mtp.dpc;
	xua->mtp.sio = in->mtp.sio;
	/* send to SCRC for transmission */
	sccp_scrc_rx_scoc_conn_msg(conn->inst, xua);
	xua_msg_free(xua);

	/* Generate RLSD towards local peer */
	xua = gen_rlsd(conn->inst->route_ctx, conn->conn_id, conn->remote_ref);
	xua->mtp.dpc = in->mtp.dpc;
	xua->mtp.opc = conn->remote_pc;
	xua->mtp.sio = in->mtp.sio;
	osmo_fsm_inst_dispatch(conn->fi, SCOC_E_RCOC_RLSD_IND, xua);
	xua_msg_free(xua);
}

/* process received message for unasigned local reference */
static void sccp_scoc_rx_unass_local_ref(struct osmo_sccp_instance *inst,
					 struct xua_msg *xua)
{
	/* we have received a message with unassigned destination local
	 * reference and thus apply the action indicated in Table
	 * B.2/Q.714 */
	switch (xua->hdr.msg_type) {
	case SUA_CO_COAK: /* CC */
	case SUA_CO_COIT: /* IT */
	case SUA_CO_RESRE: /* RSR */
	case SUA_CO_RESCO: /* RSC */
		/* Send COERR */
		tx_coerr_from_xua(inst, xua, SCCP_ERROR_LRN_MISMATCH_UNASSIGNED);
		break;
	case SUA_CO_COREF: /* CREF */
	case SUA_CO_RELCO: /* RLC */
	case SUA_CO_CODT: /* DT1 */
	case SUA_CO_CODA: /* AK */
	case SUA_CO_COERR: /* ERR */
		/* DISCARD */
		break;
	case SUA_CO_RELRE: /* RLSD */
		/* Send RLC */
		tx_relco_from_xua(inst, xua);
		break;
	default:
		LOGP(DLSCCP, LOGL_NOTICE, "Unhandled %s\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		break;
	}
}

/* process received message for invalid source local reference */
static void sccp_scoc_rx_inval_src_ref(struct sccp_connection *conn,
				       struct xua_msg *xua,
				       uint32_t inval_src_ref)
{
	LOGP(DLSCCP, LOGL_NOTICE,
	     "Received message for source ref %u on conn with mismatching remote ref %u\n",
	     inval_src_ref, conn->remote_ref);

	/* we have received a message with invalid source local
	 * reference and thus apply the action indicated in Table
	 * B.2/Q.714 */
	switch (xua->hdr.msg_type) {
	case SUA_CO_RELRE: /* RLSD */
	case SUA_CO_RESRE: /* RSR */
	case SUA_CO_RESCO: /* RSC */
		/* Send ERR */
		tx_coerr_from_xua(conn->inst, xua, SCCP_ERROR_LRN_MISMATCH_INCONSISTENT);
		break;
	case SUA_CO_COIT: /* IT */
		/* FIXME: RLSD to both sides */
		tx_rlsd_from_xua_twoway(conn, xua);
		break;
	case SUA_CO_RELCO: /* RLC */
		/* DISCARD */
		break;
	default:
		LOGP(DLSCCP, LOGL_NOTICE, "Unhandled %s\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		break;
	}
}

/* process received message for invalid origin point code */
static void sccp_scoc_rx_inval_opc(struct sccp_connection *conn,
				   struct xua_msg *xua)
{
	LOGP(DLSCCP, LOGL_NOTICE,
	     "Received message for opc=%u=%s on conn with mismatching remote pc=%u=%s\n",
	     xua->mtp.opc, osmo_ss7_pointcode_print(conn->inst->ss7, xua->mtp.opc),
	     conn->remote_pc, osmo_ss7_pointcode_print2(conn->inst->ss7, conn->remote_pc));
	/* we have received a message with invalid origin PC and thus
	 * apply the action indiacted in Table B.2/Q.714 */
	switch (xua->hdr.msg_type) {
	case SUA_CO_RELRE: /* RLSD */
	case SUA_CO_RESRE: /* RSR */
	case SUA_CO_RESCO: /* RSC */
		/* Send ERR */
		tx_coerr_from_xua(conn->inst, xua, SCCP_ERROR_POINT_CODE_MISMATCH);
		break;
	case SUA_CO_RELCO: /* RLC */
	case SUA_CO_CODT: /* DT1 */
	case SUA_CO_CODA: /* AK */
	case SUA_CO_COERR: /* ERR */
		/* DISCARD */
		break;
	default:
		LOGP(DLSCCP, LOGL_NOTICE, "Unhandled %s\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		break;
	}
}

/*! \brief Main entrance function for primitives from the SCRC (Routing Control)
 *  \param[in] inst SCCP Instance in which we operate
 *  \param[in] xua SUA message in xua_msg format */
void sccp_scoc_rx_from_scrc(struct osmo_sccp_instance *inst,
			    struct xua_msg *xua)
{
	struct sccp_connection *conn;
	struct osmo_sccp_user *scu;
	uint32_t src_loc_ref;
	int event;

	/* we basically try to convert the SUA message into an event,
	 * and then dispatch the event to the connection-specific FSM.
	 * If it is a CORE (Connect REquest), we create the connection
	 * (and imlpicitly its FSM) first */

	if (xua->hdr.msg_type == SUA_CO_CORE) {
		scu = sccp_find_user(inst, xua);
		if (!scu) {
			/* this shouldn't happen, as the caller should
			 * have already verified that a local user is
			 * equipped for this SSN */
			LOGP(DLSCCP, LOGL_ERROR, "Cannot find user for "
				"CORE ?!?\n");
			return;
		}
		/* Allocate new connection */
		conn = conn_create(scu);
		conn->incoming = true;
	} else {
		uint32_t conn_id;
		/* Resolve existing connection */
		conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
		conn = conn_find_by_id(inst, conn_id);
		if (!conn) {
			LOGP(DLSCCP, LOGL_NOTICE, "Received %s: Cannot find connection for "
			     "local reference %u\n", xua_hdr_dump(xua, &xua_dialect_sua), conn_id);
			sccp_scoc_rx_unass_local_ref(inst, xua);
			return;
		}
	}
	OSMO_ASSERT(conn);
	OSMO_ASSERT(conn->fi);

	DEBUGP(DLSCCP, "Received %s for local reference %u\n",
		xua_hdr_dump(xua, &xua_dialect_sua), conn->conn_id);

	if (xua->hdr.msg_type != SUA_CO_CORE &&
	    xua->hdr.msg_type != SUA_CO_COAK &&
	    xua->hdr.msg_type != SUA_CO_COREF) {
		if (xua_msg_find_tag(xua, SUA_IEI_SRC_REF)) {
			/* Check if received source local reference !=
			 * the one we saved in local state */
			src_loc_ref = xua_msg_get_u32(xua, SUA_IEI_SRC_REF);
			if (src_loc_ref != conn->remote_ref) {
				sccp_scoc_rx_inval_src_ref(conn, xua, src_loc_ref);
				return;
			}
		}

		/* Check if received OPC != the remote_pc we stored locally */
		if (xua->mtp.opc != conn->remote_pc) {
			sccp_scoc_rx_inval_opc(conn, xua);
			return;
		}
	}

	/* Map from XUA message to event */
	event = xua_msg_event_map(xua, sua_scoc_event_map, ARRAY_SIZE(sua_scoc_event_map));
	if (event < 0) {
		LOGP(DLSCCP, LOGL_ERROR, "Cannot map SCRC msg %s to event\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		/* Table B.1/Q714 states DISCARD for any message with
		 * unknown type */
		return;
	}

	/* Dispatch event to existing connection */
	osmo_fsm_inst_dispatch(conn->fi, event, xua);
}

/* get the Connection ID of the given SCU primitive */
static uint32_t scu_prim_conn_id(const struct osmo_scu_prim *prim)
{
	switch (prim->oph.primitive) {
	case OSMO_SCU_PRIM_N_CONNECT:
		return prim->u.connect.conn_id;
	case OSMO_SCU_PRIM_N_DATA:
		return prim->u.data.conn_id;
	case OSMO_SCU_PRIM_N_DISCONNECT:
		return prim->u.disconnect.conn_id;
	case OSMO_SCU_PRIM_N_RESET:
		return prim->u.reset.conn_id;
	default:
		return 0;
	}
}

/*! Main entrance function for primitives from SCCP User.
 * The caller is required to free oph->msg, otherwise the same as osmo_sccp_user_sap_down().
 *  \param[in] scu SCCP User sending us the primitive
 *  \param[in] oph Osmocom primitive sent by the user
 *  \returns 0 on success; negative on error */
int osmo_sccp_user_sap_down_nofree(struct osmo_sccp_user *scu, struct osmo_prim_hdr *oph)
{
	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
	struct osmo_sccp_instance *inst = scu->inst;
	struct sccp_connection *conn;
	int rc = 0;
	int event;

	LOGP(DLSCCP, LOGL_DEBUG, "Received SCCP User Primitive (%s)\n",
		osmo_scu_prim_name(&prim->oph));

	switch (OSMO_PRIM_HDR(&prim->oph)) {
	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_REQUEST):
	/* other CL primitives? */
		/* Connectionless by-passes this altogether */
		return sccp_sclc_user_sap_down_nofree(scu, oph);
	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_REQUEST):
		/* Allocate new connection structure */
		conn = conn_create_id(scu, prim->u.connect.conn_id);
		if (!conn) {
			/* FIXME: inform SCCP user with proper reply */
			LOGP(DLSCCP, LOGL_ERROR, "Cannot create conn-id for primitive %s\n",
			     osmo_scu_prim_name(&prim->oph));
			return rc;
		}
		break;
	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_RESPONSE):
	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_REQUEST):
	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_REQUEST):
	case OSMO_PRIM(OSMO_SCU_PRIM_N_RESET, PRIM_OP_REQUEST):
		/* Resolve existing connection structure */
		conn = conn_find_by_id(inst, scu_prim_conn_id(prim));
		if (!conn) {
			/* FIXME: inform SCCP user with proper reply */
			LOGP(DLSCCP, LOGL_ERROR, "Received unknown conn-id %u for primitive %s\n",
			     scu_prim_conn_id(prim), osmo_scu_prim_name(&prim->oph));
			return rc;
		}
		break;
	default:
		LOGP(DLSCCP, LOGL_ERROR, "Received unknown primitive %s\n",
			osmo_scu_prim_name(&prim->oph));
		return -1;
	}

	/* Map from primitive to event */
	event = osmo_event_for_prim(oph, scu_scoc_event_map);

	/* Dispatch event into connection */
	return osmo_fsm_inst_dispatch(conn->fi, event, prim);
}

/*! Main entrance function for primitives from SCCP User.
 * Implies a msgb_free(oph->msg), otherwise the same as osmo_sccp_user_sap().
 *  \param[in] scu SCCP User sending us the primitive
 *  \param[in] oph Osmocom primitive sent by the user
 *  \returns 0 on success; negative on error */
int osmo_sccp_user_sap_down(struct osmo_sccp_user *scu, struct osmo_prim_hdr *oph)
{
	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
	struct msgb *msg = prim->oph.msg;
	int rc = osmo_sccp_user_sap_down_nofree(scu, oph);
	msgb_free(msg);
	return rc;
}

void sccp_scoc_flush_connections(struct osmo_sccp_instance *inst)
{
	struct sccp_connection *conn, *conn2;

	llist_for_each_entry_safe(conn, conn2, &inst->connections, list)
		conn_destroy(conn);
}

#include <osmocom/vty/vty.h>

static void vty_show_connection(struct vty *vty, struct sccp_connection *conn)
{
	struct osmo_ss7_instance *s7i = conn->inst->ss7;
	struct osmo_sccp_addr *remote_addr;
	uint32_t local_pc = OSMO_SS7_PC_INVALID;

	if (osmo_ss7_pc_is_valid(conn->user->pc))
		local_pc = conn->user->pc;
	else if (osmo_ss7_pc_is_valid(s7i->cfg.primary_pc))
		local_pc = s7i->cfg.primary_pc;

	if (conn->incoming)
		remote_addr = &conn->calling_addr;
	else
		remote_addr = &conn->called_addr;

	vty_out(vty, "%c %06x %3u %7s ", conn->incoming ? 'I' : 'O',
		conn->conn_id, conn->user->ssn,
		osmo_ss7_pointcode_print(s7i, local_pc));
	vty_out(vty, "%16s %06x %3u %7s%s",
		osmo_fsm_inst_state_name(conn->fi), conn->remote_ref, remote_addr->ssn,
		osmo_ss7_pointcode_print(s7i, conn->remote_pc),
		VTY_NEWLINE);
}

void sccp_scoc_show_connections(struct vty *vty, struct osmo_sccp_instance *inst)
{
	struct sccp_connection *conn;

	vty_out(vty, "I Local              Conn.            Remote            %s", VTY_NEWLINE);
	vty_out(vty, "O Ref    SSN PC      State            Ref    SSN PC     %s", VTY_NEWLINE);
	vty_out(vty, "- ------ --- ------- ---------------- ------ --- -------%s", VTY_NEWLINE);

	llist_for_each_entry(conn, &inst->connections, list)
		vty_show_connection(vty, conn);
}
