Change in osmo-smlc[master]: initial working osmo-smlc 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 gerrit-no-reply at lists.osmocom.org
Fri Oct 16 14:00:52 UTC 2020


neels has submitted this change. ( https://gerrit.osmocom.org/c/osmo-smlc/+/20470 )

Change subject: initial working osmo-smlc implementation
......................................................................

initial working osmo-smlc implementation

The lower level Lb/SCCP interface conn handling is essentially a copy of
OsmoMSC's A/SCCP infrastructure (OsmoMSC also connects to multiple BSCs).

The smlc_subscr is mostly a copy of OsmoBSC's bsc_subscr.

smlc_loc_req FSM is the SMLC side of OsmoBSC's new lcs_loc_req FSM.

cell_locations configures geographic coordinates of individual cells.

Change-Id: I917ba8fc51a1f1150be77ae01e12a7b16a853052
---
M configure.ac
M doc/examples/osmo-smlc/osmo-smlc.cfg
M include/osmocom/smlc/Makefile.am
A include/osmocom/smlc/cell_locations.h
A include/osmocom/smlc/debug.h
A include/osmocom/smlc/lb_conn.h
A include/osmocom/smlc/lb_peer.h
A include/osmocom/smlc/sccp_lb_inst.h
M include/osmocom/smlc/smlc_data.h
A include/osmocom/smlc/smlc_loc_req.h
M include/osmocom/smlc/smlc_sigtran.h
A include/osmocom/smlc/smlc_subscr.h
A include/osmocom/smlc/smlc_vty.h
M src/osmo-smlc/Makefile.am
A src/osmo-smlc/cell_locations.c
A src/osmo-smlc/lb_conn.c
A src/osmo-smlc/lb_peer.c
A src/osmo-smlc/sccp_lb_inst.c
A src/osmo-smlc/smlc_data.c
A src/osmo-smlc/smlc_loc_req.c
M src/osmo-smlc/smlc_main.c
D src/osmo-smlc/smlc_sigtran.c
A src/osmo-smlc/smlc_subscr.c
M tests/Makefile.am
A tests/cell_locations.vty
A tests/osmo-smlc.cfg
A tests/smlc_subscr/Makefile.am
A tests/smlc_subscr/smlc_subscr_test.c
A tests/smlc_subscr/smlc_subscr_test.err
A tests/smlc_subscr/smlc_subscr_test.ok
M tests/testsuite.at
31 files changed, 2,684 insertions(+), 114 deletions(-)

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



diff --git a/configure.ac b/configure.ac
index e9c4ffc..186721b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -212,6 +212,7 @@
     src/osmo-smlc/Makefile
     tests/Makefile
     tests/atlocal
+    tests/smlc_subscr/Makefile
     doc/Makefile
     doc/examples/Makefile
     doc/manuals/Makefile
diff --git a/doc/examples/osmo-smlc/osmo-smlc.cfg b/doc/examples/osmo-smlc/osmo-smlc.cfg
index e69de29..6585d47 100644
--- a/doc/examples/osmo-smlc/osmo-smlc.cfg
+++ b/doc/examples/osmo-smlc/osmo-smlc.cfg
@@ -0,0 +1,3 @@
+cells
+ lac-ci 23 42 lat 12.34567 lon 34.56789
+ cgi 262 42 17 5 lat 12.34765 lon 34.56987
diff --git a/include/osmocom/smlc/Makefile.am b/include/osmocom/smlc/Makefile.am
index 599651d..4933441 100644
--- a/include/osmocom/smlc/Makefile.am
+++ b/include/osmocom/smlc/Makefile.am
@@ -1,4 +1,12 @@
 noinst_HEADERS = \
+	cell_locations.h \
+	debug.h \
+	lb_conn.h \
+	lb_peer.h \
+	sccp_lb_inst.h \
 	smlc_data.h \
+	smlc_loc_req.h \
 	smlc_sigtran.h \
+	smlc_subscr.h \
+	smlc_vty.h \
 	$(NULL)
diff --git a/include/osmocom/smlc/cell_locations.h b/include/osmocom/smlc/cell_locations.h
new file mode 100644
index 0000000..33023bb
--- /dev/null
+++ b/include/osmocom/smlc/cell_locations.h
@@ -0,0 +1,49 @@
+/* OsmoSMLC cell locations configuration */
+/*
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/sigtran/sccp_sap.h>
+
+struct osmo_gad;
+
+struct cell_location {
+	struct llist_head entry;
+
+	struct gsm0808_cell_id cell_id;
+
+	/*! latitude in micro degrees (degrees * 1e6) */
+	int32_t lat;
+	/*! longitude in micro degrees (degrees * 1e6) */
+	int32_t lon;
+};
+
+int cell_location_from_ta(struct osmo_gad *location_estimate,
+			  const struct gsm0808_cell_id *cell_id,
+			  uint8_t ta);
+
+int cell_locations_vty_init();
diff --git a/include/osmocom/smlc/debug.h b/include/osmocom/smlc/debug.h
new file mode 100644
index 0000000..0c64323
--- /dev/null
+++ b/include/osmocom/smlc/debug.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#define DEBUG
+#include <osmocom/core/logging.h>
+
+/* Debug Areas of the code */
+enum {
+	DSMLC,
+	DREF,
+	DLB,
+	DLCS,
+	Debug_LastEntry,
+};
diff --git a/include/osmocom/smlc/lb_conn.h b/include/osmocom/smlc/lb_conn.h
new file mode 100644
index 0000000..5640780
--- /dev/null
+++ b/include/osmocom/smlc/lb_conn.h
@@ -0,0 +1,55 @@
+#pragma once
+/* SMLC Lb connection implementation */
+
+#include <stdint.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/smlc/smlc_subscr.h>
+
+struct lb_peer;
+struct osmo_fsm_inst;
+struct msgb;
+struct bssmap_le_pdu;
+
+#define LOG_LB_CONN_SL(CONN, CAT, LEVEL, file, line, FMT, args...) \
+	LOGPSRC(CAT, LEVEL, file, line, "Lb-%d %s %s: " FMT, (CONN) ? (CONN)->sccp_conn_id : 0, \
+	     ((CONN) && (CONN)->smlc_subscr) ? smlc_subscr_to_str_c(OTC_SELECT, (CONN)->smlc_subscr) : "no-subscr", \
+	     (CONN) ? osmo_use_count_to_str_c(OTC_SELECT, &(CONN)->use_count) : "-", \
+	     ##args)
+
+#define LOG_LB_CONN_S(CONN, CAT, LEVEL, FMT, args...) \
+	LOG_LB_CONN_SL(CONN, CAT, LEVEL, NULL, 0, FMT, ##args)
+
+#define LOG_LB_CONN(CONN, LEVEL, FMT, args...) \
+	LOG_LB_CONN_S(CONN, DLB, LEVEL, FMT, ##args)
+
+#define SMLC_SUBSCR_USE_LB_CONN "Lb-conn"
+
+struct lb_conn {
+	struct llist_head entry;
+	struct osmo_use_count use_count;
+
+	struct lb_peer *lb_peer;
+	uint32_t sccp_conn_id;
+
+	bool closing;
+
+	struct smlc_subscr *smlc_subscr;
+	struct smlc_loc_req *smlc_loc_req;
+};
+
+#define lb_conn_get(lb_conn, use) \
+	OSMO_ASSERT(osmo_use_count_get_put(&(lb_conn)->use_count, use, 1) == 0)
+#define lb_conn_put(lb_conn, use) \
+	OSMO_ASSERT(osmo_use_count_get_put(&(lb_conn)->use_count, use, -1) == 0)
+
+struct lb_conn *lb_conn_create_incoming(struct lb_peer *lb_peer, uint32_t sccp_conn_id, const char *use_token);
+struct lb_conn *lb_conn_create_outgoing(struct lb_peer *lb_peer, const char *use_token);
+struct lb_conn *lb_conn_find_by_smlc_subscr(struct smlc_subscr *smlc_subscr, const char *use_token);
+
+void lb_conn_msc_role_gone(struct lb_conn *lb_conn, struct osmo_fsm_inst *msc_role);
+void lb_conn_close(struct lb_conn *lb_conn);
+void lb_conn_discard(struct lb_conn *lb_conn);
+
+int lb_conn_rx(struct lb_conn *lb_conn, struct msgb *msg, bool initial);
+int lb_conn_send_bssmap_le(struct lb_conn *lb_conn, const struct bssmap_le_pdu *bssmap_le);
diff --git a/include/osmocom/smlc/lb_peer.h b/include/osmocom/smlc/lb_peer.h
new file mode 100644
index 0000000..b5ac6d6
--- /dev/null
+++ b/include/osmocom/smlc/lb_peer.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/gsm/gsm0808.h>
+#include <osmocom/sigtran/sccp_sap.h>
+
+#include <osmocom/smlc/debug.h>
+#include <osmocom/smlc/lb_conn.h>
+
+struct vlr_subscr;
+struct lb_conn;
+struct neighbor_ident_entry;
+
+#define LOG_LB_PEER_CAT(LB_PEER, subsys, loglevel, fmt, args ...) \
+	LOGPFSMSL((LB_PEER)? (LB_PEER)->fi : NULL, subsys, loglevel, fmt, ## args)
+
+#define LOG_LB_PEER(LB_PEER, loglevel, fmt, args ...) \
+	LOG_LB_PEER_CAT(LB_PEER, DLB, loglevel, fmt, ## args)
+
+struct lb_peer {
+	struct llist_head entry;
+	struct osmo_fsm_inst *fi;
+
+	struct sccp_lb_inst *sli;
+	struct osmo_sccp_addr peer_addr;
+};
+
+#define lb_peer_for_each_lb_conn(LB_CONN, LB_PEER) \
+	llist_for_each_entry(LB_CONN, &(LB_PEER)->sli->lb_conns, entry) \
+		if ((LB_CONN)->lb_peer == (LB_PEER))
+
+#define lb_peer_for_each_lb_conn_safe(LB_CONN, LB_CONN_NEXT, LB_PEER) \
+	llist_for_each_entry_safe(LB_CONN, LB_CONN_NEXT, &(LB_PEER)->sli->lb_conns, entry) \
+		if ((LB_CONN)->lb_peer == (LB_PEER))
+
+enum lb_peer_state {
+	LB_PEER_ST_WAIT_RX_RESET = 0,
+	LB_PEER_ST_WAIT_RX_RESET_ACK,
+	LB_PEER_ST_READY,
+	LB_PEER_ST_DISCARDING,
+};
+
+enum lb_peer_event {
+	LB_PEER_EV_MSG_UP_CL = 0,
+	LB_PEER_EV_MSG_UP_CO_INITIAL,
+	LB_PEER_EV_MSG_UP_CO,
+	LB_PEER_EV_MSG_DOWN_CL,
+	LB_PEER_EV_MSG_DOWN_CO_INITIAL,
+	LB_PEER_EV_MSG_DOWN_CO,
+	LB_PEER_EV_RX_RESET,
+	LB_PEER_EV_RX_RESET_ACK,
+	LB_PEER_EV_CONNECTION_SUCCESS,
+	LB_PEER_EV_CONNECTION_TIMEOUT,
+};
+
+struct lb_peer_ev_ctx {
+	uint32_t conn_id;
+	struct lb_conn *lb_conn;
+	struct msgb *msg;
+};
+
+struct lb_peer *lb_peer_find_or_create(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *peer_addr);
+struct lb_peer *lb_peer_find(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *peer_addr);
+
+int lb_peer_up_l2(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *calling_addr, bool co, uint32_t conn_id,
+		  struct msgb *l2);
+void lb_peer_disconnect(struct sccp_lb_inst *sli, uint32_t conn_id);
diff --git a/include/osmocom/smlc/sccp_lb_inst.h b/include/osmocom/smlc/sccp_lb_inst.h
new file mode 100644
index 0000000..525eac0
--- /dev/null
+++ b/include/osmocom/smlc/sccp_lb_inst.h
@@ -0,0 +1,63 @@
+/* Lb: BSSAP-LE/SCCP */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <osmocom/core/tdef.h>
+#include <osmocom/gsm/gsm_utils.h>
+#include <osmocom/gsm/gsm0808_utils.h>
+#include <osmocom/sigtran/sccp_sap.h>
+
+struct msgb;
+struct sccp_lb_inst;
+
+#define LOG_SCCP_LB_CO(sli, peer_addr, conn_id, level, fmt, args...) \
+	LOGP(DLB, level, "(Lb-%u%s%s) " fmt, \
+	     conn_id, peer_addr ? " from " : "", \
+	     peer_addr ? osmo_sccp_inst_addr_name((sli)->sccp, peer_addr) : "", \
+	     ## args)
+
+#define LOG_SCCP_LB_CL_CAT(sli, peer_addr, subsys, level, fmt, args...) \
+	LOGP(subsys, level, "(Lb%s%s) " fmt, \
+	     peer_addr ? " from " : "", \
+	     peer_addr ? osmo_sccp_inst_addr_name((sli)->sccp, peer_addr) : "", \
+	     ## args)
+
+#define LOG_SCCP_LB_CL(sli, peer_addr, level, fmt, args...) \
+	LOG_SCCP_LB_CL_CAT(sli, peer_addr, DLB, level, fmt, ##args)
+
+#define LOG_SCCP_LB_CAT(sli, subsys, level, fmt, args...) \
+	LOG_SCCP_LB_CL_CAT(sli, NULL, subsys, level, fmt, ##args)
+
+#define LOG_SCCP_LB(sli, level, fmt, args...) \
+	LOG_SCCP_LB_CL(sli, NULL, level, fmt, ##args)
+
+enum reset_msg_type {
+	SCCP_LB_MSG_NON_RESET = 0,
+	SCCP_LB_MSG_RESET,
+	SCCP_LB_MSG_RESET_ACK,
+};
+
+struct sccp_lb_inst {
+	struct osmo_sccp_instance *sccp;
+	struct osmo_sccp_user *scu;
+	struct osmo_sccp_addr local_sccp_addr;
+
+	struct llist_head lb_peers;
+	struct llist_head lb_conns;
+
+	void *user_data;
+};
+
+struct sccp_lb_inst *sccp_lb_init(void *talloc_ctx, struct osmo_sccp_instance *sccp, enum osmo_sccp_ssn ssn,
+				  const char *sccp_user_name);
+int sccp_lb_inst_next_conn_id();
+
+int sccp_lb_down_l2_co_initial(struct sccp_lb_inst *sli,
+			       const struct osmo_sccp_addr *called_addr,
+			       uint32_t conn_id, struct msgb *l2);
+int sccp_lb_down_l2_co(struct sccp_lb_inst *sli, uint32_t conn_id, struct msgb *l2);
+int sccp_lb_down_l2_cl(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *called_addr, struct msgb *l2);
+
+int sccp_lb_disconnect(struct sccp_lb_inst *sli, uint32_t conn_id, uint32_t cause);
diff --git a/include/osmocom/smlc/smlc_data.h b/include/osmocom/smlc/smlc_data.h
index 2506fdc..dc77507 100644
--- a/include/osmocom/smlc/smlc_data.h
+++ b/include/osmocom/smlc/smlc_data.h
@@ -8,25 +8,26 @@
 
 #include <osmocom/ctrl/control_if.h>
 
-#include <osmocom/sigtran/sccp_sap.h>
+struct osmo_sccp_instance;
+struct sccp_lb_inst;
 
 struct smlc_state {
-	struct osmo_sccp_user *sccp_user;
+	struct osmo_sccp_instance *sccp_inst;
+	struct sccp_lb_inst *lb;
+
 	struct ctrl_handle *ctrl;
 
 	struct rate_ctr_group *ctrs;
 	struct osmo_stat_item_group *statg;
-	struct osmo_tdef *T_defs;
+
+	struct llist_head subscribers;
+	struct llist_head cell_locations;
 };
 
 extern struct smlc_state *g_smlc;
+struct smlc_state *smlc_state_alloc(void *ctx);
 
-
-enum {
-	DSMLC,
-	DLB,		/* Lb interface */
-};
-
+extern struct osmo_tdef g_smlc_tdefs[];
 
 int smlc_ctrl_node_lookup(void *data, vector vline, int *node_type,
 			  void **node_data, int *i);
@@ -35,3 +36,25 @@
 	CTRL_NODE_SMLC = _LAST_CTRL_NODE,
 	_LAST_CTRL_NODE_SMLC
 };
+
+enum {
+	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_REQUEST,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_TA_RESPONSE,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_REJECT,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_RESET,
+	SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_ABORT,
+
+	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_RESPONSE,
+	SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_TA_REQUEST,
+};
diff --git a/include/osmocom/smlc/smlc_loc_req.h b/include/osmocom/smlc/smlc_loc_req.h
new file mode 100644
index 0000000..a8aa27e
--- /dev/null
+++ b/include/osmocom/smlc/smlc_loc_req.h
@@ -0,0 +1,65 @@
+/* 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>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+#pragma once
+
+#include <osmocom/smlc/debug.h>
+#include <osmocom/gsm/bssmap_le.h>
+
+#define LOG_SMLC_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 smlc_ta_req;
+struct lb_conn;
+struct msgb;
+
+#define LB_CONN_USE_SMLC_LOC_REQ "smlc_loc_req"
+
+enum smlc_loc_req_fsm_event {
+	SMLC_LOC_REQ_EV_RX_TA_RESPONSE,
+	SMLC_LOC_REQ_EV_RX_BSSLAP_RESET,
+	SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT,
+};
+
+struct smlc_loc_req {
+	struct osmo_fsm_inst *fi;
+
+	struct smlc_subscr *smlc_subscr;
+	struct lb_conn *lb_conn;
+
+	struct bssmap_le_perform_loc_req req;
+
+	bool ta_present;
+	uint8_t ta;
+
+	struct gsm0808_cell_id latest_cell_id;
+
+	struct lcs_cause_ie lcs_cause;
+};
+
+int smlc_loc_req_rx_bssap_le(struct lb_conn *conn, const struct bssap_le_pdu *bssap_le);
diff --git a/include/osmocom/smlc/smlc_sigtran.h b/include/osmocom/smlc/smlc_sigtran.h
index aeaf2c5..f89f284 100644
--- a/include/osmocom/smlc/smlc_sigtran.h
+++ b/include/osmocom/smlc/smlc_sigtran.h
@@ -1,3 +1,5 @@
 #pragma once
 
 int smlc_sigtran_init(void);
+int smlc_sigtran_send(uint32_t sccp_conn_id, struct msgb *msg);
+int smlc_sigtran_send_udt(uint32_t sccp_conn_id, struct msgb *msg);
diff --git a/include/osmocom/smlc/smlc_subscr.h b/include/osmocom/smlc/smlc_subscr.h
new file mode 100644
index 0000000..fbc4103
--- /dev/null
+++ b/include/osmocom/smlc/smlc_subscr.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/use_count.h>
+#include <osmocom/gsm/gsm48.h>
+#include <osmocom/gsm/gsm0808.h>
+
+struct smlc_subscr {
+	struct llist_head entry;
+	struct osmo_use_count use_count;
+
+	struct osmo_mobile_identity imsi;
+	struct gsm0808_cell_id cell_id;
+
+	struct osmo_fsm_inst *loc_req;
+};
+
+struct smlc_subscr *smlc_subscr_find_or_create(const struct osmo_mobile_identity *imsi, const char *use_token);
+struct smlc_subscr *smlc_subscr_find(const struct osmo_mobile_identity *imsi, const char *use_token);
+
+int smlc_subscr_to_str_buf(char *buf, size_t buf_len, const struct smlc_subscr *smlc_subscr);
+char *smlc_subscr_to_str_c(void *ctx, const struct smlc_subscr *smlc_subscr);
+
+struct smlc_subscr *smlc_subscr_find_or_create(const struct osmo_mobile_identity *imsi, const char *use_token);
+
+#define smlc_subscr_get(smlc_subscr, use) \
+	OSMO_ASSERT(osmo_use_count_get_put(&(smlc_subscr)->use_count, use, 1) == 0)
+#define smlc_subscr_put(smlc_subscr, use) \
+	OSMO_ASSERT(osmo_use_count_get_put(&(smlc_subscr)->use_count, use, -1) == 0)
diff --git a/include/osmocom/smlc/smlc_vty.h b/include/osmocom/smlc/smlc_vty.h
new file mode 100644
index 0000000..d5d82f8
--- /dev/null
+++ b/include/osmocom/smlc/smlc_vty.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include <osmocom/vty/command.h>
+
+enum smlc_vty_node {
+	CELLS_NODE = _LAST_OSMOVTY_NODE + 1,
+};
diff --git a/src/osmo-smlc/Makefile.am b/src/osmo-smlc/Makefile.am
index 269c23d..37fe042 100644
--- a/src/osmo-smlc/Makefile.am
+++ b/src/osmo-smlc/Makefile.am
@@ -23,9 +23,15 @@
 	$(NULL)
 
 osmo_smlc_SOURCES = \
+	cell_locations.c \
+	lb_conn.c \
+	lb_peer.c \
+	sccp_lb_inst.c \
 	smlc_ctrl.c \
+	smlc_data.c \
+	smlc_loc_req.c \
 	smlc_main.c \
-	smlc_sigtran.c \
+	smlc_subscr.c \
 	$(NULL)
 
 osmo_smlc_LDADD = \
diff --git a/src/osmo-smlc/cell_locations.c b/src/osmo-smlc/cell_locations.c
new file mode 100644
index 0000000..e563720
--- /dev/null
+++ b/src/osmo-smlc/cell_locations.c
@@ -0,0 +1,318 @@
+/* OsmoSMLC cell locations configuration */
+/*
+ * (C) 2020 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <limits.h>
+#include <inttypes.h>
+#include <errno.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/protocol/gsm_08_08.h>
+#include <osmocom/gsm/gsm0808_utils.h>
+#include <osmocom/gsm/gad.h>
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/smlc_vty.h>
+#include <osmocom/smlc/cell_locations.h>
+
+static uint32_t ta_to_m(uint8_t ta)
+{
+	return ((uint32_t)ta) * 550;
+}
+
+static struct cell_location *cell_location_find(const struct gsm0808_cell_id *cell_id)
+{
+	struct cell_location *cell_location;
+	llist_for_each_entry(cell_location, &g_smlc->cell_locations, entry) {
+		if (gsm0808_cell_ids_match(&cell_location->cell_id, cell_id, true))
+			return cell_location;
+	}
+	llist_for_each_entry(cell_location, &g_smlc->cell_locations, entry) {
+		if (gsm0808_cell_ids_match(&cell_location->cell_id, cell_id, false))
+			return cell_location;
+	}
+	return NULL;
+}
+
+int cell_location_from_ta(struct osmo_gad *location_estimate,
+			  const struct gsm0808_cell_id *cell_id,
+			  uint8_t ta)
+{
+	const struct cell_location *cell;
+	cell = cell_location_find(cell_id);
+	if (!cell)
+		return -ENOENT;
+
+	*location_estimate = (struct osmo_gad){
+		.type = GAD_TYPE_ELL_POINT_UNC_CIRCLE,
+		.ell_point_unc_circle = {
+			.lat = cell->lat,
+			.lon = cell->lon,
+			.unc = osmo_gad_dec_unc(osmo_gad_enc_unc(ta_to_m(ta) * 1000)),
+		},
+	};
+
+	return 0;
+}
+
+static struct cell_location *cell_location_find_or_create(const struct gsm0808_cell_id *cell_id)
+{
+	struct cell_location *cell_location = cell_location_find(cell_id);
+	if (!cell_location) {
+		cell_location = talloc_zero(g_smlc, struct cell_location);
+		OSMO_ASSERT(cell_location);
+		cell_location->cell_id = *cell_id;
+		llist_add_tail(&cell_location->entry, &g_smlc->cell_locations);
+	}
+	return cell_location;
+
+}
+
+static const struct cell_location *cell_location_set(const struct gsm0808_cell_id *cell_id, int32_t lat, int32_t lon)
+{
+	struct cell_location *cell_location = cell_location_find_or_create(cell_id);
+	cell_location->lat = lat;
+	cell_location->lon = lon;
+	return 0;
+}
+
+static int cell_location_remove(const struct gsm0808_cell_id *cell_id)
+{
+	struct cell_location *cell_location = cell_location_find(cell_id);
+	if (!cell_location)
+		return -ENOENT;
+	llist_del(&cell_location->entry);
+	talloc_free(cell_location);
+	return 0;
+}
+
+#define LAC_CI_PARAMS "lac-ci <0-65535> <0-65535>"
+#define LAC_CI_DOC "Cell location by LAC and CI\n" "LAC\n" "CI\n"
+
+#define CGI_PARAMS "cgi <0-999> <0-999> <0-65535> <0-65535>"
+#define CGI_DOC "Cell location by Cell-Global ID\n" "MCC\n" "MNC\n" "LAC\n" "CI\n"
+
+#define LAT_LON_PARAMS "lat LATITUDE lon LONGITUDE"
+#define LAT_LON_DOC "Global latitute coordinate\n" "Latitude floating-point number, -90.0 (S) to 90.0 (N)\n" \
+		"Global longitude coordinate\n" "Longitude as floating-point number, -180.0 (W) to 180.0 (E)\n"
+
+static int vty_parse_lac_ci(struct vty *vty, struct gsm0808_cell_id *dst, const char **argv)
+{
+	*dst = (struct gsm0808_cell_id){
+		.id_discr = CELL_IDENT_LAC_AND_CI,
+		.id.lac_and_ci = {
+			.lac = atoi(argv[0]),
+			.ci = atoi(argv[1]),
+		},
+	};
+	return 0;
+}
+
+static int vty_parse_cgi(struct vty *vty, struct gsm0808_cell_id *dst, const char **argv)
+{
+	*dst = (struct gsm0808_cell_id){
+		.id_discr = CELL_IDENT_WHOLE_GLOBAL,
+	};
+	struct osmo_cell_global_id *cgi = &dst->id.global;
+	const char *mcc = argv[0];
+	const char *mnc = argv[1];
+	const char *lac = argv[2];
+	const char *ci = argv[3];
+
+	if (osmo_mcc_from_str(mcc, &cgi->lai.plmn.mcc)) {
+		vty_out(vty, "%% Error decoding MCC: %s%s", mcc, VTY_NEWLINE);
+		return -EINVAL;
+	}
+
+	if (osmo_mnc_from_str(mnc, &cgi->lai.plmn.mnc, &cgi->lai.plmn.mnc_3_digits)) {
+		vty_out(vty, "%% Error decoding MNC: %s%s", mnc, VTY_NEWLINE);
+		return -EINVAL;
+	}
+
+	cgi->lai.lac = atoi(lac);
+	cgi->cell_identity = atoi(ci);
+	return 0;
+}
+
+static int vty_parse_location(struct vty *vty, const struct gsm0808_cell_id *cell_id, const char **argv)
+{
+	const char *lat_str = argv[0];
+	const char *lon_str = argv[1];
+	int64_t val;
+	int32_t lat, lon;
+
+	if (osmo_float_str_to_int(&val, lat_str, 6)
+	    || val < -90000000 || val > 90000000) {
+		vty_out(vty, "%% Invalid latitude: '%s'%s", lat_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	lat = val;
+
+	if (osmo_float_str_to_int(&val, lon_str, 6)
+	    || val < -180000000 || val > 180000000) {
+		vty_out(vty, "%% Invalid longitude: '%s'%s", lon_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	lon = val;
+
+	if (cell_location_set(cell_id, lat, lon)) {
+		vty_out(vty, "%% Failed to add cell location%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cells, cfg_cells_cmd,
+      "cells",
+      "Configure cell locations\n")
+{
+	vty->node = CELLS_NODE;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cells_lac_ci, cfg_cells_lac_ci_cmd,
+      LAC_CI_PARAMS " " LAT_LON_PARAMS,
+      LAC_CI_DOC LAT_LON_DOC)
+{
+	struct gsm0808_cell_id cell_id;
+
+	if (vty_parse_lac_ci(vty, &cell_id, argv))
+		return CMD_WARNING;
+
+	return vty_parse_location(vty, &cell_id, argv + 2);
+}
+
+DEFUN(cfg_cells_no_lac_ci, cfg_cells_no_lac_ci_cmd,
+      "no " LAC_CI_PARAMS,
+      NO_STR "Remove " LAC_CI_DOC)
+{
+	struct gsm0808_cell_id cell_id;
+
+	if (vty_parse_lac_ci(vty, &cell_id, argv))
+		return CMD_WARNING;
+	if (cell_location_remove(&cell_id)) {
+		vty_out(vty, "%% cannot remove, no such entry%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cells_cgi, cfg_cells_cgi_cmd,
+      CGI_PARAMS " " LAT_LON_PARAMS,
+      CGI_DOC LAT_LON_DOC)
+{
+	struct gsm0808_cell_id cell_id;
+
+	if (vty_parse_cgi(vty, &cell_id, argv))
+		return CMD_WARNING;
+
+	return vty_parse_location(vty, &cell_id, argv + 4);
+}
+
+DEFUN(cfg_cells_no_cgi, cfg_cells_no_cgi_cmd,
+      "no " CGI_PARAMS,
+      NO_STR "Remove " CGI_DOC)
+{
+	struct gsm0808_cell_id cell_id;
+
+	if (vty_parse_cgi(vty, &cell_id, argv))
+		return CMD_WARNING;
+	if (cell_location_remove(&cell_id)) {
+		vty_out(vty, "%% cannot remove, no such entry%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+/* The above are omnidirectional cells. If we add configuration sector antennae, it would add arguments to the above,
+ * something like this:
+ *  cgi 001 01 23 42 lat 23.23 lon 42.42 arc 270 30
+ */
+
+struct cmd_node cells_node = {
+	CELLS_NODE,
+	"%s(config-cells)# ",
+	1,
+};
+
+static int config_write_cells(struct vty *vty)
+{
+	struct cell_location *cell;
+	const struct osmo_cell_global_id *cgi;
+
+	if (llist_empty(&g_smlc->cell_locations))
+		return 0;
+
+	vty_out(vty, "cells%s", VTY_NEWLINE);
+
+	llist_for_each_entry(cell, &g_smlc->cell_locations, entry) {
+		switch (cell->cell_id.id_discr) {
+		case CELL_IDENT_LAC_AND_CI:
+			vty_out(vty, " lac-ci %u %u", cell->cell_id.id.lac_and_ci.lac, cell->cell_id.id.lac_and_ci.ci);
+			break;
+		case CELL_IDENT_WHOLE_GLOBAL:
+			cgi = &cell->cell_id.id.global;
+			vty_out(vty, " cgi %s %s %u %u",
+				osmo_mcc_name(cgi->lai.plmn.mcc),
+				osmo_mnc_name(cgi->lai.plmn.mnc, cgi->lai.plmn.mnc_3_digits),
+				cgi->lai.lac, cgi->cell_identity);
+			break;
+		default:
+			vty_out(vty, " %% [unsupported cell id type: %d]",
+				cell->cell_id.id_discr);
+			break;
+		}
+
+		vty_out(vty, " lat %s lon %s%s",
+			osmo_int_to_float_str_c(OTC_SELECT, cell->lat, 6),
+			osmo_int_to_float_str_c(OTC_SELECT, cell->lon, 6),
+			VTY_NEWLINE);
+	}
+
+	return 0;
+}
+
+DEFUN(ve_show_cells, ve_show_cells_cmd,
+      "show cells",
+      SHOW_STR "Show configured cell locations\n")
+{
+	if (llist_empty(&g_smlc->cell_locations)) {
+		vty_out(vty, "%% No cell locations are configured%s", VTY_NEWLINE);
+		return CMD_SUCCESS;
+	}
+	config_write_cells(vty);
+	return CMD_SUCCESS;
+}
+
+int cell_locations_vty_init()
+{
+	install_element(CONFIG_NODE, &cfg_cells_cmd);
+	install_node(&cells_node, config_write_cells);
+	install_element(CELLS_NODE, &cfg_cells_lac_ci_cmd);
+	install_element(CELLS_NODE, &cfg_cells_no_lac_ci_cmd);
+	install_element(CELLS_NODE, &cfg_cells_cgi_cmd);
+	install_element(CELLS_NODE, &cfg_cells_no_cgi_cmd);
+	install_element_ve(&ve_show_cells_cmd);
+
+	return 0;
+}
diff --git a/src/osmo-smlc/lb_conn.c b/src/osmo-smlc/lb_conn.c
new file mode 100644
index 0000000..7e79de0
--- /dev/null
+++ b/src/osmo-smlc/lb_conn.c
@@ -0,0 +1,198 @@
+/* SMLC Lb connection implementation */
+
+/*
+ * (C) 2020 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 <errno.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/signal.h>
+#include <osmocom/gsm/bssmap_le.h>
+
+#include <osmocom/smlc/debug.h>
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/sccp_lb_inst.h>
+#include <osmocom/smlc/lb_peer.h>
+#include <osmocom/smlc/lb_conn.h>
+#include <osmocom/smlc/smlc_loc_req.h>
+
+static int lb_conn_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line)
+{
+	struct lb_conn *lb_conn = e->use_count->talloc_object;
+	int32_t total;
+	int level;
+
+	if (!e->use)
+		return -EINVAL;
+
+	total = osmo_use_count_total(&lb_conn->use_count);
+
+	if (total == 0
+	    || (total == 1 && old_use_count == 0 && e->count == 1))
+		level = LOGL_INFO;
+	else
+		level = LOGL_DEBUG;
+
+	LOG_LB_CONN_SL(lb_conn, DREF, level, file, line, "%s %s: now used by %s\n",
+		(e->count - old_use_count) > 0? "+" : "-", e->use,
+		osmo_use_count_to_str_c(OTC_SELECT, &lb_conn->use_count));
+
+	if (e->count < 0)
+		return -ERANGE;
+
+	if (total == 0)
+		lb_conn_close(lb_conn);
+	return 0;
+}
+
+static struct lb_conn *lb_conn_alloc(struct lb_peer *lb_peer, uint32_t sccp_conn_id, const char *use_token)
+{
+	struct lb_conn *lb_conn;
+
+	lb_conn = talloc(lb_peer, struct lb_conn);
+	OSMO_ASSERT(lb_conn);
+
+	*lb_conn = (struct lb_conn){
+		.lb_peer = lb_peer,
+		.sccp_conn_id = sccp_conn_id,
+		.use_count = {
+			.talloc_object = lb_conn,
+			.use_cb = lb_conn_use_cb,
+		},
+	};
+
+	llist_add(&lb_conn->entry, &lb_peer->sli->lb_conns);
+	lb_conn_get(lb_conn, use_token);
+	return lb_conn;
+}
+
+struct lb_conn *lb_conn_create_incoming(struct lb_peer *lb_peer, uint32_t sccp_conn_id, const char *use_token)
+{
+	LOG_LB_PEER(lb_peer, LOGL_DEBUG, "Incoming lb_conn id: %u\n", sccp_conn_id);
+	return lb_conn_alloc(lb_peer, sccp_conn_id, use_token);
+}
+
+struct lb_conn *lb_conn_create_outgoing(struct lb_peer *lb_peer, const char *use_token)
+{
+	int new_conn_id = sccp_lb_inst_next_conn_id();
+	if (new_conn_id < 0)
+		return NULL;
+	LOG_LB_PEER(lb_peer, LOGL_DEBUG, "Outgoing lb_conn id: %u\n", new_conn_id);
+	return lb_conn_alloc(lb_peer, new_conn_id, use_token);
+}
+
+struct lb_conn *lb_conn_find_by_smlc_subscr(struct smlc_subscr *smlc_subscr, const char *use_token)
+{
+	struct lb_conn *lb_conn;
+	llist_for_each_entry(lb_conn, &g_smlc->lb->lb_conns, entry) {
+		if (lb_conn->smlc_subscr == smlc_subscr) {
+			lb_conn_get(lb_conn, use_token);
+			return lb_conn;
+		}
+	}
+	return NULL;
+}
+
+int lb_conn_down_l2_co(struct lb_conn *lb_conn, struct msgb *l3, bool initial)
+{
+	struct lb_peer_ev_ctx co = {
+		.conn_id = lb_conn->sccp_conn_id,
+		.lb_conn = lb_conn,
+		.msg = l3,
+	};
+	if (!lb_conn->lb_peer)
+		return -EIO;
+	return osmo_fsm_inst_dispatch(lb_conn->lb_peer->fi,
+				      initial ? LB_PEER_EV_MSG_DOWN_CO_INITIAL : LB_PEER_EV_MSG_DOWN_CO,
+				      &co);
+}
+
+int lb_conn_rx(struct lb_conn *lb_conn, struct msgb *msg, bool initial)
+{
+	struct bssap_le_pdu bssap_le;
+	struct osmo_bssap_le_err *err;
+	if (osmo_bssap_le_dec(&bssap_le, &err, msg, msg)) {
+		LOG_LB_CONN(lb_conn, LOGL_ERROR, "Rx BSSAP-LE with error: %s\n", err->logmsg);
+		return -EINVAL;
+	}
+
+	return smlc_loc_req_rx_bssap_le(lb_conn, &bssap_le);
+}
+
+int lb_conn_send_bssmap_le(struct lb_conn *lb_conn, const struct bssmap_le_pdu *bssmap_le)
+{
+	struct msgb *msg;
+	int rc;
+	struct bssap_le_pdu bssap_le = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = *bssmap_le,
+	};
+
+	msg = osmo_bssap_le_enc(&bssap_le);
+	if (!msg) {
+		LOG_LB_CONN(lb_conn, LOGL_ERROR, "Unable to encode %s\n",
+			    osmo_bssap_le_pdu_to_str_c(OTC_SELECT, &bssap_le));
+		return -EINVAL;
+	}
+	rc = lb_conn_down_l2_co(lb_conn, msg, false);
+	msgb_free(msg);
+	if (rc)
+		LOG_LB_CONN(lb_conn, LOGL_ERROR, "Unable to send %s\n",
+			    osmo_bssap_le_pdu_to_str_c(OTC_SELECT, &bssap_le));
+	return rc;
+}
+
+/* Regularly close the lb_conn */
+void lb_conn_close(struct lb_conn *lb_conn)
+{
+	if (!lb_conn)
+		return;
+	if (lb_conn->closing)
+		return;
+	lb_conn->closing = true;
+	LOG_LB_PEER(lb_conn->lb_peer, LOGL_DEBUG, "Closing lb_conn\n");
+
+	if (lb_conn->lb_peer) {
+		/* Todo: pass a useful SCCP cause? */
+		sccp_lb_disconnect(lb_conn->lb_peer->sli, lb_conn->sccp_conn_id, 0);
+		lb_conn->lb_peer = NULL;
+	}
+
+	if (lb_conn->smlc_loc_req)
+		osmo_fsm_inst_term(lb_conn->smlc_loc_req->fi, OSMO_FSM_TERM_REGULAR, NULL);
+
+	if (lb_conn->smlc_subscr)
+		smlc_subscr_put(lb_conn->smlc_subscr, SMLC_SUBSCR_USE_LB_CONN);
+
+	llist_del(&lb_conn->entry);
+	talloc_free(lb_conn);
+}
+
+/* Same as lb_conn_close() but without sending any SCCP messages (e.g. after RESET) */
+void lb_conn_discard(struct lb_conn *lb_conn)
+{
+	if (!lb_conn)
+		return;
+	/* Make sure to drop dead and don't dispatch things like DISCONNECT requests on SCCP. */
+	lb_conn->lb_peer = NULL;
+	lb_conn_close(lb_conn);
+}
diff --git a/src/osmo-smlc/lb_peer.c b/src/osmo-smlc/lb_peer.c
new file mode 100644
index 0000000..986ab44
--- /dev/null
+++ b/src/osmo-smlc/lb_peer.c
@@ -0,0 +1,495 @@
+/*
+ * (C) 2019 by sysmocom - s.m.f.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0+
+ *
+ * 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/linuxlist.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/fsm.h>
+#include <osmocom/gsm/bssmap_le.h>
+#include <osmocom/sigtran/sccp_helpers.h>
+
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/sccp_lb_inst.h>
+#include <osmocom/smlc/lb_peer.h>
+
+static struct osmo_fsm lb_peer_fsm;
+
+static __attribute__((constructor)) void lb_peer_init()
+{
+	OSMO_ASSERT( osmo_fsm_register(&lb_peer_fsm) == 0);
+}
+
+static struct lb_peer *lb_peer_alloc(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *peer_addr)
+{
+	struct lb_peer *lbp;
+	struct osmo_fsm_inst *fi;
+
+	fi = osmo_fsm_inst_alloc(&lb_peer_fsm, sli, NULL, LOGL_DEBUG, NULL);
+	OSMO_ASSERT(fi);
+
+	osmo_fsm_inst_update_id(fi, osmo_sccp_addr_to_id_c(OTC_SELECT, osmo_sccp_get_ss7(sli->sccp), peer_addr));
+
+	lbp = talloc_zero(fi, struct lb_peer);
+	OSMO_ASSERT(lbp);
+	*lbp = (struct lb_peer){
+		.fi = fi,
+		.sli = sli,
+		.peer_addr = *peer_addr,
+	};
+	fi->priv = lbp;
+
+	llist_add(&lbp->entry, &sli->lb_peers);
+
+	return lbp;
+}
+
+struct lb_peer *lb_peer_find_or_create(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *peer_addr)
+{
+	struct lb_peer *lbp = lb_peer_find(sli, peer_addr);
+	if (lbp)
+		return lbp;
+	return lb_peer_alloc(sli, peer_addr);
+}
+
+struct lb_peer *lb_peer_find(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *peer_addr)
+{
+	struct lb_peer *lbp;
+	llist_for_each_entry(lbp, &sli->lb_peers, entry) {
+		if (osmo_sccp_addr_ri_cmp(peer_addr, &lbp->peer_addr))
+			continue;
+		return lbp;
+	}
+	return NULL;
+}
+
+static const struct osmo_tdef_state_timeout lb_peer_fsm_timeouts[32] = {
+	[LB_PEER_ST_WAIT_RX_RESET_ACK] = { .T = -13 },
+	[LB_PEER_ST_DISCARDING] = { .T = -14 },
+};
+
+#define lb_peer_state_chg(LB_PEER, NEXT_STATE) \
+	osmo_tdef_fsm_inst_state_chg((LB_PEER)->fi, NEXT_STATE, lb_peer_fsm_timeouts, g_smlc_tdefs, 5)
+
+void lb_peer_discard_all_conns(struct lb_peer *lbp)
+{
+	struct lb_conn *lb_conn, *next;
+
+	lb_peer_for_each_lb_conn_safe(lb_conn, next, lbp) {
+		lb_conn_discard(lb_conn);
+	}
+}
+
+/* Drop all SCCP connections for this lb_peer, respond with RESET ACKNOWLEDGE and move to READY state. */
+static void lb_peer_rx_reset(struct lb_peer *lbp, struct msgb *msg)
+{
+	struct msgb *resp;
+	struct bssap_le_pdu reset_ack = {
+		.discr = BSSAP_LE_MSG_DISCR_BSSMAP_LE,
+		.bssmap_le = {
+			.msg_type = BSSMAP_LE_MSGT_RESET_ACK,
+		},
+	};
+
+	lb_peer_discard_all_conns(lbp);
+
+	resp = osmo_bssap_le_enc(&reset_ack);
+	if (!resp) {
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Failed to compose RESET ACKNOWLEDGE message\n");
+		lb_peer_state_chg(lbp, LB_PEER_ST_WAIT_RX_RESET);
+		return;
+	}
+
+	if (sccp_lb_down_l2_cl(lbp->sli, &lbp->peer_addr, resp)) {
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Failed to send RESET ACKNOWLEDGE message\n");
+		lb_peer_state_chg(lbp, LB_PEER_ST_WAIT_RX_RESET);
+		msgb_free(msg);
+		return;
+	}
+
+	LOG_LB_PEER(lbp, LOGL_INFO, "Sent RESET ACKNOWLEDGE\n");
+
+	/* sccp_lb_down_l2_cl() doesn't free msgb */
+	msgb_free(resp);
+
+	lb_peer_state_chg(lbp, LB_PEER_ST_READY);
+}
+
+static void lb_peer_rx_reset_ack(struct lb_peer *lbp, struct msgb* msg)
+{
+	lb_peer_state_chg(lbp, LB_PEER_ST_READY);
+}
+
+void lb_peer_reset(struct lb_peer *lbp)
+{
+	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,
+		},
+	};
+	struct msgb *msg;
+	int rc;
+
+	lb_peer_state_chg(lbp, LB_PEER_ST_WAIT_RX_RESET_ACK);
+	lb_peer_discard_all_conns(lbp);
+
+	msg = osmo_bssap_le_enc(&reset);
+	if (!msg) {
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Failed to compose RESET message\n");
+		lb_peer_state_chg(lbp, LB_PEER_ST_WAIT_RX_RESET);
+		return;
+	}
+
+	rc = sccp_lb_down_l2_cl(lbp->sli, &lbp->peer_addr, msg);
+	msgb_free(msg);
+	if (rc) {
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Failed to send RESET message\n");
+		lb_peer_state_chg(lbp, LB_PEER_ST_WAIT_RX_RESET);
+	}
+}
+
+void lb_peer_allstate_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct lb_peer *lbp = fi->priv;
+	struct lb_peer_ev_ctx *ctx = data;
+	struct msgb *msg = ctx->msg;
+	enum bssmap_le_msgt msg_type;
+
+	switch (event) {
+	case LB_PEER_EV_MSG_UP_CL:
+		msg_type = osmo_bssmap_le_msgt(msgb_l2(msg), msgb_l2len(msg));
+		switch (msg_type) {
+		case BSSMAP_LE_MSGT_RESET:
+			osmo_fsm_inst_dispatch(fi, LB_PEER_EV_RX_RESET, msg);
+			return;
+		case BSSMAP_LE_MSGT_RESET_ACK:
+			osmo_fsm_inst_dispatch(fi, LB_PEER_EV_RX_RESET_ACK, msg);
+			return;
+		default:
+			LOG_LB_PEER(lbp, LOGL_ERROR, "Unhandled ConnectionLess message received: %s\n",
+				    osmo_bssmap_le_msgt_name(msg_type));
+			return;
+		}
+
+	default:
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&lb_peer_fsm, event));
+		return;
+	}
+}
+
+void lb_peer_st_wait_rx_reset(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct lb_peer *lbp = fi->priv;
+	struct lb_peer_ev_ctx *ctx;
+	struct msgb *msg;
+
+	switch (event) {
+
+	case LB_PEER_EV_MSG_UP_CO:
+	case LB_PEER_EV_MSG_UP_CO_INITIAL:
+		ctx = data;
+		OSMO_ASSERT(ctx);
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Receiving CO message on Lb peer that has not done a proper RESET yet."
+			     " Disconnecting on incoming message, sending RESET to Lb peer.\n");
+		/* No valid RESET procedure has happened here yet. Usually, we're expecting the Lb peer (BSC,
+		 * RNC) to first send a RESET message before sending Connection Oriented messages. So if we're
+		 * getting a CO message, likely we've just restarted or something. Send a RESET to the peer. */
+
+		lb_peer_disconnect(lbp->sli, ctx->conn_id);
+
+		lb_peer_reset(lbp);
+		return;
+
+	case LB_PEER_EV_RX_RESET:
+		msg = (struct msgb*)data;
+		lb_peer_rx_reset(lbp, msg);
+		return;
+
+	default:
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&lb_peer_fsm, event));
+		return;
+	}
+}
+
+void lb_peer_st_wait_rx_reset_ack(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct lb_peer *lbp = fi->priv;
+	struct lb_peer_ev_ctx *ctx;
+	struct msgb *msg;
+
+	switch (event) {
+
+	case LB_PEER_EV_RX_RESET_ACK:
+		msg = (struct msgb*)data;
+		lb_peer_rx_reset_ack(lbp, msg);
+		return;
+
+	case LB_PEER_EV_MSG_UP_CO:
+	case LB_PEER_EV_MSG_UP_CO_INITIAL:
+		ctx = data;
+		OSMO_ASSERT(ctx);
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Receiving CO message on Lb peer that has not done a proper RESET yet."
+			     " Disconnecting on incoming message, sending RESET to Lb peer.\n");
+		sccp_lb_disconnect(lbp->sli, ctx->conn_id, 0);
+		/* No valid RESET procedure has happened here yet. */
+		lb_peer_reset(lbp);
+		return;
+
+	case LB_PEER_EV_RX_RESET:
+		msg = (struct msgb*)data;
+		lb_peer_rx_reset(lbp, msg);
+		return;
+
+	default:
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&lb_peer_fsm, event));
+		return;
+	}
+}
+
+void lb_peer_st_ready(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct lb_peer *lbp = fi->priv;
+	struct lb_peer_ev_ctx *ctx;
+	struct lb_conn *lb_conn;
+	struct msgb *msg;
+
+	switch (event) {
+
+	case LB_PEER_EV_MSG_UP_CO_INITIAL:
+		ctx = data;
+		OSMO_ASSERT(ctx);
+		OSMO_ASSERT(!ctx->lb_conn);
+		OSMO_ASSERT(ctx->msg);
+
+		lb_conn = lb_conn_create_incoming(lbp, ctx->conn_id, __func__);
+		if (!lb_conn) {
+			LOG_LB_PEER(lbp, LOGL_ERROR, "Cannot allocate lb_conn\n");
+			return;
+		}
+
+		lb_conn_rx(lb_conn, ctx->msg, true);
+		lb_conn_put(lb_conn, __func__);
+		return;
+
+	case LB_PEER_EV_MSG_UP_CO:
+		ctx = data;
+		OSMO_ASSERT(ctx);
+		OSMO_ASSERT(ctx->lb_conn);
+		OSMO_ASSERT(ctx->msg);
+
+		lb_conn_rx(ctx->lb_conn, ctx->msg, false);
+		return;
+
+	case LB_PEER_EV_MSG_DOWN_CO_INITIAL:
+		ctx = data;
+		OSMO_ASSERT(ctx);
+		OSMO_ASSERT(ctx->msg);
+		sccp_lb_down_l2_co_initial(lbp->sli, &lbp->peer_addr, ctx->conn_id, ctx->msg);
+		return;
+
+	case LB_PEER_EV_MSG_DOWN_CO:
+		ctx = data;
+		OSMO_ASSERT(ctx);
+		OSMO_ASSERT(ctx->msg);
+		sccp_lb_down_l2_co(lbp->sli, ctx->conn_id, ctx->msg);
+		return;
+
+	case LB_PEER_EV_MSG_DOWN_CL:
+		OSMO_ASSERT(data);
+		sccp_lb_down_l2_cl(lbp->sli, &lbp->peer_addr, (struct msgb*)data);
+		return;
+
+	case LB_PEER_EV_RX_RESET:
+		msg = (struct msgb*)data;
+		lb_peer_rx_reset(lbp, msg);
+		return;
+
+	default:
+		LOG_LB_PEER(lbp, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&lb_peer_fsm, event));
+		return;
+	}
+}
+
+static int lb_peer_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct lb_peer *lbp = fi->priv;
+	lb_peer_state_chg(lbp, LB_PEER_ST_WAIT_RX_RESET);
+	return 0;
+}
+
+void lb_peer_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+	struct lb_peer *lbp = fi->priv;
+	lb_peer_discard_all_conns(lbp);
+	llist_del(&lbp->entry);
+}
+
+static const struct value_string lb_peer_fsm_event_names[] = {
+	OSMO_VALUE_STRING(LB_PEER_EV_MSG_UP_CL),
+	OSMO_VALUE_STRING(LB_PEER_EV_MSG_UP_CO_INITIAL),
+	OSMO_VALUE_STRING(LB_PEER_EV_MSG_UP_CO),
+	OSMO_VALUE_STRING(LB_PEER_EV_MSG_DOWN_CL),
+	OSMO_VALUE_STRING(LB_PEER_EV_MSG_DOWN_CO_INITIAL),
+	OSMO_VALUE_STRING(LB_PEER_EV_MSG_DOWN_CO),
+	OSMO_VALUE_STRING(LB_PEER_EV_RX_RESET),
+	OSMO_VALUE_STRING(LB_PEER_EV_RX_RESET_ACK),
+	OSMO_VALUE_STRING(LB_PEER_EV_CONNECTION_SUCCESS),
+	OSMO_VALUE_STRING(LB_PEER_EV_CONNECTION_TIMEOUT),
+	{}
+};
+
+#define S(x)	(1 << (x))
+
+static const struct osmo_fsm_state lb_peer_fsm_states[] = {
+	[LB_PEER_ST_WAIT_RX_RESET] = {
+		.name = "WAIT_RX_RESET",
+		.action = lb_peer_st_wait_rx_reset,
+		.in_event_mask = 0
+			| S(LB_PEER_EV_RX_RESET)
+			| S(LB_PEER_EV_MSG_UP_CO_INITIAL)
+			| S(LB_PEER_EV_MSG_UP_CO)
+			| S(LB_PEER_EV_CONNECTION_TIMEOUT)
+			,
+		.out_state_mask = 0
+			| S(LB_PEER_ST_WAIT_RX_RESET)
+			| S(LB_PEER_ST_WAIT_RX_RESET_ACK)
+			| S(LB_PEER_ST_READY)
+			| S(LB_PEER_ST_DISCARDING)
+			,
+	},
+	[LB_PEER_ST_WAIT_RX_RESET_ACK] = {
+		.name = "WAIT_RX_RESET_ACK",
+		.action = lb_peer_st_wait_rx_reset_ack,
+		.in_event_mask = 0
+			| S(LB_PEER_EV_RX_RESET)
+			| S(LB_PEER_EV_RX_RESET_ACK)
+			| S(LB_PEER_EV_MSG_UP_CO_INITIAL)
+			| S(LB_PEER_EV_MSG_UP_CO)
+			| S(LB_PEER_EV_CONNECTION_TIMEOUT)
+			,
+		.out_state_mask = 0
+			| S(LB_PEER_ST_WAIT_RX_RESET)
+			| S(LB_PEER_ST_WAIT_RX_RESET_ACK)
+			| S(LB_PEER_ST_READY)
+			| S(LB_PEER_ST_DISCARDING)
+			,
+	},
+	[LB_PEER_ST_READY] = {
+		.name = "READY",
+		.action = lb_peer_st_ready,
+		.in_event_mask = 0
+			| S(LB_PEER_EV_RX_RESET)
+			| S(LB_PEER_EV_MSG_UP_CO_INITIAL)
+			| S(LB_PEER_EV_MSG_UP_CO)
+			| S(LB_PEER_EV_MSG_DOWN_CO_INITIAL)
+			| S(LB_PEER_EV_MSG_DOWN_CO)
+			| S(LB_PEER_EV_MSG_DOWN_CL)
+			,
+		.out_state_mask = 0
+			| S(LB_PEER_ST_WAIT_RX_RESET)
+			| S(LB_PEER_ST_WAIT_RX_RESET_ACK)
+			| S(LB_PEER_ST_READY)
+			| S(LB_PEER_ST_DISCARDING)
+			,
+	},
+	[LB_PEER_ST_DISCARDING] = {
+		.name = "DISCARDING",
+	},
+};
+
+static struct osmo_fsm lb_peer_fsm = {
+	.name = "lb_peer",
+	.states = lb_peer_fsm_states,
+	.num_states = ARRAY_SIZE(lb_peer_fsm_states),
+	.log_subsys = DLB,
+	.event_names = lb_peer_fsm_event_names,
+	.timer_cb = lb_peer_fsm_timer_cb,
+	.cleanup = lb_peer_fsm_cleanup,
+	.allstate_action = lb_peer_allstate_action,
+	.allstate_event_mask = 0
+		| S(LB_PEER_EV_MSG_UP_CL)
+		,
+};
+
+int lb_peer_up_l2(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *calling_addr, bool co, uint32_t conn_id,
+		  struct msgb *l2)
+{
+	struct lb_peer *lb_peer = NULL;
+	uint32_t event;
+	struct lb_peer_ev_ctx ctx = {
+		.conn_id = conn_id,
+		.msg = l2,
+	};
+
+	if (co) {
+		struct lb_conn *lb_conn;
+		llist_for_each_entry(lb_conn, &sli->lb_conns, entry) {
+			if (lb_conn->sccp_conn_id == conn_id) {
+				lb_peer = lb_conn->lb_peer;
+				ctx.lb_conn = lb_conn;
+				break;
+			}
+		}
+
+		if (lb_peer && calling_addr) {
+			LOG_SCCP_LB_CO(sli, calling_addr, conn_id, LOGL_ERROR,
+					"Connection-Oriented Initial message for already existing conn_id."
+					" Dropping message.\n");
+			return -EINVAL;
+		}
+
+		if (!lb_peer && !calling_addr) {
+			LOG_SCCP_LB_CO(sli, calling_addr, conn_id, LOGL_ERROR,
+					"Connection-Oriented non-Initial message for unknown conn_id %u."
+					" Dropping message.\n", conn_id);
+			return -EINVAL;
+		}
+	}
+
+	if (calling_addr) {
+		lb_peer = lb_peer_find_or_create(sli, calling_addr);
+		if (!lb_peer) {
+			LOG_SCCP_LB_CL(sli, calling_addr, LOGL_ERROR, "Cannot register Lb peer\n");
+			return -EIO;
+		}
+	}
+
+	OSMO_ASSERT(lb_peer && lb_peer->fi);
+
+	if (co)
+		event = calling_addr ? LB_PEER_EV_MSG_UP_CO_INITIAL : LB_PEER_EV_MSG_UP_CO;
+	else
+		event = LB_PEER_EV_MSG_UP_CL;
+
+	return osmo_fsm_inst_dispatch(lb_peer->fi, event, &ctx);
+}
+
+void lb_peer_disconnect(struct sccp_lb_inst *sli, uint32_t conn_id)
+{
+	struct lb_conn *lb_conn;
+	llist_for_each_entry(lb_conn, &sli->lb_conns, entry) {
+		if (lb_conn->sccp_conn_id == conn_id) {
+			lb_conn_discard(lb_conn);
+			return;
+		}
+	}
+}
diff --git a/src/osmo-smlc/sccp_lb_inst.c b/src/osmo-smlc/sccp_lb_inst.c
new file mode 100644
index 0000000..5a5c5e9
--- /dev/null
+++ b/src/osmo-smlc/sccp_lb_inst.c
@@ -0,0 +1,253 @@
+/*
+ * (C) 2020 by sysmocom - s.m.f.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0+
+ *
+ * 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/sccp/sccp_types.h>
+#include <osmocom/sigtran/sccp_sap.h>
+#include <osmocom/sigtran/sccp_helpers.h>
+
+#include <osmocom/smlc/debug.h>
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/sccp_lb_inst.h>
+#include <osmocom/smlc/lb_peer.h>
+
+/* We need an unused SCCP conn_id across all SCCP users. */
+int sccp_lb_inst_next_conn_id()
+{
+	static uint32_t next_id = 1;
+	int i;
+
+	/* This looks really suboptimal, but in most cases the static next_id should indicate exactly the next unused
+	 * conn_id, and we only iterate all conns once to make super sure that it is not already in use. */
+
+	for (i = 0; i < 0xFFFFFF; i++) {
+		struct lb_peer *lb_peer;
+		uint32_t conn_id = next_id;
+		bool conn_id_already_used = false;
+		next_id = (next_id + 1) & 0xffffff;
+
+		llist_for_each_entry(lb_peer, &g_smlc->lb->lb_peers, entry) {
+			struct lb_conn *conn;
+			lb_peer_for_each_lb_conn(conn, lb_peer) {
+				if (conn_id == conn->sccp_conn_id) {
+					conn_id_already_used = true;
+					break;
+				}
+			}
+			if (conn_id_already_used)
+				break;
+		}
+
+		if (!conn_id_already_used)
+			return conn_id;
+	}
+	return -1;
+}
+
+static int sccp_lb_sap_up(struct osmo_prim_hdr *oph, void *_scu);
+
+struct sccp_lb_inst *sccp_lb_init(void *talloc_ctx, struct osmo_sccp_instance *sccp, enum osmo_sccp_ssn ssn,
+				  const char *sccp_user_name)
+{
+	struct sccp_lb_inst *sli = talloc(talloc_ctx, struct sccp_lb_inst);
+	OSMO_ASSERT(sli);
+	*sli = (struct sccp_lb_inst){
+		.sccp = sccp,
+	};
+
+	INIT_LLIST_HEAD(&sli->lb_peers);
+	INIT_LLIST_HEAD(&sli->lb_conns);
+
+	osmo_sccp_local_addr_by_instance(&sli->local_sccp_addr, sccp, ssn);
+	sli->scu = osmo_sccp_user_bind(sccp, sccp_user_name, sccp_lb_sap_up, ssn);
+	osmo_sccp_user_set_priv(sli->scu, sli);
+
+	return sli;
+}
+
+static int sccp_lb_sap_up(struct osmo_prim_hdr *oph, void *_scu)
+{
+	struct osmo_sccp_user *scu = _scu;
+	struct sccp_lb_inst *sli = osmo_sccp_user_get_priv(scu);
+	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
+	struct osmo_sccp_addr *my_addr;
+	struct osmo_sccp_addr *peer_addr;
+	uint32_t conn_id;
+	int rc;
+
+	switch (OSMO_PRIM_HDR(oph)) {
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION):
+		/* indication of new inbound connection request */
+		conn_id = prim->u.connect.conn_id;
+		my_addr = &prim->u.connect.called_addr;
+		peer_addr = &prim->u.connect.calling_addr;
+		LOG_SCCP_LB_CO(sli, peer_addr, conn_id, LOGL_DEBUG, "%s(%s)\n", __func__, osmo_scu_prim_name(oph));
+
+		if (!msgb_l2(oph->msg) || msgb_l2len(oph->msg) == 0) {
+			LOG_SCCP_LB_CO(sli, peer_addr, conn_id, LOGL_NOTICE, "Received invalid N-CONNECT.ind\n");
+			rc = -1;
+			break;
+		}
+
+		if (osmo_sccp_addr_ri_cmp(&sli->local_sccp_addr, my_addr))
+			LOG_SCCP_LB_CO(sli, peer_addr, conn_id, LOGL_ERROR,
+				       "Rx N-CONNECT: Called address is %s != local address %s\n",
+				       osmo_sccp_inst_addr_to_str_c(OTC_SELECT, sli->sccp, my_addr),
+				       osmo_sccp_inst_addr_to_str_c(OTC_SELECT, sli->sccp, &sli->local_sccp_addr));
+
+		/* ensure the local SCCP socket is ACTIVE */
+		osmo_sccp_tx_conn_resp(scu, conn_id, my_addr, NULL, 0);
+
+		rc = lb_peer_up_l2(sli, peer_addr, true, conn_id, oph->msg);
+		if (rc)
+			osmo_sccp_tx_disconn(scu, conn_id, my_addr, SCCP_RETURN_CAUSE_UNQUALIFIED);
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION):
+		/* connection-oriented data received */
+		conn_id = prim->u.data.conn_id;
+		LOG_SCCP_LB_CO(sli, NULL, conn_id, LOGL_DEBUG, "%s(%s)\n", __func__, osmo_scu_prim_name(oph));
+
+		rc = lb_peer_up_l2(sli, NULL, true, conn_id, oph->msg);
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION):
+		/* indication of disconnect */
+		conn_id = prim->u.disconnect.conn_id;
+		LOG_SCCP_LB_CO(sli, NULL, conn_id, LOGL_DEBUG, "%s(%s)\n", __func__, osmo_scu_prim_name(oph));
+
+		/* If there is no L2 payload in the N-DISCONNECT, no need to dispatch up_l2(). */
+		if (msgb_l2len(oph->msg))
+			rc = lb_peer_up_l2(sli, NULL, true, conn_id, oph->msg);
+		else
+			rc = 0;
+
+		/* Make sure the lb_conn is dropped. It might seem more optimal to combine the disconnect() into
+		 * up_l2(), but since an up_l2() dispatch might already cause the lb_conn to be discarded for other
+		 * reasons, a separate disconnect() with a separate conn_id lookup is actually necessary. */
+		sccp_lb_disconnect(sli, conn_id, 0);
+		break;
+
+	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_INDICATION):
+		/* connection-less data received */
+		my_addr = &prim->u.unitdata.called_addr;
+		peer_addr = &prim->u.unitdata.calling_addr;
+		LOG_SCCP_LB_CL(sli, peer_addr, LOGL_DEBUG, "%s(%s)\n", __func__, osmo_scu_prim_name(oph));
+
+		if (osmo_sccp_addr_ri_cmp(&sli->local_sccp_addr, my_addr))
+			LOG_SCCP_LB_CL(sli, peer_addr, LOGL_ERROR,
+					"Rx N-UNITDATA: Called address is %s != local address %s\n",
+					osmo_sccp_inst_addr_to_str_c(OTC_SELECT, sli->sccp, my_addr),
+					osmo_sccp_inst_addr_to_str_c(OTC_SELECT, sli->sccp, &sli->local_sccp_addr));
+
+		rc = lb_peer_up_l2(sli, peer_addr, false, 0, oph->msg);
+		break;
+
+	default:
+		LOG_SCCP_LB_CL(sli, NULL, LOGL_ERROR, "%s(%s) unsupported\n", __func__, osmo_scu_prim_name(oph));
+		rc = -1;
+		break;
+	}
+
+	msgb_free(oph->msg);
+	return rc;
+}
+
+/* Push some padding if necessary to reach a multiple-of-eight offset to be msgb_push() an osmo_scu_prim that will then
+ * be 8-byte aligned. */
+static void msgb_pad_mod8(struct msgb *msg)
+{
+	uint8_t mod8 = (intptr_t)(msg->data) % 8;
+	if (mod8)
+		msgb_push(msg, mod8);
+}
+
+static int sccp_lb_sap_down(struct sccp_lb_inst *sli, struct osmo_prim_hdr *oph)
+{
+	int rc;
+	if (!sli->scu) {
+		rate_ctr_inc(&g_smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_ERR_CONN_NOT_READY]);
+		return -EIO;
+	}
+	rc = osmo_sccp_user_sap_down_nofree(sli->scu, oph);
+	if (rc >= 0)
+		rate_ctr_inc(&g_smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_SUCCESS]);
+	else
+		rate_ctr_inc(&g_smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_TX_ERR_SEND]);
+	return rc;
+}
+
+int sccp_lb_down_l2_co_initial(struct sccp_lb_inst *sli,
+				const struct osmo_sccp_addr *called_addr,
+				uint32_t conn_id, struct msgb *l2)
+{
+	struct osmo_scu_prim *prim;
+
+	l2->l2h = l2->data;
+
+	msgb_pad_mod8(l2);
+	prim = (struct osmo_scu_prim *) msgb_push(l2, sizeof(*prim));
+	prim->u.connect = (struct osmo_scu_connect_param){
+		.called_addr = *called_addr,
+		.calling_addr = sli->local_sccp_addr,
+		.sccp_class = 2,
+		//.importance = ?,
+		.conn_id = conn_id,
+	};
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER, OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_REQUEST, l2);
+	return sccp_lb_sap_down(sli, &prim->oph);
+}
+
+int sccp_lb_down_l2_co(struct sccp_lb_inst *sli, uint32_t conn_id, struct msgb *l2)
+{
+	struct osmo_scu_prim *prim;
+
+	l2->l2h = l2->data;
+
+	msgb_pad_mod8(l2);
+	prim = (struct osmo_scu_prim *) msgb_push(l2, sizeof(*prim));
+	prim->u.data.conn_id = conn_id;
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER, OSMO_SCU_PRIM_N_DATA, PRIM_OP_REQUEST, l2);
+	return sccp_lb_sap_down(sli, &prim->oph);
+}
+
+int sccp_lb_down_l2_cl(struct sccp_lb_inst *sli, const struct osmo_sccp_addr *called_addr, struct msgb *l2)
+{
+	struct osmo_scu_prim *prim;
+
+	l2->l2h = l2->data;
+
+	msgb_pad_mod8(l2);
+	prim = (struct osmo_scu_prim *) msgb_push(l2, sizeof(*prim));
+	prim->u.unitdata = (struct osmo_scu_unitdata_param){
+		.called_addr = *called_addr,
+		.calling_addr = sli->local_sccp_addr,
+	};
+	osmo_prim_init(&prim->oph, SCCP_SAP_USER, OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_REQUEST, l2);
+	return sccp_lb_sap_down(sli, &prim->oph);
+}
+
+int sccp_lb_disconnect(struct sccp_lb_inst *sli, uint32_t conn_id, uint32_t cause)
+{
+	return osmo_sccp_tx_disconn(sli->scu, conn_id, NULL, cause);
+}
diff --git a/src/osmo-smlc/smlc_data.c b/src/osmo-smlc/smlc_data.c
new file mode 100644
index 0000000..d51bceb
--- /dev/null
+++ b/src/osmo-smlc/smlc_data.c
@@ -0,0 +1,65 @@
+/* (C) 2020 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/lienses/>.
+ *
+ */
+
+#include <osmocom/core/stats.h>
+#include <osmocom/smlc/smlc_data.h>
+
+struct osmo_tdef g_smlc_tdefs[] = {
+	{ .T=-12, .default_val=5, .desc="Timeout for BSSLAP TA Response from BSC" },
+	{}
+};
+
+static const struct rate_ctr_desc smlc_ctr_description[] = {
+	[SMLC_CTR_BSSMAP_LE_RX_UDT_RESET] =	{ "bssmap_le:rx_udt_reset", "Rx BSSMAP-LE Reset" },
+	[SMLC_CTR_BSSMAP_LE_RX_UDT_RESET_ACK] =	{ "bssmap_le:rx_udt_reset_ack", "Rx BSSMAP-LE Reset Acknowledge" },
+	[SMLC_CTR_BSSMAP_LE_RX_UDT_ERR_INVALID_MSG] =	{ "bssmap_le:rx_udt_err_invalid_msg", "Receive invalid UnitData message" },
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_ERR_INVALID_MSG] =	{ "bssmap_le:rx_dt1_err_invalid_msg", "Receive invalid DirectTransfer1 message" },
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_REQUEST] =	{ "bssmap_le:rx_dt1_perform_location_request", "Receive Perform Location Request from BSC" },
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_TA_RESPONSE] =	{ "bssmap_le:rx_dt1_bsslap_ta_response", "Receive BSSLAP TA Response from BSC" },
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_REJECT] =	{ "bssmap_le:rx_dt1_bsslap_reject", "Rx BSSLAP Reject from BSC" },
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_RESET] =	{ "bssmap_le:rx_dt1_bsslap_reset", "Rx BSSLAP Reset (handover) from BSC" },
+	[SMLC_CTR_BSSMAP_LE_RX_DT1_BSSLAP_ABORT] =	{ "bssmap_le:rx_dt1_bsslap_abort", "Rx BSSLAP Abort from BSC" },
+
+	[SMLC_CTR_BSSMAP_LE_TX_ERR_INVALID_MSG] =	{ "bssmap_le:tx_err_invalid_msg", "BSSMAP-LE send error: invalid message" },
+	[SMLC_CTR_BSSMAP_LE_TX_ERR_CONN_NOT_READY] =	{ "bssmap_le:tx_err_conn_not_ready", "BSSMAP-LE send error: conn not ready" },
+	[SMLC_CTR_BSSMAP_LE_TX_ERR_SEND] =	{ "bssmap_le:tx_err_send", "BSSMAP-LE send error" },
+	[SMLC_CTR_BSSMAP_LE_TX_SUCCESS] =	{ "bssmap_le:tx_success", "BSSMAP-LE send success" },
+
+	[SMLC_CTR_BSSMAP_LE_TX_UDT_RESET] =	{ "bssmap_le:tx_udt_reset", "Transmit UnitData Reset" },
+	[SMLC_CTR_BSSMAP_LE_TX_UDT_RESET_ACK] =	{ "bssmap_le:tx_udt_reset_ack", "Transmit UnitData Reset Acknowledge" },
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_PERFORM_LOCATION_RESPONSE] =	{ "bssmap_le:tx_dt1_perform_location_response", "Tx Perform Location Response to BSC" },
+	[SMLC_CTR_BSSMAP_LE_TX_DT1_BSSLAP_TA_REQUEST] =	{ "bssmap_le:tx_dt1_bsslap_ta_request", "Tx BSSLAP TA Request to BSC" },
+};
+
+static const struct rate_ctr_group_desc smlc_ctrg_desc = {
+	"smlc",
+	"serving mobile location center",
+	OSMO_STATS_CLASS_GLOBAL,
+	ARRAY_SIZE(smlc_ctr_description),
+	smlc_ctr_description,
+};
+
+struct smlc_state *smlc_state_alloc(void *ctx)
+{
+	struct smlc_state *smlc = talloc_zero(ctx, struct smlc_state);
+	OSMO_ASSERT(smlc);
+	INIT_LLIST_HEAD(&smlc->subscribers);
+	INIT_LLIST_HEAD(&smlc->cell_locations);
+	smlc->ctrs = rate_ctr_group_alloc(smlc, &smlc_ctrg_desc, 0);
+	return smlc;
+}
diff --git a/src/osmo-smlc/smlc_loc_req.c b/src/osmo-smlc/smlc_loc_req.c
new file mode 100644
index 0000000..1365bfa
--- /dev/null
+++ b/src/osmo-smlc/smlc_loc_req.c
@@ -0,0 +1,445 @@
+/* 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>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/smlc_loc_req.h>
+#include <osmocom/smlc/smlc_subscr.h>
+#include <osmocom/smlc/lb_conn.h>
+#include <osmocom/smlc/cell_locations.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/tdef.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/bsslap.h>
+#include <osmocom/gsm/bssmap_le.h>
+#include <osmocom/gsm/gad.h>
+
+enum smlc_loc_req_fsm_state {
+	SMLC_LOC_REQ_ST_INIT,
+	SMLC_LOC_REQ_ST_WAIT_TA,
+	SMLC_LOC_REQ_ST_GOT_TA,
+	SMLC_LOC_REQ_ST_FAILED,
+};
+
+static const struct value_string smlc_loc_req_fsm_event_names[] = {
+	OSMO_VALUE_STRING(SMLC_LOC_REQ_EV_RX_TA_RESPONSE),
+	OSMO_VALUE_STRING(SMLC_LOC_REQ_EV_RX_BSSLAP_RESET),
+	OSMO_VALUE_STRING(SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT),
+	{}
+};
+
+static struct osmo_fsm smlc_loc_req_fsm;
+
+static const struct osmo_tdef_state_timeout smlc_loc_req_fsm_timeouts[32] = {
+	[SMLC_LOC_REQ_ST_WAIT_TA] = { .T = -12 },
+};
+
+/* Transition to a state, using the T timer defined in smlc_loc_req_fsm_timeouts.
+ * The actual timeout value is in turn obtained from network->T_defs.
+ * Assumes local variable fi exists. */
+#define smlc_loc_req_fsm_state_chg(FI, STATE) \
+	osmo_tdef_fsm_inst_state_chg(FI, STATE, \
+				     smlc_loc_req_fsm_timeouts, \
+				     g_smlc_tdefs, \
+				     5)
+
+#define smlc_loc_req_fail(cause, fmt, args...) do { \
+		LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_ERROR, "Perform Location Request failed in state %s: " fmt "\n", \
+				 smlc_loc_req ? osmo_fsm_inst_state_name(smlc_loc_req->fi) : "NULL", ## args); \
+		smlc_loc_req->lcs_cause = (struct lcs_cause_ie){ \
+			.present = true, \
+			.cause_val = cause, \
+		}; \
+		smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_FAILED); \
+	} while(0)
+
+static struct smlc_loc_req *smlc_loc_req_alloc(void *ctx)
+{
+	struct smlc_loc_req *smlc_loc_req;
+
+	struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&smlc_loc_req_fsm, ctx, NULL, LOGL_DEBUG, "no-id");
+	OSMO_ASSERT(fi);
+
+	smlc_loc_req = talloc(fi, struct smlc_loc_req);
+	OSMO_ASSERT(smlc_loc_req);
+	fi->priv = smlc_loc_req;
+	*smlc_loc_req = (struct smlc_loc_req){
+		.fi = fi,
+	};
+
+	return smlc_loc_req;
+}
+
+static int smlc_loc_req_start(struct lb_conn *lb_conn, const struct bssmap_le_perform_loc_req *loc_req_pdu)
+{
+	struct smlc_loc_req *smlc_loc_req;
+
+	rate_ctr_inc(&g_smlc->ctrs->ctr[SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_REQUEST]);
+
+	if (lb_conn->smlc_loc_req) {
+		/* Another request is already pending. If we send Perform Location Abort, the peer doesn't know which
+		 * request we would mean. Just drop this on the floor. */
+		LOG_SMLC_LOC_REQ(lb_conn->smlc_loc_req, LOGL_ERROR,
+				"Ignoring Perform Location Request, another request is still pending\n");
+		return -EAGAIN;
+	}
+
+	if (loc_req_pdu->imsi.type == GSM_MI_TYPE_IMSI
+	    && (!lb_conn->smlc_subscr
+		|| osmo_mobile_identity_cmp(&loc_req_pdu->imsi, &lb_conn->smlc_subscr->imsi))) {
+
+		struct smlc_subscr *smlc_subscr;
+		struct lb_conn *other_conn;
+		smlc_subscr = smlc_subscr_find_or_create(&loc_req_pdu->imsi, __func__);
+		OSMO_ASSERT(smlc_subscr);
+
+		if (lb_conn->smlc_subscr && lb_conn->smlc_subscr != smlc_subscr) {
+			LOG_LB_CONN(lb_conn, LOGL_ERROR,
+				    "IMSI mismatch: lb_conn has %s, Rx Perform Location Request has %s\n",
+				    smlc_subscr_to_str_c(OTC_SELECT, lb_conn->smlc_subscr),
+				    smlc_subscr_to_str_c(OTC_SELECT, smlc_subscr));
+			smlc_subscr_put(smlc_subscr, __func__);
+			return -EINVAL;
+		}
+
+		/* Find another conn before setting this conn's subscriber */
+		other_conn = lb_conn_find_by_smlc_subscr(lb_conn->smlc_subscr, __func__);
+
+		/* Set the subscriber before logging about it, so that it shows as log context */
+		if (!lb_conn->smlc_subscr) {
+			lb_conn->smlc_subscr = smlc_subscr;
+			smlc_subscr_get(lb_conn->smlc_subscr, SMLC_SUBSCR_USE_LB_CONN);
+		}
+
+		if (other_conn && other_conn != lb_conn) {
+			LOG_LB_CONN(lb_conn, LOGL_ERROR, "Another conn already active for this subscriber\n");
+			LOG_LB_CONN(other_conn, LOGL_ERROR, "Another conn opened for this subscriber, discarding\n");
+			lb_conn_close(other_conn);
+		}
+
+		smlc_subscr_put(smlc_subscr, __func__);
+		if (other_conn)
+			lb_conn_put(other_conn, __func__);
+	}
+
+	/* smlc_loc_req has a use count on lb_conn, so its talloc ctx must not be a child of lb_conn. (Otherwise an
+	 * lb_conn_put() from smlc_loc_req could cause a free of smlc_loc_req's parent ctx, causing a use after free on
+	 * FSM termination.) */
+	smlc_loc_req = smlc_loc_req_alloc(lb_conn->lb_peer);
+
+	*smlc_loc_req = (struct smlc_loc_req){
+		.fi = smlc_loc_req->fi,
+		.lb_conn = lb_conn,
+		.req = *loc_req_pdu,
+	};
+	smlc_loc_req->latest_cell_id = loc_req_pdu->cell_id;
+	lb_conn->smlc_loc_req = smlc_loc_req;
+	lb_conn_get(smlc_loc_req->lb_conn, LB_CONN_USE_SMLC_LOC_REQ);
+
+	LOG_LB_CONN(lb_conn, LOGL_INFO, "Rx Perform Location Request (BSSLAP APDU %s), cell id is %s\n",
+		    loc_req_pdu->apdu_present ?
+		    osmo_bsslap_msgt_name(loc_req_pdu->apdu.msg_type) : "omitted",
+		    gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id));
+
+	/* state change to start the timeout */
+	smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_WAIT_TA);
+	return 0;
+}
+
+static int handle_bssmap_le_conn_oriented_info(struct smlc_loc_req *smlc_loc_req,
+					       const struct bssmap_le_conn_oriented_info *coi)
+{
+	switch (coi->apdu.msg_type) {
+
+	case BSSLAP_MSGT_TA_RESPONSE:
+		return osmo_fsm_inst_dispatch(smlc_loc_req->fi, SMLC_LOC_REQ_EV_RX_TA_RESPONSE,
+					      (void*)&coi->apdu.ta_response);
+
+	case BSSLAP_MSGT_RESET:
+		return osmo_fsm_inst_dispatch(smlc_loc_req->fi, SMLC_LOC_REQ_EV_RX_BSSLAP_RESET,
+					      (void*)&coi->apdu.reset);
+
+	case BSSLAP_MSGT_ABORT:
+		smlc_loc_req_fail(LCS_CAUSE_REQUEST_ABORTED, "Aborting Location Request due to BSSLAP Abort");
+		return 0;
+
+	case BSSLAP_MSGT_REJECT:
+		smlc_loc_req_fail(LCS_CAUSE_REQUEST_ABORTED, "Aborting Location Request due to BSSLAP Reject");
+		return 0;
+
+	default:
+		LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_ERROR, "rx BSSLAP APDU with unsupported message type %s\n",
+				 osmo_bsslap_msgt_name(coi->apdu.msg_type));
+		return -ENOTSUP;
+	};
+}
+
+int smlc_loc_req_rx_bssap_le(struct lb_conn *lb_conn, const struct bssap_le_pdu *bssap_le)
+{
+	struct smlc_loc_req *smlc_loc_req = lb_conn->smlc_loc_req;
+	const struct bssmap_le_pdu *bssmap_le = &bssap_le->bssmap_le;
+
+	LOG_LB_CONN(lb_conn, LOGL_DEBUG, "Rx %s\n", osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
+
+	if (bssap_le->discr != BSSAP_LE_MSG_DISCR_BSSMAP_LE) {
+		LOG_LB_CONN(lb_conn, LOGL_ERROR, "BSSAP-LE discr %d not implemented\n", bssap_le->discr);
+		return -ENOTSUP;
+	}
+
+	switch (bssmap_le->msg_type) {
+
+	case BSSMAP_LE_MSGT_PERFORM_LOC_REQ:
+		return smlc_loc_req_start(lb_conn, &bssmap_le->perform_loc_req);
+
+	case BSSMAP_LE_MSGT_PERFORM_LOC_ABORT:
+		return osmo_fsm_inst_dispatch(smlc_loc_req->fi, SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT,
+					      (void*)&bssmap_le->perform_loc_abort);
+
+	case BSSMAP_LE_MSGT_CONN_ORIENTED_INFO:
+		return handle_bssmap_le_conn_oriented_info(smlc_loc_req, &bssmap_le->conn_oriented_info);
+
+	default:
+		LOG_SMLC_LOC_REQ(smlc_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 smlc_loc_req_reset(struct lb_conn *lb_conn)
+{
+	struct smlc_loc_req *smlc_loc_req = lb_conn->smlc_loc_req;
+	if (!smlc_loc_req)
+		return;
+	smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Aborting Location Request due to RESET on Lb");
+}
+
+static int smlc_loc_req_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+	struct smlc_loc_req *smlc_loc_req = fi->priv;
+	smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Timeout");
+	return 1;
+}
+
+static void smlc_loc_req_wait_ta_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct smlc_loc_req *smlc_loc_req = fi->priv;
+	struct bssmap_le_pdu bssmap_le;
+
+	/* Did the original request contain a TA already? */
+	if (smlc_loc_req->req.apdu_present && smlc_loc_req->req.apdu.msg_type == BSSLAP_MSGT_TA_LAYER3) {
+		smlc_loc_req->ta_present = true;
+		smlc_loc_req->ta = smlc_loc_req->req.apdu.ta_layer3.ta;
+		LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "TA = %u\n", smlc_loc_req->ta);
+		smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_GOT_TA);
+		return;
+	}
+
+	/* No TA known yet, ask via BSSLAP */
+	bssmap_le = (struct bssmap_le_pdu){
+		.msg_type = BSSMAP_LE_MSGT_CONN_ORIENTED_INFO,
+		.conn_oriented_info = {
+			.apdu = {
+				.msg_type = BSSLAP_MSGT_TA_REQUEST,
+			},
+		},
+	};
+
+	lb_conn_send_bssmap_le(smlc_loc_req->lb_conn, &bssmap_le);
+}
+
+static void update_ci(struct gsm0808_cell_id *cell_id, int16_t new_ci)
+{
+	struct osmo_cell_global_id cgi = {};
+	struct gsm0808_cell_id ci = {
+		.id_discr = CELL_IDENT_CI,
+		.id.ci = new_ci,
+	};
+	/* Set all values from the cell_id to the cgi */
+	gsm0808_cell_id_to_cgi(&cgi, cell_id);
+	/* Overwrite the CI part */
+	gsm0808_cell_id_to_cgi(&cgi, &ci);
+	/* write back to cell_id, without changing its type */
+	gsm0808_cell_id_from_cgi(cell_id, cell_id->id_discr, &cgi);
+}
+
+static void smlc_loc_req_wait_ta_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
+{
+	struct smlc_loc_req *smlc_loc_req = fi->priv;
+	const struct bsslap_ta_response *ta_response;
+	const struct bsslap_reset *reset;
+
+	switch (event) {
+
+	case SMLC_LOC_REQ_EV_RX_TA_RESPONSE:
+		ta_response = data;
+		smlc_loc_req->ta_present = true;
+		smlc_loc_req->ta = ta_response->ta;
+		update_ci(&smlc_loc_req->latest_cell_id, ta_response->cell_id);
+		LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Rx BSSLAP TA Response: cell id is now %s\n",
+				 gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id));
+		smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_GOT_TA);
+		return;
+
+	case SMLC_LOC_REQ_EV_RX_BSSLAP_RESET:
+		reset = data;
+		smlc_loc_req->ta_present = true;
+		smlc_loc_req->ta = reset->ta;
+		update_ci(&smlc_loc_req->latest_cell_id, reset->cell_id);
+		LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Rx BSSLAP Reset: cell id is now %s\n",
+				 gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id));
+		smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_GOT_TA);
+		return;
+
+	case SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT:
+		LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Rx Perform Location Abort, stopping this request dead\n");
+		osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REQUEST, NULL);
+		return;
+
+	default:
+		OSMO_ASSERT(false);
+	}
+}
+
+static void smlc_loc_req_got_ta_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct smlc_loc_req *smlc_loc_req = fi->priv;
+	struct bssmap_le_pdu bssmap_le;
+	struct osmo_gad location;
+	int rc;
+
+	if (!smlc_loc_req->ta_present) {
+		smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
+				  "Internal error: GOT_TA event, but no TA present");
+		return;
+	}
+
+	bssmap_le = (struct bssmap_le_pdu){
+		.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_RESP,
+		.perform_loc_resp = {
+			.location_estimate_present = true,
+		},
+	};
+
+	rc = cell_location_from_ta(&location, &smlc_loc_req->latest_cell_id, smlc_loc_req->ta);
+	if (rc) {
+		smlc_loc_req_fail(LCS_CAUSE_FACILITY_NOTSUPP, "Unable to compose Location Estimate for %s: %s",
+				  gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id),
+				  rc == -ENOENT ? "No location information for this cell" : "unknown error");
+		return;
+	}
+
+	rc = osmo_gad_enc(&bssmap_le.perform_loc_resp.location_estimate, &location);
+	if (rc <= 0) {
+		smlc_loc_req_fail(LCS_CAUSE_FACILITY_NOTSUPP, "Unable to encode Location Estimate for %s (rc=%d)",
+				  gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id), rc);
+		return;
+	}
+
+	LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Returning location estimate to BSC: %s TA=%u --> %s\n",
+			 gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id),
+			 smlc_loc_req->ta, osmo_gad_to_str_c(OTC_SELECT, &location));
+
+	if (lb_conn_send_bssmap_le(smlc_loc_req->lb_conn, &bssmap_le)) {
+		smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
+				  "Unable to encode/send BSSMAP-LE Perform Location Response");
+		return;
+	}
+	osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+static void smlc_loc_req_failed_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
+{
+	struct smlc_loc_req *smlc_loc_req = fi->priv;
+	struct bssmap_le_pdu bssmap_le = {
+		.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_RESP,
+		.perform_loc_resp = {
+			.lcs_cause = smlc_loc_req->lcs_cause,
+		},
+	};
+	int rc;
+	rc = lb_conn_send_bssmap_le(smlc_loc_req->lb_conn, &bssmap_le);
+	osmo_fsm_inst_term(fi, rc ? OSMO_FSM_TERM_ERROR : OSMO_FSM_TERM_REGULAR, NULL);
+}
+
+void smlc_loc_req_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+	struct smlc_loc_req *smlc_loc_req = fi->priv;
+	if (smlc_loc_req->lb_conn && smlc_loc_req->lb_conn->smlc_loc_req == smlc_loc_req) {
+		smlc_loc_req->lb_conn->smlc_loc_req = NULL;
+		lb_conn_put(smlc_loc_req->lb_conn, LB_CONN_USE_SMLC_LOC_REQ);
+	}
+}
+
+#define S(x)    (1 << (x))
+
+static const struct osmo_fsm_state smlc_loc_req_fsm_states[] = {
+	[SMLC_LOC_REQ_ST_INIT] = {
+		.name = "INIT",
+		.out_state_mask = 0
+			| S(SMLC_LOC_REQ_ST_WAIT_TA)
+			| S(SMLC_LOC_REQ_ST_FAILED)
+			,
+	},
+	[SMLC_LOC_REQ_ST_WAIT_TA] = {
+		.name = "WAIT_TA",
+		.in_event_mask = 0
+			| S(SMLC_LOC_REQ_EV_RX_TA_RESPONSE)
+			| S(SMLC_LOC_REQ_EV_RX_BSSLAP_RESET)
+			| S(SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT)
+			,
+		.out_state_mask = 0
+			| S(SMLC_LOC_REQ_ST_GOT_TA)
+			| S(SMLC_LOC_REQ_ST_FAILED)
+			,
+		.onenter = smlc_loc_req_wait_ta_onenter,
+		.action = smlc_loc_req_wait_ta_action,
+	},
+	[SMLC_LOC_REQ_ST_GOT_TA] = {
+		.name = "GOT_TA",
+		.out_state_mask = 0
+			| S(SMLC_LOC_REQ_ST_FAILED)
+			,
+		.onenter = smlc_loc_req_got_ta_onenter,
+	},
+	[SMLC_LOC_REQ_ST_FAILED] = {
+		.name = "FAILED",
+		.onenter = smlc_loc_req_failed_onenter,
+	},
+};
+
+static struct osmo_fsm smlc_loc_req_fsm = {
+	.name = "smlc_loc_req",
+	.states = smlc_loc_req_fsm_states,
+	.num_states = ARRAY_SIZE(smlc_loc_req_fsm_states),
+	.log_subsys = DLCS,
+	.event_names = smlc_loc_req_fsm_event_names,
+	.timer_cb = smlc_loc_req_fsm_timer_cb,
+	.cleanup = smlc_loc_req_fsm_cleanup,
+};
+
+static __attribute__((constructor)) void smlc_loc_req_fsm_register(void)
+{
+	OSMO_ASSERT(osmo_fsm_register(&smlc_loc_req_fsm) == 0);
+}
diff --git a/src/osmo-smlc/smlc_main.c b/src/osmo-smlc/smlc_main.c
index 9f32441..105ced3 100644
--- a/src/osmo-smlc/smlc_main.c
+++ b/src/osmo-smlc/smlc_main.c
@@ -31,12 +31,15 @@
 #include <osmocom/vty/ports.h>
 #include <osmocom/vty/logging.h>
 #include <osmocom/vty/command.h>
+#include <osmocom/vty/misc.h>
 
 #include <osmocom/sigtran/xua_msg.h>
 #include <osmocom/sigtran/sccp_sap.h>
 
+#include <osmocom/smlc/debug.h>
 #include <osmocom/smlc/smlc_data.h>
-#include <osmocom/smlc/smlc_sigtran.h>
+#include <osmocom/smlc/sccp_lb_inst.h>
+#include <osmocom/smlc/cell_locations.h>
 
 #define _GNU_SOURCE
 #include <getopt.h>
@@ -47,9 +50,12 @@
 #include <time.h>
 #include <unistd.h>
 
-
 #include "../../config.h"
 
+#define DEFAULT_M3UA_LOCAL_IP "localhost"
+#define DEFAULT_M3UA_REMOTE_IP "localhost"
+#define SMLC_DEFAULT_PC "0.23.6"
+
 static const char *config_file = "osmo-smlc.cfg";
 static int daemonize = 0;
 static void *tall_smlc_ctx;
@@ -167,6 +173,26 @@
 }
 
 static const struct log_info_cat smlc_categories[] = {
+	[DSMLC] = {
+		.name = "DSMLC",
+		.description = "Serving Mobile Location Center",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+	[DREF] = {
+		.name = "DREF",
+		.description = "Reference Counting",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+	[DLB] = {
+		.name = "DLB",
+		.description = "Lb interface",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+	[DLCS] = {
+		.name = "DLCS",
+		.description = "Location Services",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
 };
 
 const struct log_info log_info = {
@@ -177,6 +203,7 @@
 int main(int argc, char **argv)
 {
 	int rc;
+	int default_pc;
 
 	tall_smlc_ctx = talloc_named_const(NULL, 1, "osmo-smlc");
 	msgb_talloc_ctx_init(tall_smlc_ctx, 0);
@@ -190,13 +217,14 @@
 
 	osmo_fsm_set_dealloc_ctx(OTC_SELECT);
 
-	g_smlc = talloc_zero(tall_smlc_ctx, struct smlc_state);
-	OSMO_ASSERT(g_smlc);
+	g_smlc = smlc_state_alloc(tall_smlc_ctx);
 
 	/* This needs to precede handle_options() */
 	vty_init(&vty_info);
-	//smlc_vty_init(g_smlc);
+	logging_vty_add_cmds();
+	osmo_talloc_vty_add_cmds();
 	ctrl_vty_init(tall_smlc_ctx);
+	cell_locations_vty_init();
 
 	/* Initialize SS7 */
 	OSMO_ASSERT(osmo_ss7_init() == 0);
@@ -235,9 +263,20 @@
 	}
 	*/
 
-	if (smlc_sigtran_init() != 0) {
-		LOGP(DLB, LOGL_ERROR, "Failed to initialize sigtran backhaul.\n");
-		exit(1);
+	default_pc = osmo_ss7_pointcode_parse(NULL, SMLC_DEFAULT_PC);
+	OSMO_ASSERT(default_pc);
+
+	g_smlc->sccp_inst = osmo_sccp_simple_client_on_ss7_id(g_smlc, 0, "Lb", default_pc, OSMO_SS7_ASP_PROT_M3UA,
+							     0, DEFAULT_M3UA_LOCAL_IP, 0, DEFAULT_M3UA_REMOTE_IP);
+	if (!g_smlc->sccp_inst) {
+		fprintf(stderr, "Setting up SCCP failed\n");
+		return 1;
+	}
+
+	g_smlc->lb = sccp_lb_init(g_smlc, g_smlc->sccp_inst, OSMO_SCCP_SSN_SMLC_BSSAP_LE, "OsmoSMLC-Lb");
+	if (!g_smlc->lb) {
+		fprintf(stderr, "Setting up Lb receiver failed\n");
+		return 1;
 	}
 
 	signal(SIGINT, &signal_handler);
diff --git a/src/osmo-smlc/smlc_sigtran.c b/src/osmo-smlc/smlc_sigtran.c
deleted file mode 100644
index 902df0c..0000000
--- a/src/osmo-smlc/smlc_sigtran.c
+++ /dev/null
@@ -1,94 +0,0 @@
-/* (C) 2020 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/lienses/>.
- *
- */
-
-#include <errno.h>
-
-#include <osmocom/core/utils.h>
-#include <osmocom/core/logging.h>
-#include <osmocom/core/fsm.h>
-#include <osmocom/core/linuxlist.h>
-#include <osmocom/sigtran/osmo_ss7.h>
-#include <osmocom/sigtran/sccp_sap.h>
-#include <osmocom/gsm/gsm0808.h>
-
-#include <osmocom/smlc/smlc_data.h>
-#include <osmocom/smlc/smlc_sigtran.h>
-
-
-#define DEFAULT_M3UA_REMOTE_IP	"localhost"
-#define DEFAULT_PC		"0.23.6"
-
-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;
-	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(DLB, "N-UNITDATA.ind(%s)\n", osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
-		//rc = handle_unitdata_from_bsc(&scu_prim->u.unitdata.calling_addr, oph->msg, scu);
-		break;
-	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION):
-		/* Handle inbound connections */
-		DEBUGP(DLB, "N-CONNECT.ind(X->%u)\n", scu_prim->u.connect.conn_id);
-		break;
-	case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM):
-		/* Handle outbound connection confirmation */
-		DEBUGP(DLB, "N-CONNECT.cnf(%u, %s)\n", scu_prim->u.connect.conn_id,
-			osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
-		break;
-	case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION):
-		/* Handle incoming connection oriented data */
-		DEBUGP(DLB, "N-DATA.ind(%u, %s)\n", scu_prim->u.data.conn_id,
-			osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg)));
-
-		break;
-	case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION):
-		DEBUGP(DLB, "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);
-		break;
-	default:
-		LOGP(DLB, 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;
-}
-
-int smlc_sigtran_init(void)
-{
-	struct osmo_sccp_instance *sccp;
-	int default_pc = osmo_ss7_pointcode_parse(NULL, DEFAULT_PC);
-
-	OSMO_ASSERT(default_pc);
-
-	sccp = osmo_sccp_simple_client_on_ss7_id(g_smlc, 0, "Lb", default_pc, OSMO_SS7_ASP_PROT_M3UA,
-						 0, NULL, 0, DEFAULT_M3UA_REMOTE_IP);
-
-
-	g_smlc->sccp_user = osmo_sccp_user_bind(sccp, "SMLC", sccp_sap_up, OSMO_SCCP_SSN_SMLC_BSSAP);
-	if (!g_smlc->sccp_user)
-		return -EINVAL;
-
-	return 0;
-}
diff --git a/src/osmo-smlc/smlc_subscr.c b/src/osmo-smlc/smlc_subscr.c
new file mode 100644
index 0000000..bfbb1e9
--- /dev/null
+++ b/src/osmo-smlc/smlc_subscr.c
@@ -0,0 +1,125 @@
+/* GSM subscriber details for use in SMLC */
+/*
+ * (C) 2020 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/smlc/debug.h>
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/smlc_subscr.h>
+
+static void smlc_subscr_free(struct smlc_subscr *smlc_subscr)
+{
+	llist_del(&smlc_subscr->entry);
+	talloc_free(smlc_subscr);
+}
+
+static int smlc_subscr_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line)
+{
+	struct smlc_subscr *smlc_subscr = e->use_count->talloc_object;
+	int32_t total;
+	int level;
+
+	if (!e->use)
+		return -EINVAL;
+
+	total = osmo_use_count_total(&smlc_subscr->use_count);
+
+	if (total == 0
+	    || (total == 1 && old_use_count == 0 && e->count == 1))
+		level = LOGL_INFO;
+	else
+		level = LOGL_DEBUG;
+
+	LOGPSRC(DREF, level, file, line, "%s: %s %s\n",
+		smlc_subscr_to_str_c(OTC_SELECT, smlc_subscr),
+		(e->count - old_use_count) > 0? "+" : "-", e->use);
+
+	if (e->count < 0)
+		return -ERANGE;
+
+	if (total == 0)
+		smlc_subscr_free(smlc_subscr);
+	return 0;
+}
+
+static struct smlc_subscr *smlc_subscr_alloc()
+{
+	struct smlc_subscr *smlc_subscr;
+
+	smlc_subscr = talloc_zero(g_smlc, struct smlc_subscr);
+	if (!smlc_subscr)
+		return NULL;
+
+	smlc_subscr->use_count = (struct osmo_use_count){
+		.talloc_object = smlc_subscr,
+		.use_cb = smlc_subscr_use_cb,
+	};
+
+	llist_add_tail(&smlc_subscr->entry, &g_smlc->subscribers);
+
+	return smlc_subscr;
+}
+
+struct smlc_subscr *smlc_subscr_find(const struct osmo_mobile_identity *imsi, const char *use_token)
+{
+	struct smlc_subscr *smlc_subscr;
+	if (!imsi)
+		return NULL;
+
+	llist_for_each_entry(smlc_subscr, &g_smlc->subscribers, entry) {
+		if (!osmo_mobile_identity_cmp(&smlc_subscr->imsi, imsi)) {
+			smlc_subscr_get(smlc_subscr, use_token);
+			return smlc_subscr;
+		}
+	}
+	return NULL;
+}
+
+struct smlc_subscr *smlc_subscr_find_or_create(const struct osmo_mobile_identity *imsi, const char *use_token)
+{
+	struct smlc_subscr *smlc_subscr;
+	if (!imsi)
+		return NULL;
+	smlc_subscr = smlc_subscr_find(imsi, use_token);
+	if (smlc_subscr)
+		return smlc_subscr;
+	smlc_subscr = smlc_subscr_alloc();
+	if (!smlc_subscr)
+		return NULL;
+	smlc_subscr->imsi = *imsi;
+	smlc_subscr_get(smlc_subscr, use_token);
+	return smlc_subscr;
+}
+
+int smlc_subscr_to_str_buf(char *buf, size_t buf_len, const struct smlc_subscr *smlc_subscr)
+{
+	struct osmo_strbuf sb = { .buf = buf, .len = buf_len };
+	OSMO_STRBUF_APPEND(sb, osmo_mobile_identity_to_str_buf, &smlc_subscr->imsi);
+	OSMO_STRBUF_PRINTF(sb, "[");
+	OSMO_STRBUF_APPEND(sb, osmo_use_count_to_str_buf, &smlc_subscr->use_count);
+	OSMO_STRBUF_PRINTF(sb, "]");
+	return sb.chars_needed;
+}
+
+char *smlc_subscr_to_str_c(void *ctx, const struct smlc_subscr *smlc_subscr)
+{
+	OSMO_NAME_C_IMPL(ctx, 64, "ERROR", smlc_subscr_to_str_buf, smlc_subscr)
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 0cbf998..9487d3a 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,4 +1,5 @@
 SUBDIRS = \
+	smlc_subscr \
 	$(NULL)
 
 # The `:;' works around a Bash 3.2 bug when the output is not writeable.
@@ -25,6 +26,8 @@
 	$(TESTSUITE) \
 	test_nodes.vty \
 	test_nodes.ctrl \
+	cell_locations.vty \
+	osmo-smlc.cfg \
 	$(NULL)
 
 TESTSUITE = $(srcdir)/testsuite
@@ -51,7 +54,7 @@
 vty-test:
 	osmo_verify_transcript_vty.py -v \
 		-n OsmoSMLC -p 4271 \
-		-r "$(top_builddir)/src/osmo-smlc/osmo-smlc -c $(top_srcdir)/doc/examples/osmo-smlc/osmo-smlc.cfg" \
+		-r "$(top_builddir)/src/osmo-smlc/osmo-smlc -c $(top_srcdir)/tests/osmo-smlc.cfg" \
 		$(U) $(srcdir)/$(VTY_TEST)
 
 # To update the CTRL script from current application behavior,
@@ -61,7 +64,7 @@
 	-rm -f $(CTRL_TEST_DB)
 	osmo_verify_transcript_ctrl.py -v \
 		-p 4272 \
-		-r "$(top_builddir)/src/osmo-smlc/osmo-smlc -c $(top_srcdir)/doc/examples/osmo-smlc/osmo-smlc.cfg" \
+		-r "$(top_builddir)/src/osmo-smlc/osmo-smlc -c $(top_srcdir)/tests/osmo-smlc.cfg" \
 		$(U) $(srcdir)/*.ctrl
 	-rm -f $(CTRL_TEST_DB)
 	-rm $(CTRL_TEST_DB)-*
diff --git a/tests/cell_locations.vty b/tests/cell_locations.vty
new file mode 100644
index 0000000..51dc00d
--- /dev/null
+++ b/tests/cell_locations.vty
@@ -0,0 +1,92 @@
+OsmoSMLC> enable
+
+OsmoSMLC# show cells
+% No cell locations are configured
+
+OsmoSMLC# configure terminal
+
+OsmoSMLC(config)# cells?
+  cells  Configure cell locations
+
+OsmoSMLC(config)# cells
+OsmoSMLC(config-cells)# list
+...
+  lac-ci <0-65535> <0-65535> lat LATITUDE lon LONGITUDE
+  no lac-ci <0-65535> <0-65535>
+  cgi <0-999> <0-999> <0-65535> <0-65535> lat LATITUDE lon LONGITUDE
+  no cgi <0-999> <0-999> <0-65535> <0-65535>
+
+OsmoSMLC(config-cells)# lac-ci?
+  lac-ci  Cell location by LAC and CI
+OsmoSMLC(config-cells)# lac-ci ?
+  <0-65535>  LAC
+OsmoSMLC(config-cells)# lac-ci 23 ?
+  <0-65535>  CI
+OsmoSMLC(config-cells)# lac-ci 23 42 ?
+  lat  Global latitute coordinate
+OsmoSMLC(config-cells)# lac-ci 23 42 lat ?
+  LATITUDE  Latitude floating-point number, -90.0 (S) to 90.0 (N)
+OsmoSMLC(config-cells)# lac-ci 23 42 lat 23.23 ?
+  lon  Global longitude coordinate
+OsmoSMLC(config-cells)# lac-ci 23 42 lat 23.23 lon ?
+  LONGITUDE  Longitude as floating-point number, -180.0 (W) to 180.0 (E)
+OsmoSMLC(config-cells)# lac-ci 23 42 lat 23.23 lon 42.42 ?
+  <cr>  
+
+OsmoSMLC(config-cells)# cgi?
+  cgi  Cell location by Cell-Global ID
+OsmoSMLC(config-cells)# cgi ?
+  <0-999>  MCC
+OsmoSMLC(config-cells)# cgi 001 ?
+  <0-999>  MNC
+OsmoSMLC(config-cells)# cgi 001 02 ?
+  <0-65535>  LAC
+OsmoSMLC(config-cells)# cgi 001 02 3 ?
+  <0-65535>  CI
+OsmoSMLC(config-cells)# cgi 001 02 3 4 ?
+  lat  Global latitute coordinate
+OsmoSMLC(config-cells)# cgi 001 02 3 4 lat ?
+  LATITUDE  Latitude floating-point number, -90.0 (S) to 90.0 (N)
+OsmoSMLC(config-cells)# cgi 001 02 3 4 lat 1.1 ?
+  lon  Global longitude coordinate
+OsmoSMLC(config-cells)# cgi 001 02 3 4 lat 1.1 lon ?
+  LONGITUDE  Longitude as floating-point number, -180.0 (W) to 180.0 (E)
+OsmoSMLC(config-cells)# cgi 001 02 3 4 lat 1.1 lon 2.2 ?
+  <cr>  
+
+OsmoSMLC(config-cells)# lac-ci 23 42 lat 23.23 lon 42.42
+OsmoSMLC(config-cells)# cgi 001 02 3 4 lat 1.1 lon 2.2
+
+OsmoSMLC(config-cells)# do show cells
+cells
+ lac-ci 23 42 lat 23.23 lon 42.42
+ cgi 001 02 3 4 lat 1.1 lon 2.2
+
+OsmoSMLC(config-cells)# show running-config
+...
+cells
+ lac-ci 23 42 lat 23.23 lon 42.42
+ cgi 001 02 3 4 lat 1.1 lon 2.2
+...
+
+OsmoSMLC(config-cells)# no lac-ci 99 99
+% cannot remove, no such entry
+OsmoSMLC(config-cells)# no cgi 009 08 7 6
+% cannot remove, no such entry
+
+OsmoSMLC(config-cells)# do show cells
+cells
+ lac-ci 23 42 lat 23.23 lon 42.42
+ cgi 001 02 3 4 lat 1.1 lon 2.2
+
+OsmoSMLC(config-cells)# lac-ci 23 42 lat 17.17 lon 18.18
+OsmoSMLC(config-cells)# do show cells
+cells
+ lac-ci 23 42 lat 17.17 lon 18.18
+ cgi 001 02 3 4 lat 1.1 lon 2.2
+
+OsmoSMLC(config-cells)# no lac-ci 23 42
+OsmoSMLC(config-cells)# no cgi 001 02 3 4
+
+OsmoSMLC(config-cells)# do show cells
+% No cell locations are configured
diff --git a/tests/osmo-smlc.cfg b/tests/osmo-smlc.cfg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/osmo-smlc.cfg
diff --git a/tests/smlc_subscr/Makefile.am b/tests/smlc_subscr/Makefile.am
new file mode 100644
index 0000000..9ed3b59
--- /dev/null
+++ b/tests/smlc_subscr/Makefile.am
@@ -0,0 +1,39 @@
+AM_CPPFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir)/include \
+	$(NULL)
+
+AM_CFLAGS = \
+	-Wall \
+	-ggdb3 \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(COVERAGE_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = \
+	$(COVERAGE_LDFLAGS) \
+	$(NULL)
+
+EXTRA_DIST = \
+	smlc_subscr_test.ok \
+	smlc_subscr_test.err \
+	$(NULL)
+
+noinst_PROGRAMS = \
+	smlc_subscr_test \
+	$(NULL)
+
+smlc_subscr_test_SOURCES = \
+	smlc_subscr_test.c \
+	$(NULL)
+
+smlc_subscr_test_LDADD = \
+	$(top_builddir)/src/osmo-smlc/smlc_data.o \
+	$(top_builddir)/src/osmo-smlc/smlc_subscr.o \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+update_exp:
+	$(builddir)/smlc_subscr_test >$(srcdir)/smlc_subscr_test.ok 2>$(srcdir)/smlc_subscr_test.err
diff --git a/tests/smlc_subscr/smlc_subscr_test.c b/tests/smlc_subscr/smlc_subscr_test.c
new file mode 100644
index 0000000..92b7293
--- /dev/null
+++ b/tests/smlc_subscr/smlc_subscr_test.c
@@ -0,0 +1,157 @@
+/*
+ * (C) 2020 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/smlc/debug.h>
+#include <osmocom/smlc/smlc_data.h>
+#include <osmocom/smlc/smlc_subscr.h>
+
+#include <osmocom/core/application.h>
+#include <osmocom/core/utils.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <inttypes.h>
+
+struct smlc_state *g_smlc;
+
+#define VERBOSE_ASSERT(val, expect_op, fmt) \
+	do { \
+		printf(#val " == " fmt "\n", (val)); \
+		OSMO_ASSERT((val) expect_op); \
+	} while (0);
+
+#define USE_FOO "foo"
+#define USE_BAR "bar"
+
+static void assert_smlc_subscr(const struct smlc_subscr *smlc_subscr, const struct osmo_mobile_identity *imsi)
+{
+	struct smlc_subscr *sfound;
+	OSMO_ASSERT(smlc_subscr);
+	OSMO_ASSERT(osmo_mobile_identity_cmp(&smlc_subscr->imsi, imsi) == 0);
+
+	sfound = smlc_subscr_find(imsi, __func__);
+	OSMO_ASSERT(sfound == smlc_subscr);
+
+	smlc_subscr_put(sfound, __func__);
+}
+
+static void test_smlc_subscr(void)
+{
+	struct smlc_subscr *s1, *s2, *s3;
+	const struct osmo_mobile_identity imsi1 = { .type = GSM_MI_TYPE_IMSI, .imsi = "1234567890", };
+	const struct osmo_mobile_identity imsi2 = { .type = GSM_MI_TYPE_IMSI, .imsi = "9876543210", };
+	const struct osmo_mobile_identity imsi3 = { .type = GSM_MI_TYPE_IMSI, .imsi = "423423", };
+
+	printf("Test SMLC subscriber allocation and deletion\n");
+
+	/* Check for emptiness */
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 0, "%d");
+	OSMO_ASSERT(smlc_subscr_find(&imsi1, "-") == NULL);
+	OSMO_ASSERT(smlc_subscr_find(&imsi2, "-") == NULL);
+	OSMO_ASSERT(smlc_subscr_find(&imsi3, "-") == NULL);
+
+	/* Allocate entry 1 */
+	s1 = smlc_subscr_find_or_create(&imsi1, USE_FOO);
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 1, "%d");
+	assert_smlc_subscr(s1, &imsi1);
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 1, "%d");
+	OSMO_ASSERT(smlc_subscr_find(&imsi2, "-") == NULL);
+
+	/* Allocate entry 2 */
+	s2 = smlc_subscr_find_or_create(&imsi2, USE_BAR);
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 2, "%d");
+
+	/* Allocate entry 3 */
+	s3 = smlc_subscr_find_or_create(&imsi3, USE_FOO);
+	smlc_subscr_get(s3, USE_BAR);
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 3, "%d");
+
+	/* Check entries */
+	assert_smlc_subscr(s1, &imsi1);
+	assert_smlc_subscr(s2, &imsi2);
+	assert_smlc_subscr(s3, &imsi3);
+
+	/* Free entry 1 */
+	smlc_subscr_put(s1, USE_FOO);
+	s1 = NULL;
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 2, "%d");
+	OSMO_ASSERT(smlc_subscr_find(&imsi1, "-") == NULL);
+
+	assert_smlc_subscr(s2, &imsi2);
+	assert_smlc_subscr(s3, &imsi3);
+
+	/* Free entry 2 */
+	smlc_subscr_put(s2, USE_BAR);
+	s2 = NULL;
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 1, "%d");
+	OSMO_ASSERT(smlc_subscr_find(&imsi1, "-") == NULL);
+	OSMO_ASSERT(smlc_subscr_find(&imsi2, "-") == NULL);
+	assert_smlc_subscr(s3, &imsi3);
+
+	/* Remove one use of entry 3 */
+	smlc_subscr_put(s3, USE_BAR);
+	assert_smlc_subscr(s3, &imsi3);
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 1, "%d");
+
+	/* Free entry 3 */
+	smlc_subscr_put(s3, USE_FOO);
+	s3 = NULL;
+	VERBOSE_ASSERT(llist_count(&g_smlc->subscribers), == 0, "%d");
+	OSMO_ASSERT(smlc_subscr_find(&imsi3, "-") == NULL);
+
+	OSMO_ASSERT(llist_empty(&g_smlc->subscribers));
+}
+
+static const struct log_info_cat log_categories[] = {
+	[DREF] = {
+		.name = "DREF",
+		.description = "Reference Counting",
+		.enabled = 1, .loglevel = LOGL_DEBUG,
+	},
+};
+
+static const struct log_info log_info = {
+	.cat = log_categories,
+	.num_cat = ARRAY_SIZE(log_categories),
+};
+
+int main()
+{
+	void *ctx = talloc_named_const(NULL, 0, "smlc_subscr_test");
+
+	osmo_init_logging2(ctx, &log_info);
+	log_set_print_filename(osmo_stderr_target, 0);
+	log_set_print_timestamp(osmo_stderr_target, 0);
+	log_set_use_color(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+
+	g_smlc = smlc_state_alloc(ctx);
+
+	printf("Testing SMLC subscriber code.\n");
+
+	test_smlc_subscr();
+
+	printf("Done\n");
+	return 0;
+}
+
diff --git a/tests/smlc_subscr/smlc_subscr_test.err b/tests/smlc_subscr/smlc_subscr_test.err
new file mode 100644
index 0000000..8e0d1fa
--- /dev/null
+++ b/tests/smlc_subscr/smlc_subscr_test.err
@@ -0,0 +1,24 @@
+DREF IMSI-1234567890[1 (foo)]: + foo
+DREF IMSI-1234567890[2 (foo,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-1234567890[1 (foo)]: - assert_smlc_subscr
+DREF IMSI-9876543210[1 (bar)]: + bar
+DREF IMSI-423423[1 (foo)]: + foo
+DREF IMSI-423423[2 (foo,bar)]: + bar
+DREF IMSI-1234567890[2 (foo,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-1234567890[1 (foo)]: - assert_smlc_subscr
+DREF IMSI-9876543210[2 (bar,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-9876543210[1 (bar)]: - assert_smlc_subscr
+DREF IMSI-423423[3 (foo,bar,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-423423[2 (foo,bar)]: - assert_smlc_subscr
+DREF IMSI-1234567890[0 (-)]: - foo
+DREF IMSI-9876543210[2 (bar,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-9876543210[1 (bar)]: - assert_smlc_subscr
+DREF IMSI-423423[3 (foo,bar,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-423423[2 (foo,bar)]: - assert_smlc_subscr
+DREF IMSI-9876543210[0 (-)]: - bar
+DREF IMSI-423423[3 (foo,bar,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-423423[2 (foo,bar)]: - assert_smlc_subscr
+DREF IMSI-423423[1 (foo)]: - bar
+DREF IMSI-423423[2 (foo,assert_smlc_subscr)]: + assert_smlc_subscr
+DREF IMSI-423423[1 (foo)]: - assert_smlc_subscr
+DREF IMSI-423423[0 (-)]: - foo
diff --git a/tests/smlc_subscr/smlc_subscr_test.ok b/tests/smlc_subscr/smlc_subscr_test.ok
new file mode 100644
index 0000000..c85007d
--- /dev/null
+++ b/tests/smlc_subscr/smlc_subscr_test.ok
@@ -0,0 +1,12 @@
+Testing SMLC subscriber code.
+Test SMLC subscriber allocation and deletion
+llist_count(&g_smlc->subscribers) == 0
+llist_count(&g_smlc->subscribers) == 1
+llist_count(&g_smlc->subscribers) == 1
+llist_count(&g_smlc->subscribers) == 2
+llist_count(&g_smlc->subscribers) == 3
+llist_count(&g_smlc->subscribers) == 2
+llist_count(&g_smlc->subscribers) == 1
+llist_count(&g_smlc->subscribers) == 1
+llist_count(&g_smlc->subscribers) == 0
+Done
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 09a77c3..0a3b9bb 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -1,2 +1,9 @@
 AT_INIT
 AT_BANNER([Regression tests.])
+
+AT_SETUP([smlc_subscr])
+AT_KEYWORDS([smlc_subscr])
+cat $abs_srcdir/smlc_subscr/smlc_subscr_test.ok > expout
+cat $abs_srcdir/smlc_subscr/smlc_subscr_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/smlc_subscr/smlc_subscr_test], [], [expout], [experr])
+AT_CLEANUP

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

Gerrit-Project: osmo-smlc
Gerrit-Branch: master
Gerrit-Change-Id: I917ba8fc51a1f1150be77ae01e12a7b16a853052
Gerrit-Change-Number: 20470
Gerrit-PatchSet: 7
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-CC: fixeria <vyanitskiy at sysmocom.de>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20201016/403ccd4a/attachment.htm>


More information about the gerrit-log mailing list