sgsn: Add functions to handle APN contexts

This commit adds the exported functions apn_ctx_find_alloc,
apn_ctx_free, apn_ctx_by_name, and apn_ctx_match to manage and
retrieve APN to GGSN mappings.

The following VTY commands are added to 'config-sgsn':

 - apn APN ggsn <0-255>
 - apn APN imsi-prefix PREFIX ggsn <0-255>

which maps an APN gateway string to an SGSN id. The SGSN must be
configured in advance. When matching an APN string, entries with a
leading '*' are used for suffix matching, otherwise an exact match is
done.  When a prefix is given, it is matched against the IMSI. If
several entries match, a longer matching IMSI prefix has precedence.
If there are several matching entries with the same PREFIX, the entry
with longest matching APN is returned.

Ticket: OW#1334
Sponsored-by: On-Waves ehf
diff --git a/openbsc/include/openbsc/gprs_sgsn.h b/openbsc/include/openbsc/gprs_sgsn.h
index 516b6cd..b0d7c32 100644
--- a/openbsc/include/openbsc/gprs_sgsn.h
+++ b/openbsc/include/openbsc/gprs_sgsn.h
@@ -239,9 +239,15 @@
 	struct llist_head list;
 	struct sgsn_ggsn_ctx *ggsn;
 	char *name;
+	char *imsi_prefix;
 	char *description;
 };
 
+struct apn_ctx *sgsn_apn_ctx_find_alloc(const char *name, const char *imsi_prefix);
+void sgsn_apn_ctx_free(struct apn_ctx *actx);
+struct apn_ctx *sgsn_apn_ctx_by_name(const char *name, const char *imsi_prefix);
+struct apn_ctx *sgsn_apn_ctx_match(const char *name, const char *imsi_prefix);
+
 extern struct llist_head sgsn_mm_ctxts;
 extern struct llist_head sgsn_ggsn_ctxts;
 extern struct llist_head sgsn_apn_ctxts;
diff --git a/openbsc/src/gprs/gprs_sgsn.c b/openbsc/src/gprs/gprs_sgsn.c
index 555be57..54fe15c 100644
--- a/openbsc/src/gprs/gprs_sgsn.c
+++ b/openbsc/src/gprs/gprs_sgsn.c
@@ -387,41 +387,100 @@
 
 /* APN contexts */
 
-#if 0
-struct apn_ctx *apn_ctx_alloc(const char *ap_name)
+static struct apn_ctx *sgsn_apn_ctx_alloc(const char *ap_name, const char *imsi_prefix)
 {
 	struct apn_ctx *actx;
 
-	actx = talloc_zero(talloc_bsc_ctx, struct apn_ctx);
+	actx = talloc_zero(tall_bsc_ctx, struct apn_ctx);
 	if (!actx)
 		return NULL;
 	actx->name = talloc_strdup(actx, ap_name);
+	actx->imsi_prefix = talloc_strdup(actx, imsi_prefix);
+
+	llist_add_tail(&actx->list, &sgsn_apn_ctxts);
 
 	return actx;
 }
 
-struct apn_ctx *apn_ctx_by_name(const char *name)
+void sgsn_apn_ctx_free(struct apn_ctx *actx)
+{
+	llist_del(&actx->list);
+	talloc_free(actx);
+}
+
+struct apn_ctx *sgsn_apn_ctx_match(const char *name, const char *imsi)
+{
+	struct apn_ctx *actx;
+	struct apn_ctx *found_actx = NULL;
+	size_t imsi_prio = 0;
+	size_t name_prio = 0;
+	size_t name_req_len = strlen(name);
+
+	llist_for_each_entry(actx, &sgsn_apn_ctxts, list) {
+		size_t name_ref_len, imsi_ref_len;
+		const char *name_ref_start, *name_match_start;
+
+		imsi_ref_len = strlen(actx->imsi_prefix);
+		if (strncmp(actx->imsi_prefix, imsi, imsi_ref_len) != 0)
+			continue;
+
+		if (imsi_ref_len < imsi_prio)
+			continue;
+
+		/* IMSI matches */
+
+		name_ref_start = &actx->name[0];
+		if (name_ref_start[0] == '*') {
+			/* Suffix match */
+			name_ref_start += 1;
+			name_ref_len = strlen(name_ref_start);
+			if (name_ref_len > name_req_len)
+				continue;
+		} else {
+			name_ref_len = strlen(name_ref_start);
+			if (name_ref_len != name_req_len)
+				continue;
+		}
+
+		name_match_start = name + (name_req_len - name_ref_len);
+		if (strcasecmp(name_match_start, name_ref_start) != 0)
+			continue;
+
+		/* IMSI and name match */
+
+		if (imsi_ref_len == imsi_prio && name_ref_len < name_prio)
+			/* Lower priority, skip */
+			continue;
+
+		imsi_prio = imsi_ref_len;
+		name_prio = name_ref_len;
+		found_actx = actx;
+	}
+	return found_actx;
+}
+
+struct apn_ctx *sgsn_apn_ctx_by_name(const char *name, const char *imsi_prefix)
 {
 	struct apn_ctx *actx;
 
 	llist_for_each_entry(actx, &sgsn_apn_ctxts, list) {
-		if (!strcmp(name, actx->name))
+		if (strcasecmp(name, actx->name) == 0 &&
+		    strcasecmp(imsi_prefix, actx->imsi_prefix) == 0)
 			return actx;
 	}
 	return NULL;
 }
 
-struct apn_ctx *apn_ctx_find_alloc(const char *name)
+struct apn_ctx *sgsn_apn_ctx_find_alloc(const char *name, const char *imsi_prefix)
 {
 	struct apn_ctx *actx;
 
-	actx = apn_ctx_by_name(name);
+	actx = sgsn_apn_ctx_by_name(name, imsi_prefix);
 	if (!actx)
-		actx = apn_ctx_alloc(name);
+		actx = sgsn_apn_ctx_alloc(name, imsi_prefix);
 
 	return actx;
 }
-#endif
 
 uint32_t sgsn_alloc_ptmsi(void)
 {
diff --git a/openbsc/src/gprs/sgsn_vty.c b/openbsc/src/gprs/sgsn_vty.c
index 81b9d7f..d85ea01 100644
--- a/openbsc/src/gprs/sgsn_vty.c
+++ b/openbsc/src/gprs/sgsn_vty.c
@@ -126,6 +126,7 @@
 {
 	struct sgsn_ggsn_ctx *gctx;
 	struct imsi_acl_entry *acl;
+	struct apn_ctx *actx;
 
 	vty_out(vty, "sgsn%s", VTY_NEWLINE);
 
@@ -151,6 +152,18 @@
 	llist_for_each_entry(acl, &g_cfg->imsi_acl, list)
 		vty_out(vty, " imsi-acl add %s%s", acl->imsi, VTY_NEWLINE);
 
+	if (llist_empty(&sgsn_apn_ctxts))
+		vty_out(vty, " ! apn * ggsn 0%s", VTY_NEWLINE);
+	llist_for_each_entry(actx, &sgsn_apn_ctxts, list) {
+		if (strlen(actx->imsi_prefix) > 0)
+			vty_out(vty, " apn %s imsi-prefix %s ggsn %d%s",
+				actx->name, actx->imsi_prefix, actx->ggsn->id,
+				VTY_NEWLINE);
+		else
+			vty_out(vty, " apn %s ggsn %d%s", actx->name,
+				actx->ggsn->id, VTY_NEWLINE);
+	}
+
 	return CMD_SUCCESS;
 }
 
@@ -216,14 +229,55 @@
 	return CMD_SUCCESS;
 }
 
-#if 0
+#define APN_STR	"Configure the information per APN\n"
+#define APN_GW_STR "The APN gateway name optionally prefixed by '*' (wildcard)\n"
+
+static int add_apn_ggsn_mapping(struct vty *vty, const char *apn_str,
+				const char *imsi_prefix, int ggsn_id)
+{
+	struct apn_ctx *actx;
+	struct sgsn_ggsn_ctx *ggsn;
+
+	ggsn = sgsn_ggsn_ctx_by_id(ggsn_id);
+	if (ggsn == NULL) {
+		vty_out(vty, "%% a GGSN with id %d has not been defined%s",
+			ggsn_id, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	actx = sgsn_apn_ctx_find_alloc(apn_str, imsi_prefix);
+	if (!actx) {
+		vty_out(vty, "%% unable to create APN context for %s/%s%s",
+			apn_str, imsi_prefix, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	actx->ggsn = ggsn;
+
+	return CMD_SUCCESS;
+}
+
 DEFUN(cfg_apn_ggsn, cfg_apn_ggsn_cmd,
 	"apn APNAME ggsn <0-255>",
-	"")
+	APN_STR APN_GW_STR
+	"Select the GGSN to use when the APN gateway prefix matches\n"
+	"The GGSN id")
 {
-	struct apn_ctx **
+
+	return add_apn_ggsn_mapping(vty, argv[0], "", atoi(argv[1]));
 }
-#endif
+
+DEFUN(cfg_apn_imsi_ggsn, cfg_apn_imsi_ggsn_cmd,
+	"apn APNAME imsi-prefix IMSIPRE ggsn <0-255>",
+	APN_STR APN_GW_STR
+	"Restrict rule to a certain IMSI prefix\n"
+	"An IMSI prefix\n"
+	"Select the GGSN to use when APN gateway and IMSI prefix match\n"
+	"The GGSN id")
+{
+
+	return add_apn_ggsn_mapping(vty, argv[0], argv[1], atoi(argv[2]));
+}
 
 const struct value_string gprs_mm_st_strs[] = {
 	{ GMM_DEREGISTERED, "DEREGISTERED" },
@@ -757,6 +811,8 @@
 	install_element(SGSN_NODE, &cfg_auth_policy_cmd);
 	install_element(SGSN_NODE, &cfg_gsup_remote_ip_cmd);
 	install_element(SGSN_NODE, &cfg_gsup_remote_port_cmd);
+	install_element(SGSN_NODE, &cfg_apn_ggsn_cmd);
+	install_element(SGSN_NODE, &cfg_apn_imsi_ggsn_cmd);
 
 	return 0;
 }
diff --git a/openbsc/tests/sgsn/sgsn_test.c b/openbsc/tests/sgsn/sgsn_test.c
index 7a14cde..5fa33a2 100644
--- a/openbsc/tests/sgsn/sgsn_test.c
+++ b/openbsc/tests/sgsn/sgsn_test.c
@@ -1784,6 +1784,98 @@
 	sgsn->cfg.auth_policy = saved_auth_policy;
 }
 
+static void test_apn_matching(void)
+{
+	struct apn_ctx *actx, *actxs[9];
+
+	printf("Testing APN matching\n");
+
+	actxs[0] = sgsn_apn_ctx_find_alloc("*.test", "");
+	actxs[1] = sgsn_apn_ctx_find_alloc("*.def.test", "");
+	actxs[2] = sgsn_apn_ctx_find_alloc("abc.def.test", "");
+	actxs[3] = NULL;
+
+	actxs[4] = sgsn_apn_ctx_find_alloc("abc.def.test", "456");
+	actxs[5] = sgsn_apn_ctx_find_alloc("abc.def.test", "456123");
+	actxs[6] = sgsn_apn_ctx_find_alloc("*.def.test", "456");
+	actxs[7] = sgsn_apn_ctx_find_alloc("*.def.test", "456123");
+
+	actxs[8] = sgsn_apn_ctx_find_alloc("ghi.def.test", "456");
+
+	actx = sgsn_apn_ctx_match("abc.def.test", "12345678");
+	OSMO_ASSERT(actx == actxs[2]);
+	actx = sgsn_apn_ctx_match("aBc.dEf.test", "12345678");
+	OSMO_ASSERT(actx == actxs[2]);
+	actx = sgsn_apn_ctx_match("xyz.def.test", "12345678");
+	OSMO_ASSERT(actx == actxs[1]);
+	actx = sgsn_apn_ctx_match("xyz.dEf.test", "12345678");
+	OSMO_ASSERT(actx == actxs[1]);
+	actx = sgsn_apn_ctx_match("xyz.uvw.test", "12345678");
+	OSMO_ASSERT(actx == actxs[0]);
+	actx = sgsn_apn_ctx_match("xyz.uvw.foo", "12345678");
+	OSMO_ASSERT(actx == NULL);
+
+	actxs[3] = sgsn_apn_ctx_find_alloc("*", "");
+	actx = sgsn_apn_ctx_match("xyz.uvw.foo", "12345678");
+	OSMO_ASSERT(actx == actxs[3]);
+
+	actx = sgsn_apn_ctx_match("abc.def.test", "45699900");
+	OSMO_ASSERT(actx == actxs[4]);
+
+	actx = sgsn_apn_ctx_match("xyz.def.test", "45699900");
+	OSMO_ASSERT(actx == actxs[6]);
+
+	actx = sgsn_apn_ctx_match("abc.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[5]);
+
+	actx = sgsn_apn_ctx_match("xyz.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[7]);
+
+	actx = sgsn_apn_ctx_match("ghi.def.test", "45699900");
+	OSMO_ASSERT(actx == actxs[8]);
+
+	actx = sgsn_apn_ctx_match("ghi.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[7]);
+
+	/* Free APN contexts and check how the matching changes */
+
+	sgsn_apn_ctx_free(actxs[7]);
+	actx = sgsn_apn_ctx_match("ghi.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[8]);
+
+	sgsn_apn_ctx_free(actxs[8]);
+	actx = sgsn_apn_ctx_match("ghi.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[6]);
+
+	sgsn_apn_ctx_free(actxs[6]);
+	actx = sgsn_apn_ctx_match("ghi.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[1]);
+
+	sgsn_apn_ctx_free(actxs[5]);
+	actx = sgsn_apn_ctx_match("abc.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[4]);
+
+	sgsn_apn_ctx_free(actxs[4]);
+	actx = sgsn_apn_ctx_match("abc.def.test", "45612300");
+	OSMO_ASSERT(actx == actxs[2]);
+
+	sgsn_apn_ctx_free(actxs[2]);
+	actx = sgsn_apn_ctx_match("abc.def.test", "12345678");
+	OSMO_ASSERT(actx == actxs[1]);
+
+	sgsn_apn_ctx_free(actxs[1]);
+	actx = sgsn_apn_ctx_match("abc.def.test", "12345678");
+	OSMO_ASSERT(actx == actxs[0]);
+
+	sgsn_apn_ctx_free(actxs[0]);
+	actx = sgsn_apn_ctx_match("abc.def.test", "12345678");
+	OSMO_ASSERT(actx == actxs[3]);
+
+	sgsn_apn_ctx_free(actxs[3]);
+	actx = sgsn_apn_ctx_match("abc.def.test", "12345678");
+	OSMO_ASSERT(actx == NULL);
+}
+
 static struct log_info_cat gprs_categories[] = {
 	[DMM] = {
 		.name = "DMM",
@@ -1871,6 +1963,7 @@
 	test_gmm_reject();
 	test_gmm_cancel();
 	test_gmm_ptmsi_allocation();
+	test_apn_matching();
 	printf("Done\n");
 
 	talloc_report_full(osmo_sgsn_ctx, stderr);
diff --git a/openbsc/tests/sgsn/sgsn_test.ok b/openbsc/tests/sgsn/sgsn_test.ok
index e5df504..9f14721 100644
--- a/openbsc/tests/sgsn/sgsn_test.ok
+++ b/openbsc/tests/sgsn/sgsn_test.ok
@@ -26,4 +26,5 @@
   - sgsn_alloc_ptmsi
   - Repeated Attach Request
   - Repeated RA Update Request
+Testing APN matching
 Done
diff --git a/openbsc/tests/vty_test_runner.py b/openbsc/tests/vty_test_runner.py
index d87ebde..cae1c14 100644
--- a/openbsc/tests/vty_test_runner.py
+++ b/openbsc/tests/vty_test_runner.py
@@ -794,6 +794,27 @@
         res = self.vty.command('show subscriber cache')
         self.assert_(res.find('1234567890') < 0)
 
+    def testVtyGgsn(self):
+        self.vty.enable()
+        self.assertTrue(self.vty.verify('configure terminal', ['']))
+        self.assertEquals(self.vty.node(), 'config')
+        self.assertTrue(self.vty.verify('sgsn', ['']))
+        self.assertEquals(self.vty.node(), 'config-sgsn')
+        self.assertTrue(self.vty.verify('ggsn 0 remote-ip 127.99.99.99', ['']))
+        self.assertTrue(self.vty.verify('ggsn 0 gtp-version 1', ['']))
+        self.assertTrue(self.vty.verify('apn * ggsn 0', ['']))
+        self.assertTrue(self.vty.verify('apn apn1.test ggsn 0', ['']))
+        self.assertTrue(self.vty.verify('apn apn1.test ggsn 1', ['% a GGSN with id 1 has not been defined']))
+        self.assertTrue(self.vty.verify('apn apn1.test imsi-prefix 123456 ggsn 0', ['']))
+        self.assertTrue(self.vty.verify('apn apn2.test imsi-prefix 123456 ggsn 0', ['']))
+        res = self.vty.command("show running-config")
+        self.assert_(res.find('ggsn 0 remote-ip 127.99.99.99') >= 0)
+        self.assert_(res.find('ggsn 0 gtp-version 1') >= 0)
+        self.assert_(res.find('apn * ggsn 0') >= 0)
+        self.assert_(res.find('apn apn1.test ggsn 0') >= 0)
+        self.assert_(res.find('apn apn1.test imsi-prefix 123456 ggsn 0') >= 0)
+        self.assert_(res.find('apn apn2.test imsi-prefix 123456 ggsn 0') >= 0)
+
 def add_nat_test(suite, workdir):
     if not os.path.isfile(os.path.join(workdir, "src/osmo-bsc_nat/osmo-bsc_nat")):
         print("Skipping the NAT test")