Change in osmo-hlr[master]: proxy routing refactor

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
Tue Jan 7 20:11:26 UTC 2020


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


Change subject: proxy routing refactor
......................................................................

proxy routing refactor

Change-Id: I43ad27f6d768df02abb3459ac4c43bb80cc1cbeb
---
M include/osmocom/hlr/dgsm.h
M include/osmocom/hlr/gsup_server.h
M include/osmocom/hlr/mslookup_server.h
M include/osmocom/hlr/proxy.h
M include/osmocom/hlr/remote_hlr.h
M src/dgsm.c
M src/dgsm_vty.c
M src/gsup_server.c
M src/hlr.c
M src/mslookup_server.c
M src/proxy.c
M src/remote_hlr.c
M tests/gsup_server/Makefile.am
13 files changed, 297 insertions(+), 208 deletions(-)



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

diff --git a/include/osmocom/hlr/dgsm.h b/include/osmocom/hlr/dgsm.h
index f06e381..b3d73e9 100644
--- a/include/osmocom/hlr/dgsm.h
+++ b/include/osmocom/hlr/dgsm.h
@@ -21,6 +21,7 @@
 
 #include <osmocom/mslookup/mslookup.h>
 #include <osmocom/hlr/gsup_server.h>
+#include <osmocom/hlr/logging.h>
 #include <osmocom/gsupclient/gsup_peer_id.h>
 #include <osmocom/gsupclient/gsup_req.h>
 
diff --git a/include/osmocom/hlr/gsup_server.h b/include/osmocom/hlr/gsup_server.h
index b7cfb89..22c9a10 100644
--- a/include/osmocom/hlr/gsup_server.h
+++ b/include/osmocom/hlr/gsup_server.h
@@ -74,3 +74,5 @@
 					    uint8_t *msisdn_enc, size_t msisdn_enc_size,
 				            uint8_t *apn_buf, size_t apn_buf_size,
 					    enum osmo_gsup_cn_domain cn_domain);
+int osmo_gsup_forward_to_local_peer(struct osmo_gsup_server *server, const struct osmo_gsup_peer_id *to_peer,
+				    struct osmo_gsup_req *req, struct osmo_gsup_message *modified_gsup);
diff --git a/include/osmocom/hlr/mslookup_server.h b/include/osmocom/hlr/mslookup_server.h
index f76e92f..aed7ad0 100644
--- a/include/osmocom/hlr/mslookup_server.h
+++ b/include/osmocom/hlr/mslookup_server.h
@@ -63,6 +63,10 @@
 extern const struct osmo_ipa_name mslookup_server_msc_wildcard;
 struct mslookup_server_msc_cfg *mslookup_server_msc_get(const struct osmo_ipa_name *msc_name, bool create);
 
-struct mslookup_service_host *mslookup_server_get_local_gsup_addr();
+const struct mslookup_service_host *mslookup_server_get_local_gsup_addr();
 void mslookup_server_rx(const struct osmo_mslookup_query *query,
 			     struct osmo_mslookup_result *result);
+
+bool subscriber_has_done_lu_here(const struct osmo_mslookup_query *query,
+				 uint32_t *lu_age_p, struct osmo_ipa_name *local_msc_name,
+				 char *ret_imsi, size_t ret_imsi_len);
diff --git a/include/osmocom/hlr/proxy.h b/include/osmocom/hlr/proxy.h
index 8412dd2..92ed30a 100644
--- a/include/osmocom/hlr/proxy.h
+++ b/include/osmocom/hlr/proxy.h
@@ -28,12 +28,6 @@
 struct osmo_gsup_req;
 struct remote_hlr;
 
-struct proxy_pending_gsup_req {
-	struct llist_head entry;
-	struct osmo_gsup_req *req;
-	timestamp_t received_at;
-};
-
 struct proxy {
 	struct llist_head subscr_list;
 	struct llist_head pending_gsup_reqs;
@@ -87,8 +81,8 @@
 int proxy_subscr_create_or_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);
+int 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);
 
@@ -96,6 +90,6 @@
 				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);
+				      const struct osmo_sockaddr_str *remote_hlr_addr);
 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
index 4f9f939..6a4e8a1 100644
--- a/include/osmocom/hlr/remote_hlr.h
+++ b/include/osmocom/hlr/remote_hlr.h
@@ -40,9 +40,20 @@
 	struct llist_head entry;
 	struct osmo_sockaddr_str addr;
 	struct osmo_gsup_client *gsupc;
+	struct llist_head pending_up_callbacks;
 };
 
-struct remote_hlr *remote_hlr_get(const struct osmo_sockaddr_str *addr, bool create);
+/*! Receive a remote_hlr address when connecting succeeded, or remote_hlr == NULL on error.
+ * \param addr  GSUP IP address and port for which the connection was requested.
+ * \param remote_hlr  The connected remote_hlr ready for sending, or NULL if connecting failed.
+ * \param data  Same a passed to remote_hlr_get_or_connect(). */
+typedef void (*remote_hlr_connect_result_cb_t)(const struct osmo_sockaddr_str *addr, struct remote_hlr *remote_hlr, void *data);
+
+struct remote_hlr *remote_hlr_get_or_connect(const struct osmo_sockaddr_str *addr, bool connect,
+					     remote_hlr_connect_result_cb_t connect_result_cb, void *data);
 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);
+void remote_hlr_gsup_forward_to_remote_hlr(struct remote_hlr *remote_hlr, struct osmo_gsup_req *req,
+					   struct osmo_gsup_message *modified_gsup);
+
+bool remote_hlr_is_up(struct remote_hlr *remote_hlr);
diff --git a/src/dgsm.c b/src/dgsm.c
index 647e2ee..57012c8 100644
--- a/src/dgsm.c
+++ b/src/dgsm.c
@@ -44,8 +44,7 @@
 {
 	struct proxy *proxy = g_hlr->gs->proxy;
 	struct proxy_subscr proxy_subscr;
-	const struct osmo_sockaddr_str *use_addr;
-	struct remote_hlr *remote_hlr;
+	const struct osmo_sockaddr_str *remote_hlr_addr;
 
 	/* A remote HLR is answering back, indicating that it is the home HLR for a given IMSI.
 	 * There should be a mostly empty proxy entry for that IMSI.
@@ -64,9 +63,9 @@
 	}
 
 	if (osmo_sockaddr_str_is_nonzero(&result->host_v4))
-		use_addr = &result->host_v4;
+		remote_hlr_addr = &result->host_v4;
 	else if (osmo_sockaddr_str_is_nonzero(&result->host_v6))
-		use_addr = &result->host_v6;
+		remote_hlr_addr = &result->host_v6;
 	else {
 		LOG_DGSM(query->id.imsi, LOGL_ERROR, "Invalid address for remote HLR: %s\n",
 			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
@@ -74,30 +73,13 @@
 		return;
 	}
 
-	remote_hlr = remote_hlr_get(use_addr, true);
-	if (!remote_hlr) {
-		proxy_subscr_del(proxy, query->id.imsi);
-		return;
-	}
-
 	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, query->id.imsi)) {
 		LOG_DGSM(query->id.imsi, LOGL_ERROR, "No proxy entry for mslookup result: %s\n",
 			 osmo_mslookup_result_name_c(OTC_SELECT, query, result));
 		return;
 	}
 
-	/* 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);
+	proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, remote_hlr_addr);
 }
 
 /* Return true when the message has been handled by D-GSM. */
@@ -114,8 +96,10 @@
 		return false;
 
 	/* Are we already forwarding this IMSI to a remote HLR? */
-	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi) == 0)
-		goto yes_we_are_proxying;
+	if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi) == 0) {
+		proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req);
+		return true;
+	}
 
 	/* The IMSI is not known locally, so we want to proxy to a remote HLR, but no proxy entry exists yet. We need to
 	 * look up the subscriber in remote HLRs via D-GSM mslookup, forward GSUP and reply once a result is back from
@@ -125,40 +109,41 @@
 	proxy_subscr = (struct proxy_subscr){};
 	OSMO_STRLCPY_ARRAY(proxy_subscr.imsi, req->gsup.imsi);
 	if (proxy_subscr_create_or_update(proxy, &proxy_subscr)) {
-		LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Failed to create proxy entry\n");
-		return false;
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Failed to create proxy entry\n");
+		return true;
 	}
 
 	/* Is a fixed gateway proxy configured? */
 	if (osmo_sockaddr_str_is_nonzero(&g_hlr->mslookup.client.gsup_gateway_proxy)) {
-		struct remote_hlr *gsup_gateway_proxy = remote_hlr_get(&g_hlr->mslookup.client.gsup_gateway_proxy, true);
-		if (!gsup_gateway_proxy) {
-			LOG_DGSM(req->gsup.imsi, LOGL_ERROR,
-				 "Failed to set up fixed gateway proxy " OSMO_SOCKADDR_STR_FMT "\n",
-				 OSMO_SOCKADDR_STR_FMT_ARGS(&g_hlr->mslookup.client.gsup_gateway_proxy));
-			return false;
-		}
+		proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, &g_hlr->mslookup.client.gsup_gateway_proxy);
 
-		proxy_subscr_remote_hlr_resolved(proxy, &proxy_subscr, gsup_gateway_proxy);
-
-		/* Update info */
+		/* Proxy database modified, update info */
 		if (proxy_subscr_get_by_imsi(&proxy_subscr, proxy, req->gsup.imsi)) {
-			LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Proxy entry disappeared\n");
-			return false;
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Internal proxy error\n");
+			return true;
 		}
-		goto yes_we_are_proxying;
+
+		proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req);
+		return true;
 	}
 
-	/* Kick off an mslookup for the remote HLR. */
-	if (!g_hlr->mslookup.client.client) {
+	/* Kick off an mslookup for the remote HLR?  This check could be up first on the top, but do it only now so that
+	 * if the mslookup client disconnected, we still continue to service open proxy entries. */
+	if (!osmo_mslookup_client_active(g_hlr->mslookup.client.client)) {
 		LOG_GSUP_REQ(req, LOGL_DEBUG, "mslookup client not running, cannot query remote home HLR\n");
 		return false;
 	}
 
+	/* First spool message, then kick off mslookup. If the proxy denies this message type, then don't do anything. */
+	if (proxy_subscr_forward_to_remote_hlr(proxy, &proxy_subscr, req)) {
+		/* If the proxy denied forwarding, an error response was already generated. */
+		return true;
+	}
+
 	query = (struct osmo_mslookup_query){
 		.id = {
 			.type = OSMO_MSLOOKUP_ID_IMSI,
-		}
+		},
 	};
 	OSMO_STRLCPY_ARRAY(query.id.imsi, req->gsup.imsi);
 	OSMO_STRLCPY_ARRAY(query.service, OSMO_MSLOOKUP_SERVICE_HLR_GSUP);
@@ -171,14 +156,10 @@
 		LOG_DGSM(req->gsup.imsi, LOGL_ERROR, "Error dispatching mslookup query for home HLR: %s\n",
 			 osmo_mslookup_result_name_c(OTC_SELECT, &query, NULL));
 		proxy_subscr_del(proxy, req->gsup.imsi);
+		/* mslookup seems to not be working. Try handling it locally. */
 		return false;
 	}
 
-yes_we_are_proxying:
-
-	/* 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;
 }
 
diff --git a/src/dgsm_vty.c b/src/dgsm_vty.c
index 62f07c7..88ea58a 100644
--- a/src/dgsm_vty.c
+++ b/src/dgsm_vty.c
@@ -516,7 +516,7 @@
       "List configured service addresses as sent to remote mslookup requests\n")
 {
 	struct mslookup_server_msc_cfg *msc;
-	struct mslookup_service_host *local_hlr = mslookup_server_get_local_gsup_addr();
+	const struct mslookup_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))
diff --git a/src/gsup_server.c b/src/gsup_server.c
index 4819ea4..9a9a57b 100644
--- a/src/gsup_server.c
+++ b/src/gsup_server.c
@@ -503,3 +503,39 @@
 
        return 0;
 }
+
+int osmo_gsup_forward_to_local_peer(struct osmo_gsup_server *server, const struct osmo_gsup_peer_id *to_peer,
+				    struct osmo_gsup_req *req, struct osmo_gsup_message *modified_gsup)
+{
+	int rc;
+	/* To forward to a remote entity (HLR, SMSC,...), we need to indicate the original source name in the Source
+	 * Name IE to make sure the reply can be routed back. Store the sender in gsup->source_name -- the remote entity
+	 * is required to return this as gsup->destination_name so that the reply gets routed to the original sender. */
+	struct osmo_gsup_message forward = *(modified_gsup? : &req->gsup);
+
+	if (req->source_name.type != OSMO_GSUP_PEER_ID_IPA_NAME) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Unsupported GSUP peer id type: %s",
+					  osmo_gsup_peer_id_type_name(req->source_name.type));
+		rc = -ENOTSUP;
+		goto routing_error;
+	}
+	forward.source_name = req->source_name.ipa_name.val;
+	forward.source_name_len = req->source_name.ipa_name.len;
+
+	if (to_peer->type != OSMO_GSUP_PEER_ID_IPA_NAME) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Unsupported GSUP peer id type: %s",
+					  osmo_gsup_peer_id_type_name(to_peer->type));
+		rc = -ENOTSUP;
+		goto routing_error;
+	}
+	LOG_GSUP_REQ(req, LOGL_INFO, "Forwarding to %s\n", osmo_gsup_peer_id_to_str(to_peer));
+	rc = osmo_gsup_enc_send_to_ipa_name(server, &to_peer->ipa_name, &forward);
+	if (rc)
+		goto routing_error;
+	osmo_gsup_req_free(req);
+	return 0;
+
+routing_error:
+	osmo_gsup_req_respond_msgt(req, OSMO_GSUP_MSGT_ROUTING_ERROR, true);
+	return rc;
+}
diff --git a/src/hlr.c b/src/hlr.c
index 0d8024f..79b50c2 100644
--- a/src/hlr.c
+++ b/src/hlr.c
@@ -507,6 +507,7 @@
 			return 0;
 	}
 
+	/* HLR related messages that are handled at this HLR instance */
 	switch (req->gsup.message_type) {
 	/* requests sent to us */
 	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_REQUEST:
diff --git a/src/mslookup_server.c b/src/mslookup_server.c
index 29768c8..72729b3 100644
--- a/src/mslookup_server.c
+++ b/src/mslookup_server.c
@@ -49,7 +49,7 @@
 	result->age = age;
 }
 
-struct mslookup_service_host *mslookup_server_get_local_gsup_addr()
+const struct mslookup_service_host *mslookup_server_get_local_gsup_addr()
 {
 	static struct mslookup_service_host gsup_bind = {};
 	struct mslookup_service_host *host;
@@ -190,7 +190,7 @@
 static void mslookup_server_rx_hlr_gsup(const struct osmo_mslookup_query *query,
 					struct osmo_mslookup_result *result)
 {
-	struct mslookup_service_host *host;
+	const struct mslookup_service_host *host;
 	int rc;
 	switch (query->id.type) {
 	case OSMO_MSLOOKUP_ID_IMSI:
@@ -231,18 +231,21 @@
  * 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_ipa_name *local_msc_name)
+					    struct osmo_ipa_name *local_msc_name,
+					    struct hlr_subscriber *ret_subscr)
 {
-	struct hlr_subscriber subscr;
+	struct hlr_subscriber _subscr;
 	int rc;
 	uint32_t age;
 
+	struct hlr_subscriber *subscr = ret_subscr ? : &_subscr;
+
 	switch (query->id.type) {
 	case OSMO_MSLOOKUP_ID_IMSI:
-		rc = db_subscr_get_by_imsi(g_hlr->dbc, query->id.imsi, &subscr);
+		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);
+		rc = db_subscr_get_by_msisdn(g_hlr->dbc, query->id.msisdn, subscr);
 		break;
 	default:
 		LOGP(DMSLOOKUP, LOGL_ERROR, "Unknown mslookup ID type: %d\n", query->id.type);
@@ -255,22 +258,22 @@
 		return false;
 	}
 
-	if (!subscr.vlr_number[0]) {
+	if (!subscr->vlr_number[0]) {
 		LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: not attached (vlr_number unset)\n",
 		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
 	}
 
-	if (subscr.vlr_via_proxy.len) {
+	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(DMSLOOKUP, 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_ipa_name_to_str(&subscr.vlr_via_proxy));
+		     subscr->vlr_number,
+		     osmo_ipa_name_to_str(&subscr->vlr_via_proxy));
 		return false;
 	}
 
-	if (!timestamp_age(&subscr.last_lu_seen, &age)) {
+	if (!timestamp_age(&subscr->last_lu_seen, &age)) {
 		LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: Invalid last_lu_seen timestamp for subscriber\n",
 		     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL));
 		return false;
@@ -283,7 +286,7 @@
 	}
 
 	*lu_age = age;
-	osmo_ipa_name_set_str(local_msc_name, subscr.vlr_number);
+	osmo_ipa_name_set_str(local_msc_name, subscr->vlr_number);
 	LOGP(DMSLOOKUP, LOGL_DEBUG, "%s: attached %u seconds ago at local VLR %s\n",
 	     osmo_mslookup_result_name_c(OTC_SELECT, query, NULL),
 	     age, osmo_ipa_name_to_str(local_msc_name));
@@ -297,7 +300,8 @@
  */
 static bool subscriber_has_done_lu_here_proxy(const struct osmo_mslookup_query *query,
 					      uint32_t *lu_age,
-					      struct osmo_ipa_name *local_msc_name)
+					      struct osmo_ipa_name *local_msc_name,
+					      struct proxy_subscr *ret_proxy_subscr)
 {
 	struct proxy_subscr proxy_subscr;
 	uint32_t age;
@@ -350,12 +354,14 @@
 	     age, osmo_ipa_name_to_str(local_msc_name),
 	     OSMO_SOCKADDR_STR_FMT_ARGS(&proxy_subscr.remote_hlr_addr));
 
+	if (ret_proxy_subscr)
+		*ret_proxy_subscr = proxy_subscr;
 	return true;
 }
 
-static bool subscriber_has_done_lu_here(const struct osmo_mslookup_query *query,
-					uint32_t *lu_age_p,
-					struct osmo_ipa_name *local_msc_name)
+bool subscriber_has_done_lu_here(const struct osmo_mslookup_query *query,
+				 uint32_t *lu_age_p, struct osmo_ipa_name *local_msc_name,
+				 char *ret_imsi, size_t ret_imsi_len)
 {
 	bool attached_here;
 	uint32_t lu_age = 0;
@@ -363,6 +369,9 @@
 	bool attached_here_proxy;
 	uint32_t proxy_lu_age = 0;
 	struct osmo_ipa_name proxy_msc_name = {};
+	struct proxy_subscr proxy_subscr;
+	struct hlr_subscriber db_subscr;
+
 
 	/* 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:
@@ -372,14 +381,19 @@
 	 * 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);
+	attached_here = subscriber_has_done_lu_here_hlr(query, &lu_age, &msc_name, &db_subscr);
+	attached_here_proxy = subscriber_has_done_lu_here_proxy(query, &proxy_lu_age, &proxy_msc_name, &proxy_subscr);
 
 	/* 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 (ret_imsi)
+			osmo_strlcpy(ret_imsi, proxy_subscr.imsi, ret_imsi_len);
+	} else if (attached_here) {
+		if (ret_imsi)
+			osmo_strlcpy(ret_imsi, db_subscr.imsi, ret_imsi_len);
 	}
 
 	if (attached_here && !msc_name.len) {
@@ -403,7 +417,7 @@
 
 /* A remote entity is asking us whether we are providing the given service for the given subscriber. */
 void mslookup_server_rx(const struct osmo_mslookup_query *query,
-			     struct osmo_mslookup_result *result)
+			struct osmo_mslookup_result *result)
 {
 	const struct mslookup_service_host *service_host;
 	uint32_t age;
@@ -417,7 +431,7 @@
 	/* 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)) {
+	if (!subscriber_has_done_lu_here(query, &age, &msc_name, NULL, 0)) {
 		*result = not_found;
 		return;
 	}
diff --git a/src/proxy.c b/src/proxy.c
index 0d3fd13..b9cd313 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -47,7 +47,8 @@
 				      (gsup_msg) ? osmo_gsup_message_type_name((gsup_msg)->message_type) : "NULL", \
 				      ##args)
 
-/* Why have a separate struct to add an llist_head entry?
+/* The proxy subscriber database.
+ * 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 {
@@ -56,10 +57,16 @@
 	struct proxy_subscr data;
 };
 
+struct proxy_pending_gsup_req {
+	struct llist_head entry;
+	struct osmo_gsup_req *req;
+	timestamp_t received_at;
+};
+
 /* 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)
+static void proxy_deferred_gsup_req_add(struct proxy *proxy, struct osmo_gsup_req *req)
 {
 	struct proxy_pending_gsup_req *m;
 
@@ -70,49 +77,46 @@
 	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)
+static void proxy_pending_req_remote_hlr_connect_result(struct osmo_gsup_req *req, struct remote_hlr *remote_hlr)
 {
-	osmo_gsup_req_respond_err(m->req, GMM_CAUSE_IMSI_UNKNOWN, "could not reach home HLR");
-	m->req = NULL;
+	if (!remote_hlr || !remote_hlr_is_up(remote_hlr)) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_IMSI_UNKNOWN, "Proxy: Failed to connect to home HLR");
+		return;
+	}
+
+	remote_hlr_gsup_forward_to_remote_hlr(remote_hlr, 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)
+static bool proxy_deferred_gsup_req_waiting(struct proxy *proxy, const char *imsi)
 {
-	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;
+	struct proxy_pending_gsup_req *p;
 	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))
+	llist_for_each_entry(p, &proxy->pending_gsup_reqs, entry) {
+		if (strcmp(p->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);
+		return true;
 	}
+	return false;
 }
 
+/* Result of looking for remote HLR. If it failed, pass remote_hlr as NULL. On failure, the remote_hlr may be passed
+ * NULL. */
+static void proxy_deferred_gsup_req_pop(struct proxy *proxy, const char *imsi, struct remote_hlr *remote_hlr)
+{
+	struct proxy_pending_gsup_req *p, *n;
+	OSMO_ASSERT(imsi);
+
+	llist_for_each_entry_safe(p, n, &proxy->pending_gsup_reqs, entry) {
+		if (strcmp(p->req->gsup.imsi, imsi))
+			continue;
+
+		proxy_pending_req_remote_hlr_connect_result(p->req, remote_hlr);
+		p->req = NULL;
+		llist_del(&p->entry);
+		talloc_free(p);
+	}
+}
 
 static bool proxy_subscr_matches_imsi(const struct proxy_subscr *proxy_subscr, const char *imsi)
 {
@@ -170,21 +174,6 @@
 	return 0;
 }
 
-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_create_or_update(struct proxy *proxy, const struct proxy_subscr *proxy_subscr)
 {
 	struct proxy_subscr_listentry *e = _proxy_get_by_imsi(proxy, proxy_subscr->imsi);
@@ -207,7 +196,7 @@
 int proxy_subscr_del(struct proxy *proxy, const char *imsi)
 {
 	struct proxy_subscr_listentry *e;
-	proxy_defer_gsup_message_pop(proxy, NULL, imsi, NULL);
+	proxy_deferred_gsup_req_pop(proxy, imsi, NULL);
 	e = _proxy_get_by_imsi(proxy, imsi);
 	if (!e)
 		return -ENOENT;
@@ -264,42 +253,6 @@
 	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_new.remote_hlr_addr = remote_hlr->addr;
-
-	if (proxy_subscr_create_or_update(proxy, &proxy_subscr_new)) {
-		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_new.imsi);
-		return;
-	}
-	proxy_subscr = &proxy_subscr_new;
-	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,
@@ -457,27 +410,77 @@
 	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)
+static void proxy_remote_hlr_connect_result_cb(const struct osmo_sockaddr_str *addr, struct remote_hlr *remote_hlr,
+					       void *data)
 {
-	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");
+	struct proxy *proxy = data;
+	struct proxy_subscr_listentry *e;
+	if (!proxy)
 		return;
+	llist_for_each_entry(e, &proxy->subscr_list, entry) {
+		if (!osmo_sockaddr_str_cmp(addr, &e->data.remote_hlr_addr)) {
+			proxy_deferred_gsup_req_pop(proxy, e->data.imsi, remote_hlr);
+		}
 	}
-
-	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)
+/* Store the remote HLR's GSUP address for this proxy subscriber.
+ * This can be set before the remote_hlr is connected, or after.
+ * And, this can be set before the gsup_req has been queued for this HLR, or after.
+ */
+void proxy_subscr_remote_hlr_resolved(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
+				      const struct osmo_sockaddr_str *remote_hlr_addr)
+{
+	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_create_or_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");
+
+	/* If any messages for this HLR are already spooled, connect now. Otherwise wait for
+	 * proxy_subscr_forward_to_remote_hlr() to connect then. */
+	if (proxy_deferred_gsup_req_waiting(proxy, proxy_subscr->imsi))
+		remote_hlr_get_or_connect(&proxy_subscr->remote_hlr_addr, true,
+					  proxy_remote_hlr_connect_result_cb, proxy);
+}
+
+int 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;
+	int rc;
+
+	rc = proxy_acknowledge_gsup_to_remote_hlr(proxy, proxy_subscr, req);
+	if (rc) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_PROTO_ERR_UNSPEC, "Proxy does not allow this message");
+		return rc;
+	}
 
 	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. */
+		/* We don't know the remote target yet. Still waiting for an MS lookup response, which will end up
+		 * calling proxy_subscr_remote_hlr_resolved(). See dgsm.c. */
 		LOG_PROXY_SUBSCR_MSG(proxy_subscr, &req->gsup, LOGL_DEBUG, "deferring until remote HLR is known\n");
-		proxy_defer_gsup_req(proxy, req);
-		return;
+		proxy_deferred_gsup_req_add(proxy, req);
+		return 0;
 	}
 
 	if (!osmo_gsup_peer_id_is_empty(&req->via_proxy)) {
@@ -489,23 +492,22 @@
 				     osmo_gsup_peer_id_to_str(&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;
+	/* We could always store in the defer queue and empty the queue if the connection is already up.
+	 * Slight optimisation: if the remote_hlr is already up and running, skip the defer queue.
+	 * First ask for an existing remote_hlr. */
+	remote_hlr = remote_hlr_get_or_connect(&proxy_subscr->remote_hlr_addr, false, NULL, NULL);
+	if (remote_hlr && remote_hlr_is_up(remote_hlr)) {
+		proxy_pending_req_remote_hlr_connect_result(req, remote_hlr);
+		return 0;
 	}
 
-	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);
+	/* Not existing or not up. Defer req and ask to be notified when it is up.
+	 * If the remote_hlr exists but is not connected yet, there should actually already be a pending
+	 * proxy_remote_hlr_connect_result_cb queued, but it doesn't hurt to do that more often. */
+	proxy_deferred_gsup_req_add(proxy, req);
+	remote_hlr_get_or_connect(&proxy_subscr->remote_hlr_addr, true,
+				  proxy_remote_hlr_connect_result_cb, proxy);
+	return 0;
 }
 
 int proxy_subscr_forward_to_vlr(struct proxy *proxy, const struct proxy_subscr *proxy_subscr,
diff --git a/src/remote_hlr.c b/src/remote_hlr.c
index 42bf700..e2e7d47 100644
--- a/src/remote_hlr.c
+++ b/src/remote_hlr.c
@@ -100,16 +100,16 @@
 	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;
-}
+struct remote_hlr_pending_up {
+	struct llist_head entry;
+	remote_hlr_connect_result_cb_t connect_result_cb;
+	void *data;
+};
 
 static bool remote_hlr_up_down(struct osmo_gsup_client *gsupc, bool up)
 {
 	struct remote_hlr *remote_hlr = gsupc->data;
+	struct remote_hlr_pending_up *p, *n;
 	if (!up) {
 		LOG_REMOTE_HLR(remote_hlr, LOGL_NOTICE, "link to remote HLR is down, removing GSUP client\n");
 		remote_hlr_destroy(remote_hlr);
@@ -117,22 +117,41 @@
 	}
 
 	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);
+	llist_for_each_entry_safe(p, n, &remote_hlr->pending_up_callbacks, entry) {
+		if (p->connect_result_cb)
+			p->connect_result_cb(&remote_hlr->addr, remote_hlr, p->data);
+		llist_del(&p->entry);
+	}
 	return true;
 }
 
-struct remote_hlr *remote_hlr_get(const struct osmo_sockaddr_str *addr, bool create)
+bool remote_hlr_is_up(struct remote_hlr *remote_hlr)
 {
-	struct remote_hlr *rh;
+	return remote_hlr && remote_hlr->gsupc && remote_hlr->gsupc->is_connected;
+}
+
+struct remote_hlr *remote_hlr_get_or_connect(const struct osmo_sockaddr_str *addr, bool connect,
+					     remote_hlr_connect_result_cb_t connect_result_cb, void *data)
+{
+	struct remote_hlr *rh = NULL;
+	struct remote_hlr *rh_i;
 	struct osmo_gsup_client_config cfg;
 
-	llist_for_each_entry(rh, &remote_hlrs, entry) {
-		if (!osmo_sockaddr_str_cmp(&rh->addr, addr))
-			return rh;
+	llist_for_each_entry(rh_i, &remote_hlrs, entry) {
+		if (!osmo_sockaddr_str_cmp(&rh_i->addr, addr)) {
+			rh = rh_i;
+			break;
+		}
 	}
 
-	if (!create)
+	if (rh)
+		goto add_result_cb;
+
+	if (!connect) {
+		if (connect_result_cb)
+			connect_result_cb(addr, NULL, data);
 		return NULL;
+	}
 
 	/* Doesn't exist yet, create a GSUP client to remote HLR. */
 	cfg = (struct osmo_gsup_client_config){
@@ -150,15 +169,33 @@
 		.addr = *addr,
 		.gsupc = osmo_gsup_client_create3(rh, &cfg),
 	};
+	INIT_LLIST_HEAD(&rh->pending_up_callbacks);
 	if (!rh->gsupc) {
 		LOGP(DDGSM, LOGL_ERROR,
 		     "Failed to establish connection to remote HLR " OSMO_SOCKADDR_STR_FMT "\n",
 		     OSMO_SOCKADDR_STR_FMT_ARGS(addr));
 		talloc_free(rh);
+		if (connect_result_cb)
+			connect_result_cb(addr, NULL, data);
 		return NULL;
 	}
+
 	rh->gsupc->data = rh;
 	llist_add(&rh->entry, &remote_hlrs);
+
+add_result_cb:
+	if (connect_result_cb) {
+		if (remote_hlr_is_up(rh)) {
+			connect_result_cb(addr, rh, data);
+		} else {
+			struct remote_hlr_pending_up *p;
+			p = talloc_zero(rh, struct remote_hlr_pending_up);
+			OSMO_ASSERT(p);
+			p->connect_result_cb = connect_result_cb;
+			p->data = data;
+			llist_add_tail(&p->entry, &rh->pending_up_callbacks);
+		}
+	}
 	return rh;
 }
 
@@ -182,14 +219,19 @@
 }
 
 /* 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)
+void remote_hlr_gsup_forward_to_remote_hlr(struct remote_hlr *remote_hlr, struct osmo_gsup_req *req,
+					   struct osmo_gsup_message *modified_gsup)
 {
 	int rc;
 	struct msgb *msg;
 	/* To forward to a remote HLR, we need to indicate the source MSC's name in the Source Name IE to make sure the
 	 * reply can be routed back. Store the sender MSC in gsup->source_name -- the remote HLR is required to return
 	 * this as gsup->destination_name so that the reply gets routed to the original MSC. */
-	struct osmo_gsup_message forward = req->gsup;
+	struct osmo_gsup_message forward;
+	if (modified_gsup)
+		forward = *modified_gsup;
+	else
+		forward = req->gsup;
 
 	if (req->source_name.type != OSMO_GSUP_PEER_ID_IPA_NAME) {
 		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "Unsupported GSUP peer id type: %s",
diff --git a/tests/gsup_server/Makefile.am b/tests/gsup_server/Makefile.am
index 6df538f..0b18d61 100644
--- a/tests/gsup_server/Makefile.am
+++ b/tests/gsup_server/Makefile.am
@@ -31,6 +31,7 @@
 gsup_server_test_LDADD = \
 	$(top_srcdir)/src/gsup_server.c \
 	$(top_srcdir)/src/gsup_router.c \
+	$(top_srcdir)/src/gsup_send.c \
 	$(top_srcdir)/src/gsupclient/gsup_peer_id.c \
 	$(top_srcdir)/src/gsupclient/gsup_req.c \
 	$(LIBOSMOCORE_LIBS) \

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

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


More information about the gerrit-log mailing list