Gb: add a second NS implementation

Reimplement NS with FSM.

Change-Id: I3525beef205588dfab9d3880a34115f1a2676e48
diff --git a/src/gb/Makefile.am b/src/gb/Makefile.am
index 1cc6544..65c3552 100644
--- a/src/gb/Makefile.am
+++ b/src/gb/Makefile.am
@@ -7,7 +7,7 @@
 AM_CFLAGS = -Wall ${GCC_FVISIBILITY_HIDDEN} -fno-strict-aliasing $(TALLOC_CFLAGS)
 
 # FIXME: this should eventually go into a milenage/Makefile.am
-noinst_HEADERS = common_vty.h gb_internal.h gprs_bssgp_internal.h
+noinst_HEADERS = common_vty.h gb_internal.h gprs_bssgp_internal.h gprs_ns2_internal.h
 
 if ENABLE_GB
 lib_LTLIBRARIES = libosmogb.la
@@ -20,7 +20,10 @@
 
 libosmogb_la_SOURCES = gprs_ns.c gprs_ns_frgre.c gprs_ns_vty.c gprs_ns_sns.c \
 		  gprs_bssgp.c gprs_bssgp_util.c gprs_bssgp_vty.c \
-		  gprs_bssgp_bss.c common_vty.c
+		  gprs_bssgp_bss.c \
+		  gprs_ns2.c gprs_ns2_udp.c gprs_ns2_frgre.c gprs_ns2_vc_fsm.c gprs_ns2_sns.c \
+		  gprs_ns2_message.c gprs_ns2_vty.c \
+		  common_vty.c
 endif
 
 EXTRA_DIST = libosmogb.map
diff --git a/src/gb/gprs_ns2.c b/src/gb/gprs_ns2.c
new file mode 100644
index 0000000..bae9438
--- /dev/null
+++ b/src/gb/gprs_ns2.c
@@ -0,0 +1,1010 @@
+/*! \file gprs_ns2.c
+ * GPRS Networks Service (NS) messages on the Gb interface.
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ * as well as its successor 3GPP TS 48.016 */
+
+/* (C) 2009-2018 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2016-2017,2020 sysmocom - s.f.m.c. GmbH
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/*! \addtogroup libgb
+ *  @{
+ *
+ * GPRS Networks Service (NS) messages on the Gb interface
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ *
+ * Some introduction into NS:  NS is used typically on top of frame relay,
+ * but in the ip.access world it is encapsulated in UDP packets.  It serves
+ * as an intermediate shim betwen BSSGP and the underlying medium.  It doesn't
+ * do much, apart from providing congestion notification and status indication.
+ *
+ * Terms:
+ *
+ * 	NS		Network Service
+ * 	NSVC		NS Virtual Connection
+ * 	NSEI		NS Entity Identifier
+ * 	NSVL		NS Virtual Link
+ * 	NSVLI		NS Virtual Link Identifier
+ * 	BVC		BSSGP Virtual Connection
+ * 	BVCI		BSSGP Virtual Connection Identifier
+ * 	NSVCG		NS Virtual Connection Goup
+ * 	Blocked		NS-VC cannot be used for user traffic
+ * 	Alive		Ability of a NS-VC to provide communication
+ *
+ * There can be multiple BSSGP virtual connections over one (group of) NSVC's.  BSSGP will
+ * therefore identify the BSSGP virtual connection by a BVCI passed down to NS.
+ * NS then has to figure out which NSVC's are responsible for this BVCI.
+ * Those mappings are administratively configured.
+ *
+ * This implementation has the following limitations:
+ * - Only one NS-VC for each NSE: No load-sharing function
+ * - NSVCI 65535 and 65534 are reserved for internal use
+ * - Only UDP is supported as of now, no frame relay support
+ * - There are no BLOCK and UNBLOCK timers (yet?)
+ *
+ * \file gprs_ns2.c */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdint.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/core/stats.h>
+#include <osmocom/core/stat_item.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/gprs/gprs_msgb.h>
+#include <osmocom/gsm/prim.h>
+#include <osmocom/gsm/tlv.h>
+
+#include "gprs_ns2_internal.h"
+
+#define ns_set_state(ns_, st_) ns_set_state_with_log(ns_, st_, false, __FILE__, __LINE__)
+#define ns_set_remote_state(ns_, st_) ns_set_state_with_log(ns_, st_, true, __FILE__, __LINE__)
+#define ns_mark_blocked(ns_) ns_set_state(ns_, (ns_)->state | NSE_S_BLOCKED)
+#define ns_mark_unblocked(ns_) ns_set_state(ns_, (ns_)->state & (~NSE_S_BLOCKED));
+#define ns_mark_alive(ns_) ns_set_state(ns_, (ns_)->state | NSE_S_ALIVE)
+#define ns_mark_dead(ns_) ns_set_state(ns_, (ns_)->state & (~NSE_S_ALIVE));
+
+/* HACK: The NS_IE_IP_ADDR does not follow any known TLV rules.
+ * Since it's a hard ABI break to implement 16 bit tag with fixed length entries to workaround it,
+ * the parser will be called with ns_att_tlvdef1 and if it's failed with ns_att_tlvdef2.
+ * The TLV parser depends on 8bit tag in many places.
+ * The NS_IE_IP_ADDR is only valid for SNS_ACK SNS_ADD and SNS_DELETE.
+ */
+static const struct tlv_definition ns_att_tlvdef1 = {
+	.def = {
+		[NS_IE_CAUSE]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_VCI]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_PDU]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_BVCI]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_NSEI]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_IPv4_LIST] = { TLV_TYPE_TvLV, 0 },
+		[NS_IE_IPv6_LIST] = { TLV_TYPE_TvLV, 0 },
+		[NS_IE_MAX_NR_NSVC] = { TLV_TYPE_FIXED, 2 },
+		[NS_IE_IPv4_EP_NR] = { TLV_TYPE_FIXED, 2 },
+		[NS_IE_IPv6_EP_NR] = { TLV_TYPE_FIXED, 2 },
+		[NS_IE_RESET_FLAG] = { TLV_TYPE_TV, 0 },
+		/* NS_IE_IP_ADDR in the IPv4 version */
+		[NS_IE_IP_ADDR] = { TLV_TYPE_FIXED, 5 },
+	},
+};
+
+static const struct tlv_definition ns_att_tlvdef2 = {
+	.def = {
+		[NS_IE_CAUSE]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_VCI]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_PDU]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_BVCI]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_NSEI]	= { TLV_TYPE_TvLV, 0 },
+		[NS_IE_IPv4_LIST] = { TLV_TYPE_TvLV, 0 },
+		[NS_IE_IPv6_LIST] = { TLV_TYPE_TvLV, 0 },
+		[NS_IE_MAX_NR_NSVC] = { TLV_TYPE_FIXED, 2 },
+		[NS_IE_IPv4_EP_NR] = { TLV_TYPE_FIXED, 2 },
+		[NS_IE_IPv6_EP_NR] = { TLV_TYPE_FIXED, 2 },
+		[NS_IE_RESET_FLAG] = { TLV_TYPE_TV, 0 },
+		/* NS_IE_IP_ADDR in the IPv6 version */
+		[NS_IE_IP_ADDR] = { TLV_TYPE_FIXED, 17 },
+	},
+};
+
+
+/* Section 10.3.2, Table 13 */
+static const struct value_string ns2_cause_str[] = {
+	{ NS_CAUSE_TRANSIT_FAIL,	"Transit network failure" },
+	{ NS_CAUSE_OM_INTERVENTION, 	"O&M intervention" },
+	{ NS_CAUSE_EQUIP_FAIL,		"Equipment failure" },
+	{ NS_CAUSE_NSVC_BLOCKED,	"NS-VC blocked" },
+	{ NS_CAUSE_NSVC_UNKNOWN,	"NS-VC unknown" },
+	{ NS_CAUSE_BVCI_UNKNOWN,	"BVCI unknown" },
+	{ NS_CAUSE_SEM_INCORR_PDU,	"Semantically incorrect PDU" },
+	{ NS_CAUSE_PDU_INCOMP_PSTATE,	"PDU not compatible with protocol state" },
+	{ NS_CAUSE_PROTO_ERR_UNSPEC,	"Protocol error, unspecified" },
+	{ NS_CAUSE_INVAL_ESSENT_IE,	"Invalid essential IE" },
+	{ NS_CAUSE_MISSING_ESSENT_IE,	"Missing essential IE" },
+	{ NS_CAUSE_INVAL_NR_IPv4_EP,	"Invalid Number of IPv4 Endpoints" },
+	{ NS_CAUSE_INVAL_NR_IPv6_EP,	"Invalid Number of IPv6 Endpoints" },
+	{ NS_CAUSE_INVAL_NR_NS_VC,	"Invalid Number of NS-VCs" },
+	{ NS_CAUSE_INVAL_WEIGH,		"Invalid Weights" },
+	{ NS_CAUSE_UNKN_IP_EP,		"Unknown IP Endpoint" },
+	{ NS_CAUSE_UNKN_IP_ADDR,	"Unknown IP Address" },
+	{ NS_CAUSE_UNKN_IP_TEST_FAILED,	"IP Test Failed" },
+	{ 0, NULL }
+};
+
+/*! Obtain a human-readable string for NS cause value */
+const char *gprs_ns2_cause_str(int cause)
+{
+	enum ns_cause _cause = cause;
+	return get_value_string(ns2_cause_str, _cause);
+}
+
+static const struct rate_ctr_desc nsvc_ctr_description[] = {
+	{ "packets:in", "Packets at NS Level  ( In)" },
+	{ "packets:out","Packets at NS Level  (Out)" },
+	{ "bytes:in",	"Bytes at NS Level    ( In)" },
+	{ "bytes:out",	"Bytes at NS Level    (Out)" },
+	{ "blocked",	"NS-VC Block count         " },
+	{ "dead",	"NS-VC gone dead count     " },
+	{ "replaced",	"NS-VC replaced other count" },
+	{ "nsei-chg",	"NS-VC changed NSEI count  " },
+	{ "inv-nsvci",	"NS-VCI was invalid count  " },
+	{ "inv-nsei",	"NSEI was invalid count    " },
+	{ "lost:alive",	"ALIVE ACK missing count   " },
+	{ "lost:reset",	"RESET ACK missing count   " },
+};
+
+static const struct rate_ctr_group_desc nsvc_ctrg_desc = {
+	.group_name_prefix = "ns:nsvc",
+	.group_description = "NSVC Peer Statistics",
+	.num_ctr = ARRAY_SIZE(nsvc_ctr_description),
+	.ctr_desc = nsvc_ctr_description,
+	.class_id = OSMO_STATS_CLASS_PEER,
+};
+
+
+static const struct osmo_stat_item_desc nsvc_stat_description[] = {
+	{ "alive.delay", "ALIVE response time        ", "ms", 16, 0 },
+};
+
+static const struct osmo_stat_item_group_desc nsvc_statg_desc = {
+	.group_name_prefix = "ns.nsvc",
+	.group_description = "NSVC Peer Statistics",
+	.num_items = ARRAY_SIZE(nsvc_stat_description),
+	.item_desc = nsvc_stat_description,
+	.class_id = OSMO_STATS_CLASS_PEER,
+};
+
+char *gprs_ns2_ll_str_buf(char *buf, size_t buf_len, struct gprs_ns2_vc *nsvc)
+{
+	struct osmo_sockaddr *local;
+	struct osmo_sockaddr *remote;
+	struct osmo_sockaddr_str local_str;
+	struct osmo_sockaddr_str remote_str;
+
+	if (!buf_len)
+		return 0;
+
+	switch (nsvc->ll) {
+	case GPRS_NS_LL_UDP:
+		if (!gprs_ns2_is_ip_bind(nsvc->bind)) {
+			buf[0] = '\0';
+			return buf;
+		}
+
+		local = gprs_ns2_ip_bind_sockaddr(nsvc->bind);
+		remote = gprs_ns2_ip_vc_sockaddr(nsvc);
+		if (osmo_sockaddr_str_from_sockaddr(&local_str, &local->u.sas))
+			strcpy(local_str.ip, "invalid");
+		if (osmo_sockaddr_str_from_sockaddr(&remote_str, &remote->u.sas))
+			strcpy(remote_str.ip, "invalid");
+
+		if (nsvc->nsvci_is_valid)
+			snprintf(buf, buf_len, "udp)[%s]:%u<%u>[%s]:%u",
+				 local_str.ip, local_str.port,
+				 nsvc->nsvci,
+				 remote_str.ip, remote_str.port);
+		else
+			snprintf(buf, buf_len, "udp)[%s]:%u<>[%s]:%u",
+				 local_str.ip, local_str.port,
+				 remote_str.ip, remote_str.port);
+		break;
+	case GPRS_NS_LL_FR_GRE:
+		snprintf(buf, buf_len, "frgre)");
+		break;
+	case GPRS_NS_LL_E1:
+		snprintf(buf, buf_len, "e1)");
+		break;
+	default:
+		buf[0] = '\0';
+		break;
+	}
+
+	buf[buf_len - 1] = '\0';
+
+	return buf;
+}
+
+/* udp is the longest: udp)[IP6]:65536<65536>[IP6]:65536 */
+#define NS2_LL_MAX_STR 4+2*(INET6_ADDRSTRLEN+9)+8
+
+const char *gprs_ns2_ll_str(struct gprs_ns2_vc *nsvc)
+{
+	static __thread char buf[NS2_LL_MAX_STR];
+	return gprs_ns2_ll_str_buf(buf, sizeof(buf), nsvc);
+}
+
+char *gprs_ns2_ll_str_c(const void *ctx, struct gprs_ns2_vc *nsvc)
+{
+	char *buf = talloc_size(ctx, NS2_LL_MAX_STR);
+	if (!buf)
+		return buf;
+	return gprs_ns2_ll_str_buf(buf, NS2_LL_MAX_STR, nsvc);
+}
+
+/*!
+ * Receive a primitive from the NS User (Gb)
+ * \param nsi
+ * \param oph The primitive.
+ * \return 0 on success
+ */
+int gprs_ns2_recv_prim(struct gprs_ns2_inst *nsi, struct osmo_prim_hdr *oph)
+{
+	/* TODO: implement load distribution function */
+	/* TODO: implement resource distribution */
+	/* TODO: check for empty PDUs which can be sent to Request/Confirm
+	 *       the IP endpoint */
+	struct osmo_gprs_ns2_prim *nsp;
+	struct gprs_ns2_nse *nse = NULL;
+	struct gprs_ns2_vc *nsvc = NULL, *tmp;
+	uint16_t bvci, nsei;
+	uint8_t sducontrol = 0;
+
+	if (oph->sap != SAP_NS)
+		return -EINVAL;
+
+	nsp = container_of(oph, struct osmo_gprs_ns2_prim, oph);
+
+	if (oph->operation != PRIM_OP_REQUEST || oph->primitive != PRIM_NS_UNIT_DATA)
+		return -EINVAL;
+
+	if (!oph->msg)
+		return -EINVAL;
+
+	bvci = nsp->bvci;
+	nsei = nsp->nsei;
+
+	nse = gprs_ns2_nse_by_nsei(nsi, nsei);
+	if (!nse)
+		return -EINVAL;
+
+	llist_for_each_entry(tmp, &nse->nsvc, list) {
+		if (!gprs_ns2_vc_is_unblocked(tmp))
+			continue;
+		if (bvci == 0 && tmp->sig_weight == 0)
+			continue;
+		if (bvci != 0 && tmp->data_weight == 0)
+			continue;
+
+		nsvc = tmp;
+	}
+
+	/* TODO: send a status primitive back */
+	if (!nsvc)
+		return 0;
+
+	if (nsp->u.unitdata.change == NS_ENDPOINT_REQUEST_CHANGE)
+		sducontrol = 1;
+	else if (nsp->u.unitdata.change == NS_ENDPOINT_CONFIRM_CHANGE)
+		sducontrol = 2;
+
+	return ns2_tx_unit_data(nsvc, bvci, sducontrol, oph->msg);
+}
+
+void ns2_prim_status_ind(struct gprs_ns2_inst *nsi,
+			 uint16_t nsei, uint16_t bvci,
+			 enum gprs_ns2_affecting_cause cause)
+{
+	struct osmo_gprs_ns2_prim nsp = {};
+	nsp.nsei = nsei;
+	nsp.bvci = bvci;
+	nsp.u.status.cause = cause;
+	nsp.u.status.transfer = -1;
+	osmo_prim_init(&nsp.oph, SAP_NS, PRIM_NS_STATUS,
+			PRIM_OP_INDICATION, NULL);
+	nsi->cb(&nsp.oph, nsi->cb_data);
+}
+
+/*!
+ * \brief ns2_vc_alloc
+ * \param bind
+ * \param nse
+ * \param initiater - if this is an incoming remote (!initiater) or a local outgoing connection (initater)
+ * \return
+ */
+struct gprs_ns2_vc *ns2_vc_alloc(struct gprs_ns2_vc_bind *bind, struct gprs_ns2_nse *nse, bool initiater)
+{
+	struct gprs_ns2_vc *nsvc = talloc_zero(bind, struct gprs_ns2_vc);
+
+	if (!nsvc)
+		return NULL;
+
+	nsvc->bind = bind;
+	nsvc->nse = nse;
+	nsvc->mode = bind->vc_mode;
+	nsvc->sig_weight = 1;
+	nsvc->data_weight = 1;
+
+	nsvc->ctrg = rate_ctr_group_alloc(nsvc, &nsvc_ctrg_desc, bind->nsi->rate_ctr_idx);
+	if (!nsvc->ctrg) {
+		goto err;
+	}
+	nsvc->statg = osmo_stat_item_group_alloc(nsvc, &nsvc_statg_desc, bind->nsi->rate_ctr_idx);
+	if (!nsvc->statg)
+		goto err_group;
+	if (!gprs_ns2_vc_fsm_alloc(nsvc, NULL, initiater))
+		goto err_statg;
+
+	bind->nsi->rate_ctr_idx++;
+
+	llist_add(&nsvc->list, &nse->nsvc);
+	llist_add(&nsvc->blist, &bind->nsvc);
+
+	return nsvc;
+
+err_statg:
+	osmo_stat_item_group_free(nsvc->statg);
+err_group:
+	rate_ctr_group_free(nsvc->ctrg);
+err:
+	talloc_free(nsvc);
+
+	return NULL;
+}
+
+
+void gprs_ns2_free_nsvc(struct gprs_ns2_vc *nsvc)
+{
+	if (!nsvc)
+		return;
+
+	ns2_prim_status_ind(nsvc->nse->nsi, nsvc->nse->nsei,
+			    0, NS_AFF_CAUSE_VC_FAILURE);
+
+	llist_del(&nsvc->list);
+	llist_del(&nsvc->blist);
+
+	/* notify nse this nsvc is unavailable */
+	ns2_nse_notify_unblocked(nsvc, false);
+
+	/* check if sns is using this VC */
+	ns2_sns_free_nsvc(nsvc);
+	osmo_fsm_inst_term(nsvc->fi, OSMO_FSM_TERM_REQUEST, NULL);
+
+	/* let the driver/bind clean up it's internal state */
+	if (nsvc->priv && nsvc->bind->free_vc)
+		nsvc->bind->free_vc(nsvc);
+
+	osmo_stat_item_group_free(nsvc->statg);
+	rate_ctr_group_free(nsvc->ctrg);
+
+	talloc_free(nsvc);
+}
+
+struct msgb *gprs_ns2_msgb_alloc(void)
+{
+	struct msgb *msg = msgb_alloc_headroom(NS_ALLOC_SIZE, NS_ALLOC_HEADROOM,
+					       "GPRS/NS");
+	if (!msg) {
+		LOGP(DLNS, LOGL_ERROR, "Failed to allocate NS message of size %d\n",
+			NS_ALLOC_SIZE);
+	}
+	return msg;
+}
+
+/*!
+ * Create a status message to be sent over a new connection.
+ * \param[in] orig_msg the original message
+ * \param[in] tp TLVP parsed of the original message
+ * \param[out] reject
+ * \param[in] cause
+ * \return 0 on success
+ */
+static int reject_status_msg(struct msgb *orig_msg, struct tlv_parsed *tp, struct msgb **reject, enum ns_cause cause)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	struct gprs_ns_hdr *nsh;
+	bool have_vci = false;
+	uint8_t _cause = cause;
+	uint16_t nsei = 0;
+
+	if (!msg)
+		return -ENOMEM;
+
+	if (TLVP_PRESENT(tp, NS_IE_NSEI)) {
+		nsei = tlvp_val16be(tp, NS_IE_NSEI);
+
+		LOGP(DLNS, LOGL_NOTICE, "NSEI=%u Rejecting message without NSVCI. Tx NS STATUS (cause=%s)\n",
+		     nsei, gprs_ns2_cause_str(cause));
+	}
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+	nsh->pdu_type = NS_PDUT_STATUS;
+
+	msgb_tvlv_put(msg, NS_IE_CAUSE, 1, &_cause);
+	have_vci = TLVP_PRESENT(tp, NS_IE_VCI);
+
+	/* Section 9.2.7.1: Static conditions for NS-VCI */
+	if (cause == NS_CAUSE_NSVC_BLOCKED ||
+	    cause == NS_CAUSE_NSVC_UNKNOWN) {
+		if (!have_vci) {
+			msgb_free(msg);
+			return -EINVAL;
+		}
+
+		msgb_tvlv_put(msg, NS_IE_VCI, 2, TLVP_VAL(tp, NS_IE_VCI));
+	}
+
+	/* Section 9.2.7.2: Static conditions for NS PDU */
+	switch (cause) {
+	case NS_CAUSE_SEM_INCORR_PDU:
+	case NS_CAUSE_PDU_INCOMP_PSTATE:
+	case NS_CAUSE_PROTO_ERR_UNSPEC:
+	case NS_CAUSE_INVAL_ESSENT_IE:
+	case NS_CAUSE_MISSING_ESSENT_IE:
+		msgb_tvlv_put(msg, NS_IE_PDU, msgb_l2len(orig_msg),
+			      orig_msg->l2h);
+		break;
+	default:
+		break;
+	}
+
+	*reject = msg;
+	return 0;
+}
+
+struct gprs_ns2_nse *gprs_ns2_nse_by_nsei(struct gprs_ns2_inst *nsi, uint16_t nsei)
+{
+	struct gprs_ns2_nse *nse;
+
+	llist_for_each_entry(nse, &nsi->nse, list) {
+		if (nse->nsei == nsei)
+			return nse;
+	}
+
+	return NULL;
+}
+
+struct gprs_ns2_vc *gprs_ns2_nsvc_by_nsvci(struct gprs_ns2_inst *nsi, uint16_t nsvci)
+{
+	struct gprs_ns2_nse *nse;
+	struct gprs_ns2_vc *nsvc;
+
+	llist_for_each_entry(nse, &nsi->nse, list) {
+		llist_for_each_entry(nsvc, &nse->nsvc, list) {
+			if (nsvc->nsvci_is_valid && nsvc->nsvci == nsvci)
+				return nsvc;
+		}
+	}
+
+	return NULL;
+}
+
+struct gprs_ns2_nse *gprs_ns2_create_nse(struct gprs_ns2_inst *nsi, uint16_t nsei)
+{
+	struct gprs_ns2_nse *nse;
+
+	nse = gprs_ns2_nse_by_nsei(nsi, nsei);
+	if (nse) {
+		LOGP(DLNS, LOGL_ERROR, "NSEI:%u Can not create a NSE with already taken NSEI\n", nsei);
+		return nse;
+	}
+
+	nse = talloc_zero(nsi, struct gprs_ns2_nse);
+	if (!nse)
+		return NULL;
+
+	nse->nsei = nsei;
+	nse->nsi = nsi;
+	llist_add(&nse->list, &nsi->nse);
+	INIT_LLIST_HEAD(&nse->nsvc);
+
+	return nse;
+}
+
+void gprs_ns2_free_nse(struct gprs_ns2_nse *nse)
+{
+	struct gprs_ns2_vc *nsvc, *tmp;
+
+	if (!nse)
+		return;
+
+	llist_for_each_entry_safe(nsvc, tmp, &nse->nsvc, list) {
+		gprs_ns2_free_nsvc(nsvc);
+	}
+
+	ns2_prim_status_ind(nse->nsi, nse->nsei,
+			    0, NS_AFF_CAUSE_FAILURE);
+
+	llist_del(&nse->list);
+	if (nse->bss_sns_fi)
+		osmo_fsm_inst_term(nse->bss_sns_fi, OSMO_FSM_TERM_REQUEST, NULL);
+	talloc_free(nse);
+}
+
+static inline int ns2_tlv_parse(struct tlv_parsed *dec,
+			 const uint8_t *buf, int buf_len, uint8_t lv_tag,
+			 uint8_t lv_tag2)
+{
+	/* workaround for NS_IE_IP_ADDR not following any known TLV rules.
+	 * See comment of ns_att_tlvdef1. */
+	int rc = tlv_parse(dec, &ns_att_tlvdef1, buf, buf_len, lv_tag, lv_tag2);
+	if (rc < 0)
+		return tlv_parse(dec, &ns_att_tlvdef2, buf, buf_len, lv_tag, lv_tag2);
+	return rc;
+}
+
+
+/*!
+ * Create a new VC based on a message. Depending on the bind it might create NSE.
+ * \param[in] bind
+ * \param[in] msg
+ * \param[in] logname A name to describe the VC. E.g. ip address pair
+ * \param[out] reject A message filled to be sent back. Only used in failure cases.
+ * \param[out] success A pointer which will be set to the new VC on success
+ * \return
+ */
+enum gprs_ns2_cs ns2_create_vc(struct gprs_ns2_vc_bind *bind,
+			       struct msgb *msg,
+			       const char *logname,
+			       struct msgb **reject,
+			       struct gprs_ns2_vc **success)
+{
+	struct gprs_ns_hdr *nsh = (struct gprs_ns_hdr *)msg->l2h;
+	struct tlv_parsed tp;
+	struct gprs_ns2_vc *nsvc;
+	struct gprs_ns2_nse *nse;
+	uint16_t nsvci;
+	uint16_t nsei;
+
+	int rc;
+
+	if (msg->len < sizeof(struct gprs_ns_hdr))
+		return GPRS_NS2_CS_ERROR;
+
+	if (nsh->pdu_type == NS_PDUT_STATUS) {
+		/* Do not respond, see 3GPP TS 08.16, 7.5.1 */
+		LOGP(DLNS, LOGL_INFO, "Ignoring NS STATUS from %s "
+		     "for non-existing NS-VC\n",
+		     logname);
+		return GPRS_NS2_CS_SKIPPED;
+	}
+
+	if (nsh->pdu_type == NS_PDUT_ALIVE_ACK) {
+		/* Ignore this, see 3GPP TS 08.16, 7.4.1 */
+		LOGP(DLNS, LOGL_INFO, "Ignoring NS ALIVE ACK from %s "
+		     "for non-existing NS-VC\n",
+		     logname);
+		return GPRS_NS2_CS_SKIPPED;
+	}
+
+	if (nsh->pdu_type == NS_PDUT_RESET_ACK) {
+		/* Ignore this, see 3GPP TS 08.16, 7.3.1 */
+		LOGP(DLNS, LOGL_INFO, "Ignoring NS RESET ACK from %s "
+		     "for non-existing NS-VC\n",
+		     logname);
+		return GPRS_NS2_CS_SKIPPED;
+	}
+
+	/* Only the RESET procedure creates a new NSVC */
+	if (nsh->pdu_type != NS_PDUT_RESET) {
+		rc = reject_status_msg(msg, &tp, reject, NS_CAUSE_PDU_INCOMP_PSTATE);
+
+		if (rc < 0) {
+			LOGP(DLNS, LOGL_ERROR, "Failed to generate reject message (%d)\n", rc);
+			return rc;
+		}
+		return GPRS_NS2_CS_REJECTED;
+	}
+
+	rc = ns2_tlv_parse(&tp, nsh->data,
+			   msgb_l2len(msg) - sizeof(*nsh), 0, 0);
+	if (rc < 0) {
+		LOGP(DLNS, LOGL_ERROR, "Rx NS RESET Error %d during "
+		     "TLV Parse\n", rc);
+		/* TODO: send invalid message back */
+		return GPRS_NS2_CS_REJECTED;
+	}
+
+	if (!TLVP_PRESENT(&tp, NS_IE_CAUSE) ||
+	    !TLVP_PRESENT(&tp, NS_IE_VCI) || !TLVP_PRESENT(&tp, NS_IE_NSEI)) {
+		LOGP(DLNS, LOGL_ERROR, "NS RESET Missing mandatory IE\n");
+		rc = reject_status_msg(msg, &tp, reject, NS_CAUSE_MISSING_ESSENT_IE);
+		return GPRS_NS2_CS_REJECTED;
+	}
+
+	/* find or create NSE */
+	nsei  = tlvp_val16be(&tp, NS_IE_NSEI);
+	nse = gprs_ns2_nse_by_nsei(bind->nsi, nsei);
+	if (!nse) {
+		if (!bind->nsi->create_nse) {
+			return GPRS_NS2_CS_SKIPPED;
+		}
+
+		nse = gprs_ns2_create_nse(bind->nsi, nsei);
+		if (!nse) {
+			return GPRS_NS2_CS_ERROR;
+		}
+	}
+
+	nsvc = ns2_vc_alloc(bind, nse, false);
+	if (!nsvc)
+		return GPRS_NS2_CS_SKIPPED;
+
+	nsvc->ll = GPRS_NS_LL_UDP;
+
+	nsvci = tlvp_val16be(&tp, NS_IE_VCI);
+	nsvc->nsvci = nsvci;
+	nsvc->nsvci_is_valid = true;
+
+	*success = nsvc;
+
+	return GPRS_NS2_CS_CREATED;
+}
+
+struct gprs_ns2_vc *gprs_ns2_ip_connect_inactive(struct gprs_ns2_vc_bind *bind,
+					struct osmo_sockaddr *remote,
+					struct gprs_ns2_nse *nse,
+					uint16_t nsvci)
+{
+	struct gprs_ns2_vc *nsvc;
+
+	nsvc = gprs_ns2_ip_bind_connect(bind, nse, remote);
+	if (!nsvc)
+		return NULL;
+
+	if (nsvc->mode == NS2_VC_MODE_BLOCKRESET) {
+		nsvc->nsvci = nsvci;
+		nsvc->nsvci_is_valid = true;
+	}
+
+	return nsvc;
+}
+
+/*!
+ * Create a new IP-based NSVC
+ * \param bind
+ * \param remote
+ * \param nse
+ * \param nsvci is only required when bind->vc_mode == NS2_VC_MODE_BLOCKRESET
+ * \return
+ */
+struct gprs_ns2_vc *gprs_ns2_ip_connect(struct gprs_ns2_vc_bind *bind,
+					struct osmo_sockaddr *remote,
+					struct gprs_ns2_nse *nse,
+					uint16_t nsvci)
+{
+	struct gprs_ns2_vc *nsvc;
+	nsvc = gprs_ns2_ip_connect_inactive(bind, remote, nse, nsvci);
+	if (!nsvc)
+		return NULL;
+
+	gprs_ns2_vc_fsm_start(nsvc);
+
+	return nsvc;
+}
+
+/*!
+ * Create a new IP-based NSVC
+ * \param bind
+ * \param remote
+ * \param nsei
+ * \param nsvci only required when bind->vc_mode == NS2_VC_MODE_BLOCKRESET
+ * \return
+ */
+struct gprs_ns2_vc *gprs_ns2_ip_connect2(struct gprs_ns2_vc_bind *bind,
+					 struct osmo_sockaddr *remote,
+					 uint16_t nsei,
+					 uint16_t nsvci)
+{
+	struct gprs_ns2_nse *nse = gprs_ns2_nse_by_nsei(bind->nsi, nsei);
+
+	if (!nse) {
+		nse = gprs_ns2_create_nse(bind->nsi, nsei);
+		if (!nse)
+			return NULL;
+	}
+
+	return gprs_ns2_ip_connect(bind, remote, nse, nsvci);
+}
+
+/*!
+ * Create a new IP SNS NSE
+ * \param bind
+ * \param remote
+ * \param nsei
+ * \return 0 on success
+ */
+int gprs_ns2_ip_connect_sns(struct gprs_ns2_vc_bind *bind,
+			    struct osmo_sockaddr *remote,
+			    uint16_t nsei)
+{
+	struct gprs_ns2_nse *nse = gprs_ns2_nse_by_nsei(bind->nsi, nsei);
+	struct gprs_ns2_vc *nsvc;
+
+	if (!nse) {
+		nse = gprs_ns2_create_nse(bind->nsi, nsei);
+		if (!nse)
+			return -1;
+	}
+
+	nsvc = gprs_ns2_ip_bind_connect(bind, nse, remote);
+	if (!nsvc)
+		return -1;
+
+	if (!nse->bss_sns_fi)
+		nse->bss_sns_fi = ns2_sns_bss_fsm_alloc(nse, NULL);
+
+	if (!nse->bss_sns_fi)
+		return -1;
+
+	return ns2_sns_bss_fsm_start(nse, nsvc, remote);
+}
+
+struct gprs_ns2_vc *gprs_ns2_nsvc_by_sockaddr(struct gprs_ns2_nse *nse,
+					      struct osmo_sockaddr *sockaddr)
+{
+	struct gprs_ns2_vc *nsvc;
+	struct osmo_sockaddr *remote;
+
+	OSMO_ASSERT(nse);
+	OSMO_ASSERT(sockaddr);
+
+	llist_for_each_entry(nsvc, &nse->nsvc, list) {
+		remote = gprs_ns2_ip_vc_sockaddr(nsvc);
+		if (!osmo_sockaddr_cmp(sockaddr, remote))
+			return nsvc;
+	}
+
+	return NULL;
+}
+
+
+/*!
+ * \brief gprs_ns2_recv_vc entrypoint of received NS PDU from the driver/bind
+ * \param nsi
+ * \param vc
+ * \param msg the received message. Must not be freeded.
+ * \return
+ */
+int ns2_recv_vc(struct gprs_ns2_inst *nsi,
+		struct gprs_ns2_vc *nsvc,
+		struct msgb *msg)
+{
+	struct gprs_ns_hdr *nsh = (struct gprs_ns_hdr *) msg->l2h;
+	struct tlv_parsed tp;
+	int rc = 0;
+
+	if (msg->len < sizeof(struct gprs_ns_hdr))
+		return -EINVAL;
+
+	switch (nsh->pdu_type) {
+	case SNS_PDUT_CONFIG:
+		/* one additional byte ('end flag') before the TLV part starts */
+		rc = ns2_tlv_parse(&tp, nsh->data+1,
+				   msgb_l2len(msg) - sizeof(*nsh)-1, 0, 0);
+		if (rc < 0) {
+			LOGPC(DLNS, LOGL_NOTICE, "Error during TLV Parse in %s\n", msgb_hexdump(msg));
+			return rc;
+		}
+		/* All sub-network service related message types */
+		rc = gprs_ns2_sns_rx(nsvc, msg, &tp);
+		break;
+	case SNS_PDUT_ACK:
+	case SNS_PDUT_ADD:
+	case SNS_PDUT_CHANGE_WEIGHT:
+	case SNS_PDUT_DELETE:
+		/* weird layout: NSEI TLV, then value-only transaction IE, then TLV again */
+		rc = ns2_tlv_parse(&tp, nsh->data+1,
+				   msgb_l2len(msg) - sizeof(*nsh)-1, 0, 0);
+		if (rc < 0) {
+			LOGPC(DLNS, LOGL_NOTICE, "Error during TLV Parse in %s\n", msgb_hexdump(msg));
+			return rc;
+		}
+		tp.lv[NS_IE_NSEI].val = nsh->data+2;
+		tp.lv[NS_IE_NSEI].len = 2;
+		tp.lv[NS_IE_TRANS_ID].val = nsh->data+4;
+		tp.lv[NS_IE_TRANS_ID].len = 1;
+		rc = gprs_ns2_sns_rx(nsvc, msg, &tp);
+		break;
+	case SNS_PDUT_CONFIG_ACK:
+	case SNS_PDUT_SIZE:
+	case SNS_PDUT_SIZE_ACK:
+		rc = ns2_tlv_parse(&tp, nsh->data,
+				   msgb_l2len(msg) - sizeof(*nsh), 0, 0);
+		if (rc < 0) {
+			LOGPC(DLNS, LOGL_NOTICE, "Error during TLV Parse in %s\n", msgb_hexdump(msg));
+			return rc;
+		}
+		/* All sub-network service related message types */
+		rc = gprs_ns2_sns_rx(nsvc, msg, &tp);
+		break;
+
+	case NS_PDUT_UNITDATA:
+		rc = gprs_ns2_vc_rx(nsvc, msg, NULL);
+		break;
+	default:
+		rc = ns2_tlv_parse(&tp, nsh->data,
+				   msgb_l2len(msg) - sizeof(*nsh), 0, 0);
+		if (rc < 0) {
+			LOGPC(DLNS, LOGL_NOTICE, "Error during TLV Parse\n");
+			if (nsh->pdu_type != NS_PDUT_STATUS)
+				ns2_tx_status(nsvc, NS_CAUSE_PROTO_ERR_UNSPEC, 0, msg);
+			return rc;
+		}
+		rc = gprs_ns2_vc_rx(nsvc, msg, &tp);
+		break;
+	}
+
+	return rc;
+}
+
+/* notify a nse about the change of a nsvc */
+void ns2_nse_notify_unblocked(struct gprs_ns2_vc *nsvc, bool unblocked)
+{
+	struct gprs_ns2_nse *nse = nsvc->nse;
+	struct gprs_ns2_vc *tmp;
+
+	if (unblocked == nse->alive)
+		return;
+
+	if (unblocked) {
+		/* this is the first unblocked NSVC on an unavailable NSE */
+		nse->alive = true;
+		ns2_prim_status_ind(nse->nsi, nse->nsei,
+				    0, NS_AFF_CAUSE_RECOVERY);
+		return;
+	}
+
+	/* check if there are any remaining alive vcs */
+	llist_for_each_entry(tmp, &nse->nsvc, list) {
+		if (tmp == nsvc)
+			continue;
+
+		if (gprs_ns2_vc_is_unblocked(tmp)) {
+			/* there is at least one remaining alive NSVC */
+			return;
+		}
+	}
+
+	/* nse became unavailable */
+	nse->alive = false;
+	ns2_prim_status_ind(nse->nsi, nse->nsei,
+			    0, NS_AFF_CAUSE_FAILURE);
+}
+
+/*! Create a new GPRS NS instance
+ *  \param[in] ctx a talloc context
+ *  \param[in] cb Call-back function for incoming BSSGP data
+ *  \param[in] cb_data Call-back data
+ *  \returns dynamically allocated gprs_ns_inst
+ */
+struct gprs_ns2_inst *gprs_ns2_instantiate(void *ctx, osmo_prim_cb cb, void *cb_data)
+{
+	struct gprs_ns2_inst *nsi;
+
+	nsi = talloc_zero(ctx, struct gprs_ns2_inst);
+	if (!nsi)
+		return NULL;
+
+	nsi->cb = cb;
+	nsi->cb_data = cb_data;
+	INIT_LLIST_HEAD(&nsi->binding);
+	INIT_LLIST_HEAD(&nsi->nse);
+
+	nsi->timeout[NS_TOUT_TNS_BLOCK] = 3;
+	nsi->timeout[NS_TOUT_TNS_BLOCK_RETRIES] = 3;
+	nsi->timeout[NS_TOUT_TNS_RESET] = 3;
+	nsi->timeout[NS_TOUT_TNS_RESET_RETRIES] = 3;
+	nsi->timeout[NS_TOUT_TNS_TEST] = 30;
+	nsi->timeout[NS_TOUT_TNS_ALIVE] = 3;
+	nsi->timeout[NS_TOUT_TNS_ALIVE_RETRIES] = 10;
+	nsi->timeout[NS_TOUT_TSNS_PROV] = 3; /* 1..10 */
+
+	return nsi;
+}
+
+void gprs_ns2_free(struct gprs_ns2_inst *nsi)
+{
+	struct gprs_ns2_vc_bind *bind, *tbind;
+	struct gprs_ns2_nse *nse, *ntmp;
+
+	if (!nsi)
+		return;
+
+	llist_for_each_entry_safe(nse, ntmp, &nsi->nse, list) {
+		gprs_ns2_free_nse(nse);
+	}
+
+	llist_for_each_entry_safe(bind, tbind, &nsi->binding, list) {
+		gprs_ns2_free_bind(bind);
+	}
+}
+
+/*!
+ * \brief gprs_ns2_dynamic_create_nse
+ * \param nsi the instance to modify
+ * \param create_nse if NSE can be created on receiving package. SGSN set this.
+ * \return
+ */
+int gprs_ns2_dynamic_create_nse(struct gprs_ns2_inst *nsi, bool create_nse)
+{
+	nsi->create_nse = create_nse;
+
+	return 0;
+}
+
+void gprs_ns2_start_alive_all_nsvcs(struct gprs_ns2_nse *nse)
+{
+	struct gprs_ns2_vc *nsvc;
+	OSMO_ASSERT(nse);
+
+	llist_for_each_entry(nsvc, &nse->nsvc, list) {
+		if (nsvc->sns_only)
+			continue;
+
+		gprs_ns2_vc_fsm_start(nsvc);
+	}
+}
+
+void gprs_ns2_bind_set_mode(struct gprs_ns2_vc_bind *bind, enum gprs_ns2_vc_mode mode)
+{
+	bind->vc_mode = mode;
+}
+
+void gprs_ns2_free_bind(struct gprs_ns2_vc_bind *bind)
+{
+	struct gprs_ns2_vc *nsvc, *tmp;
+	if (!bind)
+		return;
+
+	llist_for_each_entry_safe(nsvc, tmp, &bind->nsvc, blist) {
+		gprs_ns2_free_nsvc(nsvc);
+	}
+
+	if (bind->driver->free_bind)
+		bind->driver->free_bind(bind);
+
+	llist_del(&bind->list);
+	talloc_free(bind);
+}
+/*! @} */
diff --git a/src/gb/gprs_ns2_frgre.c b/src/gb/gprs_ns2_frgre.c
new file mode 100644
index 0000000..2b19157
--- /dev/null
+++ b/src/gb/gprs_ns2_frgre.c
@@ -0,0 +1,596 @@
+/*! \file gprs_ns2_frgre.c
+ * NS-over-FR-over-GRE implementation.
+ * GPRS Networks Service (NS) messages on the Gb interface.
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ * as well as its successor 3GPP TS 48.016 */
+
+/* (C) 2009-2010,2014,2017 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2020 sysmocom - s.f.m.c. GmbH
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/byteswap.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/gprs/gprs_ns2.h>
+
+#include "gprs_ns2_internal.h"
+
+#define GRE_PTYPE_FR	0x6559
+#define GRE_PTYPE_IPv4	0x0800
+#define GRE_PTYPE_IPv6	0x86dd
+#define GRE_PTYPE_KAR	0x0000	/* keepalive response */
+
+#ifndef IPPROTO_GRE
+# define IPPROTO_GRE 47
+#endif
+
+struct gre_hdr {
+	uint16_t flags;
+	uint16_t ptype;
+} __attribute__ ((packed));
+
+#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__CYGWIN__)
+/**
+ * On BSD the IPv4 struct is called struct ip and instead of iXX
+ * the members are called ip_XX. One could change this code to use
+ * struct ip but that would require to define _BSD_SOURCE and that
+ * might have other complications. Instead make sure struct iphdr
+ * is present on FreeBSD. The below is taken from GLIBC.
+ *
+ * The GNU C Library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ */
+struct iphdr
+  {
+#if BYTE_ORDER == LITTLE_ENDIAN
+    unsigned int ihl:4;
+    unsigned int version:4;
+#elif BYTE_ORDER == BIG_ENDIAN
+    unsigned int version:4;
+    unsigned int ihl:4;
+#endif
+    u_int8_t tos;
+    u_int16_t tot_len;
+    u_int16_t id;
+    u_int16_t frag_off;
+    u_int8_t ttl;
+    u_int8_t protocol;
+    u_int16_t check;
+    u_int32_t saddr;
+    u_int32_t daddr;
+    /*The options start here. */
+  };
+#endif
+
+
+static void free_bind(struct gprs_ns2_vc_bind *bind);
+static inline int frgre_sendmsg(struct gprs_ns2_vc_bind *bind,
+			       struct msgb *msg,
+			       struct osmo_sockaddr *dest);
+
+struct gprs_ns2_vc_driver vc_driver_frgre = {
+	.name = "GB frame relay over GRE",
+	.free_bind = free_bind,
+};
+
+struct priv_bind {
+	struct osmo_fd fd;
+	struct osmo_sockaddr addr;
+	uint16_t dlci;
+	int dscp;
+};
+
+struct priv_vc {
+	struct osmo_sockaddr remote;
+	uint16_t dlci;
+};
+
+static void free_vc(struct gprs_ns2_vc *nsvc)
+{
+	OSMO_ASSERT(nsvc);
+
+	if (!nsvc->priv)
+		return;
+
+	talloc_free(nsvc->priv);
+	nsvc->priv = NULL;
+}
+
+
+/*! clean up all private driver state. Should be only called by gprs_ns2_free_bind() */
+static void free_bind(struct gprs_ns2_vc_bind *bind)
+{
+	struct priv_bind *priv;
+
+	if (!bind)
+		return;
+
+	priv = bind->priv;
+
+	OSMO_ASSERT(llist_empty(&bind->nsvc));
+
+	osmo_fd_close(&priv->fd);
+	talloc_free(priv);
+}
+
+static struct priv_vc *frgre_alloc_vc(struct gprs_ns2_vc_bind *bind,
+				      struct gprs_ns2_vc *nsvc,
+				      struct osmo_sockaddr *remote,
+				      uint16_t dlci)
+{
+	struct priv_vc *priv = talloc_zero(bind, struct priv_vc);
+	if (!priv)
+		return NULL;
+
+	nsvc->priv = priv;
+	priv->remote = *remote;
+	priv->dlci = dlci;
+
+	return priv;
+}
+
+static int handle_rx_gre_ipv6(struct osmo_fd *bfd, struct msgb *msg,
+			      struct ip6_hdr *ip6hdr, struct gre_hdr *greh)
+{
+	/* RFC 7676 IPv6 Support for Generic Routing Encapsulation (GRE) */
+	struct gprs_ns2_vc_bind *bind = bfd->data;
+	struct priv_bind *priv = bind->priv;
+	int gre_payload_len;
+	struct ip6_hdr *inner_ip6h;
+	struct gre_hdr *inner_greh;
+	struct sockaddr_in6 daddr;
+	struct in6_addr ia6;
+
+	gre_payload_len = msg->len - (sizeof(*ip6hdr) + sizeof(*greh));
+
+	inner_ip6h = (struct ip6_hdr *) ((uint8_t *)greh + sizeof(*greh));
+
+	if (gre_payload_len < sizeof(*ip6hdr) + sizeof(*inner_greh)) {
+		LOGP(DLNS, LOGL_ERROR, "GRE keepalive too short\n");
+		return -EIO;
+	}
+
+	if (!memcmp(&inner_ip6h->ip6_src, &ip6hdr->ip6_src, sizeof(struct in6_addr)) ||
+	    !memcmp(&inner_ip6h->ip6_dst, &ip6hdr->ip6_dst, sizeof(struct in6_addr))) {
+		LOGP(DLNS, LOGL_ERROR,
+			"GRE keepalive with wrong tunnel addresses\n");
+		return -EIO;
+	}
+
+	/* Are IPv6 extensions header are allowed in the *inner*? In the outer they are */
+	if (inner_ip6h->ip6_ctlun.ip6_un1.ip6_un1_nxt != IPPROTO_GRE) {
+		LOGP(DLNS, LOGL_ERROR, "GRE keepalive with wrong protocol\n");
+		return -EIO;
+	}
+
+	inner_greh = (struct gre_hdr *) ((uint8_t *)inner_ip6h + sizeof(struct ip6_hdr));
+	if (inner_greh->ptype != osmo_htons(GRE_PTYPE_KAR)) {
+		LOGP(DLNS, LOGL_ERROR, "GRE keepalive inner GRE type != 0\n");
+		return -EIO;
+	}
+
+	/* Actually send the response back */
+
+	daddr.sin6_family = AF_INET6;
+	daddr.sin6_addr = inner_ip6h->ip6_dst;
+	daddr.sin6_port = IPPROTO_GRE;
+
+	ia6 = ip6hdr->ip6_src;
+	char ip6str[INET6_ADDRSTRLEN] = {};
+	inet_ntop(AF_INET6, &ia6, ip6str, INET6_ADDRSTRLEN);
+	LOGP(DLNS, LOGL_DEBUG, "GRE keepalive from %s, responding\n", ip6str);
+
+	/* why does it reduce the gre_payload_len by the ipv6 header?
+	 * make it similiar to ipv4 even this seems to be wrong */
+	return sendto(priv->fd.fd, inner_greh,
+		      gre_payload_len - sizeof(*inner_ip6h), 0,
+		      (struct sockaddr *)&daddr, sizeof(daddr));
+}
+
+/* IPv4 messages inside the GRE tunnel might be GRE keepalives */
+static int handle_rx_gre_ipv4(struct osmo_fd *bfd, struct msgb *msg,
+			      struct iphdr *iph, struct gre_hdr *greh)
+{
+	struct gprs_ns2_vc_bind *bind = bfd->data;
+	struct priv_bind *priv = bind->priv;
+	int gre_payload_len;
+	struct iphdr *inner_iph;
+	struct gre_hdr *inner_greh;
+	struct sockaddr_in daddr;
+	struct in_addr ia;
+
+	gre_payload_len = msg->len - (iph->ihl*4 + sizeof(*greh));
+
+	inner_iph = (struct iphdr *) ((uint8_t *)greh + sizeof(*greh));
+
+	if (gre_payload_len < inner_iph->ihl*4 + sizeof(*inner_greh)) {
+		LOGP(DLNS, LOGL_ERROR, "GRE keepalive too short\n");
+		return -EIO;
+	}
+
+	if (inner_iph->saddr != iph->daddr ||
+	    inner_iph->daddr != iph->saddr) {
+		LOGP(DLNS, LOGL_ERROR,
+			"GRE keepalive with wrong tunnel addresses\n");
+		return -EIO;
+	}
+
+	if (inner_iph->protocol != IPPROTO_GRE) {
+		LOGP(DLNS, LOGL_ERROR, "GRE keepalive with wrong protocol\n");
+		return -EIO;
+	}
+
+	inner_greh = (struct gre_hdr *) ((uint8_t *)inner_iph + iph->ihl*4);
+	if (inner_greh->ptype != osmo_htons(GRE_PTYPE_KAR)) {
+		LOGP(DLNS, LOGL_ERROR, "GRE keepalive inner GRE type != 0\n");
+		return -EIO;
+	}
+
+	/* Actually send the response back */
+
+	daddr.sin_family = AF_INET;
+	daddr.sin_addr.s_addr = inner_iph->daddr;
+	daddr.sin_port = IPPROTO_GRE;
+
+	ia.s_addr = iph->saddr;
+	LOGP(DLNS, LOGL_DEBUG, "GRE keepalive from %s, responding\n",
+		inet_ntoa(ia));
+
+	/* why does it reduce the gre_payload_len by the ipv4 header? */
+	return sendto(priv->fd.fd, inner_greh,
+		      gre_payload_len - inner_iph->ihl*4, 0,
+		      (struct sockaddr *)&daddr, sizeof(daddr));
+}
+
+static struct msgb *read_nsfrgre_msg(struct osmo_fd *bfd, int *error,
+				     struct osmo_sockaddr *saddr, uint16_t *dlci)
+{
+	struct msgb *msg = msgb_alloc(NS_ALLOC_SIZE, "Gb/NS/FR/GRE Rx");
+	int ret = 0;
+	socklen_t saddr_len = sizeof(*saddr);
+	struct iphdr *iph = NULL;
+	struct ip6_hdr *ip6h = NULL;
+	size_t ip46hdr;
+	struct gre_hdr *greh;
+	uint8_t *frh;
+
+	if (!msg) {
+		*error = -ENOMEM;
+		return NULL;
+	}
+
+	ret = recvfrom(bfd->fd, msg->data, NS_ALLOC_SIZE, 0,
+			&saddr->u.sa, &saddr_len);
+	if (ret < 0) {
+		LOGP(DLNS, LOGL_ERROR, "recv error %s during NS-FR-GRE recv\n",
+			strerror(errno));
+		*error = ret;
+		goto out_err;
+	} else if (ret == 0) {
+		*error = ret;
+		goto out_err;
+	}
+
+	msgb_put(msg, ret);
+
+	/* we've received a raw packet including the IPv4 or IPv6 header */
+	switch (saddr->u.sa.sa_family) {
+	case AF_INET:
+		ip46hdr = sizeof(struct iphdr);
+		break;
+	case AF_INET6:
+		ip46hdr = sizeof(struct ip6_hdr);
+	default:
+		*error = -EIO;
+		goto out_err;
+		break;
+	}
+
+	/* TODO: add support for the extension headers */
+	if (msg->len < ip46hdr + sizeof(*greh) + 2) {
+		LOGP(DLNS, LOGL_ERROR, "Short IP packet: %u bytes\n", msg->len);
+		*error = -EIO;
+		goto out_err;
+	}
+
+	switch (saddr->u.sa.sa_family) {
+	case AF_INET:
+		iph = (struct iphdr *) msg->data;
+		if (msg->len < (iph->ihl*4 + sizeof(*greh) + 2)) {
+			LOGP(DLNS, LOGL_ERROR, "Short IP packet: %u bytes\n", msg->len);
+			*error = -EIO;
+			goto out_err;
+		}
+		break;
+	case AF_INET6:
+		ip6h = (struct ip6_hdr *) msg->data;
+		break;
+	}
+
+	greh = (struct gre_hdr *) (msg->data + iph->ihl*4);
+	if (greh->flags) {
+		LOGP(DLNS, LOGL_NOTICE, "Unknown GRE flags 0x%04x\n",
+			osmo_ntohs(greh->flags));
+	}
+
+	switch (osmo_ntohs(greh->ptype)) {
+	case GRE_PTYPE_IPv4:
+		/* IPv4 messages might be GRE keepalives */
+		*error = handle_rx_gre_ipv4(bfd, msg, iph, greh);
+		goto out_err;
+		break;
+	case GRE_PTYPE_IPv6:
+		*error = handle_rx_gre_ipv6(bfd, msg, ip6h, greh);
+		goto out_err;
+		break;
+	case GRE_PTYPE_FR:
+		/* continue as usual */
+		break;
+	default:
+		LOGP(DLNS, LOGL_NOTICE, "Unknown GRE protocol 0x%04x != FR\n",
+			osmo_ntohs(greh->ptype));
+		*error = -EIO;
+		goto out_err;
+		break;
+	}
+
+	if (msg->len < sizeof(*greh) + 2) {
+		LOGP(DLNS, LOGL_ERROR, "Short FR header: %u bytes\n", msg->len);
+		*error = -EIO;
+		goto out_err;
+	}
+
+	frh = (uint8_t *)greh + sizeof(*greh);
+	if (frh[0] & 0x01) {
+		LOGP(DLNS, LOGL_NOTICE, "Unsupported single-byte FR address\n");
+		*error = -EIO;
+		goto out_err;
+	}
+	*dlci = ((frh[0] & 0xfc) << 2);
+	if ((frh[1] & 0x0f) != 0x01) {
+		LOGP(DLNS, LOGL_NOTICE, "Unknown second FR octet 0x%02x\n",
+			frh[1]);
+		*error = -EIO;
+		goto out_err;
+	}
+	*dlci |= (frh[1] >> 4);
+
+	msg->l2h = frh+2;
+
+	return msg;
+
+out_err:
+	msgb_free(msg);
+	return NULL;
+}
+
+static int gprs_ns2_find_vc_by_dlci(struct gprs_ns2_vc_bind *bind,
+				    uint16_t dlci,
+				    struct gprs_ns2_vc **result)
+{
+	struct gprs_ns2_vc *nsvc;
+	struct priv_vc *vcpriv;
+
+	if (!result)
+		return -EINVAL;
+
+	llist_for_each_entry(nsvc, &bind->nsvc, blist) {
+		vcpriv = nsvc->priv;
+		if (vcpriv->dlci != dlci) {
+			*result = nsvc;
+			return 0;
+		}
+	}
+
+	return 1;
+}
+
+static int handle_nsfrgre_read(struct osmo_fd *bfd)
+{
+	int rc;
+	struct osmo_sockaddr saddr;
+	struct gprs_ns2_vc *nsvc;
+	struct gprs_ns2_vc_bind *bind = bfd->data;
+	struct msgb *msg;
+	struct msgb *reject;
+	uint16_t dlci;
+
+	msg = read_nsfrgre_msg(bfd, &rc, &saddr, &dlci);
+	if (!msg)
+		return rc;
+
+	if (dlci == 0 || dlci == 1023) {
+		LOGP(DLNS, LOGL_INFO, "Received FR on LMI DLCI %u - ignoring\n",
+			dlci);
+		rc = 0;
+		goto out;
+	}
+
+	rc = gprs_ns2_find_vc_by_dlci(bind, dlci, &nsvc);
+	if (rc) {
+		/* VC not found */
+		rc = ns2_create_vc(bind, msg, "newconnection", &reject, &nsvc);
+		switch (rc) {
+		case GPRS_NS2_CS_FOUND:
+			rc = ns2_recv_vc(bind->nsi, nsvc, msg);
+			break;
+		case GPRS_NS2_CS_ERROR:
+		case GPRS_NS2_CS_SKIPPED:
+			rc = 0;
+			break;
+		case GPRS_NS2_CS_REJECTED:
+			/* nsip_sendmsg will free reject */
+			frgre_sendmsg(bind, reject, &saddr);
+			return 0;
+		case GPRS_NS2_CS_CREATED:
+			frgre_alloc_vc(bind, nsvc, &saddr, dlci);
+			gprs_ns2_vc_fsm_start(nsvc);
+			rc = ns2_recv_vc(bind->nsi, nsvc, msg);
+			break;
+		}
+	} else {
+		/* VC found */
+		rc = ns2_recv_vc(bind->nsi, nsvc, msg);
+	}
+out:
+	msgb_free(msg);
+
+	return rc;
+}
+
+static int handle_nsfrgre_write(struct osmo_fd *bfd)
+{
+	/* FIXME: actually send the data here instead of nsip_sendmsg() */
+	return -EIO;
+}
+
+static inline int frgre_sendmsg(struct gprs_ns2_vc_bind *bind,
+			       struct msgb *msg,
+			       struct osmo_sockaddr *dest)
+{
+	int rc;
+	struct priv_bind *priv = bind->priv;
+
+	rc = sendto(priv->fd.fd, msg->data, msg->len, 0,
+		    &dest->u.sa, sizeof(*dest));
+
+	msgb_free(msg);
+
+	return rc;
+}
+
+static int frgre_vc_sendmsg(struct gprs_ns2_vc *nsvc, struct msgb *msg)
+{
+	struct gprs_ns2_vc_bind *bind = nsvc->bind;
+	struct priv_vc *vcpriv = nsvc->priv;
+	struct priv_bind *bindpriv = bind->priv;
+
+	uint16_t dlci = osmo_htons(bindpriv->dlci);
+	uint8_t *frh;
+	struct gre_hdr *greh;
+
+	/* Prepend the FR header */
+	frh = msgb_push(msg, 2);
+	frh[0] = (dlci >> 2) & 0xfc;
+	frh[1] = ((dlci & 0xf)<<4) | 0x01;
+
+	/* Prepend the GRE header */
+	greh = (struct gre_hdr *) msgb_push(msg, sizeof(*greh));
+	greh->flags = 0;
+	greh->ptype = osmo_htons(GRE_PTYPE_FR);
+
+	return frgre_sendmsg(bind, msg, &vcpriv->remote);
+}
+
+static int frgre_fd_cb(struct osmo_fd *bfd, unsigned int what)
+{
+	int rc = 0;
+
+	if (what & OSMO_FD_READ)
+		rc = handle_nsfrgre_read(bfd);
+	if (what & OSMO_FD_WRITE)
+		rc = handle_nsfrgre_write(bfd);
+
+	return rc;
+}
+
+int gprs_ns2_is_frgre_bind(struct gprs_ns2_vc_bind *bind)
+{
+	return (bind->driver == &vc_driver_frgre);
+}
+
+int gprs_ns2_frgre_bind(struct gprs_ns2_inst *nsi,
+			struct osmo_sockaddr *local,
+			int dscp,
+			struct gprs_ns2_vc_bind **result)
+{
+	struct gprs_ns2_vc_bind *bind = talloc_zero(nsi, struct gprs_ns2_vc_bind);
+	struct priv_bind *priv;
+	int rc;
+
+	if (!bind)
+		return -ENOSPC;
+
+	if (local->u.sa.sa_family != AF_INET && local->u.sa.sa_family != AF_INET6) {
+		talloc_free(bind);
+		return -EINVAL;
+	}
+
+	bind->driver = &vc_driver_frgre;
+	bind->send_vc = frgre_vc_sendmsg;
+	bind->free_vc = free_vc;
+	bind->nsi = nsi;
+
+	priv = bind->priv = talloc_zero(bind, struct priv_bind);
+	if (!priv) {
+		talloc_free(bind);
+		return -ENOSPC;
+	}
+	priv->fd.cb = frgre_fd_cb;
+	priv->fd.data = bind;
+	priv->addr = *local;
+	INIT_LLIST_HEAD(&bind->nsvc);
+
+	llist_add(&bind->list, &nsi->binding);
+
+	rc = osmo_sock_init_osa_ofd(&priv->fd, SOCK_RAW, IPPROTO_GRE,
+				 local, NULL,
+				 OSMO_SOCK_F_BIND);
+	if (rc < 0) {
+		talloc_free(priv);
+		talloc_free(bind);
+		return rc;
+	}
+
+	if (dscp > 0) {
+		priv->dscp = dscp;
+
+		rc = setsockopt(priv->fd.fd, IPPROTO_IP, IP_TOS,
+				&dscp, sizeof(dscp));
+		if (rc < 0)
+			LOGP(DLNS, LOGL_ERROR,
+				"Failed to set the DSCP to %d with ret(%d) errno(%d)\n",
+				dscp, rc, errno);
+	}
+
+	ns2_vty_bind_apply(bind);
+
+	if (result)
+		*result = bind;
+
+	return rc;
+}
diff --git a/src/gb/gprs_ns2_internal.h b/src/gb/gprs_ns2_internal.h
new file mode 100644
index 0000000..708eb59
--- /dev/null
+++ b/src/gb/gprs_ns2_internal.h
@@ -0,0 +1,290 @@
+/*! \file gprs_ns2_internal.h */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <osmocom/gprs/protocol/gsm_08_16.h>
+#include <osmocom/gprs/gprs_ns2.h>
+
+struct osmo_fsm_inst;
+struct tlv_parsed;
+struct vty;
+
+struct gprs_ns2_vc_driver;
+struct gprs_ns2_vc_bind;
+
+
+
+#define NS_TIMERS_COUNT 8
+#define NS_TIMERS "(tns-block|tns-block-retries|tns-reset|tns-reset-retries|tns-test|tns-alive|tns-alive-retries|tsns-prov)"
+#define NS_TIMERS_HELP	\
+	"(un)blocking Timer (Tns-block) timeout\n"		\
+	"(un)blocking Timer (Tns-block) number of retries\n"	\
+	"Reset Timer (Tns-reset) timeout\n"			\
+	"Reset Timer (Tns-reset) number of retries\n"		\
+	"Test Timer (Tns-test) timeout\n"			\
+	"Alive Timer (Tns-alive) timeout\n"			\
+	"Alive Timer (Tns-alive) number of retries\n"		\
+	"SNS Provision Timer (Tsns-prov) timeout\n"
+
+/* Educated guess - LLC user payload is 1500 bytes plus possible headers */
+#define NS_ALLOC_SIZE	3072
+#define NS_ALLOC_HEADROOM 20
+
+enum ns2_timeout {
+	NS_TOUT_TNS_BLOCK,
+	NS_TOUT_TNS_BLOCK_RETRIES,
+	NS_TOUT_TNS_RESET,
+	NS_TOUT_TNS_RESET_RETRIES,
+	NS_TOUT_TNS_TEST,
+	NS_TOUT_TNS_ALIVE,
+	NS_TOUT_TNS_ALIVE_RETRIES,
+	NS_TOUT_TSNS_PROV,
+};
+
+enum nsvc_timer_mode {
+	/* standard timers */
+	NSVC_TIMER_TNS_TEST,
+	NSVC_TIMER_TNS_ALIVE,
+	NSVC_TIMER_TNS_RESET,
+	_NSVC_TIMER_NR,
+};
+
+enum ns_stat {
+	NS_STAT_ALIVE_DELAY,
+};
+
+/*! Osmocom NS link layer types */
+enum gprs_ns_ll {
+	GPRS_NS_LL_UDP,		/*!< NS/UDP/IP */
+	GPRS_NS_LL_E1,		/*!< NS/E1 */
+	GPRS_NS_LL_FR_GRE,	/*!< NS/FR/GRE/IP */
+};
+
+/*! Osmocom NS2 VC create status */
+enum gprs_ns2_cs {
+	GPRS_NS2_CS_CREATED,     /*!< A NSVC object has been created */
+	GPRS_NS2_CS_FOUND,       /*!< A NSVC object has been found */
+	GPRS_NS2_CS_REJECTED,    /*!< Rejected and answered message */
+	GPRS_NS2_CS_SKIPPED,     /*!< Skipped message */
+	GPRS_NS2_CS_ERROR,       /*!< Failed to process message */
+};
+
+
+#define NSE_S_BLOCKED	0x0001
+#define NSE_S_ALIVE	0x0002
+#define NSE_S_RESET	0x0004
+
+#define NS_DESC_B(st) ((st) & NSE_S_BLOCKED ? "BLOCKED" : "UNBLOCKED")
+#define NS_DESC_A(st) ((st) & NSE_S_ALIVE ? "ALIVE" : "DEAD")
+#define NS_DESC_R(st) ((st) & NSE_S_RESET ? "RESET" : "UNRESET")
+
+/*! An instance of the NS protocol stack */
+struct gprs_ns2_inst {
+	/*! callback to the user for incoming UNIT DATA IND */
+	osmo_prim_cb cb;
+
+	/*! callback data */
+	void *cb_data;
+
+	/*! linked lists of all NSVC binds (e.g. IPv4 bind, but could be also E1 */
+	struct llist_head binding;
+
+	/*! linked lists of all NSVC in this instance */
+	struct llist_head nse;
+
+	/*! create dynamic NSE on receiving packages */
+	bool create_nse;
+
+	uint16_t timeout[NS_TIMERS_COUNT];
+
+	/*! workaround for rate counter until rate counter accepts char str as index */
+	uint32_t rate_ctr_idx;
+};
+
+/*! Structure repesenting a NSE. The BSS/PCU will only have a single NSE, while SGSN has one for each BSS/PCU */
+struct gprs_ns2_nse {
+	uint16_t nsei;
+
+	/*! entry back to ns2_inst */
+	struct gprs_ns2_inst *nsi;
+
+	/*! llist entry for gprs_ns2_inst */
+	struct llist_head list;
+
+	/*! llist head to hold all nsvc */
+	struct llist_head nsvc;
+
+	/*! true if this NSE was created by VTY or pcu socket) */
+	bool persistent;
+
+	/*! true if this NSE has at least one alive VC */
+	bool alive;
+
+	struct osmo_fsm_inst *bss_sns_fi;
+};
+
+/*! Structure representing a single NS-VC */
+struct gprs_ns2_vc {
+	/*! list of NS-VCs within NSE */
+	struct llist_head list;
+
+	/*! list of NS-VCs within bind, bind is the owner! */
+	struct llist_head blist;
+
+	/*! pointer to NS Instance */
+	struct gprs_ns2_nse *nse;
+
+	/*! pointer to NS VL bind. bind own the memory of this instance */
+	struct gprs_ns2_vc_bind *bind;
+
+	/*! true if this NS was created by VTY or pcu socket) */
+	bool persistent;
+
+	/*! uniquely identifies NS-VC if VC contains nsvci */
+	uint16_t nsvci;
+
+	/*! signalling weight. 0 = don't use for signalling (BVCI == 0)*/
+	uint8_t sig_weight;
+
+	/*! signaling weight. 0 = don't use for user data (BVCI != 0) */
+	uint8_t data_weight;
+
+	/*! can be used by the bind/driver of the virtual circuit. e.g. ipv4/ipv6/frgre/e1 */
+	void *priv;
+
+	bool nsvci_is_valid;
+	bool sns_only;
+
+	struct rate_ctr_group *ctrg;
+	struct osmo_stat_item_group *statg;
+
+	/*! which link-layer are we based on? */
+	enum gprs_ns_ll ll;
+	enum gprs_ns2_vc_mode mode;
+
+	struct osmo_fsm_inst *fi;
+};
+
+/*! Structure repesenting a bind instance. E.g. IPv4 listen port. */
+struct gprs_ns2_vc_bind {
+	/*! list entry in nsi */
+	struct llist_head list;
+	/*! list of all VC */
+	struct llist_head nsvc;
+	/*! driver private structure */
+	void *priv;
+	/*! a pointer back to the nsi */
+	struct gprs_ns2_inst *nsi;
+	struct gprs_ns2_vc_driver *driver;
+
+	/*! if VCs use reset/block/unblock method. IP shall not use this */
+	enum gprs_ns2_vc_mode vc_mode;
+
+	/*! send a msg over a VC */
+	int (*send_vc)(struct gprs_ns2_vc *nsvc, struct msgb *msg);
+
+	/*! free the vc priv data */
+	void (*free_vc)(struct gprs_ns2_vc *nsvc);
+};
+
+
+struct gprs_ns2_vc_driver {
+	const char *name;
+	void *priv;
+	void (*free_bind)(struct gprs_ns2_vc_bind *driver);
+};
+
+enum gprs_ns2_cs ns2_create_vc(struct gprs_ns2_vc_bind *bind,
+			       struct msgb *msg,
+			       const char *logname,
+			       struct msgb **reject,
+			       struct gprs_ns2_vc **success);
+
+int ns2_recv_vc(struct gprs_ns2_inst *nsi,
+		struct gprs_ns2_vc *nsvc,
+		struct msgb *msg);
+
+struct gprs_ns2_vc *ns2_vc_alloc(struct gprs_ns2_vc_bind *bind,
+				 struct gprs_ns2_nse *nse,
+				 bool initiater);
+
+struct msgb *gprs_ns2_msgb_alloc(void);
+
+void gprs_ns2_sns_dump_vty(struct vty *vty, const struct gprs_ns2_nse *nse, bool stats);
+void ns2_prim_status_ind(struct gprs_ns2_inst *nsi,
+			 uint16_t nsei, uint16_t bvci,
+			 enum gprs_ns2_affecting_cause cause);
+void ns2_nse_notify_alive(struct gprs_ns2_vc *nsvc, bool alive);
+
+/* message */
+int gprs_ns2_validate(struct gprs_ns2_vc *nsvc,
+		      uint8_t pdu_type,
+		      struct msgb *msg,
+		      struct tlv_parsed *tp,
+		      uint8_t *cause);
+
+/* SNS messages */
+int ns2_tx_sns_ack(struct gprs_ns2_vc *nsvc, uint8_t trans_id, uint8_t *cause,
+			const struct gprs_ns_ie_ip4_elem *ip4_elems,
+			unsigned int num_ip4_elems,
+			const struct gprs_ns_ie_ip6_elem *ip6_elems,
+			unsigned int num_ip6_elems);
+int ns2_tx_sns_config(struct gprs_ns2_vc *nsvc, bool end_flag,
+			   const struct gprs_ns_ie_ip4_elem *ip4_elems,
+			   unsigned int num_ip4_elems,
+			   const struct gprs_ns_ie_ip6_elem *ip6_elems,
+			   unsigned int num_ip6_elems);
+int ns2_tx_sns_config_ack(struct gprs_ns2_vc *nsvc, uint8_t *cause);
+int ns2_tx_sns_size(struct gprs_ns2_vc *nsvc, bool reset_flag, uint16_t max_nr_nsvc,
+			 int ip4_ep_nr, int ip6_ep_nr);
+int ns2_tx_sns_size_ack(struct gprs_ns2_vc *nsvc, uint8_t *cause);
+
+/* transmit message over a VC */
+int ns2_tx_block(struct gprs_ns2_vc *nsvc, uint8_t cause);
+int ns2_tx_block_ack(struct gprs_ns2_vc *nsvc);
+
+int ns2_tx_reset(struct gprs_ns2_vc *nsvc, uint8_t cause);
+int ns2_tx_reset_ack(struct gprs_ns2_vc *nsvc);
+
+int ns2_tx_unblock(struct gprs_ns2_vc *nsvc);
+int ns2_tx_unblock_ack(struct gprs_ns2_vc *nsvc);
+
+int ns2_tx_alive(struct gprs_ns2_vc *nsvc);
+int ns2_tx_alive_ack(struct gprs_ns2_vc *nsvc);
+
+int ns2_tx_unit_data(struct gprs_ns2_vc *nsvc,
+		     uint16_t bvci, uint8_t sducontrol,
+		     struct msgb *msg);
+
+int ns2_tx_status(struct gprs_ns2_vc *nsvc, uint8_t cause,
+		       uint16_t bvci, struct msgb *orig_msg);
+
+/* driver */
+struct gprs_ns2_vc *gprs_ns2_ip_bind_connect(struct gprs_ns2_vc_bind *bind,
+					     struct gprs_ns2_nse *nse,
+					     struct osmo_sockaddr *remote);
+
+/* sns */
+int gprs_ns2_sns_rx(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp);
+struct osmo_fsm_inst *ns2_sns_bss_fsm_alloc(struct gprs_ns2_nse *nse,
+					     const char *id);
+int ns2_sns_bss_fsm_start(struct gprs_ns2_nse *nse, struct gprs_ns2_vc *nsvc,
+			  struct osmo_sockaddr *remote);
+void ns2_sns_free_nsvc(struct gprs_ns2_vc *nsvc);
+
+/* vc */
+struct osmo_fsm_inst *gprs_ns2_vc_fsm_alloc(struct gprs_ns2_vc *nsvc,
+					    const char *id, bool initiate);
+int gprs_ns2_vc_fsm_start(struct gprs_ns2_vc *nsvc);
+int gprs_ns2_vc_rx(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp);
+int gprs_ns2_vc_is_alive(struct gprs_ns2_vc *nsvc);
+int gprs_ns2_vc_is_unblocked(struct gprs_ns2_vc *nsvc);
+
+/* vty.c */
+void ns2_vty_bind_apply(struct gprs_ns2_vc_bind *bind);
+
+/* nse */
+void ns2_nse_notify_unblocked(struct gprs_ns2_vc *nsvc, bool unblocked);
diff --git a/src/gb/gprs_ns2_message.c b/src/gb/gprs_ns2_message.c
new file mode 100644
index 0000000..7742404
--- /dev/null
+++ b/src/gb/gprs_ns2_message.c
@@ -0,0 +1,709 @@
+/*! \file gprs_ns2_message.c
+ * NS-over-FR-over-GRE implementation.
+ * GPRS Networks Service (NS) messages on the Gb interface.
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ * as well as its successor 3GPP TS 48.016 */
+
+/* (C) 2020 sysmocom - s.f.m.c. GmbH
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/byteswap.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stat_item.h>
+#include <osmocom/core/stats.h>
+#include <osmocom/gsm/tlv.h>
+#include <osmocom/gprs/gprs_msgb.h>
+#include <osmocom/gprs/gprs_ns2.h>
+#include <osmocom/gprs/protocol/gsm_08_16.h>
+
+#include "gprs_ns2_internal.h"
+
+#define ERR_IF_NSVC_USES_SNS(nsvc, reason)						\
+	do {										\
+		if (!nsvc->nse->bss_sns_fi)						\
+			break;								\
+		LOGP(DLNS, LOGL_DEBUG, "NSEI=%u Rx invalid packet %s with SNS\n",	\
+				       nsvc->nse->nsei, reason);			\
+	} while (0)
+
+enum ns_ctr {
+	NS_CTR_PKTS_IN,
+	NS_CTR_PKTS_OUT,
+	NS_CTR_BYTES_IN,
+	NS_CTR_BYTES_OUT,
+	NS_CTR_BLOCKED,
+	NS_CTR_DEAD,
+	NS_CTR_REPLACED,
+	NS_CTR_NSEI_CHG,
+	NS_CTR_INV_VCI,
+	NS_CTR_INV_NSEI,
+	NS_CTR_LOST_ALIVE,
+	NS_CTR_LOST_RESET,
+};
+
+
+
+int gprs_ns2_validate_reset(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp, uint8_t *cause)
+{
+	if (!TLVP_PRESENT(tp, NS_IE_CAUSE) || !TLVP_PRESENT(tp, NS_IE_VCI) || !TLVP_PRESENT(tp, NS_IE_NSEI)) {
+		*cause = NS_CAUSE_MISSING_ESSENT_IE;
+		return -1;
+	}
+
+	return 0;
+}
+
+int gprs_ns2_validate_reset_ack(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp, uint8_t *cause)
+{
+	if (!TLVP_PRESENT(tp, NS_IE_VCI) || !TLVP_PRESENT(tp, NS_IE_NSEI)) {
+		*cause = NS_CAUSE_MISSING_ESSENT_IE;
+		return -1;
+	}
+
+	return 0;
+}
+
+int gprs_ns2_validate_block(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp, uint8_t *cause)
+{
+	if (!TLVP_PRESENT(tp, NS_IE_VCI) || !TLVP_PRESENT(tp, NS_IE_CAUSE)) {
+		*cause = NS_CAUSE_MISSING_ESSENT_IE;
+		return -1;
+	}
+
+	return 0;
+}
+
+int gprs_ns2_validate_block_ack(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp, uint8_t *cause)
+{
+	if (!TLVP_PRESENT(tp, NS_IE_VCI)) {
+		*cause = NS_CAUSE_MISSING_ESSENT_IE;
+		return -1;
+	}
+
+	return 0;
+}
+
+int gprs_ns2_validate_status(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp, uint8_t *cause)
+{
+
+	if (!TLVP_PRESENT(tp, NS_IE_CAUSE)) {
+		*cause = NS_CAUSE_MISSING_ESSENT_IE;
+		return -1;
+	}
+
+	uint8_t _cause = tlvp_val8(tp, NS_IE_VCI, 0);
+
+	switch (_cause) {
+	case NS_CAUSE_NSVC_BLOCKED:
+	case NS_CAUSE_NSVC_UNKNOWN:
+		if (!TLVP_PRESENT(tp, NS_IE_CAUSE)) {
+			*cause = NS_CAUSE_MISSING_ESSENT_IE;
+			return -1;
+		}
+		break;
+	case NS_CAUSE_SEM_INCORR_PDU:
+	case NS_CAUSE_PDU_INCOMP_PSTATE:
+	case NS_CAUSE_PROTO_ERR_UNSPEC:
+	case NS_CAUSE_INVAL_ESSENT_IE:
+	case NS_CAUSE_MISSING_ESSENT_IE:
+		if (!TLVP_PRESENT(tp, NS_IE_CAUSE)) {
+			*cause = NS_CAUSE_MISSING_ESSENT_IE;
+			return -1;
+		}
+		break;
+	case NS_CAUSE_BVCI_UNKNOWN:
+		if (!TLVP_PRESENT(tp, NS_IE_BVCI)) {
+			*cause = NS_CAUSE_MISSING_ESSENT_IE;
+			return -1;
+		}
+		break;
+	case NS_CAUSE_UNKN_IP_TEST_FAILED:
+		if (!TLVP_PRESENT (tp, NS_IE_IPv4_LIST) && !TLVP_PRESENT(tp, NS_IE_IPv6_LIST)) {
+			*cause = NS_CAUSE_MISSING_ESSENT_IE;
+			return -1;
+		}
+		break;
+	}
+
+	return 0;
+}
+
+int gprs_ns2_validate(struct gprs_ns2_vc *nsvc,
+		      uint8_t pdu_type,
+		      struct msgb *msg,
+		      struct tlv_parsed *tp,
+		      uint8_t *cause)
+{
+	switch (pdu_type) {
+	case NS_PDUT_RESET:
+		return gprs_ns2_validate_reset(nsvc, msg, tp, cause);
+	case NS_PDUT_RESET_ACK:
+		return gprs_ns2_validate_reset_ack(nsvc, msg, tp, cause);
+	case NS_PDUT_BLOCK:
+		return gprs_ns2_validate_block(nsvc, msg, tp, cause);
+	case NS_PDUT_BLOCK_ACK:
+		return gprs_ns2_validate_block_ack(nsvc, msg, tp, cause);
+	case NS_PDUT_STATUS:
+		return gprs_ns2_validate_status(nsvc, msg, tp, cause);
+
+	/* following PDUs doesn't have any payloads */
+	case NS_PDUT_ALIVE:
+	case NS_PDUT_ALIVE_ACK:
+	case NS_PDUT_UNBLOCK:
+	case NS_PDUT_UNBLOCK_ACK:
+		if (msgb_l2len(msg) != sizeof(struct gprs_ns_hdr)) {
+			*cause = NS_CAUSE_PROTO_ERR_UNSPEC;
+			return -1;
+		}
+		break;
+	}
+
+	return 0;
+}
+
+
+/* transmit functions */
+static int ns2_tx_simple(struct gprs_ns2_vc *nsvc, uint8_t pdu_type)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	struct gprs_ns_hdr *nsh;
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	if (!msg)
+		return -ENOMEM;
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = pdu_type;
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Transmit a NS-BLOCK on a given NS-VC
+ *  \param[in] vc NS-VC on which the NS-BLOCK is to be transmitted
+ *  \param[in] cause Numeric NS Cause value
+ *  \returns 0 in case of success
+ */
+int ns2_tx_block(struct gprs_ns2_vc *nsvc, uint8_t cause)
+{
+	struct msgb *msg;
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsvci = osmo_htons(nsvc->nsvci);
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	ERR_IF_NSVC_USES_SNS(nsvc, "transmit NS BLOCK");
+
+	msg = gprs_ns2_msgb_alloc();
+	if (!msg)
+		return -ENOMEM;
+
+	LOGP(DLNS, LOGL_INFO, "NSEI=%u Tx NS BLOCK (NSVCI=%u, cause=%s)\n",
+		nsvc->nse->nsei, nsvc->nsvci, gprs_ns2_cause_str(cause));
+
+	rate_ctr_inc(&nsvc->ctrg->ctr[NS_CTR_BLOCKED]);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+	nsh->pdu_type = NS_PDUT_BLOCK;
+
+	msgb_tvlv_put(msg, NS_IE_CAUSE, 1, &cause);
+	msgb_tvlv_put(msg, NS_IE_VCI, 2, (uint8_t *) &nsvci);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Transmit a NS-BLOCK-ACK on a given NS-VC
+ *  \param[in] nsvc NS-VC on which the NS-BLOCK is to be transmitted
+ *  \returns 0 in case of success
+ */
+int ns2_tx_block_ack(struct gprs_ns2_vc *nsvc)
+{
+	struct msgb *msg;
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsvci = osmo_htons(nsvc->nsvci);
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	ERR_IF_NSVC_USES_SNS(nsvc, "transmit NS BLOCK ACK");
+
+	msg = gprs_ns2_msgb_alloc();
+	if (!msg)
+		return -ENOMEM;
+
+	LOGP(DLNS, LOGL_INFO, "NSEI=%u Tx NS BLOCK ACK (NSVCI=%u)\n", nsvc->nse->nsei, nsvc->nsvci);
+
+	/* be conservative and mark it as blocked even now! */
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+	nsh->pdu_type = NS_PDUT_BLOCK_ACK;
+
+	msgb_tvlv_put(msg, NS_IE_VCI, 2, (uint8_t *) &nsvci);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Transmit a NS-RESET on a given NSVC
+ *  \param[in] nsvc NS-VC used for transmission
+ *  \paam[in] cause Numeric NS cause value
+ */
+int ns2_tx_reset(struct gprs_ns2_vc *nsvc, uint8_t cause)
+{
+	struct msgb *msg;
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsvci = osmo_htons(nsvc->nsvci);
+	uint16_t nsei = osmo_htons(nsvc->nse->nsei);
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	ERR_IF_NSVC_USES_SNS(nsvc, "transmit NS RESET");
+
+	msg = gprs_ns2_msgb_alloc();
+	if (!msg)
+		return -ENOMEM;
+
+	LOGP(DLNS, LOGL_INFO, "NSEI=%u Tx NS RESET (NSVCI=%u, cause=%s)\n",
+		nsvc->nse->nsei, nsvc->nsvci, gprs_ns2_cause_str(cause));
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+	nsh->pdu_type = NS_PDUT_RESET;
+
+	msgb_tvlv_put(msg, NS_IE_CAUSE, 1, &cause);
+	msgb_tvlv_put(msg, NS_IE_VCI, 2, (uint8_t *) &nsvci);
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *) &nsei);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/* Section 9.2.6 */
+int ns2_tx_reset_ack(struct gprs_ns2_vc *nsvc)
+{
+	struct msgb *msg;
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsvci, nsei;
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	ERR_IF_NSVC_USES_SNS(nsvc, "transmit NS RESET ACK");
+
+	msg = gprs_ns2_msgb_alloc();
+	if (!msg)
+		return -ENOMEM;
+
+	nsvci = osmo_htons(nsvc->nsvci);
+	nsei = osmo_htons(nsvc->nse->nsei);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = NS_PDUT_RESET_ACK;
+
+	LOGP(DLNS, LOGL_INFO, "NSEI=%u Tx NS RESET ACK (NSVCI=%u)\n",
+		nsvc->nse->nsei, nsvc->nsvci);
+
+	msgb_tvlv_put(msg, NS_IE_VCI, 2, (uint8_t *)&nsvci);
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *)&nsei);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Transmit a NS-UNBLOCK on a given NS-VC
+ *  \param[in] nsvc NS-VC on which the NS-UNBLOCK is to be transmitted
+ *  \returns 0 in case of success
+ */
+int ns2_tx_unblock(struct gprs_ns2_vc *nsvc)
+{
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	ERR_IF_NSVC_USES_SNS(nsvc, "transmit NS UNBLOCK");
+
+	LOGP(DLNS, LOGL_INFO, "NSEI=%u Tx NS UNBLOCK (NSVCI=%u)\n",
+		nsvc->nse->nsei, nsvc->nsvci);
+
+	return ns2_tx_simple(nsvc, NS_PDUT_UNBLOCK);
+}
+
+
+/*! Transmit a NS-UNBLOCK-ACK on a given NS-VC
+ *  \param[in] nsvc NS-VC on which the NS-UNBLOCK-ACK is to be transmitted
+ *  \returns 0 in case of success
+ */
+int ns2_tx_unblock_ack(struct gprs_ns2_vc *nsvc)
+{
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	ERR_IF_NSVC_USES_SNS(nsvc, "transmit NS UNBLOCK ACK");
+
+	LOGP(DLNS, LOGL_INFO, "NSEI=%u Tx NS UNBLOCK (NSVCI=%u)\n",
+		nsvc->nse->nsei, nsvc->nsvci);
+
+	return ns2_tx_simple(nsvc, NS_PDUT_UNBLOCK_ACK);
+}
+
+/*! Transmit a NS-ALIVE on a given NS-VC
+ *  \param[in] nsvc NS-VC on which the NS-ALIVE is to be transmitted
+ *  \returns 0 in case of success
+ */
+int ns2_tx_alive(struct gprs_ns2_vc *nsvc)
+{
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	LOGP(DLNS, LOGL_DEBUG, "NSEI=%u Tx NS ALIVE (NSVCI=%u)\n",
+		nsvc->nse->nsei, nsvc->nsvci);
+
+	return ns2_tx_simple(nsvc, NS_PDUT_ALIVE);
+}
+
+/*! Transmit a NS-ALIVE-ACK on a given NS-VC
+ *  \param[in] nsvc NS-VC on which the NS-ALIVE-ACK is to be transmitted
+ *  \returns 0 in case of success
+ */
+int ns2_tx_alive_ack(struct gprs_ns2_vc *nsvc)
+{
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	LOGP(DLNS, LOGL_DEBUG, "NSEI=%u Tx NS ALIVE_ACK (NSVCI=%u)\n",
+		nsvc->nse->nsei, nsvc->nsvci);
+
+	return ns2_tx_simple(nsvc, NS_PDUT_ALIVE_ACK);
+}
+
+int ns2_tx_unit_data(struct gprs_ns2_vc *nsvc,
+		     uint16_t bvci, uint8_t sducontrol,
+		     struct msgb *msg)
+{
+	struct gprs_ns_hdr *nsh;
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	msg->l2h = msgb_push(msg, sizeof(*nsh) + 3);
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+	if (!nsh) {
+		LOGP(DLNS, LOGL_ERROR, "Not enough headroom for NS header\n");
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	nsh->pdu_type = NS_PDUT_UNITDATA;
+	nsh->data[0] = sducontrol;
+	nsh->data[1] = bvci >> 8;
+	nsh->data[2] = bvci & 0xff;
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Transmit a NS-STATUS on a given NSVC
+ *  \param[in] nsvc NS-VC to be used for transmission
+ *  \param[in] cause Numeric NS cause value
+ *  \param[in] bvci BVCI to be reset within NSVC
+ *  \param[in] orig_msg message causing the STATUS */
+int ns2_tx_status(struct gprs_ns2_vc *nsvc, uint8_t cause,
+		       uint16_t bvci, struct msgb *orig_msg)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsvci = osmo_htons(nsvc->nsvci);
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+
+	bvci = osmo_htons(bvci);
+
+	if (!msg)
+		return -ENOMEM;
+
+	LOGP(DLNS, LOGL_NOTICE, "NSEI=%u Tx NS STATUS (NSVCI=%u, cause=%s)\n",
+		nsvc->nse->nsei, nsvc->nsvci, gprs_ns2_cause_str(cause));
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+	nsh->pdu_type = NS_PDUT_STATUS;
+
+	msgb_tvlv_put(msg, NS_IE_CAUSE, 1, &cause);
+
+	/* Section 9.2.7.1: Static conditions for NS-VCI */
+	if (cause == NS_CAUSE_NSVC_BLOCKED ||
+	    cause == NS_CAUSE_NSVC_UNKNOWN)
+		msgb_tvlv_put(msg, NS_IE_VCI, 2, (uint8_t *)&nsvci);
+
+	/* Section 9.2.7.2: Static conditions for NS PDU */
+	switch (cause) {
+	case NS_CAUSE_SEM_INCORR_PDU:
+	case NS_CAUSE_PDU_INCOMP_PSTATE:
+	case NS_CAUSE_PROTO_ERR_UNSPEC:
+	case NS_CAUSE_INVAL_ESSENT_IE:
+	case NS_CAUSE_MISSING_ESSENT_IE:
+		msgb_tvlv_put(msg, NS_IE_PDU, msgb_l2len(orig_msg),
+			      orig_msg->l2h);
+		break;
+	default:
+		break;
+	}
+
+	/* Section 9.2.7.3: Static conditions for BVCI */
+	if (cause == NS_CAUSE_BVCI_UNKNOWN)
+		msgb_tvlv_put(msg, NS_IE_VCI, 2, (uint8_t *)&bvci);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+
+/*! Encode + Transmit a SNS-ACK as per Section 9.3.1.
+ *  \param[in] nsvc NS-VC through which to transmit the ACK
+ *  \param[in] trans_id Transaction ID which to acknowledge
+ *  \param[in] cause Pointer to cause value (NULL if no cause to be sent)
+ *  \param[in] ip4_elems Array of IPv4 Elements
+ *  \param[in] num_ip4_elems number of ip4_elems
+ *  \returns 0 on success; negative in case of error */
+int ns2_tx_sns_ack(struct gprs_ns2_vc *nsvc, uint8_t trans_id, uint8_t *cause,
+			const struct gprs_ns_ie_ip4_elem *ip4_elems,
+			unsigned int num_ip4_elems,
+			const struct gprs_ns_ie_ip6_elem *ip6_elems,
+			unsigned int num_ip6_elems)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsei;
+
+	if (!nsvc)
+		return -1;
+
+	msg = gprs_ns2_msgb_alloc();
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	if (!msg)
+		return -ENOMEM;
+
+	if (!nsvc->nse->bss_sns_fi) {
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Cannot transmit SNS on NSVC without SNS active\n",
+		     nsvc->nse->nsei);
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	nsei = osmo_htons(nsvc->nse->nsei);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = SNS_PDUT_ACK;
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *)&nsei);
+	msgb_v_put(msg, trans_id);
+	if (cause)
+		msgb_tvlv_put(msg, NS_IE_CAUSE, 1, cause);
+	if (ip4_elems) {
+		/* List of IP4 Elements 10.3.2c */
+		msgb_tvlv_put(msg, NS_IE_IPv4_LIST,
+			      num_ip4_elems*sizeof(struct gprs_ns_ie_ip4_elem),
+			      (const uint8_t *)ip4_elems);
+	}
+	if (ip6_elems) {
+		/* List of IP6 elements 10.3.2d */
+		msgb_tvlv_put(msg, NS_IE_IPv6_LIST,
+			      num_ip6_elems*sizeof(struct gprs_ns_ie_ip6_elem),
+			      (const uint8_t *)ip6_elems);
+	}
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Encode + Transmit a SNS-CONFIG as per Section 9.3.4.
+ *  \param[in] nsvc NS-VC through which to transmit the SNS-CONFIG
+ *  \param[in] end_flag Whether or not this is the last SNS-CONFIG
+ *  \param[in] ip4_elems Array of IPv4 Elements
+ *  \param[in] num_ip4_elems number of ip4_elems
+ *  \returns 0 on success; negative in case of error */
+int ns2_tx_sns_config(struct gprs_ns2_vc *nsvc, bool end_flag,
+			   const struct gprs_ns_ie_ip4_elem *ip4_elems,
+			   unsigned int num_ip4_elems,
+			   const struct gprs_ns_ie_ip6_elem *ip6_elems,
+			   unsigned int num_ip6_elems)
+{
+	struct msgb *msg;
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsei;
+
+	if (!nsvc)
+		return -1;
+
+	msg = gprs_ns2_msgb_alloc();
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	if (!msg)
+		return -ENOMEM;
+
+	if (!nsvc->nse->bss_sns_fi) {
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Cannot transmit SNS on NSVC without SNS active\n",
+		     nsvc->nse->nsei);
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	nsei = osmo_htons(nsvc->nse->nsei);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = SNS_PDUT_CONFIG;
+
+	msgb_v_put(msg, end_flag ? 0x01 : 0x00);
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *)&nsei);
+
+	/* List of IP4 Elements 10.3.2c */
+	if (ip4_elems) {
+		msgb_tvlv_put(msg, NS_IE_IPv4_LIST, num_ip4_elems*sizeof(struct gprs_ns_ie_ip4_elem),
+			      (const uint8_t *)ip4_elems);
+	} else if (ip6_elems) {
+		/* List of IP6 elements 10.3.2d */
+		msgb_tvlv_put(msg, NS_IE_IPv6_LIST, num_ip6_elems*sizeof(struct gprs_ns_ie_ip6_elem),
+			      (const uint8_t *)ip6_elems);
+	}
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Encode + Transmit a SNS-CONFIG-ACK as per Section 9.3.5.
+ *  \param[in] nsvc NS-VC through which to transmit the SNS-CONFIG-ACK
+ *  \param[in] cause Pointer to cause value (NULL if no cause to be sent)
+ *  \returns 0 on success; negative in case of error */
+int ns2_tx_sns_config_ack(struct gprs_ns2_vc *nsvc, uint8_t *cause)
+{
+	struct msgb *msg;
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsei;
+
+	if (!nsvc)
+		return -1;
+
+	msg = gprs_ns2_msgb_alloc();
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	if (!msg)
+		return -ENOMEM;
+
+	if (!nsvc->nse->bss_sns_fi) {
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Cannot transmit SNS on NSVC without SNS active\n",
+		     nsvc->nse->nsei);
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	nsei = osmo_htons(nsvc->nse->nsei);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = SNS_PDUT_CONFIG_ACK;
+
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *)&nsei);
+	if (cause)
+		msgb_tvlv_put(msg, NS_IE_CAUSE, 1, cause);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+
+/*! Encode + transmit a SNS-SIZE as per Section 9.3.7.
+ *  \param[in] nsvc NS-VC through which to transmit the SNS-SIZE
+ *  \param[in] reset_flag Whether or not to add a RESET flag
+ *  \param[in] max_nr_nsvc Maximum number of NS-VCs
+ *  \param[in] ip4_ep_nr Number of IPv4 endpoints (< 0 will omit the TLV)
+ *  \param[in] ip6_ep_nr Number of IPv6 endpoints (< 0 will omit the TLV)
+ *  \returns 0 on success; negative in case of error */
+int ns2_tx_sns_size(struct gprs_ns2_vc *nsvc, bool reset_flag, uint16_t max_nr_nsvc,
+			 int ip4_ep_nr, int ip6_ep_nr)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsei;
+
+	if (!nsvc)
+		return -1;
+
+	msg = gprs_ns2_msgb_alloc();
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	if (!msg)
+		return -ENOMEM;
+
+	if (!nsvc->nse->bss_sns_fi) {
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Cannot transmit SNS on NSVC without SNS active\n",
+		     nsvc->nse->nsei);
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	nsei = osmo_htons(nsvc->nse->nsei);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = SNS_PDUT_SIZE;
+
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *)&nsei);
+	msgb_tv_put(msg, NS_IE_RESET_FLAG, reset_flag ? 0x01 : 0x00);
+	msgb_tv16_put(msg, NS_IE_MAX_NR_NSVC, max_nr_nsvc);
+	if (ip4_ep_nr >= 0)
+		msgb_tv16_put(msg, NS_IE_IPv4_EP_NR, ip4_ep_nr);
+	if (ip6_ep_nr >= 0)
+		msgb_tv16_put(msg, NS_IE_IPv6_EP_NR, ip6_ep_nr);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+/*! Encode + Transmit a SNS-SIZE-ACK as per Section 9.3.8.
+ *  \param[in] nsvc NS-VC through which to transmit the SNS-SIZE-ACK
+ *  \param[in] cause Pointer to cause value (NULL if no cause to be sent)
+ *  \returns 0 on success; negative in case of error */
+int ns2_tx_sns_size_ack(struct gprs_ns2_vc *nsvc, uint8_t *cause)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	struct gprs_ns_hdr *nsh;
+	uint16_t nsei;
+
+	log_set_context(LOG_CTX_GB_NSVC, nsvc);
+	if (!msg)
+		return -ENOMEM;
+
+	if (!nsvc->nse->bss_sns_fi) {
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Cannot transmit SNS on NSVC without SNS active\n",
+		     nsvc->nse->nsei);
+		msgb_free(msg);
+		return -EIO;
+	}
+
+	nsei = osmo_htons(nsvc->nse->nsei);
+
+	msg->l2h = msgb_put(msg, sizeof(*nsh));
+	nsh = (struct gprs_ns_hdr *) msg->l2h;
+
+	nsh->pdu_type = SNS_PDUT_SIZE_ACK;
+
+	msgb_tvlv_put(msg, NS_IE_NSEI, 2, (uint8_t *)&nsei);
+	if (cause)
+		msgb_tvlv_put(msg, NS_IE_CAUSE, 1, cause);
+
+	return nsvc->bind->send_vc(nsvc, msg);
+}
+
+
diff --git a/src/gb/gprs_ns2_sns.c b/src/gb/gprs_ns2_sns.c
new file mode 100644
index 0000000..49495fe
--- /dev/null
+++ b/src/gb/gprs_ns2_sns.c
@@ -0,0 +1,1454 @@
+/*! \file gprs_ns2_sns.c
+ * NS Sub-Network Service Protocol implementation
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ * as well as its successor 3GPP TS 48.016 */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/* The BSS NSE only has one SGSN IP address configured, and it will use the SNS procedures
+ * to communicated its local IPs/ports as well as all the SGSN side IPs/ports and
+ * associated weights. The BSS then uses this to establish a full mesh
+ * of NSVCs between all BSS-side IPs/ports and SGSN-side IPs/ports.
+ *
+ * Known limitation/expectation/bugs:
+ * - No concurrent dual stack. It supports either IPv4 or IPv6, but not both at the same time.
+ * - SNS Add/Change/Delete: Doesn't answer on the same NSVC as received SNS ADD/CHANGE/DELETE PDUs.
+ * - SNS Add/Change/Delete: Doesn't communicated the failed IPv4/IPv6 entries on the SNS_ACK.
+ */
+
+#include <errno.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdint.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/gsm/tlv.h>
+#include <osmocom/gprs/gprs_msgb.h>
+#include <osmocom/gprs/gprs_ns2.h>
+#include <osmocom/gprs/protocol/gsm_08_16.h>
+
+#include "gprs_ns2_internal.h"
+
+#define S(x)	(1 << (x))
+
+enum ns2_sns_type {
+	IPv4,
+	IPv6,
+};
+
+enum gprs_sns_bss_state {
+	GPRS_SNS_ST_UNCONFIGURED,
+	GPRS_SNS_ST_SIZE,		/*!< SNS-SIZE procedure ongoing */
+	GPRS_SNS_ST_CONFIG_BSS,		/*!< SNS-CONFIG procedure (BSS->SGSN) ongoing */
+	GPRS_SNS_ST_CONFIG_SGSN,	/*!< SNS-CONFIG procedure (SGSN->BSS) ongoing */
+	GPRS_SNS_ST_CONFIGURED,
+};
+
+enum gprs_sns_event {
+	GPRS_SNS_EV_START,
+	GPRS_SNS_EV_SIZE,
+	GPRS_SNS_EV_SIZE_ACK,
+	GPRS_SNS_EV_CONFIG,
+	GPRS_SNS_EV_CONFIG_END,		/*!< SNS-CONFIG with end flag received */
+	GPRS_SNS_EV_CONFIG_ACK,
+	GPRS_SNS_EV_ADD,
+	GPRS_SNS_EV_DELETE,
+	GPRS_SNS_EV_CHANGE_WEIGHT,
+	GPRS_SNS_EV_NO_NSVC,
+};
+
+static const struct value_string gprs_sns_event_names[] = {
+	{ GPRS_SNS_EV_START, 		"START" },
+	{ GPRS_SNS_EV_SIZE,		"SIZE" },
+	{ GPRS_SNS_EV_SIZE_ACK,		"SIZE_ACK" },
+	{ GPRS_SNS_EV_CONFIG,		"CONFIG" },
+	{ GPRS_SNS_EV_CONFIG_END,	"CONFIG_END" },
+	{ GPRS_SNS_EV_CONFIG_ACK,	"CONFIG_ACK" },
+	{ GPRS_SNS_EV_ADD,		"ADD" },
+	{ GPRS_SNS_EV_DELETE,		"DELETE" },
+	{ GPRS_SNS_EV_CHANGE_WEIGHT,	"CHANGE_WEIGHT" },
+	{ 0, NULL }
+};
+
+struct ns2_sns_state {
+	struct gprs_ns2_nse *nse;
+
+	enum ns2_sns_type ip;
+
+	/* initial connection. the initial connection will be terminated
+	 * in configured state or moved into NSE if valid */
+	struct osmo_sockaddr initial;
+	/* all SNS PDU will be sent over this nsvc */
+	struct gprs_ns2_vc *sns_nsvc;
+
+	/* local configuration to send to the remote end */
+	struct gprs_ns_ie_ip4_elem *ip4_local;
+	size_t num_ip4_local;
+
+	/* local configuration to send to the remote end */
+	struct gprs_ns_ie_ip6_elem *ip6_local;
+	size_t num_ip6_local;
+
+	/* local configuration about our capabilities in terms of connections to
+	 * remote (SGSN) side */
+	size_t num_max_nsvcs;
+	size_t num_max_ip4_remote;
+	size_t num_max_ip6_remote;
+
+	/* remote configuration as received */
+	struct gprs_ns_ie_ip4_elem *ip4_remote;
+	unsigned int num_ip4_remote;
+
+	/* remote configuration as received */
+	struct gprs_ns_ie_ip6_elem *ip6_remote;
+	unsigned int num_ip6_remote;
+};
+
+static inline struct gprs_ns2_nse *nse_inst_from_fi(struct osmo_fsm_inst *fi)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	return gss->nse;
+}
+
+/* helper function to compute the sum of all (data or signaling) weights */
+static int ip4_weight_sum(const struct gprs_ns_ie_ip4_elem *ip4, unsigned int num,
+			  bool data_weight)
+{
+	unsigned int i;
+	int weight_sum = 0;
+
+	for (i = 0; i < num; i++) {
+		if (data_weight)
+			weight_sum += ip4[i].data_weight;
+		else
+			weight_sum += ip4[i].sig_weight;
+	}
+	return weight_sum;
+}
+#define ip4_weight_sum_data(x,y)	ip4_weight_sum(x, y, true)
+#define ip4_weight_sum_sig(x,y)		ip4_weight_sum(x, y, false)
+
+/* helper function to compute the sum of all (data or signaling) weights */
+static int ip6_weight_sum(const struct gprs_ns_ie_ip6_elem *ip6, unsigned int num,
+			  bool data_weight)
+{
+	unsigned int i;
+	int weight_sum = 0;
+
+	for (i = 0; i < num; i++) {
+		if (data_weight)
+			weight_sum += ip6[i].data_weight;
+		else
+			weight_sum += ip6[i].sig_weight;
+	}
+	return weight_sum;
+}
+#define ip6_weight_sum_data(x,y)	ip6_weight_sum(x, y, true)
+#define ip6_weight_sum_sig(x,y)		ip6_weight_sum(x, y, false)
+
+static struct gprs_ns2_vc *nsvc_by_ip4_elem(struct gprs_ns2_nse *nse,
+					    const struct gprs_ns_ie_ip4_elem *ip4)
+{
+	struct osmo_sockaddr sa;
+	/* copy over. Both data structures use network byte order */
+	sa.u.sin.sin_addr.s_addr = ip4->ip_addr;
+	sa.u.sin.sin_port = ip4->udp_port;
+	sa.u.sin.sin_family = AF_INET;
+
+	return gprs_ns2_nsvc_by_sockaddr(nse, &sa);
+}
+
+static struct gprs_ns2_vc *nsvc_by_ip6_elem(struct gprs_ns2_nse *nse,
+					    const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	struct osmo_sockaddr sa;
+	/* copy over. Both data structures use network byte order */
+	sa.u.sin6.sin6_addr = ip6->ip_addr;
+	sa.u.sin6.sin6_port = ip6->udp_port;
+	sa.u.sin6.sin6_family = AF_INET;
+
+	return gprs_ns2_nsvc_by_sockaddr(nse, &sa);
+}
+
+/*! called when a nsvc is beeing freed */
+void ns2_sns_free_nsvc(struct gprs_ns2_vc *nsvc)
+{
+	struct gprs_ns2_nse *nse;
+	struct gprs_ns2_vc *tmp;
+	struct ns2_sns_state *gss;
+	struct osmo_fsm_inst *fi = nsvc->nse->bss_sns_fi;
+
+	if (!fi)
+		return;
+
+	gss = (struct ns2_sns_state *) fi->priv;
+	if (nsvc != gss->sns_nsvc)
+		return;
+
+	nse = nsvc->nse;
+	if (nse->alive) {
+		/* choose a different sns nsvc */
+		llist_for_each_entry(tmp, &nse->nsvc, list) {
+			if (gprs_ns2_vc_is_unblocked(tmp))
+				gss->sns_nsvc = tmp;
+		}
+	} else {
+		LOGPFSML(fi, LOGL_ERROR, "NSE %d: no remaining NSVC. Reseting SNS FSM.", nse->nsei);
+		gss->sns_nsvc = NULL;
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_NO_NSVC, NULL);
+	}
+}
+
+static void ns2_nsvc_create_ip4(struct osmo_fsm_inst *fi,
+				 struct gprs_ns2_nse *nse,
+				 const struct gprs_ns_ie_ip4_elem *ip4)
+{
+	struct gprs_ns2_inst *nsi = nse->nsi;
+	struct gprs_ns2_vc *nsvc;
+	struct gprs_ns2_vc_bind *bind;
+	struct osmo_sockaddr remote;
+	/* copy over. Both data structures use network byte order */
+	remote.u.sin.sin_family = AF_INET;
+	remote.u.sin.sin_addr.s_addr = ip4->ip_addr;
+	remote.u.sin.sin_port = ip4->udp_port;
+
+	/* for every bind, create a connection if bind type == IP */
+	llist_for_each_entry(bind, &nsi->binding, list) {
+		/* ignore failed connection */
+		nsvc = gprs_ns2_ip_connect_inactive(bind,
+					   &remote,
+					   nse, 0);
+		if (!nsvc) {
+			LOGPFSML(fi, LOGL_ERROR, "SNS-CONFIG: Failed to create NSVC\n");
+			continue;
+		}
+
+		nsvc->sig_weight = ip4->sig_weight;
+		nsvc->data_weight = ip4->data_weight;
+	}
+}
+
+static void ns2_nsvc_create_ip6(struct osmo_fsm_inst *fi,
+				 struct gprs_ns2_nse *nse,
+				 const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	struct gprs_ns2_inst *nsi = nse->nsi;
+	struct gprs_ns2_vc *nsvc;
+	struct gprs_ns2_vc_bind *bind;
+	struct osmo_sockaddr remote = {};
+	/* copy over. Both data structures use network byte order */
+	remote.u.sin6.sin6_family = AF_INET6;
+	remote.u.sin6.sin6_addr = ip6->ip_addr;
+	remote.u.sin6.sin6_port = ip6->udp_port;
+
+	/* for every bind, create a connection if bind type == IP */
+	llist_for_each_entry(bind, &nsi->binding, list) {
+		/* ignore failed connection */
+		nsvc = gprs_ns2_ip_connect_inactive(bind,
+					   &remote,
+					   nse, 0);
+		if (!nsvc) {
+			LOGPFSML(fi, LOGL_ERROR, "SNS-CONFIG: Failed to create NSVC\n");
+			continue;
+		}
+
+		nsvc->sig_weight = ip6->sig_weight;
+		nsvc->data_weight = ip6->data_weight;
+	}
+}
+
+
+static int create_missing_nsvcs(struct osmo_fsm_inst *fi)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_vc *nsvc;
+	struct gprs_ns2_vc_bind *bind;
+	struct osmo_sockaddr remote = { };
+	unsigned int i;
+
+	for (i = 0; i < gss->num_ip4_remote; i++) {
+		const struct gprs_ns_ie_ip4_elem *ip4 = &gss->ip4_remote[i];
+
+		remote.u.sin.sin_family = AF_INET;
+		remote.u.sin.sin_addr.s_addr = ip4->ip_addr;
+		remote.u.sin.sin_port = ip4->udp_port;
+
+		llist_for_each_entry(bind, &nse->nsi->binding, list) {
+			bool found = false;
+
+			llist_for_each_entry(nsvc, &nse->nsvc, list) {
+				if (nsvc->bind != bind)
+					continue;
+
+				if (!osmo_sockaddr_cmp(&remote, gprs_ns2_ip_vc_sockaddr(nsvc))) {
+					found = true;
+					break;
+				}
+			}
+
+			if (!found) {
+				nsvc = gprs_ns2_ip_connect_inactive(bind, &remote, nse, 0);
+				if (!nsvc) {
+					/* TODO: add to a list to send back a NS-STATUS */
+					continue;
+				}
+			}
+
+			/* update data / signalling weight */
+			nsvc->data_weight = ip4->data_weight;
+			nsvc->sig_weight = ip4->sig_weight;
+			nsvc->sns_only = false;
+		}
+	}
+
+	for (i = 0; i < gss->num_ip6_remote; i++) {
+		const struct gprs_ns_ie_ip6_elem *ip6 = &gss->ip6_remote[i];
+
+		remote.u.sin6.sin6_family = AF_INET6;
+		remote.u.sin6.sin6_addr = ip6->ip_addr;
+		remote.u.sin6.sin6_port = ip6->udp_port;
+
+		llist_for_each_entry(bind, &nse->nsi->binding, list) {
+			bool found = false;
+
+			llist_for_each_entry(nsvc, &nse->nsvc, list) {
+				if (nsvc->bind != bind)
+					continue;
+
+				if (!osmo_sockaddr_cmp(&remote, gprs_ns2_ip_vc_sockaddr(nsvc))) {
+					found = true;
+					break;
+				}
+			}
+
+			if (!found) {
+				nsvc = gprs_ns2_ip_connect_inactive(bind, &remote, nse, 0);
+				if (!nsvc) {
+					/* TODO: add to a list to send back a NS-STATUS */
+					continue;
+				}
+			}
+
+			/* update data / signalling weight */
+			nsvc->data_weight = ip6->data_weight;
+			nsvc->sig_weight = ip6->sig_weight;
+			nsvc->sns_only = false;
+		}
+	}
+
+
+	return 0;
+}
+
+/* Add a given remote IPv4 element to gprs_sns_state */
+static int add_remote_ip4_elem(struct ns2_sns_state *gss, const struct gprs_ns_ie_ip4_elem *ip4)
+{
+	unsigned int i;
+
+	if (gss->num_ip4_remote >= gss->num_max_ip4_remote)
+		return -NS_CAUSE_INVAL_NR_NS_VC;
+
+	/* check for duplicates */
+	for (i = 0; i < gss->num_ip4_remote; i++) {
+		if (memcmp(&gss->ip4_remote[i], ip4, sizeof(*ip4)))
+			continue;
+		/* TODO: log message duplicate */
+		/* TODO: check if this is the correct cause code */
+		return -NS_CAUSE_PROTO_ERR_UNSPEC;
+	}
+
+	gss->ip4_remote = talloc_realloc(gss, gss->ip4_remote, struct gprs_ns_ie_ip4_elem,
+					 gss->num_ip4_remote+1);
+	gss->ip4_remote[gss->num_ip4_remote] = *ip4;
+	gss->num_ip4_remote += 1;
+	return 0;
+}
+
+/* Remove a given remote IPv4 element from gprs_sns_state */
+static int remove_remote_ip4_elem(struct ns2_sns_state *gss, const struct gprs_ns_ie_ip4_elem *ip4)
+{
+	unsigned int i;
+
+	for (i = 0; i < gss->num_ip4_remote; i++) {
+		if (memcmp(&gss->ip4_remote[i], ip4, sizeof(*ip4)))
+			continue;
+		/* all array elements < i remain as they are; all > i are shifted left by one */
+		memmove(&gss->ip4_remote[i], &gss->ip4_remote[i+1], gss->num_ip4_remote-i-1);
+		gss->num_ip4_remote -= 1;
+		return 0;
+	}
+	return -1;
+}
+
+/* update the weights for specified remote IPv4 */
+static int update_remote_ip4_elem(struct ns2_sns_state *gss, const struct gprs_ns_ie_ip4_elem *ip4)
+{
+	unsigned int i;
+
+	for (i = 0; i < gss->num_ip4_remote; i++) {
+		if (gss->ip4_remote[i].ip_addr != ip4->ip_addr ||
+		    gss->ip4_remote[i].udp_port != ip4->udp_port)
+			continue;
+
+		gss->ip4_remote[i].sig_weight = ip4->sig_weight;
+		gss->ip4_remote[i].data_weight = ip4->data_weight;
+		return 0;
+	}
+	return -1;
+}
+
+/* Add a given remote IPv6 element to gprs_sns_state */
+static int add_remote_ip6_elem(struct ns2_sns_state *gss, const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	if (gss->num_ip6_remote >= gss->num_max_ip6_remote)
+		return -NS_CAUSE_INVAL_NR_NS_VC;
+
+	gss->ip6_remote = talloc_realloc(gss, gss->ip6_remote, struct gprs_ns_ie_ip6_elem,
+					 gss->num_ip6_remote+1);
+	gss->ip6_remote[gss->num_ip6_remote] = *ip6;
+	gss->num_ip6_remote += 1;
+	return 0;
+}
+
+/* Remove a given remote IPv6 element from gprs_sns_state */
+static int remove_remote_ip6_elem(struct ns2_sns_state *gss, const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	unsigned int i;
+
+	for (i = 0; i < gss->num_ip6_remote; i++) {
+		if (memcmp(&gss->ip6_remote[i], ip6, sizeof(*ip6)))
+			continue;
+		/* all array elements < i remain as they are; all > i are shifted left by one */
+		memmove(&gss->ip6_remote[i], &gss->ip6_remote[i+1], gss->num_ip6_remote-i-1);
+		gss->num_ip6_remote -= 1;
+		return 0;
+	}
+	return -1;
+}
+
+/* update the weights for specified remote IPv6 */
+static int update_remote_ip6_elem(struct ns2_sns_state *gss, const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	unsigned int i;
+
+	for (i = 0; i < gss->num_ip6_remote; i++) {
+		if (memcmp(&gss->ip6_remote[i].ip_addr, &ip6->ip_addr, sizeof(ip6->ip_addr)) ||
+		    gss->ip6_remote[i].udp_port != ip6->udp_port)
+			continue;
+		gss->ip6_remote[i].sig_weight = ip6->sig_weight;
+		gss->ip6_remote[i].data_weight = ip6->data_weight;
+		return 0;
+	}
+	return -1;
+}
+
+static int do_sns_change_weight(struct osmo_fsm_inst *fi, const struct gprs_ns_ie_ip4_elem *ip4, const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_vc *nsvc;
+	struct osmo_sockaddr sa = {};
+	struct osmo_sockaddr *remote;
+	uint8_t new_signal;
+	uint8_t new_data;
+
+	/* TODO: Upon receiving an SNS-CHANGEWEIGHT PDU, if the resulting sum of the
+	 * signalling weights of all the peer IP endpoints configured for this NSE is
+	 * equal to zero or if the resulting sum of the data weights of all the peer IP
+	 * endpoints configured for this NSE is equal to zero, the BSS/SGSN shall send an
+	 * SNS-ACK PDU with a cause code of "Invalid weights". */
+
+	if (ip4) {
+		if (update_remote_ip4_elem(gss, ip4))
+			return -NS_CAUSE_UNKN_IP_EP;
+
+		/* copy over. Both data structures use network byte order */
+		sa.u.sin.sin_addr.s_addr = ip4->ip_addr;
+		sa.u.sin.sin_port = ip4->udp_port;
+		sa.u.sin.sin_family = AF_INET;
+		new_signal = ip4->sig_weight;
+		new_data = ip4->data_weight;
+	} else if (ip6) {
+		if (update_remote_ip6_elem(gss, ip6))
+			return -NS_CAUSE_UNKN_IP_EP;
+
+		/* copy over. Both data structures use network byte order */
+		sa.u.sin6.sin6_addr = ip6->ip_addr;
+		sa.u.sin6.sin6_port = ip6->udp_port;
+		sa.u.sin6.sin6_family = AF_INET6;
+		new_signal = ip6->sig_weight;
+		new_data = ip6->data_weight;
+	} else {
+		OSMO_ASSERT(false);
+	}
+
+	llist_for_each_entry(nsvc, &nse->nsvc, list) {
+		remote = gprs_ns2_ip_vc_sockaddr(nsvc);
+		/* all nsvc in NSE should be IP/UDP nsvc */
+		OSMO_ASSERT(remote);
+
+		if (osmo_sockaddr_cmp(&sa, remote))
+			continue;
+
+		LOGPFSML(fi, LOGL_INFO, "CHANGE-WEIGHT NS-VC %s data_weight %u->%u, sig_weight %u->%u\n",
+			 gprs_ns2_ll_str(nsvc), nsvc->data_weight, new_data,
+			 nsvc->sig_weight, new_signal);
+
+		nsvc->data_weight = new_data;
+		nsvc->sig_weight = new_signal;
+	}
+
+	return 0;
+}
+
+static int do_sns_delete(struct osmo_fsm_inst *fi,
+			 const struct gprs_ns_ie_ip4_elem *ip4,
+			 const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_vc *nsvc, *tmp;
+	struct osmo_sockaddr *remote;
+	struct osmo_sockaddr sa = {};
+
+	if (ip4) {
+		if (remove_remote_ip4_elem(gss, ip4) < 0)
+			return -NS_CAUSE_UNKN_IP_EP;
+		/* copy over. Both data structures use network byte order */
+		sa.u.sin.sin_addr.s_addr = ip4->ip_addr;
+		sa.u.sin.sin_port = ip4->udp_port;
+		sa.u.sin.sin_family = AF_INET;
+	} else if (ip6) {
+		if (remove_remote_ip6_elem(gss, ip6))
+			return -NS_CAUSE_UNKN_IP_EP;
+
+		/* copy over. Both data structures use network byte order */
+		sa.u.sin6.sin6_addr = ip6->ip_addr;
+		sa.u.sin6.sin6_port = ip6->udp_port;
+		sa.u.sin6.sin6_family = AF_INET6;
+	} else {
+		OSMO_ASSERT(false);
+	}
+
+	llist_for_each_entry_safe(nsvc, tmp, &nse->nsvc, list) {
+		remote = gprs_ns2_ip_vc_sockaddr(nsvc);
+		/* all nsvc in NSE should be IP/UDP nsvc */
+		OSMO_ASSERT(remote);
+		if (osmo_sockaddr_cmp(&sa, remote))
+			continue;
+
+		LOGPFSML(fi, LOGL_INFO, "DELETE NS-VC %s\n", gprs_ns2_ll_str(nsvc));
+		gprs_ns2_free_nsvc(nsvc);
+	}
+
+	return 0;
+}
+
+static int do_sns_add(struct osmo_fsm_inst *fi,
+		      const struct gprs_ns_ie_ip4_elem *ip4,
+		      const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_vc *nsvc;
+	int rc = 0;
+
+	/* Upon receiving an SNS-ADD PDU, if the consequent number of IPv4 endpoints
+	 * exceeds the number of IPv4 endpoints supported by the NSE, the NSE shall send
+	 * an SNS-ACK PDU with a cause code set to "Invalid number of IP4 Endpoints". */
+	switch (gss->ip) {
+	case IPv4:
+		rc = add_remote_ip4_elem(gss, ip4);
+		break;
+	case IPv6:
+		rc = add_remote_ip6_elem(gss, ip6);
+		break;
+	default:
+		/* the gss->ip is initialized with the bss */
+		OSMO_ASSERT(false);
+	}
+
+	if (rc)
+		return rc;
+
+	/* Upon receiving an SNS-ADD PDU containing an already configured IP endpoint the
+	 * NSE shall send an SNS-ACK PDU with the cause code "Protocol error -
+	 * unspecified" */
+	switch (gss->ip) {
+	case IPv4:
+		nsvc = nsvc_by_ip4_elem(nse, ip4);
+		if (nsvc) {
+			/* the nsvc should be already in sync with the ip4 / ip6 elements */
+			return -NS_CAUSE_PROTO_ERR_UNSPEC;
+		}
+
+		/* TODO: failure case */
+		ns2_nsvc_create_ip4(fi, nse, ip4);
+		break;
+	case IPv6:
+		nsvc = nsvc_by_ip6_elem(nse, ip6);
+		if (nsvc) {
+			/* the nsvc should be already in sync with the ip4 / ip6 elements */
+			return -NS_CAUSE_PROTO_ERR_UNSPEC;
+		}
+
+		/* TODO: failure case */
+		ns2_nsvc_create_ip6(fi, nse, ip6);
+		break;
+	}
+
+	gprs_ns2_start_alive_all_nsvcs(nse);
+
+	return 0;
+}
+
+
+static void ns2_sns_st_unconfigured(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_inst *nsi = nse->nsi;
+
+	switch (event) {
+	case GPRS_SNS_EV_START:
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_SIZE, nsi->timeout[NS_TOUT_TSNS_PROV], 1);
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+}
+
+static void ns2_sns_st_size(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_inst *nsi = nse->nsi;
+	struct tlv_parsed *tp = NULL;
+
+	switch (event) {
+	case GPRS_SNS_EV_SIZE_ACK:
+		tp = data;
+		if (TLVP_VAL_MINLEN(tp, NS_IE_CAUSE, 1)) {
+			LOGPFSML(fi, LOGL_ERROR, "SNS-SIZE-ACK with cause %s\n",
+				 gprs_ns2_cause_str(*TLVP_VAL(tp, NS_IE_CAUSE)));
+			/* TODO: What to do? */
+		} else {
+			osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_CONFIG_BSS,
+						nsi->timeout[NS_TOUT_TSNS_PROV], 2);
+		}
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+}
+
+static void ns2_sns_st_size_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+
+	if (old_state != GPRS_SNS_ST_UNCONFIGURED)
+		ns2_prim_status_ind(gss->nse->nsi, gss->nse->nsei, 0, NS_AFF_CAUSE_SNS_FAILURE);
+
+	if (gss->num_max_ip4_remote > 0)
+		ns2_tx_sns_size(gss->sns_nsvc, true, gss->num_max_nsvcs, gss->num_max_ip4_remote, -1);
+	else
+		ns2_tx_sns_size(gss->sns_nsvc, true, gss->num_max_nsvcs, -1, gss->num_max_ip6_remote);
+
+}
+
+static void ns2_sns_st_config_bss(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct tlv_parsed *tp = NULL;
+
+	switch (event) {
+	case GPRS_SNS_EV_CONFIG_ACK:
+		tp = (struct tlv_parsed *) data;
+		if (TLVP_VAL_MINLEN(tp, NS_IE_CAUSE, 1)) {
+			LOGPFSML(fi, LOGL_ERROR, "SNS-CONFIG-ACK with cause %s\n",
+							 gprs_ns2_cause_str(*TLVP_VAL(tp, NS_IE_CAUSE)));
+			/* TODO: What to do? */
+		} else {
+			osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_CONFIG_SGSN, 0, 0);
+		}
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+}
+
+static void ns2_sns_st_config_bss_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	/* Transmit SNS-CONFIG */
+	/* TODO: ipv6 */
+	switch (gss->ip) {
+	case IPv4:
+		ns2_tx_sns_config(gss->sns_nsvc, true,
+				       gss->ip4_local, gss->num_ip4_local,
+				       NULL, 0);
+		break;
+	case IPv6:
+		ns2_tx_sns_config(gss->sns_nsvc, true,
+				       NULL, 0,
+				       gss->ip6_local, gss->num_ip6_local);
+		break;
+	}
+}
+
+
+static void ns_sns_st_config_sgsn_ip4(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	const struct gprs_ns_ie_ip4_elem *v4_list;
+	unsigned int num_v4;
+	struct tlv_parsed *tp = NULL;
+
+	uint8_t cause;
+
+	tp = (struct tlv_parsed *) data;
+
+	if (!TLVP_PRESENT(tp, NS_IE_IPv4_LIST)) {
+		cause = NS_CAUSE_INVAL_NR_IPv4_EP;
+		ns2_tx_sns_config_ack(gss->sns_nsvc, &cause);
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_UNCONFIGURED, 0, 0);
+		return;
+	}
+	v4_list = (const struct gprs_ns_ie_ip4_elem *) TLVP_VAL(tp, NS_IE_IPv4_LIST);
+	num_v4 = TLVP_LEN(tp, NS_IE_IPv4_LIST) / sizeof(*v4_list);
+	/* realloc to the new size */
+	gss->ip4_remote = talloc_realloc(gss, gss->ip4_remote,
+					 struct gprs_ns_ie_ip4_elem,
+					 gss->num_ip4_remote+num_v4);
+	/* append the new entries to the end of the list */
+	memcpy(&gss->ip4_remote[gss->num_ip4_remote], v4_list, num_v4*sizeof(*v4_list));
+	gss->num_ip4_remote += num_v4;
+
+	LOGPFSML(fi, LOGL_INFO, "Rx SNS-CONFIG: Remote IPv4 list now %u entries\n",
+					 gss->num_ip4_remote);
+	if (event == GPRS_SNS_EV_CONFIG_END) {
+		/* check if sum of data / sig weights == 0 */
+		if (ip4_weight_sum_data(gss->ip4_remote, gss->num_ip4_remote) == 0 ||
+				ip4_weight_sum_sig(gss->ip4_remote, gss->num_ip4_remote) == 0) {
+			cause = NS_CAUSE_INVAL_WEIGH;
+			ns2_tx_sns_config_ack(gss->sns_nsvc, &cause);
+			osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_UNCONFIGURED, 0, 0);
+			return;
+		}
+		create_missing_nsvcs(fi);
+		ns2_tx_sns_config_ack(gss->sns_nsvc, NULL);
+		/* start the test procedure on ALL NSVCs! */
+		gprs_ns2_start_alive_all_nsvcs(nse);
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_CONFIGURED, 0, 0);
+	} else {
+		/* just send CONFIG-ACK */
+		ns2_tx_sns_config_ack(gss->sns_nsvc, NULL);
+	}
+}
+
+static void ns_sns_st_config_sgsn_ip6(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	const struct gprs_ns_ie_ip6_elem *v6_list;
+	unsigned int num_v6;
+	struct tlv_parsed *tp = NULL;
+
+	uint8_t cause;
+
+	tp = (struct tlv_parsed *) data;
+
+	if (!TLVP_PRESENT(tp, NS_IE_IPv6_LIST)) {
+		cause = NS_CAUSE_INVAL_NR_IPv6_EP;
+		ns2_tx_sns_config_ack(gss->sns_nsvc, &cause);
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_UNCONFIGURED, 0, 0);
+		return;
+	}
+	v6_list = (const struct gprs_ns_ie_ip6_elem *) TLVP_VAL(tp, NS_IE_IPv6_LIST);
+	num_v6 = TLVP_LEN(tp, NS_IE_IPv6_LIST) / sizeof(*v6_list);
+	/* realloc to the new size */
+	gss->ip6_remote = talloc_realloc(gss, gss->ip6_remote,
+					 struct gprs_ns_ie_ip6_elem,
+					 gss->num_ip6_remote+num_v6);
+	/* append the new entries to the end of the list */
+	memcpy(&gss->ip6_remote[gss->num_ip6_remote], v6_list, num_v6*sizeof(*v6_list));
+	gss->num_ip6_remote += num_v6;
+
+	LOGPFSML(fi, LOGL_INFO, "Rx SNS-CONFIG: Remote IPv6 list now %u entries\n",
+					 gss->num_ip6_remote);
+	if (event == GPRS_SNS_EV_CONFIG_END) {
+		/* check if sum of data / sig weights == 0 */
+		if (ip6_weight_sum_data(gss->ip6_remote, gss->num_ip6_remote) == 0 ||
+				ip6_weight_sum_sig(gss->ip6_remote, gss->num_ip6_remote) == 0) {
+			cause = NS_CAUSE_INVAL_WEIGH;
+			ns2_tx_sns_config_ack(gss->sns_nsvc, &cause);
+			osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_UNCONFIGURED, 0, 0);
+			return;
+		}
+		create_missing_nsvcs(fi);
+		ns2_tx_sns_config_ack(gss->sns_nsvc, NULL);
+		/* start the test procedure on ALL NSVCs! */
+		gprs_ns2_start_alive_all_nsvcs(nse);
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_CONFIGURED, 0, 0);
+	} else {
+		/* just send CONFIG-ACK */
+		ns2_tx_sns_config_ack(gss->sns_nsvc, NULL);
+	}
+}
+
+static void ns2_sns_st_config_sgsn(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+
+	switch (event) {
+	case GPRS_SNS_EV_CONFIG_END:
+	case GPRS_SNS_EV_CONFIG:
+
+#if 0		/* part of incoming SNS-SIZE (doesn't happen on BSS side */
+		if (TLVP_PRESENT(tp, NS_IE_RESET_FLAG)) {
+			/* reset all existing config */
+			if (gss->ip4_remote)
+				talloc_free(gss->ip4_remote);
+			gss->num_ip4_remote = 0;
+		}
+#endif
+		/* TODO: reject IPv6 elements on IPv4 mode and vice versa */
+		switch (gss->ip) {
+		case IPv4:
+			ns_sns_st_config_sgsn_ip4(fi, event, data);
+			break;
+		case IPv6:
+			ns_sns_st_config_sgsn_ip6(fi, event, data);
+			break;
+		default:
+			OSMO_ASSERT(0);
+		}
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+}
+
+/* called when receiving GPRS_SNS_EV_ADD in state configure */
+static void ns2_sns_st_configured_add(struct osmo_fsm_inst *fi,
+				      struct ns2_sns_state *gss,
+				      struct tlv_parsed *tp)
+{
+	const struct gprs_ns_ie_ip4_elem *v4_list = NULL;
+	const struct gprs_ns_ie_ip6_elem *v6_list = NULL;
+	int num_v4 = 0, num_v6 = 0;
+	uint8_t trans_id, cause = 0xff;
+	int rc = 0;
+
+	/* TODO: refactor EV_ADD/CHANGE/REMOVE by
+	 * check uniqueness within the lists (no doublicate entries)
+	 * check not-known-by-us and sent back a list of unknown/known values
+	 * (abnormal behaviour according to 48.016)
+	 */
+
+	trans_id = *TLVP_VAL(tp, NS_IE_TRANS_ID);
+	if (gss->ip == IPv4) {
+		if (!TLVP_PRESENT(tp, NS_IE_IPv4_LIST)) {
+			cause = NS_CAUSE_INVAL_NR_IPv4_EP;
+			ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+			return;
+		}
+
+		v4_list = (const struct gprs_ns_ie_ip4_elem *) TLVP_VAL(tp, NS_IE_IPv4_LIST);
+		num_v4 = TLVP_LEN(tp, NS_IE_IPv4_LIST) / sizeof(*v4_list);
+		for (int i = 0; i < num_v4; i++) {
+			rc = do_sns_add(fi, &v4_list[i], NULL);
+			if (rc < 0) {
+				/* rollback/undo to restore previous state */
+				for (int j = 0; j < i; j++)
+					do_sns_delete(fi, &v4_list[j], NULL);
+				cause = -rc;
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				break;
+			}
+		}
+	} else { /* IPv6 */
+		if (!TLVP_PRESENT(tp, NS_IE_IPv6_LIST)) {
+			cause = NS_CAUSE_INVAL_NR_IPv6_EP;
+			ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+			return;
+		}
+
+		v6_list = (const struct gprs_ns_ie_ip6_elem *) TLVP_VAL(tp, NS_IE_IPv6_LIST);
+		num_v6 = TLVP_LEN(tp, NS_IE_IPv6_LIST) / sizeof(*v6_list);
+		for (int i = 0; i < num_v6; i++) {
+			rc = do_sns_add(fi, NULL, &v6_list[i]);
+			if (rc < 0) {
+				/* rollback/undo to restore previous state */
+				for (int j = 0; j < i; j++)
+					do_sns_delete(fi, NULL, &v6_list[j]);
+				cause = -rc;
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				break;
+			}
+		}
+	}
+
+	/* TODO: correct behaviour is to answer to the *same* NSVC from which the SNS_ADD was received */
+	ns2_tx_sns_ack(gss->sns_nsvc, trans_id, NULL, v4_list, num_v4, v6_list, num_v6);
+}
+
+static void ns2_sns_st_configured_delete(struct osmo_fsm_inst *fi,
+					 struct ns2_sns_state *gss,
+					 struct tlv_parsed *tp)
+{
+	const struct gprs_ns_ie_ip4_elem *v4_list = NULL;
+	const struct gprs_ns_ie_ip6_elem *v6_list = NULL;
+	int num_v4 = 0, num_v6 = 0;
+	uint8_t trans_id, cause = 0xff;
+	int rc = 0;
+
+	/* TODO: split up delete into v4 + v6
+	 * TODO: check if IPv4_LIST or IP_ADDR(v4) is present on IPv6 and vice versa
+	 * TODO: check if IPv4_LIST/IPv6_LIST and IP_ADDR is present at the same time
+	 */
+	trans_id = *TLVP_VAL(tp, NS_IE_TRANS_ID);
+	if (gss->ip == IPv4) {
+		if (TLVP_PRESENT(tp, NS_IE_IPv4_LIST)) {
+			v4_list = (const struct gprs_ns_ie_ip4_elem *) TLVP_VAL(tp, NS_IE_IPv4_LIST);
+			num_v4 = TLVP_LEN(tp, NS_IE_IPv4_LIST) / sizeof(*v4_list);
+			for (int i = 0; i < num_v4; i++) {
+				rc = do_sns_delete(fi, &v4_list[i], NULL);
+				if (rc < 0) {
+					cause = -rc;
+					/* continue to delete others */
+				}
+			}
+			if (cause != 0xff) {
+				/* TODO: create list of not-deleted and return it */
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				return;
+			}
+
+		} else if (TLVP_PRESENT(tp, NS_IE_IP_ADDR) && TLVP_LEN(tp, NS_IE_IP_ADDR) == 5) {
+			/* delete all NS-VCs for given IPv4 address */
+			const uint8_t *ie = TLVP_VAL(tp, NS_IE_IP_ADDR);
+			struct gprs_ns_ie_ip4_elem *ip4_remote;
+			uint32_t ip_addr = *(uint32_t *)(ie+1);
+			if (ie[0] != 0x01) { /* Address Type != IPv4 */
+				cause = NS_CAUSE_UNKN_IP_ADDR;
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				return;
+			}
+			/* make a copy as do_sns_delete() will change the array underneath us */
+			ip4_remote = talloc_memdup(fi, gss->ip4_remote,
+						   gss->num_ip4_remote * sizeof(*v4_list));
+			for (unsigned i = 0; i < gss->num_ip4_remote; i++) {
+				if (ip4_remote[i].ip_addr == ip_addr) {
+					rc = do_sns_delete(fi, &ip4_remote[i], NULL);
+					if (rc < 0) {
+						cause = -rc;
+						/* continue to delete others */
+					}
+				}
+			}
+			talloc_free(ip4_remote);
+			if (cause != 0xff) {
+				/* TODO: create list of not-deleted and return it */
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				return;
+			}
+		} else {
+			cause = NS_CAUSE_INVAL_NR_IPv4_EP;
+			ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+			return;
+		}
+	} else { /* IPv6 */
+		if (TLVP_PRESENT(tp, NS_IE_IPv6_LIST)) {
+			v6_list = (const struct gprs_ns_ie_ip6_elem *) TLVP_VAL(tp, NS_IE_IPv6_LIST);
+			num_v6 = TLVP_LEN(tp, NS_IE_IPv6_LIST) / sizeof(*v6_list);
+			for (int i = 0; i < num_v6; i++) {
+				rc = do_sns_delete(fi, NULL, &v6_list[i]);
+				if (rc < 0) {
+					cause = -rc;
+					/* continue to delete others */
+				}
+			}
+			if (cause != 0xff) {
+				/* TODO: create list of not-deleted and return it */
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				return;
+			}
+		} else if (TLVP_PRES_LEN(tp, NS_IE_IP_ADDR, 17)) {
+			/* delete all NS-VCs for given IPv4 address */
+			const uint8_t *ie = TLVP_VAL(tp, NS_IE_IP_ADDR);
+			struct gprs_ns_ie_ip6_elem *ip6_remote;
+			struct in6_addr ip6_addr;
+			if (ie[0] != 0x02) { /* Address Type != IPv6 */
+				cause = NS_CAUSE_UNKN_IP_ADDR;
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				return;
+			}
+			memcpy(&ip6_addr, (ie+1), sizeof(struct in6_addr));
+			/* make a copy as do_sns_delete() will change the array underneath us */
+			ip6_remote = talloc_memdup(fi, gss->ip6_remote,
+						   gss->num_ip6_remote * sizeof(*v4_list));
+			for (unsigned i = 0; i < gss->num_ip6_remote; i++) {
+				if (!memcmp(&ip6_remote[i].ip_addr, &ip6_addr, sizeof(struct in6_addr))) {
+					rc = do_sns_delete(fi, NULL, &ip6_remote[i]);
+					if (rc < 0) {
+						cause = -rc;
+						/* continue to delete others */
+					}
+				}
+			}
+
+			talloc_free(ip6_remote);
+			if (cause != 0xff) {
+				/* TODO: create list of not-deleted and return it */
+				ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+				return;
+			}
+		} else {
+			cause = NS_CAUSE_INVAL_NR_IPv6_EP;
+			ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+			return;
+		}
+	}
+	ns2_tx_sns_ack(gss->sns_nsvc, trans_id, NULL, v4_list, num_v4, v6_list, num_v6);
+}
+
+static void ns2_sns_st_configured_change(struct osmo_fsm_inst *fi,
+					 struct ns2_sns_state *gss,
+					 struct tlv_parsed *tp)
+{
+	const struct gprs_ns_ie_ip4_elem *v4_list = NULL;
+	const struct gprs_ns_ie_ip6_elem *v6_list = NULL;
+	int num_v4 = 0, num_v6 = 0;
+	uint8_t trans_id, cause = 0xff;
+	int rc = 0;
+
+	trans_id = *TLVP_VAL(tp, NS_IE_TRANS_ID);
+	if (TLVP_PRESENT(tp, NS_IE_IPv4_LIST)) {
+		v4_list = (const struct gprs_ns_ie_ip4_elem *) TLVP_VAL(tp, NS_IE_IPv4_LIST);
+		num_v4 = TLVP_LEN(tp, NS_IE_IPv4_LIST) / sizeof(*v4_list);
+		for (int i = 0; i < num_v4; i++) {
+			rc = do_sns_change_weight(fi, &v4_list[i], NULL);
+			if (rc < 0) {
+				cause = -rc;
+				/* continue to others */
+			}
+		}
+		if (cause != 0xff) {
+			ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+			return;
+		}
+	} else if (TLVP_PRESENT(tp, NS_IE_IPv6_LIST)) {
+		v6_list = (const struct gprs_ns_ie_ip6_elem *) TLVP_VAL(tp, NS_IE_IPv6_LIST);
+		num_v6 = TLVP_LEN(tp, NS_IE_IPv6_LIST) / sizeof(*v6_list);
+		for (int i = 0; i < num_v6; i++) {
+			rc = do_sns_change_weight(fi, NULL, &v6_list[i]);
+			if (rc < 0) {
+				cause = -rc;
+				/* continue to others */
+			}
+		}
+		if (cause != 0xff) {
+			ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+			return;
+		}
+	} else {
+		cause = NS_CAUSE_INVAL_NR_IPv4_EP;
+		ns2_tx_sns_ack(gss->sns_nsvc, trans_id, &cause, NULL, 0, NULL, 0);
+		return;
+	}
+	ns2_tx_sns_ack(gss->sns_nsvc, trans_id, NULL, v4_list, num_v4, v6_list, num_v6);
+}
+
+static void ns2_sns_st_configured(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) fi->priv;
+	struct tlv_parsed *tp = data;
+
+	switch (event) {
+	case GPRS_SNS_EV_ADD:
+		ns2_sns_st_configured_add(fi, gss, tp);
+		break;
+	case GPRS_SNS_EV_DELETE:
+		ns2_sns_st_configured_delete(fi, gss, tp);
+		break;
+	case GPRS_SNS_EV_CHANGE_WEIGHT:
+		ns2_sns_st_configured_change(fi, gss, tp);
+		break;
+	}
+}
+
+static void ns2_sns_st_configured_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	ns2_prim_status_ind(nse->nsi, nse->nsei, 0, NS_AFF_CAUSE_SNS_CONFIGURED);
+}
+
+static const struct osmo_fsm_state ns2_sns_bss_states[] = {
+	[GPRS_SNS_ST_UNCONFIGURED] = {
+		.in_event_mask = S(GPRS_SNS_EV_START),
+		.out_state_mask = S(GPRS_SNS_ST_SIZE),
+		.name = "UNCONFIGURED",
+		.action = ns2_sns_st_unconfigured,
+	},
+	[GPRS_SNS_ST_SIZE] = {
+		.in_event_mask = S(GPRS_SNS_EV_SIZE_ACK),
+		.out_state_mask = S(GPRS_SNS_ST_UNCONFIGURED) |
+				  S(GPRS_SNS_ST_SIZE) |
+				  S(GPRS_SNS_ST_CONFIG_BSS),
+		.name = "SIZE",
+		.action = ns2_sns_st_size,
+		.onenter = ns2_sns_st_size_onenter,
+	},
+	[GPRS_SNS_ST_CONFIG_BSS] = {
+		.in_event_mask = S(GPRS_SNS_EV_CONFIG_ACK),
+		.out_state_mask = S(GPRS_SNS_ST_UNCONFIGURED) |
+				  S(GPRS_SNS_ST_CONFIG_BSS) |
+				  S(GPRS_SNS_ST_CONFIG_SGSN) |
+				  S(GPRS_SNS_ST_SIZE),
+		.name = "CONFIG_BSS",
+		.action = ns2_sns_st_config_bss,
+		.onenter = ns2_sns_st_config_bss_onenter,
+	},
+	[GPRS_SNS_ST_CONFIG_SGSN] = {
+		.in_event_mask = S(GPRS_SNS_EV_CONFIG) |
+				 S(GPRS_SNS_EV_CONFIG_END),
+		.out_state_mask = S(GPRS_SNS_ST_UNCONFIGURED) |
+				  S(GPRS_SNS_ST_CONFIG_SGSN) |
+				  S(GPRS_SNS_ST_CONFIGURED) |
+				  S(GPRS_SNS_ST_SIZE),
+		.name = "CONFIG_SGSN",
+		.action = ns2_sns_st_config_sgsn,
+	},
+	[GPRS_SNS_ST_CONFIGURED] = {
+		.in_event_mask = S(GPRS_SNS_EV_ADD) |
+				 S(GPRS_SNS_EV_DELETE) |
+				 S(GPRS_SNS_EV_CHANGE_WEIGHT),
+		.out_state_mask = S(GPRS_SNS_ST_UNCONFIGURED),
+		.name = "CONFIGURED",
+		.action = ns2_sns_st_configured,
+		.onenter = ns2_sns_st_configured_onenter,
+	},
+};
+
+static int ns2_sns_fsm_bss_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+	struct gprs_ns2_inst *nsi = nse->nsi;
+
+	switch (fi->T) {
+	case 1:
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_SIZE, nsi->timeout[NS_TOUT_TSNS_PROV], 1);
+		break;
+	case 2:
+		osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_CONFIG_BSS, nsi->timeout[NS_TOUT_TSNS_PROV], 2);
+		break;
+	}
+	return 0;
+}
+
+static void ns2_sns_st_all_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_nse *nse = nse_inst_from_fi(fi);
+
+	/* reset when receiving GPRS_SNS_EV_NO_NSVC */
+	osmo_fsm_inst_state_chg(fi, GPRS_SNS_ST_SIZE, nse->nsi->timeout[NS_TOUT_TSNS_PROV], 3);
+}
+
+static struct osmo_fsm gprs_ns2_sns_bss_fsm = {
+	.name = "GPRS-NS2-SNS-BSS",
+	.states = ns2_sns_bss_states,
+	.num_states = ARRAY_SIZE(ns2_sns_bss_states),
+	.allstate_event_mask = GPRS_SNS_EV_NO_NSVC,
+	.allstate_action = ns2_sns_st_all_action,
+	.cleanup = NULL,
+	.timer_cb = ns2_sns_fsm_bss_timer_cb,
+	/* .log_subsys = DNS, "is not constant" */
+	.event_names = gprs_sns_event_names,
+	.pre_term = NULL,
+	.log_subsys = DLNS,
+};
+
+struct osmo_fsm_inst *ns2_sns_bss_fsm_alloc(struct gprs_ns2_nse *nse,
+					    const char *id)
+{
+	struct osmo_fsm_inst *fi;
+	struct ns2_sns_state *gss;
+
+	fi = osmo_fsm_inst_alloc(&gprs_ns2_sns_bss_fsm, nse, NULL, LOGL_DEBUG, id);
+	if (!fi)
+		return fi;
+
+	gss = talloc_zero(fi, struct ns2_sns_state);
+	if (!gss)
+		goto err;
+
+	fi->priv = gss;
+	gss->nse = nse;
+
+	return fi;
+err:
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_ERROR, NULL);
+	return NULL;
+}
+
+int ns2_sns_bss_fsm_start(struct gprs_ns2_nse *nse, struct gprs_ns2_vc *nsvc, struct osmo_sockaddr *remote)
+{
+	struct osmo_fsm_inst *fi = nse->bss_sns_fi;
+	struct ns2_sns_state *gss = (struct ns2_sns_state *) nse->bss_sns_fi->priv;
+	struct gprs_ns_ie_ip4_elem *ip4_elems;
+	struct gprs_ns_ie_ip6_elem *ip6_elems;
+	struct gprs_ns2_vc_bind *bind;
+	struct gprs_ns2_inst *nsi = nse->nsi;
+	struct osmo_sockaddr *sa, local;
+	gss->ip = remote->u.sa.sa_family == AF_INET ? IPv4 : IPv6;
+
+	gss->initial = *remote;
+	gss->sns_nsvc = nsvc;
+	nsvc->sns_only = true;
+
+	int count = 0;
+	llist_for_each_entry(bind, &nsi->binding, list) {
+		if (!gprs_ns2_is_ip_bind(bind))
+			continue;
+
+		sa = gprs_ns2_ip_bind_sockaddr(bind);
+		if (!sa)
+			continue;
+
+		if (sa->u.sa.sa_family == remote->u.sa.sa_family)
+			count++;
+	}
+
+	if (count == 0) {
+		/* TODO: logging */
+		goto err;
+	}
+
+	switch (gss->ip) {
+	case IPv4:
+		ip4_elems = talloc_zero_size(fi, sizeof(struct gprs_ns_ie_ip4_elem) * count);
+		if (!ip4_elems)
+			goto err;
+
+		gss->ip4_local = ip4_elems;
+
+		llist_for_each_entry(bind, &nsi->binding, list) {
+			if (!gprs_ns2_is_ip_bind(bind))
+				continue;
+
+			sa = gprs_ns2_ip_bind_sockaddr(bind);
+			if (!sa)
+				continue;
+
+			if (sa->u.sas.ss_family != AF_INET)
+				continue;
+
+			/* check if this is an specific bind */
+			if (sa->u.sin.sin_addr.s_addr == 0) {
+				if (osmo_sockaddr_local_ip(&local, remote))
+					continue;
+
+				ip4_elems->ip_addr = local.u.sin.sin_addr.s_addr;
+			} else {
+				ip4_elems->ip_addr = sa->u.sin.sin_addr.s_addr;
+			}
+
+			ip4_elems->udp_port = sa->u.sin.sin_port;
+			ip4_elems->sig_weight = 2;
+			ip4_elems->data_weight = 1;
+			ip4_elems++;
+		}
+
+		gss->num_ip4_local = count;
+		gss->num_max_ip4_remote = 4;
+		break;
+	case IPv6:
+		/* IPv6 */
+		ip6_elems = talloc_zero_size(fi, sizeof(struct gprs_ns_ie_ip6_elem) * count);
+		if (!ip6_elems)
+			goto err;
+
+		gss->ip6_local = ip6_elems;
+
+		llist_for_each_entry(bind, &nsi->binding, list) {
+			if (!gprs_ns2_is_ip_bind(bind))
+				continue;
+
+			sa = gprs_ns2_ip_bind_sockaddr(bind);
+			if (!sa)
+				continue;
+
+			if (sa->u.sas.ss_family != AF_INET6)
+				continue;
+
+			/* check if this is an specific bind */
+			if (IN6_IS_ADDR_UNSPECIFIED(&sa->u.sin6.sin6_addr)) {
+				if (osmo_sockaddr_local_ip(&local, remote))
+					continue;
+
+				ip6_elems->ip_addr = local.u.sin6.sin6_addr;
+			} else {
+				ip6_elems->ip_addr = sa->u.sin6.sin6_addr;
+			}
+
+			ip6_elems->udp_port = sa->u.sin.sin_port;
+			ip6_elems->sig_weight = 2;
+			ip6_elems->data_weight = 1;
+
+			ip6_elems++;
+		}
+		gss->num_ip6_local = count;
+		gss->num_max_ip6_remote = 4;
+		break;
+	}
+
+	gss->num_max_nsvcs = 8;
+
+	return osmo_fsm_inst_dispatch(nse->bss_sns_fi, GPRS_SNS_EV_START, NULL);
+
+err:
+	return -1;
+}
+
+/* main entry point for receiving SNS messages from the network */
+int gprs_ns2_sns_rx(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp)
+{
+	struct gprs_ns2_nse *nse = nsvc->nse;
+	struct gprs_ns_hdr *nsh = (struct gprs_ns_hdr *) msg->l2h;
+	uint16_t nsei = nsvc->nse->nsei;
+	struct osmo_fsm_inst *fi;
+
+	if (!nse->bss_sns_fi) {
+		LOGP(DLNS, LOGL_NOTICE, "NSEI=%u Rx %s for NS Instance that has no SNS!\n",
+		     nsvc->nse->nsei, get_value_string(gprs_ns_pdu_strings, nsh->pdu_type));
+		return -EINVAL;
+	}
+
+	LOGP(DLNS, LOGL_DEBUG, "NSEI=%u Rx SNS PDU type %s\n", nsei,
+		get_value_string(gprs_ns_pdu_strings, nsh->pdu_type));
+
+	/* FIXME: how to resolve SNS FSM Instance by NSEI (SGSN)? */
+	fi = nse->bss_sns_fi;
+
+	switch (nsh->pdu_type) {
+	case SNS_PDUT_SIZE:
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_SIZE, tp);
+		break;
+	case SNS_PDUT_SIZE_ACK:
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_SIZE_ACK, tp);
+		break;
+	case SNS_PDUT_CONFIG:
+		if (nsh->data[0] & 0x01)
+			osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_CONFIG_END, tp);
+		else
+			osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_CONFIG, tp);
+		break;
+	case SNS_PDUT_CONFIG_ACK:
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_CONFIG_ACK, tp);
+		break;
+	case SNS_PDUT_ADD:
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_ADD, tp);
+		break;
+	case SNS_PDUT_DELETE:
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_DELETE, tp);
+		break;
+	case SNS_PDUT_CHANGE_WEIGHT:
+		osmo_fsm_inst_dispatch(fi, GPRS_SNS_EV_CHANGE_WEIGHT, tp);
+		break;
+	case SNS_PDUT_ACK:
+		LOGP(DLNS, LOGL_NOTICE, "NSEI=%u Rx unsupported SNS PDU type %s\n", nsei,
+			get_value_string(gprs_ns_pdu_strings, nsh->pdu_type));
+		break;
+	default:
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Rx unknown SNS PDU type %s\n", nsei,
+			get_value_string(gprs_ns_pdu_strings, nsh->pdu_type));
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/misc.h>
+
+static void vty_dump_sns_ip4(struct vty *vty, const struct gprs_ns_ie_ip4_elem *ip4)
+{
+	struct in_addr in = { .s_addr = ip4->ip_addr };
+	vty_out(vty, " %s:%u, Signalling Weight: %u, Data Weight: %u%s",
+		inet_ntoa(in), ntohs(ip4->udp_port), ip4->sig_weight, ip4->data_weight, VTY_NEWLINE);
+}
+
+static void vty_dump_sns_ip6(struct vty *vty, const struct gprs_ns_ie_ip6_elem *ip6)
+{
+	char ip_addr[INET6_ADDRSTRLEN] = {};
+	if (!inet_ntop(AF_INET6, &ip6->ip_addr, ip_addr, (INET6_ADDRSTRLEN)))
+		strcpy(ip_addr, "Invalid IPv6");
+
+	vty_out(vty, " %s:%u, Signalling Weight: %u, Data Weight: %u%s",
+		ip_addr, ntohs(ip6->udp_port), ip6->sig_weight, ip6->data_weight, VTY_NEWLINE);
+}
+
+void gprs_ns2_sns_dump_vty(struct vty *vty, const struct gprs_ns2_nse *nse, bool stats)
+{
+	struct ns2_sns_state *gss;
+	unsigned int i;
+
+	if (!nse->bss_sns_fi)
+		return;
+
+	vty_out_fsm_inst(vty, nse->bss_sns_fi);
+	gss = (struct ns2_sns_state *) nse->bss_sns_fi->priv;
+
+	vty_out(vty, "Maximum number of remote  NS-VCs: %zu, IPv4 Endpoints: %zu, IPv6 Endpoints: %zu%s",
+		gss->num_max_nsvcs, gss->num_max_ip4_remote, gss->num_max_ip6_remote, VTY_NEWLINE);
+
+	if (gss->num_ip4_local && gss->num_ip4_remote) {
+		vty_out(vty, "Local IPv4 Endpoints:%s", VTY_NEWLINE);
+		for (i = 0; i < gss->num_ip4_local; i++)
+			vty_dump_sns_ip4(vty, &gss->ip4_local[i]);
+
+		vty_out(vty, "Remote IPv4 Endpoints:%s", VTY_NEWLINE);
+		for (i = 0; i < gss->num_ip4_remote; i++)
+			vty_dump_sns_ip4(vty, &gss->ip4_remote[i]);
+	}
+
+	if (gss->num_ip6_local && gss->num_ip6_remote) {
+		vty_out(vty, "Local IPv6 Endpoints:%s", VTY_NEWLINE);
+		for (i = 0; i < gss->num_ip6_local; i++)
+			vty_dump_sns_ip6(vty, &gss->ip6_local[i]);
+
+		vty_out(vty, "Remote IPv6 Endpoints:%s", VTY_NEWLINE);
+		for (i = 0; i < gss->num_ip6_remote; i++)
+			vty_dump_sns_ip6(vty, &gss->ip6_remote[i]);
+	}
+}
+
+/* initialize osmo_ctx on main tread */
+static __attribute__((constructor)) void on_dso_load_ctx(void)
+{
+	OSMO_ASSERT(osmo_fsm_register(&gprs_ns2_sns_bss_fsm) == 0);
+}
diff --git a/src/gb/gprs_ns2_udp.c b/src/gb/gprs_ns2_udp.c
new file mode 100644
index 0000000..f317219
--- /dev/null
+++ b/src/gb/gprs_ns2_udp.c
@@ -0,0 +1,378 @@
+/*! \file gprs_ns2_udp.c
+ * NS-over-UDP implementation.
+ * GPRS Networks Service (NS) messages on the Gb interface.
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ * as well as its successor 3GPP TS 48.016 */
+
+/* (C) 2020 sysmocom - s.f.m.c. GmbH
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/select.h>
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/gprs/gprs_ns2.h>
+
+#include "common_vty.h"
+#include "gprs_ns2_internal.h"
+
+
+static void free_bind(struct gprs_ns2_vc_bind *bind);
+
+
+struct gprs_ns2_vc_driver vc_driver_ip = {
+	.name = "GB UDP IPv4/IPv6",
+	.free_bind = free_bind,
+};
+
+struct priv_bind {
+	struct osmo_fd fd;
+	struct osmo_sockaddr addr;
+	int dscp;
+};
+
+struct priv_vc {
+	struct osmo_sockaddr remote;
+};
+
+/*! clean up all private driver state. Should be only called by gprs_ns2_free_bind() */
+static void free_bind(struct gprs_ns2_vc_bind *bind)
+{
+	struct priv_bind *priv;
+
+	if (!bind)
+		return;
+
+	priv = bind->priv;
+
+	osmo_fd_close(&priv->fd);
+	talloc_free(priv);
+}
+
+static void free_vc(struct gprs_ns2_vc *nsvc)
+{
+	if (!nsvc->priv)
+		return;
+
+	talloc_free(nsvc->priv);
+	nsvc->priv = NULL;
+}
+
+int gprs_ns2_find_vc_by_sockaddr(struct gprs_ns2_vc_bind *bind, struct osmo_sockaddr *saddr, struct gprs_ns2_vc **result)
+{
+	struct gprs_ns2_vc *nsvc;
+	struct priv_vc *vcpriv;
+
+	if (!result)
+		return -EINVAL;
+
+	llist_for_each_entry(nsvc, &bind->nsvc, blist) {
+		vcpriv = nsvc->priv;
+		if (vcpriv->remote.u.sa.sa_family != saddr->u.sa.sa_family)
+			continue;
+		if (osmo_sockaddr_cmp(&vcpriv->remote, saddr))
+			continue;
+
+		*result = nsvc;
+		return 0;
+	}
+
+	return 1;
+}
+
+static inline int nsip_sendmsg(struct gprs_ns2_vc_bind *bind,
+			       struct msgb *msg,
+			       struct osmo_sockaddr *dest)
+{
+	int rc;
+	struct priv_bind *priv = bind->priv;
+
+	rc = sendto(priv->fd.fd, msg->data, msg->len, 0,
+		    &dest->u.sa, sizeof(*dest));
+
+	msgb_free(msg);
+
+	return rc;
+}
+
+/*!
+ * \brief nsip_vc_sendmsg will send the msg and free msgb afterwards.
+ * \param vc
+ * \param msg
+ * \return < 0 on erros, otherwise the bytes sent.
+ */
+static int nsip_vc_sendmsg(struct gprs_ns2_vc *nsvc, struct msgb *msg)
+{
+	int rc;
+	struct gprs_ns2_vc_bind *bind = nsvc->bind;
+	struct priv_vc *priv = nsvc->priv;
+
+	rc = nsip_sendmsg(bind, msg, &priv->remote);
+
+	return rc;
+}
+
+/* Read a single NS-over-IP message */
+static struct msgb *read_nsip_msg(struct osmo_fd *bfd, int *error,
+				  struct osmo_sockaddr *saddr)
+{
+	struct msgb *msg = gprs_ns2_msgb_alloc();
+	int ret = 0;
+	socklen_t saddr_len = sizeof(*saddr);
+
+	if (!msg) {
+		*error = -ENOMEM;
+		return NULL;
+	}
+
+	ret = recvfrom(bfd->fd, msg->data, NS_ALLOC_SIZE - NS_ALLOC_HEADROOM, 0,
+			&saddr->u.sa, &saddr_len);
+	if (ret < 0) {
+		LOGP(DLNS, LOGL_ERROR, "recv error %s during NSIP recvfrom %s\n",
+		     strerror(errno), osmo_sock_get_name2(bfd->fd));
+		msgb_free(msg);
+		*error = ret;
+		return NULL;
+	} else if (ret == 0) {
+		msgb_free(msg);
+		*error = ret;
+		return NULL;
+	}
+
+	msg->l2h = msg->data;
+	msgb_put(msg, ret);
+
+	return msg;
+}
+
+static struct priv_vc *ns2_driver_alloc_vc(struct gprs_ns2_vc_bind *bind, struct gprs_ns2_vc *nsvc, struct osmo_sockaddr *remote)
+{
+	struct priv_vc *priv = talloc_zero(bind, struct priv_vc);
+	if (!priv)
+		return NULL;
+
+	nsvc->priv = priv;
+	priv->remote = *remote;
+
+	return priv;
+}
+
+static int handle_nsip_read(struct osmo_fd *bfd)
+{
+	int rc;
+	int error = 0;
+	struct gprs_ns2_vc_bind *bind = bfd->data;
+	struct osmo_sockaddr saddr;
+	struct gprs_ns2_vc *nsvc;
+	struct msgb *msg = read_nsip_msg(bfd, &error, &saddr);
+	struct msgb *reject;
+
+	if (!msg)
+		return -EINVAL;
+
+	/* check if a vc is available */
+	rc = gprs_ns2_find_vc_by_sockaddr(bind, &saddr, &nsvc);
+	if (rc) {
+		/* VC not found */
+		rc = ns2_create_vc(bind, msg, "newconnection", &reject, &nsvc);
+		switch (rc) {
+		case GPRS_NS2_CS_FOUND:
+			rc = ns2_recv_vc(bind->nsi, nsvc, msg);
+			break;
+		case GPRS_NS2_CS_ERROR:
+		case GPRS_NS2_CS_SKIPPED:
+			rc = 0;
+			break;
+		case GPRS_NS2_CS_REJECTED:
+			/* nsip_sendmsg will free reject */
+			nsip_sendmsg(bind, reject, &saddr);
+			return 0;
+		case GPRS_NS2_CS_CREATED:
+			ns2_driver_alloc_vc(bind, nsvc, &saddr);
+			gprs_ns2_vc_fsm_start(nsvc);
+			rc = ns2_recv_vc(bind->nsi, nsvc, msg);
+			break;
+		}
+	} else {
+		/* VC found */
+		rc = ns2_recv_vc(bind->nsi, nsvc, msg);
+	}
+
+	msgb_free(msg);
+
+	return rc;
+}
+
+static int handle_nsip_write(struct osmo_fd *bfd)
+{
+	/* FIXME: actually send the data here instead of nsip_sendmsg() */
+	return -EIO;
+}
+
+static int nsip_fd_cb(struct osmo_fd *bfd, unsigned int what)
+{
+	int rc = 0;
+
+	if (what & OSMO_FD_READ)
+		rc = handle_nsip_read(bfd);
+	if (what & OSMO_FD_WRITE)
+		rc = handle_nsip_write(bfd);
+
+	return rc;
+}
+
+/*!
+ * \brief bind to an IPv4/IPv6 address
+ * \param[in] nsi NS Instance in which to create the NSVC
+ * \param[in] address the address to bind to
+ * \param[out] result if set, returns the bind object
+ * \return
+ */
+int gprs_ns2_ip_bind(struct gprs_ns2_inst *nsi,
+		     struct osmo_sockaddr *local,
+		     int dscp,
+		     struct gprs_ns2_vc_bind **result)
+{
+	struct gprs_ns2_vc_bind *bind = talloc_zero(nsi, struct gprs_ns2_vc_bind);
+	struct priv_bind *priv;
+	int rc;
+
+	if (!bind)
+		return -ENOSPC;
+
+	if (local->u.sa.sa_family != AF_INET && local->u.sa.sa_family != AF_INET6) {
+		talloc_free(bind);
+		return -EINVAL;
+	}
+
+	bind->driver = &vc_driver_ip;
+	bind->send_vc = nsip_vc_sendmsg;
+	bind->free_vc = free_vc;
+	bind->nsi = nsi;
+
+	priv = bind->priv = talloc_zero(bind, struct priv_bind);
+	if (!priv) {
+		talloc_free(bind);
+		return -ENOSPC;
+	}
+	priv->fd.cb = nsip_fd_cb;
+	priv->fd.data = bind;
+	priv->addr = *local;
+	INIT_LLIST_HEAD(&bind->nsvc);
+
+	llist_add(&bind->list, &nsi->binding);
+
+	rc = osmo_sock_init_osa_ofd(&priv->fd, SOCK_DGRAM, IPPROTO_UDP,
+				 local, NULL,
+				 OSMO_SOCK_F_BIND);
+	if (rc < 0) {
+		talloc_free(priv);
+		talloc_free(bind);
+		return rc;
+	}
+
+	if (dscp > 0) {
+		priv->dscp = dscp;
+
+		rc = setsockopt(priv->fd.fd, IPPROTO_IP, IP_TOS,
+				&dscp, sizeof(dscp));
+		if (rc < 0)
+			LOGP(DLNS, LOGL_ERROR,
+				"Failed to set the DSCP to %d with ret(%d) errno(%d)\n",
+				dscp, rc, errno);
+	}
+
+	ns2_vty_bind_apply(bind);
+
+	if (result)
+		*result = bind;
+
+	return 0;
+}
+
+struct gprs_ns2_vc *gprs_ns2_ip_bind_connect(struct gprs_ns2_vc_bind *bind,
+					     struct gprs_ns2_nse *nse,
+					     struct osmo_sockaddr *remote)
+{
+	struct gprs_ns2_vc *nsvc;
+	struct priv_vc *priv;
+
+	nsvc = ns2_vc_alloc(bind, nse, true);
+	nsvc->priv = talloc_zero(bind, struct priv_vc);
+	if (!nsvc->priv) {
+		gprs_ns2_free_nsvc(nsvc);
+		return NULL;
+	}
+
+	priv = nsvc->priv;
+	priv->remote = *remote;
+
+	nsvc->ll = GPRS_NS_LL_UDP;
+
+	return nsvc;
+}
+
+struct osmo_sockaddr *gprs_ns2_ip_vc_sockaddr(struct gprs_ns2_vc *nsvc)
+{
+	struct priv_vc *priv;
+
+	if (nsvc->ll != GPRS_NS_LL_UDP)
+		return NULL;
+
+	priv = nsvc->priv;
+	return &priv->remote;
+}
+
+struct osmo_sockaddr *gprs_ns2_ip_bind_sockaddr(struct gprs_ns2_vc_bind *bind)
+{
+	struct priv_bind *priv;
+
+	priv = bind->priv;
+	return &priv->addr;
+}
+
+int gprs_ns2_is_ip_bind(struct gprs_ns2_vc_bind *bind)
+{
+	return (bind->driver == &vc_driver_ip);
+}
+
+int gprs_ns2_ip_bind_set_dscp(struct gprs_ns2_vc_bind *bind, int dscp)
+{
+	struct priv_bind *priv;
+	int rc = 0;
+
+	priv = bind->priv;
+
+	if (dscp != priv->dscp) {
+		priv->dscp = dscp;
+
+		rc = setsockopt(priv->fd.fd, IPPROTO_IP, IP_TOS,
+				&dscp, sizeof(dscp));
+		if (rc < 0)
+			LOGP(DLNS, LOGL_ERROR,
+			     "Failed to set the DSCP to %d with ret(%d) errno(%d)\n",
+			     dscp, rc, errno);
+	}
+
+	return rc;
+}
diff --git a/src/gb/gprs_ns2_vc_fsm.c b/src/gb/gprs_ns2_vc_fsm.c
new file mode 100644
index 0000000..7487ecd
--- /dev/null
+++ b/src/gb/gprs_ns2_vc_fsm.c
@@ -0,0 +1,675 @@
+/*! \file gprs_ns2_vc_fsm.c
+ * NS virtual circuit FSM implementation
+ * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05)
+ * as well as its successor 3GPP TS 48.016 */
+
+/* (C) 2020 sysmocom - s.f.m.c. GmbH
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/* The BSS NSE only has one SGSN IP address configured, and it will use the SNS procedures
+ * to communicated its local IPs/ports as well as all the SGSN side IPs/ports and
+ * associated weights.  In theory, the BSS then uses this to establish a full mesh
+ * of NSVCs between all BSS-side IPs/ports and SGSN-side IPs/ports */
+
+#include <errno.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/stat_item.h>
+#include <osmocom/gsm/prim.h>
+#include <osmocom/gsm/tlv.h>
+#include <osmocom/gprs/gprs_msgb.h>
+#include <osmocom/gprs/protocol/gsm_08_16.h>
+
+#include "gprs_ns2_internal.h"
+
+#define S(x)	(1 << (x))
+
+#define DNS 10
+
+struct gprs_ns2_vc_priv {
+	struct gprs_ns2_vc *nsvc;
+	/* how often the timer was triggered */
+	int N;
+	/* The initiater is responsible to UNBLOCK the VC. The BSS is usually the initiater.
+	 * It can change while runtime. The side which blocks an unblocked side.*/
+	bool initiater;
+
+	/* the alive counter is present in all states */
+	struct {
+		struct osmo_timer_list timer;
+		enum ns2_timeout mode;
+		int N;
+		struct timeval timer_started;
+	} alive;
+};
+
+
+/* The FSM covers both the VC with RESET/BLOCK and without RESET/BLOCK procedure..
+ *
+ * With RESET/BLOCK, the state should follow:
+ * - UNCONFIGURED -> RESET -> BLOCK -> UNBLOCKED
+ *
+ * Without RESET/BLOCK, the state should follow:
+ * - UNCONFIGURED -> ALIVE -> UNBLOCKED
+ *
+ * The UNBLOCKED and TEST states are used to send ALIVE PDU using the timeout Tns-test and Tns-alive.
+ * UNBLOCKED -> TEST: on expire of Tns-Test, send Alive PDU.
+ * TEST -> UNBLOCKED: on receive of Alive_Ack PDU, go into UNBLOCKED.
+ *
+ * The ALIVE state is used as intermediate, because a VC is only valid if it received an Alive ACK when
+ * not using RESET/BLOCK procedure.
+ */
+
+enum gprs_ns2_vc_state {
+	GPRS_NS2_ST_UNCONFIGURED,
+	GPRS_NS2_ST_RESET,
+	GPRS_NS2_ST_BLOCKED,
+	GPRS_NS2_ST_UNBLOCKED, /* allows sending NS_UNITDATA */
+
+	GPRS_NS2_ST_ALIVE, /* only used when not using RESET/BLOCK procedure */
+};
+
+enum gprs_ns2_vc_event {
+	GPRS_NS2_EV_START,
+
+	/* received messages */
+	GPRS_NS2_EV_RESET,
+	GPRS_NS2_EV_RESET_ACK,
+	GPRS_NS2_EV_UNBLOCK,
+	GPRS_NS2_EV_UNBLOCK_ACK,
+	GPRS_NS2_EV_BLOCK,
+	GPRS_NS2_EV_BLOCK_ACK,
+	GPRS_NS2_EV_ALIVE,
+	GPRS_NS2_EV_ALIVE_ACK,
+	GPRS_NS2_EV_STATUS,
+
+	GPRS_NS2_EV_UNITDATA,
+};
+
+static const struct value_string gprs_ns2_vc_event_names[] = {
+	{ GPRS_NS2_EV_START, 		"START" },
+	{ GPRS_NS2_EV_RESET,		"RESET" },
+	{ GPRS_NS2_EV_RESET_ACK,	"RESET_ACK" },
+	{ GPRS_NS2_EV_UNBLOCK,		"UNBLOCK" },
+	{ GPRS_NS2_EV_UNBLOCK_ACK,	"UNBLOCK_ACK" },
+	{ GPRS_NS2_EV_BLOCK,		"BLOCK" },
+	{ GPRS_NS2_EV_BLOCK_ACK,	"BLOCK_ACK" },
+	{ GPRS_NS2_EV_ALIVE,		"ALIVE" },
+	{ GPRS_NS2_EV_ALIVE_ACK,	"ALIVE_ACK" },
+	{ GPRS_NS2_EV_STATUS,		"STATUS" },
+	{ GPRS_NS2_EV_UNITDATA,		"UNITDATA" },
+	{ 0, NULL }
+};
+
+static inline struct gprs_ns2_inst *ns_inst_from_fi(struct osmo_fsm_inst *fi)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+	return priv->nsvc->nse->nsi;
+}
+
+static void start_test_procedure(struct gprs_ns2_vc_priv *priv)
+{
+	struct gprs_ns2_inst *nsi = priv->nsvc->nse->nsi;
+
+	if (osmo_timer_pending(&priv->alive.timer))
+		return;
+
+	priv->alive.mode = NS_TOUT_TNS_ALIVE;
+	priv->alive.N = 0;
+
+	osmo_gettimeofday(&priv->alive.timer_started, NULL);
+	ns2_tx_alive(priv->nsvc);
+	osmo_timer_schedule(&priv->alive.timer, nsi->timeout[NS_TOUT_TNS_ALIVE], 0);
+}
+
+static void stop_test_procedure(struct gprs_ns2_vc_priv *priv)
+{
+	osmo_timer_del(&priv->alive.timer);
+}
+
+static int alive_timer_elapsed_ms(struct gprs_ns2_vc_priv *priv)
+{
+	struct timeval now, elapsed;
+	osmo_gettimeofday(&now, NULL);
+	timersub(&now, &priv->alive.timer_started, &elapsed);
+
+	return 1000 * elapsed.tv_sec + elapsed.tv_usec / 1000;
+}
+
+static void recv_test_procedure(struct osmo_fsm_inst *fi)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+	struct gprs_ns2_vc *nsvc = priv->nsvc;
+
+	/* ignoring ACKs without sending an ALIVE */
+	if (priv->alive.mode != NS_TOUT_TNS_ALIVE)
+		return;
+
+	priv->alive.mode = NS_TOUT_TNS_TEST;
+	osmo_timer_schedule(&priv->alive.timer, nsi->timeout[NS_TOUT_TNS_TEST], 0);
+	osmo_stat_item_set(nsvc->statg->items[NS_STAT_ALIVE_DELAY],
+		alive_timer_elapsed_ms(priv));
+}
+
+
+static void alive_timeout_handler(void *data)
+{
+	struct osmo_fsm_inst *fi = data;
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	switch (priv->alive.mode) {
+	case NS_TOUT_TNS_TEST:
+		priv->alive.mode = NS_TOUT_TNS_ALIVE;
+		ns2_tx_alive(priv->nsvc);
+		osmo_timer_schedule(&priv->alive.timer, nsi->timeout[NS_TOUT_TNS_ALIVE], 0);
+		break;
+	case NS_TOUT_TNS_ALIVE:
+		priv->alive.N++;
+
+		if (priv->alive.N <= nsi->timeout[NS_TOUT_TNS_ALIVE_RETRIES]) {
+			/* retransmission */
+			ns2_tx_alive(priv->nsvc);
+			osmo_timer_schedule(&priv->alive.timer, nsi->timeout[NS_TOUT_TNS_ALIVE], 0);
+		} else {
+			/* lost connection */
+			if (priv->nsvc->mode == NS2_VC_MODE_BLOCKRESET) {
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_RESET, nsi->timeout[NS_TOUT_TNS_RESET], 0);
+			} else {
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_ALIVE, nsi->timeout[NS_TOUT_TNS_ALIVE], 0);
+			}
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+static void gprs_ns2_st_unconfigured(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+	struct gprs_ns2_inst *nsi = priv->nsvc->nse->nsi;
+
+	switch (event) {
+	case GPRS_NS2_EV_START:
+		switch (priv->nsvc->mode) {
+		case NS2_VC_MODE_ALIVE:
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_ALIVE, nsi->timeout[NS_TOUT_TNS_ALIVE], NS_TOUT_TNS_ALIVE);
+			break;
+		case NS2_VC_MODE_BLOCKRESET:
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_RESET, nsi->timeout[NS_TOUT_TNS_RESET], NS_TOUT_TNS_RESET);
+			break;
+		}
+
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+}
+
+
+static void gprs_ns2_st_reset_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	if (old_state != GPRS_NS2_ST_RESET)
+		priv->N = 0;
+
+	if (priv->initiater)
+		ns2_tx_reset(priv->nsvc, NS_CAUSE_OM_INTERVENTION);
+
+	stop_test_procedure(priv);
+	ns2_nse_notify_unblocked(priv->nsvc, false);
+}
+
+static void gprs_ns2_st_reset(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	if (priv->initiater) {
+		switch (event) {
+		case GPRS_NS2_EV_RESET_ACK:
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_BLOCKED,
+						nsi->timeout[NS_TOUT_TNS_BLOCK], NS_TOUT_TNS_BLOCK);
+			break;
+		}
+	} else {
+		/* we are on the receiving end */
+		switch (event) {
+		case GPRS_NS2_EV_RESET:
+			ns2_tx_reset_ack(priv->nsvc);
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_BLOCKED,
+						0, 0);
+			break;
+		}
+	}
+}
+
+static void gprs_ns2_st_blocked_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	if (old_state != GPRS_NS2_ST_BLOCKED)
+		priv->N = 0;
+
+	if (priv->initiater)
+		ns2_tx_unblock(priv->nsvc);
+
+	start_test_procedure(priv);
+}
+
+static void gprs_ns2_st_blocked(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	if (priv->initiater) {
+		switch (event) {
+		case GPRS_NS2_EV_BLOCK:
+			/* TODO: BLOCK is a UNBLOCK_NACK */
+			ns2_tx_block_ack(priv->nsvc);
+			break;
+		case GPRS_NS2_EV_UNBLOCK:
+			ns2_tx_unblock_ack(priv->nsvc);
+			/* fall through */
+		case GPRS_NS2_EV_UNBLOCK_ACK:
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_UNBLOCKED,
+						0, NS_TOUT_TNS_TEST);
+			break;
+		}
+	} else {
+		/* we are on the receiving end. The initiator who sent RESET is responsible to UNBLOCK! */
+		switch (event) {
+		case GPRS_NS2_EV_UNBLOCK:
+			ns2_tx_unblock_ack(priv->nsvc);
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_UNBLOCKED,
+						0, 0);
+			break;
+		}
+	}
+}
+
+static void gprs_ns2_st_unblocked_on_enter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	ns2_nse_notify_unblocked(priv->nsvc, true);
+}
+
+static void gprs_ns2_st_unblocked(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	switch (event) {
+	case GPRS_NS2_EV_BLOCK:
+		priv->initiater = false;
+		ns2_tx_block_ack(priv->nsvc);
+		osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_BLOCKED,
+					0, 2);
+		break;
+	}
+}
+
+static void gprs_ns2_st_alive(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	switch (event) {
+	case GPRS_NS2_EV_ALIVE_ACK:
+		osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_UNBLOCKED, 0, 0);
+		break;
+	}
+}
+
+static void gprs_ns2_st_alive_onenter(struct osmo_fsm_inst *fi, uint32_t old_state)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+
+	priv->alive.mode = NS_TOUT_TNS_TEST;
+	osmo_timer_schedule(&priv->alive.timer, nsi->timeout[NS_TOUT_TNS_TEST], 0);
+
+	if (old_state != GPRS_NS2_ST_ALIVE)
+		priv->N = 0;
+
+	ns2_tx_alive(priv->nsvc);
+	ns2_nse_notify_unblocked(priv->nsvc, false);
+}
+
+static void gprs_ns2_st_alive_onleave(struct osmo_fsm_inst *fi, uint32_t next_state)
+{
+	start_test_procedure(fi->priv);
+}
+
+static const struct osmo_fsm_state gprs_ns2_vc_states[] = {
+	[GPRS_NS2_ST_UNCONFIGURED] = {
+		.in_event_mask = S(GPRS_NS2_EV_START),
+		.out_state_mask = S(GPRS_NS2_ST_RESET) | S(GPRS_NS2_ST_ALIVE),
+		.name = "UNCONFIGURED",
+		.action = gprs_ns2_st_unconfigured,
+	},
+	[GPRS_NS2_ST_RESET] = {
+		.in_event_mask = S(GPRS_NS2_EV_RESET_ACK) | S(GPRS_NS2_EV_RESET),
+		.out_state_mask = S(GPRS_NS2_ST_RESET) |
+				  S(GPRS_NS2_ST_BLOCKED),
+		.name = "RESET",
+		.action = gprs_ns2_st_reset,
+		.onenter = gprs_ns2_st_reset_onenter,
+	},
+	[GPRS_NS2_ST_BLOCKED] = {
+		.in_event_mask = S(GPRS_NS2_EV_BLOCK) | S(GPRS_NS2_EV_BLOCK_ACK) |
+		S(GPRS_NS2_EV_UNBLOCK) | S(GPRS_NS2_EV_UNBLOCK_ACK),
+		.out_state_mask = S(GPRS_NS2_ST_RESET) |
+				  S(GPRS_NS2_ST_UNBLOCKED) |
+				  S(GPRS_NS2_ST_BLOCKED),
+		.name = "BLOCKED",
+		.action = gprs_ns2_st_blocked,
+		.onenter = gprs_ns2_st_blocked_onenter,
+	},
+	[GPRS_NS2_ST_UNBLOCKED] = {
+		.in_event_mask = S(GPRS_NS2_EV_BLOCK),
+		.out_state_mask = S(GPRS_NS2_ST_RESET) | S(GPRS_NS2_ST_ALIVE) |
+				  S(GPRS_NS2_ST_BLOCKED),
+		.name = "UNBLOCKED",
+		.action = gprs_ns2_st_unblocked,
+		.onenter = gprs_ns2_st_unblocked_on_enter,
+	},
+
+	/* ST_ALIVE is only used on VC without RESET/BLOCK */
+	[GPRS_NS2_ST_ALIVE] = {
+		.in_event_mask = S(GPRS_NS2_EV_ALIVE_ACK),
+		.out_state_mask = S(GPRS_NS2_ST_RESET) |
+				  S(GPRS_NS2_ST_UNBLOCKED),
+		.name = "ALIVE",
+		.action = gprs_ns2_st_alive,
+		.onenter = gprs_ns2_st_alive_onenter,
+		.onleave = gprs_ns2_st_alive_onleave,
+	},
+};
+
+static int gprs_ns2_vc_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+
+	if (priv->initiater) {
+		/* PCU timeouts */
+		switch (fi->state) {
+		case GPRS_NS2_ST_RESET:
+			priv->N++;
+			if (priv->N <= nsi->timeout[NS_TOUT_TNS_RESET_RETRIES]) {
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_RESET, nsi->timeout[NS_TOUT_TNS_RESET], 0);
+			} else {
+				priv->N = 0;
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_RESET, nsi->timeout[NS_TOUT_TNS_RESET], 0);
+			}
+			break;
+		case GPRS_NS2_ST_BLOCKED:
+			priv->N++;
+			if (priv->N <= nsi->timeout[NS_TOUT_TNS_BLOCK_RETRIES]) {
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_BLOCKED, nsi->timeout[NS_TOUT_TNS_BLOCK], 0);
+			} else {
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_RESET, nsi->timeout[NS_TOUT_TNS_RESET], 0);
+			}
+			break;
+		case GPRS_NS2_ST_ALIVE:
+			priv->N++;
+			if (priv->N <= nsi->timeout[NS_TOUT_TNS_ALIVE_RETRIES]) {
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_ALIVE, 0, 0);
+			} else {
+				priv->N = 0;
+				osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_ALIVE, 0, 0);
+			}
+			break;
+		}
+	}
+	return 0;
+}
+
+static void gprs_ns2_recv_unitdata(struct osmo_fsm_inst *fi,
+				   struct msgb *msg)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+	struct gprs_ns_hdr *nsh = (struct gprs_ns_hdr *) msg->l2h;
+	struct osmo_gprs_ns2_prim nsp = {};
+	uint16_t bvci;
+
+	if (msgb_l2len(msg) < sizeof(*nsh) + 3)
+		return;
+
+	/* TODO: 7.1: For an IP sub-network, an NS-UNITDATA PDU
+	 * for a PTP BVC may indicate a request to change the IP endpoint
+	 * and/or a response to a change in the IP endpoint. */
+
+	/* TODO: nsh->data[0] -> C/R only valid in IP SNS */
+	bvci = nsh->data[1] << 8 | nsh->data[2];
+
+	msgb_bssgph(msg) = &nsh->data[3];
+	msgb_bvci(msg) = nsp.bvci = bvci;
+	msgb_nsei(msg) = nsp.nsei = priv->nsvc->nse->nsei;
+
+	if (nsh->data[0])
+		nsp.u.unitdata.change = NS_ENDPOINT_REQUEST_CHANGE;
+
+	osmo_prim_init(&nsp.oph, SAP_NS, PRIM_NS_UNIT_DATA,
+			PRIM_OP_INDICATION, msg);
+	nsi->cb(&nsp.oph, nsi->cb_data);
+}
+
+static void gprs_ns2_vc_fsm_allstate_action(struct osmo_fsm_inst *fi,
+					    uint32_t event,
+					    void *data)
+{
+	struct gprs_ns2_vc_priv *priv = fi->priv;
+	struct gprs_ns2_inst *nsi = ns_inst_from_fi(fi);
+
+	switch (event) {
+	case GPRS_NS2_EV_RESET:
+		if (priv->nsvc->mode != NS2_VC_MODE_BLOCKRESET)
+			break;
+
+		/* move the FSM into reset */
+		if (fi->state != GPRS_NS2_ST_RESET) {
+			priv->initiater = false;
+			osmo_fsm_inst_state_chg(fi, GPRS_NS2_ST_RESET, nsi->timeout[NS_TOUT_TNS_RESET], NS_TOUT_TNS_RESET);
+		}
+		/* pass the event down into FSM action */
+		gprs_ns2_st_reset(fi, event, data);
+		break;
+	case GPRS_NS2_EV_ALIVE:
+		switch (fi->state) {
+		case GPRS_NS2_ST_UNCONFIGURED:
+		case GPRS_NS2_ST_RESET:
+			/* ignore ALIVE */
+			break;
+		default:
+			ns2_tx_alive_ack(priv->nsvc);
+		}
+		break;
+	case GPRS_NS2_EV_ALIVE_ACK:
+		/* for VCs without RESET/BLOCK/UNBLOCK, the connections comes after ALIVE_ACK unblocked */
+		if (fi->state == GPRS_NS2_ST_ALIVE)
+			gprs_ns2_st_alive(fi, event, data);
+		else
+			recv_test_procedure(fi);
+		break;
+	case GPRS_NS2_EV_UNITDATA:
+		switch (fi->state) {
+		case GPRS_NS2_ST_BLOCKED:
+			/* 7.2.1: the BLOCKED_ACK might be lost */
+			if (priv->initiater)
+				gprs_ns2_recv_unitdata(fi, data);
+			else
+				ns2_tx_status(priv->nsvc,
+					      NS_CAUSE_NSVC_BLOCKED,
+					      0, data);
+			break;
+		/* ALIVE can receive UNITDATA if the ALIVE_ACK is lost */
+		case GPRS_NS2_ST_ALIVE:
+		case GPRS_NS2_ST_UNBLOCKED:
+			gprs_ns2_recv_unitdata(fi, data);
+			break;
+		}
+		break;
+	}
+}
+
+static struct osmo_fsm gprs_ns2_vc_fsm = {
+	.name = "GPRS-NS2-VC",
+	.states = gprs_ns2_vc_states,
+	.num_states = ARRAY_SIZE(gprs_ns2_vc_states),
+	.allstate_event_mask = S(GPRS_NS2_EV_UNITDATA) |
+			S(GPRS_NS2_EV_RESET) |
+			       S(GPRS_NS2_EV_ALIVE) |
+			       S(GPRS_NS2_EV_ALIVE_ACK),
+	.allstate_action = gprs_ns2_vc_fsm_allstate_action,
+	.cleanup = NULL,
+	.timer_cb = gprs_ns2_vc_fsm_timer_cb,
+	/* .log_subsys = DNS, "is not constant" */
+	.event_names = gprs_ns2_vc_event_names,
+	.pre_term = NULL,
+	.log_subsys = DLNS,
+};
+
+/*!
+ * \brief gprs_ns2_vc_fsm_alloc
+ * \param ctx
+ * \param vc
+ * \param id a char representation of the virtual curcuit
+ * \param initiater initiater is the site which starts the connection. Usually the BSS.
+ * \return NULL on error, otherwise the fsm
+ */
+struct osmo_fsm_inst *gprs_ns2_vc_fsm_alloc(struct gprs_ns2_vc *nsvc,
+					    const char *id, bool initiater)
+{
+	struct osmo_fsm_inst *fi;
+	struct gprs_ns2_vc_priv *priv;
+
+	fi = osmo_fsm_inst_alloc(&gprs_ns2_vc_fsm, nsvc, NULL, LOGL_DEBUG, id);
+	if (!fi)
+		return fi;
+
+	nsvc->fi = fi;
+	priv = fi->priv = talloc_zero(fi, struct gprs_ns2_vc_priv);
+	priv->nsvc = nsvc;
+	priv->initiater = initiater;
+
+	osmo_timer_setup(&priv->alive.timer, alive_timeout_handler, fi);
+
+	return fi;
+}
+
+/*!
+ * \brief gprs_ns2_vc_fsm_start start the FSM
+ * \param vc the virtual circuit
+ * \return 0 on success
+ */
+int gprs_ns2_vc_fsm_start(struct gprs_ns2_vc *nsvc)
+{
+	/* allows to call this function even for started nsvc by gprs_ns2_start_alive_all_nsvcs */
+	if (nsvc->fi->state == GPRS_NS2_ST_UNCONFIGURED)
+		return osmo_fsm_inst_dispatch(nsvc->fi, GPRS_NS2_EV_START, NULL);
+	return 0;
+}
+
+/*!
+ * \brief gprs_ns2_vc_rx entry point for messages from the driver/VL
+ * \param vc the virtual circuit on which is recived
+ * \param msg the message
+ * \param tp the parsed TLVs
+ * \return 0 on success
+ */
+int gprs_ns2_vc_rx(struct gprs_ns2_vc *nsvc, struct msgb *msg, struct tlv_parsed *tp)
+{
+	struct gprs_ns_hdr *nsh = (struct gprs_ns_hdr *) msg->l2h;
+	struct osmo_fsm_inst *fi = nsvc->fi;
+	uint8_t cause;
+
+	/* TODO: 7.2: on UNBLOCK/BLOCK: check if NS-VCI is correct,
+	 *  if not answer STATUS with "NS-VC unknown" */
+	/* TODO: handle RESET with different VCI */
+	/* TODO: handle BLOCK/UNBLOCK/ALIVE with different VCI */
+
+	if (gprs_ns2_validate(nsvc, nsh->pdu_type, msg, tp, &cause)) {
+		if (nsh->pdu_type != NS_PDUT_STATUS) {
+			return ns2_tx_status(nsvc, cause, 0, msg);
+		}
+	}
+
+	switch (nsh->pdu_type) {
+	case NS_PDUT_RESET:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_RESET, tp);
+		break;
+	case NS_PDUT_RESET_ACK:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_RESET_ACK, tp);
+		break;
+	case NS_PDUT_BLOCK:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_BLOCK, tp);
+		break;
+	case NS_PDUT_BLOCK_ACK:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_BLOCK_ACK, tp);
+		break;
+	case NS_PDUT_UNBLOCK:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_UNBLOCK, tp);
+		break;
+	case NS_PDUT_UNBLOCK_ACK:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_UNBLOCK_ACK, tp);
+		break;
+	case NS_PDUT_ALIVE:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_ALIVE, tp);
+		break;
+	case NS_PDUT_ALIVE_ACK:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_ALIVE_ACK, tp);
+		break;
+	case NS_PDUT_UNITDATA:
+		osmo_fsm_inst_dispatch(fi, GPRS_NS2_EV_UNITDATA, msg);
+		break;
+	default:
+		LOGP(DLNS, LOGL_ERROR, "NSEI=%u Rx unknown NS PDU type %s\n", nsvc->nse->nsei,
+			get_value_string(gprs_ns_pdu_strings, nsh->pdu_type));
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/*!
+ * \brief gprs_ns2_vc_is_alive says if this
+ * \param vc
+ * \return
+ */
+int gprs_ns2_vc_is_unblocked(struct gprs_ns2_vc *nsvc)
+{
+	return (nsvc->fi->state == GPRS_NS2_ST_UNBLOCKED);
+}
+
+/* initialize osmo_ctx on main tread */
+static __attribute__((constructor)) void on_dso_load_ctx(void)
+{
+	OSMO_ASSERT(osmo_fsm_register(&gprs_ns2_vc_fsm) == 0);
+}
diff --git a/src/gb/gprs_ns2_vty.c b/src/gb/gprs_ns2_vty.c
new file mode 100644
index 0000000..81f88b6
--- /dev/null
+++ b/src/gb/gprs_ns2_vty.c
@@ -0,0 +1,833 @@
+/*! \file gprs_ns2_vty.c
+ * VTY interface for our GPRS Networks Service (NS) implementation. */
+
+/* (C) 2009-2014 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdint.h>
+
+#include <arpa/inet.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/byteswap.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/gprs/gprs_ns2.h>
+#include <osmocom/gsm/tlv.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 "gprs_ns2_internal.h"
+
+struct ns2_vty_priv {
+	/* global listen */
+	struct osmo_sockaddr_str udp;
+	struct osmo_sockaddr_str frgreaddr;
+	int dscp;
+	enum gprs_ns2_vc_mode vc_mode;
+	/* force vc mode if another configuration forces
+	 * the vc mode. E.g. SNS configuration */
+	bool force_vc_mode;
+	char *force_vc_mode_reason;
+	bool frgre;
+
+	struct llist_head vtyvc;
+};
+
+struct ns2_vty_vc {
+	struct llist_head list;
+
+	struct osmo_sockaddr_str remote;
+	enum gprs_ns_ll ll;
+
+	/* old vty code doesnt support multiple NSVCI per NSEI */
+	uint16_t nsei;
+	uint16_t nsvci;
+	uint16_t frdlci;
+
+	bool remote_end_is_sgsn;
+	bool configured;
+};
+
+static struct gprs_ns2_inst *vty_nsi = NULL;
+static struct ns2_vty_priv priv;
+
+/* FIXME: this should go to some common file as it is copied
+ * in vty_interface.c of the BSC */
+static const struct value_string gprs_ns_timer_strs[] = {
+	{ 0, "tns-block" },
+	{ 1, "tns-block-retries" },
+	{ 2, "tns-reset" },
+	{ 3, "tns-reset-retries" },
+	{ 4, "tns-test" },
+	{ 5, "tns-alive" },
+	{ 6, "tns-alive-retries" },
+	{ 7, "tsns-prov" },
+	{ 0, NULL }
+};
+
+static void log_set_nsvc_filter(struct log_target *target,
+				struct gprs_ns2_vc *nsvc)
+{
+	if (nsvc) {
+		target->filter_map |= (1 << LOG_FLT_GB_NSVC);
+		target->filter_data[LOG_FLT_GB_NSVC] = nsvc;
+	} else if (target->filter_data[LOG_FLT_GB_NSVC]) {
+		target->filter_map = ~(1 << LOG_FLT_GB_NSVC);
+		target->filter_data[LOG_FLT_GB_NSVC] = NULL;
+	}
+}
+
+static struct cmd_node ns_node = {
+	L_NS_NODE,
+	"%s(config-ns)# ",
+	1,
+};
+
+static struct ns2_vty_vc *vtyvc_alloc(uint16_t nsei) {
+	struct ns2_vty_vc *vtyvc = talloc_zero(vty_nsi, struct ns2_vty_vc);
+	if (!vtyvc)
+		return vtyvc;
+
+	vtyvc->nsei = nsei;
+
+	llist_add(&vtyvc->list, &priv.vtyvc);
+
+	return vtyvc;
+}
+
+static void ns2_vc_free(struct ns2_vty_vc *vtyvc) {
+	if (!vtyvc)
+		return;
+
+	llist_del(&vtyvc->list);
+	talloc_free(vtyvc);
+}
+
+static struct ns2_vty_vc *vtyvc_by_nsei(uint16_t nsei, bool alloc_missing) {
+	struct ns2_vty_vc *vtyvc;
+	llist_for_each_entry(vtyvc, &priv.vtyvc, list) {
+		if (vtyvc->nsei == nsei)
+			return vtyvc;
+	}
+
+	if (alloc_missing) {
+		vtyvc = vtyvc_alloc(nsei);
+		if (!vtyvc)
+			return vtyvc;
+
+		vtyvc->nsei = nsei;
+	}
+
+	return NULL;
+}
+
+static int config_write_ns(struct vty *vty)
+{
+	struct ns2_vty_vc *vtyvc;
+	unsigned int i;
+	struct osmo_sockaddr_str sockstr;
+
+	vty_out(vty, "ns%s", VTY_NEWLINE);
+
+	/* global configuration must be written first, as some of it may be
+	 * relevant when creating the NSE/NSVC later below */
+
+	vty_out(vty, " encapsulation framerelay-gre enabled %u%s",
+		priv.frgre ? 1 : 0, VTY_NEWLINE);
+
+	if (priv.frgre) {
+		if (strlen(priv.frgreaddr.ip)) {
+			vty_out(vty, " encapsulation framerelay-gre local-ip %s%s",
+				sockstr.ip, VTY_NEWLINE);
+		}
+	} else {
+		if (strlen(priv.udp.ip)) {
+			vty_out(vty, " encapsulation udp local-ip %s%s",
+				priv.udp.ip, VTY_NEWLINE);
+		}
+
+		if (priv.udp.port)
+			vty_out(vty, " encapsulation udp local-port %u%s",
+				priv.udp.port, VTY_NEWLINE);
+	}
+
+	if (priv.dscp)
+		vty_out(vty, " encapsulation udp dscp %d%s",
+			priv.dscp, VTY_NEWLINE);
+
+	vty_out(vty, " encapsulation udp use-reset-block-unblock %s%s",
+		priv.vc_mode == NS2_VC_MODE_BLOCKRESET ? "enabled" : "disabled", VTY_NEWLINE);
+
+	llist_for_each_entry(vtyvc, &priv.vtyvc, list) {
+		vty_out(vty, " nse %u nsvci %u%s",
+			vtyvc->nsei, vtyvc->nsvci, VTY_NEWLINE);
+
+		vty_out(vty, " nse %u remote-role %s%s",
+			vtyvc->nsei, vtyvc->remote_end_is_sgsn ? "sgsn" : "bss",
+			VTY_NEWLINE);
+
+		switch (vtyvc->ll) {
+		case GPRS_NS_LL_UDP:
+			vty_out(vty, " nse %u encapsulation udp%s", vtyvc->nsei, VTY_NEWLINE);
+			vty_out(vty, " nse %u remote-ip %s%s",
+				vtyvc->nsei,
+				vtyvc->remote.ip,
+				VTY_NEWLINE);
+			vty_out(vty, " nse %u remote-port %u%s",
+				vtyvc->nsei, vtyvc->remote.port,
+				VTY_NEWLINE);
+			break;
+		case GPRS_NS_LL_FR_GRE:
+			vty_out(vty, " nse %u encapsulation framerelay-gre%s",
+				vtyvc->nsei, VTY_NEWLINE);
+			vty_out(vty, " nse %u remote-ip %s%s",
+				vtyvc->nsei,
+				vtyvc->remote.ip,
+				VTY_NEWLINE);
+			vty_out(vty, " nse %u fr-dlci %u%s",
+				vtyvc->nsei, vtyvc->frdlci,
+				VTY_NEWLINE);
+			break;
+		default:
+			break;
+		}
+	}
+
+	for (i = 0; i < ARRAY_SIZE(vty_nsi->timeout); i++)
+		vty_out(vty, " timer %s %u%s",
+			get_value_string(gprs_ns_timer_strs, i),
+			vty_nsi->timeout[i], VTY_NEWLINE);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ns, cfg_ns_cmd,
+      "ns",
+      "Configure the GPRS Network Service")
+{
+	vty->node = L_NS_NODE;
+	return CMD_SUCCESS;
+}
+
+static void dump_nsvc(struct vty *vty, struct gprs_ns2_vc *nsvc, bool stats)
+{
+	struct osmo_sockaddr_str remote;
+	struct osmo_sockaddr_str local;
+	struct osmo_sockaddr *sockaddr;
+
+	switch (nsvc->ll) {
+	case GPRS_NS_LL_UDP: {
+		sockaddr = gprs_ns2_ip_vc_sockaddr(nsvc);
+		if (!sockaddr) {
+			vty_out(vty, "unknown");
+			break;
+		}
+
+		if (osmo_sockaddr_str_from_sockaddr(
+					&remote,
+					&sockaddr->u.sas)) {
+			vty_out(vty, "unknown");
+			break;
+		}
+
+		vty_out(vty, "%s:%u <> %s:%u", local.ip, local.port, remote.ip, remote.port);
+		break;
+	}
+	case GPRS_NS_LL_FR_GRE:
+		/* TODO: implement dump_nse for FR GRE */
+	case GPRS_NS_LL_E1:
+		/* TODO: implement dump_nse for E1 */
+		break;
+	}
+
+	vty_out(vty, "Remote: %s ",
+		 gprs_ns2_ll_str(nsvc));
+
+	vty_out(vty, "%s%s", nsvc->ll == GPRS_NS_LL_UDP ? "UDP" : "FR-GRE", VTY_NEWLINE);
+
+	if (stats) {
+		vty_out_rate_ctr_group(vty, " ", nsvc->ctrg);
+		vty_out_stat_item_group(vty, " ", nsvc->statg);
+	}
+}
+
+static void dump_nse(struct vty *vty, const struct gprs_ns2_nse *nse, bool stats, bool persistent_only)
+{
+	struct gprs_ns2_vc *nsvc;
+
+	vty_out(vty, "NSEI %5u%s",
+		nse->nsei, VTY_NEWLINE);
+
+	gprs_ns2_sns_dump_vty(vty, nse, stats);
+	llist_for_each_entry(nsvc, &nse->nsvc, list) {
+		if (persistent_only) {
+			if (nsvc->persistent)
+				dump_nsvc(vty, nsvc, stats);
+		} else {
+			dump_nsvc(vty, nsvc, stats);
+		}
+	}
+}
+
+static void dump_ns(struct vty *vty, const struct gprs_ns2_inst *nsi, bool stats, bool persistent_only)
+{
+	struct gprs_ns2_nse *nse;
+
+	llist_for_each_entry(nse, &nsi->nse, list) {
+		dump_nse(vty, nse, stats, persistent_only);
+		break;
+	}
+
+}
+
+DEFUN(show_ns, show_ns_cmd, "show ns",
+	SHOW_STR "Display information about the NS protocol")
+{
+	dump_ns(vty, vty_nsi, false, false);
+	return CMD_SUCCESS;
+}
+
+DEFUN(show_ns_stats, show_ns_stats_cmd, "show ns stats",
+	SHOW_STR
+	"Display information about the NS protocol\n"
+	"Include statistics\n")
+{
+	dump_ns(vty, vty_nsi, true, false);
+	return CMD_SUCCESS;
+}
+
+DEFUN(show_ns_pers, show_ns_pers_cmd, "show ns persistent",
+	SHOW_STR
+	"Display information about the NS protocol\n"
+	"Show only persistent NS\n")
+{
+	dump_ns(vty, vty_nsi, true, true);
+	return CMD_SUCCESS;
+}
+
+DEFUN(show_nse, show_nse_cmd, "show ns (nsei|nsvc) <0-65535> [stats]",
+	SHOW_STR "Display information about the NS protocol\n"
+	"Select one NSE by its NSE Identifier\n"
+	"Select one NSE by its NS-VC Identifier\n"
+	"The Identifier of selected type\n"
+	"Include Statistics\n")
+{
+	struct gprs_ns2_inst *nsi = vty_nsi;
+	struct gprs_ns2_nse *nse;
+	struct gprs_ns2_vc *nsvc;
+	uint16_t id = atoi(argv[1]);
+	bool show_stats = false;
+
+	if (argc >= 3)
+		show_stats = true;
+
+	if (!strcmp(argv[0], "nsei")) {
+		nse = gprs_ns2_nse_by_nsei(nsi, id);
+		if (!nse) {
+			return CMD_WARNING;
+		}
+
+		dump_nse(vty, nse, show_stats, false);
+	} else {
+		nsvc = gprs_ns2_nsvc_by_nsvci(nsi, id);
+
+		if (!nsvc) {
+			vty_out(vty, "No such NS Entity%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+
+		dump_nsvc(vty, nsvc, show_stats);
+	}
+
+	return CMD_SUCCESS;
+}
+
+#define NSE_CMD_STR "Persistent NS Entity\n" "NS Entity ID (NSEI)\n"
+
+DEFUN(cfg_nse_nsvc, cfg_nse_nsvci_cmd,
+	"nse <0-65535> nsvci <0-65535>",
+	NSE_CMD_STR
+	"NS Virtual Connection\n"
+	"NS Virtual Connection ID (NSVCI)\n"
+	)
+{
+	struct ns2_vty_vc *vtyvc;
+
+	uint16_t nsei = atoi(argv[0]);
+	uint16_t nsvci = atoi(argv[1]);
+
+	vtyvc = vtyvc_by_nsei(nsei, true);
+	if (!vtyvc) {
+		vty_out(vty, "Can not allocate space %s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	vtyvc->nsvci = nsvci;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nse_remoteip, cfg_nse_remoteip_cmd,
+	"nse <0-65535> remote-ip " VTY_IPV46_CMD,
+	NSE_CMD_STR
+	"Remote IP Address\n"
+	"Remote IP Address\n")
+{
+	uint16_t nsei = atoi(argv[0]);
+	struct ns2_vty_vc *vtyvc;
+
+	vtyvc = vtyvc_by_nsei(nsei, true);
+	if (!vtyvc) {
+		vty_out(vty, "Can not allocate space %s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	osmo_sockaddr_str_from_str2(&vtyvc->remote, argv[1]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nse_remoteport, cfg_nse_remoteport_cmd,
+	"nse <0-65535> remote-port <0-65535>",
+	NSE_CMD_STR
+	"Remote UDP Port\n"
+	"Remote UDP Port Number\n")
+{
+	uint16_t nsei = atoi(argv[0]);
+	uint16_t port = atoi(argv[1]);
+	struct ns2_vty_vc *vtyvc;
+
+	vtyvc = vtyvc_by_nsei(nsei, true);
+	if (!vtyvc) {
+		vty_out(vty, "Can not allocate space %s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	vtyvc->remote.port = port;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nse_fr_dlci, cfg_nse_fr_dlci_cmd,
+	"nse <0-65535> fr-dlci <16-1007>",
+	NSE_CMD_STR
+	"Frame Relay DLCI\n"
+	"Frame Relay DLCI Number\n")
+{
+	uint16_t nsei = atoi(argv[0]);
+	uint16_t dlci = atoi(argv[1]);
+	struct ns2_vty_vc *vtyvc;
+
+	vtyvc = vtyvc_by_nsei(nsei, true);
+	if (!vtyvc) {
+		vty_out(vty, "Can not allocate space %s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (vtyvc->ll != GPRS_NS_LL_FR_GRE) {
+		vty_out(vty, "Warning: seting FR DLCI on non-FR NSE%s",
+			VTY_NEWLINE);
+	}
+
+	vtyvc->frdlci = dlci;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nse_encaps, cfg_nse_encaps_cmd,
+	"nse <0-65535> encapsulation (udp|framerelay-gre)",
+	NSE_CMD_STR
+	"Encapsulation for NS\n"
+	"UDP/IP Encapsulation\n" "Frame-Relay/GRE/IP Encapsulation\n")
+{
+	uint16_t nsei = atoi(argv[0]);
+	struct ns2_vty_vc *vtyvc;
+
+	vtyvc = vtyvc_by_nsei(nsei, true);
+	if (!vtyvc) {
+		vty_out(vty, "Can not allocate space %s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!strcmp(argv[1], "udp"))
+		vtyvc->ll = GPRS_NS_LL_UDP;
+	else
+		vtyvc->ll = GPRS_NS_LL_FR_GRE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nse_remoterole, cfg_nse_remoterole_cmd,
+	"nse <0-65535> remote-role (sgsn|bss)",
+	NSE_CMD_STR
+	"Remote NSE Role\n"
+	"Remote Peer is SGSN\n"
+	"Remote Peer is BSS\n")
+{
+	uint16_t nsei = atoi(argv[0]);
+	struct ns2_vty_vc *vtyvc;
+
+	vtyvc = vtyvc_by_nsei(nsei, true);
+	if (!vtyvc) {
+		vty_out(vty, "Can not allocate space %s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!strcmp(argv[1], "sgsn"))
+		vtyvc->remote_end_is_sgsn = 1;
+	else
+		vtyvc->remote_end_is_sgsn = 0;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_nse, cfg_no_nse_cmd,
+	"no nse <0-65535>",
+	"Delete Persistent NS Entity\n"
+	"Delete " NSE_CMD_STR)
+{
+	uint16_t nsei = atoi(argv[0]);
+	struct ns2_vty_vc *vtyvc;
+
+	vtyvc = vtyvc_by_nsei(nsei, false);
+	if (!vtyvc) {
+		vty_out(vty, "The NSE %d does not exists.%s", nsei, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	ns2_vc_free(vtyvc);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ns_timer, cfg_ns_timer_cmd,
+	"timer " NS_TIMERS " <0-65535>",
+	"Network Service Timer\n"
+	NS_TIMERS_HELP "Timer Value\n")
+{
+	int idx = get_string_value(gprs_ns_timer_strs, argv[0]);
+	int val = atoi(argv[1]);
+
+	if (idx < 0 || idx >= ARRAY_SIZE(vty_nsi->timeout))
+		return CMD_WARNING;
+
+	vty_nsi->timeout[idx] = val;
+
+	return CMD_SUCCESS;
+}
+
+#define ENCAPS_STR "NS encapsulation options\n"
+
+DEFUN(cfg_nsip_local_ip, cfg_nsip_local_ip_cmd,
+      "encapsulation udp local-ip " VTY_IPV46_CMD,
+	ENCAPS_STR "NS over UDP Encapsulation\n"
+	"Set the IP address on which we listen for NS/UDP\n"
+	"IP Address\n")
+{
+	osmo_sockaddr_str_from_str2(&priv.udp, argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nsip_local_port, cfg_nsip_local_port_cmd,
+      "encapsulation udp local-port <0-65535>",
+	ENCAPS_STR "NS over UDP Encapsulation\n"
+	"Set the UDP port on which we listen for NS/UDP\n"
+	"UDP port number\n")
+{
+	unsigned int port = atoi(argv[0]);
+
+	priv.udp.port = port;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nsip_dscp, cfg_nsip_dscp_cmd,
+      "encapsulation udp dscp <0-255>",
+	ENCAPS_STR "NS over UDP Encapsulation\n"
+	"Set DSCP/TOS on the UDP socket\n" "DSCP Value\n")
+{
+	int dscp = atoi(argv[0]);
+	struct gprs_ns2_vc_bind *bind;
+
+	priv.dscp = dscp;
+
+	llist_for_each_entry(bind, &vty_nsi->binding, list) {
+		if (gprs_ns2_is_ip_bind(bind))
+			gprs_ns2_ip_bind_set_dscp(bind, dscp);
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nsip_res_block_unblock, cfg_nsip_res_block_unblock_cmd,
+	"encapsulation udp use-reset-block-unblock (enabled|disabled)",
+	ENCAPS_STR "NS over UDP Encapsulation\n"
+	"Use NS-{RESET,BLOCK,UNBLOCK} procedures in violation of 3GPP TS 48.016\n"
+	"Enable NS-{RESET,BLOCK,UNBLOCK}\n"
+	"Disable NS-{RESET,BLOCK,UNBLOCK}\n")
+{
+	enum gprs_ns2_vc_mode vc_mode;
+	struct gprs_ns2_vc_bind *bind;
+
+	if (!strcmp(argv[0], "enabled"))
+		vc_mode = NS2_VC_MODE_BLOCKRESET;
+	else
+		vc_mode = NS2_VC_MODE_ALIVE;
+
+	if (priv.force_vc_mode) {
+		if (priv.vc_mode != vc_mode)
+		{
+			vty_out(vty, "Ignoring use-reset-block because it's already set by %s.%s",
+				priv.force_vc_mode_reason, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+
+		return CMD_SUCCESS;
+	}
+
+	priv.vc_mode = vc_mode;
+
+	llist_for_each_entry(bind, &vty_nsi->binding, list) {
+		gprs_ns2_bind_set_mode(bind, priv.vc_mode);
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_frgre_local_ip, cfg_frgre_local_ip_cmd,
+      "encapsulation framerelay-gre local-ip " VTY_IPV46_CMD,
+	ENCAPS_STR "NS over Frame Relay over GRE Encapsulation\n"
+	"Set the IP address on which we listen for NS/FR/GRE\n"
+	"IP Address\n")
+{
+	osmo_sockaddr_str_from_str2(&priv.frgreaddr, argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_frgre_enable, cfg_frgre_enable_cmd,
+      "encapsulation framerelay-gre enabled (1|0)",
+	ENCAPS_STR "NS over Frame Relay over GRE Encapsulation\n"
+	"Enable or disable Frame Relay over GRE\n"
+	"Enable\n" "Disable\n")
+{
+	int enabled = atoi(argv[0]);
+
+	priv.frgre = enabled;
+
+	return CMD_SUCCESS;
+}
+
+/* TODO: allow vty to reset/block/unblock nsvc/nsei */
+
+/* TODO: add filter for NSEI as ns1 code does */
+/* TODO: add filter for single connection by description */
+DEFUN(logging_fltr_nsvc,
+      logging_fltr_nsvc_cmd,
+      "logging filter nsvc nsvci <0-65535>",
+	LOGGING_STR FILTER_STR
+	"Filter based on NS Virtual Connection\n"
+	"Identify NS-VC by NSVCI\n"
+	"Numeric identifier\n")
+{
+	struct log_target *tgt;
+	struct gprs_ns2_vc *nsvc;
+	uint16_t id = atoi(argv[1]);
+
+	log_tgt_mutex_lock();
+	tgt = osmo_log_vty2tgt(vty);
+	if (!tgt) {
+		log_tgt_mutex_unlock();
+		return CMD_WARNING;
+	}
+
+	nsvc = gprs_ns2_nsvc_by_nsvci(vty_nsi, id);
+	if (!nsvc) {
+		vty_out(vty, "No NS-VC by that identifier%s", VTY_NEWLINE);
+		log_tgt_mutex_unlock();
+		return CMD_WARNING;
+	}
+
+	log_set_nsvc_filter(tgt, nsvc);
+	log_tgt_mutex_unlock();
+	return CMD_SUCCESS;
+}
+
+int gprs_ns2_vty_init(struct gprs_ns2_inst *nsi)
+{
+	static bool vty_elements_installed = false;
+
+	vty_nsi = nsi;
+	memset(&priv, 0, sizeof(struct ns2_vty_priv));
+	INIT_LLIST_HEAD(&priv.vtyvc);
+	priv.vc_mode = NS2_VC_MODE_BLOCKRESET;
+
+	/* Regression test code may call this function repeatedly, so make sure
+	 * that VTY elements are not duplicated, which would assert. */
+	if (vty_elements_installed)
+		return 0;
+	vty_elements_installed = true;
+
+	install_element_ve(&show_ns_cmd);
+	install_element_ve(&show_ns_stats_cmd);
+	install_element_ve(&show_ns_pers_cmd);
+	install_element_ve(&show_nse_cmd);
+	install_element_ve(&logging_fltr_nsvc_cmd);
+
+	install_element(CFG_LOG_NODE, &logging_fltr_nsvc_cmd);
+
+	install_element(CONFIG_NODE, &cfg_ns_cmd);
+	install_node(&ns_node, config_write_ns);
+	install_element(L_NS_NODE, &cfg_nse_nsvci_cmd);
+	install_element(L_NS_NODE, &cfg_nse_remoteip_cmd);
+	install_element(L_NS_NODE, &cfg_nse_remoteport_cmd);
+	install_element(L_NS_NODE, &cfg_nse_fr_dlci_cmd);
+	install_element(L_NS_NODE, &cfg_nse_encaps_cmd);
+	install_element(L_NS_NODE, &cfg_nse_remoterole_cmd);
+	install_element(L_NS_NODE, &cfg_no_nse_cmd);
+	install_element(L_NS_NODE, &cfg_ns_timer_cmd);
+	install_element(L_NS_NODE, &cfg_nsip_local_ip_cmd);
+	install_element(L_NS_NODE, &cfg_nsip_local_port_cmd);
+	install_element(L_NS_NODE, &cfg_nsip_dscp_cmd);
+	install_element(L_NS_NODE, &cfg_nsip_res_block_unblock_cmd);
+	install_element(L_NS_NODE, &cfg_frgre_enable_cmd);
+	install_element(L_NS_NODE, &cfg_frgre_local_ip_cmd);
+
+	/* TODO: nsvc/nsei command to reset states or reset/block/unblock nsei/nsvcs */
+
+	return 0;
+}
+
+/*!
+ * \brief gprs_ns2_vty_create parse the vty tree into ns nodes
+ * It has to be in different steps to ensure the bind is created before creating VCs.
+ * \return 0 on success
+ */
+int gprs_ns2_vty_create() {
+	struct ns2_vty_vc *vtyvc;
+	struct gprs_ns2_vc_bind *bind;
+	struct gprs_ns2_nse *nse;
+	struct gprs_ns2_vc *nsvc;
+	struct osmo_sockaddr sockaddr;
+
+	if (!vty_nsi)
+		return -1;
+
+	/* create binds, only support a single bind. either FR or UDP */
+	if (priv.frgre) {
+		/* TODO not yet supported !*/
+		return -1;
+	} else {
+		/* UDP */
+		osmo_sockaddr_str_to_sockaddr(&priv.udp, &sockaddr.u.sas);
+		gprs_ns2_ip_bind(vty_nsi, &sockaddr, priv.dscp, &bind);
+		if (!bind) {
+			/* TODO: could not bind on the specific address */
+			return -1;
+		}
+		gprs_ns2_bind_set_mode(bind, priv.vc_mode);
+	}
+
+	/* create vcs */
+	llist_for_each_entry(vtyvc, &priv.vtyvc, list) {
+		if (strlen(vtyvc->remote.ip) == 0) {
+			/* Invalid IP for VC */
+			continue;
+		}
+
+		if (!vtyvc->remote.port) {
+			/* Invalid port for VC */
+			continue;
+		}
+
+		if (osmo_sockaddr_str_to_sockaddr(&vtyvc->remote, &sockaddr.u.sas)) {
+			/* Invalid sockaddr for VC */
+			continue;
+		}
+
+		nse = gprs_ns2_nse_by_nsei(vty_nsi, vtyvc->nsei);
+		if (!nse) {
+			nse = gprs_ns2_create_nse(vty_nsi, vtyvc->nsei);
+			if (!nse) {
+				/* Could not create NSE for VTY */
+				continue;
+			}
+		}
+		nse->persistent = true;
+
+		if (bind) {
+			nsvc = gprs_ns2_ip_connect(bind,
+						   &sockaddr,
+						   nse,
+						   vtyvc->nsvci);
+			if (!nsvc) {
+				/* Could not create NSVC, connect failed */
+				continue;
+			}
+			nsvc->persistent = true;
+		}
+	}
+
+
+	return 0;
+}
+
+/*!
+ * \brief ns2_vty_bind_apply will be called when a new bind is created to apply vty settings
+ * \param bind
+ * \return
+ */
+void ns2_vty_bind_apply(struct gprs_ns2_vc_bind *bind)
+{
+	gprs_ns2_bind_set_mode(bind, priv.vc_mode);
+}
+
+/*!
+ * \brief ns2_vty_force_vc_mode force a mode and prevents the vty from overwriting it.
+ * \param force if true mode and reason will be set. false to allow modification via vty.
+ * \param mode
+ * \param reason A description shown to the user when a vty command wants to change the mode.
+ */
+void gprs_ns2_vty_force_vc_mode(bool force, enum gprs_ns2_vc_mode mode, char *reason)
+{
+	priv.force_vc_mode = force;
+
+	if (force) {
+		priv.vc_mode = mode;
+		priv.force_vc_mode_reason = reason;
+	}
+}
diff --git a/src/gb/libosmogb.map b/src/gb/libosmogb.map
index 0c0c5c4..d08e85e 100644
--- a/src/gb/libosmogb.map
+++ b/src/gb/libosmogb.map
@@ -72,6 +72,40 @@
 gprs_ns_ll_clear;
 gprs_ns_msgb_alloc;
 
+gprs_ns2_bind_set_mode;
+gprs_ns2_cause_str;
+gprs_ns2_create_nse;
+gprs_ns2_dynamic_create_nse;
+gprs_ns2_find_vc_by_sockaddr;
+gprs_ns2_free;
+gprs_ns2_free_bind;
+gprs_ns2_free_nse;
+gprs_ns2_free_nsvc;
+gprs_ns2_frgre_bind;
+gprs_ns2_instantiate;
+gprs_ns2_ip_bind;
+gprs_ns2_ip_bind_set_dscp;
+gprs_ns2_ip_bind_sockaddr;
+gprs_ns2_ip_connect;
+gprs_ns2_ip_connect2;
+gprs_ns2_ip_connect_inactive;
+gprs_ns2_ip_connect_sns;
+gprs_ns2_ip_vc_sockaddr;
+gprs_ns2_is_frgre_bind;
+gprs_ns2_is_ip_bind;
+gprs_ns2_ll_str;
+gprs_ns2_ll_str_buf;
+gprs_ns2_ll_str_c;
+gprs_ns2_nse_by_nsei;
+gprs_ns2_nsvc_by_nsvci;
+gprs_ns2_nsvc_by_sockaddr;
+gprs_ns2_recv_prim;
+gprs_ns2_reset_persistent_nsvcs;
+gprs_ns2_start_alive_all_nsvcs;
+gprs_ns2_vty_create;
+gprs_ns2_vty_force_vc_mode;
+gprs_ns2_vty_init;
+
 gprs_nsvc_create2;
 gprs_nsvc_delete;
 gprs_nsvc_reset;
diff --git a/src/logging.c b/src/logging.c
index af64541..212b0b9 100644
--- a/src/logging.c
+++ b/src/logging.c
@@ -230,6 +230,11 @@
 		.description = "Remote SIM protocol",
 		.enabled = 1, .loglevel = LOGL_NOTICE,
 	},
+	[INT2IDX(DLNS)] = {
+		.name = "DLNS",
+		.description = "GPRS NS layer",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
 };
 
 void assert_loginfo(const char *src)