SMS over GSUP: implement vty config of SMSC routing

At the user-visible level (advanced settings menus on phones,
GSM 07.05 AT commands, SIM programming) each SMSC is identified
by a numeric address that looks like a phone number, originally
meant to be a Global Title.  OsmoMSC passes these SMSC addresses
through as-is to MO-forwardSM.req GSUP message - however, SMSCs
that connect to OsmoHLR via GSUP identify themselves by their
IPA names instead.  Hence we need a mapping mechanism in OsmoHLR
config.

To accommodate different styles of network design ranging from
strict recreation of classic GSM architecture to guest roaming
arrangements, a two-level configuration is implemented, modeled
after EUSE/USSD configuration: first one defines which SMSCs exist
as entities, identified only by their IPA names, and then one
defines which numeric SMSC address (in SM-RP-DA) should go to which
configured SMSC, with the additional possibility of a default route.

Related: OS#6135
Change-Id: I1624dcd9d22b4efca965ccdd1c74f0063a94a33c
diff --git a/src/Makefile.am b/src/Makefile.am
index 380e34a..6a3bb3f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -52,6 +52,7 @@
 	hlr_vty.c \
 	hlr_vty_subscr.c \
 	gsup_send.c \
+	hlr_sms.c \
 	hlr_ussd.c \
 	proxy.c \
 	dgsm.c \
diff --git a/src/hlr.c b/src/hlr.c
index 457850e..17acdab 100644
--- a/src/hlr.c
+++ b/src/hlr.c
@@ -750,8 +750,10 @@
 
 	g_hlr = talloc_zero(hlr_ctx, struct hlr);
 	INIT_LLIST_HEAD(&g_hlr->euse_list);
+	INIT_LLIST_HEAD(&g_hlr->smsc_list);
 	INIT_LLIST_HEAD(&g_hlr->ss_sessions);
 	INIT_LLIST_HEAD(&g_hlr->ussd_routes);
+	INIT_LLIST_HEAD(&g_hlr->smsc_routes);
 	INIT_LLIST_HEAD(&g_hlr->mslookup.server.local_site_services);
 	g_hlr->db_file_path = talloc_strdup(g_hlr, HLR_DEFAULT_DB_FILE_PATH);
 	g_hlr->mslookup.server.mdns.domain_suffix = talloc_strdup(g_hlr, OSMO_MDNS_DOMAIN_SUFFIX_DEFAULT);
diff --git a/src/hlr_sms.c b/src/hlr_sms.c
new file mode 100644
index 0000000..5866afa
--- /dev/null
+++ b/src/hlr_sms.c
@@ -0,0 +1,103 @@
+/* OsmoHLR SMS-over-GSUP routing implementation */
+
+/* Author: Mychaela N. Falconia <falcon@freecalypso.org>, 2023 - however,
+ * Mother Mychaela's contributions are NOT subject to copyright.
+ * No rights reserved, all rights relinquished.
+ *
+ * Based on earlier unmerged work by Vadim Yanitskiy, 2019.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/gsm/gsup.h>
+
+#include <osmocom/hlr/hlr.h>
+#include <osmocom/hlr/hlr_sms.h>
+#include <osmocom/hlr/gsup_server.h>
+#include <osmocom/hlr/gsup_router.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/db.h>
+
+/***********************************************************************
+ * core data structures expressing config from VTY
+ ***********************************************************************/
+
+struct hlr_smsc *smsc_find(struct hlr *hlr, const char *name)
+{
+	struct hlr_smsc *smsc;
+
+	llist_for_each_entry(smsc, &hlr->smsc_list, list) {
+		if (!strcmp(smsc->name, name))
+			return smsc;
+	}
+	return NULL;
+}
+
+struct hlr_smsc *smsc_alloc(struct hlr *hlr, const char *name)
+{
+	struct hlr_smsc *smsc = smsc_find(hlr, name);
+	if (smsc)
+		return NULL;
+
+	smsc = talloc_zero(hlr, struct hlr_smsc);
+	smsc->name = talloc_strdup(smsc, name);
+	smsc->hlr = hlr;
+	llist_add_tail(&smsc->list, &hlr->smsc_list);
+
+	return smsc;
+}
+
+void smsc_free(struct hlr_smsc *smsc)
+{
+	llist_del(&smsc->list);
+	talloc_free(smsc);
+}
+
+struct hlr_smsc_route *smsc_route_find(struct hlr *hlr, const char *num_addr)
+{
+	struct hlr_smsc_route *rt;
+
+	llist_for_each_entry(rt, &hlr->smsc_routes, list) {
+		if (!strcmp(rt->num_addr, num_addr))
+			return rt;
+	}
+	return NULL;
+}
+
+struct hlr_smsc_route *smsc_route_alloc(struct hlr *hlr, const char *num_addr,
+					struct hlr_smsc *smsc)
+{
+	struct hlr_smsc_route *rt;
+
+	if (smsc_route_find(hlr, num_addr))
+		return NULL;
+
+	rt = talloc_zero(hlr, struct hlr_smsc_route);
+	rt->num_addr = talloc_strdup(rt, num_addr);
+	rt->smsc = smsc;
+	llist_add_tail(&rt->list, &hlr->smsc_routes);
+
+	return rt;
+}
+
+void smsc_route_free(struct hlr_smsc_route *rt)
+{
+	llist_del(&rt->list);
+	talloc_free(rt);
+}
diff --git a/src/hlr_vty.c b/src/hlr_vty.c
index f8cf852..c4e99e2 100644
--- a/src/hlr_vty.c
+++ b/src/hlr_vty.c
@@ -44,6 +44,7 @@
 #include <osmocom/hlr/hlr_vty.h>
 #include <osmocom/hlr/hlr_vty_subscr.h>
 #include <osmocom/hlr/hlr_ussd.h>
+#include <osmocom/hlr/hlr_sms.h>
 #include <osmocom/hlr/gsup_server.h>
 
 static const struct value_string gsm48_gmm_cause_vty_names[] = {
@@ -608,6 +609,160 @@
 	return CMD_SUCCESS;
 }
 
+/***********************************************************************
+ * Routing of SM-RL to GSUP-attached SMSCs
+ ***********************************************************************/
+
+#define SMSC_STR "Configuration of GSUP routing to SMSCs\n"
+
+struct cmd_node smsc_node = {
+	SMSC_NODE,
+	"%s(config-hlr-smsc)# ",
+	1,
+};
+
+DEFUN(cfg_smsc_entity, cfg_smsc_entity_cmd,
+	"smsc entity NAME",
+	SMSC_STR
+	"Configure a particular external SMSC\n"
+	"IPA name of the external SMSC\n")
+{
+	struct hlr_smsc *smsc;
+	const char *id = argv[0];
+
+	smsc = smsc_find(g_hlr, id);
+	if (!smsc) {
+		smsc = smsc_alloc(g_hlr, id);
+		if (!smsc)
+			return CMD_WARNING;
+	}
+	vty->index = smsc;
+	vty->index_sub = &smsc->description;
+	vty->node = SMSC_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_smsc_entity, cfg_no_smsc_entity_cmd,
+	"no smsc entity NAME",
+	NO_STR SMSC_STR "Remove a particular external SMSC\n"
+	"IPA name of the external SMSC\n")
+{
+	struct hlr_smsc *smsc = smsc_find(g_hlr, argv[0]);
+	if (!smsc) {
+		vty_out(vty, "%% Cannot remove non-existent SMSC %s%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (g_hlr->smsc_default == smsc) {
+		vty_out(vty,
+			"%% Cannot remove SMSC %s, it is the default route%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	smsc_free(smsc);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_smsc_route, cfg_smsc_route_cmd,
+	"smsc route NUMBER NAME",
+	SMSC_STR
+	"Configure GSUP route to a particular SMSC\n"
+	"Numeric address of this SMSC, must match EF.SMSP programming in SIMs\n"
+	"IPA name of the external SMSC\n")
+{
+	struct hlr_smsc *smsc = smsc_find(g_hlr, argv[1]);
+	struct hlr_smsc_route *rt = smsc_route_find(g_hlr, argv[0]);
+	if (rt) {
+		vty_out(vty,
+			"%% Cannot add [another?] route for SMSC address %s%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!smsc) {
+		vty_out(vty, "%% Cannot find SMSC '%s'%s", argv[1],
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	smsc_route_alloc(g_hlr, argv[0], smsc);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_smsc_route, cfg_no_smsc_route_cmd,
+	"no smsc route NUMBER",
+	NO_STR SMSC_STR "Remove GSUP route to a particular SMSC\n"
+	"Numeric address of the SMSC\n")
+{
+	struct hlr_smsc_route *rt = smsc_route_find(g_hlr, argv[0]);
+	if (!rt) {
+		vty_out(vty, "%% Cannot find route for SMSC address %s%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	smsc_route_free(rt);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_smsc_defroute, cfg_smsc_defroute_cmd,
+	"smsc default-route NAME",
+	SMSC_STR
+	"Configure default SMSC route for unknown SMSC numeric addresses\n"
+	"IPA name of the external SMSC\n")
+{
+	struct hlr_smsc *smsc;
+
+	smsc = smsc_find(g_hlr, argv[0]);
+	if (!smsc) {
+		vty_out(vty, "%% Cannot find SMSC %s%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (g_hlr->smsc_default != smsc) {
+		vty_out(vty, "Switching default route from %s to %s%s",
+			g_hlr->smsc_default ? g_hlr->smsc_default->name : "<none>",
+			smsc->name, VTY_NEWLINE);
+		g_hlr->smsc_default = smsc;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_smsc_defroute, cfg_no_smsc_defroute_cmd,
+	"no smsc default-route",
+	NO_STR SMSC_STR
+	"Remove default SMSC route for unknown SMSC numeric addresses\n")
+{
+	g_hlr->smsc_default = NULL;
+
+	return CMD_SUCCESS;
+}
+
+static void dump_one_smsc(struct vty *vty, struct hlr_smsc *smsc)
+{
+	vty_out(vty, " smsc entity %s%s", smsc->name, VTY_NEWLINE);
+}
+
+static int config_write_smsc(struct vty *vty)
+{
+	struct hlr_smsc *smsc;
+	struct hlr_smsc_route *rt;
+
+	llist_for_each_entry(smsc, &g_hlr->smsc_list, list)
+		dump_one_smsc(vty, smsc);
+
+	llist_for_each_entry(rt, &g_hlr->smsc_routes, list) {
+		vty_out(vty, " smsc route %s %s%s", rt->num_addr,
+			rt->smsc->name, VTY_NEWLINE);
+	}
+
+	if (g_hlr->smsc_default)
+		vty_out(vty, " smsc default-route %s%s",
+			g_hlr->smsc_default->name, VTY_NEWLINE);
+
+	return 0;
+}
 
 DEFUN(cfg_reject_cause, cfg_reject_cause_cmd,
       "reject-cause TYPE CAUSE", "") /* Dynamically Generated */
@@ -771,6 +926,15 @@
 	install_element(HLR_NODE, &cfg_ussd_defaultroute_cmd);
 	install_element(HLR_NODE, &cfg_ussd_no_defaultroute_cmd);
 	install_element(HLR_NODE, &cfg_ncss_guard_timeout_cmd);
+
+	install_node(&smsc_node, config_write_smsc);
+	install_element(HLR_NODE, &cfg_smsc_entity_cmd);
+	install_element(HLR_NODE, &cfg_no_smsc_entity_cmd);
+	install_element(HLR_NODE, &cfg_smsc_route_cmd);
+	install_element(HLR_NODE, &cfg_no_smsc_route_cmd);
+	install_element(HLR_NODE, &cfg_smsc_defroute_cmd);
+	install_element(HLR_NODE, &cfg_no_smsc_defroute_cmd);
+
 	install_element(HLR_NODE, &cfg_reject_cause_cmd);
 	install_element(HLR_NODE, &cfg_store_imei_cmd);
 	install_element(HLR_NODE, &cfg_no_store_imei_cmd);