D-GSM 3/n: implement roaming by mslookup in osmo-hlr

Add mslookup client to find remote home HLRs of unknown IMSIs, and
proxy/forward GSUP for those to the right remote HLR instances.

Add remote_hlr.c to manage one GSUP client per remote HLR GSUP address.

Add proxy.c to keep state about remotely handled IMSIs (remote GSUP address,
MSISDN, and probably more in future patches).  The mslookup_server that
determines whether a given MSISDN is attached locally now also needs to look in
the proxy record: it is always the osmo-hlr immediately peering for the MSC
that should respond to mslookup service address queries like SIP and SMPP.
(Only gsup.hlr service is always answered by the home HLR.)

Add dgsm.c to set up an mdns mslookup client, ask for IMSI homes, and to decide
which GSUP is handled locally and which needs to go to a remote HLR.

Add full VTY config and VTY tests.

For a detailed overview of the D-GSM and mslookup related files, please see the
elaborate comment at the top of mslookup.c (already added in an earlier patch).

Change-Id: I2fe453553c90e6ee527ed13a13089900efd488aa
diff --git a/src/dgsm.c b/src/dgsm.c
new file mode 100644
index 0000000..bfa5df8
--- /dev/null
+++ b/src/dgsm.c
@@ -0,0 +1,247 @@
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * 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 <errno.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/mslookup/mslookup_client.h>
+#include <osmocom/mslookup/mslookup_client_mdns.h>
+#include <osmocom/gsupclient/gsup_client.h>
+#include <osmocom/gsupclient/cni_peer_id.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/hlr.h>
+#include <osmocom/hlr/db.h>
+#include <osmocom/hlr/gsup_router.h>
+#include <osmocom/hlr/gsup_server.h>
+#include <osmocom/hlr/dgsm.h>
+#include <osmocom/hlr/proxy.h>
+#include <osmocom/hlr/remote_hlr.h>
+#include <osmocom/hlr/mslookup_server.h>
+#include <osmocom/hlr/mslookup_server_mdns.h>
+#include <osmocom/hlr/dgsm.h>
+
+void *dgsm_ctx = NULL;
+
+static void resolve_hlr_result_cb(struct osmo_mslookup_client *client,
+				  uint32_t request_handle,
+				  const struct osmo_mslookup_query *query,
+				  const struct osmo_mslookup_result *result)
+{
+	struct proxy *proxy = g_hlr->gs->proxy;
+	struct proxy_subscr proxy_subscr;
+	const struct osmo_sockaddr_str *remote_hlr_addr;
+
+	/* A remote HLR is answering back, indicating that it is the home HLR for a given IMSI.
+	 * There should be a mostly empty proxy entry for that IMSI.
+	 * Add the remote address data in the proxy. */
+	if (query->id.type != OSMO_MSLOOKUP_ID_IMSI) {
+		LOGP(DDGSM, LOGL_ERROR, "Expected IMSI ID type in mslookup query+result: %s\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, result));
+		return;
+	}
+
+	if (result->rc != OSMO_MSLOOKUP_RC_RESULT) {
+		LOG_DGSM(query->id.imsi, LOGL_ERROR, "Failed to resolve remote HLR: %s\n",
+			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
+		proxy_subscr_del(proxy, query->id.imsi);
+		return;
+	}
+
+	if (osmo_sockaddr_str_is_nonzero(&result->host_v4))
+		remote_hlr_addr = &result->host_v4;
+	else if (osmo_sockaddr_str_is_nonzero(&result->host_v6))
+		remote_hlr_addr = &result->host_v6;
+	else {
+		LOG_DGSM(query->id.imsi, LOGL_ERROR, "Invalid address for remote HLR: %s\n",
+			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
+		proxy_subscr_del(proxy, query->id.imsi);
+		return;
+	}
+
+	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, query->id.imsi)) {
+		LOG_DGSM(query->id.imsi, LOGL_ERROR, "No proxy entry for mslookup result: %s\n",
+			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
+		return;
+	}
+
+	proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, remote_hlr_addr);
+}
+
+/* Return true when the message has been handled by D-GSM. */
+bool dgsm_check_forward_gsup_msg(struct osmo_gsup_req *req)
+{
+	struct proxy_subscr proxy_subscr;
+	struct proxy *proxy = g_hlr->gs->proxy;
+	struct osmo_mslookup_query query;
+	struct osmo_mslookup_query_handling handling;
+	uint32_t request_handle;
+
+	/* If the IMSI is known in the local HLR, then we won't proxy. */
+	if (db_subscr_exists_by_imsi(g_hlr->dbc, req->gsup.imsi) == 0)
+		return false;
+
+	/* Are we already forwarding this IMSI to a remote HLR? */
+	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi) == 0) {
+		proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req);
+		return true;
+	}
+
+	/* The IMSI is not known locally, so we want to proxy to a remote HLR, but no proxy entry exists yet. We need to
+	 * look up the subscriber in remote HLRs via D-GSM mslookup, forward GSUP and reply once a result is back from
+	 * there.  Defer message and kick off MS lookup. */
+
+	/* Add a proxy entry without a remote address to indicate that we are busy querying for a remote HLR. */
+	proxy_subscr = (struct proxy_subscr){};
+	OSMO_STRLCPY_ARRAY(proxy_subscr.imsi, req->gsup.imsi);
+	if (proxy_subscr_create_or_update(proxy, &proxy_subscr)) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Failed to create proxy entry\n");
+		return true;
+	}
+
+	/* Is a fixed gateway proxy configured? */
+	if (osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy)) {
+		proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, &g_hlr->mslookup.client.gsup_gateway_proxy);
+
+		/* Proxy database modified, update info */
+		if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi)) {
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Internal proxy error\n");
+			return true;
+		}
+
+		proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req);
+		return true;
+	}
+
+	/* Kick off an mslookup for the remote HLR?  This check could be up first on the top, but do it only now so that
+	 * if the mslookup client disconnected, we still continue to service open proxy entries. */
+	if (!osmo_mslookup_client_active(g_hlr->mslookup.client.client)) {
+		LOG_GSUP_REQ(req, LOGL_DEBUG, "mslookup client not running, cannot query remote home HLR\n");
+		return false;
+	}
+
+	/* First spool message, then kick off mslookup. If the proxy denies this message type, then don't do anything. */
+	if (proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req)) {
+		/* If the proxy denied forwarding, an error response was already generated. */
+		return true;
+	}
+
+	query = (struct osmo_mslookup_query){
+		.id = {
+			.type = OSMO_MSLOOKUP_ID_IMSI,
+		},
+	};
+	OSMO_STRLCPY_ARRAY(query.id.imsi, req->gsup.imsi);
+	OSMO_STRLCPY_ARRAY(query.service, OSMO_MSLOOKUP_SERVICE_HLR_GSUP);
+	handling = (struct osmo_mslookup_query_handling){
+		.min_wait_milliseconds = g_hlr->mslookup.client.result_timeout_milliseconds,
+		.result_cb = resolve_hlr_result_cb,
+	};
+	request_handle = osmo_mslookup_client_request(g_hlr->mslookup.client.client, &query, &handling);
+	if (!request_handle) {
+		LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Error dispatching mslookup query for home HLR: %s\n",
+			 osmo_mslookup_result_name_c(OTC_SELECT, &query, NULL));
+		proxy_subscr_del(proxy, req->gsup.imsi);
+		/* mslookup seems to not be working. Try handling it locally. */
+		return false;
+	}
+
+	return true;
+}
+
+void dgsm_init(void *ctx)
+{
+	dgsm_ctx = talloc_named_const(ctx, 0, "dgsm");
+	INIT_LLIST_HEAD(&g_hlr->mslookup.server.local_site_services);
+
+	g_hlr->mslookup.server.local_attach_max_age = 60 * 60;
+
+	g_hlr->mslookup.client.result_timeout_milliseconds = 2000;
+
+	g_hlr->gsup_unit_name.unit_name = "HLR";
+	g_hlr->gsup_unit_name.serno = "unnamed-HLR";
+	g_hlr->gsup_unit_name.swversion = PACKAGE_NAME "-" PACKAGE_VERSION;
+
+	osmo_sockaddr_str_from_str(&g_hlr->mslookup.server.mdns.bind_addr,
+				   OSMO_MSLOOKUP_MDNS_IP4, OSMO_MSLOOKUP_MDNS_PORT);
+	osmo_sockaddr_str_from_str(&g_hlr->mslookup.client.mdns.query_addr,
+				   OSMO_MSLOOKUP_MDNS_IP4, OSMO_MSLOOKUP_MDNS_PORT);
+}
+
+void dgsm_start(void *ctx)
+{
+	g_hlr->mslookup.client.client = osmo_mslookup_client_new(dgsm_ctx);
+	OSMO_ASSERT(g_hlr->mslookup.client.client);
+	g_hlr->mslookup.allow_startup = true;
+	mslookup_server_mdns_config_apply();
+	dgsm_mdns_client_config_apply();
+}
+
+void dgsm_stop()
+{
+	g_hlr->mslookup.allow_startup = false;
+	mslookup_server_mdns_config_apply();
+	dgsm_mdns_client_config_apply();
+}
+
+void dgsm_mdns_client_config_apply(void)
+{
+	/* Check whether to start/stop/restart mDNS client */
+	const struct osmo_sockaddr_str *current_bind_addr;
+	const char *current_domain_suffix;
+	current_bind_addr = osmo_mslookup_client_method_mdns_get_bind_addr(g_hlr->mslookup.client.mdns.running);
+	current_domain_suffix = osmo_mslookup_client_method_mdns_get_domain_suffix(g_hlr->mslookup.client.mdns.running);
+
+	bool should_run = g_hlr->mslookup.allow_startup
+		&& g_hlr->mslookup.client.enable && g_hlr->mslookup.client.mdns.enable;
+
+	bool should_stop = g_hlr->mslookup.client.mdns.running &&
+		(!should_run
+		 || osmo_sockaddr_str_cmp(&g_hlr->mslookup.client.mdns.query_addr,
+					  current_bind_addr)
+		 || strcmp(g_hlr->mslookup.client.mdns.domain_suffix,
+			   current_domain_suffix));
+
+	if (should_stop) {
+		osmo_mslookup_client_method_del(g_hlr->mslookup.client.client, g_hlr->mslookup.client.mdns.running);
+		g_hlr->mslookup.client.mdns.running = NULL;
+		LOGP(DDGSM, LOGL_NOTICE, "Stopped mslookup mDNS client\n");
+	}
+
+	if (should_run && !g_hlr->mslookup.client.mdns.running) {
+		g_hlr->mslookup.client.mdns.running =
+			osmo_mslookup_client_add_mdns(g_hlr->mslookup.client.client,
+						      g_hlr->mslookup.client.mdns.query_addr.ip,
+						      g_hlr->mslookup.client.mdns.query_addr.port,
+						      -1,
+						      g_hlr->mslookup.client.mdns.domain_suffix);
+		if (!g_hlr->mslookup.client.mdns.running)
+			LOGP(DDGSM, LOGL_ERROR, "Failed to start mslookup mDNS client with target "
+			     OSMO_SOCKADDR_STR_FMT "\n",
+			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.mdns.query_addr));
+		else
+			LOGP(DDGSM, LOGL_NOTICE, "Started mslookup mDNS client, sending mDNS requests to multicast "
+			     OSMO_SOCKADDR_STR_FMT "\n",
+			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.mdns.query_addr));
+	}
+
+	if (g_hlr->mslookup.client.enable && osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy))
+			LOGP(DDGSM, LOGL_NOTICE,
+			     "mslookup client: all GSUP requests for unknown IMSIs will be forwarded to"
+			     " gateway-proxy " OSMO_SOCKADDR_STR_FMT "\n",
+			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.gsup_gateway_proxy));
+}