| /* 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/mgcp_client/mgcp_client_endpoint_fsm.h> |
| |
| #include <osmocom/bsc/debug.h> |
| #include <osmocom/bsc/bsc_subscriber.h> |
| |
| #include <osmocom/bsc/handover_fsm.h> |
| #include <osmocom/bsc/handover_cfg.h> |
| #include <osmocom/bsc/bsc_subscr_conn_fsm.h> |
| #include <osmocom/bsc/lchan_select.h> |
| #include <osmocom/bsc/lchan_fsm.h> |
| #include <osmocom/bsc/lchan_rtp_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/codec_pref.h> |
| #include <osmocom/bsc/gsm_08_08.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 osmo_tdef_state_timeout ho_fsm_timeouts[32] = { |
| [HO_ST_WAIT_LCHAN_ACTIVE] = { .T = 23042 }, |
| [HO_ST_WAIT_MGW_ENDPOINT_TO_MSC] = { .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_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) \ |
| osmo_tdef_fsm_inst_state_chg(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); |
| |
| /* Make sure the handover target neighbor_ident_key contains the correct source bts nr */ |
| req->target_nik.from_bts = req->old_lchan->ts->trx->bts->nr; |
| |
| /* To make sure we're allowed to start a handover, go through a gscon event dispatch. If that is accepted, the |
| * same req is passed to handover_start(). */ |
| 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 osmo_mgcpc_ep_ci *ci; |
| if (conn->ho.new_lchan) |
| /* New lchan was activated but never passed to a conn */ |
| lchan_release(conn->ho.new_lchan, false, 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. */ |
| osmo_mgcpc_ep_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; |
| const struct neighbor_ident_key *search_for = &req->target_nik; |
| struct handover *ho = &conn->ho; |
| struct gsm_bts *local_target_cell = NULL; |
| const struct gsm0808_cell_id_list2 *remote_target_cell = NULL; |
| |
| if (conn->ho.fi) { |
| LOG_HO(conn, LOGL_ERROR, "Handover requested while another handover is ongoing; Ignore\n"); |
| return; |
| } |
| handover_reset(conn); |
| |
| /* When handover_start() is invoked by the gscon, it expects a HANDOVER_END event. The best way to ensure this |
| * is to always create a handover_fsm instance, even if the target cell is not resolved yet. Any failure should |
| * then call handover_end(), which ensures that the conn snaps back to a valid state. */ |
| 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; |
| |
| if (find_handover_target_cell(&local_target_cell, &remote_target_cell, |
| conn, search_for, true)) |
| goto no_handover; |
| |
| if (local_target_cell) { |
| ho->new_bts = local_target_cell; |
| handover_start_intra_bsc(conn); |
| return; |
| } |
| |
| if (remote_target_cell) { |
| handover_start_inter_bsc_out(conn, remote_target_cell); |
| return; |
| } |
| |
| /* should never reach this, because find_handover_target_cell() would have returned error. */ |
| OSMO_ASSERT(false); |
| |
| no_handover: |
| 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, |
| .encr = conn->lchan->encr, |
| .requires_voice_stream = conn->lchan->mgw_endpoint_ci_bts ? true : false, |
| .msc_assigned_cic = conn->ho.inter_bsc_in.msc_assigned_cic, |
| .re_use_mgw_endpoint_from_lchan = conn->lchan, |
| .wait_before_switching_rtp = true, |
| .s15_s0 = conn->lchan->activate.info.s15_s0, |
| }; |
| |
| 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_INFO, |
| "Missing mandatory IE: 3GPP mandates either Classmark Information 1 or 2" |
| " in BSSMAP Handover Request, but neither are present. Will continue without.\n"); |
| |
| if ((e = TLVP_GET(tp, GSM0808_IE_CHOSEN_ENCR_ALG))) { |
| req->chosen_encr_alg = e->val[0]; |
| if (req->chosen_encr_alg < 1 || req->chosen_encr_alg > 8) |
| LOG_HO(conn, LOGL_ERROR, "Chosen Encryption Algorithm (Serving) is invalid: %u\n", |
| req->chosen_encr_alg); |
| } |
| |
| LOG_HO(conn, LOGL_DEBUG, "Handover Request encryption info: chosen=A5/%u key=%s\n", |
| (req->chosen_encr_alg ? : 1) - 1, req->ei.key_len? |
| osmo_hexdump_nospc(req->ei.key, req->ei.key_len) : "none"); |
| |
| 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. */ |
| OSMO_STRLCPY_ARRAY(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; |
| } |
| OSMO_STRLCPY_ARRAY(req->cell_id_target_name, gsm0808_cell_id_name(&req->cell_id_target)); |
| |
| if ((e = TLVP_GET(tp, GSM0808_IE_CIRCUIT_IDENTITY_CODE))) { |
| /* CIC is permitted in both AoIP and SCCPlite */ |
| req->msc_assigned_cic = osmo_load16be(e->val); |
| } 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; |
| struct osmo_fsm_inst *fi; |
| struct channel_mode_and_rate ch_mode_rate = {}; |
| |
| 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(&ch_mode_rate, &req->ct, &req->scl, msc, bts, RATE_PREF_NONE)) { |
| LOG_HO(conn, LOGL_DEBUG, |
| "BTS %u has no matching channel codec (%s, speech codec list len = %u)\n", |
| 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(ch_mode_rate.chan_mode), |
| ch_mode_rate.chan_rate == CH_RATE_FULL ? "full-rate" : "half-rate", |
| gsm0808_channel_type_name(&req->ct)); |
| |
| lchan = lchan_select_by_chan_mode(bts, ch_mode_rate.chan_mode, ch_mode_rate.chan_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 (speech codec list len = %u)", |
| req->cell_id_target_name, gsm0808_channel_type_name(&req->ct), req->scl.len); |
| 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 = ch_mode_rate.chan_mode, |
| .s15_s0 = ch_mode_rate.s15_s0, |
| .requires_voice_stream = chan_mode_is_tch(ch_mode_rate.chan_mode), |
| .msc_assigned_cic = req->msc_assigned_cic, |
| }; |
| |
| if (req->chosen_encr_alg) { |
| info.encr.alg_id = req->chosen_encr_alg; |
| if (info.encr.alg_id > 1 && !req->ei.key_len) { |
| ho_fail(HO_RESULT_ERROR, "Chosen Encryption Algorithm (Serving) reflects A5/%u" |
| " but there is no key (Encryption Information)", info.encr.alg_id - 1); |
| return; |
| } |
| } |
| |
| if (req->ei.key_len) { |
| if (req->ei.key_len > sizeof(info.encr.key)) { |
| ho_fail(HO_RESULT_ERROR, "Encryption Information IE key length is too large: %u\n", |
| req->ei.key_len); |
| return; |
| } |
| memcpy(info.encr.key, req->ei.key, req->ei.key_len); |
| info.encr.key_len = req->ei.key_len; |
| } |
| |
| 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_OUT_COMPLETED; |
| case HO_RESULT_FAIL_TIMEOUT: |
| return BSC_CTR_INTER_BSC_HO_OUT_TIMEOUT; |
| case HO_RESULT_CONN_RELEASE: |
| return BSC_CTR_INTER_BSC_HO_OUT_STOPPED; |
| default: |
| case HO_RESULT_ERROR: |
| return BSC_CTR_INTER_BSC_HO_OUT_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_NO_HANDOVER: |
| 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); |
| } |
| } |
| |
| static void send_handover_performed(struct gsm_subscriber_connection *conn) |
| { |
| struct gsm_lchan *lchan = conn->lchan; |
| struct handover *ho = &conn->ho; |
| struct osmo_cell_global_id *cell; |
| struct gsm0808_handover_performed ho_perf_params = {}; |
| struct msgb *msg; |
| struct gsm0808_speech_codec sc; |
| int rc; |
| |
| /* Cause 3.2.2.5 */ |
| ho_perf_params.cause = GSM0808_CAUSE_HANDOVER_SUCCESSFUL; |
| |
| /* Cell Identifier 3.2.2.17 */ |
| cell = cgi_for_msc(conn->sccp.msc, conn_get_bts(conn)); |
| if (!cell) { |
| LOG_HO(conn, LOGL_ERROR, "Failed to generate Cell Identifier IE, can't send HANDOVER PERFORMED!\n"); |
| return; |
| } |
| ho_perf_params.cell_id = (struct gsm0808_cell_id){ |
| .id_discr = CELL_IDENT_WHOLE_GLOBAL, |
| .id.global = *cell |
| }; |
| |
| /* Chosen Channel 3.2.2.33 */ |
| ho_perf_params.chosen_channel = gsm0808_chosen_channel(lchan->type, lchan->tch_mode); |
| if (!ho_perf_params.chosen_channel) { |
| LOG_HO(conn, LOGL_ERROR, "Failed to generate Chosen Channel IE, can't send HANDOVER PERFORMED!\n"); |
| return; |
| } |
| ho_perf_params.chosen_channel_present = true; |
| |
| /* Chosen Encryption Algorithm 3.2.2.44 */ |
| ho_perf_params.chosen_encr_alg = lchan->encr.alg_id; |
| ho_perf_params.chosen_encr_alg_present = true; |
| |
| if (ho->new_lchan->activate.info.requires_voice_stream) { |
| /* Speech Version (chosen) 3.2.2.51 */ |
| ho_perf_params.speech_version_chosen = gsm0808_permitted_speech(lchan->type, lchan->tch_mode); |
| ho_perf_params.speech_version_chosen_present = true; |
| |
| /* Speech Codec (chosen) 3.2.2.104 */ |
| if (gscon_is_aoip(conn)) { |
| /* Extrapolate speech codec from speech mode */ |
| gsm0808_speech_codec_from_chan_type(&sc, ho_perf_params.speech_version_chosen); |
| sc.cfg = conn->lchan->ch_mode_rate.s15_s0; |
| memcpy(&ho_perf_params.speech_codec_chosen, &sc, sizeof(sc)); |
| ho_perf_params.speech_codec_chosen_present = true; |
| } |
| } |
| |
| msg = gsm0808_create_handover_performed(&ho_perf_params); |
| if (!msg) { |
| LOG_HO(conn, LOGL_ERROR, "Failed to generate message, can't send HANDOVER PERFORMED!\n"); |
| return; |
| } |
| |
| rate_ctr_inc(&conn->sccp.msc->msc_ctrs->ctr[MSC_CTR_BSSMAP_TX_DT1_HANDOVER_PERFORMED]); |
| rc = gscon_sigtran_send(conn, msg); |
| if (rc < 0) { |
| LOG_HO(conn, LOGL_ERROR, "message sending failed, can't send HANDOVER PERFORMED!\n"); |
| return; |
| } |
| } |
| |
| /* 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); |
| } |
| /* Not 'else': above checks may still result in HO_RESULT_ERROR. */ |
| if (result == HO_RESULT_ERROR) { |
| /* Return a BSSMAP Handover Failure, as described in 3GPP TS 48.008 3.1.5.2.2 |
| * "Handover Resource Allocation Failure" */ |
| bsc_tx_bssmap_ho_failure(conn); |
| } |
| } else if (ho->scope & HO_INTER_BSC_OUT) { |
| switch (result) { |
| case HO_RESULT_OK: |
| break; |
| case HO_RESULT_FAIL_RR_HO_FAIL: |
| /* Return a BSSMAP Handover Failure, as described in 3GPP TS 48.008 3.1.5.3.2 |
| * "Handover Failure" */ |
| bsc_tx_bssmap_ho_failure(conn); |
| break; |
| default: |
| case HO_RESULT_FAIL_TIMEOUT: |
| switch (ho->fi->state) { |
| case HO_OUT_ST_WAIT_HO_COMMAND: |
| /* MSC never replied with a Handover Command. Fail and ignore the |
| * handover, continue to use the lchan. */ |
| break; |
| default: |
| case HO_OUT_ST_WAIT_CLEAR: |
| /* 3GPP TS 48.008 3.1.5.3.3 "Abnormal Conditions": if neither MS reports |
| * HO Failure nor the MSC sends a Clear Command, we should release the |
| * dedicated radio resources and send a Clear Request to the MSC. */ |
| lchan_release(conn->lchan, true, true, GSM48_RR_CAUSE_ABNORMAL_TIMER); |
| /* Once the channel release is through, the BSSMAP Clear will follow. */ |
| break; |
| } |
| break; |
| } |
| } |
| |
| /* 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; |
| |
| /* If the performed handover was an INTRA BSC HANDOVER, inform the MSC that a handover has happened */ |
| if (result == HO_RESULT_OK && ((ho->scope & HO_INTRA_CELL) || (ho->scope & HO_INTRA_BSC))) |
| send_handover_performed(conn); |
| |
| 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); |
| struct handover *ho = &conn->ho; |
| switch (event) { |
| |
| case HO_EV_LCHAN_ACTIVE: |
| /* - If the lchan is voiceless, no need to even think about the MGW. |
| * - If this is an intra-BSC Handover, we already have an RTP stream towards the MSC and aren't |
| * touching it. |
| * - If we're on SCCPlite, the MSC manages the MGW endpoint, all we do is the BTS side CI, so we can |
| * skip the part that would CRCX towards the MSC. |
| * So create an MSC side endpoint CI only if a voice lchan is established for an incoming inter-BSC |
| * handover on AoIP. Otherwise go on to send a Handover Command and wait for the Detect. |
| */ |
| if (ho->new_lchan->activate.info.requires_voice_stream |
| && (ho->scope & HO_INTER_BSC_IN) |
| && gscon_is_aoip(conn)) |
| ho_fsm_state_chg(HO_ST_WAIT_MGW_ENDPOINT_TO_MSC); |
| else |
| 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); |
| } |
| } |
| |
| /* Only for voice, only for inter-BSC Handover into this BSC, and only for AoIP: |
| * |
| * Establish the MGW endpoint CI that points towards the MSC. This needs to happen after the lchan (lchan_rtp_fsm) has |
| * created an MGW endpoint with the first CRCX, so that an endpoint is available, and before sending the Handover |
| * Request Acknowledge, so that the RTP address and port established towards the MSC can be included in the Handover |
| * Request Acknowledge message. |
| * (For SCCPlite, the MSC manages the CN side endpoint CI itself, and we don't need to send any RTP address in the |
| * Handover Request Acknowledge.) |
| * |
| * Actually, it should be possible to kick this off even above in handover_start_inter_bsc_in(), to do the CRCX towards |
| * the MSC at the same time as establishing the lchan. The gscon_ensure_mgw_endpoint() doesn't care which one of |
| * lchan_rtp_fsm or handover_start_inter_bsc_in() calls it first. The benefit would be that we'd send out the Handover |
| * Command ever so slightly sooner -- which isn't critical really, because a) how long does a CRCX take, milliseconds? |
| * and b) the time critical part is *after* the Handover Command was kicked off to keep the transition between cells as |
| * short as possible. The drawback of doing this earlier is code complexity: receiving the HO_EV_MSC_MGW_OK / |
| * HO_EV_MSC_MGW_FAIL events would need to be juggled in between the HO_EV_LCHAN_ACTIVE / HO_EV_LCHAN_ERROR. So the |
| * decision for now is to leave it here. |
| */ |
| 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); |
| const struct mgcp_conn_peer *mgw_info; |
| |
| switch (event) { |
| |
| case HO_EV_MSC_MGW_OK: |
| /* Ensure the endpoint is really there, and log it. This state is only entered for AoIP connections, see |
| * ho_fsm_wait_lchan_active() above. */ |
| mgw_info = osmo_mgcpc_ep_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_fsm_state_chg(HO_ST_WAIT_RR_HO_DETECT); |
| 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); |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| if (ho->new_lchan->fi_rtp) |
| osmo_fsm_inst_dispatch(ho->new_lchan->fi_rtp, |
| LCHAN_RTP_EV_READY_TO_SWITCH_RTP, 0); |
| 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"); |
| if (ho->new_lchan->fi_rtp) |
| osmo_fsm_inst_dispatch(ho->new_lchan->fi_rtp, |
| LCHAN_RTP_EV_READY_TO_SWITCH_RTP, 0); |
| 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_DETECT: |
| /* Numerous HO Detect RACH bursts may follow after the initial one, ignore. */ |
| return; |
| |
| case HO_EV_LCHAN_ESTABLISHED: |
| LOG_HO(conn, LOGL_DEBUG, "lchan established, still waiting for RR Handover Complete\n"); |
| /* The lchan is already done with all of its RTP setup. We will notice the lchan state |
| * being LCHAN_ST_ESTABLISHED in ho_fsm_wait_lchan_established_onenter(). */ |
| return; |
| |
| 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_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)) { |
| LOG_HO(conn, LOGL_DEBUG, "lchan already established earlier\n"); |
| ho_success(); |
| } |
| } |
| |
| static void ho_fsm_wait_lchan_established(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_ESTABLISHED: |
| ho_success(); |
| break; |
| |
| 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); |
| } |
| } |
| |
| static void ho_out_fsm_wait_clear(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_FAIL: |
| ho_fail(HO_RESULT_FAIL_RR_HO_FAIL, "Received RR Handover Failure message"); |
| 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_MGW_ENDPOINT_TO_MSC) |
| | S(HO_ST_WAIT_RR_HO_DETECT) |
| , |
| }, |
| [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) |
| , |
| .out_state_mask = 0 |
| | 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_DETECT) /* ignore extra HO RACH */ |
| | S(HO_EV_LCHAN_ESTABLISHED) |
| | 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) |
| , |
| }, |
| [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", |
| .in_event_mask = 0 |
| | S(HO_EV_RR_HO_FAIL) |
| , |
| .action = ho_out_fsm_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, |
| }; |