Change in osmo-hlr[master]: 1/2: refactor: add and use lu_fsm, osmo_gsup_req, osmo_ipa_name

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/.

laforge gerrit-no-reply at lists.osmocom.org
Fri May 1 14:37:32 UTC 2020


laforge has submitted this change. ( https://gerrit.osmocom.org/c/osmo-hlr/+/16205 )

Change subject: 1/2: refactor: add and use lu_fsm, osmo_gsup_req, osmo_ipa_name
......................................................................

1/2: refactor: add and use lu_fsm, osmo_gsup_req, osmo_ipa_name

These are seemingly orthogonal changes in one patch, because they are in fact
sufficiently intertwined that we are not willing to spend the time to separate
them. They are also refactoring changes, unlikely to make sense on their own.

** lu_fsm:

Attempting to make luop.c keep state about incoming GSUP requests made me find
shortcomings in several places:
- since it predates osmo_fsm, it is a state machine that does not strictly
  enforce the order of state transitions or the right sequence of incoming
  events.
- several places OSMO_ASSERT() on data received from the network.
- modifies the subscriber state before a LU is accepted.
- dead code about canceling a subscriber in a previous VLR. That would be a
  good thing to actually do, which should also be trivial now that we record
  vlr_name and sgsn_name, but I decided to remove the dead code for now.

To both step up the LU game *and* make it easier for me to integrate
osmo_gsup_req handling, I decided to create a lu_fsm, drawing from my, by now,
ample experience of writing osmo_fsms.

** osmo_gsup_req:

Prepare for D-GSM, where osmo-hlr will do proxy routing for remote HLRs /
communicate with remote MSCs via a proxy:

a) It is important that a response that osmo-hlr generates and that is sent
back to a requesting MSC contains all IEs that are needed to route it back to
the requester. Particularly source_name must become destination_name in the
response to be able to even reach the requesting MSC. Other fields are also
necessary to match, which were so far taken care of in individual numerous code
paths.

b) For some operations, the response to a GSUP request is generated
asynchronously (like Update Location Request -> Response, or taking the
response from an EUSE, or the upcoming proxying to a remote HLR). To be able to
feed a request message's information back into the response, we must thus keep
the request data around. Since struct osmo_gsup_message references a lot of
external data, usually with pointers directly into the received msgb, it is not
so trivial to pass GSUP message data around asynchronously, on its own.

osmo_gsup_req is the combined solution for both a and b: it keeps all data for
a GSUP message by taking ownership of the incoming msgb, and it provides an
explicit API "forcing" callers to respond with osmo_gsup_req_respond(), so that
all code paths trivially are definitely responding with the correct IEs set to
match the request's routing (by using osmo_gsup_make_response() recently added
to libosmocore).

Adjust all osmo-hlr code paths to use *only* osmo_gsup_req to respond to
incoming requests received on the GSUP server (above LU code being one of
them).

In fact, the same should be done on the client side. Hence osmo_gsup_req is
implemented in a server/client agnostic way, and is placed in
libosmo-gsupclient. As soon as we see routing errors in complex GSUP setups,
using osmo_gsup_req in the related GSUP client is likely to resolve those
problems without much thinking required beyond making all code paths use it.

libosmo-gsupclient is hence added to osmo-hlr binary's own library
dependencies. It would have been added by the D-GSM proxy routing anyway, we
are just doing it a little sooner.

** cni_peer_id.c / osmo_ipa_name:

We so far handle an IPA unit name as pointer + size, or as just pointer with
implicit talloc size. To ease working with GSUP peer identification data, I
require:

- a non-allocated storage of an IPA Name. It brings the drawback of being
  size limited, but our current implementation is anyway only able to handle
  MSC and SGSN names of 31 characters (see struct hlr_subscriber).
- a single-argument handle for IPA Name,
- easy to use utility functions like osmo_ipa_name_to_str(), osmo_ipa_name_cmp(), and copying
  by simple assignment, a = b.

Hence this patch adds a osmo_ipa_name in cni_peer_id.h and cni_peer_id.c. Heavily
used in LU and osmo_gsup_req.

Depends: libosmocore Id9692880079ea0f219f52d81b1923a76fc640566
Change-Id: I3a8dff3d4a1cbe10d6ab08257a0138d6b2a082d9
---
M configure.ac
M include/Makefile.am
A include/osmocom/gsupclient/gsup_req.h
A include/osmocom/gsupclient/ipa_name.h
M include/osmocom/hlr/Makefile.am
M include/osmocom/hlr/db.h
M include/osmocom/hlr/gsup_router.h
M include/osmocom/hlr/gsup_server.h
M include/osmocom/hlr/hlr.h
M include/osmocom/hlr/hlr_ussd.h
M include/osmocom/hlr/logging.h
A include/osmocom/hlr/lu_fsm.h
D include/osmocom/hlr/luop.h
M src/Makefile.am
M src/db_hlr.c
M src/gsup_router.c
M src/gsup_send.c
M src/gsup_server.c
M src/gsupclient/Makefile.am
A src/gsupclient/gsup_req.c
A src/gsupclient/ipa_name.c
M src/hlr.c
M src/hlr_ussd.c
M src/logging.c
A src/lu_fsm.c
D src/luop.c
M tests/Makefile.am
M tests/db/Makefile.am
M tests/db/db_test.c
M tests/db/db_test.err
A tests/gsup/Makefile.am
A tests/gsup/gsup_test.c
A tests/gsup/gsup_test.err
A tests/gsup/gsup_test.ok
M tests/gsup_server/Makefile.am
M tests/test_nodes.vty
M tests/testsuite.at
37 files changed, 1,692 insertions(+), 865 deletions(-)

Approvals:
  Jenkins Builder: Verified
  laforge: Looks good to me, approved



diff --git a/configure.ac b/configure.ac
index 73ff752..0e03ff0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -204,6 +204,7 @@
 	tests/auc/Makefile
 	tests/auc/gen_ts_55_205_test_sets/Makefile
 	tests/gsup_server/Makefile
+	tests/gsup/Makefile
 	tests/db/Makefile
 	tests/db_upgrade/Makefile
 	tests/mslookup/Makefile
diff --git a/include/Makefile.am b/include/Makefile.am
index 9827950..aeeb03f 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -1,7 +1,9 @@
 SUBDIRS = osmocom
 
 nobase_include_HEADERS = \
+	osmocom/gsupclient/ipa_name.h \
 	osmocom/gsupclient/gsup_client.h \
+	osmocom/gsupclient/gsup_req.h \
 	osmocom/mslookup/mdns.h \
 	osmocom/mslookup/mdns_sock.h \
 	osmocom/mslookup/mslookup_client_fake.h \
diff --git a/include/osmocom/gsupclient/gsup_req.h b/include/osmocom/gsupclient/gsup_req.h
new file mode 100644
index 0000000..c61483b
--- /dev/null
+++ b/include/osmocom/gsupclient/gsup_req.h
@@ -0,0 +1,119 @@
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsupclient/ipa_name.h>
+
+struct osmo_gsup_req;
+
+#define LOG_GSUP_REQ_CAT_SRC(req, subsys, level, file, line, fmt, args...) \
+	LOGPSRC(subsys, level, file, line, "GSUP %u: %s: IMSI-%s %s: " fmt, \
+		(req) ? (req)->nr : 0, \
+		(req) ? osmo_ipa_name_to_str(&(req)->source_name) : "NULL", \
+		(req) ? (req)->gsup.imsi : "NULL", \
+		(req) ? osmo_gsup_message_type_name((req)->gsup.message_type) : "NULL", \
+		##args)
+#define LOG_GSUP_REQ_CAT(req, subsys, level, fmt, args...) \
+	LOG_GSUP_REQ_CAT_SRC(req, subsys, level, __FILE__, __LINE__, fmt, ##args)
+
+#define LOG_GSUP_REQ_SRC(req, level, file, line, fmt, args...) \
+	LOG_GSUP_REQ_CAT_SRC(req, DLGSUP, level, file, line, fmt, ##args)
+
+#define LOG_GSUP_REQ(req, level, fmt, args...) \
+	LOG_GSUP_REQ_SRC(req, level, __FILE__, __LINE__, fmt, ##args)
+
+typedef void (*osmo_gsup_req_send_response_t)(struct osmo_gsup_req *req, struct osmo_gsup_message *response);
+
+/* Keep track of an incoming request, to route back a response when it is ready.
+ * Particularly, a GSUP response to a request must contain various bits of information that need to be copied from the
+ * request for proxy/routing to work and for session states to remain valid. That is the main reason why (almost) all
+ * GSUP request/response should go through an osmo_gsup_req, even if it is handled synchronously.
+ */
+struct osmo_gsup_req {
+	/* The incoming GSUP message in decoded form. */
+	const struct osmo_gsup_message gsup;
+
+	/* Decoding result code. If decoding failed, this will be != 0. */
+	int decode_rc;
+
+	/* The ultimate source of this message: the source_name form the GSUP message, or, if not present, then the
+	 * immediate GSUP peer. GSUP messages going via a proxy reflect the initial source in the source_name.
+	 * This source_name is implicitly added to the routes for the conn the message was received on. */
+	struct osmo_ipa_name source_name;
+
+	/* If the source_name is not an immediate GSUP peer, this is set to the closest intermediate peer between here
+	 * and source_name. */
+	struct osmo_ipa_name via_proxy;
+
+	/* Identify this request by number, for logging. */
+	unsigned int nr;
+
+	/* osmo_gsup_req can be used by both gsup_server and gsup_client. The individual method of actually sending a
+	 * GSUP message is provided by this callback. */
+	osmo_gsup_req_send_response_t send_response_cb;
+
+	/* User supplied data pointer, may be used to provide context to send_response_cb(). */
+	void *cb_data;
+
+	/* List entry that can be used to keep a list of osmo_gsup_req instances; not used directly by osmo_gsup_req.c,
+	 * it is up to using implementations to keep a list. If this is non-NULL, osmo_gsup_req_free() calls
+	 * llist_del() on this. */
+	struct llist_head entry;
+
+	/* A decoded GSUP message still points into the received msgb. For a decoded osmo_gsup_message to remain valid,
+	 * we also need to keep the msgb. */
+	struct msgb *msg;
+};
+
+struct osmo_gsup_req *osmo_gsup_req_new(void *ctx, const struct osmo_ipa_name *from_peer, struct msgb *msg,
+					osmo_gsup_req_send_response_t send_response_cb, void *cb_data,
+					struct llist_head *add_to_list);
+void osmo_gsup_req_free(struct osmo_gsup_req *req);
+
+/*! See _osmo_gsup_req_respond() for details.
+ * Call _osmo_gsup_req_respond(), passing the caller's source file and line for logging. */
+#define osmo_gsup_req_respond(REQ, RESPONSE, ERROR, FINAL_RESPONSE) \
+	_osmo_gsup_req_respond(REQ, RESPONSE, ERROR, FINAL_RESPONSE, __FILE__, __LINE__)
+int _osmo_gsup_req_respond(struct osmo_gsup_req *req, struct osmo_gsup_message *response,
+			   bool error, bool final_response, const char *file, int line);
+
+/*! See _osmo_gsup_req_respond_msgt() for details.
+ * Call _osmo_gsup_req_respond_msgt(), passing the caller's source file and line for logging. */
+#define osmo_gsup_req_respond_msgt(REQ, MESSAGE_TYPE, FINAL_RESPONSE) \
+	_osmo_gsup_req_respond_msgt(REQ, MESSAGE_TYPE, FINAL_RESPONSE, __FILE__, __LINE__)
+int _osmo_gsup_req_respond_msgt(struct osmo_gsup_req *req, enum osmo_gsup_message_type message_type,
+				bool final_response, const char *file, int line);
+
+/*! See _osmo_gsup_req_respond_err() for details.
+ * Log an error message, and call _osmo_gsup_req_respond_err(), passing the caller's source file and line for logging.
+ */
+#define osmo_gsup_req_respond_err(REQ, CAUSE, FMT, args...) do { \
+		LOG_GSUP_REQ(REQ, LOGL_ERROR, "%s: " FMT "\n", \
+			     get_value_string(gsm48_gmm_cause_names, CAUSE), ##args); \
+		_osmo_gsup_req_respond_err(REQ, CAUSE, __FILE__, __LINE__); \
+	} while(0)
+void _osmo_gsup_req_respond_err(struct osmo_gsup_req *req, enum gsm48_gmm_cause cause,
+				const char *file, int line);
+
+int osmo_gsup_make_response(struct osmo_gsup_message *reply,
+			    const struct osmo_gsup_message *rx, bool error, bool final_response);
+
+size_t osmo_gsup_message_to_str_buf(char *buf, size_t bufsize, const struct osmo_gsup_message *msg);
+char *osmo_gsup_message_to_str_c(void *ctx, const struct osmo_gsup_message *msg);
diff --git a/include/osmocom/gsupclient/ipa_name.h b/include/osmocom/gsupclient/ipa_name.h
new file mode 100644
index 0000000..73cd1fb
--- /dev/null
+++ b/include/osmocom/gsupclient/ipa_name.h
@@ -0,0 +1,38 @@
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <unistd.h>
+#include <stdint.h>
+
+/*! IPA Name: Arbitrary length blob, not necessarily zero-terminated.
+ * In osmo-hlr, struct hlr_subscriber is mostly used as static reference and cannot serve as talloc context, which is
+ * why this is also implemented as a fixed-maximum-size buffer instead of a talloc'd arbitrary sized buffer.
+ * NOTE: The length of val may be extended in the future if it becomes necessary.
+ * At the time of writing, this holds IPA unit name strings of very limited length.
+ */
+struct osmo_ipa_name {
+	size_t len;
+	uint8_t val[128];
+};
+
+int osmo_ipa_name_set(struct osmo_ipa_name *ipa_name, const uint8_t *val, size_t len);
+int osmo_ipa_name_set_str(struct osmo_ipa_name *ipa_name, const char *str_fmt, ...);
+int osmo_ipa_name_cmp(const struct osmo_ipa_name *a, const struct osmo_ipa_name *b);
+const char *osmo_ipa_name_to_str_c(void *ctx, const struct osmo_ipa_name *ipa_name);
+const char *osmo_ipa_name_to_str(const struct osmo_ipa_name *ipa_name);
diff --git a/include/osmocom/hlr/Makefile.am b/include/osmocom/hlr/Makefile.am
index 77a8764..532fa5d 100644
--- a/include/osmocom/hlr/Makefile.am
+++ b/include/osmocom/hlr/Makefile.am
@@ -9,6 +9,6 @@
 	hlr_vty.h \
 	hlr_vty_subscr.h \
 	logging.h \
-	luop.h \
+	lu_fsm.h \
 	rand.h \
 	$(NULL)
diff --git a/include/osmocom/hlr/db.h b/include/osmocom/hlr/db.h
index c927099..5c627be 100644
--- a/include/osmocom/hlr/db.h
+++ b/include/osmocom/hlr/db.h
@@ -3,6 +3,8 @@
 #include <stdbool.h>
 #include <sqlite3.h>
 
+#include <osmocom/gsupclient/ipa_name.h>
+
 struct hlr;
 
 enum stmt_idx {
@@ -151,13 +153,12 @@
 int db_subscr_get_by_imei(struct db_context *dbc, const char *imei, struct hlr_subscriber *subscr);
 int db_subscr_nam(struct db_context *dbc, const char *imsi, bool nam_val, bool is_ps);
 int db_subscr_lu(struct db_context *dbc, int64_t subscr_id,
-		 const char *vlr_or_sgsn_number, bool is_ps);
+		 const struct osmo_ipa_name *vlr_name, bool is_ps,
+		 const struct osmo_ipa_name *via_proxy);
 
 int db_subscr_purge(struct db_context *dbc, const char *by_imsi,
 		    bool purge_val, bool is_ps);
 
-int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps);
-
 /*! Call sqlite3_column_text() and copy result to a char[].
  * \param[out] buf  A char[] used as sizeof() arg(!) and osmo_strlcpy() target.
  * \param[in] stmt  An sqlite3_stmt*.
@@ -168,3 +169,14 @@
 		const char *_txt = (const char *) sqlite3_column_text(stmt, idx); \
 		osmo_strlcpy(buf, _txt, sizeof(buf)); \
 	} while (0)
+
+/*! Call sqlite3_column_text() and copy result to a struct osmo_ipa_name.
+ * \param[out] ipa_name  A struct osmo_ipa_name* to write to.
+ * \param[in] stmt  An sqlite3_stmt*.
+ * \param[in] idx  Index in stmt's returned columns.
+ */
+#define copy_sqlite3_text_to_ipa_name(ipa_name, stmt, idx) \
+	do { \
+		const char *_txt = (const char *) sqlite3_column_text(stmt, idx); \
+		osmo_ipa_name_set_str(ipa_name, _txt); \
+	} while (0)
diff --git a/include/osmocom/hlr/gsup_router.h b/include/osmocom/hlr/gsup_router.h
index 0fc10d0..ee12a2b 100644
--- a/include/osmocom/hlr/gsup_router.h
+++ b/include/osmocom/hlr/gsup_router.h
@@ -3,6 +3,8 @@
 #include <stdint.h>
 #include <osmocom/hlr/gsup_server.h>
 
+struct osmo_ipa_name;
+
 struct gsup_route {
 	struct llist_head list;
 
@@ -12,10 +14,12 @@
 
 struct osmo_gsup_conn *gsup_route_find(struct osmo_gsup_server *gs,
 					const uint8_t *addr, size_t addrlen);
+struct osmo_gsup_conn *gsup_route_find_by_ipa_name(struct osmo_gsup_server *gs, const struct osmo_ipa_name *ipa_name);
 
 struct gsup_route *gsup_route_find_by_conn(const struct osmo_gsup_conn *conn);
 
 /* add a new route for the given address to the given conn */
+int gsup_route_add_ipa_name(struct osmo_gsup_conn *conn, const struct osmo_ipa_name *ipa_name);
 int gsup_route_add(struct osmo_gsup_conn *conn, const uint8_t *addr, size_t addrlen);
 
 /* delete all routes for the given connection */
@@ -24,3 +28,6 @@
 int osmo_gsup_addr_send(struct osmo_gsup_server *gs,
 			const uint8_t *addr, size_t addrlen,
 			struct msgb *msg);
+int osmo_gsup_send_to_ipa_name(struct osmo_gsup_server *gs, const struct osmo_ipa_name *ipa_name, struct msgb *msg);
+int osmo_gsup_enc_send_to_ipa_name(struct osmo_gsup_server *gs, const struct osmo_ipa_name *ipa_name,
+			  const struct osmo_gsup_message *gsup);
diff --git a/include/osmocom/hlr/gsup_server.h b/include/osmocom/hlr/gsup_server.h
index 14f5013..149971a 100644
--- a/include/osmocom/hlr/gsup_server.h
+++ b/include/osmocom/hlr/gsup_server.h
@@ -5,6 +5,8 @@
 #include <osmocom/abis/ipa.h>
 #include <osmocom/abis/ipaccess.h>
 #include <osmocom/gsm/gsup.h>
+#include <osmocom/gsupclient/ipa_name.h>
+#include <osmocom/gsupclient/gsup_req.h>
 
 #ifndef OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN
 #define OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN	43 /* TS 24.008 10.5.4.7 */
@@ -22,9 +24,6 @@
 	/* list of osmo_gsup_conn */
 	struct llist_head clients;
 
-	/* lu_operations list */
-	struct llist_head *luop;
-
 	struct ipa_server_link *link;
 	osmo_gsup_read_cb_t read_cb;
 	struct llist_head routes;
@@ -45,10 +44,15 @@
 	/* Set when Location Update is received: */
 	bool supports_cs; /* client supports OSMO_GSUP_CN_DOMAIN_CS */
 	bool supports_ps; /* client supports OSMO_GSUP_CN_DOMAIN_PS */
+
+	/* The IPA unit name received on this link. Routes with more unit names serviced by this link may exist in
+	 * osmo_gsup_server->routes, but this is the name the immediate peer identified as in the IPA handshake. */
+	struct osmo_ipa_name peer_name;
 };
 
 struct msgb *osmo_gsup_msgb_alloc(const char *label);
 
+struct osmo_gsup_req *osmo_gsup_conn_rx(struct osmo_gsup_conn *conn, struct msgb *msg);
 int osmo_gsup_conn_send(struct osmo_gsup_conn *conn, struct msgb *msg);
 int osmo_gsup_conn_ccm_get(const struct osmo_gsup_conn *clnt, uint8_t **addr,
 			   uint8_t tag);
@@ -57,7 +61,6 @@
 						 const char *ip_addr,
 						 uint16_t tcp_port,
 						 osmo_gsup_read_cb_t read_cb,
-						 struct llist_head *lu_op_lst,
 						 void *priv);
 
 void osmo_gsup_server_destroy(struct osmo_gsup_server *gsups);
diff --git a/include/osmocom/hlr/hlr.h b/include/osmocom/hlr/hlr.h
index 0564518..5885600 100644
--- a/include/osmocom/hlr/hlr.h
+++ b/include/osmocom/hlr/hlr.h
@@ -24,10 +24,16 @@
 
 #include <stdbool.h>
 #include <osmocom/core/linuxlist.h>
+#include <osmocom/gsm/ipa.h>
+#include <osmocom/core/tdef.h>
 
 #define HLR_DEFAULT_DB_FILE_PATH "hlr.db"
 
 struct hlr_euse;
+struct osmo_gsup_conn;
+enum osmo_gsup_message_type;
+
+extern struct osmo_tdef g_hlr_tdefs[];
 
 struct hlr {
 	/* GSUP server pointer */
@@ -43,6 +49,7 @@
 
 	/* Local bind addr */
 	char *gsup_bind_addr;
+	struct ipaccess_unit gsup_unit_name;
 
 	struct llist_head euse_list;
 	struct hlr_euse *euse_default;
@@ -67,3 +74,4 @@
 struct hlr_subscriber;
 
 void osmo_hlr_subscriber_update_notify(struct hlr_subscriber *subscr);
+int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps);
diff --git a/include/osmocom/hlr/hlr_ussd.h b/include/osmocom/hlr/hlr_ussd.h
index 08e810e..8b2e837 100644
--- a/include/osmocom/hlr/hlr_ussd.h
+++ b/include/osmocom/hlr/hlr_ussd.h
@@ -46,8 +46,8 @@
 						   struct hlr_euse *euse);
 void ussd_route_del(struct hlr_ussd_route *rt);
 
-int rx_proc_ss_req(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup);
-int rx_proc_ss_error(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup);
+void rx_proc_ss_req(struct osmo_gsup_req *req);
+void rx_proc_ss_error(struct osmo_gsup_req *req);
 
 struct ss_session;
 struct ss_request;
@@ -56,6 +56,5 @@
 struct hlr_iuse {
 	const char *name;
 	/* call-back to be called for any incoming USSD messages for this IUSE */
-	int (*handle_ussd)(struct osmo_gsup_conn *conn, struct ss_session *ss,
-			   const struct osmo_gsup_message *gsup, const struct ss_request *req);
+	int (*handle_ussd)(struct ss_session *ss, const struct osmo_gsup_message *gsup, const struct ss_request *req);
 };
diff --git a/include/osmocom/hlr/logging.h b/include/osmocom/hlr/logging.h
index 83f1acd..4e0a25c 100644
--- a/include/osmocom/hlr/logging.h
+++ b/include/osmocom/hlr/logging.h
@@ -9,6 +9,7 @@
 	DAUC,
 	DSS,
 	DMSLOOKUP,
+	DLU,
 };
 
 extern const struct log_info hlr_log_info;
diff --git a/include/osmocom/hlr/lu_fsm.h b/include/osmocom/hlr/lu_fsm.h
new file mode 100644
index 0000000..2440185
--- /dev/null
+++ b/include/osmocom/hlr/lu_fsm.h
@@ -0,0 +1,22 @@
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+void lu_rx_gsup(struct osmo_gsup_req *req);
diff --git a/include/osmocom/hlr/luop.h b/include/osmocom/hlr/luop.h
deleted file mode 100644
index 77a1dec..0000000
--- a/include/osmocom/hlr/luop.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/* OsmoHLR TX/RX lu operations */
-
-/* (C) 2017 sysmocom s.f.m.c. GmbH <info at sysmocom.de>
- * All Rights Reserved
- *
- * Author: Harald Welte <laforge at gnumonks.org>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#pragma once
-
-#include <stdbool.h>
-
-#include <osmocom/core/timer.h>
-#include <osmocom/gsm/gsup.h>
-
-#include <osmocom/hlr/db.h>
-#include <osmocom/hlr/gsup_server.h>
-
-#define CANCEL_TIMEOUT_SECS	30
-#define ISD_TIMEOUT_SECS	30
-
-enum lu_state {
-	LU_S_NULL,
-	LU_S_LU_RECEIVED,
-	LU_S_CANCEL_SENT,
-	LU_S_CANCEL_ACK_RECEIVED,
-	LU_S_ISD_SENT,
-	LU_S_ISD_ACK_RECEIVED,
-	LU_S_COMPLETE,
-};
-
-extern const struct value_string lu_state_names[];
-
-struct lu_operation {
-	/*! entry in global list of location update operations */
-	struct llist_head list;
-	/*! to which gsup_server do we belong */
-	struct osmo_gsup_server *gsup_server;
-	/*! state of the location update */
-	enum lu_state state;
-	/*! CS (false) or PS (true) Location Update? */
-	bool is_ps;
-	/*! currently running timer */
-	struct osmo_timer_list timer;
-
-	/*! subscriber related to this operation */
-	struct hlr_subscriber subscr;
-	/*! peer VLR/SGSN starting the request */
-	uint8_t *peer;
-};
-
-
-struct lu_operation *lu_op_alloc(struct osmo_gsup_server *srv);
-struct lu_operation *lu_op_alloc_conn(struct osmo_gsup_conn *conn);
-void lu_op_statechg(struct lu_operation *luop, enum lu_state new_state);
-bool lu_op_fill_subscr(struct lu_operation *luop, struct db_context *dbc,
-		       const char *imsi);
-struct lu_operation *lu_op_by_imsi(const char *imsi,
-				   const struct llist_head *lst);
-
-void lu_op_tx_error(struct lu_operation *luop, enum gsm48_gmm_cause cause);
-void lu_op_tx_ack(struct lu_operation *luop);
-void lu_op_tx_cancel_old(struct lu_operation *luop);
-void lu_op_tx_insert_subscr_data(struct lu_operation *luop);
-void lu_op_tx_del_subscr_data(struct lu_operation *luop);
-
-void lu_op_free(struct lu_operation *luop);
diff --git a/src/Makefile.am b/src/Makefile.am
index f858ff0..bfbe775 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -41,7 +41,6 @@
 	auc.c \
 	ctrl.c \
 	db.c \
-	luop.c \
 	db_auc.c \
 	db_hlr.c \
 	gsup_router.c \
@@ -53,9 +52,11 @@
 	hlr_vty_subscr.c \
 	gsup_send.c \
 	hlr_ussd.c \
+	lu_fsm.c \
 	$(NULL)
 
 osmo_hlr_LDADD = \
+	$(top_builddir)/src/gsupclient/libosmo-gsup-client.la \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOVTY_LIBS) \
@@ -71,6 +72,7 @@
 	logging.c \
 	rand_urandom.c \
 	dbd_decode_binary.c \
+	$(srcdir)/gsupclient/ipa_name.c \
 	$(NULL)
 
 osmo_hlr_db_tool_LDADD = \
diff --git a/src/db_hlr.c b/src/db_hlr.c
index b3e3887..fdac75f 100644
--- a/src/db_hlr.c
+++ b/src/db_hlr.c
@@ -28,6 +28,7 @@
 #include <time.h>
 
 #include <osmocom/core/utils.h>
+#include <osmocom/core/timer.h>
 #include <osmocom/crypt/auth.h>
 #include <osmocom/gsm/gsm23003.h>
 
@@ -36,8 +37,7 @@
 #include <osmocom/hlr/logging.h>
 #include <osmocom/hlr/hlr.h>
 #include <osmocom/hlr/db.h>
-#include <osmocom/hlr/gsup_server.h>
-#include <osmocom/hlr/luop.h>
+#include <osmocom/gsupclient/ipa_name.h>
 
 #define LOGHLR(imsi, level, fmt, args ...)	LOGP(DAUC, level, "IMSI='%s': " fmt, imsi, ## args)
 
@@ -734,7 +734,8 @@
  *         -EIO on database errors.
  */
 int db_subscr_lu(struct db_context *dbc, int64_t subscr_id,
-		 const char *vlr_or_sgsn_number, bool is_ps)
+		 const struct osmo_ipa_name *vlr_name, bool is_ps,
+		 const struct osmo_ipa_name *via_proxy)
 {
 	sqlite3_stmt *stmt;
 	int rc, ret = 0;
@@ -746,7 +747,7 @@
 	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
 		return -EIO;
 
-	if (!db_bind_text(stmt, "$number", vlr_or_sgsn_number))
+	if (!db_bind_text(stmt, "$number", (char*)vlr_name->val))
 		return -EIO;
 
 	/* execute the statement */
@@ -873,51 +874,3 @@
 
 	return ret;
 }
-
-/*! Update nam_cs/nam_ps in the db and trigger notifications to GSUP clients.
- * \param[in,out] hlr  Global hlr context.
- * \param[in] subscr   Subscriber from a fresh db_subscr_get_by_*() call.
- * \param[in] nam_val  True to enable CS/PS, false to disable.
- * \param[in] is_ps    True to enable/disable PS, false for CS.
- * \returns 0 on success, ENOEXEC if there is no need to change, a negative
- *          value on error.
- */
-int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps)
-{
-	int rc;
-        struct lu_operation *luop;
-        struct osmo_gsup_conn *co;
-	bool is_val = is_ps? subscr->nam_ps : subscr->nam_cs;
-
-	if (is_val == nam_val) {
-		LOGHLR(subscr->imsi, LOGL_DEBUG, "Already has the requested value when asked to %s %s\n",
-		       nam_val ? "enable" : "disable", is_ps ? "PS" : "CS");
-		return ENOEXEC;
-	}
-
-	rc = db_subscr_nam(hlr->dbc, subscr->imsi, nam_val, is_ps);
-	if (rc)
-		return rc > 0? -rc : rc;
-
-	/* If we're disabling, send a notice out to the GSUP client that is
-	 * responsible. Otherwise no need. */
-	if (nam_val)
-		return 0;
-
-	/* FIXME: only send to single SGSN where latest update for IMSI came from */
-	llist_for_each_entry(co, &hlr->gs->clients, list) {
-		luop = lu_op_alloc_conn(co);
-		if (!luop) {
-			LOGHLR(subscr->imsi, LOGL_ERROR,
-			       "Cannot notify GSUP client, cannot allocate lu_operation,"
-			       " for %s:%u\n",
-			       co && co->conn && co->conn->server? co->conn->server->addr : "unset",
-			       co && co->conn && co->conn->server? co->conn->server->port : 0);
-			continue;
-		}
-		luop->subscr = *subscr;
-		lu_op_tx_del_subscr_data(luop);
-		lu_op_free(luop);
-	}
-	return 0;
-}
diff --git a/src/gsup_router.c b/src/gsup_router.c
index adf3af7..ba71fe4 100644
--- a/src/gsup_router.c
+++ b/src/gsup_router.c
@@ -47,6 +47,11 @@
 	return NULL;
 }
 
+struct osmo_gsup_conn *gsup_route_find_by_ipa_name(struct osmo_gsup_server *gs, const struct osmo_ipa_name *ipa_name)
+{
+	return gsup_route_find(gs, ipa_name->val, ipa_name->len);
+}
+
 /*! Find a GSUP connection's route (to read the IPA address from the route).
  * \param[in] conn GSUP connection
  * \return GSUP route
@@ -67,10 +72,15 @@
 int gsup_route_add(struct osmo_gsup_conn *conn, const uint8_t *addr, size_t addrlen)
 {
 	struct gsup_route *gr;
+	struct osmo_gsup_conn *exists_on_conn;
 
 	/* Check if we already have a route for this address */
-	if (gsup_route_find(conn->server, addr, addrlen))
-		return -EEXIST;
+	exists_on_conn = gsup_route_find(conn->server, addr, addrlen);
+	if (exists_on_conn) {
+		if (exists_on_conn != conn)
+			return -EEXIST;
+		return 0;
+	}
 
 	/* allocate new route and populate it */
 	gr = talloc_zero(conn->server, struct gsup_route);
@@ -86,6 +96,11 @@
 	return 0;
 }
 
+int gsup_route_add_ipa_name(struct osmo_gsup_conn *conn, const struct osmo_ipa_name *ipa_name)
+{
+	return gsup_route_add(conn, ipa_name->val, ipa_name->len);
+}
+
 /* delete all routes for the given connection */
 int gsup_route_del_conn(struct osmo_gsup_conn *conn)
 {
@@ -95,7 +110,7 @@
 	llist_for_each_entry_safe(gr, gr2, &conn->server->routes, list) {
 		if (gr->conn == conn) {
 			LOGP(DMAIN, LOGL_INFO, "Removing GSUP route for %s (GSUP disconnect)\n",
-			     gr->addr);
+			     osmo_quote_str_c(OTC_SELECT, (char*)gr->addr, talloc_total_size(gr->addr)));
 			llist_del(&gr->list);
 			talloc_free(gr);
 			num_deleted++;
diff --git a/src/gsup_send.c b/src/gsup_send.c
index 29aeaa5..99fae93 100644
--- a/src/gsup_send.c
+++ b/src/gsup_send.c
@@ -42,7 +42,8 @@
 
 	conn = gsup_route_find(gs, addr, addrlen);
 	if (!conn) {
-		DEBUGP(DLGSUP, "Cannot find route for addr %s\n", osmo_quote_str((const char*)addr, addrlen));
+		LOGP(DLGSUP, LOGL_ERROR,
+		     "Cannot find route for addr %s\n", osmo_quote_str((const char*)addr, addrlen));
 		msgb_free(msg);
 		return -ENODEV;
 	}
@@ -50,3 +51,41 @@
 	return osmo_gsup_conn_send(conn, msg);
 }
 
+/*! Send a msgb to a given address using routing.
+ * \param[in] gs  gsup server
+ * \param[in] ipa_name  IPA unit name of the client (SGSN, MSC/VLR, proxy).
+ * \param[in] msg  message buffer
+ */
+int osmo_gsup_send_to_ipa_name(struct osmo_gsup_server *gs, const struct osmo_ipa_name *ipa_name, struct msgb *msg)
+{
+	if (ipa_name->val[ipa_name->len - 1]) {
+		/* Is not nul terminated. But for legacy reasons we (still) require that. */
+		if (ipa_name->len >= sizeof(ipa_name->val)) {
+			LOGP(DLGSUP, LOGL_ERROR, "IPA unit name is too long: %s\n",
+			     osmo_ipa_name_to_str(ipa_name));
+			return -EINVAL;
+		}
+		struct osmo_ipa_name ipa_name2 = *ipa_name;
+		ipa_name2.val[ipa_name->len] = '\0';
+		ipa_name2.len++;
+		return osmo_gsup_addr_send(gs, ipa_name2.val, ipa_name2.len, msg);
+	}
+	return osmo_gsup_addr_send(gs, ipa_name->val, ipa_name->len, msg);
+}
+
+int osmo_gsup_enc_send_to_ipa_name(struct osmo_gsup_server *gs, const struct osmo_ipa_name *ipa_name,
+			  const struct osmo_gsup_message *gsup)
+{
+	struct msgb *msg = osmo_gsup_msgb_alloc("GSUP Tx");
+	int rc;
+	rc = osmo_gsup_encode(msg, gsup);
+	if (rc) {
+		LOGP(DLGSUP, LOGL_ERROR, "IMSI-%s: Cannot encode GSUP: %s\n",
+		     gsup->imsi, osmo_gsup_message_type_name(gsup->message_type));
+		msgb_free(msg);
+		return -EINVAL;
+	}
+
+	LOGP(DLGSUP, LOGL_DEBUG, "IMSI-%s: Tx: %s\n", gsup->imsi, osmo_gsup_message_type_name(gsup->message_type));
+	return osmo_gsup_send_to_ipa_name(gs, ipa_name, msg);
+}
diff --git a/src/gsup_server.c b/src/gsup_server.c
index ed1b285..ba2d456 100644
--- a/src/gsup_server.c
+++ b/src/gsup_server.c
@@ -26,10 +26,15 @@
 #include <osmocom/abis/ipaccess.h>
 #include <osmocom/gsm/gsm48_ie.h>
 #include <osmocom/gsm/apn.h>
+#include <osmocom/gsm/gsm23003.h>
 
 #include <osmocom/hlr/gsup_server.h>
 #include <osmocom/hlr/gsup_router.h>
 
+#define LOG_GSUP_CONN(conn, level, fmt, args...) \
+	LOGP(DLGSUP, level, "GSUP peer %s: " fmt, \
+	     (conn) ? osmo_ipa_name_to_str(&(conn)->peer_name) : "NULL", ##args)
+
 struct msgb *osmo_gsup_msgb_alloc(const char *label)
 {
 	struct msgb *msg = msgb_alloc_headroom(1024+16, 16, label);
@@ -57,6 +62,57 @@
 	return 0;
 }
 
+static void gsup_server_send_req_response(struct osmo_gsup_req *req, struct osmo_gsup_message *response)
+{
+	struct osmo_gsup_server *server = req->cb_data;
+	struct osmo_gsup_conn *conn;
+	struct msgb *msg = osmo_gsup_msgb_alloc("GSUP Tx");
+	int rc;
+
+	conn = gsup_route_find_by_ipa_name(server, &req->source_name);
+	if (!conn) {
+		LOG_GSUP_REQ(req, LOGL_ERROR, "GSUP client that sent this request was disconnected, cannot respond\n");
+		msgb_free(msg);
+		return;
+	}
+
+	rc = osmo_gsup_encode(msg, response);
+	if (rc) {
+		LOG_GSUP_REQ(req, LOGL_ERROR, "Unable to encode: {%s}\n",
+			     osmo_gsup_message_to_str_c(OTC_SELECT, response));
+		msgb_free(msg);
+		return;
+	}
+
+	rc = osmo_gsup_conn_send(conn, msg);
+	if (rc)
+		LOG_GSUP_CONN(conn, LOGL_ERROR, "Unable to send: %s\n", osmo_gsup_message_to_str_c(OTC_SELECT, response));
+}
+
+struct osmo_gsup_req *osmo_gsup_conn_rx(struct osmo_gsup_conn *conn, struct msgb *msg)
+{
+	struct osmo_gsup_req *req = osmo_gsup_req_new(conn->server, &conn->peer_name, msg, gsup_server_send_req_response,
+						      conn->server, NULL);
+	if (!req)
+		return NULL;
+
+	if (req->via_proxy.len) {
+		/* The source of the GSUP message is not the immediate GSUP peer, but that peer is our proxy for that
+		 * source. Add it to the routes for this conn (so we can route responses back). */
+		if (gsup_route_add_ipa_name(conn, &req->source_name)) {
+			LOG_GSUP_REQ(req, LOGL_ERROR,
+				     "GSUP message received from %s via peer %s, but there already exists a"
+				     " different route to this source, message is not routable\n",
+				     osmo_ipa_name_to_str(&req->source_name),
+				     osmo_ipa_name_to_str(&conn->peer_name));
+			osmo_gsup_req_respond_msgt(req, OSMO_GSUP_MSGT_ROUTING_ERROR, true);
+			return NULL;
+		}
+	}
+
+	return req;
+}
+
 static int osmo_gsup_conn_oap_handle(struct osmo_gsup_conn *conn,
 				struct msgb *msg_rx)
 {
@@ -202,10 +258,18 @@
 		return -EINVAL;
 	}
 
-	gsup_route_add(clnt, addr, addr_len);
+	osmo_ipa_name_set(&clnt->peer_name, addr, addr_len);
+	gsup_route_add_ipa_name(clnt, &clnt->peer_name);
 	return 0;
 }
 
+static void osmo_gsup_conn_free(struct osmo_gsup_conn *conn)
+{
+	gsup_route_del_conn(conn);
+	llist_del(&conn->list);
+	talloc_free(conn);
+}
+
 static int osmo_gsup_server_closed_cb(struct ipa_server_conn *conn)
 {
 	struct osmo_gsup_conn *clnt = (struct osmo_gsup_conn *)conn->data;
@@ -213,10 +277,7 @@
 	LOGP(DLGSUP, LOGL_INFO, "Lost GSUP client %s:%d\n",
 		conn->addr, conn->port);
 
-	gsup_route_del_conn(clnt);
-	llist_del(&clnt->list);
-	talloc_free(clnt);
-
+	osmo_gsup_conn_free(clnt);
 	return 0;
 }
 
@@ -298,8 +359,7 @@
 
 struct osmo_gsup_server *
 osmo_gsup_server_create(void *ctx, const char *ip_addr, uint16_t tcp_port,
-			osmo_gsup_read_cb_t read_cb,
-			struct llist_head *lu_op_lst, void *priv)
+			osmo_gsup_read_cb_t read_cb, void *priv)
 {
 	struct osmo_gsup_server *gsups;
 	int rc;
@@ -325,8 +385,6 @@
 	if (rc < 0)
 		goto failed;
 
-	gsups->luop = lu_op_lst;
-
 	return gsups;
 
 failed:
@@ -390,8 +448,10 @@
 	int len;
 
 	OSMO_ASSERT(gsup);
+	*gsup = (struct osmo_gsup_message){
+		.message_type = OSMO_GSUP_MSGT_INSERT_DATA_REQUEST,
+	};
 
-	gsup->message_type = OSMO_GSUP_MSGT_INSERT_DATA_REQUEST;
 	osmo_strlcpy(gsup->imsi, imsi, sizeof(gsup->imsi));
 
 	if (msisdn_enc_size < OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN)
diff --git a/src/gsupclient/Makefile.am b/src/gsupclient/Makefile.am
index 4a449ec..38b1582 100644
--- a/src/gsupclient/Makefile.am
+++ b/src/gsupclient/Makefile.am
@@ -8,7 +8,11 @@
 
 lib_LTLIBRARIES = libosmo-gsup-client.la
 
-libosmo_gsup_client_la_SOURCES = gsup_client.c
+libosmo_gsup_client_la_SOURCES = \
+	ipa_name.c \
+	gsup_client.c \
+	gsup_req.c \
+	$(NULL)
 
 libosmo_gsup_client_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined
 libosmo_gsup_client_la_LIBADD = $(TALLOC_LIBS) $(LIBOSMOCORE_LIBS) $(LIBOSMOABIS_LIBS)
diff --git a/src/gsupclient/gsup_req.c b/src/gsupclient/gsup_req.c
new file mode 100644
index 0000000..4a2ff23
--- /dev/null
+++ b/src/gsupclient/gsup_req.c
@@ -0,0 +1,410 @@
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/gsm/gsm23003.h>
+
+#include <osmocom/gsupclient/gsup_req.h>
+
+/*! Create a new osmo_gsup_req record, decode GSUP and add to a provided list of requests.
+ *
+ * Rationales:
+ *
+ * - osmo_gsup_req makes it easy to handle GSUP requests asynchronously. Before this, a GSUP message struct would be
+ *   valid only within a read callback function, and would not survive asynchronous handling, because the struct often
+ *   points directly into the received msgb. An osmo_gsup_req takes ownership of the msgb and ensures that the data
+ *   remains valid, so that it can easily be queued for later handling.
+ * - osmo_gsup_req unifies the composition of response messages to ensure that all IEs that identify it to belong to
+ *   the initial request are preserved / derived, like the source_name, destination_name, session_id, etc (see
+ *   osmo_gsup_make_response() for details).
+ * - Deallocation of an osmo_gsup_req is implicit upon sending a response. The idea is that msgb memory leaks are a
+ *   recurring source of bugs. By enforcing a request-response relation with implicit deallocation, osmo_gsup_req aims
+ *   to help avoid most such memory leaks implicitly.
+ *
+ * The typical GSUP message sequence is:
+ *   -> rx request,
+ *   <- tx response.
+ *
+ * With osmo_gsup_req we can easily expand to:
+ *   -> rx request,
+ *   ... wait asynchronously,
+ *   <- tx response.
+ *
+ * Only few GSUP conversations go beyond a 1:1 request-response match. But some have a session (e.g. USSD) or more
+ * negotiation may happen before the initial request is completed (e.g. Update Location with interleaved Insert
+ * Subscriber Data), so osmo_gsup_req also allows passing non-final responses.
+ * The final_response flag allows for:
+ *    -> rx request,
+ *    ... wait async,
+ *    <- tx intermediate message to same peer (final_response = false, req remains open),
+ *    ... wait async,
+ *    -> rx intermediate response,
+ *    ... wait async,
+ *    <- tx final response (final_response = true, req is deallocated).
+ *
+ * This function takes ownership of the msgb, which will, on success, be owned by the returned osmo_gsup_req instance
+ * until osmo_gsup_req_free(). If a decoding error occurs, send an error response immediately, and return NULL.
+ *
+ * The original CNI entity that sent the message is found in req->source_name. If the message was passed on by an
+ * intermediate CNI peer, then req->via_proxy is set to the immediate peer, and it is the responsibility of the caller
+ * to add req->source_name to the GSUP routes that are serviced by req->via_proxy (usually not relevant for clients with
+ * a single GSUP conn).
+ * Examples:
+ *
+ *   "msc" ---> here
+ *   source_name = "msc"
+ *   via_proxy = <empty>
+ *
+ *   "msc" ---> "proxy-HLR" ---> here (e.g. home HLR)
+ *   source_name = "msc"
+ *   via_proxy = "proxy-HLR"
+ *
+ *   "msc" ---> "proxy-HLR" ---> "home-HLR" ---> here (e.g. EUSE)
+ *   source_name = "msc"
+ *   via_proxy = "home-HLR"
+ *
+ * An osmo_gsup_req must be concluded (and deallocated) by calling one of the osmo_gsup_req_respond* functions.
+ *
+ * Note: osmo_gsup_req API makes use of OTC_SELECT to allocate volatile buffers for logging. Use of
+ * osmo_select_main_ctx() is mandatory when using osmo_gsup_req.
+ *
+ * \param[in] ctx  Talloc context for allocation of the new request.
+ * \param[in] from_peer  The IPA unit name of the immediate GSUP peer from which this msgb was received.
+ * \param[in] msg  The message buffer containing the received GSUP message, where msgb_l2() shall point to the GSUP
+ *                 message start. The caller no longer owns the msgb when it is passed to this function: on error, the
+ *                 msgb is freed immediately, and on success, the msgb is owned by the returned osmo_gsup_req.
+ * \param[in] send_response_cb  User specific method to send a GSUP response message, invoked upon
+ *				osmo_gsup_req_respond*() functions. Typically this invokes encoding and transmitting the
+ *				GSUP message over a network socket. See for example gsup_server_send_req_response().
+ * \param[inout] cb_data  Context data to be used freely by the caller.
+ * \param[inout] add_to_list  List to which to append this request, or NULL for no list.
+ * \return a newly allocated osmo_gsup_req, or NULL on error. If NULL is returned, an error response has already been
+ *         dispatched to the send_response_cb.
+ */
+struct osmo_gsup_req *osmo_gsup_req_new(void *ctx, const struct osmo_ipa_name *from_peer, struct msgb *msg,
+					osmo_gsup_req_send_response_t send_response_cb, void *cb_data,
+					struct llist_head *add_to_list)
+{
+	static unsigned int next_req_nr = 1;
+	struct osmo_gsup_req *req;
+	int rc;
+
+	if (!msgb_l2(msg) || !msgb_l2len(msg)) {
+		LOGP(DLGSUP, LOGL_ERROR, "Rx GSUP from %s: missing or empty L2 data\n",
+		     osmo_ipa_name_to_str(from_peer));
+		msgb_free(msg);
+		return NULL;
+	}
+
+	req = talloc_zero(ctx, struct osmo_gsup_req);
+	OSMO_ASSERT(req);
+	/* Note: req->gsup is declared const, so that the incoming message cannot be modified by handlers. */
+	req->nr = next_req_nr++;
+	req->msg = msg;
+	req->send_response_cb = send_response_cb;
+	req->cb_data = cb_data;
+	if (from_peer)
+		req->source_name = *from_peer;
+	rc = osmo_gsup_decode(msgb_l2(req->msg), msgb_l2len(req->msg), (struct osmo_gsup_message*)&req->gsup);
+	if (rc < 0) {
+		LOGP(DLGSUP, LOGL_ERROR, "Rx GSUP from %s: cannot decode (rc=%d)\n", osmo_ipa_name_to_str(from_peer), rc);
+		osmo_gsup_req_free(req);
+		return NULL;
+	}
+
+	LOG_GSUP_REQ(req, LOGL_DEBUG, "new request: {%s}\n", osmo_gsup_message_to_str_c(OTC_SELECT, &req->gsup));
+
+	if (req->gsup.source_name_len) {
+		if (osmo_ipa_name_set(&req->source_name, req->gsup.source_name, req->gsup.source_name_len)) {
+			LOGP(DLGSUP, LOGL_ERROR,
+			     "Rx GSUP from %s: failed to decode source_name, message is not routable\n",
+			     osmo_ipa_name_to_str(from_peer));
+			osmo_gsup_req_respond_msgt(req, OSMO_GSUP_MSGT_ROUTING_ERROR, true);
+			return NULL;
+		}
+
+		/* The source of the GSUP message is not the immediate GSUP peer; the peer is our proxy for that source.
+		 */
+		if (osmo_ipa_name_cmp(&req->source_name, from_peer))
+			req->via_proxy = *from_peer;
+	}
+
+	if (!osmo_imsi_str_valid(req->gsup.imsi)) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_INV_MAND_INFO, "invalid IMSI: %s",
+					  osmo_quote_str(req->gsup.imsi, -1));
+		return NULL;
+	}
+
+	if (add_to_list)
+		llist_add_tail(&req->entry, add_to_list);
+	return req;
+}
+
+/*! Free an osmo_gsup_req and its msgb -- this is usually implicit in osmo_gsup_req_resond_*(), it should not be
+ * necessary to call this directly. */
+void osmo_gsup_req_free(struct osmo_gsup_req *req)
+{
+	LOG_GSUP_REQ(req, LOGL_DEBUG, "free\n");
+	if (req->msg)
+		msgb_free(req->msg);
+	if (req->entry.prev)
+		llist_del(&req->entry);
+	talloc_free(req);
+}
+
+/*! Send a response to a GSUP request.
+ *
+ * Ensure that the response message contains all GSUP IEs that identify it as a response for the request req, by calling
+ * osmo_gsup_make_response().
+ *
+ * The final complete response message is passed to req->send_response_cb() to take care of the transmission.
+ *
+ * \param req  Request as previously initialized by osmo_gsup_req_new().
+ * \param response  Buffer to compose the response, possibly with some pre-configured IEs.
+ *                  Any missing IEs are added via osmo_gsup_make_response().
+ *                  Must not be NULL. Does not need to remain valid memory beyond the function call,
+ *                  i.e. this can just be a local variable in the calling function.
+ * \param error  True when the response message indicates an error response (error message type).
+ * \param final_response  True when the request is concluded by this response, which deallocates the req.
+ *                        False when the request should remain open after this response.
+ *                        For most plain request->response GSUP messages, this should be True.
+ * \param file  Source file for logging as in __FILE__, added by osmo_gsup_req_respond() macro.
+ * \param line  Source line for logging as in __LINE__, added by osmo_gsup_req_respond() macro.
+ */
+int _osmo_gsup_req_respond(struct osmo_gsup_req *req, struct osmo_gsup_message *response,
+			   bool error, bool final_response, const char *file, int line)
+{
+	int rc;
+
+	rc = osmo_gsup_make_response(response, &req->gsup, error, final_response);
+	if (rc) {
+		LOG_GSUP_REQ_SRC(req, LOGL_ERROR, file, line, "Invalid response (rc=%d): {%s}\n",
+				 rc, osmo_gsup_message_to_str_c(OTC_SELECT, response));
+		rc = -EINVAL;
+		goto exit_cleanup;
+	}
+
+	if (!req->send_response_cb) {
+		LOG_GSUP_REQ_SRC(req, LOGL_ERROR, file, line, "No send_response_cb set, cannot send: {%s}\n",
+				 osmo_gsup_message_to_str_c(OTC_SELECT, response));
+		rc = -EINVAL;
+		goto exit_cleanup;
+	}
+
+	LOG_GSUP_REQ_SRC(req, LOGL_DEBUG, file, line, "Tx response: {%s}\n",
+			 osmo_gsup_message_to_str_c(OTC_SELECT, response));
+	req->send_response_cb(req, response);
+
+exit_cleanup:
+	if (final_response)
+		osmo_gsup_req_free(req);
+	return rc;
+}
+
+/*! Shorthand for _osmo_gsup_req_respond() with no additional IEs and a fixed message type.
+ * Set the message type in a local osmo_gsup_message and feed it to _osmo_gsup_req_respond().
+ * That will ensure to add all IEs that identify it as a response to req.
+ *
+ * \param req  Request as previously initialized by osmo_gsup_req_new().
+ * \param message_type  The GSUP message type discriminator to respond with.
+ * \param final_response  True when the request is concluded by this response, which deallocates the req.
+ *                        False when the request should remain open after this response.
+ *                        For most plain request->response GSUP messages, this should be True.
+ * \param file  Source file for logging as in __FILE__, added by osmo_gsup_req_respond_msgt() macro.
+ * \param line  Source line for logging as in __LINE__, added by osmo_gsup_req_respond_msgt() macro.
+ */
+int _osmo_gsup_req_respond_msgt(struct osmo_gsup_req *req, enum osmo_gsup_message_type message_type,
+				bool final_response, const char *file, int line)
+{
+	struct osmo_gsup_message response = {
+		.message_type = message_type,
+	};
+	return _osmo_gsup_req_respond(req, &response, OSMO_GSUP_IS_MSGT_ERROR(message_type), final_response,
+				      file, line);
+}
+
+/*! Shorthand for _osmo_gsup_req_respond() with an error cause IEs and using the req's matched error message type.
+ * Set the error cause in a local osmo_gsup_message and feed it to _osmo_gsup_req_respond().
+ * That will ensure to add all IEs that identify it as a response to req.
+ *
+ * Responding with an error always implies a final response: req is implicitly deallocated.
+ *
+ * \param req  Request as previously initialized by osmo_gsup_req_new().
+ * \param cause  The error cause to include in a OSMO_GSUP_CAUSE_IE.
+ * \param file  Source file for logging as in __FILE__, added by osmo_gsup_req_respond_err() macro.
+ * \param line  Source line for logging as in __LINE__, added by osmo_gsup_req_respond_err() macro.
+ */
+void _osmo_gsup_req_respond_err(struct osmo_gsup_req *req, enum gsm48_gmm_cause cause,
+				const char *file, int line)
+{
+	struct osmo_gsup_message response = {
+		.cause = cause,
+	};
+
+	/* 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(req->gsup.message_type)) {
+		osmo_gsup_req_free(req);
+		return;
+	}
+
+	osmo_gsup_req_respond(req, &response, true, true);
+}
+
+/*! This function is implicitly called by the osmo_gsup_req API, if at all possible rather use osmo_gsup_req_respond().
+ * This function is non-static mostly to allow unit testing.
+ *
+ * Set fields, if still unset, that need to be copied from a received message over to its response message, to ensure
+ * the response can be routed back to the requesting peer even via GSUP proxies.
+ *
+ * Note: after calling this function, fields in the reply may reference the same memory as rx and are not deep-copied,
+ * as is the usual way we are handling decoded GSUP messages.
+ *
+ * These fields are set in the reply message, iff they are still unset:
+ * - Set reply->message_type to the rx's matching RESULT code (or ERROR code if error == true).
+ * - IMSI,
+ * - Set reply->destination_name to rx->source_name (for proxy routing),
+ * - sm_rp_mr (for SMS),
+ * - session_id (for SS/USSD),
+ * - if rx->session_state is not NONE, set tx->session_state depending on the final_response argument:
+ *   If false, set to OSMO_GSUP_SESSION_STATE_CONTINUE, else OSMO_GSUP_SESSION_STATE_END.
+ *
+ * If values in reply are already set, they will not be overwritten. The return code is an optional way of finding out
+ * whether all values that were already set in 'reply' are indeed matching the 'rx' values that would have been set.
+ *
+ * \param[in] rx  Received GSUP message that is being replied to.
+ * \param[inout] reply  The message that should be the response to rx, either empty or with some values already set up.
+ * \return 0 if the resulting message is a valid response for rx, nonzero otherwise. A nonzero rc has no effect on the
+ *         values set in the reply message: all unset fields are first updated, and then the rc is determined.
+ *         The rc is intended to merely warn if the reply message already contained data that is incompatible with rx,
+ *         e.g. a mismatching IMSI.
+ */
+int osmo_gsup_make_response(struct osmo_gsup_message *reply,
+			    const struct osmo_gsup_message *rx, bool error, bool final_response)
+{
+	int rc = 0;
+
+	if (!reply->message_type) {
+		if (error)
+			reply->message_type = OSMO_GSUP_TO_MSGT_ERROR(rx->message_type);
+		else
+			reply->message_type = OSMO_GSUP_TO_MSGT_RESULT(rx->message_type);
+	}
+
+	if (*reply->imsi == '\0')
+		OSMO_STRLCPY_ARRAY(reply->imsi, rx->imsi);
+
+	if (reply->message_class == OSMO_GSUP_MESSAGE_CLASS_UNSET)
+		reply->message_class = rx->message_class;
+
+	if (!reply->destination_name || !reply->destination_name_len) {
+		reply->destination_name = rx->source_name;
+		reply->destination_name_len = rx->source_name_len;
+	}
+
+	/* RP-Message-Reference is mandatory for SM Service */
+	if (!reply->sm_rp_mr)
+		reply->sm_rp_mr = rx->sm_rp_mr;
+
+	/* For SS/USSD, it's important to keep both session state and ID IEs */
+	if (!reply->session_id)
+		reply->session_id = rx->session_id;
+	if (rx->session_state != OSMO_GSUP_SESSION_STATE_NONE
+	    && reply->session_state == OSMO_GSUP_SESSION_STATE_NONE) {
+		if (final_response || rx->session_state == OSMO_GSUP_SESSION_STATE_END)
+			reply->session_state = OSMO_GSUP_SESSION_STATE_END;
+		else
+			reply->session_state = OSMO_GSUP_SESSION_STATE_CONTINUE;
+	}
+
+	if (strcmp(reply->imsi, rx->imsi))
+		rc |= 1 << 0;
+	if (reply->message_class != rx->message_class)
+		rc |= 1 << 1;
+	if (rx->sm_rp_mr && (!reply->sm_rp_mr || *rx->sm_rp_mr != *reply->sm_rp_mr))
+		rc |= 1 << 2;
+	if (reply->session_id != rx->session_id)
+		rc |= 1 << 3;
+	return rc;
+}
+
+/*! Print the most important value of a GSUP message to a string buffer in human readable form.
+ * \param[out] buf  The buffer to write to.
+ * \param[out] buflen  sizeof(buf).
+ * \param[in] msg  GSUP message to print.
+ */
+size_t osmo_gsup_message_to_str_buf(char *buf, size_t buflen, const struct osmo_gsup_message *msg)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buflen };
+	if (!msg) {
+		OSMO_STRBUF_PRINTF(sb, "NULL");
+		return sb.chars_needed;
+	}
+
+	if (msg->message_class)
+		OSMO_STRBUF_PRINTF(sb, "%s ", osmo_gsup_message_class_name(msg->message_class));
+
+	OSMO_STRBUF_PRINTF(sb, "%s:", osmo_gsup_message_type_name(msg->message_type));
+
+	OSMO_STRBUF_PRINTF(sb, " imsi=");
+	OSMO_STRBUF_APPEND(sb, osmo_quote_cstr_buf, msg->imsi, strnlen(msg->imsi, sizeof(msg->imsi)));
+
+	if (msg->cause)
+		OSMO_STRBUF_PRINTF(sb, " cause=%s", get_value_string(gsm48_gmm_cause_names, msg->cause));
+
+	switch (msg->cn_domain) {
+	case OSMO_GSUP_CN_DOMAIN_CS:
+		OSMO_STRBUF_PRINTF(sb, " cn_domain=CS");
+		break;
+	case OSMO_GSUP_CN_DOMAIN_PS:
+		OSMO_STRBUF_PRINTF(sb, " cn_domain=PS");
+		break;
+	default:
+		if (msg->cn_domain)
+			OSMO_STRBUF_PRINTF(sb, " cn_domain=?(%d)", msg->cn_domain);
+		break;
+	}
+
+	if (msg->source_name_len) {
+		OSMO_STRBUF_PRINTF(sb, " source_name=");
+		OSMO_STRBUF_APPEND(sb, osmo_quote_cstr_buf, (char*)msg->source_name, msg->source_name_len);
+	}
+
+	if (msg->destination_name_len) {
+		OSMO_STRBUF_PRINTF(sb, " destination_name=");
+		OSMO_STRBUF_APPEND(sb, osmo_quote_cstr_buf, (char*)msg->destination_name, msg->destination_name_len);
+	}
+
+	if (msg->session_id)
+		OSMO_STRBUF_PRINTF(sb, " session_id=%" PRIu32, msg->session_id);
+	if (msg->session_state)
+		OSMO_STRBUF_PRINTF(sb, " session_state=%s", osmo_gsup_session_state_name(msg->session_state));
+
+	if (msg->sm_rp_mr)
+		OSMO_STRBUF_PRINTF(sb, " sm_rp_mr=%" PRIu8, *msg->sm_rp_mr);
+
+	return sb.chars_needed;
+}
+
+/*! Same as  osmo_gsup_message_to_str_buf() but returns a talloc allocated string. */
+char *osmo_gsup_message_to_str_c(void *ctx, const struct osmo_gsup_message *msg)
+{
+	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", osmo_gsup_message_to_str_buf, msg)
+}
diff --git a/src/gsupclient/ipa_name.c b/src/gsupclient/ipa_name.c
new file mode 100644
index 0000000..2db069f
--- /dev/null
+++ b/src/gsupclient/ipa_name.c
@@ -0,0 +1,97 @@
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsupclient/ipa_name.h>
+
+int osmo_ipa_name_set(struct osmo_ipa_name *ipa_name, const uint8_t *val, size_t len)
+{
+	if (!val || !len) {
+		*ipa_name = (struct osmo_ipa_name){};
+		return 0;
+	}
+	if (len > sizeof(ipa_name->val))
+		return -ENOSPC;
+	ipa_name->len = len;
+	memcpy(ipa_name->val, val, len);
+	return 0;
+}
+
+int osmo_ipa_name_set_str(struct osmo_ipa_name *ipa_name, const char *str_fmt, ...)
+{
+	va_list ap;
+	if (!str_fmt)
+		return osmo_ipa_name_set(ipa_name, NULL, 0);
+
+	va_start(ap, str_fmt);
+	vsnprintf((char*)(ipa_name->val), sizeof(ipa_name->val), str_fmt, ap);
+	va_end(ap);
+	ipa_name->len = strlen((char*)(ipa_name->val))+1;
+	return 0;
+}
+
+int osmo_ipa_name_cmp(const struct osmo_ipa_name *a, const struct osmo_ipa_name *b)
+{
+	int cmp;
+	if (a == b)
+		return 0;
+	if (!a)
+		return -1;
+	if (!b)
+		return 1;
+	if (!a->len && !b->len)
+		return 0;
+	if (!a->len && b->len)
+		return -1;
+	if (!b->len && a->len)
+		return 1;
+
+	if (a->len == b->len)
+		return memcmp(a->val, b->val, a->len);
+	else if (a->len < b->len) {
+		cmp = memcmp(a->val, b->val, a->len);
+		if (!cmp)
+			cmp = -1;
+		return cmp;
+	} else {
+		/* a->len > b->len */
+		cmp = memcmp(a->val, b->val, b->len);
+		if (!cmp)
+			cmp = 1;
+		return cmp;
+	}
+}
+
+/* Call osmo_ipa_name_to_str_c with OTC_SELECT. */
+const char *osmo_ipa_name_to_str(const struct osmo_ipa_name *ipa_name)
+{
+	return osmo_ipa_name_to_str_c(OTC_SELECT, ipa_name);
+}
+
+/* Return an unquoted string, not including the terminating zero. Used for writing VTY config. */
+const char *osmo_ipa_name_to_str_c(void *ctx, const struct osmo_ipa_name *ipa_name)
+{
+	size_t len = ipa_name->len;
+	if (!len)
+		return talloc_strdup(ctx, "");
+	if (ipa_name->val[len-1] == '\0')
+		len--;
+	return osmo_escape_str_c(ctx, (char*)ipa_name->val, len);
+}
diff --git a/src/hlr.c b/src/hlr.c
index ac6afc3..47b3211 100644
--- a/src/hlr.c
+++ b/src/hlr.c
@@ -35,8 +35,9 @@
 #include <osmocom/gsm/apn.h>
 #include <osmocom/gsm/gsm48_ie.h>
 #include <osmocom/gsm/gsm_utils.h>
-#include <osmocom/gsm/protocol/gsm_23_003.h>
+#include <osmocom/gsm/gsm23003.h>
 
+#include <osmocom/gsupclient/ipa_name.h>
 #include <osmocom/hlr/db.h>
 #include <osmocom/hlr/hlr.h>
 #include <osmocom/hlr/ctrl.h>
@@ -44,14 +45,20 @@
 #include <osmocom/hlr/gsup_server.h>
 #include <osmocom/hlr/gsup_router.h>
 #include <osmocom/hlr/rand.h>
-#include <osmocom/hlr/luop.h>
 #include <osmocom/hlr/hlr_vty.h>
 #include <osmocom/hlr/hlr_ussd.h>
+#include <osmocom/hlr/lu_fsm.h>
 
 struct hlr *g_hlr;
 static void *hlr_ctx = NULL;
 static int quit = 0;
 
+struct osmo_tdef g_hlr_tdefs[] = {
+	/* 4222 is also the OSMO_GSUP_PORT */
+	{ .T = -4222, .default_val = 30, .desc = "GSUP Update Location timeout" },
+	{}
+};
+
 /* Trigger 'Insert Subscriber Data' messages to all connected GSUP clients.
  *
  * \param[in] subscr  A subscriber we have new data to send for.
@@ -69,6 +76,8 @@
 		return;
 	}
 
+	/* FIXME: send only to current vlr_number and sgsn_number */
+
 	llist_for_each_entry(co, &g_hlr->gs->clients, list) {
 		struct osmo_gsup_message gsup = { };
 		uint8_t msisdn_enc[OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN];
@@ -222,145 +231,102 @@
 	return 0;
 }
 
+/*! Update nam_cs/nam_ps in the db and trigger notifications to GSUP clients.
+ * \param[in,out] hlr  Global hlr context.
+ * \param[in] subscr   Subscriber from a fresh db_subscr_get_by_*() call.
+ * \param[in] nam_val  True to enable CS/PS, false to disable.
+ * \param[in] is_ps    True to enable/disable PS, false for CS.
+ * \returns 0 on success, ENOEXEC if there is no need to change, a negative
+ *          value on error.
+ */
+int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps)
+{
+	int rc;
+	bool is_val = is_ps? subscr->nam_ps : subscr->nam_cs;
+	struct osmo_ipa_name vlr_name;
+	struct osmo_gsup_message gsup_del_data = {
+		.message_type = OSMO_GSUP_MSGT_DELETE_DATA_REQUEST,
+	};
+	OSMO_STRLCPY_ARRAY(gsup_del_data.imsi, subscr->imsi);
+
+	if (is_val == nam_val) {
+		LOGP(DAUC, LOGL_DEBUG, "IMSI-%s: Already has the requested value when asked to %s %s\n",
+		     subscr->imsi, nam_val ? "enable" : "disable", is_ps ? "PS" : "CS");
+		return ENOEXEC;
+	}
+
+	rc = db_subscr_nam(hlr->dbc, subscr->imsi, nam_val, is_ps);
+	if (rc)
+		return rc > 0? -rc : rc;
+
+	/* If we're disabling, send a notice out to the GSUP client that is
+	 * responsible. Otherwise no need. */
+	if (nam_val)
+		return 0;
+
+	if (subscr->vlr_number && osmo_ipa_name_set_str(&vlr_name, subscr->vlr_number))
+		osmo_gsup_enc_send_to_ipa_name(g_hlr->gs, &vlr_name, &gsup_del_data);
+	if (subscr->sgsn_number && osmo_ipa_name_set_str(&vlr_name, subscr->sgsn_number))
+		osmo_gsup_enc_send_to_ipa_name(g_hlr->gs, &vlr_name, &gsup_del_data);
+	return 0;
+}
+
 /***********************************************************************
  * Send Auth Info handling
  ***********************************************************************/
 
 /* process an incoming SAI request */
-static int rx_send_auth_info(struct osmo_gsup_conn *conn,
-			     const struct osmo_gsup_message *gsup,
-			     struct db_context *dbc)
+static int rx_send_auth_info(unsigned int auc_3g_ind, struct osmo_gsup_req *req)
 {
-	struct osmo_gsup_message gsup_out;
-	struct msgb *msg_out;
+	struct osmo_gsup_message gsup_out = {
+		.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_RESULT,
+	};
 	bool separation_bit = false;
 	int num_auth_vectors = OSMO_GSUP_MAX_NUM_AUTH_INFO;
 	int rc;
 
-	subscr_create_on_demand(gsup->imsi);
+	subscr_create_on_demand(req->gsup.imsi);
 
-	/* initialize return message structure */
-	memset(&gsup_out, 0, sizeof(gsup_out));
-	memcpy(&gsup_out.imsi, &gsup->imsi, sizeof(gsup_out.imsi));
-
-	if (gsup->current_rat_type == OSMO_RAT_EUTRAN_SGS)
+	if (req->gsup.current_rat_type == OSMO_RAT_EUTRAN_SGS)
 		separation_bit = true;
 
-	if (gsup->num_auth_vectors > 0 &&
-			gsup->num_auth_vectors <= OSMO_GSUP_MAX_NUM_AUTH_INFO)
-		num_auth_vectors = gsup->num_auth_vectors;
+	if (req->gsup.num_auth_vectors > 0 &&
+			req->gsup.num_auth_vectors <= OSMO_GSUP_MAX_NUM_AUTH_INFO)
+		num_auth_vectors = req->gsup.num_auth_vectors;
 
-	rc = db_get_auc(dbc, gsup->imsi, conn->auc_3g_ind,
+	rc = db_get_auc(g_hlr->dbc, req->gsup.imsi, auc_3g_ind,
 			gsup_out.auth_vectors,
 			num_auth_vectors,
-			gsup->rand, gsup->auts, separation_bit);
+			req->gsup.rand, req->gsup.auts, separation_bit);
+
 	if (rc <= 0) {
-		gsup_out.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_ERROR;
 		switch (rc) {
 		case 0:
 			/* 0 means "0 tuples generated", which shouldn't happen.
 			 * Treat the same as "no auth data". */
 		case -ENOKEY:
-			LOGP(DAUC, LOGL_NOTICE, "%s: IMSI known, but has no auth data;"
-			     " Returning slightly inaccurate cause 'IMSI Unknown' via GSUP\n",
-			     gsup->imsi);
-			gsup_out.cause = GMM_CAUSE_IMSI_UNKNOWN;
-			break;
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_IMSI_UNKNOWN,
+						  "IMSI known, but has no auth data;"
+						  " Returning slightly inaccurate cause 'IMSI Unknown' via GSUP");
+			return rc;
 		case -ENOENT:
-			LOGP(DAUC, LOGL_NOTICE, "%s: IMSI not known\n", gsup->imsi);
-			gsup_out.cause = GMM_CAUSE_IMSI_UNKNOWN;
-			break;
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_IMSI_UNKNOWN, "IMSI unknown");
+			return rc;
 		default:
-			LOGP(DAUC, LOGL_ERROR, "%s: failure to look up IMSI in db\n", gsup->imsi);
-			gsup_out.cause = GMM_CAUSE_NET_FAIL;
-			break;
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "failure to look up IMSI in db");
+			return rc;
 		}
-	} else {
-		gsup_out.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_RESULT;
-		gsup_out.num_auth_vectors = rc;
 	}
+	gsup_out.num_auth_vectors = rc;
 
-	msg_out = osmo_gsup_msgb_alloc("GSUP AUC response");
-	osmo_gsup_encode(msg_out, &gsup_out);
-	return osmo_gsup_conn_send(conn, msg_out);
+	osmo_gsup_req_respond(req, &gsup_out, false, true);
+	return 0;
 }
 
-/***********************************************************************
- * LU Operation State / Structure
- ***********************************************************************/
-
-static LLIST_HEAD(g_lu_ops);
-
-/*! Receive Cancel Location Result from old VLR/SGSN */
-void lu_op_rx_cancel_old_ack(struct lu_operation *luop,
-			     const struct osmo_gsup_message *gsup)
+/*! Receive Update Location Request, creates new lu_operation */
+static int rx_upd_loc_req(struct osmo_gsup_conn *conn, struct osmo_gsup_req *req)
 {
-	OSMO_ASSERT(luop->state == LU_S_CANCEL_SENT);
-	/* FIXME: Check for spoofing */
-
-	osmo_timer_del(&luop->timer);
-
-	/* FIXME */
-
-	lu_op_tx_insert_subscr_data(luop);
-}
-
-/*! Receive Insert Subscriber Data Result from new VLR/SGSN */
-static void lu_op_rx_insert_subscr_data_ack(struct lu_operation *luop,
-				    const struct osmo_gsup_message *gsup)
-{
-	OSMO_ASSERT(luop->state == LU_S_ISD_SENT);
-	/* FIXME: Check for spoofing */
-
-	osmo_timer_del(&luop->timer);
-
-	/* Subscriber_Present_HLR */
-	/* CS only: Check_SS_required? -> MAP-FW-CHECK_SS_IND.req */
-
-	/* Send final ACK towards inquiring VLR/SGSN */
-	lu_op_tx_ack(luop);
-}
-
-/*! Receive GSUP message for given \ref lu_operation */
-void lu_op_rx_gsup(struct lu_operation *luop,
-		  const struct osmo_gsup_message *gsup)
-{
-	switch (gsup->message_type) {
-	case OSMO_GSUP_MSGT_INSERT_DATA_ERROR:
-		/* FIXME */
-		break;
-	case OSMO_GSUP_MSGT_INSERT_DATA_RESULT:
-		lu_op_rx_insert_subscr_data_ack(luop, gsup);
-		break;
-	case OSMO_GSUP_MSGT_LOCATION_CANCEL_ERROR:
-		/* FIXME */
-		break;
-	case OSMO_GSUP_MSGT_LOCATION_CANCEL_RESULT:
-		lu_op_rx_cancel_old_ack(luop, gsup);
-		break;
-	default:
-		LOGP(DMAIN, LOGL_ERROR, "Unhandled GSUP msg_type 0x%02x\n",
-			gsup->message_type);
-		break;
-	}
-}
-
-/*! Receive Update Location Request, creates new \ref lu_operation */
-static int rx_upd_loc_req(struct osmo_gsup_conn *conn,
-			  const struct osmo_gsup_message *gsup)
-{
-	struct hlr_subscriber *subscr;
-	struct lu_operation *luop = lu_op_alloc_conn(conn);
-	if (!luop) {
-		LOGP(DMAIN, LOGL_ERROR, "LU REQ from conn without addr?\n");
-		return -EINVAL;
-	}
-
-	subscr = &luop->subscr;
-
-	lu_op_statechg(luop, LU_S_LU_RECEIVED);
-
-	switch (gsup->cn_domain) {
+	switch (req->gsup.cn_domain) {
 	case OSMO_GSUP_CN_DOMAIN_CS:
 		conn->supports_cs = true;
 		break;
@@ -371,143 +337,64 @@
 		 * a request, the PS Domain is assumed." */
 	case OSMO_GSUP_CN_DOMAIN_PS:
 		conn->supports_ps = true;
-		luop->is_ps = true;
 		break;
 	}
-	llist_add(&luop->list, &g_lu_ops);
 
-	subscr_create_on_demand(gsup->imsi);
+	subscr_create_on_demand(req->gsup.imsi);
 
-	/* Roughly follwing "Process Update_Location_HLR" of TS 09.02 */
-
-	/* check if subscriber is known at all */
-	if (!lu_op_fill_subscr(luop, g_hlr->dbc, gsup->imsi)) {
-		/* Send Error back: Subscriber Unknown in HLR */
-		osmo_strlcpy(luop->subscr.imsi, gsup->imsi, sizeof(luop->subscr.imsi));
-		lu_op_tx_error(luop, GMM_CAUSE_IMSI_UNKNOWN);
-		return 0;
-	}
-
-	/* Check if subscriber is generally permitted on CS or PS
-	 * service (as requested) */
-	if (!luop->is_ps && !luop->subscr.nam_cs) {
-		lu_op_tx_error(luop, GMM_CAUSE_PLMN_NOTALLOWED);
-		return 0;
-	} else if (luop->is_ps && !luop->subscr.nam_ps) {
-		lu_op_tx_error(luop, GMM_CAUSE_GPRS_NOTALLOWED);
-		return 0;
-	}
-
-	/* TODO: Set subscriber tracing = deactive in VLR/SGSN */
-
-#if 0
-	/* Cancel in old VLR/SGSN, if new VLR/SGSN differs from old (FIXME: OS#4491) */
-	if (luop->is_ps == false &&
-	    strcmp(subscr->vlr_number, vlr_number)) {
-		lu_op_tx_cancel_old(luop);
-	} else if (luop->is_ps == true &&
-		   strcmp(subscr->sgsn_number, sgsn_number)) {
-		lu_op_tx_cancel_old(luop);
-	} else
-#endif
-
-	/* Store the VLR / SGSN number with the subscriber, so we know where it was last seen. */
-	LOGP(DAUC, LOGL_DEBUG, "IMSI='%s': storing %s = %s\n",
-	     subscr->imsi, luop->is_ps ? "SGSN number" : "VLR number",
-	     osmo_quote_str((const char*)luop->peer, -1));
-	if (db_subscr_lu(g_hlr->dbc, subscr->id, (const char *)luop->peer, luop->is_ps))
-		LOGP(DAUC, LOGL_ERROR, "IMSI='%s': Cannot update %s in the database\n",
-		     subscr->imsi, luop->is_ps ? "SGSN number" : "VLR number");
-
-	/* TODO: Subscriber allowed to roam in PLMN? */
-	/* TODO: Update RoutingInfo */
-	/* TODO: Reset Flag MS Purged (cs/ps) */
-	/* TODO: Control_Tracing_HLR / Control_Tracing_HLR_with_SGSN */
-	lu_op_tx_insert_subscr_data(luop);
-
+	lu_rx_gsup(req);
 	return 0;
 }
 
-static int rx_purge_ms_req(struct osmo_gsup_conn *conn,
-			   const struct osmo_gsup_message *gsup)
+static int rx_purge_ms_req(struct osmo_gsup_req *req)
 {
-	struct osmo_gsup_message gsup_reply = {0};
-	struct msgb *msg_out;
-	bool is_ps = false;
+	bool is_ps = (req->gsup.cn_domain != OSMO_GSUP_CN_DOMAIN_CS);
 	int rc;
 
-	LOGP(DAUC, LOGL_INFO, "%s: Purge MS (%s)\n", gsup->imsi,
-		is_ps ? "PS" : "CS");
-
-	memcpy(gsup_reply.imsi, gsup->imsi, sizeof(gsup_reply.imsi));
-
-	if (gsup->cn_domain == OSMO_GSUP_CN_DOMAIN_PS)
-		is_ps = true;
+	LOG_GSUP_REQ_CAT(req, DAUC, LOGL_INFO, "Purge MS (%s)\n", is_ps ? "PS" : "CS");
 
 	/* FIXME: check if the VLR that sends the purge is the same that
 	 * we have on record. Only update if yes */
 
 	/* Perform the actual update of the DB */
-	rc = db_subscr_purge(g_hlr->dbc, gsup->imsi, true, is_ps);
+	rc = db_subscr_purge(g_hlr->dbc, req->gsup.imsi, true, is_ps);
 
 	if (rc == 0)
-		gsup_reply.message_type = OSMO_GSUP_MSGT_PURGE_MS_RESULT;
-	else if (rc == -ENOENT) {
-		gsup_reply.message_type = OSMO_GSUP_MSGT_PURGE_MS_ERROR;
-		gsup_reply.cause = GMM_CAUSE_IMSI_UNKNOWN;
-	} else {
-		gsup_reply.message_type = OSMO_GSUP_MSGT_PURGE_MS_ERROR;
-		gsup_reply.cause = GMM_CAUSE_NET_FAIL;
-	}
-
-	msg_out = osmo_gsup_msgb_alloc("GSUP AUC response");
-	osmo_gsup_encode(msg_out, &gsup_reply);
-	return osmo_gsup_conn_send(conn, msg_out);
+		osmo_gsup_req_respond_msgt(req, OSMO_GSUP_MSGT_PURGE_MS_RESULT, true);
+	else if (rc == -ENOENT)
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_IMSI_UNKNOWN, "IMSI unknown");
+	else
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_NET_FAIL, "db error");
+	return rc;
 }
 
-static int gsup_send_err_reply(struct osmo_gsup_conn *conn, const char *imsi,
-				enum osmo_gsup_message_type type_in, uint8_t err_cause)
+static int rx_check_imei_req(struct osmo_gsup_req *req)
 {
-	int type_err = OSMO_GSUP_TO_MSGT_ERROR(type_in);
-	struct osmo_gsup_message gsup_reply = {0};
-	struct msgb *msg_out;
-
-	OSMO_STRLCPY_ARRAY(gsup_reply.imsi, imsi);
-	gsup_reply.message_type = type_err;
-	gsup_reply.cause = err_cause;
-	msg_out = osmo_gsup_msgb_alloc("GSUP ERR response");
-	osmo_gsup_encode(msg_out, &gsup_reply);
-	LOGP(DMAIN, LOGL_NOTICE, "Tx %s\n", osmo_gsup_message_type_name(type_err));
-	return osmo_gsup_conn_send(conn, msg_out);
-}
-
-static int rx_check_imei_req(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup)
-{
-	struct osmo_gsup_message gsup_reply = {0};
-	struct msgb *msg_out;
+	struct osmo_gsup_message gsup_reply;
 	char imei[GSM23003_IMEI_NUM_DIGITS_NO_CHK+1] = {0};
+	const struct osmo_gsup_message *gsup = &req->gsup;
 	int rc;
 
 	/* Require IMEI */
 	if (!gsup->imei_enc) {
-		LOGP(DMAIN, LOGL_ERROR, "%s: missing IMEI\n", gsup->imsi);
-		gsup_send_err_reply(conn, gsup->imsi, gsup->message_type, GMM_CAUSE_INV_MAND_INFO);
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_INV_MAND_INFO, "missing IMEI");
 		return -1;
 	}
 
 	/* Decode IMEI (fails if IMEI is too long) */
 	rc = gsm48_decode_bcd_number2(imei, sizeof(imei), gsup->imei_enc, gsup->imei_enc_len, 0);
 	if (rc < 0) {
-		LOGP(DMAIN, LOGL_ERROR, "%s: failed to decode IMEI (rc: %i)\n", gsup->imsi, rc);
-		gsup_send_err_reply(conn, gsup->imsi, gsup->message_type, GMM_CAUSE_INV_MAND_INFO);
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_INV_MAND_INFO,
+					  "failed to decode IMEI %s (rc: %d)",
+					  osmo_hexdump_c(OTC_SELECT, gsup->imei_enc, gsup->imei_enc_len),
+					  rc);
 		return -1;
 	}
 
 	/* Check if IMEI is too short */
-	if (strlen(imei) != GSM23003_IMEI_NUM_DIGITS_NO_CHK) {
-		LOGP(DMAIN, LOGL_ERROR, "%s: wrong encoded IMEI length (IMEI: '%s', %lu, %i)\n", gsup->imsi, imei,
-		     strlen(imei), GSM23003_IMEI_NUM_DIGITS_NO_CHK);
-		gsup_send_err_reply(conn, gsup->imsi, gsup->message_type, GMM_CAUSE_INV_MAND_INFO);
+	if (!osmo_imei_str_valid(imei, false)) {
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_INV_MAND_INFO,
+					  "invalid IMEI: %s", osmo_quote_str_c(OTC_SELECT, imei, -1));
 		return -1;
 	}
 
@@ -517,7 +404,7 @@
 	if (g_hlr->store_imei) {
 		LOGP(DAUC, LOGL_DEBUG, "IMSI='%s': storing IMEI = %s\n", gsup->imsi, imei);
 		if (db_subscr_update_imei_by_imsi(g_hlr->dbc, gsup->imsi, imei) < 0) {
-			gsup_send_err_reply(conn, gsup->imsi, gsup->message_type, GMM_CAUSE_INV_MAND_INFO);
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_INV_MAND_INFO, "Failed to store IMEI in HLR db");
 			return -1;
 		}
 	} else {
@@ -525,18 +412,17 @@
 		LOGP(DMAIN, LOGL_INFO, "IMSI='%s': has IMEI = %s (consider setting 'store-imei')\n", gsup->imsi, imei);
 		struct hlr_subscriber subscr;
 		if (db_subscr_get_by_imsi(g_hlr->dbc, gsup->imsi, &subscr) < 0) {
-			gsup_send_err_reply(conn, gsup->imsi, gsup->message_type, GMM_CAUSE_INV_MAND_INFO);
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_INV_MAND_INFO, "IMSI unknown");
 			return -1;
 		}
 	}
 
 	/* Accept all IMEIs */
-	gsup_reply.imei_result = OSMO_GSUP_IMEI_RESULT_ACK;
-	gsup_reply.message_type = OSMO_GSUP_MSGT_CHECK_IMEI_RESULT;
-	msg_out = osmo_gsup_msgb_alloc("GSUP Check_IMEI response");
-	memcpy(gsup_reply.imsi, gsup->imsi, sizeof(gsup_reply.imsi));
-	osmo_gsup_encode(msg_out, &gsup_reply);
-	return osmo_gsup_conn_send(conn, msg_out);
+	gsup_reply = (struct osmo_gsup_message){
+		.message_type = OSMO_GSUP_MSGT_CHECK_IMEI_RESULT,
+		.imei_result = OSMO_GSUP_IMEI_RESULT_ACK,
+	};
+	return osmo_gsup_req_respond(req, &gsup_reply, false, true);
 }
 
 static char namebuf[255];
@@ -549,151 +435,112 @@
 	     osmo_quote_str_buf2(namebuf, sizeof(namebuf), (const char *)(gsup)->destination_name, (gsup)->destination_name_len), \
 	     ## args)
 
-static int read_cb_forward(struct osmo_gsup_conn *conn, struct msgb *msg, const struct osmo_gsup_message *gsup)
+static int read_cb_forward(struct osmo_gsup_req *req)
 {
 	int ret = -EINVAL;
-	struct osmo_gsup_message *gsup_err;
-
-	/* FIXME: it would be better if the msgb never were deallocated immediately by osmo_gsup_addr_send(), which a
-	 * select-loop volatile talloc context could facilitate. Then we would still be able to access gsup-> members
-	 * (pointing into the msgb) even after sending failed, and we wouldn't need to copy this data before sending: */
-	/* Prepare error message (before IEs get deallocated) */
-	gsup_err = talloc_zero(hlr_ctx, struct osmo_gsup_message);
-	OSMO_STRLCPY_ARRAY(gsup_err->imsi, gsup->imsi);
-	gsup_err->message_class = gsup->message_class;
-	gsup_err->destination_name = talloc_memdup(gsup_err, gsup->destination_name, gsup->destination_name_len);
-	gsup_err->destination_name_len = gsup->destination_name_len;
-	gsup_err->message_type = gsup->message_type;
-	gsup_err->session_state = gsup->session_state;
-	gsup_err->session_id = gsup->session_id;
-	gsup_err->source_name = talloc_memdup(gsup_err, gsup->source_name, gsup->source_name_len);
-	gsup_err->source_name_len = gsup->source_name_len;
+	const struct osmo_gsup_message *gsup = &req->gsup;
+	struct osmo_gsup_message gsup_err;
+	struct msgb *forward_msg;
+	struct osmo_ipa_name destination_name;
 
 	/* Check for routing IEs */
-	if (!gsup->source_name || !gsup->source_name_len || !gsup->destination_name || !gsup->destination_name_len) {
-		LOGP_GSUP_FWD(gsup, LOGL_ERROR, "missing routing IEs\n");
-		goto end;
+	if (!req->gsup.source_name || !req->gsup.source_name_len
+	    || !req->gsup.destination_name || !req->gsup.destination_name_len) {
+		LOGP_GSUP_FWD(&req->gsup, LOGL_ERROR, "missing routing IEs\n");
+		goto routing_error;
 	}
 
-	/* Verify source name (e.g. "MSC-00-00-00-00-00-00") */
-	if (gsup_route_find(conn->server, gsup->source_name, gsup->source_name_len) != conn) {
-		LOGP_GSUP_FWD(gsup, LOGL_ERROR, "mismatching source name\n");
-		goto end;
+	if (osmo_ipa_name_set(&destination_name, req->gsup.destination_name, req->gsup.destination_name_len)) {
+		LOGP_GSUP_FWD(&req->gsup, LOGL_ERROR, "invalid destination name\n");
+		goto routing_error;
 	}
 
-	/* Forward message without re-encoding (so we don't remove unknown IEs) */
-	LOGP_GSUP_FWD(gsup, LOGL_INFO, "checks passed, forwarding\n");
+	LOG_GSUP_REQ(req, LOGL_INFO, "Forwarding to %s\n", osmo_ipa_name_to_str(&destination_name));
 
-	/* Remove incoming IPA header to be able to prepend an outgoing IPA header */
-	msgb_pull_to_l2(msg);
-	ret = osmo_gsup_addr_send(g_hlr->gs, gsup->destination_name, gsup->destination_name_len, msg);
-	/* AT THIS POINT, THE msg MAY BE DEALLOCATED and the data like gsup->imsi, gsup->source_name etc may all be
-	 * invalid and cause segfaults. */
-	msg = NULL;
-	gsup = NULL;
-	if (ret == -ENODEV)
-		LOGP_GSUP_FWD(gsup_err, LOGL_ERROR, "destination not connected\n");
-	else if (ret)
-		LOGP_GSUP_FWD(gsup_err, LOGL_ERROR, "unknown error %i\n", ret);
-
-end:
-	/* Send error back to source */
+	/* Forward message without re-encoding (so we don't remove unknown IEs).
+	 * Copy GSUP part to forward, removing incoming IPA header to be able to prepend an outgoing IPA header */
+	forward_msg = osmo_gsup_msgb_alloc("GSUP forward");
+	forward_msg->l2h = msgb_put(forward_msg, msgb_l2len(req->msg));
+	memcpy(forward_msg->l2h, msgb_l2(req->msg), msgb_l2len(req->msg));
+	ret = osmo_gsup_send_to_ipa_name(g_hlr->gs, &destination_name, forward_msg);
 	if (ret) {
-		struct msgb *msg_err = osmo_gsup_msgb_alloc("GSUP forward ERR response");
-		gsup_err->message_type = OSMO_GSUP_MSGT_E_ROUTING_ERROR;
-		osmo_gsup_encode(msg_err, gsup_err);
-		LOGP_GSUP_FWD(gsup_err, LOGL_NOTICE, "Tx %s\n", osmo_gsup_message_type_name(gsup_err->message_type));
-		osmo_gsup_conn_send(conn, msg_err);
+		LOGP_GSUP_FWD(gsup, LOGL_ERROR, "%s (rc=%d)\n",
+			      ret == -ENODEV ? "destination not connected" : "unknown error",
+			      ret);
+		goto routing_error;
 	}
-	talloc_free(gsup_err);
-	if (msg)
-		msgb_free(msg);
-	return ret;
+	osmo_gsup_req_free(req);
+	return 0;
+
+routing_error:
+	gsup_err = (struct osmo_gsup_message){
+		.message_type = OSMO_GSUP_MSGT_ROUTING_ERROR,
+		.source_name = gsup->destination_name,
+		.source_name_len = gsup->destination_name_len,
+	};
+	osmo_gsup_req_respond(req, &gsup_err, true, true);
+	return -1;
 }
 
 static int read_cb(struct osmo_gsup_conn *conn, struct msgb *msg)
 {
-	static struct osmo_gsup_message gsup;
-	int rc;
-
-	if (!msgb_l2(msg) || !msgb_l2len(msg)) {
-		LOGP(DMAIN, LOGL_ERROR, "missing or empty L2 data\n");
-		msgb_free(msg);
+	struct osmo_gsup_req *req = osmo_gsup_conn_rx(conn, msg);
+	if (!req)
 		return -EINVAL;
+
+	/* If the GSUP recipient is other than this HLR, forward. */
+	if (req->gsup.destination_name_len) {
+		struct osmo_ipa_name destination_name;
+		struct osmo_ipa_name my_name;
+		osmo_ipa_name_set_str(&my_name, g_hlr->gsup_unit_name.serno);
+		if (!osmo_ipa_name_set(&destination_name, req->gsup.destination_name, req->gsup.destination_name_len)
+		    && osmo_ipa_name_cmp(&destination_name, &my_name)) {
+			return read_cb_forward(req);
+		}
 	}
 
-	rc = osmo_gsup_decode(msgb_l2(msg), msgb_l2len(msg), &gsup);
-	if (rc < 0) {
-		LOGP(DMAIN, LOGL_ERROR, "error in GSUP decode: %d\n", rc);
-		msgb_free(msg);
-		return rc;
-	}
-
-	/* 3GPP TS 23.003 Section 2.2 clearly states that an IMSI with less than 5
-	 * digits is impossible.  Even 5 digits is a highly theoretical case */
-	if (strlen(gsup.imsi) < 5) { /* TODO: move this check to libosmogsm/gsup.c? */
-		LOGP(DMAIN, LOGL_ERROR, "IMSI too short: %s\n", osmo_quote_str(gsup.imsi, -1));
-		gsup_send_err_reply(conn, gsup.imsi, gsup.message_type, GMM_CAUSE_INV_MAND_INFO);
-		msgb_free(msg);
-		return -EINVAL;
-	}
-
-	if (gsup.destination_name_len)
-		return read_cb_forward(conn, msg, &gsup);
-
-	switch (gsup.message_type) {
+	switch (req->gsup.message_type) {
 	/* requests sent to us */
 	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_REQUEST:
-		rx_send_auth_info(conn, &gsup, g_hlr->dbc);
+		rx_send_auth_info(conn->auc_3g_ind, req);
 		break;
 	case OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST:
-		rx_upd_loc_req(conn, &gsup);
+		rx_upd_loc_req(conn, req);
 		break;
 	case OSMO_GSUP_MSGT_PURGE_MS_REQUEST:
-		rx_purge_ms_req(conn, &gsup);
+		rx_purge_ms_req(req);
 		break;
 	/* responses to requests sent by us */
 	case OSMO_GSUP_MSGT_DELETE_DATA_ERROR:
-		LOGP(DMAIN, LOGL_ERROR, "Error while deleting subscriber data "
-		     "for IMSI %s\n", gsup.imsi);
+		LOG_GSUP_REQ(req, LOGL_ERROR, "Peer responds with: Error while deleting subscriber data\n");
+		osmo_gsup_req_free(req);
 		break;
 	case OSMO_GSUP_MSGT_DELETE_DATA_RESULT:
-		LOGP(DMAIN, LOGL_ERROR, "Deleting subscriber data for IMSI %s\n",
-		     gsup.imsi);
+		LOG_GSUP_REQ(req, LOGL_DEBUG, "Peer responds with: Subscriber data deleted\n");
+		osmo_gsup_req_free(req);
 		break;
 	case OSMO_GSUP_MSGT_PROC_SS_REQUEST:
 	case OSMO_GSUP_MSGT_PROC_SS_RESULT:
-		rx_proc_ss_req(conn, &gsup);
+		rx_proc_ss_req(req);
 		break;
 	case OSMO_GSUP_MSGT_PROC_SS_ERROR:
-		rx_proc_ss_error(conn, &gsup);
+		rx_proc_ss_error(req);
 		break;
 	case OSMO_GSUP_MSGT_INSERT_DATA_ERROR:
 	case OSMO_GSUP_MSGT_INSERT_DATA_RESULT:
 	case OSMO_GSUP_MSGT_LOCATION_CANCEL_ERROR:
 	case OSMO_GSUP_MSGT_LOCATION_CANCEL_RESULT:
-		{
-			struct lu_operation *luop = lu_op_by_imsi(gsup.imsi,
-								  &g_lu_ops);
-			if (!luop) {
-				LOGP(DMAIN, LOGL_ERROR, "GSUP message %s for "
-				     "unknown IMSI %s\n",
-				     osmo_gsup_message_type_name(gsup.message_type),
-					gsup.imsi);
-				break;
-			}
-			lu_op_rx_gsup(luop, &gsup);
-		}
+		lu_rx_gsup(req);
 		break;
 	case OSMO_GSUP_MSGT_CHECK_IMEI_REQUEST:
-		rx_check_imei_req(conn, &gsup);
+		rx_check_imei_req(req);
 		break;
 	default:
 		LOGP(DMAIN, LOGL_DEBUG, "Unhandled GSUP message type %s\n",
-		     osmo_gsup_message_type_name(gsup.message_type));
+		     osmo_gsup_message_type_name(req->gsup.message_type));
+		osmo_gsup_req_free(req);
 		break;
 	}
-	msgb_free(msg);
 	return 0;
 }
 
@@ -908,7 +755,7 @@
 
 
 	g_hlr->gs = osmo_gsup_server_create(hlr_ctx, g_hlr->gsup_bind_addr, OSMO_GSUP_PORT,
-					    read_cb, &g_lu_ops, g_hlr);
+					    read_cb, g_hlr);
 	if (!g_hlr->gs) {
 		LOGP(DMAIN, LOGL_FATAL, "Error starting GSUP server\n");
 		exit(1);
@@ -931,7 +778,8 @@
 	}
 
 	while (!quit)
-		osmo_select_main(0);
+		osmo_select_main_ctx(0);
+
 
 	osmo_gsup_server_destroy(g_hlr->gs);
 	db_close(g_hlr->dbc);
diff --git a/src/hlr_ussd.c b/src/hlr_ussd.c
index 8cdc15c..aa7614e 100644
--- a/src/hlr_ussd.c
+++ b/src/hlr_ussd.c
@@ -170,12 +170,14 @@
 	/* subscriber's vlr_number
 	 * MO USSD: originating MSC's vlr_number
 	 * MT USSD: looked up once per session and cached here */
-	uint8_t *vlr_number;
-	size_t vlr_number_len;
+	struct osmo_ipa_name vlr_name;
 
 	/* we don't keep a pointer to the osmo_gsup_{route,conn} towards the MSC/VLR here,
 	 * as this might change during inter-VLR hand-over, and we simply look-up the serving MSC/VLR
 	 * every time we receive an USSD component from the EUSE */
+
+	struct osmo_gsup_req *initial_req_from_ms;
+	struct osmo_gsup_req *initial_req_from_euse;
 };
 
 struct ss_session *ss_session_find(struct hlr *hlr, const char *imsi, uint32_t session_id)
@@ -191,6 +193,10 @@
 void ss_session_free(struct ss_session *ss)
 {
 	osmo_timer_del(&ss->timeout);
+	if (ss->initial_req_from_ms)
+		osmo_gsup_req_free(ss->initial_req_from_ms);
+	if (ss->initial_req_from_euse)
+		osmo_gsup_req_free(ss->initial_req_from_euse);
 	llist_del(&ss->list);
 	talloc_free(ss);
 }
@@ -230,32 +236,46 @@
  ***********************************************************************/
 
 /* Resolve the target MSC by ss->imsi and send GSUP message. */
-static int ss_gsup_send(struct ss_session *ss, struct osmo_gsup_server *gs, struct msgb *msg)
+static int ss_gsup_send_to_ms(struct ss_session *ss, struct osmo_gsup_server *gs, struct osmo_gsup_message *gsup)
 {
 	struct hlr_subscriber subscr = {};
+	struct msgb *msg;
 	int rc;
 
+	if (ss->initial_req_from_ms) {
+		/* Use non-final osmo_gsup_req_respond() to not deallocate the ss->initial_req_from_ms */
+		osmo_gsup_req_respond(ss->initial_req_from_ms, gsup, false, false);
+		return 0;
+	}
+
+	msg = osmo_gsup_msgb_alloc("GSUP USSD FW");
+	rc = osmo_gsup_encode(msg, gsup);
+	if (rc) {
+		LOGPSS(ss, LOGL_ERROR, "Failed to encode GSUP message\n");
+		msgb_free(msg);
+		return rc;
+	}
+
 	/* Use vlr_number as looked up by the caller, or look up now. */
-	if (!ss->vlr_number) {
+	if (!ss->vlr_name.len) {
 		rc = db_subscr_get_by_imsi(g_hlr->dbc, ss->imsi, &subscr);
 		if (rc < 0) {
 			LOGPSS(ss, LOGL_ERROR, "Cannot find subscriber, cannot route GSUP message\n");
 			msgb_free(msg);
 			return -EINVAL;
 		}
-		ss->vlr_number = (uint8_t *)talloc_strdup(ss, subscr.vlr_number);
-		ss->vlr_number_len = strlen(subscr.vlr_number) + 1;
+		osmo_ipa_name_set_str(&ss->vlr_name, subscr.vlr_number);
 	}
 
 	/* Check for empty string (all vlr_number strings end in "\0", because otherwise gsup_route_find() fails) */
-	if (ss->vlr_number_len == 1) {
+	if (ss->vlr_name.len <= 1) {
 		LOGPSS(ss, LOGL_ERROR, "Cannot send GSUP message, no VLR number stored for subscriber\n");
 		msgb_free(msg);
 		return -EINVAL;
 	}
 
-	LOGPSS(ss, LOGL_DEBUG, "Tx SS/USSD to VLR %s\n", osmo_quote_str((char *)ss->vlr_number, ss->vlr_number_len));
-	return osmo_gsup_addr_send(gs, ss->vlr_number, ss->vlr_number_len, msg);
+	LOGPSS(ss, LOGL_DEBUG, "Tx SS/USSD to VLR %s\n", osmo_ipa_name_to_str(&ss->vlr_name));
+	return osmo_gsup_send_to_ipa_name(gs, &ss->vlr_name, msg);
 }
 
 static int ss_tx_to_ms(struct ss_session *ss, enum osmo_gsup_message_type gsup_msg_type,
@@ -263,7 +283,7 @@
 
 {
 	struct osmo_gsup_message resp = {0};
-	struct msgb *resp_msg;
+	int rc;
 
 	resp.message_type = gsup_msg_type;
 	OSMO_STRLCPY_ARRAY(resp.imsi, ss->imsi);
@@ -277,12 +297,10 @@
 		resp.ss_info_len = msgb_length(ss_msg);
 	}
 
-	resp_msg = msgb_alloc_headroom(4000, 64, __func__);
-	OSMO_ASSERT(resp_msg);
-	osmo_gsup_encode(resp_msg, &resp);
-	msgb_free(ss_msg);
+	rc = ss_gsup_send_to_ms(ss, g_hlr->gs, &resp);
 
-	return ss_gsup_send(ss, g_hlr->gs, resp_msg);
+	msgb_free(ss_msg);
+	return rc;
 }
 
 #if 0
@@ -297,7 +315,7 @@
 }
 #endif
 
-static int ss_tx_error(struct ss_session *ss, uint8_t invoke_id, uint8_t error_code)
+static int ss_tx_to_ms_error(struct ss_session *ss, uint8_t invoke_id, uint8_t error_code)
 {
 	struct msgb *msg = gsm0480_gen_return_error(invoke_id, error_code);
 	LOGPSS(ss, LOGL_NOTICE, "Tx ReturnError(%u, 0x%02x)\n", invoke_id, error_code);
@@ -305,7 +323,7 @@
 	return ss_tx_to_ms(ss, OSMO_GSUP_MSGT_PROC_SS_RESULT, true, msg);
 }
 
-static int ss_tx_ussd_7bit(struct ss_session *ss, bool final, uint8_t invoke_id, const char *text)
+static int ss_tx_to_ms_ussd_7bit(struct ss_session *ss, bool final, uint8_t invoke_id, const char *text)
 {
 	struct msgb *msg = gsm0480_gen_ussd_resp_7bit(invoke_id, text);
 	LOGPSS(ss, LOGL_INFO, "Tx USSD '%s'\n", text);
@@ -319,7 +337,7 @@
 
 #include <osmocom/hlr/db.h>
 
-static int handle_ussd_own_msisdn(struct osmo_gsup_conn *conn, struct ss_session *ss,
+static int handle_ussd_own_msisdn(struct ss_session *ss,
 				  const struct osmo_gsup_message *gsup, const struct ss_request *req)
 {
 	struct hlr_subscriber subscr;
@@ -333,25 +351,25 @@
 			snprintf(buf, sizeof(buf), "You have no MSISDN!");
 		else
 			snprintf(buf, sizeof(buf), "Your extension is %s", subscr.msisdn);
-		ss_tx_ussd_7bit(ss, true, req->invoke_id, buf);
+		ss_tx_to_ms_ussd_7bit(ss, true, req->invoke_id, buf);
 		break;
 	case -ENOENT:
-		ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_UNKNOWN_SUBSCRIBER);
+		ss_tx_to_ms_error(ss, req->invoke_id, GSM0480_ERR_CODE_UNKNOWN_SUBSCRIBER);
 		break;
 	case -EIO:
 	default:
-		ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_SYSTEM_FAILURE);
+		ss_tx_to_ms_error(ss, req->invoke_id, GSM0480_ERR_CODE_SYSTEM_FAILURE);
 		break;
 	}
 	return 0;
 }
 
-static int handle_ussd_own_imsi(struct osmo_gsup_conn *conn, struct ss_session *ss,
+static int handle_ussd_own_imsi(struct ss_session *ss,
 				const struct osmo_gsup_message *gsup, const struct ss_request *req)
 {
 	char buf[GSM0480_USSD_7BIT_STRING_LEN+1];
 	snprintf(buf, sizeof(buf), "Your IMSI is %s", ss->imsi);
-	ss_tx_ussd_7bit(ss, true, req->invoke_id, buf);
+	ss_tx_to_ms_ussd_7bit(ss, true, req->invoke_id, buf);
 	return 0;
 }
 
@@ -398,37 +416,26 @@
 }
 
 /* is this GSUP connection an EUSE (true) or not (false)? */
-static bool conn_is_euse(struct osmo_gsup_conn *conn)
+static bool peer_name_is_euse(const struct osmo_ipa_name *peer_name)
 {
-	int rc;
-	uint8_t *addr;
-
-	rc = osmo_gsup_conn_ccm_get(conn, &addr, IPAC_IDTAG_SERNR);
-	if (rc <= 5)
+	if (peer_name->len <= 5)
 		return false;
-	if (!strncmp((char *)addr, "EUSE-", 5))
+	if (!strncmp((char *)(peer_name->val), "EUSE-", 5))
 		return true;
 	else
 		return false;
 }
 
-static struct hlr_euse *euse_by_conn(struct osmo_gsup_conn *conn)
+static struct hlr_euse *euse_by_name(const struct osmo_ipa_name *peer_name)
 {
-	int rc;
-	char *addr;
-	struct hlr *hlr = conn->server->priv;
-
-	rc = osmo_gsup_conn_ccm_get(conn, (uint8_t **) &addr, IPAC_IDTAG_SERNR);
-	if (rc <= 5)
-		return NULL;
-	if (strncmp(addr, "EUSE-", 5))
+	if (!peer_name_is_euse(peer_name))
 		return NULL;
 
-	return euse_find(hlr, addr+5);
+	return euse_find(g_hlr, (const char*)(peer_name->val)+5);
 }
 
-static int handle_ss(struct ss_session *ss, const struct osmo_gsup_message *gsup,
-			const struct ss_request *req)
+static int handle_ss(struct ss_session *ss, bool is_euse_originated, const struct osmo_gsup_message *gsup,
+		     const struct ss_request *req)
 {
 	uint8_t comp_type = gsup->ss_info[0];
 
@@ -441,17 +448,16 @@
 	 * we don't handle "structured" SS requests at all.
 	 */
 	LOGPSS(ss, LOGL_NOTICE, "Structured SS requests are not supported, rejecting...\n");
-	ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_FACILITY_NOT_SUPPORTED);
+	ss_tx_to_ms_error(ss, req->invoke_id, GSM0480_ERR_CODE_FACILITY_NOT_SUPPORTED);
 	return -ENOTSUP;
 }
 
 /* Handle a USSD GSUP message for a given SS Session received from VLR or EUSE */
-static int handle_ussd(struct osmo_gsup_conn *conn, struct ss_session *ss,
-			const struct osmo_gsup_message *gsup, const struct ss_request *req)
+static int handle_ussd(struct ss_session *ss, bool is_euse_originated, const struct osmo_gsup_message *gsup,
+		       const struct ss_request *req)
 {
 	uint8_t comp_type = gsup->ss_info[0];
 	struct msgb *msg_out;
-	bool is_euse_originated = conn_is_euse(conn);
 
 	LOGPSS(ss, LOGL_INFO, "USSD CompType=%s, OpCode=%s '%s'\n",
 		gsm0480_comp_type_name(comp_type), gsm0480_op_code_name(req->opcode),
@@ -459,26 +465,27 @@
 
 	if ((ss->is_external && !ss->u.euse) || !ss->u.iuse) {
 		LOGPSS(ss, LOGL_NOTICE, "USSD for unknown code '%s'\n", req->ussd_text);
-		ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_SS_NOT_AVAILABLE);
+		ss_tx_to_ms_error(ss, req->invoke_id, GSM0480_ERR_CODE_SS_NOT_AVAILABLE);
 		return 0;
 	}
 
 	if (is_euse_originated) {
-		msg_out = osmo_gsup_msgb_alloc("GSUP USSD FW");
 		/* Received from EUSE, Forward to VLR */
-		osmo_gsup_encode(msg_out, gsup);
-		ss_gsup_send(ss, conn->server, msg_out);
+		/* Need a non-const osmo_gsup_message, because sending might modify some (routing related?) parts. */
+		struct osmo_gsup_message forward = *gsup;
+		ss_gsup_send_to_ms(ss, g_hlr->gs, &forward);
 	} else {
 		/* Received from VLR (MS) */
 		if (ss->is_external) {
 			/* Forward to EUSE */
-			char addr[128];
-			strcpy(addr, "EUSE-");
-			osmo_strlcpy(addr+5, ss->u.euse->name, sizeof(addr)-5);
-			conn = gsup_route_find(conn->server, (uint8_t *)addr, strlen(addr)+1);
+			struct osmo_ipa_name euse_name;
+			struct osmo_gsup_conn *conn;
+			osmo_ipa_name_set_str(&euse_name, "EUSE-%s", ss->u.euse->name);
+			conn = gsup_route_find_by_ipa_name(g_hlr->gs, &euse_name);
 			if (!conn) {
-				LOGPSS(ss, LOGL_ERROR, "Cannot find conn for EUSE %s\n", addr);
-				ss_tx_error(ss, req->invoke_id, GSM0480_ERR_CODE_SYSTEM_FAILURE);
+				LOGPSS(ss, LOGL_ERROR, "Cannot find conn for EUSE %s\n",
+				       osmo_ipa_name_to_str(&euse_name));
+				ss_tx_to_ms_error(ss, req->invoke_id, GSM0480_ERR_CODE_SYSTEM_FAILURE);
 			} else {
 				msg_out = osmo_gsup_msgb_alloc("GSUP USSD FW");
 				osmo_gsup_encode(msg_out, gsup);
@@ -486,7 +493,7 @@
 			}
 		} else {
 			/* Handle internally */
-			ss->u.iuse->handle_ussd(conn, ss, gsup, req);
+			ss->u.iuse->handle_ussd(ss, gsup, req);
 			/* Release session immediately */
 			ss_session_free(ss);
 		}
@@ -498,12 +505,16 @@
 
 /* this function is called for any SS_REQ/SS_RESP messages from both the MSC/VLR side as well
  * as from the EUSE side */
-int rx_proc_ss_req(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup)
+void rx_proc_ss_req(struct osmo_gsup_req *gsup_req)
 {
-	struct hlr *hlr = conn->server->priv;
+	struct hlr *hlr = g_hlr;
 	struct ss_session *ss;
 	struct ss_request req = {0};
-	struct gsup_route *gsup_rt;
+	const struct osmo_gsup_message *gsup = &gsup_req->gsup;
+	/* Remember whether this function should free the incoming gsup_req: if it is placed as ss->initial_req_from_*,
+	 * do not free it here. If not, free it here. */
+	struct osmo_gsup_req *free_gsup_req = gsup_req;
+	bool is_euse_originated = peer_name_is_euse(&gsup_req->source_name);
 
 	LOGP(DSS, LOGL_DEBUG, "%s/0x%08x: Process SS (%s)\n", gsup->imsi, gsup->session_id,
 		osmo_gsup_session_state_name(gsup->session_state));
@@ -514,14 +525,15 @@
 			LOGP(DSS, LOGL_ERROR, "%s/0x%082x: Unable to parse SS request: %s\n",
 				gsup->imsi, gsup->session_id,
 				osmo_hexdump(gsup->ss_info, gsup->ss_info_len));
-			/* FIXME: Send a Reject component? */
-			goto out_err;
+			osmo_gsup_req_respond_err(gsup_req, GMM_CAUSE_INV_MAND_INFO, "error parsing SS request");
+			return;
 		}
 	} else if (gsup->session_state != OSMO_GSUP_SESSION_STATE_END) {
 		LOGP(DSS, LOGL_ERROR, "%s/0x%082x: Missing SS payload for '%s'\n",
 		     gsup->imsi, gsup->session_id,
 		     osmo_gsup_session_state_name(gsup->session_state));
-		goto out_err;
+		osmo_gsup_req_respond_err(gsup_req, GMM_CAUSE_INV_MAND_INFO, "missing SS payload");
+		return;
 	}
 
 	switch (gsup->session_state) {
@@ -530,32 +542,29 @@
 		if (ss_session_find(hlr, gsup->imsi, gsup->session_id)) {
 			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: BEGIN with non-unique session ID!\n",
 				gsup->imsi, gsup->session_id);
-			goto out_err;
+			osmo_gsup_req_respond_err(gsup_req, GMM_CAUSE_INV_MAND_INFO, "BEGIN with non-unique session ID");
+			return;
 		}
 		ss = ss_session_alloc(hlr, gsup->imsi, gsup->session_id);
 		if (!ss) {
 			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: Unable to allocate SS session\n",
 				gsup->imsi, gsup->session_id);
-			goto out_err;
+			osmo_gsup_req_respond_err(gsup_req, GMM_CAUSE_NET_FAIL, "Unable to allocate SS session");
+			return;
 		}
 		/* Get IPA name from VLR conn and save as ss->vlr_number */
-		if (!conn_is_euse(conn)) {
-			gsup_rt = gsup_route_find_by_conn(conn);
-			if (gsup_rt) {
-				ss->vlr_number = (uint8_t *)talloc_strdup(ss, (const char *)gsup_rt->addr);
-				ss->vlr_number_len = strlen((const char *)gsup_rt->addr) + 1;
-				LOGPSS(ss, LOGL_DEBUG, "Destination IPA name retrieved from GSUP route: %s\n",
-				       osmo_quote_str((const char *)ss->vlr_number, ss->vlr_number_len));
-			} else {
-				LOGPSS(ss, LOGL_NOTICE, "Could not find GSUP route, therefore can't set the destination"
-							" IPA name. We'll try to look it up later, but this should not"
-							" have happened.\n");
-			}
+		if (!is_euse_originated) {
+			ss->initial_req_from_ms = gsup_req;
+			free_gsup_req = NULL;
+			ss->vlr_name = gsup_req->source_name;
+		} else {
+			ss->initial_req_from_euse = gsup_req;
+			free_gsup_req = NULL;
 		}
 		if (ss_op_is_ussd(req.opcode)) {
-			if (conn_is_euse(conn)) {
+			if (is_euse_originated) {
 				/* EUSE->VLR: MT USSD. EUSE is known ('conn'), VLR is to be resolved */
-				ss->u.euse = euse_by_conn(conn);
+				ss->u.euse = euse_by_name(&gsup_req->source_name);
 			} else {
 				/* VLR->EUSE: MO USSD. VLR is known ('conn'), EUSE is to be resolved */
 				struct hlr_ussd_route *rt;
@@ -576,10 +585,10 @@
 				}
 			}
 			/* dispatch unstructured SS to routing */
-			handle_ussd(conn, ss, gsup, &req);
+			handle_ussd(ss, is_euse_originated, &gsup_req->gsup, &req);
 		} else {
 			/* dispatch non-call SS to internal code */
-			handle_ss(ss, gsup, &req);
+			handle_ss(ss, is_euse_originated, &gsup_req->gsup, &req);
 		}
 		break;
 	case OSMO_GSUP_SESSION_STATE_CONTINUE:
@@ -587,7 +596,8 @@
 		if (!ss) {
 			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: CONTINUE for unknown SS session\n",
 				gsup->imsi, gsup->session_id);
-			goto out_err;
+			osmo_gsup_req_respond_err(gsup_req, GMM_CAUSE_INV_MAND_INFO, "CONTINUE for unknown SS session");
+			return;
 		}
 
 		/* Reschedule self-destruction timer */
@@ -596,10 +606,10 @@
 
 		if (ss_op_is_ussd(req.opcode)) {
 			/* dispatch unstructured SS to routing */
-			handle_ussd(conn, ss, gsup, &req);
+			handle_ussd(ss, is_euse_originated, &gsup_req->gsup, &req);
 		} else {
 			/* dispatch non-call SS to internal code */
-			handle_ss(ss, gsup, &req);
+			handle_ss(ss, is_euse_originated, &gsup_req->gsup, &req);
 		}
 		break;
 	case OSMO_GSUP_SESSION_STATE_END:
@@ -607,17 +617,17 @@
 		if (!ss) {
 			LOGP(DSS, LOGL_ERROR, "%s/0x%08x: END for unknown SS session\n",
 				gsup->imsi, gsup->session_id);
-			goto out_err;
+			return;
 		}
 
 		/* SS payload is optional for END */
 		if (gsup->ss_info && gsup->ss_info_len) {
 			if (ss_op_is_ussd(req.opcode)) {
 				/* dispatch unstructured SS to routing */
-				handle_ussd(conn, ss, gsup, &req);
+				handle_ussd(ss, is_euse_originated, &gsup_req->gsup, &req);
 			} else {
 				/* dispatch non-call SS to internal code */
-				handle_ss(ss, gsup, &req);
+				handle_ss(ss, is_euse_originated, &gsup_req->gsup, &req);
 			}
 		}
 
@@ -626,18 +636,15 @@
 	default:
 		LOGP(DSS, LOGL_ERROR, "%s/0x%08x: Unknown SS State %d\n", gsup->imsi,
 			gsup->session_id, gsup->session_state);
-		goto out_err;
+		break;
 	}
 
-	return 0;
-
-out_err:
-	return 0;
+	if (free_gsup_req)
+		osmo_gsup_req_free(free_gsup_req);
 }
 
-int rx_proc_ss_error(struct osmo_gsup_conn *conn, const struct osmo_gsup_message *gsup)
+void rx_proc_ss_error(struct osmo_gsup_req *req)
 {
-	LOGP(DSS, LOGL_NOTICE, "%s/0x%08x: Process SS ERROR (%s)\n", gsup->imsi, gsup->session_id,
-		osmo_gsup_session_state_name(gsup->session_state));
-	return 0;
+	LOGP(DSS, LOGL_NOTICE, "%s/0x%08x: Process SS ERROR (%s)\n", req->gsup.imsi, req->gsup.session_id,
+		osmo_gsup_session_state_name(req->gsup.session_state));
 }
diff --git a/src/logging.c b/src/logging.c
index d0b79cf..15ef596 100644
--- a/src/logging.c
+++ b/src/logging.c
@@ -31,6 +31,12 @@
 		.color = "\033[1;35m",
 		.enabled = 1, .loglevel = LOGL_NOTICE,
 	},
+	[DLU] = {
+		.name = "DLU",
+		.description = "Location Updating",
+		.color = "\033[1;33m",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
 };
 
 const struct log_info hlr_log_info = {
diff --git a/src/lu_fsm.c b/src/lu_fsm.c
new file mode 100644
index 0000000..bded4ef
--- /dev/null
+++ b/src/lu_fsm.c
@@ -0,0 +1,308 @@
+/* Roughly following "Process Update_Location_HLR" of TS 09.02 */
+
+/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/tdef.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/gsm/apn.h>
+#include <osmocom/gsm/gsm48_ie.h>
+
+#include <osmocom/gsupclient/ipa_name.h>
+#include <osmocom/gsupclient/gsup_req.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/hlr/hlr.h>
+#include <osmocom/hlr/gsup_server.h>
+
+#include <osmocom/hlr/db.h>
+
+#define LOG_LU(lu, level, fmt, args...) \
+	LOGPFSML((lu)? (lu)->fi : NULL, level, fmt, ##args)
+
+#define LOG_LU_REQ(lu, req, level, fmt, args...) \
+	LOGPFSML((lu)? (lu)->fi : NULL, level, "%s:" fmt, \
+		 osmo_gsup_message_type_name((req)->gsup.message_type), ##args)
+
+struct lu {
+	struct llist_head entry;
+	struct osmo_fsm_inst *fi;
+
+	struct osmo_gsup_req *update_location_req;
+
+	/* Subscriber state at time of initial Update Location Request */
+	struct hlr_subscriber subscr;
+	bool is_ps;
+
+	/* VLR requesting the LU. */
+	struct osmo_ipa_name vlr_name;
+
+	/* If the LU request was received via a proxy and not immediately from a local VLR, this indicates the closest
+	 * peer that forwarded the GSUP message. */
+	struct osmo_ipa_name via_proxy;
+};
+LLIST_HEAD(g_all_lu);
+
+enum lu_fsm_event {
+	LU_EV_RX_GSUP,
+};
+
+enum lu_fsm_state {
+	LU_ST_UNVALIDATED,
+	LU_ST_WAIT_INSERT_DATA_RESULT,
+	LU_ST_WAIT_LOCATION_CANCEL_RESULT,
+};
+
+static const struct value_string lu_fsm_event_names[] = {
+	OSMO_VALUE_STRING(LU_EV_RX_GSUP),
+	{}
+};
+
+static struct osmo_tdef_state_timeout lu_fsm_timeouts[32] = {
+	[LU_ST_WAIT_INSERT_DATA_RESULT] = { .T = -4222 },
+	[LU_ST_WAIT_LOCATION_CANCEL_RESULT] = { .T = -4222 },
+};
+
+#define lu_state_chg(lu, state) \
+	osmo_tdef_fsm_inst_state_chg((lu)->fi, state, lu_fsm_timeouts, g_hlr_tdefs, 5)
+
+static void lu_success(struct lu *lu)
+{
+	if (!lu->update_location_req)
+		LOG_LU(lu, LOGL_ERROR, "No request for this LU\n");
+	else
+		osmo_gsup_req_respond_msgt(lu->update_location_req, OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT, true);
+	lu->update_location_req = NULL;
+	osmo_fsm_inst_term(lu->fi, OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+#define lu_failure(LU, CAUSE, log_msg, args...) do { \
+		if (!(LU)->update_location_req) \
+			LOG_LU(LU, LOGL_ERROR, "No request for this LU\n"); \
+		else \
+			osmo_gsup_req_respond_err((LU)->update_location_req, CAUSE, log_msg, ##args); \
+		(LU)->update_location_req = NULL; \
+		osmo_fsm_inst_term((LU)->fi, OSMO_FSM_TERM_REGULAR, NULL); \
+	} while(0)
+
+static struct osmo_fsm lu_fsm;
+
+static void lu_start(struct osmo_gsup_req *update_location_req)
+{
+	struct osmo_fsm_inst *fi;
+	struct lu *lu;
+
+	OSMO_ASSERT(update_location_req);
+	OSMO_ASSERT(update_location_req->gsup.message_type == OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST);
+
+	fi = osmo_fsm_inst_alloc(&lu_fsm, g_hlr, NULL, LOGL_DEBUG, update_location_req->gsup.imsi);
+	OSMO_ASSERT(fi);
+
+	lu = talloc(fi, struct lu);
+	OSMO_ASSERT(lu);
+	fi->priv = lu;
+	*lu = (struct lu){
+		.fi = fi,
+		.update_location_req = update_location_req,
+		.vlr_name = update_location_req->source_name,
+		.via_proxy = update_location_req->via_proxy,
+		/* According to GSUP specs, OSMO_GSUP_CN_DOMAIN_PS is the default. */
+		.is_ps = (update_location_req->gsup.cn_domain != OSMO_GSUP_CN_DOMAIN_CS),
+	};
+	llist_add(&lu->entry, &g_all_lu);
+
+	osmo_fsm_inst_update_id_f_sanitize(fi, '_', "%s:IMSI-%s", lu->is_ps ? "PS" : "CS", update_location_req->gsup.imsi);
+
+	if (!lu->vlr_name.len) {
+		lu_failure(lu, GMM_CAUSE_NET_FAIL, "LU without a VLR");
+		return;
+	}
+
+	if (db_subscr_get_by_imsi(g_hlr->dbc, update_location_req->gsup.imsi, &lu->subscr) < 0) {
+		lu_failure(lu, GMM_CAUSE_IMSI_UNKNOWN, "Subscriber does not exist");
+		return;
+	}
+
+	/* Check if subscriber is generally permitted on CS or PS
+	 * service (as requested) */
+	if (!lu->is_ps && !lu->subscr.nam_cs) {
+		lu_failure(lu, GMM_CAUSE_PLMN_NOTALLOWED, "nam_cs == false");
+		return;
+	}
+	if (lu->is_ps && !lu->subscr.nam_ps) {
+		lu_failure(lu, GMM_CAUSE_GPRS_NOTALLOWED, "nam_ps == false");
+		return;
+	}
+
+	/* TODO: Set subscriber tracing = deactive in VLR/SGSN */
+
+#if 0
+	/* Cancel in old VLR/SGSN, if new VLR/SGSN differs from old (FIXME: OS#4491) */
+	if (!lu->is_ps && strcmp(subscr->vlr_number, vlr_number)) {
+		lu_op_tx_cancel_old(lu);
+	} else if (lu->is_ps && strcmp(subscr->sgsn_number, sgsn_number)) {
+		lu_op_tx_cancel_old(lu);
+	}
+#endif
+
+	/* Store the VLR / SGSN number with the subscriber, so we know where it was last seen. */
+	if (lu->via_proxy.len) {
+		LOG_GSUP_REQ(update_location_req, LOGL_DEBUG, "storing %s = %s, via proxy %s\n",
+			     lu->is_ps ? "SGSN number" : "VLR number",
+			     osmo_ipa_name_to_str(&lu->vlr_name),
+			     osmo_ipa_name_to_str(&lu->via_proxy));
+	} else {
+		LOG_GSUP_REQ(update_location_req, LOGL_DEBUG, "storing %s = %s\n",
+		     lu->is_ps ? "SGSN number" : "VLR number",
+		     osmo_ipa_name_to_str(&lu->vlr_name));
+	}
+
+	if (db_subscr_lu(g_hlr->dbc, lu->subscr.id, &lu->vlr_name, lu->is_ps, &lu->via_proxy)) {
+		lu_failure(lu, GMM_CAUSE_NET_FAIL, "Cannot update %s in the database",
+			   lu->is_ps ? "SGSN number" : "VLR number");
+		return;
+	}
+
+	/* TODO: Subscriber allowed to roam in PLMN? */
+	/* TODO: Update RoutingInfo */
+	/* TODO: Reset Flag MS Purged (cs/ps) */
+	/* TODO: Control_Tracing_HLR / Control_Tracing_HLR_with_SGSN */
+
+	lu_state_chg(lu, LU_ST_WAIT_INSERT_DATA_RESULT);
+}
+
+void lu_rx_gsup(struct osmo_gsup_req *req)
+{
+	struct lu *lu;
+	if (req->gsup.message_type == OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST)
+		return lu_start(req);
+
+	llist_for_each_entry(lu, &g_all_lu, entry) {
+		if (strcmp(lu->subscr.imsi, req->gsup.imsi))
+			continue;
+		if (osmo_fsm_inst_dispatch(lu->fi, LU_EV_RX_GSUP, req)) {
+			LOG_LU_REQ(lu, req, LOGL_ERROR, "Cannot receive GSUP messages in this state\n");
+			osmo_gsup_req_respond_err(req, GMM_CAUSE_MSGT_INCOMP_P_STATE,
+						  "LU does not accept GSUP rx");
+		}
+		return;
+	}
+	osmo_gsup_req_respond_err(req, GMM_CAUSE_MSGT_INCOMP_P_STATE, "No Location Updating in progress for this IMSI");
+}
+
+static int lu_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct lu *lu = fi->priv;
+	lu_failure(lu, GSM_CAUSE_NET_FAIL, "Timeout");
+	return 0;
+}
+
+static void lu_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+	struct lu *lu = fi->priv;
+	if (lu->update_location_req)
+		osmo_gsup_req_respond_err(lu->update_location_req, GSM_CAUSE_NET_FAIL, "LU aborted");
+	lu->update_location_req = NULL;
+	llist_del(&lu->entry);
+}
+
+static void lu_fsm_wait_insert_data_result_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	/* Transmit Insert Data Request to the VLR */
+	struct lu *lu = fi->priv;
+	struct hlr_subscriber *subscr = &lu->subscr;
+	struct osmo_gsup_message gsup;
+	uint8_t msisdn_enc[OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN];
+	uint8_t apn[APN_MAXLEN];
+
+	if (osmo_gsup_create_insert_subscriber_data_msg(&gsup, subscr->imsi,
+							subscr->msisdn, msisdn_enc, sizeof(msisdn_enc),
+							apn, sizeof(apn),
+							lu->is_ps? OSMO_GSUP_CN_DOMAIN_PS : OSMO_GSUP_CN_DOMAIN_CS)) {
+		lu_failure(lu, GMM_CAUSE_NET_FAIL, "cannot encode Insert Subscriber Data message");
+		return;
+	}
+
+	if (osmo_gsup_req_respond(lu->update_location_req, &gsup, false, false))
+		lu_failure(lu, GMM_CAUSE_NET_FAIL, "cannot send %s", osmo_gsup_message_type_name(gsup.message_type));
+}
+
+void lu_fsm_wait_insert_data_result(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct lu *lu = fi->priv;
+	struct osmo_gsup_req *req;
+
+	switch (event) {
+	case LU_EV_RX_GSUP:
+		req = data;
+		break;
+	default:
+		OSMO_ASSERT(false);
+	}
+
+	switch (req->gsup.message_type) {
+	case OSMO_GSUP_MSGT_INSERT_DATA_RESULT:
+		osmo_gsup_req_free(req);
+		lu_success(lu);
+		break;
+
+	case OSMO_GSUP_MSGT_INSERT_DATA_ERROR:
+		lu_failure(lu, GMM_CAUSE_NET_FAIL, "Rx %s", osmo_gsup_message_type_name(req->gsup.message_type));
+		break;
+
+	default:
+		osmo_gsup_req_respond_err(req, GMM_CAUSE_MSGT_INCOMP_P_STATE, "unexpected message type in this state");
+		break;
+	}
+}
+
+#define S(x) (1 << (x))
+
+static const struct osmo_fsm_state lu_fsm_states[] = {
+	[LU_ST_UNVALIDATED] = {
+		.name = "UNVALIDATED",
+		.out_state_mask = 0
+			| S(LU_ST_WAIT_INSERT_DATA_RESULT)
+			,
+	},
+	[LU_ST_WAIT_INSERT_DATA_RESULT] = {
+		.name = "WAIT_INSERT_DATA_RESULT",
+		.in_event_mask = 0
+			| S(LU_EV_RX_GSUP)
+			,
+		.onenter = lu_fsm_wait_insert_data_result_onenter,
+		.action = lu_fsm_wait_insert_data_result,
+	},
+};
+
+static struct osmo_fsm lu_fsm = {
+	.name = "lu",
+	.states = lu_fsm_states,
+	.num_states = ARRAY_SIZE(lu_fsm_states),
+	.log_subsys = DLU,
+	.event_names = lu_fsm_event_names,
+	.timer_cb = lu_fsm_timer_cb,
+	.cleanup = lu_fsm_cleanup,
+};
+
+static __attribute__((constructor)) void lu_fsm_init()
+{
+	OSMO_ASSERT(osmo_fsm_register(&lu_fsm) == 0);
+}
diff --git a/src/luop.c b/src/luop.c
deleted file mode 100644
index e63ba91..0000000
--- a/src/luop.c
+++ /dev/null
@@ -1,258 +0,0 @@
-/* OsmoHLR TX/RX lu operations */
-
-/* (C) 2017 sysmocom s.f.m.c. GmbH <info at sysmocom.de>
- * All Rights Reserved
- *
- * Author: Harald Welte <laforge at gnumonks.org>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include <stdbool.h>
-#include <string.h>
-#include <errno.h>
-
-#include <osmocom/core/logging.h>
-#include <osmocom/gsm/gsup.h>
-#include <osmocom/gsm/apn.h>
-
-#include <osmocom/hlr/gsup_server.h>
-#include <osmocom/hlr/gsup_router.h>
-#include <osmocom/hlr/logging.h>
-#include <osmocom/hlr/luop.h>
-
-const struct value_string lu_state_names[] = {
-	{ LU_S_NULL,			"NULL" },
-	{ LU_S_LU_RECEIVED,		"LU RECEIVED" },
-	{ LU_S_CANCEL_SENT,		"CANCEL SENT" },
-	{ LU_S_CANCEL_ACK_RECEIVED,	"CANCEL-ACK RECEIVED" },
-	{ LU_S_ISD_SENT,		"ISD SENT" },
-	{ LU_S_ISD_ACK_RECEIVED,	"ISD-ACK RECEIVED" },
-	{ LU_S_COMPLETE,		"COMPLETE" },
-	{ 0, NULL }
-};
-
-/* Transmit a given GSUP message for the given LU operation */
-static void _luop_tx_gsup(struct lu_operation *luop,
-			  const struct osmo_gsup_message *gsup)
-{
-	struct msgb *msg_out;
-
-	msg_out = osmo_gsup_msgb_alloc("GSUP LUOP");
-	osmo_gsup_encode(msg_out, gsup);
-
-	osmo_gsup_addr_send(luop->gsup_server, luop->peer,
-			    talloc_total_size(luop->peer),
-			    msg_out);
-}
-
-static inline void fill_gsup_msg(struct osmo_gsup_message *out,
-				 const struct lu_operation *lu,
-				 enum osmo_gsup_message_type mt)
-{
-	memset(out, 0, sizeof(struct osmo_gsup_message));
-	if (lu)
-		osmo_strlcpy(out->imsi, lu->subscr.imsi,
-			     GSM23003_IMSI_MAX_DIGITS + 1);
-	out->message_type = mt;
-}
-
-/* timer call-back in case LU operation doesn't receive an response */
-static void lu_op_timer_cb(void *data)
-{
-	struct lu_operation *luop = data;
-
-	DEBUGP(DMAIN, "LU OP timer expired in state %s\n",
-		get_value_string(lu_state_names, luop->state));
-
-	switch (luop->state) {
-	case LU_S_CANCEL_SENT:
-		break;
-	case LU_S_ISD_SENT:
-		break;
-	default:
-		break;
-	}
-
-	lu_op_tx_error(luop, GMM_CAUSE_NET_FAIL);
-}
-
-bool lu_op_fill_subscr(struct lu_operation *luop, struct db_context *dbc,
-		       const char *imsi)
-{
-	struct hlr_subscriber *subscr = &luop->subscr;
-
-	if (db_subscr_get_by_imsi(dbc, imsi, subscr) < 0)
-		return false;
-
-	return true;
-}
-
-struct lu_operation *lu_op_alloc(struct osmo_gsup_server *srv)
-{
-	struct lu_operation *luop;
-
-	luop = talloc_zero(srv, struct lu_operation);
-	OSMO_ASSERT(luop);
-	luop->gsup_server = srv;
-	osmo_timer_setup(&luop->timer, lu_op_timer_cb, luop);
-
-	return luop;
-}
-
-void lu_op_free(struct lu_operation *luop)
-{
-	/* Only attempt to remove when it was ever added to a list. */
-	if (luop->list.next)
-		llist_del(&luop->list);
-
-	/* Delete timer just in case it is still pending. */
-	osmo_timer_del(&luop->timer);
-
-	talloc_free(luop);
-}
-
-struct lu_operation *lu_op_alloc_conn(struct osmo_gsup_conn *conn)
-{
-	uint8_t *peer_addr;
-	struct lu_operation *luop = lu_op_alloc(conn->server);
-	int rc = osmo_gsup_conn_ccm_get(conn, &peer_addr, IPAC_IDTAG_SERNR);
-	if (rc < 0) {
-		lu_op_free(luop);
-		return NULL;
-	}
-
-	luop->peer = talloc_memdup(luop, peer_addr, rc);
-
-	return luop;
-}
-
-/* FIXME: this doesn't seem to work at all */
-struct lu_operation *lu_op_by_imsi(const char *imsi,
-				   const struct llist_head *lst)
-{
-	struct lu_operation *luop;
-
-	llist_for_each_entry(luop, lst, list) {
-		if (!strcmp(imsi, luop->subscr.imsi))
-			return luop;
-	}
-	return NULL;
-}
-
-void lu_op_statechg(struct lu_operation *luop, enum lu_state new_state)
-{
-	enum lu_state old_state = luop->state;
-
-	DEBUGP(DMAIN, "LU OP state change: %s -> ",
-		get_value_string(lu_state_names, old_state));
-	DEBUGPC(DMAIN, "%s\n",
-		get_value_string(lu_state_names, new_state));
-
-	luop->state = new_state;
-}
-
-/*! Transmit UPD_LOC_ERROR and destroy lu_operation */
-void lu_op_tx_error(struct lu_operation *luop, enum gsm48_gmm_cause cause)
-{
-	struct osmo_gsup_message gsup;
-
-	DEBUGP(DMAIN, "%s: LU OP Tx Error (cause %s)\n",
-	       luop->subscr.imsi, get_value_string(gsm48_gmm_cause_names,
-						   cause));
-
-	fill_gsup_msg(&gsup, luop, OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR);
-	gsup.cause = cause;
-
-	_luop_tx_gsup(luop, &gsup);
-
-	lu_op_free(luop);
-}
-
-/*! Transmit UPD_LOC_RESULT and destroy lu_operation */
-void lu_op_tx_ack(struct lu_operation *luop)
-{
-	struct osmo_gsup_message gsup;
-
-	fill_gsup_msg(&gsup, luop, OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT);
-	//FIXME gsup.hlr_enc;
-
-	_luop_tx_gsup(luop, &gsup);
-
-	lu_op_free(luop);
-}
-
-/*! Send Cancel Location to old VLR/SGSN (FIXME: OS#4491) */
-void lu_op_tx_cancel_old(struct lu_operation *luop)
-{
-	struct osmo_gsup_message gsup;
-
-	OSMO_ASSERT(luop->state == LU_S_LU_RECEIVED);
-
-	fill_gsup_msg(&gsup, NULL, OSMO_GSUP_MSGT_LOCATION_CANCEL_REQUEST);
-	//gsup.cause = FIXME;
-	//gsup.cancel_type = FIXME;
-
-	_luop_tx_gsup(luop, &gsup);
-
-	lu_op_statechg(luop, LU_S_CANCEL_SENT);
-	osmo_timer_schedule(&luop->timer, CANCEL_TIMEOUT_SECS, 0);
-}
-
-/*! Transmit Insert Subscriber Data to new VLR/SGSN */
-void lu_op_tx_insert_subscr_data(struct lu_operation *luop)
-{
-	struct hlr_subscriber *subscr = &luop->subscr;
-	struct osmo_gsup_message gsup = { };
-	uint8_t msisdn_enc[OSMO_GSUP_MAX_CALLED_PARTY_BCD_LEN];
-	uint8_t apn[APN_MAXLEN];
-	enum osmo_gsup_cn_domain cn_domain;
-
-	OSMO_ASSERT(luop->state == LU_S_LU_RECEIVED ||
-		    luop->state == LU_S_CANCEL_ACK_RECEIVED);
-
-	if (luop->is_ps)
-		cn_domain = OSMO_GSUP_CN_DOMAIN_PS;
-	else
-		cn_domain = OSMO_GSUP_CN_DOMAIN_CS;
-
-	if (osmo_gsup_create_insert_subscriber_data_msg(&gsup, subscr->imsi, subscr->msisdn, msisdn_enc,
-							sizeof(msisdn_enc), apn, sizeof(apn), cn_domain) != 0) {
-		LOGP(DMAIN, LOGL_ERROR,
-		       "IMSI='%s': Cannot notify GSUP client; could not create gsup message "
-		       "for %s\n", subscr->imsi, luop->peer);
-		return;
-	}
-
-	/* Send ISD to new VLR/SGSN */
-	_luop_tx_gsup(luop, &gsup);
-
-	lu_op_statechg(luop, LU_S_ISD_SENT);
-	osmo_timer_schedule(&luop->timer, ISD_TIMEOUT_SECS, 0);
-}
-
-/*! Transmit Delete Subscriber Data to new VLR/SGSN.
- * The luop is not freed. */
-void lu_op_tx_del_subscr_data(struct lu_operation *luop)
-{
-	struct osmo_gsup_message gsup;
-
-	fill_gsup_msg(&gsup, luop, OSMO_GSUP_MSGT_DELETE_DATA_REQUEST);
-
-	gsup.cn_domain = OSMO_GSUP_CN_DOMAIN_PS;
-
-	/* Send ISD to new VLR/SGSN */
-	_luop_tx_gsup(luop, &gsup);
-}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 776f8a9..9015494 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -2,6 +2,7 @@
 	auc \
 	gsup_server \
 	db \
+	gsup \
 	db_upgrade \
 	mslookup \
 	$(NULL)
diff --git a/tests/db/Makefile.am b/tests/db/Makefile.am
index f13824d..0194957 100644
--- a/tests/db/Makefile.am
+++ b/tests/db/Makefile.am
@@ -30,6 +30,7 @@
 	$(top_builddir)/src/db_auc.o \
 	$(top_builddir)/src/db_hlr.o \
 	$(top_builddir)/src/db.o \
+	$(top_builddir)/src/ipa_name.o \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOABIS_LIBS) \
diff --git a/tests/db/db_test.c b/tests/db/db_test.c
index b9b263d..c53baed 100644
--- a/tests/db/db_test.c
+++ b/tests/db/db_test.c
@@ -27,6 +27,7 @@
 #include <osmocom/core/utils.h>
 #include <osmocom/core/logging.h>
 
+#include <osmocom/gsupclient/ipa_name.h>
 #include <osmocom/hlr/db.h>
 #include <osmocom/hlr/logging.h>
 
@@ -145,6 +146,8 @@
 #define Ps(name) \
 	if (*subscr->name) \
 		Pfo(name, "'%s'", subscr)
+#define Pgt(name) \
+	Pfv(name, "%s", osmo_ipa_name_to_str(&subscr->name))
 #define Pd(name) \
 	Pfv(name, "%"PRId64, (int64_t)subscr->name)
 #define Pd_nonzero(name) \
@@ -235,6 +238,14 @@
 static const char *short_imsi = "123456";
 static const char *unknown_imsi = "999999999";
 
+static int db_subscr_lu_str(struct db_context *dbc, int64_t subscr_id,
+			    const char *vlr_or_sgsn_number, bool is_ps)
+{
+	struct osmo_ipa_name vlr_nr;
+	osmo_ipa_name_set_str(&vlr_nr, vlr_or_sgsn_number);
+	return db_subscr_lu(dbc, subscr_id, &vlr_nr, is_ps, NULL);
+}
+
 static void test_subscr_create_update_sel_delete()
 {
 	int64_t id0, id1, id2, id_short;
@@ -386,39 +397,39 @@
 
 	comment("Record LU for PS and CS (SGSN and VLR names)");
 
-	ASSERT_RC(db_subscr_lu(dbc, id0, "5952", true), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "5952", true), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, "712", false), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "712", false), 0);
 	ASSERT_SEL(id, id0, 0);
 
 	comment("Record LU for PS and CS (SGSN and VLR names) *again*");
 
-	ASSERT_RC(db_subscr_lu(dbc, id0, "111", true), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "111", true), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, "111", true), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "111", true), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, "222", false), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "222", false), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, "222", false), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "222", false), 0);
 	ASSERT_SEL(id, id0, 0);
 
 	comment("Unset LU info for PS and CS (SGSN and VLR names)");
-	ASSERT_RC(db_subscr_lu(dbc, id0, "", true), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "", true), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, "", false), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "", false), 0);
 	ASSERT_SEL(id, id0, 0);
 
-	ASSERT_RC(db_subscr_lu(dbc, id0, "111", true), 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, "222", false), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "111", true), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, "222", false), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, NULL, true), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, NULL, true), 0);
 	ASSERT_SEL(id, id0, 0);
-	ASSERT_RC(db_subscr_lu(dbc, id0, NULL, false), 0);
+	ASSERT_RC(db_subscr_lu_str(dbc, id0, NULL, false), 0);
 	ASSERT_SEL(id, id0, 0);
 
 	comment("Record LU for non-existent ID");
-	ASSERT_RC(db_subscr_lu(dbc, 99999, "5952", true), -ENOENT);
-	ASSERT_RC(db_subscr_lu(dbc, 99999, "712", false), -ENOENT);
+	ASSERT_RC(db_subscr_lu_str(dbc, 99999, "5952", true), -ENOENT);
+	ASSERT_RC(db_subscr_lu_str(dbc, 99999, "712", false), -ENOENT);
 	ASSERT_SEL(id, 99999, -ENOENT);
 
 	comment("Purge and un-purge PS and CS");
diff --git a/tests/db/db_test.err b/tests/db/db_test.err
index 6423550..e4d43c9 100644
--- a/tests/db/db_test.err
+++ b/tests/db/db_test.err
@@ -435,7 +435,7 @@
 
 --- Record LU for PS and CS (SGSN and VLR names)
 
-db_subscr_lu(dbc, id0, "5952", true) --> 0
+db_subscr_lu_str(dbc, id0, "5952", true) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -445,7 +445,7 @@
   .sgsn_number = '5952',
 }
 
-db_subscr_lu(dbc, id0, "712", false) --> 0
+db_subscr_lu_str(dbc, id0, "712", false) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -459,7 +459,7 @@
 
 --- Record LU for PS and CS (SGSN and VLR names) *again*
 
-db_subscr_lu(dbc, id0, "111", true) --> 0
+db_subscr_lu_str(dbc, id0, "111", true) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -470,7 +470,7 @@
   .sgsn_number = '111',
 }
 
-db_subscr_lu(dbc, id0, "111", true) --> 0
+db_subscr_lu_str(dbc, id0, "111", true) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -481,7 +481,7 @@
   .sgsn_number = '111',
 }
 
-db_subscr_lu(dbc, id0, "222", false) --> 0
+db_subscr_lu_str(dbc, id0, "222", false) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -492,7 +492,7 @@
   .sgsn_number = '111',
 }
 
-db_subscr_lu(dbc, id0, "222", false) --> 0
+db_subscr_lu_str(dbc, id0, "222", false) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -506,7 +506,7 @@
 
 --- Unset LU info for PS and CS (SGSN and VLR names)
 
-db_subscr_lu(dbc, id0, "", true) --> 0
+db_subscr_lu_str(dbc, id0, "", true) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -516,7 +516,7 @@
   .vlr_number = '222',
 }
 
-db_subscr_lu(dbc, id0, "", false) --> 0
+db_subscr_lu_str(dbc, id0, "", false) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -525,9 +525,9 @@
   .msisdn = '543210123456789',
 }
 
-db_subscr_lu(dbc, id0, "111", true) --> 0
+db_subscr_lu_str(dbc, id0, "111", true) --> 0
 
-db_subscr_lu(dbc, id0, "222", false) --> 0
+db_subscr_lu_str(dbc, id0, "222", false) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -538,7 +538,7 @@
   .sgsn_number = '111',
 }
 
-db_subscr_lu(dbc, id0, NULL, true) --> 0
+db_subscr_lu_str(dbc, id0, NULL, true) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -548,7 +548,7 @@
   .vlr_number = '222',
 }
 
-db_subscr_lu(dbc, id0, NULL, false) --> 0
+db_subscr_lu_str(dbc, id0, NULL, false) --> 0
 
 db_subscr_get_by_id(dbc, id0, &g_subscr) --> 0
 struct hlr_subscriber {
@@ -560,10 +560,10 @@
 
 --- Record LU for non-existent ID
 
-db_subscr_lu(dbc, 99999, "5952", true) --> -ENOENT
+db_subscr_lu_str(dbc, 99999, "5952", true) --> -ENOENT
 DAUC Cannot update SGSN number for subscriber ID=99999: no such subscriber
 
-db_subscr_lu(dbc, 99999, "712", false) --> -ENOENT
+db_subscr_lu_str(dbc, 99999, "712", false) --> -ENOENT
 DAUC Cannot update VLR number for subscriber ID=99999: no such subscriber
 
 db_subscr_get_by_id(dbc, 99999, &g_subscr) --> -ENOENT
diff --git a/tests/gsup/Makefile.am b/tests/gsup/Makefile.am
new file mode 100644
index 0000000..5dbb180
--- /dev/null
+++ b/tests/gsup/Makefile.am
@@ -0,0 +1,36 @@
+AM_CPPFLAGS = \
+	$(all_includes) \
+	$(NULL)
+
+AM_CFLAGS = \
+	-I$(top_srcdir)/include \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = \
+	-no-install \
+	$(NULL)
+
+EXTRA_DIST = \
+	gsup_test.ok \
+	gsup_test.err \
+	$(NULL)
+
+noinst_PROGRAMS = \
+	gsup_test \
+	$(NULL)
+
+gsup_test_SOURCES = \
+	gsup_test.c \
+	$(NULL)
+
+gsup_test_LDADD = \
+	$(top_builddir)/src/gsupclient/libosmo-gsup-client.la \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+.PHONY: update_exp
+update_exp:
+	$(builddir)/gsup_test >"$(srcdir)/gsup_test.ok" 2>"$(srcdir)/gsup_test.err"
diff --git a/tests/gsup/gsup_test.c b/tests/gsup/gsup_test.c
new file mode 100644
index 0000000..1a7bb76
--- /dev/null
+++ b/tests/gsup/gsup_test.c
@@ -0,0 +1,113 @@
+/* (C) 2018 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <string.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/application.h>
+#include <osmocom/gsupclient/gsup_req.h>
+
+void *ctx = NULL;
+
+static void test_gsup_make_response(void)
+{
+	char *source_name = "incoming-source-name";
+	char *destination_name = "preset-destination-name";
+	uint8_t sm_rp_mr = 23;
+	uint8_t other_sm_rp_mr = 17;
+	struct osmo_gsup_message rx = {
+		.message_type = OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST,
+		.imsi = "1234567",
+		.message_class = OSMO_GSUP_MESSAGE_CLASS_SUBSCRIBER_MANAGEMENT,
+		.source_name = (uint8_t*)source_name,
+		.source_name_len = strlen(source_name) + 1,
+		.sm_rp_mr = &sm_rp_mr,
+		.session_id = 42,
+		.session_state = OSMO_GSUP_SESSION_STATE_BEGIN,
+	};
+	struct osmo_gsup_message nonempty = {
+		.message_type = OSMO_GSUP_MSGT_ROUTING_ERROR,
+		.imsi = "987654321",
+		.message_class = OSMO_GSUP_MESSAGE_CLASS_INTER_MSC,
+		.destination_name = (uint8_t*)destination_name,
+		.destination_name_len = strlen(destination_name) + 1,
+		.sm_rp_mr = &other_sm_rp_mr,
+		.session_id = 11,
+		.session_state = OSMO_GSUP_SESSION_STATE_END,
+	};
+	void *name_ctx = talloc_named_const(ctx, 0, __func__);
+	int error;
+	int final;
+	char *nonempty_str;
+	int rc;
+
+	printf("\n%s()\n", __func__);
+	printf("rx = %s\n", osmo_gsup_message_to_str_c(name_ctx, &rx));
+
+	printf("\nwriting to an empty struct osmo_gsup_message should populate values as needed:\n");
+	for (error = 0; error <= 1; error++) {
+		for (final = 0; final <= 1; final++) {
+			struct osmo_gsup_message target = {};
+			printf("- args (error=%d, final=%d)\n", error, final);
+			rc = osmo_gsup_make_response(&target, &rx, error, final);
+			printf("  %s\n", osmo_gsup_message_to_str_c(name_ctx, &target));
+			printf("  rc = %d\n", rc);
+		}
+	}
+
+	printf("\nwriting to an already populated struct osmo_gsup_message, should have no effect:\n");
+	nonempty_str = osmo_gsup_message_to_str_c(name_ctx, &nonempty);
+	for (error = 0; error <= 1; error++) {
+		for (final = 0; final <= 1; final++) {
+			struct osmo_gsup_message target = nonempty;
+			char *result;
+			printf("- args (error=%d, final=%d)\n", error, final);
+			rc = osmo_gsup_make_response(&target, &rx, error, final);
+			result = osmo_gsup_message_to_str_c(name_ctx, &target);
+			printf("  %s\n", result);
+			if (strcmp(result, nonempty_str))
+				printf("  ERROR: expected: %s\n", nonempty_str);
+			printf("  rc = %d\n", rc);
+		}
+	}
+}
+
+const struct log_info_cat default_categories[] = {
+};
+
+static struct log_info info = {
+	.cat = default_categories,
+	.num_cat = ARRAY_SIZE(default_categories),
+};
+
+int main(int argc, char **argv)
+{
+	ctx = talloc_named_const(NULL, 0, "gsup_test");
+	osmo_init_logging2(ctx, &info);
+	log_set_print_filename(osmo_stderr_target, 0);
+	log_set_print_timestamp(osmo_stderr_target, 0);
+	log_set_use_color(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+
+	test_gsup_make_response();
+
+	printf("Done.\n");
+	return EXIT_SUCCESS;
+}
diff --git a/tests/gsup/gsup_test.err b/tests/gsup/gsup_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/gsup/gsup_test.err
diff --git a/tests/gsup/gsup_test.ok b/tests/gsup/gsup_test.ok
new file mode 100644
index 0000000..0899633
--- /dev/null
+++ b/tests/gsup/gsup_test.ok
@@ -0,0 +1,32 @@
+
+test_gsup_make_response()
+rx = Subscriber-Management OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST: imsi="1234567" source_name="incoming-source-name\0" session_id=42 session_state=BEGIN sm_rp_mr=23
+
+writing to an empty struct osmo_gsup_message should populate values as needed:
+- args (error=0, final=0)
+  Subscriber-Management OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT: imsi="1234567" destination_name="incoming-source-name\0" session_id=42 session_state=CONTINUE sm_rp_mr=23
+  rc = 0
+- args (error=0, final=1)
+  Subscriber-Management OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT: imsi="1234567" destination_name="incoming-source-name\0" session_id=42 session_state=END sm_rp_mr=23
+  rc = 0
+- args (error=1, final=0)
+  Subscriber-Management OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR: imsi="1234567" destination_name="incoming-source-name\0" session_id=42 session_state=CONTINUE sm_rp_mr=23
+  rc = 0
+- args (error=1, final=1)
+  Subscriber-Management OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR: imsi="1234567" destination_name="incoming-source-name\0" session_id=42 session_state=END sm_rp_mr=23
+  rc = 0
+
+writing to an already populated struct osmo_gsup_message, should have no effect:
+- args (error=0, final=0)
+  Inter-MSC OSMO_GSUP_MSGT_ROUTING_ERROR: imsi="987654321" destination_name="preset-destination-name\0" session_id=11 session_state=END sm_rp_mr=17
+  rc = 15
+- args (error=0, final=1)
+  Inter-MSC OSMO_GSUP_MSGT_ROUTING_ERROR: imsi="987654321" destination_name="preset-destination-name\0" session_id=11 session_state=END sm_rp_mr=17
+  rc = 15
+- args (error=1, final=0)
+  Inter-MSC OSMO_GSUP_MSGT_ROUTING_ERROR: imsi="987654321" destination_name="preset-destination-name\0" session_id=11 session_state=END sm_rp_mr=17
+  rc = 15
+- args (error=1, final=1)
+  Inter-MSC OSMO_GSUP_MSGT_ROUTING_ERROR: imsi="987654321" destination_name="preset-destination-name\0" session_id=11 session_state=END sm_rp_mr=17
+  rc = 15
+Done.
diff --git a/tests/gsup_server/Makefile.am b/tests/gsup_server/Makefile.am
index e64ac4a..48fda88 100644
--- a/tests/gsup_server/Makefile.am
+++ b/tests/gsup_server/Makefile.am
@@ -31,6 +31,8 @@
 gsup_server_test_LDADD = \
 	$(top_srcdir)/src/gsup_server.c \
 	$(top_srcdir)/src/gsup_router.c \
+	$(top_srcdir)/src/gsupclient/ipa_name.c \
+	$(top_srcdir)/src/gsupclient/gsup_req.c \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOABIS_LIBS) \
diff --git a/tests/test_nodes.vty b/tests/test_nodes.vty
index a752c93..3f31b0d 100644
--- a/tests/test_nodes.vty
+++ b/tests/test_nodes.vty
@@ -148,6 +148,8 @@
  logging level db notice
  logging level auc notice
  logging level ss info
+ logging level mslookup notice
+ logging level lu notice
 ...
 hlr
  store-imei
diff --git a/tests/testsuite.at b/tests/testsuite.at
index d30b5e9..956ef87 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -15,6 +15,13 @@
 AT_CHECK([$abs_top_builddir/tests/auc/auc_ts_55_205_test_sets], [], [expout], [experr])
 AT_CLEANUP
 
+AT_SETUP([gsup])
+AT_KEYWORDS([gsup])
+cat $abs_srcdir/gsup/gsup_test.ok > expout
+cat $abs_srcdir/gsup/gsup_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/gsup/gsup_test], [], [expout], [experr])
+AT_CLEANUP
+
 AT_SETUP([gsup_server])
 AT_KEYWORDS([gsup_server])
 cat $abs_srcdir/gsup_server/gsup_server_test.ok > expout

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

Gerrit-Project: osmo-hlr
Gerrit-Branch: master
Gerrit-Change-Id: I3a8dff3d4a1cbe10d6ab08257a0138d6b2a082d9
Gerrit-Change-Number: 16205
Gerrit-PatchSet: 31
Gerrit-Owner: neels <nhofmeyr at sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: fixeria <axilirator at gmail.com>
Gerrit-Reviewer: laforge <laforge at osmocom.org>
Gerrit-Reviewer: neels <nhofmeyr at sysmocom.de>
Gerrit-Reviewer: osmith <osmith at sysmocom.de>
Gerrit-Reviewer: pespin <pespin at sysmocom.de>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20200501/8b04d3bc/attachment.htm>


More information about the gerrit-log mailing list