Change in osmo-hlr[master]: D-GSM 3/n: implement roaming by mslookup in osmo-hlr

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

neels gerrit-no-reply at lists.osmocom.org
Wed Nov 27 06:13:34 UTC 2019


neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-hlr/+/16258 )


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

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
---
M include/osmocom/hlr/Makefile.am
A include/osmocom/hlr/dgsm.h
M include/osmocom/hlr/gsup_server.h
M include/osmocom/hlr/hlr.h
M include/osmocom/hlr/logging.h
A include/osmocom/hlr/proxy.h
A include/osmocom/hlr/remote_hlr.h
M src/Makefile.am
A src/dgsm.c
M src/dgsm_vty.c
M src/hlr.c
M src/hlr_vty.c
M src/logging.c
M src/mslookup_server.c
M src/mslookup_server_mdns.c
A src/proxy.c
A src/remote_hlr.c
M tests/test_nodes.vty
M tests/test_subscriber.vty
19 files changed, 1,738 insertions(+), 6 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-hlr refs/changes/58/16258/1

diff --git a/include/osmocom/hlr/Makefile.am b/include/osmocom/hlr/Makefile.am
index b24f084..aceda4a 100644
--- a/include/osmocom/hlr/Makefile.am
+++ b/include/osmocom/hlr/Makefile.am
@@ -2,6 +2,7 @@
 	auc.h \
 	ctrl.h \
 	db.h \
+	dgsm.h \
 	gsup_router.h \
 	gsup_server.h \
 	hlr.h \
@@ -12,6 +13,8 @@
 	lu_fsm.h \
 	mslookup_server.h \
 	mslookup_server_mdns.h \
+	proxy.h \
 	rand.h \
+	remote_hlr.h \
 	timestamp.h \
 	$(NULL)
diff --git a/include/osmocom/hlr/dgsm.h b/include/osmocom/hlr/dgsm.h
new file mode 100644
index 0000000..24e30fe
--- /dev/null
+++ b/include/osmocom/hlr/dgsm.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <osmocom/mslookup/mslookup.h>
+#include <osmocom/hlr/gsup_server.h>
+#include <osmocom/gsupclient/global_title.h>
+#include <osmocom/gsupclient/gsup_req.h>
+
+#define LOG_DGSM(imsi, level, fmt, args...) \
+	LOGP(DDGSM, level, "(IMSI-%s) " fmt, imsi, ##args)
+
+struct vty;
+struct remote_hlr;
+struct hlr_subscriber;
+
+extern void *dgsm_ctx;
+
+void dgsm_init(void *ctx);
+void dgsm_start(void *ctx);
+void dgsm_stop();
+
+bool dgsm_check_forward_gsup_msg(struct osmo_gsup_req *req);
+
+void dgsm_vty_init();
+void dgsm_mdns_client_config_apply(void);
+
+bool hlr_subscr_lu_age(const struct hlr_subscriber *subscr, uint32_t *age_p);
diff --git a/include/osmocom/hlr/gsup_server.h b/include/osmocom/hlr/gsup_server.h
index c3efea2..7002da0 100644
--- a/include/osmocom/hlr/gsup_server.h
+++ b/include/osmocom/hlr/gsup_server.h
@@ -27,6 +27,9 @@
 	struct ipa_server_link *link;
 	osmo_gsup_read_cb_t read_cb;
 	struct llist_head routes;
+
+	/* Proxy requests from this server's clients to remote GSUP servers. */
+	struct proxy *proxy;
 };
 
 
diff --git a/include/osmocom/hlr/hlr.h b/include/osmocom/hlr/hlr.h
index 8bc7def..cc7d52e 100644
--- a/include/osmocom/hlr/hlr.h
+++ b/include/osmocom/hlr/hlr.h
@@ -28,6 +28,8 @@
 #include <osmocom/core/tdef.h>
 #include <osmocom/core/sockaddr_str.h>
 
+#include <osmocom/hlr/dgsm.h>
+
 #define HLR_DEFAULT_DB_FILE_PATH "hlr.db"
 
 struct hlr_euse;
@@ -85,6 +87,28 @@
 				struct osmo_mslookup_server_mdns *running;
 			} mdns;
 		} server;
+
+		/* The mslookup client in osmo-hlr is used to find out which remote HLRs service a locally unknown IMSI.
+		 * (It may also be used to resolve recipients for SMS-over-GSUP in the future.) */
+		struct {
+			/* Whether to proxy/forward to remote HLRs */
+			bool enable;
+
+			/* If this is set, all GSUP for unknown IMSIs is forwarded directly to this GSUP address,
+			 * unconditionally. */
+			struct osmo_sockaddr_str gsup_gateway_proxy;
+
+			/* mslookup client request handling */
+			unsigned int result_timeout_milliseconds;
+
+			struct osmo_mslookup_client *client;
+			struct {
+				/* Whether to use mDNS for IMSI MS Lookup */
+				bool enable;
+				struct osmo_sockaddr_str query_addr;
+				struct osmo_mslookup_client_method *running;
+			} mdns;
+		} client;
 	} mslookup;
 };
 
diff --git a/include/osmocom/hlr/logging.h b/include/osmocom/hlr/logging.h
index 4e0a25c..a8081af 100644
--- a/include/osmocom/hlr/logging.h
+++ b/include/osmocom/hlr/logging.h
@@ -10,6 +10,7 @@
 	DSS,
 	DMSLOOKUP,
 	DLU,
+	DDGSM,
 };
 
 extern const struct log_info hlr_log_info;
diff --git a/include/osmocom/hlr/proxy.h b/include/osmocom/hlr/proxy.h
new file mode 100644
index 0000000..0fd574c
--- /dev/null
+++ b/include/osmocom/hlr/proxy.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <time.h>
+#include <osmocom/gsm/protocol/gsm_23_003.h>
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/gsupclient/global_title.h>
+#include <osmocom/hlr/timestamp.h>
+
+struct osmo_gsup_req;
+struct remote_hlr;
+
+struct proxy_pending_gsup_req {
+	struct llist_head entry;
+	struct osmo_gsup_req *req;
+	timestamp_t received_at;
+};
+
+struct proxy {
+	struct llist_head subscr_list;
+	struct llist_head pending_gsup_reqs;
+
+	/* When messages arrive back from a remote HLR that this is the proxy for, reach the VLR to forward the response
+	 * to via this osmo_gsup_server. */
+	struct osmo_gsup_server *gsup_server_to_vlr;
+
+	/* How long to keep proxy entries without a refresh, in seconds. */
+	uint32_t fresh_time;
+
+	/* How often to garbage collect the proxy cache, period in seconds.
+	 * To change this and take effect immediately, rather use proxy_set_gc_period(). */
+	uint32_t gc_period;
+
+	struct osmo_timer_list gc_timer;
+};
+
+struct proxy_subscr_domain_state {
+	struct osmo_gt vlr_name;
+	timestamp_t last_lu;
+
+	/* The name from which an Update Location Request was received. Copied to vlr_name as soon as the LU is
+	 * completed successfully. */
+	struct osmo_gt vlr_name_preliminary;
+
+	/* Set if this is a middle proxy, i.e. a proxy behind another proxy.
+	 * That is mostly to know whether the MS is attached at a local MSC/SGSN or further away.
+	 * It could be a boolean, but store the full name for logging. Set only at successful LU acceptance. */
+	struct osmo_gt vlr_via_proxy;
+};
+
+struct proxy_subscr {
+	char imsi[GSM23003_IMSI_MAX_DIGITS+1];
+	char msisdn[GSM23003_MSISDN_MAX_DIGITS+1];
+	struct osmo_sockaddr_str remote_hlr_addr;
+	struct proxy_subscr_domain_state cs, ps;
+};
+
+void proxy_init(struct osmo_gsup_server *gsup_server_to_vlr);
+void proxy_del(struct proxy *proxy);
+void proxy_set_gc_period(struct proxy *proxy, uint32_t gc_period);
+
+/* The API to access / modify proxy entries keeps the implementation opaque, to make sure that we can easily move proxy
+ * storage to SQLite db. */
+const struct proxy_subscr *proxy_subscr_get_by_imsi(struct proxy *proxy, const char *imsi);
+const struct proxy_subscr *proxy_subscr_get_by_msisdn(struct proxy *proxy, const char *msisdn);
+void proxy_subscrs_get_by_remote_hlr(struct proxy *proxy, const struct osmo_sockaddr_str *remote_hlr_addr,
+				     bool (*yield)(struct proxy *proxy, const struct proxy_subscr *subscr, void *data),
+				     void *data);
+const struct proxy_subscr *proxy_subscr_get_by_imsi(struct proxy *proxy, const char *imsi);
+int proxy_subscr_update(struct proxy *proxy, const struct proxy_subscr *proxy_subscr);
+int proxy_subscr_del(struct proxy *proxy, const char *imsi);
+
+void proxy_subscr_forward_to_remote_hlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+					struct osmo_gsup_req *req);
+void proxy_subscr_forward_to_remote_hlr_resolved(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+						 struct remote_hlr *remote_hlr, struct osmo_gsup_req *req);
+
+int proxy_subscr_forward_to_vlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				const struct osmo_gsup_message *gsup, struct remote_hlr *from_remote_hlr);
+
+void proxy_subscr_remote_hlr_resolved(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				      struct remote_hlr *remote_hlr);
+void proxy_subscr_remote_hlr_up(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				struct remote_hlr *remote_hlr);
diff --git a/include/osmocom/hlr/remote_hlr.h b/include/osmocom/hlr/remote_hlr.h
new file mode 100644
index 0000000..bfa3d95
--- /dev/null
+++ b/include/osmocom/hlr/remote_hlr.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <stdbool.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/sockaddr_str.h>
+
+struct osmo_gsup_client;
+struct osmo_gsup_message;
+struct osmo_gsup_req;
+struct msgb;
+
+#define LOG_REMOTE_HLR(remote_hlr, level, fmt, args...) \
+	LOGP(DDGSM, level, "(Proxy HLR-" OSMO_SOCKADDR_STR_FMT ") " fmt, \
+	     OSMO_SOCKADDR_STR_FMT_ARGS((remote_hlr) ? &(remote_hlr)->addr : NULL), ##args)
+
+#define LOG_REMOTE_HLR_MSG(remote_hlr, gsup_msg, level, fmt, args...) \
+	LOG_REMOTE_HLR(remote_hlr, level, "%s: " fmt, osmo_gsup_message_type_name((gsup_msg)->message_type), ##args)
+
+/* GSUP client link for proxying to a remote HLR. */
+struct remote_hlr {
+	struct llist_head entry;
+	struct osmo_sockaddr_str addr;
+	struct osmo_gsup_client *gsupc;
+};
+
+struct remote_hlr *remote_hlr_get(const struct osmo_sockaddr_str *addr, bool create);
+void remote_hlr_destroy(struct remote_hlr *remote_hlr);
+int remote_hlr_msgb_send(struct remote_hlr *remote_hlr, struct msgb *msg);
+void remote_hlr_gsup_forward_to_remote_hlr(struct remote_hlr *remote_hlr, struct osmo_gsup_req *req);
diff --git a/src/Makefile.am b/src/Makefile.am
index a03aeff..0a07aaf 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -53,6 +53,9 @@
 	hlr_vty_subscr.c \
 	gsup_send.c \
 	hlr_ussd.c \
+	proxy.c \
+	dgsm.c \
+	remote_hlr.c \
 	lu_fsm.c \
 	timestamp.c \
 	mslookup_server.c \
diff --git a/src/dgsm.c b/src/dgsm.c
new file mode 100644
index 0000000..5e0d494
--- /dev/null
+++ b/src/dgsm.c
@@ -0,0 +1,247 @@
+#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/global_title.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;
+	const struct proxy_subscr *proxy_subscr;
+	const struct osmo_sockaddr_str *use_addr;
+	struct remote_hlr *remote_hlr;
+
+	/* 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))
+		use_addr = &result->host_v4;
+	else if (osmo_sockaddr_str_is_nonzero(&result->host_v6))
+		use_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;
+	}
+
+	remote_hlr = remote_hlr_get(use_addr, true);
+	if (!remote_hlr) {
+		proxy_subscr_del(proxy, query->id.imsi);
+		return;
+	}
+
+	proxy_subscr = proxy_subscr_get_by_imsi(proxy, query->id.imsi);
+	if (!proxy_subscr) {
+		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;
+	}
+
+	/* The remote HLR already exists and is connected. Messages for this IMSI were spooled because we did not know
+	 * which remote HLR was responsible. Now we know, send this IMSI's messages now. */
+	LOG_DGSM(query->id.imsi, LOGL_DEBUG, "Resolved remote HLR, sending spooled GSUP messages: %s\n",
+		 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
+
+	proxy_subscr_remote_hlr_resolved(proxy, proxy_subscr, remote_hlr);
+
+	if (!remote_hlr->gsupc || !remote_hlr->gsupc->is_connected) {
+		LOG_REMOTE_HLR(remote_hlr, LOGL_DEBUG, "Waiting for link-up\n");
+		return;
+	}
+	proxy_subscr_remote_hlr_up(proxy, proxy_subscr, remote_hlr);
+}
+
+/* Return true when the message has been handled by D-GSM. */
+bool dgsm_check_forward_gsup_msg(struct osmo_gsup_req *req)
+{
+	const struct proxy_subscr *proxy_subscr;
+	struct proxy_subscr proxy_subscr_new;
+	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? */
+	proxy_subscr = proxy_subscr_get_by_imsi(proxy, req->gsup.imsi);
+	if (proxy_subscr)
+		goto yes_we_are_proxying;
+
+	/* 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_new = (struct proxy_subscr){};
+	OSMO_STRLCPY_ARRAY(proxy_subscr_new.imsi, req->gsup.imsi);
+	proxy_subscr = &proxy_subscr_new;
+	if (proxy_subscr_update(proxy, proxy_subscr)) {
+		LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Failed to create proxy entry\n");
+		return false;
+	}
+
+	/* Is a fixed gateway proxy configured? */
+	if (osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy)) {
+		struct remote_hlr *gsup_gateway_proxy = remote_hlr_get(&g_hlr->mslookup.client.gsup_gateway_proxy, true);
+		if (!gsup_gateway_proxy) {
+			LOG_DGSM(req->gsup.imsi, LOGL_ERROR,
+				 "Failed to set up fixed gateway proxy " OSMO_SOCKADDR_STR_FMT "\n",
+				 OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.gsup_gateway_proxy));
+			return false;
+		}
+
+		proxy_subscr_remote_hlr_resolved(proxy, proxy_subscr, gsup_gateway_proxy);
+
+		/* Update info */
+		proxy_subscr = proxy_subscr_get_by_imsi(proxy, req->gsup.imsi);
+		if (!proxy_subscr) {
+			LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Proxy entry disappeared\n");
+			return false;
+		}
+		goto yes_we_are_proxying;
+	}
+
+	/* Kick off an mslookup for the remote HLR. */
+	if (!g_hlr->mslookup.client.client) {
+		LOG_GSUP_REQ(req, LOGL_DEBUG, "mslookup client not running, cannot query remote home HLR\n");
+		return false;
+	}
+
+	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_delay_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);
+		return false;
+	}
+
+yes_we_are_proxying:
+	OSMO_ASSERT(proxy_subscr);
+
+	/* If the remote HLR is already known, directly forward the GSUP message; otherwise, spool the GSUP message
+	 * until the remote HLR will respond / until timeout aborts. */
+	proxy_subscr_forward_to_remote_hlr(proxy, proxy_subscr, req);
+	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;
+	current_bind_addr = osmo_mslookup_client_method_mdns_get_bind_addr(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));
+
+	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);
+		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));
+}
diff --git a/src/dgsm_vty.c b/src/dgsm_vty.c
index 7cbc257..68222ca 100644
--- a/src/dgsm_vty.c
+++ b/src/dgsm_vty.c
@@ -41,12 +41,57 @@
 	return CMD_SUCCESS;
 }
 
+static int mslookup_client_mdns_to(struct vty *vty, int argc, const char **argv)
+{
+	const char *ip_str = argc > 0? argv[0] : g_hlr->mslookup.client.mdns.query_addr.ip;
+	const char *port_str = argc > 1? argv[1] : NULL;
+	uint16_t port_nr = port_str ? atoi(port_str) : g_hlr->mslookup.client.mdns.query_addr.port;
+	struct osmo_sockaddr_str addr;
+	if (osmo_sockaddr_str_from_str(&addr, ip_str, port_nr)
+	    || !osmo_sockaddr_str_is_nonzero(&addr)) {
+		vty_out(vty, "%% mslookup client: Invalid mDNS target address: %s %u%s",
+			ip_str, port_nr, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	g_hlr->mslookup.client.mdns.query_addr = addr;
+	g_hlr->mslookup.client.mdns.enable = true;
+	g_hlr->mslookup.client.enable = true;
+	dgsm_mdns_client_config_apply();
+	return CMD_SUCCESS;
+}
+
 #define MDNS_IP46_STR "multicast IPv4 address like " OSMO_MSLOOKUP_MDNS_IP4 \
 			" or IPv6 address like " OSMO_MSLOOKUP_MDNS_IP6 "\n"
 #define MDNS_PORT_STR "mDNS UDP Port number\n"
 #define IP46_STR "IPv4 address like 1.2.3.4 or IPv6 address like a:b:c:d::1\n"
 #define PORT_STR "Service-specific port number\n"
 
+DEFUN(cfg_mslookup_mdns,
+      cfg_mslookup_mdns_cmd,
+      "mdns [IP] [<1-65535>]",
+      "Convenience shortcut: enable and configure both server and client for mDNS mslookup\n"
+      MDNS_IP46_STR MDNS_PORT_STR)
+{
+	int rc1 = mslookup_server_mdns_bind(vty, argc, argv);
+	int rc2 = mslookup_client_mdns_to(vty, argc, argv);
+	if (rc1 != CMD_SUCCESS)
+		return rc1;
+	return rc2;
+}
+
+DEFUN(cfg_mslookup_no_mdns,
+      cfg_mslookup_no_mdns_cmd,
+      "no mdns",
+      NO_STR "Disable both server and client for mDNS mslookup\n")
+{
+	g_hlr->mslookup.server.mdns.enable = false;
+	g_hlr->mslookup.client.mdns.enable = false;
+	mslookup_server_mdns_config_apply();
+	dgsm_mdns_client_config_apply();
+	return CMD_SUCCESS;
+}
+
 struct cmd_node mslookup_server_node = {
 	MSLOOKUP_SERVER_NODE,
 	"%s(config-mslookup-server)# ",
@@ -227,6 +272,68 @@
 	return CMD_SUCCESS;
 }
 
+struct cmd_node mslookup_client_node = {
+	MSLOOKUP_CLIENT_NODE,
+	"%s(config-mslookup-client)# ",
+	1,
+};
+
+DEFUN(cfg_mslookup_client,
+      cfg_mslookup_client_cmd,
+      "client",
+      "Enable and configure Distributed GSM mslookup client")
+{
+	vty->node = MSLOOKUP_CLIENT_NODE;
+	g_hlr->mslookup.client.enable = true;
+	dgsm_mdns_client_config_apply();
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_no_client,
+      cfg_mslookup_no_client_cmd,
+      "no client",
+      NO_STR "Disable Distributed GSM mslookup client")
+{
+	g_hlr->mslookup.client.enable = false;
+	dgsm_mdns_client_config_apply();
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_client_timeout,
+      cfg_mslookup_client_timeout_cmd,
+      "timeout <1-100000>",
+      "How long should the mslookup client wait for remote responses before evaluating received results\n"
+      "timeout in milliseconds\n")
+{
+	uint32_t val = atol(argv[0]);
+	g_hlr->mslookup.client.result_timeout_milliseconds = val;
+	return CMD_SUCCESS;
+}
+
+#define EXIT_HINT() \
+	if (vty->type != VTY_FILE) \
+		vty_out(vty, "%% 'exit' this node to apply changes%s", VTY_NEWLINE)
+
+
+DEFUN(cfg_mslookup_client_mdns,
+      cfg_mslookup_client_mdns_cmd,
+      "mdns [IP] [<1-65535>]",
+      "Enable mDNS client, and configure multicast address to send mDNS mslookup requests to\n"
+      MDNS_IP46_STR MDNS_PORT_STR)
+{
+	return mslookup_client_mdns_to(vty, argc, argv);
+}
+
+DEFUN(cfg_mslookup_client_no_mdns,
+      cfg_mslookup_client_no_mdns_cmd,
+      "no mdns",
+      NO_STR "Disable mDNS client, do not query remote services by mDNS\n")
+{
+	g_hlr->mslookup.client.mdns.enable = false;
+	dgsm_mdns_client_config_apply();
+	return CMD_SUCCESS;
+}
+
 void config_write_msc_services(struct vty *vty, const char *indent, struct mslookup_server_msc_cfg *msc)
 {
 	struct mslookup_service_host *e;
@@ -244,7 +351,8 @@
 int config_write_mslookup(struct vty *vty)
 {
 	if (!g_hlr->mslookup.server.enable
-	    && llist_empty(&g_hlr->mslookup.server.local_site_services))
+	    && llist_empty(&g_hlr->mslookup.server.local_site_services)
+	    && !g_hlr->mslookup.client.enable)
 		return CMD_SUCCESS;
 
 	vty_out(vty, "mslookup%s", VTY_NEWLINE);
@@ -277,6 +385,53 @@
 			vty_out(vty, " no server%s", VTY_NEWLINE);
 	}
 
+	if (g_hlr->mslookup.client.enable) {
+		vty_out(vty, " client%s", VTY_NEWLINE);
+
+		if (osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy))
+			vty_out(vty, "  gateway-proxy %s %u%s",
+				g_hlr->mslookup.client.gsup_gateway_proxy.ip,
+				g_hlr->mslookup.client.gsup_gateway_proxy.port,
+				VTY_NEWLINE);
+
+		if (g_hlr->mslookup.client.mdns.enable
+		    && osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.mdns.query_addr))
+			vty_out(vty, "  mdns to %s %u%s",
+				g_hlr->mslookup.client.mdns.query_addr.ip,
+				g_hlr->mslookup.client.mdns.query_addr.port,
+				VTY_NEWLINE);
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_client_gateway_proxy,
+      cfg_mslookup_client_gateway_proxy_cmd,
+      "gateway-proxy IP [<1-65535>]",
+      "Configure a fixed IP address to send all GSUP requests for unknown IMSIs to, without invoking a lookup for IMSI\n"
+      "IP address of the remote HLR\n" "GSUP port number (omit for default " OSMO_STRINGIFY_VAL(OSMO_GSUP_PORT) ")\n")
+{
+	const char *ip_str = argv[0];
+	const char *port_str = argc > 1 ? argv[1] : NULL;
+	struct osmo_sockaddr_str addr;
+
+	if (osmo_sockaddr_str_from_str(&addr, ip_str, port_str ? atoi(port_str) : OSMO_GSUP_PORT)
+	    || !osmo_sockaddr_str_is_nonzero(&addr)) {
+		vty_out(vty, "%% mslookup client: Invalid address for gateway-proxy: %s %s%s",
+			ip_str, port_str ? : "", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	g_hlr->mslookup.client.gsup_gateway_proxy = addr;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_client_no_gateway_proxy,
+      cfg_mslookup_client_no_gateway_proxy_cmd,
+      "no gateway-proxy",
+      NO_STR "Disable gateway proxy for GSUP with unknown IMSIs\n")
+{
+	g_hlr->mslookup.client.gsup_gateway_proxy = (struct osmo_sockaddr_str){};
 	return CMD_SUCCESS;
 }
 
@@ -316,6 +471,8 @@
 	install_element(CONFIG_NODE, &cfg_mslookup_cmd);
 
 	install_node(&mslookup_node, config_write_mslookup);
+	install_element(MSLOOKUP_NODE, &cfg_mslookup_mdns_cmd);
+	install_element(MSLOOKUP_NODE, &cfg_mslookup_no_mdns_cmd);
 	install_element(MSLOOKUP_NODE, &cfg_mslookup_server_cmd);
 	install_element(MSLOOKUP_NODE, &cfg_mslookup_no_server_cmd);
 
@@ -332,5 +489,14 @@
 	install_element(MSLOOKUP_SERVER_MSC_NODE, &cfg_mslookup_server_msc_no_service_cmd);
 	install_element(MSLOOKUP_SERVER_MSC_NODE, &cfg_mslookup_server_msc_no_service_addr_cmd);
 
+	install_element(MSLOOKUP_NODE, &cfg_mslookup_client_cmd);
+	install_element(MSLOOKUP_NODE, &cfg_mslookup_no_client_cmd);
+	install_node(&mslookup_client_node, NULL);
+	install_element(MSLOOKUP_CLIENT_NODE, &cfg_mslookup_client_timeout_cmd);
+	install_element(MSLOOKUP_CLIENT_NODE, &cfg_mslookup_client_mdns_cmd);
+	install_element(MSLOOKUP_CLIENT_NODE, &cfg_mslookup_client_no_mdns_cmd);
+	install_element(MSLOOKUP_CLIENT_NODE, &cfg_mslookup_client_gateway_proxy_cmd);
+	install_element(MSLOOKUP_CLIENT_NODE, &cfg_mslookup_client_no_gateway_proxy_cmd);
+
 	install_element_ve(&do_mslookup_show_services_cmd);
 }
diff --git a/src/hlr.c b/src/hlr.c
index b19acf8..ce1510f 100644
--- a/src/hlr.c
+++ b/src/hlr.c
@@ -36,6 +36,7 @@
 #include <osmocom/gsm/gsm48_ie.h>
 #include <osmocom/gsm/gsm_utils.h>
 #include <osmocom/gsm/gsm23003.h>
+#include <osmocom/mslookup/mslookup_client.h>
 
 #include <osmocom/gsupclient/global_title.h>
 #include <osmocom/hlr/db.h>
@@ -47,6 +48,8 @@
 #include <osmocom/hlr/rand.h>
 #include <osmocom/hlr/hlr_vty.h>
 #include <osmocom/hlr/hlr_ussd.h>
+#include <osmocom/hlr/dgsm.h>
+#include <osmocom/hlr/proxy.h>
 #include <osmocom/hlr/lu_fsm.h>
 
 struct hlr *g_hlr;
@@ -489,6 +492,15 @@
 		}
 	}
 
+	/* Distributed GSM: check whether to proxy for / lookup a remote HLR.
+	 * It would require less database hits to do this only if a local-only operation fails with "unknown IMSI", but
+	 * it becomes semantically easier if we do this once-off ahead of time. */
+	if (osmo_mslookup_client_active(g_hlr->mslookup.client.client)
+	    || osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy)) {
+		if (dgsm_check_forward_gsup_msg(req))
+			return 0;
+	}
+
 	switch (req->gsup.message_type) {
 	/* requests sent to us */
 	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_REQUEST:
@@ -693,6 +705,9 @@
 		exit(1);
 	}
 
+	/* Set up llists and objects, startup is happening from VTY commands. */
+	dgsm_init(hlr_ctx);
+
 	osmo_stats_init(hlr_ctx);
 	vty_init(&vty_info);
 	ctrl_vty_init(hlr_ctx);
@@ -748,10 +763,13 @@
 		LOGP(DMAIN, LOGL_FATAL, "Error starting GSUP server\n");
 		exit(1);
 	}
+	proxy_init(g_hlr->gs);
 
 	g_hlr->ctrl_bind_addr = ctrl_vty_get_bind_addr();
 	g_hlr->ctrl = hlr_controlif_setup(g_hlr);
 
+	dgsm_start(hlr_ctx);
+
 	osmo_init_ignore_signals();
 	signal(SIGINT, &signal_hdlr);
 	signal(SIGTERM, &signal_hdlr);
@@ -768,6 +786,7 @@
 	while (!quit)
 		osmo_select_main_ctx(0);
 
+	dgsm_stop();
 
 	osmo_gsup_server_destroy(g_hlr->gs);
 	db_close(g_hlr->dbc);
diff --git a/src/hlr_vty.c b/src/hlr_vty.c
index 6701cd9..e959be8 100644
--- a/src/hlr_vty.c
+++ b/src/hlr_vty.c
@@ -146,6 +146,24 @@
 	return CMD_SUCCESS;
 }
 
+DEFUN(cfg_hlr_gsup_ipa_name,
+      cfg_hlr_gsup_ipa_name_cmd,
+      "ipa-name NAME",
+      "Set the IPA name of this HLR, for proxying to remote HLRs\n"
+      "A globally unique name for this HLR. For example: PLMN + redundancy server number: HLR-901-70-0. "
+      "This name is used for GSUP routing and must be set if multiple HLRs interconnect (e.g. mslookup "
+      "for Distributed GSM).\n")
+{
+	if (vty->type != VTY_FILE) {
+		vty_out(vty, "gsup/ipa-name: The GSUP IPA name cannot be changed at run-time; "
+			"It can only be set in the configuraton file.%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	g_hlr->gsup_unit_name.serno = talloc_strdup(g_hlr, argv[0]);
+	return CMD_SUCCESS;
+}
+
 /***********************************************************************
  * USSD Entity
  ***********************************************************************/
@@ -444,6 +462,7 @@
 	install_node(&gsup_node, config_write_hlr_gsup);
 
 	install_element(GSUP_NODE, &cfg_hlr_gsup_bind_ip_cmd);
+	install_element(GSUP_NODE, &cfg_hlr_gsup_ipa_name_cmd);
 
 	install_element(HLR_NODE, &cfg_database_cmd);
 
diff --git a/src/logging.c b/src/logging.c
index d123fcd..9d3cc62 100644
--- a/src/logging.c
+++ b/src/logging.c
@@ -31,6 +31,12 @@
 		.color = "\033[1;33m",
 		.enabled = 1, .loglevel = LOGL_NOTICE,
 	},
+	[DDGSM] = {
+		.name = "DDGSM",
+		.description = "Distributed GSM: MS lookup and proxy",
+		.color = "\033[1;35m",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
 
 };
 
diff --git a/src/mslookup_server.c b/src/mslookup_server.c
index 0af0407..43d3f58 100644
--- a/src/mslookup_server.c
+++ b/src/mslookup_server.c
@@ -8,6 +8,7 @@
 #include <osmocom/hlr/db.h>
 #include <osmocom/hlr/timestamp.h>
 #include <osmocom/hlr/mslookup_server.h>
+#include <osmocom/hlr/proxy.h>
 
 static const struct osmo_mslookup_result not_found = {
 		.rc = OSMO_MSLOOKUP_RC_NOT_FOUND,
@@ -271,6 +272,67 @@
 	return true;
 }
 
+
+/* Determine whether the subscriber with the given ID has routed a Location Updating via this HLR as first hop. Return
+ * true if it is attached at a local VLR, and we are serving as proxy for a remote home HLR.
+ */
+static bool subscriber_has_done_lu_here_proxy(const struct osmo_mslookup_query *query,
+					      uint32_t *lu_age,
+					      struct osmo_gt *local_msc_name)
+{
+	const struct proxy_subscr *proxy_subscr;
+	uint32_t age;
+
+	/* See the local HLR record. If the subscriber is "at home" in this HLR and is also currently located here, we
+	 * will find a valid location updating and no vlr_via_proxy entry. */
+	switch (query->id.type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		proxy_subscr = proxy_subscr_get_by_imsi(g_hlr->gs->proxy, query->id.imsi);
+		break;
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		proxy_subscr = proxy_subscr_get_by_msisdn(g_hlr->gs->proxy, query->id.msisdn);
+		break;
+	default:
+		LOGP(DDGSM, LOGL_ERROR, "%s: unknown ID type\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+		return false;
+	}
+
+	if (!proxy_subscr) {
+		LOGP(DDGSM, LOGL_DEBUG, "%s: does not exist in GSUP proxy\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+		return false;
+	}
+
+	/* We only need to care about CS LU, since only CS services need D-GSM routing. */
+	if (!timestamp_age(&proxy_subscr->cs.last_lu, &age)
+	    || age > g_hlr->mslookup.server.local_attach_max_age) {
+		LOGP(DDGSM, LOGL_ERROR,
+		     "%s: last attach was at local VLR (proxying for remote HLR), but too long ago: %us > %us\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+		     age, g_hlr->mslookup.server.local_attach_max_age);
+		return false;
+	}
+
+	if (proxy_subscr->cs.vlr_via_proxy.len) {
+		LOGP(DDGSM, LOGL_DEBUG, "%s: last attach is not at local VLR, but at VLR '%s' via proxy '%s'\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+		     osmo_gt_name(&proxy_subscr->cs.vlr_name),
+		     osmo_gt_name(&proxy_subscr->cs.vlr_via_proxy));
+		return false;
+	}
+
+	*lu_age = age;
+	*local_msc_name = proxy_subscr->cs.vlr_name;
+	LOGP(DDGSM, LOGL_DEBUG, "%s: attached %u seconds ago at local VLR %s; proxying for remote HLR "
+	     OSMO_SOCKADDR_STR_FMT "\n",
+	     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+	     age, osmo_gt_name(local_msc_name),
+	     OSMO_SOCKADDR_STR_FMT_ARGS(&proxy_subscr->remote_hlr_addr));
+
+	return true;
+}
+
 static bool subscriber_has_done_lu_here(const struct osmo_mslookup_query *query,
 					uint32_t *lu_age_p,
 					struct osmo_gt *local_msc_name)
@@ -278,6 +340,9 @@
 	bool attached_here;
 	uint32_t lu_age = 0;
 	struct osmo_gt msc_name = {};
+	bool attached_here_proxy;
+	uint32_t proxy_lu_age = 0;
+	struct osmo_gt proxy_msc_name = {};
 
 	/* First ask the local HLR db, but if the local proxy record indicates a more recent LU, use that instead.
 	 * For all usual cases, only one of these will reflect a LU, even if a subscriber had more than one home HLR:
@@ -288,8 +353,14 @@
 	 * situations, compare the two entries.
 	 */
 	attached_here = subscriber_has_done_lu_here_hlr(query, &lu_age, &msc_name);
+	attached_here_proxy = subscriber_has_done_lu_here_proxy(query, &proxy_lu_age, &proxy_msc_name);
 
-	/* Future: If proxy has a younger lu, replace. */
+	/* If proxy has a younger lu, replace. */
+	if (attached_here_proxy && (!attached_here || (proxy_lu_age < lu_age))) {
+		attached_here = true;
+		lu_age = proxy_lu_age;
+		msc_name = proxy_msc_name;
+	}
 
 	if (attached_here && !msc_name.len) {
 		LOGP(DMSLOOKUP, LOGL_ERROR, "%s: attached here, but no VLR name known\n",
diff --git a/src/mslookup_server_mdns.c b/src/mslookup_server_mdns.c
index 50c13f1..ed4b23f 100644
--- a/src/mslookup_server_mdns.c
+++ b/src/mslookup_server_mdns.c
@@ -105,10 +105,9 @@
 	/* Check whether to start/stop/restart mDNS server */
 	bool should_run;
 	bool should_stop;
-	if (!g_hlr->mslookup.allow_startup)
-		return;
 
-	should_run = g_hlr->mslookup.server.enable && g_hlr->mslookup.server.mdns.enable;
+	should_run = g_hlr->mslookup.allow_startup
+		&& g_hlr->mslookup.server.enable && g_hlr->mslookup.server.mdns.enable;
 	should_stop = g_hlr->mslookup.server.mdns.running
 		&& (!should_run
 		    || osmo_sockaddr_str_cmp(&g_hlr->mslookup.server.mdns.bind_addr,
diff --git a/src/proxy.c b/src/proxy.c
new file mode 100644
index 0000000..6c61f77
--- /dev/null
+++ b/src/proxy.c
@@ -0,0 +1,525 @@
+
+#include <string.h>
+#include <talloc.h>
+#include <errno.h>
+#include <inttypes.h>
+
+#include <osmocom/core/timer.h>
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsm/gsm23003.h>
+#include <osmocom/gsm/gsm48_ie.h>
+#include <osmocom/gsupclient/gsup_client.h>
+#include <osmocom/gsupclient/gsup_req.h>
+
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/proxy.h>
+#include <osmocom/hlr/remote_hlr.h>
+#include <osmocom/hlr/gsup_server.h>
+#include <osmocom/hlr/gsup_router.h>
+
+#define LOG_PROXY_SUBSCR(proxy_subscr, level, fmt, args...) \
+	LOGP(DDGSM, level, "(Proxy IMSI-%s MSISDN-%s HLR-" OSMO_SOCKADDR_STR_FMT ") " fmt, \
+	     ((proxy_subscr) && *(proxy_subscr)->imsi)? (proxy_subscr)->imsi : "?", \
+	     ((proxy_subscr) && *(proxy_subscr)->msisdn)? (proxy_subscr)->msisdn : "?", \
+	     OSMO_SOCKADDR_STR_FMT_ARGS((proxy_subscr)? &(proxy_subscr)->remote_hlr_addr : NULL), \
+	     ##args)
+
+#define LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup_msg, level, fmt, args...) \
+		     LOG_PROXY_SUBSCR(proxy_subscr, level, "%s: " fmt, \
+				      (gsup_msg) ? osmo_gsup_message_type_name((gsup_msg)->message_type) : "NULL", \
+				      ##args)
+
+/* Why have a separate struct to add an llist_head entry?
+ * This is to keep the option open to store the proxy data in the database instead, without any visible effect outside
+ * of proxy.c. */
+struct proxy_subscr_listentry {
+	struct llist_head entry;
+	timestamp_t last_update;
+	struct proxy_subscr data;
+};
+
+/* Defer a GSUP message until we know a remote HLR to proxy to.
+ * Where to send this GSUP message is indicated by its IMSI: as soon as an MS lookup has yielded the IMSI's home HLR,
+ * that's where the message should go. */
+static void proxy_defer_gsup_req(struct proxy *proxy, struct osmo_gsup_req *req)
+{
+	struct proxy_pending_gsup_req *m;
+
+	m = talloc_zero(proxy, struct proxy_pending_gsup_req);
+	OSMO_ASSERT(m);
+	m->req = req;
+	timestamp_update(&m->received_at);
+	llist_add_tail(&m->entry, &proxy->pending_gsup_reqs);
+}
+
+/* Unable to resolve remote HLR for this IMSI, Answer with error back to the sender. */
+static void proxy_defer_gsup_message_err(struct proxy *proxy, struct proxy_pending_gsup_req *m)
+{
+	osmo_gsup_req_respond_err(m->req, GMM_CAUSE_IMSI_UNKNOWN, "could not reach home HLR");
+	m->req = NULL;
+}
+
+/* Forward spooled message for this IMSI to remote HLR. */
+static void proxy_defer_gsup_message_send(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+					  struct proxy_pending_gsup_req *m, struct remote_hlr *remote_hlr)
+{
+	LOG_PROXY_SUBSCR_MSG(proxy_subscr, &m->req->gsup, LOGL_INFO, "Forwarding deferred message\n");
+	proxy_subscr_forward_to_remote_hlr_resolved(proxy, proxy_subscr, remote_hlr, m->req);
+	m->req = NULL;
+}
+
+/* Result of looking for remote HLR. If it failed, pass remote_hlr as NULL. On failure, the proxy_subscr and the
+ * remote_hlr may be passed NULL. The IMSI then reflects who the error was for. */
+static void proxy_defer_gsup_message_pop(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+					 const char *imsi, struct remote_hlr *remote_hlr)
+{
+	struct proxy_pending_gsup_req *m, *n;
+	if (!imsi && proxy_subscr)
+		imsi = proxy_subscr->imsi;
+	OSMO_ASSERT(imsi);
+
+	if (!remote_hlr)
+		LOGP(DDGSM, LOGL_ERROR, "IMSI-%s: No remote HLR found, dropping spooled GSUP messages\n", imsi);
+
+	llist_for_each_entry_safe(m, n, &proxy->pending_gsup_reqs, entry) {
+		if (strcmp(m->req->gsup.imsi, imsi))
+			continue;
+
+		if (!remote_hlr)
+			proxy_defer_gsup_message_err(proxy, m);
+		else
+			proxy_defer_gsup_message_send(proxy, proxy_subscr, m, remote_hlr);
+
+		llist_del(&m->entry);
+		talloc_free(m);
+	}
+}
+
+
+static bool proxy_subscr_matches_imsi(const struct proxy_subscr *proxy_subscr, const char *imsi)
+{
+	if (!proxy_subscr || !imsi)
+		return false;
+	return strcmp(proxy_subscr->imsi, imsi) == 0;
+}
+
+static bool proxy_subscr_matches_msisdn(const struct proxy_subscr *proxy_subscr, const char *msisdn)
+{
+	if (!proxy_subscr || !msisdn)
+		return false;
+	return strcmp(proxy_subscr->msisdn, msisdn) == 0;
+}
+
+static struct proxy_subscr_listentry *_proxy_get_by_imsi(struct proxy *proxy, const char *imsi)
+{
+	struct proxy_subscr_listentry *e;
+	if (!proxy)
+		return NULL;
+	llist_for_each_entry(e, &proxy->subscr_list, entry) {
+		if (proxy_subscr_matches_imsi(&e->data, imsi))
+			return e;
+	}
+	return NULL;
+}
+
+static struct proxy_subscr_listentry *_proxy_get_by_msisdn(struct proxy *proxy, const char *msisdn)
+{
+	struct proxy_subscr_listentry *e;
+	if (!proxy)
+		return NULL;
+	llist_for_each_entry(e, &proxy->subscr_list, entry) {
+		if (proxy_subscr_matches_msisdn(&e->data, msisdn))
+			return e;
+	}
+	return NULL;
+}
+
+const struct proxy_subscr *proxy_subscr_get_by_imsi(struct proxy *proxy, const char *imsi)
+{
+	struct proxy_subscr_listentry *e = _proxy_get_by_imsi(proxy, imsi);
+	if (!e)
+		return NULL;
+	return &e->data;
+}
+
+const struct proxy_subscr *proxy_subscr_get_by_msisdn(struct proxy *proxy, const char *msisdn)
+{
+	struct proxy_subscr_listentry *e = _proxy_get_by_msisdn(proxy, msisdn);
+	if (!e)
+		return NULL;
+	return &e->data;
+}
+
+void proxy_subscrs_get_by_remote_hlr(struct proxy *proxy, const struct osmo_sockaddr_str *remote_hlr_addr,
+				     bool (*yield)(struct proxy *proxy, const struct proxy_subscr *subscr, void *data),
+				     void *data)
+{
+	struct proxy_subscr_listentry *e;
+	if (!proxy)
+		return;
+	llist_for_each_entry(e, &proxy->subscr_list, entry) {
+		if (!osmo_sockaddr_str_cmp(remote_hlr_addr, &e->data.remote_hlr_addr)) {
+			if (!yield(proxy, &e->data, data))
+				return;
+		}
+	}
+}
+
+int proxy_subscr_update(struct proxy *proxy, const struct proxy_subscr *proxy_subscr)
+{
+	struct proxy_subscr_listentry *e = _proxy_get_by_imsi(proxy, proxy_subscr->imsi);
+	if (!e) {
+		/* Does not exist yet */
+		e = talloc_zero(proxy, struct proxy_subscr_listentry);
+		llist_add(&e->entry, &proxy->subscr_list);
+	}
+	e->data = *proxy_subscr;
+	timestamp_update(&e->last_update);
+	return 0;
+}
+
+int _proxy_subscr_del(struct proxy_subscr_listentry *e)
+{
+	llist_del(&e->entry);
+	return 0;
+}
+
+int proxy_subscr_del(struct proxy *proxy, const char *imsi)
+{
+	struct proxy_subscr_listentry *e;
+	proxy_defer_gsup_message_pop(proxy, NULL, imsi, NULL);
+	e = _proxy_get_by_imsi(proxy, imsi);
+	if (!e)
+		return -ENOENT;
+	return _proxy_subscr_del(e);
+}
+
+/* Discard stale proxy entries. */
+static void proxy_cleanup(void *proxy_v)
+{
+	struct proxy *proxy = proxy_v;
+	struct proxy_subscr_listentry *e, *n;
+	uint32_t age;
+	llist_for_each_entry_safe(e, n, &proxy->subscr_list, entry) {
+		if (!timestamp_age(&e->last_update, &age))
+			LOGP(DDGSM, LOGL_ERROR, "Invalid timestamp, deleting proxy entry\n");
+		else if (age <= proxy->fresh_time)
+			continue;
+		LOG_PROXY_SUBSCR(&e->data, LOGL_INFO, "proxy entry timed out, deleting\n");
+		_proxy_subscr_del(e);
+	}
+	if (proxy->gc_period)
+		osmo_timer_schedule(&proxy->gc_timer, proxy->gc_period, 0);
+	else
+		LOGP(DDGSM, LOGL_NOTICE, "Proxy cleanup is switched off (gc_period == 0)\n");
+}
+
+void proxy_set_gc_period(struct proxy *proxy, uint32_t gc_period)
+{
+	proxy->gc_period = gc_period;
+	proxy_cleanup(proxy);
+}
+
+void proxy_init(struct osmo_gsup_server *gsup_server_to_vlr)
+{
+	OSMO_ASSERT(!gsup_server_to_vlr->proxy);
+	struct proxy *proxy = talloc_zero(gsup_server_to_vlr, struct proxy);
+	*proxy = (struct proxy){
+		.gsup_server_to_vlr = gsup_server_to_vlr,
+		.fresh_time = 60*60,
+		.gc_period = 60,
+	};
+	INIT_LLIST_HEAD(&proxy->subscr_list);
+	INIT_LLIST_HEAD(&proxy->pending_gsup_reqs);
+
+	osmo_timer_setup(&proxy->gc_timer, proxy_cleanup, proxy);
+	/* Invoke to trigger the first timer schedule */
+	proxy_set_gc_period(proxy, proxy->gc_period);
+	gsup_server_to_vlr->proxy = proxy;
+}
+
+void proxy_del(struct proxy *proxy)
+{
+	osmo_timer_del(&proxy->gc_timer);
+	talloc_free(proxy);
+}
+
+void proxy_subscr_remote_hlr_up(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				struct remote_hlr *remote_hlr)
+{
+	proxy_defer_gsup_message_pop(proxy, proxy_subscr, proxy_subscr->imsi, remote_hlr);
+}
+
+void proxy_subscr_remote_hlr_resolved(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				      struct remote_hlr *remote_hlr)
+{
+	struct proxy_subscr proxy_subscr_new;
+
+	if (osmo_sockaddr_str_is_nonzero(&proxy_subscr->remote_hlr_addr)) {
+		if (!osmo_sockaddr_str_cmp(&remote_hlr->addr, &proxy_subscr->remote_hlr_addr)) {
+			/* Already have this remote address */
+			return;
+		} else {
+			LOG_PROXY_SUBSCR(proxy_subscr, LOGL_NOTICE,
+					 "Remote HLR address changes to " OSMO_SOCKADDR_STR_FMT "\n",
+					 OSMO_SOCKADDR_STR_FMT_ARGS(&remote_hlr->addr));
+		}
+	}
+
+	/* Store the address. Make a copy to modify. */
+	proxy_subscr_new = *proxy_subscr;
+	proxy_subscr = &proxy_subscr_new;
+	proxy_subscr_new.remote_hlr_addr = remote_hlr->addr;
+
+	if (proxy_subscr_update(proxy, proxy_subscr)) {
+		LOG_PROXY_SUBSCR(proxy_subscr, LOGL_ERROR, "Failed to store proxy entry for remote HLR\n");
+		/* If no remote HLR is known for the IMSI, the proxy entry is pointless. */
+		proxy_subscr_del(proxy, proxy_subscr->imsi);
+		return;
+	}
+	LOG_PROXY_SUBSCR(proxy_subscr, LOGL_DEBUG, "Remote HLR resolved, stored address\n");
+}
+
+/* All GSUP messages sent to the remote HLR pass through this function, to modify the subscriber state or disallow
+ * sending the message. Return 0 to allow sending the message. */
+static int proxy_acknowledge_gsup_to_remote_hlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+						const struct osmo_gsup_req *req)
+{
+	struct proxy_subscr proxy_subscr_new = *proxy_subscr;
+	bool ps;
+	bool cs;
+	int rc;
+
+	switch (req->gsup.message_type) {
+
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST:
+		/* Store the CS and PS VLR name in vlr_name_preliminary to later update the right {cs,ps} LU timestamp
+		 * when receiving an OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT. Store in vlr_name_preliminary so that in
+		 * case the LU fails, we keep the vlr_name intact. */
+		switch (req->gsup.cn_domain) {
+		case OSMO_GSUP_CN_DOMAIN_CS:
+			proxy_subscr_new.cs.vlr_name_preliminary = req->source_name;
+			break;
+		case OSMO_GSUP_CN_DOMAIN_PS:
+			proxy_subscr_new.ps.vlr_name_preliminary = req->source_name;
+			break;
+		default:
+			break;
+		}
+
+		ps = cs = false;
+		if (osmo_gt_cmp(&proxy_subscr_new.cs.vlr_name_preliminary, &proxy_subscr->cs.vlr_name_preliminary))
+			cs = true;
+		if (osmo_gt_cmp(&proxy_subscr_new.ps.vlr_name_preliminary, &proxy_subscr->ps.vlr_name_preliminary))
+			ps = true;
+
+		if (!(cs || ps)) {
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, LOGL_DEBUG, "VLR names remain unchanged\n");
+			break;
+		}
+
+		rc = proxy_subscr_update(proxy, &proxy_subscr_new);
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, rc ? LOGL_ERROR : LOGL_INFO,
+				     "%s: preliminary VLR name for%s%s to %s\n",
+				     rc ? "failed to update" : "updated",
+				     cs ? " CS" : "", ps ? " PS" : "",
+				     osmo_gt_name(&req->source_name));
+		break;
+	/* TODO: delete proxy entry in case of a Purge Request? */
+	default:
+		break;
+	}
+	return 0;
+}
+
+/* All GSUP messages received from the remote HLR to be sent to a local MSC pass through this function, to modify the
+ * subscriber state or disallow sending the message. Return 0 to allow sending the message.
+ * The local MSC shall be indicated by gsup.destination_name. */
+static int proxy_acknowledge_gsup_from_remote_hlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+						  const struct osmo_gsup_message *gsup,
+						  struct remote_hlr *from_remote_hlr,
+						  const struct osmo_gt *destination,
+						  const struct osmo_gt *via_peer)
+{
+	struct proxy_subscr proxy_subscr_new = *proxy_subscr;
+	bool ps;
+	bool cs;
+	bool vlr_name_changed_cs;
+	bool vlr_name_changed_ps;
+	int rc;
+	struct osmo_gt via_proxy = {};
+	if (osmo_gt_cmp(via_peer, destination))
+		via_proxy = *via_peer;
+
+	switch (gsup->message_type) {
+	case OSMO_GSUP_MSGT_INSERT_DATA_REQUEST:
+		/* Remember the MSISDN of the subscriber. This does not need to be a preliminary record, because when
+		 * the HLR tells us about subscriber data, it is definitive info and there is no ambiguity (like there
+		 * would be with failed LU attempts from various sources). */
+		if (!gsup->msisdn_enc_len)
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_DEBUG, "No MSISDN in this Insert Data Request\n");
+		else if (gsm48_decode_bcd_number2(proxy_subscr_new.msisdn, sizeof(proxy_subscr_new.msisdn),
+						  gsup->msisdn_enc, gsup->msisdn_enc_len, 0))
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR, "Failed to decode MSISDN\n");
+		else if (!osmo_msisdn_str_valid(proxy_subscr_new.msisdn))
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR, "invalid MSISDN: %s\n",
+					     osmo_quote_str_c(OTC_SELECT, proxy_subscr_new.msisdn, -1));
+		else if (!strcmp(proxy_subscr->msisdn, proxy_subscr_new.msisdn))
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_DEBUG, "already have MSISDN = %s\n",
+					     proxy_subscr_new.msisdn);
+		else if (proxy_subscr_update(proxy, &proxy_subscr_new))
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR, "failed to update MSISDN to %s\n",
+					     proxy_subscr_new.msisdn);
+		else
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_INFO, "stored MSISDN=%s\n",
+					     proxy_subscr_new.msisdn);
+		break;
+
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT:
+		/* Update the Location Updating timestamp */
+		cs = ps = false;
+		if (!osmo_gt_cmp(destination, &proxy_subscr->cs.vlr_name_preliminary)) {
+			timestamp_update(&proxy_subscr_new.cs.last_lu);
+			proxy_subscr_new.cs.vlr_name_preliminary = (struct osmo_gt){};
+			vlr_name_changed_cs =
+				osmo_gt_cmp(&proxy_subscr->cs.vlr_name, destination)
+				|| osmo_gt_cmp(&proxy_subscr->cs.vlr_via_proxy, &via_proxy);
+			proxy_subscr_new.cs.vlr_name = *destination;
+			proxy_subscr_new.cs.vlr_via_proxy = via_proxy;
+			cs = true;
+		}
+		if (!osmo_gt_cmp(destination, &proxy_subscr->ps.vlr_name_preliminary)) {
+			timestamp_update(&proxy_subscr_new.ps.last_lu);
+			proxy_subscr_new.ps.vlr_name_preliminary = (struct osmo_gt){};
+			proxy_subscr_new.ps.vlr_name = *destination;
+			vlr_name_changed_ps =
+				osmo_gt_cmp(&proxy_subscr->ps.vlr_name, destination)
+				|| osmo_gt_cmp(&proxy_subscr->ps.vlr_via_proxy, &via_proxy);
+			proxy_subscr_new.ps.vlr_via_proxy = via_proxy;
+			ps = true;
+		}
+		if (!(cs || ps)) {
+			LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR,
+					     "destination is neither CS nor PS VLR: %s\n",
+					     osmo_gt_name(destination));
+			return GMM_CAUSE_PROTO_ERR_UNSPEC;
+		}
+		rc = proxy_subscr_update(proxy, &proxy_subscr_new);
+
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, rc ? LOGL_ERROR : LOGL_INFO,
+				     "%s LU: timestamp for%s%s%s%s%s%s%s%s%s%s\n",
+				     rc ? "failed to update" : "updated",
+				     cs ? " CS" : "", ps ? " PS" : "",
+				     vlr_name_changed_cs? ", CS VLR=" : "",
+				     vlr_name_changed_cs? osmo_gt_name(&proxy_subscr_new.cs.vlr_name) : "",
+				     proxy_subscr_new.cs.vlr_via_proxy.len ? " via proxy " : "",
+				     proxy_subscr_new.cs.vlr_via_proxy.len ?
+					     osmo_gt_name(&proxy_subscr_new.cs.vlr_via_proxy) : "",
+				     vlr_name_changed_ps? ", PS VLR=" : "",
+				     vlr_name_changed_ps? osmo_gt_name(&proxy_subscr_new.ps.vlr_name) : "",
+				     proxy_subscr_new.ps.vlr_via_proxy.len ? " via proxy " : "",
+				     proxy_subscr_new.ps.vlr_via_proxy.len ?
+					     osmo_gt_name(&proxy_subscr_new.ps.vlr_via_proxy) : ""
+				    );
+		break;
+
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+
+void proxy_subscr_forward_to_remote_hlr_resolved(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+						 struct remote_hlr *remote_hlr, struct osmo_gsup_req *req)
+{
+	if (proxy_acknowledge_gsup_to_remote_hlr(proxy, proxy_subscr, req)) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_PROTO_ERR_UNSPEC, "Proxy does not allow this message");
+		return;
+	}
+
+	remote_hlr_gsup_forward_to_remote_hlr(remote_hlr, req);
+}
+
+void proxy_subscr_forward_to_remote_hlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr, struct osmo_gsup_req *req)
+{
+	struct remote_hlr *remote_hlr;
+
+	if (!osmo_sockaddr_str_is_nonzero(&proxy_subscr->remote_hlr_addr)) {
+		/* We don't know the remote target yet. Still waiting for an MS lookup response. */
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, LOGL_DEBUG, "deferring until remote HLR is known\n");
+		proxy_defer_gsup_req(proxy, req);
+		return;
+	}
+
+	if (req->via_proxy.len) {
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, LOGL_INFO, "VLR->HLR: forwarding from %s via proxy %s\n",
+				     osmo_gt_name(&req->source_name),
+				     osmo_gt_name(&req->via_proxy));
+	} else {
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, LOGL_INFO, "VLR->HLR: forwarding from %s\n",
+				     osmo_gt_name(&req->source_name));
+	}
+
+	remote_hlr = remote_hlr_get(&proxy_subscr->remote_hlr_addr, true);
+	if (!remote_hlr) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL,
+					  "Proxy: Failed to establish connection to remote HLR " OSMO_SOCKADDR_STR_FMT,
+					  OSMO_SOCKADDR_STR_FMT_ARGS(&proxy_subscr->remote_hlr_addr));
+		return;
+	}
+
+	if (!remote_hlr->gsupc || !remote_hlr->gsupc->is_connected) {
+		/* GSUP link is still busy establishing... */
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, LOGL_DEBUG,
+				     "deferring until link to remote HLR is up\n");
+		proxy_defer_gsup_req(proxy, req);
+		return;
+	}
+
+	proxy_subscr_forward_to_remote_hlr_resolved(proxy, proxy_subscr, remote_hlr, req);
+}
+
+int proxy_subscr_forward_to_vlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				const struct osmo_gsup_message *gsup, struct remote_hlr *from_remote_hlr)
+{
+	struct osmo_gt destination;
+	struct osmo_gsup_conn *vlr_conn;
+	struct msgb *msg;
+
+	if (osmo_gt_set(&destination, gsup->destination_name, gsup->destination_name_len)) {
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR,
+				     "no valid Destination Name IE, cannot route to VLR.\n");
+		return GMM_CAUSE_INV_MAND_INFO;
+	}
+
+	/* Route to MSC/SGSN that we're proxying for */
+	vlr_conn = gsup_route_find_gt(proxy->gsup_server_to_vlr, &destination);
+	if (!vlr_conn) {
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR,
+				     "Destination VLR unreachable: %s\n", osmo_gt_name(&destination));
+		return GMM_CAUSE_MSC_TEMP_NOTREACH;
+	}
+
+	if (proxy_acknowledge_gsup_from_remote_hlr(proxy, proxy_subscr, gsup, from_remote_hlr, &destination,
+						   &vlr_conn->peer_name)) {
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR,
+				     "Proxy does not allow forwarding this message\n");
+		return GMM_CAUSE_PROTO_ERR_UNSPEC;
+	}
+
+	msg = osmo_gsup_msgb_alloc("GSUP proxy to VLR");
+	if (osmo_gsup_encode(msg, gsup)) {
+		LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_ERROR,
+				     "Failed to re-encode GSUP message, cannot forward\n");
+		return GMM_CAUSE_INV_MAND_INFO;
+	}
+
+	LOG_PROXY_SUBSCR_MSG(proxy_subscr, gsup, LOGL_INFO, "VLR<-HLR: forwarding to %s%s%s\n",
+			     osmo_gt_name(&destination),
+			     osmo_gt_cmp(&destination, &vlr_conn->peer_name) ? " via " : "",
+			     osmo_gt_cmp(&destination, &vlr_conn->peer_name) ?
+						       osmo_gt_name(&vlr_conn->peer_name) : "");
+	return osmo_gsup_conn_send(vlr_conn, msg);
+}
diff --git a/src/remote_hlr.c b/src/remote_hlr.c
new file mode 100644
index 0000000..08f7bb5
--- /dev/null
+++ b/src/remote_hlr.c
@@ -0,0 +1,180 @@
+#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;
+	const 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;
+	}
+
+	proxy_subscr = proxy_subscr_get_by_imsi(g_hlr->gs->proxy, gsup.imsi);
+	if (!proxy_subscr) {
+		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;
+}
+
+static bool remote_hlr_up_yield(struct proxy *proxy, const struct proxy_subscr *proxy_subscr, void *data)
+{
+	struct remote_hlr *remote_hlr = data;
+	proxy_subscr_remote_hlr_up(proxy, proxy_subscr, remote_hlr);
+	return true;
+}
+
+static bool remote_hlr_up_down(struct osmo_gsup_client *gsupc, bool up)
+{
+	struct remote_hlr *remote_hlr = gsupc->data;
+	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");
+	proxy_subscrs_get_by_remote_hlr(g_hlr->gs->proxy, &remote_hlr->addr, remote_hlr_up_yield, remote_hlr);
+	return true;
+}
+
+struct remote_hlr *remote_hlr_get(const struct osmo_sockaddr_str *addr, bool create)
+{
+	struct remote_hlr *rh;
+
+	llist_for_each_entry(rh, &remote_hlrs, entry) {
+		if (!osmo_sockaddr_str_cmp(&rh->addr, addr))
+			return rh;
+	}
+
+	if (!create)
+		return NULL;
+
+	/* Doesn't exist yet, create a GSUP client to remote HLR. */
+	rh = talloc_zero(dgsm_ctx, struct remote_hlr);
+	OSMO_ASSERT(rh);
+	*rh = (struct remote_hlr){
+		.addr = *addr,
+		.gsupc = osmo_gsup_client_create3(rh, &g_hlr->gsup_unit_name,
+						  addr->ip, addr->port,
+						  NULL,
+						  remote_hlr_rx,
+						  remote_hlr_up_down,
+						  rh),
+	};
+	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);
+		return NULL;
+	}
+	rh->gsupc->data = rh;
+	llist_add(&rh->entry, &remote_hlrs);
+	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)
+{
+	int rc;
+	struct msgb *msg = osmo_gsup_msgb_alloc("GSUP proxy to remote HLR");
+	/* 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 = req->gsup;
+	forward.source_name = req->source_name.val;
+	forward.source_name_len = req->source_name.len;
+
+	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\n");
+		return;
+	}
+	remote_hlr_msgb_send(remote_hlr, msg);
+	osmo_gsup_req_free(req);
+}
diff --git a/tests/test_nodes.vty b/tests/test_nodes.vty
index 610c610..ac9ff31 100644
--- a/tests/test_nodes.vty
+++ b/tests/test_nodes.vty
@@ -35,6 +35,7 @@
   show gsup-connections
   subscriber (imsi|msisdn|id|imei) IDENT show
   show subscriber (imsi|msisdn|id|imei) IDENT
+  show mslookup services
 
 OsmoHLR> enable
 OsmoHLR# ?
@@ -81,6 +82,7 @@
   log       Configure logging sub-system
   stats     Configure stats sub-system
   hlr       Configure the HLR
+  mslookup  Configure Distributed GSM mslookup
 OsmoHLR(config)# list
   help
 ...
@@ -88,6 +90,7 @@
   end
 ...
   hlr
+  mslookup
 
 OsmoHLR(config)# hlr
 OsmoHLR(config-hlr)# ?
@@ -121,10 +124,12 @@
 OsmoHLR(config-hlr)# gsup
 OsmoHLR(config-hlr-gsup)# ?
 ...
-  bind   Listen/Bind related socket option
+  bind      Listen/Bind related socket option
+  ipa-name  Set the IPA name of this HLR, for proxying to remote HLRs
 OsmoHLR(config-hlr-gsup)# list
 ...
   bind ip A.B.C.D
+  ipa-name NAME
 
 OsmoHLR(config-hlr-gsup)# exit
 OsmoHLR(config-hlr)# exit
@@ -149,6 +154,7 @@
  logging level auc notice
  logging level ss info
  logging level lu notice
+ logging level dgsm notice
 ...
 hlr
  store-imei
@@ -158,3 +164,324 @@
  ussd route prefix *#100# internal own-msisdn
  ussd route prefix *#101# internal own-imsi
 end
+
+OsmoHLR# configure terminal
+
+OsmoHLR(config)# mslookup
+OsmoHLR(config-mslookup)# ?
+...
+  mdns    Convenience shortcut: enable and configure both server and client for mDNS mslookup
+  no      Negate a command or set its defaults
+  server  Enable and configure Distributed GSM mslookup server
+  client  Enable and configure Distributed GSM mslookup client
+OsmoHLR(config-mslookup)# list
+...
+  mdns [IP] [<1-65535>]
+  no mdns
+  server
+  no server
+  client
+  no client
+
+OsmoHLR(config-mslookup)# ?
+...
+  mdns    Convenience shortcut: enable and configure both server and client for mDNS mslookup
+  no      Negate a command or set its defaults
+  server  Enable and configure Distributed GSM mslookup server
+  client  Enable and configure Distributed GSM mslookup client
+OsmoHLR(config-mslookup)# no?
+  no  Negate a command or set its defaults
+OsmoHLR(config-mslookup)# no ?
+  mdns    Disable both server and client for mDNS mslookup
+  server  Disable Distributed GSM mslookup server
+  client  Disable Distributed GSM mslookup client
+OsmoHLR(config-mslookup)# mdns ?
+  [IP]  multicast IPv4 address like 239.192.23.42 or IPv6 address like ff08::23:42
+OsmoHLR(config-mslookup)# mdns 1.2.3.4 ?
+  [<1-65535>]  mDNS UDP Port number
+
+OsmoHLR(config-mslookup)# server
+OsmoHLR(config-mslookup-server)# ?
+...
+  mdns     Configure where the mDNS server listens for mslookup requests
+  no       Negate a command or set its defaults
+  service  Configure addresses of local services, as sent in replies to remote mslookup requests.
+  msc      Configure services for individual local MSCs
+OsmoHLR(config-mslookup-server)# list
+...
+  mdns [IP] [<1-65535>]
+  no mdns
+  service NAME at IP <1-65535>
+  no service NAME
+  no service NAME at IP <1-65535>
+  msc .UNIT_NAME
+
+OsmoHLR(config-mslookup-server)# mdns?
+  mdns  Configure where the mDNS server listens for mslookup requests
+OsmoHLR(config-mslookup-server)# mdns ?
+  [IP]  multicast IPv4 address like 239.192.23.42 or IPv6 address like ff08::23:42
+OsmoHLR(config-mslookup-server)# mdns bind ?
+  [<1-65535>]  mDNS UDP Port number
+OsmoHLR(config-mslookup-server)# mdns bind 1.2.3.4 ?
+% There is no matched command.
+OsmoHLR(config-mslookup-server)# mdns bind 1.2.3.4 ?
+% There is no matched command.
+
+OsmoHLR(config-mslookup-server)# service?
+  service  Configure addresses of local services, as sent in replies to remote mslookup requests.
+OsmoHLR(config-mslookup-server)# service ?
+  NAME  mslookup service name, e.g. sip.voice or smpp.sms
+OsmoHLR(config-mslookup-server)# service foo ?
+  at  at
+OsmoHLR(config-mslookup-server)# service foo at ?
+  IP  IPv4 address like 1.2.3.4 or IPv6 address like a:b:c:d::1
+OsmoHLR(config-mslookup-server)# service foo at 1.2.3.4 ?
+  <1-65535>  Service-specific port number
+
+OsmoHLR(config-mslookup-server)# no ?
+  mdns     Disable server for mDNS mslookup (do not answer remote requests)
+  service  Remove one or more service address entries
+OsmoHLR(config-mslookup-server)# no service ?
+  NAME  mslookup service name, e.g. sip.voice or smpp.sms
+OsmoHLR(config-mslookup-server)# no service foo ?
+  at    at
+  <cr>  
+OsmoHLR(config-mslookup-server)# no service foo at ?
+  IP  IPv4 address like 1.2.3.4 or IPv6 address like a:b:c:d::1
+OsmoHLR(config-mslookup-server)# no service foo at 1.2.3.4 ?
+  <1-65535>  Service-specific port number
+
+OsmoHLR(config-mslookup-server)# msc?
+  msc  Configure services for individual local MSCs
+OsmoHLR(config-mslookup-server)# msc ?
+  UNIT_NAME  IPA Unit Name of the local MSC to configure
+
+OsmoHLR(config-mslookup-server)# msc MSC-1
+OsmoHLR(config-mslookup-server-msc)# ?
+...
+  service  Configure addresses of local services, as sent in replies to remote mslookup requests.
+  no       Negate a command or set its defaults
+OsmoHLR(config-mslookup-server-msc)# list
+...
+  service NAME at IP <1-65535>
+  no service NAME
+  no service NAME at IP <1-65535>
+
+OsmoHLR(config-mslookup-server-msc)# service?
+  service  Configure addresses of local services, as sent in replies to remote mslookup requests.
+OsmoHLR(config-mslookup-server-msc)# service ?
+  NAME  mslookup service name, e.g. sip.voice or smpp.sms
+OsmoHLR(config-mslookup-server-msc)# service foo ?
+  at  at
+OsmoHLR(config-mslookup-server-msc)# service foo at ?
+  IP  IPv4 address like 1.2.3.4 or IPv6 address like a:b:c:d::1
+OsmoHLR(config-mslookup-server-msc)# service foo at 1.2.3.4 ?
+  <1-65535>  Service-specific port number
+
+OsmoHLR(config-mslookup-server-msc)# no ?
+  service  Remove one or more service address entries
+OsmoHLR(config-mslookup-server-msc)# no service ?
+  NAME  mslookup service name, e.g. sip.voice or smpp.sms
+OsmoHLR(config-mslookup-server-msc)# no service foo ?
+  at    at
+  <cr>  
+OsmoHLR(config-mslookup-server-msc)# no service foo at ?
+  IP  IPv4 address like 1.2.3.4 or IPv6 address like a:b:c:d::1
+OsmoHLR(config-mslookup-server-msc)# no service foo at 1.2.3.4 ?
+  <1-65535>  Service-specific port number
+
+OsmoHLR(config-mslookup-server-msc)# exit
+OsmoHLR(config-mslookup-server)# exit
+OsmoHLR(config-mslookup)# client
+OsmoHLR(config-mslookup-client)# ?
+...
+  timeout        How long should the mslookup client wait for remote responses before evaluating received results
+  mdns           Enable mDNS client, and configure multicast address to send mDNS mslookup requests to
+  no             Negate a command or set its defaults
+  gateway-proxy  Configure a fixed IP address to send all GSUP requests for unknown IMSIs to, without invoking a lookup for IMSI
+OsmoHLR(config-mslookup-client)# list
+...
+  timeout <1-100000>
+  mdns [IP] [<1-65535>]
+  no mdns
+  gateway-proxy IP [<1-65535>]
+  no gateway-proxy
+
+OsmoHLR(config-mslookup-client)# timeout?
+  timeout  How long should the mslookup client wait for remote responses before evaluating received results
+OsmoHLR(config-mslookup-client)# timeout ?
+  <1-100000>  timeout in milliseconds
+
+OsmoHLR(config-mslookup-client)# mdns?
+  mdns  Enable mDNS client, and configure multicast address to send mDNS mslookup requests to
+OsmoHLR(config-mslookup-client)# mdns to ?
+  [<1-65535>]  mDNS UDP Port number
+OsmoHLR(config-mslookup-client)# mdns to 1.2.3.4 ?
+% There is no matched command.
+
+OsmoHLR(config-mslookup-client)# gateway-proxy?
+  gateway-proxy  Configure a fixed IP address to send all GSUP requests for unknown IMSIs to, without invoking a lookup for IMSI
+OsmoHLR(config-mslookup-client)# gateway-proxy ?
+  IP  IP address of the remote HLR
+OsmoHLR(config-mslookup-client)# gateway-proxy 1.2.3.4 ?
+  [<1-65535>]  GSUP port number (omit for default 4222)
+
+OsmoHLR(config-mslookup-client)# no?
+  no  Negate a command or set its defaults
+OsmoHLR(config-mslookup-client)# no ?
+  mdns           Disable mDNS client, do not query remote services by mDNS
+  gateway-proxy  Disable gateway proxy for GSUP with unknown IMSIs
+
+OsmoHLR(config-mslookup-client)# gateway-proxy ?
+  IP  IP address of the remote HLR
+OsmoHLR(config-mslookup-client)# gateway-proxy 1.2.3.4 ?
+  [<1-65535>]  GSUP port number (omit for default 4222)
+
+OsmoHLR(config-mslookup-client)# do show mslookup?
+  mslookup  Distributed GSM / mslookup related information
+OsmoHLR(config-mslookup-client)# do show mslookup ?
+  services  List configured service addresses as sent to remote mslookup requests
+
+OsmoHLR(config-mslookup-client)# gateway-proxy 1.2.3.4
+
+OsmoHLR(config-mslookup-client)# exit
+
+OsmoHLR(config-mslookup)# mdns
+OsmoHLR(config-mslookup)# server
+OsmoHLR(config-mslookup-server)# service qwert at 123.45.67.89 qwert
+% Unknown command.
+OsmoHLR(config-mslookup-server)# service qwert at qwert 1234
+% mslookup server: Invalid address for service qwert: qwert 1234
+OsmoHLR(config-mslookup-server)# service foo.bar at 123.45.67.89 1011
+OsmoHLR(config-mslookup-server)# service baz.bar at 121.31.41.5 1617
+OsmoHLR(config-mslookup-server)# service baz.bar at a:b:c::d 1819
+OsmoHLR(config-mslookup-server)# msc msc-901-70-23
+OsmoHLR(config-mslookup-server-msc)# service foo.bar at 76.54.32.10 1234
+OsmoHLR(config-mslookup-server-msc)# service baz.bar at 12.11.10.98 7654
+OsmoHLR(config-mslookup-server-msc)# service baz.bar at 999:999:999::999 9999
+OsmoHLR(config-mslookup-server-msc)# service baz.bar at dd:cc:bb::a 3210
+OsmoHLR(config-mslookup-server-msc)# exit
+OsmoHLR(config-mslookup-server)# msc msc-901-70-42
+OsmoHLR(config-mslookup-server-msc)# service foo.bar at 1.1.1.1 1111
+OsmoHLR(config-mslookup-server-msc)# service baz.bar at 2.2.2.2 2222
+OsmoHLR(config-mslookup-server-msc)# service baz.bar at 2222:2222:2222::2 2222
+OsmoHLR(config-mslookup-server-msc)# do show mslookup services
+Local GSUP HLR address returned in mslookup responses for local IMSIs: 127.0.0.1:4222
+service foo.bar at 123.45.67.89 1011
+service baz.bar at 121.31.41.5 1617
+service baz.bar at a:b:c::d 1819
+msc MSC-1
+msc msc-901-70-23
+ service foo.bar at 76.54.32.10 1234
+ service baz.bar at 12.11.10.98 7654
+ service baz.bar at dd:cc:bb::a 3210
+msc msc-901-70-42
+ service foo.bar at 1.1.1.1 1111
+ service baz.bar at 2.2.2.2 2222
+ service baz.bar at 2222:2222:2222::2 2222
+
+OsmoHLR(config-mslookup-server-msc)# show running-config
+...
+mslookup
+ server
+  mdns bind 239.192.23.42 4266
+ service foo.bar at 123.45.67.89 1011
+ service baz.bar at 121.31.41.5 1617
+ service baz.bar at a:b:c::d 1819
+ msc MSC-1
+ msc msc-901-70-23
+  service foo.bar at 76.54.32.10 1234
+  service baz.bar at 12.11.10.98 7654
+  service baz.bar at dd:cc:bb::a 3210
+ msc msc-901-70-42
+  service foo.bar at 1.1.1.1 1111
+  service baz.bar at 2.2.2.2 2222
+  service baz.bar at 2222:2222:2222::2 2222
+ client
+  gateway-proxy 1.2.3.4 4222
+  mdns to 239.192.23.42 4266
+...
+
+OsmoHLR(config-mslookup-server-msc)# no service baz.bar
+OsmoHLR(config-mslookup-server-msc)# no service asdf
+% mslookup server: cannot remove service 'asdf'
+OsmoHLR(config-mslookup-server-msc)# exit
+OsmoHLR(config-mslookup-server)# msc msc-901-70-23
+OsmoHLR(config-mslookup-server-msc)# no service baz.bar at dd:cc:bb::a 3210
+% mslookup server: cannot remove service 'baz.bar' to dd:cc:bb::a 3210
+OsmoHLR(config-mslookup-server-msc)# no service asdf at asdf asdf
+% Unknown command.
+OsmoHLR(config-mslookup-server-msc)# no service asdf at asdf 3210
+% mslookup server: Invalid address for 'no service' asdf: asdf 3210
+OsmoHLR(config-mslookup-server-msc)# no service asdf at dd:cc:bb::a 3210
+% mslookup server: cannot remove service 'asdf' to dd:cc:bb::a 3210
+OsmoHLR(config-mslookup-server-msc)# exit
+OsmoHLR(config-mslookup-server)# no service baz.bar at 2.2.2.2 2222
+% mslookup server: cannot remove service 'baz.bar' to 2.2.2.2 2222
+OsmoHLR(config-mslookup-server)# no service baz.bar at a:b:c::d 1819
+% mslookup server: cannot remove service 'baz.bar' to a:b:c::d 1819
+
+OsmoHLR(config-mslookup-server)# exit
+OsmoHLR(config-mslookup)# client
+OsmoHLR(config-mslookup-client)# no gateway-proxy
+
+OsmoHLR(config-mslookup-client)# do show mslookup services
+Local GSUP HLR address returned in mslookup responses for local IMSIs: 127.0.0.1:4222
+service foo.bar at 123.45.67.89 1011
+service baz.bar at 121.31.41.5 1617
+msc MSC-1
+msc msc-901-70-23
+ service foo.bar at 76.54.32.10 1234
+ service baz.bar at 12.11.10.98 7654
+msc msc-901-70-42
+ service foo.bar at 1.1.1.1 1111
+
+OsmoHLR(config-mslookup-client)# show running-config
+...
+mslookup
+ server
+  mdns bind 239.192.23.42 4266
+ service foo.bar at 123.45.67.89 1011
+ service baz.bar at 121.31.41.5 1617
+ msc MSC-1
+ msc msc-901-70-23
+  service foo.bar at 76.54.32.10 1234
+  service baz.bar at 12.11.10.98 7654
+ msc msc-901-70-42
+  service foo.bar at 1.1.1.1 1111
+ client
+  mdns to 239.192.23.42 4266
+...
+
+OsmoHLR(config-mslookup-client)# exit
+OsmoHLR(config-mslookup)# server
+OsmoHLR(config-mslookup-server)# service gsup.hlr at 23.42.17.11 4223
+OsmoHLR(config-mslookup-server)# do show mslookup services
+Local GSUP HLR address returned in mslookup responses for local IMSIs: 23.42.17.11:4223
+service foo.bar at 123.45.67.89 1011
+service baz.bar at 121.31.41.5 1617
+service gsup.hlr at 23.42.17.11 4223
+msc MSC-1
+msc msc-901-70-23
+ service foo.bar at 76.54.32.10 1234
+ service baz.bar at 12.11.10.98 7654
+msc msc-901-70-42
+ service foo.bar at 1.1.1.1 1111
+
+OsmoHLR(config-mslookup-server)# show running-config
+...
+mslookup
+ server
+  mdns bind 239.192.23.42 4266
+ service foo.bar at 123.45.67.89 1011
+ service baz.bar at 121.31.41.5 1617
+ service gsup.hlr at 23.42.17.11 4223
+ msc MSC-1
+ msc msc-901-70-23
+  service foo.bar at 76.54.32.10 1234
+  service baz.bar at 12.11.10.98 7654
+ msc msc-901-70-42
+  service foo.bar at 1.1.1.1 1111
+ client
+  mdns to 239.192.23.42 4266
+...
diff --git a/tests/test_subscriber.vty b/tests/test_subscriber.vty
index 8e9026d..fb5da0e 100644
--- a/tests/test_subscriber.vty
+++ b/tests/test_subscriber.vty
@@ -13,6 +13,7 @@
   subscriber (imsi|msisdn|id|imei) IDENT update aud3g milenage k K (op|opc) OP_C [ind-bitlen] [<0-28>]
   subscriber (imsi|msisdn|id|imei) IDENT update imei (none|IMEI)
   subscriber (imsi|msisdn|id|imei) IDENT update network-access-mode (none|cs|ps|cs+ps)
+  show mslookup services
 
 OsmoHLR# subscriber?
   subscriber  Subscriber management commands

-- 
To view, visit https://gerrit.osmocom.org/c/osmo-hlr/+/16258
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-hlr
Gerrit-Branch: master
Gerrit-Change-Id: I2fe453553c90e6ee527ed13a13089900efd488aa
Gerrit-Change-Number: 16258
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr at sysmocom.de>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20191127/37b2fc02/attachment.htm>


More information about the gerrit-log mailing list