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
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@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels@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@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels@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@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels@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@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@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@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@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@sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels@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@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@sysmocom.de>
+ *
+ * Author: Neels Hofmeyr <neels@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@sysmocom.de>
+ *
+ * Author: Neels Hofmeyr <neels@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