Change in osmo-hlr[master]: implement D-GSM 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
Mon Nov 25 04:46:05 UTC 2019


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


Change subject: implement D-GSM in osmo-hlr
......................................................................

implement D-GSM in osmo-hlr

Implement the mslookup server to service remote mslookup requests:
- VTY mslookup/server config with service names,
- the mslookup_mdns_server listening for mslookup requests,
- determine whether a subscriber is on a local MSC.

Use the mslookup client to proxy GSUP to remote HLRs:
- VTY mslookup/client config,
- remote_hlr.c to connect GSUP clients to remote GSUP servers,
- proxy.c as local GSUP proxy state, so far in-memory.
  This is kept an opaque API without a mutable struct, so that it can be easily
  moved to a persistent database implementation.

dgsm.c orchestrates mslookup server, client, and MUXes GSUP messages to the
right proxy peers.

Change-Id: Ife4a61d71926d08f310a1aeed9d9f1974f64178b
---
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/hlr_vty.h
M include/osmocom/hlr/logging.h
A include/osmocom/hlr/mslookup_server.h
A include/osmocom/hlr/mslookup_server_mdns.h
A include/osmocom/hlr/proxy.h
A include/osmocom/hlr/remote_hlr.h
M src/Makefile.am
A src/dgsm.c
A src/dgsm_vty.c
M src/gsup_server.c
M src/hlr.c
M src/hlr_vty.c
M src/hlr_vty_subscr.c
M src/logging.c
A src/mslookup_server.c
A src/mslookup_server_mdns.c
A src/proxy.c
A src/remote_hlr.c
M tests/test_nodes.vty
23 files changed, 2,697 insertions(+), 0 deletions(-)



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

diff --git a/include/osmocom/hlr/Makefile.am b/include/osmocom/hlr/Makefile.am
index 532fa5d..38011f6 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 \
@@ -10,5 +11,9 @@
 	hlr_vty_subscr.h \
 	logging.h \
 	lu_fsm.h \
+	mslookup_server.h \
+	mslookup_server_mdns.h \
+	proxy.h \
 	rand.h \
+	remote_hlr.h \
 	$(NULL)
diff --git a/include/osmocom/hlr/dgsm.h b/include/osmocom/hlr/dgsm.h
new file mode 100644
index 0000000..d89ace7
--- /dev/null
+++ b/include/osmocom/hlr/dgsm.h
@@ -0,0 +1,78 @@
+#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;
+
+struct dgsm_service_host {
+	struct llist_head entry;
+	char service[OSMO_MSLOOKUP_SERVICE_MAXLEN+1];
+	struct osmo_sockaddr_str host_v4;
+	struct osmo_sockaddr_str host_v6;
+};
+
+struct dgsm_msc_config {
+	struct llist_head entry;
+	struct osmo_gt name;
+	struct llist_head service_hosts;
+};
+
+/* "Sketch pad" where the VTY can store config items without yet applying. The changes will be applied by e.g.
+ * dgsm_mdns_server_config_apply() and dgsm_mdns_client_config_apply(). */
+struct dgsm_config {
+	struct {
+		/* Whether to listen for incoming MS Lookup requests */
+		bool enable;
+
+		struct {
+			bool enable;
+			struct osmo_sockaddr_str bind_addr;
+		} mdns;
+
+		struct llist_head msc_configs;
+	} server;
+
+	struct {
+		/* Whether to ask remote HLRs via MS Lookup if an IMSI is not known locally. */
+		bool enable;
+		struct timeval timeout;
+
+		struct {
+			/* Whether to use mDNS for IMSI MS Lookup */
+			bool enable;
+			struct osmo_sockaddr_str query_addr;
+		} mdns;
+
+		struct osmo_sockaddr_str gateway_proxy;
+	} client;
+};
+
+struct dgsm_service_host *dgsm_config_service_get(const struct osmo_gt *msc_name, const char *service);
+
+struct dgsm_service_host *dgsm_config_msc_service_get(struct dgsm_msc_config *msc, const char *service, bool create);
+int dgsm_config_msc_service_set(struct dgsm_msc_config *msc, const char *service, const struct osmo_sockaddr_str *addr);
+int dgsm_config_msc_service_del(struct dgsm_msc_config *msc, const char *service, const struct osmo_sockaddr_str *addr);
+
+extern const struct osmo_gt dgsm_config_msc_wildcard;
+struct dgsm_msc_config *dgsm_config_msc_get(const struct osmo_gt *msc_name, bool create);
+
+void dgsm_init(void *ctx);
+void dgsm_start(void *ctx);
+void dgsm_config_apply(const struct dgsm_config *c);
+void dgsm_stop();
+
+bool dgsm_check_forward_gsup_msg(struct osmo_gsup_req *req);
+
+void dgsm_vty_init();
+
+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 2214a8b..f9d7450 100644
--- a/include/osmocom/hlr/hlr.h
+++ b/include/osmocom/hlr/hlr.h
@@ -27,6 +27,8 @@
 #include <osmocom/gsm/ipa.h>
 #include <osmocom/core/tdef.h>
 
+#include <osmocom/hlr/dgsm.h>
+
 #define HLR_DEFAULT_DB_FILE_PATH "hlr.db"
 
 struct hlr_euse;
@@ -68,6 +70,22 @@
 	/* Bitmask of DB_SUBSCR_FLAG_* */
 	uint8_t subscr_create_on_demand_flags;
 	unsigned int subscr_create_on_demand_rand_msisdn_len;
+
+	struct {
+		bool allow_startup;
+		struct dgsm_config vty;
+
+		struct {
+			struct osmo_mslookup_server_mdns *mdns;
+			uint32_t max_age;
+		} server;
+
+		struct {
+			unsigned int result_timeout_milliseconds;
+			struct osmo_mslookup_client *client;
+			struct osmo_mslookup_client_method *mdns;
+		} client;
+	} mslookup;
 };
 
 extern struct hlr *g_hlr;
diff --git a/include/osmocom/hlr/hlr_vty.h b/include/osmocom/hlr/hlr_vty.h
index acd6510..1b9b59e 100644
--- a/include/osmocom/hlr/hlr_vty.h
+++ b/include/osmocom/hlr/hlr_vty.h
@@ -31,6 +31,10 @@
 	HLR_NODE = _LAST_OSMOVTY_NODE + 1,
 	GSUP_NODE,
 	EUSE_NODE,
+	MSLOOKUP_NODE,
+	MSLOOKUP_SERVER_NODE,
+	MSLOOKUP_SERVER_MSC_NODE,
+	MSLOOKUP_CLIENT_NODE,
 };
 
 int hlr_vty_is_config_node(struct vty *vty, int node);
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/mslookup_server.h b/include/osmocom/hlr/mslookup_server.h
new file mode 100644
index 0000000..68a8695
--- /dev/null
+++ b/include/osmocom/hlr/mslookup_server.h
@@ -0,0 +1,8 @@
+#pragma once
+
+struct osmo_mslookup_query;
+struct osmo_mslookup_result;
+
+struct dgsm_service_host *mslookup_server_get_local_gsup_addr();
+void osmo_mslookup_server_rx(const struct osmo_mslookup_query *query,
+			     struct osmo_mslookup_result *result);
diff --git a/include/osmocom/hlr/mslookup_server_mdns.h b/include/osmocom/hlr/mslookup_server_mdns.h
new file mode 100644
index 0000000..8d4d4fc
--- /dev/null
+++ b/include/osmocom/hlr/mslookup_server_mdns.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <stdbool.h>
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/mslookup/mdns_sock.h>
+
+struct osmo_mslookup_server_mdns {
+	struct osmo_mslookup_server *mslookup;
+	struct osmo_sockaddr_str bind_addr;
+	struct osmo_mdns_sock *sock;
+};
+
+struct osmo_mslookup_server_mdns *osmo_mslookup_server_mdns_start(void *ctx, const struct osmo_sockaddr_str *bind_addr);
+void osmo_mslookup_server_mdns_stop(struct osmo_mslookup_server_mdns *server);
diff --git a/include/osmocom/hlr/proxy.h b/include/osmocom/hlr/proxy.h
new file mode 100644
index 0000000..1aaee81
--- /dev/null
+++ b/include/osmocom/hlr/proxy.h
@@ -0,0 +1,86 @@
+#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>
+
+struct osmo_gsup_req;
+struct remote_hlr;
+
+typedef time_t timestamp_t;
+void timestamp_update(timestamp_t *timestamp);
+bool timestamp_age(const timestamp_t *timestamp, uint32_t *age);
+
+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 3a83616..5113aa4 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -9,6 +9,7 @@
 	$(LIBOSMOGSM_CFLAGS) \
 	$(LIBOSMOVTY_CFLAGS) \
 	$(LIBOSMOCTRL_CFLAGS) \
+	$(LIBOSMOMSLOOKUP_CFLAGS) \
 	$(LIBOSMOABIS_CFLAGS) \
 	$(SQLITE3_CFLAGS) \
 	$(NULL)
@@ -52,15 +53,23 @@
 	hlr_vty_subscr.c \
 	gsup_send.c \
 	hlr_ussd.c \
+	proxy.c \
+	dgsm.c \
+	dgsm_vty.c \
+	remote_hlr.c \
+	mslookup_server.c \
+	mslookup_server_mdns.c \
 	lu_fsm.c \
 	$(NULL)
 
 osmo_hlr_LDADD = \
 	$(top_builddir)/src/gsupclient/libosmo-gsup-client.la \
+	$(top_builddir)/src/mslookup/libosmo-mslookup.la \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOVTY_LIBS) \
 	$(LIBOSMOCTRL_LIBS) \
+	$(LIBOSMOMSLOOKUP_LIBS) \
 	$(LIBOSMOABIS_LIBS) \
 	$(SQLITE3_LIBS) \
 	$(NULL)
diff --git a/src/dgsm.c b/src/dgsm.c
new file mode 100644
index 0000000..9856188
--- /dev/null
+++ b/src/dgsm.c
@@ -0,0 +1,402 @@
+#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_mdns.h>
+#include <osmocom/hlr/dgsm.h>
+
+void *dgsm_ctx = NULL;
+
+const struct osmo_gt dgsm_config_msc_wildcard = {};
+
+struct dgsm_msc_config *dgsm_config_msc_get(const struct osmo_gt *msc_name, bool create)
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	struct dgsm_msc_config *msc;
+
+	if (!msc_name)
+		return NULL;
+
+	llist_for_each_entry(msc, &c->server.msc_configs, entry) {
+		if (osmo_gt_cmp(&msc->name, msc_name))
+			continue;
+		return msc;
+	}
+	if (!create)
+		return NULL;
+
+	msc = talloc_zero(dgsm_ctx, struct dgsm_msc_config);
+	OSMO_ASSERT(msc);
+	INIT_LLIST_HEAD(&msc->service_hosts);
+	msc->name = *msc_name;
+	llist_add_tail(&msc->entry, &c->server.msc_configs);
+	return msc;
+}
+
+struct dgsm_service_host *dgsm_config_msc_service_get(struct dgsm_msc_config *msc, const char *service, bool create)
+{
+	struct dgsm_service_host *e;
+	if (!msc)
+		return NULL;
+
+	llist_for_each_entry(e, &msc->service_hosts, entry) {
+		if (!strcmp(e->service, service))
+			return e;
+	}
+
+	if (!create)
+		return NULL;
+
+	e = talloc_zero(msc, struct dgsm_service_host);
+	OSMO_ASSERT(e);
+	OSMO_STRLCPY_ARRAY(e->service, service);
+	llist_add_tail(&e->entry, &msc->service_hosts);
+	return e;
+}
+
+struct dgsm_service_host *dgsm_config_service_get(const struct osmo_gt *msc_name, const char *service)
+{
+	struct dgsm_msc_config *msc = dgsm_config_msc_get(msc_name, false);
+	if (!msc)
+		return NULL;
+	return dgsm_config_msc_service_get(msc, service, false);
+}
+
+int dgsm_config_msc_service_set(struct dgsm_msc_config *msc, const char *service, const struct osmo_sockaddr_str *addr)
+{
+	struct dgsm_service_host *e;
+
+	if (!service || !service[0]
+	    || strlen(service) > OSMO_MSLOOKUP_SERVICE_MAXLEN)
+		return -EINVAL;
+	if (!addr || !osmo_sockaddr_str_is_nonzero(addr))
+		return -EINVAL;
+
+	e = dgsm_config_msc_service_get(msc, service, true);
+	if (!e)
+		return -EINVAL;
+
+	switch (addr->af) {
+	case AF_INET:
+		e->host_v4 = *addr;
+		break;
+	case AF_INET6:
+		e->host_v6 = *addr;
+		break;
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+int dgsm_config_msc_service_del(struct dgsm_msc_config *msc, const char *service, const struct osmo_sockaddr_str *addr)
+{
+	struct dgsm_service_host *e, *n;
+	int deleted = 0;
+
+	if (!msc)
+		return -ENOENT;
+
+	llist_for_each_entry_safe(e, n, &msc->service_hosts, entry) {
+		if (service && strcmp(service, e->service))
+			continue;
+
+		if (addr) {
+			if (!osmo_sockaddr_str_cmp(addr, &e->host_v4)) {
+				e->host_v4 = (struct osmo_sockaddr_str){};
+				/* Removed one addr. If the other is still there, keep the entry. */
+				if (osmo_sockaddr_str_is_nonzero(&e->host_v6))
+					continue;
+			} else if (!osmo_sockaddr_str_cmp(addr, &e->host_v6)) {
+				e->host_v6 = (struct osmo_sockaddr_str){};
+				/* Removed one addr. If the other is still there, keep the entry. */
+				if (osmo_sockaddr_str_is_nonzero(&e->host_v4))
+					continue;
+			} else
+				/* No addr match, keep the entry. */
+				continue;
+			/* Addr matched and none is left. Delete. */
+		}
+		llist_del(&e->entry);
+		talloc_free(e);
+		deleted++;
+	}
+	return deleted;
+}
+
+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.vty.client.gateway_proxy)) {
+		struct remote_hlr *gateway_proxy = remote_hlr_get(&g_hlr->mslookup.vty.client.gateway_proxy, true);
+		if (!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.vty.client.gateway_proxy));
+			return false;
+		}
+
+		proxy_subscr_remote_hlr_resolved(proxy, proxy_subscr, 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)
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	dgsm_ctx = talloc_named_const(ctx, 0, "dgsm");
+	INIT_LLIST_HEAD(&c->server.msc_configs);
+
+	g_hlr->mslookup.server.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(&c->server.mdns.bind_addr,
+				   OSMO_MSLOOKUP_MDNS_IP4, OSMO_MSLOOKUP_MDNS_PORT);
+	osmo_sockaddr_str_from_str(&c->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;
+	dgsm_config_apply(&g_hlr->mslookup.vty);
+}
+
+void dgsm_stop()
+{
+	struct dgsm_config disabled = {};
+	dgsm_config_apply(&disabled);
+}
+
+static void dgsm_mdns_server_config_apply(const struct dgsm_config *c)
+{
+	/* Check whether to start/stop/restart mDNS server */
+	bool should_run;
+	bool should_stop;
+	if (!g_hlr->mslookup.allow_startup)
+		return;
+
+	should_run = c->server.enable && c->server.mdns.enable;
+	should_stop = g_hlr->mslookup.server.mdns
+		&& (!should_run
+		    || osmo_sockaddr_str_cmp(&c->server.mdns.bind_addr,
+					     &g_hlr->mslookup.server.mdns->bind_addr));
+
+	if (should_stop) {
+		osmo_mslookup_server_mdns_stop(g_hlr->mslookup.server.mdns);
+		g_hlr->mslookup.server.mdns = NULL;
+		LOGP(DDGSM, LOGL_NOTICE, "Stopped mslookup mDNS server\n");
+	}
+
+	if (should_run && !g_hlr->mslookup.server.mdns) {
+		g_hlr->mslookup.server.mdns =
+			osmo_mslookup_server_mdns_start(g_hlr, &c->server.mdns.bind_addr);
+		if (!g_hlr->mslookup.server.mdns)
+			LOGP(DDGSM, LOGL_ERROR, "Failed to start mslookup mDNS server on " OSMO_SOCKADDR_STR_FMT "\n",
+			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.server.mdns->bind_addr));
+		else
+			LOGP(DDGSM, LOGL_NOTICE, "Started mslookup mDNS server, receiving mDNS requests at multicast "
+			     OSMO_SOCKADDR_STR_FMT "\n",
+			     OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.server.mdns->bind_addr));
+	}
+}
+
+static void dgsm_mdns_client_config_apply(const struct dgsm_config *c)
+{
+	if (!g_hlr->mslookup.allow_startup)
+		return;
+
+	/* 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);
+
+	bool should_run = c->client.enable && c->client.mdns.enable;
+	bool should_stop = g_hlr->mslookup.client.mdns &&
+		(!should_run
+		 || osmo_sockaddr_str_cmp(&c->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);
+		g_hlr->mslookup.client.mdns = NULL;
+		LOGP(DDGSM, LOGL_NOTICE, "Stopped mslookup mDNS client\n");
+	}
+
+	if (should_run && !g_hlr->mslookup.client.mdns) {
+		g_hlr->mslookup.client.mdns =
+			osmo_mslookup_client_add_mdns(g_hlr->mslookup.client.client,
+						      c->client.mdns.query_addr.ip,
+						      c->client.mdns.query_addr.port,
+						      -1);
+		if (!g_hlr->mslookup.client.mdns)
+			LOGP(DDGSM, LOGL_ERROR, "Failed to start mslookup mDNS client with target "
+			     OSMO_SOCKADDR_STR_FMT "\n",
+			     OSMO_SOCKADDR_STR_FMT_ARGS(&c->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(&c->client.mdns.query_addr));
+	}
+
+	if (c->client.enable && osmo_sockaddr_str_is_nonzero(&c->client.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(&c->client.gateway_proxy));
+}
+
+void dgsm_config_apply(const struct dgsm_config *c)
+{
+	dgsm_mdns_server_config_apply(c);
+	dgsm_mdns_client_config_apply(c);
+}
diff --git a/src/dgsm_vty.c b/src/dgsm_vty.c
new file mode 100644
index 0000000..83c31a2
--- /dev/null
+++ b/src/dgsm_vty.c
@@ -0,0 +1,515 @@
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/mslookup/mslookup_client_mdns.h>
+#include <osmocom/hlr/hlr_vty.h>
+#include <osmocom/hlr/dgsm.h>
+#include <osmocom/hlr/mslookup_server.h>
+
+struct cmd_node mslookup_node = {
+	MSLOOKUP_NODE,
+	"%s(config-mslookup)# ",
+	1,
+};
+
+DEFUN(cfg_mslookup,
+      cfg_mslookup_cmd,
+      "mslookup",
+      "Configure Distributed GSM mslookup")
+{
+	vty->node = MSLOOKUP_NODE;
+	return CMD_SUCCESS;
+}
+
+static int mslookup_server_mdns_bind(struct vty *vty, int argc, const char **argv)
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	const char *ip_str = argc > 0? argv[0] : c->server.mdns.bind_addr.ip;
+	const char *port_str = argc > 1? argv[1] : NULL;
+	uint16_t port_nr = port_str ? atoi(port_str) : c->server.mdns.bind_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 server: Invalid mDNS bind address: %s %u%s",
+			ip_str, port_nr, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	c->server.mdns.bind_addr = addr;
+	c->server.mdns.enable = true;
+	c->server.enable = true;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+static int mslookup_client_mdns_to(struct vty *vty, int argc, const char **argv)
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	const char *ip_str = argc > 0? argv[0] : c->client.mdns.query_addr.ip;
+	const char *port_str = argc > 1? argv[1] : NULL;
+	uint16_t port_nr = port_str ? atoi(port_str) : c->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;
+	}
+
+	c->client.mdns.query_addr = addr;
+	c->client.mdns.enable = true;
+	c->client.enable = true;
+	dgsm_config_apply(c);
+	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")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	c->server.mdns.enable = false;
+	c->client.mdns.enable = false;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+struct cmd_node mslookup_server_node = {
+	MSLOOKUP_SERVER_NODE,
+	"%s(config-mslookup-server)# ",
+	1,
+};
+
+DEFUN(cfg_mslookup_server,
+      cfg_mslookup_server_cmd,
+      "server",
+      "Enable and configure Distributed GSM mslookup server")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	vty->node = MSLOOKUP_SERVER_NODE;
+	c->server.enable = true;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_no_server,
+      cfg_mslookup_no_server_cmd,
+      "no server",
+      NO_STR "Disable Distributed GSM mslookup server")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	c->server.enable = false;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_server_mdns_bind,
+      cfg_mslookup_server_mdns_bind_cmd,
+      "mdns [IP] [<1-65535>]",
+      "Configure where the mDNS server listens for mslookup requests\n"
+      MDNS_IP46_STR MDNS_PORT_STR)
+{
+	return mslookup_server_mdns_bind(vty, argc, argv);
+}
+
+DEFUN(cfg_mslookup_server_no_mdns,
+      cfg_mslookup_server_no_mdns_cmd,
+      "no mdns",
+      NO_STR "Disable server for mDNS mslookup (do not answer remote requests)\n")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	c->server.mdns.enable = false;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+struct cmd_node mslookup_server_msc_node = {
+	MSLOOKUP_SERVER_MSC_NODE,
+	"%s(config-mslookup-server-msc)# ",
+	1,
+};
+
+DEFUN(cfg_mslookup_server_msc,
+      cfg_mslookup_server_msc_cmd,
+      "msc .UNIT_NAME",
+      "Configure services for individual local MSCs\n"
+      "IPA Unit Name of the local MSC to configure\n")
+{
+	struct osmo_gt msc_name;
+	struct dgsm_msc_config *msc;
+	osmo_gt_set_str(&msc_name, argv_concat(argv, argc, 0));
+
+	msc = dgsm_config_msc_get(&msc_name, true);
+	if (!msc) {
+		vty_out(vty, "%% Error creating MSC %s%s", osmo_gt_name(&msc_name), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	vty->node = MSLOOKUP_SERVER_MSC_NODE;
+	vty->index = msc;
+	return CMD_SUCCESS;
+}
+
+#define SERVICE_NAME_STR \
+	"mslookup service name, e.g. " OSMO_MSLOOKUP_SERVICE_SIP " or " OSMO_MSLOOKUP_SERVICE_SMPP "\n"
+
+static struct dgsm_msc_config *msc_from_node(struct vty *vty)
+{
+	switch (vty->node) {
+	case MSLOOKUP_SERVER_NODE:
+		/* On the mslookup.server node, set services on the wildcard msc, without a particular name. */
+		return dgsm_config_msc_get(&dgsm_config_msc_wildcard, true);
+	case MSLOOKUP_SERVER_MSC_NODE:
+		return vty->index;
+	default:
+		return NULL;
+	}
+}
+
+DEFUN(cfg_mslookup_server_msc_service,
+      cfg_mslookup_server_msc_service_cmd,
+      "service NAME at IP <1-65535>",
+      "Configure addresses of local services, as sent in replies to remote mslookup requests.\n"
+      SERVICE_NAME_STR "at\n" IP46_STR PORT_STR)
+{
+	/* If this command is run on the 'server' node, it produces an empty unit name and serves as wildcard for all
+	 * MSCs. If on a 'server' / 'msc' node, set services only for that MSC Unit Name. */
+	struct dgsm_msc_config *msc = msc_from_node(vty);
+	const char *service = argv[0];
+	const char *ip_str = argv[1];
+	const char *port_str = argv[2];
+	struct osmo_sockaddr_str addr;
+
+	if (!msc) {
+		vty_out(vty, "%% Error: no MSC object on this node%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (osmo_sockaddr_str_from_str(&addr, ip_str, atoi(port_str))
+	    || !osmo_sockaddr_str_is_nonzero(&addr)) {
+		vty_out(vty, "%% mslookup server: Invalid address for service %s: %s %s%s",
+			service, ip_str, port_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (dgsm_config_msc_service_set(msc, service, &addr)) {
+		vty_out(vty, "%% mslookup server: Error setting service %s to %s %s%s",
+			service, ip_str, port_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+#define NO_SERVICE_AND_NAME_STR NO_STR "Remove one or more service address entries\n" SERVICE_NAME_STR
+
+DEFUN(cfg_mslookup_server_msc_no_service,
+      cfg_mslookup_server_msc_no_service_cmd,
+      "no service NAME",
+      NO_SERVICE_AND_NAME_STR)
+{
+	/* If this command is run on the 'server' node, it produces an empty unit name and serves as wildcard for all
+	 * MSCs. If on a 'server' / 'msc' node, set services only for that MSC Unit Name. */
+	struct dgsm_msc_config *msc = msc_from_node(vty);
+	const char *service = argv[0];
+
+	if (!msc) {
+		vty_out(vty, "%% Error: no MSC object on this node%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (dgsm_config_msc_service_del(msc, service, NULL) < 1) {
+		vty_out(vty, "%% mslookup server: cannot remove service '%s'%s",
+			service, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_server_msc_no_service_addr,
+      cfg_mslookup_server_msc_no_service_addr_cmd,
+      "no service NAME at IP <1-65535>",
+      NO_SERVICE_AND_NAME_STR "at\n" IP46_STR PORT_STR)
+{
+	/* If this command is run on the 'server' node, it produces an empty unit name and serves as wildcard for all
+	 * MSCs. If on a 'server' / 'msc' node, set services only for that MSC Unit Name. */
+	struct dgsm_msc_config *msc = msc_from_node(vty);
+	const char *service = argv[0];
+	const char *ip_str = argv[1];
+	const char *port_str = argv[2];
+	struct osmo_sockaddr_str addr;
+
+	if (!msc) {
+		vty_out(vty, "%% Error: no MSC object on this node%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (osmo_sockaddr_str_from_str(&addr, ip_str, atoi(port_str))
+	    || !osmo_sockaddr_str_is_nonzero(&addr)) {
+		vty_out(vty, "%% mslookup server: Invalid address for 'no service' %s: %s %s%s",
+			service, ip_str, port_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (dgsm_config_msc_service_del(msc, service, &addr) < 1) {
+		vty_out(vty, "%% mslookup server: cannot remove service '%s' to %s %s%s",
+			service, ip_str, port_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	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")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	vty->node = MSLOOKUP_CLIENT_NODE;
+	c->client.enable = true;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_mslookup_no_client,
+      cfg_mslookup_no_client_cmd,
+      "no client",
+      NO_STR "Disable Distributed GSM mslookup client")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	c->client.enable = false;
+	dgsm_config_apply(c);
+	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")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	uint32_t val = atol(argv[0]);
+	c->client.timeout.tv_sec = val / 1000;
+	c->client.timeout.tv_usec = (val % 1000) * 1000;
+	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")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	c->client.mdns.enable = false;
+	dgsm_config_apply(c);
+	return CMD_SUCCESS;
+}
+
+void config_write_msc_services(struct vty *vty, const char *indent, struct dgsm_msc_config *msc)
+{
+	struct dgsm_service_host *e;
+
+	llist_for_each_entry(e, &msc->service_hosts, entry) {
+		if (osmo_sockaddr_str_is_nonzero(&e->host_v4))
+			vty_out(vty, "%sservice %s at %s %u%s", indent, e->service, e->host_v4.ip, e->host_v4.port,
+				VTY_NEWLINE);
+		if (osmo_sockaddr_str_is_nonzero(&e->host_v6))
+			vty_out(vty, "%sservice %s at %s %u%s", indent, e->service, e->host_v6.ip, e->host_v6.port,
+				VTY_NEWLINE);
+	}
+}
+
+int config_write_mslookup(struct vty *vty)
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+
+	if (!c->server.enable && !c->client.enable
+	    && llist_empty(&c->server.msc_configs))
+		return CMD_SUCCESS;
+
+	vty_out(vty, "mslookup%s", VTY_NEWLINE);
+
+	if (c->server.enable || !llist_empty(&c->server.msc_configs)) {
+		struct dgsm_msc_config *msc;
+
+		vty_out(vty, " server%s", VTY_NEWLINE);
+
+		if (c->server.mdns.enable
+		    && osmo_sockaddr_str_is_nonzero(&c->server.mdns.bind_addr))
+			vty_out(vty, "  mdns bind %s %u%s",
+				c->server.mdns.bind_addr.ip,
+				c->server.mdns.bind_addr.port,
+				VTY_NEWLINE);
+
+		msc = dgsm_config_msc_get(&dgsm_config_msc_wildcard, false);
+		if (msc)
+			config_write_msc_services(vty, " ", msc);
+
+		llist_for_each_entry(msc, &c->server.msc_configs, entry) {
+			if (!osmo_gt_cmp(&dgsm_config_msc_wildcard, &msc->name))
+				continue;
+			vty_out(vty, " msc %s%s", osmo_gt_name(&msc->name), VTY_NEWLINE);
+			config_write_msc_services(vty, "  ", msc);
+		}
+
+		/* If the server is disabled, still output the above to not lose the service config. */
+		if (!c->server.enable)
+			vty_out(vty, " no server%s", VTY_NEWLINE);
+	}
+
+	if (c->client.enable) {
+		vty_out(vty, " client%s", VTY_NEWLINE);
+
+		if (osmo_sockaddr_str_is_nonzero(&c->client.gateway_proxy))
+			vty_out(vty, "  gateway-proxy %s %u%s",
+				c->client.gateway_proxy.ip,
+				c->client.gateway_proxy.port,
+				VTY_NEWLINE);
+
+		if (c->client.mdns.enable
+		    && osmo_sockaddr_str_is_nonzero(&c->client.mdns.query_addr))
+			vty_out(vty, "  mdns to %s %u%s",
+				c->client.mdns.query_addr.ip,
+				c->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")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	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;
+	}
+
+	c->client.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")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	c->client.gateway_proxy = (struct osmo_sockaddr_str){};
+	return CMD_SUCCESS;
+}
+
+DEFUN(do_mslookup_show_services,
+      do_mslookup_show_services_cmd,
+      "show mslookup services",
+      SHOW_STR "Distributed GSM / mslookup related information\n"
+      "List configured service addresses as sent to remote mslookup requests\n")
+{
+	struct dgsm_config *c = &g_hlr->mslookup.vty;
+	struct dgsm_msc_config *msc;
+	struct dgsm_service_host *local_hlr = mslookup_server_get_local_gsup_addr();
+
+	vty_out(vty, "Local GSUP HLR address returned in mslookup responses for local IMSIs:");
+	if (osmo_sockaddr_str_is_nonzero(&local_hlr->host_v4))
+		vty_out(vty, " " OSMO_SOCKADDR_STR_FMT,
+			OSMO_SOCKADDR_STR_FMT_ARGS(&local_hlr->host_v4));
+	if (osmo_sockaddr_str_is_nonzero(&local_hlr->host_v6))
+		vty_out(vty, " " OSMO_SOCKADDR_STR_FMT,
+			OSMO_SOCKADDR_STR_FMT_ARGS(&local_hlr->host_v6));
+	vty_out(vty, "%s", VTY_NEWLINE);
+
+	msc = dgsm_config_msc_get(&dgsm_config_msc_wildcard, false);
+	if (msc)
+		config_write_msc_services(vty, "", msc);
+
+	llist_for_each_entry(msc, &c->server.msc_configs, entry) {
+		if (!osmo_gt_cmp(&dgsm_config_msc_wildcard, &msc->name))
+			continue;
+		vty_out(vty, "msc %s%s", osmo_gt_name(&msc->name), VTY_NEWLINE);
+		config_write_msc_services(vty, " ", msc);
+	}
+	return CMD_SUCCESS;
+}
+
+void dgsm_vty_init()
+{
+	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);
+
+	install_node(&mslookup_server_node, NULL);
+	install_element(MSLOOKUP_SERVER_NODE, &cfg_mslookup_server_mdns_bind_cmd);
+	install_element(MSLOOKUP_SERVER_NODE, &cfg_mslookup_server_no_mdns_cmd);
+	install_element(MSLOOKUP_SERVER_NODE, &cfg_mslookup_server_msc_service_cmd);
+	install_element(MSLOOKUP_SERVER_NODE, &cfg_mslookup_server_msc_no_service_cmd);
+	install_element(MSLOOKUP_SERVER_NODE, &cfg_mslookup_server_msc_no_service_addr_cmd);
+	install_element(MSLOOKUP_SERVER_NODE, &cfg_mslookup_server_msc_cmd);
+
+	install_node(&mslookup_server_msc_node, NULL);
+	install_element(MSLOOKUP_SERVER_MSC_NODE, &cfg_mslookup_server_msc_service_cmd);
+	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/gsup_server.c b/src/gsup_server.c
index 83329a0..151a882 100644
--- a/src/gsup_server.c
+++ b/src/gsup_server.c
@@ -28,8 +28,10 @@
 #include <osmocom/gsm/apn.h>
 #include <osmocom/gsm/gsm23003.h>
 
+#include <osmocom/hlr/hlr.h>
 #include <osmocom/hlr/gsup_server.h>
 #include <osmocom/hlr/gsup_router.h>
+#include <osmocom/hlr/proxy.h>
 
 #define LOG_GSUP_CONN(conn, level, fmt, args...) \
 	LOGP(DLGSUP, level, "GSUP peer %s: " fmt, \
diff --git a/src/hlr.c b/src/hlr.c
index d1647a0..536a240 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.vty.client.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:
@@ -692,6 +704,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);
@@ -746,10 +761,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);
@@ -766,6 +784,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..e20d2bb 100644
--- a/src/hlr_vty.c
+++ b/src/hlr_vty.c
@@ -39,6 +39,7 @@
 #include <osmocom/hlr/hlr_vty_subscr.h>
 #include <osmocom/hlr/hlr_ussd.h>
 #include <osmocom/hlr/gsup_server.h>
+#include <osmocom/hlr/dgsm.h>
 
 struct cmd_node hlr_node = {
 	HLR_NODE,
@@ -146,6 +147,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
  ***********************************************************************/
@@ -403,6 +422,17 @@
 		vty->index = NULL;
 		vty->index_sub = NULL;
 		break;
+	case MSLOOKUP_CLIENT_NODE:
+	case MSLOOKUP_SERVER_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		vty->index_sub = NULL;
+		break;
+	case MSLOOKUP_SERVER_MSC_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		vty->index_sub = NULL;
+		break;
 	default:
 	case HLR_NODE:
 		vty->node = CONFIG_NODE;
@@ -444,6 +474,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);
 
@@ -462,4 +493,5 @@
 	install_element(HLR_NODE, &cfg_no_subscr_create_on_demand_cmd);
 
 	hlr_vty_subscriber_init();
+	dgsm_vty_init();
 }
diff --git a/src/hlr_vty_subscr.c b/src/hlr_vty_subscr.c
index adbfcab..b8eba4d 100644
--- a/src/hlr_vty_subscr.c
+++ b/src/hlr_vty_subscr.c
@@ -30,6 +30,7 @@
 
 #include <osmocom/hlr/hlr.h>
 #include <osmocom/hlr/db.h>
+#include <osmocom/hlr/proxy.h>
 
 struct vty;
 
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
new file mode 100644
index 0000000..af25e06
--- /dev/null
+++ b/src/mslookup_server.c
@@ -0,0 +1,302 @@
+#include <string.h>
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/mslookup/mslookup.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/hlr.h>
+#include <osmocom/hlr/db.h>
+#include <osmocom/hlr/dgsm.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,
+	};
+
+static void set_result(struct osmo_mslookup_result *result,
+		       const struct dgsm_service_host *service_host,
+		       uint32_t age)
+{
+	if (!osmo_sockaddr_str_is_nonzero(&service_host->host_v4)
+	    && !osmo_sockaddr_str_is_nonzero(&service_host->host_v6)) {
+		*result = not_found;
+		return;
+	}
+	result->rc = OSMO_MSLOOKUP_RC_RESULT;
+	result->host_v4 = service_host->host_v4;
+	result->host_v6 = service_host->host_v6;
+	result->age = age;
+}
+
+struct dgsm_service_host *mslookup_server_get_local_gsup_addr()
+{
+	static struct dgsm_service_host gsup_bind = {};
+	struct dgsm_service_host *host;
+
+	/* Find a HLR/GSUP service set for the server (no VLR unit name) */
+	host = dgsm_config_service_get(&dgsm_config_msc_wildcard, OSMO_MSLOOKUP_SERVICE_HLR_GSUP);
+	if (host)
+		return host;
+
+	/* Try to use the locally configured GSUP bind address */
+	osmo_sockaddr_str_from_str(&gsup_bind.host_v4, g_hlr->gsup_bind_addr, OSMO_GSUP_PORT);
+	if (gsup_bind.host_v4.af == AF_INET6) {
+		gsup_bind.host_v6 = gsup_bind.host_v4;
+		gsup_bind.host_v4 = (struct osmo_sockaddr_str){};
+	}
+	return &gsup_bind;
+}
+
+/* A remote entity is asking us whether we are the home HLR of the given subscriber. */
+static void mslookup_server_rx_hlr_gsup(const struct osmo_mslookup_query *query,
+					struct osmo_mslookup_result *result)
+{
+	struct dgsm_service_host *host;
+	int rc;
+	switch (query->id.type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		rc = db_subscr_exists_by_imsi(g_hlr->dbc, query->id.imsi);
+		break;
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		rc = db_subscr_exists_by_msisdn(g_hlr->dbc, query->id.msisdn);
+		break;
+	default:
+		LOGP(DDGSM, LOGL_ERROR, "Unknown mslookup ID type: %d\n", query->id.type);
+		*result = not_found;
+		return;
+	}
+
+	if (rc) {
+		LOGP(DDGSM, LOGL_DEBUG, "%s: does not exist in local HLR\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+		*result = not_found;
+		return;
+	}
+
+	LOGP(DDGSM, LOGL_DEBUG, "%s: found in local HLR\n",
+	     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+
+	host = mslookup_server_get_local_gsup_addr();
+
+	set_result(result, host, 0);
+	if (result->rc != OSMO_MSLOOKUP_RC_RESULT) {
+		LOGP(DDGSM, LOGL_ERROR,
+		     "Subscriber found, but error in service '" OSMO_MSLOOKUP_SERVICE_HLR_GSUP "' config:"
+		     " v4: " OSMO_SOCKADDR_STR_FMT "  v6: " OSMO_SOCKADDR_STR_FMT "\n",
+		     OSMO_SOCKADDR_STR_FMT_ARGS(&host->host_v4),
+		     OSMO_SOCKADDR_STR_FMT_ARGS(&host->host_v6));
+	}
+}
+
+/* Look in the local HLR record: If the subscriber is "at home" in this HLR and is also currently located at a local
+ * VLR, we will find a valid location updating with vlr_number, and no vlr_via_proxy entry. */
+static bool subscriber_has_done_lu_here_hlr(const struct osmo_mslookup_query *query,
+					    uint32_t *lu_age,
+					    struct osmo_gt *local_msc_name)
+{
+	struct hlr_subscriber subscr;
+	int rc;
+	uint32_t age;
+
+	switch (query->id.type) {
+	case OSMO_MSLOOKUP_ID_IMSI:
+		rc = db_subscr_get_by_imsi(g_hlr->dbc, query->id.imsi, &subscr);
+		break;
+	case OSMO_MSLOOKUP_ID_MSISDN:
+		rc = db_subscr_get_by_msisdn(g_hlr->dbc, query->id.msisdn, &subscr);
+		break;
+	default:
+		LOGP(DDGSM, LOGL_ERROR, "Unknown mslookup ID type: %d\n", query->id.type);
+		return false;
+	}
+
+	if (rc) {
+		LOGP(DDGSM, LOGL_DEBUG, "%s: does not exist in local HLR\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+		return false;
+	}
+
+	if (!subscr.vlr_number[0]) {
+		LOGP(DDGSM, LOGL_DEBUG, "%s: not attached (vlr_number unset)\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+	}
+
+	if (subscr.vlr_via_proxy.len) {
+		/* The VLR is behind a proxy, the subscriber is not attached to a local VLR but a remote one. That
+		 * remote proxy should instead respond to the service lookup request. */
+		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),
+		     subscr.vlr_number,
+		     osmo_gt_name(&subscr.vlr_via_proxy));
+		return false;
+	}
+
+	if (!timestamp_age(&subscr.last_lu_seen, &age)) {
+		LOG_DGSM(subscr.imsi, LOGL_ERROR, "Invalid last_lu_seen timestamp for subscriber\n");
+		return false;
+	}
+	if (age > g_hlr->mslookup.server.max_age) {
+		LOGP(DDGSM, LOGL_DEBUG, "%s: last attach was here, but too long ago: %us > %us\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+		     age, g_hlr->mslookup.server.max_age);
+		return false;
+	}
+
+	*lu_age = age;
+	osmo_gt_set_str(local_msc_name, subscr.vlr_number);
+	LOGP(DDGSM, LOGL_DEBUG, "%s: attached %u seconds ago at local VLR %s\n",
+	     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+	     age, osmo_gt_name(local_msc_name));
+
+	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.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.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)
+{
+	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:
+	 *   - if the subscriber is known here, we will never proxy.
+	 *   - if the subscriber is not known here, this local HLR db will never record a LU.
+	 * However, if a subscriber was being proxied to a remote home HLR, and if then the subscriber was also added to
+	 * the local HLR database, there might occur a situation where both reflect a LU. So, to be safe against all
+	 * 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);
+
+	/* 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(DDGSM, LOGL_ERROR, "%s: attached here, but no VLR name known\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
+		return false;
+	}
+
+	if (!attached_here) {
+		/* Already logged "not attached" for both local-db and proxy attach */
+		return false;
+	}
+
+	LOGP(DDGSM, LOGL_INFO, "%s: attached here, at VLR %s\n",
+	     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+	     osmo_gt_name(&msc_name));
+	*lu_age_p = lu_age;
+	*local_msc_name = msc_name;
+	return true;
+}
+
+/* A remote entity is asking us whether we are providing the given service for the given subscriber. */
+void osmo_mslookup_server_rx(const struct osmo_mslookup_query *query,
+			     struct osmo_mslookup_result *result)
+{
+	const struct dgsm_service_host *service_host;
+	uint32_t age;
+	struct osmo_gt msc_name;
+
+	/* A request for a home HLR: answer exactly if this is the subscriber's home HLR, i.e. the IMSI is listed in the
+	 * HLR database. */
+	if (strcmp(query->service, OSMO_MSLOOKUP_SERVICE_HLR_GSUP) == 0)
+		return mslookup_server_rx_hlr_gsup(query, result);
+
+	/* All other service types: answer when the subscriber has done a LU that is either listed in the local HLR or
+	 * in the GSUP proxy database: i.e. if the subscriber has done a Location Updating at an VLR belonging to this
+	 * HLR. Respond with whichever services are configured in the osmo-hlr.cfg. */
+	if (!subscriber_has_done_lu_here(query, &age, &msc_name)) {
+		*result = not_found;
+		return;
+	}
+
+	/* We've detected a LU here. The VLR where the LU happened is stored in msc_unit_name, and the LU age is stored
+	 * in 'age'. Figure out the address configured for that VLR and service name. */
+	service_host = dgsm_config_service_get(&msc_name, query->service);
+
+	if (!service_host) {
+		/* Find such service set globally (no VLR unit name) */
+		service_host = dgsm_config_service_get(&dgsm_config_msc_wildcard, query->service);
+	}
+
+	if (!service_host) {
+		LOGP(DDGSM, LOGL_ERROR,
+		     "%s: subscriber found, but no service %s configured, cannot service lookup request\n",
+		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
+		     osmo_quote_str_c(OTC_SELECT, query->service, -1));
+		*result = not_found;
+		return;
+	}
+
+	set_result(result, service_host, age);
+}
diff --git a/src/mslookup_server_mdns.c b/src/mslookup_server_mdns.c
new file mode 100644
index 0000000..b758cb5
--- /dev/null
+++ b/src/mslookup_server_mdns.c
@@ -0,0 +1,100 @@
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <osmocom/mslookup/mslookup.h>
+#include <osmocom/mslookup/mdns.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/mslookup_server.h>
+#include <osmocom/hlr/mslookup_server_mdns.h>
+
+static void osmo_mslookup_server_mdns_tx(struct osmo_mslookup_server_mdns *server,
+					 uint16_t packet_id,
+					 const struct osmo_mslookup_query *query,
+					 const struct osmo_mslookup_result *result)
+{
+	struct msgb *msg;
+	const char *errmsg = NULL;
+	void *ctx = talloc_named_const(server, 0, __func__);
+
+	msg = osmo_mdns_result_encode(ctx, packet_id, query, result);
+	if (!msg)
+		errmsg = "Error encoding mDNS answer packet";
+	else if (osmo_mdns_sock_send(server->sock, msg))
+		errmsg = "Error sending mDNS answer";
+	if (errmsg)
+		LOGP(DDGSM, LOGL_ERROR, "%s: mDNS: %s\n", osmo_mslookup_result_name_c(ctx, query, result), errmsg);
+	talloc_free(ctx);
+}
+
+static void osmo_mslookup_server_mdns_handle_request(uint16_t packet_id,
+						     struct osmo_mslookup_server_mdns *server,
+						     const struct osmo_mslookup_query *query)
+{
+	struct osmo_mslookup_result result;
+
+	osmo_mslookup_server_rx(query, &result);
+	/* Error logging already happens in osmo_mslookup_server_rx() */
+	if (result.rc != OSMO_MSLOOKUP_RC_RESULT)
+		return;
+
+	osmo_mslookup_server_mdns_tx(server, packet_id, query, &result);
+}
+
+static int osmo_mslookup_server_mdns_rx(struct osmo_fd *osmo_fd, unsigned int what)
+{
+	struct osmo_mslookup_server_mdns *server = osmo_fd->data;
+	struct osmo_mslookup_query *query;
+	uint16_t packet_id;
+	int n;
+	uint8_t buffer[1024];
+	void *ctx;
+
+	/* Parse the message and print it */
+	n = read(osmo_fd->fd, buffer, sizeof(buffer));
+	if (n < 0)
+		return n;
+
+	ctx = talloc_named_const(server, 0, __func__);
+	query = osmo_mdns_query_decode(ctx, buffer, n, &packet_id);
+	if (!query) {
+		talloc_free(ctx);
+		return -1;
+	}
+
+	osmo_mslookup_id_name_buf((char *)buffer, sizeof(buffer), &query->id);
+	LOGP(DDGSM, LOGL_DEBUG, "mDNS rx request: %s.%s\n", query->service, buffer);
+	osmo_mslookup_server_mdns_handle_request(packet_id, server, query);
+	talloc_free(ctx);
+	return n;
+}
+
+struct osmo_mslookup_server_mdns *osmo_mslookup_server_mdns_start(void *ctx, const struct osmo_sockaddr_str *bind_addr)
+{
+	struct osmo_mslookup_server_mdns *server = talloc_zero(ctx, struct osmo_mslookup_server_mdns);
+	OSMO_ASSERT(server);
+	*server = (struct osmo_mslookup_server_mdns){
+		.bind_addr = *bind_addr,
+	};
+
+	server->sock = osmo_mdns_sock_init(server,
+					   bind_addr->ip, bind_addr->port,
+					   osmo_mslookup_server_mdns_rx,
+					   server, 0);
+	if (!server->sock) {
+		LOGP(DDGSM, LOGL_ERROR,
+		     "mslookup mDNS server: error initializing multicast bind on " OSMO_SOCKADDR_STR_FMT "\n",
+		     OSMO_SOCKADDR_STR_FMT_ARGS(bind_addr));
+		talloc_free(server);
+		return NULL;
+	}
+
+	return server;
+}
+
+void osmo_mslookup_server_mdns_stop(struct osmo_mslookup_server_mdns *server)
+{
+	if (!server)
+		return;
+	osmo_mdns_sock_cleanup(server->sock);
+	talloc_free(server);
+}
diff --git a/src/proxy.c b/src/proxy.c
new file mode 100644
index 0000000..7ccedbb
--- /dev/null
+++ b/src/proxy.c
@@ -0,0 +1,557 @@
+
+#include <sys/time.h>
+#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;
+};
+
+/* Central implementation to set a timestamp to the current time, in case we want to modify this in the future. */
+void timestamp_update(timestamp_t *timestamp)
+{
+	struct timeval tv;
+	time_t raw;
+	struct tm utc;
+	/* The simpler way would be just time(&raw), but by using osmo_gettimeofday() we can also use
+	 * osmo_gettimeofday_override for unit tests independent from real time. */
+	osmo_gettimeofday(&tv, NULL);
+	raw = tv.tv_sec;
+	gmtime_r(&raw, &utc);
+	*timestamp = mktime(&utc);
+}
+
+/* Calculate seconds since a given timestamp was taken. Return true for a valid age returned in age_p, return false if
+ * the timestamp is either in the future or the age surpasses uint32_t range. When false is returned, *age_p is set to
+ * UINT32_MAX. */
+bool timestamp_age(const timestamp_t *timestamp, uint32_t *age_p)
+{
+	int64_t age64;
+	timestamp_t now;
+	timestamp_update(&now);
+	age64 = (int64_t)now - (int64_t)(*timestamp);
+	if (age64 < 0 || age64 > UINT32_MAX) {
+		*age_p = UINT32_MAX;
+		return false;
+	}
+	*age_p = (uint32_t)age64;
+	return true;
+}
+
+/* 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..78339d4
--- /dev/null
+++ b/src/remote_hlr.c
@@ -0,0 +1,181 @@
+#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/dgsm.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 dd8dbcf..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# ?
@@ -89,6 +90,7 @@
   end
 ...
   hlr
+  mslookup
 
 OsmoHLR(config)# hlr
 OsmoHLR(config-hlr)# ?
@@ -127,6 +129,7 @@
 OsmoHLR(config-hlr-gsup)# list
 ...
   bind ip A.B.C.D
+  ipa-name NAME
 
 OsmoHLR(config-hlr-gsup)# exit
 OsmoHLR(config-hlr)# exit
@@ -151,6 +154,7 @@
  logging level auc notice
  logging level ss info
  logging level lu notice
+ logging level dgsm notice
 ...
 hlr
  store-imei
@@ -160,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
+...

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

Gerrit-Project: osmo-hlr
Gerrit-Branch: master
Gerrit-Change-Id: Ife4a61d71926d08f310a1aeed9d9f1974f64178b
Gerrit-Change-Number: 16209
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr at sysmocom.de>
Gerrit-CC: Jenkins Builder
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20191125/b8effe1c/attachment.htm>


More information about the gerrit-log mailing list