Change in osmo-bsc[master]: LCS: implement the bulk of Location Services

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

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

laforge gerrit-no-reply at lists.osmocom.org
Fri Oct 9 19:26:31 UTC 2020


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

Change subject: LCS: implement the bulk of Location Services
......................................................................

LCS: implement the bulk of Location Services

Depends: I4d7302a4853518916b6b425e710c10568eb2ffe5 (libosmocore)
Change-Id: I28314ba97df86a118497e9b2770e2e6e2484e872
---
M TODO-RELEASE
M include/osmocom/bsc/Makefile.am
M include/osmocom/bsc/bsc_msc_data.h
M include/osmocom/bsc/bsc_subscr_conn_fsm.h
M include/osmocom/bsc/debug.h
M include/osmocom/bsc/gsm_data.h
A include/osmocom/bsc/lb.h
A include/osmocom/bsc/lcs_loc_req.h
A include/osmocom/bsc/lcs_ta_req.h
M include/osmocom/bsc/paging.h
M include/osmocom/bsc/vty.h
M src/osmo-bsc/Makefile.am
M src/osmo-bsc/bsc_init.c
M src/osmo-bsc/bsc_sccp.c
M src/osmo-bsc/bsc_subscr_conn_fsm.c
M src/osmo-bsc/gsm_04_08_rr.c
M src/osmo-bsc/gsm_08_08.c
M src/osmo-bsc/gsm_data.c
M src/osmo-bsc/handover_fsm.c
A src/osmo-bsc/lb.c
A src/osmo-bsc/lcs_loc_req.c
A src/osmo-bsc/lcs_ta_req.c
M src/osmo-bsc/net_init.c
M src/osmo-bsc/osmo_bsc_bssap.c
M src/osmo-bsc/osmo_bsc_main.c
M src/osmo-bsc/osmo_bsc_msc.c
M src/osmo-bsc/paging.c
M tests/bsc/bsc_test.c
M tests/handover/Makefile.am
M tests/timer.vty
30 files changed, 1,858 insertions(+), 17 deletions(-)

Approvals:
  Jenkins Builder: Verified
  pespin: Looks good to me, but someone else must approve
  laforge: Looks good to me, approved



diff --git a/TODO-RELEASE b/TODO-RELEASE
index f5d70c2..6a39a21 100644
--- a/TODO-RELEASE
+++ b/TODO-RELEASE
@@ -9,3 +9,4 @@
 #library	what		description / commit summary line
 manual				needs common chapter cs7-config.adoc, vty_cpu_sched.adoc from osmo-gsm-manuals > 0.3.0
 osmo-bsc	Mobile Identity Coding	OsmoBSC is stricter in rejecting invalid coding of Mobile Identity IEs
+libosmocore	>1.4.0 (1.4.1?)	need BSSMAP-LE, BSSLAP, GAD coding
diff --git a/include/osmocom/bsc/Makefile.am b/include/osmocom/bsc/Makefile.am
index 1ee96ed..8c42287 100644
--- a/include/osmocom/bsc/Makefile.am
+++ b/include/osmocom/bsc/Makefile.am
@@ -27,9 +27,12 @@
 	handover_fsm.h \
 	handover_vty.h \
 	ipaccess.h \
+	lb.h \
 	lchan_fsm.h \
 	lchan_rtp_fsm.h \
 	lchan_select.h \
+	lcs_loc_req.h \
+	lcs_ta_req.h \
 	meas_feed.h \
 	meas_rep.h \
 	misdn.h \
diff --git a/include/osmocom/bsc/bsc_msc_data.h b/include/osmocom/bsc/bsc_msc_data.h
index 43ace25..5699b77 100644
--- a/include/osmocom/bsc/bsc_msc_data.h
+++ b/include/osmocom/bsc/bsc_msc_data.h
@@ -68,6 +68,8 @@
 	MSC_CTR_BSSMAP_RX_DT1_UNKNOWN,
 	MSC_CTR_BSSMAP_RX_DT1_DTAP,
 	MSC_CTR_BSSMAP_RX_DT1_DTAP_ERROR,
+	MSC_CTR_BSSMAP_RX_DT1_PERFORM_LOCATION_REQUEST,
+	MSC_CTR_BSSMAP_RX_DT1_PERFORM_LOCATION_ABORT,
 
 	/* Tx message counters (per connection type) */
 	MSC_CTR_BSSMAP_TX_BSS_MANAGEMENT,
@@ -97,6 +99,8 @@
 	MSC_CTR_BSSMAP_TX_DT1_HANDOVER_COMPLETE,
 	MSC_CTR_BSSMAP_TX_DT1_HANDOVER_FAILURE,
 	MSC_CTR_BSSMAP_TX_DT1_DTAP,
+	MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_SUCCESS,
+	MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE,
 
 	MSC_CTR_MSCPOOL_SUBSCR_NEW,
 	MSC_CTR_MSCPOOL_SUBSCR_REATTACH,
diff --git a/include/osmocom/bsc/bsc_subscr_conn_fsm.h b/include/osmocom/bsc/bsc_subscr_conn_fsm.h
index ccac4fa..142d535 100644
--- a/include/osmocom/bsc/bsc_subscr_conn_fsm.h
+++ b/include/osmocom/bsc/bsc_subscr_conn_fsm.h
@@ -44,6 +44,8 @@
 
 	GSCON_EV_FORGET_LCHAN,
 	GSCON_EV_FORGET_MGW_ENDPOINT,
+
+	GSCON_EV_LCS_LOC_REQ_END,
 };
 
 struct gscon_clear_cmd_data {
diff --git a/include/osmocom/bsc/debug.h b/include/osmocom/bsc/debug.h
index 82c0703..0380b74 100644
--- a/include/osmocom/bsc/debug.h
+++ b/include/osmocom/bsc/debug.h
@@ -27,6 +27,7 @@
 	DTS,
 	DAS,
 	DCBS,
+	DLCS,
 	Debug_LastEntry,
 };
 
diff --git a/include/osmocom/bsc/gsm_data.h b/include/osmocom/bsc/gsm_data.h
index 7021fa4..050cd7a 100644
--- a/include/osmocom/bsc/gsm_data.h
+++ b/include/osmocom/bsc/gsm_data.h
@@ -52,6 +52,7 @@
 struct gprs_ra_id;
 struct handover;
 struct osmo_sccp_instance;
+struct smlc_config;
 
 #define OBSC_LINKID_CB(__msgb)	(__msgb)->cb[3]
 
@@ -304,6 +305,23 @@
 	uint8_t ms_power_class:3;
 
 	bool rx_clear_command;
+
+	/* Location Services handling for this subscriber */
+	struct {
+		/* FSM to handle Perform Location Request coming in from the MSC via A interface,
+		 * and receive BSSMAP-LE responses from the SMLC. */
+		struct lcs_loc_req *loc_req;
+
+		/* FSM to handle BSSLAP requests coming in from the SMLC via Lb interface.
+		 * BSSLAP APDU are encapsulated in BSSMAP-LE Connection Oriented Information messages. */
+		struct lcs_bsslap *bsslap;
+
+		/* Lb interface to the SMLC: BSSMAP-LE/SCCP connection associated with this subscriber */
+		struct {
+			int conn_id;
+			enum subscr_sccp_state state;
+		} lb;
+	} lcs;
 };
 
 
@@ -1200,6 +1218,8 @@
 
 	uint8_t nri_bitlen;
 	struct osmo_nri_ranges *null_nri_ranges;
+
+	struct smlc_config *smlc;
 };
 
 struct gsm_audio_support {
diff --git a/include/osmocom/bsc/lb.h b/include/osmocom/bsc/lb.h
new file mode 100644
index 0000000..0649986
--- /dev/null
+++ b/include/osmocom/bsc/lb.h
@@ -0,0 +1,60 @@
+/* Location Services (LCS): low level Lb/SCCP handling in OsmoBSC, API */
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/sigtran/sccp_sap.h>
+
+struct bssap_le_pdu;
+struct gsm_subscriber_connection;
+
+enum {
+	SMLC_CTR_BSSMAP_LE_RX_UNKNOWN_PEER,
+	SMLC_CTR_BSSMAP_LE_RX_UDT_RESET,
+	SMLC_CTR_BSSMAP_LE_RX_UDT_RESET_ACK,
+	SMLC_CTR_BSSMAP_LE_RX_UDT_ERR_INVALID_MSG,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_ERR_INVALID_MSG,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_RESPONSE_SUCCESS,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_TA_REQUEST,
+
+	SMLC_CTR_BSSMAP_LE_TX_ERR_INVALID_MSG,
+	SMLC_CTR_BSSMAP_LE_TX_ERR_CONN_NOT_READY,
+	SMLC_CTR_BSSMAP_LE_TX_ERR_SEND,
+	SMLC_CTR_BSSMAP_LE_TX_SUCCESS,
+
+	SMLC_CTR_BSSMAP_LE_TX_UDT_RESET,
+	SMLC_CTR_BSSMAP_LE_TX_UDT_RESET_ACK,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_REQUEST,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_ABORT,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_TA_RESPONSE,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_REJECT,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_RESET,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_ABORT,
+};
+
+struct smlc_config {
+	uint32_t cs7_instance;
+	bool cs7_instance_valid;
+	struct osmo_sccp_instance *sccp;
+	struct osmo_sccp_user *sccp_user;
+
+	struct osmo_sccp_addr bsc_addr;
+	char *bsc_addr_name;
+
+	struct osmo_sccp_addr smlc_addr;
+	char *smlc_addr_name;
+
+	/*! True after either side has sent a BSSMAP-LE RESET-ACK */
+	bool ready;
+
+	struct rate_ctr_group *ctrs;
+};
+
+extern const struct rate_ctr_desc smlc_ctr_description[];
+extern const struct rate_ctr_group_desc smlc_ctrg_desc;
+
+int lb_init();
+int lb_send(struct gsm_subscriber_connection *conn, const struct bssap_le_pdu *bssap_le);
+void lb_close_conn(struct gsm_subscriber_connection *conn);
diff --git a/include/osmocom/bsc/lcs_loc_req.h b/include/osmocom/bsc/lcs_loc_req.h
new file mode 100644
index 0000000..ba677e8
--- /dev/null
+++ b/include/osmocom/bsc/lcs_loc_req.h
@@ -0,0 +1,48 @@
+/* Location Services (LCS): BSSMAP and BSSMAP-LE Perform Location Request handling in OsmoBSC, API */
+#pragma once
+
+#include <osmocom/gsm/bssmap_le.h>
+
+#define LOG_LCS_LOC_REQ(LOC_REQ, level, fmt, args...) do { \
+		if (LOC_REQ) \
+			LOGPFSML((LOC_REQ)->fi, level, fmt, ## args); \
+		else \
+			LOGP(DLCS, level, "LCS Perf Loc Req: " fmt, ## args); \
+	} while(0)
+
+struct lcs_ta_req;
+
+enum lcs_loc_req_fsm_event {
+	LCS_LOC_REQ_EV_RX_LB_PERFORM_LOCATION_RESPONSE,
+	LCS_LOC_REQ_EV_RX_A_PERFORM_LOCATION_ABORT,
+	LCS_LOC_REQ_EV_TA_REQ_START,
+	LCS_LOC_REQ_EV_TA_REQ_END,
+	LCS_LOC_REQ_EV_HANDOVER_PERFORMED,
+	LCS_LOC_REQ_EV_CONN_CLEAR,
+};
+
+struct lcs_loc_req {
+	struct osmo_fsm_inst *fi;
+	struct gsm_subscriber_connection *conn;
+
+	struct {
+		struct bssmap_le_location_type location_type;
+
+		bool cell_id_present;
+		struct gsm0808_cell_id cell_id;
+
+		struct osmo_mobile_identity imsi;
+		struct osmo_mobile_identity imei;
+	} req;
+
+	bool resp_present;
+	struct bssmap_le_perform_loc_resp resp;
+
+	struct lcs_cause_ie lcs_cause;
+
+	struct lcs_ta_req *ta_req;
+};
+
+void lcs_loc_req_start(struct gsm_subscriber_connection *conn, struct msgb *msg);
+int lcs_loc_req_rx_bssmap_le(struct gsm_subscriber_connection *conn, struct msgb *msg);
+void lcs_loc_req_reset(struct gsm_subscriber_connection *conn);
diff --git a/include/osmocom/bsc/lcs_ta_req.h b/include/osmocom/bsc/lcs_ta_req.h
new file mode 100644
index 0000000..b9b7a4e
--- /dev/null
+++ b/include/osmocom/bsc/lcs_ta_req.h
@@ -0,0 +1,29 @@
+/* Location Services (LCS): BSSLAP TA Request handling in OsmoBSC, API */
+#pragma once
+
+#include <osmocom/bsc/debug.h>
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/gsm/bssmap_le.h>
+
+#define LOG_LCS_TA_REQ(TA_REQ, level, fmt, args...) do { \
+	if (TA_REQ) \
+		LOGPFSML((TA_REQ)->fi, level, fmt, ## args); \
+	else \
+		LOGP(DLCS, level, "LCS TA Req: " fmt, ## args); \
+	} while(0)
+
+enum lcs_ta_req_fsm_event {
+	LCS_TA_REQ_EV_GOT_TA,
+	LCS_TA_REQ_EV_ABORT,
+};
+
+struct lcs_ta_req {
+	struct osmo_fsm_inst *fi;
+	struct lcs_loc_req *loc_req;
+	enum lcs_cause failure_cause;
+	uint8_t failure_diagnostic_val;
+};
+int lcs_ta_req_start(struct lcs_loc_req *lcs_loc_req);
+
+void lcs_bsslap_rx(struct gsm_subscriber_connection *conn, struct msgb *msg);
diff --git a/include/osmocom/bsc/paging.h b/include/osmocom/bsc/paging.h
index cd351cb..2d0f8da 100644
--- a/include/osmocom/bsc/paging.h
+++ b/include/osmocom/bsc/paging.h
@@ -94,6 +94,7 @@
 
 int paging_request_stop(struct bsc_msc_data **msc_p, enum bsc_paging_reason *reasons_p,
 			struct gsm_bts *bts, struct bsc_subscr *bsub);
+int paging_request_cancel(struct bsc_subscr *bsub, enum bsc_paging_reason reasons);
 
 /* update paging load */
 void paging_update_buffer_space(struct gsm_bts *bts, uint16_t);
diff --git a/include/osmocom/bsc/vty.h b/include/osmocom/bsc/vty.h
index a5a8452..d2361b0 100644
--- a/include/osmocom/bsc/vty.h
+++ b/include/osmocom/bsc/vty.h
@@ -27,6 +27,7 @@
 	CBC_NODE,
 	CBC_SERVER_NODE,
 	CBC_CLIENT_NODE,
+	SMLC_NODE,
 };
 
 struct log_info;
diff --git a/src/osmo-bsc/Makefile.am b/src/osmo-bsc/Makefile.am
index b0fc181..8d109fd 100644
--- a/src/osmo-bsc/Makefile.am
+++ b/src/osmo-bsc/Makefile.am
@@ -65,9 +65,12 @@
 	handover_fsm.c \
 	handover_logic.c \
 	handover_vty.c \
+	lb.c \
 	lchan_fsm.c \
 	lchan_rtp_fsm.c \
 	lchan_select.c \
+	lcs_loc_req.c \
+	lcs_ta_req.c \
 	meas_feed.c \
 	meas_rep.c \
 	neighbor_ident.c \
diff --git a/src/osmo-bsc/bsc_init.c b/src/osmo-bsc/bsc_init.c
index 1460af4..b959c9f 100644
--- a/src/osmo-bsc/bsc_init.c
+++ b/src/osmo-bsc/bsc_init.c
@@ -37,6 +37,7 @@
 #include <osmocom/bsc/gsm_04_08_rr.h>
 #include <osmocom/bsc/neighbor_ident.h>
 #include <osmocom/bsc/bts.h>
+#include <osmocom/bsc/lb.h>
 
 #include <osmocom/bsc/smscb.h>
 #include <osmocom/gsm/protocol/gsm_48_049.h>
diff --git a/src/osmo-bsc/bsc_sccp.c b/src/osmo-bsc/bsc_sccp.c
index 9d4289f..0cd1dc9 100644
--- a/src/osmo-bsc/bsc_sccp.c
+++ b/src/osmo-bsc/bsc_sccp.c
@@ -23,6 +23,7 @@
 
 #include <osmocom/bsc/gsm_data.h>
 #include <osmocom/bsc/bsc_msc_data.h>
+#include <osmocom/bsc/lb.h>
 
 /* We need an unused SCCP conn_id across all SCCP users. */
 int bsc_sccp_inst_next_conn_id(struct osmo_sccp_instance *sccp)
@@ -47,7 +48,13 @@
 				}
 			}
 
-			/* Future for LCS: also check Lb-interface conn IDs here */
+			if (bsc_gsmnet->smlc->sccp == sccp
+			    && conn->lcs.lb.state != SUBSCR_SCCP_ST_NONE) {
+				if (conn_id == conn->lcs.lb.conn_id) {
+					conn_id_already_used = true;
+					break;
+				}
+			}
 		}
 
 		if (!conn_id_already_used)
diff --git a/src/osmo-bsc/bsc_subscr_conn_fsm.c b/src/osmo-bsc/bsc_subscr_conn_fsm.c
index b127e7f..5893ea3 100644
--- a/src/osmo-bsc/bsc_subscr_conn_fsm.c
+++ b/src/osmo-bsc/bsc_subscr_conn_fsm.c
@@ -46,6 +46,8 @@
 #include <osmocom/bsc/codec_pref.h>
 #include <osmocom/mgcp_client/mgcp_client_endpoint_fsm.h>
 #include <osmocom/core/byteswap.h>
+#include <osmocom/bsc/lb.h>
+#include <osmocom/bsc/lcs_loc_req.h>
 
 #define S(x)	(1 << (x))
 
@@ -86,6 +88,7 @@
 	{GSCON_EV_LCLS_FAIL, "LCLS_FAIL"},
 	{GSCON_EV_FORGET_LCHAN, "FORGET_LCHAN"},
 	{GSCON_EV_FORGET_MGW_ENDPOINT, "FORGET_MGW_ENDPOINT"},
+	{GSCON_EV_LCS_LOC_REQ_END, "LCS_LOC_REQ_END"},
 	{}
 };
 
@@ -249,22 +252,41 @@
 
 	switch (bssmap_type) {
 	case BSS_MAP_MSG_HANDOVER_RQST:
-		/* First off, accept the new conn. */
-		osmo_sccp_tx_conn_resp(conn->sccp.msc->a.sccp_user, scu_prim->u.connect.conn_id,
-				       &scu_prim->u.connect.called_addr, NULL, 0);
+	case BSS_MAP_MSG_PERFORM_LOCATION_RQST:
+		break;
 
-		/* Make sure the conn FSM will osmo_sccp_tx_disconn() on term */
-		conn->sccp.state = SUBSCR_SCCP_ST_CONNECTED;
+	default:
+		LOGPFSML(fi, LOGL_NOTICE, "No support for N-CONNECT: %s: %s\n",
+			 gsm0808_bssap_name(bs->type), gsm0808_bssmap_name(bssmap_type));
+		goto refuse;
+	}
 
+	/* First off, accept the new conn. */
+	if (osmo_sccp_tx_conn_resp(conn->sccp.msc->a.sccp_user, scu_prim->u.connect.conn_id,
+				   &scu_prim->u.connect.called_addr, NULL, 0)) {
+		LOGPFSML(fi, LOGL_ERROR, "Cannot send SCCP CONN RESP\n");
+		goto refuse;
+	}
+
+	/* Make sure the conn FSM will osmo_sccp_tx_disconn() on term */
+	conn->sccp.state = SUBSCR_SCCP_ST_CONNECTED;
+
+	switch (bssmap_type) {
+	case BSS_MAP_MSG_HANDOVER_RQST:
 		/* Inter-BSC MT Handover Request, another BSS is handovering to us. */
 		handover_start_inter_bsc_in(conn, msg);
 		return;
+
+	case BSS_MAP_MSG_PERFORM_LOCATION_RQST:
+		/* Location Services: MSC asks for location of an IDLE subscriber */
+		conn_fsm_state_chg(ST_ACTIVE);
+		lcs_loc_req_start(conn, msg);
+		return;
+
 	default:
-		break;
+		OSMO_ASSERT(false);
 	}
 
-	LOGPFSML(fi, LOGL_NOTICE, "No support for N-CONNECT: %s: %s\n",
-		 gsm0808_bssap_name(bs->type), gsm0808_bssmap_name(bssmap_type));
 refuse:
 	osmo_sccp_tx_disconn(conn->sccp.msc->a.sccp_user, scu_prim->u.connect.conn_id,
 			     &scu_prim->u.connect.called_addr, 0);
@@ -404,6 +426,14 @@
 	case GSCON_EV_TX_SCCP:
 		gscon_sigtran_send(conn, (struct msgb *)data);
 		break;
+
+	case GSCON_EV_LCS_LOC_REQ_END:
+		/* On the A-interface, there is nothing to do. If there still is an lchan, the conn should stay open. If
+		 * not, it is up to the MSC to send a Clear Command.
+		 * On the Lb-interface, tear down the SCCP connection. */
+		lb_close_conn(conn);
+		break;
+
 	default:
 		OSMO_ASSERT(false);
 	}
@@ -628,7 +658,8 @@
 	[ST_ACTIVE] = {
 		.name = "ACTIVE",
 		.in_event_mask = EV_TRANSPARENT_SCCP | S(GSCON_EV_ASSIGNMENT_START) |
-				 S(GSCON_EV_HANDOVER_START),
+				 S(GSCON_EV_HANDOVER_START)
+				 | S(GSCON_EV_LCS_LOC_REQ_END),
 		.out_state_mask = S(ST_CLEARING) | S(ST_ASSIGNMENT) |
 				  S(ST_HANDOVER),
 		.action = gscon_fsm_active,
@@ -656,6 +687,9 @@
 	/* On release, do not receive release events that look like the primary lchan is gone. */
 	struct gsm_lchan *old_lchan = conn->lchan;
 
+	if (old_lchan == new_lchan)
+		return;
+
 	conn->lchan = new_lchan;
 	conn->lchan->conn = conn;
 
@@ -738,7 +772,8 @@
 	if ((conn->fi && conn->fi->state != ST_CLEARING)
 	    && !conn->lchan
 	    && !conn->ho.new_lchan
-	    && !conn->assignment.new_lchan)
+	    && !conn->assignment.new_lchan
+	    && !conn->lcs.loc_req)
 		gscon_bssmap_clear(conn, GSM0808_CAUSE_EQUIPMENT_FAILURE);
 }
 
@@ -777,6 +812,9 @@
 		if (conn->ho.fi)
 			osmo_fsm_inst_dispatch(conn->ho.fi, HO_EV_CONN_RELEASING, NULL);
 
+		if (conn->lcs.loc_req)
+			osmo_fsm_inst_dispatch(conn->lcs.loc_req->fi, LCS_LOC_REQ_EV_CONN_CLEAR, NULL);
+
 		OSMO_ASSERT(data);
 		ccd = data;
 		if (conn->lchan)
@@ -846,6 +884,8 @@
 	lchan_forget_conn(conn->assignment.new_lchan);
 	lchan_forget_conn(conn->ho.new_lchan);
 
+	lb_close_conn(conn);
+
 	if (conn->sccp.state != SUBSCR_SCCP_ST_NONE) {
 		LOGPFSML(fi, LOGL_DEBUG, "Disconnecting SCCP\n");
 		struct bsc_msc_data *msc = conn->sccp.msc;
diff --git a/src/osmo-bsc/gsm_04_08_rr.c b/src/osmo-bsc/gsm_04_08_rr.c
index 49ec848..a448126 100644
--- a/src/osmo-bsc/gsm_04_08_rr.c
+++ b/src/osmo-bsc/gsm_04_08_rr.c
@@ -1027,7 +1027,6 @@
 		 * MSC */
 		dispatch_dtap(lchan->conn, link_id, msg);
 	} else {
-		/* fwd via bsc_api to send COMPLETE L3 INFO to MSC */
 		return bsc_compl_l3(lchan, msg, 0);
 	}
 
diff --git a/src/osmo-bsc/gsm_08_08.c b/src/osmo-bsc/gsm_08_08.c
index b7c7448..cd8b77f 100644
--- a/src/osmo-bsc/gsm_08_08.c
+++ b/src/osmo-bsc/gsm_08_08.c
@@ -30,6 +30,9 @@
 #include <osmocom/bsc/gsm_04_08_rr.h>
 #include <osmocom/bsc/a_reset.h>
 
+#include <osmocom/bsc/lcs_ta_req.h>
+#include <osmocom/bsc/lcs_loc_req.h>
+
 #include <osmocom/gsm/protocol/gsm_08_08.h>
 #include <osmocom/gsm/gsm0808.h>
 #include <osmocom/gsm/mncc.h>
@@ -495,6 +498,10 @@
 
 	parse_powercap(conn, msg);
 
+	/* If a BSSLAP TA Request from the SMLC is waiting for a TA value, we have one now. */
+	if (conn->lcs.loc_req && conn->lcs.loc_req->ta_req)
+		osmo_fsm_inst_dispatch(conn->lcs.loc_req->ta_req->fi, LCS_TA_REQ_EV_GOT_TA, NULL);
+
 	/* If the Paging was issued only by OsmoBSC for LCS, don't bother to establish Layer 3 to the MSC. */
 	if (paged_from_msc && !(paging_reasons & BSC_PAGING_FROM_CN)) {
 		LOG_COMPL_L3(pdisc, mtype, LOGL_DEBUG,
diff --git a/src/osmo-bsc/gsm_data.c b/src/osmo-bsc/gsm_data.c
index fbc2ae2..1152783 100644
--- a/src/osmo-bsc/gsm_data.c
+++ b/src/osmo-bsc/gsm_data.c
@@ -45,6 +45,7 @@
 #include <osmocom/bsc/timeslot_fsm.h>
 #include <osmocom/bsc/lchan_fsm.h>
 #include <osmocom/bsc/bts.h>
+#include <osmocom/bsc/bsc_msc_data.h>
 
 void *tall_bsc_ctx = NULL;
 
diff --git a/src/osmo-bsc/handover_fsm.c b/src/osmo-bsc/handover_fsm.c
index 8e231e0..573f249 100644
--- a/src/osmo-bsc/handover_fsm.c
+++ b/src/osmo-bsc/handover_fsm.c
@@ -44,6 +44,7 @@
 #include <osmocom/bsc/codec_pref.h>
 #include <osmocom/bsc/gsm_08_08.h>
 #include <osmocom/bsc/bts.h>
+#include <osmocom/bsc/lcs_loc_req.h>
 
 #define LOG_FMT_BTS "bts %u lac-ci %u-%u arfcn-bsic %d-%d"
 #define LOG_ARGS_BTS(bts) \
@@ -938,6 +939,10 @@
 	if (ho->new_lchan && result == HO_RESULT_OK) {
 		gscon_change_primary_lchan(conn, conn->ho.new_lchan);
 		ho->new_lchan = NULL;
+
+		/* If a Perform Location Request (LCS) is busy, inform the SMLC that there is a new lchan */
+		if (conn->lcs.loc_req)
+			osmo_fsm_inst_dispatch(conn->lcs.loc_req->fi, LCS_LOC_REQ_EV_HANDOVER_PERFORMED, NULL);
 	}
 
 	osmo_fsm_inst_dispatch(conn->fi, GSCON_EV_HANDOVER_END, &result);
diff --git a/src/osmo-bsc/lb.c b/src/osmo-bsc/lb.c
new file mode 100644
index 0000000..6ab131f
--- /dev/null
+++ b/src/osmo-bsc/lb.c
@@ -0,0 +1,662 @@
+/* Lb interface low level SCCP handling */
+/*
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * 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/bsc/lb.h>
+
+#include <osmocom/gsm/bssmap_le.h>
+#include <osmocom/sigtran/sccp_helpers.h>
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/bsc/vty.h>
+#include <osmocom/bsc/debug.h>
+#include <osmocom/bsc/osmo_bsc_sigtran.h>
+#include <osmocom/bsc/lcs_loc_req.h>
+
+static struct gsm_subscriber_connection *get_bsc_conn_by_lb_conn_id(int conn_id)
+{
+	struct gsm_subscriber_connection *conn;
+
+	llist_for_each_entry(conn, &bsc_gsmnet->subscr_conns, entry) {
+		if (conn->lcs.lb.state != SUBSCR_SCCP_ST_NONE
+		    && conn->lcs.lb.conn_id == conn_id)
+			return conn;
+	}
+
+	return NULL;
+}
+
+/* Send reset to SMLC */
+int bssmap_le_tx_reset()
+	// TODO use this -- patch coming up
+{
+	struct osmo_ss7_instance *ss7;
+	struct msgb *msg;
+	struct bssap_le_pdu reset = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_RESET,
+			.reset = GSM0808_CAUSE_EQUIPMENT_FAILURE,
+		},
+	};
+
+	ss7 = osmo_ss7_instance_find(bsc_gsmnet->smlc->cs7_instance);
+	OSMO_ASSERT(ss7);
+	LOGP(DLCS, LOGL_NOTICE, "Sending RESET to SMLC: %s\n", osmo_sccp_addr_name(ss7, &bsc_gsmnet->smlc->smlc_addr));
+	msg = osmo_bssap_le_enc(&reset);
+
+	rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_UDT_RESET]);
+	return osmo_sccp_tx_unitdata_msg(bsc_gsmnet->smlc->sccp_user, &bsc_gsmnet->smlc->bsc_addr,
+					 &bsc_gsmnet->smlc->smlc_addr, msg);
+}
+
+/* Send reset-ack to SMLC */
+int bssmap_le_tx_reset_ack()
+{
+	struct osmo_ss7_instance *ss7;
+	struct msgb *msg;
+	struct bssap_le_pdu reset_ack = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_RESET_ACK,
+		},
+	};
+
+	ss7 = osmo_ss7_instance_find(bsc_gsmnet->smlc->cs7_instance);
+	OSMO_ASSERT(ss7);
+	LOGP(DLCS, LOGL_NOTICE, "Tx RESET ACK to SMLC: %s\n", osmo_sccp_addr_name(ss7, &bsc_gsmnet->smlc->smlc_addr));
+	msg = osmo_bssap_le_enc(&reset_ack);
+
+	rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_UDT_RESET_ACK]);
+	return osmo_sccp_tx_unitdata_msg(bsc_gsmnet->smlc->sccp_user, &bsc_gsmnet->smlc->bsc_addr,
+					 &bsc_gsmnet->smlc->smlc_addr, msg);
+}
+
+static int bssmap_le_handle_reset(const struct bssmap_le_pdu *pdu)
+{
+	struct gsm_subscriber_connection *conn;
+	int rc;
+
+	/* Abort all ongoing Location Requests */
+	llist_for_each_entry(conn, &bsc_gsmnet->subscr_conns, entry)
+		lcs_loc_req_reset(conn);
+
+	rc = bssmap_le_tx_reset_ack();
+	if (!rc)
+		bsc_gsmnet->smlc->ready = true;
+	return rc;
+}
+
+static int bssmap_le_handle_reset_ack()
+{
+	bsc_gsmnet->smlc->ready = true;
+	return 0;
+}
+
+static int handle_unitdata_from_smlc(const struct osmo_sccp_addr *smlc_addr, struct msgb *msg,
+				     const struct osmo_sccp_user *scu)
+{
+	struct osmo_ss7_instance *ss7;
+	struct bssap_le_pdu bssap_le;
+	struct osmo_bssap_le_err *err;
+	struct rate_ctr *ctr = bsc_gsmnet->smlc->ctrs->ctr;
+
+	ss7 = osmo_sccp_get_ss7(osmo_sccp_get_sccp(scu));
+	OSMO_ASSERT(ss7);
+
+	if (osmo_sccp_addr_cmp(smlc_addr, &bsc_gsmnet->smlc->smlc_addr, OSMO_SCCP_ADDR_T_MASK)) {
+		LOGP(DLCS, LOGL_ERROR, "Rx BSSMAP-LE UnitData from unknown remote address: %s\n",
+		     osmo_sccp_addr_name(ss7, smlc_addr));
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_UNKNOWN_PEER]);
+		return -EINVAL;
+	}
+
+	if (osmo_bssap_le_dec(&bssap_le, &err, msg, msg)) {
+		LOGP(DLCS, LOGL_ERROR, "Rx BSSAP-LE UnitData with error: %s\n", err->logmsg);
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_UDT_ERR_INVALID_MSG]);
+		return -EINVAL;
+	}
+
+	if (bssap_le.discr != BSSAP_LE_MSG_DISCR_BSSMAP_LE) {
+		LOGP(DLCS, LOGL_ERROR, "Rx BSSAP-LE: discr %d not implemented\n", bssap_le.discr);
+		return -ENOTSUP;
+	}
+
+	switch (bssap_le.bssmap_le.msg_type) {
+	case BSSMAP_LE_MSGT_RESET:
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_UDT_RESET]);
+		LOGP(DLCS, LOGL_NOTICE, "RESET from SMLC: %s\n", osmo_sccp_addr_name(ss7, smlc_addr));
+		return bssmap_le_handle_reset(&bssap_le.bssmap_le);
+	case BSSMAP_LE_MSGT_RESET_ACK:
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_UDT_RESET_ACK]);
+		LOGP(DLCS, LOGL_NOTICE, "RESET-ACK from SMLC: %s\n", osmo_sccp_addr_name(ss7, smlc_addr));
+		return bssmap_le_handle_reset_ack();
+	default:
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_UDT_ERR_INVALID_MSG]);
+		LOGP(DLCS, LOGL_ERROR, "Rx unimplemented UDT message type %s\n",
+		     osmo_bssap_le_pdu_to_str_c(OTC_SELECT, &bssap_le));
+		return -EINVAL;
+	}
+}
+
+static int sccp_sap_up(struct osmo_prim_hdr *oph, void *_scu)
+{
+	struct osmo_scu_prim *scu_prim = (struct osmo_scu_prim *)oph;
+	struct osmo_sccp_user *scu = _scu;
+	struct gsm_subscriber_connection *conn;
+	int rc = 0;
+
+	switch (OSMO_PRIM_HDR(&scu_prim->oph)) {
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_INDICATION):
+		/* Handle inbound UnitData */
+		DEBUGP(DLCS, "N-UNITDATA.ind(%s)\n", osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
+		rc = handle_unitdata_from_smlc(&scu_prim->u.unitdata.calling_addr, oph->msg, scu);
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION):
+		/* Handle inbound connections. A Location Request is always started on the A interface, and OsmoBSC
+		 * forwards this to the SMLC by performing an N-CONNECT from BSC -> SMLC. This is the reverse
+		 * direction: N-CONNECT from SMLC -> BSC, which should never happen. */
+		LOGP(DLCS, LOGL_ERROR, "N-CONNECT.ind(X->%u): inbound connect from SMLC is not expected to happen\n",
+		     scu_prim->u.connect.conn_id);
+		rc = osmo_sccp_tx_disconn(scu, scu_prim->u.connect.conn_id, &scu_prim->u.connect.called_addr, 0);
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM):
+		/* Handle inbound confirmation of outbound connection */
+		DEBUGP(DLCS, "N-CONNECT.cnf(%u)\n", scu_prim->u.connect.conn_id);
+		conn = get_bsc_conn_by_lb_conn_id(scu_prim->u.connect.conn_id);
+		if (conn) {
+			conn->lcs.lb.state = SUBSCR_SCCP_ST_CONNECTED;
+			if (msgb_l2len(oph->msg) > 0) {
+				rc = lcs_loc_req_rx_bssmap_le(conn, oph->msg);
+			}
+		} else {
+			LOGP(DLCS, LOGL_ERROR, "N-CONNECT.cfm(%u) for unknown conn\n", scu_prim->u.connect.conn_id);
+			rc = -EINVAL;
+		}
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION):
+		/* Handle incoming connection oriented data */
+		DEBUGP(DLCS, "N-DATA.ind(%u)\n", scu_prim->u.data.conn_id);
+
+		conn = get_bsc_conn_by_lb_conn_id(scu_prim->u.data.conn_id);
+		if (!conn) {
+			LOGP(DLCS, LOGL_ERROR, "N-DATA.ind(%u) for unknown conn_id\n", scu_prim->u.data.conn_id);
+			rc = -EINVAL;
+		} else if (conn->lcs.lb.state != SUBSCR_SCCP_ST_CONNECTED) {
+			LOGP(DLCS, LOGL_ERROR, "N-DATA.ind(%u) for conn that is not confirmed\n",
+			     scu_prim->u.data.conn_id);
+			rc = -EINVAL;
+		} else {
+			rc = lcs_loc_req_rx_bssmap_le(conn, oph->msg);
+		}
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION):
+		DEBUGP(DLCS, "N-DISCONNECT.ind(%u, %s, cause=%i)\n", scu_prim->u.disconnect.conn_id,
+		       osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)),
+		       scu_prim->u.disconnect.cause);
+		/* indication of disconnect */
+		conn = get_bsc_conn_by_lb_conn_id(scu_prim->u.disconnect.conn_id);
+		if (!conn) {
+			LOGP(DLCS, LOGL_ERROR, "N-DISCONNECT.ind for unknown conn_id %u\n",
+			     scu_prim->u.disconnect.conn_id);
+			rc = -EINVAL;
+		} else {
+			conn->lcs.lb.state = SUBSCR_SCCP_ST_NONE;
+			if (msgb_l2len(oph->msg) > 0) {
+				rc = lcs_loc_req_rx_bssmap_le(conn, oph->msg);
+			}
+		}
+		break;
+
+	default:
+		LOGP(DLCS, LOGL_ERROR, "Unhandled SIGTRAN operation %s on primitive %u\n",
+		     get_value_string(osmo_prim_op_names, oph->operation), oph->primitive);
+		break;
+	}
+
+	msgb_free(oph->msg);
+	return rc;
+}
+
+static int lb_open_conn(struct gsm_subscriber_connection *conn, struct msgb *msg)
+{
+	struct osmo_ss7_instance *ss7;
+	int conn_id;
+	int rc;
+
+	OSMO_ASSERT(conn);
+	OSMO_ASSERT(msg);
+
+	if (conn->lcs.lb.state != SUBSCR_SCCP_ST_NONE) {
+		LOGPFSMSL(conn->fi, DLCS, LOGL_ERROR,
+			  "Cannot open BSSMAP-LE conn to SMLC, another conn is still active for this subscriber\n");
+		return -EINVAL;
+	}
+
+	conn_id = bsc_sccp_inst_next_conn_id(bsc_gsmnet->smlc->sccp);
+	if (conn_id < 0) {
+		LOGPFSMSL(conn->fi, DLCS, LOGL_ERROR, "Unable to allocate SCCP Connection ID for BSSMAP-LE to SMLC\n");
+		return -ENOSPC;
+	}
+	conn->lcs.lb.conn_id = conn_id;
+	ss7 = osmo_ss7_instance_find(bsc_gsmnet->smlc->cs7_instance);
+	OSMO_ASSERT(ss7);
+	LOGPFSMSL(conn->fi, DLCS, LOGL_INFO, "Opening new SCCP connection (id=%i) to SMLC: %s\n", conn_id,
+		  osmo_sccp_addr_name(ss7, &bsc_gsmnet->smlc->smlc_addr));
+
+	rc = osmo_sccp_tx_conn_req_msg(bsc_gsmnet->smlc->sccp_user, conn_id, &bsc_gsmnet->smlc->bsc_addr,
+				       &bsc_gsmnet->smlc->smlc_addr, msg);
+	if (rc >= 0)
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_SUCCESS]);
+	else
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_ERR_SEND]);
+	if (rc >= 0)
+		conn->lcs.lb.state = SUBSCR_SCCP_ST_WAIT_CONN_CONF;
+
+	return rc;
+}
+
+void lb_close_conn(struct gsm_subscriber_connection *conn)
+{
+	if (conn->lcs.lb.state == SUBSCR_SCCP_ST_NONE)
+		return;
+	osmo_sccp_tx_disconn(bsc_gsmnet->smlc->sccp_user, conn->lcs.lb.conn_id, &bsc_gsmnet->smlc->bsc_addr, 0);
+	conn->lcs.lb.state = SUBSCR_SCCP_ST_NONE;
+}
+
+/* Send data to SMLC, take ownership of *msg */
+int lb_send(struct gsm_subscriber_connection *conn, const struct bssap_le_pdu *bssap_le)
+{
+	int rc;
+	struct msgb *msg;
+
+	OSMO_ASSERT(conn);
+
+	msg = osmo_bssap_le_enc(bssap_le);
+	if (!msg) {
+		LOGPFSMSL(conn->fi, DLCS, LOGL_ERROR, "Failed to encode %s\n",
+			  osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
+		return -EINVAL;
+	}
+
+	if (conn->lcs.lb.state == SUBSCR_SCCP_ST_NONE) {
+		rc = lb_open_conn(conn, msg);
+		goto count_tx;
+	}
+
+	LOGPFSMSL(conn->fi, DLCS, LOGL_DEBUG, "Tx %s\n", osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
+	rc = osmo_sccp_tx_data_msg(bsc_gsmnet->smlc->sccp_user, conn->lcs.lb.conn_id, msg);
+	if (rc >= 0)
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_SUCCESS]);
+	else
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_ERR_SEND]);
+
+count_tx:
+	if (rc < 0)
+		return rc;
+
+	switch (bssap_le->bssmap_le.msg_type) {
+	case BSSMAP_LE_MSGT_PERFORM_LOC_REQ:
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_REQUEST]);
+		break;
+	case BSSMAP_LE_MSGT_PERFORM_LOC_ABORT:
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_ABORT]);
+		break;
+	case BSSMAP_LE_MSGT_CONN_ORIENTED_INFO:
+		switch (bssap_le->bssmap_le.conn_oriented_info.apdu.msg_type) {
+		case BSSLAP_MSGT_TA_RESPONSE:
+			rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_TA_RESPONSE]);
+			break;
+		case BSSLAP_MSGT_REJECT:
+			rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_REJECT]);
+			break;
+		case BSSLAP_MSGT_RESET:
+			rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_RESET]);
+			break;
+		case BSSLAP_MSGT_ABORT:
+			rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_ABORT]);
+			break;
+		default:
+			break;
+		}
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+/* Default point-code to be used as local address (BSC) */
+#define BSC_DEFAULT_PC "0.23.3"
+
+/* Default point-code to be used as remote address (SMLC) */
+#define SMLC_DEFAULT_PC "0.23.6"
+
+#define DEFAULT_ASP_LOCAL_IP "localhost"
+#define DEFAULT_ASP_REMOTE_IP "localhost"
+
+/* Initialize Lb interface to SMLC */
+int lb_init()
+{
+	uint32_t default_pc;
+	struct osmo_ss7_instance *cs7_inst = NULL;
+	struct osmo_sccp_instance *sccp;
+	enum osmo_ss7_asp_protocol used_proto = OSMO_SS7_ASP_PROT_M3UA;
+	char inst_name[32];
+	const char *smlc_name = "smlc";
+
+	if (!bsc_gsmnet->smlc) {
+		bsc_gsmnet->smlc = talloc_zero(bsc_gsmnet, struct smlc_config);
+		bsc_gsmnet->smlc->ctrs = rate_ctr_group_alloc(bsc_gsmnet, &smlc_ctrg_desc, 0);
+	}
+	OSMO_ASSERT(bsc_gsmnet->smlc);
+
+	if (!bsc_gsmnet->smlc->cs7_instance_valid) {
+		bsc_gsmnet->smlc->cs7_instance = 0;
+	}
+	cs7_inst = osmo_ss7_instance_find_or_create(tall_bsc_ctx, bsc_gsmnet->smlc->cs7_instance);
+	OSMO_ASSERT(cs7_inst);
+
+	/* If unset, use default SCCP address for the SMLC */
+	if (!bsc_gsmnet->smlc->smlc_addr.presence)
+		osmo_sccp_make_addr_pc_ssn(&bsc_gsmnet->smlc->smlc_addr,
+					   osmo_ss7_pointcode_parse(NULL, SMLC_DEFAULT_PC),
+					   OSMO_SCCP_SSN_SMLC_BSSAP_LE);
+
+	/* Set up SCCP user and one ASP+AS */
+	snprintf(inst_name, sizeof(inst_name), "Lb-%u-%s", cs7_inst->cfg.id, osmo_ss7_asp_protocol_name(used_proto));
+	LOGP(DLCS, LOGL_NOTICE, "Initializing SCCP connection for Lb/%s on cs7 instance %u\n",
+	     osmo_ss7_asp_protocol_name(used_proto), cs7_inst->cfg.id);
+
+	/* SS7 Protocol stack */
+	default_pc = osmo_ss7_pointcode_parse(NULL, BSC_DEFAULT_PC);
+	sccp = osmo_sccp_simple_client_on_ss7_id(tall_bsc_ctx, cs7_inst->cfg.id, inst_name,
+						 default_pc, used_proto,
+						 0, DEFAULT_ASP_LOCAL_IP,
+						 0, DEFAULT_ASP_REMOTE_IP);
+	if (!sccp)
+		return -EINVAL;
+	bsc_gsmnet->smlc->sccp = sccp;
+
+	/* If unset, use default local SCCP address */
+	if (!bsc_gsmnet->smlc->bsc_addr.presence)
+		osmo_sccp_local_addr_by_instance(&bsc_gsmnet->smlc->bsc_addr, sccp,
+						 OSMO_SCCP_SSN_BSC_BSSAP_LE);
+
+	if (!osmo_sccp_check_addr(&bsc_gsmnet->smlc->bsc_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) {
+		LOGP(DLCS, LOGL_ERROR,
+		     "%s %s: invalid local (BSC) SCCP address: %s\n",
+		     inst_name, smlc_name, osmo_sccp_inst_addr_name(sccp, &bsc_gsmnet->smlc->bsc_addr));
+		return -EINVAL;
+	}
+
+	if (!osmo_sccp_check_addr(&bsc_gsmnet->smlc->smlc_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) {
+		LOGP(DLCS, LOGL_ERROR,
+		     "%s %s: invalid remote (SMLC) SCCP address: %s\n",
+		     inst_name, smlc_name, osmo_sccp_inst_addr_name(sccp, &bsc_gsmnet->smlc->smlc_addr));
+		return -EINVAL;
+	}
+
+	LOGP(DLCS, LOGL_NOTICE, "Lb: %s %s: local (BSC) SCCP address: %s\n",
+	     inst_name, smlc_name, osmo_sccp_inst_addr_name(sccp, &bsc_gsmnet->smlc->bsc_addr));
+	LOGP(DLCS, LOGL_NOTICE, "Lb: %s %s: remote (SMLC) SCCP address: %s\n",
+	     inst_name, smlc_name, osmo_sccp_inst_addr_name(sccp, &bsc_gsmnet->smlc->smlc_addr));
+
+	/* Bind SCCP user. */
+	bsc_gsmnet->smlc->sccp_user = osmo_sccp_user_find(sccp, bsc_gsmnet->smlc->bsc_addr.ssn, bsc_gsmnet->smlc->bsc_addr.pc);
+	LOGP(DLCS, LOGL_NOTICE, "%s %s: %s\n", inst_name, smlc_name,
+	     bsc_gsmnet->smlc->sccp_user ? "user already bound for this SCCP instance" : "binding SCCP user");
+	if (!bsc_gsmnet->smlc->sccp_user)
+		bsc_gsmnet->smlc->sccp_user = osmo_sccp_user_bind(sccp, smlc_name, sccp_sap_up, bsc_gsmnet->smlc->bsc_addr.ssn);
+	if (!bsc_gsmnet->smlc->sccp_user)
+		return -EINVAL;
+
+	return 0;
+}
+
+/*********************************************************************************
+ * VTY Interface (Configuration + Introspection)
+ *********************************************************************************/
+
+DEFUN(cfg_smlc, cfg_smlc_cmd,
+	"smlc", "Configure Lb Link to Serving Mobile Location Centre\n")
+{
+	vty->node = SMLC_NODE;
+	return CMD_SUCCESS;
+}
+
+static struct cmd_node smlc_node = {
+	SMLC_NODE,
+	"%s(config-smlc)# ",
+	1,
+};
+
+static void enforce_ssn(struct vty *vty, struct osmo_sccp_addr *addr, enum osmo_sccp_ssn want_ssn)
+{
+	if (addr->presence & OSMO_SCCP_ADDR_T_SSN) {
+		if (addr->ssn != want_ssn)
+			vty_out(vty,
+				"setting an SSN (%u) different from the standard (%u) is not allowed, will use standard SSN for address: %s%s",
+				addr->ssn, want_ssn, osmo_sccp_addr_dump(addr), VTY_NEWLINE);
+	}
+
+	addr->presence |= OSMO_SCCP_ADDR_T_SSN;
+	addr->ssn = want_ssn;
+}
+
+DEFUN(cfg_smlc_cs7_bsc_addr,
+      cfg_smlc_cs7_bsc_addr_cmd,
+      "bsc-addr NAME",
+      "Local SCCP address of this BSC towards the SMLC\n" "Name of cs7 addressbook entry\n")
+{
+	const char *bsc_addr_name = argv[0];
+	struct osmo_ss7_instance *ss7;
+
+	ss7 = osmo_sccp_addr_by_name(&bsc_gsmnet->smlc->bsc_addr, bsc_addr_name);
+	if (!ss7) {
+		vty_out(vty, "Error: No such SCCP addressbook entry: '%s'%s", bsc_addr_name, VTY_NEWLINE);
+		return CMD_ERR_INCOMPLETE;
+	}
+
+	/* Prevent mixing addresses from different CS7 instances */
+	if (bsc_gsmnet->smlc->cs7_instance_valid
+	    && bsc_gsmnet->smlc->cs7_instance != ss7->cfg.id) {
+		vty_out(vty,
+			"Error: SCCP addressbook entry from mismatching CS7 instance: '%s'%s",
+			bsc_addr_name, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	bsc_gsmnet->smlc->cs7_instance = ss7->cfg.id;
+	bsc_gsmnet->smlc->cs7_instance_valid = true;
+	enforce_ssn(vty, &bsc_gsmnet->smlc->bsc_addr, OSMO_SCCP_SSN_BSC_BSSAP_LE);
+	bsc_gsmnet->smlc->bsc_addr_name = talloc_strdup(bsc_gsmnet, bsc_addr_name);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_smlc_cs7_smlc_addr,
+      cfg_smlc_cs7_smlc_addr_cmd,
+      "smlc-addr NAME",
+      "Remote SCCP address of the SMLC\n" "Name of cs7 addressbook entry\n")
+{
+	const char *smlc_addr_name = argv[0];
+	struct osmo_ss7_instance *ss7;
+
+	ss7 = osmo_sccp_addr_by_name(&bsc_gsmnet->smlc->smlc_addr, smlc_addr_name);
+	if (!ss7) {
+		vty_out(vty, "Error: No such SCCP addressbook entry: '%s'%s", smlc_addr_name, VTY_NEWLINE);
+		return CMD_ERR_INCOMPLETE;
+	}
+
+	/* Prevent mixing addresses from different CS7/SS7 instances */
+	if (bsc_gsmnet->smlc->cs7_instance_valid) {
+		if (bsc_gsmnet->smlc->cs7_instance != ss7->cfg.id) {
+			vty_out(vty,
+				"Error: SCCP addressbook entry from mismatching CS7 instance: '%s'%s",
+				smlc_addr_name, VTY_NEWLINE);
+			return CMD_ERR_INCOMPLETE;
+		}
+	}
+
+	bsc_gsmnet->smlc->cs7_instance = ss7->cfg.id;
+	bsc_gsmnet->smlc->cs7_instance_valid = true;
+	enforce_ssn(vty, &bsc_gsmnet->smlc->smlc_addr, OSMO_SCCP_SSN_SMLC_BSSAP_LE);
+	bsc_gsmnet->smlc->smlc_addr_name = talloc_strdup(bsc_gsmnet, smlc_addr_name);
+	return CMD_SUCCESS;
+}
+
+static int config_write_smlc(struct vty *vty)
+{
+	vty_out(vty, "smlc%s", VTY_NEWLINE);
+	if (bsc_gsmnet->smlc->bsc_addr_name) {
+		vty_out(vty, " bsc-addr %s%s",
+			bsc_gsmnet->smlc->bsc_addr_name, VTY_NEWLINE);
+	}
+	if (bsc_gsmnet->smlc->smlc_addr_name) {
+		vty_out(vty, " smlc-addr %s%s",
+			bsc_gsmnet->smlc->smlc_addr_name, VTY_NEWLINE);
+	}
+
+	return 0;
+}
+
+DEFUN(show_smlc, show_smlc_cmd,
+	"show smlc",
+	SHOW_STR "Display state of SMLC / Lb\n")
+{
+	vty_out(vty, "not implemented%s", VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+void smlc_vty_init(void)
+{
+	install_element_ve(&show_smlc_cmd);
+
+	install_element(CONFIG_NODE, &cfg_smlc_cmd);
+	install_node(&smlc_node, config_write_smlc);
+	install_element(SMLC_NODE, &cfg_smlc_cs7_bsc_addr_cmd);
+	install_element(SMLC_NODE, &cfg_smlc_cs7_smlc_addr_cmd);
+}
+
+const struct rate_ctr_desc smlc_ctr_description[] = {
+	[SMLC_CTR_BSSMAP_LE_RX_UNKNOWN_PEER] = {
+		"bssmap_le:rx:unknown_peer",
+		"Number of received BSSMAP-LE messages from an unknown Calling SCCP address"
+	},
+	[SMLC_CTR_BSSMAP_LE_RX_UDT_RESET] = {
+		"bssmap_le:rx:udt:reset:request",
+		"Number of received BSSMAP-LE UDT RESET messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_RX_UDT_RESET_ACK] = {
+		"bssmap_le:rx:udt:reset:ack",
+		"Number of received BSSMAP-LE UDT RESET ACKNOWLEDGE messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_RX_UDT_ERR_INVALID_MSG] = {
+		"bssmap_le:rx:udt:err:inval",
+		"Number of received invalid BSSMAP-LE UDT messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_ERR_INVALID_MSG] = {
+		"bssmap_le:rx:dt1:err:inval",
+		"Number of received invalid BSSMAP-LE"
+	},
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_RESPONSE_SUCCESS] = {
+		"bssmap_le:rx:dt1:location:response_success",
+		"Number of received BSSMAP-LE Perform Location Response messages containing a location estimate"
+	},
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE] = {
+		"bssmap_le:rx:dt1:location:response_failure",
+		"Number of received BSSMAP-LE Perform Location Response messages containing a failure cause"
+	},
+
+	[SMLC_CTR_BSSMAP_LE_TX_ERR_INVALID_MSG] = {
+		"bssmap_le:tx:err:inval",
+		"Number of outgoing BSSMAP-LE messages that are invalid (a bug?)"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_ERR_CONN_NOT_READY] = {
+		"bssmap_le:tx:err:conn_not_ready",
+		"Number of BSSMAP-LE messages we tried to send when the connection was not ready yet"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_ERR_SEND] = {
+		"bssmap_le:tx:err:send",
+		"Number of socket errors while sending BSSMAP-LE messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_SUCCESS] = {
+		"bssmap_le:tx:success",
+		"Number of successfully sent BSSMAP-LE messages"
+	},
+
+	[SMLC_CTR_BSSMAP_LE_TX_UDT_RESET] = {
+		"bssmap_le:tx:udt:reset:request",
+		"Number of transmitted BSSMAP-LE UDT RESET messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_UDT_RESET_ACK] = {
+		"bssmap_le:tx:udt:reset:ack",
+		"Number of transmitted BSSMAP-LE UDT RESET ACK messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_REQUEST] = {
+		"bssmap_le:tx:dt1:location:response",
+		"Number of transmitted BSSMAP-LE DT1 Perform Location Request messages"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_ABORT] = {
+		"bssmap_le:rx:dt1:location:abort",
+		"Number of received BSSMAP-LE Perform Location Abort messages"
+	},
+
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_TA_REQUEST] = {
+		"bssmap_le:rx:dt1:bsslap:ta_request",
+		"Number of received BSSMAP-LE Connection Oriented Information messages"
+		" with BSSLAP APDU containing TA Request"
+	},
+
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_TA_RESPONSE] = {
+		"bssmap_le:tx:dt1:bsslap:ta_response",
+		"Number of sent BSSMAP-LE Connection Oriented Information messages"
+		" with BSSLAP APDU containing TA Response"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_REJECT] = {
+		"bssmap_le:tx:dt1:bsslap:reject",
+		"Number of sent BSSMAP-LE Connection Oriented Information messages"
+		" with BSSLAP APDU containing Reject"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_RESET] = {
+		"bssmap_le:tx:dt1:bsslap:reset",
+		"Number of sent BSSMAP-LE Connection Oriented Information messages"
+		" with BSSLAP APDU containing Reset"
+	},
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_ABORT] = {
+		"bssmap_le:tx:dt1:bsslap:abort",
+		"Number of sent BSSMAP-LE Connection Oriented Information messages"
+		" with BSSLAP APDU containing Abort"
+	},
+
+};
+
+const struct rate_ctr_group_desc smlc_ctrg_desc = {
+	"smlc",
+	"serving mobile location centre",
+	OSMO_STATS_CLASS_GLOBAL,
+	ARRAY_SIZE(smlc_ctr_description),
+	smlc_ctr_description,
+};
diff --git a/src/osmo-bsc/lcs_loc_req.c b/src/osmo-bsc/lcs_loc_req.c
new file mode 100644
index 0000000..ca5c7b9
--- /dev/null
+++ b/src/osmo-bsc/lcs_loc_req.c
@@ -0,0 +1,581 @@
+/* Handle LCS BSSMAP-LE Perform Location Request */
+/*
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * 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/bsc/lcs_loc_req.h>
+
+#include <osmocom/bsc/bsc_msc_data.h>
+#include <osmocom/bsc/bsc_subscr_conn_fsm.h>
+#include <osmocom/bsc/bsc_subscriber.h>
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/bsc/lb.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/tdef.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/gad.h>
+#include <osmocom/gsm/bsslap.h>
+#include <osmocom/gsm/bssmap_le.h>
+#include <osmocom/gsm/gsm0808_lcs.h>
+#include <osmocom/bsc/lcs_ta_req.h>
+#include <osmocom/bsc/paging.h>
+#include <osmocom/bsc/bts_trx.h>
+#include <osmocom/bsc/bts.h>
+
+enum lcs_loc_req_fsm_state {
+	LCS_LOC_REQ_ST_INIT,
+	LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE,
+	LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING,
+	LCS_LOC_REQ_ST_GOT_LOCATION_RESPONSE,
+	LCS_LOC_REQ_ST_FAILED,
+};
+
+static const struct value_string lcs_loc_req_fsm_event_names[] = {
+	OSMO_VALUE_STRING(LCS_LOC_REQ_EV_RX_LB_PERFORM_LOCATION_RESPONSE),
+	OSMO_VALUE_STRING(LCS_LOC_REQ_EV_RX_A_PERFORM_LOCATION_ABORT),
+	OSMO_VALUE_STRING(LCS_LOC_REQ_EV_TA_REQ_START),
+	OSMO_VALUE_STRING(LCS_LOC_REQ_EV_TA_REQ_END),
+	OSMO_VALUE_STRING(LCS_LOC_REQ_EV_HANDOVER_PERFORMED),
+	OSMO_VALUE_STRING(LCS_LOC_REQ_EV_CONN_CLEAR),
+	{}
+};
+
+static struct osmo_fsm lcs_loc_req_fsm;
+
+static const struct osmo_tdef_state_timeout lcs_loc_req_fsm_timeouts[32] = {
+	[LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE] = { .T = -11 },
+};
+
+/* Transition to a state, using the T timer defined in lcs_loc_req_fsm_timeouts.
+ * The actual timeout value is in turn obtained from network->T_defs.
+ * Assumes local variable fi exists. */
+#define lcs_loc_req_fsm_state_chg(FI, STATE) \
+	osmo_tdef_fsm_inst_state_chg(FI, STATE, \
+				     lcs_loc_req_fsm_timeouts, \
+				     (bsc_gsmnet)->T_defs, \
+				     5)
+
+#define lcs_loc_req_fail(cause, fmt, args...) do { \
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR, "Perform Location Request failed in state %s: " fmt "\n", \
+				lcs_loc_req ? osmo_fsm_inst_state_name(lcs_loc_req->fi) : "NULL", ## args); \
+		lcs_loc_req->lcs_cause = (struct lcs_cause_ie){ \
+			.present = true, \
+			.cause_val = cause, \
+		}; \
+		lcs_loc_req_fsm_state_chg(lcs_loc_req->fi, LCS_LOC_REQ_ST_FAILED); \
+	} while(0)
+
+static struct lcs_loc_req *lcs_loc_req_alloc(struct osmo_fsm_inst *parent_fi, uint32_t parent_event_term)
+{
+	struct lcs_loc_req *lcs_loc_req;
+
+	struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc_child(&lcs_loc_req_fsm, parent_fi, parent_event_term);
+	OSMO_ASSERT(fi);
+
+	lcs_loc_req = talloc(fi, struct lcs_loc_req);
+	OSMO_ASSERT(lcs_loc_req);
+	fi->priv = lcs_loc_req;
+	*lcs_loc_req = (struct lcs_loc_req){
+		.fi = fi,
+	};
+
+	return lcs_loc_req;
+}
+
+static bool parse_bssmap_perf_loc_req(struct lcs_loc_req *lcs_loc_req, struct msgb *msg)
+{
+	struct tlv_parsed tp_arr[1];
+	struct tlv_parsed *tp = &tp_arr[0];
+	struct tlv_p_entry *e;
+	int payload_length;
+
+#define PARSE_ERR(ERRMSG) do { \
+			lcs_loc_req_fail(LCS_CAUSE_PROTOCOL_ERROR, "rx BSSMAP Perform Location Request: " ERRMSG); \
+			return false; \
+		} while(0)
+
+	payload_length = msg->tail - msg->l4h;
+	if (tlv_parse2(tp_arr, 1, gsm0808_att_tlvdef(), msg->l4h + 1, payload_length - 1, 0, 0) <= 0)
+		PARSE_ERR("Failed to parse IEs");
+
+	if (!(e = TLVP_GET(tp, GSM0808_IE_LOCATION_TYPE)))
+		PARSE_ERR("Missing Location Type IE");
+	if (osmo_bssmap_le_ie_dec_location_type(&lcs_loc_req->req.location_type, -1, -1, NULL, NULL, e->val, e->len))
+		PARSE_ERR("Failed to parse Location Type IE");
+
+	if ((e = TLVP_GET(tp, GSM0808_IE_CELL_IDENTIFIER))) {
+		if (gsm0808_dec_cell_id(&lcs_loc_req->req.cell_id, e->val, e->len) <= 0)
+			PARSE_ERR("Failed to parse Cell Identifier IE");
+		lcs_loc_req->req.cell_id_present = true;
+	}
+
+	if ((e = TLVP_GET(tp, GSM0808_IE_IMSI))) {
+		if (osmo_mobile_identity_decode(&lcs_loc_req->req.imsi, e->val, e->len, false)
+		    || lcs_loc_req->req.imsi.type != GSM_MI_TYPE_IMSI)
+			PARSE_ERR("Failed to parse IMSI IE");
+	}
+
+	if ((e = TLVP_GET(tp, GSM0808_IE_IMEI))) {
+		if (osmo_mobile_identity_decode(&lcs_loc_req->req.imei, e->val, e->len, false)
+		    || lcs_loc_req->req.imei.type != GSM_MI_TYPE_IMEI)
+			PARSE_ERR("Failed to parse IMEI IE");
+	}
+
+	// FIXME LCS QoS IE is mandatory for requesting the location
+
+	/* A lot of IEs remain ignored... */
+
+	return true;
+#undef PARSE_ERR
+}
+
+void lcs_loc_req_start(struct gsm_subscriber_connection *conn, struct msgb *loc_req_msg)
+{
+	struct lcs_loc_req *lcs_loc_req;
+
+	if (conn->lcs.loc_req) {
+		LOG_LCS_LOC_REQ(conn, LOGL_ERROR,
+				"Ignoring Perform Location Request, another request is still pending\n");
+		return;
+	}
+
+	lcs_loc_req = lcs_loc_req_alloc(conn->fi, GSCON_EV_LCS_LOC_REQ_END);
+
+	lcs_loc_req->conn = conn;
+	conn->lcs.loc_req = lcs_loc_req;
+
+	if (!parse_bssmap_perf_loc_req(lcs_loc_req, loc_req_msg))
+		return;
+
+	if (!conn->bsub) {
+		if (lcs_loc_req->req.imsi.type != GSM_MI_TYPE_IMSI) {
+			lcs_loc_req_fail(LCS_CAUSE_DATA_MISSING_IN_REQ,
+					 "tx Perform Location Request: Missing identity:"
+					 " No IMSI included in request, and also no active subscriber");
+			return;
+		}
+
+		conn->bsub = bsc_subscr_find_or_create_by_mi(bsc_gsmnet->bsc_subscribers, &lcs_loc_req->req.imsi,
+							     BSUB_USE_CONN);
+		if (!conn->bsub) {
+			lcs_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
+					 "tx Perform Location Request: Cannot assign subscriber");
+			return;
+		}
+	}
+
+	/* state change to start the timeout */
+	lcs_loc_req_fsm_state_chg(lcs_loc_req->fi, LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE);
+}
+
+static int handle_bssmap_le_conn_oriented_info(struct lcs_loc_req *lcs_loc_req, const struct bssmap_le_pdu *bssmap_le)
+{
+	switch (bssmap_le->conn_oriented_info.apdu.msg_type) {
+	case BSSLAP_MSGT_TA_REQUEST:
+		rate_ctr_inc(&bsc_gsmnet->smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_TA_REQUEST]);
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_DEBUG, "rx BSSLAP TA Request\n");
+		/* The TA Request message contains only the message type. */
+		return lcs_ta_req_start(lcs_loc_req);
+	default:
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR, "rx BSSLAP APDU with unsupported message type %d\n",
+				bssmap_le->conn_oriented_info.apdu.msg_type);
+		return -ENOTSUP;
+	};
+}
+
+int lcs_loc_req_rx_bssmap_le(struct gsm_subscriber_connection *conn, struct msgb *msg)
+{
+	struct lcs_loc_req *lcs_loc_req = conn->lcs.loc_req;
+	struct bssap_le_pdu bssap_le;
+	struct osmo_bssap_le_err *err;
+	struct rate_ctr *ctr = bsc_gsmnet->smlc->ctrs->ctr;
+
+	if (!lcs_loc_req) {
+		LOGPFSMSL(conn->fi, DLCS, LOGL_ERROR,
+			  "Rx BSSMAP-LE message, but no Location Request is ongoing\n");
+		return -EINVAL;
+	}
+
+	if (osmo_bssap_le_dec(&bssap_le, &err, msg, msg)) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR, "Rx BSSAP-LE message with error: %s\n", err->logmsg);
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_DT1_ERR_INVALID_MSG]);
+		return -EINVAL;
+	}
+
+	if (bssap_le.discr != BSSAP_LE_MSG_DISCR_BSSMAP_LE) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR, "Rx BSSAP-LE: discr %d not implemented\n", bssap_le.discr);
+		rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_DT1_ERR_INVALID_MSG]);
+		return -ENOTSUP;
+	}
+
+	LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_DEBUG, "Rx %s\n", osmo_bssap_le_pdu_to_str_c(OTC_SELECT, &bssap_le));
+
+	switch (bssap_le.bssmap_le.msg_type) {
+	case BSSMAP_LE_MSGT_PERFORM_LOC_RESP:
+		if (bssap_le.bssmap_le.perform_loc_resp.location_estimate_present)
+			rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_RESPONSE_SUCCESS]);
+		else
+			rate_ctr_inc(&ctr[SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE]);
+		return osmo_fsm_inst_dispatch(lcs_loc_req->fi, LCS_LOC_REQ_EV_RX_LB_PERFORM_LOCATION_RESPONSE,
+					      &bssap_le.bssmap_le);
+
+	case BSSMAP_LE_MSGT_CONN_ORIENTED_INFO:
+		return handle_bssmap_le_conn_oriented_info(lcs_loc_req, &bssap_le.bssmap_le);
+
+	default:
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR, "Rx BSSMAP-LE from SMLC with unsupported message type: %s\n",
+				osmo_bssap_le_pdu_to_str_c(OTC_SELECT, &bssap_le));
+		return -ENOTSUP;
+	}
+}
+
+void lcs_loc_req_reset(struct gsm_subscriber_connection *conn)
+{
+	struct lcs_loc_req *lcs_loc_req = conn->lcs.loc_req;
+	if (!lcs_loc_req)
+		return;
+	lcs_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Aborting Location Request due to RESET on Lb");
+}
+
+static int lcs_loc_req_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct lcs_loc_req *lcs_loc_req = fi->priv;
+	lcs_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Timeout");
+	return 1;
+}
+
+static int lcs_loc_req_send(struct lcs_loc_req *lcs_loc_req, const struct bssap_le_pdu *bssap_le)
+{
+	int rc = lb_send(lcs_loc_req->conn, bssap_le);
+	if (rc)
+		lcs_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
+				 "Failed to send %s", osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
+	return rc;
+}
+
+static void lcs_loc_req_wait_loc_resp_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct lcs_loc_req *lcs_loc_req = fi->priv;
+	struct bssap_le_pdu plr;
+	struct gsm_lchan *lchan;
+
+	if (prev_state == LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING) {
+		/* LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING should halt the FSM timeout. As soon as the TA Request is
+		 * served, re-entering LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE, but of course there is then no need to
+		 * send a second BSSMAP-LE Perform Location Request to the SMLC. */
+		return;
+	}
+
+	if (!lcs_loc_req->req.cell_id_present) {
+		lcs_loc_req_fail(LCS_CAUSE_PROTOCOL_ERROR,
+				 "Cannot encode BSSMAP-LE Perform Location Request,"
+				 " because mandatory Cell Identity is not known");
+		return;
+	}
+
+	plr = (struct bssap_le_pdu){
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_REQ,
+			.perform_loc_req = {
+				.location_type = lcs_loc_req->req.location_type,
+				.cell_id = lcs_loc_req->req.cell_id,
+				.imsi = lcs_loc_req->req.imsi,
+				.imei = lcs_loc_req->req.imei,
+			},
+		},
+	};
+
+	/* If we already have an active lchan, send the known TA directly to the SMLC */
+	lchan = lcs_loc_req->conn->lchan;
+	if (lchan) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_DEBUG,
+				"Active lchan present, including BSSLAP APDU with TA Layer 3\n");
+		plr.bssmap_le.perform_loc_req.apdu_present = true;
+		plr.bssmap_le.perform_loc_req.apdu = (struct bsslap_pdu){
+			.msg_type = BSSLAP_MSGT_TA_LAYER3,
+			.ta_layer3 = {
+				.ta = lchan->rqd_ta,
+			},
+		};
+	} else {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_DEBUG,
+				"No active lchan, not including BSSLAP APDU\n");
+	}
+
+	/* Establish Lb connection to SMLC and send the BSSMAP-LE Perform Location Request */
+	lcs_loc_req_send(lcs_loc_req, &plr);
+}
+
+static void lcs_loc_req_bssmap_le_abort(struct lcs_loc_req *lcs_loc_req)
+{
+	struct bssap_le_pdu pla = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_ABORT,
+			.perform_loc_abort = {
+				.present = true,
+				.cause_val = LCS_CAUSE_REQUEST_ABORTED,
+			},
+		},
+	};
+
+	lcs_loc_req_send(lcs_loc_req, &pla);
+}
+
+/* After a handover, send the new lchan information to the SMLC via a BSSLAP Reset message.
+ * See 3GPP TS 48.071 4.2.6 Reset. */
+static void lcs_loc_req_handover_performed(struct lcs_loc_req *lcs_loc_req)
+{
+	struct gsm_lchan *lchan = lcs_loc_req->conn->lchan;
+	struct bssap_le_pdu bsslap = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_CONN_ORIENTED_INFO,
+		},
+	};
+	struct bsslap_pdu *apdu = &bsslap.bssmap_le.conn_oriented_info.apdu;
+
+	if (!lchan) {
+		/* The handover was out of this BSS. Abort the location procedure. */
+		*apdu = (struct bsslap_pdu){
+			.msg_type = BSSLAP_MSGT_ABORT,
+			.abort = BSSLAP_CAUSE_INTER_BSS_HO,
+		};
+	} else {
+		*apdu = (struct bsslap_pdu){
+			.msg_type = BSSLAP_MSGT_RESET,
+			.reset = {
+				.cell_id = lchan->ts->trx->bts->cell_identity,
+				.ta = lchan->rqd_ta,
+				.cause = BSSLAP_CAUSE_INTRA_BSS_HO,
+			},
+		};
+		gsm48_lchan2chan_desc(&apdu->reset.chan_desc, lchan);
+	}
+
+	lcs_loc_req_send(lcs_loc_req, &bsslap);
+}
+
+static void lcs_loc_req_wait_loc_resp_and_ta_req_ongoing_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct lcs_loc_req *lcs_loc_req = fi->priv;
+	const struct bssmap_le_pdu *bssmap_le;
+
+	switch (event) {
+
+	case LCS_LOC_REQ_EV_RX_LB_PERFORM_LOCATION_RESPONSE:
+		bssmap_le = data;
+		OSMO_ASSERT(bssmap_le->msg_type == BSSMAP_LE_MSGT_PERFORM_LOC_RESP);
+		lcs_loc_req->resp = bssmap_le->perform_loc_resp;
+		lcs_loc_req->resp_present = true;
+		lcs_loc_req_fsm_state_chg(fi, LCS_LOC_REQ_ST_GOT_LOCATION_RESPONSE);
+		break;
+
+	case LCS_LOC_REQ_EV_TA_REQ_START:
+		if (fi->state != LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING)
+			lcs_loc_req_fsm_state_chg(fi, LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING);
+		break;
+
+	case LCS_LOC_REQ_EV_TA_REQ_END:
+		if (fi->state != LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE)
+			lcs_loc_req_fsm_state_chg(fi, LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE);
+		break;
+
+	case LCS_LOC_REQ_EV_HANDOVER_PERFORMED:
+		lcs_loc_req_handover_performed(lcs_loc_req);
+		break;
+
+	case LCS_LOC_REQ_EV_RX_A_PERFORM_LOCATION_ABORT:
+	case LCS_LOC_REQ_EV_CONN_CLEAR:
+		if (lcs_loc_req->ta_req)
+			osmo_fsm_inst_dispatch(lcs_loc_req->ta_req->fi, LCS_TA_REQ_EV_ABORT, NULL);
+		lcs_loc_req_bssmap_le_abort(lcs_loc_req);
+		osmo_fsm_inst_term(lcs_loc_req->fi, OSMO_FSM_TERM_REGULAR, NULL);
+		break;
+
+	default:
+		OSMO_ASSERT(false);
+	}
+}
+
+static void lcs_loc_req_got_loc_resp_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct lcs_loc_req *lcs_loc_req = fi->priv;
+	struct msgb *msg;
+	int rc;
+	struct gsm0808_perform_location_response plr = {
+		.location_estimate_present = lcs_loc_req->resp.location_estimate_present,
+		.location_estimate = lcs_loc_req->resp.location_estimate,
+		.lcs_cause = lcs_loc_req->resp.lcs_cause,
+	};
+
+	if (plr.location_estimate_present) {
+		struct osmo_gad gad;
+		struct osmo_gad_err *err;
+		if (osmo_gad_dec(&gad, &err, OTC_SELECT, &plr.location_estimate))
+			LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR,
+					"Perform Location Response contains Location Estimate with error: %s\n",
+					err->logmsg);
+		else
+			LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_INFO,
+					"Perform Location Response contains Location Estimate: %s\n",
+					osmo_gad_to_str_c(OTC_SELECT, &gad));
+	}
+
+	if (plr.lcs_cause.present) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR,
+				"Perform Location Response contains error cause: %d\n",
+				plr.lcs_cause.cause_val);
+	}
+
+	msg = gsm0808_create_perform_location_response(&plr);
+	if (!msg) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR,
+				"Failed to encode BSSMAP Perform Location Response (A-interface)\n");
+	} else {
+		rc = gscon_sigtran_send(lcs_loc_req->conn, msg);
+		if (rc < 0)
+			LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR,
+					"Failed to send Perform Location Response (A-interface)\n");
+		else
+			rate_ctr_inc(&lcs_loc_req->conn->sccp.msc->msc_ctrs->ctr[
+				plr.location_estimate_present ? MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_SUCCESS
+				: MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE]);
+	}
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+static void lcs_loc_req_failed_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct lcs_loc_req *lcs_loc_req = fi->priv;
+	struct msgb *msg;
+	int rc;
+	struct bssap_le_pdu pla = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_ABORT,
+			.perform_loc_abort = lcs_loc_req->lcs_cause,
+		},
+	};
+	struct gsm0808_perform_location_response plr = {
+		.lcs_cause = lcs_loc_req->lcs_cause,
+	};
+
+	/* If we're paging this subscriber for LCS, stop paging. */
+	paging_request_cancel(lcs_loc_req->conn->bsub, BSC_PAGING_FOR_LCS);
+
+	/* Send Perform Location Abort to SMLC, only if we got started on the Lb */
+	if (lcs_loc_req->conn->lcs.lb.state == SUBSCR_SCCP_ST_CONNECTED)
+		lcs_loc_req_send(lcs_loc_req, &pla);
+
+	/* Send Perform Location Result with failure cause to MSC */
+	msg = gsm0808_create_perform_location_response(&plr);
+	if (!msg) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR,
+				"Failed to encode BSSMAP Perform Location Response (A-interface)\n");
+	} else {
+		rc = gscon_sigtran_send(lcs_loc_req->conn, msg);
+		if (rc < 0)
+			LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR,
+					"Failed to send BSSMAP Perform Location Response (A-interface)\n");
+		else
+			rate_ctr_inc(&lcs_loc_req->conn->sccp.msc->msc_ctrs->ctr[MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE]);
+	}
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+void lcs_loc_req_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+	struct lcs_loc_req *lcs_loc_req = fi->priv;
+	if (lcs_loc_req->conn && lcs_loc_req->conn->lcs.loc_req == lcs_loc_req)
+		lcs_loc_req->conn->lcs.loc_req = NULL;
+	/* FSM termination will dispatch GSCON_EV_LCS_LOC_REQ_END to the conn FSM */
+}
+
+#define S(x)    (1 << (x))
+
+static const struct osmo_fsm_state lcs_loc_req_fsm_states[] = {
+	[LCS_LOC_REQ_ST_INIT] = {
+		.name = "INIT",
+		.out_state_mask = 0
+			| S(LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE)
+			| S(LCS_LOC_REQ_ST_FAILED)
+			,
+	},
+	[LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE] = {
+		.name = "WAIT_LOCATION_RESPONSE",
+		.in_event_mask = 0
+			| S(LCS_LOC_REQ_EV_RX_LB_PERFORM_LOCATION_RESPONSE)
+			| S(LCS_LOC_REQ_EV_RX_A_PERFORM_LOCATION_ABORT)
+			| S(LCS_LOC_REQ_EV_TA_REQ_START)
+			| S(LCS_LOC_REQ_EV_TA_REQ_END)
+			| S(LCS_LOC_REQ_EV_HANDOVER_PERFORMED)
+			| S(LCS_LOC_REQ_EV_CONN_CLEAR)
+			,
+		.out_state_mask = 0
+			| S(LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING)
+			| S(LCS_LOC_REQ_ST_GOT_LOCATION_RESPONSE)
+			| S(LCS_LOC_REQ_ST_FAILED)
+			,
+		.onenter = lcs_loc_req_wait_loc_resp_onenter,
+		.action = lcs_loc_req_wait_loc_resp_and_ta_req_ongoing_action,
+	},
+	[LCS_LOC_REQ_ST_BSSLAP_TA_REQ_ONGOING] = {
+		.name = "BSSLAP_TA_REQ_ONGOING",
+		.in_event_mask = 0
+			| S(LCS_LOC_REQ_EV_RX_LB_PERFORM_LOCATION_RESPONSE)
+			| S(LCS_LOC_REQ_EV_RX_A_PERFORM_LOCATION_ABORT)
+			| S(LCS_LOC_REQ_EV_TA_REQ_END)
+			| S(LCS_LOC_REQ_EV_HANDOVER_PERFORMED)
+			| S(LCS_LOC_REQ_EV_CONN_CLEAR)
+			,
+		.out_state_mask = 0
+			| S(LCS_LOC_REQ_ST_WAIT_LOCATION_RESPONSE)
+			| S(LCS_LOC_REQ_ST_GOT_LOCATION_RESPONSE)
+			| S(LCS_LOC_REQ_ST_FAILED)
+			,
+		.action = lcs_loc_req_wait_loc_resp_and_ta_req_ongoing_action,
+	},
+	[LCS_LOC_REQ_ST_GOT_LOCATION_RESPONSE] = {
+		.name = "GOT_LOCATION_RESPONSE",
+		.onenter = lcs_loc_req_got_loc_resp_onenter,
+	},
+	[LCS_LOC_REQ_ST_FAILED] = {
+		.name = "FAILED",
+		.onenter = lcs_loc_req_failed_onenter,
+	},
+};
+
+static struct osmo_fsm lcs_loc_req_fsm = {
+	.name = "lcs_loc_req",
+	.states = lcs_loc_req_fsm_states,
+	.num_states = ARRAY_SIZE(lcs_loc_req_fsm_states),
+	.log_subsys = DLCS,
+	.event_names = lcs_loc_req_fsm_event_names,
+	.timer_cb = lcs_loc_req_fsm_timer_cb,
+	.cleanup = lcs_loc_req_fsm_cleanup,
+};
+
+static __attribute__((constructor)) void lcs_loc_req_fsm_register(void)
+{
+	OSMO_ASSERT(osmo_fsm_register(&lcs_loc_req_fsm) == 0);
+}
diff --git a/src/osmo-bsc/lcs_ta_req.c b/src/osmo-bsc/lcs_ta_req.c
new file mode 100644
index 0000000..97d6eb5
--- /dev/null
+++ b/src/osmo-bsc/lcs_ta_req.c
@@ -0,0 +1,305 @@
+/* Handle LCS BSSLAP TA Request */
+/*
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * 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/bsc/lcs_ta_req.h>
+
+#include <osmocom/bsc/lcs_loc_req.h>
+#include <osmocom/bsc/lb.h>
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/bsc/paging.h>
+#include <osmocom/bsc/bts.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/tdef.h>
+#include <osmocom/gsm/bsslap.h>
+
+enum lcs_ta_req_fsm_state {
+	LCS_TA_REQ_ST_INIT,
+	LCS_TA_REQ_ST_WAIT_TA,
+	LCS_TA_REQ_ST_GOT_TA,
+	LCS_TA_REQ_ST_FAILED,
+};
+
+static const struct value_string lcs_ta_req_fsm_event_names[] = {
+	OSMO_VALUE_STRING(LCS_TA_REQ_EV_GOT_TA),
+	OSMO_VALUE_STRING(LCS_TA_REQ_EV_ABORT),
+	{}
+};
+
+static const struct osmo_tdef_state_timeout lcs_ta_req_fsm_timeouts[32] = {
+	[LCS_TA_REQ_ST_WAIT_TA] = { .T = -12 },
+};
+
+/* Transition to a state, using the T timer defined in lcs_ta_req_fsm_timeouts.
+ * The actual timeout value is in turn obtained from network->T_defs.
+ * Assumes local variable fi exists. */
+#define lcs_ta_req_fsm_state_chg(FI, STATE) \
+	osmo_tdef_fsm_inst_state_chg(FI, STATE, \
+				     lcs_ta_req_fsm_timeouts, \
+				     (bsc_gsmnet)->T_defs, \
+				     5)
+
+#define lcs_ta_req_fail(cause, fmt, args...) do { \
+		LOG_LCS_TA_REQ(lcs_ta_req, LOGL_ERROR, "BSSLAP TA Request failed in state %s: " fmt "\n", \
+			       lcs_ta_req ? osmo_fsm_inst_state_name(lcs_ta_req->fi) : "NULL", ## args); \
+		lcs_ta_req->failure_cause = cause; \
+		lcs_ta_req_fsm_state_chg(lcs_ta_req->fi, LCS_TA_REQ_ST_FAILED); \
+	} while(0)
+
+static struct osmo_fsm lcs_ta_req_fsm;
+
+static struct lcs_ta_req *lcs_ta_req_alloc(struct osmo_fsm_inst *parent_fi, uint32_t parent_event_term)
+{
+	struct lcs_ta_req *lcs_ta_req;
+
+	struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc_child(&lcs_ta_req_fsm, parent_fi, parent_event_term);
+	OSMO_ASSERT(fi);
+
+	lcs_ta_req = talloc(fi, struct lcs_ta_req);
+	OSMO_ASSERT(lcs_ta_req);
+	fi->priv = lcs_ta_req;
+	*lcs_ta_req = (struct lcs_ta_req){
+		.fi = fi,
+	};
+
+	return lcs_ta_req;
+}
+
+int lcs_ta_req_start(struct lcs_loc_req *lcs_loc_req)
+{
+	struct lcs_ta_req *lcs_ta_req;
+	if (lcs_loc_req->ta_req) {
+		LOG_LCS_TA_REQ(lcs_loc_req->ta_req, LOGL_ERROR,
+			       "Cannot start anoter TA Request FSM, this TA Request is still active\n");
+		return -ENOTSUP;
+	}
+	lcs_ta_req = lcs_ta_req_alloc(lcs_loc_req->fi, LCS_LOC_REQ_EV_TA_REQ_END);
+	if (!lcs_ta_req) {
+		LOG_LCS_LOC_REQ(lcs_loc_req, LOGL_ERROR, "Cannot allocate TA Request FSM");
+		return -ENOSPC;
+	}
+	lcs_ta_req->loc_req = lcs_loc_req;
+	lcs_loc_req->ta_req = lcs_ta_req;
+
+	return lcs_ta_req_fsm_state_chg(lcs_ta_req->fi, LCS_TA_REQ_ST_WAIT_TA);
+}
+
+static int lcs_ta_req_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct lcs_ta_req *lcs_ta_req = fi->priv;
+	lcs_ta_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Timeout");
+	return 1;
+}
+
+void lcs_ta_req_wait_ta_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct lcs_ta_req *lcs_ta_req = fi->priv;
+	struct lcs_loc_req *loc_req = lcs_ta_req->loc_req;
+	struct gsm_lchan *lchan;
+	struct bsc_paging_params paging;
+
+	if (osmo_fsm_inst_dispatch(loc_req->fi, LCS_LOC_REQ_EV_TA_REQ_START, lcs_ta_req)) {
+		lcs_ta_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Failed to dispatch LCS_LOC_REQ_EV_TA_REQ_START");
+		return;
+	}
+
+	paging = (struct bsc_paging_params){
+		.reason = BSC_PAGING_FOR_LCS,
+		.msc = loc_req->conn->sccp.msc,
+		.bsub = loc_req->conn->bsub,
+		.tmsi = GSM_RESERVED_TMSI,
+		.imsi = loc_req->req.imsi,
+		.chan_needed = RSL_CHANNEED_ANY,
+	};
+	if (paging.bsub)
+		bsc_subscr_get(paging.bsub, BSUB_USE_PAGING_START);
+
+	/* Do we already have an active lchan with knowledge of TA? */
+	lchan = loc_req->conn->lchan;
+	if (lchan) {
+		lcs_ta_req_fsm_state_chg(fi, LCS_TA_REQ_ST_GOT_TA);
+		return;
+	}
+
+	/* No lchan yet, need to start Paging */
+	if (loc_req->req.imsi.type != GSM_MI_TYPE_IMSI) {
+		lcs_ta_req_fail(LCS_CAUSE_PROTOCOL_ERROR,
+				"No IMSI in BSSMAP Location Request and no active lchan, cannot start Paging");
+		return;
+	}
+
+	if (!loc_req->req.cell_id_present) {
+		LOG_LCS_TA_REQ(lcs_ta_req, LOGL_DEBUG,
+			       "No Cell Identity in BSSMAP Location Request, paging entire BSS\n");
+		paging.cil = (struct gsm0808_cell_id_list2){
+			.id_discr = CELL_IDENT_BSS,
+		};
+	} else {
+		paging.cil = (struct gsm0808_cell_id_list2){
+			.id_discr = loc_req->req.cell_id.id_discr,
+			.id_list = { loc_req->req.cell_id.id },
+			.id_list_len = 1,
+		};
+	}
+
+	bsc_paging_start(&paging);
+}
+
+static void lcs_ta_req_wait_ta_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	switch (event) {
+
+	case LCS_TA_REQ_EV_GOT_TA:
+		lcs_ta_req_fsm_state_chg(fi, LCS_TA_REQ_ST_GOT_TA);
+		break;
+
+	case LCS_TA_REQ_EV_ABORT:
+		lcs_ta_req_fsm_state_chg(fi, LCS_TA_REQ_ST_FAILED);
+		break;
+
+	default:
+		OSMO_ASSERT(false);
+	}
+}
+
+static int lcs_ta_req_send(struct lcs_ta_req *lcs_ta_req, const struct bssap_le_pdu *bssap_le)
+{
+	int rc = lb_send(lcs_ta_req->loc_req->conn, bssap_le);
+	if (rc)
+		lcs_ta_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
+				 "Failed to send %s", osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
+	return rc;
+}
+
+void lcs_ta_req_got_ta_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct lcs_ta_req *lcs_ta_req = fi->priv;
+	struct bssap_le_pdu bsslap_ta_resp;
+	struct gsm_lchan *lchan = lcs_ta_req->loc_req->conn->lchan;
+
+	if (!lchan) {
+		lcs_ta_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Internal error: no lchan");
+		return;
+	}
+
+	bsslap_ta_resp = (struct bssap_le_pdu) {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_CONN_ORIENTED_INFO,
+			.conn_oriented_info = {
+				.apdu = {
+					.msg_type = BSSLAP_MSGT_TA_RESPONSE,
+					.ta_response = {
+						.cell_id = lchan->ts->trx->bts->cell_identity,
+						.ta = lchan->rqd_ta,
+					},
+				},
+			},
+		},
+	};
+
+	lcs_ta_req_send(lcs_ta_req, &bsslap_ta_resp);
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+void lcs_ta_req_failed_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct lcs_ta_req *lcs_ta_req = fi->priv;
+	struct bssap_le_pdu bsslap_abort;
+
+	bsslap_abort = (struct bssap_le_pdu) {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_CONN_ORIENTED_INFO,
+			.conn_oriented_info = {
+				.apdu = {
+					.msg_type = BSSLAP_MSGT_ABORT,
+					.abort = BSSLAP_CAUSE_OTHER_RADIO_EVT_FAIL,
+				},
+			},
+		},
+	};
+
+	lcs_ta_req_send(lcs_ta_req, &bsslap_abort);
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_ERROR, NULL);
+}
+
+void lcs_ta_req_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+	struct lcs_ta_req *lcs_ta_req = fi->priv;
+	if (lcs_ta_req->loc_req->ta_req == lcs_ta_req)
+		lcs_ta_req->loc_req->ta_req = NULL;
+	/* FSM termination will dispatch LCS_LOC_REQ_EV_TA_REQ_END to the lcs_loc_req FSM */
+}
+
+
+#define S(x)    (1 << (x))
+
+static const struct osmo_fsm_state lcs_ta_req_fsm_states[] = {
+	[LCS_TA_REQ_ST_INIT] = {
+		.name = "init",
+		.out_state_mask = 0
+			| S(LCS_TA_REQ_ST_WAIT_TA)
+			| S(LCS_TA_REQ_ST_GOT_TA)
+			,
+	},
+	[LCS_TA_REQ_ST_WAIT_TA] = {
+		.name = "wait_ta",
+		.in_event_mask = 0
+			| S(LCS_TA_REQ_EV_GOT_TA)
+			| S(LCS_TA_REQ_EV_ABORT)
+			,
+		.out_state_mask = 0
+			| S(LCS_TA_REQ_ST_GOT_TA)
+			| S(LCS_TA_REQ_ST_FAILED)
+			,
+		.onenter = lcs_ta_req_wait_ta_onenter,
+		.action = lcs_ta_req_wait_ta_action,
+	},
+	[LCS_TA_REQ_ST_GOT_TA] = {
+		.name = "got_ta",
+		.in_event_mask = 0
+			,
+		.out_state_mask = 0
+			,
+		.onenter = lcs_ta_req_got_ta_onenter,
+	},
+	[LCS_TA_REQ_ST_FAILED] = {
+		.name = "failed",
+		.onenter = lcs_ta_req_failed_onenter,
+	},
+};
+
+static struct osmo_fsm lcs_ta_req_fsm = {
+	.name = "lcs_ta_req",
+	.states = lcs_ta_req_fsm_states,
+	.num_states = ARRAY_SIZE(lcs_ta_req_fsm_states),
+	.log_subsys = DLCS,
+	.event_names = lcs_ta_req_fsm_event_names,
+	.timer_cb = lcs_ta_req_fsm_timer_cb,
+	.cleanup = lcs_ta_req_fsm_cleanup,
+};
+
+static __attribute__((constructor)) void lcs_ta_req_fsm_register(void)
+{
+	OSMO_ASSERT(osmo_fsm_register(&lcs_ta_req_fsm) == 0);
+}
diff --git a/src/osmo-bsc/net_init.c b/src/osmo-bsc/net_init.c
index 8b1e080..6cbb40c 100644
--- a/src/osmo-bsc/net_init.c
+++ b/src/osmo-bsc/net_init.c
@@ -52,6 +52,7 @@
 	{ .T=-8, .default_val=5, .desc="Timeout for RSL IPA MDCX ACK after sending RSL IPA MDCX" },
 	{ .T=-9, .default_val=5, .desc="Timeout for availability of MGW endpoint" },
 	{ .T=-10, .default_val=5, .desc="Timeout for fully configured MGW endpoint" },
+	{ .T=-11, .default_val=5, .desc="Timeout for Perform Location Response from SMLC" },
 	{ .T=-3111, .default_val=4, .desc="Wait time after lchan was released in error (should be T3111 + 2s)" },
 	{ .T=-3210, .default_val=20, .desc="After L3 Complete, wait for MSC to confirm" },
 	{}
diff --git a/src/osmo-bsc/osmo_bsc_bssap.c b/src/osmo-bsc/osmo_bsc_bssap.c
index 10f0edd..c2c05af 100644
--- a/src/osmo-bsc/osmo_bsc_bssap.c
+++ b/src/osmo-bsc/osmo_bsc_bssap.c
@@ -44,6 +44,7 @@
 #include <osmocom/core/fsm.h>
 #include <osmocom/core/socket.h>
 #include <osmocom/core/sockaddr_str.h>
+#include <osmocom/bsc/lcs_loc_req.h>
 
 #define IP_V4_ADDR_LEN 4
 
@@ -1176,6 +1177,21 @@
 		rate_ctr_inc(&ctrs[MSC_CTR_BSSMAP_RX_DT1_COMMON_ID]);
 		ret = bssmap_handle_common_id(conn, msg, length);
 		break;
+	case BSS_MAP_MSG_PERFORM_LOCATION_RQST:
+		rate_ctr_inc(&ctrs[MSC_CTR_BSSMAP_RX_DT1_PERFORM_LOCATION_REQUEST]);
+		lcs_loc_req_start(conn, msg);
+		ret = 0;
+		break;
+	case BSS_MAP_MSG_PERFORM_LOCATION_ABORT:
+		rate_ctr_inc(&ctrs[MSC_CTR_BSSMAP_RX_DT1_PERFORM_LOCATION_ABORT]);
+		if (conn->lcs.loc_req) {
+			ret = osmo_fsm_inst_dispatch(conn->lcs.loc_req->fi, LCS_LOC_REQ_EV_RX_A_PERFORM_LOCATION_ABORT,
+						     msg);
+		} else {
+			LOGP(DMSC, LOGL_ERROR, "Rx BSSMAP Perform Location Abort without ongoing Location Request\n");
+			ret = 0;
+		}
+		break;
 	default:
 		rate_ctr_inc(&ctrs[MSC_CTR_BSSMAP_RX_DT1_UNKNOWN]);
 		LOGP(DMSC, LOGL_NOTICE, "Unimplemented msg type: %s\n",
diff --git a/src/osmo-bsc/osmo_bsc_main.c b/src/osmo-bsc/osmo_bsc_main.c
index ce9df1d..26d32d1 100644
--- a/src/osmo-bsc/osmo_bsc_main.c
+++ b/src/osmo-bsc/osmo_bsc_main.c
@@ -38,6 +38,7 @@
 #include <osmocom/bsc/assignment_fsm.h>
 #include <osmocom/bsc/handover_fsm.h>
 #include <osmocom/bsc/smscb.h>
+#include <osmocom/bsc/lb.h>
 
 #include <osmocom/ctrl/control_cmd.h>
 #include <osmocom/ctrl/control_if.h>
@@ -788,8 +789,12 @@
 		.name = "DCBS",
 		.description = "Cell Broadcast System",
 		.enabled = 1, .loglevel = LOGL_NOTICE,
-	}
-
+	},
+	[DLCS] = {
+		.name = "DLCS",
+		.description = "Location Services",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
 };
 
 static int filter_fn(const struct log_context *ctx, struct log_target *tar)
@@ -947,6 +952,7 @@
 	handover_decision_1_init();
 	hodec2_init(bsc_gsmnet);
 	bsc_cbc_link_restart();
+	lb_init();
 
 	signal(SIGINT, &signal_handler);
 	signal(SIGTERM, &signal_handler);
diff --git a/src/osmo-bsc/osmo_bsc_msc.c b/src/osmo-bsc/osmo_bsc_msc.c
index 26d2bc8..583b6ff 100644
--- a/src/osmo-bsc/osmo_bsc_msc.c
+++ b/src/osmo-bsc/osmo_bsc_msc.c
@@ -62,6 +62,8 @@
 	[MSC_CTR_BSSMAP_RX_DT1_UNKNOWN] =           {"bssmap:rx:dt1:err_unknown", "Number of received BSSMAP unknown DT1 messages"},
 	[MSC_CTR_BSSMAP_RX_DT1_DTAP] =              {"bssmap:rx:dt1:dtap:good", "Number of received BSSMAP DTAP messages"},
 	[MSC_CTR_BSSMAP_RX_DT1_DTAP_ERROR] =        {"bssmap:rx:dt1:dtap:error", "Number of received BSSMAP DTAP messages with errors"},
+	[MSC_CTR_BSSMAP_RX_DT1_PERFORM_LOCATION_REQUEST] = {"bssmap:rx:dt1:location:request", "Number of received BSSMAP Perform Location Request messages"},
+	[MSC_CTR_BSSMAP_RX_DT1_PERFORM_LOCATION_ABORT] = {"bssmap:tx:dt1:location:abort", "Number of received BSSMAP Perform Location Abort messages"},
 
 	/* Tx message counters (per message type)
 	 *
@@ -102,6 +104,10 @@
 	[MSC_CTR_BSSMAP_TX_DT1_HANDOVER_COMPLETE] =         {"bssmap:tx:dt1:handover:complete", "Number of transmitted BSSMAP DT1 HANDOVER COMPLETE messages"},
 	[MSC_CTR_BSSMAP_TX_DT1_HANDOVER_FAILURE] =          {"bssmap:tx:dt1:handover:failure", "Number of transmitted BSSMAP DT1 HANDOVER FAILURE messages"},
 	[MSC_CTR_BSSMAP_TX_DT1_DTAP] =                      {"bssmap:tx:dt1:dtap", "Number of transmitted BSSMAP DT1 DTAP messages"},
+	[MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_SUCCESS] = {"bssmap:tx:dt1:location:response_success",
+		"Number of transmitted BSSMAP Perform Location Response messages containing a location estimate"},
+	[MSC_CTR_BSSMAP_TX_DT1_PERFORM_LOCATION_RESPONSE_FAILURE] = {"bssmap:tx:dt1:location:response_failure",
+		"Number of transmitted BSSMAP Perform Location Response messages containing a failure cause"},
 
 	/* Indicators for MSC pool usage */
 	[MSC_CTR_MSCPOOL_SUBSCR_NEW] = {
diff --git a/src/osmo-bsc/paging.c b/src/osmo-bsc/paging.c
index a4a5a1e..15aca00 100644
--- a/src/osmo-bsc/paging.c
+++ b/src/osmo-bsc/paging.c
@@ -79,9 +79,9 @@
 
 	log_set_context(LOG_CTX_BSC_SUBSCR, request->bsub);
 
-	LOG_BTS(bts, DPAG, LOGL_INFO, "Going to send paging commands: imsi: %s tmsi: "
-		"0x%08x for ch. type %d (attempt %d)\n", request->bsub->imsi,
-		request->bsub->tmsi, request->chan_type, request->attempts);
+	LOG_BTS(bts, DPAG, LOGL_INFO, "Going to send paging commands: %s"
+		" for ch. type %d (attempt %d)\n", bsc_subscr_name(request->bsub),
+		request->chan_type, request->attempts);
 
 	if (request->bsub->tmsi == GSM_RESERVED_TMSI) {
 		mi = (struct osmo_mobile_identity){
@@ -457,6 +457,30 @@
 	return count;
 }
 
+/* Remove all paging requests, for specific reasons only. */
+int paging_request_cancel(struct bsc_subscr *bsub, enum bsc_paging_reason reasons)
+{
+	struct gsm_bts *bts;
+	int count = 0;
+
+	llist_for_each_entry(bts, &bsc_gsmnet->bts_list, list) {
+		struct gsm_paging_request *req, *req2;
+
+		paging_init_if_needed(bts);
+
+		llist_for_each_entry_safe(req, req2, &bts->paging.pending_requests, entry) {
+			if (req->bsub != bsub)
+				continue;
+			if (!(req->reason & reasons))
+				continue;
+			LOG_BTS(bts, DPAG, LOGL_DEBUG, "Cancel paging %s\n", bsc_subscr_name(bsub));
+			paging_remove_request(&bts->paging, req);
+			count++;
+		}
+	}
+	return count;
+}
+
 /*! Update the BTS paging buffer slots on given BTS */
 void paging_update_buffer_space(struct gsm_bts *bts, uint16_t free_slots)
 {
diff --git a/tests/bsc/bsc_test.c b/tests/bsc/bsc_test.c
index 6a6ff8b..0ed504b 100644
--- a/tests/bsc/bsc_test.c
+++ b/tests/bsc/bsc_test.c
@@ -30,6 +30,7 @@
 #include <osmocom/bsc/osmo_bsc.h>
 #include <osmocom/bsc/bsc_msc_data.h>
 
+#include <osmocom/gsm/gad.h>
 #include <osmocom/core/application.h>
 #include <osmocom/core/backtrace.h>
 #include <osmocom/core/talloc.h>
diff --git a/tests/handover/Makefile.am b/tests/handover/Makefile.am
index 87981d9..0f7953c 100644
--- a/tests/handover/Makefile.am
+++ b/tests/handover/Makefile.am
@@ -93,6 +93,10 @@
 	$(top_builddir)/src/osmo-bsc/smscb.o \
 	$(top_builddir)/src/osmo-bsc/cbch_scheduler.o \
 	$(top_builddir)/src/osmo-bsc/cbsp_link.o \
+	$(top_builddir)/src/osmo-bsc/lcs_loc_req.o \
+	$(top_builddir)/src/osmo-bsc/lcs_ta_req.o \
+	$(top_builddir)/src/osmo-bsc/lb.o \
+	$(top_builddir)/src/osmo-bsc/bsc_sccp.o \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOCTRL_LIBS) \
diff --git a/tests/timer.vty b/tests/timer.vty
index ad1d167..6c26681 100644
--- a/tests/timer.vty
+++ b/tests/timer.vty
@@ -25,6 +25,7 @@
 net: X8 = 5 s	Timeout for RSL IPA MDCX ACK after sending RSL IPA MDCX (default: 5 s)
 net: X9 = 5 s	Timeout for availability of MGW endpoint (default: 5 s)
 net: X10 = 5 s	Timeout for fully configured MGW endpoint (default: 5 s)
+net: X11 = 5 s	Timeout for Perform Location Response from SMLC (default: 5 s)
 net: X3111 = 4 s	Wait time after lchan was released in error (should be T3111 + 2s) (default: 4 s)
 net: X3210 = 20 s	After L3 Complete, wait for MSC to confirm (default: 20 s)
 mgw: X2427 = 5 s	timeout for MGCP response from MGW (default: 5 s)
@@ -70,6 +71,7 @@
 net: X8 = 5 s	Timeout for RSL IPA MDCX ACK after sending RSL IPA MDCX (default: 5 s)
 net: X9 = 5 s	Timeout for availability of MGW endpoint (default: 5 s)
 net: X10 = 5 s	Timeout for fully configured MGW endpoint (default: 5 s)
+net: X11 = 5 s	Timeout for Perform Location Response from SMLC (default: 5 s)
 net: X3111 = 4 s	Wait time after lchan was released in error (should be T3111 + 2s) (default: 4 s)
 net: X3210 = 20 s	After L3 Complete, wait for MSC to confirm (default: 20 s)
 mgw: X2427 = 5 s	timeout for MGCP response from MGW (default: 5 s)

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

Gerrit-Project: osmo-bsc
Gerrit-Branch: master
Gerrit-Change-Id: I28314ba97df86a118497e9b2770e2e6e2484e872
Gerrit-Change-Number: 20357
Gerrit-PatchSet: 10
Gerrit-Owner: neels <nhofmeyr at sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge at osmocom.org>
Gerrit-Reviewer: neels <nhofmeyr at sysmocom.de>
Gerrit-Reviewer: pespin <pespin at sysmocom.de>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20201009/da71a600/attachment.htm>


More information about the gerrit-log mailing list