osmo-bsc/src/osmo-bsc/smscb.c

1162 lines
39 KiB
C

/* SMSCB (SMS Cell Broadcast) Handling for OsmoBSC */
/*
* (C) 2019 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/licenses/>.
*
*/
#include <limits.h>
#include <osmocom/core/stats.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/select.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/byteswap.h>
#include <osmocom/core/signal.h>
#include <osmocom/gsm/cbsp.h>
#include <osmocom/gsm/protocol/gsm_23_041.h>
#include <osmocom/gsm/protocol/gsm_48_049.h>
#include <osmocom/gsm/protocol/gsm_03_41.h>
#include <osmocom/netif/stream.h>
#include <osmocom/bsc/debug.h>
#include <osmocom/bsc/gsm_data.h>
#include <osmocom/bsc/smscb.h>
#include <osmocom/bsc/gsm_04_08_rr.h>
#include <osmocom/bsc/lchan_fsm.h>
#include <osmocom/bsc/abis_rsl.h>
#include <osmocom/bsc/bts.h>
#include <osmocom/bsc/signal.h>
/*********************************************************************************
* Helper Functions
*********************************************************************************/
/* replace the old head of an entire list with a new head; effectively moves the entire
* list from old to new head */
static void llist_replace_head(struct llist_head *new, struct llist_head *old)
{
if (llist_empty(old))
INIT_LLIST_HEAD(new);
else
__llist_add(new, old->prev, old->next);
INIT_LLIST_HEAD(old);
}
/* Build a ETWS Primary Notification message as per TS 23.041 9.4.1.3 */
static int gen_etws_primary_notification(uint8_t *out, uint16_t serial_nr, uint16_t msg_id,
uint16_t warn_type, const uint8_t *sec_info)
{
struct gsm341_etws_message *etws = (struct gsm341_etws_message *)out;
memset(out, 0, ETWS_PRIM_NOTIF_SIZE);
osmo_store16be(serial_nr, out);
etws->msg_id = osmo_htons(msg_id);
etws->warning_type = osmo_htons(warn_type);
memcpy(etws->data, sec_info, ETWS_PRIM_NOTIF_SIZE - sizeof(*etws));
return ETWS_PRIM_NOTIF_SIZE;
}
static void bts_cbch_init_state(struct bts_smscb_chan_state *cstate, struct gsm_bts *bts)
{
cstate->bts = bts;
INIT_LLIST_HEAD(&cstate->messages);
}
void bts_cbch_init(struct gsm_bts *bts)
{
bts_cbch_init_state(&bts->cbch_basic, bts);
bts_cbch_init_state(&bts->cbch_extended, bts);
osmo_timer_setup(&bts->cbch_timer, &bts_cbch_timer_cb, bts);
}
/*! Obtain SMSCB Channel State for given BTS (basic or extended CBCH) */
struct bts_smscb_chan_state *bts_get_smscb_chan(struct gsm_bts *bts, bool extended)
{
struct bts_smscb_chan_state *chan_state;
if (extended)
chan_state = &bts->cbch_extended;
else
chan_state = &bts->cbch_basic;
return chan_state;
}
/* do an ordered list insertion. we keep the list with increasing period, i.e. the most
* frequent message first */
static void __bts_smscb_add(struct bts_smscb_chan_state *cstate, struct bts_smscb_message *new)
{
struct bts_smscb_message *tmp, *tmp2;
if (llist_empty(&cstate->messages)) {
llist_add(&new->list, &cstate->messages);
return;
}
llist_for_each_entry_safe(tmp, tmp2, &cstate->messages, list) {
if (tmp->input.rep_period > new->input.rep_period) {
/* we found the first message with longer period than the new message,
* we must insert ourselves before that one */
__llist_add(&new->list, tmp->list.prev, &tmp->list);
return;
}
}
/* we didn't find any messages with longer period than us, insert us at tail */
llist_add_tail(&new->list, &cstate->messages);
}
/* stringify a SMSCB for logging */
const char *bts_smscb_msg2str(const struct bts_smscb_message *smscb)
{
static char buf[128];
snprintf(buf, sizeof(buf), "MsgId=0x%04x/SerialNr=0x%04x/Pages=%u/Period=%u/NumBcastReq=%u",
smscb->input.msg_id, smscb->input.serial_nr, smscb->num_pages,
smscb->input.rep_period, smscb->input.num_bcast_req);
return buf;
}
const char *bts_smscb_chan_state_name(const struct bts_smscb_chan_state *cstate)
{
if (cstate == &cstate->bts->cbch_basic)
return "BASIC";
else if (cstate == &cstate->bts->cbch_extended)
return "EXTENDED";
else
return "UNKNOWN";
}
unsigned int bts_smscb_chan_load_percent(const struct bts_smscb_chan_state *cstate)
{
unsigned int sched_arr_used = 0;
unsigned int i;
if (cstate->sched_arr_size == 0)
return 0;
/* count the number of used slots */
for (i = 0; i < cstate->sched_arr_size; i++) {
if (cstate->sched_arr[i])
sched_arr_used++;
}
OSMO_ASSERT(sched_arr_used <= UINT_MAX/100);
return (sched_arr_used * 100) / cstate->sched_arr_size;
}
unsigned int bts_smscb_chan_page_count(const struct bts_smscb_chan_state *cstate)
{
struct bts_smscb_message *smscb;
unsigned int page_count = 0;
llist_for_each_entry(smscb, &cstate->messages, list)
page_count += smscb->num_pages;
return page_count;
}
/*! Obtain the Cell Global Identifier (CGI) of given BTS; returned in static buffer. */
static struct osmo_cell_global_id *bts_get_cgi(struct gsm_bts *bts)
{
static struct osmo_cell_global_id cgi;
cgi.lai.plmn = bts->network->plmn;
cgi.lai.lac = bts->location_area_code;
cgi.cell_identity = bts->cell_identity;
return &cgi;
}
/* represents the various lists that the BSC can create as part of a response */
struct response_state {
struct osmo_cbsp_cell_list success; /* osmo_cbsp_cell_ent */
struct llist_head fail; /* osmo_cbsp_fail_ent */
struct osmo_cbsp_num_compl_list num_completed; /* osmo_cbsp_num_compl_ent */
struct osmo_cbsp_loading_list loading; /* osmo_cbsp_loading_ent */
};
/*! per-BTS callback function used by cbsp_per_bts().
* \param[in] bts BTS currently being processed
* \param[in] dec decoded CBSP message currently being processed
* \param r_state response state accumulating cell lists (success/failure/...)
* \param priv opaque private data provided by caller of cbsp_per_bts()
* \returns 0 on success; negative TS 48.049 cause value on error */
typedef int bts_cb_fn(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
struct response_state *r_state, void *priv);
/* append a success for given cell to response state */
static void append_success(struct response_state *r_state, struct gsm_bts *bts)
{
struct osmo_cbsp_cell_ent *cent = talloc_zero(r_state, struct osmo_cbsp_cell_ent);
struct osmo_cell_global_id *cgi = bts_get_cgi(bts);
LOG_BTS(bts, DCBS, LOGL_INFO, "Success\n");
OSMO_ASSERT(cent);
cent->cell_id.global = *cgi;
llist_add_tail(&cent->list, &r_state->success.list);
}
/* append a failure for given cell to response state */
static void append_fail(struct response_state *r_state, struct gsm_bts *bts, uint8_t cause)
{
struct osmo_cbsp_fail_ent *fent = talloc_zero(r_state, struct osmo_cbsp_fail_ent);
struct osmo_cell_global_id *cgi = bts_get_cgi(bts);
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Failure Cause 0x%02x\n", cause);
OSMO_ASSERT(fent);
fent->id_discr = CELL_IDENT_WHOLE_GLOBAL;
fent->cell_id.global = *cgi;
fent->cause = cause;
llist_add_tail(&fent->list, &r_state->fail);
}
/* append a 'number of broadcasts completed' for given cell to response state */
static void append_bcast_compl(struct response_state *r_state, struct gsm_bts *bts,
struct bts_smscb_message *smscb)
{
struct osmo_cbsp_num_compl_ent *cent = talloc_zero(r_state, struct osmo_cbsp_num_compl_ent);
struct osmo_cell_global_id *cgi = bts_get_cgi(bts);
LOG_BTS(bts, DCBS, LOGL_DEBUG, "Number of Broadcasts Completed: %u\n", smscb->bcast_count);
OSMO_ASSERT(cent);
r_state->num_completed.id_discr = CELL_IDENT_WHOLE_GLOBAL;
cent->cell_id.global = *cgi;
if (smscb->bcast_count > INT16_MAX) {
cent->num_compl = INT16_MAX;
cent->num_bcast_info = 0x01; /* Overflow */
} else {
cent->num_compl = smscb->bcast_count;
cent->num_bcast_info = 0x00;
}
llist_add_tail(&cent->list, &r_state->num_completed.list);
}
static bool etws_msg_id_matches(uint16_t a, uint16_t b)
{
/* ETWS messages are identified by the twelve most significant bits of the Message ID */
return (a & 0xFFF0) == (b & 0xFFF0);
}
/*! Iterate over all BTSs, find matching ones, execute command on BTS, add result
* to succeeded/failed lists.
* \param[in] net GSM network in which we operate
* \param[in] caller-allocated Response state structure collecting results
* \param[in] cell_list Decoded CBSP cell list describing BTSs to operate on
* \param[in] cb_fn Call-back function to call for each matching BTS
* \param[in] priv Opqaue private data; passed to cb_fn
* */
static int cbsp_per_bts(struct gsm_network *net, struct response_state *r_state,
const struct osmo_cbsp_cell_list *cell_list,
bts_cb_fn *cb_fn, const struct osmo_cbsp_decoded *dec, void *priv)
{
struct osmo_cbsp_cell_ent *ent;
struct gsm_bts *bts;
uint8_t bts_status[net->num_bts];
int rc, ret = 0;
memset(bts_status, 0, sizeof(bts_status));
INIT_LLIST_HEAD(&r_state->success.list);
INIT_LLIST_HEAD(&r_state->fail);
INIT_LLIST_HEAD(&r_state->num_completed.list);
INIT_LLIST_HEAD(&r_state->loading.list);
/* special case as cell_list->list is empty in this case */
if (cell_list->id_discr == CELL_IDENT_BSS) {
llist_for_each_entry(bts, &net->bts_list, list) {
bts_status[bts->nr] = 1;
/* call function on this BTS */
rc = cb_fn(bts, dec, r_state, priv);
if (rc < 0) {
append_fail(r_state, bts, -rc);
ret = -1;
} else
append_success(r_state, bts);
}
} else {
/* normal case: iterate over cell list */
llist_for_each_entry(ent, &cell_list->list, list) {
bool found_at_least_one = false;
/* find all matching BTSs for this entry */
llist_for_each_entry(bts, &net->bts_list, list) {
struct gsm0808_cell_id cell_id = {
.id_discr = cell_list->id_discr,
.id = ent->cell_id
};
if (!gsm_bts_matches_cell_id(bts, &cell_id))
continue;
found_at_least_one = true;
/* skip any BTSs which we've already processed */
if (bts_status[bts->nr])
continue;
bts_status[bts->nr] = 1;
/* call function on this BTS */
rc = cb_fn(bts, dec, r_state, priv);
if (rc < 0) {
append_fail(r_state, bts, -rc);
ret = -1;
} else
append_success(r_state, bts);
}
if (!found_at_least_one) {
struct osmo_cbsp_fail_ent *fent;
LOGP(DCBS, LOGL_NOTICE, "CBSP: Couldn't find a single matching BTS\n");
fent = talloc_zero(r_state, struct osmo_cbsp_fail_ent);
OSMO_ASSERT(fent);
fent->id_discr = cell_list->id_discr;
fent->cell_id = ent->cell_id;
llist_add_tail(&fent->list, &r_state->fail);
ret = -1;
}
}
}
return ret;
}
/*! Find an existing SMSCB message within given BTS.
* \param[in] chan_state BTS CBCH channel state
* \param[in] msg_id Message Id of to-be-found message
* \param[in] serial_nr Serial Number of to-be-found message
* \returns SMSCB message if found; NULL otherwise */
struct bts_smscb_message *bts_find_smscb(struct bts_smscb_chan_state *chan_state,
uint16_t msg_id, uint16_t serial_nr)
{
struct bts_smscb_message *smscb;
llist_for_each_entry(smscb, &chan_state->messages, list) {
if (smscb->input.msg_id == msg_id && smscb->input.serial_nr == serial_nr)
return smscb;
}
return NULL;
}
/*! create a new SMSCB message for specified BTS; don't link it yet.
* \param[in] bts BTS for which the SMSCB is to be allocated
* \param[in] wrepl CBSP write-replace message
* \returns callee-allocated SMSCB message filled with data from wrepl */
static struct bts_smscb_message *bts_smscb_msg_from_wrepl(struct gsm_bts *bts,
const struct osmo_cbsp_write_replace *wrepl)
{
struct bts_smscb_message *smscb = talloc_zero(bts, struct bts_smscb_message);
struct osmo_cbsp_content *cont;
int i;
if (!smscb)
return NULL;
OSMO_ASSERT(wrepl->is_cbs);
/* initialize all pages inside the message */
for (i = 0; i < ARRAY_SIZE(smscb->page); i++) {
struct bts_smscb_page *page = &smscb->page[i];
page->nr = i+1; /* page numbers are 1-based */
page->msg = smscb;
}
/* initialize "header" part */
smscb->input.msg_id = wrepl->msg_id;
smscb->input.serial_nr = wrepl->new_serial_nr;
smscb->input.category = wrepl->u.cbs.category;
smscb->input.rep_period = wrepl->u.cbs.rep_period;
smscb->input.num_bcast_req = wrepl->u.cbs.num_bcast_req;
smscb->input.dcs = wrepl->u.cbs.dcs;
smscb->num_pages = llist_count(&wrepl->u.cbs.msg_content);
if (smscb->num_pages > ARRAY_SIZE(smscb->page)) {
LOG_BTS(bts, DCBS, LOGL_ERROR, "SMSCB with too many pages (%u > %zu)\n",
smscb->num_pages, ARRAY_SIZE(smscb->page));
talloc_free(smscb);
return NULL;
}
i = 0;
llist_for_each_entry(cont, &wrepl->u.cbs.msg_content, list) {
struct gsm23041_msg_param_gsm *msg_param;
struct bts_smscb_page *page;
size_t bytes_used;
/* we have just ensured a few lines above that this cannot overflow */
page = &smscb->page[i++];
msg_param = (struct gsm23041_msg_param_gsm *) &page->data[0];
/* ensure we don't overflow in the memcpy below */
osmo_static_assert(sizeof(*page) > sizeof(*msg_param) + sizeof(cont->data), smscb_space);
/* build 6 byte header according to TS 23.041 9.4.1.2 */
osmo_store16be(wrepl->new_serial_nr, &msg_param->serial_nr);
osmo_store16be(wrepl->msg_id, &msg_param->message_id);
msg_param->dcs = wrepl->u.cbs.dcs;
msg_param->page_param.num_pages = smscb->num_pages;
msg_param->page_param.page_nr = page->nr;
OSMO_ASSERT(cont->user_len <= ARRAY_SIZE(cont->data));
OSMO_ASSERT(cont->user_len <= ARRAY_SIZE(page->data) - sizeof(*msg_param));
/* we must not use cont->user_len as length here, as it would truncate any
* possible 7-bit padding at the end. Always copy the whole page */
memcpy(&msg_param->content, cont->data, sizeof(cont->data));
bytes_used = sizeof(*msg_param) + cont->user_len;
/* compute number of valid blocks in page */
page->num_blocks = bytes_used / 22;
if (bytes_used % 22)
page->num_blocks += 1;
}
return smscb;
}
/*! remove a SMSCB message */
void bts_smscb_del(struct bts_smscb_message *smscb, struct bts_smscb_chan_state *cstate,
const char *reason)
{
struct bts_smscb_page **arr;
int rc;
LOG_BTS(cstate->bts, DCBS, LOGL_INFO, "%s Deleting %s (Reason: %s)\n",
bts_smscb_chan_state_name(cstate), bts_smscb_msg2str(smscb), reason);
llist_del(&smscb->list);
/* we must recompute the scheduler array here, as the old one will have pointers
* to the pages of the just-to-be-deleted message */
rc = bts_smscb_gen_sched_arr(cstate, &arr);
if (rc < 0) {
LOG_BTS(cstate->bts, DCBS, LOGL_ERROR, "Cannot generate new CBCH scheduler array after "
"removing message %s. WTF?\n", bts_smscb_msg2str(smscb));
/* we cannot free the message now, to ensure the page pointers in the old
* array are still valid. let's re-add it to keep things sane */
__bts_smscb_add(cstate, smscb);
} else {
/* success */
talloc_free(smscb);
/* replace array with new one */
talloc_free(cstate->sched_arr);
cstate->sched_arr = arr;
cstate->sched_arr_size = rc;
cstate->next_idx = 0;
}
}
/*********************************************************************************
* Transmit of CBSP to CBC
*********************************************************************************/
/* transmit a CBSP RESTART message stating all message data was lost for entire BSS */
int cbsp_tx_restart(struct bsc_cbc_link *cbc, bool is_emerg)
{
struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESTART);
if (is_emerg)
cbsp->u.restart.bcast_msg_type = 0x01;
cbsp->u.restart.recovery_ind = 0x01; /* message data lost */
cbsp->u.restart.cell_list.id_discr = CELL_IDENT_BSS;
return cbsp_tx_decoded(cbc, cbsp);
}
/* transmit a CBSP RESTART-INDICATION message stating a cell is operative again */
int cbsp_tx_restart_bts(struct bsc_cbc_link *cbc, bool is_emerg, struct gsm_bts *bts)
{
struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESTART);
struct osmo_cbsp_cell_ent cell_ent;
if (is_emerg)
cbsp->u.restart.bcast_msg_type = 0x01;
cbsp->u.restart.recovery_ind = 0x00; /* message data available */
cbsp->u.restart.cell_list.id_discr = CELL_IDENT_LAC_AND_CI;
cell_ent = (struct osmo_cbsp_cell_ent){
.cell_id = {
.lac_and_ci = {
.lac = bts->location_area_code,
.ci = bts->cell_identity,
}
}
};
llist_add(&cell_ent.list, &cbsp->u.restart.cell_list.list);
return cbsp_tx_decoded(cbc, cbsp);
}
/* transmit a CBSP FAILURE-INDICATION message stating all message data was lost for one cell */
int cbsp_tx_failure_bts(struct bsc_cbc_link *cbc, bool is_emerg, struct gsm_bts *bts)
{
struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_FAILURE);
struct osmo_cbsp_fail_ent fail_ent;
if (is_emerg)
cbsp->u.failure.bcast_msg_type = 0x01;
fail_ent = (struct osmo_cbsp_fail_ent){
.id_discr = CELL_IDENT_LAC_AND_CI,
.cell_id = {
.lac_and_ci = {
.lac = bts->location_area_code,
.ci = bts->cell_identity,
}
},
.cause = OSMO_CBSP_CAUSE_CELL_BROADCAST_NOT_OPERATIONAL
};
llist_add(&fail_ent.list, &cbsp->u.failure.fail_list);
return cbsp_tx_decoded(cbc, cbsp);
}
/* transmit a CBSP KEEPALIVE COMPLETE to the CBC */
static int tx_cbsp_keepalive_compl(struct bsc_cbc_link *cbc)
{
struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KEEP_ALIVE_COMPL);
return cbsp_tx_decoded(cbc, cbsp);
}
/*********************************************************************************
* Per-BTS Processing of CBSP from CBC, called via cbsp_per_bts()
*********************************************************************************/
static void etws_pn_stop(struct gsm_bts *bts, bool timeout)
{
if (osmo_bts_has_feature(&bts->features, BTS_FEAT_ETWS_PN)) {
LOG_BTS(bts, DCBS, LOGL_NOTICE, "ETWS PN broadcast via PCH disabled (cause=%s)\n",
timeout ? "timeout" : "request");
rsl_etws_pn_command(bts, RSL_CHAN_PCH_AGCH, NULL, 0);
}
bts->etws.active = false;
if (!timeout)
osmo_timer_del(&bts->etws.timer);
}
/* timer call-back once ETWS warning period has expired */
static void etws_pn_cb(void *data)
{
struct gsm_bts *bts = (struct gsm_bts *)data;
etws_pn_stop(bts, true);
}
/* the actual "execution" part: Send ETWS to all active lchan in the BTS and via PCH */
static void bts_send_etws(struct gsm_bts *bts)
{
struct bts_etws_state *bes = &bts->etws;
struct gsm_bts_trx *trx;
unsigned int count = 0;
int i, j;
/* iterate over all lchan in each TS in each TRX of this BTS */
llist_for_each_entry(trx, &bts->trx_list, list) {
for (i = 0; i < ARRAY_SIZE(trx->ts); i++) {
struct gsm_bts_trx_ts *ts = &trx->ts[i];
for (j = 0; j < ARRAY_SIZE(ts->lchan); j++) {
struct gsm_lchan *lchan = &ts->lchan[j];
if (!lchan_may_receive_data(lchan))
continue;
gsm48_send_rr_app_info(lchan, 0x1, 0x0, bes->primary,
sizeof(bes->primary));
count++;
}
}
}
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Sent ETWS Primary Notification via %u dedicated channels\n",
count);
/* Notify BTS of primary ETWS notification via vendor-specific Abis message */
if (osmo_bts_has_feature(&bts->features, BTS_FEAT_ETWS_PN)) {
rsl_etws_pn_command(bts, RSL_CHAN_PCH_AGCH, bes->primary, sizeof(bes->primary));
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Sent ETWS Primary Notification via common channel\n");
} else
LOG_BTS(bts, DCBS, LOGL_ERROR, "BTS doesn't support RSL command for ETWS PN\n");
}
static int etws_primary_to_bts(struct gsm_bts *bts, const struct osmo_cbsp_write_replace *wrepl)
{
struct bts_etws_state *bes = &bts->etws;
if (bes->active) {
/* we were already broadcasting emergency before receiving this WRITE-REPLACE */
/* If only the New Serial Number IE, and not the Old Serial Number IE, is included in the
* WRITE-REPLACE message, then the BSC shall interpret the message as a write request, i.e. a
* broadcast request of a new emergency message without replacing an ongoing emergency message
* broadcast. */
if (!wrepl->old_serial_nr) {
/* If a write request is received for a cell where an emergency message broadcast is
* currently ongoing, the write request is considered as failed */
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Rx CBSP WRITE rejected due to ongoing emergency "
"while no Old Serial Nr IE present in CBSP WRITE\n");
return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED;
}
if (!etws_msg_id_matches(*wrepl->old_serial_nr, bes->input.serial_nr)) {
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Rx CBSP WRITE-REPLACE old_serial 0x%04x doesn't match "
"current serial 0x%04x. Is the CBC confused?\n",
*wrepl->old_serial_nr, bes->input.serial_nr);
/* we allow the WRITE-REPLACE to continue, TS 48.049 doesn't specify how to
* handle situations like this */
}
}
/* copy over all the data to per-BTS private state */
bes->input.msg_id = wrepl->msg_id;
bes->input.serial_nr = wrepl->new_serial_nr;
bes->input.warn_type = wrepl->u.emergency.warning_type;
memcpy(bes->input.sec_info, wrepl->u.emergency.warning_sec_info, sizeof(bes->input.sec_info));
/* generate the encoded ETWS PN */
gen_etws_primary_notification(bes->primary, bes->input.serial_nr, bes->input.msg_id,
bes->input.warn_type, bes->input.sec_info);
bes->active = true;
bts_send_etws(bts);
/* start the expiration timer, if any */
if (wrepl->u.emergency.warning_period != 0xffffffff) {
osmo_timer_schedule(&bts->etws.timer, wrepl->u.emergency.warning_period, 0);
} else
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Unlimited ETWS PN broadcast, this breaks "
"normal network operation due to PCH blockage\n");
return 0;
}
/*! Try to execute a write-replace operation; roll-back if it fails.
* \param[in] chan_state BTS CBCH channel state
* \param[in] extended_cbch Basic (false) or Extended (true) CBCH
* \param[in] new_msg New SMSCB message which should be added
* \param[in] exclude_msg Existing SMSCB message that shall be replaced (if possible). Can be NULL
* \return 0 on success; negative on error */
static int bts_try_write_replace(struct bts_smscb_chan_state *chan_state,
struct bts_smscb_message *new_msg,
struct bts_smscb_message *exclude_msg,
struct response_state *r_state)
{
struct bts_smscb_page **arr;
int rc;
if (exclude_msg) {
/* temporarily remove from list of SMSCB */
llist_del(&exclude_msg->list);
}
/* temporarily add new_msg to list of SMSCB */
__bts_smscb_add(chan_state, new_msg);
/* attempt to create scheduling array */
rc = bts_smscb_gen_sched_arr(chan_state, &arr);
if (rc < 0) {
/* it didn't work out; we couldn't schedule it */
/* remove the new message again */
llist_del(&new_msg->list);
/* up to the caller to free() it */
if (exclude_msg) {
/* re-add the temporarily removed message */
__bts_smscb_add(chan_state, new_msg);
}
return -1;
}
/* success! */
if (exclude_msg) {
LOG_BTS(chan_state->bts, DCBS, LOGL_INFO, "%s Replaced MsgId=0x%04x/Serial=0x%04x, "
"pages(%u -> %u), period(%u -> %u), num_bcast(%u -> %u)\n",
bts_smscb_chan_state_name(chan_state),
new_msg->input.msg_id, new_msg->input.serial_nr,
exclude_msg->num_pages, new_msg->num_pages,
exclude_msg->input.rep_period, new_msg->input.rep_period,
exclude_msg->input.num_bcast_req, new_msg->input.num_bcast_req);
append_bcast_compl(r_state, chan_state->bts, exclude_msg);
talloc_free(exclude_msg);
} else
LOG_BTS(chan_state->bts, DCBS, LOGL_INFO, "%s Added %s\n",
bts_smscb_chan_state_name(chan_state), bts_smscb_msg2str(new_msg));
/* replace array with new one */
talloc_free(chan_state->sched_arr);
chan_state->sched_arr = arr;
chan_state->sched_arr_size = rc;
chan_state->next_idx = 0;
return 0;
}
static int bts_rx_write_replace(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
struct response_state *r_state, void *priv)
{
const struct osmo_cbsp_write_replace *wrepl = &dec->u.write_replace;
bool extended_cbch = wrepl->u.cbs.channel_ind;
struct bts_smscb_chan_state *chan_state = bts_get_smscb_chan(bts, extended_cbch);
struct bts_smscb_message *smscb;
int rc;
if (!wrepl->is_cbs) {
return etws_primary_to_bts(bts, wrepl);
}
/* check if cell has a CBCH at all */
if (!gsm_bts_get_cbch(bts))
return -CBSP_CAUSE_CB_NOT_SUPPORTED;
/* check for duplicate */
if (bts_find_smscb(chan_state, wrepl->msg_id, wrepl->new_serial_nr))
return -CBSP_CAUSE_MSG_REF_ALREADY_USED;
if (!wrepl->old_serial_nr) { /* new message */
/* create new message */
smscb = bts_smscb_msg_from_wrepl(bts, wrepl);
if (!smscb)
return -CBSP_CAUSE_BSC_MEMORY_EXCEEDED;
/* check if scheduling permits this additional message */
rc = bts_try_write_replace(chan_state, smscb, NULL, r_state);
if (rc < 0) {
talloc_free(smscb);
return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED;
}
} else { /* modify / replace existing message */
struct bts_smscb_message *smscb_old;
/* find existing message */
smscb_old = bts_find_smscb(chan_state, wrepl->msg_id, *wrepl->old_serial_nr);
if (!smscb_old)
return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
/* create new message */
smscb = bts_smscb_msg_from_wrepl(bts, wrepl);
if (!smscb)
return -CBSP_CAUSE_BSC_MEMORY_EXCEEDED;
/* check if scheduling permits this modified message */
rc = bts_try_write_replace(chan_state, smscb, smscb_old, r_state);
if (rc < 0) {
talloc_free(smscb);
return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED;
}
}
return 0;
}
static int bts_rx_kill(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
struct response_state *r_state, void *priv)
{
const struct osmo_cbsp_kill *kill = &dec->u.kill;
if (kill->channel_ind) {
/* KILL for CBS message */
struct bts_smscb_chan_state *chan_state;
struct bts_smscb_message *smscb;
bool extended = false;
if (*kill->channel_ind == 0x01)
extended = true;
chan_state = bts_get_smscb_chan(bts, extended);
/* Find message by msg_id + old_serial_nr */
smscb = bts_find_smscb(chan_state, kill->msg_id, kill->old_serial_nr);
if (!smscb)
return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
append_bcast_compl(r_state, chan_state->bts, smscb);
/* Remove it */
bts_smscb_del(smscb, chan_state, "KILL");
} else {
/* KILL for Emergency */
struct bts_etws_state *bes = &bts->etws;
if (!bes->active) {
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Rx CBSP KILL (Emerg) but no emergency "
"broadcast is currently active in this cell\n");
return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
}
if (kill->msg_id != bes->input.msg_id) {
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Rx CBSP KILL (Emerg) for msg_id 0x%04x, but "
"current emergency msg_id is 0x%04x\n", kill->msg_id, bes->input.msg_id);
return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
}
if (!etws_msg_id_matches(kill->old_serial_nr, bes->input.serial_nr)) {
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Rx CBSP KILL (Emerg) for old_serial_nr 0x%04x, but "
"current emergency serial_nr is 0x%04x\n",
kill->old_serial_nr, bes->input.serial_nr);
return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
}
/* stop broadcasting the PN in this BTS */
etws_pn_stop(bts, false);
}
return 0;
}
static int bts_rx_reset(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
struct response_state *r_state, void *priv)
{
struct bts_smscb_chan_state *chan_state;
struct bts_smscb_message *smscb, *smscb2;
LOG_BTS(bts, DCBS, LOGL_NOTICE, "Rx CBSP RESET: clearing all state; disabling broadcast\n");
/* remove all SMSCB from CBCH BASIC this BTS */
chan_state = bts_get_smscb_chan(bts, false);
llist_for_each_entry_safe(smscb, smscb2, &chan_state->messages, list)
bts_smscb_del(smscb, chan_state, "RESET");
/* remove all SMSCB from CBCH EXTENDED this BTS */
chan_state = bts_get_smscb_chan(bts, true);
llist_for_each_entry_safe(smscb, smscb2, &chan_state->messages, list)
bts_smscb_del(smscb, chan_state, "RESET");
osmo_timer_del(&bts->etws.timer);
/* Make sure that broadcast is disabled */
rsl_etws_pn_command(bts, RSL_CHAN_PCH_AGCH, NULL, 0);
return 0;
}
static int bts_rx_status_query(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
struct response_state *r_state, void *priv)
{
const struct osmo_cbsp_msg_status_query *query = &dec->u.msg_status_query;
struct bts_smscb_chan_state *chan_state;
struct bts_smscb_message *smscb;
bool extended = false;
if (query->channel_ind == 0x01)
extended = true;
chan_state = bts_get_smscb_chan(bts, extended);
/* Find message by msg_id + old_serial_nr */
smscb = bts_find_smscb(chan_state, query->msg_id, query->old_serial_nr);
if (!smscb)
return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
append_bcast_compl(r_state, chan_state->bts, smscb);
return 0;
}
/*********************************************************************************
* Receive of CBSP from CBC
*********************************************************************************/
static int cbsp_rx_write_replace(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
{
const struct osmo_cbsp_write_replace *wrepl = &dec->u.write_replace;
struct gsm_network *net = cbc->net;
struct response_state *r_state = talloc_zero(cbc, struct response_state);
struct osmo_cbsp_decoded *resp;
enum cbsp_channel_ind channel_ind;
int rc;
LOGP(DCBS, LOGL_INFO, "CBSP Rx WRITE_REPLACE (%s)\n", wrepl->is_cbs ? "CBS" : "EMERGENCY");
rc = cbsp_per_bts(net, r_state, &dec->u.write_replace.cell_list,
bts_rx_write_replace, dec, NULL);
/* generate response */
if (rc < 0) {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_WRITE_REPLACE_FAIL);
struct osmo_cbsp_write_replace_failure *fail = &resp->u.write_replace_fail;
fail->msg_id = wrepl->msg_id;
fail->new_serial_nr = wrepl->new_serial_nr;
fail->old_serial_nr = wrepl->old_serial_nr;
llist_replace_head(&fail->fail_list, &r_state->fail);
fail->cell_list.id_discr = r_state->success.id_discr;
llist_replace_head(&fail->cell_list.list, &r_state->success.list);
if (wrepl->is_cbs) {
channel_ind = wrepl->u.cbs.channel_ind;
fail->channel_ind = &channel_ind;
}
if (wrepl->old_serial_nr) {
fail->num_compl_list.id_discr = r_state->num_completed.id_discr;
llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list);
}
} else {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_WRITE_REPLACE_COMPL);
struct osmo_cbsp_write_replace_complete *compl = &resp->u.write_replace_compl;
compl->msg_id = wrepl->msg_id;
compl->new_serial_nr = wrepl->new_serial_nr;
compl->old_serial_nr = wrepl->old_serial_nr;
compl->cell_list.id_discr = r_state->success.id_discr;
llist_replace_head(&compl->cell_list.list, &r_state->success.list);
if (wrepl->is_cbs) {
channel_ind = wrepl->u.cbs.channel_ind;
compl->channel_ind = &channel_ind;
}
if (wrepl->old_serial_nr) {
compl->num_compl_list.id_discr = r_state->num_completed.id_discr;
llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list);
}
}
cbsp_tx_decoded(cbc, resp);
talloc_free(r_state);
return rc;
}
static int cbsp_rx_keep_alive(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
{
LOGP(DCBS, LOGL_DEBUG, "CBSP Rx KEEP_ALIVE\n");
/* FIXME: repetition period */
return tx_cbsp_keepalive_compl(cbc);
}
static int cbsp_rx_kill(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
{
const struct osmo_cbsp_kill *kill = &dec->u.kill;
struct gsm_network *net = cbc->net;
struct response_state *r_state = talloc_zero(cbc, struct response_state);
struct osmo_cbsp_decoded *resp;
int rc;
LOGP(DCBS, LOGL_DEBUG, "CBSP Rx KILL\n");
rc = cbsp_per_bts(net, r_state, &dec->u.kill.cell_list, bts_rx_kill, dec, NULL);
if (rc < 0) {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KILL_FAIL);
struct osmo_cbsp_kill_failure *fail = &resp->u.kill_fail;
fail->msg_id = kill->msg_id;
fail->old_serial_nr = kill->old_serial_nr;
fail->channel_ind = kill->channel_ind;
llist_replace_head(&fail->fail_list, &r_state->fail);
/* if the KILL relates to CBS, the "Channel Indicator" IE is present */
if (kill->channel_ind) {
/* only if it was CBS */
fail->num_compl_list.id_discr = r_state->num_completed.id_discr;
llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list);
} else {
/* only if it was emergency */
fail->cell_list.id_discr = r_state->success.id_discr;
llist_replace_head(&fail->cell_list.list, &r_state->success.list);
}
} else {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KILL_COMPL);
struct osmo_cbsp_kill_complete *compl = &resp->u.kill_compl;
compl->msg_id = kill->msg_id;
compl->old_serial_nr = kill->old_serial_nr;
compl->channel_ind = kill->channel_ind;
/* if the KILL relates to CBS, the "Channel Indicator" IE is present */
if (kill->channel_ind) {
/* only if it was CBS */
compl->num_compl_list.id_discr = r_state->num_completed.id_discr;
llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list);
} else {
/* only if it was emergency */
compl->cell_list.id_discr = r_state->success.id_discr;
llist_replace_head(&compl->cell_list.list, &r_state->success.list);
}
}
cbsp_tx_decoded(cbc, resp);
talloc_free(r_state);
return rc;
}
static int cbsp_rx_reset(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
{
struct gsm_network *net = cbc->net;
struct response_state *r_state = talloc_zero(cbc, struct response_state);
struct osmo_cbsp_decoded *resp;
int rc;
LOGP(DCBS, LOGL_DEBUG, "CBSP Rx RESET\n");
rc = cbsp_per_bts(net, r_state, &dec->u.reset.cell_list, bts_rx_reset, dec, NULL);
if (rc < 0) {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESET_FAIL);
struct osmo_cbsp_reset_failure *fail = &resp->u.reset_fail;
llist_replace_head(&fail->fail_list, &r_state->fail);
fail->cell_list.id_discr = r_state->success.id_discr;
llist_replace_head(&fail->cell_list.list, &r_state->success.list);
} else {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESET_COMPL);
struct osmo_cbsp_reset_complete *compl = &resp->u.reset_compl;
if (dec->u.reset.cell_list.id_discr == CELL_IDENT_BSS) {
/* replace the list of individual cell identities with CELL_IDENT_BSS */
compl->cell_list.id_discr = CELL_IDENT_BSS;
/* no need to free success_list entries, hierarchical talloc works */
} else {
compl->cell_list.id_discr = r_state->success.id_discr;
llist_replace_head(&compl->cell_list.list, &r_state->success.list);
}
}
cbsp_tx_decoded(cbc, resp);
talloc_free(r_state);
return rc;
}
static int cbsp_rx_status_query(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
{
const struct osmo_cbsp_msg_status_query *query = &dec->u.msg_status_query;
struct gsm_network *net = cbc->net;
struct response_state *r_state = talloc_zero(cbc, struct response_state);
struct osmo_cbsp_decoded *resp;
int rc;
LOGP(DCBS, LOGL_DEBUG, "CBSP Rx MESSAGE STATUS QUERY\n");
rc = cbsp_per_bts(net, r_state, &dec->u.msg_status_query.cell_list, bts_rx_status_query, dec, NULL);
if (rc < 0) {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_MSG_STATUS_QUERY_FAIL);
struct osmo_cbsp_msg_status_query_failure *fail = &resp->u.msg_status_query_fail;
fail->msg_id = query->msg_id;
fail->old_serial_nr = query->old_serial_nr;
fail->channel_ind = query->channel_ind;
llist_replace_head(&fail->fail_list, &r_state->fail);
fail->num_compl_list.id_discr = r_state->num_completed.id_discr;
llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list);
} else {
resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_MSG_STATUS_QUERY_COMPL);
struct osmo_cbsp_msg_status_query_complete *compl = &resp->u.msg_status_query_compl;
compl->msg_id = query->msg_id;
compl->old_serial_nr = query->old_serial_nr;
compl->channel_ind = query->channel_ind;
if (dec->u.msg_status_query.cell_list.id_discr == CELL_IDENT_BSS) {
/* replace the list of individual cell identities with CELL_IDENT_BSS */
compl->num_compl_list.id_discr = CELL_IDENT_BSS;
/* no need to free num_completed_list entries, hierarchical talloc works */
} else {
compl->num_compl_list.id_discr = r_state->num_completed.id_discr;
llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list);
}
}
cbsp_tx_decoded(cbc, resp);
talloc_free(r_state);
return rc;
}
/*! process an incoming, already decoded CBSP message from the CBC.
* \param[in] cbc link to the CBC
* \param[in] dec decoded CBSP message structure. Ownership not transferred.
* \returns 0 on success; negative on error. */
int cbsp_rx_decoded(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
{
int rc = -1;
switch (dec->msg_type) {
case CBSP_MSGT_WRITE_REPLACE: /* create or modify message */
rc = cbsp_rx_write_replace(cbc, dec);
break;
case CBSP_MSGT_KEEP_ALIVE: /* solicit an acknowledgement */
rc = cbsp_rx_keep_alive(cbc, dec);
break;
case CBSP_MSGT_KILL: /* remove message */
rc = cbsp_rx_kill(cbc, dec);
break;
case CBSP_MSGT_RESET: /* stop broadcasting of all messages */
rc = cbsp_rx_reset(cbc, dec);
break;
case CBSP_MSGT_MSG_STATUS_QUERY:
rc = cbsp_rx_status_query(cbc, dec);
break;
case CBSP_MSGT_LOAD_QUERY:
case CBSP_MSGT_SET_DRX:
LOGP(DCBS, LOGL_ERROR, "Received Unimplemented CBSP Message Type %s",
get_value_string(cbsp_msg_type_names, dec->msg_type));
/* we should implement those eventually */
break;
default:
LOGP(DCBS, LOGL_ERROR, "Received Unknown/Unexpected CBSP Message Type %s",
get_value_string(cbsp_msg_type_names, dec->msg_type));
break;
}
return rc;
}
/* initialize the ETWS state of a BTS */
void bts_etws_init(struct gsm_bts *bts)
{
bts->etws.active = false;
osmo_timer_setup(&bts->etws.timer, etws_pn_cb, bts);
}
/* BSC is bootstrapping a BTS; install any currently active ETWS PN */
static void bts_etws_bootstrap(struct gsm_bts *bts)
{
if (bts->etws.active)
bts_send_etws(bts);
}
/* Callback function to be called every time we receive a signal from NM */
static int nm_sig_cb(unsigned int subsys, unsigned int signal,
void *handler_data, void *signal_data)
{
struct nm_running_chg_signal_data *nsd;
struct gsm_bts *bts;
struct gsm_bts_trx *trx;
if (signal != S_NM_RUNNING_CHG)
return 0;
nsd = signal_data;
bts = nsd->bts;
switch (nsd->obj_class) {
case NM_OC_RADIO_CARRIER:
trx = (struct gsm_bts_trx *)nsd->obj;
break;
case NM_OC_BASEB_TRANSC:
trx = gsm_bts_bb_trx_get_trx((struct gsm_bts_bb_trx *)nsd->obj);
break;
default:
return 0;
}
struct gsm_lchan *cbch = gsm_bts_get_cbch(bts);
if (!cbch)
return 0;
/* We only care about state changes of TRX holding the CBCH. */
if (trx != cbch->ts->trx)
return 0;
if (nsd->running) {
if (trx_is_usable(trx)) {
LOG_BTS(bts, DCBS, LOGL_INFO, "BTS becomes available for CBCH\n");
/* Start CBCH transmit timer if CBCH is present */
bts_cbch_timer_schedule(trx->bts);
/* Start ETWS/PWS Primary Notification, if active */
bts_etws_bootstrap(trx->bts);
cbsp_tx_restart_bts(bts->network->cbc, false, bts);
}
} else {
if (osmo_timer_pending(&bts->cbch_timer)) {
/* If timer is ongoing it means CBCH was available */
LOG_BTS(bts, DCBS, LOGL_INFO, "BTS becomes unavailable for CBCH\n");
osmo_timer_del(&bts->cbch_timer);
cbsp_tx_failure_bts(bts->network->cbc, false, bts);
} /* else: CBCH was already unavailable before */
}
return 0;
}
/* To be called once at startup of the process: */
void smscb_global_init(void)
{
osmo_signal_register_handler(SS_NM, nm_sig_cb, NULL);
}