diff options
Diffstat (limited to 'src/osmo-bsc/handover_fsm.c')
-rw-r--r-- | src/osmo-bsc/handover_fsm.c | 1187 |
1 files changed, 1187 insertions, 0 deletions
diff --git a/src/osmo-bsc/handover_fsm.c b/src/osmo-bsc/handover_fsm.c new file mode 100644 index 000000000..0430a7f6d --- /dev/null +++ b/src/osmo-bsc/handover_fsm.c @@ -0,0 +1,1187 @@ +/* Handover FSM implementation for intra-BSC and inter-BSC Handover. + * + * (C) 2018 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> + * All Rights Reserved + * + * Author: Neels Hofmeyr <neels@hofmeyr.de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#include <osmocom/core/socket.h> + +#include <osmocom/gsm/rsl.h> +#include <osmocom/gsm/gsm0808.h> + +#include <osmocom/bsc/debug.h> +#include <osmocom/bsc/bsc_subscriber.h> + +#include <osmocom/bsc/handover_fsm.h> +#include <osmocom/bsc/bsc_subscr_conn_fsm.h> +#include <osmocom/bsc/lchan_select.h> +#include <osmocom/bsc/lchan_fsm.h> +#include <osmocom/bsc/gsm_04_08_rr.h> +#include <osmocom/bsc/abis_rsl.h> +#include <osmocom/bsc/bsc_msc_data.h> +#include <osmocom/bsc/osmo_bsc.h> +#include <osmocom/bsc/osmo_bsc_lcls.h> +#include <osmocom/bsc/mgw_endpoint_fsm.h> +#include <osmocom/bsc/codec_pref.h> + +#define LOG_FMT_BTS "bts %u lac-ci %u-%u arfcn-bsic %d-%d" +#define LOG_ARGS_BTS(bts) \ + (bts) ? (bts)->nr : 0, \ + (bts) ? (bts)->location_area_code : 0, \ + (bts) ? (bts)->cell_identity : 0, \ + (bts) ? (bts)->c0->arfcn : 0, \ + (bts) ? (bts)->bsic : 0 + +#define LOG_FMT_FROM_LCHAN "%u-%u-%u-%s-%u-%s" +#define LOG_ARGS_FROM_LCHAN(lchan) \ + lchan ? lchan->ts->trx->bts->nr : 0, \ + lchan ? lchan->ts->trx->nr : 0, \ + lchan ? lchan->ts->nr : 0, \ + lchan ? gsm_lchant_name(lchan->type) : "?", \ + lchan ? lchan->nr : 0, \ + lchan ? gsm48_chan_mode_name(lchan->tch_mode) : "?" + +#define LOG_FMT_TO_LCHAN "%u-%u-%u-%s%s%s-%u" +#define LOG_ARGS_TO_LCHAN(lchan) \ + lchan ? lchan->ts->trx->bts->nr : 0, \ + lchan ? lchan->ts->trx->nr : 0, \ + lchan ? lchan->ts->nr : 0, \ + lchan ? gsm_pchan_name(lchan->ts->pchan_on_init) : "?", \ + (!lchan || lchan->ts->pchan_on_init == lchan->ts->pchan_is)? "" : ":", \ + (!lchan || lchan->ts->pchan_on_init == lchan->ts->pchan_is)? "" \ + : gsm_pchan_name(lchan->ts->pchan_is), \ + lchan ? lchan->nr : 0 + +#define LOG_FMT_HO_SCOPE "(subscr %s) %s" +#define LOG_ARGS_HO_SCOPE(conn) \ + bsc_subscr_name(conn->bsub), \ + handover_scope_name(conn->ho.scope) + +/* Assume presence of local var 'conn' as struct gsm_subscriber_connection. + * This is a macro to preserve the source file and line number in logging. */ +#define ho_count(counter) do { \ + LOG_HO(conn, LOGL_DEBUG, "incrementing rate counter: %s %s\n", \ + bsc_ctr_description[counter].name, \ + bsc_ctr_description[counter].description); \ + rate_ctr_inc(&conn->network->bsc_ctrs->ctr[counter]); \ + } while(0) + +static uint8_t g_next_ho_ref = 1; + +const char *handover_status(struct gsm_subscriber_connection *conn) +{ + static char buf[256]; + struct handover *ho = &conn->ho; + + if (!conn) + return ""; + + if (ho->scope & (HO_INTRA_CELL | HO_INTRA_BSC)) { + if (ho->new_lchan) + snprintf(buf, sizeof(buf), + "("LOG_FMT_FROM_LCHAN") --HO-> (" LOG_FMT_TO_LCHAN ") " LOG_FMT_HO_SCOPE, + LOG_ARGS_FROM_LCHAN(conn->lchan), + LOG_ARGS_TO_LCHAN(ho->new_lchan), + LOG_ARGS_HO_SCOPE(conn)); + else if (ho->new_bts) + snprintf(buf, sizeof(buf), + "("LOG_FMT_FROM_LCHAN") --HO-> ("LOG_FMT_BTS",%s) " LOG_FMT_HO_SCOPE, + LOG_ARGS_FROM_LCHAN(conn->lchan), + LOG_ARGS_BTS(ho->new_bts), + gsm_lchant_name(ho->new_lchan_type), + LOG_ARGS_HO_SCOPE(conn)); + else + snprintf(buf, sizeof(buf), + "("LOG_FMT_FROM_LCHAN") --HO->(?) " LOG_FMT_HO_SCOPE, + LOG_ARGS_FROM_LCHAN(conn->lchan), + LOG_ARGS_HO_SCOPE(conn)); + + } else if (ho->scope & HO_INTER_BSC_OUT) + snprintf(buf, sizeof(buf), + "("LOG_FMT_FROM_LCHAN") --HO-> (%s) " LOG_FMT_HO_SCOPE, + LOG_ARGS_FROM_LCHAN(conn->lchan), + neighbor_ident_key_name(&ho->target_cell), + LOG_ARGS_HO_SCOPE(conn)); + + else if (ho->scope & HO_INTER_BSC_IN) { + if (ho->new_lchan) + snprintf(buf, sizeof(buf), + "(remote:%s) --HO-> (local:%s|"LOG_FMT_TO_LCHAN") " LOG_FMT_HO_SCOPE, + ho->inter_bsc_in.cell_id_serving_name, + ho->inter_bsc_in.cell_id_target_name, + LOG_ARGS_TO_LCHAN(ho->new_lchan), + LOG_ARGS_HO_SCOPE(conn)); + else if (ho->new_bts) + snprintf(buf, sizeof(buf), + "(remote:%s) --HO-> (local:%s|"LOG_FMT_BTS",%s) " LOG_FMT_HO_SCOPE, + ho->inter_bsc_in.cell_id_serving_name, + ho->inter_bsc_in.cell_id_target_name, + LOG_ARGS_BTS(ho->new_bts), + gsm_lchant_name(ho->new_lchan_type), + LOG_ARGS_HO_SCOPE(conn)); + else + snprintf(buf, sizeof(buf), + "(remote:%s) --HO-> (local:%s,%s) " LOG_FMT_HO_SCOPE, + ho->inter_bsc_in.cell_id_serving_name, + ho->inter_bsc_in.cell_id_target_name, + gsm_lchant_name(ho->new_lchan_type), + LOG_ARGS_HO_SCOPE(conn)); + } else + snprintf(buf, sizeof(buf), LOG_FMT_HO_SCOPE, LOG_ARGS_HO_SCOPE(conn)); + return buf; +} + +static struct osmo_fsm ho_fsm; + +struct gsm_subscriber_connection *ho_fi_conn(struct osmo_fsm_inst *fi) +{ + OSMO_ASSERT(fi); + OSMO_ASSERT(fi->fsm == &ho_fsm); + OSMO_ASSERT(fi->priv); + return fi->priv; +} + +static const struct state_timeout ho_fsm_timeouts[32] = { + [HO_ST_WAIT_LCHAN_ACTIVE] = { .T = 23042 }, + [HO_ST_WAIT_RR_HO_DETECT] = { .T = 23042 }, + [HO_ST_WAIT_RR_HO_COMPLETE] = { .T = 23042 }, + [HO_ST_WAIT_LCHAN_ESTABLISHED] = { .T = 23042 }, + [HO_ST_WAIT_MGW_ENDPOINT_TO_MSC] = { .T = 23042 }, + [HO_OUT_ST_WAIT_HO_COMMAND] = { .T = 7 }, + [HO_OUT_ST_WAIT_CLEAR] = { .T = 8 }, +}; + +/* Transition to a state, using the T timer defined in ho_fsm_timeouts. + * The actual timeout value is in turn obtained from network->T_defs. + * Assumes local variable fi exists. */ +#define ho_fsm_state_chg(state) \ + fsm_inst_state_chg_T(fi, state, \ + ho_fsm_timeouts, \ + ((struct gsm_subscriber_connection*)(fi->priv))->network->T_defs, \ + 5) + +/* Log failure and transition to HO_ST_FAILURE, which triggers the appropriate actions. */ +#define ho_fail(result, fmt, args...) do { \ + LOG_HO(conn, LOGL_ERROR, "Handover failed in state %s, %s: " fmt "\n", \ + osmo_fsm_inst_state_name(conn->fi), handover_result_name(result), ## args); \ + handover_end(conn, result); \ + } while(0) + +#define ho_success() do { \ + LOG_HO(conn, LOGL_DEBUG, "Handover succeeded\n"); \ + handover_end(conn, HO_RESULT_OK); \ + } while(0) + +/* issue handover to a cell identified by ARFCN and BSIC */ +void handover_request(struct handover_out_req *req) +{ + struct gsm_subscriber_connection *conn; + OSMO_ASSERT(req->old_lchan); + + conn = req->old_lchan->conn; + OSMO_ASSERT(conn && conn->fi); + + /* To make sure we're allowed to start a handover, go through a gscon event dispatch. */ + osmo_fsm_inst_dispatch(conn->fi, GSCON_EV_HANDOVER_START, req); +} + +/* Check that ho has old_lchan and/or new_lchan and conn pointers match. + * If old_lchan and/or new_lchan are NULL, omit those checks. + * On error, return false, log an error and call handover_end() with HO_RESULT_ERROR. */ +bool handover_is_sane(struct gsm_subscriber_connection *conn, struct gsm_lchan *old_lchan, struct gsm_lchan *new_lchan) +{ + if (!conn->ho.fi) { + LOG_HO(conn, LOGL_ERROR, "No handover ongoing\n"); + return false; + } + + if (old_lchan + && (conn != old_lchan->conn || conn->lchan != old_lchan)) + goto insane; + if (new_lchan + && (conn != new_lchan->conn || conn->ho.new_lchan != new_lchan)) + goto insane; + if (conn->lchan && conn->lchan == conn->ho.new_lchan) + goto insane; + + return true; +insane: + LOG_HO(conn, LOGL_ERROR, "Handover state is corrupted\n"); + handover_end(conn, HO_RESULT_ERROR); + return false; +} + +static void ho_fsm_update_id(struct osmo_fsm_inst *fi, const char *label) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + if (conn->fi->id) + osmo_fsm_inst_update_id_f(fi, "%s_%s", label, conn->fi->id); + else + osmo_fsm_inst_update_id_f(fi, "%s_conn%u", label, conn->sccp.conn_id); +} + +static void handover_reset(struct gsm_subscriber_connection *conn) +{ + struct mgwep_ci *ci; + if (conn->ho.new_lchan) + /* New lchan was activated but never passed to a conn */ + lchan_release(conn->ho.new_lchan, true, true, RSL_ERR_EQUIPMENT_FAIL); + + ci = conn->ho.created_ci_for_msc; + if (ci) { + gscon_forget_mgw_endpoint_ci(conn, ci); + /* If this is the last endpoint released, the mgw_endpoint_fsm will terminate and tell + * the gscon about it. */ + mgw_endpoint_ci_dlcx(ci); + } + + conn->ho = (struct handover){ + .fi = conn->ho.fi, + }; +} + +void handover_fsm_init() +{ + OSMO_ASSERT(osmo_fsm_register(&ho_fsm) == 0); +} + +void handover_fsm_alloc(struct gsm_subscriber_connection *conn) +{ + OSMO_ASSERT(conn->fi); + OSMO_ASSERT(!conn->ho.fi); + + conn->ho.fi = osmo_fsm_inst_alloc_child(&ho_fsm, conn->fi, GSCON_EV_HANDOVER_END); + OSMO_ASSERT(conn->ho.fi); + conn->ho.fi->priv = conn; +} + +static void handover_start_intra_bsc(struct gsm_subscriber_connection *conn); +static void handover_start_inter_bsc_out(struct gsm_subscriber_connection *conn, + const struct gsm0808_cell_id_list2 *target_cells); + +/* Invoked by gscon if a handover was accepted to start now. */ +void handover_start(struct handover_out_req *req) +{ + + OSMO_ASSERT(req && req->old_lchan && req->old_lchan->conn); + struct gsm_subscriber_connection *conn = req->old_lchan->conn; + struct handover *ho = &conn->ho; + struct gsm_bts *bts; + const struct gsm0808_cell_id_list2 *cil; + + if (conn->ho.fi) { + LOG_HO(conn, LOGL_ERROR, "Handover requested while another handover is ongoing; Ignore\n"); + return; + } + handover_reset(conn); + + handover_fsm_alloc(conn); + + ho->from_hodec_id = req->from_hodec_id; + ho->new_lchan_type = req->new_lchan_type == GSM_LCHAN_NONE ? + req->old_lchan->type : req->new_lchan_type; + ho->target_cell = req->target_nik; + + bts = bts_by_neighbor_ident(conn->network, &req->target_nik); + if (bts) { + ho->new_bts = bts; + handover_start_intra_bsc(conn); + return; + } + + cil = neighbor_ident_get(conn->network->neighbor_bss_cells, &req->target_nik); + if (cil) { + handover_start_inter_bsc_out(conn, cil); + return; + } + + LOG_HO(conn, LOGL_ERROR, "Cannot handover %s: neighbor unknown\n", + neighbor_ident_key_name(&req->target_nik)); + handover_end(conn, HO_RESULT_FAIL_NO_CHANNEL); +} + +/*! Hand over the specified logical channel to the specified new BTS and possibly change the lchan type. + * This is the main entry point for the actual handover algorithm, after the decision whether to initiate + * HO to a specific BTS. To not change the lchan type, pass old_lchan->type. */ +static void handover_start_intra_bsc(struct gsm_subscriber_connection *conn) +{ + struct handover *ho = &conn->ho; + struct osmo_fsm_inst *fi = conn->ho.fi; + struct lchan_activate_info info; + + OSMO_ASSERT(ho->new_bts); + OSMO_ASSERT(ho->new_lchan_type != GSM_LCHAN_NONE); + OSMO_ASSERT(!ho->new_lchan); + + ho->scope = (ho->new_bts == conn->lchan->ts->trx->bts) ? HO_INTRA_CELL : HO_INTRA_BSC; + ho->ho_ref = g_next_ho_ref++; + ho->async = true; + + ho->new_lchan = lchan_select_by_type(ho->new_bts, ho->new_lchan_type); + + if (ho->scope & HO_INTRA_CELL) + ho_fsm_update_id(fi, "intraCell"); + else + ho_fsm_update_id(fi, "intraBSC"); + + ho_count(BSC_CTR_HANDOVER_ATTEMPTED); + + if (!ho->new_lchan) { + ho_fail(HO_RESULT_FAIL_NO_CHANNEL, + "No %s lchan available on BTS %u", + gsm_lchant_name(ho->new_lchan_type), ho->new_bts->nr); + return; + } + LOG_HO(conn, LOGL_DEBUG, "Selected lchan %s\n", gsm_lchan_name(ho->new_lchan)); + + ho_fsm_state_chg(HO_ST_WAIT_LCHAN_ACTIVE); + + info = (struct lchan_activate_info){ + .activ_for = FOR_HANDOVER, + .for_conn = conn, + .chan_mode = conn->lchan->tch_mode, + .requires_voice_stream = conn->lchan->mgw_endpoint_ci_bts ? true : false, + .msc_assigned_cic = conn->ho.inter_bsc_in.msc_assigned_cic, + .old_lchan = conn->lchan, + }; + + lchan_activate(ho->new_lchan, &info); +} + +/* 3GPP TS 48.008 ยง 3.2.1.8 Handover Request */ +static bool parse_ho_request(struct gsm_subscriber_connection *conn, const struct msgb *msg, + struct handover_in_req *req) +{ + struct tlv_parsed tp_arr[2]; + struct tlv_parsed *tp = &tp_arr[0]; + struct tlv_parsed *tp2 = &tp_arr[1]; + struct tlv_p_entry *e; + int payload_length; + bool aoip = gscon_is_aoip(conn); + bool sccplite = gscon_is_sccplite(conn); + + if ((aoip && sccplite) || !(aoip || sccplite)) { + LOG_HO(conn, LOGL_ERROR, "Received BSSMAP Handover Request, but conn is not" + " marked as exactly one of AoIP or SCCPlite\n"); + return false; + } + + payload_length = msg->tail - msg->l4h; + if (tlv_parse2(tp_arr, 2, gsm0808_att_tlvdef(), msg->l4h + 1, payload_length - 1, 0, 0) <= 0) { + LOG_HO(conn, LOGL_ERROR, "Failed to parse IEs\n"); + return false; + } + + if (!(e = TLVP_GET(tp, GSM0808_IE_CHANNEL_TYPE))) { + LOG_HO(conn, LOGL_ERROR, "Missing Channel Type IE\n"); + return false; + } + if (gsm0808_dec_channel_type(&req->ct, e->val, e->len) <= 0) { + LOG_HO(conn, LOGL_ERROR, "Failed to parse Channel Type IE\n"); + return false; + } + + if (!(e = TLVP_GET(tp, GSM0808_IE_ENCRYPTION_INFORMATION))) { + LOG_HO(conn, LOGL_ERROR, "Missing Encryption Information IE\n"); + return false; + } + if (gsm0808_dec_encrypt_info(&req->ei, e->val, e->len) <= 0) { + LOG_HO(conn, LOGL_ERROR, "Failed to parse Encryption Information IE\n"); + return false; + } + + if ((e = TLVP_GET(tp, GSM0808_IE_CLASSMARK_INFORMATION_TYPE_1))) { + if (e->len != sizeof(req->classmark.classmark1)) { + LOG_HO(conn, LOGL_ERROR, "Classmark Information 1 has wrong size\n"); + return false; + } + req->classmark.classmark1 = *(struct gsm48_classmark1*)e->val; + req->classmark.classmark1_set = true; + } else if ((e = TLVP_GET(tp, GSM0808_IE_CLASSMARK_INFORMATION_T2))) { + uint8_t len = OSMO_MIN(sizeof(req->classmark.classmark2), + e->len); + if (!len) { + LOG_HO(conn, LOGL_ERROR, "Classmark Information 2 has zero size\n"); + return false; + } + memcpy(&req->classmark.classmark2, e->val, len); + req->classmark.classmark2_len = len; + } else { + LOG_HO(conn, LOGL_ERROR, "Missing IE: either Classmark Information 1 or 2 required\n"); + return false; + } + + if (TLVP_PRESENT(tp, GSM0808_IE_AOIP_TRASP_ADDR)) { + int rc; + unsigned int u; + struct sockaddr_storage msc_rtp_sa; + + if (!aoip) { + LOG_HO(conn, LOGL_ERROR, + "BSSMAP Handover Request contains AoIP Transport Address," + " but this is not an AoIP connection\n"); + return false; + } + rc = gsm0808_dec_aoip_trasp_addr(&msc_rtp_sa, + TLVP_VAL(tp, GSM0808_IE_AOIP_TRASP_ADDR), + TLVP_LEN(tp, GSM0808_IE_AOIP_TRASP_ADDR)); + if (rc < 0) { + LOG_HO(conn, LOGL_ERROR, "Unable to decode AoIP Transport Address.\n"); + return false; + } + + u = osmo_sockaddr_to_str_and_uint(req->msc_assigned_rtp_addr, + sizeof(req->msc_assigned_rtp_addr), + &req->msc_assigned_rtp_port, + (const struct sockaddr*)&msc_rtp_sa); + if (!u || u >= sizeof(req->msc_assigned_rtp_addr)) { + LOG_HO(conn, LOGL_ERROR, "MSC's RTP address is too long\n"); + return false; + } + } else if (aoip) { + LOG_HO(conn, LOGL_ERROR, + "BSSMAP Handover Request lacks AoIP Transport Address on an AoIP connection\n"); + return false; + } + + /* The Cell Identifier (Serving) and Cell Identifier (Target) are both 3.2.2.17 and are + * identified by the same tag. So get one from tp and the other from tp2. */ + if (!(e = TLVP_GET(tp, GSM0808_IE_CELL_IDENTIFIER))) { + LOG_HO(conn, LOGL_ERROR, "Missing IE: Cell Identifier (Serving)\n"); + return false; + } + if (gsm0808_dec_cell_id(&req->cell_id_serving, e->val, e->len) < 0) { + LOG_HO(conn, LOGL_ERROR, "Invalid IE: Cell Identifier (Serving)\n"); + return false; + } + /* LOG_HO() also calls gsm0808_cell_id_name(), so to be able to use gsm0808_cell_id_name() in + * logging without getting mixed up with those static buffers, store the result. */ + snprintf(req->cell_id_serving_name, sizeof(req->cell_id_serving_name), + gsm0808_cell_id_name(&req->cell_id_serving)); + + if (!(e = TLVP_GET(tp2, GSM0808_IE_CELL_IDENTIFIER))) { + LOG_HO(conn, LOGL_ERROR, "Missing IE: Cell Identifier (Target)\n"); + return false; + } + if (gsm0808_dec_cell_id(&req->cell_id_target, e->val, e->len) < 0) { + LOG_HO(conn, LOGL_ERROR, "Invalid IE: Cell Identifier (Target)\n"); + return false; + } + snprintf(req->cell_id_target_name, sizeof(req->cell_id_target_name), + gsm0808_cell_id_name(&req->cell_id_target)); + + if ((e = TLVP_GET(tp, GSM0808_IE_CIRCUIT_IDENTITY_CODE))) { + int timeslot; + int multiplex; + + /* CIC is permitted in both AoIP and SCCPlite */ + req->msc_assigned_cic = osmo_load16be(e->val); + + /* For SCCPlite, the CIC implies the RTP port to use */ + if (sccplite) { + timeslot = req->msc_assigned_cic & 0x1f; + multiplex = (req->msc_assigned_cic & ~0x1f) >> 5; + req->msc_assigned_rtp_port = mgcp_timeslot_to_port(multiplex, timeslot, + conn->sccp.msc->rtp_base); + LOG_HO(conn, LOGL_DEBUG, "Derived RTP port from MSC assigned CIC:" + " CIC=0x%x (timeslot=%d multiplex=%d) rtp_base=%d=0x%x RTP-port=%u\n", + req->msc_assigned_cic, timeslot, multiplex, + conn->sccp.msc->rtp_base, conn->sccp.msc->rtp_base, + req->msc_assigned_rtp_port); + } + + } else if (sccplite) { + /* no CIC but SCCPlite: illegal */ + LOG_HO(conn, LOGL_ERROR, "SCCPlite MSC, but no CIC in incoming inter-BSC Handover\n"); + return false; + } + + /* A lot of IEs remain ignored... */ + + return true; +} + +static bool chan_mode_is_tch(enum gsm48_chan_mode mode) +{ + switch (mode) { + case GSM48_CMODE_SPEECH_V1: + case GSM48_CMODE_SPEECH_EFR: + case GSM48_CMODE_SPEECH_AMR: + return true; + default: + return false; + } +} + +void handover_start_inter_bsc_in(struct gsm_subscriber_connection *conn, + struct msgb *ho_request_msg) +{ + struct lchan_activate_info info; + struct handover *ho = &conn->ho; + struct bsc_msc_data *msc = conn->sccp.msc; + struct handover_in_req *req = &ho->inter_bsc_in; + int match_idx; + enum gsm48_chan_mode mode; + bool full_rate; + struct osmo_fsm_inst *fi; + + handover_fsm_alloc(conn); + + *ho = (struct handover){ + .fi = ho->fi, + .from_hodec_id = HODEC_REMOTE, + .scope = HO_INTER_BSC_IN, + .ho_ref = g_next_ho_ref++, + .async = true, + }; + + fi = ho->fi; + ho_fsm_update_id(fi, "interBSCin"); + + if (!parse_ho_request(conn, ho_request_msg, req)) { + ho_fail(HO_RESULT_ERROR, "Invalid Handover Request message from MSC\n"); + return; + } + + ho_count(BSC_CTR_INTER_BSC_HO_IN_ATTEMPTED); + + /* Figure out which cell to handover to. */ + for (match_idx = 0; ; match_idx++) { + struct gsm_bts *bts; + struct gsm_lchan *lchan; + + bts = gsm_bts_by_cell_id(conn->network, &req->cell_id_target, + match_idx); + + /* Did we iterate all matches? */ + if (!bts) + break; + + LOG_HO(conn, LOGL_DEBUG, "BTS %u matches cell id %s\n", + bts->nr, req->cell_id_target_name); + + /* Figure out channel type */ + if (match_codec_pref(&mode, &full_rate, &req->ct, &req->scl, msc->audio_support, + msc->audio_length, &bts->codec)) { + LOG_HO(conn, LOGL_DEBUG, + "BTS %u has no matching channel codec (%s, speech codec list len = %u)", + bts->nr, gsm0808_channel_type_name(&req->ct), req->scl.len); + continue; + } + + LOG_HO(conn, LOGL_DEBUG, "BTS %u: Found matching audio type: %s %s (for %s)\n", + bts->nr, gsm48_chan_mode_name(mode), full_rate? "full-rate" : "half-rate", + gsm0808_channel_type_name(&req->ct)); + + lchan = lchan_select_by_chan_mode(bts, mode, full_rate); + if (!lchan) { + LOG_HO(conn, LOGL_DEBUG, "BTS %u has no matching free channels\n", bts->nr); + continue; + } + + /* Found a match. */ + ho->new_bts = bts; + ho->new_lchan = lchan; + break; + } + + if (!ho->new_bts) { + ho_fail(HO_RESULT_ERROR, "No local cell matches the target %s", + req->cell_id_target_name); + return; + } + + if (!ho->new_lchan) { + ho_fail(HO_RESULT_ERROR, "No free/matching lchan found for %s %s %s\n", + req->cell_id_target_name, + gsm48_chan_mode_name(mode), full_rate ? "full-rate" : "half-rate"); + return; + } + + /* Just for completeness' sake, maybe some logging uses it? */ + ho->new_lchan_type = ho->new_lchan->type; + + ho_fsm_state_chg(HO_ST_WAIT_LCHAN_ACTIVE); + + info = (struct lchan_activate_info){ + .activ_for = FOR_HANDOVER, + .for_conn = conn, + .chan_mode = mode, + .requires_voice_stream = chan_mode_is_tch(mode), + .msc_assigned_cic = req->msc_assigned_cic, + }; + + lchan_activate(ho->new_lchan, &info); +} + +#define FUNC_RESULT_COUNTER(name) \ +static int result_counter_##name(enum handover_result result) \ +{ \ + switch (result) { \ + case HO_RESULT_OK: \ + return BSC_CTR_##name##_COMPLETED; \ + case HO_RESULT_FAIL_NO_CHANNEL: \ + return BSC_CTR_##name##_NO_CHANNEL; \ + case HO_RESULT_FAIL_RR_HO_FAIL: \ + return BSC_CTR_##name##_FAILED; \ + case HO_RESULT_FAIL_TIMEOUT: \ + return BSC_CTR_##name##_TIMEOUT; \ + case HO_RESULT_CONN_RELEASE: \ + return BSC_CTR_##name##_STOPPED; \ + default: \ + case HO_RESULT_ERROR: \ + return BSC_CTR_##name##_ERROR; \ + } \ +} + +FUNC_RESULT_COUNTER(ASSIGNMENT) +FUNC_RESULT_COUNTER(HANDOVER) +FUNC_RESULT_COUNTER(INTER_BSC_HO_IN) + +static int result_counter_INTER_BSC_HO_OUT(enum handover_result result) { + switch (result) { + case HO_RESULT_OK: + return BSC_CTR_INTER_BSC_HO_IN_COMPLETED; + case HO_RESULT_FAIL_TIMEOUT: + return BSC_CTR_INTER_BSC_HO_IN_TIMEOUT; + case HO_RESULT_CONN_RELEASE: + return BSC_CTR_INTER_BSC_HO_IN_STOPPED; + default: + case HO_RESULT_ERROR: + return BSC_CTR_INTER_BSC_HO_IN_ERROR; + } +} + +static int result_counter(enum handover_scope scope, enum handover_result result) +{ + switch (scope) { + case HO_INTRA_CELL: + return result_counter_ASSIGNMENT(result); + default: + LOGP(DHO, LOGL_ERROR, "invalid enum handover_scope value: %s\n", + handover_scope_name(scope)); + /* use "normal" HO_INTRA_BSC counter... */ + case HO_INTRA_BSC: + return result_counter_HANDOVER(result); + case HO_INTER_BSC_OUT: + return result_counter_INTER_BSC_HO_OUT(result); + case HO_INTER_BSC_IN: + return result_counter_INTER_BSC_HO_IN(result); + } +} + +/* Notify the handover decision algorithm of failure and clear out any handover state. */ +void handover_end(struct gsm_subscriber_connection *conn, enum handover_result result) +{ + struct handover_decision_callbacks *hdc; + struct handover *ho = &conn->ho; + + /* Sanity -- an error result ensures beyond doubt that we don't use the new lchan below + * when the handover isn't actually allowed to change this conn. */ + if (result == HO_RESULT_OK && ho->new_lchan) { + if (!(ho->scope & (HO_INTRA_CELL | HO_INTRA_BSC | HO_INTER_BSC_IN))) { + LOG_HO(conn, LOGL_ERROR, "Got new lchan, but this is not an incoming inter-BSC HO\n"); + result = HO_RESULT_ERROR; + } + if (ho->new_lchan->conn != conn) { + LOG_HO(conn, LOGL_ERROR, "Got new lchan, but it is for another conn\n"); + result = HO_RESULT_ERROR; + } + } + + if (ho->scope & HO_INTER_BSC_IN) { + if (result == HO_RESULT_OK) { + if (!ho->new_lchan) { + LOG_HO(conn, LOGL_ERROR, "Inter-BSC HO IN ends in success," + " but there is no lchan\n"); + result = HO_RESULT_ERROR; + } else + result = bsc_tx_bssmap_ho_complete(conn, ho->new_lchan); + } else { + bsc_tx_bssmap_ho_failure(conn); + /* TODO: Also send BSSMAP Clear Request? */ + } + } + + /* Rembered this only for error handling: should handover fail, handover_reset() will release the + * MGW endpoint right away. If successful, the conn continues to use the endpoint. */ + if (result == HO_RESULT_OK) + conn->ho.created_ci_for_msc = NULL; + + hdc = handover_decision_callbacks_get(ho->from_hodec_id); + if (hdc && hdc->on_handover_end) + hdc->on_handover_end(conn, result); + + ho_count(result_counter(ho->scope, result)); + + LOG_HO(conn, LOGL_INFO, "Result: %s\n", handover_result_name(result)); + + if (ho->new_lchan && result == HO_RESULT_OK) { + gscon_change_primary_lchan(conn, conn->ho.new_lchan); + ho->new_lchan = NULL; + } + + osmo_fsm_inst_dispatch(conn->fi, GSCON_EV_HANDOVER_END, &result); + + /* Detach the new_lchan last, so we can still see it in above logging */ + if (ho->new_lchan) { + /* Release new lchan, it didn't work out */ + lchan_release(ho->new_lchan, false, true, RSL_ERR_EQUIPMENT_FAIL); + ho->new_lchan = NULL; + } + + if ((ho->scope & HO_INTER_BSC_IN) && result == HO_RESULT_OK) { + conn->user_plane.msc_assigned_cic = conn->ho.inter_bsc_in.msc_assigned_cic; + osmo_strlcpy(conn->user_plane.msc_assigned_rtp_addr, + conn->ho.inter_bsc_in.msc_assigned_rtp_addr, + sizeof(conn->user_plane.msc_assigned_rtp_addr)); + conn->user_plane.msc_assigned_rtp_port = conn->ho.inter_bsc_in.msc_assigned_rtp_port; + } + + handover_reset(conn); + + /* We've dispatched the handover result above, let's disconnect to not fire the same event again. + * The parent term event is a safety measure for unplanned termination. */ + osmo_fsm_inst_unlink_parent(conn->ho.fi, conn); + osmo_fsm_inst_term(conn->ho.fi, OSMO_FSM_TERM_REGULAR, 0); +} + +static void ho_fsm_wait_lchan_active(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + switch (event) { + + case HO_EV_LCHAN_ACTIVE: + ho_fsm_state_chg(HO_ST_WAIT_RR_HO_DETECT); + return; + + case HO_EV_LCHAN_ERROR: + ho_fail(HO_RESULT_ERROR, "error while activating lchan %s", + gsm_lchan_name(conn->ho.new_lchan)); + return; + + default: + OSMO_ASSERT(false); + } +} + +static void ho_fsm_wait_rr_ho_detect_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + int rc; + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + struct handover *ho = &conn->ho; + + struct msgb *rr_ho_cmd = gsm48_make_ho_cmd(ho->new_lchan, + ho->new_lchan->ms_power, + ho->ho_ref); + if (!rr_ho_cmd) { + ho_fail(HO_RESULT_ERROR, "Unable to compose RR Handover Command"); + return; + } + + + if (ho->scope & (HO_INTRA_CELL | HO_INTRA_BSC)) { + /* conn->lchan is the old lchan being handovered from */ + rr_ho_cmd->lchan = conn->lchan; + rc = gsm48_sendmsg(rr_ho_cmd); + if (rc) + ho_fail(HO_RESULT_ERROR, "Unable to Tx RR Handover Command (rc=%d %s)", + rc, strerror(-rc)); + return; + } + + if (ho->scope & HO_INTER_BSC_IN) { + rc = bsc_tx_bssmap_ho_request_ack(conn, rr_ho_cmd); + if (rc) + ho_fail(HO_RESULT_ERROR, "Unable to Tx BSSMAP Handover Request Ack (rc=%d %s)", + rc, strerror(-rc)); + return; + } + + ho_fail(HO_RESULT_ERROR, "Invalid situation, no target for RR Handover Command"); +} + +static void ho_fsm_wait_rr_ho_detect(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + struct handover *ho = &conn->ho; + switch (event) { + + case HO_EV_RR_HO_DETECT: + { + struct handover_rr_detect_data *d = data; + OSMO_ASSERT(d); + if (d->access_delay) { + LOG_HO(conn, LOGL_DEBUG, "RR Handover Detect (Access Delay=%u)\n", + *(d->access_delay)); + } else + LOG_HO(conn, LOGL_DEBUG, "RR Handover Detect (no Access Delay IE)\n"); + } + + if (ho->scope & HO_INTER_BSC_IN) { + int rc = bsc_tx_bssmap_ho_detect(conn); + if (rc) { + ho_fail(HO_RESULT_ERROR, + "Unable to send BSSMAP Handover Detect"); + return; + } + } + + ho_fsm_state_chg(HO_ST_WAIT_RR_HO_COMPLETE); + /* The lchan FSM will already start to redirect the RTP stream */ + return; + + case HO_EV_RR_HO_COMPLETE: + LOG_HO(conn, LOGL_ERROR, + "Received RR Handover Complete, but haven't even seen a Handover Detect yet;" + " Accepting handover anyway\n"); + + ho_fsm_state_chg(HO_ST_WAIT_LCHAN_ESTABLISHED); + return; + + case HO_EV_RR_HO_FAIL: + ho_fail(HO_RESULT_FAIL_RR_HO_FAIL, "Received RR Handover Fail message"); + return; + + default: + OSMO_ASSERT(false); + } +} + +static void ho_fsm_wait_rr_ho_complete(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + + switch (event) { + + case HO_EV_RR_HO_COMPLETE: + ho_fsm_state_chg(HO_ST_WAIT_LCHAN_ESTABLISHED); + return; + + case HO_EV_RR_HO_FAIL: + ho_fail(HO_RESULT_FAIL_RR_HO_FAIL, "Received RR Handover Fail message"); + return; + + default: + OSMO_ASSERT(false); + } +} + +static void ho_fsm_post_lchan_established(struct osmo_fsm_inst *fi); + +static void ho_fsm_wait_lchan_established_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + + if (conn->ho.fi && lchan_state_is(conn->ho.new_lchan, LCHAN_ST_ESTABLISHED)) + ho_fsm_post_lchan_established(fi); +} + +static void ho_fsm_wait_lchan_established(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + switch (event) { + + case HO_EV_LCHAN_ESTABLISHED: + ho_fsm_post_lchan_established(fi); + break; + + default: + OSMO_ASSERT(false); + } +} + +static void ho_fsm_post_lchan_established(struct osmo_fsm_inst *fi) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + struct handover *ho = &conn->ho; + + if (ho->new_lchan->activate.requires_voice_stream + && (ho->scope & HO_INTER_BSC_IN)) + ho_fsm_state_chg(HO_ST_WAIT_MGW_ENDPOINT_TO_MSC); + else + ho_success(); +} + +static void ho_fsm_wait_mgw_endpoint_to_msc_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + struct handover *ho = &conn->ho; + + if (!gscon_connect_mgw_to_msc(conn, + ho->new_lchan, + ho->inter_bsc_in.msc_assigned_rtp_addr, + ho->inter_bsc_in.msc_assigned_rtp_port, + fi, + HO_EV_MSC_MGW_OK, + HO_EV_MSC_MGW_FAIL, + NULL, + &ho->created_ci_for_msc)) { + ho_fail(HO_RESULT_ERROR, + "Unable to connect MGW endpoint to the MSC side"); + } +} + +static void ho_fsm_wait_mgw_endpoint_to_msc(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + switch (event) { + + case HO_EV_MSC_MGW_OK: + /* For AoIP, we created the MGW endpoint. Ensure it is really there, and log it. */ + if (gscon_is_aoip(conn)) { + const struct mgcp_conn_peer *mgw_info; + mgw_info = mgwep_ci_get_rtp_info(conn->user_plane.mgw_endpoint_ci_msc); + if (!mgw_info) { + ho_fail(HO_RESULT_ERROR, + "Unable to retrieve RTP port info allocated by MGW for" + " the MSC side."); + return; + } + LOG_HO(conn, LOGL_DEBUG, "MGW's MSC side CI: %s:%u\n", + mgw_info->addr, mgw_info->port); + } + ho_success(); + return; + + case HO_EV_MSC_MGW_FAIL: + ho_fail(HO_RESULT_ERROR, + "Unable to connect MGW endpoint to the MSC side"); + return; + + default: + OSMO_ASSERT(false); + } +} + +/* Inter-BSC OUT */ + +static void handover_start_inter_bsc_out(struct gsm_subscriber_connection *conn, + const struct gsm0808_cell_id_list2 *target_cells) +{ + int rc; + struct handover *ho = &conn->ho; + struct osmo_fsm_inst *fi = conn->ho.fi; + + ho->scope = HO_INTER_BSC_OUT; + ho_fsm_update_id(fi, "interBSCout"); + ho_count(BSC_CTR_INTER_BSC_HO_OUT_ATTEMPTED); + + rc = bsc_tx_bssmap_ho_required(conn->lchan, target_cells); + if (rc) { + ho_fail(HO_RESULT_ERROR, "Unable to send BSSMAP Handover Required message"); + return; + } + + ho_fsm_state_chg(HO_OUT_ST_WAIT_HO_COMMAND); +} + +static void ho_out_fsm_wait_ho_command(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + int rc; + struct ho_out_rx_bssmap_ho_command *rx; + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + switch (event) { + + case HO_OUT_EV_BSSMAP_HO_COMMAND: + rx = data; + if (!rx) { + ho_fail(HO_RESULT_ERROR, + "Rx BSSMAP Handover Command: no L3 info passed with event"); + return; + } + + LOG_HO(conn, LOGL_DEBUG, "Rx BSSMAP Handover Command: forwarding Layer 3 Info: %s\n", + osmo_hexdump(rx->l3_info, rx->l3_info_len)); + + rc = rsl_forward_layer3_info(conn->lchan, rx->l3_info, rx->l3_info_len); + if (rc) { + ho_fail(HO_RESULT_ERROR, + "Rx BSSMAP Handover Command: Failed to forward Layer 3 Info (rc=%d %s)", + rc, strerror(-rc)); + return; + } + + ho_fsm_state_chg(HO_OUT_ST_WAIT_CLEAR); + return; + + default: + OSMO_ASSERT(false); + } +} + + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state ho_fsm_states[] = { + [HO_ST_NOT_STARTED] = { + .name = "NOT_STARTED", + .out_state_mask = 0 + | S(HO_ST_WAIT_LCHAN_ACTIVE) + | S(HO_OUT_ST_WAIT_HO_COMMAND) + , + }, + [HO_ST_WAIT_LCHAN_ACTIVE] = { + .name = "WAIT_LCHAN_ACTIVE", + .action = ho_fsm_wait_lchan_active, + .in_event_mask = 0 + | S(HO_EV_LCHAN_ACTIVE) + | S(HO_EV_LCHAN_ERROR) + , + .out_state_mask = 0 + | S(HO_ST_WAIT_LCHAN_ACTIVE) + | S(HO_ST_WAIT_RR_HO_DETECT) + , + }, + [HO_ST_WAIT_RR_HO_DETECT] = { + .name = "WAIT_RR_HO_DETECT", + .onenter = ho_fsm_wait_rr_ho_detect_onenter, + .action = ho_fsm_wait_rr_ho_detect, + .in_event_mask = 0 + | S(HO_EV_RR_HO_DETECT) + | S(HO_EV_RR_HO_COMPLETE) /* actually as error */ + | S(HO_EV_RR_HO_FAIL) + , + .out_state_mask = 0 + | S(HO_ST_WAIT_RR_HO_COMPLETE) + | S(HO_ST_WAIT_LCHAN_ESTABLISHED) + , + }, + [HO_ST_WAIT_RR_HO_COMPLETE] = { + .name = "WAIT_RR_HO_COMPLETE", + .action = ho_fsm_wait_rr_ho_complete, + .in_event_mask = 0 + | S(HO_EV_RR_HO_COMPLETE) + | S(HO_EV_RR_HO_FAIL) + , + .out_state_mask = 0 + | S(HO_ST_WAIT_LCHAN_ESTABLISHED) + , + }, + [HO_ST_WAIT_LCHAN_ESTABLISHED] = { + .name = "WAIT_LCHAN_ESTABLISHED", + .onenter = ho_fsm_wait_lchan_established_onenter, + .action = ho_fsm_wait_lchan_established, + .in_event_mask = 0 + | S(HO_EV_LCHAN_ESTABLISHED) + , + .out_state_mask = 0 + | S(HO_ST_WAIT_MGW_ENDPOINT_TO_MSC) + , + }, + [HO_ST_WAIT_MGW_ENDPOINT_TO_MSC] = { + .name = "WAIT_MGW_ENDPOINT_TO_MSC", + .onenter = ho_fsm_wait_mgw_endpoint_to_msc_onenter, + .action = ho_fsm_wait_mgw_endpoint_to_msc, + .in_event_mask = 0 + | S(HO_EV_MSC_MGW_OK) + | S(HO_EV_MSC_MGW_FAIL) + , + }, + + [HO_OUT_ST_WAIT_HO_COMMAND] = { + .name = "inter-BSC-OUT:WAIT_HO_COMMAND", + .action = ho_out_fsm_wait_ho_command, + .in_event_mask = 0 + | S(HO_OUT_EV_BSSMAP_HO_COMMAND) + , + .out_state_mask = 0 + | S(HO_OUT_ST_WAIT_CLEAR) + , + }, + [HO_OUT_ST_WAIT_CLEAR] = { + .name = "inter-BSC-OUT:WAIT_CLEAR", + }, +}; + +static const struct value_string ho_fsm_event_names[] = { + OSMO_VALUE_STRING(HO_EV_LCHAN_ACTIVE), + OSMO_VALUE_STRING(HO_EV_LCHAN_ESTABLISHED), + OSMO_VALUE_STRING(HO_EV_LCHAN_ERROR), + OSMO_VALUE_STRING(HO_EV_RR_HO_DETECT), + OSMO_VALUE_STRING(HO_EV_RR_HO_COMPLETE), + OSMO_VALUE_STRING(HO_EV_RR_HO_FAIL), + OSMO_VALUE_STRING(HO_EV_MSC_MGW_OK), + OSMO_VALUE_STRING(HO_EV_MSC_MGW_FAIL), + OSMO_VALUE_STRING(HO_EV_CONN_RELEASING), + OSMO_VALUE_STRING(HO_OUT_EV_BSSMAP_HO_COMMAND), + {} +}; + +void ho_fsm_allstate_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + switch (event) { + + case HO_EV_CONN_RELEASING: + switch (fi->state) { + case HO_OUT_ST_WAIT_CLEAR: + ho_success(); + return; + default: + ho_fail(HO_RESULT_CONN_RELEASE, + "Connection releasing in the middle of handover"); + return; + } + + case HO_EV_LCHAN_ERROR: + switch (fi->state) { + case HO_OUT_ST_WAIT_HO_COMMAND: + case HO_OUT_ST_WAIT_CLEAR: + LOG_HO(conn, LOGL_ERROR, "Event not permitted: %s\n", + osmo_fsm_event_name(fi->fsm, event)); + return; + + default: + ho_fail(HO_RESULT_ERROR, "Error while establishing lchan %s", + gsm_lchan_name(data)); + return; + } + + default: + OSMO_ASSERT(false); + } +} + +int ho_fsm_timer_cb(struct osmo_fsm_inst *fi) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + ho_fail(HO_RESULT_FAIL_TIMEOUT, "Timeout"); + return 0; +} + +void ho_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + struct gsm_subscriber_connection *conn = ho_fi_conn(fi); + conn->ho.fi = NULL; +} + +static struct osmo_fsm ho_fsm = { + .name = "handover", + .states = ho_fsm_states, + .num_states = ARRAY_SIZE(ho_fsm_states), + .log_subsys = DRSL, + .event_names = ho_fsm_event_names, + .allstate_action = ho_fsm_allstate_action, + .allstate_event_mask = 0 + | S(HO_EV_CONN_RELEASING) + | S(HO_EV_LCHAN_ERROR) + , + .timer_cb = ho_fsm_timer_cb, + .cleanup = ho_fsm_cleanup, +}; |