[PATCH] openbsc[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
Wed May 10 12:16:53 UTC 2017


Review at  https://gerrit.osmocom.org/2560

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 development collapsed in one patch. The original
history may still be available as branch neels/vlr_orig.

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


  git pull ssh://gerrit.osmocom.org:29418/openbsc refs/changes/60/2560/1

diff --git a/openbsc/configure.ac b/openbsc/configure.ac
index c6ae159..8381305 100644
--- a/openbsc/configure.ac
+++ b/openbsc/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,8 @@
     tests/slhc/Makefile
     tests/v42bis/Makefile
     tests/nanobts_omlattr/Makefile
+    tests/vlr/Makefile
+    tests/subscr_conn/Makefile
     doc/Makefile
     doc/examples/Makefile
     Makefile)
diff --git a/openbsc/include/openbsc/Makefile.am b/openbsc/include/openbsc/Makefile.am
index 7ddf323..b903782 100644
--- a/openbsc/include/openbsc/Makefile.am
+++ b/openbsc/include/openbsc/Makefile.am
@@ -82,6 +82,7 @@
 	trau_mux.h \
 	trau_upqueue.h \
 	ussd.h \
+	vlr.h \
 	vty.h \
 	v42bis.h \
 	v42bis_private.h \
diff --git a/openbsc/include/openbsc/vlr.h b/openbsc/include/openbsc/vlr.h
new file mode 100644
index 0000000..d7e8f19
--- /dev/null
+++ b/openbsc/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/openbsc/src/Makefile.am b/openbsc/src/Makefile.am
index cfad7df..c66f9e5 100644
--- a/openbsc/src/Makefile.am
+++ b/openbsc/src/Makefile.am
@@ -22,6 +22,7 @@
 # Libraries
 SUBDIRS = \
 	libcommon \
+	libvlr \
 	libmgcp \
 	libbsc \
 	libmsc \
diff --git a/openbsc/src/libmsc/subscr_conn.c b/openbsc/src/libmsc/subscr_conn.c
new file mode 100644
index 0000000..91ffe40
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/Makefile.am b/openbsc/src/libvlr/Makefile.am
new file mode 100644
index 0000000..17ad411
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr.c b/openbsc/src/libvlr/vlr.c
new file mode 100644
index 0000000..0e0d31c
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_access_req_fsm.c b/openbsc/src/libvlr/vlr_access_req_fsm.c
new file mode 100644
index 0000000..241478f
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_access_req_fsm.h b/openbsc/src/libvlr/vlr_access_req_fsm.h
new file mode 100644
index 0000000..8386da6
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_auth_fsm.c b/openbsc/src/libvlr/vlr_auth_fsm.c
new file mode 100644
index 0000000..0eb86e7
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_auth_fsm.h b/openbsc/src/libvlr/vlr_auth_fsm.h
new file mode 100644
index 0000000..226435f
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_core.h b/openbsc/src/libvlr/vlr_core.h
new file mode 100644
index 0000000..0e63c7e
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_lu_fsm.c b/openbsc/src/libvlr/vlr_lu_fsm.c
new file mode 100644
index 0000000..d32659f
--- /dev/null
+++ b/openbsc/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/openbsc/src/libvlr/vlr_lu_fsm.h b/openbsc/src/libvlr/vlr_lu_fsm.h
new file mode 100644
index 0000000..5cf13c7
--- /dev/null
+++ b/openbsc/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/openbsc/src/osmo-nitb/Makefile.am b/openbsc/src/osmo-nitb/Makefile.am
index f4ef487..a99334d 100644
--- a/openbsc/src/osmo-nitb/Makefile.am
+++ b/openbsc/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/openbsc/tests/vlr/Makefile.am b/openbsc/tests/vlr/Makefile.am
new file mode 100644
index 0000000..2208a6f
--- /dev/null
+++ b/openbsc/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/openbsc/tests/vlr/vlr_test.c b/openbsc/tests/vlr/vlr_test.c
new file mode 100644
index 0000000..19beb69
--- /dev/null
+++ b/openbsc/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/2560
To unsubscribe, visit https://gerrit.osmocom.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3f75de5f0cc2ff77f276fd39832dd3621309c4b9
Gerrit-PatchSet: 1
Gerrit-Project: openbsc
Gerrit-Branch: master
Gerrit-Owner: Neels Hofmeyr <nhofmeyr at sysmocom.de>



More information about the gerrit-log mailing list