add libosmo-mslookup abstract client

mslookup is a key concept in Distributed GSM, which allows querying the current
location of a subscriber in a number of cooperating but independent core
network sites, by arbitrary service names and by MSISDN/IMSI.

Add the abstract mslookup client library. An actual lookup method (besides
mslookup_client_fake.c) is added in a subsequent patch.

For a detailed overview of this and upcoming patches, please see the elaborate
comment at the top of mslookup.c.

Add as separate library, libosmo-mslookup, to allow adding D-GSM capability to
arbitrary client programs.

osmo-hlr will be the only mslookup server implementation, added in a subsequent
patch.

osmo-hlr itself will also use this library and act as an mslookup client, when
requesting the home HLR for locally unknown IMSIs.

Related: OS#4237
Patch-by: osmith, nhofmeyr
Change-Id: I83487ab8aad1611eb02e997dafbcb8344da13df1
diff --git a/src/mslookup/mslookup.c b/src/mslookup/mslookup.c
new file mode 100644
index 0000000..d399e3a
--- /dev/null
+++ b/src/mslookup/mslookup.c
@@ -0,0 +1,321 @@
+/* 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 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/>.
+ */
+
+#include <string.h>
+#include <errno.h>
+#include <osmocom/gsm/gsm23003.h>
+#include <osmocom/mslookup/mslookup.h>
+
+/*! \addtogroup mslookup
+ *
+ * Distributed GSM: finding subscribers
+ *
+ * There are various aspects of the D-GSM code base in osmo-hlr.git, here is an overview:
+ *
+ * mslookup is the main enabler of D-GSM, a concept for connecting services between independent core network stacks.
+ *
+ * D-GSM consists of:
+ * (1) mslookup client to find subscribers:
+ *     (a) external clients like ESME, SIP PBX, ... ask osmo-hlr to tell where to send SMS, voice calls, ...
+ *     (b) osmo-hlr's own mslookup client asks remote osmo-hlrs whether they know a given IMSI.
+ * (2) when a subscriber was found at a remote HLR, GSUP gets forwarded there:
+ *     (a) to deliver messages for the GSUP proxy, osmo-hlr manages many GSUP clients to establish links to remote HLRs.
+ *     (b) osmo-hlr has a GSUP proxy layer that caches data of IMSIs that get proxied to a remote HLR.
+ *     (c) decision making to distinguish local IMSIs from ones proxied to a remote HLR.
+ *
+ * (1) mslookup is a method of finding subscribers using (multicast) queries, by MSISDN or by IMSI.
+ * It is open to various lookup methods, the first one being multicast DNS.
+ * An mslookup client sends a request, and an mslookup server responds.
+ * The mslookup server is implemented by osmo-hlr. mslookup clients are arbitrary programs, like an ESME or a SIP PBX.
+ * Hence the mslookup client is public API, while the mslookup server is implemented "privately" in osmo-hlr.
+ *
+ * (1a) Public mslookup client: libosmo-mslookup
+ *   src/mslookup/mslookup.c               Things useful for both client and server.
+ *   src/mslookup/mslookup_client.c        The client API, which can use various lookup methods,
+ *                                         and consolidates results from various responders.
+ *   src/mslookup/mslookup_client_mdns.c   lookup method implementing multicast DNS, client side.
+ *
+ *   src/mslookup/osmo-mslookup-client.c   Utility program to ease invocation for (blocking) mslookup clients.
+ *
+ *   src/mslookup/mslookup_client_fake.c   lookup method generating fake results, for testing client implementations.
+ *
+ *   src/mslookup/mdns*.c                  implementation of DNS to be used by mslookup_client_mdns.c,
+ *                                         and the mslookup_server.c.
+ *
+ *   contrib/dgsm/esme_dgsm.py                 Example implementation for an mslookup enabled SMS handler.
+ *   contrib/dgsm/freeswitch_dialplan_dgsm.py  Example implementation for an mslookup enabled FreeSWITCH dialplan.
+ *   contrib/dgsm/osmo-mslookup-pipe.py        Example for writing a python client using the osmo-mslookup-client
+ *                                             cmdline.
+ *   contrib/dgsm/osmo-mslookup-socket.py      Example for writing a python client using the osmo-mslookup-client
+ *                                             unix domain socket.
+ *
+ * (1b) "Private" mslookup server in osmo-hlr:
+ *   src/mslookup_server.c        Respond to mslookup queries, independent from the particular lookup method.
+ *   src/mslookup_server_mdns.c   mDNS specific implementation for mslookup_server.c.
+ *   src/dgsm_vty.c               Configure services that mslookup server sends to remote requests.
+ *
+ * (2) Proxy and GSUP clients to remote HLR instances:
+ *
+ * (a) Be a GSUP client to forward to a remote HLR:
+ *  src/gsupclient/   The same API that is used by osmo-{msc,sgsn} is also used to forward GSUP to remote osmo-hlrs.
+ *  src/remote_hlr.c  Establish links to remote osmo-hlrs, where this osmo-hlr is a client (proxying e.g. for an MSC).
+ *
+ * (b) Keep track of remotely handled IMSIs:
+ *  src/proxy.c       Keep track of proxied IMSIs and cache important subscriber data.
+ *
+ * (c) Direct GSUP request to the right destination: either the local or a remote HLR:
+ *  src/dgsm.c        The glue that makes osmo-hlr distinguish between local IMSIs and those that are proxied to another
+ *                    osmo-hlr.
+ *  src/dgsm_vty.c    Config.
+ *
+ *  @{
+ * \file mslookup.c
+ */
+
+const struct value_string osmo_mslookup_id_type_names[] = {
+	{ OSMO_MSLOOKUP_ID_NONE, "none" },
+	{ OSMO_MSLOOKUP_ID_IMSI, "imsi" },
+	{ OSMO_MSLOOKUP_ID_MSISDN, "msisdn" },
+	{}
+};
+
+const struct value_string osmo_mslookup_result_code_names[] = {
+	{ OSMO_MSLOOKUP_RC_NONE, "none" },
+	{ OSMO_MSLOOKUP_RC_RESULT, "result" },
+	{ OSMO_MSLOOKUP_RC_NOT_FOUND, "not-found" },
+	{}
+};
+
+/*! Compare two struct osmo_mslookup_id.
+ * \returns   0 if a and b are equal,
+ *          < 0 if a (or the ID type / start of ID) is < b,
+ *          > 0 if a (or the ID type / start of ID) is > b.
+ */
+int osmo_mslookup_id_cmp(const struct osmo_mslookup_id *a, const struct osmo_mslookup_id *b)
+{
+	int cmp;
+	if (a == b)
+		return 0;
+	if (!a)
+		return -1;
+	if (!b)
+		return 1;
+
+	cmp = OSMO_CMP(a->type, b->type);
+	if (cmp)
+		return cmp;
+
+	switch (a->type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		return strncmp(a->imsi, b->imsi, sizeof(a->imsi));
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		return strncmp(a->msisdn, b->msisdn, sizeof(a->msisdn));
+	default:
+		return 0;
+	}
+}
+
+bool osmo_mslookup_id_valid(const struct osmo_mslookup_id *id)
+{
+	switch (id->type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		return osmo_imsi_str_valid(id->imsi);
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		return osmo_msisdn_str_valid(id->msisdn);
+	default:
+		return false;
+	}
+}
+
+bool osmo_mslookup_service_valid(const char *service)
+{
+	return strlen(service) > 0;
+}
+
+/*! Write ID and ID type to a buffer.
+ * \param[out] buf  nul-terminated {id}.{id_type} string (e.g. "1234.msisdn") or
+* 		    "?.none" if the ID type is invalid.
+ * \returns amount of bytes written to buf.
+ */
+size_t osmo_mslookup_id_name_buf(char *buf, size_t buflen, const struct osmo_mslookup_id *id)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+	switch (id->type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		OSMO_STRBUF_PRINTF(sb, "%s", id->imsi);
+		break;
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		OSMO_STRBUF_PRINTF(sb, "%s", id->msisdn);
+		break;
+	default:
+		OSMO_STRBUF_PRINTF(sb, "?");
+		break;
+	}
+	OSMO_STRBUF_PRINTF(sb, ".%s", osmo_mslookup_id_type_name(id->type));
+	return sb.chars_needed;
+}
+
+/*! Same as osmo_mslookup_id_name_buf(), but return a talloc allocated string of sufficient size. */
+char *osmo_mslookup_id_name_c(void *ctx, const struct osmo_mslookup_id *id)
+{
+	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", osmo_mslookup_id_name_buf, id)
+}
+
+/*! Same as osmo_mslookup_id_name_buf(), but directly return the char* (for printf-like string formats). */
+char *osmo_mslookup_id_name_b(char *buf, size_t buflen, const struct osmo_mslookup_id *id)
+{
+	int rc = osmo_mslookup_id_name_buf(buf, buflen, id);
+	if (rc < 0 && buflen)
+		buf[0] = '\0';
+	return buf;
+}
+
+/*! Write mslookup result string to buffer.
+ * \param[in] query  with the service, ID and ID type to be written to buf like a domain string, or NULL to omit.
+ * \param[in] result with the result code, IPv4/v6 and age to be written to buf or NULL to omit.
+ * \param[out] buf  result as flat string, which looks like the following for a valid query and result with IPv4 and v6
+ *                  answer: "sip.voice.1234.msisdn -> ipv4: 42.42.42.42:1337 -> ipv6: [1234:5678:9ABC::]:1338 (age=1)",
+ *                  the result part can also be " -> timeout" or " -> rc=5" depending on the result code.
+ * \returns amount of bytes written to buf.
+ */
+size_t osmo_mslookup_result_to_str_buf(char *buf, size_t buflen,
+				     const struct osmo_mslookup_query *query,
+				     const struct osmo_mslookup_result *result)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+	if (query) {
+		OSMO_STRBUF_PRINTF(sb, "%s.", query->service);
+		OSMO_STRBUF_APPEND(sb, osmo_mslookup_id_name_buf, &query->id);
+	}
+	if (result && result->rc == OSMO_MSLOOKUP_RC_NONE)
+		result = NULL;
+	if (result) {
+		if (result->rc != OSMO_MSLOOKUP_RC_RESULT) {
+			OSMO_STRBUF_PRINTF(sb, " %s", osmo_mslookup_result_code_name(result->rc));
+		} else {
+			if (result->host_v4.ip[0]) {
+				OSMO_STRBUF_PRINTF(sb, " -> ipv4: " OSMO_SOCKADDR_STR_FMT,
+						   OSMO_SOCKADDR_STR_FMT_ARGS(&result->host_v4));
+			}
+			if (result->host_v6.ip[0]) {
+				OSMO_STRBUF_PRINTF(sb, " -> ipv6: " OSMO_SOCKADDR_STR_FMT,
+						   OSMO_SOCKADDR_STR_FMT_ARGS(&result->host_v6));
+			}
+			OSMO_STRBUF_PRINTF(sb, " (age=%u)", result->age);
+		}
+		OSMO_STRBUF_PRINTF(sb, " %s", result->last ? "(last)" : "(not-last)");
+	}
+	return sb.chars_needed;
+}
+
+/*! Same as osmo_mslookup_result_to_str_buf(), but return a talloc allocated string of sufficient size. */
+char *osmo_mslookup_result_name_c(void *ctx,
+				  const struct osmo_mslookup_query *query,
+				  const struct osmo_mslookup_result *result)
+{
+	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", osmo_mslookup_result_to_str_buf, query, result)
+}
+
+/*! Same as osmo_mslookup_result_to_str_buf(), but directly return the char* (for printf-like string formats). */
+char *osmo_mslookup_result_name_b(char *buf, size_t buflen,
+				  const struct osmo_mslookup_query *query,
+				  const struct osmo_mslookup_result *result)
+{
+	int rc = osmo_mslookup_result_to_str_buf(buf, buflen, query, result);
+	if (rc < 0 && buflen)
+		buf[0] = '\0';
+	return buf;
+}
+
+/*! Copy part of a string to a buffer and nul-terminate it.
+ * \returns 0 on success, negative on error.
+ */
+static int token(char *dest, size_t dest_size, const char *start, const char *end)
+{
+	int len;
+	if (start >= end)
+		return -10;
+	len = end - start;
+	if (len >= dest_size)
+		return -11;
+	strncpy(dest, start, len);
+	dest[len] = '\0';
+	return 0;
+}
+
+/*! Parse a string like "foo.moo.goo.123456789012345.msisdn" into service="foo.moo.goo", id="123456789012345" and
+ * id_type="msisdn", placed in a struct osmo_mslookup_query.
+ * \param q  Write parsed query to this osmo_mslookup_query.
+ * \param domain  Human readable domain string like "sip.voice.12345678.msisdn".
+ * \returns 0 on success, negative on error.
+ */
+int osmo_mslookup_query_init_from_domain_str(struct osmo_mslookup_query *q, const char *domain)
+{
+	const char *last_dot;
+	const char *second_last_dot;
+	const char *id_type;
+	const char *id;
+	int rc;
+
+	*q = (struct osmo_mslookup_query){};
+
+	if (!domain)
+		return -1;
+
+	last_dot = strrchr(domain, '.');
+
+	if (!last_dot)
+		return -2;
+
+	if (last_dot <= domain)
+		return -3;
+
+	for (second_last_dot = last_dot - 1; second_last_dot > domain && *second_last_dot != '.'; second_last_dot--);
+	if (second_last_dot == domain || *second_last_dot != '.')
+		return -3;
+
+	id_type = last_dot + 1;
+	if (!*id_type)
+		return -4;
+
+	q->id.type = get_string_value(osmo_mslookup_id_type_names, id_type);
+
+	id = second_last_dot + 1;
+	switch (q->id.type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		rc = token(q->id.imsi, sizeof(q->id.imsi), id, last_dot);
+		if (rc)
+			return rc;
+		if (!osmo_imsi_str_valid(q->id.imsi))
+			return -5;
+		break;
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		rc = token(q->id.msisdn, sizeof(q->id.msisdn), id, last_dot);
+		if (rc)
+			return rc;
+		if (!osmo_msisdn_str_valid(q->id.msisdn))
+			return -6;
+		break;
+	default:
+		return -7;
+	}
+
+	return token(q->service, sizeof(q->service), domain, second_last_dot);
+}
+
+/*! @} */