Gb: add a second NS implementation

Reimplement NS with FSM.

Change-Id: I3525beef205588dfab9d3880a34115f1a2676e48
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);
+}