[PATCH] libosmo-sccp[master]: Add new 'osmo_ss7' SS7 core code with M3UA, ASP/AS FSM, ...

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

Harald Welte gerrit-no-reply at lists.osmocom.org
Mon Apr 3 20:22:08 UTC 2017


Review at  https://gerrit.osmocom.org/2209

Add new 'osmo_ss7' SS7 core code with M3UA, ASP/AS FSM, ...

This is what aims to be a rather complete/proper implementation of the
SIGTRAN + SS7 protocol suite.  It has proper abstraction between the
layers with primitives, finite state machines for things like the AS and
ASP state machines, support for point code routing, etc.

What's not implemented at this point:
* re-integration of pre-existing SUA (pending)
* actual MTP2 and physical E1/T1 link support
* different trafic modes like broadcast/fail-over/load-balance

Change-Id: I375eb80f01acc013094851d91d1d3333ebc12bc7
---
M configure.ac
M include/osmocom/sigtran/Makefile.am
A include/osmocom/sigtran/osmo_ss7.h
A include/osmocom/sigtran/protocol/mtp.h
M include/osmocom/sigtran/sigtran_sap.h
M src/Makefile.am
A src/m3ua.c
A src/osmo_ss7.c
A src/osmo_ss7_hmrt.c
A src/osmo_ss7_vty.c
A src/xua_as_fsm.c
A src/xua_as_fsm.h
A src/xua_asp_fsm.c
A src/xua_asp_fsm.h
A src/xua_internal.h
M tests/Makefile.am
A tests/ss7/Makefile.am
A tests/ss7/ss7_test.c
A tests/ss7/ss7_test.err
A tests/ss7/ss7_test.ok
M tests/testsuite.at
21 files changed, 4,980 insertions(+), 4 deletions(-)


  git pull ssh://gerrit.osmocom.org:29418/libosmo-sccp refs/changes/09/2209/1

diff --git a/configure.ac b/configure.ac
index 4c3c937..2e55c57 100644
--- a/configure.ac
+++ b/configure.ac
@@ -56,5 +56,6 @@
     tests/mtp/Makefile
     tests/m2ua/Makefile
     tests/sigtran/Makefile
+    tests/ss7/Makefile
     Makefile)
 
diff --git a/include/osmocom/sigtran/Makefile.am b/include/osmocom/sigtran/Makefile.am
index 2f2912f..ca7a304 100644
--- a/include/osmocom/sigtran/Makefile.am
+++ b/include/osmocom/sigtran/Makefile.am
@@ -1,7 +1,7 @@
 sigtran_HEADERS = xua_types.h xua_msg.h m2ua_types.h sccp_sap.h \
-		  sua.h sigtran_sap.h sccp_helpers.h mtp_sap.h
+		  sua.h sigtran_sap.h sccp_helpers.h mtp_sap.h osmo_ss7.h
 
 sigtrandir = $(includedir)/osmocom/sigtran
 
-sigtran_prot_HEADERS = protocol/sua.h protocol/m3ua.h
+sigtran_prot_HEADERS = protocol/sua.h protocol/m3ua.h protocol/mtp.h
 sigtran_protdir = $(includedir)/osmocom/sigtran/protocol
diff --git a/include/osmocom/sigtran/osmo_ss7.h b/include/osmocom/sigtran/osmo_ss7.h
new file mode 100644
index 0000000..5bbcd65
--- /dev/null
+++ b/include/osmocom/sigtran/osmo_ss7.h
@@ -0,0 +1,408 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/prim.h>
+
+struct osmo_ss7_instance;
+struct osmo_ss7_user;
+struct osmo_sccp_instance;
+struct osmo_mtp_prim;
+
+int osmo_ss7_init(void);
+
+bool osmo_ss7_pc_is_local(struct osmo_ss7_instance *inst, uint32_t pc);
+int osmo_ss7_pointcode_parse(struct osmo_ss7_instance *inst, const char *str);
+int osmo_ss7_pointcode_parse_mask_or_len(struct osmo_ss7_instance *inst, const char *in);
+const char *osmo_ss7_pointcode_print(struct osmo_ss7_instance *inst, uint32_t pc);
+
+/***********************************************************************
+ * SS7 Routing Tables
+ ***********************************************************************/
+
+struct osmo_ss7_route_table {
+	/*! member in list of routing tables */
+	struct llist_head list;
+	/*! \ref osmo_ss7_instance to which we belong */
+	struct osmo_ss7_instance *inst;
+	/*! list of \ref osmo_ss7_route */
+	struct llist_head routes;
+
+	struct {
+		char *name;
+		char *description;
+	} cfg;
+};
+
+struct osmo_ss7_route_table *
+osmo_ss7_route_table_find(struct osmo_ss7_instance *inst, const char *name);
+struct osmo_ss7_route_table *
+osmo_ss7_route_table_find_or_create(struct osmo_ss7_instance *inst, const char *name);
+void osmo_ss7_route_table_destroy(struct osmo_ss7_route_table *rtbl);
+
+/***********************************************************************
+ * SS7 Instances
+ ***********************************************************************/
+
+struct osmo_ss7_instance {
+	/*! member of global list of instances */
+	struct llist_head list;
+	/*! list of \ref osmo_ss7_linkset */
+	struct llist_head linksets;
+	/*! list of \ref osmo_ss7_as */
+	struct llist_head as_list;
+	/*! list of \ref osmo_ss7_asp */
+	struct llist_head asp_list;
+	/*! list of \ref osmo_ss7_route_table */
+	struct llist_head rtable_list;
+	/* array for faster lookup of user (indexed by service
+	 * indicator) */
+	const struct osmo_ss7_user *user[16];
+
+	struct osmo_ss7_route_table *rtable_system;
+
+	struct osmo_sccp_instance *sccp;
+
+	struct {
+		uint32_t id;
+		char *name;
+		char *description;
+		uint32_t primary_pc;
+		/* secondary PCs */
+		/* capability PCs */
+		uint8_t network_indicator;
+		struct {
+			char delimiter;
+			uint8_t component_len[3];
+		} pc_fmt;
+	} cfg;
+};
+
+struct osmo_ss7_instance *osmo_ss7_instance_find(uint32_t id);
+struct osmo_ss7_instance *
+osmo_ss7_instance_find_or_create(void *ctx, uint32_t id);
+void osmo_ss7_instance_destroy(struct osmo_ss7_instance *inst);
+int osmo_ss7_instance_set_pc_fmt(struct osmo_ss7_instance *inst,
+				uint8_t c0, uint8_t c1, uint8_t c2);
+
+/***********************************************************************
+ * MTP Users (Users of MTP, such as SCCP or ISUP)
+ ***********************************************************************/
+
+struct osmo_ss7_user {
+	/* pointer back to SS7 instance */
+	struct osmo_ss7_instance *inst;
+	/* name of the user */
+	const char *name;
+	/* primitive call-back for incoming MTP primitives */
+	osmo_prim_cb prim_cb;
+	/* private data */
+	void *priv;
+};
+
+int osmo_ss7_user_register(struct osmo_ss7_instance *inst, uint8_t service_ind,
+			   struct osmo_ss7_user *user);
+
+int osmo_ss7_user_unregister(struct osmo_ss7_instance *inst, uint8_t service_ind,
+			     struct osmo_ss7_user *user);
+
+int osmo_ss7_mtp_to_user(struct osmo_ss7_instance *inst, struct osmo_mtp_prim *omp);
+
+/* SS7 User wants to issue MTP-TRANSFER.req */
+int osmo_ss7_user_mtp_xfer_req(struct osmo_ss7_instance *inst,
+				struct osmo_mtp_prim *omp);
+
+/***********************************************************************
+ * SS7 Links
+ ***********************************************************************/
+
+enum osmo_ss7_link_adm_state {
+	OSMO_SS7_LS_SHUTDOWN,
+	OSMO_SS7_LS_INHIBITED,
+	OSMO_SS7_LS_ENABLED,
+	_NUM_OSMO_SS7_LS
+};
+
+struct osmo_ss7_linkset;
+struct osmo_ss7_link;
+
+struct osmo_ss7_link {
+	/*! \ref osmo_ss7_linkset to which we belong */
+	struct osmo_ss7_linkset *linkset;
+	struct {
+		char *name;
+		char *description;
+		uint32_t id;
+
+		enum osmo_ss7_link_adm_state adm_state;
+	} cfg;
+};
+
+void osmo_ss7_link_destroy(struct osmo_ss7_link *link);
+struct osmo_ss7_link *
+osmo_ss7_link_find_or_create(struct osmo_ss7_linkset *lset, uint32_t id);
+
+/***********************************************************************
+ * SS7 Linksets
+ ***********************************************************************/
+
+struct osmo_ss7_linkset {
+	struct llist_head list;
+	/*! \ref osmo_ss7_instance to which we belong */
+	struct osmo_ss7_instance *inst;
+	/*! array of \ref osmo_ss7_link */
+	struct osmo_ss7_link *links[16];
+
+	struct {
+		char *name;
+		char *description;
+		uint32_t adjacent_pc;
+		uint32_t local_pc;
+	} cfg;
+};
+
+void osmo_ss7_linkset_destroy(struct osmo_ss7_linkset *lset);
+struct osmo_ss7_linkset *
+osmo_ss7_linkset_find_by_name(struct osmo_ss7_instance *inst, const char *name);
+struct osmo_ss7_linkset *
+osmo_ss7_linkset_find_or_create(struct osmo_ss7_instance *inst, const char *name, uint32_t pc);
+
+
+/***********************************************************************
+ * SS7 Routes
+ ***********************************************************************/
+
+struct osmo_ss7_route {
+	/*! member in \ref osmo_ss7_route_table.routes */
+	struct llist_head list;
+	/*! \ref osmo_ss7_route_table to which we belong */
+	struct osmo_ss7_route_table *rtable;
+
+	struct {
+		/*! pointer to linkset (destination) of route */
+		struct osmo_ss7_linkset *linkset;
+		/*! pointer to Application Server */
+		struct osmo_ss7_as *as;
+	} dest;
+
+	struct {
+		/* FIXME: presence? */
+		uint32_t pc;
+		uint32_t mask;
+		/*! human-specified linkset name */
+		char *linkset_name;
+		/*! lower priority is higher */
+		uint32_t priority;
+		uint8_t qos_class;
+	} cfg;
+};
+
+struct osmo_ss7_route *
+osmo_ss7_route_find_dpc(struct osmo_ss7_route_table *rtbl, uint32_t dpc);
+struct osmo_ss7_route *
+osmo_ss7_route_find_dpc_mask(struct osmo_ss7_route_table *rtbl, uint32_t dpc,
+			     uint32_t mask);
+struct osmo_ss7_route *
+osmo_ss7_route_lookup(struct osmo_ss7_instance *inst, uint32_t dpc);
+struct osmo_ss7_route *
+osmo_ss7_route_create(struct osmo_ss7_route_table *rtbl, uint32_t dpc,
+			uint32_t mask, const char *linkset_name);
+void osmo_ss7_route_destroy(struct osmo_ss7_route *rt);
+
+
+/***********************************************************************
+ * SS7 Application Servers
+ ***********************************************************************/
+
+struct osmo_ss7_routing_key {
+	uint32_t context;
+
+	uint32_t pc;
+	uint8_t si;
+	uint32_t ssn;
+	/* FIXME: more complex routing keys */
+};
+
+enum osmo_ss7_as_traffic_mode {
+	OSMO_SS7_AS_TMOD_BCAST,
+	OSMO_SS7_AS_TMOD_LOADSHARE,
+	OSMO_SS7_AS_TMOD_ROUNDROBIN,
+	OSMO_SS7_AS_TMOD_OVERRIDE,
+	_NUM_OSMO_SS7_ASP_TMOD
+};
+
+extern struct value_string osmo_ss7_as_traffic_mode_vals[];
+
+static inline const char *
+osmo_ss7_as_traffic_mode_name(enum osmo_ss7_as_traffic_mode mode)
+{
+	return get_value_string(osmo_ss7_as_traffic_mode_vals, mode);
+}
+
+enum osmo_ss7_asp_protocol {
+	OSMO_SS7_ASP_PROT_NONE,
+	OSMO_SS7_ASP_PROT_SUA,
+	OSMO_SS7_ASP_PROT_M3UA,
+	_NUM_OSMO_SS7_ASP_PROT
+};
+
+extern struct value_string osmo_ss7_asp_protocol_vals[];
+
+static inline const char *
+osmo_ss7_asp_protocol_name(enum osmo_ss7_asp_protocol mode)
+{
+	return get_value_string(osmo_ss7_asp_protocol_vals, mode);
+}
+
+int osmo_ss7_asp_protocol_port(enum osmo_ss7_asp_protocol prot);
+
+struct osmo_ss7_as {
+	/*! entry in 'ref osmo_ss7_instance.as_list */
+	struct llist_head list;
+	struct osmo_ss7_instance *inst;
+
+	/*! AS FSM */
+	struct osmo_fsm_inst *fi;
+
+	struct {
+		char *name;
+		char *description;
+		enum osmo_ss7_asp_protocol proto;
+		struct osmo_ss7_routing_key routing_key;
+		enum osmo_ss7_as_traffic_mode mode;
+		uint32_t recovery_timeout_msec;
+		uint8_t qos_class;
+
+		struct osmo_ss7_asp *asps[16];
+	} cfg;
+};
+
+struct osmo_ss7_as *
+osmo_ss7_as_find_by_name(struct osmo_ss7_instance *inst, const char *name);
+struct osmo_ss7_as *
+osmo_ss7_as_find_by_rctx(struct osmo_ss7_instance *inst, uint32_t rctx);
+struct osmo_ss7_as *
+osmo_ss7_as_find_or_create(struct osmo_ss7_instance *inst, const char *name,
+			  enum osmo_ss7_asp_protocol proto);
+int osmo_ss7_as_add_asp(struct osmo_ss7_as *as, const char *asp_name);
+int osmo_ss7_as_del_asp(struct osmo_ss7_as *as, const char *asp_name);
+void osmo_ss7_as_destroy(struct osmo_ss7_as *as);
+bool osmo_ss7_as_has_asp(struct osmo_ss7_as *as,
+			 struct osmo_ss7_asp *asp);
+
+
+/***********************************************************************
+ * SS7 Application Server Processes
+ ***********************************************************************/
+
+struct osmo_ss7_asp_peer {
+	char *host;
+	uint16_t port;
+};
+
+enum osmo_ss7_asp_admin_state {
+	/*! no SCTP association with peer */
+	OSMO_SS7_ASP_ADM_S_SHUTDOWN,
+	/*! SCP association, but reject ASP-ACTIVE */
+	OSMO_SS7_ASP_ADM_S_BLOCKED,
+	/*! in normal operation */
+	OSMO_SS7_ASP_ADM_S_ENABLED,
+};
+
+struct osmo_ss7_asp {
+	/*! entry in \ref osmo_ss7_instance.asp_list */
+	struct llist_head list;
+	struct osmo_ss7_instance *inst;
+
+	/*! ASP FSM */
+	struct osmo_fsm_inst *fi;
+
+	/*! \ref osmo_xua_server over which we were established */
+	struct osmo_xua_server *xua_server;
+
+	/*! osmo_stream / libosmo-netif handles */
+	struct osmo_stream_cli *client;
+	struct osmo_stream_srv *server;
+	/*! pre-formatted human readable local/remote socket name */
+	char *sock_name;
+
+	/* ASP Identifier for ASP-UP + NTFY */
+	uint32_t asp_id;
+	bool asp_id_present;
+
+	struct {
+		char *name;
+		char *description;
+		enum osmo_ss7_asp_protocol proto;
+		enum osmo_ss7_asp_admin_state adm_state;
+		bool is_server;
+
+		struct osmo_ss7_asp_peer local;
+		struct osmo_ss7_asp_peer remote;
+		uint8_t qos_class;
+	} cfg;
+};
+
+struct osmo_ss7_asp *
+osmo_ss7_asp_find_by_name(struct osmo_ss7_instance *inst, const char *name);
+struct osmo_ss7_asp *
+osmo_ss7_asp_find_or_create(struct osmo_ss7_instance *inst, const char *name,
+			    uint16_t remote_port, uint16_t local_port,
+			    enum osmo_ss7_asp_protocol proto);
+void osmo_ss7_asp_destroy(struct osmo_ss7_asp *asp);
+int osmo_ss7_asp_send(struct osmo_ss7_asp *asp, struct msgb *msg);
+int osmo_ss7_asp_restart(struct osmo_ss7_asp *asp);
+
+#define LOGPASP(asp, subsys, level, fmt, args ...)		\
+	LOGP(subsys, level, "asp-%s: " fmt, (asp)->cfg.name, ## args)
+
+/***********************************************************************
+ * xUA Servers
+ ***********************************************************************/
+
+struct osmo_xua_server {
+	struct llist_head list;
+	struct osmo_ss7_instance *inst;
+
+	struct osmo_stream_srv_link *server;
+
+	struct {
+		struct osmo_ss7_asp_peer local;
+		enum osmo_ss7_asp_protocol proto;
+	} cfg;
+};
+
+struct osmo_xua_server *
+osmo_ss7_xua_server_find(struct osmo_ss7_instance *inst, enum osmo_ss7_asp_protocol proto,
+			 uint16_t local_port);
+
+struct osmo_xua_server *
+osmo_ss7_xua_server_create(struct osmo_ss7_instance *inst, enum osmo_ss7_asp_protocol proto,
+			   uint16_t local_port, const char *local_host);
+
+int
+osmo_ss7_xua_server_set_local_host(struct osmo_xua_server *xs, const char *local_host);
+
+void osmo_ss7_xua_server_destroy(struct osmo_xua_server *xs);
+
+struct osmo_sccp_instance *
+osmo_sccp_simple_client(void *ctx, const char *name, uint32_t pc,
+			enum osmo_ss7_asp_protocol prot,
+			int local_port, int remote_port, const char *remote_ip);
+
+struct osmo_sccp_instance *
+osmo_sccp_simple_server(void *ctx, uint32_t pc,
+			enum osmo_ss7_asp_protocol prot, int local_port,
+			const char *local_ip);
+
+struct osmo_sccp_instance *
+osmo_sccp_simple_server_add_clnt(struct osmo_sccp_instance *inst,
+				 enum osmo_ss7_asp_protocol prot,
+				 const char *name, uint32_t pc,
+				 int local_port, int remote_port,
+				 const char *remote_ip);
diff --git a/include/osmocom/sigtran/protocol/mtp.h b/include/osmocom/sigtran/protocol/mtp.h
new file mode 100644
index 0000000..8b990c0
--- /dev/null
+++ b/include/osmocom/sigtran/protocol/mtp.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <osmocom/core/utils.h>
+
+/* Chapter 15.17.4 of Q.704 + RFC4666 3.4.5. */
+/* Section 5.1 of ETSI EG 201 693: MTP SI code allocations (for NI= 00) */
+enum mtp_si_ni00 {
+	MTP_SI_SNM	= 0,
+	MTP_SI_STM	= 1,
+	MTP_SI_SCCP	= 3,
+	MTP_SI_TUP	= 4,
+	MTP_SI_ISUP	= 5,
+	MTP_SI_DUP	= 6, /* call related */
+	MTP_SI_DUP_FAC	= 7, /* facility related */
+	MTP_SI_TESTING	= 8,
+	MTP_SI_B_ISUP	= 9,
+	MTP_SI_SAT_ISUP = 10,
+	MTP_SI_SPEECH	= 11, /* speech processing element */
+	MTP_SI_AAL2_SIG	= 12,
+	MTP_SI_BICC	= 13,
+	MTP_SI_GCP	= 14,
+};
+
+extern const struct value_string mtp_si_vals[];
diff --git a/include/osmocom/sigtran/sigtran_sap.h b/include/osmocom/sigtran/sigtran_sap.h
index 544fc44..d29c37d 100644
--- a/include/osmocom/sigtran/sigtran_sap.h
+++ b/include/osmocom/sigtran/sigtran_sap.h
@@ -3,4 +3,39 @@
 
 enum osmo_sigtran_sap {
 	SCCP_SAP_USER	= _SAP_SS7_BASE,
+	/* xUA Layer Manager */
+	XUA_SAP_LM,
+	MTP_SAP_USER,
 };
+
+enum osmo_xlm_prim_type {
+	OSMO_XLM_PRIM_M_SCTP_ESTABLISH,
+	OSMO_XLM_PRIM_M_SCTP_RELEASE,
+	OSMO_XLM_PRIM_M_SCTP_RESTART,
+	OSMO_XLM_PRIM_M_SCTP_STATUS,
+	OSMO_XLM_PRIM_M_ASP_STATUS,
+	OSMO_XLM_PRIM_M_AS_STATUS,
+	OSMO_XLM_PRIM_M_NOTIFY,
+	OSMO_XLM_PRIM_M_ERROR,
+	OSMO_XLM_PRIM_M_ASP_UP,
+	OSMO_XLM_PRIM_M_ASP_DOWN,
+	OSMO_XLM_PRIM_M_ASP_ACTIVE,
+	OSMO_XLM_PRIM_M_ASP_INACTIVE,
+	OSMO_XLM_PRIM_M_AS_ACTIVE,
+	OSMO_XLM_PRIM_M_AS_INACTIVE,
+	OSMO_XLM_PRIM_M_AS_DOWN,
+	/* optional as per spec, not implemented yet */
+	OSMO_XLM_PRIM_M_RK_REG,
+	OSMO_XLM_PRIM_M_RK_DEREG,
+};
+
+
+struct osmo_xlm_prim {
+	struct osmo_prim_hdr oph;
+	union {
+	} u;
+};
+
+#define msgb_xlm_prim(msg) ((struct osmo_xlm_prim *)(msg)->l1h)
+
+char *osmo_xlm_prim_name(struct osmo_prim_hdr *oph);
diff --git a/src/Makefile.am b/src/Makefile.am
index 26482a0..f7f4ccc 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,6 +1,8 @@
 AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)
 AM_CFLAGS=-Wall $(LIBOSMOCORE_CFLAGS) $(LIBOSMONETIF_CFLAGS)
 
+noinst_HEADERS = xua_asp_fsm.h xua_as_fsm.h xua_internal.h
+
 # Legacy static libs
 
 sccpdir = $(libdir)
@@ -24,6 +26,7 @@
 # documentation before making any modification
 LIBVERSION=0:0:0
 
-libosmo_sigtran_la_SOURCES = sccp_sap.c sua.c xua_msg.c sccp_helpers.c
+libosmo_sigtran_la_SOURCES = sccp_sap.c sua.c m3ua.c xua_msg.c sccp_helpers.c \
+			     osmo_ss7.c osmo_ss7_hmrt.c xua_asp_fsm.c xua_as_fsm.c
 libosmo_sigtran_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined -export-symbols-regex '^osmo_'
 libosmo_sigtran_la_LIBADD = $(LIBOSMOCORE_LIBS) $(LIBOSMONETIF_LIBS) $(LIBSCTP_LIBS)
diff --git a/src/m3ua.c b/src/m3ua.c
new file mode 100644
index 0000000..8ec82c5
--- /dev/null
+++ b/src/m3ua.c
@@ -0,0 +1,669 @@
+/* Minimal implementation of RFC 4666 - MTP3 User Adaptation Layer */
+
+/* (C) 2015-2017 by Harald Welte <laforge at gnumonks.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <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/core/socket.h>
+
+#include <osmocom/netif/stream.h>
+#include <osmocom/sigtran/xua_msg.h>
+
+#include <osmocom/sigtran/mtp_sap.h>
+#include <osmocom/sigtran/sccp_sap.h>
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/protocol/m3ua.h>
+#include <osmocom/sigtran/protocol/sua.h>
+
+#include "xua_asp_fsm.h"
+#include "xua_internal.h"
+
+#define M3UA_MSGB_SIZE 1500
+
+/***********************************************************************
+ * Protocol Definition (string tables, mandatory IE checking)
+ ***********************************************************************/
+
+/* Section 3.8.1 */
+const struct value_string m3ua_err_names[] = {
+	{ M3UA_ERR_INVALID_VERSION,	"Invalid Version" },
+	{ M3UA_ERR_UNSUPP_MSG_CLASS,	"Unsupported Message Class" },
+	{ M3UA_ERR_UNSUPP_MSG_TYPE,	"Unsupported Message Type" },
+	{ M3UA_ERR_UNSUPP_TRAF_MOD_TYP,	"Unsupported Traffic Mode Type" },
+	{ M3UA_ERR_UNEXPECTED_MSG,	"Unexpected Message" },
+	{ M3UA_ERR_PROTOCOL_ERR,	"Protocol Error" },
+	{ M3UA_ERR_INVAL_STREAM_ID,	"Invalid Stream Identifier" },
+	{ M3UA_ERR_REFUSED_MGMT_BLOCKING, "Refused - Management Blocking" },
+	{ M3UA_ERR_ASP_ID_REQD,		"ASP Identifier Required" },
+	{ M3UA_ERR_INVAL_ASP_ID,	"Invalid ASP Identifier" },
+	{ M3UA_ERR_INVAL_PARAM_VAL,	"Invalid Parameter Value" },
+	{ M3UA_ERR_PARAM_FIELD_ERR,	"Parameter Field Error" },
+	{ M3UA_ERR_UNEXP_PARAM,		"Unexpected Parameter" },
+	{ M3UA_ERR_DEST_STATUS_UNKN,	"Destination Status Unknown" },
+	{ M3UA_ERR_INVAL_NET_APPEAR,	"Invalid Network Appearance" },
+	{ M3UA_ERR_MISSING_PARAM,	"Missing Parameter" },
+	{ M3UA_ERR_INVAL_ROUT_CTX,	"Invalid Routing Context" },
+	{ M3UA_ERR_NO_CONFGD_AS_FOR_ASP,"No Configured AS for ASP" },
+	{ SUA_ERR_SUBSYS_STATUS_UNKN,	"Subsystem Status Unknown" },
+	{ SUA_ERR_INVAL_LOADSH_LEVEL,	"Invalid loadsharing level" },
+	{ 0, NULL }
+};
+
+const struct value_string m3ua_ntfy_type_names[] = {
+	{ M3UA_NOTIFY_T_STATCHG,	"State Change" },
+	{ M3UA_NOTIFY_T_OTHER,		"Other" },
+	{ 0, NULL }
+};
+
+const struct value_string m3ua_ntfy_stchg_names[] = {
+	{ M3UA_NOTIFY_I_RESERVED,	"Reserved" },
+	{ M3UA_NOTIFY_I_AS_INACT,	"AS Inactive" },
+	{ M3UA_NOTIFY_I_AS_ACT,		"AS Active" },
+	{ M3UA_NOTIFY_I_AS_PEND,	"AS Pending" },
+	{ 0, NULL }
+};
+
+const struct value_string m3ua_ntfy_other_names[] = {
+	{ M3UA_NOTIFY_I_OT_INS_RES,	"Insufficient ASP Resouces active in AS" },
+	{ M3UA_NOTIFY_I_OT_ALT_ASP_ACT,	"Alternative ASP Active" },
+	{ M3UA_NOTIFY_I_OT_ASP_FAILURE,	"ASP Failure" },
+	{ 0, NULL }
+};
+
+static const struct value_string m3ua_iei_names[] = {
+	{ M3UA_IEI_INFO_STRING,		"INFO String" },
+	{ M3UA_IEI_ROUTE_CTX,		"Routing Context" },
+	{ M3UA_IEI_DIAG_INFO,		"Diagnostic Info" },
+	{ M3UA_IEI_HEARDBT_DATA,	"Heartbeat Data" },
+	{ M3UA_IEI_TRAF_MODE_TYP,	"Traffic Mode Type" },
+	{ M3UA_IEI_ERR_CODE,		"Error Code" },
+	{ M3UA_IEI_STATUS,		"Status" },
+	{ M3UA_IEI_ASP_ID,		"ASP Identifier" },
+	{ M3UA_IEI_AFFECTED_PC,		"Affected Point Code" },
+	{ M3UA_IEI_CORR_ID,		"Correlation Id" },
+
+	{ M3UA_IEI_NET_APPEAR,		"Network Appearance" },
+	{ M3UA_IEI_USER_CAUSE,		"User/Cause" },
+	{ M3UA_IEI_CONG_IND,		"Congestion Indication" },
+	{ M3UA_IEI_CONC_DEST,		"Concerned Destination" },
+	{ M3UA_IEI_ROUT_KEY,		"Routing Key" },
+	{ M3UA_IEI_REG_RESULT,		"Registration Result" },
+	{ M3UA_IEI_DEREG_RESULT,	"De-Registration Result" },
+	{ M3UA_IEI_LOC_RKEY_ID,		"Local Routing-Key Identifier" },
+	{ M3UA_IEI_DEST_PC,		"Destination Point Code" },
+	{ M3UA_IEI_SVC_IND,		"Service Indicators" },
+	{ M3UA_IEI_ORIG_PC,		"Originating Point Code List" },
+	{ M3UA_IEI_PROT_DATA,		"Protocol Data" },
+	{ M3UA_IEI_REG_STATUS,		"Registration Status" },
+	{ M3UA_IEI_DEREG_STATUS,	"De-Registration Status" },
+	{ 0, NULL }
+};
+
+#define MAND_IES(msgt, ies)	[msgt] = (ies)
+
+/* XFER */
+static const uint16_t data_mand_ies[] = {
+	M3UA_IEI_PROT_DATA, 0
+};
+static const struct value_string m3ua_xfer_msgt_names[] = {
+	{ M3UA_XFER_DATA,	"DATA" },
+	{ 0, NULL }
+};
+static const struct xua_msg_class msg_class_xfer = {
+	.name = "XFER",
+	.msgt_names = m3ua_xfer_msgt_names,
+	.mand_ies = {
+		MAND_IES(M3UA_XFER_DATA, data_mand_ies),
+	},
+};
+
+/* SNM */
+static const uint16_t duna_mand_ies[] = {
+	M3UA_IEI_AFFECTED_PC, 0
+};
+static const uint16_t dava_mand_ies[] = {
+	M3UA_IEI_AFFECTED_PC, 0
+};
+static const uint16_t daud_mand_ies[] = {
+	M3UA_IEI_AFFECTED_PC, 0
+};
+static const uint16_t scon_mand_ies[] = {
+	M3UA_IEI_AFFECTED_PC, 0
+};
+static const uint16_t dupu_mand_ies[] = {
+	M3UA_IEI_AFFECTED_PC, M3UA_IEI_USER_CAUSE, 0
+};
+static const uint16_t drst_mand_ies[] = {
+	M3UA_IEI_AFFECTED_PC, 0
+};
+static const struct value_string m3ua_snm_msgt_names[] = {
+	{ M3UA_SNM_DUNA,	"DUNA" },
+	{ M3UA_SNM_DAVA,	"DAVA" },
+	{ M3UA_SNM_DAUD,	"DAUD" },
+	{ M3UA_SNM_SCON,	"SCON" },
+	{ M3UA_SNM_DUPU,	"DUPU" },
+	{ M3UA_SNM_DRST,	"DRST" },
+	{ 0, NULL }
+};
+const struct xua_msg_class m3ua_msg_class_snm = {
+	.name = "SNM",
+	.msgt_names = m3ua_snm_msgt_names,
+	.mand_ies = {
+		MAND_IES(M3UA_SNM_DUNA, duna_mand_ies),
+		MAND_IES(M3UA_SNM_DAVA, dava_mand_ies),
+		MAND_IES(M3UA_SNM_DAUD, daud_mand_ies),
+		MAND_IES(M3UA_SNM_SCON, scon_mand_ies),
+		MAND_IES(M3UA_SNM_DUPU, dupu_mand_ies),
+		MAND_IES(M3UA_SNM_DRST, drst_mand_ies),
+	},
+};
+
+/* ASPSM */
+static const struct value_string m3ua_aspsm_msgt_names[] = {
+	{ M3UA_ASPSM_UP,	"UP" },
+	{ M3UA_ASPSM_DOWN,	"DOWN" },
+	{ M3UA_ASPSM_BEAT,	"BEAT" },
+	{ M3UA_ASPSM_UP_ACK,	"UP-ACK" },
+	{ M3UA_ASPSM_DOWN_ACK,	"DOWN-ACK" },
+	{ M3UA_ASPSM_BEAT_ACK,	"BEAT-ACK" },
+	{ 0, NULL }
+};
+const struct xua_msg_class m3ua_msg_class_aspsm = {
+	.name = "ASPSM",
+	.msgt_names = m3ua_aspsm_msgt_names,
+};
+
+/* ASPTM */
+const struct value_string m3ua_asptm_msgt_names[] = {
+	{ M3UA_ASPTM_ACTIVE,	"ACTIVE" },
+	{ M3UA_ASPTM_INACTIVE,	"INACTIVE" },
+	{ M3UA_ASPTM_ACTIVE_ACK,"ACTIVE-ACK" },
+	{ M3UA_ASPTM_INACTIVE_ACK, "INACTIVE-ACK" },
+	{ 0, NULL }
+};
+const struct xua_msg_class m3ua_msg_class_asptm = {
+	.name = "ASPTM",
+	.msgt_names = m3ua_asptm_msgt_names,
+	.iei_names = m3ua_iei_names,
+};
+
+/* MGMT */
+static const uint16_t err_req_ies[] = {
+	M3UA_IEI_ERR_CODE, 0
+};
+static const uint16_t ntfy_req_ies[] = {
+	M3UA_IEI_STATUS, 0
+};
+static const struct value_string m3ua_mgmt_msgt_names[] = {
+	{ M3UA_MGMT_ERR,	"ERROR" },
+	{ M3UA_MGMT_NTFY,	"NOTIFY" },
+	{ 0, NULL }
+};
+const struct xua_msg_class m3ua_msg_class_mgmt = {
+	.name = "MGMT",
+	.msgt_names = m3ua_mgmt_msgt_names,
+	.iei_names = m3ua_iei_names,
+	.mand_ies = {
+		MAND_IES(M3UA_MGMT_ERR, err_req_ies),
+		MAND_IES(M3UA_MGMT_NTFY, ntfy_req_ies),
+	},
+};
+
+/* RKM */
+static const uint16_t reg_req_ies[] = {
+	M3UA_IEI_ROUT_KEY, 0
+};
+static const uint16_t reg_rsp_ies[] = {
+	M3UA_IEI_REG_RESULT, 0
+};
+static const uint16_t dereg_req_ies[] = {
+	M3UA_IEI_ROUT_KEY, 0
+};
+static const uint16_t dereg_rsp_ies[] = {
+	M3UA_IEI_DEREG_RESULT, 0
+};
+static const struct value_string m3ua_rkm_msgt_names[] = {
+	{ M3UA_RKM_REG_REQ,	"REG-REQ" },
+	{ M3UA_RKM_REG_RSP,	"REG-RESP" },
+	{ M3UA_RKM_DEREG_REQ,	"DEREG-REQ" },
+	{ M3UA_RKM_DEREG_RSP,	"DEREG-RESP" },
+	{ 0, NULL }
+};
+const struct xua_msg_class m3ua_msg_class_rkm = {
+	.name = "RKM",
+	.msgt_names = m3ua_rkm_msgt_names,
+	.iei_names = m3ua_iei_names,
+	.mand_ies = {
+		MAND_IES(M3UA_RKM_REG_REQ, reg_req_ies),
+		MAND_IES(M3UA_RKM_REG_RSP, reg_rsp_ies),
+		MAND_IES(M3UA_RKM_DEREG_REQ, dereg_req_ies),
+		MAND_IES(M3UA_RKM_DEREG_RSP, dereg_rsp_ies),
+	},
+};
+
+/* M3UA dialect of XUA, MGMT,XFER,SNM,ASPSM,ASPTM,RKM */
+const struct xua_dialect xua_dialect_m3ua = {
+	.name = "M3UA",
+	.ppid = M3UA_PPID,
+	.port = M3UA_PORT,
+	.log_subsys = DLM3UA,
+	.class = {
+		[M3UA_MSGC_MGMT] = &m3ua_msg_class_mgmt,
+		[M3UA_MSGC_XFER] = &msg_class_xfer,
+		[M3UA_MSGC_SNM] = &m3ua_msg_class_snm,
+		[M3UA_MSGC_ASPSM] = &m3ua_msg_class_aspsm,
+		[M3UA_MSGC_ASPTM] = &m3ua_msg_class_asptm,
+		[M3UA_MSGC_RKM] = &m3ua_msg_class_rkm,
+	},
+};
+
+/* convert osmo_mtp_transfer_param to m3ua_data_hdr */
+void mtp_xfer_param_to_m3ua_dh(struct m3ua_data_hdr *mdh,
+				const struct osmo_mtp_transfer_param *param)
+{
+	mdh->opc = htonl(param->opc);
+	mdh->dpc = htonl(param->dpc);
+	mdh->si = param->sio & 0xF;
+	mdh->ni = (param->sio >> 6) & 0x3;
+	mdh->mp = (param->sio >> 4) & 0x3;
+	mdh->sls = param->sls;
+}
+
+/* convert m3ua_data_hdr to osmo_mtp_transfer_param */
+void m3ua_dh_to_xfer_param(struct osmo_mtp_transfer_param *param,
+			   const struct m3ua_data_hdr *mdh)
+{
+	param->opc = ntohl(mdh->opc);
+	param->dpc = ntohl(mdh->dpc);
+	param->sls = mdh->sls;
+	/* re-construct SIO */
+	param->sio = (mdh->si & 0xF) |
+		     (mdh->mp & 0x3 << 4) |
+		     (mdh->ni & 0x3 << 6);
+}
+
+#define M3UA_MSG_SIZE 2048
+#define M3UA_MSG_HEADROOM 512
+
+struct msgb *m3ua_msgb_alloc(const char *name)
+{
+	if (!name)
+		name = "M3UA";
+	return msgb_alloc_headroom(M3UA_MSG_SIZE+M3UA_MSG_HEADROOM,
+				   M3UA_MSG_HEADROOM, name);
+}
+
+/***********************************************************************
+ * ERROR generation
+ ***********************************************************************/
+
+static struct xua_msg *m3ua_gen_error(uint32_t err_code)
+{
+	struct xua_msg *xua = xua_msg_alloc();
+
+	xua->hdr = XUA_HDR(M3UA_MSGC_MGMT, M3UA_MGMT_ERR);
+	xua->hdr.version = M3UA_VERSION;
+	xua_msg_add_u32(xua, M3UA_IEI_ERR_CODE, err_code);
+
+	return xua;
+}
+
+static struct xua_msg *m3ua_gen_error_msg(uint32_t err_code, struct msgb *msg)
+{
+	struct xua_msg *xua = m3ua_gen_error(err_code);
+	unsigned int len_max_40 = msgb_length(msg);
+
+	if (len_max_40 > 40)
+		len_max_40 = 40;
+
+	xua_msg_add_data(xua, M3UA_IEI_DIAG_INFO, len_max_40, msgb_data(msg));
+
+	return xua;
+}
+
+/***********************************************************************
+ * NOTIFY generation
+ ***********************************************************************/
+
+/* RFC4666 Ch. 3.8.2. Notify */
+struct xua_msg *m3ua_encode_notify(const struct m3ua_notify_params *npar)
+{
+	struct xua_msg *xua = xua_msg_alloc();
+	uint32_t status;
+
+	xua->hdr = XUA_HDR(M3UA_MSGC_MGMT, M3UA_MGMT_NTFY);
+
+	status = M3UA_NOTIFY(htons(npar->status_type), htons(npar->status_info));
+	/* cannot use xua_msg_add_u32() as it does endian conversion */
+	xua_msg_add_data(xua, M3UA_IEI_STATUS, sizeof(status), (uint8_t *) &status);
+
+	/* Conditional: ASP Identifier */
+	if (npar->presence & NOTIFY_PAR_P_ASP_ID)
+		xua_msg_add_u32(xua, M3UA_IEI_ASP_ID, npar->asp_id);
+
+	/* Optional Routing Context */
+	if (npar->presence & NOTIFY_PAR_P_ROUTE_CTX)
+		xua_msg_add_u32(xua, M3UA_IEI_ROUTE_CTX, npar->route_ctx);
+
+	/* Optional: Info String */
+	if (npar->info_string)
+		xua_msg_add_data(xua, M3UA_IEI_INFO_STRING,
+				 strlen(npar->info_string)+1,
+				 (uint8_t *) npar->info_string);
+
+	return xua;
+}
+
+/* RFC4666 Ch. 3.8.2. Notify */
+int m3ua_decode_notify(struct m3ua_notify_params *npar, void *ctx,
+			const struct xua_msg *xua)
+{
+	struct xua_msg_part *info_ie, *aspid_ie, *status_ie, *rctx_ie;
+	uint32_t status;
+
+	/* cannot use xua_msg_get_u32() as it does endian conversion */
+	status_ie = xua_msg_find_tag(xua, M3UA_IEI_STATUS);
+	status = *(uint32_t *) status_ie->dat;
+
+	aspid_ie = xua_msg_find_tag(xua, M3UA_IEI_ASP_ID);
+	rctx_ie = xua_msg_find_tag(xua, M3UA_IEI_ROUTE_CTX);
+	info_ie = xua_msg_find_tag(xua, M3UA_IEI_INFO_STRING);
+
+	npar->presence = 0;
+	npar->status_type = ntohs(status & 0xffff);
+	npar->status_info = ntohs(status >> 16);
+
+	if (aspid_ie) {
+		npar->asp_id = xua_msg_part_get_u32(aspid_ie);
+		npar->presence |= NOTIFY_PAR_P_ASP_ID;
+	}
+
+	if (rctx_ie) {
+		npar->route_ctx = xua_msg_part_get_u32(rctx_ie);
+		npar->presence |= NOTIFY_PAR_P_ROUTE_CTX;
+	}
+
+	if (info_ie) {
+		npar->info_string = talloc_size(ctx, info_ie->len);
+		memcpy(npar->info_string, info_ie->dat, info_ie->len);
+	} else
+		npar->info_string = NULL;
+
+	return 0;
+}
+
+/***********************************************************************
+ * Transmitting M3UA messsages to SCTP
+ ***********************************************************************/
+
+static int m3ua_tx_xua_asp(struct osmo_ss7_asp *asp, struct xua_msg *xua)
+{
+	struct msgb *msg = xua_to_msg(M3UA_VERSION, xua);
+
+	OSMO_ASSERT(asp->cfg.proto == OSMO_SS7_ASP_PROT_M3UA);
+
+	xua_msg_free(xua);
+
+	if (!msg) {
+		LOGP(DLM3UA, LOGL_ERROR, "Error encoding M3UA Msg\n");
+		return -1;
+	}
+
+	msgb_sctp_ppid(msg) = M3UA_PPID;
+	return osmo_ss7_asp_send(asp, msg);
+}
+
+/*! \brief Send a given xUA message via a given M3UA Application Server
+ *  \param[in] as Application Server through which to send \ref xua
+ *  \param[in] xua xUA message to be sent
+ *  \return 0 on success; negative on error */
+int m3ua_tx_xua_as(struct osmo_ss7_as *as, struct xua_msg *xua)
+{
+	struct osmo_ss7_asp *asp;
+	unsigned int i;
+
+	OSMO_ASSERT(as->cfg.proto == OSMO_SS7_ASP_PROT_M3UA);
+
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		asp = as->cfg.asps[i];
+		if (!asp)
+			continue;
+		if (asp)
+			break;
+	}
+	if (!asp) {
+		LOGP(DLM3UA, LOGL_ERROR, "No ASP entroy in AS, dropping message\n");
+		xua_msg_free(xua);
+		return -ENODEV;
+	}
+
+	return m3ua_tx_xua_asp(asp, xua);
+}
+
+/***********************************************************************
+ * Receiving M3UA messsages from SCTP
+ ***********************************************************************/
+
+/* obtain the destination point code from a M3UA message in XUA fmt * */
+struct m3ua_data_hdr *data_hdr_from_m3ua(struct xua_msg *xua)
+{
+	struct xua_msg_part *data_ie;
+	struct m3ua_data_hdr *data_hdr;
+
+	if (xua->hdr.msg_class != M3UA_MSGC_XFER ||
+	    xua->hdr.msg_type != M3UA_XFER_DATA)
+		return NULL;
+
+	data_ie = xua_msg_find_tag(xua, M3UA_IEI_PROT_DATA);
+	if (!data_ie)
+		return NULL;
+	data_hdr = (struct m3ua_data_hdr *) data_ie->dat;
+
+	return data_hdr;
+}
+
+static int m3ua_rx_xfer(struct osmo_ss7_asp *asp, struct xua_msg *xua)
+{
+	struct m3ua_data_hdr *dh;
+
+	/* store the MTP-level information in the xua_msg for use by
+	 * higher layer protocols */
+	dh = data_hdr_from_m3ua(xua);
+	OSMO_ASSERT(dh);
+	m3ua_dh_to_xfer_param(&xua->mtp, dh);
+
+	return m3ua_hmdc_rx_from_l2(asp->inst, xua);
+}
+
+static int m3ua_rx_mgmt_err(struct osmo_ss7_asp *asp, struct xua_msg *xua)
+{
+	uint32_t err_code = xua_msg_get_u32(xua, M3UA_IEI_ERR_CODE);
+
+	LOGPASP(asp, DLM3UA, LOGL_ERROR, "Received MGMT_ERR '%s': %s\n",
+		get_value_string(m3ua_err_names, err_code),
+		xua_msg_dump(xua, &xua_dialect_m3ua));
+
+	/* NEVER return != 0 here, as we cannot respont to an ERR
+	 * message with another ERR! */
+	return 0;
+}
+
+static int m3ua_rx_mgmt_ntfy(struct osmo_ss7_asp *asp, struct xua_msg *xua)
+{
+	struct m3ua_notify_params ntfy;
+	const char *type_name, *info_name;
+
+	m3ua_decode_notify(&ntfy, asp, xua);
+
+	type_name = get_value_string(m3ua_ntfy_type_names, ntfy.status_type);
+
+	switch (ntfy.status_type) {
+	case M3UA_NOTIFY_T_STATCHG:
+		info_name = get_value_string(m3ua_ntfy_stchg_names,
+						ntfy.status_info);
+		break;
+	case M3UA_NOTIFY_T_OTHER:
+		info_name = get_value_string(m3ua_ntfy_other_names,
+						ntfy.status_info);
+		break;
+	default:
+		info_name = "NULL";
+		break;
+	}
+	LOGPASP(asp, DLM3UA, LOGL_NOTICE, "Received NOTIFY Type %s:%s (%s)\n",
+		type_name, info_name,
+		ntfy.info_string ? ntfy.info_string : "");
+
+	if (ntfy.info_string)
+		talloc_free(ntfy.info_string);
+
+	/* TODO: should we report this soemwhere? */
+	return 0;
+}
+
+static int m3ua_rx_mgmt(struct osmo_ss7_asp *asp, struct xua_msg *xua)
+{
+	switch (xua->hdr.msg_type) {
+	case M3UA_MGMT_ERR:
+		return m3ua_rx_mgmt_err(asp, xua);
+	case M3UA_MGMT_NTFY:
+		return m3ua_rx_mgmt_ntfy(asp, xua);
+	default:
+		return M3UA_ERR_UNSUPP_MSG_TYPE;
+	}
+}
+
+/* map from M3UA ASPSM/ASPTM to xua_asp_fsm event */
+static const struct xua_msg_event_map m3ua_aspxm_map[] = {
+	{ M3UA_MSGC_ASPSM, M3UA_ASPSM_UP, XUA_ASP_E_ASPSM_ASPUP },
+	{ M3UA_MSGC_ASPSM, M3UA_ASPSM_DOWN, XUA_ASP_E_ASPSM_ASPDN },
+	{ M3UA_MSGC_ASPSM, M3UA_ASPSM_BEAT, XUA_ASP_E_ASPSM_BEAT },
+	{ M3UA_MSGC_ASPSM, M3UA_ASPSM_UP_ACK, XUA_ASP_E_ASPSM_ASPUP_ACK },
+	{ M3UA_MSGC_ASPSM, M3UA_ASPSM_DOWN_ACK, XUA_ASP_E_ASPSM_ASPDN_ACK },
+	{ M3UA_MSGC_ASPSM, M3UA_ASPSM_BEAT_ACK, XUA_ASP_E_ASPSM_BEAT_ACK },
+	{ M3UA_MSGC_ASPTM, M3UA_ASPTM_ACTIVE, XUA_ASP_E_ASPTM_ASPAC },
+	{ M3UA_MSGC_ASPTM, M3UA_ASPTM_INACTIVE, XUA_ASP_E_ASPTM_ASPIA },
+	{ M3UA_MSGC_ASPTM, M3UA_ASPTM_ACTIVE_ACK, XUA_ASP_E_ASPTM_ASPAC_ACK },
+	{ M3UA_MSGC_ASPTM, M3UA_ASPTM_INACTIVE_ACK, XUA_ASP_E_ASPTM_ASPIA_ACK },
+};
+
+
+static int m3ua_rx_asp(struct osmo_ss7_asp *asp, struct xua_msg *xua)
+{
+	int event;
+
+	/* map from the M3UA message class and message type to the XUA
+	 * ASP FSM event number */
+	event = xua_msg_event_map(xua, m3ua_aspxm_map,
+				  ARRAY_SIZE(m3ua_aspxm_map));
+	if (event < 0)
+		return M3UA_ERR_UNSUPP_MSG_TYPE;
+
+	/* deliver that event to the ASP FSM */
+	osmo_fsm_inst_dispatch(asp->fi, event, xua);
+
+	return 0;
+}
+
+/*! \brief process M3UA message received from socket
+ *  \param[in] asp Application Server Process receiving \ref msg
+ *  \param[in] msg received message buffer
+ *  \returns 0 on success; negative on error */
+int m3ua_rx_msg(struct osmo_ss7_asp *asp, struct msgb *msg)
+{
+	struct xua_msg *xua = NULL, *err = NULL;
+	int rc = 0;
+
+	OSMO_ASSERT(asp->cfg.proto == OSMO_SS7_ASP_PROT_M3UA);
+
+	/* caller owns msg memory, we shall neither free it here nor
+	 * keep references beyond the executin of this function and its
+	 * callees */
+
+	xua = xua_from_msg(M3UA_VERSION, msgb_length(msg), msgb_data(msg));
+	if (!xua) {
+		struct xua_common_hdr *hdr = (struct xua_common_hdr *) msg->data;
+
+		LOGPASP(asp, DLM3UA, LOGL_ERROR, "Unable to parse incoming "
+			"M3UA message\n");
+
+		if (hdr->version != M3UA_VERSION)
+			err = m3ua_gen_error_msg(M3UA_ERR_INVALID_VERSION, msg);
+		else
+			err = m3ua_gen_error_msg(M3UA_ERR_PARAM_FIELD_ERR, msg);
+		goto out;
+	}
+
+	LOGPASP(asp, DLM3UA, LOGL_DEBUG, "Received M3UA Message (%s)\n",
+		xua_hdr_dump(xua, &xua_dialect_m3ua));
+
+	if (!xua_dialect_check_all_mand_ies(&xua_dialect_m3ua, xua)) {
+		err = m3ua_gen_error_msg(M3UA_ERR_MISSING_PARAM, msg);
+		goto out;
+	}
+
+	/* TODO: check for SCTP Strema ID */
+	/* TODO: check if any AS configured in ASP */
+	/* TODO: check for valid routing context */
+
+	switch (xua->hdr.msg_class) {
+	case M3UA_MSGC_XFER:
+		rc = m3ua_rx_xfer(asp, xua);
+		break;
+	case M3UA_MSGC_ASPSM:
+	case M3UA_MSGC_ASPTM:
+		rc = m3ua_rx_asp(asp, xua);
+		break;
+		break;
+	case M3UA_MSGC_MGMT:
+		rc = m3ua_rx_mgmt(asp, xua);
+		break;
+	case M3UA_MSGC_SNM:
+	case M3UA_MSGC_RKM:
+		/* FIXME */
+		LOGPASP(asp, DLM3UA, LOGL_NOTICE, "Received unsupported M3UA "
+			"Message Class %u\n", xua->hdr.msg_class);
+		err = m3ua_gen_error_msg(M3UA_ERR_UNSUPP_MSG_CLASS, msg);
+		break;
+	default:
+		LOGPASP(asp, DLM3UA, LOGL_NOTICE, "Received unknown M3UA "
+			"Message Class %u\n", xua->hdr.msg_class);
+		err = m3ua_gen_error_msg(M3UA_ERR_UNSUPP_MSG_CLASS, msg);
+		break;
+	}
+
+	if (rc > 0)
+		err = m3ua_gen_error_msg(rc, msg);
+
+out:
+	if (err)
+		m3ua_tx_xua_asp(asp, err);
+
+	xua_msg_free(xua);
+
+	return rc;
+}
diff --git a/src/osmo_ss7.c b/src/osmo_ss7.c
new file mode 100644
index 0000000..74c54bb
--- /dev/null
+++ b/src/osmo_ss7.c
@@ -0,0 +1,1490 @@
+/* Core SS7 Instance/Linkset/Link/AS/ASP Handling */
+
+/* (C) 2015-2017 by Harald Welte <laforge at gnumonks.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <netinet/sctp.h>
+
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/mtp_sap.h>
+#include <osmocom/sigtran/protocol/sua.h>
+#include <osmocom/sigtran/protocol/m3ua.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/socket.h>
+
+#include <osmocom/netif/stream.h>
+
+#include "xua_internal.h"
+#include "xua_asp_fsm.h"
+#include "xua_as_fsm.h"
+
+#define ASP_MSGB_SIZE	1500
+#define MAX_PC_STR_LEN 32
+
+static bool ss7_initialized = false;
+
+static LLIST_HEAD(ss7_instances);
+static LLIST_HEAD(ss7_xua_servers);
+
+struct value_string osmo_ss7_as_traffic_mode_vals[] = {
+	{ OSMO_SS7_AS_TMOD_BCAST,	"broadcast" },
+	{ OSMO_SS7_AS_TMOD_LOADSHARE,	"loadshare" },
+	{ OSMO_SS7_AS_TMOD_ROUNDROBIN,	"round-robin" },
+	{ OSMO_SS7_AS_TMOD_OVERRIDE,	"override" },
+	{ 0, NULL }
+};
+
+struct value_string osmo_ss7_asp_protocol_vals[] = {
+	{ OSMO_SS7_ASP_PROT_NONE,	"none" },
+	{ OSMO_SS7_ASP_PROT_SUA,	"sua" },
+	{ OSMO_SS7_ASP_PROT_M3UA,	"m3ua" },
+	{ 0, NULL }
+};
+
+#define LOGSS7(inst, level, fmt, args ...)	\
+	LOGP(DLSS7, level, "%u: " fmt, (inst)->cfg.id, ## args)
+
+
+/***********************************************************************
+ * SS7 Point Code Parsing / Printing
+ ***********************************************************************/
+
+/* like strcat() but appends a single character */
+static int strnappendchar(char *str, char c, size_t n)
+{
+	unsigned int curlen = strlen(str);
+
+	if (n < curlen + 2)
+		return -1;
+
+	str[curlen] = c;
+	str[curlen+1] = '\0';
+
+	return curlen+1;
+}
+
+/* generate a format string for formatting a point code. The result can
+ * e.g. be used with sscanf() or sprintf() */
+static const char *gen_pc_fmtstr(struct osmo_ss7_instance *inst,
+				 unsigned int *num_comp_exp)
+{
+	static char buf[MAX_PC_STR_LEN];
+	unsigned int num_comp = 0;
+
+	buf[0] = '\0';
+	strcat(buf, "%u");
+	num_comp++;
+
+	if (inst->cfg.pc_fmt.component_len[1] == 0)
+		goto out;
+	strnappendchar(buf, inst->cfg.pc_fmt.delimiter, sizeof(buf));
+	strcat(buf, "%u");
+	num_comp++;
+
+	if (inst->cfg.pc_fmt.component_len[2] == 0)
+		goto out;
+	strnappendchar(buf, inst->cfg.pc_fmt.delimiter, sizeof(buf));
+	strcat(buf, "%u");
+	num_comp++;
+out:
+	if (num_comp_exp)
+		*num_comp_exp = num_comp;
+	return buf;
+}
+
+/* get number of components we expect for a point code, depending on the
+ * configuration of this ss7_instance */
+static unsigned int num_pc_comp_exp(struct osmo_ss7_instance *inst)
+{
+	unsigned int num_comp_exp = 1;
+
+	if (inst->cfg.pc_fmt.component_len[1])
+		num_comp_exp++;
+	if (inst->cfg.pc_fmt.component_len[2])
+		num_comp_exp++;
+
+	return num_comp_exp;
+}
+
+/* get the total width (in bits) of the point-codes in this ss7_instance */
+static unsigned int get_pc_width(struct osmo_ss7_instance *inst)
+{
+	return inst->cfg.pc_fmt.component_len[0] +
+		inst->cfg.pc_fmt.component_len[1] +
+		inst->cfg.pc_fmt.component_len[2];
+}
+
+/* get the number of bits we must shift the given component of a point
+ * code in this ss7_instance */
+static unsigned int get_pc_comp_shift(struct osmo_ss7_instance *inst,
+					unsigned int comp_num)
+{
+	uint32_t pc_width = get_pc_width(inst);
+	switch (comp_num) {
+	case 0:
+		return pc_width - inst->cfg.pc_fmt.component_len[0];
+	case 1:
+		return pc_width - inst->cfg.pc_fmt.component_len[0] -
+			inst->cfg.pc_fmt.component_len[1];
+	case 2:
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
+
+static uint32_t pc_comp_shift_and_mask(struct osmo_ss7_instance *inst,
+					unsigned int comp_num, uint32_t pc)
+{
+	unsigned int shift = get_pc_comp_shift(inst, comp_num);
+	uint32_t mask = (1 << inst->cfg.pc_fmt.component_len[comp_num]) - 1;
+
+	return (pc >> shift) & mask;
+}
+
+/* parse a point code according to the structure configured for this
+ * ss7_instance */
+int osmo_ss7_pointcode_parse(struct osmo_ss7_instance *inst, const char *str)
+{
+	unsigned int component[3];
+	unsigned int num_comp_exp = num_pc_comp_exp(inst);
+	const char *fmtstr = gen_pc_fmtstr(inst, &num_comp_exp);
+	int i, rc;
+
+	rc = sscanf(str, fmtstr, &component[0], &component[1], &component[2]);
+	/* ensure all components were parsed */
+	if (rc != num_comp_exp)
+		goto err;
+
+	/* check none of the component values exceeds what can be
+	 * represented within its bit-width */
+	for (i = 0; i < num_comp_exp; i++) {
+		if (component[i] >= (1 << inst->cfg.pc_fmt.component_len[i]))
+			goto err;
+	}
+
+	/* shift them all together */
+	rc = (component[0] << get_pc_comp_shift(inst, 0));
+	if (num_comp_exp > 1)
+		rc |= (component[1] << get_pc_comp_shift(inst, 1));
+	if (num_comp_exp > 2)
+		rc |= (component[2] << get_pc_comp_shift(inst, 2));
+
+	return rc;
+
+err:
+	LOGSS7(inst, LOGL_NOTICE, "Error parsing Pointcode '%s'\n", str);
+	return -EINVAL;
+}
+
+/* print a pointcode according to the structure configured for this
+ * ss7_instance */
+const char *osmo_ss7_pointcode_print(struct osmo_ss7_instance *inst, uint32_t pc)
+{
+	static char buf[MAX_PC_STR_LEN];
+	unsigned int num_comp_exp = num_pc_comp_exp(inst);
+	const char *fmtstr = gen_pc_fmtstr(inst, &num_comp_exp);
+
+	OSMO_ASSERT(fmtstr);
+	snprintf(buf, sizeof(buf), fmtstr,
+		 pc_comp_shift_and_mask(inst, 0, pc),
+		 pc_comp_shift_and_mask(inst, 1, pc),
+		 pc_comp_shift_and_mask(inst, 2, pc));
+
+	return buf;
+}
+
+int osmo_ss7_pointcode_parse_mask_or_len(struct osmo_ss7_instance *inst, const char *in)
+{
+	unsigned int width = get_pc_width(inst);
+
+	if (in[0] == '/') {
+		/* parse mask by length */
+		int masklen = atoi(in+1);
+		if (masklen < 0 || masklen > 32)
+			return -EINVAL;
+		if (masklen == 0)
+			return 0;
+		return (0xFFFFFFFF << (width - masklen)) & ((1 << width)-1);
+	} else {
+		/* parse mask as point code */
+		return osmo_ss7_pointcode_parse(inst, in);
+	}
+}
+
+static const uint16_t prot2port[] = {
+	[OSMO_SS7_ASP_PROT_NONE] = 0,
+	[OSMO_SS7_ASP_PROT_SUA] = SUA_PORT,
+	[OSMO_SS7_ASP_PROT_M3UA] = M3UA_PORT,
+};
+
+int osmo_ss7_asp_protocol_port(enum osmo_ss7_asp_protocol prot)
+{
+	if (prot >= ARRAY_SIZE(prot2port))
+		return -EINVAL;
+	else
+		return prot2port[prot];
+}
+
+/***********************************************************************
+ * SS7 Instance
+ ***********************************************************************/
+
+/*! \brief Find a SS7 Instance with given ID
+ *  \param[in] id ID for which to search
+ *  \returns \ref osmo_ss7_instance on success; NULL on error */
+struct osmo_ss7_instance *
+osmo_ss7_instance_find(uint32_t id)
+{
+	OSMO_ASSERT(ss7_initialized);
+
+	struct osmo_ss7_instance *inst;
+	llist_for_each_entry(inst, &ss7_instances, list) {
+		if (inst->cfg.id == id)
+			return inst;
+	}
+	return NULL;
+}
+
+/*! \brief Find or create a SS7 Instance
+ *  \param[in] ctx talloc allocation context to use for allocations
+ *  \param[in] id ID of SS7 Instance
+ *  \returns \ref osmo_ss7_instance on success; NULL on error */
+struct osmo_ss7_instance *
+osmo_ss7_instance_find_or_create(void *ctx, uint32_t id)
+{
+	struct osmo_ss7_instance *inst;
+
+	OSMO_ASSERT(ss7_initialized);
+
+	inst = osmo_ss7_instance_find(id);
+	if (!inst)
+		inst = talloc_zero(ctx, struct osmo_ss7_instance);
+	if (!inst)
+		return NULL;
+
+	inst->cfg.id = id;
+	LOGSS7(inst, LOGL_INFO, "Creating SS7 Instance\n");
+
+	INIT_LLIST_HEAD(&inst->linksets);
+	INIT_LLIST_HEAD(&inst->as_list);
+	INIT_LLIST_HEAD(&inst->asp_list);
+	INIT_LLIST_HEAD(&inst->rtable_list);
+	inst->rtable_system = osmo_ss7_route_table_find_or_create(inst, "system");
+
+	/* default point code structure + formatting */
+	inst->cfg.pc_fmt.delimiter = '.';
+	inst->cfg.pc_fmt.component_len[0] = 3;
+	inst->cfg.pc_fmt.component_len[1] = 8;
+	inst->cfg.pc_fmt.component_len[2] = 3;
+
+	llist_add(&inst->list, &ss7_instances);
+
+	return inst;
+}
+
+/*! \brief Destroy a SS7 Instance
+ *  \param[in] inst SS7 Instance to be destroyed */
+void osmo_ss7_instance_destroy(struct osmo_ss7_instance *inst)
+{
+	struct osmo_ss7_linkset *lset;
+	struct osmo_ss7_as *as;
+	struct osmo_ss7_asp *asp;
+
+	OSMO_ASSERT(ss7_initialized);
+	LOGSS7(inst, LOGL_INFO, "Destroying SS7 Instance\n");
+
+	llist_for_each_entry(asp, &inst->asp_list, list)
+		osmo_ss7_asp_destroy(asp);
+
+	llist_for_each_entry(as, &inst->as_list, list)
+		osmo_ss7_as_destroy(as);
+
+	llist_for_each_entry(lset, &inst->linksets, list)
+		osmo_ss7_linkset_destroy(lset);
+
+	llist_del(&inst->list);
+	talloc_free(inst);
+}
+
+/*! \brief Set the point code format used in given SS7 instance */
+int osmo_ss7_instance_set_pc_fmt(struct osmo_ss7_instance *inst,
+				uint8_t c0, uint8_t c1, uint8_t c2)
+{
+	if (c0+c1+c2 > 32)
+		return -EINVAL;
+
+	if (c0+c1+c2 > 14)
+		LOGSS7(inst, LOGL_NOTICE, "Point Code Format %u-%u-%u "
+			"is longer than 14 bits, odd?\n", c0, c1, c2);
+
+	inst->cfg.pc_fmt.component_len[0] = c0;
+	inst->cfg.pc_fmt.component_len[1] = c1;
+	inst->cfg.pc_fmt.component_len[2] = c2;
+
+	return 0;
+}
+
+/***********************************************************************
+ * MTP Users (Users of MTP, such as SCCP or ISUP)
+ ***********************************************************************/
+
+/*! \brief Register a MTP user for a given service indicator
+ *  \param[in] inst SS7 instance for which we register the user
+ *  \param[in] service_ind Service (ISUP, SCCP, ...)
+ *  \param[in] user SS7 user (including primitive call-back)
+ *  \returns 0 on success; negative on error */
+int osmo_ss7_user_register(struct osmo_ss7_instance *inst, uint8_t service_ind,
+			   struct osmo_ss7_user *user)
+{
+	if (service_ind >= ARRAY_SIZE(inst->user))
+		return -EINVAL;
+
+	if (inst->user[service_ind])
+		return -EBUSY;
+
+	DEBUGP(DLSS7, "registering user=%s for SI %u with priv %p\n",
+		user->name, service_ind, user->priv);
+
+	user->inst = inst;
+	inst->user[service_ind] = user;
+
+	return 0;
+}
+
+/*! \brief Unregister a MTP user for a given service indicator
+ *  \param[in] inst SS7 instance for which we register the user
+ *  \param[in] service_ind Service (ISUP, SCCP, ...)
+ *  \param[in] user (optional) SS7 user. If present, we will not
+ * 		unregister other users 
+ *  \returns 0 on success; negative on error */
+int osmo_ss7_user_unregister(struct osmo_ss7_instance *inst, uint8_t service_ind,
+			     struct osmo_ss7_user *user)
+{
+	if (service_ind >= ARRAY_SIZE(inst->user))
+		return -EINVAL;
+
+	if (!inst->user[service_ind])
+		return -ENODEV;
+
+	if (user && (inst->user[service_ind] != user))
+		return -EINVAL;
+
+	user->inst = NULL;
+	inst->user[service_ind] = NULL;
+
+	return 0;
+}
+
+/* deliver to a local MTP user */
+int osmo_ss7_mtp_to_user(struct osmo_ss7_instance *inst, struct osmo_mtp_prim *omp)
+{
+	uint32_t service_ind;
+	const struct osmo_ss7_user *osu;
+
+	if (omp->oph.sap != MTP_SAP_USER ||
+	    omp->oph.primitive != OSMO_MTP_PRIM_TRANSFER ||
+	    omp->oph.operation != PRIM_OP_INDICATION) {
+		LOGP(DLSS7, LOGL_ERROR, "Unsupported Primitive\n");
+		return -EINVAL;
+	}
+
+	service_ind = omp->u.transfer.sio & 0xF;
+	osu = inst->user[service_ind];
+
+	if (!osu) {
+		LOGP(DLSS7, LOGL_NOTICE, "No MTP-User for SI %u\n", service_ind);
+		return -ENODEV;
+	}
+
+	DEBUGP(DLSS7, "delivering MTP-TRANSFER.ind to user %s, priv=%p\n",
+		osu->name, osu->priv);
+	return osu->prim_cb(&omp->oph, (void *) osu->priv);
+}
+
+/***********************************************************************
+ * SS7 Linkset
+ ***********************************************************************/
+
+/*! \brief Destroy a SS7 Linkset
+ *  \param[in] lset Linkset to be destroyed */
+void osmo_ss7_linkset_destroy(struct osmo_ss7_linkset *lset)
+{
+	unsigned int i;
+
+	OSMO_ASSERT(ss7_initialized);
+	LOGSS7(lset->inst, LOGL_INFO, "Destroying Linkset %s\n",
+		lset->cfg.name);
+
+	for (i = 0; i < ARRAY_SIZE(lset->links); i++) {
+		struct osmo_ss7_link *link = lset->links[i];
+		if (!link)
+			continue;
+		osmo_ss7_link_destroy(link);
+	}
+	llist_del(&lset->list);
+	talloc_free(lset);
+}
+
+/*! \brief Find SS7 Linkset by given name
+ *  \param[in] inst SS7 Instance in which to look
+ *  \param[in] name Name of SS7 Linkset
+ *  \returns pointer to linkset on success; NULL on error */
+struct osmo_ss7_linkset *
+osmo_ss7_linkset_find_by_name(struct osmo_ss7_instance *inst, const char *name)
+{
+	struct osmo_ss7_linkset *lset;
+	OSMO_ASSERT(ss7_initialized);
+	llist_for_each_entry(lset, &inst->linksets, list) {
+		if (!strcmp(name, lset->cfg.name))
+			return lset;
+	}
+	return NULL;
+}
+
+/*! \brief Find or allocate SS7 Linkset
+ *  \param[in] inst SS7 Instance in which we operate
+ *  \param[in] name Name of SS7 Linkset
+ *  \param[in] pc Adjacent Pointcode
+ *  \returns pointer to Linkset on success; NULL on error */
+struct osmo_ss7_linkset *
+osmo_ss7_linkset_find_or_create(struct osmo_ss7_instance *inst, const char *name, uint32_t pc)
+{
+	struct osmo_ss7_linkset *lset;
+
+	OSMO_ASSERT(ss7_initialized);
+	lset = osmo_ss7_linkset_find_by_name(inst, name);
+	if (lset && lset->cfg.adjacent_pc != pc)
+		return NULL;
+
+	if (!lset) {
+		LOGSS7(inst, LOGL_INFO, "Creating Linkset %s\n", name);
+		lset = talloc_zero(inst, struct osmo_ss7_linkset);
+		lset->inst = inst;
+		lset->cfg.adjacent_pc = pc;
+		lset->cfg.name = talloc_strdup(lset, name);
+		llist_add_tail(&lset->list, &inst->linksets);
+	}
+
+	return lset;
+}
+
+/***********************************************************************
+ * SS7 Link
+ ***********************************************************************/
+
+/*! \brief Destryo SS7 Link
+ *  \param[in] link SS7 Link to be destroyed */
+void osmo_ss7_link_destroy(struct osmo_ss7_link *link)
+{
+	struct osmo_ss7_linkset *lset = link->linkset;
+
+	OSMO_ASSERT(ss7_initialized);
+	LOGSS7(lset->inst, LOGL_INFO, "Destroying Link %s:%u\n",
+		lset->cfg.name, link->cfg.id);
+	/* FIXME: do cleanup */
+	lset->links[link->cfg.id] = NULL;
+	talloc_free(link);
+}
+
+/*! \brief Find or create SS7 Link with given ID in given Linkset
+ *  \param[in] lset SS7 Linkset on which we operate
+ *  \param[in] id Link number within Linkset
+ *  \returns pointer to SS7 Link on success; NULL on error */
+struct osmo_ss7_link *
+osmo_ss7_link_find_or_create(struct osmo_ss7_linkset *lset, uint32_t id)
+{
+	struct osmo_ss7_link *link;
+
+	OSMO_ASSERT(ss7_initialized);
+	if (id >= ARRAY_SIZE(lset->links))
+		return NULL;
+
+	if (lset->links[id]) {
+		link = lset->links[id];
+	} else {
+		LOGSS7(lset->inst, LOGL_INFO, "Creating Link %s:%u\n",
+			lset->cfg.name, id);
+		link = talloc_zero(lset, struct osmo_ss7_link);
+		if (!link)
+			return NULL;
+		link->linkset = lset;
+		lset->links[id] = link;
+		link->cfg.id = id;
+	}
+
+	return link;
+}
+
+
+/***********************************************************************
+ * SS7 Route Tables
+ ***********************************************************************/
+
+struct osmo_ss7_route_table *
+osmo_ss7_route_table_find(struct osmo_ss7_instance *inst, const char *name)
+{
+	struct osmo_ss7_route_table *rtbl;
+	OSMO_ASSERT(ss7_initialized);
+	llist_for_each_entry(rtbl, &inst->rtable_list, list) {
+		if (!strcmp(rtbl->cfg.name, name))
+			return rtbl;
+	}
+	return NULL;
+}
+
+struct osmo_ss7_route_table *
+osmo_ss7_route_table_find_or_create(struct osmo_ss7_instance *inst, const char *name)
+{
+	struct osmo_ss7_route_table *rtbl;
+
+	OSMO_ASSERT(ss7_initialized);
+	rtbl = osmo_ss7_route_table_find(inst, name);
+	if (!rtbl) {
+		LOGSS7(inst, LOGL_INFO, "Creating Route Table %s\n", name);
+		rtbl = talloc_zero(inst, struct osmo_ss7_route_table);
+		rtbl->inst = inst;
+		rtbl->cfg.name = talloc_strdup(rtbl, name);
+		INIT_LLIST_HEAD(&rtbl->routes);
+		llist_add_tail(&rtbl->list, &inst->rtable_list);
+	}
+	return rtbl;
+}
+
+void osmo_ss7_route_table_destroy(struct osmo_ss7_route_table *rtbl)
+{
+	llist_del(&rtbl->list);
+	/* routes are allocated as children of route table, will be
+	 * automatically freed() */
+	talloc_free(rtbl);
+}
+
+/***********************************************************************
+ * SS7 Routes
+ ***********************************************************************/
+
+/*! \brief Find a SS7 route for given destination point code in given table */
+struct osmo_ss7_route *
+osmo_ss7_route_find_dpc(struct osmo_ss7_route_table *rtbl, uint32_t dpc)
+{
+	struct osmo_ss7_route *rt;
+
+	OSMO_ASSERT(ss7_initialized);
+	/* we assume the routes are sorted by mask length, i.e. more
+	 * specific routes first, and less specific routes with shorter
+	 * mask later */
+	llist_for_each_entry(rt, &rtbl->routes, list) {
+		if ((dpc & rt->cfg.mask) == rt->cfg.pc)
+			return rt;
+	}
+	return NULL;
+}
+
+/*! \brief Find a SS7 route for given destination point code + mask in given table */
+struct osmo_ss7_route *
+osmo_ss7_route_find_dpc_mask(struct osmo_ss7_route_table *rtbl, uint32_t dpc,
+				uint32_t mask)
+{
+	struct osmo_ss7_route *rt;
+
+	OSMO_ASSERT(ss7_initialized);
+	/* we assume the routes are sorted by mask length, i.e. more
+	 * specific routes first, and less specific routes with shorter
+	 * mask later */
+	llist_for_each_entry(rt, &rtbl->routes, list) {
+		if (dpc == rt->cfg.pc && mask == rt->cfg.mask)
+			return rt;
+	}
+	return NULL;
+}
+
+/*! \brief Find a SS7 route for given destination point code in given SS7 */
+struct osmo_ss7_route *
+osmo_ss7_route_lookup(struct osmo_ss7_instance *inst, uint32_t dpc)
+{
+	OSMO_ASSERT(ss7_initialized);
+	return osmo_ss7_route_find_dpc(inst->rtable_system, dpc);
+}
+
+/* insert the route in the ordered list of routes. The list is sorted by
+ * mask length, so that the more specific (longer mask) routes are
+ * first, while the less specific routes with shorter masks are last.
+ * Hence, the first matching route in a linear iteration is the most
+ * specific match. */
+static void route_insert_sorted(struct osmo_ss7_route_table *rtbl,
+				struct osmo_ss7_route *cmp)
+{
+	struct osmo_ss7_route *rt;
+
+	llist_for_each_entry(rt, &rtbl->routes, list) {
+		if (rt->cfg.mask < cmp->cfg.mask) {
+			/* insert before the current entry */
+			llist_add(&cmp->list, rt->list.prev);
+			return;
+		}
+	}
+	/* not added, i.e. no smaller mask length found: we are the
+	 * smallest mask and thus should go last */
+	llist_add_tail(&cmp->list, &rtbl->routes);
+}
+
+/*! \brief Create a new route in the given routing table
+ *  \param[in] rtbl Routing Table in which the route is to be created
+ *  \param[in] pc Point Code of the destination of the route
+ *  \param[in] mask Mask of the destination Point Code \ref pc
+ *  \param[in] linkset_name string name of the linkset to be used
+ *  \returns caller-allocated + initialized route, NULL on error
+ */
+struct osmo_ss7_route *
+osmo_ss7_route_create(struct osmo_ss7_route_table *rtbl, uint32_t pc,
+		      uint32_t mask, const char *linkset_name)
+{
+	struct osmo_ss7_route *rt;
+	struct osmo_ss7_linkset *lset;
+	struct osmo_ss7_as *as;
+
+	OSMO_ASSERT(ss7_initialized);
+	lset = osmo_ss7_linkset_find_by_name(rtbl->inst, linkset_name);
+	if (!lset) {
+		as = osmo_ss7_as_find_by_name(rtbl->inst, linkset_name);
+		if (!as)
+			return NULL;
+	}
+
+	rt = talloc_zero(rtbl, struct osmo_ss7_route);
+	if (!rt)
+		return NULL;
+
+	rt->cfg.pc = pc;
+	rt->cfg.mask = mask;
+	rt->cfg.linkset_name = talloc_strdup(rt, linkset_name);
+	if (lset)
+		rt->dest.linkset = lset;
+	else
+		rt->dest.as = as;
+	rt->rtable = rtbl;
+
+	route_insert_sorted(rtbl, rt);
+
+	return rt;
+}
+
+/*! \brief Destroy a given SS7 route */
+void osmo_ss7_route_destroy(struct osmo_ss7_route *rt)
+{
+	OSMO_ASSERT(ss7_initialized);
+	llist_del(&rt->list);
+	talloc_free(rt);
+}
+
+/***********************************************************************
+ * SS7 Application Server
+ ***********************************************************************/
+
+/*! \brief Find Application Server by given name
+ *  \param[in] inst SS7 Instance on which we operate
+ *  \param[in] name Name of AS
+ *  \returns pointer to Application Server on success; NULL otherwise */
+struct osmo_ss7_as *
+osmo_ss7_as_find_by_name(struct osmo_ss7_instance *inst, const char *name)
+{
+	struct osmo_ss7_as *as;
+
+	OSMO_ASSERT(ss7_initialized);
+	llist_for_each_entry(as, &inst->as_list, list) {
+		if (!strcmp(name, as->cfg.name))
+			return as;
+	}
+	return NULL;
+}
+
+/*! \brief Find Application Server by given routing context
+ *  \param[in] inst SS7 Instance on which we operate
+ *  \param[in] rctx Routing Context
+ *  \returns pointer to Application Server on success; NULL otherwise */
+struct osmo_ss7_as *
+osmo_ss7_as_find_by_rctx(struct osmo_ss7_instance *inst, uint32_t rctx)
+{
+	struct osmo_ss7_as *as;
+
+	OSMO_ASSERT(ss7_initialized);
+	llist_for_each_entry(as, &inst->as_list, list) {
+		if (as->cfg.routing_key.context == rctx)
+			return as;
+	}
+	return NULL;
+}
+
+/*! \brief Find or Create Application Server
+ *  \param[in] inst SS7 Instance on which we operate
+ *  \param[in] name Name of Application Server
+ *  \param[in] proto Protocol of Application Server
+ *  \returns pointer to Application Server on suuccess; NULL otherwise */
+struct osmo_ss7_as *
+osmo_ss7_as_find_or_create(struct osmo_ss7_instance *inst, const char *name,
+			   enum osmo_ss7_asp_protocol proto)
+{
+	struct osmo_ss7_as *as;
+
+	OSMO_ASSERT(ss7_initialized);
+	as = osmo_ss7_as_find_by_name(inst, name);
+
+	if (as && as->cfg.proto != proto)
+		return NULL;
+
+	if (!as) {
+		LOGSS7(inst, LOGL_INFO, "Creating AS %s\n", name);
+		as = talloc_zero(inst, struct osmo_ss7_as);
+		if (!as)
+			return NULL;
+		as->inst = inst;
+		as->cfg.name = talloc_strdup(as, name);
+		as->cfg.proto = proto;
+		as->cfg.mode = OSMO_SS7_AS_TMOD_LOADSHARE;
+		as->cfg.recovery_timeout_msec = 2000;
+		as->fi = xua_as_fsm_start(as, LOGL_DEBUG);
+		llist_add_tail(&as->list, &inst->as_list);
+	}
+
+	return as;
+}
+
+/*! \brief Add given ASP to given AS
+ *  \param[in] as Application Server to which \ref asp is added
+ *  \param[in] asp Application Server Process to be added to \ref as
+ *  \returns 0 on success; negative in case of error */
+int osmo_ss7_as_add_asp(struct osmo_ss7_as *as, const char *asp_name)
+{
+	struct osmo_ss7_asp *asp;
+	unsigned int i;
+
+	OSMO_ASSERT(ss7_initialized);
+	asp = osmo_ss7_asp_find_by_name(as->inst, asp_name);
+	if (!asp)
+		return -ENODEV;
+
+	LOGSS7(as->inst, LOGL_INFO, "Adding ASP %s to AS %s\n",
+		asp->cfg.name, as->cfg.name);
+
+	if (osmo_ss7_as_has_asp(as, asp))
+		return 0;
+
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		if (!as->cfg.asps[i]) {
+			as->cfg.asps[i] = asp;
+			return 0;
+		}
+	}
+
+	return -ENOSPC;
+}
+
+/*! \brief Delete given ASP from given AS
+ *  \param[in] as Application Server from which \ref asp is deleted
+ *  \param[in] asp Application Server Process to delete from \ref as
+ *  \returns 0 on success; negative in case of error */
+int osmo_ss7_as_del_asp(struct osmo_ss7_as *as, const char *asp_name)
+{
+	struct osmo_ss7_asp *asp;
+	unsigned int i;
+
+	OSMO_ASSERT(ss7_initialized);
+	asp = osmo_ss7_asp_find_by_name(as->inst, asp_name);
+	if (!asp)
+		return -ENODEV;
+
+	LOGSS7(as->inst, LOGL_INFO, "Removing ASP %s from AS %s\n",
+		asp->cfg.name, as->cfg.name);
+
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		if (as->cfg.asps[i] == asp) {
+			as->cfg.asps[i] = NULL;
+			return 0;
+		}
+	}
+
+	return -EINVAL;
+}
+
+/*! \brief Destroy given Application Server
+ *  \param[in] as Application Server to destroy */
+void osmo_ss7_as_destroy(struct osmo_ss7_as *as)
+{
+	OSMO_ASSERT(ss7_initialized);
+	LOGSS7(as->inst, LOGL_INFO, "Destroying AS %s\n", as->cfg.name);
+
+	if (as->fi)
+		osmo_fsm_inst_term(as->fi, OSMO_FSM_TERM_REQUEST, NULL);
+
+	as->inst = NULL;
+	llist_del(&as->list);
+	talloc_free(as);
+}
+
+/*! \brief Determine if given AS contains ASP
+ *  \param[in] as Application Server in which to look for \ref asp
+ *  \param[in] asp Application Server Process to look for in \ref as
+ *  \returns true in case \ref asp is part of \ref as; false otherwise */
+bool osmo_ss7_as_has_asp(struct osmo_ss7_as *as,
+			 struct osmo_ss7_asp *asp)
+{
+	unsigned int i;
+
+	OSMO_ASSERT(ss7_initialized);
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		if (as->cfg.asps[i] == asp)
+			return true;
+	}
+	return false;
+}
+
+/***********************************************************************
+ * SS7 Application Server Process
+ ***********************************************************************/
+
+struct osmo_ss7_asp *
+osmo_ss7_asp_find_by_name(struct osmo_ss7_instance *inst, const char *name)
+{
+	struct osmo_ss7_asp *asp;
+
+	OSMO_ASSERT(ss7_initialized);
+	llist_for_each_entry(asp, &inst->asp_list, list) {
+		if (!strcmp(name, asp->cfg.name))
+			return asp;
+	}
+	return NULL;
+}
+
+static uint16_t get_in_port(struct sockaddr *sa)
+{
+	switch (sa->sa_family) {
+	case AF_INET:
+		return (((struct sockaddr_in*)sa)->sin_port);
+	case AF_INET6:
+	        return (((struct sockaddr_in6*)sa)->sin6_port);
+	default:
+		return 0;
+	}
+}
+
+/*! \brief Find an ASP definition matching the local+remote IP/PORT of given fd
+ *  \param[in] fd socket descriptor of given socket
+ *  \returns SS7 ASP in case a matching one is found; NULL otherwise */
+static struct osmo_ss7_asp *
+osmo_ss7_asp_find_by_socket_addr(int fd)
+{
+	struct osmo_ss7_instance *inst;
+	struct sockaddr sa_l, sa_r;
+	socklen_t sa_len_l = sizeof(sa_l);
+	socklen_t sa_len_r = sizeof(sa_r);
+	char hostbuf_l[64], hostbuf_r[64];
+	uint16_t local_port, remote_port;
+	int rc;
+
+	OSMO_ASSERT(ss7_initialized);
+	/* convert local and remote IP to string */
+	rc = getsockname(fd, &sa_l, &sa_len_l);
+	if (rc < 0)
+		return NULL;
+	rc = getnameinfo(&sa_l, sa_len_l, hostbuf_l, sizeof(hostbuf_l),
+			 NULL, 0, NI_NUMERICHOST);
+	if (rc < 0)
+		return NULL;
+	local_port = ntohs(get_in_port(&sa_l));
+
+	rc = getpeername(fd, &sa_r, &sa_len_r);
+	if (rc < 0)
+		return NULL;
+	rc = getnameinfo(&sa_r, sa_len_r, hostbuf_r, sizeof(hostbuf_r),
+			 NULL, 0, NI_NUMERICHOST);
+	if (rc < 0)
+		return NULL;
+	remote_port = ntohs(get_in_port(&sa_r));
+
+	/* check all instances for any ASP definition matching the
+	 * address combination of local/remote ip/port */
+	llist_for_each_entry(inst, &ss7_instances, list) {
+		struct osmo_ss7_asp *asp;
+		llist_for_each_entry(asp, &inst->asp_list, list) {
+			if (asp->cfg.local.port == local_port &&
+			    (!asp->cfg.remote.port ||asp->cfg.remote.port == remote_port) &&
+			    (!asp->cfg.local.host || !strcmp(asp->cfg.local.host, hostbuf_l)) &&
+			    (!asp->cfg.remote.host || !strcmp(asp->cfg.remote.host, hostbuf_r)))
+				return asp;
+		}
+	}
+
+	return NULL;
+}
+
+struct osmo_ss7_asp *
+osmo_ss7_asp_find_or_create(struct osmo_ss7_instance *inst, const char *name,
+			    uint16_t remote_port, uint16_t local_port,
+			    enum osmo_ss7_asp_protocol proto)
+{
+	struct osmo_ss7_asp *asp;
+
+	OSMO_ASSERT(ss7_initialized);
+	asp = osmo_ss7_asp_find_by_name(inst, name);
+
+	if (asp && (asp->cfg.remote.port != remote_port ||
+		    asp->cfg.local.port != local_port ||
+		    asp->cfg.proto != proto))
+		return NULL;
+
+	if (!asp) {
+		/* FIXME: check if local port has SCTP? */
+		asp = talloc_zero(inst, struct osmo_ss7_asp);
+		asp->inst = inst;
+		asp->cfg.remote.port = remote_port;
+		asp->cfg.local.port = local_port;
+		asp->cfg.proto = proto;
+		asp->cfg.name = talloc_strdup(asp, name);
+		llist_add_tail(&asp->list, &inst->asp_list);
+	}
+	return asp;
+}
+
+void osmo_ss7_asp_destroy(struct osmo_ss7_asp *asp)
+{
+	struct osmo_ss7_as *as;
+
+	OSMO_ASSERT(ss7_initialized);
+	LOGSS7(asp->inst, LOGL_INFO, "Destroying ASP %s\n", asp->cfg.name);
+
+	if (asp->server)
+		osmo_stream_srv_destroy(asp->server);
+	if (asp->client)
+		osmo_stream_cli_destroy(asp->client);
+	if (asp->fi)
+		osmo_fsm_inst_term(asp->fi, OSMO_FSM_TERM_REQUEST, NULL);
+
+	/* unlink from all ASs we are part of */
+	llist_for_each_entry(as, &asp->inst->as_list, list) {
+		unsigned int i;
+		for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+			if (as->cfg.asps[i] == asp) {
+				as->cfg.asps[i] = NULL;
+			}
+		}
+	}
+	/* unlink from ss7_instance */
+	asp->inst = NULL;
+	llist_del(&asp->list);
+	/* release memory */
+	talloc_free(asp);
+}
+
+static int xua_cli_read_cb(struct osmo_stream_cli *conn);
+static int xua_cli_connect_cb(struct osmo_stream_cli *cli);
+
+int osmo_ss7_asp_restart(struct osmo_ss7_asp *asp)
+{
+	int rc;
+	enum xua_asp_role role;
+
+	OSMO_ASSERT(ss7_initialized);
+	LOGSS7(asp->inst, LOGL_INFO, "Restarting ASP %s\n", asp->cfg.name);
+
+	if (!asp->cfg.is_server) {
+		/* We are in client mode now */
+		if (asp->server) {
+			/* if we previously were in server mode,
+			 * destroy it */
+			osmo_stream_srv_destroy(asp->server);
+			asp->server = NULL;
+		}
+		if (!asp->client)
+			asp->client = osmo_stream_cli_create(asp);
+		if (!asp->client) {
+			LOGSS7(asp->inst, LOGL_ERROR, "Unable to create stream"
+				" client for ASP %s\n", asp->cfg.name);
+			return -1;
+		}
+		osmo_stream_cli_set_addr(asp->client, asp->cfg.remote.host);
+		osmo_stream_cli_set_port(asp->client, asp->cfg.remote.port);
+		osmo_stream_cli_set_proto(asp->client, IPPROTO_SCTP);
+		osmo_stream_cli_set_reconnect_timeout(asp->client, 5);
+		osmo_stream_cli_set_connect_cb(asp->client, xua_cli_connect_cb);
+		osmo_stream_cli_set_read_cb(asp->client, xua_cli_read_cb);
+		osmo_stream_cli_set_data(asp->client, asp);
+		rc = osmo_stream_cli_open2(asp->client, 1);
+		if (rc < 0) {
+			LOGSS7(asp->inst, LOGL_ERROR, "Unable to open stream"
+				" client for ASP %s\n", asp->cfg.name);
+		}
+		/* TODO: make this configurable and not implicit */
+		role = XUA_ASPFSM_ROLE_ASP;
+	} else {
+		/* We are in server mode now */
+		if (asp->client) {
+			/* if we previously were in client mode,
+			 * destroy it */
+			osmo_stream_cli_destroy(asp->client);
+			asp->client = NULL;
+		}
+		/* FIXME: ensure we have a SCTP server */
+		LOGSS7(asp->inst, LOGL_NOTICE, "ASP Restart for server "
+			"not implemented yet!\n");
+		/* TODO: make this configurable and not implicit */
+		role = XUA_ASPFSM_ROLE_SG;
+	}
+
+	/* (re)start the ASP FSM */
+	if (asp->fi)
+		osmo_fsm_inst_term(asp->fi, OSMO_FSM_TERM_REQUEST, NULL);
+	asp->fi = xua_asp_fsm_start(asp, role, LOGL_DEBUG);
+
+	return 0;
+}
+
+/***********************************************************************
+ * libosmo-netif integration for SCTP stream server/client
+ ***********************************************************************/
+
+static const struct value_string sctp_assoc_chg_vals[] = {
+	{ SCTP_COMM_UP,		"COMM_UP" },
+	{ SCTP_COMM_LOST,	"COMM_LOST" },
+	{ SCTP_RESTART,		"RESTART" },
+	{ SCTP_SHUTDOWN_COMP,	"SHUTDOWN_COMP" },
+	{ SCTP_CANT_STR_ASSOC,	"CANT_STR_ASSOC" },
+	{ 0, NULL }
+};
+
+static const struct value_string sctp_sn_type_vals[] = {
+	{ SCTP_ASSOC_CHANGE,		"ASSOC_CHANGE" },
+	{ SCTP_PEER_ADDR_CHANGE,	"PEER_ADDR_CHANGE" },
+	{ SCTP_SHUTDOWN_EVENT, 		"SHUTDOWN_EVENT" },
+	{ SCTP_SEND_FAILED,		"SEND_FAILED" },
+	{ SCTP_REMOTE_ERROR,		"REMOTE_ERROR" },
+	{ SCTP_PARTIAL_DELIVERY_EVENT,	"PARTIAL_DELIVERY_EVENT" },
+	{ SCTP_ADAPTATION_INDICATION,	"ADAPTATION_INDICATION" },
+#ifdef SCTP_AUTHENTICATION_INDICATION
+	{ SCTP_AUTHENTICATION_INDICATION, "UTHENTICATION_INDICATION" },
+#endif
+#ifdef SCTP_SENDER_DRY_EVENT
+	{ SCTP_SENDER_DRY_EVENT,	"SENDER_DRY_EVENT" },
+#endif
+	{ 0, NULL }
+};
+
+static int get_logevel_by_sn_type(int sn_type)
+{
+	switch (sn_type) {
+	case SCTP_ADAPTATION_INDICATION:
+	case SCTP_PEER_ADDR_CHANGE:
+#ifdef SCTP_AUTHENTICATION_INDICATION
+	case SCTP_AUTHENTICATION_INDICATION:
+#endif
+#ifdef SCTP_SENDER_DRY_EVENT
+	case SCTP_SENDER_DRY_EVENT:
+#endif
+		return LOGL_INFO;
+	case SCTP_ASSOC_CHANGE:
+		return LOGL_NOTICE;
+	case SCTP_SHUTDOWN_EVENT:
+	case SCTP_PARTIAL_DELIVERY_EVENT:
+		return LOGL_NOTICE;
+	case SCTP_SEND_FAILED:
+	case SCTP_REMOTE_ERROR:
+		return LOGL_ERROR;
+	default:
+		return LOGL_NOTICE;
+	}
+}
+
+static void log_sctp_notification(struct osmo_ss7_asp *asp, const char *pfx,
+				  union sctp_notification *notif)
+{
+	int log_level;
+
+	LOGPASP(asp, DLSS7, LOGL_INFO, "%s SCTP NOTIFICATION %u flags=0x%0x\n",
+		pfx, notif->sn_header.sn_type,
+		notif->sn_header.sn_flags);
+
+	log_level = get_logevel_by_sn_type(notif->sn_header.sn_type);
+
+	switch (notif->sn_header.sn_type) {
+	case SCTP_ASSOC_CHANGE:
+		LOGPASP(asp, DLSS7, log_level, "%s SCTP_ASSOC_CHANGE: %s\n",
+			pfx, get_value_string(sctp_assoc_chg_vals,
+				notif->sn_assoc_change.sac_state));
+		break;
+	default:
+		LOGPASP(asp, DLSS7, log_level, "%s %s\n",
+			pfx, get_value_string(sctp_sn_type_vals,
+				notif->sn_header.sn_type));
+		break;
+	}
+}
+
+/* netif code tells us we can read something from the socket */
+static int xua_srv_conn_cb(struct osmo_stream_srv *conn)
+{
+	struct osmo_fd *ofd = osmo_stream_srv_get_ofd(conn);
+	struct osmo_ss7_asp *asp = osmo_stream_srv_get_data(conn);
+	struct msgb *msg = msgb_alloc(ASP_MSGB_SIZE, "xUA Server Rx");
+	struct sctp_sndrcvinfo sinfo;
+	unsigned int ppid;
+	int flags = 0;
+	int rc;
+
+	if (!msg)
+		return -ENOMEM;
+
+	/* read xUA message from socket and process it */
+	rc = sctp_recvmsg(ofd->fd, msgb_data(msg), msgb_tailroom(msg),
+			  NULL, NULL, &sinfo, &flags);
+	LOGPASP(asp, DLSS7, LOGL_DEBUG, "%s(): sctp_recvmsg() returned %d\n",
+		__func__, rc);
+	if (rc < 0) {
+		osmo_stream_srv_destroy(conn);
+		goto out;
+	} else if (rc == 0) {
+		osmo_stream_srv_destroy(conn);
+		goto out;
+	} else {
+		msgb_put(msg, rc);
+	}
+
+	if (flags & MSG_NOTIFICATION) {
+		union sctp_notification *notif = (union sctp_notification *) msgb_data(msg);
+
+		log_sctp_notification(asp, "xUA SRV", notif);
+
+		switch (notif->sn_header.sn_type) {
+		case SCTP_SHUTDOWN_EVENT:
+			osmo_stream_srv_destroy(conn);
+			osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_SCTP_COMM_DOWN_IND, asp);
+			break;
+		default:
+			break;
+		}
+		rc = 0;
+		goto out;
+	}
+
+	ppid = ntohl(sinfo.sinfo_ppid);
+	msgb_sctp_ppid(msg) = ppid;
+	msgb_sctp_stream(msg) = ntohl(sinfo.sinfo_stream);
+	msg->dst = asp;
+
+	if (ppid == M3UA_PPID && asp->cfg.proto == OSMO_SS7_ASP_PROT_M3UA)
+		rc = m3ua_rx_msg(asp, msg);
+	else {
+		LOGPASP(asp, DLSS7, LOGL_NOTICE, "SCTP chunk for unknown PPID %u "
+			"received\n", ppid);
+		rc = 0;
+	}
+
+out:
+	msgb_free(msg);
+	return rc;
+}
+
+/* client has established SCTP connection to server */
+static int xua_cli_connect_cb(struct osmo_stream_cli *cli)
+{
+	struct osmo_fd *ofd = osmo_stream_cli_get_ofd(cli);
+	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(cli);
+
+	/* update the socket name */
+	osmo_talloc_replace_string(asp, &asp->sock_name, osmo_sock_get_name(asp, ofd->fd));
+
+	LOGPASP(asp, DLSS7, LOGL_INFO, "Client connected %s\n", asp->sock_name);
+
+	/* Notify the ASP FSM that the connection has just been
+	 * established */
+	osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_M_ASP_UP_REQ, NULL);
+
+	return 0;
+}
+
+static int xua_cli_read_cb(struct osmo_stream_cli *conn)
+{
+	struct osmo_fd *ofd = osmo_stream_cli_get_ofd(conn);
+	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(conn);
+	struct msgb *msg = msgb_alloc(ASP_MSGB_SIZE, "xUA Client Rx");
+	struct sctp_sndrcvinfo sinfo;
+	unsigned int ppid;
+	int flags = 0;
+	int rc;
+
+	if (!msg)
+		return -ENOMEM;
+
+	/* read xUA message from socket and process it */
+	rc = sctp_recvmsg(ofd->fd, msgb_data(msg), msgb_tailroom(msg),
+			  NULL, NULL, &sinfo, &flags);
+	LOGPASP(asp, DLSS7, LOGL_DEBUG, "%s(): sctp_recvmsg() returned %d (flags=%d)\n",
+		__func__, rc, flags);
+	if (rc < 0) {
+		osmo_stream_cli_reconnect(conn);
+		goto out;
+	} else if (rc == 0) {
+		osmo_stream_cli_reconnect(conn);
+	} else {
+		msgb_put(msg, rc);
+	}
+
+	if (flags & MSG_NOTIFICATION) {
+		union sctp_notification *notif = (union sctp_notification *) msgb_data(msg);
+
+		log_sctp_notification(asp, "xUA CLNT", notif);
+
+		switch (notif->sn_header.sn_type) {
+		case SCTP_SHUTDOWN_EVENT:
+			osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_SCTP_COMM_DOWN_IND, asp);
+			osmo_stream_cli_reconnect(conn);
+			break;
+		default:
+			break;
+		}
+		rc = 0;
+		goto out;
+	}
+
+	if (rc == 0)
+		goto out;
+
+	ppid = ntohl(sinfo.sinfo_ppid);
+	msgb_sctp_ppid(msg) = ppid;
+	msgb_sctp_stream(msg) = ntohl(sinfo.sinfo_stream);
+	msg->dst = asp;
+
+	if (ppid == M3UA_PPID && asp->cfg.proto == OSMO_SS7_ASP_PROT_M3UA)
+		rc = m3ua_rx_msg(asp, msg);
+	else {
+		LOGPASP(asp, DLSS7, LOGL_NOTICE, "SCTP chunk for unknown PPID %u "
+			"received\n", ppid);
+		rc = 0;
+	}
+
+out:
+	msgb_free(msg);
+	return rc;
+}
+
+static int xua_srv_conn_closed_cb(struct osmo_stream_srv *srv)
+{
+	struct osmo_ss7_asp *asp = osmo_stream_srv_get_data(srv);
+
+	LOGP(DLSS7, LOGL_INFO, "%s: SCTP connection closed\n",
+		asp ? asp->cfg.name : "?");
+
+	/* FIXME: somehow notify ASP FSM and everyone else */
+
+	return 0;
+}
+
+
+/* server has accept()ed a new SCTP association, let's find the ASP for
+ * it (if any) */
+static int xua_accept_cb(struct osmo_stream_srv_link *link, int fd)
+{
+	struct osmo_xua_server *oxs = osmo_stream_srv_link_get_data(link);
+	struct osmo_stream_srv *srv;
+	struct osmo_ss7_asp *asp;
+	char *sock_name = osmo_sock_get_name(link, fd);
+
+	LOGP(DLSS7, LOGL_INFO, "%s: New SCTP connection accepted\n",
+		sock_name);
+
+	srv = osmo_stream_srv_create(oxs, link, fd,
+				     xua_srv_conn_cb,
+				     xua_srv_conn_closed_cb, NULL);
+	if (!srv) {
+		LOGP(DLSS7, LOGL_ERROR, "%s: Unable to create stream server "
+		     "for SCTP connection\n", sock_name);
+		close(fd);
+		talloc_free(sock_name);
+		return -1;
+	}
+
+	asp = osmo_ss7_asp_find_by_socket_addr(fd);
+	if (!asp) {
+		LOGP(DLSS7, LOGL_NOTICE, "%s: SCTP connection without matching "
+		     "ASP definition, terminating\n", sock_name);
+		osmo_stream_srv_destroy(srv);
+		talloc_free(sock_name);
+		return -1;
+	}
+	LOGP(DLSS7, LOGL_INFO, "%s: matched connection to ASP %s\n",
+		sock_name, asp->cfg.name);
+	/* update the ASP reference back to the server over which the
+	 * connection came in */
+	asp->server = srv;
+	/* update the ASP socket name */
+	if (asp->sock_name)
+		talloc_free(asp->sock_name);
+	asp->sock_name = talloc_reparent(link, asp, sock_name);
+	/* make sure the conn_cb() is called with the asp as private
+	 * data */
+	osmo_stream_srv_set_data(srv, asp);
+
+	return 0;
+}
+
+/*! \brief send a fully encoded msgb via a given ASP
+ *  \param[in] asp Application Server Process through which to send
+ *  \param[in] msg message buffer to transmit. Ownership transferred.
+ *  \returns 0 on success; negative in case of error */
+int osmo_ss7_asp_send(struct osmo_ss7_asp *asp, struct msgb *msg)
+{
+	OSMO_ASSERT(ss7_initialized);
+
+	switch (asp->cfg.proto) {
+	case OSMO_SS7_ASP_PROT_SUA:
+		msgb_sctp_ppid(msg) = SUA_PPID;
+		break;
+	case OSMO_SS7_ASP_PROT_M3UA:
+		msgb_sctp_ppid(msg) = M3UA_PPID;
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+
+	if (asp->cfg.is_server)
+		osmo_stream_srv_send(asp->server, msg);
+	else
+		osmo_stream_cli_send(asp->client, msg);
+
+	return 0;
+}
+
+/***********************************************************************
+ * SS7 xUA Server
+ ***********************************************************************/
+
+struct osmo_xua_server *
+osmo_ss7_xua_server_find(struct osmo_ss7_instance *inst, enum osmo_ss7_asp_protocol proto,
+			 uint16_t local_port)
+{
+	struct osmo_xua_server *xs;
+
+	OSMO_ASSERT(ss7_initialized);
+	llist_for_each_entry(xs, &ss7_xua_servers, list) {
+		if (proto == xs->cfg.proto &&
+		    local_port == xs->cfg.local.port)
+			return xs;
+	}
+	return NULL;
+}
+
+/*! \brief create a new xUA server listening to given ip/port
+ *  \param[in] ctx talloc allocation context
+ *  \param[in] proto protocol (xUA variant) to use
+ *  \param[in] local_port local SCTP port to bind/listen to
+ *  \param[in] local_host local IP address to bind/listen to (optional)
+ *  \returns callee-allocated \ref osmo_xua_server in case of success
+ */
+struct osmo_xua_server *
+osmo_ss7_xua_server_create(struct osmo_ss7_instance *inst, enum osmo_ss7_asp_protocol proto,
+			   uint16_t local_port, const char *local_host)
+{
+	struct osmo_xua_server *oxs = talloc_zero(inst, struct osmo_xua_server);
+	int rc;
+
+	OSMO_ASSERT(ss7_initialized);
+	if (!oxs)
+		return NULL;
+
+	LOGP(DLSS7, LOGL_INFO, "Creating XUA Server %s:%u\n",
+		local_host, local_port);
+
+	oxs->cfg.proto = proto;
+	oxs->cfg.local.port = local_port;
+	oxs->cfg.local.host = talloc_strdup(oxs, local_host);
+
+	oxs->server = osmo_stream_srv_link_create(oxs);
+	osmo_stream_srv_link_set_data(oxs->server, oxs);
+	osmo_stream_srv_link_set_accept_cb(oxs->server, xua_accept_cb);
+
+	osmo_stream_srv_link_set_addr(oxs->server, oxs->cfg.local.host);
+	osmo_stream_srv_link_set_port(oxs->server, oxs->cfg.local.port);
+	osmo_stream_srv_link_set_proto(oxs->server, IPPROTO_SCTP);
+
+	rc = osmo_stream_srv_link_open(oxs->server);
+	if (rc < 0) {
+		osmo_stream_srv_link_destroy(oxs->server);
+		oxs->server = NULL;
+		talloc_free(oxs);
+	}
+
+	oxs->inst = inst;
+	llist_add_tail(&oxs->list, &ss7_xua_servers);
+
+	return oxs;
+}
+
+int
+osmo_ss7_xua_server_set_local_host(struct osmo_xua_server *xs, const char *local_host)
+{
+	OSMO_ASSERT(ss7_initialized);
+	if (xs->cfg.local.host)
+		talloc_free(xs->cfg.local.host);
+	xs->cfg.local.host = talloc_strdup(xs, local_host);
+
+	osmo_stream_srv_link_set_addr(xs->server, xs->cfg.local.host);
+
+	return 0;
+}
+
+void osmo_ss7_xua_server_destroy(struct osmo_xua_server *xs)
+{
+	if (xs->server) {
+		osmo_stream_srv_link_close(xs->server);
+		osmo_stream_srv_link_destroy(xs->server);
+	}
+	/* FIXME: add asp_list to xua_server so we can iterate it here
+	 * and close all connections established in relation with this
+	 * server */
+	llist_del(&xs->list);
+	talloc_free(xs);
+}
+
+bool osmo_ss7_pc_is_local(struct osmo_ss7_instance *inst, uint32_t pc)
+{
+	OSMO_ASSERT(ss7_initialized);
+	if (pc == inst->cfg.primary_pc)
+		return true;
+	/* FIXME: Secondary and Capability Point Codes */
+	return false;
+}
+
+int osmo_ss7_init(void)
+{
+	if (ss7_initialized)
+		return 1;
+	osmo_fsm_register(&xua_as_fsm);
+	osmo_fsm_register(&xua_asp_fsm);
+	ss7_initialized = true;
+	return 0;
+}
diff --git a/src/osmo_ss7_hmrt.c b/src/osmo_ss7_hmrt.c
new file mode 100644
index 0000000..bc2b8e5
--- /dev/null
+++ b/src/osmo_ss7_hmrt.c
@@ -0,0 +1,219 @@
+/***********************************************************************
+ * MTP Level 3 - Signalling message handling (SMH) Figure 23/Q.704
+ ***********************************************************************/
+
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+
+#include <arpa/inet.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/sigtran/mtp_sap.h>
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/protocol/m3ua.h>
+
+#include "xua_internal.h"
+
+/* convert from M3UA message to MTP-TRANSFER.ind osmo_mtp_prim */
+struct osmo_mtp_prim *m3ua_to_xfer_ind(struct xua_msg *xua)
+{
+	struct osmo_mtp_prim *prim;
+	struct osmo_mtp_transfer_param *param;
+	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, M3UA_IEI_PROT_DATA);
+	struct m3ua_data_hdr *data_hdr;
+	struct msgb *upmsg = m3ua_msgb_alloc("M3UA MTP-TRANSFER.ind");
+
+	if (data_ie->len < sizeof(*data_hdr)) {
+		/* FIXME: ERROR message */
+		msgb_free(upmsg);
+		return NULL;
+	}
+	data_hdr = (struct m3ua_data_hdr *) data_ie->dat;
+
+	/* fill primitive */
+	prim = (struct osmo_mtp_prim *) msgb_put(upmsg, sizeof(*prim));
+	param = &prim->u.transfer;
+	osmo_prim_init(&prim->oph, MTP_SAP_USER,
+			OSMO_MTP_PRIM_TRANSFER,
+			PRIM_OP_INDICATION, upmsg);
+
+	m3ua_dh_to_xfer_param(param, data_hdr);
+	/* copy data */
+	upmsg->l2h = msgb_put(upmsg, data_ie->len - sizeof(*data_hdr));
+	memcpy(upmsg->l2h, data_ie->dat+sizeof(*data_hdr), data_ie->len - sizeof(*data_hdr));
+
+	return prim;
+}
+
+/* convert from MTP-TRANSFER.req to osmo_mtp_prim */
+static struct xua_msg *mtp_prim_to_m3ua(struct osmo_mtp_prim *prim)
+{
+	struct msgb *msg = prim->oph.msg;
+	struct xua_msg *xua = xua_msg_alloc();
+	struct osmo_mtp_transfer_param *param = &prim->u.transfer;
+	struct xua_msg_part *data_part;
+	struct m3ua_data_hdr data_hdr;
+
+	mtp_xfer_param_to_m3ua_dh(&data_hdr, param);
+
+	xua->hdr = XUA_HDR(M3UA_MSGC_XFER, M3UA_XFER_DATA);
+	/* Network Appearance: Optional */
+	/* Routing Context: Conditional */
+	/* Protocol Data: Mandatory */
+	data_part = talloc_zero(xua, struct xua_msg_part);
+	data_part->tag = M3UA_IEI_PROT_DATA;
+	data_part->len = sizeof(data_hdr) + msgb_l2len(msg);
+	data_part->dat = talloc_size(data_part, data_part->len);
+	memcpy(data_part->dat, &data_hdr, sizeof(data_hdr));
+	memcpy(data_part->dat+sizeof(data_hdr), msgb_l2(msg), msgb_l2len(msg));
+	llist_add_tail(&data_part->entry, &xua->headers);
+	/* Correlation Id: Optional */
+
+	return xua;
+}
+
+/* delivery given XUA message to given SS7 user */
+static int deliver_to_mtp_user(const struct osmo_ss7_user *osu,
+				struct xua_msg *xua)
+{
+	struct osmo_mtp_prim *prim;
+
+	/* Create MTP-TRANSFER.ind and feed to user */
+	prim = m3ua_to_xfer_ind(xua);
+	prim->u.transfer = xua->mtp;
+	if (!prim)
+		return -1;
+
+	return osu->prim_cb(&prim->oph, (void *) osu->priv);
+}
+
+/* HMDC -> HMDT: Message for distribution; Figure 25/Q.704 */
+/* This means it is a message we received from remote/L2, and it is to
+ * be routed to a local user part */
+static int hmdt_message_for_distribution(struct osmo_ss7_instance *inst, struct xua_msg *xua)
+{
+	struct m3ua_data_hdr *mdh;
+	const struct osmo_ss7_user *osu;
+	uint32_t service_ind;
+
+	switch (xua->hdr.msg_class) {
+	case M3UA_MSGC_XFER:
+		switch (xua->hdr.msg_type) {
+		case M3UA_XFER_DATA:
+			mdh = data_hdr_from_m3ua(xua);
+			service_ind = mdh->si & 0xf;
+			break;
+		default:
+			LOGP(DLSS7, LOGL_ERROR, "Unknown M3UA XFER Message "
+				"Type %u\n", xua->hdr.msg_type);
+			return -1;
+		}
+		break;
+	case M3UA_MSGC_SNM:
+		/* FIXME */
+		/* FIXME: SI = Signalling Network Management -> SRM/SLM/STM */
+		/* FIXME: SI = Signalling Network Testing and Maintenance -> SLTC */
+	default:
+		/* Discard Message */
+		LOGP(DLSS7, LOGL_ERROR, "Unknown M3UA Message Class %u\n",
+			xua->hdr.msg_class);
+		return -1;
+	}
+
+	/* Check for local SSN registered for this DPC/SSN */
+	osu = inst->user[service_ind];
+	if (osu) {
+		return deliver_to_mtp_user(osu, xua);
+	} else {
+		LOGP(DLSS7, LOGL_NOTICE, "No MTP-User for SI %u\n", service_ind);
+		/* Discard Message */
+		/* FIXME: User Part Unavailable HMDT -> HMRT */
+		return -1;
+	}
+}
+
+/* HMDC->HMRT Msg For Routing; Figure 26/Q.704 */
+/* local message was receive d from L4, SRM, SLM, STM or SLTC, or
+ * remote message received from L2 and HMDC determined msg for routing */
+static int hmrt_message_for_routing(struct osmo_ss7_instance *inst,
+				    struct xua_msg *xua)
+{
+	uint32_t dpc = xua->mtp.dpc;
+	struct osmo_ss7_route *rt;
+
+	/* find route for DPC */
+	/* FIXME: unify with gen_mtp_transfer_req_xua() */
+	rt = osmo_ss7_route_lookup(inst, dpc);
+	if (rt) {
+		/* FIXME: DPC SP restart? */
+		/* FIXME: DPC Congested? */
+		/* FIXME: Select link based on SLS */
+		/* FIXME: Transmit over respective Link */
+		if (rt->dest.as) {
+			struct osmo_ss7_as *as = rt->dest.as;
+			switch (as->cfg.proto) {
+			case OSMO_SS7_ASP_PROT_M3UA:
+				return m3ua_tx_xua_as(as,xua);
+			default:
+				LOGP(DLSS7, LOGL_ERROR, "MTP message "
+					"for ASP of unknown protocol%u\n",
+					as->cfg.proto);
+				break;
+			}
+		} else if (rt->dest.linkset) {
+			LOGP(DLSS7, LOGL_ERROR, "MTP-TRANSFER.req for linkset"
+				"%s unsupported\n",rt->dest.linkset->cfg.name);
+		} else
+			OSMO_ASSERT(0);
+	} else {
+		LOGP(DLSS7, LOGL_ERROR, "MTP-TRANSFER.req for DPC %u: "
+			"no route!\n", dpc);
+		/* DPC unknown HMRT -> MGMT */
+		/* Message Received for inaccesible SP HMRT ->RTPC */
+		/* Discard Message */
+	}
+	return -1;
+}
+
+/* HMDC: Received Message L2 -> L3; Figure 24/Q.704 */
+/* This means a message was received from L2 and we have to decide if it
+ * is for the local stack (HMDT) or for routng (HMRT) */
+int m3ua_hmdc_rx_from_l2(struct osmo_ss7_instance *inst, struct xua_msg *xua)
+{
+	uint32_t dpc = xua->mtp.dpc;
+	if (osmo_ss7_pc_is_local(inst, dpc)) {
+		return hmdt_message_for_distribution(inst, xua);
+	} else {
+		return hmrt_message_for_routing(inst, xua);
+	}
+}
+
+/* MTP-User requests to send a MTP-TRANSFER.req via the stack */
+int osmo_ss7_user_mtp_xfer_req(struct osmo_ss7_instance *inst,
+				struct osmo_mtp_prim *omp)
+{
+	struct xua_msg *xua;
+
+	OSMO_ASSERT(omp->oph.sap == MTP_SAP_USER);
+
+	switch (OSMO_PRIM_HDR(&omp->oph)) {
+	case OSMO_PRIM(OSMO_MTP_PRIM_TRANSFER, PRIM_OP_REQUEST):
+		xua = mtp_prim_to_m3ua(omp);
+		xua->mtp = omp->u.transfer;
+		/* normally we would call hmrt_message_for_routing()
+		 * here, if we were to follow the state diagrams of the
+		 * ITU-T Q.70x specifications.  However, what if a local
+		 * MTP user sends a MTP-TRANSFER.req to a local SSN?
+		 * This wouldn't work as per the spec, but I believe it
+		 * is a very useful feature (aka "loopback device" in
+		 * IPv4). So we call m3ua_hmdc_rx_from_l2() just like
+		 * the MTP-TRANSFER had been received from L2. */
+		return m3ua_hmdc_rx_from_l2(inst, xua);
+	default:
+		LOGP(DLSS7, LOGL_ERROR, "Ignoring unknown primitive %u:%u\n",
+			omp->oph.primitive, omp->oph.operation);
+		return -1;
+	}
+}
diff --git a/src/osmo_ss7_vty.c b/src/osmo_ss7_vty.c
new file mode 100644
index 0000000..80cd4d0
--- /dev/null
+++ b/src/osmo_ss7_vty.c
@@ -0,0 +1,681 @@
+/* Core SS7 Instance/Linkset/Link/AS/ASP VTY Interface */
+
+/* (C) 2015-2017 by Harald Welte <laforge at gnumonks.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <arpa/inet.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/logging.h>
+#include <osmocom/vty/telnet_interface.h>
+#include <osmocom/vty/misc.h>
+
+#include <osmocom/sigtran/osmo_ss7.h>
+
+#define CS7_STR	"ITU-T Signaling System 7\n"
+#define PC_STR	"Point Code\n"
+
+/***********************************************************************
+ * Core CS7 Configuration
+ ***********************************************************************/
+
+static const struct value_string ss7_network_indicator_vals[] = {
+	{ 0,	"international" },
+	{ 1,	"spare" },
+	{ 2,	"national" },
+	{ 3,	"reserved" },
+	{ 0,	NULL }
+};
+
+/* cs7 network-indicator */
+DEFUN(cs7_net_ind, cs7_net_ind_cmd,
+	"cs7 network-indicator (international | national | reserved | spare)",
+	CS7_STR "Configure the Network Indicator\n"
+	"International Network\n"
+	"National Network\n"
+	"Reserved Network\n"
+	"Spare Network\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	int ni = get_string_value(ss7_network_indicator_vals, argv[0]);
+
+	inst->cfg.network_indicator = ni;
+	return CMD_SUCCESS;
+}
+
+/* TODO: cs7 point-code format */
+DEFUN(cs7_pc_format, cs7_pc_format_cmd,
+	"cs7 point-code format <1-24> [<1-23> [<1-22>]]",
+	CS7_STR PC_STR "Configure Point Code Format\n"
+	"Length of first PC component\n"
+	"Length of second PC component\n"
+	"Length of third PC component\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	int argind = 0;
+
+	inst->cfg.pc_fmt.component_len[0] = atoi(argv[argind++]);
+
+	if (argc >= 2)
+		inst->cfg.pc_fmt.component_len[1] = atoi(argv[argind++]);
+	else
+		inst->cfg.pc_fmt.component_len[1] = 0;
+
+	if (argc >= 3)
+		inst->cfg.pc_fmt.component_len[2] = atoi(argv[argind++]);
+	else
+		inst->cfg.pc_fmt.component_len[2] = 0;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cs7_pc_format_def, cs7_pc_format_def_cmd,
+	"cs7 point-code format default",
+	CS7_STR PC_STR "Configure Point Code Format\n"
+	"Default Point Code Format (3.8.3)\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	inst->cfg.pc_fmt.component_len[0] = 3;
+	inst->cfg.pc_fmt.component_len[1] = 8;
+	inst->cfg.pc_fmt.component_len[2] = 3;
+	return CMD_SUCCESS;
+}
+
+
+/* cs7 point-code delimiter */
+DEFUN(cs7_pc_delimiter, cs7_pc_delimiter_cmd,
+	"cs7 point-code delimiter (default|dash)",
+	CS7_STR PC_STR "Configure Point Code Delimiter\n"
+	"Use dot as delimiter\n"
+	"User dash as delimiter\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+
+	if (!strcmp(argv[0], "dash"))
+		inst->cfg.pc_fmt.delimiter = '-';
+	else
+		inst->cfg.pc_fmt.delimiter = '.';
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cs7_point_code, cs7_point_code_cmd,
+	"cs7 point-code POINT_CODE",
+	CS7_STR "Configure the local Point Code\n"
+	"Point Code\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	uint32_t pc = osmo_ss7_pointcode_parse(inst, argv[0]);
+
+	inst->cfg.primary_pc = pc;
+	return CMD_SUCCESS;
+}
+/* TODO: cs7 secondary-pc */
+/* TODO: cs7 capability-pc */
+
+
+/***********************************************************************
+ * Routing Table Configuration
+ ***********************************************************************/
+
+static struct cmd_node rtable_node = {
+	L_CS7_RTABLE_NODE,
+	"%s(config-cs7-rt)# ",
+	1,
+};
+
+DEFUN(cs7_route_table, cs7_route_table_cmd,
+	"cs7 route-table system",
+	CS7_STR "Specify the name of the route table\n"
+	"Name of the route table\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	struct osmo_ss7_route_table *rtable;
+
+	rtable = inst->rtable_system;
+	vty->node = L_CS7_RTABLE_NODE;
+	vty->index = rtable;
+	vty->index_sub = &rtable->cfg.description;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cs7_rt_upd, cs7_rt_upd_cmd,
+	"update route POINT_CODE [MASK | LENGTH] linkset LS_NAME [priority PRIO] [qos-class (CLASS | default",
+	"Update the Route\n"
+	"Update the Route\n"
+	"Destination Point Code\n"
+	"Point Code Mask\n"
+	"Point Code Length\n"
+	"Specify Destination Linkset\n"
+	"Linkset Name\n"
+	"Specity Priority\n"
+	"Priority\n"
+	"Specify QoS Class\n"
+	"QoS Class\n"
+	"Default QoS Class\n")
+{
+	struct osmo_ss7_route_table *rtable = vty->index;
+	struct osmo_ss7_route *rt;
+	uint32_t dpc = osmo_ss7_pointcode_parse(rtable->inst, argv[0]);
+	uint32_t mask = osmo_ss7_pointcode_parse_mask_or_len(rtable->inst, argv[1]);
+	const char *ls_name = argv[2];
+	unsigned int argind;
+
+	rt = osmo_ss7_route_create(rtable, dpc, mask, ls_name);
+	if (!rt)
+		return CMD_WARNING;
+
+	argind = 3;
+	if (argc > argind && !strcmp(argv[argind], "priority")) {
+		argind++;
+		rt->cfg.priority = atoi(argv[argind++]);
+	}
+
+	if (argc > argind && !strcmp(argv[argind], "qos-class")) {
+		argind++;
+		rt->cfg.qos_class = atoi(argv[argind++]);
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cs7_rt_rem, cs7_rt_rem_cmd,
+	"remove route POINT_CODE [MASK | LENGTH]",
+	"Remove a Route\n"
+	"Remove a Route\n"
+	"Destination Point Code\n"
+	"Point Code Mask\n"
+	"Point Code Length\n")
+{
+	struct osmo_ss7_route_table *rtable = vty->index;
+	struct osmo_ss7_route *rt;
+	uint32_t dpc = osmo_ss7_pointcode_parse(rtable->inst, argv[0]);
+	uint32_t mask = osmo_ss7_pointcode_parse_mask_or_len(rtable->inst, argv[1]);
+
+	rt = osmo_ss7_route_find_dpc_mask(rtable, dpc, mask);
+	if (!rt)
+		return CMD_WARNING;
+
+	osmo_ss7_route_destroy(rt);
+	return CMD_SUCCESS;
+}
+
+static int config_write_rtable(struct vty *vty)
+{
+	struct osmo_ss7_route_table *rtable = vty->index;
+	struct osmo_ss7_route *rt;
+
+	vty_out(vty, "cs7 route-table %s%s", rtable->cfg.name, VTY_NEWLINE);
+	llist_for_each_entry(rt, &rtable->routes, list) {
+		vty_out(vty, " update route %s %s linkset %s",
+			osmo_ss7_pointcode_print(rtable->inst, rt->cfg.pc),
+			osmo_ss7_pointcode_print(rtable->inst, rt->cfg.mask),
+			rt->cfg.linkset_name);
+		if (rt->cfg.priority)
+			vty_out(vty, " priority %u", rt->cfg.priority);
+		if (rt->cfg.qos_class)
+			vty_out(vty, " qos-class %u", rt->cfg.qos_class);
+		vty_out(vty, "%s", VTY_NEWLINE);
+	}
+	return 0;
+}
+
+/***********************************************************************
+ * SUA Configuration
+ ***********************************************************************/
+
+static struct cmd_node sua_node = {
+	L_CS7_SUA_NODE,
+	"%s(config-cs7-sua)# ",
+	1,
+};
+
+DEFUN(cs7_sua, cs7_sua_cmd,
+	"cs7 sua <0-65534>",
+	CS7_STR
+	"Configure/Enable SUA\n"
+	"SCTP Port number for SUA\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	struct osmo_xua_server *xs;
+	uint16_t port = atoi(argv[0]);
+
+	xs = osmo_ss7_xua_server_find(inst, OSMO_SS7_ASP_PROT_SUA, port);
+	if (!xs) {
+		xs = osmo_ss7_xua_server_create(inst, OSMO_SS7_ASP_PROT_SUA, port, NULL);
+		if (!xs)
+			return CMD_SUCCESS;
+	}
+
+	vty->node = L_CS7_SUA_NODE;
+	vty->index = xs;
+	return CMD_SUCCESS;
+}
+
+DEFUN(sua_local_ip, sua_local_ip_cmd,
+	"local-ip A.B.C.D",
+	"Configure the Local IP Address for SUA\n"
+	"IP Address to use for SUA\n")
+{
+	struct osmo_xua_server *xs = vty->index;
+
+	osmo_ss7_xua_server_set_local_host(xs, argv[0]);
+	return CMD_SUCCESS;
+}
+
+enum osmo_ss7_asp_protocol parse_asp_proto(const char *protocol)
+{
+	return get_string_value(osmo_ss7_asp_protocol_vals, protocol);
+}
+
+static int config_write_sua(struct vty *vty)
+{
+	struct osmo_xua_server *xs = vty->index;
+
+	vty_out(vty, "cs7 sua %u%s", xs->cfg.local.port, VTY_NEWLINE);
+	vty_out(vty, " local-ip %s%s", xs->cfg.local.host, VTY_NEWLINE);
+	return 0;
+}
+
+/***********************************************************************
+ * M3UA Configuration
+ ***********************************************************************/
+
+static struct cmd_node m3ua_node = {
+	L_CS7_M3UA_NODE,
+	"%s(config-cs7-m3ua)# ",
+	1,
+};
+
+DEFUN(cs7_m3ua, cs7_m3ua_cmd,
+	"cs7 m3ua <0-65534>",
+	CS7_STR
+	"Configure/Enable M3UA\n"
+	"SCTP Port number for M3UA\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	struct osmo_xua_server *xs;
+	uint16_t port = atoi(argv[0]);
+
+	xs = osmo_ss7_xua_server_find(inst, OSMO_SS7_ASP_PROT_M3UA, port);
+	if (!xs) {
+		xs = osmo_ss7_xua_server_create(inst, OSMO_SS7_ASP_PROT_M3UA, port, NULL);
+		if (!xs)
+			return CMD_SUCCESS;
+	}
+
+	vty->node = L_CS7_M3UA_NODE;
+	vty->index = xs;
+	return CMD_SUCCESS;
+}
+
+DEFUN(m3ua_local_ip, m3ua_local_ip_cmd,
+	"local-ip A.B.C.D",
+	"Configure the Local IP Address for M3UA\n"
+	"IP Address to use for M3UA\n")
+{
+	struct osmo_xua_server *xs = vty->index;
+
+	osmo_ss7_xua_server_set_local_host(xs, argv[0]);
+	return CMD_SUCCESS;
+}
+
+static int config_write_m3ua(struct vty *vty)
+{
+	struct osmo_xua_server *xs = vty->index;
+
+	vty_out(vty, "cs7 m3ua %u%s", xs->cfg.local.port, VTY_NEWLINE);
+	vty_out(vty, " local-ip %s%s", xs->cfg.local.host, VTY_NEWLINE);
+	return 0;
+}
+
+/***********************************************************************
+ * Application Server Process
+ ***********************************************************************/
+
+static struct cmd_node asp_node = {
+	L_CS7_ASP_NODE,
+	"%s(config-cs7-asp)# ",
+	1,
+};
+
+DEFUN(cs7_asp, cs7_asp_cmd,
+	"cs7 asp NAME <0-65535> <0-65535> [m3ua | sua]",
+	CS7_STR
+	"Configure Application Server Process\n"
+	"Name of ASP\n"
+	"Remote SCTP port number\n"
+	"Local SCTP port number\n"
+	"M3UA Protocol\n"
+	"SUA Protocol\n")
+{
+	struct osmo_ss7_instance *inst = FIXME;
+	const char *name = argv[0];
+	uint16_t remote_port = atoi(argv[1]);
+	uint16_t local_port = atoi(argv[2]);
+	enum osmo_ss7_asp_protocol protocol = parse_asp_proto(argv[3]);
+	struct osmo_ss7_asp *asp;
+
+	if (protocol == OSMO_SS7_ASP_PROT_NONE)
+		return CMD_WARNING;
+
+	asp = osmo_ss7_asp_find_or_create(inst, name, remote_port, local_port, protocol);
+	if (!asp)
+		return CMD_WARNING;
+
+	vty->node = L_CS7_ASP_NODE;
+	vty->index = asp;
+	vty->index_sub = &asp->cfg.description;
+	return CMD_SUCCESS;
+}
+
+DEFUN(asp_remote_ip, asp_remote_ip_cmd,
+	"remote-ip A.B.C.D",
+	"Specity Remote IP Address of ASP\n"
+	"Remote IP Address of ASP\n")
+{
+	struct osmo_ss7_asp *asp = vty->index;
+	osmo_talloc_replace_string(asp, &asp->cfg.remote.host, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(asp_qos_clas, asp_qos_class_cmd,
+	"qos-class <0-255>",
+	"Specity QoS Class of ASP\n"
+	"QoS Class of ASP\n")
+{
+	struct osmo_ss7_asp *asp = vty->index;
+	asp->cfg.qos_class = atoi(argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(asp_block, asp_block_cmd,
+	"block",
+	"Allows a SCTP Association with ASP, but doesn't let it become active\n")
+{
+	struct osmo_ss7_asp *asp = vty->index;
+	vty_out(vty, "Not supported yet\n");
+	return CMD_WARNING;
+}
+
+DEFUN(asp_shutdown, asp_shutdown_cmd,
+	"shutdown",
+	"Terminates SCTP association; New associations will be rejected\n")
+{
+	struct osmo_ss7_asp *asp = vty->index;
+	vty_out(vty, "Not supported yet\n");
+	return CMD_WARNING;
+}
+
+static int config_write_asp(struct vty *vty)
+{
+	struct osmo_ss7_asp *asp = vty->index;
+
+	vty_out(vty, "cs7 asp %s %u %u %s%s",
+		asp->cfg.name, asp->cfg.remote.port, asp->cfg.local.port,
+		osmo_ss7_asp_protocol_name(asp->cfg.proto), VTY_NEWLINE);
+	vty_out(vty, " remote-ip %s%s", asp->cfg.remote.host, VTY_NEWLINE);
+	if (asp->cfg.qos_class)
+		vty_out(vty, " qos-class %u%s", asp->cfg.qos_class, VTY_NEWLINE);
+	return 0;
+}
+
+/***********************************************************************
+ * Application Server
+ ***********************************************************************/
+
+static struct cmd_node as_node = {
+	L_CS7_AS_NODE,
+	"%s(config-cs7-as)# ",
+	1,
+};
+
+DEFUN(cs7_as, cs7_as_cmd,
+	"cs7 as NAME [m3ua | sua]",
+	CS7_STR
+	"Configure an Application Server\n"
+	"Name of the Application Server\n"
+	"M3UA Application Server\n"
+	"SUA Application Server\n")
+{
+	struct osmo_ss7_as *as;
+	const char *name = argv[0];
+	enum osmo_ss7_asp_protocol protocol = parse_asp_proto(argv[1]);
+
+	if (protocol == OSMO_SS7_ASP_PROT_NONE)
+		return CMD_WARNING;
+
+	/* FIXME */
+	as->cfg.name = talloc_strdup(as, name);
+
+	vty->node = L_CS7_AS_NODE;
+	vty->index = as;
+	vty->index_sub = &as->cfg.description;
+
+	return CMD_SUCCESS;
+}
+
+/* TODO: routing-key */
+DEFUN(as_asp, as_asp_cmd,
+	"asp NAME",
+	"Specify that a given ASP is part of this AS\n"
+	"Name of ASP to be added to AS\n")
+{
+	struct osmo_ss7_as *as = vty->index;
+
+	if (osmo_ss7_as_add_asp(as, argv[0]))
+		return CMD_WARNING;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(as_no_asp, as_no_asp_cmd,
+	"no asp NAME",
+	NO_STR "Specify ASP to be removed from this AS\n"
+	"Name of ASP to be removed\n")
+{
+	struct osmo_ss7_as *as = vty->index;
+
+	if (osmo_ss7_as_del_asp(as, argv[0]))
+		return CMD_WARNING;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(as_traf_mode, as_traf_mode_cmd,
+	"traffic-mode (broadcast | loadshare | roundrobin | override)",
+	"Specifies traffic mode of operation of the ASP within the AS\n"
+	"Broadcast to all ASP within AS\n"
+	"Share Load among all ASP within AS\n"
+	"Round-Robin between all ASP within AS\n"
+	"Override\n")
+{
+	struct osmo_ss7_as *as = vty->index;
+
+	as->cfg.mode = get_string_value(osmo_ss7_as_traffic_mode_vals, argv[0]);
+	return CMD_WARNING;
+}
+
+DEFUN(as_recov_tout, as_recov_tout_cmd,
+	"recovery-timeout <1-2000>",
+	"Specifies the recovery timeout value in milliseconds\n"
+	"Recovery Timeout in Milliseconds\n")
+{
+	struct osmo_ss7_as *as = vty->index;
+	as->cfg.recovery_timeout_msec = atoi(argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(as_qos_clas, as_qos_class_cmd,
+	"qos-class <0-255>",
+	"Specity QoS Class of AS\n"
+	"QoS Class of AS\n")
+{
+	struct osmo_ss7_as *as = vty->index;
+	as->cfg.qos_class = atoi(argv[0]);
+	return CMD_SUCCESS;
+}
+
+const struct value_string mtp_si_vals[] = {
+	{ MTP_SI_SCCP,		"sccp" },
+	{ MTP_SI_TUP,		"tup" },
+	{ MTP_SI_ISUP,		"isup" },
+	{ MTP_SI_DUP,		"dup" },
+	{ MTP_SI_TESTING,	"testing" },
+	{ MTP_SI_B_ISUP,	"b-isup" },
+	{ MTP_SI_SAT_ISUP,	"sat-isup" },
+	{ MTP_SI_AAL2_SIG,	"aal2" },
+	{ MTP_SI_BICC,		"bicc" },
+	{ MTP_SI_GCP,		"h248" },
+	{ 0, NULL }
+};
+
+DEFUN(as_rout_key, as_rout_key_cmd,
+	"routing-key RCONTEXT DPC [si {aal2 | bicc | b-isup | h248 | isup | sat-isup | sccp | tup }] [ssn SSN]}",
+	"Define a routing key\n"
+	"Routing context number\n"
+	"Destination Point Code\n"
+	"Optional Match on Service Indicator\n"
+	"ATM Adaption Layer 2\n"
+	"Bearer Independent Call Control\n"
+	"Broadband ISDN User Part\n"
+	"H.248\n"
+	"ISDN User Part\n"
+	"Sattelite ISDN User Part\n"
+	"Signalling Connection Control Part\n"
+	"Telephony User Part\n"
+	"Optional Match on Sub-System Number\n"
+	"Sub-System Number to match on\n")
+{
+	struct osmo_ss7_as *as = vty->index;
+	uint32_t key = atoi(argv[0]);
+	struct osmo_ss7_routing_key *rkey;
+	int argind;
+
+	rkey = osmo_ss7_rkey_find_or_create(as, key);
+	if (!rkey)
+		return CMD_WARNING;
+
+	rkey->pc = osmo_ss7_pointcode_parse(as->inst, argv[1]);
+	argind = 2;
+
+	if (!strcmp(argv[argind], "si")) {
+		const char *si_str;
+		argind++;
+		si_str = argv[argind++];
+		/* parse numeric SI from string */
+		rkey->si = get_string_value(mtp_si_vals, si_str);
+	}
+	if (!strcmp(argv[argind], "ssn")) {
+		argind++;
+		rkey->ssn = atoi(argv[argind]);
+	}
+
+	return CMD_SUCCESS;
+}
+
+static int config_write_as(struct vty *vty)
+{
+	struct osmo_ss7_as *as = vty->index;
+	struct osmo_ss7_routing_key *rkey;
+	unsigned int i;
+
+	vty_out(vty, "cs7 as %s %s%s", as->cfg.name,
+		osmo_ss7_asp_protocol_name(as->cfg.proto), VTY_NEWLINE);
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		struct osmo_ss7_asp *asp = as->cfg.asps[i];
+		if (!asp)
+			continue;
+		vty_out(vty, " asp %s%s", asp->cfg.name, VTY_NEWLINE);
+	}
+	if (as->cfg.mode != OSMO_SS7_AS_TMOD_LOADSHARE)
+		vty_out(vty, " traffic-mode %s%s",
+			osmo_ss7_as_traffic_mode_name(as->cfg.mode), VTY_NEWLINE);
+	if (as->cfg.recovery_timeout_msec != 2000) {
+		vty_out(vty, " recovery-timeout %u%s",
+			as->cfg.recovery_timeout_msec, VTY_NEWLINE);
+	}
+	vty_out(vty, " qos-class %u%s", as->cfg.qos_class, VTY_NEWLINE);
+	rkey = &as->cfg.routing_key;
+	vty_out(vty, " routing-key %u %s", rkey->context,
+		osmo_ss7_pointcode_print(as->inst, rkey->pc));
+	if (rkey->si)
+		vty_out(vty, " si %s",
+			get_value_string(mtp_si_vals, rkey->si));
+	if (rkey->ssn)
+		vty_out(vty, " ssn %u", rkey->ssn);
+	vty_out(vty, "%s", VTY_NEWLINE);
+
+	return 0;
+}
+
+int osmo_ss7_vty_init(void)
+{
+	install_element(CONFIG_NODE, &cs7_net_ind_cmd);
+	install_element(CONFIG_NODE, &cs7_point_code_cmd);
+	install_element(CONFIG_NODE, &cs7_pc_format_cmd);
+	install_element(CONFIG_NODE, &cs7_pc_format_def_cmd);
+	install_element(CONFIG_NODE, &cs7_pc_delimiter_cmd);
+
+	install_node(&rtable_node, config_write_rtable);
+	vty_install_default(L_CS7_RTABLE_NODE);
+	install_element(CONFIG_NODE, &cs7_route_table_cmd);
+	install_element(L_CS7_RTABLE_NODE, &cfg_description_cmd);
+	install_element(L_CS7_RTABLE_NODE, &cs7_rt_upd_cmd);
+	install_element(L_CS7_RTABLE_NODE, &cs7_rt_rem_cmd);
+
+	install_node(&sua_node, config_write_sua);
+	vty_install_default(L_CS7_SUA_NODE);
+	install_element(CONFIG_NODE, &cs7_sua_cmd);
+	install_element(L_CS7_SUA_NODE, &sua_local_ip_cmd);
+
+	install_node(&m3ua_node, config_write_m3ua);
+	vty_install_default(L_CS7_M3UA_NODE);
+	install_element(CONFIG_NODE, &cs7_m3ua_cmd);
+	install_element(L_CS7_M3UA_NODE, &m3ua_local_ip_cmd);
+
+	install_node(&asp_node, config_write_asp);
+	vty_install_default(L_CS7_ASP_NODE);
+	install_element(CONFIG_NODE, &cs7_asp_cmd);
+	install_element(L_CS7_ASP_NODE, &cfg_description_cmd);
+	install_element(L_CS7_ASP_NODE, &asp_remote_ip_cmd);
+	install_element(L_CS7_ASP_NODE, &asp_qos_class_cmd);
+	install_element(L_CS7_ASP_NODE, &asp_block_cmd);
+	install_element(L_CS7_ASP_NODE, &asp_shutdown_cmd);
+
+	install_node(&as_node, config_write_as);
+	vty_install_default(L_CS7_AS_NODE);
+	install_element(CONFIG_NODE, &cs7_as_cmd);
+	install_element(L_CS7_AS_NODE, &cfg_description_cmd);
+	install_element(L_CS7_AS_NODE, &as_asp_cmd);
+	install_element(L_CS7_AS_NODE, &as_no_asp_cmd);
+	install_element(L_CS7_AS_NODE, &as_traf_mode_cmd);
+	install_element(L_CS7_AS_NODE, &as_recov_tout_cmd);
+	install_element(L_CS7_AS_NODE, &as_qos_class_cmd);
+	install_element(L_CS7_AS_NODE, &as_rout_key_cmd);
+
+	return 0;
+}
diff --git a/src/xua_as_fsm.c b/src/xua_as_fsm.c
new file mode 100644
index 0000000..887a9ec
--- /dev/null
+++ b/src/xua_as_fsm.c
@@ -0,0 +1,308 @@
+/* SCCP M3UA / SUA AS osmo_fsm according to RFC3868 4.3.1 / RFC4666 4.3.2 */
+/* (C) Copyright 2017 by Harald Welte <laforge at gnumonks.org>
+ * 
+ * All Rights reserved.
+ *
+ * Based on Erlang implementation xua_as_fsm.erl in osmo-ss7.git
+ */
+
+#include <string.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/core/prim.h>
+#include <osmocom/core/logging.h>
+
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/sigtran_sap.h>
+#include <osmocom/sigtran/xua_msg.h>
+#include <osmocom/sigtran/protocol/sua.h>
+#include <osmocom/sigtran/protocol/m3ua.h>
+
+#include "xua_asp_fsm.h"
+#include "xua_as_fsm.h"
+#include "xua_internal.h"
+
+static struct msgb *encode_notify(const struct m3ua_notify_params *npar)
+{
+	struct xua_msg *xua = m3ua_encode_notify(npar);
+	struct msgb *msg = xua_to_msg(M3UA_VERSION, xua);
+	xua_msg_free(xua);
+	return msg;
+}
+
+static int asp_notify_all_as(struct osmo_ss7_as *as, struct m3ua_notify_params *npar)
+{
+	struct msgb *msg;
+	unsigned int i, sent = 0;
+
+	/* iterate over all non-DOWN ASPs and send them the message */
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		struct osmo_ss7_asp *asp = as->cfg.asps[i];
+
+		if (!asp)
+			continue;
+
+		if (!asp->fi || asp->fi->state == XUA_ASP_S_DOWN)
+			continue;
+
+		/* Optional: ASP Identifier (if sent in ASP-UP) */
+		if (asp->asp_id_present) {
+			npar->presence |= NOTIFY_PAR_P_ASP_ID;
+			npar->asp_id = asp->asp_id;
+		} else
+			npar->presence &= ~NOTIFY_PAR_P_ASP_ID;
+
+		/* TODO: Optional Routing Context */
+
+		msg = encode_notify(npar);
+		osmo_ss7_asp_send(asp, msg);
+		sent++;
+	}
+
+	return sent;
+}
+
+
+/***********************************************************************
+ * Actual FSM
+ ***********************************************************************/
+
+#define S(x)	(1 << (x))
+
+enum xua_as_state {
+	XUA_AS_S_DOWN,
+	XUA_AS_S_INACTIVE,
+	XUA_AS_S_ACTIVE,
+	XUA_AS_S_PENDING,
+};
+
+static const struct value_string xua_as_event_names[] = {
+	{ XUA_ASPAS_ASP_INACTIVE_IND, 	"ASPAS-ASP_INACTIVE.ind" },
+	{ XUA_ASPAS_ASP_DOWN_IND,	"ASPAS-ASP_DOWN.ind" },
+	{ XUA_ASPAS_ASP_ACTIVE_IND,	"ASPAS-ASP_ACTIVE.ind" },
+	{ 0, NULL }
+};
+
+struct xua_as_fsm_priv {
+	struct osmo_ss7_as *as;
+};
+
+/* is any other ASP in this AS in state != DOWN? */
+static bool check_any_other_asp_not_down(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp_cmp)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		struct osmo_ss7_asp *asp = as->cfg.asps[i];
+		if (!asp)
+			continue;
+
+		if (asp_cmp == asp)
+			continue;
+
+		if (asp->fi && asp->fi->state != XUA_ASP_S_DOWN)
+			return true;
+	}
+
+	return false;
+}
+
+/* is any other ASP in this AS in state ACTIVE? */
+static bool check_any_other_asp_in_active(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp_cmp)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+		struct osmo_ss7_asp *asp = as->cfg.asps[i];
+		if (!asp)
+			continue;
+
+		if (asp_cmp == asp)
+			continue;
+
+		if (asp->fi && asp->fi->state == XUA_ASP_S_ACTIVE)
+			return true;
+	}
+
+	return false;
+}
+
+
+static void xua_as_fsm_down(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	switch (event) {
+	case XUA_ASPAS_ASP_INACTIVE_IND:
+		/* one ASP transitions into ASP-INACTIVE */
+		osmo_fsm_inst_state_chg(fi, XUA_AS_S_INACTIVE, 0, 0);
+		break;
+	case XUA_ASPAS_ASP_DOWN_IND:
+		/* ignore */
+		break;
+	}
+}
+
+/* onenter call-back responsible of transmitting NTFY to all ASPs in
+ * case of AS state changes */
+static void xua_as_fsm_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct xua_as_fsm_priv *xafp = (struct xua_as_fsm_priv *) fi->priv;
+	struct m3ua_notify_params npar = {
+		.status_type = M3UA_NOTIFY_T_STATCHG,
+	};
+
+	switch (fi->state) {
+	case XUA_AS_S_INACTIVE:
+		npar.status_info = M3UA_NOTIFY_I_AS_INACT;
+		break;
+	case XUA_AS_S_ACTIVE:
+		npar.status_info = M3UA_NOTIFY_I_AS_ACT;
+		break;
+	case XUA_AS_S_PENDING:
+		npar.status_info = M3UA_NOTIFY_I_AS_PEND;
+		break;
+	default:
+		return;
+	}
+
+	asp_notify_all_as(xafp->as, &npar);
+};
+
+static void xua_as_fsm_inactive(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct xua_as_fsm_priv *xafp = (struct xua_as_fsm_priv *) fi->priv;
+	struct osmo_ss7_asp *asp = data;
+
+	switch (event) {
+	case XUA_ASPAS_ASP_DOWN_IND:
+		/* one ASP transitions into ASP-DOWN */
+		if (check_any_other_asp_not_down(xafp->as, asp)) {
+			/* ignore, we stay AS_INACTIVE */
+		} else
+			osmo_fsm_inst_state_chg(fi, XUA_AS_S_DOWN, 0, 0);
+		break;
+	case XUA_ASPAS_ASP_ACTIVE_IND:
+		/* one ASP transitions into ASP-ACTIVE */
+		osmo_fsm_inst_state_chg(fi, XUA_AS_S_ACTIVE, 0, 0);
+		break;
+	case XUA_ASPAS_ASP_INACTIVE_IND:
+		/* ignore */
+		break;
+	}
+}
+
+static void xua_as_fsm_active(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct xua_as_fsm_priv *xafp = (struct xua_as_fsm_priv *) fi->priv;
+	struct osmo_ss7_asp *asp = data;
+
+	switch (event) {
+	case XUA_ASPAS_ASP_DOWN_IND:
+	case XUA_ASPAS_ASP_INACTIVE_IND:
+		if (check_any_other_asp_in_active(xafp->as, asp)) {
+			/* ignore, we stay AS_ACTIVE */
+		} else {
+			osmo_fsm_inst_state_chg(fi, XUA_AS_S_PENDING, 0, 0);
+			/* FIXME: Start T(r) */
+			/* FIXME: Queue all signalling messages until
+			 * recovery or T(r) expiry */
+		}
+		break;
+	case XUA_ASPAS_ASP_ACTIVE_IND:
+		/* ignore */
+		break;
+	}
+}
+
+static void xua_as_fsm_pending(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	switch (event) {
+	case XUA_ASPAS_ASP_ACTIVE_IND:
+		/* one ASP transitions into ASP-ACTIVE */
+		osmo_fsm_inst_state_chg(fi, XUA_AS_S_ACTIVE, 0, 0);
+		break;
+	case XUA_ASPAS_ASP_INACTIVE_IND:
+		/* ignore */
+		break;
+	case XUA_ASPAS_ASP_DOWN_IND:
+		/* ignore */
+		break;
+	}
+}
+
+static const struct osmo_fsm_state xua_as_fsm_states[] = {
+	[XUA_AS_S_DOWN] = {
+		.in_event_mask = S(XUA_ASPAS_ASP_INACTIVE_IND) |
+				 S(XUA_ASPAS_ASP_DOWN_IND),
+		.out_state_mask = S(XUA_AS_S_DOWN) |
+				  S(XUA_AS_S_INACTIVE),
+		.name = "AS_DOWN",
+		.action = xua_as_fsm_down,
+	},
+	[XUA_AS_S_INACTIVE] = {
+		.in_event_mask = S(XUA_ASPAS_ASP_DOWN_IND) |
+				 S(XUA_ASPAS_ASP_ACTIVE_IND) |
+				 S(XUA_ASPAS_ASP_INACTIVE_IND),
+		.out_state_mask = S(XUA_AS_S_DOWN) |
+				  S(XUA_AS_S_INACTIVE) |
+				  S(XUA_AS_S_ACTIVE),
+		.name = "AS_INACTIVE",
+		.action = xua_as_fsm_inactive,
+		.onenter = xua_as_fsm_onenter,
+	},
+	[XUA_AS_S_ACTIVE] = {
+		.in_event_mask = S(XUA_ASPAS_ASP_DOWN_IND) |
+				 S(XUA_ASPAS_ASP_INACTIVE_IND) |
+				 S(XUA_ASPAS_ASP_ACTIVE_IND),
+		.out_state_mask = S(XUA_AS_S_ACTIVE) |
+				  S(XUA_AS_S_PENDING),
+		.name = "AS_ACTIVE",
+		.action = xua_as_fsm_active,
+		.onenter = xua_as_fsm_onenter,
+	},
+	[XUA_AS_S_PENDING] = {
+		.in_event_mask = S(XUA_ASPAS_ASP_INACTIVE_IND) |
+				 S(XUA_ASPAS_ASP_DOWN_IND) |
+				 S(XUA_ASPAS_ASP_ACTIVE_IND),
+		.out_state_mask = S(XUA_AS_S_DOWN) |
+				  S(XUA_AS_S_INACTIVE) |
+				  S(XUA_AS_S_ACTIVE) |
+				  S(XUA_AS_S_PENDING),
+		.name = "AS_PENDING",
+		.action = xua_as_fsm_pending,
+		.onenter = xua_as_fsm_onenter,
+	},
+};
+
+struct osmo_fsm xua_as_fsm = {
+	.name = "XUA_AS",
+	.states = xua_as_fsm_states,
+	.num_states = ARRAY_SIZE(xua_as_fsm_states),
+	.log_subsys = DLSS7,
+	.event_names = xua_as_event_names,
+};
+
+/*! \brief Start an AS FSM for a given Application Server
+ *  \param[in] as Application Server for which to start the AS FSM
+ *  \param[in] log_level Logging level for logging of this FSM
+ *  \returns FSM instance in case of success; NULL in case of error */
+struct osmo_fsm_inst *xua_as_fsm_start(struct osmo_ss7_as *as, int log_level)
+{
+	struct osmo_fsm_inst *fi;
+	struct xua_as_fsm_priv *xafp;
+
+	fi = osmo_fsm_inst_alloc(&xua_as_fsm, as, NULL, log_level, as->cfg.name);
+
+	xafp = talloc_zero(fi, struct xua_as_fsm_priv);
+	if (!xafp) {
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_ERROR, NULL);
+		return NULL;
+	}
+	xafp->as = as;
+
+	fi->priv = xafp;
+
+	return fi;
+}
diff --git a/src/xua_as_fsm.h b/src/xua_as_fsm.h
new file mode 100644
index 0000000..3b8e5b7
--- /dev/null
+++ b/src/xua_as_fsm.h
@@ -0,0 +1,13 @@
+#pragma once
+
+struct osmo_ss7_as;
+
+enum xua_as_event {
+	XUA_ASPAS_ASP_INACTIVE_IND,
+	XUA_ASPAS_ASP_DOWN_IND,
+	XUA_ASPAS_ASP_ACTIVE_IND,
+};
+
+extern struct osmo_fsm xua_as_fsm;
+
+struct osmo_fsm_inst *xua_as_fsm_start(struct osmo_ss7_as *as, int log_level);
diff --git a/src/xua_asp_fsm.c b/src/xua_asp_fsm.c
new file mode 100644
index 0000000..80faeb2
--- /dev/null
+++ b/src/xua_asp_fsm.c
@@ -0,0 +1,610 @@
+/* SCCP M3UA / SUA ASP osmo_fsm according to RFC3868 4.3.1 */
+/* (C) Copyright 2017 by Harald Welte <laforge at gnumonks.org>
+ * 
+ * All Rights reserved.
+ *
+ * Based on my earlier Erlang implementation xua_asp_fsm.erl in
+ * osmo-ss7.git
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/core/prim.h>
+#include <osmocom/core/logging.h>
+
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/sigtran_sap.h>
+#include <osmocom/sigtran/xua_msg.h>
+#include <osmocom/sigtran/protocol/sua.h>
+
+#include "xua_asp_fsm.h"
+#include "xua_as_fsm.h"
+
+#define S(x)	(1 << (x))
+
+/* The general idea is:
+ * * translate incoming SUA/M3UA msg_class/msg_type to xua_asp_event
+ * * propagate state transitions to XUA_AS_FSM via _onenter functiosn
+ * * notify the Layer Management of any relevant changes
+ * * 
+ */
+
+/* According to RFC3868 Section 8 */
+#define XUA_T_A_SEC	2
+#define XUA_T_R_SEC	2
+#define XUA_T_ACK_SEC	2
+#define XUA_T_BEAT_SEC	30
+#define SUA_T_IAS_SEC	(7*60)		/* SUA only */
+#define SUA_T_IAR_SEC	(15*60)		/* SUA only */
+
+static const struct value_string xua_asp_role_names[] = {
+	{ XUA_ASPFSM_ROLE_ASP,	"ASP" },
+	{ XUA_ASPFSM_ROLE_SG,	"SG" },
+	{ XUA_ASPFSM_ROLE_IPSP,	"IPSP" },
+	{ 0, NULL }
+};
+
+static const struct value_string xua_asp_event_names[] = {
+	{ XUA_ASP_E_M_ASP_UP_REQ,	"M-ASP_UP.req" },
+	{ XUA_ASP_E_M_ASP_ACTIVE_REQ,	"M-ASP_ACTIVE.req" },
+	{ XUA_ASP_E_M_ASP_DOWN_REQ,	"M-ASP_DOWN.req" },
+	{ XUA_ASP_E_M_ASP_INACTIVE_REQ,	"M-ASP_INACTIVE.req" },
+
+	{ XUA_ASP_E_SCTP_COMM_DOWN_IND,	"SCTP-COMM_DOWN.ind" },
+	{ XUA_ASP_E_SCTP_RESTART_IND,	"SCTP-RESTART.ind" },
+
+	{ XUA_ASP_E_ASPSM_ASPUP,	"ASPSM-ASP_UP" },
+	{ XUA_ASP_E_ASPSM_ASPUP_ACK,	"ASPSM-ASP_UP_ACK" },
+	{ XUA_ASP_E_ASPTM_ASPAC,	"ASPTM-ASP_AC" },
+	{ XUA_ASP_E_ASPTM_ASPAC_ACK,	"ASPTM-ASP_AC_ACK" },
+	{ XUA_ASP_E_ASPSM_ASPDN,	"ASPSM-ASP_DN" },
+	{ XUA_ASP_E_ASPSM_ASPDN_ACK,	"ASPSM-ASP_DN_ACK" },
+	{ XUA_ASP_E_ASPTM_ASPIA,	"ASPTM-ASP_IA" },
+	{ XUA_ASP_E_ASPTM_ASPIA_ACK,	"ASPTM_ASP_IA_ACK" },
+	{ 0, NULL }
+};
+
+struct xua_layer_manager {
+	osmo_prim_cb prim_cb;
+};
+
+/* private data structure for each FSM instance */
+struct xua_asp_fsm_priv {
+	/* pointer back to ASP to which we belong */
+	struct osmo_ss7_asp *asp;
+	/* Role (ASP/SG/IPSP) */
+	enum xua_asp_role role;
+	/* Layer Manager to which we talk */
+	struct xua_layer_manager *lm;
+
+	/* routing context[s]: list of 32bit integers */
+	/* ACTIVE: traffic mode type, tid label, drn label ? */
+
+	struct {
+		struct osmo_timer_list timer;
+		int out_event;
+	} t_ack;
+};
+
+static struct msgb *xlm_msgb_alloc(void)
+{
+	return msgb_alloc_headroom(2048+128, 128, "xua_asp-xlm msgb");
+}
+
+/* Send a XUA LM Primitive to the XUA Layer Manager (LM) */
+static int send_xlm_prim(struct osmo_fsm_inst *fi,
+			 enum osmo_xlm_prim_type prim_type,
+			 enum osmo_prim_operation op,
+			 const uint8_t *data, unsigned int data_len)
+{
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+	struct msgb *xlmsg;
+	struct osmo_xlm_prim *prim;
+	struct xua_layer_manager *lm = xafp->lm;
+
+	if (!lm || !lm->prim_cb)
+		return 0;
+
+	xlmsg = xlm_msgb_alloc();
+	if (!xlmsg)
+		return -ENOMEM;
+	prim = (struct osmo_xlm_prim *) msgb_put(xlmsg, sizeof(*prim));
+	osmo_prim_init(&prim->oph, XUA_SAP_LM, prim_type, op, xlmsg);
+
+	lm->prim_cb(&prim->oph, xafp->asp);
+
+	return 0;
+}
+
+/* wrapper around send_xlm_prim for primitives without data */
+static int send_xlm_prim_simple(struct osmo_fsm_inst *fi,
+				enum osmo_xlm_prim_type prim,
+				enum osmo_prim_operation op)
+{
+	return send_xlm_prim(fi, prim, op, NULL, 0);
+}
+
+/* ask the xUA implementation to transmit a specific message */
+static int peer_send(struct osmo_fsm_inst *fi, int out_event)
+{
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+	struct osmo_ss7_asp *asp = xafp->asp;
+	struct xua_msg *xua = xua_msg_alloc();
+	struct msgb *msg;
+
+	switch (out_event) {
+	case XUA_ASP_E_ASPSM_ASPUP:
+		/* RFC 3868 Ch. 3.5.1 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_UP);
+		/* Optional: ASP ID */
+		if (asp->asp_id_present)
+			xua_msg_add_u32(xua, SUA_IEI_ASP_ID, asp->asp_id);
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPSM_ASPUP_ACK:
+		/* RFC3868 Ch. 3.5.2 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_UP_ACK);
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPSM_ASPDN:
+		/* RFC3868 Ch. 3.5.3 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_DOWN);
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPSM_ASPDN_ACK:
+		/* RFC3868 Ch. 3.5.4 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_DOWN_ACK);
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPSM_BEAT:
+		/* RFC3868 Ch. 3.5.5 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_BEAT);
+		/* Optional: Heartbeat Data */
+		break;
+	case XUA_ASP_E_ASPSM_BEAT_ACK:
+		/* RFC3868 Ch. 3.5.6 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_BEAT_ACK);
+		/* Optional: Heartbeat Data */
+		break;
+	case XUA_ASP_E_ASPTM_ASPAC:
+		/* RFC3868 Ch. 3.6.1 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPTM, SUA_ASPTM_ACTIVE);
+		/* Optional: Traffic Mode Type */
+		/* Optional: Routing Context */
+		/* Optional: TID Label */
+		/* Optional: DRN Label */
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPTM_ASPAC_ACK:
+		/* RFC3868 Ch. 3.6.2 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPTM, SUA_ASPTM_ACTIVE_ACK);
+		/* Optional: Traffic Mode Type */
+		/* Mandatory: Routing Context */
+		//FIXME xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPTM_ASPIA:
+		/* RFC3868 Ch. 3.6.3 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPTM, SUA_ASPTM_INACTIVE);
+		/* Optional: Routing Context */
+		/* Optional: Info String */
+		break;
+	case XUA_ASP_E_ASPTM_ASPIA_ACK:
+		/* RFC3868 Ch. 3.6.4 */
+		xua->hdr = XUA_HDR(SUA_MSGC_ASPTM, SUA_ASPTM_INACTIVE_ACK);
+		/* Optional: Routing Context */
+		/* Optional: Info String */
+		break;
+	}
+
+	msg = xua_to_msg(SUA_VERSION, xua);
+	xua_msg_free(xua);
+	if (!msg)
+		return -1;
+
+	return osmo_ss7_asp_send(asp, msg);
+}
+
+static void xua_t_ack_cb(void *data)
+{
+	struct osmo_fsm_inst *fi = data;
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+
+	LOGPFSML(fi, LOGL_INFO, "T(ack) callback: re-transmitting event %s\n",
+		osmo_fsm_event_name(fi->fsm, xafp->t_ack.out_event));
+
+	/* Re-transmit message */
+	peer_send(fi, xafp->t_ack.out_event);
+
+	/* Re-start the timer */
+	osmo_timer_schedule(&xafp->t_ack.timer, XUA_T_ACK_SEC, 0);
+}
+
+static int peer_send_and_start_t_ack(struct osmo_fsm_inst *fi,
+				     int out_event)
+{
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+	int rc;
+
+	rc = peer_send(fi, out_event);
+	if (rc < 0)
+		return rc;
+
+	xafp->t_ack.out_event = out_event;
+	xafp->t_ack.timer.cb = xua_t_ack_cb,
+	xafp->t_ack.timer.data = fi;
+
+	osmo_timer_schedule(&xafp->t_ack.timer, XUA_T_ACK_SEC, 0);
+
+	return rc;
+}
+
+static const uint32_t evt_ack_map[_NUM_XUA_ASP_E] = {
+	[XUA_ASP_E_ASPSM_ASPUP] = XUA_ASP_E_ASPSM_ASPUP_ACK,
+	[XUA_ASP_E_ASPTM_ASPAC] = XUA_ASP_E_ASPTM_ASPAC_ACK,
+	[XUA_ASP_E_ASPSM_ASPDN] = XUA_ASP_E_ASPSM_ASPDN_ACK,
+	[XUA_ASP_E_ASPTM_ASPIA] = XUA_ASP_E_ASPTM_ASPIA_ACK,
+	[XUA_ASP_E_ASPSM_BEAT] = XUA_ASP_E_ASPSM_BEAT_ACK,
+};
+
+
+/* check if expected message was received + stop t_ack */
+static void check_stop_t_ack(struct osmo_fsm_inst *fi, uint32_t event)
+{
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+	int exp_ack;
+
+	if (event >= ARRAY_SIZE(evt_ack_map))
+		return;
+
+	exp_ack = evt_ack_map[xafp->t_ack.out_event];
+	if (exp_ack && event == exp_ack) {
+		LOGPFSML(fi, LOGL_DEBUG, "T(ack) stopped\n");
+		osmo_timer_del(&xafp->t_ack.timer);
+	}
+}
+
+#define ENSURE_ASP_OR_IPSP(fi, event) 					\
+	do {								\
+		struct xua_asp_fsm_priv *_xafp = fi->priv;		\
+		if (_xafp->role != XUA_ASPFSM_ROLE_ASP &&		\
+		    _xafp->role != XUA_ASPFSM_ROLE_IPSP) {		\
+			LOGPFSML(fi, LOGL_ERROR, "event %s not permitted " \
+				 "in role %s\n",			\
+				 osmo_fsm_event_name(fi->fsm, event),	\
+				 get_value_string(xua_asp_role_names, _xafp->role));\
+			return;						\
+		}							\
+	} while(0)
+
+#define ENSURE_SG_OR_IPSP(fi, event) 					\
+	do {								\
+		struct xua_asp_fsm_priv *_xafp = fi->priv;		\
+		if (_xafp->role != XUA_ASPFSM_ROLE_SG &&		\
+		    _xafp->role != XUA_ASPFSM_ROLE_IPSP) {		\
+			LOGPFSML(fi, LOGL_ERROR, "event %s not permitted " \
+				 "in role %s\n",			\
+				 osmo_fsm_event_name(fi->fsm, event),	\
+				 get_value_string(xua_asp_role_names, _xafp->role));\
+			return;						\
+		}							\
+	} while(0)
+
+static void xua_asp_fsm_down(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+	struct osmo_ss7_asp *asp = xafp->asp;
+	struct xua_msg_part *asp_id_ie;
+
+	check_stop_t_ack(fi, event);
+
+	switch (event) {
+	case XUA_ASP_E_M_ASP_UP_REQ:
+		/* only if role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		/* Send M3UA_MSGT_ASPSM_ASPUP and start t_ack */
+		peer_send_and_start_t_ack(fi, XUA_ASP_E_ASPSM_ASPUP);
+		break;
+	case XUA_ASP_E_ASPSM_ASPUP_ACK:
+		/* only if role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_INACTIVE, 0, 0);
+		/* inform layer manager */
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_UP,
+				     PRIM_OP_CONFIRM);
+		/* FIXME: This hack should be in layer manager? */
+		osmo_fsm_inst_dispatch(fi, XUA_ASP_E_M_ASP_ACTIVE_REQ, NULL);
+		break;
+	case XUA_ASP_E_ASPSM_ASPUP:
+		/* only if role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		asp_id_ie = xua_msg_find_tag(data, SUA_IEI_ASP_ID);
+		/* Optional ASP Identifier: Store for NTFY */
+		if (asp_id_ie) {
+			asp->asp_id = xua_msg_part_get_u32(asp_id_ie);
+			asp->asp_id_present = true;
+		}
+		/* send ACK */
+		peer_send(fi, XUA_ASP_E_ASPSM_ASPUP_ACK);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_INACTIVE, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_UP,
+				     PRIM_OP_INDICATION);
+		break;
+	case XUA_ASP_E_ASPSM_ASPDN:
+		/* only if role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* The SGP MUST send an ASP Down Ack message in response
+		 * to a received ASP Down message from the ASP even if
+		 * the ASP is already marked as ASP-DOWN at the SGP. */
+		peer_send(fi, XUA_ASP_E_ASPSM_ASPDN_ACK);
+		break;
+	}
+}
+
+/* Helper function to dispatch an ASP->AS event to all AS of which this
+ * ASP is a memmber.  Ignores routing contexts for now. */
+static void dispatch_to_all_as(struct osmo_fsm_inst *fi, uint32_t event)
+{
+	struct xua_asp_fsm_priv *xafp = fi->priv;
+	struct osmo_ss7_asp *asp = xafp->asp;
+	struct osmo_ss7_instance *inst = asp->inst;
+	struct osmo_ss7_as *as;
+
+	if (xafp->role != XUA_ASPFSM_ROLE_SG)
+		return;
+
+	llist_for_each_entry(as, &inst->as_list, list) {
+		if (!osmo_ss7_as_has_asp(as, asp))
+			continue;
+		osmo_fsm_inst_dispatch(as->fi, event, asp);
+	}
+}
+
+static void xua_asp_fsm_down_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	dispatch_to_all_as(fi, XUA_ASPAS_ASP_DOWN_IND);
+}
+
+static void xua_asp_fsm_inactive(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	check_stop_t_ack(fi, event);
+	switch (event) {
+	case XUA_ASP_E_M_ASP_ACTIVE_REQ:
+		/* send M3UA_MSGT_ASPTM_ASPAC and start t_ack */
+		peer_send_and_start_t_ack(fi, XUA_ASP_E_ASPTM_ASPAC);
+		break;
+	case XUA_ASP_E_M_ASP_DOWN_REQ:
+		/* send M3UA_MSGT_ASPSM_ASPDN and start t_ack */
+		peer_send_and_start_t_ack(fi, XUA_ASP_E_ASPSM_ASPDN);
+		break;
+	case XUA_ASP_E_ASPTM_ASPAC_ACK:
+		/* only in role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_ACTIVE, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_ACTIVE,
+				     PRIM_OP_CONFIRM);
+		break;
+	case XUA_ASP_E_ASPSM_ASPDN_ACK:
+		/* only in role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_DOWN, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_DOWN,
+				     PRIM_OP_CONFIRM);
+		break;
+	case XUA_ASP_E_ASPTM_ASPAC:
+		/* only in role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* send ACK */
+		peer_send(fi, XUA_ASP_E_ASPTM_ASPAC_ACK);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_ACTIVE, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_ACTIVE,
+				     PRIM_OP_INDICATION);
+		break;
+	case XUA_ASP_E_ASPSM_ASPDN:
+		/* only in role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* send ACK */
+		peer_send(fi, XUA_ASP_E_ASPSM_ASPDN_ACK);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_DOWN, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_DOWN,
+				     PRIM_OP_INDICATION);
+		break;
+	case XUA_ASP_E_ASPSM_ASPUP:
+		/* only if role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* If an ASP Up message is received and internally the
+		 * remote ASP is already in the ASP-INACTIVE state, an
+		 * ASP Up Ack message is returned and no further action
+		 * is taken. */
+		peer_send(fi, XUA_ASP_E_ASPSM_ASPUP_ACK);
+		break;
+	}
+}
+
+static void xua_asp_fsm_inactive_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	dispatch_to_all_as(fi, XUA_ASPAS_ASP_INACTIVE_IND);
+}
+
+static void xua_asp_fsm_active(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	check_stop_t_ack(fi, event);
+	switch (event) {
+	case XUA_ASP_E_ASPSM_ASPDN_ACK:
+		/* only in role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_DOWN, 0, 0);
+		/* inform layer manager */
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_DOWN,
+				     PRIM_OP_CONFIRM);
+		break;
+	case XUA_ASP_E_ASPTM_ASPIA_ACK:
+		/* only in role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_INACTIVE, 0, 0);
+		/* inform layer manager */
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_INACTIVE,
+				     PRIM_OP_CONFIRM);
+		break;
+	case XUA_ASP_E_M_ASP_DOWN_REQ:
+		/* only in role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		/* send M3UA_MSGT_ASPSM_ASPDN and star t_ack */
+		peer_send_and_start_t_ack(fi, XUA_ASP_E_ASPSM_ASPDN);
+		break;
+	case XUA_ASP_E_M_ASP_INACTIVE_REQ:
+		/* only in role ASP */
+		ENSURE_ASP_OR_IPSP(fi, event);
+		/* send M3UA_MSGT_ASPTM_ASPIA and star t_ack */
+		peer_send_and_start_t_ack(fi, XUA_ASP_E_ASPTM_ASPIA);
+		break;
+	case XUA_ASP_E_ASPTM_ASPIA:
+		/* only in role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* send ACK */
+		peer_send(fi, XUA_ASP_E_ASPTM_ASPIA_ACK);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_INACTIVE, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_INACTIVE,
+				     PRIM_OP_INDICATION);
+		break;
+	case XUA_ASP_E_ASPSM_ASPDN:
+		/* only in role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* send ACK */
+		peer_send(fi, XUA_ASP_E_ASPSM_ASPDN_ACK);
+		/* transition state and inform layer manager */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_DOWN, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_DOWN,
+				     PRIM_OP_INDICATION);
+		break;
+	case XUA_ASP_E_ASPSM_ASPUP:
+		/* only if role SG */
+		ENSURE_SG_OR_IPSP(fi, event);
+		/* an ASP Up Ack message is returned, as well as
+		 * an Error message ("Unexpected Message), and the
+		 * remote ASP state is changed to ASP-INACTIVE in all
+		 * relevant Application Servers */
+		/* FIXME: Send ERROR message */
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_INACTIVE, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_INACTIVE,
+				     PRIM_OP_INDICATION);
+		break;
+	}
+}
+
+static void xua_asp_fsm_active_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	dispatch_to_all_as(fi, XUA_ASPAS_ASP_ACTIVE_IND);
+}
+
+static void xua_asp_allstate(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	switch (event) {
+	case XUA_ASP_E_SCTP_COMM_DOWN_IND:
+	case XUA_ASP_E_SCTP_RESTART_IND:
+		osmo_fsm_inst_state_chg(fi, XUA_ASP_S_DOWN, 0, 0);
+		send_xlm_prim_simple(fi, OSMO_XLM_PRIM_M_ASP_DOWN,
+				     PRIM_OP_INDICATION);
+		break;
+	default:
+		break;
+	}
+}
+
+static int xua_asp_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	/* We don't use the fsm timer, so any calls to this are an error */
+	OSMO_ASSERT(0);
+	return 0;
+}
+
+static const struct osmo_fsm_state xua_asp_states[] = {
+	[XUA_ASP_S_DOWN] = {
+		.in_event_mask = S(XUA_ASP_E_M_ASP_UP_REQ) |
+				 S(XUA_ASP_E_ASPSM_ASPUP) |
+				 S(XUA_ASP_E_ASPSM_ASPUP_ACK) |
+				 S(XUA_ASP_E_ASPSM_ASPDN),
+		.out_state_mask = S(XUA_ASP_S_INACTIVE),
+		.name = "ASP_DOWN",
+		.action = xua_asp_fsm_down,
+		.onenter = xua_asp_fsm_down_onenter,
+	},
+	[XUA_ASP_S_INACTIVE] = {
+		.in_event_mask = S(XUA_ASP_E_M_ASP_ACTIVE_REQ) |
+				 S(XUA_ASP_E_M_ASP_DOWN_REQ) |
+				 S(XUA_ASP_E_ASPTM_ASPAC) |
+				 S(XUA_ASP_E_ASPTM_ASPAC_ACK) |
+				 S(XUA_ASP_E_ASPSM_ASPDN) |
+				 S(XUA_ASP_E_ASPSM_ASPDN_ACK) |
+				 S(XUA_ASP_E_ASPSM_ASPUP),
+		.out_state_mask = S(XUA_ASP_S_DOWN) |
+				  S(XUA_ASP_S_ACTIVE),
+		.name = "ASP_INACTIVE",
+		.action = xua_asp_fsm_inactive,
+		.onenter = xua_asp_fsm_inactive_onenter,
+	},
+	[XUA_ASP_S_ACTIVE] = {
+		.in_event_mask = S(XUA_ASP_E_ASPSM_ASPDN) |
+				 S(XUA_ASP_E_ASPSM_ASPDN_ACK) |
+				 S(XUA_ASP_E_ASPSM_ASPUP) |
+				 S(XUA_ASP_E_ASPTM_ASPIA) |
+				 S(XUA_ASP_E_ASPTM_ASPIA_ACK) |
+				 S(XUA_ASP_E_M_ASP_DOWN_REQ) |
+				 S(XUA_ASP_E_M_ASP_INACTIVE_REQ),
+		.out_state_mask = S(XUA_ASP_S_INACTIVE) |
+				  S(XUA_ASP_S_DOWN),
+		.name = "ASP_ACTIVE",
+		.action = xua_asp_fsm_active,
+		.onenter = xua_asp_fsm_active_onenter,
+	},
+};
+
+
+struct osmo_fsm xua_asp_fsm = {
+	.name = "XUA_ASP",
+	.states = xua_asp_states,
+	.num_states = ARRAY_SIZE(xua_asp_states),
+	.timer_cb = xua_asp_fsm_timer_cb,
+	.log_subsys = DLSS7,
+	.event_names = xua_asp_event_names,
+	.allstate_event_mask = S(XUA_ASP_E_SCTP_COMM_DOWN_IND) |
+			       S(XUA_ASP_E_SCTP_RESTART_IND),
+	.allstate_action = xua_asp_allstate,
+};
+
+
+/*! \brief Start a new ASP finite stae machine for given ASP
+ *  \param[in] asp Application Server Process for which to start FSM
+ *  \param[in] role Role (ASP, SG, IPSP) of this FSM
+ *  \param[in] log_level Logging Level for ASP FSM logging
+ *  \returns FSM instance on success; NULL on error */
+struct osmo_fsm_inst *xua_asp_fsm_start(struct osmo_ss7_asp *asp,
+					enum xua_asp_role role, int log_level)
+{
+	struct osmo_fsm_inst *fi;
+	struct xua_asp_fsm_priv *xafp;
+
+	/* allocate as child of AS? */
+	fi = osmo_fsm_inst_alloc(&xua_asp_fsm, asp, NULL, log_level, asp->cfg.name);
+
+	xafp = talloc_zero(fi, struct xua_asp_fsm_priv);
+	if (!xafp) {
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_ERROR, NULL);
+		return NULL;
+	}
+	xafp->role = role;
+	xafp->asp = asp;
+
+	fi->priv = xafp;
+
+	return fi;
+}
diff --git a/src/xua_asp_fsm.h b/src/xua_asp_fsm.h
new file mode 100644
index 0000000..ea62484
--- /dev/null
+++ b/src/xua_asp_fsm.h
@@ -0,0 +1,42 @@
+#pragma once
+
+enum xua_asp_state {
+	XUA_ASP_S_DOWN,
+	XUA_ASP_S_INACTIVE,
+	XUA_ASP_S_ACTIVE,
+};
+
+enum xua_asp_event {
+	XUA_ASP_E_M_ASP_UP_REQ,
+	XUA_ASP_E_M_ASP_ACTIVE_REQ,
+	XUA_ASP_E_M_ASP_DOWN_REQ,
+	XUA_ASP_E_M_ASP_INACTIVE_REQ,
+
+	XUA_ASP_E_SCTP_COMM_DOWN_IND,
+	XUA_ASP_E_SCTP_RESTART_IND,
+
+	XUA_ASP_E_ASPSM_ASPUP,
+	XUA_ASP_E_ASPSM_ASPUP_ACK,
+	XUA_ASP_E_ASPTM_ASPAC,
+	XUA_ASP_E_ASPTM_ASPAC_ACK,
+	XUA_ASP_E_ASPSM_ASPDN,
+	XUA_ASP_E_ASPSM_ASPDN_ACK,
+	XUA_ASP_E_ASPTM_ASPIA,
+	XUA_ASP_E_ASPTM_ASPIA_ACK,
+
+	XUA_ASP_E_ASPSM_BEAT,
+	XUA_ASP_E_ASPSM_BEAT_ACK,
+
+	_NUM_XUA_ASP_E
+};
+
+enum xua_asp_role {
+	XUA_ASPFSM_ROLE_ASP,
+	XUA_ASPFSM_ROLE_SG,
+	XUA_ASPFSM_ROLE_IPSP,
+};
+
+extern struct osmo_fsm xua_asp_fsm;
+
+struct osmo_fsm_inst *xua_asp_fsm_start(struct osmo_ss7_asp *asp,
+					enum xua_asp_role role, int log_level);
diff --git a/src/xua_internal.h b/src/xua_internal.h
new file mode 100644
index 0000000..66b5a26
--- /dev/null
+++ b/src/xua_internal.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/xua_msg.h>
+
+struct osmo_sccp_addr;
+struct m3ua_data_hdr;
+
+int sua_addr_parse_part(struct osmo_sccp_addr *out,
+			const struct xua_msg_part *param);
+int sua_addr_parse(struct osmo_sccp_addr *out, struct xua_msg *xua, uint16_t iei);
+
+int sua_parse_gt(struct osmo_sccp_gt *gt, const uint8_t *data, unsigned int datalen);
+
+struct xua_msg *osmo_sccp_to_xua(struct msgb *msg);
+struct msgb *osmo_sua_to_sccp(struct xua_msg *xua);
+
+int sua_tx_xua_as(struct osmo_ss7_as *as, struct xua_msg *xua);
+
+struct osmo_mtp_prim *m3ua_to_xfer_ind(struct xua_msg *xua);
+int m3ua_hmdc_rx_from_l2(struct osmo_ss7_instance *inst, struct xua_msg *xua);
+int m3ua_tx_xua_as(struct osmo_ss7_as *as, struct xua_msg *xua);
+int m3ua_rx_msg(struct osmo_ss7_asp *asp, struct msgb *msg);
+
+struct msgb *m3ua_msgb_alloc(const char *name);
+struct m3ua_data_hdr *data_hdr_from_m3ua(struct xua_msg *xua);
+void m3ua_dh_to_xfer_param(struct osmo_mtp_transfer_param *param,
+			   const struct m3ua_data_hdr *mdh);
+void mtp_xfer_param_to_m3ua_dh(struct m3ua_data_hdr *mdh,
+				const struct osmo_mtp_transfer_param *param);
+
+
+extern const struct xua_msg_class m3ua_msg_class_mgmt;
+extern const struct xua_msg_class m3ua_msg_class_snm;
+extern const struct xua_msg_class m3ua_msg_class_rkm;
+extern const struct xua_msg_class m3ua_msg_class_aspsm;
+extern const struct xua_msg_class m3ua_msg_class_asptm;
+
+extern const struct value_string m3ua_err_names[];
+extern const struct value_string m3ua_ntfy_type_names[];
+extern const struct value_string m3ua_ntfy_stchg_names[];
+extern const struct value_string m3ua_ntfy_other_names[];
+
+#define NOTIFY_PAR_P_ASP_ID	(1 << 0)
+#define NOTIFY_PAR_P_ROUTE_CTX	(1 << 1)
+
+struct m3ua_notify_params {
+	uint32_t presence;
+	uint16_t status_type;
+	uint16_t status_info;
+	uint32_t asp_id;
+	uint32_t route_ctx;
+	char *info_string;
+};
+
+struct xua_msg *m3ua_encode_notify(const struct m3ua_notify_params *npar);
+int m3ua_decode_notify(struct m3ua_notify_params *npar, void *ctx,
+			const struct xua_msg *xua);
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 6e9b807..9c251fe 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = sccp mtp m2ua sigtran
+SUBDIRS = sccp mtp m2ua sigtran ss7
 
 # The `:;' works around a Bash 3.2 bug when the output is not writeable.
 $(srcdir)/package.m4: $(top_srcdir)/configure.ac
diff --git a/tests/ss7/Makefile.am b/tests/ss7/Makefile.am
new file mode 100644
index 0000000..bcfd354
--- /dev/null
+++ b/tests/ss7/Makefile.am
@@ -0,0 +1,12 @@
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include -Wall
+AM_CFLAGS=-Wall $(LIBOSMOCORE_CFLAGS)
+
+AM_LDFLAGS = -static
+LDADD = $(top_builddir)/src/libosmo-sigtran.la \
+	$(LIBOSMOCORE_LIBS) $(LIBOSMONETIF_LIBS) -lsctp
+
+EXTRA_DIST = ss7_test.ok ss7_test.err
+
+noinst_PROGRAMS = ss7_test
+
+ss7_test_SOURCES = ss7_test.c
diff --git a/tests/ss7/ss7_test.c b/tests/ss7/ss7_test.c
new file mode 100644
index 0000000..24eabc9
--- /dev/null
+++ b/tests/ss7/ss7_test.c
@@ -0,0 +1,321 @@
+#include "../src/xua_internal.h"
+#include "../src/xua_asp_fsm.h"
+
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/protocol/m3ua.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <errno.h>
+
+static struct osmo_ss7_instance *s7i;
+
+static void test_pc_transcode(uint32_t pc)
+{
+	const char *pc_str = osmo_ss7_pointcode_print(s7i, pc);
+	uint32_t pc_reenc = osmo_ss7_pointcode_parse(s7i, pc_str);
+
+	printf("%s(%u) -> %s -> %u\n", __func__, pc, pc_str, pc_reenc);
+	OSMO_ASSERT(pc == pc_reenc);
+}
+
+static void test_pc_defaults(void)
+{
+	/* ensure the default point code format settings apply */
+	OSMO_ASSERT(s7i->cfg.pc_fmt.component_len[0] == 3);
+	OSMO_ASSERT(s7i->cfg.pc_fmt.component_len[1] == 8);
+	OSMO_ASSERT(s7i->cfg.pc_fmt.component_len[2] == 3);
+	OSMO_ASSERT(s7i->cfg.pc_fmt.delimiter = '.');
+}
+
+static void parse_print_mask(const char *in)
+{
+	uint32_t mask = osmo_ss7_pointcode_parse_mask_or_len(s7i, in);
+	const char *pc_str = osmo_ss7_pointcode_print(s7i, mask);
+	printf("mask %s => %u (0x%x) %s\n", in, mask, mask, pc_str);
+}
+
+static void test_pc_parser_itu(void)
+{
+	/* ITU Style */
+	printf("Testing ITU-style point code format\n");
+	osmo_ss7_instance_set_pc_fmt(s7i, 3, 8, 3);
+	test_pc_transcode(0);
+	test_pc_transcode(1);
+	test_pc_transcode(1 << 3);
+	test_pc_transcode(1 << (3+8));
+	test_pc_transcode(7 << (3+8));
+	test_pc_transcode(100);
+	test_pc_transcode(2342);
+	test_pc_transcode((1 << 14)-1);
+
+	parse_print_mask("/1");
+	parse_print_mask("7.0.0");
+	parse_print_mask("/14");
+}
+
+static void test_pc_parser_ansi(void)
+{
+	/* ANSI Style */
+	printf("Testing ANSI-style point code format\n");
+	osmo_ss7_instance_set_pc_fmt(s7i, 8, 8, 8);
+	s7i->cfg.pc_fmt.delimiter = '-';
+	test_pc_transcode(0);
+	test_pc_transcode(1);
+	test_pc_transcode(1 << 8);
+	test_pc_transcode(1 << 16);
+	test_pc_transcode(1 << (3+8));
+	test_pc_transcode((1 << 24)-1);
+	test_pc_transcode(100);
+	test_pc_transcode(2342);
+
+	parse_print_mask("/1");
+	parse_print_mask("/16");
+	parse_print_mask("/24");
+
+	/* re-set to default (ITU) */
+	osmo_ss7_instance_set_pc_fmt(s7i, 3, 8, 3);
+	s7i->cfg.pc_fmt.delimiter = '.';
+}
+
+static int test_user_prim_cb(struct osmo_prim_hdr *oph, void *priv)
+{
+	OSMO_ASSERT(priv == (void *) 0x1234);
+
+	return 23;
+}
+
+static void test_user(void)
+{
+	struct osmo_ss7_user user, user2;
+	struct osmo_mtp_prim omp = {
+		.oph = {
+			.sap = MTP_SAP_USER,
+			.primitive = OSMO_MTP_PRIM_TRANSFER,
+			.operation = PRIM_OP_INDICATION,
+		},
+		.u.transfer = {
+			.sio = 1,
+		},
+	};
+
+	printf("Testing SS7 user\n");
+
+	user.name = "testuser";
+	user.priv = (void *) 0x1234;
+	user.prim_cb = test_user_prim_cb;
+
+	/* registration */
+	OSMO_ASSERT(osmo_ss7_user_register(s7i, 1, &user) == 0);
+	OSMO_ASSERT(osmo_ss7_user_register(s7i, 1, NULL) == -EBUSY);
+	OSMO_ASSERT(osmo_ss7_user_register(s7i, 255, NULL) == -EINVAL);
+
+	/* primitive delivery */
+	OSMO_ASSERT(osmo_ss7_mtp_to_user(s7i, &omp) == 23);
+
+	/* cleanup */
+	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 255, NULL) == -EINVAL);
+	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 10, NULL) == -ENODEV);
+	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 1, &user2) == -EINVAL);
+	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 1, &user) == 0);
+
+	/* primitive delivery should fail now */
+	OSMO_ASSERT(osmo_ss7_mtp_to_user(s7i, &omp) == -ENODEV);
+
+	/* wrong primitive delivery should also fail */
+	omp.oph.primitive = OSMO_MTP_PRIM_PAUSE;
+	OSMO_ASSERT(osmo_ss7_mtp_to_user(s7i, &omp) == -EINVAL);
+}
+
+static void test_route(void)
+{
+	struct osmo_ss7_route_table *rtbl;
+	struct osmo_ss7_linkset *lset_a, *lset_b;
+	struct osmo_ss7_route *rt, *rt12, *rtdef;
+
+	printf("Testing SS7 routing\n");
+
+	/* creation / destruction */
+	OSMO_ASSERT(osmo_ss7_route_table_find(s7i, "foobar") == NULL);
+	rtbl = osmo_ss7_route_table_find_or_create(s7i, "foobar");
+	OSMO_ASSERT(rtbl);
+	OSMO_ASSERT(osmo_ss7_route_table_find_or_create(s7i, "foobar") == rtbl);
+	osmo_ss7_route_table_destroy(rtbl);
+	OSMO_ASSERT(osmo_ss7_route_table_find(s7i, "foobar") == NULL);
+
+	/* we now work with system route table */
+	rtbl = osmo_ss7_route_table_find(s7i, "system");
+	OSMO_ASSERT(rtbl && rtbl == s7i->rtable_system);
+
+	lset_a = osmo_ss7_linkset_find_or_create(s7i, "a", 100);
+	OSMO_ASSERT(lset_a);
+	lset_b = osmo_ss7_linkset_find_or_create(s7i, "b", 200);
+	OSMO_ASSERT(lset_b);
+
+	/* route with full mask */
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 12) == NULL);
+	rt = osmo_ss7_route_create(rtbl, 12, 0xffff, "a");
+	OSMO_ASSERT(rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 12) == rt);
+	osmo_ss7_route_destroy(rt);
+
+	/* route with partial mask */
+	rt = osmo_ss7_route_create(rtbl, 8, 0xfff8, "a");
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 8) == rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 9) == rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 12) == rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 15) == rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 16) == NULL);
+	/* insert more specific route for 12, must have higher priority
+	 * than existing one */
+	rt12 = osmo_ss7_route_create(rtbl, 12, 0xffff, "b");
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 12) == rt12);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 15) == rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 16) == NULL);
+	/* add a default route, which should have lowest precedence */
+	rtdef = osmo_ss7_route_create(rtbl, 0, 0, "a");
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 12) == rt12);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 15) == rt);
+	OSMO_ASSERT(osmo_ss7_route_find_dpc(rtbl, 16) == rtdef);
+
+	osmo_ss7_route_destroy(rtdef);
+	osmo_ss7_route_destroy(rt12);
+	osmo_ss7_route_destroy(rt);
+
+	osmo_ss7_linkset_destroy(lset_a);
+	osmo_ss7_linkset_destroy(lset_b);
+}
+
+static void test_linkset(void)
+{
+	struct osmo_ss7_linkset *lset_a, *lset_b;
+	struct osmo_ss7_link *l_a1, *l_a2;
+
+	printf("Testing SS7 linkset/link\n");
+
+	OSMO_ASSERT(osmo_ss7_linkset_find_by_name(s7i, "a") == NULL);
+	OSMO_ASSERT(osmo_ss7_linkset_find_by_name(s7i, "b") == NULL);
+
+	lset_a = osmo_ss7_linkset_find_or_create(s7i, "a", 100);
+	OSMO_ASSERT(lset_a);
+	OSMO_ASSERT(osmo_ss7_linkset_find_by_name(s7i, "a") == lset_a);
+
+	lset_b = osmo_ss7_linkset_find_or_create(s7i, "b", 200);
+	OSMO_ASSERT(lset_b);
+	OSMO_ASSERT(osmo_ss7_linkset_find_by_name(s7i, "b") == lset_b);
+
+	l_a1 = osmo_ss7_link_find_or_create(lset_a, 1);
+	OSMO_ASSERT(l_a1);
+	l_a2 = osmo_ss7_link_find_or_create(lset_a, 2);
+	OSMO_ASSERT(l_a2);
+
+	/* ID too high */
+	OSMO_ASSERT(osmo_ss7_link_find_or_create(lset_a, 1000) == NULL);
+	/* already exists */
+	OSMO_ASSERT(osmo_ss7_link_find_or_create(lset_a, 1) == l_a1);
+
+	osmo_ss7_link_destroy(l_a1);
+	osmo_ss7_link_destroy(l_a2);
+
+	osmo_ss7_linkset_destroy(lset_a);
+	osmo_ss7_linkset_destroy(lset_b);
+}
+
+static void test_as(void)
+{
+	struct osmo_ss7_as *as;
+	struct osmo_ss7_asp *asp;
+
+	OSMO_ASSERT(osmo_ss7_as_find_by_name(s7i, "as1") == NULL);
+	as = osmo_ss7_as_find_or_create(s7i, "as1", OSMO_SS7_ASP_PROT_M3UA);
+	OSMO_ASSERT(osmo_ss7_as_find_by_name(s7i, "as1") == as);
+	OSMO_ASSERT(osmo_ss7_as_find_by_rctx(s7i, 2342) == NULL);
+	as->cfg.routing_key.context = 2342;
+	OSMO_ASSERT(osmo_ss7_as_find_by_rctx(s7i, 2342) == as);
+	OSMO_ASSERT(osmo_ss7_as_add_asp(as, "asp1") == -ENODEV);
+
+	asp = osmo_ss7_asp_find_or_create(s7i, "asp1", 0, M3UA_PORT, OSMO_SS7_ASP_PROT_M3UA);
+	OSMO_ASSERT(asp);
+
+	OSMO_ASSERT(osmo_ss7_as_has_asp(as, asp) == false);
+	OSMO_ASSERT(osmo_ss7_as_add_asp(as, "asp1") == 0);
+
+	osmo_ss7_asp_restart(asp);
+
+	/* ask FSM to send ASP-UP.req */
+	osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_M_ASP_UP_REQ, NULL);
+	osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_ASPSM_ASPUP_ACK, NULL);
+	osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_ASPTM_ASPAC_ACK, NULL);
+
+	OSMO_ASSERT(osmo_ss7_as_del_asp(as, "asp1") == 0);
+	OSMO_ASSERT(osmo_ss7_as_del_asp(as, "asp2") == -ENODEV);
+	OSMO_ASSERT(osmo_ss7_as_del_asp(as, "asp1") == -EINVAL);
+
+	osmo_ss7_asp_destroy(asp);
+	osmo_ss7_as_destroy(as);
+	OSMO_ASSERT(osmo_ss7_as_find_by_name(s7i, "as1") == NULL);
+}
+
+/***********************************************************************
+ * Initialization
+ ***********************************************************************/
+
+static const struct log_info_cat log_info_cat[] = {
+};
+
+static const struct log_info log_info = {
+	.cat = log_info_cat,
+	.num_cat = ARRAY_SIZE(log_info_cat),
+};
+
+static void init_logging(void)
+{
+	const int log_cats[] = { DLSS7, DLSUA, DLM3UA, DLSCCP, DLINP };
+	unsigned int i;
+
+	osmo_init_logging(&log_info);
+
+	log_set_print_filename(osmo_stderr_target, 0);
+
+	for (i = 0; i < ARRAY_SIZE(log_cats); i++)
+		log_set_category_filter(osmo_stderr_target, log_cats[i], 1, LOGL_DEBUG);
+}
+
+int main(int argc, char **argv)
+{
+	init_logging();
+	osmo_fsm_log_addr(false);
+
+	/* init */
+	osmo_ss7_init();
+	s7i = osmo_ss7_instance_find_or_create(NULL, 0);
+	OSMO_ASSERT(osmo_ss7_instance_find(0) == s7i);
+	OSMO_ASSERT(osmo_ss7_instance_find(23) == NULL);
+
+	/* test osmo_ss7_pc_is_local() */
+	s7i->cfg.primary_pc = 55;
+	OSMO_ASSERT(osmo_ss7_pc_is_local(s7i, 55) == true);
+	OSMO_ASSERT(osmo_ss7_pc_is_local(s7i, 23) == false);
+
+	/* further tests */
+	test_pc_defaults();
+	test_pc_parser_itu();
+	test_pc_parser_ansi();
+	test_user();
+	test_route();
+	test_linkset();
+	test_as();
+
+	/* destroy */
+	osmo_ss7_instance_destroy(s7i);
+	OSMO_ASSERT(osmo_ss7_instance_find(0) == NULL);
+
+	exit(0);
+}
diff --git a/tests/ss7/ss7_test.err b/tests/ss7/ss7_test.err
new file mode 100644
index 0000000..8823d1c
--- /dev/null
+++ b/tests/ss7/ss7_test.err
@@ -0,0 +1,49 @@
+0: Creating SS7 Instance
+0: Creating Route Table system
+0: Point Code Format 8-8-8 is longer than 14 bits, odd?
+registering user=testuser for SI 1 with priv 0x1234
+delivering MTP-TRANSFER.ind to user testuser, priv=0x1234
+No MTP-User for SI 1
+Unsupported Primitive
+0: Creating Route Table foobar
+0: Creating Linkset a
+0: Creating Linkset b
+0: Destroying Linkset a
+0: Destroying Linkset b
+0: Creating Linkset a
+0: Creating Linkset b
+0: Creating Link a:1
+0: Creating Link a:2
+0: Destroying Link a:1
+0: Destroying Link a:2
+0: Destroying Linkset a
+0: Destroying Linkset b
+0: Creating AS as1
+XUA_AS(as1){AS_DOWN}: Allocated
+0: Adding ASP asp1 to AS as1
+0: Restarting ASP asp1
+unable to connect/bind socket: (null):0: Connection refused
+connection closed
+retrying in 5 seconds...
+0: Unable to open stream client for ASP asp1
+XUA_ASP(asp1){ASP_DOWN}: Allocated
+XUA_ASP(asp1){ASP_DOWN}: Received Event M-ASP_UP.req
+XUA_ASP(asp1){ASP_DOWN}: Received Event ASPSM-ASP_UP_ACK
+XUA_ASP(asp1){ASP_DOWN}: T(ack) stopped
+XUA_ASP(asp1){ASP_DOWN}: state_chg to ASP_INACTIVE
+XUA_ASP(asp1){ASP_INACTIVE}: Received Event M-ASP_ACTIVE.req
+XUA_ASP(asp1){ASP_INACTIVE}: Received Event ASPTM-ASP_AC_ACK
+XUA_ASP(asp1){ASP_INACTIVE}: T(ack) stopped
+XUA_ASP(asp1){ASP_INACTIVE}: state_chg to ASP_ACTIVE
+0: Removing ASP asp1 from AS as1
+0: Removing ASP asp1 from AS as1
+0: Destroying ASP asp1
+XUA_ASP(asp1){ASP_ACTIVE}: Terminating (cause = OSMO_FSM_TERM_REQUEST)
+XUA_ASP(asp1){ASP_ACTIVE}: Freeing instance
+XUA_ASP(asp1){ASP_ACTIVE}: Deallocated
+0: Destroying AS as1
+XUA_AS(as1){AS_DOWN}: Terminating (cause = OSMO_FSM_TERM_REQUEST)
+XUA_AS(as1){AS_DOWN}: Freeing instance
+XUA_AS(as1){AS_DOWN}: Deallocated
+0: Destroying SS7 Instance
+
\ No newline at end of file
diff --git a/tests/ss7/ss7_test.ok b/tests/ss7/ss7_test.ok
new file mode 100644
index 0000000..8aea63d
--- /dev/null
+++ b/tests/ss7/ss7_test.ok
@@ -0,0 +1,27 @@
+Testing ITU-style point code format
+test_pc_transcode(0) -> 0.0.0 -> 0
+test_pc_transcode(1) -> 0.0.1 -> 1
+test_pc_transcode(8) -> 0.1.0 -> 8
+test_pc_transcode(2048) -> 1.0.0 -> 2048
+test_pc_transcode(14336) -> 7.0.0 -> 14336
+test_pc_transcode(100) -> 0.12.4 -> 100
+test_pc_transcode(2342) -> 1.36.6 -> 2342
+test_pc_transcode(16383) -> 7.255.7 -> 16383
+mask /1 => 8192 (0x2000) 4.0.0
+mask 7.0.0 => 14336 (0x3800) 7.0.0
+mask /14 => 16383 (0x3fff) 7.255.7
+Testing ANSI-style point code format
+test_pc_transcode(0) -> 0-0-0 -> 0
+test_pc_transcode(1) -> 0-0-1 -> 1
+test_pc_transcode(256) -> 0-1-0 -> 256
+test_pc_transcode(65536) -> 1-0-0 -> 65536
+test_pc_transcode(2048) -> 0-8-0 -> 2048
+test_pc_transcode(16777215) -> 255-255-255 -> 16777215
+test_pc_transcode(100) -> 0-0-100 -> 100
+test_pc_transcode(2342) -> 0-9-38 -> 2342
+mask /1 => 8388608 (0x800000) 128-0-0
+mask /16 => 16776960 (0xffff00) 255-255-0
+mask /24 => 16777215 (0xffffff) 255-255-255
+Testing SS7 user
+Testing SS7 routing
+Testing SS7 linkset/link
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 8907ffa..171f488 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -18,3 +18,9 @@
 cat $abs_srcdir/sccp/sccp_test.ok > expout
 AT_CHECK([$abs_top_builddir/tests/sccp/sccp_test], [], [expout], [ignore])
 AT_CLEANUP
+
+AT_SETUP([ss7])
+AT_KEYWORDS([ss7])
+cat $abs_srcdir/ss7/ss7_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/ss7/ss7_test], [], [expout], [ignore])
+AT_CLEANUP

-- 
To view, visit https://gerrit.osmocom.org/2209
To unsubscribe, visit https://gerrit.osmocom.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I375eb80f01acc013094851d91d1d3333ebc12bc7
Gerrit-PatchSet: 1
Gerrit-Project: libosmo-sccp
Gerrit-Branch: master
Gerrit-Owner: Harald Welte <laforge at gnumonks.org>



More information about the gerrit-log mailing list