/* SCCP Handling */ /* (C) 2025 by sysmocom - s.f.m.c. GmbH * 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 /* Entry to cache conn_id <-> sccp_addr mapping in case we receive an empty CR */ struct iu_new_ctx_entry { struct llist_head list; uint32_t conn_id; struct osmo_sccp_addr sccp_addr; }; static int sccp_sap_up(struct osmo_prim_hdr *oph, void *_scu); struct sgsn_sccp_user_iups *sgsn_scu_iups_inst_alloc(struct sgsn_instance *sgsn, struct osmo_sccp_instance *sccp) { struct sgsn_sccp_user_iups *scu_iups; scu_iups = talloc_zero(sgsn, struct sgsn_sccp_user_iups); OSMO_ASSERT(scu_iups); scu_iups->sgsn = sgsn; scu_iups->sccp = sccp; INIT_LLIST_HEAD(&scu_iups->ue_conn_ctx_list); INIT_LLIST_HEAD(&scu_iups->ue_conn_sccp_addr_list); osmo_sccp_local_addr_by_instance(&scu_iups->local_sccp_addr, scu_iups->sccp, OSMO_SCCP_SSN_RANAP); scu_iups->scu = osmo_sccp_user_bind(scu_iups->sccp, "OsmoSGSN-IuPS", sccp_sap_up, OSMO_SCCP_SSN_RANAP); osmo_sccp_user_set_priv(scu_iups->scu, scu_iups); return scu_iups; } void sgsn_scu_iups_free(struct sgsn_sccp_user_iups *scu_iups) { if (!scu_iups) return; if (scu_iups->scu) osmo_sccp_user_unbind(scu_iups->scu); talloc_free(scu_iups); } /* wrap RANAP message in SCCP N-DATA.req * ranap_msg becomes owned by the callee. */ int sgsn_scu_iups_tx_data_req(struct sgsn_sccp_user_iups *scu_iups, uint32_t conn_id, struct msgb *ranap_msg) { struct osmo_scu_prim *prim; int rc; if (!scu_iups) { LOGP(DSUA, LOGL_ERROR, "Failed to send SCCP N-DATA.req(%u): no SCCP User\n", conn_id); return -1; } ranap_msg->l2h = ranap_msg->data; prim = (struct osmo_scu_prim *)msgb_push(ranap_msg, sizeof(*prim)); osmo_prim_init(&prim->oph, SCCP_SAP_USER, OSMO_SCU_PRIM_N_DATA, PRIM_OP_REQUEST, ranap_msg); prim->u.data.conn_id = conn_id; rc = osmo_sccp_user_sap_down(scu_iups->scu, &prim->oph); if (rc) LOGP(DSUA, LOGL_ERROR, "Failed to send SCCP N-DATA.req(%u)\n", conn_id); return rc; } struct ranap_ue_conn_ctx *sgsn_scu_iups_ue_conn_ctx_find(struct sgsn_sccp_user_iups *scu_iups, uint32_t conn_id) { struct ranap_ue_conn_ctx *ctx; llist_for_each_entry(ctx, &scu_iups->ue_conn_ctx_list, list) { if (ctx->conn_id == conn_id) return ctx; } return NULL; } static void ue_conn_sccp_addr_add(struct sgsn_sccp_user_iups *scu_iups, uint32_t conn_id, const struct osmo_sccp_addr *calling_addr) { struct iu_new_ctx_entry *entry = talloc_zero(scu_iups, struct iu_new_ctx_entry); entry->conn_id = conn_id; entry->sccp_addr = *calling_addr; llist_add(&entry->list, &scu_iups->ue_conn_sccp_addr_list); } static const struct osmo_sccp_addr *ue_conn_sccp_addr_find(struct sgsn_sccp_user_iups *scu_iups, uint32_t conn_id) { struct iu_new_ctx_entry *entry; llist_for_each_entry(entry, &scu_iups->ue_conn_sccp_addr_list, list) { if (entry->conn_id == conn_id) return &entry->sccp_addr; } return NULL; } static void ue_conn_sccp_addr_del(struct sgsn_sccp_user_iups *scu_iups, uint32_t conn_id) { struct iu_new_ctx_entry *entry; llist_for_each_entry(entry, &scu_iups->ue_conn_sccp_addr_list, list) { if (entry->conn_id == conn_id) { llist_del(&entry->list); talloc_free(entry); return; } } } static void handle_notice_ind(struct sgsn_sccp_user_iups *scu_iups, const struct osmo_scu_notice_param *ni) { struct ranap_iu_rnc *rnc; rnc = iu_rnc_find_by_addr(&ni->calling_addr); if (!rnc) { LOGP(DSUA, LOGL_DEBUG, "(calling_addr=%s) N-NOTICE.ind cause=%u='%s' importance=%u didn't match any RNC, ignoring\n", osmo_sccp_addr_dump(&ni->calling_addr), ni->cause, osmo_sccp_return_cause_name(ni->cause), ni->importance); return; } LOG_RNC(rnc, LOGL_NOTICE, "N-NOTICE.ind cause=%u='%s' importance=%u\n", ni->cause, osmo_sccp_return_cause_name(ni->cause), ni->importance); switch (ni->cause) { case SCCP_RETURN_CAUSE_SUBSYSTEM_CONGESTION: case SCCP_RETURN_CAUSE_NETWORK_CONGESTION: /* Transient failures (hopefully), keep going. */ return; default: break; } /* Messages are not arriving to rnc. Signal it is unavailable to update local state. */ osmo_fsm_inst_dispatch(rnc->fi, IU_RNC_EV_UNAVAILABLE, NULL); } static void handle_pcstate_ind(struct sgsn_sccp_user_iups *scu_iups, const struct osmo_scu_pcstate_param *pcst) { struct osmo_ss7_instance *cs7 = osmo_sccp_get_ss7(scu_iups->sccp); struct osmo_sccp_addr rem_addr; struct ranap_iu_rnc *rnc; bool connected; bool disconnected; LOGP(DSUA, LOGL_DEBUG, "N-PCSTATE ind: affected_pc=%u=%s sp_status=%s remote_sccp_status=%s\n", pcst->affected_pc, osmo_ss7_pointcode_print(cs7, pcst->affected_pc), osmo_sccp_sp_status_name(pcst->sp_status), osmo_sccp_rem_sccp_status_name(pcst->remote_sccp_status)); osmo_sccp_make_addr_pc_ssn(&rem_addr, pcst->affected_pc, OSMO_SCCP_SSN_RANAP); rnc = iu_rnc_find_by_addr(&rem_addr); if (!rnc) { LOGP(DSUA, LOGL_DEBUG, "No RNC found under pc=%u=s%s\n", pcst->affected_pc, osmo_ss7_pointcode_print(cs7, pcst->affected_pc)); 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) { LOG_RNC(rnc, LOGL_NOTICE, "now unreachable: N-PCSTATE ind: pc=%u=%s sp_status=%s remote_sccp_status=%s\n", pcst->affected_pc, osmo_ss7_pointcode_print(cs7, pcst->affected_pc), osmo_sccp_sp_status_name(pcst->sp_status), osmo_sccp_rem_sccp_status_name(pcst->remote_sccp_status)); osmo_fsm_inst_dispatch(rnc->fi, IU_RNC_EV_UNAVAILABLE, NULL); } else if (connected) { LOG_RNC(rnc, LOGL_NOTICE, "now available: N-PCSTATE ind: pc=%u=%s sp_status=%s remote_sccp_status=%s\n", pcst->affected_pc, osmo_ss7_pointcode_print(cs7, pcst->affected_pc), osmo_sccp_sp_status_name(pcst->sp_status), osmo_sccp_rem_sccp_status_name(pcst->remote_sccp_status)); osmo_fsm_inst_dispatch(rnc->fi, IU_RNC_EV_AVAILABLE, NULL); } } static struct osmo_prim_hdr *make_conn_resp(struct osmo_scu_connect_param *param) { struct msgb *msg = msgb_alloc(1024, "conn_resp"); struct osmo_scu_prim *prim; prim = (struct osmo_scu_prim *) msgb_put(msg, sizeof(*prim)); osmo_prim_init(&prim->oph, SCCP_SAP_USER, OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_RESPONSE, msg); memcpy(&prim->u.connect, param, sizeof(prim->u.connect)); return &prim->oph; } static int sccp_sap_up(struct osmo_prim_hdr *oph, void *_scu) { struct osmo_sccp_user *scu = _scu; struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph; struct sgsn_sccp_user_iups *scu_iups = osmo_sccp_user_get_priv(scu); struct osmo_prim_hdr *resp = NULL; int rc = -1; struct ranap_ue_conn_ctx *ue; uint32_t conn_id; LOGP(DSUA, LOGL_DEBUG, "sccp_sap_up(%s)\n", osmo_scu_prim_name(oph)); switch (OSMO_PRIM_HDR(oph)) { case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_CONFIRM): /* confirmation of outbound connection */ break; case OSMO_PRIM(OSMO_SCU_PRIM_N_CONNECT, PRIM_OP_INDICATION): /* indication of new inbound connection request*/ conn_id = prim->u.connect.conn_id; LOGP(DSUA, LOGL_DEBUG, "N-CONNECT.ind(X->%u)\n", conn_id); /* first ensure the local SCCP socket is ACTIVE */ resp = make_conn_resp(&prim->u.connect); osmo_sccp_user_sap_down(scu, resp); /* then handle the RANAP payload */ if (/* prim->u.connect.called_addr.ssn != OSMO_SCCP_SSN_RANAP || */ !msgb_l2(oph->msg) || msgb_l2len(oph->msg) == 0) { LOGP(DSUA, LOGL_DEBUG, "Received N-CONNECT.ind without data\n"); ue_conn_sccp_addr_add(scu_iups, conn_id, &prim->u.connect.calling_addr); } else { rc = sgsn_ranap_iu_rx_co_initial_msg(scu_iups, &prim->u.connect.calling_addr, conn_id, msgb_l2(oph->msg), msgb_l2len(oph->msg)); } break; case OSMO_PRIM(OSMO_SCU_PRIM_N_DISCONNECT, PRIM_OP_INDICATION): /* indication of disconnect */ conn_id = prim->u.disconnect.conn_id; LOGP(DSUA, LOGL_DEBUG, "N-DISCONNECT.ind(%u)\n", conn_id); ue_conn_sccp_addr_del(scu_iups, conn_id); ue = sgsn_scu_iups_ue_conn_ctx_find(scu_iups, conn_id); if (!ue) break; rc = 0; if (msgb_l2len(oph->msg) > 0) rc = sgsn_ranap_iu_rx_co_msg(ue, msgb_l2(oph->msg), msgb_l2len(oph->msg)); /* A Iu Release event might be used to free the UE in cn_ranap_handle_co(). */ ue = sgsn_scu_iups_ue_conn_ctx_find(scu_iups, conn_id); if (!ue) break; ue_conn_ctx_link_invalidated_free(ue); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_DATA, PRIM_OP_INDICATION): /* connection-oriented data received */ conn_id = prim->u.data.conn_id; LOGP(DSUA, LOGL_DEBUG, "N-DATA.ind(%u, %s)\n", conn_id, osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg))); /* resolve UE context */ ue = sgsn_scu_iups_ue_conn_ctx_find(scu_iups, conn_id); if (!ue) { /* Could be an InitialUE-Message after an empty CR, recreate new_ctx */ const struct osmo_sccp_addr *sccp_addr = ue_conn_sccp_addr_find(scu_iups, conn_id); if (!sccp_addr) { LOGP(DSUA, LOGL_NOTICE, "N-DATA.ind for unknown conn_id (%u)\n", conn_id); break; } /* Hold copy of address before deleting it: */ struct osmo_sccp_addr rem_sccp_addr = *sccp_addr; ue_conn_sccp_addr_del(scu_iups, conn_id); rc = sgsn_ranap_iu_rx_co_initial_msg(scu_iups, &rem_sccp_addr, conn_id, msgb_l2(oph->msg), msgb_l2len(oph->msg)); break; } rc = sgsn_ranap_iu_rx_co_msg(ue, msgb_l2(oph->msg), msgb_l2len(oph->msg)); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_INDICATION): /* connection-less data received */ LOGP(DSUA, LOGL_DEBUG, "N-UNITDATA.ind(%s)\n", osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg))); rc = sgsn_ranap_iu_rx_cl_msg(scu_iups, &prim->u.unitdata, msgb_l2(oph->msg), msgb_l2len(oph->msg)); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_NOTICE, PRIM_OP_INDICATION): LOGP(DSUA, LOGL_DEBUG, "N-NOTICE.ind(%s)\n", osmo_hexdump(msgb_l2(oph->msg), msgb_l2len(oph->msg))); handle_notice_ind(scu_iups, &prim->u.notice); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_PCSTATE, PRIM_OP_INDICATION): handle_pcstate_ind(scu_iups, &prim->u.pcstate); break; case OSMO_PRIM(OSMO_SCU_PRIM_N_STATE, PRIM_OP_INDICATION): LOGP(DSUA, LOGL_DEBUG, "SCCP-User-SAP: Ignoring %s.%s\n", osmo_scu_prim_type_name(oph->primitive), get_value_string(osmo_prim_op_names, oph->operation)); break; default: break; } msgb_free(oph->msg); return rc; } int sgsn_sccp_init(struct sgsn_instance *sgi) { /* Note that these are mostly defaults and can be overridden from the VTY */ sgi->sccp.sccp = osmo_sccp_simple_client_on_ss7_id(tall_sgsn_ctx, sgi->cfg.iu.cs7_instance, "OsmoSGSN", (23 << 3) + 4, OSMO_SS7_ASP_PROT_M3UA, 0, "localhost", 0, "localhost"); if (!sgi->sccp.sccp) { LOGP(DGPRS, LOGL_ERROR, "Setting up SCCP instance on cs7 instance %d failed!\n", sgi->cfg.iu.cs7_instance); return -EINVAL; } osmo_sccp_set_priv(sgi->sccp.sccp, sgsn); sgi->sccp.scu_iups = sgsn_scu_iups_inst_alloc(sgsn, sgi->sccp.sccp); OSMO_ASSERT(sgi->sccp.scu_iups); return 0; } void sgsn_sccp_release(struct sgsn_instance *sgi) { sgsn_scu_iups_free(sgi->sccp.scu_iups); sgi->sccp.scu_iups = NULL; if (sgi->sccp.sccp) { osmo_sccp_instance_destroy(sgi->sccp.sccp); sgi->sccp.sccp = NULL; } }