[PATCH] osmo-msc[master]: Add libvlr implementation

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

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

Neels Hofmeyr gerrit-no-reply at lists.osmocom.org
Thu Jul 13 02:20:40 UTC 2017


Hello Jenkins Builder,

I'd like you to reexamine a change.  Please visit

    https://gerrit.osmocom.org/3194

to look at the new patch set (#4).

Add libvlr implementation

Original libvlr code is by Harald Welte <laforge at gnumonks.org>,
polished and tweaked by Neels Hofmeyr <nhofmeyr at sysmocom.de>.

This is a long series of trial-and-error development collapsed in one patch.
This may be split in smaller commits if reviewers prefer that. If we can keep
it as one, we have saved ourselves the additional separation work.

Related: OS#1592
Change-Id: Ie303c98f8c18e40c87c1b68474b35de332033622
---
M configure.ac
M include/openbsc/Makefile.am
A include/openbsc/vlr.h
M src/Makefile.am
A src/libmsc/subscr_conn.c
A src/libvlr/Makefile.am
A src/libvlr/vlr.c
A src/libvlr/vlr_access_req_fsm.c
A src/libvlr/vlr_access_req_fsm.h
A src/libvlr/vlr_auth_fsm.c
A src/libvlr/vlr_auth_fsm.h
A src/libvlr/vlr_core.h
A src/libvlr/vlr_lu_fsm.c
A src/libvlr/vlr_lu_fsm.h
M src/osmo-nitb/Makefile.am
A tests/vlr/Makefile.am
A tests/vlr/vlr_test.c
17 files changed, 5,439 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.osmocom.org:29418/osmo-msc refs/changes/94/3194/4

diff --git a/configure.ac b/configure.ac
index ff51ccf..3089a2e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -225,6 +225,7 @@
     src/libtrau/Makefile
     src/libbsc/Makefile
     src/libmsc/Makefile
+    src/libvlr/Makefile
     src/libmgcp/Makefile
     src/libcommon/Makefile
     src/libfilter/Makefile
@@ -261,6 +262,7 @@
     tests/slhc/Makefile
     tests/v42bis/Makefile
     tests/nanobts_omlattr/Makefile
+    tests/vlr/Makefile
     doc/Makefile
     doc/examples/Makefile
     Makefile)
diff --git a/include/openbsc/Makefile.am b/include/openbsc/Makefile.am
index 2740a5d..532328c 100644
--- a/include/openbsc/Makefile.am
+++ b/include/openbsc/Makefile.am
@@ -84,6 +84,7 @@
 	trau_mux.h \
 	trau_upqueue.h \
 	ussd.h \
+	vlr.h \
 	vty.h \
 	v42bis.h \
 	v42bis_private.h \
diff --git a/include/openbsc/vlr.h b/include/openbsc/vlr.h
new file mode 100644
index 0000000..d7e8f19
--- /dev/null
+++ b/include/openbsc/vlr.h
@@ -0,0 +1,409 @@
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/gsm/protocol/gsm_23_003.h>
+#include <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+#include <osmocom/gsm/gsm23003.h>
+#include <openbsc/gsm_data.h>
+// for GSM_NAME_LENGTH
+#include <openbsc/gsm_subscriber.h>
+
+/* from 3s to 10s */
+#define GSM_29002_TIMER_S	10
+/* from 15s to 30s */
+#define GSM_29002_TIMER_M	30
+/* from 1min to 10min */
+#define GSM_29002_TIMER_ML	(10*60)
+/* from 28h to 38h */
+#define GSM_29002_TIMER_L	(32*60*60)
+
+
+/* VLR subscriber authentication state */
+enum vlr_subscr_auth_state {
+	/* subscriber needs to be autenticated */
+	VLR_SUB_AS_NEEDS_AUTH,
+	/* waiting for AuthInfo from HLR/AUC */
+	VLR_SUB_AS_NEEDS_AUTH_WAIT_AI,
+	/* waiting for response from subscriber */
+	VLR_SUB_AS_WAIT_RESP,
+	/* successfully authenticated */
+	VLR_SUB_AS_AUTHENTICATED,
+	/* subscriber needs re-sync */
+	VLR_SUB_AS_NEEDS_RESYNC,
+	/* waiting for AuthInfo with ReSync */
+	VLR_SUB_AS_NEEDS_AUTH_WAIT_SAI_RESYNC,
+	/* waiting for response from subscr, resync case */
+	VLR_SUB_AS_WAIT_RESP_RESYNC,
+	/* waiting for IMSI from subscriber */
+	VLR_SUB_AS_WAIT_ID_IMSI,
+	/* authentication has failed */
+	VLR_SUB_AS_AUTH_FAILED,
+};
+
+enum vlr_lu_event {
+	VLR_ULA_E_UPDATE_LA,	/* Initial trigger (LU from MS) */
+	VLR_ULA_E_SEND_ID_ACK,	/* Result of Send-ID from PVLR */
+	VLR_ULA_E_SEND_ID_NACK,	/* Result of Send-ID from PVLR */
+	VLR_ULA_E_AUTH_RES,	/* Result of auth procedure */
+	VLR_ULA_E_CIPH_RES,	/* Result of Ciphering Mode Command */
+	VLR_ULA_E_ID_IMSI,	/* IMSI recieved from MS */
+	VLR_ULA_E_ID_IMEI,	/* IMEI received from MS */
+	VLR_ULA_E_ID_IMEISV,	/* IMEISV received from MS */
+	VLR_ULA_E_HLR_LU_RES,	/* HLR UpdateLocation result */
+	VLR_ULA_E_UPD_HLR_COMPL,/* UpdatE_HLR_VLR result */
+	VLR_ULA_E_LU_COMPL_SUCCESS,/* Location_Update_Completion_VLR result */
+	VLR_ULA_E_LU_COMPL_FAILURE,/* Location_Update_Completion_VLR result */
+	VLR_ULA_E_NEW_TMSI_ACK,	/* TMSI Reallocation Complete */
+};
+
+enum vlr_ciph_result_cause {
+	VLR_CIPH_REJECT, /* ? */
+	VLR_CIPH_COMPL,
+};
+
+struct vlr_ciph_result {
+	enum vlr_ciph_result_cause cause;
+	const char *imeisv;
+};
+
+enum vlr_subscr_security_context {
+	VLR_SEC_CTX_NONE,
+	VLR_SEC_CTX_GSM,
+	VLR_SEC_CTX_UMTS,
+};
+
+enum vlr_lu_type {
+	VLR_LU_TYPE_PERIODIC,
+	VLR_LU_TYPE_IMSI_ATTACH,
+	VLR_LU_TYPE_REGULAR,
+};
+
+#define OSMO_LBUF_DECL(name, xlen)		\
+	struct {				\
+		uint8_t buf[xlen];		\
+		size_t len;			\
+	} name
+
+struct sgsn_mm_ctx;
+struct vlr_instance;
+
+/* The VLR subscriber is the part of the GSM subscriber state in VLR (CS) or
+ * SGSN (PS), particularly while interacting with the HLR via GSUP */
+struct vlr_subscr {
+	struct llist_head list;
+	struct vlr_instance *vlr;
+
+	/* TODO either populate from HLR or drop this completely? */
+	long long unsigned int id;
+
+	/* Data from HLR */				/* 3GPP TS 23.008 */
+	/* Always use vlr_subscr_set_imsi() to write to imsi[] */
+	char imsi[GSM23003_IMSI_MAX_DIGITS+1];		/* 2.1.1.1 */
+	char msisdn[GSM_EXTENSION_LENGTH+1];		/* 2.1.2 */
+	char name[GSM_NAME_LENGTH+1];			/* proprietary */
+	OSMO_LBUF_DECL(hlr, 16);			/* 2.4.7 */
+	uint32_t periodic_lu_timer;			/* 2.4.24 */
+	uint32_t age_indicator;				/* 2.17.1 */
+
+	/* Authentication Data */
+	struct gsm_auth_tuple auth_tuples[5];		/* 2.3.1-2.3.4 */
+	struct gsm_auth_tuple *last_tuple;
+	enum vlr_subscr_security_context sec_ctx;
+
+	/* Data local to VLR is below */
+	uint32_t tmsi;					/* 2.1.4 */
+	/* Newly allocated TMSI that was not yet acked by MS */
+	uint32_t tmsi_new;
+
+	/* some redundancy in information below? */
+	struct osmo_cell_global_id cgi;			/* 2.4.16 */
+	uint16_t lac;					/* 2.4.2 */
+
+	char imeisv[GSM23003_IMEISV_NUM_DIGITS+1];	/* 2.2.3 */
+	char imei[GSM23003_IMEISV_NUM_DIGITS+1];	/* 2.1.9 */
+	bool imsi_detached_flag;			/* 2.7.1 */
+	bool conf_by_radio_contact_ind;			/* 2.7.4.1 */
+	bool sub_dataconf_by_hlr_ind;			/* 2.7.4.2 */
+	bool loc_conf_in_hlr_ind;			/* 2.7.4.3 */
+	bool dormant_ind;				/* 2.7.8 */
+	bool cancel_loc_rx;				/* 2.7.8A */
+	bool ms_not_reachable_flag;			/* 2.10.2 (MNRF) */
+	bool la_allowed;
+
+	int use_count;
+	time_t expire_lu;	/* FIXME: overlap with periodic_lu_timer/age_indicator */
+
+	struct osmo_fsm_inst *lu_fsm;
+	struct osmo_fsm_inst *auth_fsm;
+	struct osmo_fsm_inst *proc_arq_fsm;
+
+	bool lu_complete;
+
+	void *msc_conn_ref;
+
+	/* PS (SGSN) specific parts */
+	struct {
+		struct llist_head pdp_list;
+		uint8_t rac;
+		uint8_t sac;
+		struct gprs_mm_ctx *mmctx;
+	} ps;
+	/* CS (NITB/CSCN) specific parts */
+	struct {
+		/* pending requests */
+		bool is_paging;
+		struct llist_head requests;
+	} cs;
+};
+
+enum vlr_proc_arq_result;
+
+enum vlr_ciph {
+	VLR_CIPH_NONE, /*< A5/0, no encryption */
+	VLR_CIPH_A5_1, /*< A5/1, encryption */
+	VLR_CIPH_A5_2, /*< A5/2, deprecated export-grade encryption */
+	VLR_CIPH_A5_3, /*< A5/3, 'new secure' encryption */
+};
+
+struct vlr_ops {
+	/* encode + transmit an AUTH REQ towards the MS.
+	 * \param[in] at  auth tuple providing rand, key_seq and autn.
+	 * \param[in] send_autn  True to send AUTN, for r99 UMTS auth.
+	 */
+	int (*tx_auth_req)(void *msc_conn_ref, struct gsm_auth_tuple *at,
+			   bool send_autn);
+	/* encode + transmit an AUTH REJECT towards the MS */
+	int (*tx_auth_rej)(void *msc_conn_ref);
+
+	/* encode + transmit an IDENTITY REQUEST towards the MS */
+	int (*tx_id_req)(void *msc_conn_ref, uint8_t mi_type);
+
+	int (*tx_lu_acc)(void *msc_conn_ref, uint32_t send_tmsi);
+	int (*tx_lu_rej)(void *msc_conn_ref, uint8_t cause);
+	int (*tx_cm_serv_acc)(void *msc_conn_ref);
+	int (*tx_cm_serv_rej)(void *msc_conn_ref, enum vlr_proc_arq_result result);
+
+	int (*set_ciph_mode)(void *msc_conn_ref, enum vlr_ciph ciph_mode,
+			     bool retrieve_imeisv);
+
+	/* notify MSC/SGSN that the subscriber data in VLR has been updated */
+	void (*subscr_update)(struct vlr_subscr *vsub);
+	/* notify MSC/SGSN that the given subscriber has been associated
+	 * with this msc_conn_ref */
+	void (*subscr_assoc)(void *msc_conn_ref, struct vlr_subscr *vsub);
+};
+
+enum vlr_timer {
+	VLR_T_3250,
+	VLR_T_3260,
+	VLR_T_3270,
+	_NUM_VLR_TIMERS
+};
+
+/* An instance of the VLR codebase */
+struct vlr_instance {
+	struct llist_head subscribers;
+	struct llist_head operations;
+	struct gsup_client *gsup_client;
+	struct vlr_ops ops;
+	struct {
+		bool retrieve_imeisv;
+		bool assign_tmsi;
+		bool check_imei_rqd;
+		int auth_tuple_max_use_count;
+		bool auth_reuse_old_sets_on_error;
+		bool parq_retrieve_imsi;
+		bool is_ps;
+		uint32_t timer[_NUM_VLR_TIMERS];
+	} cfg;
+	/* A free-form pointer for use by the caller */
+	void *user_ctx;
+};
+
+extern const struct value_string vlr_ciph_names[];
+static inline const char *vlr_ciph_name(enum vlr_ciph val)
+{
+	return get_value_string(vlr_ciph_names, val);
+}
+
+/* Location Updating request */
+struct osmo_fsm_inst *
+vlr_loc_update(struct osmo_fsm_inst *parent,
+	       uint32_t parent_event_success,
+	       uint32_t parent_event_failure,
+	       void *parent_event_data,
+	       struct vlr_instance *vlr, void *msc_conn_ref,
+	       enum vlr_lu_type type, uint32_t tmsi, const char *imsi,
+	       const struct osmo_location_area_id *old_lai,
+	       const struct osmo_location_area_id *new_lai,
+	       bool authentication_required,
+	       enum vlr_ciph ciphering_required,
+	       bool is_r99, bool is_utran,
+	       bool assign_tmsi);
+
+void vlr_loc_update_conn_timeout(struct osmo_fsm_inst *fi);
+
+/* tell the VLR that the subscriber connection is gone */
+int vlr_subscr_disconnected(struct vlr_subscr *vsub);
+
+int vlr_subscr_rx_id_resp(struct vlr_subscr *vsub, const uint8_t *mi, size_t mi_len);
+int vlr_subscr_rx_auth_resp(struct vlr_subscr *vsub, bool is_r99, bool is_utran,
+			    const uint8_t *res, uint8_t res_len);
+int vlr_subscr_rx_auth_fail(struct vlr_subscr *vsub, const uint8_t *auts);
+int vlr_subscr_tx_auth_fail_rep(struct vlr_subscr *vsub);
+void vlr_subscr_rx_ciph_res(struct vlr_subscr *vsub, struct vlr_ciph_result *res);
+int vlr_subscr_rx_tmsi_reall_compl(struct vlr_subscr *vsub);
+int vlr_subscr_rx_imsi_detach(struct vlr_subscr *vsub);
+void vlr_subscr_conn_timeout(struct vlr_subscr *vsub);
+
+struct vlr_instance *vlr_alloc(void *ctx, const struct vlr_ops *ops);
+int vlr_start(const char *gsup_unit_name, struct vlr_instance *vlr,
+	      const char *gsup_server_addr_str, uint16_t gsup_server_port);
+
+/* internal use only */
+
+struct osmo_fsm_inst *sub_pres_vlr_fsm_start(struct osmo_fsm_inst *parent,
+					     struct vlr_subscr *vsub,
+					     uint32_t term_event);
+struct osmo_fsm_inst *
+upd_hlr_vlr_proc_start(struct osmo_fsm_inst *parent,
+		       struct vlr_subscr *vsub,
+		       uint32_t parent_event);
+
+struct osmo_fsm_inst *
+lu_compl_vlr_proc_start(struct osmo_fsm_inst *parent,
+			struct vlr_subscr *vsub,
+			void *msc_conn_ref,
+			uint32_t parent_event_success,
+			uint32_t parent_event_failure);
+
+
+const char *vlr_subscr_name(struct vlr_subscr *vsub);
+const char *vlr_subscr_msisdn_or_name(struct vlr_subscr *vsub);
+
+#define vlr_subscr_find_by_imsi(vlr, imsi) \
+	_vlr_subscr_find_by_imsi(vlr, imsi, __BASE_FILE__, __LINE__)
+#define vlr_subscr_find_or_create_by_imsi(vlr, imsi, created) \
+	_vlr_subscr_find_or_create_by_imsi(vlr, imsi, created, \
+					   __BASE_FILE__, __LINE__)
+
+#define vlr_subscr_find_by_tmsi(vlr, tmsi) \
+	_vlr_subscr_find_by_tmsi(vlr, tmsi, __BASE_FILE__, __LINE__)
+#define vlr_subscr_find_or_create_by_tmsi(vlr, tmsi, created) \
+	_vlr_subscr_find_or_create_by_tmsi(vlr, tmsi, created, \
+					   __BASE_FILE__, __LINE__)
+
+#define vlr_subscr_find_by_msisdn(vlr, msisdn) \
+	_vlr_subscr_find_by_msisdn(vlr, msisdn, __BASE_FILE__, __LINE__)
+
+struct vlr_subscr *_vlr_subscr_find_by_imsi(struct vlr_instance *vlr,
+					    const char *imsi,
+					    const char *file, int line);
+struct vlr_subscr *_vlr_subscr_find_or_create_by_imsi(struct vlr_instance *vlr,
+						      const char *imsi,
+						      bool *created,
+						      const char *file,
+						      int line);
+
+struct vlr_subscr *_vlr_subscr_find_by_tmsi(struct vlr_instance *vlr,
+					    uint32_t tmsi,
+					    const char *file, int line);
+struct vlr_subscr *_vlr_subscr_find_or_create_by_tmsi(struct vlr_instance *vlr,
+						      uint32_t tmsi,
+						      bool *created,
+						      const char *file,
+						      int line);
+
+struct vlr_subscr *_vlr_subscr_find_by_msisdn(struct vlr_instance *vlr,
+					      const char *msisdn,
+					      const char *file, int line);
+
+#define vlr_subscr_get(sub) _vlr_subscr_get(sub, __BASE_FILE__, __LINE__)
+#define vlr_subscr_put(sub) _vlr_subscr_put(sub, __BASE_FILE__, __LINE__)
+struct vlr_subscr *_vlr_subscr_get(struct vlr_subscr *sub, const char *file, int line);
+struct vlr_subscr *_vlr_subscr_put(struct vlr_subscr *sub, const char *file, int line);
+
+struct vlr_subscr *vlr_subscr_alloc(struct vlr_instance *vlr);
+void vlr_subscr_free(struct vlr_subscr *vsub);
+int vlr_subscr_alloc_tmsi(struct vlr_subscr *vsub);
+
+void vlr_subscr_set_imsi(struct vlr_subscr *vsub, const char *imsi);
+void vlr_subscr_set_imei(struct vlr_subscr *vsub, const char *imei);
+void vlr_subscr_set_imeisv(struct vlr_subscr *vsub, const char *imeisv);
+void vlr_subscr_set_msisdn(struct vlr_subscr *vsub, const char *msisdn);
+
+bool vlr_subscr_matches_imsi(struct vlr_subscr *vsub, const char *imsi);
+bool vlr_subscr_matches_tmsi(struct vlr_subscr *vsub, uint32_t tmsi);
+bool vlr_subscr_matches_msisdn(struct vlr_subscr *vsub, const char *msisdn);
+bool vlr_subscr_matches_imei(struct vlr_subscr *vsub, const char *imei);
+
+uint32_t vlr_timer(struct vlr_instance *vlr, uint32_t timer);
+
+int vlr_subscr_changed(struct vlr_subscr *vsub);
+int vlr_subscr_purge(struct vlr_subscr *vsub);
+void vlr_subscr_cancel(struct vlr_subscr *vsub, enum gsm48_gmm_cause cause);
+
+
+/* Process Acccess Request FSM */
+
+enum vlr_proc_arq_result {
+	VLR_PR_ARQ_RES_NONE,
+	VLR_PR_ARQ_RES_SYSTEM_FAILURE,
+	VLR_PR_ARQ_RES_ILLEGAL_SUBSCR,
+	VLR_PR_ARQ_RES_UNIDENT_SUBSCR,
+	VLR_PR_ARQ_RES_ROAMING_NOTALLOWED,
+	VLR_PR_ARQ_RES_ILLEGAL_EQUIP,
+	VLR_PR_ARQ_RES_UNKNOWN_ERROR,
+	VLR_PR_ARQ_RES_TIMEOUT,
+	VLR_PR_ARQ_RES_PASSED,
+};
+
+extern const struct value_string vlr_proc_arq_result_names[];
+static inline const char *vlr_proc_arq_result_name(enum vlr_proc_arq_result res)
+{
+	return get_value_string(vlr_proc_arq_result_names, res);
+}
+
+enum proc_arq_vlr_event {
+	PR_ARQ_E_START,
+	PR_ARQ_E_ID_IMSI,
+	PR_ARQ_E_AUTH_RES,
+	PR_ARQ_E_CIPH_RES,
+	PR_ARQ_E_UPD_LOC_RES,
+	PR_ARQ_E_TRACE_RES,
+	PR_ARQ_E_IMEI_RES,
+	PR_ARQ_E_PRES_RES,
+	PR_ARQ_E_TMSI_ACK,
+};
+
+enum vlr_parq_type {
+	VLR_PR_ARQ_T_INVALID = 0, /* to guard against unset vars */
+	VLR_PR_ARQ_T_CM_SERV_REQ,
+	VLR_PR_ARQ_T_PAGING_RESP,
+	/* FIXME: differentiate between services of 24.008 10.5.3.3 */
+};
+
+/* Process Access Request (CM SERV REQ / PAGING RESP) */
+void
+vlr_proc_acc_req(struct osmo_fsm_inst *parent,
+		 uint32_t parent_event_success,
+		 uint32_t parent_event_failure,
+		 void *parent_event_data,
+		 struct vlr_instance *vlr, void *msc_conn_ref,
+		 enum vlr_parq_type type, const uint8_t *mi_lv,
+		 const struct osmo_location_area_id *lai,
+		 bool authentication_required,
+		 enum vlr_ciph ciphering_required,
+		 bool is_r99, bool is_utran);
+
+void vlr_parq_conn_timeout(struct osmo_fsm_inst *fi);
+
+void vlr_parq_fsm_init(void);
+
+int vlr_set_ciph_mode(struct vlr_instance *vlr,
+		      struct osmo_fsm_inst *fi,
+		      void *msc_conn_ref,
+		      enum vlr_ciph ciph_mode,
+		      bool retrieve_imeisv);
diff --git a/src/Makefile.am b/src/Makefile.am
index cfad7df..c66f9e5 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -22,6 +22,7 @@
 # Libraries
 SUBDIRS = \
 	libcommon \
+	libvlr \
 	libmgcp \
 	libbsc \
 	libmsc \
diff --git a/src/libmsc/subscr_conn.c b/src/libmsc/subscr_conn.c
new file mode 100644
index 0000000..91ffe40
--- /dev/null
+++ b/src/libmsc/subscr_conn.c
@@ -0,0 +1,269 @@
+/* MSC subscriber connection implementation */
+
+/*
+ * (C) 2016 by sysmocom s.m.f.c. <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr
+ *
+ * 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/logging.h>
+#include <osmocom/core/fsm.h>
+
+#include <openbsc/osmo_msc.h>
+#include <openbsc/vlr.h>
+#include <openbsc/debug.h>
+#include <openbsc/transaction.h>
+
+static const struct value_string subscr_conn_fsm_event_names[] = {
+	OSMO_VALUE_STRING(SUBSCR_CONN_E_INVALID),
+	OSMO_VALUE_STRING(SUBSCR_CONN_E_ACCEPTED),
+	OSMO_VALUE_STRING(SUBSCR_CONN_E_BUMP),
+	OSMO_VALUE_STRING(SUBSCR_CONN_E_MO_CLOSE),
+	OSMO_VALUE_STRING(SUBSCR_CONN_E_CN_CLOSE),
+	OSMO_VALUE_STRING(SUBSCR_CONN_E_CLOSE_CONF),
+	{ 0, NULL }
+};
+
+const struct value_string subscr_conn_from_names[] = {
+	OSMO_VALUE_STRING(SUBSCR_CONN_FROM_INVALID),
+	OSMO_VALUE_STRING(SUBSCR_CONN_FROM_LU),
+	OSMO_VALUE_STRING(SUBSCR_CONN_FROM_CM_SERVICE_REQ),
+	OSMO_VALUE_STRING(SUBSCR_CONN_FROM_PAGING_RESP),
+	{ 0, NULL }
+};
+
+static void paging_resp(struct gsm_subscriber_connection *conn,
+			       enum gsm_paging_event pe)
+{
+	subscr_paging_dispatch(GSM_HOOK_RR_PAGING, pe, NULL, conn, conn->subscr);
+}
+
+void subscr_conn_fsm_new(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gsm_subscriber_connection *conn = fi->priv;
+	enum subscr_conn_from from = SUBSCR_CONN_FROM_INVALID;
+	enum gsm_paging_event pe;
+
+	if (data) {
+		from = *(enum subscr_conn_from*)data;
+		LOGPFSM(fi, "%s\n", subscr_conn_from_name(from));
+	}
+
+	/* If accepted, transition the state, all other cases mean failure. */
+	switch (event) {
+	case SUBSCR_CONN_E_ACCEPTED:
+		osmo_fsm_inst_state_chg(fi, SUBSCR_CONN_S_ACCEPTED, 0, 0);
+		break;
+
+	case SUBSCR_CONN_E_MO_CLOSE:
+	case SUBSCR_CONN_E_CN_CLOSE:
+	case SUBSCR_CONN_E_CLOSE_CONF:
+		break;
+
+	default:
+		LOGPFSM(fi, "Unexpected event: %d %s\n",
+			event, osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+
+	/* if appropriate, signal paging success or failure */
+	if (from == SUBSCR_CONN_FROM_PAGING_RESP) {
+		pe = (fi->state == SUBSCR_CONN_S_ACCEPTED)?
+			GSM_PAGING_SUCCEEDED : GSM_PAGING_EXPIRED;
+		paging_resp(conn, pe);
+	}
+
+	/* On failure, discard the conn */
+	if (fi->state != SUBSCR_CONN_S_ACCEPTED) {
+		/* TODO: on MO_CLOSE or CN_CLOSE, first go to RELEASING and
+		 * await BSC confirmation? */
+		osmo_fsm_inst_state_chg(fi, SUBSCR_CONN_S_RELEASED, 0, 0);
+		return;
+	}
+
+	/* On success, handle pending requests and/or close conn */
+
+	if (from == SUBSCR_CONN_FROM_CM_SERVICE_REQ) {
+		conn->received_cm_service_request = true;
+		LOGPFSM(fi, "received_cm_service_request = true\n");
+	}
+
+	osmo_fsm_inst_dispatch(fi, SUBSCR_CONN_E_BUMP, data);
+}
+
+#if 0
+	case SUBSCR_CONN_E_PARQ_SUCCESS:
+		osmo_fsm_inst_state_chg(fi, SUBSCR_CONN_S_ACCEPTED, 0, 0);
+		accept_conn = true;
+		/* fall through */
+	case SUBSCR_CONN_E_PARQ_FAILURE:
+		parq_type = data ? *(enum vlr_parq_type*)data : VLR_PR_ARQ_T_INVALID;
+		switch (parq_type) {
+
+		case VLR_PR_ARQ_T_CM_SERV_REQ:
+			accept_conn = handle_cm_serv_result(fi, accept_conn);
+			break;
+
+		case VLR_PR_ARQ_T_PAGING_RESP:
+			accept_conn = handle_paging_result(fi, accept_conn);
+			break;
+
+		default:
+			LOGPFSML(fi, LOGL_ERROR,
+				 "Invalid VLR Process Access Request type"
+				 " %d\n", parq_type);
+			accept_conn = false;
+			break;
+		}
+		break;
+#endif
+
+static void subscr_conn_fsm_bump(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct gsm_subscriber_connection *conn = fi->priv;
+
+	if (conn->silent_call)
+		return;
+
+	if (conn->received_cm_service_request)
+		return;
+
+	/* is this needed? */
+	if (conn->subscr && !llist_empty(&conn->subscr->requests))
+		return;
+
+	if (trans_has_conn(conn))
+		return;
+
+	osmo_fsm_inst_state_chg(fi, SUBSCR_CONN_S_RELEASED, 0, 0);
+}
+
+static void subscr_conn_fsm_accepted(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	switch (event) {
+	case SUBSCR_CONN_E_BUMP:
+		subscr_conn_fsm_bump(fi, event, data);
+		return;
+
+	default:
+		break;
+	}
+	/* Whatever unexpected happens in the accepted state, it means release.
+	 * Even if an unexpected event is passed, the safest thing to do is
+	 * discard the conn. We don't expect another SUBSCR_CONN_E_ACCEPTED. */
+	osmo_fsm_inst_state_chg(fi, SUBSCR_CONN_S_RELEASED, 0, 0);
+}
+
+static void subscr_conn_fsm_release(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct gsm_subscriber_connection *conn = fi->priv;
+	if (!conn)
+		return;
+
+	/* temporary hack, see owned_by_msc */
+	if (!conn->owned_by_msc) {
+		DEBUGP(DMM, "%s leaving bsc_subscr_con_free() to bsc_api.c, owned_by_msc = false\n",
+		       subscr_name(conn->subscr));
+		return;
+	}
+
+	DEBUGP(DMM, "%s calling bsc_subscr_con_free(), owned_by_msc = true\n",
+	       subscr_name(conn->subscr));
+	gsm0808_clear(conn);
+	bsc_subscr_con_free(conn);
+}
+
+#define S(x)	(1 << (x))
+
+static const struct osmo_fsm_state subscr_conn_fsm_states[] = {
+	[SUBSCR_CONN_S_NEW] = {
+		.name = OSMO_STRINGIFY(SUBSCR_CONN_S_NEW),
+		.in_event_mask = S(SUBSCR_CONN_E_ACCEPTED) |
+				 S(SUBSCR_CONN_E_MO_CLOSE) |
+				 S(SUBSCR_CONN_E_CN_CLOSE) |
+				 S(SUBSCR_CONN_E_CLOSE_CONF),
+		.out_state_mask = S(SUBSCR_CONN_S_ACCEPTED) |
+				  S(SUBSCR_CONN_S_RELEASED),
+		.action = subscr_conn_fsm_new,
+	},
+	[SUBSCR_CONN_S_ACCEPTED] = {
+		.name = OSMO_STRINGIFY(SUBSCR_CONN_S_ACCEPTED),
+		/* allow everything to release for any odd behavior */
+		.in_event_mask = S(SUBSCR_CONN_E_ACCEPTED) |
+				 S(SUBSCR_CONN_E_BUMP) |
+				 S(SUBSCR_CONN_E_MO_CLOSE) |
+				 S(SUBSCR_CONN_E_CN_CLOSE) |
+				 S(SUBSCR_CONN_E_CLOSE_CONF),
+		.out_state_mask = S(SUBSCR_CONN_S_RELEASED),
+		.action = subscr_conn_fsm_accepted,
+	},
+	[SUBSCR_CONN_S_RELEASED] = {
+		.name = OSMO_STRINGIFY(SUBSCR_CONN_S_RELEASED),
+		.onenter = subscr_conn_fsm_release,
+	},
+};
+
+static struct osmo_fsm subscr_conn_fsm = {
+	.name = "Subscr_Conn",
+	.states = subscr_conn_fsm_states,
+	.num_states = ARRAY_SIZE(subscr_conn_fsm_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = subscr_conn_fsm_event_names,
+};
+
+int msc_create_conn_fsm(struct gsm_subscriber_connection *conn, const char *id)
+{
+	struct osmo_fsm_inst *fi;
+	OSMO_ASSERT(conn);
+
+	if (conn->conn_fsm) {
+		LOGP(DMM, LOGL_ERROR,
+		     "%s: Error: connection already in use\n", id);
+		return -EINVAL;
+	}
+
+	fi = osmo_fsm_inst_alloc(&subscr_conn_fsm, conn, conn, LOGL_DEBUG, id);
+
+	if (!fi) {
+		LOGP(DMM, LOGL_ERROR,
+		     "%s: Failed to allocate subscr conn master FSM\n", id);
+		return -ENOMEM;
+	}
+	conn->conn_fsm = fi;
+	return 0;
+}
+
+bool msc_subscr_conn_is_accepted(struct gsm_subscriber_connection *conn)
+{
+	if (!conn)
+		return false;
+	if (!conn->subscr)
+		return false;
+	if (!conn->conn_fsm)
+		return false;
+	if (conn->conn_fsm->state != SUBSCR_CONN_S_ACCEPTED)
+		return false;
+	return true;
+}
+
+void msc_subscr_conn_init(void)
+{
+	osmo_fsm_register(&subscr_conn_fsm);
+}
diff --git a/src/libvlr/Makefile.am b/src/libvlr/Makefile.am
new file mode 100644
index 0000000..17ad411
--- /dev/null
+++ b/src/libvlr/Makefile.am
@@ -0,0 +1,19 @@
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)
+AM_CFLAGS=-Wall $(LIBOSMOCORE_CFLAGS) $(LIBOSMOVTY_CFLAGS) \
+	$(COVERAGE_CFLAGS) $(LIBCRYPTO_CFLAGS)
+
+noinst_HEADERS = \
+	vlr_access_req_fsm.h \
+	vlr_auth_fsm.h \
+	vlr_core.h \
+	vlr_lu_fsm.h \
+	$(NULL)
+
+noinst_LIBRARIES = libvlr.a
+
+libvlr_a_SOURCES = \
+	vlr.c \
+	vlr_access_req_fsm.c \
+	vlr_auth_fsm.c \
+	vlr_lu_fsm.c \
+	$(NULL)
diff --git a/src/libvlr/vlr.c b/src/libvlr/vlr.c
new file mode 100644
index 0000000..0e0d31c
--- /dev/null
+++ b/src/libvlr/vlr.c
@@ -0,0 +1,1108 @@
+/* Osmocom Visitor Location Register (VLR) code base */
+
+/* (C) 2016 by Harald Welte <laforge at 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 <osmocom/core/linuxlist.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+#include <osmocom/gsm/gsup.h>
+#include <osmocom/gsm/apn.h>
+#include <openbsc/gsm_subscriber.h>
+#include <openbsc/gsup_client.h>
+#include <openbsc/vlr.h>
+#include <openbsc/gprs_sgsn.h>
+#include <openbsc/gprs_utils.h>
+#include <openbsc/debug.h>
+
+#include <openssl/rand.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <limits.h>
+
+#include "vlr_core.h"
+#include "vlr_auth_fsm.h"
+#include "vlr_lu_fsm.h"
+#include "vlr_access_req_fsm.h"
+
+#define SGSN_SUBSCR_MAX_RETRIES 3
+#define SGSN_SUBSCR_RETRY_INTERVAL 10
+
+/***********************************************************************
+ * Convenience functions
+ ***********************************************************************/
+
+const struct value_string vlr_ciph_names[] = {
+	OSMO_VALUE_STRING(VLR_CIPH_NONE),
+	OSMO_VALUE_STRING(VLR_CIPH_A5_1),
+	OSMO_VALUE_STRING(VLR_CIPH_A5_2),
+	OSMO_VALUE_STRING(VLR_CIPH_A5_3),
+	{ 0, NULL }
+};
+
+uint32_t vlr_timer(struct vlr_instance *vlr, uint32_t timer)
+{
+	uint32_t tidx = 0xffffffff;
+
+	switch (timer) {
+	case 3270:
+		tidx = VLR_T_3270;
+		break;
+	case 3260:
+		tidx = VLR_T_3260;
+		break;
+	case 3250:
+		tidx = VLR_T_3250;
+		break;
+	}
+
+	OSMO_ASSERT(tidx < sizeof(vlr->cfg.timer));
+	return vlr->cfg.timer[tidx];
+}
+
+struct vlr_subscr *_vlr_subscr_find_by_imsi(struct vlr_instance *vlr,
+					    const char *imsi,
+					    const char *file, int line)
+{
+	struct vlr_subscr *vsub;
+
+	if (!imsi || !*imsi)
+		return NULL;
+
+	llist_for_each_entry(vsub, &vlr->subscribers, list) {
+		if (vlr_subscr_matches_imsi(vsub, imsi))
+			return _vlr_subscr_get(vsub, file, line);
+	}
+	return NULL;
+}
+
+struct vlr_subscr *_vlr_subscr_find_by_tmsi(struct vlr_instance *vlr,
+					    uint32_t tmsi,
+					    const char *file, int line)
+{
+	struct vlr_subscr *vsub;
+
+	if (tmsi == GSM_RESERVED_TMSI)
+		return NULL;
+
+	llist_for_each_entry(vsub, &vlr->subscribers, list) {
+		if (vlr_subscr_matches_tmsi(vsub, tmsi))
+			return _vlr_subscr_get(vsub, file, line);
+	}
+	return NULL;
+}
+
+struct vlr_subscr *_vlr_subscr_find_by_msisdn(struct vlr_instance *vlr,
+					      const char *msisdn,
+					      const char *file, int line)
+{
+	struct vlr_subscr *vsub;
+
+	if (!msisdn || !*msisdn)
+		return NULL;
+
+	llist_for_each_entry(vsub, &vlr->subscribers, list) {
+		if (vlr_subscr_matches_msisdn(vsub, msisdn))
+			return _vlr_subscr_get(vsub, file, line);
+	}
+	return NULL;
+}
+
+/* Transmit GSUP message to HLR */
+static int vlr_tx_gsup_message(struct vlr_instance *vlr,
+			       struct osmo_gsup_message *gsup_msg)
+{
+	struct msgb *msg = gsup_client_msgb_alloc();
+
+	osmo_gsup_encode(msg, gsup_msg);
+
+	if (!vlr->gsup_client) {
+		LOGP(DVLR, LOGL_NOTICE, "GSUP link is down, cannot "
+			"send GSUP: %s\n", msgb_hexdump(msg));
+		msgb_free(msg);
+		return -ENOTSUP;
+	}
+
+	LOGP(DVLR, LOGL_DEBUG, "GSUP tx: %s\n",
+	     osmo_hexdump_nospc(msg->data, msg->len));
+
+	return gsup_client_send(vlr->gsup_client, msg);
+}
+
+/* Transmit GSUP message for subscriber to HLR, using IMSI from subscriber */
+static int vlr_subscr_tx_gsup_message(struct vlr_subscr *vsub,
+				      struct osmo_gsup_message *gsup_msg)
+{
+	struct vlr_instance *vlr = vsub->vlr;
+
+	if (strlen(gsup_msg->imsi) == 0)
+		osmo_strlcpy(gsup_msg->imsi, vsub->imsi, sizeof(gsup_msg->imsi));
+
+	return vlr_tx_gsup_message(vlr, gsup_msg);
+}
+
+/* Transmit GSUP error in response to original message */
+static int vlr_tx_gsup_error_reply(struct vlr_instance *vlr,
+				   struct osmo_gsup_message *gsup_orig,
+				   enum gsm48_gmm_cause cause)
+{
+	struct osmo_gsup_message gsup_reply = {0};
+
+	osmo_strlcpy(gsup_reply.imsi, gsup_orig->imsi, sizeof(gsup_reply.imsi));
+	gsup_reply.cause = cause;
+	gsup_reply.message_type =
+		OSMO_GSUP_TO_MSGT_ERROR(gsup_orig->message_type);
+
+	return vlr_tx_gsup_message(vlr, &gsup_reply);
+}
+
+struct vlr_subscr *_vlr_subscr_get(struct vlr_subscr *sub, const char *file, int line)
+{
+	if (!sub)
+		return NULL;
+	OSMO_ASSERT(sub->use_count < INT_MAX);
+	sub->use_count++;
+	LOGPSRC(DREF, LOGL_DEBUG, file, line,
+		"VLR subscr %s usage increases to: %d\n",
+		vlr_subscr_name(sub), sub->use_count);
+	return sub;
+}
+
+struct vlr_subscr *_vlr_subscr_put(struct vlr_subscr *sub, const char *file, int line)
+{
+	if (!sub)
+		return NULL;
+	sub->use_count--;
+	LOGPSRC(DREF, sub->use_count >= 0? LOGL_DEBUG : LOGL_ERROR,
+		file, line,
+		"VLR subscr %s usage decreases to: %d\n",
+		vlr_subscr_name(sub), sub->use_count);
+	if (sub->use_count <= 0)
+		vlr_subscr_free(sub);
+	return NULL;
+}
+
+/* Allocate a new subscriber and insert it into list */
+static struct vlr_subscr *_vlr_subscr_alloc(struct vlr_instance *vlr)
+{
+	struct vlr_subscr *vsub;
+	int i;
+
+	vsub = talloc_zero(vlr, struct vlr_subscr);
+	vsub->vlr = vlr;
+	vsub->tmsi = GSM_RESERVED_TMSI;
+	vsub->tmsi_new = GSM_RESERVED_TMSI;
+
+	for (i = 0; i < ARRAY_SIZE(vsub->auth_tuples); i++)
+		vsub->auth_tuples[i].key_seq = GSM_KEY_SEQ_INVAL;
+
+	INIT_LLIST_HEAD(&vsub->cs.requests);
+	INIT_LLIST_HEAD(&vsub->ps.pdp_list);
+
+	llist_add_tail(&vsub->list, &vlr->subscribers);
+	return vsub;
+}
+
+struct vlr_subscr *vlr_subscr_alloc(struct vlr_instance *vlr)
+{
+	return vlr_subscr_get(_vlr_subscr_alloc(vlr));
+}
+
+/* Send a GSUP Purge MS request.
+ * TODO: this should be sent to the *previous* VLR when this VLR is "taking"
+ * this subscriber, not to the HLR? */
+int vlr_subscr_purge(struct vlr_subscr *vsub)
+{
+	struct osmo_gsup_message gsup_msg = {0};
+
+	gsup_msg.message_type = OSMO_GSUP_MSGT_PURGE_MS_REQUEST;
+
+	/* provide HLR number in case we know it */
+	gsup_msg.hlr_enc_len = vsub->hlr.len;
+	gsup_msg.hlr_enc = vsub->hlr.buf;
+
+	return vlr_subscr_tx_gsup_message(vsub, &gsup_msg);
+}
+
+void vlr_subscr_cancel(struct vlr_subscr *vsub, enum gsm48_gmm_cause cause)
+{
+	if (!vsub)
+		return;
+
+	if (vsub->lu_fsm) {
+		if (vsub->lu_fsm->state == VLR_ULA_S_WAIT_HLR_UPD)
+			osmo_fsm_inst_dispatch(vsub->lu_fsm,
+					       VLR_ULA_E_HLR_LU_RES,
+					       (void*)&cause);
+		else
+			osmo_fsm_inst_term(vsub->lu_fsm, OSMO_FSM_TERM_ERROR,
+					   0);
+	}
+
+	if (vsub->proc_arq_fsm)
+		osmo_fsm_inst_term(vsub->proc_arq_fsm, OSMO_FSM_TERM_ERROR, 0);
+}
+
+/* Call vlr_subscr_cancel(), then completely drop the entry from the VLR */
+void vlr_subscr_free(struct vlr_subscr *vsub)
+{
+	llist_del(&vsub->list);
+	DEBUGP(DREF, "freeing VLR subscr %s\n", vlr_subscr_name(vsub));
+	talloc_free(vsub);
+}
+
+/* Generate a new TMSI and store in vsub->tmsi_new.
+ * Search all known subscribers to ensure that the TMSI is unique. */
+int vlr_subscr_alloc_tmsi(struct vlr_subscr *vsub)
+{
+	struct vlr_instance *vlr = vsub->vlr;
+	uint32_t tmsi;
+	int tried;
+
+	for (tried = 0; tried < 100; tried++) {
+		if (RAND_bytes((uint8_t *) &tmsi, sizeof(tmsi)) != 1) {
+			LOGP(DVLR, LOGL_ERROR, "RAND_bytes failed\n");
+			return -1;
+		}
+		/* throw the dice again, if the TSMI doesn't fit */
+		if (tmsi == GSM_RESERVED_TMSI)
+			continue;
+
+		/* Section 2.4 of 23.003: MSC has two MSB 00/01/10, SGSN 11 */
+		if (vlr->cfg.is_ps) {
+			/* SGSN */
+			tmsi |= 0xC000000;
+		} else {
+			/* MSC */
+			if ((tmsi & 0xC0000000) == 0xC0000000)
+				tmsi &= ~0xC0000000;
+		}
+
+		/* If this TMSI is already in use, try another one. */
+		if (vlr_subscr_find_by_tmsi(vlr, tmsi))
+			continue;
+
+		vsub->tmsi_new = tmsi;
+		return 0;
+	}
+
+	LOGP(DVLR, LOGL_ERROR, "subscr %s: unable to generate valid TMSI"
+	     " after %d tries\n", vlr_subscr_name(vsub), tried);
+	return -1;
+}
+
+/* Find subscriber by IMSI, or create new subscriber if not found.
+ * \param[in] vlr  VLR instace.
+ * \param[in] imsi  IMSI string.
+ * \param[out] created  if non-NULL, returns whether a new entry was created. */
+struct vlr_subscr *_vlr_subscr_find_or_create_by_imsi(struct vlr_instance *vlr,
+						      const char *imsi,
+						      bool *created,
+						      const char *file,
+						      int line)
+{
+	struct vlr_subscr *vsub;
+	vsub = _vlr_subscr_find_by_imsi(vlr, imsi, file, line);
+	if (vsub) {
+		if (created)
+			*created = false;
+		return vsub;
+	}
+
+	vsub = _vlr_subscr_get(_vlr_subscr_alloc(vlr), file, line);
+	if (!vsub)
+		return NULL;
+	vlr_subscr_set_imsi(vsub, imsi);
+	LOGP(DVLR, LOGL_INFO, "New subscr, IMSI: %s\n", vsub->imsi);
+	if (created)
+		*created = true;
+	return vsub;
+}
+
+/* Find subscriber by TMSI, or create new subscriber if not found.
+ * \param[in] vlr  VLR instace.
+ * \param[in] tmsi  TMSI.
+ * \param[out] created  if non-NULL, returns whether a new entry was created. */
+struct vlr_subscr *_vlr_subscr_find_or_create_by_tmsi(struct vlr_instance *vlr,
+						      uint32_t tmsi,
+						      bool *created,
+						      const char *file,
+						      int line)
+{
+	struct vlr_subscr *vsub;
+	vsub = _vlr_subscr_find_by_tmsi(vlr, tmsi, file, line);
+	if (vsub) {
+		if (created)
+			*created = false;
+		return vsub;
+	}
+
+	vsub = _vlr_subscr_get(_vlr_subscr_alloc(vlr), file, line);
+	if (!vsub)
+		return NULL;
+	vsub->tmsi = tmsi;
+	LOGP(DVLR, LOGL_INFO, "New subscr, TMSI: 0x%08x\n", vsub->tmsi);
+	if (created)
+		*created = true;
+	return vsub;
+}
+
+void vlr_subscr_set_imsi(struct vlr_subscr *vsub, const char *imsi)
+{
+	if (!vsub)
+		return;
+	osmo_strlcpy(vsub->imsi, imsi, sizeof(vsub->imsi));
+	vsub->id = atoll(vsub->imsi);
+	DEBUGP(DVLR, "set IMSI on subscriber; IMSI=%s id=%llu\n",
+	       vsub->imsi, vsub->id);
+}
+
+void vlr_subscr_set_imei(struct vlr_subscr *vsub, const char *imei)
+{
+	if (!vsub)
+		return;
+	osmo_strlcpy(vsub->imei, imei, sizeof(vsub->imei));
+	DEBUGP(DVLR, "set IMEI on subscriber; IMSI=%s IMEI=%s\n",
+	       vsub->imsi, vsub->imei);
+}
+
+void vlr_subscr_set_imeisv(struct vlr_subscr *vsub, const char *imeisv)
+{
+	if (!vsub)
+		return;
+	osmo_strlcpy(vsub->imeisv, imeisv, sizeof(vsub->imeisv));
+	DEBUGP(DVLR, "set IMEISV on subscriber; IMSI=%s IMEISV=%s\n",
+	       vsub->imsi, vsub->imeisv);
+}
+
+/* Safely copy the given MSISDN string to vsub->msisdn */
+void vlr_subscr_set_msisdn(struct vlr_subscr *vsub, const char *msisdn)
+{
+	if (!vsub)
+		return;
+	osmo_strlcpy(vsub->msisdn, msisdn, sizeof(vsub->msisdn));
+	DEBUGP(DVLR, "set MSISDN on subscriber; IMSI=%s MSISDN=%s\n",
+	       vsub->imsi, vsub->msisdn);
+}
+
+bool vlr_subscr_matches_imsi(struct vlr_subscr *vsub, const char *imsi)
+{
+	return vsub && imsi && vsub->imsi[0] && !strcmp(vsub->imsi, imsi);
+}
+
+bool vlr_subscr_matches_tmsi(struct vlr_subscr *vsub, uint32_t tmsi)
+{
+	return vsub && tmsi != GSM_RESERVED_TMSI
+		&& (vsub->tmsi == tmsi || vsub->tmsi_new == tmsi);
+}
+
+bool vlr_subscr_matches_msisdn(struct vlr_subscr *vsub, const char *msisdn)
+{
+	return vsub && msisdn && vsub->msisdn[0]
+		&& !strcmp(vsub->msisdn, msisdn);
+}
+
+bool vlr_subscr_matches_imei(struct vlr_subscr *vsub, const char *imei)
+{
+	return vsub && imei && vsub->imei[0]
+		&& !strcmp(vsub->imei, imei);
+}
+
+/* Send updated subscriber information to HLR */
+int vlr_subscr_changed(struct vlr_subscr *vsub)
+{
+	/* FIXME */
+	LOGP(DVLR, LOGL_ERROR, "Not implemented: %s\n", __func__);
+	return 0;
+}
+
+/***********************************************************************
+ * PDP context data
+ ***********************************************************************/
+
+struct sgsn_subscriber_pdp_data *
+vlr_subscr_pdp_data_alloc(struct vlr_subscr *vsub)
+{
+	struct sgsn_subscriber_pdp_data* pdata;
+
+	pdata = talloc_zero(vsub, struct sgsn_subscriber_pdp_data);
+
+	llist_add_tail(&pdata->list, &vsub->ps.pdp_list);
+
+	return pdata;
+}
+
+static int vlr_subscr_pdp_data_clear(struct vlr_subscr *vsub)
+{
+	struct sgsn_subscriber_pdp_data *pdp, *pdp2;
+	int count = 0;
+
+	llist_for_each_entry_safe(pdp, pdp2, &vsub->ps.pdp_list, list) {
+		llist_del(&pdp->list);
+		talloc_free(pdp);
+		count += 1;
+	}
+
+	return count;
+}
+
+static struct sgsn_subscriber_pdp_data *
+vlr_subscr_pdp_data_get_by_id(struct vlr_subscr *vsub, unsigned context_id)
+{
+	struct sgsn_subscriber_pdp_data *pdp;
+
+	llist_for_each_entry(pdp, &vsub->ps.pdp_list, list) {
+		if (pdp->context_id == context_id)
+			return pdp;
+	}
+
+	return NULL;
+}
+
+/***********************************************************************
+ * Actual Implementation
+ ***********************************************************************/
+
+static int vlr_rx_gsup_unknown_imsi(struct vlr_instance *vlr,
+				   struct osmo_gsup_message *gsup_msg)
+{
+	if (OSMO_GSUP_IS_MSGT_REQUEST(gsup_msg->message_type)) {
+		vlr_tx_gsup_error_reply(vlr, gsup_msg,
+					GMM_CAUSE_IMSI_UNKNOWN);
+		LOGP(DVLR, LOGL_NOTICE,
+		     "Unknown IMSI %s, discarding GSUP request "
+		     "of type 0x%02x\n",
+		     gsup_msg->imsi, gsup_msg->message_type);
+	} else if (OSMO_GSUP_IS_MSGT_ERROR(gsup_msg->message_type)) {
+		LOGP(DVLR, LOGL_NOTICE,
+		     "Unknown IMSI %s, discarding GSUP error "
+		     "of type 0x%02x, cause '%s' (%d)\n",
+		     gsup_msg->imsi, gsup_msg->message_type,
+		     get_value_string(gsm48_gmm_cause_names, gsup_msg->cause),
+		     gsup_msg->cause);
+	} else {
+		LOGP(DVLR, LOGL_NOTICE,
+		     "Unknown IMSI %s, discarding GSUP response "
+		     "of type 0x%02x\n",
+		     gsup_msg->imsi, gsup_msg->message_type);
+	}
+
+	return -GMM_CAUSE_IMSI_UNKNOWN;
+}
+
+static int vlr_rx_gsup_purge_no_subscr(struct vlr_instance *vlr,
+				struct osmo_gsup_message *gsup_msg)
+{
+	if (OSMO_GSUP_IS_MSGT_ERROR(gsup_msg->message_type)) {
+		LOGGSUPP(LOGL_NOTICE, gsup_msg,
+			 "Purge MS has failed with cause '%s' (%d)\n",
+			 get_value_string(gsm48_gmm_cause_names, gsup_msg->cause),
+			 gsup_msg->cause);
+		return -gsup_msg->cause;
+	}
+	LOGGSUPP(LOGL_INFO, gsup_msg, "Completing purge MS\n");
+	return 0;
+}
+
+/* VLR internal call to request UpdateLocation from HLR */
+int vlr_subscr_req_lu(struct vlr_subscr *vsub, bool is_ps)
+{
+	struct osmo_gsup_message gsup_msg = {0};
+	int rc;
+
+	gsup_msg.message_type = OSMO_GSUP_MSGT_UPDATE_LOCATION_REQUEST;
+	rc = vlr_subscr_tx_gsup_message(vsub, &gsup_msg);
+
+	return rc;
+}
+
+/* VLR internal call to request tuples from HLR */
+int vlr_subscr_req_sai(struct vlr_subscr *vsub,
+		       const uint8_t *auts, const uint8_t *auts_rand)
+{
+	struct osmo_gsup_message gsup_msg = {0};
+
+	gsup_msg.message_type = OSMO_GSUP_MSGT_SEND_AUTH_INFO_REQUEST;
+	gsup_msg.auts = auts;
+	gsup_msg.rand = auts_rand;
+
+	return vlr_subscr_tx_gsup_message(vsub, &gsup_msg);
+}
+
+/* Tell HLR that authentication failure occurred */
+int vlr_subscr_tx_auth_fail_rep(struct vlr_subscr *vsub)
+{
+	struct osmo_gsup_message gsup_msg = {0};
+
+	gsup_msg.message_type = OSMO_GSUP_MSGT_AUTH_FAIL_REPORT;
+	osmo_strlcpy(gsup_msg.imsi, vsub->imsi, sizeof(gsup_msg.imsi));
+	return vlr_tx_gsup_message(vsub->vlr, &gsup_msg);
+}
+
+/* Update the subscriber with GSUP-received auth tuples */
+void vlr_subscr_update_tuples(struct vlr_subscr *vsub,
+			      const struct osmo_gsup_message *gsup)
+{
+	unsigned int i;
+	unsigned int got_tuples;
+
+	if (gsup->num_auth_vectors) {
+		memset(&vsub->auth_tuples, 0, sizeof(vsub->auth_tuples));
+		for (i = 0; i < ARRAY_SIZE(vsub->auth_tuples); i++)
+			vsub->auth_tuples[i].key_seq = GSM_KEY_SEQ_INVAL;
+	}
+
+	got_tuples = 0;
+	for (i = 0; i < gsup->num_auth_vectors; i++) {
+		size_t key_seq = i;
+
+		if (key_seq >= ARRAY_SIZE(vsub->auth_tuples)) {
+			LOGVSUBP(LOGL_NOTICE, vsub,
+				"Skipping auth tuple wih invalid cksn %zu\n",
+				key_seq);
+			continue;
+		}
+		vsub->auth_tuples[i].vec = gsup->auth_vectors[i];
+		vsub->auth_tuples[i].key_seq = key_seq;
+		got_tuples ++;
+	}
+
+	LOGVSUBP(LOGL_DEBUG, vsub, "Received %u auth tuples\n", got_tuples);
+
+	if (!got_tuples) {
+		/* FIXME what now? */
+		// vlr_subscr_cancel(vsub, GMM_CAUSE_GSM_AUTH_UNACCEPT); ?
+	}
+
+	/* New tuples means last_tuple becomes invalid */
+	vsub->last_tuple = NULL;
+}
+
+/* Handle SendAuthInfo Result/Error from HLR */
+static int vlr_subscr_handle_sai_res(struct vlr_subscr *vsub,
+				     const struct osmo_gsup_message *gsup)
+{
+	struct osmo_fsm_inst *auth_fi = vsub->auth_fsm;
+	void *data = (void *) gsup;
+
+	switch (gsup->message_type) {
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_RESULT:
+		osmo_fsm_inst_dispatch(auth_fi, VLR_AUTH_E_HLR_SAI_ACK, data);
+		break;
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_ERROR:
+		osmo_fsm_inst_dispatch(auth_fi, VLR_AUTH_E_HLR_SAI_NACK, data);
+		break;
+	default:
+		return -1;
+	}
+
+	return 0;
+}
+
+static int decode_bcd_number_safe(char *output, int output_len,
+				  const uint8_t *bcd_lv, int input_len,
+				  int h_len)
+{
+	uint8_t len;
+	OSMO_ASSERT(output_len >= 1);
+	*output = '\0';
+	if (input_len < 1)
+		return -EIO;
+	len = bcd_lv[0];
+	if (input_len < len)
+		return -EIO;
+	return gsm48_decode_bcd_number(output, output_len, bcd_lv, h_len);
+}
+
+static void vlr_subscr_gsup_insert_data(struct vlr_subscr *vsub,
+					const struct osmo_gsup_message *gsup_msg)
+{
+	unsigned idx;
+	int rc;
+
+	if (gsup_msg->msisdn_enc) {
+		decode_bcd_number_safe(vsub->msisdn, sizeof(vsub->msisdn),
+				       gsup_msg->msisdn_enc,
+				       gsup_msg->msisdn_enc_len, 0);
+		LOGP(DVLR, LOGL_DEBUG, "IMSI:%s has MSISDN:%s\n",
+		     vsub->imsi, vsub->msisdn);
+	}
+
+	if (gsup_msg->hlr_enc) {
+		if (gsup_msg->hlr_enc_len > sizeof(vsub->hlr.buf)) {
+			LOGP(DVLR, LOGL_ERROR, "HLR-Number too long (%zu)\n",
+				gsup_msg->hlr_enc_len);
+			vsub->hlr.len = 0;
+		} else {
+			memcpy(vsub->hlr.buf, gsup_msg->hlr_enc,
+				gsup_msg->hlr_enc_len);
+			vsub->hlr.len = gsup_msg->hlr_enc_len;
+		}
+	}
+
+	if (gsup_msg->pdp_info_compl) {
+		rc = vlr_subscr_pdp_data_clear(vsub);
+		if (rc > 0)
+			LOGP(DVLR, LOGL_INFO, "Cleared existing PDP info\n");
+	}
+
+	for (idx = 0; idx < gsup_msg->num_pdp_infos; idx++) {
+		const struct osmo_gsup_pdp_info *pdp_info = &gsup_msg->pdp_infos[idx];
+		size_t ctx_id = pdp_info->context_id;
+		struct sgsn_subscriber_pdp_data *pdp_data;
+
+		if (pdp_info->apn_enc_len >= sizeof(pdp_data->apn_str)-1) {
+			LOGVSUBP(LOGL_ERROR, vsub,
+			     "APN too long, context id = %zu, APN = %s\n",
+			     ctx_id, osmo_hexdump(pdp_info->apn_enc,
+						  pdp_info->apn_enc_len));
+			continue;
+		}
+
+		if (pdp_info->qos_enc_len > sizeof(pdp_data->qos_subscribed)) {
+			LOGVSUBP(LOGL_ERROR, vsub,
+				"QoS info too long (%zu)\n",
+				pdp_info->qos_enc_len);
+			continue;
+		}
+
+		LOGVSUBP(LOGL_INFO, vsub,
+		     "Will set PDP info, context id = %zu, APN = %s\n",
+		     ctx_id, osmo_hexdump(pdp_info->apn_enc, pdp_info->apn_enc_len));
+
+		/* Set PDP info [ctx_id] */
+		pdp_data = vlr_subscr_pdp_data_get_by_id(vsub, ctx_id);
+		if (!pdp_data) {
+			pdp_data = vlr_subscr_pdp_data_alloc(vsub);
+			pdp_data->context_id = ctx_id;
+		}
+
+		OSMO_ASSERT(pdp_data != NULL);
+		pdp_data->pdp_type = pdp_info->pdp_type;
+		osmo_apn_to_str(pdp_data->apn_str,
+				pdp_info->apn_enc, pdp_info->apn_enc_len);
+		memcpy(pdp_data->qos_subscribed, pdp_info->qos_enc, pdp_info->qos_enc_len);
+		pdp_data->qos_subscribed_len = pdp_info->qos_enc_len;
+	}
+}
+
+
+/* Handle InsertSubscrData Result from HLR */
+static int vlr_subscr_handle_isd_req(struct vlr_subscr *vsub,
+				     const struct osmo_gsup_message *gsup)
+{
+	struct osmo_gsup_message gsup_reply = {0};
+
+	vlr_subscr_gsup_insert_data(vsub, gsup);
+	vsub->vlr->ops.subscr_update(vsub);
+
+	gsup_reply.message_type = OSMO_GSUP_MSGT_INSERT_DATA_RESULT;
+	return vlr_subscr_tx_gsup_message(vsub, &gsup_reply);
+}
+
+/* Handle UpdateLocation Result from HLR */
+static int vlr_subscr_handle_lu_res(struct vlr_subscr *vsub,
+				    const struct osmo_gsup_message *gsup)
+{
+	if (!vsub->lu_fsm) {
+		LOGVSUBP(LOGL_ERROR, vsub, "Rx GSUP LU Result "
+			 "without LU in progress\n");
+		return -ENODEV;
+	}
+
+	/* contrary to MAP, we allow piggy-backing subscriber data onto the
+	 * UPDATE LOCATION RESULT, and don't mandate the use of a separate
+	 * nested INSERT SUBSCRIBER DATA transaction */
+	vlr_subscr_gsup_insert_data(vsub, gsup);
+
+	osmo_fsm_inst_dispatch(vsub->lu_fsm, VLR_ULA_E_HLR_LU_RES, NULL);
+
+	return 0;
+}
+
+/* Handle UpdateLocation Result from HLR */
+static int vlr_subscr_handle_lu_err(struct vlr_subscr *vsub,
+				    const struct osmo_gsup_message *gsup)
+{
+	if (!vsub->lu_fsm) {
+		LOGVSUBP(LOGL_ERROR, vsub, "Rx GSUP LU Error "
+			 "without LU in progress\n");
+		return -ENODEV;
+	}
+
+	LOGVSUBP(LOGL_DEBUG, vsub, "UpdateLocation failed; gmm_cause: %s\n",
+		 get_value_string(gsm48_gmm_cause_names, gsup->cause));
+
+	osmo_fsm_inst_dispatch(vsub->lu_fsm, VLR_ULA_E_HLR_LU_RES,
+				(void *)&gsup->cause);
+
+	return 0;
+}
+
+/* Handle LOCATION CANCEL request from HLR */
+static int vlr_subscr_handle_cancel_req(struct vlr_subscr *vsub,
+					struct osmo_gsup_message *gsup_msg)
+{
+	struct osmo_gsup_message gsup_reply = {0};
+	int is_update_procedure = !gsup_msg->cancel_type ||
+		gsup_msg->cancel_type == OSMO_GSUP_CANCEL_TYPE_UPDATE;
+
+	LOGVSUBP(LOGL_INFO, vsub, "Cancelling MS subscriber (%s)\n",
+		 is_update_procedure ?
+		 "update procedure" : "subscription withdraw");
+
+	gsup_reply.message_type = OSMO_GSUP_MSGT_LOCATION_CANCEL_RESULT;
+	vlr_subscr_tx_gsup_message(vsub, &gsup_reply);
+
+	vlr_subscr_cancel(vsub, gsup_msg->cause);
+
+	return 0;
+}
+
+/* Incoming handler for GSUP from HLR.
+ * Keep this function non-static for direct invocation by unit tests. */
+int vlr_gsupc_read_cb(struct gsup_client *gsupc, struct msgb *msg)
+{
+	struct vlr_instance *vlr = (struct vlr_instance *) gsupc->data;
+	struct vlr_subscr *vsub;
+	struct osmo_gsup_message gsup;
+	int rc;
+
+	DEBUGP(DVLR, "GSUP rx %u: %s\n", msgb_l2len(msg),
+	       osmo_hexdump_nospc(msgb_l2(msg), msgb_l2len(msg)));
+
+	rc = osmo_gsup_decode(msgb_l2(msg), msgb_l2len(msg), &gsup);
+	if (rc < 0) {
+		LOGP(DVLR, LOGL_ERROR,
+			"decoding GSUP message fails with error '%s' (%d)\n",
+			get_value_string(gsm48_gmm_cause_names, -rc), -rc);
+		return rc;
+	}
+
+	if (!gsup.imsi[0]) {
+		LOGP(DVLR, LOGL_ERROR, "Missing IMSI in GSUP message\n");
+		if (OSMO_GSUP_IS_MSGT_REQUEST(gsup.message_type))
+			vlr_tx_gsup_error_reply(vlr, &gsup,
+						GMM_CAUSE_INV_MAND_INFO);
+		return -GMM_CAUSE_INV_MAND_INFO;
+	}
+
+	vsub = vlr_subscr_find_by_imsi(vlr, gsup.imsi);
+	if (!vsub) {
+		switch (gsup.message_type) {
+		case OSMO_GSUP_MSGT_PURGE_MS_RESULT:
+		case OSMO_GSUP_MSGT_PURGE_MS_ERROR:
+			return vlr_rx_gsup_purge_no_subscr(vlr, &gsup);
+		default:
+			return vlr_rx_gsup_unknown_imsi(vlr, &gsup);
+		}
+	}
+
+	switch (gsup.message_type) {
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_RESULT:
+	case OSMO_GSUP_MSGT_SEND_AUTH_INFO_ERROR:
+		rc = vlr_subscr_handle_sai_res(vsub, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_INSERT_DATA_REQUEST:
+		rc = vlr_subscr_handle_isd_req(vsub, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_LOCATION_CANCEL_REQUEST:
+		rc = vlr_subscr_handle_cancel_req(vsub, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_RESULT:
+		rc = vlr_subscr_handle_lu_res(vsub, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_UPDATE_LOCATION_ERROR:
+		rc = vlr_subscr_handle_lu_err(vsub, &gsup);
+		break;
+	case OSMO_GSUP_MSGT_PURGE_MS_ERROR:
+	case OSMO_GSUP_MSGT_PURGE_MS_RESULT:
+	case OSMO_GSUP_MSGT_DELETE_DATA_REQUEST:
+		LOGVSUBP(LOGL_ERROR, vsub,
+			"Rx GSUP msg_type=%d not yet implemented\n",
+			gsup.message_type);
+		rc = -GMM_CAUSE_MSGT_NOTEXIST_NOTIMPL;
+		break;
+	default:
+		LOGVSUBP(LOGL_ERROR, vsub,
+			"Rx GSUP msg_type=%d not valid at VLR/SGSN side\n",
+			gsup.message_type);
+		rc = -GMM_CAUSE_MSGT_NOTEXIST_NOTIMPL;
+		break;
+	}
+
+	vlr_subscr_put(vsub);
+	return rc;
+}
+
+/* MSC->VLR: Subscriber has provided IDENTITY RESPONSE */
+int vlr_subscr_rx_id_resp(struct vlr_subscr *vsub,
+			  const uint8_t *mi, size_t mi_len)
+{
+	char mi_string[GSM48_MI_SIZE];
+	uint8_t mi_type = mi[0] & GSM_MI_TYPE_MASK;
+
+	gsm48_mi_to_string(mi_string, sizeof(mi_string), mi, mi_len);
+
+	/* update the vlr_subscr with the given identity */
+	switch (mi_type) {
+	case GSM_MI_TYPE_IMSI:
+		if (vsub->imsi[0]
+		    && !vlr_subscr_matches_imsi(vsub, mi_string)) {
+			LOGVSUBP(LOGL_ERROR, vsub, "IMSI in ID RESP differs:"
+				 " %s\n", mi_string);
+		} else
+			vlr_subscr_set_imsi(vsub, mi_string);
+		break;
+	case GSM_MI_TYPE_IMEI:
+		vlr_subscr_set_imei(vsub, mi_string);
+		break;
+	case GSM_MI_TYPE_IMEISV:
+		vlr_subscr_set_imeisv(vsub, mi_string);
+		break;
+	}
+
+	if (vsub->auth_fsm) {
+		switch (mi_type) {
+		case GSM_MI_TYPE_IMSI:
+			osmo_fsm_inst_dispatch(vsub->auth_fsm,
+					VLR_AUTH_E_MS_ID_IMSI, mi_string);
+			break;
+		}
+	}
+
+	if (vsub->lu_fsm) {
+		uint32_t event = 0;
+		switch (mi_type) {
+		case GSM_MI_TYPE_IMSI:
+			event = VLR_ULA_E_ID_IMSI;
+			break;
+		case GSM_MI_TYPE_IMEI:
+			event = VLR_ULA_E_ID_IMEI;
+			break;
+		case GSM_MI_TYPE_IMEISV:
+			event = VLR_ULA_E_ID_IMEISV;
+			break;
+		default:
+			OSMO_ASSERT(0);
+			break;
+		}
+		osmo_fsm_inst_dispatch(vsub->lu_fsm, event, mi_string);
+	} else {
+		LOGVSUBP(LOGL_NOTICE,  vsub, "gratuitous ID RESPONSE?!?\n");
+	}
+
+	return 0;
+}
+
+/* MSC->VLR: Subscriber has provided IDENTITY RESPONSE */
+int vlr_subscr_rx_tmsi_reall_compl(struct vlr_subscr *vsub)
+{
+	if (vsub->lu_fsm) {
+		return osmo_fsm_inst_dispatch(vsub->lu_fsm,
+					      VLR_ULA_E_NEW_TMSI_ACK, NULL);
+	} else if (vsub->proc_arq_fsm) {
+		return osmo_fsm_inst_dispatch(vsub->proc_arq_fsm,
+					      PR_ARQ_E_TMSI_ACK, NULL);
+	} else {
+		LOGVSUBP(LOGL_NOTICE, vsub,
+			 "gratuitous TMSI REALLOC COMPL");
+		return -EINVAL;
+	}
+}
+
+int vlr_subscr_rx_imsi_detach(struct vlr_subscr *vsub)
+{
+	/* paranoia: should any LU or PARQ FSMs still be running, stop them. */
+	vlr_subscr_cancel(vsub, GMM_CAUSE_IMPL_DETACHED);
+
+	vsub->imsi_detached_flag = true;
+	if (vsub->lu_complete) {
+		vsub->lu_complete = false;
+		/* balancing the get from vlr_lu_compl_fsm_success() */
+		vlr_subscr_put(vsub);
+	}
+	return 0;
+}
+
+/* Tear down any running FSMs due to MSC connection timeout.
+ * Visit all vsub->*_fsm pointers and give them a queue to send a final reject
+ * message before the entire connection is torn down.
+ * \param[in] vsub  subscriber to tear down
+ */
+void vlr_subscr_conn_timeout(struct vlr_subscr *vsub)
+{
+	if (!vsub)
+		return;
+
+	vlr_loc_update_conn_timeout(vsub->lu_fsm);
+	vlr_parq_conn_timeout(vsub->proc_arq_fsm);
+}
+
+struct vlr_instance *vlr_alloc(void *ctx, const struct vlr_ops *ops)
+{
+	struct vlr_instance *vlr = talloc_zero(ctx, struct vlr_instance);
+	OSMO_ASSERT(vlr);
+	OSMO_ASSERT(ops->tx_auth_req);
+	OSMO_ASSERT(ops->tx_auth_rej);
+	OSMO_ASSERT(ops->tx_id_req);
+	OSMO_ASSERT(ops->tx_lu_acc);
+	OSMO_ASSERT(ops->tx_lu_rej);
+	OSMO_ASSERT(ops->tx_cm_serv_acc);
+	OSMO_ASSERT(ops->tx_cm_serv_rej);
+	OSMO_ASSERT(ops->set_ciph_mode);
+	OSMO_ASSERT(ops->subscr_update);
+	OSMO_ASSERT(ops->subscr_assoc);
+
+	INIT_LLIST_HEAD(&vlr->subscribers);
+	INIT_LLIST_HEAD(&vlr->operations);
+	memcpy(&vlr->ops, ops, sizeof(vlr->ops));
+
+	/* osmo_auth_fsm.c */
+	osmo_fsm_register(&vlr_auth_fsm);
+	/* osmo_lu_fsm.c */
+	vlr_lu_fsm_init();
+	/* vlr_access_request_fsm.c */
+	vlr_parq_fsm_init();
+
+	return vlr;
+}
+
+int vlr_start(const char *gsup_unit_name, struct vlr_instance *vlr,
+	      const char *gsup_server_addr_str, uint16_t gsup_server_port)
+{
+	OSMO_ASSERT(vlr);
+
+	vlr->gsup_client = gsup_client_create(gsup_unit_name,
+					      gsup_server_addr_str,
+					      gsup_server_port,
+					      &vlr_gsupc_read_cb, NULL);
+	if (!vlr->gsup_client)
+		return -ENOMEM;
+	vlr->gsup_client->data = vlr;
+
+	return 0;
+}
+
+/* MSC->VLR: Subscribre has disconnected */
+int vlr_subscr_disconnected(struct vlr_subscr *vsub)
+{
+	/* This corresponds to a MAP-ABORT from MSC->VLR on a classic B
+	 * interface */
+	osmo_fsm_inst_term(vsub->lu_fsm, OSMO_FSM_TERM_REQUEST, NULL);
+	osmo_fsm_inst_term(vsub->auth_fsm, OSMO_FSM_TERM_REQUEST, NULL);
+	vsub->msc_conn_ref = NULL;
+
+	return 0;
+}
+
+/* MSC->VLR: Receive Authentication Failure from Subscriber */
+int vlr_subscr_rx_auth_fail(struct vlr_subscr *vsub, const uint8_t *auts)
+{
+	struct vlr_auth_resp_par par = {0};
+	par.auts = auts;
+
+	osmo_fsm_inst_dispatch(vsub->auth_fsm, VLR_AUTH_E_MS_AUTH_FAIL, &par);
+	return 0;
+}
+
+/* MSC->VLR: Receive Authentication Response from MS
+ * \returns 1 in case of success, 0 in case of delay, -1 on auth error */
+int vlr_subscr_rx_auth_resp(struct vlr_subscr *vsub, bool is_r99,
+			 bool is_utran, const uint8_t *res, uint8_t res_len)
+{
+	struct osmo_fsm_inst *auth_fi = vsub->auth_fsm;
+	struct vlr_auth_resp_par par;
+
+	par.is_r99 = is_r99;
+	par.is_utran = is_utran;
+	par.res = res;
+	par.res_len = res_len;
+	osmo_fsm_inst_dispatch(auth_fi, VLR_AUTH_E_MS_AUTH_RESP, (void *) &par);
+
+	return 0;
+}
+
+/* MSC->VLR: Receive result of Ciphering Mode Command from MS */
+void vlr_subscr_rx_ciph_res(struct vlr_subscr *vsub, struct vlr_ciph_result *res)
+{
+	if (vsub->lu_fsm && vsub->lu_fsm->state == VLR_ULA_S_WAIT_CIPH)
+		osmo_fsm_inst_dispatch(vsub->lu_fsm, VLR_ULA_E_CIPH_RES, res);
+	if (vsub->proc_arq_fsm
+	    && vsub->proc_arq_fsm->state == PR_ARQ_S_WAIT_CIPH)
+		osmo_fsm_inst_dispatch(vsub->proc_arq_fsm, PR_ARQ_E_CIPH_RES,
+				       res);
+}
+
+/* Internal evaluation of requested ciphering mode.
+ * Send set_ciph_mode() to MSC depending on the ciph_mode argument.
+ * \param[in] vlr  VLR instance.
+ * \param[in] fi  Calling FSM instance, for logging.
+ * \param[in] msc_conn_ref  MSC conn to send to.
+ * \param[in] ciph_mode  Ciphering config, to decide whether to do ciphering.
+ * \returns 0 if no ciphering is needed or message was sent successfully,
+ *          or a negative value if ciph_mode is invalid or sending failed.
+ */
+int vlr_set_ciph_mode(struct vlr_instance *vlr,
+		      struct osmo_fsm_inst *fi,
+		      void *msc_conn_ref,
+		      enum vlr_ciph ciph_mode,
+		      bool retrieve_imeisv)
+{
+	switch (ciph_mode) {
+	case VLR_CIPH_NONE:
+		return 0;
+
+	case VLR_CIPH_A5_1:
+	case VLR_CIPH_A5_3:
+		return vlr->ops.set_ciph_mode(msc_conn_ref,
+					      ciph_mode,
+					      retrieve_imeisv);
+
+	case VLR_CIPH_A5_2:
+		/* TODO policy by user config? */
+		LOGPFSML(fi, LOGL_ERROR, "A5/2 ciphering is not allowed\n");
+		return -EINVAL;
+
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "unknown ciphering value: %d\n",
+			 ciph_mode);
+		return -EINVAL;
+	}
+}
+
+void log_set_filter_vlr_subscr(struct log_target *target,
+			       struct vlr_subscr *vlr_subscr)
+{
+	struct vlr_subscr **fsub = (void*)&target->filter_data[LOG_FLT_VLR_SUBSCR];
+
+	/* free the old data */
+	if (*fsub) {
+		vlr_subscr_put(*fsub);
+		*fsub = NULL;
+	}
+
+	if (vlr_subscr) {
+		target->filter_map |= (1 << LOG_FLT_VLR_SUBSCR);
+		*fsub = vlr_subscr_get(vlr_subscr);
+	} else
+		target->filter_map &= ~(1 << LOG_FLT_VLR_SUBSCR);
+}
diff --git a/src/libvlr/vlr_access_req_fsm.c b/src/libvlr/vlr_access_req_fsm.c
new file mode 100644
index 0000000..241478f
--- /dev/null
+++ b/src/libvlr/vlr_access_req_fsm.c
@@ -0,0 +1,776 @@
+/* Osmocom Visitor Location Register (VLR): Access Request FSMs */
+
+/* (C) 2016 by Harald Welte <laforge at 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 <osmocom/core/fsm.h>
+#include <osmocom/gsm/gsup.h>
+#include <openbsc/vlr.h>
+#include <openbsc/debug.h>
+
+#include "vlr_core.h"
+#include "vlr_auth_fsm.h"
+#include "vlr_lu_fsm.h"
+#include "vlr_access_req_fsm.h"
+
+#define S(x)	(1 << (x))
+
+/***********************************************************************
+ * Process_Access_Request_VLR, TS 29.002 Chapter 25.4.2
+ ***********************************************************************/
+
+const struct value_string vlr_proc_arq_result_names[] = {
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_NONE),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_SYSTEM_FAILURE),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_ILLEGAL_SUBSCR),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_UNIDENT_SUBSCR),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_ROAMING_NOTALLOWED),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_ILLEGAL_EQUIP),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_UNKNOWN_ERROR),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_TIMEOUT),
+	OSMO_VALUE_STRING(VLR_PR_ARQ_RES_PASSED),
+	{ 0, NULL }
+};
+
+static const struct value_string proc_arq_vlr_event_names[] = {
+	OSMO_VALUE_STRING(PR_ARQ_E_START),
+	OSMO_VALUE_STRING(PR_ARQ_E_ID_IMSI),
+	OSMO_VALUE_STRING(PR_ARQ_E_AUTH_RES),
+	OSMO_VALUE_STRING(PR_ARQ_E_CIPH_RES),
+	OSMO_VALUE_STRING(PR_ARQ_E_UPD_LOC_RES),
+	OSMO_VALUE_STRING(PR_ARQ_E_TRACE_RES),
+	OSMO_VALUE_STRING(PR_ARQ_E_IMEI_RES),
+	OSMO_VALUE_STRING(PR_ARQ_E_PRES_RES),
+	OSMO_VALUE_STRING(PR_ARQ_E_TMSI_ACK),
+	{ 0, NULL }
+};
+
+struct proc_arq_priv {
+	struct vlr_instance *vlr;
+	struct vlr_subscr *vsub;
+	void *msc_conn_ref;
+	struct osmo_fsm_inst *ul_child_fsm;
+	struct osmo_fsm_inst *sub_pres_vlr_fsm;
+	uint32_t parent_event_success;
+	uint32_t parent_event_failure;
+	void *parent_event_data;
+
+	enum vlr_parq_type type;
+	enum vlr_proc_arq_result result;
+	bool by_tmsi;
+	char imsi[16];
+	uint32_t tmsi;
+	struct osmo_location_area_id lai;
+	bool authentication_required;
+	enum vlr_ciph ciphering_required;
+	bool is_r99;
+	bool is_utran;
+	bool implicitly_accepted_parq_by_ciphering_cmd;
+};
+
+static void assoc_par_with_subscr(struct osmo_fsm_inst *fi, struct vlr_subscr *vsub)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_instance *vlr = par->vlr;
+
+	vsub->msc_conn_ref = par->msc_conn_ref;
+	par->vsub = vsub;
+	/* Tell MSC to associate this subscriber with the given
+	 * connection */
+	vlr->ops.subscr_assoc(par->msc_conn_ref, par->vsub);
+}
+
+#define proc_arq_fsm_done(fi, res) _proc_arq_fsm_done(fi, res, __FILE__, __LINE__)
+static void _proc_arq_fsm_done(struct osmo_fsm_inst *fi,
+			       enum vlr_proc_arq_result res,
+			       const char *file, int line)
+{
+	struct proc_arq_priv *par = fi->priv;
+	LOGPFSMSRC(fi, file, line, "proc_arq_fsm_done(%s)\n",
+		   vlr_proc_arq_result_name(res));
+	par->result = res;
+	osmo_fsm_inst_state_chg(fi, PR_ARQ_S_DONE, 0, 0);
+}
+
+static void proc_arq_vlr_dispatch_result(struct osmo_fsm_inst *fi,
+					 uint32_t prev_state)
+{
+	struct proc_arq_priv *par = fi->priv;
+	bool success;
+	int rc;
+	LOGPFSM(fi, "Process Access Request result: %s\n",
+		vlr_proc_arq_result_name(par->result));
+
+	success = (par->result == VLR_PR_ARQ_RES_PASSED);
+
+	/* It would be logical to first dispatch the success event to the
+	 * parent FSM, but that could start actions that send messages to the
+	 * MS. Rather send the CM Service Accept message first and then signal
+	 * success. Since messages are handled synchronously, the success event
+	 * will be processed before we handle new incoming data from the MS. */
+
+	if (par->type == VLR_PR_ARQ_T_CM_SERV_REQ) {
+		if (success
+		    && !par->implicitly_accepted_parq_by_ciphering_cmd) {
+			rc = par->vlr->ops.tx_cm_serv_acc(par->msc_conn_ref);
+			if (rc) {
+				LOGPFSML(fi, LOGL_ERROR,
+					 "Failed to send CM Service Accept\n");
+				success = false;
+			}
+		}
+		if (!success) {
+			rc = par->vlr->ops.tx_cm_serv_rej(par->msc_conn_ref,
+							  par->result);
+			if (rc)
+				LOGPFSML(fi, LOGL_ERROR,
+					 "Failed to send CM Service Reject\n");
+		}
+	}
+
+	/* For VLR_PR_ARQ_T_PAGING_RESP, there is nothing to send. The conn_fsm
+	 * will start handling pending paging transactions. */
+
+	if (!fi->proc.parent) {
+		LOGPFSML(fi, LOGL_ERROR, "No parent FSM");
+		return;
+	}
+	osmo_fsm_inst_dispatch(fi->proc.parent,
+			       success ? par->parent_event_success
+				       : par->parent_event_failure,
+			       par->parent_event_data);
+}
+
+void proc_arq_vlr_cleanup(struct osmo_fsm_inst *fi,
+			  enum osmo_fsm_term_cause cause)
+{
+	struct proc_arq_priv *par = fi->priv;
+	if (par->vsub && par->vsub->proc_arq_fsm == fi)
+		par->vsub->proc_arq_fsm = NULL;
+}
+
+static void _proc_arq_vlr_post_imei(struct osmo_fsm_inst *fi)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	/* TODO: Identity := IMSI */
+	if (0 /* TODO: TMSI reallocation at access: vlr->cfg.alloc_tmsi_arq */) {
+		vlr_subscr_alloc_tmsi(vsub);
+		/* TODO: forward TMSI to MS, wait for TMSI
+		 * REALLOC COMPLETE */
+		/* TODO: Freeze old TMSI */
+		osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_TMSI_ACK, 0, 0);
+		return;
+	}
+
+	proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_PASSED);
+}
+
+static void _proc_arq_vlr_post_trace(struct osmo_fsm_inst *fi)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+	struct vlr_instance *vlr = vsub->vlr;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	/* Node 3 */
+	if (0 /* IMEI check required */) {
+		/* Chck_IMEI_VLR */
+		vlr->ops.tx_id_req(par->msc_conn_ref, GSM_MI_TYPE_IMEI);
+		osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_CHECK_IMEI,
+					vlr_timer(vlr, 3270), 3270);
+	} else
+		_proc_arq_vlr_post_imei(fi);
+}
+
+/* After Subscriber_Present_VLR */
+static void _proc_arq_vlr_post_pres(struct osmo_fsm_inst *fi)
+{
+	LOGPFSM(fi, "%s()\n", __func__);
+	if (0 /* TODO: tracing required */) {
+		/* TODO: Trace_Subscriber_Activity_VLR */
+		osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_TRACE_SUB, 0, 0);
+	}
+	_proc_arq_vlr_post_trace(fi);
+}
+
+/* After Update_Location_Child_VLR */
+static void _proc_arq_vlr_node2_post_vlr(struct osmo_fsm_inst *fi)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	if (!vsub->sub_dataconf_by_hlr_ind) {
+		/* Set User Error: Unidentified Subscriber */
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_UNIDENT_SUBSCR);
+		return;
+	}
+	if (0 /* roaming not allowed in LA */) {
+		/* Set User Error: Roaming not allowed in this LA */
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_ROAMING_NOTALLOWED);
+		return;
+	}
+	vsub->imsi_detached_flag = false;
+	if (vsub->ms_not_reachable_flag) {
+		/* Start Subscriber_Present_VLR */
+		osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_SUB_PRES, 0, 0);
+		par->sub_pres_vlr_fsm = sub_pres_vlr_fsm_start(fi, vsub,
+							PR_ARQ_E_PRES_RES);
+		return;
+	}
+	_proc_arq_vlr_post_pres(fi);
+}
+
+static void _proc_arq_vlr_node2_post_ciph(struct osmo_fsm_inst *fi)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	vsub->conf_by_radio_contact_ind = true;
+	if (vsub->loc_conf_in_hlr_ind == false) {
+		/* start Update_Location_Child_VLR.  WE use
+		 * Update_HLR_VLR instead, the differences appear
+		 * insignificant for now. */
+		par->ul_child_fsm = upd_hlr_vlr_proc_start(fi, vsub,
+							PR_ARQ_E_UPD_LOC_RES);
+		osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_UPD_LOC_CHILD, 0, 0);
+		return;
+	}
+	_proc_arq_vlr_node2_post_vlr(fi);
+}
+
+static bool is_ciph_required(struct proc_arq_priv *par)
+{
+	return par->ciphering_required != VLR_CIPH_NONE;
+}
+
+static void _proc_arq_vlr_node2(struct osmo_fsm_inst *fi)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	if (!is_ciph_required(par)) {
+		_proc_arq_vlr_node2_post_ciph(fi);
+		return;
+	}
+
+	if (vlr_set_ciph_mode(vsub->vlr, fi, par->msc_conn_ref,
+			      par->ciphering_required,
+			      vsub->vlr->cfg.retrieve_imeisv)) {
+		LOGPFSML(fi, LOGL_ERROR,
+			 "Failed to send Ciphering Mode Command\n");
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_SYSTEM_FAILURE);
+		return;
+	}
+
+	par->implicitly_accepted_parq_by_ciphering_cmd = true;
+	osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_CIPH, 0, 0);
+}
+
+static bool is_auth_required(struct proc_arq_priv *par)
+{
+	/* The cases where the authentication procedure should be used
+	 * are defined in 3GPP TS 33.102 */
+	/* For now we use a default value passed in to vlr_lu_fsm(). */
+	return par->authentication_required
+	       || (par->ciphering_required != VLR_CIPH_NONE);
+}
+
+/* after the IMSI is known */
+static void proc_arq_vlr_fn_post_imsi(struct osmo_fsm_inst *fi)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	OSMO_ASSERT(vsub);
+
+	/* TODO: Identity IMEI -> System Failure */
+	if (is_auth_required(par)) {
+		osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_AUTH,
+					0, 0);
+		vsub->auth_fsm = auth_fsm_start(vsub, fi->log_level, fi,
+						PR_ARQ_E_AUTH_RES,
+						par->is_r99,
+						par->is_utran);
+	} else {
+		_proc_arq_vlr_node2(fi);
+	}
+}
+
+static void proc_arq_vlr_fn_init(struct osmo_fsm_inst *fi,
+				 uint32_t event, void *data)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_instance *vlr = par->vlr;
+	struct vlr_subscr *vsub = NULL;
+
+	OSMO_ASSERT(event == PR_ARQ_E_START);
+
+	/* Obtain_Identity_VLR */
+	if (!par->by_tmsi) {
+		/* IMSI was included */
+		vsub = vlr_subscr_find_by_imsi(par->vlr, par->imsi);
+	} else {
+		/* TMSI was included */
+		vsub = vlr_subscr_find_by_tmsi(par->vlr, par->tmsi);
+	}
+	if (vsub) {
+		if (vsub->proc_arq_fsm && fi != vsub->proc_arq_fsm) {
+			LOGPFSML(fi, LOGL_ERROR,
+				 "Another proc_arq_fsm is already"
+				 " associated with subscr %s,"
+				 " terminating the other FSM.\n",
+				 vlr_subscr_name(vsub));
+			proc_arq_fsm_done(vsub->proc_arq_fsm,
+					  VLR_PR_ARQ_RES_SYSTEM_FAILURE);
+		}
+		vsub->proc_arq_fsm = fi;
+		assoc_par_with_subscr(fi, vsub);
+		proc_arq_vlr_fn_post_imsi(fi);
+		vlr_subscr_put(vsub);
+		return;
+	}
+	/* No VSUB could be resolved. What now? */
+
+	if (!par->by_tmsi) {
+		/* We couldn't find a subscriber even by IMSI,
+		 * Set User Error: Unidentified Subscriber */
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_UNIDENT_SUBSCR);
+		return;
+	} else {
+		/* TMSI was included, are we permitted to use it? */
+		if (vlr->cfg.parq_retrieve_imsi) {
+			/* Obtain_IMSI_VLR */
+			osmo_fsm_inst_state_chg(fi, PR_ARQ_S_WAIT_OBTAIN_IMSI,
+						vlr_timer(vlr, 3270), 3270);
+			return;
+		} else {
+			/* Set User Error: Unidentified Subscriber */
+			proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_UNIDENT_SUBSCR);
+			return;
+		}
+	}
+}
+
+/* ID REQ(IMSI) has returned */
+static void proc_arq_vlr_fn_w_obt_imsi(struct osmo_fsm_inst *fi,
+					uint32_t event, void *data)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_instance *vlr = par->vlr;
+	struct vlr_subscr *vsub;
+
+	OSMO_ASSERT(event == PR_ARQ_E_ID_IMSI);
+
+	vsub = vlr_subscr_find_by_imsi(vlr, par->imsi);
+	if (!vsub) {
+		/* Set User Error: Unidentified Subscriber */
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_UNIDENT_SUBSCR);
+		return;
+	}
+	assoc_par_with_subscr(fi, vsub);
+	proc_arq_vlr_fn_post_imsi(fi);
+	vlr_subscr_put(vsub);
+}
+
+/* Authenticate_VLR has completed */
+static void proc_arq_vlr_fn_w_auth(struct osmo_fsm_inst *fi,
+				   uint32_t event, void *data)
+{
+	enum vlr_auth_fsm_result *res = data;
+	enum vlr_proc_arq_result ret = VLR_PR_ARQ_RES_UNKNOWN_ERROR;
+
+	OSMO_ASSERT(res);
+	LOGPFSM(fi, "got %s\n", vlr_auth_fsm_result_name(*res));
+
+	OSMO_ASSERT(event == PR_ARQ_E_AUTH_RES);
+
+	if (res) {
+		switch (*res) {
+		case VLR_AUTH_RES_PASSED:
+			/* Node 2 */
+			_proc_arq_vlr_node2(fi);
+			return;
+		case VLR_AUTH_RES_ABORTED:
+			/* Error */
+			ret = VLR_PR_ARQ_RES_UNKNOWN_ERROR;
+			break;
+		case VLR_AUTH_RES_UNKNOWN_SUBSCR:
+			/* Set User Error: Unidentified Subscriber */
+			ret = VLR_PR_ARQ_RES_UNIDENT_SUBSCR;
+			break;
+		case VLR_AUTH_RES_AUTH_FAILED:
+			/* Set User Error: Illegal Subscriber */
+			ret = VLR_PR_ARQ_RES_ILLEGAL_SUBSCR;
+			break;
+		case VLR_AUTH_RES_PROC_ERR:
+			/* Set User Error: System failure */
+			ret = VLR_PR_ARQ_RES_SYSTEM_FAILURE;
+			break;
+		}
+	}
+	/* send process_access_req response to caller */
+	/* enter error state */
+	proc_arq_fsm_done(fi, ret);
+}
+
+static void proc_arq_vlr_fn_w_ciph(struct osmo_fsm_inst *fi,
+				   uint32_t event, void *data)
+{
+	struct proc_arq_priv *par = fi->priv;
+	struct vlr_subscr *vsub = par->vsub;
+	struct vlr_ciph_result res = { .cause = VLR_CIPH_REJECT };
+
+	OSMO_ASSERT(event == PR_ARQ_E_CIPH_RES);
+
+	if (!data)
+		LOGPFSML(fi, LOGL_ERROR, "invalid ciphering result: NULL\n");
+	else
+		res = *(struct vlr_ciph_result*)data;
+
+	switch (res.cause) {
+	case VLR_CIPH_COMPL:
+		break;
+	case VLR_CIPH_REJECT:
+		LOGPFSM(fi, "ciphering rejected\n");
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_ILLEGAL_SUBSCR);
+		return;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "invalid ciphering result: %d\n",
+			 res.cause);
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_ILLEGAL_SUBSCR);
+		return;
+	}
+
+
+	if (res.imeisv) {
+		LOGPFSM(fi, "got IMEISV: %s\n", res.imeisv);
+		vlr_subscr_set_imeisv(vsub, res.imeisv);
+	}
+	_proc_arq_vlr_node2_post_ciph(fi);
+}
+
+/* Update_Location_Child_VLR has completed */
+static void proc_arq_vlr_fn_w_upd_loc(struct osmo_fsm_inst *fi,
+					uint32_t event, void *data)
+{
+	OSMO_ASSERT(event == PR_ARQ_E_UPD_LOC_RES);
+
+	_proc_arq_vlr_node2_post_vlr(fi);
+}
+
+/* Subscriber_Present_VLR has completed */
+static void proc_arq_vlr_fn_w_pres(struct osmo_fsm_inst *fi,
+					uint32_t event, void *data)
+{
+	OSMO_ASSERT(event == PR_ARQ_E_PRES_RES);
+
+	_proc_arq_vlr_post_pres(fi);
+}
+
+static void proc_arq_vlr_fn_w_trace(struct osmo_fsm_inst *fi,
+					uint32_t event, void *data)
+{
+	OSMO_ASSERT(event == PR_ARQ_E_TRACE_RES);
+
+	_proc_arq_vlr_post_trace(fi);
+}
+
+/* we have received the ID RESPONSE (IMEI) */
+static void proc_arq_vlr_fn_w_imei(struct osmo_fsm_inst *fi,
+				uint32_t event, void *data)
+{
+	OSMO_ASSERT(event == PR_ARQ_E_IMEI_RES);
+
+	_proc_arq_vlr_post_imei(fi);
+}
+
+/* MSC tells us that MS has acknowleded TMSI re-allocation */
+static void proc_arq_vlr_fn_w_tmsi(struct osmo_fsm_inst *fi,
+				uint32_t event, void *data)
+{
+	OSMO_ASSERT(event == PR_ARQ_E_TMSI_ACK);
+
+	/* FIXME: check confirmation? unfreeze? */
+	proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_PASSED);
+}
+
+static const struct osmo_fsm_state proc_arq_vlr_states[] = {
+	[PR_ARQ_S_INIT] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_INIT),
+		.in_event_mask = S(PR_ARQ_E_START),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_OBTAIN_IMSI) |
+				  S(PR_ARQ_S_WAIT_AUTH) |
+				  S(PR_ARQ_S_WAIT_UPD_LOC_CHILD) |
+				  S(PR_ARQ_S_WAIT_SUB_PRES) |
+				  S(PR_ARQ_S_WAIT_TRACE_SUB) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_init,
+	},
+	[PR_ARQ_S_WAIT_OBTAIN_IMSI] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_OBTAIN_IMSI),
+		.in_event_mask = S(PR_ARQ_E_ID_IMSI),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_AUTH) |
+				  S(PR_ARQ_S_WAIT_UPD_LOC_CHILD) |
+				  S(PR_ARQ_S_WAIT_SUB_PRES) |
+				  S(PR_ARQ_S_WAIT_TRACE_SUB) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_obt_imsi,
+	},
+	[PR_ARQ_S_WAIT_AUTH] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_AUTH),
+		.in_event_mask = S(PR_ARQ_E_AUTH_RES),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_CIPH) |
+				  S(PR_ARQ_S_WAIT_UPD_LOC_CHILD) |
+				  S(PR_ARQ_S_WAIT_SUB_PRES) |
+				  S(PR_ARQ_S_WAIT_TRACE_SUB) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_auth,
+	},
+	[PR_ARQ_S_WAIT_CIPH] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_CIPH),
+		.in_event_mask = S(PR_ARQ_E_CIPH_RES),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_UPD_LOC_CHILD) |
+				  S(PR_ARQ_S_WAIT_SUB_PRES) |
+				  S(PR_ARQ_S_WAIT_TRACE_SUB) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_ciph,
+	},
+	[PR_ARQ_S_WAIT_UPD_LOC_CHILD] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_UPD_LOC_CHILD),
+		.in_event_mask = S(PR_ARQ_E_UPD_LOC_RES),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_SUB_PRES) |
+				  S(PR_ARQ_S_WAIT_TRACE_SUB) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_upd_loc,
+	},
+	[PR_ARQ_S_WAIT_SUB_PRES] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_SUB_PRES),
+		.in_event_mask = S(PR_ARQ_E_PRES_RES),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_TRACE_SUB) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_pres,
+	},
+	[PR_ARQ_S_WAIT_TRACE_SUB] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_TRACE_SUB),
+		.in_event_mask = S(PR_ARQ_E_TRACE_RES),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_CHECK_IMEI) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_trace,
+	},
+	[PR_ARQ_S_WAIT_CHECK_IMEI] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_CHECK_IMEI),
+		.in_event_mask = S(PR_ARQ_E_IMEI_RES),
+		.out_state_mask = S(PR_ARQ_S_DONE) |
+				  S(PR_ARQ_S_WAIT_TMSI_ACK),
+		.action = proc_arq_vlr_fn_w_imei,
+	},
+	[PR_ARQ_S_WAIT_TMSI_ACK] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_WAIT_TMSI_ACK),
+		.in_event_mask = S(PR_ARQ_E_TMSI_ACK),
+		.out_state_mask = S(PR_ARQ_S_DONE),
+		.action = proc_arq_vlr_fn_w_tmsi,
+	},
+	[PR_ARQ_S_DONE] = {
+		.name = OSMO_STRINGIFY(PR_ARQ_S_DONE),
+		.onenter = proc_arq_vlr_dispatch_result,
+	},
+};
+
+static struct osmo_fsm proc_arq_vlr_fsm = {
+	.name = "Process_Access_Request_VLR",
+	.states = proc_arq_vlr_states,
+	.num_states = ARRAY_SIZE(proc_arq_vlr_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = proc_arq_vlr_event_names,
+	.cleanup = proc_arq_vlr_cleanup,
+};
+
+void
+vlr_proc_acc_req(struct osmo_fsm_inst *parent,
+		 uint32_t parent_event_success,
+		 uint32_t parent_event_failure,
+		 void *parent_event_data,
+		 struct vlr_instance *vlr, void *msc_conn_ref,
+		 enum vlr_parq_type type, const uint8_t *mi_lv,
+		 const struct osmo_location_area_id *lai,
+		 bool authentication_required,
+		 enum vlr_ciph ciphering_required,
+		 bool is_r99, bool is_utran)
+{
+	struct osmo_fsm_inst *fi;
+	struct proc_arq_priv *par;
+	char mi_string[GSM48_MI_SIZE];
+	uint8_t mi_type;
+
+	fi = osmo_fsm_inst_alloc_child(&proc_arq_vlr_fsm, parent,
+				       parent_event_failure);
+	if (!fi)
+		return;
+
+	par = talloc_zero(fi, struct proc_arq_priv);
+	fi->priv = par;
+	par->vlr = vlr;
+	par->msc_conn_ref = msc_conn_ref;
+	par->type = type;
+	par->lai = *lai;
+	par->parent_event_success = parent_event_success;
+	par->parent_event_failure = parent_event_failure;
+	par->parent_event_data = parent_event_data;
+	par->authentication_required = authentication_required;
+	par->ciphering_required = ciphering_required;
+	par->is_r99 = is_r99;
+	par->is_utran = is_utran;
+
+	LOGPFSM(fi, "rev=%s net=%s%s%s\n",
+		is_r99 ? "R99" : "GSM",
+		is_utran ? "UTRAN" : "GERAN",
+		(authentication_required || ciphering_required)?
+		" Auth" : " (no Auth)",
+		(authentication_required || ciphering_required)?
+			(ciphering_required? "+Ciph" : " (no Ciph)")
+			: "");
+
+	gsm48_mi_to_string(mi_string, sizeof(mi_string), mi_lv+1, mi_lv[0]);
+	mi_type = mi_lv[1] & GSM_MI_TYPE_MASK;
+	switch (mi_type) {
+	case GSM_MI_TYPE_IMSI:
+		strncpy(par->imsi, mi_string, sizeof(par->imsi)-1);
+		par->imsi[sizeof(par->imsi)-1] = '\0';
+		par->by_tmsi = false;
+		break;
+	case GSM_MI_TYPE_TMSI:
+		par->by_tmsi = true;
+		par->tmsi = osmo_load32be(mi_lv+2);
+		break;
+	case GSM_MI_TYPE_IMEI:
+		/* TODO: IMEI (emergency call) */
+	default:
+		/* FIXME: directly send reject? */
+		proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_UNIDENT_SUBSCR);
+		return;
+	}
+
+	osmo_fsm_inst_dispatch(fi, PR_ARQ_E_START, NULL);
+}
+
+/* Gracefully terminate an FSM created by vlr_proc_acc_req() in case of
+ * external timeout (i.e. from MSC). */
+void vlr_parq_conn_timeout(struct osmo_fsm_inst *fi)
+{
+	if (!fi || fi->state == PR_ARQ_S_DONE)
+		return;
+	LOGPFSM(fi, "Connection timed out\n");
+	proc_arq_fsm_done(fi, VLR_PR_ARQ_RES_TIMEOUT);
+}
+
+
+#if 0
+/***********************************************************************
+ * Update_Location_Child_VLR, TS 29.002 Chapter 25.4.4
+ ***********************************************************************/
+
+enum upd_loc_child_vlr_state {
+	ULC_S_IDLE,
+	ULC_S_WAIT_HLR_RESP,
+	ULC_S_DONE,
+};
+
+enum upd_loc_child_vlr_event {
+	ULC_E_START,
+};
+
+static const struct value_string upd_loc_child_vlr_event_names[] = {
+	{ ULC_E_START, "START" },
+	{ 0, NULL }
+};
+
+static void upd_loc_child_f_idle(struct osmo_fsm_inst *fi, uint32_t event,
+				 void *data)
+{
+	OSMO_ASSERT(event == ULC_E_START);
+
+	/* send update location */
+}
+
+static void upd_loc_child_f_w_hlr(struct osmo_fsm_inst *fi, uint32_t event,
+				  void *data)
+{
+}
+
+static const struct osmo_fsm_state upd_loc_child_vlr_states[] = {
+	[ULC_S_IDLE] = {
+		.in_event_mask = ,
+		.out_state_mask = S(ULC_S_WAIT_HLR_RESP) |
+				  S(ULC_S_DONE),
+		.name = "IDLE",
+		.action = upd_loc_child_f_idle,
+	},
+	[ULC_S_WAIT_HLR_RESP] = {
+		.in_event_mask = ,
+		.out_state_mask = S(ULC_S_DONE),
+		.name = "WAIT-HLR-RESP",
+		.action = upd_loc_child_f_w_hlr,
+	},
+	[ULC_S_DONE] = {
+		.name = "DONE",
+	},
+};
+
+static struct osmo_fsm upd_loc_child_vlr_fsm = {
+	.name = "Update_Location_Child_VLR",
+	.states = upd_loc_child_vlr_states,
+	.num_states = ARRAY_SIZE(upd_loc_child_vlr_states),
+	.log_subsys = DVLR,
+	.event_names = upd_loc_child_vlr_event_names,
+};
+#endif
+
+void vlr_parq_fsm_init(void)
+{
+	//osmo_fsm_register(&upd_loc_child_vlr_fsm);
+	osmo_fsm_register(&proc_arq_vlr_fsm);
+}
diff --git a/src/libvlr/vlr_access_req_fsm.h b/src/libvlr/vlr_access_req_fsm.h
new file mode 100644
index 0000000..8386da6
--- /dev/null
+++ b/src/libvlr/vlr_access_req_fsm.h
@@ -0,0 +1,17 @@
+#pragma once
+
+enum proc_arq_vlr_state {
+	PR_ARQ_S_INIT,
+	/* Waiting for Obtain_Identity_VLR (IMSI) result */
+	PR_ARQ_S_WAIT_OBTAIN_IMSI,
+	/* Waiting for Authenticate_VLR result */
+	PR_ARQ_S_WAIT_AUTH,
+	PR_ARQ_S_WAIT_CIPH,
+	PR_ARQ_S_WAIT_UPD_LOC_CHILD,
+	PR_ARQ_S_WAIT_SUB_PRES,
+	PR_ARQ_S_WAIT_TRACE_SUB,
+	PR_ARQ_S_WAIT_CHECK_IMEI,
+	PR_ARQ_S_WAIT_TMSI_ACK,
+	PR_ARQ_S_WAIT_CECK_CONF,
+	PR_ARQ_S_DONE,
+};
diff --git a/src/libvlr/vlr_auth_fsm.c b/src/libvlr/vlr_auth_fsm.c
new file mode 100644
index 0000000..0eb86e7
--- /dev/null
+++ b/src/libvlr/vlr_auth_fsm.c
@@ -0,0 +1,605 @@
+/* Osmocom Visitor Location Register (VLR) Autentication FSM */
+
+/* (C) 2016 by Harald Welte <laforge at 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 <osmocom/core/fsm.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/gsup.h>
+#include <openbsc/vlr.h>
+#include <openbsc/debug.h>
+
+#include "vlr_core.h"
+#include "vlr_auth_fsm.h"
+
+#define S(x)	(1 << (x))
+
+static const struct value_string fsm_auth_event_names[] = {
+	OSMO_VALUE_STRING(VLR_AUTH_E_START),
+	OSMO_VALUE_STRING(VLR_AUTH_E_HLR_SAI_ACK),
+	OSMO_VALUE_STRING(VLR_AUTH_E_HLR_SAI_NACK),
+	OSMO_VALUE_STRING(VLR_AUTH_E_HLR_SAI_ABORT),
+	OSMO_VALUE_STRING(VLR_AUTH_E_MS_AUTH_RESP),
+	OSMO_VALUE_STRING(VLR_AUTH_E_MS_AUTH_FAIL),
+	OSMO_VALUE_STRING(VLR_AUTH_E_MS_ID_IMSI),
+	{ 0, NULL }
+};
+
+const struct value_string vlr_auth_fsm_result_names[] = {
+	OSMO_VALUE_STRING(VLR_AUTH_RES_ABORTED),
+	OSMO_VALUE_STRING(VLR_AUTH_RES_UNKNOWN_SUBSCR),
+	OSMO_VALUE_STRING(VLR_AUTH_RES_PROC_ERR),
+	OSMO_VALUE_STRING(VLR_AUTH_RES_AUTH_FAILED),
+	OSMO_VALUE_STRING(VLR_AUTH_RES_PASSED),
+	{0, NULL}
+};
+
+/* private state of the auth_fsm_instance */
+struct auth_fsm_priv {
+	struct vlr_subscr *vsub;
+	bool by_imsi;
+	bool is_r99;
+	bool is_utran;
+	bool auth_requested;
+
+	int auth_tuple_max_use_count; /* see vlr->cfg instead */
+};
+
+/***********************************************************************
+ * Utility functions
+ ***********************************************************************/
+
+/* Always use either vlr_subscr_get_auth_tuple() or vlr_subscr_has_auth_tuple()
+ * instead, to ensure proper use count.
+ * Return an auth tuple with the lowest use_count among the auth tuples. If
+ * max_use_count >= 0, return NULL if all available auth tuples have a use
+ * count > max_use_count. If max_use_count is negative, return a currently
+ * least used auth tuple without enforcing a maximum use count.  If there are
+ * no auth tuples, return NULL.
+ */
+static struct gsm_auth_tuple *
+_vlr_subscr_next_auth_tuple(struct vlr_subscr *vsub, int max_use_count)
+{
+	unsigned int count;
+	unsigned int idx;
+	struct gsm_auth_tuple *at = NULL;
+	unsigned int key_seq = GSM_KEY_SEQ_INVAL;
+
+	if (!vsub)
+		return NULL;
+
+	if (vsub->last_tuple)
+		key_seq = vsub->last_tuple->key_seq;
+
+	if (key_seq == GSM_KEY_SEQ_INVAL)
+		/* Start with 0 after increment modulo array size */
+		idx = ARRAY_SIZE(vsub->auth_tuples) - 1;
+	else
+		idx = key_seq;
+
+	for (count = ARRAY_SIZE(vsub->auth_tuples); count > 0; count--) {
+		idx = (idx + 1) % ARRAY_SIZE(vsub->auth_tuples);
+
+		if (vsub->auth_tuples[idx].key_seq == GSM_KEY_SEQ_INVAL)
+			continue;
+
+		if (!at || vsub->auth_tuples[idx].use_count < at->use_count)
+			at = &vsub->auth_tuples[idx];
+	}
+
+	if (!at || (max_use_count >= 0 && at->use_count > max_use_count))
+		return NULL;
+
+	return at;
+}
+
+/* Return an auth tuple and increment its use count. */
+static struct gsm_auth_tuple *
+vlr_subscr_get_auth_tuple(struct vlr_subscr *vsub, int max_use_count)
+{
+	struct gsm_auth_tuple *at = _vlr_subscr_next_auth_tuple(vsub,
+							       max_use_count);
+	if (!at)
+		return NULL;
+	at->use_count++;
+	return at;
+}
+
+/* Return whether an auth tuple with the given max_use_count is available. */
+static bool vlr_subscr_has_auth_tuple(struct vlr_subscr *vsub,
+				      int max_use_count)
+{
+	return _vlr_subscr_next_auth_tuple(vsub, max_use_count) != NULL;
+}
+
+static bool check_auth_resp(struct vlr_subscr *vsub, bool is_r99,
+			    bool is_utran, const uint8_t *res,
+			    uint8_t res_len)
+{
+	struct gsm_auth_tuple *at = vsub->last_tuple;
+	struct osmo_auth_vector *vec = &at->vec;
+	bool check_umts;
+	OSMO_ASSERT(at);
+
+	LOGVSUBP(LOGL_DEBUG, vsub, "received res: %s\n",
+		 osmo_hexdump(res, res_len));
+
+	/* RES must be present and at least 32bit */
+	if (!res || res_len < sizeof(vec->sres)) {
+		LOGVSUBP(LOGL_NOTICE, vsub, "AUTH RES missing or too short "
+			 "(%u)\n", res_len);
+		goto out_false;
+	}
+
+	check_umts = false;
+	if (is_r99 && (vec->auth_types & OSMO_AUTH_TYPE_UMTS)) {
+		check_umts = true;
+		/* We have a R99 capable UE and have a UMTS AKA capable USIM.
+		 * However, the ME may still choose to only perform GSM AKA, as
+		 * long as the bearer is GERAN */
+		if (res_len != vec->res_len) {
+			if (is_utran) {
+				LOGVSUBP(LOGL_NOTICE, vsub,
+					 "AUTH via UTRAN but "
+					 "res_len(%u) != vec->res_len(%u)\n",
+					 res_len, vec->res_len);
+				goto out_false;
+			}
+			check_umts = false;
+		}
+	}
+
+	if (check_umts) {
+		if (res_len != vec->res_len
+		    || memcmp(res, vec->res, res_len)) {
+			LOGVSUBP(LOGL_INFO, vsub, "UMTS AUTH failure:"
+				 " mismatching res (expected res=%s)\n",
+				 osmo_hexdump(vec->res, vec->res_len));
+			goto out_false;
+		}
+
+		LOGVSUBP(LOGL_INFO, vsub, "AUTH established UMTS security"
+			 " context\n");
+		vsub->sec_ctx = VLR_SEC_CTX_UMTS;
+		return true;
+	} else {
+		if (res_len != sizeof(vec->sres)
+		    || memcmp(res, vec->sres, sizeof(vec->sres))) {
+			LOGVSUBP(LOGL_INFO, vsub, "GSM AUTH failure:"
+				 " mismatching sres (expected sres=%s)\n",
+				 osmo_hexdump(vec->sres, sizeof(vec->sres)));
+			goto out_false;
+		}
+
+		LOGVSUBP(LOGL_INFO, vsub, "AUTH established GSM security"
+			 " context\n");
+		vsub->sec_ctx = VLR_SEC_CTX_GSM;
+		return true;
+	}
+
+out_false:
+	vsub->sec_ctx = VLR_SEC_CTX_NONE;
+	return false;
+}
+
+static void auth_fsm_onenter_failed(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+
+	/* If authentication hasn't even started, e.g. the HLR sent no auth
+	 * info, then we also don't need to tell the HLR about an auth failure.
+	 */
+	if (afp->auth_requested)
+		vlr_subscr_tx_auth_fail_rep(vsub);
+}
+
+static bool is_umts_auth(struct auth_fsm_priv *afp,
+			 uint32_t auth_types)
+{
+	if (!afp->is_r99)
+		return false;
+	if (!(auth_types & OSMO_AUTH_TYPE_UMTS))
+		return false;
+	return true;
+}
+
+/* Terminate the Auth FSM Instance and notify parent */
+static void auth_fsm_term(struct osmo_fsm_inst *fi, enum vlr_auth_fsm_result res)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+
+	LOGPFSM(fi, "Authentication terminating with result %s\n",
+		vlr_auth_fsm_result_name(res));
+
+	/* Do one final state transition (mostly for logging purpose) */
+	if (res == VLR_AUTH_RES_PASSED)
+		osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_AUTHENTICATED, 0, 0);
+	else
+		osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_AUTH_FAILED, 0, 0);
+
+	/* return the result to the parent FSM */
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, &res);
+	vsub->auth_fsm = NULL;
+}
+
+/* back-end function transmitting authentication. Caller ensures we have valid
+ * tuple */
+static int _vlr_subscr_authenticate(struct osmo_fsm_inst *fi)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+	struct gsm_auth_tuple *at;
+
+	/* Caller ensures we have vectors available */
+	at = vlr_subscr_get_auth_tuple(vsub, afp->auth_tuple_max_use_count);
+	if (!at) {
+		LOGPFSML(fi, LOGL_ERROR, "A previous check ensured that an"
+			 " auth tuple was available, but now there is in fact"
+			 " none.\n");
+		auth_fsm_term(fi, VLR_AUTH_RES_PROC_ERR);
+		return -1;
+	}
+
+	LOGPFSM(fi, "got auth tuple: use_count=%d key_seq=%d\n",
+		at->use_count, at->key_seq);
+
+	OSMO_ASSERT(at);
+
+	/* Transmit auth req to subscriber */
+	afp->auth_requested = true;
+	vsub->last_tuple = at;
+	vsub->vlr->ops.tx_auth_req(vsub->msc_conn_ref, at,
+				   is_umts_auth(afp, at->vec.auth_types));
+	return 0;
+}
+
+/***********************************************************************
+ * FSM State Action functions
+ ***********************************************************************/
+
+/* Initial State of TS 23.018 AUT_VLR */
+static void auth_fsm_needs_auth(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+
+	OSMO_ASSERT(event == VLR_AUTH_E_START);
+
+	/* Start off with the default max_use_count, possibly change that if we
+	 * need to re-use an old tuple. */
+	afp->auth_tuple_max_use_count = vsub->vlr->cfg.auth_tuple_max_use_count;
+
+	/* Check if we have vectors available */
+	if (!vlr_subscr_has_auth_tuple(vsub, afp->auth_tuple_max_use_count)) {
+		/* Obtain_Authentication_Sets_VLR */
+		vlr_subscr_req_sai(vsub, NULL, NULL);
+		osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_NEEDS_AUTH_WAIT_AI,
+					GSM_29002_TIMER_M, 0);
+	} else {
+		/* go straight ahead with sending auth request */
+		osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_WAIT_RESP,
+					vlr_timer(vsub->vlr, 3260), 3260);
+		_vlr_subscr_authenticate(fi);
+	}
+}
+
+/* Waiting for Authentication Info from HLR */
+static void auth_fsm_wait_ai(struct osmo_fsm_inst *fi, uint32_t event,
+			     void *data)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+	struct osmo_gsup_message *gsup = data;
+
+	if (event == VLR_AUTH_E_HLR_SAI_NACK)
+		LOGPFSM(fi, "GSUP: rx Auth Info Error cause: %d: %s\n",
+			gsup->cause,
+			get_value_string(gsm48_gmm_cause_names, gsup->cause));
+
+	/* We are in what corresponds to the
+	 * Wait_For_Authentication_Sets state of TS 23.018 OAS_VLR */
+	if ((event == VLR_AUTH_E_HLR_SAI_ACK && !gsup->num_auth_vectors)
+	    || (event == VLR_AUTH_E_HLR_SAI_NACK &&
+		gsup->cause != GMM_CAUSE_IMSI_UNKNOWN)
+	    || (event == VLR_AUTH_E_HLR_SAI_ABORT)) {
+		if (vsub->vlr->cfg.auth_reuse_old_sets_on_error
+		    && vlr_subscr_has_auth_tuple(vsub, -1)) {
+			/* To re-use an old tuple, disable the max_use_count
+			 * constraint. */
+			afp->auth_tuple_max_use_count = -1;
+			goto pass;
+		}
+		/* result = procedure error */
+		auth_fsm_term(fi, VLR_AUTH_RES_PROC_ERR);
+		return;
+	}
+
+	switch (event) {
+	case VLR_AUTH_E_HLR_SAI_ACK:
+		vlr_subscr_update_tuples(vsub, gsup);
+		goto pass;
+		break;
+	case VLR_AUTH_E_HLR_SAI_NACK:
+		auth_fsm_term(fi,
+			      gsup->cause == GMM_CAUSE_IMSI_UNKNOWN?
+				      VLR_AUTH_RES_UNKNOWN_SUBSCR
+				      : VLR_AUTH_RES_PROC_ERR);
+		break;
+	}
+
+	return;
+pass:
+	osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_WAIT_RESP,
+				vlr_timer(vsub->vlr, 3260), 3260);
+	_vlr_subscr_authenticate(fi);
+}
+
+/* Waiting for Authentication Response from MS */
+static void auth_fsm_wait_auth_resp(struct osmo_fsm_inst *fi, uint32_t event,
+				    void *data)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+	struct vlr_instance *vlr = vsub->vlr;
+	struct vlr_auth_resp_par *par = data;
+	int rc;
+
+	switch (event) {
+	case VLR_AUTH_E_MS_AUTH_RESP:
+		rc = check_auth_resp(vsub, par->is_r99, par->is_utran,
+				     par->res, par->res_len);
+		if (rc == false) {
+			if (!afp->by_imsi) {
+				vlr->ops.tx_id_req(vsub->msc_conn_ref,
+						   GSM_MI_TYPE_IMSI);
+				osmo_fsm_inst_state_chg(fi,
+						VLR_SUB_AS_WAIT_ID_IMSI,
+						vlr_timer(vlr, 3270), 3270);
+			} else {
+				auth_fsm_term(fi, VLR_AUTH_RES_AUTH_FAILED);
+			}
+		} else {
+			auth_fsm_term(fi, VLR_AUTH_RES_PASSED);
+		}
+		break;
+	case VLR_AUTH_E_MS_AUTH_FAIL:
+		if (par->auts) {
+			/* First failure, start re-sync attempt */
+			vlr_subscr_req_sai(vsub, par->auts,
+					   vsub->last_tuple->vec.rand);
+			osmo_fsm_inst_state_chg(fi,
+					VLR_SUB_AS_NEEDS_AUTH_WAIT_SAI_RESYNC,
+					GSM_29002_TIMER_M, 0);
+		} else
+			auth_fsm_term(fi, VLR_AUTH_RES_AUTH_FAILED);
+		break;
+	}
+}
+
+/* Waiting for Authentication Info from HLR (resync case) */
+static void auth_fsm_wait_ai_resync(struct osmo_fsm_inst *fi,
+				    uint32_t event, void *data)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+	struct osmo_gsup_message *gsup = data;
+
+	/* We are in what corresponds to the
+	 * Wait_For_Authentication_Sets state of TS 23.018 OAS_VLR */
+	if ((event == VLR_AUTH_E_HLR_SAI_ACK && !gsup->num_auth_vectors) ||
+	    (event == VLR_AUTH_E_HLR_SAI_NACK &&
+	     gsup->cause != GMM_CAUSE_IMSI_UNKNOWN) ||
+	    (event == VLR_AUTH_E_HLR_SAI_ABORT)) {
+		/* result = procedure error */
+		auth_fsm_term(fi, VLR_AUTH_RES_PROC_ERR);
+	}
+	switch (event) {
+	case VLR_AUTH_E_HLR_SAI_ACK:
+		vlr_subscr_update_tuples(vsub, gsup);
+		goto pass;
+		break;
+	case VLR_AUTH_E_HLR_SAI_NACK:
+		auth_fsm_term(fi,
+			      gsup->cause == GMM_CAUSE_IMSI_UNKNOWN?
+				      VLR_AUTH_RES_UNKNOWN_SUBSCR
+				      : VLR_AUTH_RES_PROC_ERR);
+		break;
+	}
+
+	return;
+pass:
+	osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_WAIT_RESP_RESYNC,
+				vlr_timer(vsub->vlr, 3260), 3260);
+	_vlr_subscr_authenticate(fi);
+}
+
+/* Waiting for AUTH RESP from MS (re-sync case) */
+static void auth_fsm_wait_auth_resp_resync(struct osmo_fsm_inst *fi,
+					   uint32_t event, void *data)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+	struct vlr_auth_resp_par *par = data;
+	struct vlr_instance *vlr = vsub->vlr;
+	int rc;
+
+	switch (event) {
+	case VLR_AUTH_E_MS_AUTH_RESP:
+		rc = check_auth_resp(vsub, par->is_r99, par->is_utran,
+				     par->res, par->res_len);
+		if (rc == false) {
+			if (!afp->by_imsi) {
+				vlr->ops.tx_id_req(vsub->msc_conn_ref,
+						   GSM_MI_TYPE_IMSI);
+				osmo_fsm_inst_state_chg(fi,
+						VLR_SUB_AS_WAIT_ID_IMSI,
+						vlr_timer(vlr, 3270), 3270);
+			} else {
+				/* Result = Aborted */
+				auth_fsm_term(fi, VLR_AUTH_RES_ABORTED);
+			}
+		} else {
+			/* Result = Pass */
+			auth_fsm_term(fi, VLR_AUTH_RES_PASSED);
+		}
+		break;
+	case VLR_AUTH_E_MS_AUTH_FAIL:
+		/* Second failure: Result = Fail */
+		auth_fsm_term(fi, VLR_AUTH_RES_AUTH_FAILED);
+		break;
+	}
+}
+
+/* AUT_VLR waiting for Obtain_IMSI_VLR result */
+static void auth_fsm_wait_imsi(struct osmo_fsm_inst *fi, uint32_t event,
+				void *data)
+{
+	struct auth_fsm_priv *afp = fi->priv;
+	struct vlr_subscr *vsub = afp->vsub;
+	const char *mi_string = data;
+
+	switch (event) {
+	case VLR_AUTH_E_MS_ID_IMSI:
+		if (vsub->imsi[0]
+		    && !vlr_subscr_matches_imsi(vsub, mi_string)) {
+			LOGVSUBP(LOGL_ERROR, vsub, "IMSI in ID RESP differs:"
+				 " %s\n", mi_string);
+		} else {
+			strncpy(vsub->imsi, mi_string, sizeof(vsub->imsi));
+			vsub->imsi[sizeof(vsub->imsi)-1] = '\0';
+		}
+		/* retry with identity=IMSI */
+		afp->by_imsi = true;
+		osmo_fsm_inst_state_chg(fi, VLR_SUB_AS_NEEDS_AUTH, 0, 0);
+		osmo_fsm_inst_dispatch(fi, VLR_AUTH_E_START, NULL);
+		break;
+	}
+}
+
+static const struct osmo_fsm_state auth_fsm_states[] = {
+	[VLR_SUB_AS_NEEDS_AUTH] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_NEEDS_AUTH),
+		.in_event_mask = S(VLR_AUTH_E_START),
+		.out_state_mask = S(VLR_SUB_AS_NEEDS_AUTH_WAIT_AI) |
+				  S(VLR_SUB_AS_WAIT_RESP),
+		.action = auth_fsm_needs_auth,
+	},
+	[VLR_SUB_AS_NEEDS_AUTH_WAIT_AI] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_NEEDS_AUTH_WAIT_AI),
+		.in_event_mask = S(VLR_AUTH_E_HLR_SAI_ACK) |
+				 S(VLR_AUTH_E_HLR_SAI_NACK),
+		.out_state_mask = S(VLR_SUB_AS_AUTH_FAILED) |
+				  S(VLR_SUB_AS_WAIT_RESP),
+		.action = auth_fsm_wait_ai,
+	},
+	[VLR_SUB_AS_WAIT_RESP] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_WAIT_RESP),
+		.in_event_mask = S(VLR_AUTH_E_MS_AUTH_RESP) |
+				 S(VLR_AUTH_E_MS_AUTH_FAIL),
+		.out_state_mask = S(VLR_SUB_AS_WAIT_ID_IMSI) |
+				  S(VLR_SUB_AS_AUTH_FAILED) |
+				  S(VLR_SUB_AS_AUTHENTICATED) |
+				  S(VLR_SUB_AS_NEEDS_AUTH_WAIT_SAI_RESYNC),
+		.action = auth_fsm_wait_auth_resp,
+	},
+	[VLR_SUB_AS_NEEDS_AUTH_WAIT_SAI_RESYNC] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_NEEDS_AUTH_WAIT_SAI_RESYNC),
+		.in_event_mask = S(VLR_AUTH_E_HLR_SAI_ACK) |
+				 S(VLR_AUTH_E_HLR_SAI_NACK),
+		.out_state_mask = S(VLR_SUB_AS_AUTH_FAILED) |
+				  S(VLR_SUB_AS_WAIT_RESP_RESYNC),
+		.action = auth_fsm_wait_ai_resync,
+	},
+	[VLR_SUB_AS_WAIT_RESP_RESYNC] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_WAIT_RESP_RESYNC),
+		.in_event_mask = S(VLR_AUTH_E_MS_AUTH_RESP) |
+				 S(VLR_AUTH_E_MS_AUTH_FAIL),
+		.out_state_mask = S(VLR_SUB_AS_AUTH_FAILED) |
+				  S(VLR_SUB_AS_AUTHENTICATED),
+		.action = auth_fsm_wait_auth_resp_resync,
+	},
+	[VLR_SUB_AS_WAIT_ID_IMSI] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_WAIT_ID_IMSI),
+		.in_event_mask = S(VLR_AUTH_E_MS_ID_IMSI),
+		.out_state_mask = S(VLR_SUB_AS_NEEDS_AUTH),
+		.action = auth_fsm_wait_imsi,
+	},
+	[VLR_SUB_AS_AUTHENTICATED] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_AUTHENTICATED),
+		.in_event_mask = 0,
+		.out_state_mask = 0,
+	},
+	[VLR_SUB_AS_AUTH_FAILED] = {
+		.name = OSMO_STRINGIFY(VLR_SUB_AS_AUTH_FAILED),
+		.in_event_mask = 0,
+		.out_state_mask = 0,
+		.onenter = auth_fsm_onenter_failed,
+	},
+};
+
+struct osmo_fsm vlr_auth_fsm = {
+	.name = "VLR_Authenticate",
+	.states = auth_fsm_states,
+	.num_states = ARRAY_SIZE(auth_fsm_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = fsm_auth_event_names,
+};
+
+/***********************************************************************
+ * User API (for SGSN/MSC code)
+ ***********************************************************************/
+
+/* MSC->VLR: Start Procedure Authenticate_VLR (TS 23.012 Ch. 4.1.2.2) */
+struct osmo_fsm_inst *auth_fsm_start(struct vlr_subscr *vsub,
+				     uint32_t log_level,
+				     struct osmo_fsm_inst *parent,
+				     uint32_t parent_term_event,
+				     bool is_r99,
+				     bool is_utran)
+{
+	struct osmo_fsm_inst *fi;
+	struct auth_fsm_priv *afp;
+
+	fi = osmo_fsm_inst_alloc_child(&vlr_auth_fsm, parent,
+					parent_term_event);
+
+
+	afp = talloc_zero(fi, struct auth_fsm_priv);
+	if (!afp) {
+		osmo_fsm_inst_dispatch(parent, parent_term_event, 0);
+		return NULL;
+	}
+
+	afp->vsub = vsub;
+	if (vsub->imsi[0])
+		afp->by_imsi = true;
+	afp->is_r99 = is_r99;
+	afp->is_utran = is_utran;
+	fi->priv = afp;
+	vsub->auth_fsm = fi;
+
+	osmo_fsm_inst_dispatch(fi, VLR_AUTH_E_START, NULL);
+
+	return fi;
+}
diff --git a/src/libvlr/vlr_auth_fsm.h b/src/libvlr/vlr_auth_fsm.h
new file mode 100644
index 0000000..226435f
--- /dev/null
+++ b/src/libvlr/vlr_auth_fsm.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <osmocom/core/utils.h>
+
+/* Parameters to VLR_AUTH_E_MS_AUTH_RESP */
+struct vlr_auth_resp_par {
+	bool is_r99;
+	bool is_utran;
+	const uint8_t *res;
+	unsigned int res_len;
+	const uint8_t *auts;
+};
+
+/* Result communicated back to parent FMS */
+enum vlr_auth_fsm_result {
+	VLR_AUTH_RES_ABORTED,
+	VLR_AUTH_RES_UNKNOWN_SUBSCR,
+	VLR_AUTH_RES_PROC_ERR,
+	VLR_AUTH_RES_AUTH_FAILED,
+	VLR_AUTH_RES_PASSED,
+};
+
+extern const struct value_string vlr_auth_fsm_result_names[];
+static inline const char *vlr_auth_fsm_result_name(enum vlr_auth_fsm_result val)
+{
+	return get_value_string(vlr_auth_fsm_result_names, val);
+}
+
+enum vlr_fsm_auth_event {
+	VLR_AUTH_E_START,
+	/* TS 23.018 OAS_VLR1(2): SendAuthInfo ACK from HLR */
+	VLR_AUTH_E_HLR_SAI_ACK,
+	/* TS 23.018 OAS_VLR1(2): SendAuthInfo NACK from HLR */
+	VLR_AUTH_E_HLR_SAI_NACK,
+	/* FIXME: merge with NACK? */
+	VLR_AUTH_E_HLR_SAI_ABORT,
+	/* Authentication Response from MS */
+	VLR_AUTH_E_MS_AUTH_RESP,
+	/* Authentication Failure from MS */
+	VLR_AUTH_E_MS_AUTH_FAIL,
+	/* Identity Response (IMSI) from MS */
+	VLR_AUTH_E_MS_ID_IMSI,
+};
+
+struct osmo_fsm vlr_auth_fsm;
+
+struct osmo_fsm_inst *auth_fsm_start(struct vlr_subscr *vsub,
+				     uint32_t log_level,
+				     struct osmo_fsm_inst *parent,
+				     uint32_t parent_term_event,
+				     bool is_r99,
+				     bool is_utran);
diff --git a/src/libvlr/vlr_core.h b/src/libvlr/vlr_core.h
new file mode 100644
index 0000000..0e63c7e
--- /dev/null
+++ b/src/libvlr/vlr_core.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <openbsc/vlr.h>
+
+#define LOGGSUPP(level, gsup, fmt, args...) \
+	LOGP(DVLR, level, "GSUP(%s) " fmt, \
+	     (gsup)->imsi, \
+	     ## args)
+
+#define LOGVSUBP(level, vsub, fmt, args...) \
+	LOGP(DVLR, level, "SUBSCR(%s) " fmt, \
+		vlr_subscr_name(vsub), ## args)
+
+
+const char *vlr_subscr_name(struct vlr_subscr *vsub);
+int vlr_subscr_req_lu(struct vlr_subscr *vsub, bool is_ps);
+int vlr_subscr_req_sai(struct vlr_subscr *vsub, const uint8_t *auts,
+		       const uint8_t *auts_rand);
+struct vlr_subscr *vlr_subscr_alloc(struct vlr_instance *vlr);
+void vlr_subscr_update_tuples(struct vlr_subscr *vsub,
+			      const struct osmo_gsup_message *gsup);
diff --git a/src/libvlr/vlr_lu_fsm.c b/src/libvlr/vlr_lu_fsm.c
new file mode 100644
index 0000000..d32659f
--- /dev/null
+++ b/src/libvlr/vlr_lu_fsm.c
@@ -0,0 +1,1424 @@
+/* Osmocom Visitor Location Register (VLR): Location Update FSMs */
+
+/* (C) 2016 by Harald Welte <laforge at 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 <osmocom/core/fsm.h>
+#include <osmocom/gsm/gsup.h>
+#include <openbsc/vlr.h>
+#include <openbsc/debug.h>
+
+#include "vlr_core.h"
+#include "vlr_auth_fsm.h"
+#include "vlr_lu_fsm.h"
+
+#define S(x)	(1 << (x))
+
+#define LU_TIMEOUT_LONG		30
+
+enum vlr_fsm_result {
+	VLR_FSM_RESULT_NONE,
+	VLR_FSM_RESULT_SUCCESS,
+	VLR_FSM_RESULT_FAILURE,
+};
+
+
+/***********************************************************************
+ * Update_HLR_VLR, TS 23.012 Chapter 4.1.2.4
+ ***********************************************************************/
+
+enum upd_hlr_vlr_state {
+	UPD_HLR_VLR_S_INIT,
+	UPD_HLR_VLR_S_WAIT_FOR_DATA,
+	UPD_HLR_VLR_S_DONE,
+};
+
+enum upd_hlr_vlr_evt {
+	UPD_HLR_VLR_E_START,
+	UPD_HLR_VLR_E_INS_SUB_DATA,
+	UPD_HLR_VLR_E_ACT_TRACE_MODE,
+	UPD_HLR_VLR_E_FW_CHECK_SS_IND,
+	UPD_HLR_VLR_E_UPD_LOC_ACK,
+	UPD_HLR_VLR_E_UPD_LOC_NACK,
+};
+
+static const struct value_string upd_hlr_vlr_event_names[] = {
+	OSMO_VALUE_STRING(UPD_HLR_VLR_E_START),
+	OSMO_VALUE_STRING(UPD_HLR_VLR_E_INS_SUB_DATA),
+	OSMO_VALUE_STRING(UPD_HLR_VLR_E_ACT_TRACE_MODE),
+	OSMO_VALUE_STRING(UPD_HLR_VLR_E_FW_CHECK_SS_IND),
+	OSMO_VALUE_STRING(UPD_HLR_VLR_E_UPD_LOC_ACK),
+	OSMO_VALUE_STRING(UPD_HLR_VLR_E_UPD_LOC_NACK),
+	{ 0, NULL }
+};
+
+static void upd_hlr_vlr_fsm_init(struct osmo_fsm_inst *fi, uint32_t event,
+				 void *data)
+{
+	struct vlr_subscr *vsub = fi->priv;
+
+	OSMO_ASSERT(event == UPD_HLR_VLR_E_START);
+
+	/* Send UpdateLocation to HLR */
+	vlr_subscr_req_lu(vsub, vsub->vlr->cfg.is_ps);
+	osmo_fsm_inst_state_chg(fi, UPD_HLR_VLR_S_WAIT_FOR_DATA,
+				LU_TIMEOUT_LONG, 0);
+}
+
+static void upd_hlr_vlr_fsm_wait_data(struct osmo_fsm_inst *fi, uint32_t event,
+				      void *data)
+{
+	struct vlr_subscr *vsub = fi->priv;
+
+	switch (event) {
+	case UPD_HLR_VLR_E_INS_SUB_DATA:
+		/* FIXME: Insert_Subscr_Data_VLR */
+		break;
+	case UPD_HLR_VLR_E_ACT_TRACE_MODE:
+		/* TODO: Activate_Tracing_VLR */
+		break;
+	case UPD_HLR_VLR_E_FW_CHECK_SS_IND:
+		/* TODO: Forward Check SS Ind to MSC */
+		break;
+	case UPD_HLR_VLR_E_UPD_LOC_ACK:
+		/* Inside Update_HLR_VLR after UpdateLocationAck */
+		vsub->sub_dataconf_by_hlr_ind = true;
+		vsub->loc_conf_in_hlr_ind = true;
+		osmo_fsm_inst_state_chg(fi, UPD_HLR_VLR_S_DONE, 0, 0);
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+		break;
+	case UPD_HLR_VLR_E_UPD_LOC_NACK:
+		/* Inside Update_HLR_VLR after UpdateLocationNack */
+		/* TODO: Check_User_Error_In_Serving_Network_Entity */
+		vsub->sub_dataconf_by_hlr_ind = false;
+		vsub->loc_conf_in_hlr_ind = false;
+		osmo_fsm_inst_state_chg(fi, UPD_HLR_VLR_S_DONE, 0, 0);
+		/* Data is a pointer to a gsm48_gmm_cause which we
+		 * simply pass through */
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, data);
+		break;
+	}
+}
+
+static const struct osmo_fsm_state upd_hlr_vlr_states[] = {
+	[UPD_HLR_VLR_S_INIT] = {
+		.in_event_mask = S(UPD_HLR_VLR_E_START),
+		.out_state_mask = S(UPD_HLR_VLR_S_WAIT_FOR_DATA),
+		.name = OSMO_STRINGIFY(UPD_HLR_VLR_S_INIT),
+		.action = upd_hlr_vlr_fsm_init,
+	},
+	[UPD_HLR_VLR_S_WAIT_FOR_DATA] = {
+		.in_event_mask = S(UPD_HLR_VLR_E_INS_SUB_DATA) |
+				 S(UPD_HLR_VLR_E_ACT_TRACE_MODE) |
+				 S(UPD_HLR_VLR_E_FW_CHECK_SS_IND) |
+				 S(UPD_HLR_VLR_E_UPD_LOC_ACK) |
+				 S(UPD_HLR_VLR_E_UPD_LOC_NACK),
+		.out_state_mask = S(UPD_HLR_VLR_S_DONE),
+		.name = OSMO_STRINGIFY(UPD_HLR_VLR_S_WAIT_FOR_DATA),
+		.action = upd_hlr_vlr_fsm_wait_data,
+	},
+	[UPD_HLR_VLR_S_DONE] = {
+		.name = OSMO_STRINGIFY(UPD_HLR_VLR_S_DONE),
+	},
+};
+
+static struct osmo_fsm upd_hlr_vlr_fsm = {
+	.name = "upd_hlr_vlr_fsm",
+	.states = upd_hlr_vlr_states,
+	.num_states = ARRAY_SIZE(upd_hlr_vlr_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = upd_hlr_vlr_event_names,
+};
+
+struct osmo_fsm_inst *
+upd_hlr_vlr_proc_start(struct osmo_fsm_inst *parent,
+		       struct vlr_subscr *vsub,
+		       uint32_t parent_event)
+{
+	struct osmo_fsm_inst *fi;
+
+	fi = osmo_fsm_inst_alloc_child(&upd_hlr_vlr_fsm, parent,
+					parent_event);
+	if (!fi)
+		return NULL;
+
+	fi->priv = vsub;
+	osmo_fsm_inst_dispatch(fi, UPD_HLR_VLR_E_START, NULL);
+
+	return fi;
+}
+
+
+/***********************************************************************
+ * Subscriber_Present_VLR, TS 29.002 Chapter 25.10.1
+ ***********************************************************************/
+
+enum sub_pres_vlr_state {
+	SUB_PRES_VLR_S_INIT,
+	SUB_PRES_VLR_S_WAIT_FOR_HLR,
+	SUB_PRES_VLR_S_DONE,
+};
+
+enum sub_pres_vlr_event {
+	SUB_PRES_VLR_E_START,
+	SUB_PRES_VLR_E_READY_SM_CNF,
+	SUB_PRES_VLR_E_READY_SM_ERR,
+};
+
+static const struct value_string sub_pres_vlr_event_names[] = {
+	OSMO_VALUE_STRING(SUB_PRES_VLR_E_START),
+	OSMO_VALUE_STRING(SUB_PRES_VLR_E_READY_SM_CNF),
+	OSMO_VALUE_STRING(SUB_PRES_VLR_E_READY_SM_ERR),
+	{ 0, NULL }
+};
+
+static void sub_pres_vlr_fsm_init(struct osmo_fsm_inst *fi, uint32_t event,
+				  void *data)
+{
+	struct vlr_subscr *vsub = fi->priv;
+	OSMO_ASSERT(event == SUB_PRES_VLR_E_START);
+
+	if (!vsub->ms_not_reachable_flag) {
+		osmo_fsm_inst_state_chg(fi, SUB_PRES_VLR_S_DONE, 0, 0);
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+		return;
+	}
+	/* FIXME: Send READY_FOR_SM via GSUP */
+	osmo_fsm_inst_state_chg(fi, SUB_PRES_VLR_S_WAIT_FOR_HLR,
+				LU_TIMEOUT_LONG, 0);
+}
+
+static void sub_pres_vlr_fsm_wait_hlr(struct osmo_fsm_inst *fi, uint32_t event,
+				      void *data)
+{
+	struct vlr_subscr *vsub = fi->priv;
+
+	switch (event) {
+	case SUB_PRES_VLR_E_READY_SM_CNF:
+		vsub->ms_not_reachable_flag = false;
+		break;
+	case SUB_PRES_VLR_E_READY_SM_ERR:
+		break;
+	}
+	osmo_fsm_inst_state_chg(fi, SUB_PRES_VLR_S_DONE, 0, 0);
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+static const struct osmo_fsm_state sub_pres_vlr_states[] = {
+	[SUB_PRES_VLR_S_INIT] = {
+		.in_event_mask = S(SUB_PRES_VLR_E_START),
+		.out_state_mask = S(SUB_PRES_VLR_S_WAIT_FOR_HLR) |
+				  S(SUB_PRES_VLR_S_DONE),
+		.name = OSMO_STRINGIFY(SUB_PRES_VLR_S_INIT),
+		.action = sub_pres_vlr_fsm_init,
+	},
+	[SUB_PRES_VLR_S_WAIT_FOR_HLR] = {
+		.in_event_mask = S(SUB_PRES_VLR_E_READY_SM_CNF) |
+				 S(SUB_PRES_VLR_E_READY_SM_ERR),
+		.out_state_mask = S(SUB_PRES_VLR_S_DONE),
+		.name = OSMO_STRINGIFY(SUB_PRES_VLR_S_WAIT_FOR_HLR),
+		.action = sub_pres_vlr_fsm_wait_hlr,
+	},
+	[SUB_PRES_VLR_S_DONE] = {
+		.name = OSMO_STRINGIFY(SUB_PRES_VLR_S_DONE),
+	},
+};
+
+static struct osmo_fsm sub_pres_vlr_fsm = {
+	.name = "sub_pres_vlr_fsm",
+	.states = sub_pres_vlr_states,
+	.num_states = ARRAY_SIZE(sub_pres_vlr_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = sub_pres_vlr_event_names,
+};
+
+struct osmo_fsm_inst *sub_pres_vlr_fsm_start(struct osmo_fsm_inst *parent,
+					     struct vlr_subscr *vsub,
+					     uint32_t term_event)
+{
+	struct osmo_fsm_inst *fi;
+
+	fi = osmo_fsm_inst_alloc_child(&sub_pres_vlr_fsm, parent,
+					term_event);
+	if (!fi)
+		return NULL;
+
+	fi->priv = vsub;
+	osmo_fsm_inst_dispatch(fi, SUB_PRES_VLR_E_START, NULL);
+
+	return fi;
+}
+
+/***********************************************************************
+ * Location_Update_Completion_VLR, TS 23.012 Chapter 4.1.2.3
+ ***********************************************************************/
+
+enum lu_compl_vlr_state {
+	LU_COMPL_VLR_S_INIT,
+	LU_COMPL_VLR_S_WAIT_SUB_PRES,
+	LU_COMPL_VLR_S_WAIT_IMEI,
+	LU_COMPL_VLR_S_WAIT_IMEI_TMSI,
+	LU_COMPL_VLR_S_WAIT_TMSI_CNF,
+	LU_COMPL_VLR_S_DONE,
+};
+
+enum lu_compl_vlr_event {
+	LU_COMPL_VLR_E_START,
+	LU_COMPL_VLR_E_SUB_PRES_COMPL,
+	LU_COMPL_VLR_E_IMEI_CHECK_ACK,
+	LU_COMPL_VLR_E_IMEI_CHECK_NACK,
+	LU_COMPL_VLR_E_NEW_TMSI_ACK,
+};
+
+static const struct value_string lu_compl_vlr_event_names[] = {
+	OSMO_VALUE_STRING(LU_COMPL_VLR_E_START),
+	OSMO_VALUE_STRING(LU_COMPL_VLR_E_SUB_PRES_COMPL),
+	OSMO_VALUE_STRING(LU_COMPL_VLR_E_IMEI_CHECK_ACK),
+	OSMO_VALUE_STRING(LU_COMPL_VLR_E_IMEI_CHECK_NACK),
+	OSMO_VALUE_STRING(LU_COMPL_VLR_E_NEW_TMSI_ACK),
+	{ 0, NULL }
+};
+
+struct lu_compl_vlr_priv {
+	struct vlr_subscr *vsub;
+	void *msc_conn_ref;
+	struct osmo_fsm_inst *sub_pres_vlr_fsm;
+	uint32_t parent_event_success;
+	uint32_t parent_event_failure;
+	void *parent_event_data;
+	enum vlr_fsm_result result;
+	uint8_t cause;
+	bool assign_tmsi;
+};
+
+static void _vlr_lu_compl_fsm_done(struct osmo_fsm_inst *fi,
+				   enum vlr_fsm_result result,
+				   uint8_t cause)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	lcvp->result = result;
+	lcvp->cause = cause;
+	osmo_fsm_inst_state_chg(fi, LU_COMPL_VLR_S_DONE, 0, 0);
+}
+
+static void vlr_lu_compl_fsm_success(struct osmo_fsm_inst *fi)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	struct vlr_subscr *vsub = lcvp->vsub;
+	if (!vsub->lu_complete) {
+		vsub->lu_complete = true;
+		/* Balanced by vlr_subscr_rx_imsi_detach() */
+		vlr_subscr_get(vsub);
+	}
+	_vlr_lu_compl_fsm_done(fi, VLR_FSM_RESULT_SUCCESS, 0);
+}
+
+static void vlr_lu_compl_fsm_failure(struct osmo_fsm_inst *fi, uint8_t cause)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	lcvp->vsub->vlr->ops.tx_lu_rej(lcvp->msc_conn_ref, cause);
+	_vlr_lu_compl_fsm_done(fi, VLR_FSM_RESULT_FAILURE, cause);
+}
+
+static void vlr_lu_compl_fsm_dispatch_result(struct osmo_fsm_inst *fi,
+					     uint32_t prev_state)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	if (!fi->proc.parent) {
+		LOGPFSML(fi, LOGL_ERROR, "No parent FSM\n");
+		return;
+	}
+	osmo_fsm_inst_dispatch(fi->proc.parent,
+			       (lcvp->result == VLR_FSM_RESULT_SUCCESS)
+			       ? lcvp->parent_event_success
+			       : lcvp->parent_event_failure,
+			       &lcvp->cause);
+}
+
+static void lu_compl_vlr_init(struct osmo_fsm_inst *fi, uint32_t event,
+			      void *data)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	struct vlr_subscr *vsub = lcvp->vsub;
+	struct vlr_instance *vlr;
+	OSMO_ASSERT(vsub);
+	vlr = vsub->vlr;
+	OSMO_ASSERT(vlr);
+
+	OSMO_ASSERT(event == LU_COMPL_VLR_E_START);
+
+	/* TODO: National Roaming restrictions? */
+	/* TODO: Roaming restriction due to unsupported feature in subscriber
+	 * data? */
+	/* TODO: Regional subscription restriction? */
+	/* TODO: Administrative restriction of subscribres' access feature? */
+	/* TODO: AccessRestrictuionData parameter available? */
+	/* TODO: AccessRestrictionData permits RAT? */
+	/* Node 1 */
+	/* TODO: Autonomous CSG supported in VPLMN and allowed by HPLMN? */
+	/* TODO: Hybrid Cel / CSG Cell */
+	/* Node 2 */
+	vsub->la_allowed = true;
+	vsub->imsi_detached_flag = false;
+	/* Start Subscriber_Present_VLR Procedure */
+	osmo_fsm_inst_state_chg(fi, LU_COMPL_VLR_S_WAIT_SUB_PRES,
+				LU_TIMEOUT_LONG, 0);
+
+	lcvp->sub_pres_vlr_fsm = sub_pres_vlr_fsm_start(fi, vsub,
+						LU_COMPL_VLR_E_SUB_PRES_COMPL);
+
+}
+
+static void lu_compl_vlr_new_tmsi(struct osmo_fsm_inst *fi)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	struct vlr_subscr *vsub = lcvp->vsub;
+	struct vlr_instance *vlr = vsub->vlr;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	if (vlr_subscr_alloc_tmsi(vsub)) {
+		vlr_lu_compl_fsm_failure(fi,
+					 GSM48_REJECT_SRV_OPT_TMP_OUT_OF_ORDER);
+		return;
+	}
+
+	osmo_fsm_inst_state_chg(fi, LU_COMPL_VLR_S_WAIT_TMSI_CNF,
+				vlr_timer(vlr, 3250), 3250);
+
+	vlr->ops.tx_lu_acc(lcvp->msc_conn_ref, vsub->tmsi_new);
+}
+
+/* After completion of Subscriber_Present_VLR */
+static void lu_compl_vlr_wait_subscr_pres(struct osmo_fsm_inst *fi,
+					  uint32_t event,
+					  void *data)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	struct vlr_subscr *vsub = lcvp->vsub;
+	struct vlr_instance *vlr = vsub->vlr;
+
+	OSMO_ASSERT(event == LU_COMPL_VLR_E_SUB_PRES_COMPL);
+
+	lcvp->sub_pres_vlr_fsm = NULL;
+
+	/* TODO: Trace_Subscriber_Activity_VLR */
+
+	if (vlr->cfg.check_imei_rqd) {
+		/* Check IMEI VLR */
+		osmo_fsm_inst_state_chg(fi,
+					lcvp->assign_tmsi ?
+					  LU_COMPL_VLR_S_WAIT_IMEI_TMSI
+					: LU_COMPL_VLR_S_WAIT_IMEI,
+					vlr_timer(vlr, 3270), 3270);
+		vlr->ops.tx_id_req(lcvp->msc_conn_ref, GSM_MI_TYPE_IMEI);
+		return;
+	}
+
+	/* Do we need to allocate a TMSI? */
+	if (lcvp->assign_tmsi) {
+		lu_compl_vlr_new_tmsi(fi);
+		return;
+	}
+
+	/* Location Updating Accept */
+	vlr->ops.tx_lu_acc(lcvp->msc_conn_ref, GSM_RESERVED_TMSI);
+	vlr_lu_compl_fsm_success(fi);
+}
+
+/* Waiting for completion of CHECK_IMEI_VLR */
+static void lu_compl_vlr_wait_imei(struct osmo_fsm_inst *fi, uint32_t event,
+				   void *data)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	struct vlr_subscr *vsub = lcvp->vsub;
+	struct vlr_instance *vlr = vsub->vlr;
+
+	switch (event) {
+	case LU_COMPL_VLR_E_IMEI_CHECK_ACK:
+		if (!vsub->imei[0]) {
+			/* Abort: Do nothing */
+			vlr_lu_compl_fsm_failure(fi,
+						 GSM48_REJECT_PROTOCOL_ERROR);
+			return;
+		}
+		/* Pass */
+		break;
+
+	case LU_COMPL_VLR_E_IMEI_CHECK_NACK:
+		vlr_lu_compl_fsm_failure(fi, GSM48_REJECT_ILLEGAL_ME);
+		/* FIXME: IMEI Check Fail to VLR Application (Detach IMSI VLR) */
+		return;
+	}
+
+	/* IMEI is available. Allocate TMSI if needed. */
+	if (lcvp->assign_tmsi) {
+		if (fi->state != LU_COMPL_VLR_S_WAIT_IMEI_TMSI)
+			LOGPFSML(fi, LOGL_ERROR,
+				 "TMSI required, expected to be in state"
+				 " LU_COMPL_VLR_S_WAIT_IMEI_TMSI,"
+				 " am in %s instead\n",
+				 osmo_fsm_state_name(fi->fsm, fi->state));
+			/* Logged an error, continue anyway. */
+
+		lu_compl_vlr_new_tmsi(fi);
+
+		/* Wait for TMSI ack */
+		return;
+	}
+
+	/* No TMSI needed, accept now. */
+	vlr->ops.tx_lu_acc(lcvp->msc_conn_ref, GSM_RESERVED_TMSI);
+	vlr_lu_compl_fsm_success(fi);
+}
+
+/* Waiting for TMSI confirmation */
+static void lu_compl_vlr_wait_tmsi(struct osmo_fsm_inst *fi, uint32_t event,
+				   void *data)
+{
+	struct lu_compl_vlr_priv *lcvp = fi->priv;
+	struct vlr_subscr *vsub = lcvp->vsub;
+
+	OSMO_ASSERT(event == LU_COMPL_VLR_E_NEW_TMSI_ACK);
+
+	if (!vsub || vsub->tmsi_new == GSM_RESERVED_TMSI) {
+		LOGPFSML(fi, LOGL_ERROR, "TMSI Realloc Compl implies that"
+			 " the subscriber has a new TMSI allocated, but"
+			 " the new TMSI is unset.\n");
+		vlr_lu_compl_fsm_failure(fi, GSM48_REJECT_NETWORK_FAILURE);
+		return;
+	}
+
+	vsub->tmsi = vsub->tmsi_new;
+	vsub->tmsi_new = GSM_RESERVED_TMSI;
+
+	vlr_lu_compl_fsm_success(fi);
+}
+
+static const struct osmo_fsm_state lu_compl_vlr_states[] = {
+	[LU_COMPL_VLR_S_INIT] = {
+		.in_event_mask = S(LU_COMPL_VLR_E_START),
+		.out_state_mask = S(LU_COMPL_VLR_S_DONE) |
+				  S(LU_COMPL_VLR_S_WAIT_SUB_PRES) |
+				  S(LU_COMPL_VLR_S_WAIT_IMEI),
+		.name = OSMO_STRINGIFY(LU_COMPL_VLR_S_INIT),
+		.action = lu_compl_vlr_init,
+	},
+	[LU_COMPL_VLR_S_WAIT_SUB_PRES] = {
+		.in_event_mask = S(LU_COMPL_VLR_E_SUB_PRES_COMPL),
+		.out_state_mask = S(LU_COMPL_VLR_S_WAIT_IMEI) |
+				  S(LU_COMPL_VLR_S_WAIT_IMEI_TMSI) |
+				  S(LU_COMPL_VLR_S_WAIT_TMSI_CNF) |
+				  S(LU_COMPL_VLR_S_DONE),
+		.name = OSMO_STRINGIFY(LU_COMPL_VLR_S_WAIT_SUB_PRES),
+		.action = lu_compl_vlr_wait_subscr_pres,
+	},
+	[LU_COMPL_VLR_S_WAIT_IMEI] = {
+		.in_event_mask = S(LU_COMPL_VLR_E_IMEI_CHECK_ACK) |
+				 S(LU_COMPL_VLR_E_IMEI_CHECK_NACK),
+		.out_state_mask = S(LU_COMPL_VLR_S_DONE),
+		.name = OSMO_STRINGIFY(LU_COMPL_VLR_S_WAIT_IMEI),
+		.action = lu_compl_vlr_wait_imei,
+	},
+	[LU_COMPL_VLR_S_WAIT_IMEI_TMSI] = {
+		.in_event_mask = S(LU_COMPL_VLR_E_IMEI_CHECK_ACK) |
+				 S(LU_COMPL_VLR_E_IMEI_CHECK_NACK),
+		.out_state_mask = S(LU_COMPL_VLR_S_DONE) |
+				  S(LU_COMPL_VLR_S_WAIT_TMSI_CNF),
+		.name = OSMO_STRINGIFY(LU_COMPL_VLR_S_WAIT_IMEI_TMSI),
+		.action = lu_compl_vlr_wait_imei,
+	},
+	[LU_COMPL_VLR_S_WAIT_TMSI_CNF] = {
+		.in_event_mask = S(LU_COMPL_VLR_E_NEW_TMSI_ACK),
+		.out_state_mask = S(LU_COMPL_VLR_S_DONE),
+		.name = OSMO_STRINGIFY(LU_COMPL_VLR_S_WAIT_TMSI_CNF),
+		.action = lu_compl_vlr_wait_tmsi,
+	},
+	[LU_COMPL_VLR_S_DONE] = {
+		.name = OSMO_STRINGIFY(LU_COMPL_VLR_S_DONE),
+		.onenter = vlr_lu_compl_fsm_dispatch_result,
+	},
+};
+
+static struct osmo_fsm lu_compl_vlr_fsm = {
+	.name = "lu_compl_vlr_fsm",
+	.states = lu_compl_vlr_states,
+	.num_states = ARRAY_SIZE(lu_compl_vlr_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = lu_compl_vlr_event_names,
+};
+
+struct osmo_fsm_inst *
+lu_compl_vlr_proc_alloc(struct osmo_fsm_inst *parent,
+			struct vlr_subscr *vsub,
+			void *msc_conn_ref,
+			uint32_t parent_event_success,
+			uint32_t parent_event_failure,
+			bool assign_tmsi)
+{
+	struct osmo_fsm_inst *fi;
+	struct lu_compl_vlr_priv *lcvp;
+
+	fi = osmo_fsm_inst_alloc_child(&lu_compl_vlr_fsm, parent,
+				       parent_event_failure);
+	if (!fi)
+		return NULL;
+
+	lcvp = talloc_zero(fi, struct lu_compl_vlr_priv);
+	lcvp->vsub = vsub;
+	lcvp->msc_conn_ref = msc_conn_ref;
+	lcvp->parent_event_success = parent_event_success;
+	lcvp->parent_event_failure = parent_event_failure;
+	lcvp->assign_tmsi = assign_tmsi;
+	fi->priv = lcvp;
+
+	return fi;
+}
+
+
+/***********************************************************************
+ * Update_Location_Area_VLR, TS 23.012 Chapter 4.1.2.1
+ ***********************************************************************/
+
+static const struct value_string fsm_lu_event_names[] = {
+	OSMO_VALUE_STRING(VLR_ULA_E_UPDATE_LA),
+	OSMO_VALUE_STRING(VLR_ULA_E_SEND_ID_ACK),
+	OSMO_VALUE_STRING(VLR_ULA_E_SEND_ID_NACK),
+	OSMO_VALUE_STRING(VLR_ULA_E_AUTH_RES),
+	OSMO_VALUE_STRING(VLR_ULA_E_CIPH_RES),
+	OSMO_VALUE_STRING(VLR_ULA_E_ID_IMSI),
+	OSMO_VALUE_STRING(VLR_ULA_E_ID_IMEI),
+	OSMO_VALUE_STRING(VLR_ULA_E_ID_IMEISV),
+	OSMO_VALUE_STRING(VLR_ULA_E_HLR_LU_RES),
+	OSMO_VALUE_STRING(VLR_ULA_E_UPD_HLR_COMPL),
+	OSMO_VALUE_STRING(VLR_ULA_E_LU_COMPL_SUCCESS),
+	OSMO_VALUE_STRING(VLR_ULA_E_LU_COMPL_FAILURE),
+	OSMO_VALUE_STRING(VLR_ULA_E_NEW_TMSI_ACK),
+	{ 0, NULL }
+};
+
+struct lu_fsm_priv {
+	struct vlr_instance *vlr;
+	struct vlr_subscr *vsub;
+	void *msc_conn_ref;
+	struct osmo_fsm_inst *upd_hlr_vlr_fsm;
+	struct osmo_fsm_inst *lu_compl_vlr_fsm;
+	uint32_t parent_event_success;
+	uint32_t parent_event_failure;
+	void *parent_event_data;
+	enum vlr_fsm_result result;
+	uint8_t rej_cause;
+
+	enum vlr_lu_type type;
+	bool lu_by_tmsi;
+	char imsi[16];
+	uint32_t tmsi;
+	struct osmo_location_area_id old_lai;
+	struct osmo_location_area_id new_lai;
+	bool authentication_required;
+	enum vlr_ciph ciphering_required;
+	bool is_r99;
+	bool is_utran;
+	bool assign_tmsi;
+};
+
+
+/* Determine if given location area is served by this VLR */
+static bool lai_in_this_vlr(struct vlr_instance *vlr,
+			    const struct osmo_location_area_id *lai)
+{
+	/* TODO: VLR needs to keep a locally configued list of LAIs */
+	return true;
+}
+
+/* Determine if authentication is required */
+static bool is_auth_required(struct lu_fsm_priv *lfp)
+{
+	/* The cases where the authentication procedure should be used
+	 * are defined in 3GPP TS 33.102 */
+	/* For now we use a default value passed in to vlr_lu_fsm(). */
+	return lfp->authentication_required
+	       || (lfp->ciphering_required != VLR_CIPH_NONE);
+}
+
+/* Determine if ciphering is required */
+static bool is_ciph_required(struct lu_fsm_priv *lfp)
+{
+	return lfp->ciphering_required != VLR_CIPH_NONE;
+}
+
+/* Determine if a HLR Update is required */
+static bool hlr_update_needed(struct vlr_subscr *vsub)
+{
+	/* TODO: properly decide this, rather than always assuming we
+	 * need to update the HLR. */
+	return true;
+}
+
+static void lu_fsm_dispatch_result(struct osmo_fsm_inst *fi,
+				   uint32_t prev_state)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	if (!fi->proc.parent) {
+		LOGPFSML(fi, LOGL_ERROR, "No parent FSM\n");
+		return;
+	}
+	osmo_fsm_inst_dispatch(fi->proc.parent,
+			       (lfp->result == VLR_FSM_RESULT_SUCCESS)
+			       ? lfp->parent_event_success
+			       : lfp->parent_event_failure,
+			       lfp->parent_event_data);
+}
+
+static void _lu_fsm_done(struct osmo_fsm_inst *fi,
+			 enum vlr_fsm_result result)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	lfp->result = result;
+	osmo_fsm_inst_state_chg(fi, VLR_ULA_S_DONE, 0, 0);
+}
+
+static void lu_fsm_success(struct osmo_fsm_inst *fi)
+{
+	_lu_fsm_done(fi, VLR_FSM_RESULT_SUCCESS);
+}
+
+static void lu_fsm_failure(struct osmo_fsm_inst *fi, uint8_t rej_cause)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	if (rej_cause)
+		lfp->vlr->ops.tx_lu_rej(lfp->msc_conn_ref, rej_cause);
+	_lu_fsm_done(fi, VLR_FSM_RESULT_FAILURE);
+}
+
+static void vlr_loc_upd_start_lu_compl_fsm(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	lfp->lu_compl_vlr_fsm =
+		lu_compl_vlr_proc_alloc(fi, lfp->vsub, lfp->msc_conn_ref,
+					VLR_ULA_E_LU_COMPL_SUCCESS,
+					VLR_ULA_E_LU_COMPL_FAILURE,
+					lfp->assign_tmsi);
+
+	osmo_fsm_inst_dispatch(lfp->lu_compl_vlr_fsm, LU_COMPL_VLR_E_START, NULL);
+}
+
+static void lu_fsm_discard_lu_compl_fsm(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	if (!lfp->lu_compl_vlr_fsm)
+		return;
+	osmo_fsm_inst_term(lfp->lu_compl_vlr_fsm, OSMO_FSM_TERM_PARENT, NULL);
+}
+
+/* 4.1.2.1 Node 4 */
+static void vlr_loc_upd_node_4(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+	bool hlr_unknown = false;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	if (hlr_unknown) {
+		/* FIXME: Delete subscriber record */
+		/* LU REJ: Roaming not allowed */
+		lu_fsm_failure(fi, GSM48_REJECT_ROAMING_NOT_ALLOWED);
+	} else {
+		/* Update_HLR_VLR */
+		osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_HLR_UPD,
+					LU_TIMEOUT_LONG, 0);
+		lfp->upd_hlr_vlr_fsm =
+			upd_hlr_vlr_proc_start(fi, vsub, VLR_ULA_E_UPD_HLR_COMPL);
+	}
+}
+
+/* 4.1.2.1 Node B */
+static void vlr_loc_upd_node_b(struct osmo_fsm_inst *fi)
+{
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	/* FIXME */
+	if (0) { /* IMEISV or PgA to send */
+		vlr_loc_upd_node_4(fi);
+	} else {
+		/* Location_Update_Completion */
+		osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_LU_COMPL,
+					LU_TIMEOUT_LONG, 0);
+		vlr_loc_upd_start_lu_compl_fsm(fi);
+	}
+}
+
+/* Non-standard: after Ciphering Mode Complete (or no ciph required) */
+static void vlr_loc_upd_post_ciph(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	OSMO_ASSERT(vsub);
+
+	vsub->conf_by_radio_contact_ind = true;
+	/* Update LAI */
+	vsub->cgi.lai = lfp->new_lai;
+	vsub->dormant_ind = false;
+	vsub->cancel_loc_rx = false;
+	if (hlr_update_needed(vsub)) {
+		vlr_loc_upd_node_4(fi);
+	} else {
+		/* TODO: ADD Support */
+		/* TODO: Node A: PgA Support */
+		vlr_loc_upd_node_b(fi);
+	}
+}
+
+/* 4.1.2.1 after Authentication successful (or no auth rqd) */
+static void vlr_loc_upd_post_auth(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	OSMO_ASSERT(vsub);
+
+	if (!is_ciph_required(lfp)) {
+		vlr_loc_upd_post_ciph(fi);
+		return;
+	}
+
+	if (vlr_set_ciph_mode(vsub->vlr, fi, lfp->msc_conn_ref,
+			      lfp->ciphering_required,
+			      vsub->vlr->cfg.retrieve_imeisv)) {
+		LOGPFSML(fi, LOGL_ERROR,
+			 "Failed to send Ciphering Mode Command\n");
+		vlr_lu_compl_fsm_failure(fi, GSM48_REJECT_NETWORK_FAILURE);
+		return;
+	}
+
+	osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_CIPH, LU_TIMEOUT_LONG, 0);
+}
+
+static void vlr_loc_upd_node1(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	OSMO_ASSERT(vsub);
+
+	if (is_auth_required(lfp)) {
+		/* Authenticate_VLR */
+		osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_AUTH,
+					LU_TIMEOUT_LONG, 0);
+		vsub->auth_fsm = auth_fsm_start(lfp->vsub, fi->log_level,
+						fi, VLR_ULA_E_AUTH_RES,
+						lfp->is_r99,
+						lfp->is_utran);
+	} else {
+		/* no need for authentication */
+		vlr_loc_upd_post_auth(fi);
+	}
+}
+
+static void vlr_loc_upd_want_imsi(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_instance *vlr = lfp->vlr;
+
+	LOGPFSM(fi, "%s()\n", __func__);
+
+	OSMO_ASSERT(lfp->vsub);
+
+	/* Obtain_IMSI_VLR */
+	osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_IMSI,
+				vlr_timer(vlr, 3270), 3270);
+	vlr->ops.tx_id_req(lfp->msc_conn_ref, GSM_MI_TYPE_IMSI);
+	/* will continue at vlr_loc_upd_node1() once IMSI arrives */
+}
+
+static int assoc_lfp_with_sub(struct osmo_fsm_inst *fi, struct vlr_subscr *vsub)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_instance *vlr = lfp->vlr;
+
+	if (vsub->lu_fsm) {
+		LOGPFSML(fi, LOGL_ERROR,
+			 "A Location Updating process is already pending for"
+			 " this subscriber. Aborting.\n");
+		/* Also get rid of the other pending LU attempt? */
+		/*lu_fsm_failure(vsub->lu_fsm, GSM48_REJECT_CONGESTION);*/
+		lu_fsm_failure(fi, GSM48_REJECT_CONGESTION);
+		return -EINVAL;
+	}
+	vsub->lu_fsm = fi;
+	vsub->msc_conn_ref = lfp->msc_conn_ref;
+	/* FIXME: send new LAC to HLR? */
+	vsub->lac = lfp->new_lai.lac;
+	lfp->vsub = vsub;
+	/* Tell MSC to associate this subscriber with the given
+	 * connection */
+	vlr->ops.subscr_assoc(lfp->msc_conn_ref, lfp->vsub);
+	return 0;
+}
+
+static const char *lai_name(struct osmo_location_area_id *lai)
+{
+	static char buf[64];
+	snprintf(buf, sizeof(buf),"MCC:%u, MNC:%u, LAC:%u",
+		 lai->plmn.mcc, lai->plmn.mnc, lai->lac);
+	return buf;
+}
+
+/* 4.1.2.1: Subscriber (via MSC/SGSN) requests location update */
+static void _start_lu_main(struct osmo_fsm_inst *fi)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_instance *vlr = lfp->vlr;
+	struct vlr_subscr *vsub = NULL;
+	bool created;
+
+	/* TODO: PUESBINE related handling */
+
+	/* Is previous LAI in this VLR? */
+	if (!lai_in_this_vlr(vlr, &lfp->old_lai)) {
+#if 0
+		/* FIXME: check previous VLR, (3) */
+		osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_PVLR,
+					LU_TIMEOUT_LONG, 0);
+		return;
+#endif
+		LOGPFSML(fi, LOGL_NOTICE, "LAI change from %s,"
+			 " but checking previous VLR not implemented\n",
+			 lai_name(&lfp->old_lai));
+	}
+
+	if (!lfp->imsi[0]) {
+		/* TMSI was used */
+		lfp->lu_by_tmsi = true;
+		/* TMSI clash: if a different subscriber already has this TMSI,
+		 * we will find that other subscriber in the VLR. So the IMSIs
+		 * would mismatch, but we don't know about it. Theoretically,
+		 * an authentication process would thwart any attempt to use
+		 * someone else's TMSI.
+		 * TODO: Otherwise we can ask for the IMSI and verify that it
+		 * matches the IMSI on record. */
+		vsub = vlr_subscr_find_or_create_by_tmsi(vlr, lfp->tmsi,
+							 &created);
+
+		if (!vsub) {
+			LOGPFSML(fi, LOGL_ERROR,
+				 "VLR subscriber allocation failed\n");
+			lu_fsm_failure(fi, GSM48_REJECT_SRV_OPT_TMP_OUT_OF_ORDER);
+			return;
+		}
+
+		vsub->sub_dataconf_by_hlr_ind = false;
+		if (assoc_lfp_with_sub(fi, vsub)) {
+			vlr_subscr_put(vsub);
+			return; /* error */
+		}
+
+		if (created)
+			vlr_loc_upd_want_imsi(fi);
+		else
+			vlr_loc_upd_node1(fi);
+		/* We cannot have MSC area change, as the VLR
+		 * serves only one MSC */
+		vlr_subscr_put(vsub);
+	} else {
+		/* IMSI was used */
+		vsub = vlr_subscr_find_or_create_by_imsi(vlr, lfp->imsi, NULL);
+
+		if (!vsub) {
+			LOGPFSML(fi, LOGL_ERROR,
+				 "VLR subscriber allocation failed\n");
+			lu_fsm_failure(fi, GSM48_REJECT_SRV_OPT_TMP_OUT_OF_ORDER);
+			vlr_subscr_put(vsub);
+			return;
+		}
+
+		vsub->sub_dataconf_by_hlr_ind = false;
+		if (assoc_lfp_with_sub(fi, vsub)) {
+			vlr_subscr_put(vsub);
+			return; /* error */
+		}
+		vlr_loc_upd_node1(fi);
+		vlr_subscr_put(vsub);
+	}
+}
+
+
+static void lu_fsm_idle(struct osmo_fsm_inst *fi, uint32_t event,
+			void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_instance *vlr = lfp->vlr;
+
+	OSMO_ASSERT(event == VLR_ULA_E_UPDATE_LA);
+
+	if (1) { // FIXME
+	//if (lfp->type == VLR_LU_TYPE_PERIODIC && lfp->vsub->imeisv[0])
+		/* R_IMEISV_IR1 passed */
+		_start_lu_main(fi);
+	} else {
+		vlr->ops.tx_id_req(lfp->msc_conn_ref, GSM_MI_TYPE_IMEISV);
+		osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_IMEISV,
+					vlr_timer(vlr, 3270), 3270);
+	}
+}
+
+static void lu_fsm_wait_imeisv(struct osmo_fsm_inst *fi, uint32_t event,
+			       void *data)
+{
+	switch (event) {
+	case VLR_ULA_E_ID_IMEISV:
+		/* FIXME: copy IMEISV */
+		_start_lu_main(fi);
+		break;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+			 osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+}
+
+/* Wait for response from Send_Identification to PVLR */
+static void lu_fsm_wait_pvlr(struct osmo_fsm_inst *fi, uint32_t event,
+			     void *data)
+{
+	switch (event) {
+	case VLR_ULA_E_SEND_ID_ACK:
+		vlr_loc_upd_node1(fi);
+		break;
+	case VLR_ULA_E_SEND_ID_NACK:
+		vlr_loc_upd_want_imsi(fi);
+		break;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+			 osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+}
+
+/* Wait for result of Authenticate_VLR procedure */
+static void lu_fsm_wait_auth(struct osmo_fsm_inst *fi, uint32_t event,
+			     void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	enum vlr_auth_fsm_result *res = data;
+	uint8_t rej_cause = 0;
+
+	OSMO_ASSERT(event == VLR_ULA_E_AUTH_RES);
+
+	lfp->upd_hlr_vlr_fsm = NULL;
+
+	if (res) {
+		switch (*res) {
+		case VLR_AUTH_RES_PASSED:
+			/* Result == Pass */
+			vlr_loc_upd_post_auth(fi);
+			return;
+		case VLR_AUTH_RES_ABORTED:
+			/* go to Idle with no response */
+			rej_cause = 0;
+			break;
+		case VLR_AUTH_RES_UNKNOWN_SUBSCR:
+			/* FIXME: delete subscribe record */
+			rej_cause = GSM48_REJECT_IMSI_UNKNOWN_IN_HLR;
+			break;
+		case VLR_AUTH_RES_AUTH_FAILED:
+			/* cause = illegal subscriber */
+			rej_cause = GSM48_REJECT_ILLEGAL_MS;
+			break;
+		case VLR_AUTH_RES_PROC_ERR:
+			/* cause = system failure */
+			rej_cause = GSM48_REJECT_NETWORK_FAILURE;
+			break;
+		default:
+			LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+				 osmo_fsm_event_name(fi->fsm, event));
+			break;
+		}
+	} else
+		rej_cause = GSM48_REJECT_NETWORK_FAILURE;
+
+	lu_fsm_failure(fi, rej_cause);
+}
+
+static void lu_fsm_wait_ciph(struct osmo_fsm_inst *fi, uint32_t event,
+			     void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+	struct vlr_ciph_result res = { .cause = VLR_CIPH_REJECT };
+
+	OSMO_ASSERT(event == VLR_ULA_E_CIPH_RES);
+
+	if (!data)
+		LOGPFSML(fi, LOGL_ERROR, "invalid ciphering result: NULL\n");
+	else
+		res = *(struct vlr_ciph_result*)data;
+
+	switch (res.cause) {
+	case VLR_CIPH_COMPL:
+		break;
+	case VLR_CIPH_REJECT:
+		LOGPFSM(fi, "ciphering rejected\n");
+		lu_fsm_failure(fi, GSM48_REJECT_INVALID_MANDANTORY_INF);
+		return;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "invalid ciphering result: %d\n",
+			 res.cause);
+		lu_fsm_failure(fi, GSM48_REJECT_INVALID_MANDANTORY_INF);
+		return;
+	}
+
+	if (res.imeisv) {
+		LOGPFSM(fi, "got IMEISV: %s\n", res.imeisv);
+		vlr_subscr_set_imeisv(vsub, res.imeisv);
+	}
+	vlr_loc_upd_post_ciph(fi);
+}
+
+static void lu_fsm_wait_imsi(struct osmo_fsm_inst *fi, uint32_t event,
+			     void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+	char *mi_string = data;
+
+	switch (event) {
+	case VLR_ULA_E_ID_IMSI:
+		vlr_subscr_set_imsi(vsub, mi_string);
+		vlr_loc_upd_node1(fi);
+		break;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+			 osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+}
+
+/* At the end of Update_HLR_VLR */
+static void lu_fsm_wait_hlr_ul_res(struct osmo_fsm_inst *fi, uint32_t event,
+				   void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+
+	switch (event) {
+	case VLR_ULA_E_HLR_LU_RES:
+		/* pass-through this event to Update_HLR_VLR */
+		if (data == NULL)
+			osmo_fsm_inst_dispatch(lfp->upd_hlr_vlr_fsm, UPD_HLR_VLR_E_UPD_LOC_ACK, NULL);
+		else
+			osmo_fsm_inst_dispatch(lfp->upd_hlr_vlr_fsm, UPD_HLR_VLR_E_UPD_LOC_NACK, data);
+		break;
+	case VLR_ULA_E_UPD_HLR_COMPL:
+		if (data == NULL) {
+			/* successful case */
+			osmo_fsm_inst_state_chg(fi, VLR_ULA_S_WAIT_LU_COMPL,
+						LU_TIMEOUT_LONG, 0);
+			vlr_loc_upd_start_lu_compl_fsm(fi);
+			/* continue in MSC ?!? */
+		} else {
+			/* unsuccessful case */
+			enum gsm48_gmm_cause cause =
+				*(enum gsm48_gmm_cause *)data;
+			if (0 /* procedure_error && vlr->cfg.standalone_mode */) {
+				osmo_fsm_inst_state_chg(fi,
+						VLR_ULA_S_WAIT_LU_COMPL_STANDALONE,
+						LU_TIMEOUT_LONG, 0);
+				vlr_loc_upd_start_lu_compl_fsm(fi);
+			} else {
+				lu_fsm_failure(fi, cause);
+			}
+		}
+		break;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+			 osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+}
+
+/* Wait for end of Location_Update_Completion_VLR */
+static void lu_fsm_wait_lu_compl(struct osmo_fsm_inst *fi, uint32_t event,
+				 void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	uint8_t cause;
+
+	switch (event) {
+	case VLR_ULA_E_NEW_TMSI_ACK:
+		osmo_fsm_inst_dispatch(lfp->lu_compl_vlr_fsm,
+				       LU_COMPL_VLR_E_NEW_TMSI_ACK, NULL);
+		break;
+	case VLR_ULA_E_ID_IMEI:
+		osmo_fsm_inst_dispatch(lfp->lu_compl_vlr_fsm,
+				       LU_COMPL_VLR_E_IMEI_CHECK_ACK, NULL);
+		break;
+	case VLR_ULA_E_LU_COMPL_SUCCESS:
+		lu_fsm_discard_lu_compl_fsm(fi);
+
+		/* Update Register */
+		/* TODO: Set_Notification_Type 23.078 */
+		/* TODO: Notify_gsmSCF 23.078 */
+		/* TODO: Authenticated Radio Contact Established -> ARC */
+		lu_fsm_success(fi);
+		break;
+	case VLR_ULA_E_LU_COMPL_FAILURE:
+		cause = GSM48_REJECT_NETWORK_FAILURE;
+		if (data)
+			cause = *(uint8_t*)data;
+		lu_fsm_discard_lu_compl_fsm(fi);
+		lu_fsm_failure(fi, cause);
+		break;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+			 osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+}
+
+/* Wait for end of Location_Update_Completion_VLR (standalone case) */
+static void lu_fsm_wait_lu_compl_standalone(struct osmo_fsm_inst *fi,
+					uint32_t event, void *data)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+	uint8_t cause;
+
+	switch (event) {
+	case VLR_ULA_E_NEW_TMSI_ACK:
+		osmo_fsm_inst_dispatch(lfp->lu_compl_vlr_fsm,
+				       LU_COMPL_VLR_E_NEW_TMSI_ACK, NULL);
+		break;
+	case VLR_ULA_E_LU_COMPL_SUCCESS:
+		lu_fsm_discard_lu_compl_fsm(fi);
+		vsub->sub_dataconf_by_hlr_ind = false;
+		lu_fsm_success(fi);
+		break;
+	case VLR_ULA_E_LU_COMPL_FAILURE:
+		vsub->sub_dataconf_by_hlr_ind = false;
+		cause = GSM48_REJECT_NETWORK_FAILURE;
+		if (data)
+			cause = *(uint8_t*)data;
+		lu_fsm_discard_lu_compl_fsm(fi);
+		lu_fsm_failure(fi, cause);
+		break;
+	default:
+		LOGPFSML(fi, LOGL_ERROR, "event without effect: %s\n",
+			 osmo_fsm_event_name(fi->fsm, event));
+		break;
+	}
+}
+
+static const struct osmo_fsm_state vlr_lu_fsm_states[] = {
+	[VLR_ULA_S_IDLE] = {
+		.in_event_mask = S(VLR_ULA_E_UPDATE_LA),
+		.out_state_mask = S(VLR_ULA_S_WAIT_IMEISV) |
+				  S(VLR_ULA_S_WAIT_PVLR) |
+				  S(VLR_ULA_S_WAIT_IMSI) |
+				  S(VLR_ULA_S_WAIT_AUTH) |
+				  S(VLR_ULA_S_WAIT_HLR_UPD) |
+				  S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_IDLE),
+		.action = lu_fsm_idle,
+	},
+	[VLR_ULA_S_WAIT_IMEISV] = {
+		.in_event_mask = S(VLR_ULA_E_ID_IMEISV),
+		.out_state_mask = S(VLR_ULA_S_WAIT_PVLR) |
+				  S(VLR_ULA_S_WAIT_IMSI) |
+				  S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_IMEISV),
+		.action = lu_fsm_wait_imeisv,
+	},
+	[VLR_ULA_S_WAIT_PVLR] = {
+		.in_event_mask = S(VLR_ULA_E_SEND_ID_ACK) |
+				 S(VLR_ULA_E_SEND_ID_NACK),
+		.out_state_mask = S(VLR_ULA_S_WAIT_IMSI) |
+				  S(VLR_ULA_S_WAIT_AUTH) |
+				  S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_PVLR),
+		.action = lu_fsm_wait_pvlr,
+	},
+	[VLR_ULA_S_WAIT_AUTH] = {
+		.in_event_mask = S(VLR_ULA_E_AUTH_RES),
+		.out_state_mask = S(VLR_ULA_S_WAIT_CIPH) |
+				  S(VLR_ULA_S_WAIT_LU_COMPL) |
+				  S(VLR_ULA_S_WAIT_HLR_UPD) |
+				  S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_AUTH),
+		.action = lu_fsm_wait_auth,
+	},
+	[VLR_ULA_S_WAIT_CIPH] = {
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_CIPH),
+		.in_event_mask = S(VLR_ULA_E_CIPH_RES),
+		.out_state_mask = S(VLR_ULA_S_WAIT_LU_COMPL) |
+				  S(VLR_ULA_S_WAIT_HLR_UPD) |
+				  S(VLR_ULA_S_DONE),
+		.action = lu_fsm_wait_ciph,
+	},
+	[VLR_ULA_S_WAIT_IMSI] = {
+		.in_event_mask = S(VLR_ULA_E_ID_IMSI),
+		.out_state_mask = S(VLR_ULA_S_WAIT_AUTH) |
+				  S(VLR_ULA_S_WAIT_HLR_UPD) |
+				  S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_IMSI),
+		.action = lu_fsm_wait_imsi,
+	},
+	[VLR_ULA_S_WAIT_HLR_UPD] = {
+		.in_event_mask = S(VLR_ULA_E_HLR_LU_RES) |
+				 S(VLR_ULA_E_UPD_HLR_COMPL),
+		.out_state_mask = S(VLR_ULA_S_WAIT_LU_COMPL) |
+				  S(VLR_ULA_S_WAIT_LU_COMPL_STANDALONE) |
+				  S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_HLR_UPD),
+		.action = lu_fsm_wait_hlr_ul_res,
+	},
+	[VLR_ULA_S_WAIT_LU_COMPL] = {
+		.in_event_mask = S(VLR_ULA_E_LU_COMPL_SUCCESS) |
+				 S(VLR_ULA_E_LU_COMPL_FAILURE) |
+				 S(VLR_ULA_E_NEW_TMSI_ACK) |
+				 S(VLR_ULA_E_ID_IMEI) |
+				 S(VLR_ULA_E_ID_IMEISV),
+		.out_state_mask = S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_LU_COMPL),
+		.action = lu_fsm_wait_lu_compl,
+	},
+	[VLR_ULA_S_WAIT_LU_COMPL_STANDALONE] = {
+		.in_event_mask = S(VLR_ULA_E_LU_COMPL_SUCCESS) |
+				 S(VLR_ULA_E_LU_COMPL_FAILURE) |
+				 S(VLR_ULA_E_NEW_TMSI_ACK),
+		.out_state_mask = S(VLR_ULA_S_DONE),
+		.name = OSMO_STRINGIFY(VLR_ULA_S_WAIT_LU_COMPL_STANDALONE),
+		.action = lu_fsm_wait_lu_compl_standalone,
+	},
+	[VLR_ULA_S_DONE] = {
+		.name = OSMO_STRINGIFY(VLR_ULA_S_DONE),
+		.onenter = lu_fsm_dispatch_result,
+	},
+};
+
+static void fsm_lu_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+	struct lu_fsm_priv *lfp = fi->priv;
+	struct vlr_subscr *vsub = lfp->vsub;
+
+	LOGPFSM(fi, "fsm_lu_cleanup called with cause %s\n",
+		osmo_fsm_term_cause_name(cause));
+	if (vsub && vsub->lu_fsm == fi)
+		vsub->lu_fsm = NULL;
+}
+
+static struct osmo_fsm vlr_lu_fsm = {
+	.name = "vlr_lu_fsm",
+	.states = vlr_lu_fsm_states,
+	.num_states = ARRAY_SIZE(vlr_lu_fsm_states),
+	.allstate_event_mask = 0,
+	.allstate_action = NULL,
+	.log_subsys = DVLR,
+	.event_names = fsm_lu_event_names,
+	.cleanup = fsm_lu_cleanup,
+};
+
+struct osmo_fsm_inst *
+vlr_loc_update(struct osmo_fsm_inst *parent,
+	       uint32_t parent_event_success,
+	       uint32_t parent_event_failure,
+	       void *parent_event_data,
+	       struct vlr_instance *vlr, void *msc_conn_ref,
+	       enum vlr_lu_type type, uint32_t tmsi, const char *imsi,
+	       const struct osmo_location_area_id *old_lai,
+	       const struct osmo_location_area_id *new_lai,
+	       bool authentication_required,
+	       enum vlr_ciph ciphering_required,
+	       bool is_r99, bool is_utran,
+	       bool assign_tmsi)
+{
+	struct osmo_fsm_inst *fi;
+	struct lu_fsm_priv *lfp;
+
+	fi = osmo_fsm_inst_alloc_child(&vlr_lu_fsm, parent, parent_event_failure);
+	if (!fi)
+		return NULL;
+
+	lfp = talloc_zero(fi, struct lu_fsm_priv);
+	lfp->vlr = vlr;
+	lfp->msc_conn_ref = msc_conn_ref;
+	lfp->tmsi = tmsi;
+	lfp->type = type;
+	lfp->old_lai = *old_lai;
+	lfp->new_lai = *new_lai;
+	lfp->lu_by_tmsi = true;
+	lfp->parent_event_success = parent_event_success;
+	lfp->parent_event_failure = parent_event_failure;
+	lfp->parent_event_data = parent_event_data;
+	lfp->authentication_required = authentication_required;
+	lfp->ciphering_required = ciphering_required;
+	lfp->is_r99 = is_r99;
+	lfp->is_utran = is_utran;
+	lfp->assign_tmsi = assign_tmsi;
+	if (imsi) {
+		strncpy(lfp->imsi, imsi, sizeof(lfp->imsi)-1);
+		lfp->imsi[sizeof(lfp->imsi)-1] = '\0';
+		lfp->lu_by_tmsi = false;
+	}
+	fi->priv = lfp;
+
+	LOGPFSM(fi, "rev=%s net=%s%s%s\n",
+		is_r99 ? "R99" : "GSM",
+		is_utran ? "UTRAN" : "GERAN",
+		(authentication_required || ciphering_required)?
+		" Auth" : " (no Auth)",
+		(authentication_required || ciphering_required)?
+			(ciphering_required? "+Ciph" : " (no Ciph)")
+			: "");
+
+	osmo_fsm_inst_dispatch(fi, VLR_ULA_E_UPDATE_LA, NULL);
+
+	return fi;
+}
+
+/* Gracefully terminate an FSM created by vlr_loc_update() in case of external
+ * timeout (i.e. from MSC). */
+void vlr_loc_update_conn_timeout(struct osmo_fsm_inst *fi)
+{
+	if (!fi || fi->state == VLR_ULA_S_DONE)
+		return;
+	LOGPFSM(fi, "Connection timed out\n");
+	lu_fsm_failure(fi, GSM48_REJECT_CONGESTION);
+}
+
+void vlr_lu_fsm_init(void)
+{
+	osmo_fsm_register(&vlr_lu_fsm);
+	osmo_fsm_register(&upd_hlr_vlr_fsm);
+	osmo_fsm_register(&sub_pres_vlr_fsm);
+	osmo_fsm_register(&lu_compl_vlr_fsm);
+}
diff --git a/src/libvlr/vlr_lu_fsm.h b/src/libvlr/vlr_lu_fsm.h
new file mode 100644
index 0000000..5cf13c7
--- /dev/null
+++ b/src/libvlr/vlr_lu_fsm.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <osmocom/core/fsm.h>
+
+enum vlr_lu_state {
+	VLR_ULA_S_IDLE,
+	VLR_ULA_S_WAIT_IMEISV,
+	VLR_ULA_S_WAIT_PVLR,	/* Waiting for ID from PVLR */
+	VLR_ULA_S_WAIT_AUTH,	/* Waiting for Authentication */
+	VLR_ULA_S_WAIT_CIPH,	/* Waiting for Ciphering Complete */
+	VLR_ULA_S_WAIT_IMSI,	/* Waiting for IMSI from MS */
+	VLR_ULA_S_WAIT_HLR_UPD,	/* Waiting for end of HLR update */
+	VLR_ULA_S_WAIT_LU_COMPL,/* Waiting for LU complete */
+	VLR_ULA_S_WAIT_LU_COMPL_STANDALONE, /* Standalone VLR */
+	VLR_ULA_S_DONE
+};
+
+void vlr_lu_fsm_init(void);
diff --git a/src/osmo-nitb/Makefile.am b/src/osmo-nitb/Makefile.am
index f4ef487..a99334d 100644
--- a/src/osmo-nitb/Makefile.am
+++ b/src/osmo-nitb/Makefile.am
@@ -32,6 +32,7 @@
 	$(top_builddir)/src/libbsc/libbsc.a \
 	$(top_builddir)/src/libcommon-cs/libcommon-cs.a \
 	$(top_builddir)/src/libmsc/libmsc.a \
+	$(top_builddir)/src/libvlr/libvlr.a \
 	$(top_builddir)/src/libtrau/libtrau.a \
 	$(top_builddir)/src/libcommon/libcommon.a \
 	$(LIBOSMOGSM_LIBS) \
diff --git a/tests/vlr/Makefile.am b/tests/vlr/Makefile.am
new file mode 100644
index 0000000..2208a6f
--- /dev/null
+++ b/tests/vlr/Makefile.am
@@ -0,0 +1,22 @@
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include
+AM_CFLAGS = -Wall -ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(COVERAGE_CFLAGS) \
+	$(LIBOSMOABIS_CFLAGS) \
+	$(NULL)
+AM_LDFLAGS = $(COVERAGE_LDFLAGS)
+
+noinst_PROGRAMS = vlr_test
+
+vlr_test_SOURCES = vlr_test.c
+vlr_test_LDADD = \
+	$(top_builddir)/src/libvlr/libvlr.a \
+	$(top_builddir)/src/libcommon/libcommon.a \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(LIBOSMOVTY_LIBS) \
+	$(LIBOSMOABIS_LIBS) \
+	$(LIBRARY_DL) \
+	$(LIBCRYPTO_LIBS) \
+	$(NULL)
diff --git a/tests/vlr/vlr_test.c b/tests/vlr/vlr_test.c
new file mode 100644
index 0000000..19beb69
--- /dev/null
+++ b/tests/vlr/vlr_test.c
@@ -0,0 +1,694 @@
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <signal.h>
+
+#include <osmocom/gsm/gsm48.h>
+#include <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+#include <osmocom/core/application.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/fsm.h>
+
+#include <openbsc/vlr.h>
+#include <openbsc/debug.h>
+
+#define S(x)	(1 << (x))
+
+/*
+ * TODO:
+ * * test FSM for all testvlr_mode (and more)
+ * * test also the time-outs in the vlr code
+ * * test for memory leaks
+ * * how to get the HLR running? Or test against stub?
+ * * test disappearing MS connection
+ * * test absence of HLR
+ */
+
+void *tall_bsc_ctx;
+static struct vlr_instance *g_vlr;
+
+/***********************************************************************
+ * Finite State Machine simulating MS and MSC towards VLR
+ ***********************************************************************/
+
+static int timer_error_cb(struct osmo_fsm_inst *fi)
+{
+	LOGPFSM(fi, "timer expired waiting for completion\n");
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_ERROR, NULL);
+	return 1;
+}
+
+enum testvlr_mode {
+	MODE_SUCCESS,
+	MODE_SUCCESS_TMSI,
+	MODE_AUTH_FAIL,
+	MODE_AUTH_RESYNC,
+};
+
+struct testvlr_priv {
+	enum testvlr_mode mode;
+	uint32_t tmsi;
+	char imsi[16];
+	char imei[16];
+	struct osmo_location_area_id old_lai;
+	struct osmo_location_area_id new_lai;
+
+	struct vlr_subscr *subscr;
+
+	struct osmo_fsm_inst *lu_fsm;
+};
+
+#define fsi_priv(x)	(struct testvlr_priv *)(x)->priv
+
+enum f_state {
+	/*! initial state */
+	ST_NULL,
+	/*! LU was sent by MS */
+	ST_LU_SENT,
+	/*! waiting for auth re-sync */
+	ST_RESYNC_SENT,
+	/* Waiting for LU ACK */
+	ST_WAIT_LU_ACK,
+	ST_DONE,
+	ST_FAILED,
+};
+
+enum f_event {
+	/* events from MS */
+	EVT_MS_TX_LU,		/* transmit LU REQ to network */
+	EVT_MS_TX_ID_RESP,	/* tranmit ID RSP to network */
+	EVT_MS_TX_AUTH_RESP,	/* transmit AUTH RESP to network */
+	EVT_MS_TX_AUTH_FAIL,	/* transmit AUTH FAIL to network */
+	EVT_MS_CONN_LOST,	/* connection to MS was lost */
+
+	/* events from VLR */
+	EVT_VLR_AUTH_REQ,	/* transmit AUTH REQ to MS */
+	EVT_VLR_ID_REQ_IMSI,	/* transmit ID REQ(IMSI) to MS */
+	EVT_VLR_ID_REQ_IMEI,	/* tramsmit ID REQ(IMEI) to MS */
+	EVT_VLR_ID_REQ_IMEISV,	/* trasnmit ID REQ(IMEISV) to MS */
+	EVT_VLR_AUTH_REJ,	/* transmit AUTH REJ to MS */
+	EVT_VLR_SET_CIPH,	/* transmit SET CIPH to MS */
+	EVT_VLR_LU_ACK,		/* transmit LU ACK to MS */
+	EVT_VLR_LU_REJ,		/* transmit LU REJ to MS */
+};
+
+static struct value_string f_event_names[] = {
+	{ EVT_MS_TX_LU,		"MS-TX-LU" },
+	{ EVT_MS_TX_ID_RESP,	"MS-TX-ID-RESP" },
+	{ EVT_MS_TX_AUTH_RESP,	"MS-TX-AUTH-RESP" },
+	{ EVT_MS_TX_AUTH_FAIL,	"MS-TX-AUTH-FAIL" },
+	{ EVT_MS_CONN_LOST,	"MS-CONN-LOST" },
+
+	{ EVT_VLR_AUTH_REQ,	"VLR-AUTH-REQ" },
+	{ EVT_VLR_ID_REQ_IMSI,	"VLR-ID-REQ-IMSI" },
+	{ EVT_VLR_ID_REQ_IMEI,	"VLR-ID-REQ-IMEI" },
+	{ EVT_VLR_ID_REQ_IMEISV,"VLR-ID-REQ-IMEISV" },
+	{ EVT_VLR_AUTH_REJ,	"VLR-AUTH-REJ" },
+	{ EVT_VLR_SET_CIPH,	"VLR-SET-CIPH" },
+	{ EVT_VLR_LU_ACK,	"VLR-LU-ACK" },
+	{ EVT_VLR_LU_REJ,	"VLR-LU-REJ" },
+	{ 0, NULL }
+};
+
+static void fsm_f_allstate(struct osmo_fsm_inst *fi, uint32_t event,
+			   void *data)
+{
+	struct testvlr_priv *priv = fsi_priv(fi);
+	uint8_t mi[16];
+	unsigned int mi_len;
+
+	switch (event) {
+	case EVT_VLR_ID_REQ_IMSI:
+		if (priv->mode != MODE_SUCCESS_TMSI) {
+			LOGP(DGPRS, LOGL_NOTICE, "Unexpected ID REQ "
+			     "(IMSI)\n");
+		}
+		mi_len = gsm48_generate_mid_from_imsi(mi, priv->imsi);
+		vlr_subscr_rx_id_resp(priv->subscr, mi+2, mi_len-2);
+		break;
+	case EVT_VLR_ID_REQ_IMEI:
+		mi_len = gsm48_generate_mid_from_imsi(mi, priv->imei);
+		mi[0] = (mi[0] & 0xf8) | GSM_MI_TYPE_IMEI;
+		vlr_subscr_rx_id_resp(priv->subscr+2, mi, mi_len-2);
+		break;
+	case EVT_VLR_ID_REQ_IMEISV:
+		mi_len = gsm48_generate_mid_from_imsi(mi, priv->imei);
+		mi[0] = (mi[0] & 0xf8) | GSM_MI_TYPE_IMEISV;
+		vlr_subscr_rx_id_resp(priv->subscr, mi+2, mi_len-2);
+		break;
+	case EVT_MS_CONN_LOST:
+		vlr_subscr_disconnected(priv->subscr);
+		/* IDEA: not release but keep around in extra state to
+		 * see if VLR still sends us anything? */
+		osmo_fsm_inst_free(fi);
+		break;
+	}
+}
+
+static void fsm_f_null(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct testvlr_priv *priv = fsi_priv(fi);
+	uint32_t tmsi = 0;
+	const char *imsi = NULL;
+
+	switch (event) {
+	case EVT_MS_TX_LU:
+		/* send LU to VLR */
+		if (priv->mode == MODE_SUCCESS)
+			imsi = priv->imsi;
+		else
+			tmsi = priv->tmsi;
+		priv->lu_fsm = vlr_loc_update(fi,
+					      EVT_VLR_LU_ACK,
+					      EVT_VLR_LU_REJ,
+					      NULL,
+					      g_vlr, NULL,
+					      VLR_LU_TYPE_IMSI_ATTACH, tmsi,
+					      imsi, &priv->old_lai,
+					      &priv->new_lai,
+					      true,
+					      true,
+					      false,
+					      false,
+					      true);
+		OSMO_ASSERT(priv->subscr);
+		osmo_fsm_inst_state_chg(fi, ST_LU_SENT, 0, 0);
+		break;
+	default:
+		break;
+	}
+}
+
+static void fsm_f_lu_sent(struct osmo_fsm_inst *fi, uint32_t event,
+			  void *data)
+{
+	struct gsm_auth_tuple *at = NULL;
+	struct testvlr_priv *priv = fsi_priv(fi);
+	uint8_t res_fail[4];
+	uint8_t auts[14];
+
+	switch (event) {
+	case EVT_VLR_AUTH_REQ:
+		at = data;
+		OSMO_ASSERT(at);
+		DEBUGP(DGPRS, "%s: at->res=%s\n", __func__, osmo_hexdump(at->vec.res, at->vec.res_len));
+		switch (priv->mode) {
+		case MODE_SUCCESS:
+		case MODE_SUCCESS_TMSI:
+			/* return matching SRES/AUTS */
+			vlr_subscr_rx_auth_resp(priv->subscr, true, false,
+					       at->vec.res, at->vec.res_len);
+			break;
+		case MODE_AUTH_FAIL:
+			/* return not matching SRES/AUTS */
+			vlr_subscr_rx_auth_resp(priv->subscr, true, false,
+					       res_fail, sizeof(res_fail));
+			/* FIXME: state transition? */
+			break;
+		case MODE_AUTH_RESYNC:
+			/* return SRES/AUTS requesting re-sync */
+			/* FIXME: generate a proper authenticating
+			 * re-sync request */
+			vlr_subscr_rx_auth_fail(priv->subscr, auts);
+			/* FIXME: state transition? */
+			osmo_fsm_inst_state_chg(fi, ST_RESYNC_SENT, 0, 0);
+			break;
+		}
+		osmo_fsm_inst_state_chg(fi, ST_WAIT_LU_ACK, 0, 0);
+		break;
+	case EVT_VLR_LU_REJ:
+		{
+		uint8_t cause = *(uint8_t *)data;
+		LOGP(DGPRS, LOGL_NOTICE, "LU(%s): Rejected; cause=0x%02x\n",
+		    priv->imsi, cause);
+
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+#if 0
+static void fsm_f_resync_sent(struct osmo_fsm_inst *fi, uint32_t event,
+			      void *data)
+{
+	struct testvlr_priv *priv = fsi_priv(fi);
+	struct gsm_auth_tuple *at = NULL;
+
+	/* second auth request is supposed to succed after the
+	 * re-sync procedure before */
+	switch (event) {
+	case EVT_VLR_AUTH_REQ:
+		at = data;
+		/* return matching SRES/AUTS now */
+		vlr_subscr_rx_auth_resp(priv->subscr, true, false,
+				       at->vec.res, at->vec.res_len);
+		osmo_fsm_inst_state_chg(fi, ST_WAIT_LU_ACK, 0, 0);
+		break;
+	}
+}
+#endif
+
+static void fsm_f_wait_lu_ack(struct osmo_fsm_inst *fi, uint32_t event,
+			      void *data)
+{
+	struct testvlr_priv *priv = fsi_priv(fi);
+
+	switch (event) {
+	case EVT_VLR_LU_ACK:
+		if (priv->subscr->tmsi != GSM_RESERVED_TMSI) {
+			/* we need to send an TMSI REALLOC COMPL */
+			vlr_subscr_rx_tmsi_reall_compl(priv->subscr);
+		}
+		osmo_fsm_inst_state_chg(fi, ST_DONE, 0, 0);
+		break;
+	case EVT_VLR_LU_REJ:
+		osmo_fsm_inst_state_chg(fi, ST_FAILED, 0, 0);
+		break;
+	}
+}
+
+static void fsm_f_imsi_sent(struct osmo_fsm_inst *fi, uint32_t event,
+			    void *data)
+{
+	switch (event) {
+	case EVT_MS_TX_ID_RESP:
+		break;
+	}
+}
+
+#if 0
+static void fsm_f_areq_sent(struct osmo_fsm_inst *fi, uint32_t event,
+			    void *data)
+{
+	switch (event) {
+	case EVT_MS_TX_AUTH_RESP:
+		break;
+	case EVT_MS_TX_AUTH_FAIL:
+		break;
+	}
+}
+#endif
+
+static struct osmo_fsm_state fsm_success_states[] = {
+	[ST_NULL] = {
+		.in_event_mask = S(EVT_MS_TX_LU),
+		.out_state_mask = S(ST_LU_SENT),
+		.name = "NULL",
+		.action = fsm_f_null,
+	},
+	[ST_LU_SENT] = {
+		.in_event_mask = S(EVT_VLR_AUTH_REQ) |
+				 S(EVT_VLR_LU_REJ),
+		//.out_state_mask = S(ST_IDREQ_IMSI_SENT) | S(ST_AUTH_REQ_SENT),
+		.out_state_mask = S(ST_WAIT_LU_ACK),
+		.name = "LU Sent",
+		.action = fsm_f_lu_sent,
+	},
+	[ST_RESYNC_SENT] = {
+		.in_event_mask = S(EVT_VLR_AUTH_REQ),
+		.out_state_mask = S(ST_WAIT_LU_ACK),
+		.name = "AUTH-RESYNC sent",
+		.action = fsm_f_imsi_sent,
+	},
+	[ST_WAIT_LU_ACK] = {
+		.in_event_mask = S(EVT_VLR_LU_ACK) |
+				 S(EVT_VLR_SET_CIPH) |
+				 S(EVT_VLR_LU_REJ),
+		.out_state_mask = S(ST_DONE),
+		.name = "WAIT-LU-ACK",
+		.action = fsm_f_wait_lu_ack,
+	},
+	[ST_DONE] = {
+		.name = "DONE"
+	},
+};
+
+static struct osmo_fsm vlr_test_fsm = {
+	.name = "VLR Test FSM",
+	.states = fsm_success_states,
+	.num_states = ARRAY_SIZE(fsm_success_states),
+	.log_subsys = DGPRS,
+	.event_names = f_event_names,
+	.allstate_event_mask = S(EVT_MS_CONN_LOST) |
+			       S(EVT_VLR_ID_REQ_IMSI) |
+			       S(EVT_VLR_ID_REQ_IMEI) |
+			       S(EVT_VLR_ID_REQ_IMEISV),
+	.allstate_action = fsm_f_allstate,
+};
+
+/* Testing of Subscriber_Present_VLR */
+
+enum test_sub_pres_state {
+	TSPV_S_INIT,
+	TSPV_S_RUNNING,
+};
+
+enum test_sub_pres_evt {
+	TSPV_E_START,
+	TSPV_E_COMPL,
+};
+
+static void tspv_f_running(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct vlr_subscr *vsub = fi->priv;
+
+	switch (event) {
+	case TSPV_E_COMPL:
+		OSMO_ASSERT(vsub);
+		OSMO_ASSERT(vsub->ms_not_reachable_flag == false);
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+		break;
+	}
+}
+
+static void tspv_f_init(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct osmo_fsm_inst *spv;
+	struct vlr_subscr *vsub = fi->priv;
+
+	switch (event) {
+	case TSPV_E_START:
+		OSMO_ASSERT(vsub);
+		vsub->ms_not_reachable_flag = true;
+		spv = sub_pres_vlr_fsm_start(fi, vsub, TSPV_E_COMPL);
+		OSMO_ASSERT(spv);
+		osmo_fsm_inst_state_chg(fi, TSPV_S_RUNNING, 4, 0);
+		break;
+	}
+}
+
+static const struct osmo_fsm_state test_sub_pres_vlr_states[] = {
+	[TSPV_S_INIT] = {
+		.in_event_mask = S(TSPV_E_START),
+		.out_state_mask = S(TSPV_S_RUNNING),
+		.name = "INIT",
+		.action = tspv_f_init,
+	},
+	[TSPV_S_RUNNING] = {
+		.in_event_mask = S(TSPV_E_COMPL),
+		.out_state_mask = 0,
+		.name = "RUNNING",
+		.action = tspv_f_running,
+	},
+};
+
+static struct osmo_fsm test_sub_pres_vlr_fsm = {
+	.name = "Test Subscriber_Present_VLR",
+	.states = test_sub_pres_vlr_states,
+	.num_states = ARRAY_SIZE(test_sub_pres_vlr_states),
+	.log_subsys = DGPRS,
+	.event_names = f_event_names,
+	.timer_cb = timer_error_cb,
+};
+
+#if 0
+static void start_sub_pres_vlr(void *ctx, uint32_t tmsi, const char *imsi)
+{
+	struct osmo_fsm_inst *fi;
+	struct vlr_subscr *vsub = vlr_subscr_alloc(g_vlr);
+
+	vsub->tmsi = tmsi;
+	strncpy(vsub->imsi, imsi, sizeof(vsub->imsi));
+	fi = osmo_fsm_inst_alloc(&test_sub_pres_vlr_fsm, ctx, vsub, LOGL_DEBUG, vsub->imsi);
+	osmo_fsm_inst_dispatch(fi, TSPV_E_START, NULL);
+}
+#endif
+
+/* Testing of Update_HLR_VLR */
+
+enum test_update_hlr_vlr_state {
+	TUHV_S_INIT,
+	TUHV_S_RUNNING,
+};
+
+enum test_update_hlr_vlr_event {
+	TUHV_E_START,
+	TUHV_E_COMPL,
+};
+
+static void tuhv_f_running(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	enum gsm48_gmm_cause *res = data;
+
+	switch (event) {
+	case TUHV_E_COMPL:
+		if (!res) {
+			/* Success */
+			LOGPFSM(fi, "success\n");
+		} else {
+			/* error */
+			LOGPFSM(fi, "errror cause=0x%u\n", *res);
+		}
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+		break;
+	}
+}
+
+static void tuhv_f_init(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct osmo_fsm_inst *child;
+	struct vlr_subscr *vsub = fi->priv;
+
+	switch (event) {
+	case TUHV_E_START:
+		child = upd_hlr_vlr_proc_start(fi, vsub, TUHV_E_COMPL);
+		OSMO_ASSERT(child);
+		osmo_fsm_inst_state_chg(fi, TUHV_S_RUNNING, 4, 0);
+		break;
+	}
+}
+
+static const struct osmo_fsm_state test_upd_hlr_vlr_states[] = {
+	[TUHV_S_INIT] = {
+		.in_event_mask = S(TUHV_E_START),
+		.out_state_mask = S(TUHV_S_RUNNING),
+		.name = "INIT",
+		.action = tuhv_f_init,
+	},
+	[TUHV_S_RUNNING] = {
+		.in_event_mask = S(TUHV_E_COMPL),
+		.out_state_mask = 0,
+		.name = "RUNNING",
+		.action = tuhv_f_running,
+	},
+};
+
+static struct osmo_fsm test_upd_hlr_vlr_fsm = {
+	.name = "Test Update_HLR_VLR",
+	.states = test_upd_hlr_vlr_states,
+	.num_states = ARRAY_SIZE(test_upd_hlr_vlr_states),
+	.log_subsys = DGPRS,
+	.event_names = f_event_names,
+	.timer_cb = timer_error_cb,
+};
+
+#if 0
+static void start_upd_hlr_vlr(void *ctx, uint32_t tmsi, const char *imsi)
+{
+	struct osmo_fsm_inst *fi;
+	struct vlr_subscr *vsub = vlr_subscr_alloc(g_vlr);
+
+	vsub->tmsi = tmsi;
+	strncpy(vsub->imsi, imsi, sizeof(vsub->imsi));
+
+
+	fi = osmo_fsm_inst_alloc(&test_upd_hlr_vlr_fsm, ctx, vsub, LOGL_DEBUG,
+				vsub->imsi);
+	/* we need to set this to fool vlr.c in an ongoing LU */
+	vsub->lu_fsm = fi;
+	osmo_fsm_inst_dispatch(fi, TUHV_E_START, NULL);
+}
+#endif
+
+/***********************************************************************
+ * Integration with VLR code
+ ***********************************************************************/
+
+static struct vlr_instance *g_vlr;
+
+/* VLR asks us to send an authentication request */
+static int msc_vlr_tx_auth_req(void *msc_conn_ref, struct gsm_auth_tuple *at,
+			       bool send_autn)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	OSMO_ASSERT(at);
+	DEBUGP(DGPRS, "%s: RES=%s\n", __func__,
+		osmo_hexdump_nospc(at->vec.res, at->vec.res_len));
+	osmo_fsm_inst_dispatch(fi, EVT_VLR_AUTH_REQ, at);
+	return 0;
+}
+
+/* VLR asks us to send an authentication reject */
+static int msc_vlr_tx_auth_rej(void *msc_conn_ref)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	DEBUGP(DGPRS, "%s\n", __func__);
+	osmo_fsm_inst_dispatch(fi, EVT_VLR_AUTH_REJ, NULL);
+	return 0;
+}
+
+/* VLR asks us to transmit an Identity Request of given type */
+static int msc_vlr_tx_id_req(void *msc_conn_ref, uint8_t mi_type)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	uint32_t event;
+
+	DEBUGP(DGPRS, "%s (%u)\n", __func__, mi_type);
+
+	switch (mi_type) {
+	case GSM_MI_TYPE_IMSI:
+		event = EVT_VLR_ID_REQ_IMSI;
+		break;
+	case GSM_MI_TYPE_IMEI:
+		event = EVT_VLR_ID_REQ_IMEI;
+		break;
+	case GSM_MI_TYPE_IMEISV:
+		event = EVT_VLR_ID_REQ_IMEISV;
+		break;
+	default:
+		LOGP(DGPRS, LOGL_ERROR, "Unknown identity 0x%02x\n",
+			mi_type);
+		return -1;
+	}
+	osmo_fsm_inst_dispatch(fi, event, NULL);
+	return 0;
+}
+
+/* VLR asks us to transmit a Location Update Accept */
+static int msc_vlr_tx_lu_ack(void *msc_conn_ref, uint32_t send_tmsi)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	DEBUGP(DGPRS, "%s\n", __func__);
+	osmo_fsm_inst_dispatch(fi, EVT_VLR_LU_ACK, NULL);
+	return 0;
+}
+
+/* VLR asks us to transmit a Location Update Reject */
+static int msc_vlr_tx_lu_rej(void *msc_conn_ref, uint8_t cause)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	DEBUGP(DGPRS, "%s\n", __func__);
+	osmo_fsm_inst_dispatch(fi, EVT_VLR_LU_REJ, (void *) &cause);
+	return 0;
+}
+
+static int msc_vlr_set_ciph_mode(void *msc_conn_ref, enum vlr_ciph mode,
+				 bool retrieve_imeisv)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	DEBUGP(DGPRS, "%s\n", __func__);
+	osmo_fsm_inst_dispatch(fi, EVT_VLR_SET_CIPH, NULL);
+	return 0;
+}
+
+/* VLR informs us that the subscriber data has somehow been modified */
+static void msc_vlr_subscr_update(struct vlr_subscr *subscr)
+{
+	DEBUGP(DGPRS, "%s\n", __func__);
+	/* FIXME */
+}
+
+static void msc_vlr_subscr_assoc(void *msc_conn_ref, struct vlr_subscr *vsub)
+{
+	struct osmo_fsm_inst *fi = msc_conn_ref;
+	struct testvlr_priv *priv = fsi_priv(fi);
+	DEBUGP(DGPRS, "%s(%p, %s)\n", __func__, msc_conn_ref, vlr_subscr_name(vsub));
+	priv->subscr = vsub;
+}
+
+/* operations that we need to implement for libvlr */
+static const struct vlr_ops test_vlr_ops = {
+	.tx_auth_req = msc_vlr_tx_auth_req,
+	.tx_auth_rej = msc_vlr_tx_auth_rej,
+	.tx_id_req = msc_vlr_tx_id_req,
+	.tx_lu_acc = msc_vlr_tx_lu_ack,
+	.tx_lu_rej = msc_vlr_tx_lu_rej,
+	.set_ciph_mode = msc_vlr_set_ciph_mode,
+	.subscr_update = msc_vlr_subscr_update,
+	.subscr_assoc = msc_vlr_subscr_assoc,
+};
+
+/***********************************************************************
+ * Actual test cases
+ ***********************************************************************/
+
+
+static struct osmo_fsm_inst *
+start_lu(enum testvlr_mode mode, uint32_t tmsi,
+	 const char *imsi, const char *imei)
+{
+	struct testvlr_priv *vp;
+	struct osmo_fsm_inst *fi;
+
+	vp = talloc_zero(tall_bsc_ctx, struct testvlr_priv);
+	vp->mode = mode;
+	vp->tmsi = tmsi;
+	strncpy(vp->imsi, imsi, sizeof(vp->imsi));
+	strncpy(vp->imei, imei, sizeof(vp->imei));
+
+	fi = osmo_fsm_inst_alloc(&vlr_test_fsm, vp, vp, LOGL_DEBUG, vp->imsi);
+	osmo_fsm_inst_dispatch(fi, EVT_MS_TX_LU, NULL);
+	return fi;
+}
+
+/***********************************************************************
+ * Main / Misc
+ ***********************************************************************/
+
+static struct osmo_timer_list tmr;
+
+static void timer_cb(void *data)
+{
+	uint32_t tmsi = rand() % 1000000;
+	uint64_t imsi = 901790000000000 + tmsi;
+	char imsi_str[32];
+
+	snprintf(imsi_str, sizeof(imsi_str), "%lu", imsi);
+	//start_lu(MODE_AUTH_FAIL, tmsi, imsi_str, "23422342");
+	start_lu(MODE_SUCCESS_TMSI, tmsi, imsi_str, "23422342");
+	//start_lu(MODE_SUCCESS, tmsi, imsi_str, "23422342");
+	//start_upd_hlr_vlr(tall_bsc_ctx, tmsi, imsi_str);
+	//start_sub_pres_vlr(tall_bsc_ctx);
+	osmo_timer_schedule(&tmr, 8, 0);
+}
+
+static void sighdlr(int sig)
+{
+	switch (sig) {
+	case SIGUSR1:
+		talloc_report_full(tall_bsc_ctx, stderr);
+		break;
+	}
+}
+
+int main(int argc, char **argv)
+{
+	tall_bsc_ctx = talloc_named_const(NULL, 1, "tall_bsc_ctx");
+
+	signal(SIGUSR1, sighdlr);
+
+	osmo_init_logging(&log_info);
+
+	g_vlr = vlr_alloc(NULL, &test_vlr_ops);
+	vlr_start("VLRTEST", g_vlr, "localhost", 2222);
+	OSMO_ASSERT(g_vlr);
+	osmo_fsm_register(&vlr_test_fsm);
+	osmo_fsm_register(&test_sub_pres_vlr_fsm);
+	osmo_fsm_register(&test_upd_hlr_vlr_fsm);
+
+	g_vlr->cfg.assign_tmsi = true;
+
+	tmr.cb = timer_cb;
+	timer_cb(NULL);
+
+	while (1) {
+		osmo_select_main(0);
+	}
+
+	exit(0);
+}
+
+struct gsm_subscriber_connection;
+int gsm48_tx_mm_serv_ack(struct gsm_subscriber_connection *conn) { return 0; }

-- 
To view, visit https://gerrit.osmocom.org/3194
To unsubscribe, visit https://gerrit.osmocom.org/settings

Gerrit-MessageType: newpatchset
Gerrit-Change-Id: Ie303c98f8c18e40c87c1b68474b35de332033622
Gerrit-PatchSet: 4
Gerrit-Project: osmo-msc
Gerrit-Branch: master
Gerrit-Owner: Neels Hofmeyr <nhofmeyr at sysmocom.de>
Gerrit-Reviewer: Jenkins Builder



More information about the gerrit-log mailing list