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/remote_hlr.c b/src/remote_hlr.c
new file mode 100644
index 0000000..00bfbb1
--- /dev/null
+++ b/src/remote_hlr.c
@@ -0,0 +1,252 @@
+/* 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 <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsm/gsm23003.h>
+#include <osmocom/abis/ipa.h>
+#include <osmocom/gsupclient/gsup_client.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/hlr.h>
+#include <osmocom/hlr/gsup_server.h>
+#include <osmocom/hlr/gsup_router.h>
+#include <osmocom/hlr/remote_hlr.h>
+#include <osmocom/hlr/proxy.h>
+
+static LLIST_HEAD(remote_hlrs);
+
+static void remote_hlr_err_reply(struct remote_hlr *rh, const struct osmo_gsup_message *gsup_orig,
+				 enum gsm48_gmm_cause cause)
+{
+	struct osmo_gsup_message gsup_reply;
+
+	/* No need to answer if we couldn't parse an ERROR message type, only REQUESTs need an error reply. */
+	if (!OSMO_GSUP_IS_MSGT_REQUEST(gsup_orig->message_type))
+		return;
+
+	gsup_reply = (struct osmo_gsup_message){
+		.cause = cause,
+		.message_type = OSMO_GSUP_TO_MSGT_ERROR(gsup_orig->message_type),
+		.message_class = gsup_orig->message_class,
+
+		/* RP-Message-Reference is mandatory for SM Service */
+		.sm_rp_mr = gsup_orig->sm_rp_mr,
+	};
+
+	OSMO_STRLCPY_ARRAY(gsup_reply.imsi, gsup_orig->imsi);
+
+	/* For SS/USSD, it's important to keep both session state and ID IEs */
+	if (gsup_orig->session_state != OSMO_GSUP_SESSION_STATE_NONE) {
+		gsup_reply.session_state = OSMO_GSUP_SESSION_STATE_END;
+		gsup_reply.session_id = gsup_orig->session_id;
+	}
+
+	if (osmo_gsup_client_enc_send(rh->gsupc, &gsup_reply))
+		LOGP(DLGSUP, LOGL_ERROR, "Failed to send Error reply (imsi=%s)\n",
+		     osmo_quote_str(gsup_orig->imsi, -1));
+}
+
+/* We are receiving a GSUP message from a remote HLR to go back to a local MSC.
+ * The local MSC shall be indicated by gsup.destination_name. */
+static int remote_hlr_rx(struct osmo_gsup_client *gsupc, struct msgb *msg)
+{
+	struct remote_hlr *rh = gsupc->data;
+	struct proxy_subscr proxy_subscr;
+	struct osmo_gsup_message gsup;
+	int rc;
+
+	rc = osmo_gsup_decode(msgb_l2(msg), msgb_l2len(msg), &gsup);
+	if (rc < 0) {
+		LOG_REMOTE_HLR(rh, LOGL_ERROR, "Failed to decode GSUP message: '%s' (%d) [ %s]\n",
+			       get_value_string(gsm48_gmm_cause_names, -rc),
+			       -rc, osmo_hexdump(msg->data, msg->len));
+		return rc;
+	}
+
+	if (!osmo_imsi_str_valid(gsup.imsi)) {
+		LOG_REMOTE_HLR_MSG(rh, &gsup, LOGL_ERROR, "Invalid IMSI\n");
+		remote_hlr_err_reply(rh, &gsup, GMM_CAUSE_INV_MAND_INFO);
+		return -GMM_CAUSE_INV_MAND_INFO;
+	}
+
+	if (proxy_subscr_get_by_imsi(&proxy_subscr, g_hlr->gs->proxy, gsup.imsi)) {
+		LOG_REMOTE_HLR_MSG(rh, &gsup, LOGL_ERROR, "No proxy entry for this IMSI\n");
+		remote_hlr_err_reply(rh, &gsup, GMM_CAUSE_NET_FAIL);
+		return -GMM_CAUSE_NET_FAIL;
+	}
+
+	rc = proxy_subscr_forward_to_vlr(g_hlr->gs->proxy, &proxy_subscr, &gsup, rh);
+	if (rc) {
+		LOG_REMOTE_HLR_MSG(rh, &gsup, LOGL_ERROR, "Failed to forward GSUP message towards VLR\n");
+		remote_hlr_err_reply(rh, &gsup, GMM_CAUSE_NET_FAIL);
+		return -GMM_CAUSE_NET_FAIL;
+	}
+	return 0;
+}
+
+struct remote_hlr_pending_up {
+	struct llist_head entry;
+	remote_hlr_connect_result_cb_t connect_result_cb;
+	void *data;
+};
+
+static bool remote_hlr_up_down(struct osmo_gsup_client *gsupc, bool up)
+{
+	struct remote_hlr *remote_hlr = gsupc->data;
+	struct remote_hlr_pending_up *p, *n;
+	if (!up) {
+		LOG_REMOTE_HLR(remote_hlr, LOGL_NOTICE, "link to remote HLR is down, removing GSUP client\n");
+		remote_hlr_destroy(remote_hlr);
+		return false;
+	}
+
+	LOG_REMOTE_HLR(remote_hlr, LOGL_NOTICE, "link up\n");
+	llist_for_each_entry_safe(p, n, &remote_hlr->pending_up_callbacks, entry) {
+		if (p->connect_result_cb)
+			p->connect_result_cb(&remote_hlr->addr, remote_hlr, p->data);
+		llist_del(&p->entry);
+	}
+	return true;
+}
+
+bool remote_hlr_is_up(struct remote_hlr *remote_hlr)
+{
+	return remote_hlr && remote_hlr->gsupc && remote_hlr->gsupc->is_connected;
+}
+
+struct remote_hlr *remote_hlr_get_or_connect(const struct osmo_sockaddr_str *addr, bool connect,
+					     remote_hlr_connect_result_cb_t connect_result_cb, void *data)
+{
+	struct remote_hlr *rh = NULL;
+	struct remote_hlr *rh_i;
+	struct osmo_gsup_client_config cfg;
+
+	llist_for_each_entry(rh_i, &remote_hlrs, entry) {
+		if (!osmo_sockaddr_str_cmp(&rh_i->addr, addr)) {
+			rh = rh_i;
+			break;
+		}
+	}
+
+	if (rh)
+		goto add_result_cb;
+
+	if (!connect) {
+		if (connect_result_cb)
+			connect_result_cb(addr, NULL, data);
+		return NULL;
+	}
+
+	/* Doesn't exist yet, create a GSUP client to remote HLR. */
+	cfg = (struct osmo_gsup_client_config){
+		.ipa_dev = &g_hlr->gsup_unit_name,
+		.ip_addr = addr->ip,
+		.tcp_port = addr->port,
+		.oapc_config = NULL,
+		.read_cb = remote_hlr_rx,
+		.up_down_cb = remote_hlr_up_down,
+		.data = rh,
+	};
+	rh = talloc_zero(dgsm_ctx, struct remote_hlr);
+	OSMO_ASSERT(rh);
+	*rh = (struct remote_hlr){
+		.addr = *addr,
+		.gsupc = osmo_gsup_client_create3(rh, &cfg),
+	};
+	INIT_LLIST_HEAD(&rh->pending_up_callbacks);
+	if (!rh->gsupc) {
+		LOGP(DDGSM, LOGL_ERROR,
+		     "Failed to establish connection to remote HLR " OSMO_SOCKADDR_STR_FMT "\n",
+		     OSMO_SOCKADDR_STR_FMT_ARGS(addr));
+		talloc_free(rh);
+		if (connect_result_cb)
+			connect_result_cb(addr, NULL, data);
+		return NULL;
+	}
+
+	rh->gsupc->data = rh;
+	llist_add(&rh->entry, &remote_hlrs);
+
+add_result_cb:
+	if (connect_result_cb) {
+		if (remote_hlr_is_up(rh)) {
+			connect_result_cb(addr, rh, data);
+		} else {
+			struct remote_hlr_pending_up *p;
+			p = talloc_zero(rh, struct remote_hlr_pending_up);
+			OSMO_ASSERT(p);
+			p->connect_result_cb = connect_result_cb;
+			p->data = data;
+			llist_add_tail(&p->entry, &rh->pending_up_callbacks);
+		}
+	}
+	return rh;
+}
+
+void remote_hlr_destroy(struct remote_hlr *remote_hlr)
+{
+	osmo_gsup_client_destroy(remote_hlr->gsupc);
+	remote_hlr->gsupc = NULL;
+	llist_del(&remote_hlr->entry);
+	talloc_free(remote_hlr);
+}
+
+/* This function takes ownership of the msg, do not free it after passing to this function. */
+int remote_hlr_msgb_send(struct remote_hlr *remote_hlr, struct msgb *msg)
+{
+	int rc = osmo_gsup_client_send(remote_hlr->gsupc, msg);
+	if (rc) {
+		LOGP(DDGSM, LOGL_ERROR, "Failed to send GSUP message to " OSMO_SOCKADDR_STR_FMT "\n",
+		     OSMO_SOCKADDR_STR_FMT_ARGS(&remote_hlr->addr));
+	}
+	return rc;
+}
+
+/* A GSUP message was received from the MS/MSC side, forward it to the remote HLR. */
+void remote_hlr_gsup_forward_to_remote_hlr(struct remote_hlr *remote_hlr, struct osmo_gsup_req *req,
+					   struct osmo_gsup_message *modified_gsup)
+{
+	int rc;
+	struct msgb *msg;
+	/* To forward to a remote HLR, we need to indicate the source MSC's name in the Source Name IE to make sure the
+	 * reply can be routed back. Store the sender MSC in gsup->source_name -- the remote HLR is required to return
+	 * this as gsup->destination_name so that the reply gets routed to the original MSC. */
+	struct osmo_gsup_message forward;
+	if (modified_gsup)
+		forward = *modified_gsup;
+	else
+		forward = req->gsup;
+
+	if (req->source_name.type != OSMO_CNI_PEER_ID_IPA_NAME) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Unsupported GSUP peer id type: %s",
+					  osmo_cni_peer_id_type_name(req->source_name.type));
+		return;
+	}
+	forward.source_name = req->source_name.ipa_name.val;
+	forward.source_name_len = req->source_name.ipa_name.len;
+
+	msg = osmo_gsup_msgb_alloc("GSUP proxy to remote HLR");
+	rc = osmo_gsup_encode(msg, &forward);
+	if (rc) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Failed to encode GSUP message for forwarding");
+		return;
+	}
+	remote_hlr_msgb_send(remote_hlr, msg);
+	osmo_gsup_req_free(req);
+}