/* IuCS/IuPS Core Network interface of HNB-GW */ /* (C) 2015 by Harald Welte * 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 . * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*********************************************************************** * Incoming primitives from SCCP User SAP ***********************************************************************/ static int cn_ranap_rx_reset_cmd(struct hnbgw_cnlink *cnlink, const struct osmo_scu_unitdata_param *unitdata, RANAP_InitiatingMessage_t *imsg) { RANAP_CN_DomainIndicator_t domain; RANAP_ResetIEs_t ies; int rc; rc = ranap_decode_reseties(&ies, &imsg->value); domain = ies.cN_DomainIndicator; ranap_free_reseties(&ies); if (rc) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "Rx RESET: cannot decode IEs\n"); return -1; } if (cnlink->pool->domain != domain) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "Rx RESET indicates domain %s, but this is %s on domain %s\n", ranap_domain_name(domain), cnlink->name, ranap_domain_name(cnlink->pool->domain)); return -1; } cnlink_rx_reset_cmd(cnlink); return 0; } static int cn_ranap_rx_reset_ack(struct hnbgw_cnlink *cnlink, RANAP_SuccessfulOutcome_t *omsg) { RANAP_CN_DomainIndicator_t domain; RANAP_ResetAcknowledgeIEs_t ies; int rc; rc = ranap_decode_resetacknowledgeies(&ies, &omsg->value); domain = ies.cN_DomainIndicator; ranap_free_resetacknowledgeies(&ies); if (rc) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "Rx RESET ACK: cannot decode IEs\n"); return -1; } if (cnlink->pool->domain != domain) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "Rx RESET ACK indicates domain %s, but this is %s on domain %s\n", ranap_domain_name(domain), cnlink->name, ranap_domain_name(cnlink->pool->domain)); return -1; } cnlink_rx_reset_ack(cnlink); return 0; } struct cnlink_paging { struct llist_head entry; struct osmo_mobile_identity mi; struct osmo_mobile_identity mi2; time_t timestamp; }; static int cnlink_paging_destructor(struct cnlink_paging *p) { llist_del(&p->entry); return 0; } /* Return current timestamp in *timestamp, and the oldest still valid timestamp according to T3113 timeout. */ static const char *cnlink_paging_gettime(time_t *timestamp_p, time_t *timeout_p) { struct timespec now; time_t timestamp; /* get timestamp */ if (osmo_clock_gettime(CLOCK_MONOTONIC, &now) != 0) return "cannot get timestamp"; timestamp = now.tv_sec; if (timestamp_p) *timestamp_p = timestamp; if (timeout_p) *timeout_p = timestamp - osmo_tdef_get(hnbgw_T_defs, 3113, OSMO_TDEF_S, 15); return NULL; } static const char *cnlink_paging_add(struct hnbgw_cnlink *cnlink, const struct osmo_mobile_identity *mi, const struct osmo_mobile_identity *mi2) { struct cnlink_paging *p, *p2; time_t timestamp; time_t timeout; const char *errmsg; errmsg = cnlink_paging_gettime(×tamp, &timeout); if (errmsg) return errmsg; /* Prune all paging records that are older than the configured timeout. */ llist_for_each_entry_safe(p, p2, &cnlink->paging, entry) { if (p->timestamp >= timeout) continue; talloc_free(p); } /* Add new entry */ p = talloc_zero(cnlink, struct cnlink_paging); *p = (struct cnlink_paging){ .timestamp = timestamp, .mi = *mi, .mi2 = *mi2, }; llist_add_tail(&p->entry, &cnlink->paging); talloc_set_destructor(p, cnlink_paging_destructor); LOG_CNLINK(cnlink, DCN, LOGL_INFO, "Rx Paging from CN for %s %s\n", osmo_mobile_identity_to_str_c(OTC_SELECT, mi), osmo_mobile_identity_to_str_c(OTC_SELECT, mi2)); return NULL; } static const char *omi_from_ranap_ue_id(struct osmo_mobile_identity *mi, const RANAP_PermanentNAS_UE_ID_t *ranap_mi) { if (!ranap_mi) return "null UE ID"; if (ranap_mi->present != RANAP_PermanentNAS_UE_ID_PR_iMSI) return talloc_asprintf(OTC_SELECT, "unsupported UE ID type %u in RANAP Paging", ranap_mi->present); if (ranap_mi->choice.iMSI.size > sizeof(mi->imsi)) return talloc_asprintf(OTC_SELECT, "invalid IMSI size %d > %zu", ranap_mi->choice.iMSI.size, sizeof(mi->imsi)); *mi = (struct osmo_mobile_identity){ .type = GSM_MI_TYPE_IMSI, }; ranap_bcd_decode(mi->imsi, sizeof(mi->imsi), ranap_mi->choice.iMSI.buf, ranap_mi->choice.iMSI.size); LOGP(DCN, LOGL_DEBUG, "ranap MI %s = %s\n", osmo_hexdump(ranap_mi->choice.iMSI.buf, ranap_mi->choice.iMSI.size), mi->imsi); return NULL; } static const char *omi_from_ranap_temp_ue_id(struct osmo_mobile_identity *mi, const RANAP_TemporaryUE_ID_t *ranap_tmsi) { const OCTET_STRING_t *tmsi_str; if (!ranap_tmsi) return "null UE ID"; switch (ranap_tmsi->present) { case RANAP_TemporaryUE_ID_PR_tMSI: tmsi_str = &ranap_tmsi->choice.tMSI; break; case RANAP_TemporaryUE_ID_PR_p_TMSI: tmsi_str = &ranap_tmsi->choice.p_TMSI; break; default: return talloc_asprintf(OTC_SELECT, "unsupported Temporary UE ID type %u in RANAP Paging", ranap_tmsi->present); } *mi = (struct osmo_mobile_identity){ .type = GSM_MI_TYPE_TMSI, .tmsi = asn1str_to_u32(tmsi_str), }; LOGP(DCN, LOGL_DEBUG, "ranap temp UE ID = %s\n", osmo_mobile_identity_to_str_c(OTC_SELECT, mi)); return NULL; } static const char *cnlink_paging_add_ranap(struct hnbgw_cnlink *cnlink, RANAP_InitiatingMessage_t *imsg) { RANAP_PagingIEs_t ies; struct osmo_mobile_identity mi = {}; struct osmo_mobile_identity mi2 = {}; RANAP_CN_DomainIndicator_t domain; const char *errmsg; if (ranap_decode_pagingies(&ies, &imsg->value) < 0) return "decoding RANAP IEs failed"; domain = ies.cN_DomainIndicator; errmsg = omi_from_ranap_ue_id(&mi, &ies.permanentNAS_UE_ID); if (!errmsg && (ies.presenceMask & PAGINGIES_RANAP_TEMPORARYUE_ID_PRESENT)) errmsg = omi_from_ranap_temp_ue_id(&mi2, &ies.temporaryUE_ID); ranap_free_pagingies(&ies); LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "Decoded Paging: %s %s %s%s%s\n", ranap_domain_name(domain), osmo_mobile_identity_to_str_c(OTC_SELECT, &mi), mi2.type ? osmo_mobile_identity_to_str_c(OTC_SELECT, &mi2) : "-", errmsg ? " -- MI error: " : "", errmsg ? : ""); if (cnlink->pool->domain != domain) return talloc_asprintf(OTC_SELECT, "message indicates domain %s, but this is %s on domain %s\n", ranap_domain_name(domain), cnlink->name, ranap_domain_name(cnlink->pool->domain)); if (errmsg) return errmsg; return cnlink_paging_add(cnlink, &mi, &mi2); } /* If this cnlink has a recent Paging for the given MI, return true and drop the Paging record. * Else return false. */ static bool cnlink_match_paging_mi(struct hnbgw_cnlink *cnlink, const struct osmo_mobile_identity *mi, time_t timeout) { struct cnlink_paging *p, *p2; llist_for_each_entry_safe(p, p2, &cnlink->paging, entry) { if (p->timestamp < timeout) { talloc_free(p); continue; } if (osmo_mobile_identity_cmp(&p->mi, mi) && osmo_mobile_identity_cmp(&p->mi2, mi)) continue; talloc_free(p); return true; } return false; } static struct hnbgw_cnlink *cnlink_find_by_paging_mi(struct hnbgw_cnpool *cnpool, const struct osmo_mobile_identity *mi) { struct hnbgw_cnlink *cnlink; time_t timeout = 0; const char *errmsg; errmsg = cnlink_paging_gettime(NULL, &timeout); if (errmsg) LOGP(DCN, LOGL_ERROR, "%s\n", errmsg); llist_for_each_entry(cnlink, &cnpool->cnlinks, entry) { if (!cnlink_match_paging_mi(cnlink, mi, timeout)) continue; return cnlink; } return NULL; } static int cn_ranap_rx_paging_cmd(struct hnbgw_cnlink *cnlink, RANAP_InitiatingMessage_t *imsg, const uint8_t *data, unsigned int len) { const char *errmsg; struct hnb_context *hnb; bool is_ps = cnlink->pool->domain == DOMAIN_PS; errmsg = cnlink_paging_add_ranap(cnlink, imsg); if (errmsg) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "Rx Paging from CN: %s. Dropping paging record." " Later on, the Paging Response may be forwarded to the wrong CN peer.\n", errmsg); return -1; } /* FIXME: determine which HNBs to send this Paging command, * rather than broadcasting to all HNBs */ llist_for_each_entry(hnb, &g_hnbgw->hnb_list, list) { if (!hnb->hnb_registered) continue; if (is_ps) HNBP_CTR_INC(hnb->persistent, HNB_CTR_PS_PAGING_ATTEMPTED); else HNBP_CTR_INC(hnb->persistent, HNB_CTR_CS_PAGING_ATTEMPTED); rua_tx_udt(hnb, data, len); } return 0; } static int ranap_rx_udt_dl_initiating_msg(struct hnbgw_cnlink *cnlink, const struct osmo_scu_unitdata_param *unitdata, RANAP_InitiatingMessage_t *imsg, const uint8_t *data, unsigned int len) { switch (imsg->procedureCode) { case RANAP_ProcedureCode_id_Reset: CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_RESET); return cn_ranap_rx_reset_cmd(cnlink, unitdata, imsg); case RANAP_ProcedureCode_id_Paging: CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_PAGING); return cn_ranap_rx_paging_cmd(cnlink, imsg, data, len); case RANAP_ProcedureCode_id_OverloadControl: /* Overload ind */ CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_OVERLOAD_IND); break; case RANAP_ProcedureCode_id_ErrorIndication: /* Error ind */ CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_ERROR_IND); break; case RANAP_ProcedureCode_id_ResetResource: /* request */ case RANAP_ProcedureCode_id_InformationTransfer: case RANAP_ProcedureCode_id_DirectInformationTransfer: case RANAP_ProcedureCode_id_UplinkInformationExchange: CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_UNSUPPORTED); LOGP(DRANAP, LOGL_NOTICE, "Received unsupported RANAP " "Procedure %ld from CN, ignoring\n", imsg->procedureCode); break; default: CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_UNKNOWN); LOGP(DRANAP, LOGL_NOTICE, "Received suspicious RANAP " "Procedure %ld from CN, ignoring\n", imsg->procedureCode); break; } return 0; } static int ranap_rx_udt_dl_successful_msg(struct hnbgw_cnlink *cnlink, RANAP_SuccessfulOutcome_t *omsg) { switch (omsg->procedureCode) { case RANAP_ProcedureCode_id_Reset: /* Reset acknowledge */ CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_RESET); return cn_ranap_rx_reset_ack(cnlink, omsg); case RANAP_ProcedureCode_id_ResetResource: /* response */ case RANAP_ProcedureCode_id_InformationTransfer: case RANAP_ProcedureCode_id_DirectInformationTransfer: case RANAP_ProcedureCode_id_UplinkInformationExchange: CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_UNSUPPORTED); LOGP(DRANAP, LOGL_NOTICE, "Received unsupported RANAP " "Procedure %ld from CN, ignoring\n", omsg->procedureCode); break; default: CNLINK_CTR_INC(cnlink, CNLINK_CTR_RANAP_RX_UDT_UNKNOWN); LOGP(DRANAP, LOGL_NOTICE, "Received suspicious RANAP " "Procedure %ld from CN, ignoring\n", omsg->procedureCode); break; } return 0; } static int hnbgw_ranap_rx_udt_dl(struct hnbgw_cnlink *cnlink, const struct osmo_scu_unitdata_param *unitdata, RANAP_RANAP_PDU_t *pdu, const uint8_t *data, unsigned int len) { int rc; switch (pdu->present) { case RANAP_RANAP_PDU_PR_initiatingMessage: rc = ranap_rx_udt_dl_initiating_msg(cnlink, unitdata, &pdu->choice.initiatingMessage, data, len); break; case RANAP_RANAP_PDU_PR_successfulOutcome: rc = ranap_rx_udt_dl_successful_msg(cnlink, &pdu->choice.successfulOutcome); break; case RANAP_RANAP_PDU_PR_unsuccessfulOutcome: LOGP(DRANAP, LOGL_NOTICE, "Received unsupported RANAP " "unsuccessful outcome procedure %ld from CN, ignoring\n", pdu->choice.unsuccessfulOutcome.procedureCode); rc = -ENOTSUP; break; default: LOGP(DRANAP, LOGL_NOTICE, "Received suspicious RANAP " "presence %u from CN, ignoring\n", pdu->present); rc = -EINVAL; break; } return rc; } static int handle_cn_ranap(struct hnbgw_cnlink *cnlink, const struct osmo_scu_unitdata_param *unitdata, const uint8_t *data, unsigned int len) { RANAP_RANAP_PDU_t _pdu, *pdu = &_pdu; asn_dec_rval_t dec_ret; int rc; memset(pdu, 0, sizeof(*pdu)); dec_ret = aper_decode(NULL, &asn_DEF_RANAP_RANAP_PDU, (void **) &pdu, data, len, 0, 0); if (dec_ret.code != RC_OK) { LOGP(DRANAP, LOGL_ERROR, "Error in RANAP ASN.1 decode\n"); return -1; } rc = hnbgw_ranap_rx_udt_dl(cnlink, unitdata, pdu, data, len); ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_RANAP_RANAP_PDU, pdu); return rc; } static struct hnbgw_cnlink *cnlink_from_addr(struct hnbgw_sccp_user *hsu, const struct osmo_sccp_addr *calling_addr, const struct osmo_prim_hdr *oph) { struct hnbgw_cnlink *cnlink = NULL; cnlink = hnbgw_cnlink_find_by_addr(hsu, calling_addr); if (!cnlink) { LOG_HSI(hsu, DRANAP, LOGL_ERROR, "Rx from unknown SCCP peer: %s: %s\n", osmo_sccp_inst_addr_name(hsu->ss7->sccp, calling_addr), osmo_scu_prim_hdr_name_c(OTC_SELECT, oph)); return NULL; } return cnlink; } static struct hnbgw_context_map *map_from_conn_id(struct hnbgw_sccp_user *hsu, uint32_t conn_id, const struct osmo_prim_hdr *oph) { struct hnbgw_context_map *map; hash_for_each_possible(hsu->hnbgw_context_map_by_conn_id, map, hnbgw_sccp_user_entry, conn_id) return map; LOGP(DRANAP, LOGL_ERROR, "Rx for unknown SCCP connection ID: %u: %s\n", conn_id, osmo_scu_prim_hdr_name_c(OTC_SELECT, oph)); return NULL; } static int handle_cn_unitdata(struct hnbgw_sccp_user *hsu, const struct osmo_scu_unitdata_param *param, struct osmo_prim_hdr *oph) { struct hnbgw_cnlink *cnlink = cnlink_from_addr(hsu, ¶m->calling_addr, oph); if (!cnlink) return -ENOENT; if (param->called_addr.ssn != OSMO_SCCP_SSN_RANAP) { LOGP(DCN, LOGL_NOTICE, "N-UNITDATA.ind for unknown SSN %u\n", param->called_addr.ssn); return -1; } return handle_cn_ranap(cnlink, param, msgb_l2(oph->msg), msgb_l2len(oph->msg)); } static int handle_cn_conn_conf(struct hnbgw_sccp_user *hsu, const struct osmo_scu_connect_param *param, struct osmo_prim_hdr *oph) { struct hnbgw_context_map *map; map = map_from_conn_id(hsu, param->conn_id, oph); if (!map || !map->cnlink) return -ENOENT; LOGP(DCN, LOGL_DEBUG, "handle_cn_conn_conf() conn_id=%d, addrs: called=%s calling=%s responding=%s\n", param->conn_id, cnlink_sccp_addr_to_str(map->cnlink, ¶m->called_addr), cnlink_sccp_addr_to_str(map->cnlink, ¶m->calling_addr), cnlink_sccp_addr_to_str(map->cnlink, ¶m->responding_addr)); map_sccp_dispatch(map, MAP_SCCP_EV_RX_CONNECTION_CONFIRM, oph->msg); return 0; } static int handle_cn_data_ind(struct hnbgw_sccp_user *hsu, const struct osmo_scu_data_param *param, struct osmo_prim_hdr *oph) { struct hnbgw_context_map *map; map = map_from_conn_id(hsu, param->conn_id, oph); if (!map || !map->cnlink) return -ENOENT; return map_sccp_dispatch(map, MAP_SCCP_EV_RX_DATA_INDICATION, oph->msg); } static int handle_cn_disc_ind(struct hnbgw_sccp_user *hsu, const struct osmo_scu_disconn_param *param, struct osmo_prim_hdr *oph) { struct hnbgw_context_map *map; map = map_from_conn_id(hsu, param->conn_id, oph); if (!map || !map->cnlink) return -ENOENT; LOGP(DCN, LOGL_DEBUG, "handle_cn_disc_ind() conn_id=%u responding_addr=%s\n", param->conn_id, cnlink_sccp_addr_to_str(map->cnlink, ¶m->responding_addr)); return map_sccp_dispatch(map, MAP_SCCP_EV_RX_RELEASED, oph->msg); } static struct hnbgw_cnlink *_cnlink_find_by_remote_pc(struct hnbgw_cnpool *cnpool, struct osmo_ss7_instance *cs7, uint32_t pc) { struct hnbgw_cnlink *cnlink; llist_for_each_entry(cnlink, &cnpool->cnlinks, entry) { if (!cnlink->hnbgw_sccp_user) continue; if (cnlink->hnbgw_sccp_user->ss7 != cs7) continue; if ((cnlink->remote_addr.presence & OSMO_SCCP_ADDR_T_PC) == 0) continue; if (cnlink->remote_addr.pc != pc) continue; return cnlink; } return NULL; } /* Find a cnlink by its remote sigtran point code on a given cs7 instance. */ static struct hnbgw_cnlink *cnlink_find_by_remote_pc(struct osmo_ss7_instance *cs7, uint32_t pc) { struct hnbgw_cnlink *cnlink; cnlink = _cnlink_find_by_remote_pc(&g_hnbgw->sccp.cnpool_iucs, cs7, pc); if (!cnlink) cnlink = _cnlink_find_by_remote_pc(&g_hnbgw->sccp.cnpool_iups, cs7, pc); return cnlink; } static void handle_pcstate_ind(struct hnbgw_sccp_user *hsu, const struct osmo_scu_pcstate_param *pcst) { struct hnbgw_cnlink *cnlink; bool connected; bool disconnected; struct osmo_ss7_instance *cs7 = hsu->ss7; LOGP(DCN, LOGL_DEBUG, "N-PCSTATE ind: affected_pc=%u sp_status=%s remote_sccp_status=%s\n", pcst->affected_pc, osmo_sccp_sp_status_name(pcst->sp_status), osmo_sccp_rem_sccp_status_name(pcst->remote_sccp_status)); /* If we don't care about that point-code, ignore PCSTATE. */ cnlink = cnlink_find_by_remote_pc(cs7, pcst->affected_pc); if (!cnlink) return; /* See if this marks the point code to have become available, or to have been lost. * * I want to detect two events: * - connection event (both indicators say PC is reachable). * - disconnection event (at least one indicator says the PC is not reachable). * * There are two separate incoming indicators with various possible values -- the incoming events can be: * * - neither connection nor disconnection indicated -- just indicating congestion * connected == false, disconnected == false --> do nothing. * - both incoming values indicate that we are connected * --> trigger connected * - both indicate we are disconnected * --> trigger disconnected * - one value indicates 'connected', the other indicates 'disconnected' * --> trigger disconnected * * Congestion could imply that we're connected, but it does not indicate that a PC's reachability changed, so no need to * trigger on that. */ connected = false; disconnected = false; switch (pcst->sp_status) { case OSMO_SCCP_SP_S_ACCESSIBLE: connected = true; break; case OSMO_SCCP_SP_S_INACCESSIBLE: disconnected = true; break; default: case OSMO_SCCP_SP_S_CONGESTED: /* Neither connecting nor disconnecting */ break; } switch (pcst->remote_sccp_status) { case OSMO_SCCP_REM_SCCP_S_AVAILABLE: if (!disconnected) connected = true; break; case OSMO_SCCP_REM_SCCP_S_UNAVAILABLE_UNKNOWN: case OSMO_SCCP_REM_SCCP_S_UNEQUIPPED: case OSMO_SCCP_REM_SCCP_S_INACCESSIBLE: disconnected = true; connected = false; break; default: case OSMO_SCCP_REM_SCCP_S_CONGESTED: /* Neither connecting nor disconnecting */ break; } if (disconnected && cnlink_is_conn_ready(cnlink)) { LOG_CNLINK(cnlink, DCN, LOGL_NOTICE, "now unreachable: N-PCSTATE ind: pc=%u sp_status=%s remote_sccp_status=%s\n", pcst->affected_pc, osmo_sccp_sp_status_name(pcst->sp_status), osmo_sccp_rem_sccp_status_name(pcst->remote_sccp_status)); /* A previously usable cnlink has disconnected. Kick it back to DISC state. */ cnlink_set_disconnected(cnlink); } else if (connected && !cnlink_is_conn_ready(cnlink)) { LOG_CNLINK(cnlink, DCN, LOGL_NOTICE, "now available: N-PCSTATE ind: pc=%u sp_status=%s remote_sccp_status=%s\n", pcst->affected_pc, osmo_sccp_sp_status_name(pcst->sp_status), osmo_sccp_rem_sccp_status_name(pcst->remote_sccp_status)); /* A previously unusable cnlink has become reachable. Trigger immediate RANAP RESET -- we would resend a * RESET either way, but we might as well do it now to speed up connecting. */ cnlink_resend_reset(cnlink); } } /* Entry point for primitives coming up from SCCP User SAP */ static int sccp_sap_up(struct osmo_prim_hdr *oph, void *ctx) { struct osmo_sccp_user *scu = ctx; struct hnbgw_sccp_user *hsu; struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph; int rc = 0; LOGP(DCN, LOGL_DEBUG, "sccp_sap_up(%s)\n", osmo_scu_prim_name(oph)); if (!scu) { LOGP(DCN, LOGL_ERROR, "sccp_sap_up(): NULL osmo_sccp_user, cannot send prim (sap %u prim %u op %d)\n", oph->sap, oph->primitive, oph->operation); return -1; } hsu = osmo_sccp_user_get_priv(scu); if (!hsu) { LOGP(DCN, LOGL_ERROR, "sccp_sap_up(): NULL hnbgw_sccp_user, cannot send prim (sap %u prim %u op %d)\n", oph->sap, oph->primitive, oph->operation); return -1; } talloc_steal(OTC_SELECT, oph->msg); switch (OSMO_PRIM_HDR(oph)) { case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_INDICATION): rc = handle_cn_unitdata(hsu, &prim->u.unitdata, oph); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM): rc = handle_cn_conn_conf(hsu, &prim->u.connect, oph); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION): rc = handle_cn_data_ind(hsu, &prim->u.data, oph); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION): rc = handle_cn_disc_ind(hsu, &prim->u.disconnect, oph); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_PCSTATE, PRIM_OP_INDICATION): handle_pcstate_ind(hsu, &prim->u.pcstate); break; default: LOGP(DCN, LOGL_ERROR, "Received unknown prim %u from SCCP USER SAP\n", OSMO_PRIM_HDR(oph)); break; } return rc; } static bool addr_has_pc_and_ssn(const struct osmo_sccp_addr *addr) { if (!(addr->presence & OSMO_SCCP_ADDR_T_SSN)) return false; if (!(addr->presence & OSMO_SCCP_ADDR_T_PC)) return false; return true; } static int resolve_addr_name(struct osmo_sccp_addr *dest, struct osmo_ss7_instance **ss7, const char *addr_name, const char *label, uint32_t default_pc) { if (!addr_name) { osmo_sccp_make_addr_pc_ssn(dest, default_pc, OSMO_SCCP_SSN_RANAP); if (label) LOGP(DCN, LOGL_INFO, "%s remote addr not configured, using default: %s\n", label, osmo_sccp_addr_name(*ss7, dest)); return 0; } *ss7 = osmo_sccp_addr_by_name(dest, addr_name); if (!*ss7) { if (label) LOGP(DCN, LOGL_ERROR, "%s remote addr: no such SCCP address book entry: '%s'\n", label, addr_name); return -1; } osmo_sccp_addr_set_ssn(dest, OSMO_SCCP_SSN_RANAP); if (!addr_has_pc_and_ssn(dest)) { if (label) LOGP(DCN, LOGL_ERROR, "Invalid/incomplete %s remote-addr: %s\n", label, osmo_sccp_addr_name(*ss7, dest)); return -1; } return 0; } void hnbgw_cnpool_apply_cfg(struct hnbgw_cnpool *cnpool) { struct osmo_nri_range *r; cnpool->use.nri_bitlen = cnpool->vty.nri_bitlen; osmo_nri_ranges_free(cnpool->use.null_nri_ranges); cnpool->use.null_nri_ranges = osmo_nri_ranges_alloc(g_hnbgw); llist_for_each_entry(r, &cnpool->vty.null_nri_ranges->entries, entry) osmo_nri_ranges_add(cnpool->use.null_nri_ranges, r); } static void hnbgw_cnlink_cfg_copy(struct hnbgw_cnlink *cnlink) { struct osmo_nri_range *r; osmo_talloc_replace_string(cnlink, &cnlink->use.remote_addr_name, cnlink->vty.remote_addr_name); osmo_nri_ranges_free(cnlink->use.nri_ranges); cnlink->use.nri_ranges = osmo_nri_ranges_alloc(cnlink); llist_for_each_entry(r, &cnlink->vty.nri_ranges->entries, entry) osmo_nri_ranges_add(cnlink->use.nri_ranges, r); } static bool hnbgw_cnlink_sccp_cfg_changed(struct hnbgw_cnlink *cnlink) { bool changed = false; if (cnlink->vty.remote_addr_name && cnlink->use.remote_addr_name) { struct osmo_ss7_instance *ss7; struct osmo_sccp_addr remote_addr = {}; /* Instead of comparing whether the address book entry names are different, actually resolve the * resulting SCCP address, and only restart the cnlink if the resulting address changed. */ resolve_addr_name(&remote_addr, &ss7, cnlink->vty.remote_addr_name, NULL, DEFAULT_PC_HNBGW); if (osmo_sccp_addr_cmp(&remote_addr, &cnlink->remote_addr, OSMO_SCCP_ADDR_T_PC | OSMO_SCCP_ADDR_T_SSN)) changed = true; } else if (cnlink->vty.remote_addr_name != cnlink->use.remote_addr_name) { /* One of them is NULL, the other is not. */ changed = true; } /* if more cnlink configuration is added in the future, it needs to be compared here. */ return changed; } static void hnbgw_cnlink_drop_sccp(struct hnbgw_cnlink *cnlink) { struct hnbgw_context_map *map, *map2; llist_for_each_entry_safe(map, map2, &cnlink->map_list, hnbgw_cnlink_entry) { map_sccp_dispatch(map, MAP_SCCP_EV_USER_ABORT, NULL); } cnlink->hnbgw_sccp_user = NULL; } static void hnbgw_cnlink_log_self(struct hnbgw_cnlink *cnlink) { struct osmo_ss7_instance *ss7 = cnlink->hnbgw_sccp_user->ss7; LOG_CNLINK(cnlink, DCN, LOGL_NOTICE, "using: cs7-%u %s <-> %s %s %s\n", ss7->cfg.id, /* printing the entire SCCP address is quite long, rather just print the point-code */ osmo_ss7_pointcode_print(ss7, cnlink->hnbgw_sccp_user->local_addr.pc), osmo_ss7_pointcode_print2(ss7, cnlink->remote_addr.pc), cnlink->name, cnlink->use.remote_addr_name ? : "(default remote point-code)"); } /* If not present yet, set up all of osmo_ss7_instance, osmo_sccp_instance and hnbgw_sccp_user for the given cnlink. * The cs7 instance nr to use is determined by cnlink->remote_addr_name, or cs7 instance 0 if that is not present. * Set cnlink->hnbgw_sccp_user to the new SCCP instance. Return 0 on success, negative on error. */ int hnbgw_cnlink_start_or_restart(struct hnbgw_cnlink *cnlink) { struct osmo_ss7_instance *ss7 = NULL; struct osmo_sccp_instance *sccp; struct osmo_sccp_user *sccp_user; uint32_t local_pc; struct hnbgw_sccp_user *hsu; /* If a hnbgw_sccp_user has already been set up, use that. */ if (cnlink->hnbgw_sccp_user) { if (hnbgw_cnlink_sccp_cfg_changed(cnlink)) { LOG_CNLINK(cnlink, DCN, LOGL_NOTICE, "config changed, restarting SCCP\n"); hnbgw_cnlink_drop_sccp(cnlink); } else { LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "SCCP instance already set up, using %s\n", cnlink->hnbgw_sccp_user->name); return 0; } } else { LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "no SCCP instance selected yet\n"); } /* Copy the current configuration: cnlink->use = cnlink->vty */ hnbgw_cnlink_cfg_copy(cnlink); /* Figure out which cs7 instance to use. If cnlink->remote_addr_name is set, it points to an address book entry * in a specific cs7 instance. If it is not set, leave ss7 == NULL to use cs7 instance 0. */ if (cnlink->use.remote_addr_name) { if (resolve_addr_name(&cnlink->remote_addr, &ss7, cnlink->use.remote_addr_name, cnlink->name, DEFAULT_PC_HNBGW)) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "cannot initialize SCCP: there is no SCCP address named '%s'\n", cnlink->use.remote_addr_name); return -ENOENT; } LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "remote-addr is '%s', using cs7 instance %u\n", cnlink->use.remote_addr_name, ss7->cfg.id); } else { /* If no address is configured, use the default remote CN address, according to legacy behavior. */ osmo_sccp_make_addr_pc_ssn(&cnlink->remote_addr, cnlink->pool->default_remote_pc, OSMO_SCCP_SSN_RANAP); } /* If no 'cs7 instance' has been selected by the address, see if there already is a cs7 0 we can use by default. * If it doesn't exist, it will get created by osmo_sccp_simple_client_on_ss7_id(). */ if (!ss7) { ss7 = osmo_ss7_instance_find(0); LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "Using default 'cs7 instance 0' (%s)\n", ss7 ? "already exists" : "will create"); } if (ss7) { /* Has another cnlink already set up an SCCP instance for this ss7? */ llist_for_each_entry(hsu, &g_hnbgw->sccp.users, entry) { if (hsu->ss7 != ss7) continue; cnlink->hnbgw_sccp_user = hsu; LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "using existing SCCP instance %s on cs7 instance %u\n", hsu->name, ss7->cfg.id); hnbgw_cnlink_log_self(cnlink); return 0; } /* else cnlink->hnbgw_sccp_user stays NULL and is set up below. */ LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "cs7 instance %u has no configured SCCP instance yet\n", ss7->cfg.id); } /* No SCCP instance yet for this ss7. Create it. If no address name is given that resolves to a * particular cs7 instance above, use 'cs7 instance 0'. */ sccp = osmo_sccp_simple_client_on_ss7_id(g_hnbgw, ss7 ? ss7->cfg.id : 0, cnlink->name, DEFAULT_PC_HNBGW, OSMO_SS7_ASP_PROT_M3UA, 0, "localhost", -1, "localhost"); if (!sccp) { LOG_CNLINK(cnlink, DCN, LOGL_ERROR, "Failed to configure SCCP on 'cs7 instance %u'\n", ss7 ? ss7->cfg.id : 0); return -1; } ss7 = osmo_sccp_get_ss7(sccp); LOG_CNLINK(cnlink, DCN, LOGL_NOTICE, "created SCCP instance on cs7 instance %u\n", ss7->cfg.id); /* Bind the SCCP user, using the cs7 instance's default point-code if one is configured, or osmo-hnbgw's default * local PC. */ if (osmo_ss7_pc_is_valid(ss7->cfg.primary_pc)) local_pc = ss7->cfg.primary_pc; else local_pc = DEFAULT_PC_HNBGW; LOG_CNLINK(cnlink, DCN, LOGL_DEBUG, "binding OsmoHNBGW user to cs7 instance %u, local PC %u = %s\n", ss7->cfg.id, local_pc, osmo_ss7_pointcode_print(ss7, local_pc)); sccp_user = osmo_sccp_user_bind_pc(sccp, "OsmoHNBGW", sccp_sap_up, OSMO_SCCP_SSN_RANAP, local_pc); if (!sccp_user) { LOGP(DCN, LOGL_ERROR, "Failed to init SCCP User\n"); return -1; } hsu = talloc_zero(cnlink, struct hnbgw_sccp_user); *hsu = (struct hnbgw_sccp_user){ .name = talloc_asprintf(hsu, "cs7-%u.sccp", ss7->cfg.id), .ss7 = ss7, .sccp_user = sccp_user, }; osmo_sccp_make_addr_pc_ssn(&hsu->local_addr, local_pc, OSMO_SCCP_SSN_RANAP); hash_init(hsu->hnbgw_context_map_by_conn_id); osmo_sccp_user_set_priv(sccp_user, hsu); llist_add_tail(&hsu->entry, &g_hnbgw->sccp.users); cnlink->hnbgw_sccp_user = hsu; hnbgw_cnlink_log_self(cnlink); return 0; } void hnbgw_cnpool_cnlinks_start_or_restart(struct hnbgw_cnpool *cnpool) { struct hnbgw_cnlink *cnlink; hnbgw_cnpool_apply_cfg(cnpool); llist_for_each_entry(cnlink, &cnpool->cnlinks, entry) { hnbgw_cnlink_start_or_restart(cnlink); } } void hnbgw_cnpool_start(struct hnbgw_cnpool *cnpool) { /* Legacy compat: when there is no 'msc N' at all in the config file, set up 'msc 0' with default values (or * 'sgsn' depending on cnpool). */ if (llist_empty(&cnpool->cnlinks)) cnlink_get_nr(cnpool, 0, true); hnbgw_cnpool_cnlinks_start_or_restart(cnpool); } struct hnbgw_cnlink *cnlink_get_nr(struct hnbgw_cnpool *cnpool, int nr, bool create_if_missing) { struct hnbgw_cnlink *cnlink; llist_for_each_entry(cnlink, &cnpool->cnlinks, entry) { if (cnlink->nr == nr) return cnlink; } if (!create_if_missing) return NULL; return cnlink_alloc(cnpool, nr); } static bool cnlink_matches(const struct hnbgw_cnlink *cnlink, const struct hnbgw_sccp_user *hsu, const struct osmo_sccp_addr *remote_addr) { if (cnlink->hnbgw_sccp_user != hsu) return false; if (osmo_sccp_addr_cmp(&cnlink->remote_addr, remote_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) return false; return true; } struct hnbgw_cnlink *hnbgw_cnlink_find_by_addr(const struct hnbgw_sccp_user *hsu, const struct osmo_sccp_addr *remote_addr) { struct hnbgw_cnlink *cnlink; llist_for_each_entry(cnlink, &g_hnbgw->sccp.cnpool_iucs.cnlinks, entry) { if (cnlink_matches(cnlink, hsu, remote_addr)) return cnlink; } llist_for_each_entry(cnlink, &g_hnbgw->sccp.cnpool_iups.cnlinks, entry) { if (cnlink_matches(cnlink, hsu, remote_addr)) return cnlink; } return NULL; } static bool is_cnlink_usable(struct hnbgw_cnlink *cnlink, bool is_emerg) { if (is_emerg && !cnlink->allow_emerg) return false; if (!cnlink->hnbgw_sccp_user || !cnlink->hnbgw_sccp_user->sccp_user) return false; if (!cnlink_is_conn_ready(cnlink)) return false; return true; } /* Decide which MSC/SGSN to forward this Complete Layer 3 request to. The current Layer 3 Info is passed in map->l3. * a) If the subscriber was previously paged from a particular CN link, that CN link shall receive the Paging Response. * b) If the message contains an NRI indicating a particular CN link that is currently connected, that CN link shall * handle this conn. * c) All other cases distribute the messages across connected CN links in a round-robin fashion. */ struct hnbgw_cnlink *hnbgw_cnlink_select(struct hnbgw_context_map *map) { struct hnbgw_cnpool *cnpool = map->is_ps ? &g_hnbgw->sccp.cnpool_iups : &g_hnbgw->sccp.cnpool_iucs; struct hnbgw_cnlink *cnlink; struct hnbgw_cnlink *round_robin_next = NULL; struct hnbgw_cnlink *round_robin_first = NULL; unsigned int round_robin_next_nr; int16_t nri_v = -1; bool is_null_nri = false; uint8_t nri_bitlen = cnpool->use.nri_bitlen; /* Match IMSI with previous Paging */ if (map->l3.gsm48_msg_type == GSM48_MT_RR_PAG_RESP) { cnlink = cnlink_find_by_paging_mi(cnpool, &map->l3.mi); if (cnlink) { LOG_MAP(map, DCN, LOGL_INFO, "CN link paging record selects %s %d\n", cnpool->peer_name, cnlink->nr); CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_SUBSCR_PAGED); return cnlink; } /* If there is no match, go on with other ways */ } #define LOG_NRI(LOGLEVEL, FORMAT, ARGS...) \ LOG_MAP(map, DCN, LOGLEVEL, "%s NRI(%dbit)=0x%x=%d: " FORMAT, osmo_mobile_identity_to_str_c(OTC_SELECT, &map->l3.mi), \ nri_bitlen, nri_v, nri_v, ##ARGS) /* Get the NRI bits either from map->l3.nri, or extract NRI bits from TMSI. * The NRI possibly indicates which MSC is responsible. */ if (map->l3.gmm_nri_container >= 0) { nri_v = map->l3.gmm_nri_container; /* The 'TMSI based NRI container' is always 10 bits long. If the relevant NRI length is configured to be * less than that, ignore the lower bits. */ if (nri_bitlen < 10) nri_v >>= 10 - nri_bitlen; } else if (map->l3.mi.type == GSM_MI_TYPE_TMSI) { if (osmo_tmsi_nri_v_get(&nri_v, map->l3.mi.tmsi, nri_bitlen)) { LOG_NRI(LOGL_ERROR, "Unable to retrieve NRI from TMSI 0x%x, nri_bitlen == %u\n", map->l3.mi.tmsi, nri_bitlen); nri_v = -1; } } if (map->l3.from_other_plmn && nri_v >= 0) { /* If a subscriber was previously attached to a different PLMN, it might still send the other * PLMN's TMSI identity in an IMSI Attach. The LU sends a LAI indicating the previous PLMN. If * it mismatches our PLMN, ignore the NRI. */ LOG_NRI(LOGL_DEBUG, "This Complete Layer 3 message indicates a switch from another PLMN. Ignoring the NRI.\n"); nri_v = -1; } if (nri_v >= 0) is_null_nri = osmo_nri_v_matches_ranges(nri_v, cnpool->use.null_nri_ranges); if (is_null_nri) LOG_NRI(LOGL_DEBUG, "this is a NULL-NRI\n"); /* Iterate CN links to find one that matches the extracted NRI, and the next round-robin target for the case no * NRI match is found. */ round_robin_next_nr = (map->l3.is_emerg ? cnpool->round_robin_next_emerg_nr : cnpool->round_robin_next_nr); llist_for_each_entry(cnlink, &cnpool->cnlinks, entry) { bool nri_matches_cnlink = (nri_v >= 0 && osmo_nri_v_matches_ranges(nri_v, cnlink->use.nri_ranges)); if (!is_cnlink_usable(cnlink, map->l3.is_emerg)) { if (nri_matches_cnlink) { LOG_NRI(LOGL_DEBUG, "NRI matches %s %d, but this %s is currently not connected\n", cnpool->peer_name, cnlink->nr, cnpool->peer_name); CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_SUBSCR_ATTACH_LOST); } continue; } /* Return CN link if it matches this NRI, with some debug logging. */ if (nri_matches_cnlink) { if (is_null_nri) { LOG_NRI(LOGL_DEBUG, "NRI matches %s %d, but this NRI is also configured as NULL-NRI\n", cnpool->peer_name, cnlink->nr); } else { LOG_NRI(LOGL_INFO, "NRI match selects %s %d\n", cnpool->peer_name, cnlink->nr); CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_SUBSCR_KNOWN); if (map->l3.is_emerg) { CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_EMERG_FORWARDED); CNPOOL_CTR_INC(cnpool, CNPOOL_CTR_EMERG_FORWARDED); } return cnlink; } } /* Figure out the next round-robin MSC. The MSCs may appear unsorted in net->mscs. Make sure to linearly * round robin the MSCs by number: pick the lowest msc->nr >= round_robin_next_nr, and also remember the * lowest available msc->nr to wrap back to that in case no next MSC is left. * * MSCs configured with `no allow-attach` do not accept new subscribers and hence must not be picked by * round-robin. Such an MSC still provides service for already attached subscribers: those that * successfully performed IMSI-Attach and have a TMSI with an NRI pointing at that MSC. We only avoid * adding IMSI-Attach of new subscribers. The idea is that the MSC is in a mode of off-loading * subscribers, and the MSC decides when each subscriber is off-loaded, by assigning the NULL-NRI in a * new TMSI (at the next periodical LU). So until the MSC decides to offload, an attached subscriber * remains attached to that MSC and is free to use its services. */ if (!cnlink->allow_attach) continue; /* Find the allowed cnlink with the lowest nr */ if (!round_robin_first || cnlink->nr < round_robin_first->nr) round_robin_first = cnlink; /* Find the allowed cnlink with the lowest nr >= round_robin_next_nr */ if (cnlink->nr >= round_robin_next_nr && (!round_robin_next || cnlink->nr < round_robin_next->nr)) round_robin_next = cnlink; } if (nri_v >= 0 && !is_null_nri) LOG_NRI(LOGL_DEBUG, "No %s found for this NRI, doing round-robin\n", cnpool->peer_name); /* No dedicated CN link found. Choose by round-robin. * If round_robin_next is NULL, there are either no more CN links at/after round_robin_next_nr, or none of * them are usable -- wrap to the start. */ cnlink = round_robin_next ? : round_robin_first; if (!cnlink) { CNPOOL_CTR_INC(cnpool, CNPOOL_CTR_SUBSCR_NO_CNLINK); if (map->l3.is_emerg) CNPOOL_CTR_INC(cnpool, CNPOOL_CTR_EMERG_LOST); return NULL; } LOG_MAP(map, DCN, LOGL_INFO, "CN link round-robin selects %s %d\n", cnpool->peer_name, cnlink->nr); if (is_null_nri) CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_SUBSCR_REATTACH); else CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_SUBSCR_NEW); if (map->l3.is_emerg) { CNLINK_CTR_INC(cnlink, CNLINK_CTR_CNPOOL_EMERG_FORWARDED); CNPOOL_CTR_INC(cnpool, CNPOOL_CTR_EMERG_FORWARDED); } /* A CN link was picked by round-robin, so update the next round-robin nr to pick */ if (map->l3.is_emerg) cnpool->round_robin_next_emerg_nr = cnlink->nr + 1; else cnpool->round_robin_next_nr = cnlink->nr + 1; return cnlink; #undef LOG_NRI } char *cnlink_sccp_addr_to_str(struct hnbgw_cnlink *cnlink, const struct osmo_sccp_addr *addr) { struct osmo_sccp_instance *sccp = cnlink_sccp(cnlink); if (!sccp) return osmo_sccp_addr_dump(addr); return osmo_sccp_inst_addr_to_str_c(OTC_SELECT, sccp, addr); } static const struct rate_ctr_desc cnlink_ctr_description[] = { [CNLINK_CTR_RANAP_RX_UDT_RESET] = { "ranap:rx:udt:reset", "RANAP Unitdata RESET messages received" }, [CNLINK_CTR_RANAP_RX_UDT_RESET_ACK] = { "ranap:rx:udt:reset_ack", "RANAP Unitdata RESET ACK messages received", }, [CNLINK_CTR_RANAP_RX_UDT_PAGING] = { "ranap:rx:udt:paging", "RANAP Unitdata PAGING messages received", }, [CNLINK_CTR_RANAP_RX_UDT_UNKNOWN] = { "ranap:rx:udt:unknown", "Unknown RANAP Unitdata messages received", }, [CNLINK_CTR_RANAP_RX_UDT_UNSUPPORTED] = { "ranap:rx:udt:unsupported", "Unsupported RANAP Unitdata messages received", }, [CNLINK_CTR_RANAP_RX_UDT_OVERLOAD_IND] = { "ranap:rx:udt:overload_ind", "RANAP Unitdata Overload Indications received", }, [CNLINK_CTR_RANAP_RX_UDT_ERROR_IND] = { "ranap:rx:udt:error_ind", "RANAP Unitdata Error Indications received", }, [CNLINK_CTR_RANAP_TX_UDT_RESET] = { "ranap:tx:udt:reset", "RANAP Unitdata RESET messages transmitted", }, [CNLINK_CTR_RANAP_TX_UDT_RESET_ACK] = { "ranap:tx:udt:reset_ack", "RANAP Unitdata RESET ACK messages transmitted", }, /* Indicators for CN pool usage */ [CNLINK_CTR_CNPOOL_SUBSCR_NEW] = { "cnpool:subscr:new", "Complete Layer 3 requests assigned to this CN link by round-robin (no NRI was assigned yet).", }, [CNLINK_CTR_CNPOOL_SUBSCR_REATTACH] = { "cnpool:subscr:reattach", "Complete Layer 3 requests assigned to this CN link by round-robin because the subscriber indicates a" " NULL-NRI (previously assigned by another CN link).", }, [CNLINK_CTR_CNPOOL_SUBSCR_KNOWN] = { "cnpool:subscr:known", "Complete Layer 3 requests directed to this CN link because the subscriber indicates an NRI of this CN link.", }, [CNLINK_CTR_CNPOOL_SUBSCR_PAGED] = { "cnpool:subscr:paged", "Paging Response directed to this CN link because the subscriber was recently paged by this CN link.", }, [CNLINK_CTR_CNPOOL_SUBSCR_ATTACH_LOST] = { "cnpool:subscr:attach_lost", "A subscriber indicates an NRI value matching this CN link, but the CN link is not connected:" " a re-attach to another CN link (if available) was forced, with possible service failure.", }, [CNLINK_CTR_CNPOOL_EMERG_FORWARDED] = { "cnpool:emerg:forwarded", "Emergency call requests forwarded to this CN link.", }, }; const struct rate_ctr_group_desc msc_ctrg_desc = { "msc", "MSC", OSMO_STATS_CLASS_GLOBAL, ARRAY_SIZE(cnlink_ctr_description), cnlink_ctr_description, }; const struct rate_ctr_group_desc sgsn_ctrg_desc = { "sgsn", "SGSN", OSMO_STATS_CLASS_GLOBAL, ARRAY_SIZE(cnlink_ctr_description), cnlink_ctr_description, }; static const struct rate_ctr_desc cnpool_ctr_description[] = { [CNPOOL_CTR_SUBSCR_NO_CNLINK] = { "cnpool:subscr:no_cnlink", "Complete Layer 3 requests lost because no connected CN link is found available", }, [CNPOOL_CTR_EMERG_FORWARDED] = { "cnpool:emerg:forwarded", "Emergency call requests forwarded to a CN link (see also per-CN-link counters)", }, [CNPOOL_CTR_EMERG_LOST] = { "cnpool:emerg:lost", "Emergency call requests lost because no CN link was found available", }, }; const struct rate_ctr_group_desc iucs_ctrg_desc = { "iucs", "IuCS", OSMO_STATS_CLASS_GLOBAL, ARRAY_SIZE(cnpool_ctr_description), cnpool_ctr_description, }; const struct rate_ctr_group_desc iups_ctrg_desc = { "iups", "IuPS", OSMO_STATS_CLASS_GLOBAL, ARRAY_SIZE(cnpool_ctr_description), cnpool_ctr_description, };