Add WIP code for SCCP User Adaptation (SUA) and SCCP User SAP

The idea of this code is to
* provide a SCCP User SAP as boundary between the User of SCCP
  or SCCP-like transport like SUA
* implement the minimum subset of SUA to transport RANAP messages
  betweene HNB-GW and MSC/SGSN

At this point
* we don't yet implement the proper state machines and timer
* we don't imp[lement the SCCP RESET procedure
* we don't implement AS/ASP management

The code is full of FIXMEs whihc hopefully will get fixed gradually.

After some cleanup + verification, it should move to a library, possibly
either replacing/renaming libomo-sccp, or adding it to libosmo-netif?
diff --git a/sigtran/Makefile b/sigtran/Makefile
new file mode 100644
index 0000000..06a8ba6
--- /dev/null
+++ b/sigtran/Makefile
@@ -0,0 +1,17 @@
+LDADD := -ltalloc -lsctp $(shell pkg-config --libs libosmo-sccp) $(shell pkg-config --libs libosmocore) $(shell pkg-config --libs libosmo-netif)
+LDFLAGS += $(LDADD)
+CFLAGS += -Wall
+
+all: sua_client_test sua_server_test
+
+%.o: %.c
+	$(CC) $(CFLAGS) -o $@ -c $^
+
+sua_client_test: sccp_sap.o sua.o sua_client_test.o sua_test_common.o /usr/local/lib/libxua.a
+	$(CC) $(LDFLAGS) -o $@ $^
+
+sua_server_test: sccp_sap.o sua.o sua_server_test.o sua_test_common.o /usr/local/lib/libxua.a
+	$(CC) $(LDFLAGS) -o $@ $^
+
+clean:
+	@rm *.o sua_client_test sua_server_test
diff --git a/sigtran/proto_sua.h b/sigtran/proto_sua.h
new file mode 100644
index 0000000..4b97d9f
--- /dev/null
+++ b/sigtran/proto_sua.h
@@ -0,0 +1,122 @@
+/* RFC 3868 SUA SCCP User Adaption */
+
+/* (C) 2012 by Harald Welte <laforge@gnumonks.org>
+ %
+ % All Rights Reserved
+ %
+ % This program is free software; you can redistribute it and/or modify
+ % it under the terms of the GNU Affero General Public License as
+ % published by the Free Software Foundation; either version 3 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 Affero General Public License
+ % along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <stdint.h>
+
+//#include <osmocom/ss7/xua.h>
+
+#define SUA_PPID	4
+#define SUA_PORT 	14001
+
+/* 3.1.2 Message Classes */
+#define SUA_MSGC_MGMT	0
+#define SUA_MSGC_SNM	2
+#define SUA_MSGC_ASPSM	3
+#define SUA_MSGC_ASPTM	4
+#define SUA_MSGC_CL	7
+#define SUA_MSGC_CO	8
+#define SUA_MSGC_RKM	9
+
+/* 3.1.3 Message Types */
+#define SUA_MGMT_ERR	0
+#define SUA_MGMT_NTFY	1
+
+#define SUA_SNM_DUNA	1
+#define SUA_SNM_DAVA	2
+#define SUA_SNM_DAUD	3
+#define SUA_SNM_SCON	4
+#define SUA_SNM_DUPU	5
+#define SUA_SNM_DRST	6
+
+#define SUA_ASPSM_UP		1
+#define SUA_ASPSM_DOWN		2
+#define SUA_ASPSM_BEAT		3
+#define SUA_ASPSM_UP_ACK 	4
+#define SUA_ASPSM_DOWN_ACK	5
+#define SUA_ASPSM_BEAT_ACK	6
+
+#define SUA_ASPTM_ACTIVE	1
+#define SUA_ASPTM_INACTIVE	2
+#define SUA_ASPTM_ACTIVE_ACK	3
+#define SUA_ASPTM_INACTIVE_ACK	4
+
+#define SUA_RKM_REG_REQ		1
+#define SUA_RKM_REG_RSP		2
+#define SUA_RKM_DEREG_REQ	3
+#define SUA_RKM_DEREG_RSP	4
+
+#define SUA_CL_CLDT	 	1
+#define SUA_CL_CLDR	 	2
+
+#define SUA_CO_CORE		1
+#define SUA_CO_COAK		2
+#define SUA_CO_COREF		3
+#define SUA_CO_RELRE		4
+#define SUA_CO_RELCO		5
+#define SUA_CO_RESCO		6
+#define SUA_CO_RESRE		7
+#define SUA_CO_CODT		8
+#define SUA_CO_CODA		9
+#define SUA_CO_COERR		10
+#define SUA_CO_COIT		11
+
+#define SUA_IEI_ROUTE_CTX	0x0006
+#define SUA_IEI_CORR_ID		0x0013
+#define SUA_IEI_REG_RESULT	0x0014
+#define SUA_IEI_DEREG_RESULT	0x0015
+
+/* 3.10 SUA specific parameters */
+
+#define SUA_IEI_S7_HOP_CTR	0x0101
+#define SUA_IEI_SRC_ADDR	0x0102
+#define SUA_IEI_DEST_ADDR	0x0103
+#define SUA_IEI_SRC_REF		0x0104
+#define SUA_IEI_DEST_REF	0x0105
+#define SUA_IEI_CAUSE		0x0106
+#define SUA_IEI_SEQ_NR		0x0107
+#define SUA_IEI_RX_SEQ_NR	0x0108
+#define SUA_IEI_ASP_CAPA	0x0109
+#define SUA_IEI_CREDIT		0x010A
+#define SUA_IEI_DATA		0x010B
+#define SUA_IEI_USER_CAUSE	0x010C
+#define SUA_IEI_NET_APPEARANCE	0x010D
+#define SUA_IEI_ROUTING_KEY	0x010E
+#define SUA_IEI_DRN		0x010F
+#define SUA_IEI_TID		0x0110
+#define SUA_IEI_SMI		0x0112
+#define SUA_IEI_IMPORTANCE	0x0113
+#define SUA_IEI_MSG_PRIO	0x0114
+#define SUA_IEI_PROTO_CLASS	0x0115
+#define SUA_IEI_SEQ_CTRL	0x0116
+#define SUA_IEI_SEGMENTATION	0x0117
+#define SUA_IEI_CONG_LEVEL	0x0118
+
+#define SUA_IEI_GT	0x8001
+#define SUA_IEI_PC	0x8002
+#define SUA_IEI_SSN	0x8003
+#define SUA_IEI_IPv4	0x8004
+#define SUA_IEI_HOST	0x8005
+#define SUA_IEI_IPv6	0x8006
+
+#define SUA_RI_GT	1
+#define SUA_RI_SSN_PC	2
+#define SUA_RI_HOST	3
+#define SUA_RI_SSN_IP	4
diff --git a/sigtran/sccp_sap.c b/sigtran/sccp_sap.c
new file mode 100644
index 0000000..207d7ea
--- /dev/null
+++ b/sigtran/sccp_sap.c
@@ -0,0 +1,43 @@
+#include <string.h>
+#include <osmocom/core/utils.h>
+
+#include "sccp_sap.h"
+
+const struct value_string osmo_scu_prim_names[] = {
+	{ OSMO_SCU_PRIM_N_CONNECT,		"N-CONNECT" },
+	{ OSMO_SCU_PRIM_N_DATA,			"N-DATA" },
+	{ OSMO_SCU_PRIM_N_EXPEDITED_DATA,	"N-EXPEDITED-DATA" },
+	{ OSMO_SCU_PRIM_N_DISCONNECT,		"N-DISCONNECT" },
+	{ OSMO_SCU_PRIM_N_RESET,		"N-RESET" },
+	{ OSMO_SCU_PRIM_N_INFORM,		"N-INFORM" },
+	{ OSMO_SCU_PRIM_N_UNITDATA,		"N-UNITDATA" },
+	{ OSMO_SCU_PRIM_N_NOTICE,		"N-NOTICE" },
+	/* management */
+	{ OSMO_SCU_PRIM_N_COORD,		"N-COORD" },
+	{ OSMO_SCU_PRIM_N_STATE,		"N-STATE" },
+	{ OSMO_SCU_PRIM_N_PCSTATE,		"N-PCSATE" },
+	{ 0, NULL }
+};
+
+const struct value_string osmo_prim_op_names[] = {
+	{ PRIM_OP_REQUEST,			"request" },
+	{ PRIM_OP_RESPONSE,			"response" },
+	{ PRIM_OP_INDICATION,			"indication" },
+	{ PRIM_OP_CONFIRM,			"confirm" },
+	{ 0, NULL }
+};
+
+static char prim_name_buf[128];
+
+char *osmo_sccp_prim_name(struct osmo_prim_hdr *oph)
+{
+	const char *name = get_value_string(osmo_scu_prim_names, oph->primitive);
+
+	prim_name_buf[0] = '\0';
+	strncpy(prim_name_buf, name, sizeof(prim_name_buf)-1);
+	prim_name_buf[sizeof(prim_name_buf)-1] = '\0';
+	name = get_value_string(osmo_prim_op_names, oph->operation);
+	strncat(prim_name_buf, name, sizeof(prim_name_buf)-strlen(prim_name_buf)-2);
+
+	return prim_name_buf;
+}
diff --git a/sigtran/sccp_sap.h b/sigtran/sccp_sap.h
new file mode 100644
index 0000000..cd55ec2
--- /dev/null
+++ b/sigtran/sccp_sap.h
@@ -0,0 +1,198 @@
+#pragma once
+
+/* SCCP User SAP description */
+
+/* (C) 2015 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdint.h>
+#include <osmocom/core/prim.h>
+
+#include <netinet/in.h>
+
+#define SCCP_SAP_USER 0x2342
+
+/* detailed coding of primitives at the SAP_SCCP_USER */
+
+/*! \brief SCCP-User primitives as per Q.711 */
+enum osmo_scu_prim_type {
+	/* connection oriented, 6.1.1 */
+	OSMO_SCU_PRIM_N_CONNECT,
+	OSMO_SCU_PRIM_N_DATA,
+	OSMO_SCU_PRIM_N_EXPEDITED_DATA,
+	OSMO_SCU_PRIM_N_DISCONNECT,
+	OSMO_SCU_PRIM_N_RESET,
+	OSMO_SCU_PRIM_N_INFORM,
+	/* connectionless, 6.2.2 */
+	OSMO_SCU_PRIM_N_UNITDATA,
+	OSMO_SCU_PRIM_N_NOTICE,
+	/* management */
+	OSMO_SCU_PRIM_N_COORD,
+	OSMO_SCU_PRIM_N_STATE,
+	OSMO_SCU_PRIM_N_PCSTATE,
+};
+
+#define OSMO_SCCP_ADDR_T_GT	0x0001
+#define OSMO_SCCP_ADDR_T_PC	0x0002
+#define OSMO_SCCP_ADDR_T_SSN	0x0004
+#define OSMO_SCCP_ADDR_T_IPv4	0x0008
+#define OSMO_SCCP_ADDR_T_IPv6	0x0010
+
+/* Q.713 3.4.1 + RFC 3868 3.10.2.3 */
+enum osmo_sccp_routing_ind {
+	OSMO_SCCP_RI_GT,
+	OSMO_SCCP_RI_SSN_PC,
+	OSMO_SCCP_RI_SSN_IP,
+};
+
+/* Q.713 3.4.1 + RFC 3868 3.10.2.3 */
+enum osmo_sccp_gti {
+	OSMO_SCCP_GTI_NO_GT,
+	OSMO_SCCP_GTI_NAI_ONLY,
+	OSMO_SCCP_GTI_TT_ONLY,
+	OSMO_SCCP_GTI_TT_NPL_ENC,
+	OSMO_SCCP_GTI_TT_NPL_ENC_NAI,
+};
+
+/* RFC 3868 3.10.2.3 */
+enum osmo_sccp_npi {
+	OSMO_SCCP_NPI_UNKNOWN = 0,
+	OSMO_SCCP_NPI_E164_ISDN		= 1,
+	OSMO_SCCP_NPI_GEERIC		= 2,
+	OSMO_SCCP_NPI_X121_DATA		= 3,
+	OSMO_SCCP_NPI_F69_TELEX		= 4,
+	OSMO_SCCP_NPI_E210_MARITIME	= 5,
+	OSMO_SCCP_NPI_E212_LAND		= 6,
+	OSMO_SCCP_NPI_E214_ISDN_MOBILE	= 7,
+	OSMO_SCCP_NPI_PRIVATE		= 14,
+};
+
+/* Q.713 3.4.2.3.1 + RFC 3868 3.10.2.3 */
+enum osmo_sccp_nai {
+	OSMO_SCCP_NAI_UNKNOWN		= 0,
+	OSMO_SCCP_NAI_SUBSCR		= 1,
+	OSMO_SCCP_NAI_RES_NAT_USE	= 2,
+	OSMO_SCCP_NAI_NATL		= 3,
+	OSMO_SCCP_NAI_INTL		= 4,
+	/* 5.. 255: Spare */
+};
+
+/* Q.713 3.4.2.2 */
+enum osmo_sccp_ssn {
+	OSMO_SCCP_SSN_MGMT		= 1,
+	OSMO_SCCP_SSN_ISUP		= 3,
+	OSMO_SCCP_SSN_OMAP		= 4,
+	OSMO_SCCP_SSN_MAP		= 5,
+	OSMO_SCCP_SSN_HLR		= 6,
+	OSMO_SCCP_SSN_VLR		= 7,
+	OSMO_SCCP_SSN_MSC		= 8,
+	OSMO_SCCP_SSN_EIR		= 9,
+	OSMO_SCCP_SSN_AUC		= 0x0a,
+	OSMO_SCCP_SSN_ISDN_SS		= 0x0b,
+	OSMO_SCCP_SSN_RES_INTL		= 0x0c,
+	OSMO_SCCP_SSN_BISDN		= 0x0d,
+	OSMO_SCCP_SSN_TC_TEST		= 0x0e,
+};
+
+struct osmo_sccp_gt {
+	uint8_t gti;
+	uint8_t nr_digits;
+	uint8_t tt;
+	uint32_t npi;
+	uint32_t nai;
+	uint8_t digits[32];
+};
+
+struct osmo_sccp_addr {
+	uint32_t presence;
+	struct osmo_sccp_gt gt;
+	uint32_t pc;
+	uint32_t ssn;
+	union {
+		struct in_addr v4;
+		struct in6_addr v6;
+	} ip;
+	/* we don't do hostnames */
+};
+
+/* OSMO_SCU_PRIM_N_CONNECT */
+struct osmo_scu_connect_param {
+	struct osmo_sccp_addr called_addr;
+	struct osmo_sccp_addr calling_addr;
+	struct osmo_sccp_addr responding_addr;
+	//struct osmo_sccp_qos_pars qos_pars;
+	uint32_t sccp_class;
+	uint32_t importance;
+	uint32_t conn_id;
+	/* user data */
+};
+
+/* OSMO_SCU_PRIM_N_DATA / OSMO_SCU_PRIM_N_EXPEDITED_DATA */
+struct osmo_scu_data_param {
+	uint32_t conn_id;
+	uint32_t importance;
+	/* user data */
+};
+
+enum osmo_sccp_originator {
+	OSMO_SCCP_ORIG_NS_PROVIDER,
+	OSMO_SCCP_ORIG_NS_USER,
+	OSMO_SCCP_ORIG_UNDEFINED,
+};
+
+/* OSMO_SCU_PRIM_N_DISCONNECT */
+struct osmo_scu_disconn_param {
+	enum osmo_sccp_originator originator;
+	struct osmo_sccp_addr repsonding_addr;
+	uint32_t cause;
+	uint32_t conn_id;
+	uint32_t importance;
+	/* user data */
+};
+
+/* OSMO_SCU_PRIM_N_RESET */
+struct osmo_scu_reset_param {
+	enum osmo_sccp_originator originator;
+	uint32_t cause;
+	uint32_t conn_id;
+};
+
+/* OSMO_SCU_PRIM_N_UNITDATA */
+struct osmo_scu_unitdata_param {
+	struct osmo_sccp_addr called_addr;
+	struct osmo_sccp_addr calling_addr;
+	uint32_t in_sequence_control;
+	uint32_t return_option;
+	uint32_t importance;
+	/* user data */
+};
+
+struct osmo_scu_prim {
+	struct osmo_prim_hdr oph;
+	union {
+		struct osmo_scu_connect_param connect;
+		struct osmo_scu_data_param data;
+		struct osmo_scu_disconn_param disconnect;
+		struct osmo_scu_reset_param reset;
+		struct osmo_scu_unitdata_param unitdata;
+	} u;
+};
+
+#define msgb_scu_prim(msg) ((struct osmo_scu_prim *)(msg)->l1h)
+
+char *osmo_sccp_prim_name(struct osmo_prim_hdr *oph);
diff --git a/sigtran/sua.c b/sigtran/sua.c
new file mode 100644
index 0000000..e7326cb
--- /dev/null
+++ b/sigtran/sua.c
@@ -0,0 +1,1431 @@
+/* Minimal implementation of RFC 3868 - SCCP User Adaptation Layer */
+
+/* (C) 2015 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdint.h>
+#include <errno.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/write_queue.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/timer.h>
+
+#include <osmocom/netif/stream.h>
+#include <osmocom/sigtran/xua_msg.h>
+
+#include "sccp_sap.h"
+#include "proto_sua.h"
+#include "sua.h"
+
+#define SUA_MSGB_SIZE 1500
+
+/* Appendix C.4 of Q.714 (all in milliseconds) */
+#define CONNECTION_TIMER	( 1 * 60 * 100)
+#define TX_INACT_TIMER		( 7 * 60 * 100)	/* RFC 3868 Ch. 8. */
+#define RX_INACT_TIMER		(15 * 60 * 100) /* RFC 3868 Ch. 8. */
+#define RELEASE_TIMER		(     10 * 100)
+#define RELEASE_REP_TIMER	(     10 * 100)
+#define INT_TIMER		( 1 * 60 * 100)
+#define GUARD_TIMER		(23 * 60 * 100)
+#define RESET_TIMER		(     10 * 100)
+
+static int DSUA = -1;
+
+struct osmo_sua_user {
+	/* global list of SUA users? */
+	struct llist_head list;
+	/* set if we are a server */
+	struct osmo_stream_srv_link *server;
+	struct osmo_stream_cli *client;
+	struct llist_head links;
+	/* user call-back function in case of incoming primitives */
+	osmo_prim_cb prim_cb;
+};
+
+struct sua_link {
+	/* list of SUA links per sua_user */
+	struct llist_head list;
+	/* sua user to which we belong */
+	struct osmo_sua_user *user;
+	/* local list of (SCCP) connections in this link */
+	struct llist_head connections;
+	/* next connection local reference */
+	uint32_t next_id;
+	int is_server;
+	void *data;
+};
+
+enum sua_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,
+};
+
+static const struct value_string conn_state_names[] = {
+	{ S_IDLE, 		"IDLE" },
+	{ S_CONN_PEND_IN,	"CONN_PEND_IN" },
+	{ S_CONN_PEND_OUT,	"CONN_PEND_OUT" },
+	{ S_ACTIVE,		"ACTIVE" },
+	{ S_DISCONN_PEND,	"DISCONN_PEND" },
+	{ S_RESET_IN,		"RESET_IN" },
+	{ S_RESET_OUT,		"RESET_OUT" },
+	{ S_BOTHWAY_RESET,	"BOTHWAY_RESET" },
+	{ S_WAIT_CONN_CONF,	"WAIT_CONN_CONF" },
+	{ 0, NULL }
+};
+
+struct sua_connection {
+	struct llist_head list;
+	struct sua_link *link;
+	struct osmo_sccp_addr calling_addr;
+	struct osmo_sccp_addr called_addr;
+	uint32_t conn_id;
+	uint32_t remote_ref;
+	enum sua_connection_state state;
+	struct osmo_timer_list timer;
+	/* inactivity timers */
+	struct osmo_timer_list tias;
+	struct osmo_timer_list tiar;
+};
+
+
+/***********************************************************************
+ * Message encoding helper functions
+ ***********************************************************************/
+
+#define XUA_HDR(class, type)	((struct xua_common_hdr) { .spare = 0, .msg_class = (class), .msg_type = (type) })
+
+static int msgb_t16l16vp_put(struct msgb *msg, uint16_t tag, uint16_t len, const uint8_t *data)
+{
+	uint8_t *cur;
+	unsigned int rest;
+	unsigned int tlv_len = 4 + len + (4 - (len % 4));
+
+	if (msgb_tailroom(msg) < tlv_len)
+		return -ENOMEM;
+
+	/* tag */
+	msgb_put_u16(msg, tag);
+	/* length */
+	msgb_put_u16(msg, len + 4);
+	/* value */
+	cur = msgb_put(msg, len);
+	memcpy(cur, data, len);
+	/* padding */
+	rest = (4 - (len % 4)) & 0x3;
+	if (rest > 0) {
+		cur = msgb_put(msg, rest);
+		memset(cur, 0, rest);
+	}
+
+	return 0;
+}
+
+static int msgb_t16l16vp_put_u32(struct msgb *msg, uint16_t tag, uint32_t val)
+{
+	uint32_t val_n = htonl(val);
+
+	return msgb_t16l16vp_put(msg, tag, sizeof(val_n), (uint8_t *)&val_n);
+}
+
+static int xua_msg_add_u32(struct xua_msg *xua, uint16_t iei, uint32_t val)
+{
+	uint32_t val_n = htonl(val);
+	return xua_msg_add_data(xua, iei, sizeof(val_n), (uint8_t *) &val_n);
+}
+
+static uint32_t xua_msg_get_u32(struct xua_msg *xua, uint16_t iei)
+{
+	struct xua_msg_part *part = xua_msg_find_tag(xua, iei);
+	uint32_t rc = 0;
+	if (part)
+		rc = ntohl(*(uint32_t *)part->dat);
+	return rc;
+}
+
+static int xua_msg_add_sccp_addr(struct xua_msg *xua, uint16_t iei, const struct osmo_sccp_addr *addr)
+{
+	struct msgb *tmp = msgb_alloc(128, "SCCP Address");
+	int rc;
+
+	if (!tmp)
+		return -ENOMEM;
+
+	msgb_put_u16(tmp, 2); /* route on SSN + PC */
+	msgb_put_u16(tmp, 7); /* always put all addresses on SCCP side */
+
+	if (addr->presence & OSMO_SCCP_ADDR_T_GT) {
+		/* FIXME */
+	}
+	if (addr->presence & OSMO_SCCP_ADDR_T_PC) {
+		msgb_t16l16vp_put_u32(tmp, SUA_IEI_PC, addr->pc);
+	}
+	if (addr->presence & OSMO_SCCP_ADDR_T_SSN) {
+		msgb_t16l16vp_put_u32(tmp, SUA_IEI_SSN, addr->ssn);
+	}
+	if (addr->presence & OSMO_SCCP_ADDR_T_IPv4) {
+		/* FIXME: IPv4 address */
+	} else if (addr->presence & OSMO_SCCP_ADDR_T_IPv6) {
+		/* FIXME: IPv6 address */
+	}
+	rc = xua_msg_add_data(xua, iei, msgb_length(tmp), tmp->data);
+	msgb_free(tmp);
+
+	return rc;
+}
+
+
+/***********************************************************************
+ * SUA Link and Connection handling
+ ***********************************************************************/
+
+static struct sua_link *sua_link_new(struct osmo_sua_user *user, int is_server)
+{
+	struct sua_link *link;
+
+	link = talloc_zero(user, struct sua_link);
+	if (!link)
+		return NULL;
+
+	link->user = user;
+	link->is_server = is_server;
+	INIT_LLIST_HEAD(&link->connections);
+
+	llist_add_tail(&link->list, &user->links);
+
+	return link;
+}
+
+static void conn_destroy(struct sua_connection *conn);
+
+static void sua_link_destroy(struct sua_link *link)
+{
+	struct sua_connection *conn;
+
+	llist_for_each_entry(conn, &link->connections, list)
+		conn_destroy(conn);
+
+	llist_del(&link->list);
+
+	/* FIXME: do we need to cleanup the sccp link? */
+
+	talloc_free(link);
+}
+
+static int sua_link_send(struct sua_link *link, struct msgb *msg)
+{
+	msgb_sctp_ppid(msg) = SUA_PPID;
+
+	if (link->is_server)
+		osmo_stream_srv_send(link->data, msg);
+	else
+		osmo_stream_cli_send(link->data, msg);
+
+	return 0;
+}
+
+static struct sua_connection *conn_find_by_id(struct sua_link *link, uint32_t id)
+{
+	struct sua_connection *conn;
+
+	llist_for_each_entry(conn, &link->connections, list) {
+		if (conn->conn_id == id)
+			return conn;
+	}
+	return NULL;
+}
+
+static void tx_inact_tmr_cb(void *data)
+{
+	struct sua_connection *conn = data;
+	struct xua_msg *xua = xua_msg_alloc();
+	struct msgb *outmsg;
+
+	/* encode + send the CLDT */
+	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COIT);
+	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0);	/* FIXME */
+	xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, 2);
+	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);
+	/* optional: sequence number; credit (both class 3 only) */
+
+	outmsg = xua_to_msg(1, xua);
+	xua_msg_free(xua);
+
+	sua_link_send(conn->link, outmsg);
+}
+
+static void rx_inact_tmr_cb(void *data)
+{
+	struct sua_connection *conn = data;
+
+	/* FIXME: release connection */
+	/* Send N-DISCONNECT.ind to local user */
+	/* Send RLSD to peer */
+	/* enter disconnect pending state with release timer pending */
+}
+
+
+static struct sua_connection *conn_create_id(struct sua_link *link, uint32_t conn_id)
+{
+	struct sua_connection *conn = talloc_zero(link, struct sua_connection);
+
+	conn->conn_id = conn_id;
+	conn->link = link;
+	conn->state = S_IDLE;
+
+	llist_add_tail(&conn->list, &link->connections);
+
+	conn->tias.cb = tx_inact_tmr_cb;
+	conn->tias.data = conn;
+	conn->tiar.cb = rx_inact_tmr_cb;
+	conn->tiar.data = conn;
+
+	return conn;
+}
+
+static struct sua_connection *conn_create(struct sua_link *link)
+{
+	uint32_t conn_id;
+
+	do {
+		conn_id = link->next_id++;
+	} while (conn_find_by_id(link, conn_id));
+
+	return conn_create_id(link, conn_id);
+}
+
+static void conn_destroy(struct sua_connection *conn)
+{
+	/* FIXME: do some cleanup; inform user? */
+	osmo_timer_del(&conn->tias);
+	osmo_timer_del(&conn->tiar);
+	llist_del(&conn->list);
+	talloc_free(conn);
+}
+
+static void conn_state_set(struct sua_connection *conn,
+			   enum sua_connection_state state)
+{
+	DEBUGP(DSUA, "(%u) state chg %s->", conn->conn_id,
+		get_value_string(conn_state_names, conn->state));
+	DEBUGPC(DSUA, "%s\n",
+		get_value_string(conn_state_names, state));
+	conn->state = state;
+}
+
+static void conn_restart_tx_inact_timer(struct sua_connection *conn)
+{
+	osmo_timer_schedule(&conn->tias, TX_INACT_TIMER / 100,
+			    (TX_INACT_TIMER % 100) * 10);
+}
+
+static void conn_restart_rx_inact_timer(struct sua_connection *conn)
+{
+	osmo_timer_schedule(&conn->tiar, RX_INACT_TIMER / 100,
+			    (RX_INACT_TIMER % 100) * 10);
+}
+
+static void conn_start_inact_timers(struct sua_connection *conn)
+{
+	conn_restart_tx_inact_timer(conn);
+	conn_restart_rx_inact_timer(conn);
+}
+
+
+static struct msgb *sua_msgb_alloc(void)
+{
+	return msgb_alloc(SUA_MSGB_SIZE, "SUA Primitive");
+}
+
+
+/***********************************************************************
+ * Handling of messages from the User SAP
+ ***********************************************************************/
+
+/* user program sends us a N-CONNNECT.req to initiate a new connection */
+static int sua_connect_req(struct sua_link *link, struct osmo_scu_prim *prim)
+{
+	struct osmo_scu_connect_param *par = &prim->u.connect;
+	struct xua_msg *xua = xua_msg_alloc();
+	struct sua_connection *conn;
+	struct msgb *outmsg;
+
+	if (par->sccp_class != 2) {
+		LOGP(DSUA, LOGL_ERROR, "N-CONNECT.req for unsupported "
+			"SCCP class %u\n", par->sccp_class);
+		/* FIXME: Send primitive to user */
+		return -EINVAL;
+	}
+
+	conn = conn_create_id(link, par->conn_id);
+	if (!conn) {
+		/* FIXME: Send primitive to user */
+		return -EINVAL;
+	}
+
+	memcpy(&conn->called_addr, &par->called_addr,
+		sizeof(conn->called_addr));
+	memcpy(&conn->calling_addr, &par->calling_addr,
+		sizeof(conn->calling_addr));
+
+	/* encode + send the CLDT */
+	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_CORE);
+	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0);	/* FIXME */
+	xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, par->sccp_class);
+	xua_msg_add_u32(xua, SUA_IEI_SRC_REF, conn->conn_id);
+	xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &par->called_addr);
+	xua_msg_add_u32(xua, SUA_IEI_SEQ_CTRL, 0); /* FIXME */
+	/* sequence number */
+	if (par->calling_addr.presence)
+		xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &par->calling_addr);
+	/* optional: hop count; importance; priority; credit */
+	if (msgb_l2(prim->oph.msg))
+		xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
+				 msgb_l2(prim->oph.msg));
+
+	outmsg = xua_to_msg(1, xua);
+	xua_msg_free(xua);
+
+	/* FIXME: Start CONNECTION_TIMER */
+	conn_state_set(conn, S_CONN_PEND_OUT);
+
+	return sua_link_send(link, outmsg);
+}
+
+/* user program sends us a N-CONNNECT.resp, presumably against a
+ * N-CONNECT.ind */
+static int sua_connect_resp(struct sua_link *link, struct osmo_scu_prim *prim)
+{
+	struct osmo_scu_connect_param *par = &prim->u.connect;
+	struct xua_msg *xua = xua_msg_alloc();
+	struct sua_connection *conn;
+	struct msgb *outmsg;
+
+	/* check if we already know a connection for this conn_id */
+	conn = conn_find_by_id(link, par->conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "N-CONNECT.resp for unknown "
+			"connection ID %u\n", par->conn_id);
+		/* FIXME: Send primitive to user */
+		return -ENODEV;
+	}
+
+	if (conn->state != S_CONN_PEND_IN) {
+		LOGP(DSUA, LOGL_ERROR, "N-CONNECT.resp in wrong state %s\n",
+			get_value_string(conn_state_names, conn->state));
+		/* FIXME: Send primitive to user */
+		return -EINVAL;
+	}
+
+	/* encode + send the COAK message */
+	xua = xua_msg_alloc();
+	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_COAK);
+	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0);	/* FIXME */
+	xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, par->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);	/* FIXME */
+	/* sequence number */
+	if (par->calling_addr.presence)
+		xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &par->calling_addr);
+	/* optional: hop count; importance; priority */
+	/* FIXME: destination address will be present in case the CORE
+	 * message conveys the source address parameter */
+	if (par->called_addr.presence)
+		xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &par->called_addr);
+	if (msgb_l2(prim->oph.msg))
+		xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
+				 msgb_l2(prim->oph.msg));
+
+	outmsg = xua_to_msg(1, xua);
+	xua_msg_free(xua);
+
+	conn_state_set(conn, S_ACTIVE);
+	conn_start_inact_timers(conn);
+
+	return sua_link_send(link, outmsg);
+}
+
+/* user wants to send connection-oriented data */
+static int sua_data_req(struct sua_link *link, struct osmo_scu_prim *prim)
+{
+	struct osmo_scu_data_param *par = &prim->u.data;
+	struct xua_msg *xua;
+	struct sua_connection *conn;
+	struct msgb *outmsg;
+
+	/* check if we know about this conncetion, and obtain reference */
+	conn = conn_find_by_id(link, par->conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "N-DATA.req for unknown "
+			"connection ID %u\n", par->conn_id);
+		/* FIXME: Send primitive to user */
+		return -ENODEV;
+	}
+
+	if (conn->state != S_ACTIVE) {
+		LOGP(DSUA, LOGL_ERROR, "N-DATA.req in wrong state %s\n",
+			get_value_string(conn_state_names, conn->state));
+		/* FIXME: Send primitive to user */
+		return -EINVAL;
+	}
+
+	conn_restart_tx_inact_timer(conn);
+
+	/* encode + send the CODT message */
+	xua = xua_msg_alloc();
+	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_CODT);
+	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0);	/* FIXME */
+	/* 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));
+
+	outmsg = xua_to_msg(1, xua);
+	xua_msg_free(xua);
+
+	return sua_link_send(link, outmsg);
+}
+
+/* user wants to disconnect a connection */
+static int sua_disconnect_req(struct sua_link *link, struct osmo_scu_prim *prim)
+{
+	struct osmo_scu_disconn_param *par = &prim->u.disconnect;
+	struct xua_msg *xua;
+	struct sua_connection *conn;
+	struct msgb *outmsg;
+
+	/* resolve reference of connection */
+	conn = conn_find_by_id(link, par->conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "N-DISCONNECT.resp for unknown "
+			"connection ID %u\n", par->conn_id);
+		/* FIXME: Send primitive to user */
+		return -ENODEV;
+	}
+
+	/* encode + send the RELRE */
+	xua = xua_msg_alloc();
+	xua->hdr = XUA_HDR(SUA_MSGC_CO, SUA_CO_RELRE);
+	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0);	/* FIXME */
+	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, par->cause);
+	/* optional: importance */
+	if (msgb_l2(prim->oph.msg))
+		xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
+				 msgb_l2(prim->oph.msg));
+
+	outmsg = xua_to_msg(1, xua);
+	xua_msg_free(xua);
+
+	conn_state_set(conn, S_DISCONN_PEND);
+
+	return sua_link_send(link, outmsg);
+}
+
+/* user wants to send connectionless data */
+static int sua_unitdata_req(struct sua_link *link, struct osmo_scu_prim *prim)
+{
+	struct osmo_scu_unitdata_param *par = &prim->u.unitdata;
+	struct xua_msg *xua = xua_msg_alloc();
+	struct msgb *outmsg;
+
+	/* encode + send the CLDT */
+	xua->hdr = XUA_HDR(SUA_MSGC_CL, SUA_CL_CLDT);
+	xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0);	/* FIXME */
+	xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, 0);
+	xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &par->calling_addr);
+	xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &par->called_addr);
+	xua_msg_add_u32(xua, SUA_IEI_SEQ_CTRL, par->in_sequence_control);
+	/* optional: importance, ... correlation id? */
+	xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
+			 msgb_l2(prim->oph.msg));
+
+	outmsg = xua_to_msg(1, xua);
+	xua_msg_free(xua);
+
+	return sua_link_send(link, outmsg);
+}
+
+/* user hands us a SCCP-USER SAP primitive down into the stack */
+int osmo_osmo_sua_user_link_down(struct sua_link *link, struct osmo_prim_hdr *oph)
+{
+	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
+	struct msgb *msg = prim->oph.msg;
+	int rc = 0;
+
+	LOGP(DSUA, LOGL_DEBUG, "Received SCCP User Primitive (%s)\n",
+		osmo_sccp_prim_name(&prim->oph));
+
+	switch (OSMO_PRIM_HDR(&prim->oph)) {
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_REQUEST):
+		rc = sua_connect_req(link, prim);
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_RESPONSE):
+		rc = sua_connect_resp(link, prim);
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_REQUEST):
+		rc = sua_data_req(link, prim);
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_REQUEST):
+		rc = sua_disconnect_req(link, prim);
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_REQUEST):
+		rc = sua_unitdata_req(link, prim);
+		break;
+	default:
+		rc = -1;
+	}
+
+	if (rc != 1)
+		msgb_free(msg);
+
+	return rc;
+}
+
+
+/***********************************************************************
+ * Mandatory IE checking
+ ***********************************************************************/
+
+#define MAND_IES(msgt, ies)	[msgt] = (ies)
+
+static const uint16_t cldt_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_PROTO_CLASS, SUA_IEI_SRC_ADDR,
+	SUA_IEI_DEST_ADDR, SUA_IEI_SEQ_CTRL, SUA_IEI_DATA, 0
+};
+
+static const uint16_t cldr_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_CAUSE, SUA_IEI_SRC_ADDR,
+	SUA_IEI_DEST_ADDR, 0
+};
+
+static const uint16_t codt_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_DATA, 0
+};
+
+static const uint16_t coda_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, 0
+};
+
+static const uint16_t core_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_PROTO_CLASS, SUA_IEI_SRC_REF,
+	SUA_IEI_DEST_ADDR, SUA_IEI_SEQ_CTRL, 0
+};
+
+static const uint16_t coak_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_PROTO_CLASS, SUA_IEI_DEST_REF,
+	SUA_IEI_SRC_REF, SUA_IEI_SEQ_CTRL, 0
+};
+
+static const uint16_t coref_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_CAUSE, 0
+};
+
+static const uint16_t relre_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_SRC_REF,
+	SUA_IEI_CAUSE, 0
+};
+
+static const uint16_t relco_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_SRC_REF, 0
+};
+
+static const uint16_t resre_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_SRC_REF,
+	SUA_IEI_CAUSE, 0
+};
+
+static const uint16_t resco_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_SRC_REF, 0
+};
+
+static const uint16_t coerr_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_DEST_REF, SUA_IEI_CAUSE, 0
+};
+
+static const uint16_t coit_mand_ies[] = {
+	SUA_IEI_ROUTE_CTX, SUA_IEI_PROTO_CLASS, SUA_IEI_SRC_REF,
+	SUA_IEI_DEST_REF, 0
+};
+
+static const uint16_t *mand_ies_cl[256] = {
+	MAND_IES(SUA_CL_CLDT, cldt_mand_ies),
+	MAND_IES(SUA_CL_CLDR, cldr_mand_ies),
+};
+
+static const uint16_t *mand_ies_co[256] = {
+	MAND_IES(SUA_CO_CODT, codt_mand_ies),
+	MAND_IES(SUA_CO_CODA, coda_mand_ies),
+	MAND_IES(SUA_CO_CORE, core_mand_ies),
+	MAND_IES(SUA_CO_COAK, coak_mand_ies),
+	MAND_IES(SUA_CO_COREF, coref_mand_ies),
+	MAND_IES(SUA_CO_RELRE, relre_mand_ies),
+	MAND_IES(SUA_CO_RELCO, relco_mand_ies),
+	MAND_IES(SUA_CO_RESRE, resre_mand_ies),
+	MAND_IES(SUA_CO_RESCO, resco_mand_ies),
+	MAND_IES(SUA_CO_COERR, coerr_mand_ies),
+	MAND_IES(SUA_CO_COIT, coit_mand_ies),
+};
+
+static int check_all_mand_ies(const uint16_t **mand_ies, struct xua_msg *xua)
+{
+	uint8_t msg_type = xua->hdr.msg_type;
+	const uint16_t *ies = mand_ies[msg_type];
+	uint16_t ie;
+
+	for (ie = *ies; ie; ie = *ies++) {
+		if (!xua_msg_find_tag(xua, ie)) {
+			LOGP(DSUA, LOGL_ERROR, "SUA Message %u:%u should "
+				"contain IE 0x%04x, but doesn't\n",
+				xua->hdr.msg_class, msg_type, ie);
+			return 0;
+		}
+	}
+
+	return 1;
+}
+
+
+/***********************************************************************
+ * Receiving SUA messsages from SCTP
+ ***********************************************************************/
+
+static int sua_parse_addr(struct osmo_sccp_addr *out,
+			  struct xua_msg *xua,
+			  uint16_t iei)
+{
+	const struct xua_msg_part *param = xua_msg_find_tag(xua, iei);
+
+	if (!param)
+		return -ENODEV;
+
+	/* FIXME */
+	return 0;
+}
+
+static int sua_rx_cldt(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct osmo_scu_unitdata_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
+	struct msgb *upmsg = sua_msgb_alloc();
+	uint32_t protocol_class;
+
+	/* fill primitive */
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.unitdata;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_UNITDATA,
+			PRIM_OP_INDICATION, upmsg);
+	sua_parse_addr(&param->called_addr, xua, SUA_IEI_DEST_ADDR);
+	sua_parse_addr(&param->calling_addr, xua, SUA_IEI_SRC_ADDR);
+	param->in_sequence_control = xua_msg_get_u32(xua, SUA_IEI_SEQ_CTRL);
+	protocol_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS);
+	param->return_option = protocol_class & 0x80;
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+
+	/* copy data */
+	upmsg->l2h = msgb_put(upmsg, data_ie->len);
+	memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	return 0;
+}
+
+
+/* connectioness messages received from socket */
+static int sua_rx_cl(struct sua_link *link,
+		     struct xua_msg *xua, struct msgb *msg)
+{
+	int rc = -1;
+
+	if (!check_all_mand_ies(mand_ies_cl, xua))
+		return -1;
+
+	switch (xua->hdr.msg_type) {
+	case SUA_CL_CLDT:
+		rc = sua_rx_cldt(link, xua);
+		break;
+	case SUA_CL_CLDR:
+	default:
+		break;
+	}
+
+	return rc;
+}
+
+/* RFC 3868 3.3.3 / SCCP CR */
+static int sua_rx_core(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct osmo_scu_connect_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
+	struct msgb *upmsg;
+	struct sua_connection *conn;
+	uint8_t *cur;
+
+	/* fill conn */
+	conn = conn_create(link);
+	sua_parse_addr(&conn->called_addr, xua, SUA_IEI_DEST_ADDR);
+	sua_parse_addr(&conn->calling_addr, xua, SUA_IEI_SRC_ADDR);
+	conn->remote_ref = xua_msg_get_u32(xua, SUA_IEI_SRC_REF);
+
+	/* fill primitive */
+	upmsg = sua_msgb_alloc();
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.connect;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_CONNECT,
+			PRIM_OP_INDICATION, upmsg);
+	param->conn_id = conn->conn_id;
+	memcpy(&param->called_addr, &conn->called_addr,
+		sizeof(param->called_addr));
+	memcpy(&param->calling_addr, &conn->calling_addr,
+		sizeof(param->calling_addr));
+	//param->in_sequence_control;
+	param->sccp_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS) & 3;
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+
+	if (data_ie) {
+		/* copy data */
+		upmsg->l2h = msgb_put(upmsg, data_ie->len);
+		memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
+	}
+
+	conn_state_set(conn, S_CONN_PEND_IN);
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	return 0;
+}
+
+/* RFC 3868 3.3.4 / SCCP CC */
+static int sua_rx_coak(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct sua_connection *conn;
+	struct osmo_scu_connect_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
+	struct msgb *upmsg;
+	uint32_t conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
+
+	/* resolve conn */
+	conn = conn_find_by_id(link, conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "COAK for unknwon reference %u\n",
+			conn_id);
+		/* FIXME: error message? */
+		return -1;
+	}
+	conn_restart_rx_inact_timer(conn);
+
+	if (conn->state != S_CONN_PEND_OUT) {
+		LOGP(DSUA, LOGL_ERROR, "COAK in wrong state %s\n",
+			get_value_string(conn_state_names, conn->state));
+		/* FIXME: error message? */
+		return -EINVAL;
+	}
+
+	/* track remote reference */
+	conn->remote_ref = xua_msg_get_u32(xua, SUA_IEI_SRC_REF);
+
+	/* fill primitive */
+	upmsg = sua_msgb_alloc();
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.connect;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_CONNECT,
+			PRIM_OP_CONFIRM, upmsg);
+	param->conn_id = conn->conn_id;
+	memcpy(&param->called_addr, &conn->called_addr,
+		sizeof(param->called_addr));
+	memcpy(&param->calling_addr, &conn->calling_addr,
+		sizeof(param->calling_addr));
+	//param->in_sequence_control;
+	param->sccp_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS) & 3;
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+
+	if (data_ie) {
+		/* copy data */
+		upmsg->l2h = msgb_put(upmsg, data_ie->len);
+		memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
+	}
+
+	conn_state_set(conn, S_ACTIVE);
+	conn_start_inact_timers(conn);
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	return 0;
+}
+
+/* RFC 3868 3.3.5 / SCCP CREF */
+static int sua_rx_coref(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct sua_connection *conn;
+	struct osmo_scu_connect_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
+	struct msgb *upmsg;
+	uint32_t conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
+	uint32_t cause;
+
+	/* resolve conn */
+	conn = conn_find_by_id(link, conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "COREF for unknwon reference %u\n",
+			conn_id);
+		/* FIXME: error message? */
+		return -1;
+	}
+	conn_restart_rx_inact_timer(conn);
+
+	/* fill primitive */
+	upmsg = sua_msgb_alloc();
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.connect;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_DISCONNECT,
+			PRIM_OP_INDICATION, upmsg);
+	param->conn_id = conn_id;
+	memcpy(&param->called_addr, &conn->called_addr,
+		sizeof(param->called_addr));
+	memcpy(&param->calling_addr, &conn->calling_addr,
+		sizeof(param->calling_addr));
+	//param->in_sequence_control;
+	cause = xua_msg_get_u32(xua, SUA_IEI_CAUSE);
+	/* optional: src addr */
+	/* optional: dest addr */
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+	if (data_ie) {
+		/* copy data */
+		upmsg->l2h = msgb_put(upmsg, data_ie->len);
+		memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
+	}
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	conn_state_set(conn, S_IDLE);
+	conn_destroy(conn);
+
+	return 0;
+}
+
+/* RFC 3868 3.3.6 / SCCP RLSD */
+static int sua_rx_relre(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct sua_connection *conn;
+	struct osmo_scu_connect_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
+	struct msgb *upmsg;
+	uint32_t conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
+	uint32_t cause;
+
+	/* resolve conn */
+	conn = conn_find_by_id(link, conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "RELRE for unknwon reference %u\n",
+			conn_id);
+		/* FIXME: error message? */
+		return -1;
+	}
+
+	/* fill primitive */
+	upmsg = sua_msgb_alloc();
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.connect;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_DISCONNECT,
+			PRIM_OP_INDICATION, upmsg); /* what primitive? */
+
+	param->conn_id = conn_id;
+	/* source reference */
+	cause = xua_msg_get_u32(xua, SUA_IEI_CAUSE);
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+	if (data_ie) {
+		/* copy data */
+		upmsg->l2h = msgb_put(upmsg, data_ie->len);
+		memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
+	}
+
+	memcpy(&param->called_addr, &conn->called_addr,
+		sizeof(param->called_addr));
+	memcpy(&param->calling_addr, &conn->calling_addr,
+		sizeof(param->calling_addr));
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	conn_state_set(conn, S_IDLE);
+	conn_destroy(conn);
+
+	return 0;
+}
+
+/* RFC 3868 3.3.7 / SCCP RLC */
+static int sua_rx_relco(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct sua_connection *conn;
+	struct osmo_scu_connect_param *param;
+	struct msgb *upmsg;
+	uint32_t conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
+
+	/* resolve conn */
+	conn = conn_find_by_id(link, conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "RELCO for unknwon reference %u\n",
+			conn_id);
+		/* FIXME: error message? */
+		return -1;
+	}
+	conn_restart_rx_inact_timer(conn);
+
+	/* fill primitive */
+	upmsg = sua_msgb_alloc();
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.connect;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_DISCONNECT,
+			PRIM_OP_CONFIRM, upmsg); /* what primitive? */
+
+	param->conn_id = conn_id;
+	/* source reference */
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+
+	memcpy(&param->called_addr, &conn->called_addr,
+		sizeof(param->called_addr));
+	memcpy(&param->calling_addr, &conn->calling_addr,
+		sizeof(param->calling_addr));
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	conn_destroy(conn);
+
+	return 0;
+
+}
+
+/* RFC3868 3.3.1 / SCCP DT1 */
+static int sua_rx_codt(struct sua_link *link, struct xua_msg *xua)
+{
+	struct osmo_scu_prim *prim;
+	struct sua_connection *conn;
+	struct osmo_scu_data_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
+	struct msgb *upmsg;
+	uint32_t conn_id = xua_msg_get_u32(xua, SUA_IEI_DEST_REF);
+	uint8_t *cur;
+
+	/* resolve conn */
+	conn = conn_find_by_id(link, conn_id);
+	if (!conn) {
+		LOGP(DSUA, LOGL_ERROR, "DT1 for unknwon reference %u\n",
+			conn_id);
+		/* FIXME: error message? */
+		return -1;
+	}
+
+	if (conn->state != S_ACTIVE) {
+		LOGP(DSUA, LOGL_ERROR, "DT1 in invalid state %s\n",
+			get_value_string(conn_state_names, conn->state));
+		/* FIXME: error message? */
+		return -1;
+	}
+
+	conn_restart_rx_inact_timer(conn);
+
+	/* fill primitive */
+	upmsg = sua_msgb_alloc();
+	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.data;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_DATA,
+			PRIM_OP_INDICATION, upmsg);
+	param->conn_id = conn_id;
+	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
+
+	/* copy data */
+	upmsg->l2h = msgb_put(upmsg, data_ie->len);
+	memcpy(upmsg->l2h, data_ie->dat, data_ie->len);
+
+	/* send to user SAP */
+	link->user->prim_cb(&prim->oph, link);
+
+	return 0;
+}
+
+
+/* connection-oriented messages received from socket */
+static int sua_rx_co(struct sua_link *link,
+		     struct xua_msg *xua, struct msgb *msg)
+{
+	int rc = -1;
+
+	if (!check_all_mand_ies(mand_ies_co, xua))
+		return -1;
+
+	switch (xua->hdr.msg_type) {
+	case SUA_CO_CORE:
+		rc = sua_rx_core(link, xua);
+		break;
+	case SUA_CO_COAK:
+		rc = sua_rx_coak(link, xua);
+		break;
+	case SUA_CO_COREF:
+		rc = sua_rx_coref(link, xua);
+		break;
+	case SUA_CO_RELRE:
+		rc = sua_rx_relre(link, xua);
+		break;
+	case SUA_CO_RELCO:
+		rc = sua_rx_relco(link, xua);
+		break;
+	case SUA_CO_CODT:
+		rc = sua_rx_codt(link, xua);
+		break;
+	case SUA_CO_RESCO:
+	case SUA_CO_RESRE:
+	case SUA_CO_CODA:
+	case SUA_CO_COERR:
+	case SUA_CO_COIT:
+		/* FIXME */
+	default:
+		break;
+	}
+
+	return rc;
+}
+
+/* process SUA message received from socket */
+static int sua_rx_msg(struct sua_link *link, struct msgb *msg)
+{
+	struct xua_msg *xua;
+	int rc = -1;
+
+	xua = xua_from_msg(1, msgb_length(msg), msg->data);
+	if (!xua) {
+		LOGP(DSUA, LOGL_ERROR, "Unable to parse incoming "
+			"SUA message\n");
+		return -EIO;
+	}
+
+	LOGP(DSUA, LOGL_DEBUG, "Received SUA Message (%u:%u)\n",
+		xua->hdr.msg_class, xua->hdr.msg_type);
+
+	switch (xua->hdr.msg_class) {
+	case SUA_MSGC_CL:
+		rc = sua_rx_cl(link, xua, msg);
+		break;
+	case SUA_MSGC_CO:
+		rc = sua_rx_co(link, xua, msg);
+		break;
+	case SUA_MSGC_MGMT:
+	case SUA_MSGC_SNM:
+	case SUA_MSGC_ASPSM:
+	case SUA_MSGC_ASPTM:
+	case SUA_MSGC_RKM:
+		/* FIXME */
+	default:
+		break;
+	}
+
+	xua_msg_free(xua);
+
+	return rc;
+}
+
+/***********************************************************************
+ * libosmonetif integration
+ ***********************************************************************/
+
+#include <osmocom/netif/stream.h>
+#include <netinet/sctp.h>
+
+/* netif code tells us we can read something from the socket */
+static int sua_srv_conn_cb(struct osmo_stream_srv *conn)
+{
+	struct osmo_fd *ofd = osmo_stream_srv_get_ofd(conn);
+	struct sua_link *link = osmo_stream_srv_get_data(conn);
+	struct msgb *msg = msgb_alloc(SUA_MSGB_SIZE, "SUA Server Rx");
+	struct sctp_sndrcvinfo sinfo;
+	unsigned int ppid;
+	int flags = 0;
+	int rc;
+
+	if (!msg)
+		return -ENOMEM;
+
+	/* read SUA message from socket and process it */
+	rc = sctp_recvmsg(ofd->fd, msgb_data(msg), msgb_tailroom(msg),
+			  NULL, NULL, &sinfo, &flags);
+	if (rc < 0) {
+		close(ofd->fd);
+		osmo_fd_unregister(ofd);
+		ofd->fd = -1;
+		return rc;
+	} else if (rc == 0) {
+		close(ofd->fd);
+		osmo_fd_unregister(ofd);
+		ofd->fd = -1;
+	} else {
+		msgb_put(msg, rc);
+	}
+
+	if (flags & MSG_NOTIFICATION) {
+		msgb_free(msg);
+		return 0;
+	}
+
+	ppid = ntohl(sinfo.sinfo_ppid);
+	msgb_sctp_ppid(msg) = ppid;
+	msgb_sctp_stream(msg) = ntohl(sinfo.sinfo_stream);
+	msg->dst = link;
+
+	switch (ppid) {
+	case SUA_PPID:
+		rc = sua_rx_msg(link, msg);
+		break;
+	default:
+		LOGP(DSUA, LOGL_NOTICE, "SCTP chunk for unknown PPID %u "
+			"received\n", ppid);
+		rc = 0;
+		break;
+	}
+
+	msgb_free(msg);
+	return rc;
+}
+
+static int sua_srv_conn_closed_cb(struct osmo_stream_srv *srv)
+{
+	struct sua_link *sual = osmo_stream_srv_get_data(srv);
+	struct sua_connection *conn;
+
+	LOGP(DSUA, LOGL_INFO, "SCTP connection closed\n");
+
+	/* remove from per-user list of sua links */
+	llist_del(&sual->list);
+
+	llist_for_each_entry(conn, &sual->connections, list) {
+		/* FIXME: send RELEASE request */
+	}
+	talloc_free(sual);
+	osmo_stream_srv_set_data(srv, NULL);
+
+	return 0;
+}
+
+static int sua_accept_cb(struct osmo_stream_srv_link *link, int fd)
+{
+	struct osmo_sua_user *user = osmo_stream_srv_link_get_data(link);
+	struct osmo_stream_srv *srv;
+	struct sua_link *sual;
+
+	LOGP(DSUA, LOGL_INFO, "New SCTP connection accepted\n");
+
+	srv = osmo_stream_srv_create(user, link, fd,
+				     sua_srv_conn_cb,
+				     sua_srv_conn_closed_cb, NULL);
+	if (!srv)
+		close(fd);
+
+	/* create new SUA link and connect both data structures */
+	sual = sua_link_new(user, 1);
+	if (!sual) {
+		osmo_stream_srv_destroy(srv);
+		return -1;
+	}
+	sual->data = srv;
+	osmo_stream_srv_set_data(srv, sual);
+
+	return 0;
+}
+
+int sua_server_listen(struct osmo_sua_user *user, const char *hostname, uint16_t port)
+{
+	int rc;
+
+	if (user->server)
+		osmo_stream_srv_link_close(user->server);
+	else {
+		user->server = osmo_stream_srv_link_create(user);
+		osmo_stream_srv_link_set_data(user->server, user);
+		osmo_stream_srv_link_set_accept_cb(user->server, sua_accept_cb);
+	}
+
+	osmo_stream_srv_link_set_addr(user->server, hostname);
+	osmo_stream_srv_link_set_port(user->server, port);
+	osmo_stream_srv_link_set_proto(user->server, IPPROTO_SCTP);
+
+	rc = osmo_stream_srv_link_open(user->server);
+	if (rc < 0) {
+		osmo_stream_srv_link_destroy(user->server);
+		user->server = NULL;
+		return rc;
+	}
+
+	return 0;
+}
+
+/* netif code tells us we can read something from the socket */
+static int sua_cli_conn_cb(struct osmo_stream_cli *conn)
+{
+	struct osmo_fd *ofd = osmo_stream_cli_get_ofd(conn);
+	struct sua_link *link = osmo_stream_cli_get_data(conn);
+	struct msgb *msg = msgb_alloc(SUA_MSGB_SIZE, "SUA Client Rx");
+	struct sctp_sndrcvinfo sinfo;
+	unsigned int ppid;
+	int flags = 0;
+	int rc;
+
+	if (!msg)
+		return -ENOMEM;
+
+	/* read SUA message from socket and process it */
+	rc = sctp_recvmsg(ofd->fd, msgb_data(msg), msgb_tailroom(msg),
+			  NULL, NULL, &sinfo, &flags);
+	if (rc < 0) {
+		close(ofd->fd);
+		osmo_fd_unregister(ofd);
+		ofd->fd = -1;
+		return rc;
+	} else if (rc == 0) {
+		close(ofd->fd);
+		osmo_fd_unregister(ofd);
+		ofd->fd = -1;
+	} else {
+		msgb_put(msg, rc);
+	}
+
+	if (flags & MSG_NOTIFICATION) {
+		msgb_free(msg);
+		return 0;
+	}
+
+	ppid = ntohl(sinfo.sinfo_ppid);
+	msgb_sctp_ppid(msg) = ppid;
+	msgb_sctp_stream(msg) = ntohl(sinfo.sinfo_stream);
+	msg->dst = link;
+
+	switch (ppid) {
+	case SUA_PPID:
+		rc = sua_rx_msg(link, msg);
+		break;
+	default:
+		LOGP(DSUA, LOGL_NOTICE, "SCTP chunk for unknown PPID %u "
+			"received\n", ppid);
+		rc = 0;
+		break;
+	}
+
+	msgb_free(msg);
+	return rc;
+}
+
+int sua_client_connect(struct osmo_sua_user *user, const char *hostname, uint16_t port)
+{
+	struct osmo_stream_cli *cli;
+	struct sua_link *sual;
+	int rc;
+
+	cli = osmo_stream_cli_create(user);
+	if (!cli)
+		return -1;
+	osmo_stream_cli_set_addr(cli, hostname);
+	osmo_stream_cli_set_port(cli, port);
+	osmo_stream_cli_set_proto(cli, IPPROTO_SCTP);
+	osmo_stream_cli_set_reconnect_timeout(cli, 5);
+	osmo_stream_cli_set_read_cb(cli, sua_cli_conn_cb);
+
+	/* create SUA link and associate it with stream_cli */
+	sual = sua_link_new(user, 0);
+	if (!sual) {
+		osmo_stream_cli_destroy(cli);
+		return -1;
+	}
+	sual->data = cli;
+	osmo_stream_cli_set_data(cli, sual);
+
+	rc = osmo_stream_cli_open(cli);
+	if (rc < 0) {
+		sua_link_destroy(sual);
+		osmo_stream_cli_destroy(cli);
+		return rc;
+	}
+	user->client = cli;
+
+	return 0;
+}
+
+struct sua_link *sua_client_get_link(struct osmo_sua_user *user)
+{
+	return osmo_stream_cli_get_data(user->client);
+}
+
+static LLIST_HEAD(sua_users);
+
+struct osmo_sua_user *osmo_sua_user_create(void *ctx, osmo_prim_cb prim_cb)
+{
+	struct osmo_sua_user *user = talloc_zero(ctx, struct osmo_sua_user);
+
+	user->prim_cb = prim_cb;
+	INIT_LLIST_HEAD(&user->links);
+
+	llist_add_tail(&user->list, &sua_users);
+
+	return user;
+}
+
+void osmo_sua_user_destroy(struct osmo_sua_user *user)
+{
+	struct sua_link *link;
+
+	llist_del(&user->list);
+
+	llist_for_each_entry(link, &user->links, list)
+		sua_link_destroy(link);
+
+	talloc_free(user);
+}
+
+void osmo_sua_set_log_area(int area)
+{
+	DSUA = area;
+}
diff --git a/sigtran/sua.h b/sigtran/sua.h
new file mode 100644
index 0000000..add2fd8
--- /dev/null
+++ b/sigtran/sua.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/core/prim.h>
+
+struct osmo_sua_user;
+
+void osmo_sua_set_log_area(int area);
+
+struct osmo_sua_user *osmo_sua_user_create(void *ctx, osmo_prim_cb prim_cb);
+void osmo_sua_user_destroy(struct osmo_sua_user *user);
+
+int sua_server_listen(struct osmo_sua_user *user, const char *hostname, uint16_t port);
+
+int sua_client_connect(struct osmo_sua_user *user, const char *hostname, uint16_t port);
+struct sua_link *sua_client_get_link(struct osmo_sua_user *user);
+
+/* user hands us a SCCP-USER SAP primitive down into the stack */
+int osmo_osmo_sua_user_link_down(struct sua_link *link, struct osmo_prim_hdr *oph);
+
diff --git a/sigtran/sua_client_test.c b/sigtran/sua_client_test.c
new file mode 100644
index 0000000..552e0f0
--- /dev/null
+++ b/sigtran/sua_client_test.c
@@ -0,0 +1,57 @@
+#include "sua_test_common.h"
+
+struct osmo_sua_user *g_user;
+struct sua_link *g_link;
+
+static int sccp_sap_up(struct osmo_prim_hdr *oph, void *link)
+{
+	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
+	struct osmo_prim_hdr *resp = NULL;
+	uint8_t payload[] = { 0xa1, 0xa2, 0xa3 };
+
+	printf("sccp_sap_up(%s)\n", osmo_sccp_prim_name(oph));
+
+	switch (OSMO_PRIM_HDR(oph)) {
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM):
+		printf("N-CONNECT.ind(%u), issuing DATA.req\n",
+			prim->u.connect.conn_id);
+		resp = make_dt1_req(prim->u.connect.conn_id, payload, sizeof(payload));
+		break;
+	}
+
+	if (resp)
+		osmo_osmo_sua_user_link_down(link, resp);
+
+	msgb_free(oph->msg);
+	return 0;
+}
+
+
+int main(int argc, char **argv)
+{
+	void *ctx = talloc_named_const(NULL, 1, "root");
+	int rc;
+
+	osmo_sua_set_log_area(DSUA);
+	xua_set_log_area(DXUA);
+
+	osmo_init_logging(&test_log_info);
+
+	g_user = osmo_sua_user_create(ctx, sccp_sap_up);
+
+	rc = sua_client_connect(g_user, "127.0.0.1", 2342);
+	if (rc < 0) {
+		exit(1);
+	}
+
+	g_link = sua_client_get_link(g_user);
+
+	int i = 8000;
+
+	while (1) {
+		if (i < 8010)
+			tx_conn_req(g_link, i++);
+		//tx_unitdata(g_link);
+		osmo_select_main(0);
+	}
+}
diff --git a/sigtran/sua_server_test.c b/sigtran/sua_server_test.c
new file mode 100644
index 0000000..be123c2
--- /dev/null
+++ b/sigtran/sua_server_test.c
@@ -0,0 +1,79 @@
+#include "sua_test_common.h"
+
+struct osmo_prim_hdr *make_conn_resp(struct osmo_scu_connect_param *param)
+{
+	struct msgb *msg = msgb_alloc(1024, "conn_resp");
+	struct osmo_scu_prim *prim;
+
+	prim = (struct osmo_scu_prim *) msgb_put(msg, sizeof(*prim));
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_CONNECT,
+			PRIM_OP_RESPONSE, msg);
+	memcpy(&prim->u.connect, param, sizeof(prim->u.connect));
+	return &prim->oph;
+}
+
+static int sccp_sap_up(struct osmo_prim_hdr *oph, void *link)
+{
+	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
+	struct osmo_prim_hdr *resp = NULL;
+	const uint8_t payload[] = { 0xb1, 0xb2, 0xb3 };
+
+	printf("sccp_sap_up(%s)\n", osmo_sccp_prim_name(oph));
+
+	switch (OSMO_PRIM_HDR(oph)) {
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM):
+		/* confirmation of outbound connection */
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION):
+		/* indication of new inbound connection request*/
+		printf("N-CONNECT.ind(X->%u)\n", prim->u.connect.conn_id);
+		resp = make_conn_resp(&prim->u.connect);
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION):
+		/* indication of disconnect */
+		printf("N-DISCONNECT.ind(%u)\n", prim->u.disconnect.conn_id);
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION):
+		/* connection-oriented data received */
+		printf("N-DATA.ind(%u, %s)\n", prim->u.data.conn_id,
+			osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
+		resp = make_dt1_req(prim->u.data.conn_id, payload, sizeof(payload));
+		break;
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_INDICATION):
+		/* connection-oriented data received */
+		printf("N-UNITDATA.ind(%s)\n", 
+			osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
+		tx_unitdata(link);
+		break;
+	}
+
+	if (resp)
+		osmo_osmo_sua_user_link_down(link, resp);
+
+	msgb_free(oph->msg);
+	return 0;
+}
+
+int main(int argc, char **argv)
+{
+	struct osmo_sua_user *user;
+	void *ctx = talloc_named_const(NULL, 1, "root");
+	int rc;
+
+	osmo_sua_set_log_area(DSUA);
+	xua_set_log_area(DXUA);
+
+	osmo_init_logging(&test_log_info);
+
+	user = osmo_sua_user_create(ctx, sccp_sap_up);
+
+	rc = sua_server_listen(user, "127.0.0.1", 2342);
+	if (rc < 0) {
+		exit(1);
+	}
+
+	while (1) {
+		osmo_select_main(0);
+	}
+}
diff --git a/sigtran/sua_test_common.c b/sigtran/sua_test_common.c
new file mode 100644
index 0000000..92c2b38
--- /dev/null
+++ b/sigtran/sua_test_common.c
@@ -0,0 +1,95 @@
+#include "sua_test_common.h"
+
+static const struct log_info_cat log_cat[] = {
+	[DMAIN] = {
+		.name = "DMAIN", .loglevel = LOGL_DEBUG, .enabled = 1,
+		.color = "",
+		.description = "Main program",
+	},
+	[DSUA] = {
+		.name = "DSUA", .loglevel = LOGL_DEBUG, .enabled = 1,
+		.color = "",
+		.description = "SCCP User Adaption",
+	},
+	[DXUA] = {
+		.name = "DXUA", .loglevel = LOGL_DEBUG, .enabled = 1,
+		.color = "",
+		.description = "X User Adaption encoding/decoding",
+	},
+
+};
+
+const struct log_info test_log_info = {
+	.cat = log_cat,
+	.num_cat = ARRAY_SIZE(log_cat),
+};
+
+int tx_unitdata(struct sua_link *link)
+{
+	struct msgb *msg = msgb_alloc(1024, "tx_unitdata");
+	struct osmo_scu_prim *prim;
+	struct osmo_scu_unitdata_param *param;
+	uint8_t *cur;
+
+	prim = (struct osmo_scu_prim *) msgb_put(msg, sizeof(*prim));
+	param = &prim->u.unitdata;
+	param->calling_addr.presence = OSMO_SCCP_ADDR_T_SSN;
+	param->called_addr.presence = OSMO_SCCP_ADDR_T_SSN;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER, OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_REQUEST, msg);
+
+	cur = msg->l2h = msgb_put(msg, 3);
+	cur[0] = 1; cur[1] = 2; cur[2] = 3;
+
+	return osmo_osmo_sua_user_link_down(link, &prim->oph);
+}
+
+static void sccp_make_addr_pc_ssn(struct osmo_sccp_addr *addr, uint32_t pc, uint32_t ssn)
+{
+	addr->presence = OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC;
+	addr->ssn = ssn;
+	addr->pc = pc;
+}
+
+#define SSN_RANAP 142
+
+struct osmo_prim_hdr *make_conn_req(uint32_t conn_id)
+{
+	struct msgb *msg = msgb_alloc(1024, "conn_req");
+	struct osmo_scu_prim *prim;
+
+	prim = (struct osmo_scu_prim *) msgb_put(msg, sizeof(*prim));
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_CONNECT,
+			PRIM_OP_REQUEST, msg);
+	/* Set SSN for calling and called addr */
+	sccp_make_addr_pc_ssn(&prim->u.connect.called_addr, 2, SSN_RANAP);
+	sccp_make_addr_pc_ssn(&prim->u.connect.calling_addr, 1, SSN_RANAP);
+	prim->u.connect.sccp_class = 2;
+	prim->u.connect.conn_id = conn_id;
+
+	return &prim->oph;
+}
+
+int tx_conn_req(struct sua_link *link, uint32_t conn_id)
+{
+	struct osmo_prim_hdr *prim = make_conn_req(conn_id);
+	return osmo_osmo_sua_user_link_down(link, prim);
+}
+
+struct osmo_prim_hdr *
+make_dt1_req(uint32_t conn_id, const uint8_t *data, unsigned int len)
+{
+	struct msgb *msg = msgb_alloc(1024, "dt1");
+	struct osmo_scu_prim *prim;
+
+	prim = (struct osmo_scu_prim *) msgb_put(msg, sizeof(*prim));
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
+			OSMO_SCU_PRIM_N_DATA,
+			PRIM_OP_REQUEST, msg);
+	prim->u.data.conn_id = conn_id;
+
+	msg->l2h = msgb_put(msg, len);
+	memcpy(msg->l2h, data, len);
+
+	return &prim->oph;
+}
diff --git a/sigtran/sua_test_common.h b/sigtran/sua_test_common.h
new file mode 100644
index 0000000..7c6f0f7
--- /dev/null
+++ b/sigtran/sua_test_common.h
@@ -0,0 +1,31 @@
+#pragma once 
+
+#include <stdint.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <osmocom/core/select.h>
+#include <osmocom/core/prim.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/vty/logging.h>
+
+#include "sua.h"
+#include "sccp_sap.h"
+
+
+enum log_cat {
+	DMAIN,
+	DSUA,
+	DXUA,
+};
+
+extern const struct log_info test_log_info;
+
+int tx_unitdata(struct sua_link *link);
+int tx_conn_req(struct sua_link *link, uint32_t conn_id);
+
+struct osmo_prim_hdr *make_conn_req(uint32_t conn_id);
+struct osmo_prim_hdr *make_dt1_req(uint32_t conn_id, const uint8_t *data, unsigned int len);