Major update; Code now supports SAI, LU and ISD transactions

We also introduce a 'gsup_router' which enables us to route
a transaction to a given VLR.  It works based on the SERIAL attribute
communicated at time of the IPA multiplex setup as part of the CCM
sub-protocol.
diff --git a/src/Makefile b/src/Makefile
index d7c597e..adc3e89 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,12 +1,14 @@
 LDFLAGS += -losmocore -losmogsm -losmoabis -lsqlite3 -ltalloc
 CFLAGS += -g -Wall
 
-OBJS = auc.o db.o db_auc.o logging.o
+OBJS = auc.o db.o db_auc.o db_hlr.o logging.o
+
+all: db_test hlr
 
 db_test: db_test.o rand_fake.o $(OBJS)
 	$(CC) $(LDFLAGS) -o $@ $^
 
-hlr: hlr.o gsup_server.o rand_urandom.o $(OBJS)
+hlr: hlr.o gsup_server.o gsup_router.o rand_urandom.o $(OBJS)
 	$(CC) $(LDFLAGS) -o $@ $^
 
 %.o: %.c
diff --git a/src/db.c b/src/db.c
index b4fadca..4424307 100644
--- a/src/db.c
+++ b/src/db.c
@@ -25,8 +25,9 @@
 #include "db.h"
 
 static const char *stmt_sql[] = {
-	[SEL_BY_IMSI] = "SELECT * FROM subscriber WHERE imsi = ?",
-	[UPD_BY_IMSI] = "UPDATE subscriber SET vlr_number = ? WHERE imsi = ?",
+	[SEL_BY_IMSI] = "SELECT id,imsi,msisdn,vlr_number,sgsn_number,sgsn_address,periodic_lu_tmr,periodic_rau_tau_tmr,nam_cs,nam_ps,lmsi,ms_purged_cs,ms_purged_ps FROM subscriber WHERE imsi = ?",
+	[UPD_VLR_BY_ID] = "UPDATE subscriber SET vlr_number = ? WHERE id = ?",
+	[UPD_SGSN_BY_ID] = "UPDATE subscriber SET sgsn_number = ? WHERE id = ?",
 	[AUC_BY_IMSI] = "SELECT id, algo_id_2g, ki, algo_id_3g, k, op, opc, sqn FROM subscriber LEFT JOIN auc_2g ON auc_2g.subscriber_id = subscriber.id LEFT JOIN auc_3g ON auc_3g.subscriber_id = subscriber.id WHERE imsi = ?",
 	[AUC_UPD_SQN] = "UPDATE auc_3g SET sqn = ? WHERE subscriber_id = ?",
 };
diff --git a/src/db.h b/src/db.h
index f0545a4..a776099 100644
--- a/src/db.h
+++ b/src/db.h
@@ -1,12 +1,14 @@
 #pragma once
 
+#include <stdbool.h>
 #include <sqlite3.h>
 
 enum stmt_idx {
 	SEL_BY_IMSI	= 0,
-	UPD_BY_IMSI	= 1,
-	AUC_BY_IMSI	= 2,
-	AUC_UPD_SQN	= 3,
+	UPD_VLR_BY_ID	= 1,
+	UPD_SGSN_BY_ID	= 2,
+	AUC_BY_IMSI	= 3,
+	AUC_UPD_SQN	= 4,
 	_NUM_STMT
 };
 
@@ -33,3 +35,39 @@
 int db_get_auc(struct db_context *dbc, const char *imsi,
 	    struct osmo_auth_vector *vec, unsigned int num_vec,
 	    const uint8_t *rand_auts, const uint8_t *auts);
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/gsm/protocol/gsm_23_003.h>
+
+/* TODO: Get this from somewhere? */
+#define GT_MAX_DIGITS	15
+
+struct hlr_subscriber {
+	struct llist_head list;
+
+	uint64_t	id;
+	char		imsi[GSM23003_IMSI_MAX_DIGITS+1];
+	char		msisdn[GT_MAX_DIGITS+1];
+	/* imeisv? */
+	char		vlr_number[GT_MAX_DIGITS+1];
+	char		sgsn_number[GT_MAX_DIGITS+1];
+	char		sgsn_address[GT_MAX_DIGITS+1];
+	/* ggsn number + address */
+	/* gmlc number */
+	/* smsc number */
+	uint32_t	periodic_lu_timer;
+	uint32_t	periodic_rau_tau_timer;
+	bool		nam_cs;
+	bool		nam_ps;
+	uint32_t	lmsi;
+	bool		ms_purged_cs;
+	bool		ms_purged_ps;
+};
+
+int db_subscr_get(struct db_context *dbc, const char *imsi,
+		  struct hlr_subscriber *subscr);
+
+int db_subscr_lu(struct db_context *dbc,
+		 const struct hlr_subscriber *subscr,
+		 const char *vlr_or_sgsn_number,
+		 bool lu_is_ps);
diff --git a/src/db_hlr.c b/src/db_hlr.c
new file mode 100644
index 0000000..5d22717
--- /dev/null
+++ b/src/db_hlr.c
@@ -0,0 +1,136 @@
+/* (C) 2015 by Harald Welte <laforge@gnumonks.org>
+ *
+ * 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/utils.h>
+#include <osmocom/crypt/auth.h>
+
+#include <sqlite3.h>
+
+#include "logging.h"
+#include "db.h"
+
+#define LOGHLR(imsi, level, fmt, args ...)	LOGP(DAUC, level, "%s: " fmt, imsi, ## args)
+
+#define SL3_TXT(x, stmt, idx)					do {	\
+		const char *_txt = (const char *) sqlite3_column_text(stmt, idx);	\
+		if (_txt)						\
+			strncpy(x, _txt, sizeof(x));			\
+			x[sizeof(x)-1] = '\0';				\
+		} while (0)
+
+int db_subscr_get(struct db_context *dbc, const char *imsi,
+		  struct hlr_subscriber *subscr)
+{
+	sqlite3_stmt *stmt = dbc->stmt[SEL_BY_IMSI];
+	int rc, ret = 0;
+
+	rc = sqlite3_bind_text(stmt, 1, imsi, -1, SQLITE_STATIC);
+	if (rc != SQLITE_OK) {
+		LOGHLR(imsi, LOGL_ERROR, "Error binding IMSI: %d\n", rc);
+		return -1;
+	}
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_ROW) {
+		LOGHLR(imsi, LOGL_ERROR, "Error executing SQL: %d\n", rc);
+		ret = -2;
+		goto out;
+	}
+
+	/* obtain the various columns */
+	subscr->id = sqlite3_column_int64(stmt, 0);
+	SL3_TXT(subscr->imsi, stmt, 1);
+	SL3_TXT(subscr->msisdn, stmt, 2);
+	SL3_TXT(subscr->vlr_number, stmt, 3);
+	SL3_TXT(subscr->sgsn_number, stmt, 4);
+	SL3_TXT(subscr->sgsn_address, stmt, 5);
+	subscr->periodic_lu_timer = sqlite3_column_int(stmt, 6);
+	subscr->periodic_rau_tau_timer = sqlite3_column_int(stmt, 7);
+	subscr->nam_cs = sqlite3_column_int(stmt, 8);
+	subscr->nam_ps = sqlite3_column_int(stmt, 9);
+	subscr->lmsi = sqlite3_column_int(stmt, 10);
+	subscr->ms_purged_cs = sqlite3_column_int(stmt, 11);
+	subscr->ms_purged_ps = sqlite3_column_int(stmt, 12);
+
+out:
+	/* remove bindings and reset statement to be re-executed */
+	rc = sqlite3_clear_bindings(stmt);
+	if (rc != SQLITE_OK) {
+		LOGP(DAUC, LOGL_ERROR, "Error clerearing bindings: %d\n", rc);
+	}
+	rc = sqlite3_reset(stmt);
+	if (rc != SQLITE_OK) {
+		LOGP(DAUC, LOGL_ERROR, "Error in sqlite3_reset: %d\n", rc);
+	}
+
+	return ret;
+}
+
+int db_subscr_lu(struct db_context *dbc,
+		 const struct hlr_subscriber *subscr,
+		 const char *vlr_or_sgsn_number, bool lu_is_ps)
+{
+	sqlite3_stmt *stmt = dbc->stmt[UPD_VLR_BY_ID];
+	const char *txt;
+	int rc, ret = 0;
+
+	if (lu_is_ps) {
+		stmt = dbc->stmt[UPD_SGSN_BY_ID];
+		txt = subscr->sgsn_number;
+	} else {
+		stmt = dbc->stmt[UPD_VLR_BY_ID];
+		txt = subscr->vlr_number;
+	}
+
+	rc = sqlite3_bind_int64(stmt, 1, subscr->id);
+	if (rc != SQLITE_OK) {
+		LOGP(DAUC, LOGL_ERROR, "Error binding ID: %d\n", rc);
+		return -1;
+	}
+
+	rc = sqlite3_bind_text(stmt, 2, txt, -1, SQLITE_STATIC);
+	if (rc != SQLITE_OK) {
+		LOGP(DAUC, LOGL_ERROR, "Error binding VLR/SGSN Number: %d\n", rc);
+		ret = -2;
+		goto out;
+	}
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DAUC, LOGL_ERROR, "Error updating SQN: %d\n", rc);
+		ret = -3;
+		goto out;
+	}
+out:
+	/* remove bindings and reset statement to be re-executed */
+	rc = sqlite3_clear_bindings(stmt);
+	if (rc != SQLITE_OK) {
+		LOGP(DAUC, LOGL_ERROR, "Error clerearing bindings: %d\n", rc);
+	}
+	rc = sqlite3_reset(stmt);
+	if (rc != SQLITE_OK) {
+		LOGP(DAUC, LOGL_ERROR, "Error in sqlite3_reset: %d\n", rc);
+	}
+
+	return ret;
+}
diff --git a/src/gsup_router.c b/src/gsup_router.c
new file mode 100644
index 0000000..e9aed78
--- /dev/null
+++ b/src/gsup_router.c
@@ -0,0 +1,85 @@
+/* (C) 2016 by Harald Welte <laforge@gnumonks.org>
+ *
+ * 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 <errno.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/talloc.h>
+
+#include "gsup_server.h"
+
+struct gsup_route {
+	struct llist_head list;
+
+	uint8_t *addr;
+	struct osmo_gsup_conn *conn;
+};
+
+/* find a route for the given address */
+struct osmo_gsup_conn *gsup_route_find(struct osmo_gsup_server *gs,
+					const uint8_t *addr, size_t addrlen)
+{
+	struct gsup_route *gr;
+
+	llist_for_each_entry(gr, &gs->routes, list) {
+		if (talloc_total_size(gr->addr) == addrlen &&
+		    !memcmp(gr->addr, addr, addrlen))
+			return gr->conn;
+	}
+	return NULL;
+}
+
+/* add a new route for the given address to the given conn */
+int gsup_route_add(struct osmo_gsup_conn *conn, const uint8_t *addr, size_t addrlen)
+{
+	struct gsup_route *gr;
+
+	/* Check if we already have a route for this address */
+	if (gsup_route_find(conn->server, addr, addrlen))
+		return -EEXIST;
+
+	/* allocate new route and populate it */
+	gr = talloc_zero(conn->server, struct gsup_route);
+	if (!gr)
+		return -ENOMEM;
+
+	gr->addr = talloc_memdup(gr, addr, addrlen);
+	gr->conn = conn;
+	llist_add_tail(&gr->list, &conn->server->routes);
+
+	return 0;
+}
+
+/* delete all routes for the given connection */
+int gsup_route_del_conn(struct osmo_gsup_conn *conn)
+{
+	struct gsup_route *gr, *gr2;
+	unsigned int num_deleted = 0;
+
+	llist_for_each_entry_safe(gr, gr2, &conn->server->routes, list) {
+		if (gr->conn == conn) {
+			llist_del(&gr->list);
+			talloc_free(gr);
+			num_deleted++;
+		}
+	}
+
+	return num_deleted;
+}
diff --git a/src/gsup_router.h b/src/gsup_router.h
new file mode 100644
index 0000000..7a5bd25
--- /dev/null
+++ b/src/gsup_router.h
@@ -0,0 +1,8 @@
+struct osmo_gsup_conn *gsup_route_find(struct osmo_gsup_server *gs,
+					const uint8_t *addr, size_t addrlen);
+
+/* add a new route for the given address to the given conn */
+int gsup_route_add(struct osmo_gsup_conn *conn, const uint8_t *addr, size_t addrlen);
+
+/* delete all routes for the given connection */
+int gsup_route_del_conn(struct osmo_gsup_conn *conn);
diff --git a/src/gsup_server.c b/src/gsup_server.c
index b9a21ab..c86a2a1 100644
--- a/src/gsup_server.c
+++ b/src/gsup_server.c
@@ -103,12 +103,71 @@
 
 }
 
+static void osmo_tlvp_dump(const struct tlv_parsed *tlvp,
+			   int subsys, int level)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(tlvp->lv); i++) {
+		if (!TLVP_PRESENT(tlvp, i))
+			continue;
+
+		LOGP(subsys, level, "%u: %s\n", i,
+			TLVP_VAL(tlvp, i));
+		LOGP(subsys, level, "%u: %s\n", i,
+			osmo_hexdump(TLVP_VAL(tlvp, i),
+				     TLVP_LEN(tlvp, i)));
+	}
+}
+
+/* FIXME: should this be parrt of ipas_server handling, not GSUP? */
+static void tlvp_copy(void *ctx, struct tlv_parsed *out, const struct tlv_parsed *in)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(out->lv); i++) {
+		if (!TLVP_PRESENT(in, i)) {
+			if (TLVP_PRESENT(out, i)) {
+				talloc_free((void *) out->lv[i].val);
+				out->lv[i].val = NULL;
+				out->lv[i].len = 0;
+			}
+			continue;
+		}
+		out->lv[i].val = talloc_memdup(ctx, in->lv[i].val, in->lv[i].len);
+		out->lv[i].len = in->lv[i].len;
+	}
+}
+
+int osmo_gsup_conn_ccm_get(const struct osmo_gsup_conn *clnt, uint8_t **addr,
+			   uint8_t tag)
+{
+	if (!TLVP_PRESENT(&clnt->ccm, tag))
+		return -ENODEV;
+	*addr = (uint8_t *) TLVP_VAL(&clnt->ccm, tag);
+
+	return TLVP_LEN(&clnt->ccm, tag);
+}
+
 static int osmo_gsup_server_ccm_cb(struct ipa_server_conn *conn,
 				   struct msgb *msg, struct tlv_parsed *tlvp,
 				   struct ipaccess_unit *unit)
 {
+	struct osmo_gsup_conn *clnt = (struct osmo_gsup_conn *)conn->data;
+	uint8_t *addr;
+	size_t addr_len;
+
 	LOGP(DLGSUP, LOGL_INFO, "CCM Callback\n");
-	/* TODO: ? */
+
+	/* FIXME: should this be parrt of ipas_server handling, not
+	 * GSUP? */
+	tlvp_copy(clnt, &clnt->ccm, tlvp);
+	osmo_tlvp_dump(tlvp, DLGSUP, LOGL_INFO);
+
+	addr_len = osmo_gsup_conn_ccm_get(clnt, &addr, IPAC_IDTAG_SERNR);
+	if (addr_len)
+		gsup_route_add(clnt, addr, addr_len);
+
 	return 0;
 }
 
@@ -119,6 +178,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);
 
@@ -176,6 +236,7 @@
 	OSMO_ASSERT(gsups);
 
 	INIT_LLIST_HEAD(&gsups->clients);
+	INIT_LLIST_HEAD(&gsups->routes);
 
 	gsups->link = ipa_server_link_create(gsups,
 					/* no e1inp */ NULL,
diff --git a/src/gsup_server.h b/src/gsup_server.h
index 390bf2e..bf1a570 100644
--- a/src/gsup_server.h
+++ b/src/gsup_server.h
@@ -16,6 +16,7 @@
 
 	struct ipa_server_link *link;
 	osmo_gsup_read_cb_t read_cb;
+	struct llist_head routes;
 };
 
 
@@ -26,6 +27,7 @@
 	struct osmo_gsup_server *server;
 	struct ipa_server_conn *conn;
 	//struct oap_state oap_state;
+	struct tlv_parsed ccm;
 };
 
 
diff --git a/src/hlr.c b/src/hlr.c
index 054558e..237950d 100644
--- a/src/hlr.c
+++ b/src/hlr.c
@@ -1,4 +1,5 @@
 #include <signal.h>
+#include <errno.h>
 
 #include <osmocom/core/msgb.h>
 #include <osmocom/core/logging.h>
@@ -8,10 +9,15 @@
 #include "db.h"
 #include "logging.h"
 #include "gsup_server.h"
+#include "gsup_router.h"
 #include "rand.h"
 
 static struct db_context *g_dbc;
 
+/***********************************************************************
+ * 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)
@@ -32,17 +38,350 @@
 		gsup_out.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_ERROR;
 	}
 
-	msg_out = msgb_alloc(1024, "GSUP response");
+	msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP AUC response");
 	osmo_gsup_encode(msg_out, &gsup_out);
 	return osmo_gsup_conn_send(conn, msg_out);
 }
 
+/***********************************************************************
+ * LU Operation State / Structure
+ ***********************************************************************/
+
+static LLIST_HEAD(g_lu_ops);
+
+#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,
+};
+
+static 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 }
+};
+
+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;
+};
+
+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;
+}
+
+struct lu_operation *lu_op_by_imsi(const char *imsi)
+{
+	struct lu_operation *luop;
+
+	llist_for_each_entry(luop, &g_lu_ops, list) {
+		if (!strcmp(imsi, luop->subscr.imsi))
+			return luop;
+	}
+	return NULL;
+}
+
+/* Send a msgb to a given address using routing */
+int osmo_gsup_addr_send(struct osmo_gsup_server *gs,
+			const uint8_t *addr, size_t addrlen,
+			struct msgb *msg)
+{
+	struct osmo_gsup_conn *conn;
+
+	conn = gsup_route_find(gs, addr, addrlen);
+	if (!conn) {
+		DEBUGP(DMAIN, "Cannot find route for addr %s\n", addr);
+		msgb_free(msg);
+		return -ENODEV;
+	}
+
+	return osmo_gsup_conn_send(conn, msg);
+}
+
+static void _luop_tx_gsup(struct lu_operation *luop,
+			  const struct osmo_gsup_message *gsup)
+{
+	struct msgb *msg_out;
+
+	msg_out = msgb_alloc_headroom(1024+16, 16, "GSUP LUOP");
+	osmo_gsup_encode(msg_out, gsup);
+
+	osmo_gsup_addr_send(luop->gsup_server, luop->peer,
+			    talloc_total_size(luop->peer),
+			    msg_out);
+}
+
+/*! 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=%u)\n",
+		luop->subscr.imsi, cause);
+
+	memset(&gsup, 0, sizeof(gsup));
+	gsup.message_type = OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR;
+	strncpy(&gsup.imsi, luop->subscr.imsi, sizeof(gsup.imsi));
+	gsup.imsi[sizeof(gsup.imsi)-1] = '\0';
+	gsup.cause = cause;
+
+	_luop_tx_gsup(luop, &gsup);
+
+	llist_del(&luop->list);
+	talloc_free(luop);
+}
+
+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);
+}
+
+/*! Transmit UPD_LOC_RESULT and destroy lu_operation */
+void lu_op_tx_ack(struct lu_operation *luop)
+{
+	struct osmo_gsup_message gsup;
+
+	memset(&gsup, 0, sizeof(gsup));
+	gsup.message_type = OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT;
+	strncpy(gsup.imsi, luop->subscr.imsi, sizeof(gsup.imsi)-1);
+	//FIXME gsup.hlr_enc;
+
+	_luop_tx_gsup(luop, &gsup);
+
+	llist_del(&luop->list);
+	talloc_free(luop);
+}
+
+/*! Send Cancel Location to old VLR/SGSN */
+void lu_op_tx_cancel_old(struct lu_operation *luop)
+{
+	struct osmo_gsup_message gsup;
+
+	OSMO_ASSERT(luop->state == LU_S_LU_RECEIVED);
+
+	memset(&gsup, 0, sizeof(gsup));
+	gsup.message_type = 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);
+}
+
+/*! 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)
+{
+	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);
+}
+
+/*! Transmit Insert Subscriber Data to new VLR/SGSN */
+void lu_op_tx_insert_subscr_data(struct lu_operation *luop)
+{
+	struct osmo_gsup_message gsup;
+
+	OSMO_ASSERT(luop->state == LU_S_LU_RECEIVED ||
+		    luop->state == LU_S_CANCEL_ACK_RECEIVED);
+
+	memset(&gsup, 0, sizeof(gsup));
+	gsup.message_type = OSMO_GSUP_MSGT_INSERT_DATA_REQUEST;
+	strncpy(gsup.imsi, luop->subscr.imsi, sizeof(gsup.imsi)-1);
+	gsup.msisdn_enc;
+	gsup.hlr_enc;
+
+	if (luop->is_ps) {
+		/* FIXME: PDP infos */
+	}
+
+	/* 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);
+}
+
+/*! 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;
+	}
+}
+
+static 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;
+	luop->timer.cb = lu_op_timer_cb;
+	luop->timer.data = luop;
+
+	return luop;
+}
+
+/*! 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)
+{
+	int rc;
+	bool is_ps;
+	struct lu_operation *luop;
+	struct hlr_subscriber *subscr;
+	uint8_t *peer_addr;
+
+	rc = osmo_gsup_conn_ccm_get(conn, &peer_addr, IPAC_IDTAG_SERNR);
+	if (rc < 0) {
+		LOGP(DMAIN, LOGL_ERROR, "LU REQ from conn without addr?\n");
+		return rc;
+	}
+
+	luop = lu_op_alloc(conn->server);
+	luop->peer = talloc_memdup(luop, peer_addr, rc);
+	lu_op_statechg(luop, LU_S_LU_RECEIVED);
+	subscr = &luop->subscr;
+	if (gsup->cn_domain == OSMO_GSUP_CN_DOMAIN_PS)
+		luop->is_ps = true;
+	llist_add(&luop->list, &g_lu_ops);
+
+	/* Roughly follwing "Process Update_Location_HLR" of TS 09.02 */
+
+	/* check if subscriber is known at all */
+	rc = db_subscr_get(g_dbc, gsup->imsi, subscr);
+	if (rc < 0) {
+		/* Send Error back: Subscriber Unknown in HLR */
+		strcpy(luop->subscr.imsi, gsup->imsi);
+		lu_op_tx_error(luop, GMM_CAUSE_IMSI_UNKNOWN);
+		return 0;
+	}
+
+	if (!is_ps && !subscr->nam_cs) {
+		lu_op_tx_error(luop, GMM_CAUSE_PLMN_NOTALLOWED);
+		return 0;
+	} else if (is_ps && !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 */
+	if (luop->is_ps == false &&
+	    strcmp(subscr->vlr_number, vlr_number)) {
+		/* FIXME: start location cancel towards old VLR */
+		lu_op_tx_cancel_old(luop);
+	} else if (luop->is_ps == true &&
+		   strcmp(subscr->sgsn_number, sgsn_number)) {
+		/* FIXME: start location cancel towards old VLR */
+		lu_op_tx_cancel_old(luop);
+	} else
+#endif
+	{
+		/* 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);
+	}
+	return 0;
+}
+
 static int read_cb(struct osmo_gsup_conn *conn, struct msgb *msg)
 {
 	static struct osmo_gsup_message gsup;
 	int rc;
 
-	rc = osmo_gsup_decode(msgb_l3(msg), msgb_l3len(msg), &gsup);
+	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);
 		return rc;
@@ -54,11 +393,23 @@
 		rx_send_auth_info(conn, &gsup);
 		break;
 	case OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST:
+		rx_upd_loc_req(conn, &gsup);
 		break;
 	/* responses to requests sent by us */
 	case OSMO_GSUP_MSGT_INSERT_DATA_ERROR:
-		break;
 	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);
+			if (!luop) {
+				LOGP(DMAIN, LOGL_ERROR, "GSUP message %u for "
+					"unknown IMSI %s\n", gsup.message_type,
+					gsup.imsi);
+				break;
+			}
+			lu_op_rx_gsup(luop, &gsup);
+		}
 		break;
 	default:
 		LOGP(DMAIN, LOGL_DEBUG, "Unhandled GSUP message type %u\n",