lynxis lazus has submitted this change. ( https://gerrit.osmocom.org/c/osmo-sgsn/+/40988?usp=email )
Change subject: Introduce iu_rnc FSM ...................................................................... Introduce iu_rnc FSM This FSM is similar to the already existing ran_peer_fsm in osmo-msc, which already had better logic around SCCP and RANAP state handling. Similarly, osmo-sgsn's struct ranap_iu_rnc maps to osmo-msc's struct ran_peer. With this FSM we can currently track the RANAP link state towards a given RNC peer: * Reject (RANAP Error Indication) all UE-related messages until a RANAP RESET from RNC is received first. * Tear down all subsriber connections whenever the RANAP peer sends us a RESET message. * Tear down all subscriber connections whenever the SCCP link towards RNC becomes unavailable. * Send a RESET towards RNC peer once the SCCP link towrdards it becomes available again. This commit only implements so far the Rx path of the FSM, ie. when receiving events/messages from a peer over SCCP and pushing them locally up the stack (RANAP). The Tx side will be implemented in a follow-up commit, which will allow discarding messages if the lower layers towards a given RNC are known to be down. Related: OS#3403 Change-Id: I18b7803500163e78ff6a684095194174b0fb6ee1 --- M include/osmocom/sgsn/Makefile.am M include/osmocom/sgsn/gprs_ranap.h M include/osmocom/sgsn/iu_rnc.h A include/osmocom/sgsn/iu_rnc_fsm.h M include/osmocom/sgsn/sccp.h M include/osmocom/sgsn/sgsn.h M src/sgsn/Makefile.am M src/sgsn/gprs_ranap.c M src/sgsn/iu_rnc.c A src/sgsn/iu_rnc_fsm.c M src/sgsn/sccp.c M src/sgsn/sgsn.c M src/sgsn/sgsn_vty.c M tests/gprs_routing_area/Makefile.am M tests/osmo-sgsn_test-nodes.vty M tests/sgsn/Makefile.am 16 files changed, 591 insertions(+), 95 deletions(-) Approvals: Jenkins Builder: Verified osmith: Looks good to me, but someone else must approve lynxis lazus: Looks good to me, approved diff --git a/include/osmocom/sgsn/Makefile.am b/include/osmocom/sgsn/Makefile.am index 342b568..9c50618 100644 --- a/include/osmocom/sgsn/Makefile.am +++ b/include/osmocom/sgsn/Makefile.am @@ -29,6 +29,7 @@ gtp_mme.h \ iu_client.h \ iu_rnc.h \ + iu_rnc_fsm.h \ mmctx.h \ pdpctx.h \ sccp.h \ diff --git a/include/osmocom/sgsn/gprs_ranap.h b/include/osmocom/sgsn/gprs_ranap.h index 3c4f593..62914ba 100644 --- a/include/osmocom/sgsn/gprs_ranap.h +++ b/include/osmocom/sgsn/gprs_ranap.h @@ -13,6 +13,15 @@ struct sgsn_mm_ctx; struct sgsn_pdp_ctx; +/* struct RANAP_GlobalRNC_ID with a coupled buffer where .buf points to. + * Used to easily generate a struct RANAP_GlobalRNC_ID to encode, + * see sgsn_ranap_iu_grnc_id_compose(). */ +struct iu_grnc_id { + uint8_t plmn_buf[3]; + struct RANAP_GlobalRNC_ID grnc_id; +}; +int sgsn_ranap_iu_grnc_id_compose(struct iu_grnc_id *dst, const struct osmo_rnc_id *src); + int sgsn_ranap_iu_event(struct ranap_ue_conn_ctx *ctx, enum ranap_iu_event_type type, void *data); int sgsn_ranap_iu_tx(struct msgb *msg, uint8_t sapi); @@ -40,6 +49,12 @@ const struct osmo_sccp_addr *dst_addr, const RANAP_Cause_t *cause); +void sgsn_ranap_iu_handle_co_initial(struct ranap_iu_rnc *iu_rnc, + uint32_t conn_id, + const ranap_message *message); +void sgsn_ranap_iu_handle_co(struct ranap_ue_conn_ctx *ue_ctx, const ranap_message *message); + +/* Entry points from rx SCCP: */ int sgsn_ranap_iu_rx_cl_msg(struct sgsn_sccp_user_iups *scu_iups, const struct osmo_scu_unitdata_param *ud_prim, const uint8_t *data, size_t len); diff --git a/include/osmocom/sgsn/iu_rnc.h b/include/osmocom/sgsn/iu_rnc.h index 9bdb559..b0e5be3 100644 --- a/include/osmocom/sgsn/iu_rnc.h +++ b/include/osmocom/sgsn/iu_rnc.h @@ -4,6 +4,7 @@ #include <osmocom/core/defs.h> #include <osmocom/core/linuxlist.h> +#include <osmocom/core/fsm.h> #include <osmocom/gsm/gsm48.h> #include <osmocom/iuh/common.h> #include <osmocom/sigtran/sccp_sap.h> @@ -26,6 +27,7 @@ struct osmo_rnc_id rnc_id; struct sgsn_sccp_user_iups *scu_iups; struct osmo_sccp_addr sccp_addr; + struct osmo_fsm_inst *fi; /* A list of struct iu_lac_rac_entry */ struct llist_head lac_rac_list; @@ -34,6 +36,15 @@ struct ranap_iu_rnc *iu_rnc_find_or_create(const struct osmo_rnc_id *rnc_id, struct sgsn_sccp_user_iups *scu_iups, const struct osmo_sccp_addr *addr); + +struct ranap_iu_rnc *iu_rnc_find_by_addr(const struct osmo_sccp_addr *rnc_sccp_addr); + void iu_rnc_update_rai_seen(struct ranap_iu_rnc *rnc, const struct osmo_routing_area_id *rai); void iu_rnc_discard_all_ue_ctx(struct ranap_iu_rnc *rnc); + +#define LOG_RNC_CAT(IU_RNC, subsys, loglevel, fmt, args ...) \ + LOGPFSMSL((IU_RNC)->fi, subsys, loglevel, fmt, ## args) + +#define LOG_RNC(IU_RNC, loglevel, fmt, args ...) \ + LOG_RNC_CAT(IU_RNC, DRANAP, loglevel, fmt, ## args) diff --git a/include/osmocom/sgsn/iu_rnc_fsm.h b/include/osmocom/sgsn/iu_rnc_fsm.h new file mode 100644 index 0000000..d9be576 --- /dev/null +++ b/include/osmocom/sgsn/iu_rnc_fsm.h @@ -0,0 +1,36 @@ +#include <stdint.h> + +#include <osmocom/core/fsm.h> + +#include <osmocom/ranap/ranap_ies_defs.h> + +struct ranap_iu_rnc; + +enum iu_rnc_state { + IU_RNC_ST_WAIT_RX_RESET = 0, + IU_RNC_ST_WAIT_RX_RESET_ACK, + IU_RNC_ST_READY, + IU_RNC_ST_DISCARDING, +}; + +struct iu_rnc_ev_msg_up_co_initial_ctx { + struct ranap_iu_rnc *rnc; + uint32_t conn_id; + ranap_message message; +}; + +struct iu_rnc_ev_msg_up_co_ctx { + struct ranap_ue_conn_ctx *ue_ctx; + ranap_message message; +}; + +enum iu_rnc_event { + IU_RNC_EV_MSG_UP_CO_INITIAL, /* struct iu_rnc_ev_msg_up_co_initial_ctx* */ + IU_RNC_EV_MSG_UP_CO, /* struct iu_rnc_ev_msg_up_co_ctx* */ + IU_RNC_EV_RX_RESET, /* no param */ + IU_RNC_EV_RX_RESET_ACK, /* no param */ + IU_RNC_EV_AVAILABLE, + IU_RNC_EV_UNAVAILABLE +}; + +extern struct osmo_fsm iu_rnc_fsm; diff --git a/include/osmocom/sgsn/sccp.h b/include/osmocom/sgsn/sccp.h index 67388a4..0c05f15 100644 --- a/include/osmocom/sgsn/sccp.h +++ b/include/osmocom/sgsn/sccp.h @@ -23,6 +23,7 @@ #include <osmocom/sigtran/sccp_sap.h> struct sgsn_instance; +struct ranap_ue_conn_ctx; struct sgsn_sccp_user_iups { struct sgsn_instance *sgsn; /* backpointer */ diff --git a/include/osmocom/sgsn/sgsn.h b/include/osmocom/sgsn/sgsn.h index 7d323ed..8fc97b0 100644 --- a/include/osmocom/sgsn/sgsn.h +++ b/include/osmocom/sgsn/sgsn.h @@ -180,6 +180,7 @@ #endif /* if BUILD_IU */ }; +extern struct osmo_tdef sgsn_T_defs[]; extern struct sgsn_instance *sgsn; extern void *tall_sgsn_ctx; diff --git a/src/sgsn/Makefile.am b/src/sgsn/Makefile.am index fdad7dc..e64fe94 100644 --- a/src/sgsn/Makefile.am +++ b/src/sgsn/Makefile.am @@ -99,6 +99,7 @@ gprs_ranap.c \ iu_client.c \ iu_rnc.c \ + iu_rnc_fsm.c \ sccp.c \ $(NULL) diff --git a/src/sgsn/gprs_ranap.c b/src/sgsn/gprs_ranap.c index d05cc7b..df9cc9e 100644 --- a/src/sgsn/gprs_ranap.c +++ b/src/sgsn/gprs_ranap.c @@ -29,6 +29,7 @@ #include <osmocom/core/rate_ctr.h> #include <osmocom/core/tdef.h> +#include <osmocom/gsm/gsm23003.h> #include <osmocom/gprs/gprs_msgb.h> #include <osmocom/ranap/ranap_common.h> @@ -49,17 +50,13 @@ #include <osmocom/sgsn/gtp_ggsn.h> #include <osmocom/sgsn/gtp.h> #include <osmocom/sgsn/iu_rnc.h> +#include <osmocom/sgsn/iu_rnc_fsm.h> #include <osmocom/sgsn/pdpctx.h> #include <osmocom/sgsn/mmctx.h> /* Parsed global RNC id. See also struct RANAP_GlobalRNC_ID, and note that the * PLMN identity is a BCD representation of the MCC and MNC. * See iu_grnc_id_parse(). */ -struct iu_grnc_id { - struct osmo_plmn_id plmn; - uint16_t rnc_id; -}; - static int iu_grnc_id_parse(struct osmo_rnc_id *dst, const struct RANAP_GlobalRNC_ID *src) { /* The size is coming from arbitrary sender, check it gracefully */ @@ -73,18 +70,15 @@ return 0; } -#if 0 /* not used at present */ -static int iu_grnc_id_compose(struct iu_grnc_id *src, struct RANAP_GlobalRNC_ID *dst) +int sgsn_ranap_iu_grnc_id_compose(struct iu_grnc_id *dst, const struct osmo_rnc_id *src) { - /* The caller must ensure proper size */ - OSMO_ASSERT(dst->pLMNidentity.size == 3); - gsm48_mcc_mnc_to_bcd(&dst->pLMNidentity.buf[0], - src->mcc, src->mnc); - dst->rNC_ID = src->rnc_id; + dst->grnc_id.pLMNidentity.buf = &dst->plmn_buf[0]; + dst->grnc_id.pLMNidentity.size = 3; + osmo_plmn_to_bcd(dst->grnc_id.pLMNidentity.buf, &src->plmn); + dst->grnc_id.rNC_ID = src->rnc_id; return 0; } -#endif /* Callback for RAB assignment response */ static int sgsn_ranap_rab_ass_resp(struct sgsn_mm_ctx *ctx, RANAP_RAB_SetupOrModifiedItemIEs_t *setup_ies) @@ -385,8 +379,7 @@ osmo_timer_schedule(&ctx->release_timeout, timeout, 0); } -static int ranap_handle_co_initial_ue(struct sgsn_sccp_user_iups *scu_iups, - const struct osmo_sccp_addr *rem_sccp_addr, +static int ranap_handle_co_initial_ue(struct ranap_iu_rnc *rnc, uint32_t conn_id, const RANAP_InitialUE_MessageIEs_t *ies) { @@ -396,7 +389,6 @@ uint16_t sai; struct ranap_ue_conn_ctx *ue; struct msgb *msg = msgb_alloc(256, "RANAP->NAS"); - struct ranap_iu_rnc *rnc; if (ranap_parse_lai(&ra_id, &ies->lai) != 0) { LOGP(DRANAP, LOGL_ERROR, "Failed to parse RANAP LAI IE\n"); @@ -427,9 +419,7 @@ gprs_rai_to_osmo(&ra_id2, &ra_id); - /* Make sure we know the RNC Id and LAC+RAC coming in on this connection. */ - rnc = iu_rnc_find_or_create(&rnc_id, scu_iups, rem_sccp_addr); - OSMO_ASSERT(rnc); + /* Make sure we update LAC+RAC coming in on this connection. */ iu_rnc_update_rai_seen(rnc, &ra_id2); ue = ue_conn_ctx_alloc(rnc, conn_id); @@ -445,10 +435,9 @@ return 0; } -static void cn_ranap_handle_co_initial(struct sgsn_sccp_user_iups *scu_iups, - const struct osmo_sccp_addr *rem_sccp_addr, - uint32_t conn_id, - const ranap_message *message) +void sgsn_ranap_iu_handle_co_initial(struct ranap_iu_rnc *iu_rnc, + uint32_t conn_id, + const ranap_message *message) { int rc; @@ -461,7 +450,7 @@ message->direction, message->procedureCode); rc = -1; } else - rc = ranap_handle_co_initial_ue(scu_iups, rem_sccp_addr, conn_id, &message->msg.initialUE_MessageIEs); + rc = ranap_handle_co_initial_ue(iu_rnc, conn_id, &message->msg.initialUE_MessageIEs); if (rc) { LOGP(DRANAP, LOGL_ERROR, "Error in %s (%d)\n", __func__, rc); @@ -474,20 +463,37 @@ uint32_t conn_id, const uint8_t *data, size_t len) { - ranap_message message; + struct iu_rnc_ev_msg_up_co_initial_ctx ev_ctx = { + .conn_id = conn_id, + }; + RANAP_Cause_t cause; int rc; - rc = ranap_cn_rx_co_decode2(&message, data, len); + rc = ranap_cn_rx_co_decode2(&ev_ctx.message, data, len); if (rc != 0) { LOGP(DRANAP, LOGL_ERROR, "Not calling cn_ranap_handle_co_initial() due to rc=%d\n", rc); goto free_ret; } - cn_ranap_handle_co_initial(scu_iups, rem_sccp_addr, conn_id, &message); + ev_ctx.rnc = iu_rnc_find_by_addr(rem_sccp_addr); + if (!ev_ctx.rnc) + goto tx_err_ind; + rc = osmo_fsm_inst_dispatch(ev_ctx.rnc->fi, IU_RNC_EV_MSG_UP_CO_INITIAL, &ev_ctx); + if (rc != 0) + goto tx_err_ind; + + goto free_ret; + +tx_err_ind: + cause = (RANAP_Cause_t){ + .present = RANAP_Cause_PR_protocol, + .choice.protocol = RANAP_CauseProtocol_message_not_compatible_with_receiver_state, + }; + sgsn_ranap_iu_tx_error_ind(scu_iups, rem_sccp_addr, &cause); free_ret: /* Free the asn1 structs in message */ - ranap_cn_rx_co_free(&message); + ranap_cn_rx_co_free(&ev_ctx.message); return rc; } @@ -569,7 +575,7 @@ } /* Entry point for connection-oriented RANAP message */ -static void cn_ranap_handle_co(struct ranap_ue_conn_ctx *ue_ctx, const ranap_message *message) +void sgsn_ranap_iu_handle_co(struct ranap_ue_conn_ctx *ue_ctx, const ranap_message *message) { int rc; @@ -649,20 +655,34 @@ int sgsn_ranap_iu_rx_co_msg(struct ranap_ue_conn_ctx *ue_ctx, const uint8_t *data, size_t len) { - ranap_message message; + struct iu_rnc_ev_msg_up_co_ctx ev_ctx = { + .ue_ctx = ue_ctx, + }; + RANAP_Cause_t cause; int rc; - rc = ranap_cn_rx_co_decode2(&message, data, len); + rc = ranap_cn_rx_co_decode2(&ev_ctx.message, data, len); if (rc != 0) { LOGP(DRANAP, LOGL_ERROR, "Not calling cn_ranap_handle_co() due to rc=%d\n", rc); goto free_ret; } - cn_ranap_handle_co(ue_ctx, &message); + rc = osmo_fsm_inst_dispatch(ue_ctx->rnc->fi, IU_RNC_EV_MSG_UP_CO, &ev_ctx); + if (rc != 0) + goto tx_err_ind; + + goto free_ret; + +tx_err_ind: + cause = (RANAP_Cause_t){ + .present = RANAP_Cause_PR_protocol, + .choice.protocol = RANAP_CauseProtocol_message_not_compatible_with_receiver_state, + }; + sgsn_ranap_iu_tx_error_ind(ue_ctx->rnc->scu_iups, &ue_ctx->rnc->sccp_addr, &cause); free_ret: /* Free the asn1 structs in message */ - ranap_cn_rx_co_free(&message); + ranap_cn_rx_co_free(&ev_ctx.message); return rc; } @@ -674,7 +694,7 @@ RANAP_Cause_t cause; struct osmo_rnc_id rnc_id = {}; struct ranap_iu_rnc *rnc; - struct msgb *resp; + int rc; if (ies->presenceMask & ERRORINDICATIONIES_RANAP_CN_DOMAININDICATOR_PRESENT) { if (ies->cN_DomainIndicator != RANAP_CN_DomainIndicator_ps_domain) { @@ -701,7 +721,7 @@ } grnc_id = &ies->globalRNC_ID; - if (iu_grnc_id_parse(&rnc_id, &ies->globalRNC_ID) != 0) { + if (iu_grnc_id_parse(&rnc_id, grnc_id) != 0) { LOGP(DRANAP, LOGL_ERROR, "Rx RESET: Failed to parse RANAP Global-RNC-ID IE\n"); cause = (RANAP_Cause_t){ @@ -713,12 +733,41 @@ rnc = iu_rnc_find_or_create(&rnc_id, scu_iups, &ud_prim->calling_addr); OSMO_ASSERT(rnc); + rc = osmo_fsm_inst_dispatch(rnc->fi, IU_RNC_EV_RX_RESET, NULL); + if (rc != 0) { + cause = (RANAP_Cause_t){ + .present = RANAP_Cause_PR_protocol, + .choice.protocol = RANAP_CauseProtocol_message_not_compatible_with_receiver_state, + }; + return sgsn_ranap_iu_tx_error_ind(scu_iups, &ud_prim->calling_addr, &cause); + } + return 0; +} - /* send reset response */ - resp = ranap_new_msg_reset_ack(ies->cN_DomainIndicator, grnc_id); - if (!resp) - return -ENOMEM; - return sgsn_ranap_iu_tx_cl(scu_iups, &ud_prim->calling_addr, resp); +static int ranap_handle_cl_reset_ack(struct sgsn_sccp_user_iups *scu_iups, + const struct osmo_scu_unitdata_param *ud_prim, + const RANAP_ResetAcknowledgeIEs_t *ies) +{ + struct ranap_iu_rnc *rnc; + RANAP_Cause_t cause; + int rc; + + rnc = iu_rnc_find_by_addr(&ud_prim->calling_addr); + if (!rnc) + goto tx_err_ind; + + rc = osmo_fsm_inst_dispatch(rnc->fi, IU_RNC_EV_RX_RESET_ACK, NULL); + if (rc != 0) + goto tx_err_ind; + + return 0; + +tx_err_ind: + cause = (RANAP_Cause_t){ + .present = RANAP_Cause_PR_protocol, + .choice.protocol = RANAP_CauseProtocol_message_not_compatible_with_receiver_state, + }; + return sgsn_ranap_iu_tx_error_ind(scu_iups, &ud_prim->calling_addr, &cause); } static int ranap_handle_cl_err_ind(struct sgsn_sccp_user_iups *scu_iups, @@ -757,6 +806,15 @@ } break; case RANAP_RANAP_PDU_PR_successfulOutcome: + switch (message->procedureCode) { + case RANAP_ProcedureCode_id_Reset: + rc = ranap_handle_cl_reset_ack(scu_iups, ud_prim, &message->msg.resetAcknowledgeIEs); + break; + default: + rc = -1; + break; + } + break; case RANAP_RANAP_PDU_PR_unsuccessfulOutcome: case RANAP_RANAP_PDU_PR_outcome: default: diff --git a/src/sgsn/iu_rnc.c b/src/sgsn/iu_rnc.c index 158381c..abe6f61 100644 --- a/src/sgsn/iu_rnc.c +++ b/src/sgsn/iu_rnc.c @@ -40,25 +40,45 @@ #include <osmocom/sgsn/gprs_ranap.h> #include <osmocom/sgsn/iu_client.h> #include <osmocom/sgsn/iu_rnc.h> +#include <osmocom/sgsn/iu_rnc_fsm.h> #include <osmocom/sgsn/sccp.h> #include <osmocom/sgsn/sgsn.h> static struct ranap_iu_rnc *iu_rnc_alloc(const struct osmo_rnc_id *rnc_id, struct sgsn_sccp_user_iups *scu_iups, - const struct osmo_sccp_addr *addr) + const struct osmo_sccp_addr *rnc_sccp_addr) { - struct ranap_iu_rnc *rnc = talloc_zero(sgsn, struct ranap_iu_rnc); + struct ranap_iu_rnc *rnc; + char *addr_str, *pos; + + rnc = talloc_zero(sgsn, struct ranap_iu_rnc); OSMO_ASSERT(rnc); INIT_LLIST_HEAD(&rnc->lac_rac_list); rnc->rnc_id = *rnc_id; rnc->scu_iups = scu_iups; - rnc->sccp_addr = *addr; + rnc->sccp_addr = *rnc_sccp_addr; + + rnc->fi = osmo_fsm_inst_alloc(&iu_rnc_fsm, rnc, rnc, LOGL_INFO, NULL); + OSMO_ASSERT(rnc->fi); + + /* Unfortunately, osmo_sccp_inst_addr_name() returns "RI=SSN_PC,PC=0.24.1,SSN=BSSAP" but neither commas nor + * full-stops are allowed as FSM inst id. Make it "RI-SSN_PC:PC-0-24-1:SSN-BSSAP". */ + addr_str = osmo_sccp_addr_dump(rnc_sccp_addr); + for (pos = addr_str; *pos; pos++) { + if (*pos == ',') + *pos = ':'; + else if (*pos == '.' || *pos == '=') + *pos = '-'; + } + osmo_fsm_inst_update_id_f(rnc->fi, "RNC_ID-%s:%s", + osmo_rnc_id_name(rnc_id), addr_str); + llist_add(&rnc->entry, &sgsn->rnc_list); LOGP(DRANAP, LOGL_NOTICE, "New RNC %s at %s\n", - osmo_rnc_id_name(&rnc->rnc_id), osmo_sccp_addr_dump(addr)); + osmo_rnc_id_name(&rnc->rnc_id), osmo_sccp_addr_dump(rnc_sccp_addr)); return rnc; } @@ -73,6 +93,17 @@ return NULL; } +struct ranap_iu_rnc *iu_rnc_find_by_addr(const struct osmo_sccp_addr *rnc_sccp_addr) +{ + struct ranap_iu_rnc *rnc; + llist_for_each_entry(rnc, &sgsn->rnc_list, entry) { + if (osmo_sccp_addr_ri_cmp(rnc_sccp_addr, &rnc->sccp_addr)) + continue; + return rnc; + } + return NULL; +} + struct ranap_iu_rnc *iu_rnc_find_or_create(const struct osmo_rnc_id *rnc_id, struct sgsn_sccp_user_iups *scu_iups, const struct osmo_sccp_addr *addr) diff --git a/src/sgsn/iu_rnc_fsm.c b/src/sgsn/iu_rnc_fsm.c new file mode 100644 index 0000000..4085586 --- /dev/null +++ b/src/sgsn/iu_rnc_fsm.c @@ -0,0 +1,349 @@ +/* A remote RNC (Radio Network Controller) FSM */ + +/* (C) 2025 by sysmocom s.f.m.c. GmbH <i...@sysmocom.de> + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <stdint.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <stdbool.h> + +#include <osmocom/core/logging.h> +#include <osmocom/core/fsm.h> +#include <osmocom/core/tdef.h> + +#include <osmocom/sigtran/sccp_helpers.h> + +#include <osmocom/sgsn/debug.h> +#include <osmocom/sgsn/gprs_ranap.h> +#include <osmocom/sgsn/iu_rnc_fsm.h> +#include <osmocom/sgsn/iu_rnc.h> +#include <osmocom/sgsn/sgsn.h> + +#define S(x) (1 << (x)) + +struct osmo_fsm iu_rnc_fsm; + + +static const struct osmo_tdef_state_timeout iu_rnc_fsm_timeouts[32] = { + [IU_RNC_ST_WAIT_RX_RESET_ACK] = { .T = -1002 }, + [IU_RNC_ST_DISCARDING] = { .T = -1002 }, +}; + +#define iu_rnc_state_chg(iu_rnc, next_st) \ + osmo_tdef_fsm_inst_state_chg((iu_rnc)->fi, next_st, iu_rnc_fsm_timeouts, sgsn_T_defs, 5) + +static const struct value_string iu_rnc_fsm_event_names[] = { + OSMO_VALUE_STRING(IU_RNC_EV_MSG_UP_CO_INITIAL), + OSMO_VALUE_STRING(IU_RNC_EV_MSG_UP_CO), + OSMO_VALUE_STRING(IU_RNC_EV_RX_RESET), + OSMO_VALUE_STRING(IU_RNC_EV_RX_RESET_ACK), + OSMO_VALUE_STRING(IU_RNC_EV_AVAILABLE), + OSMO_VALUE_STRING(IU_RNC_EV_UNAVAILABLE), + {} +}; + +/* Drop all SCCP connections for this iu_rnc, respond with RESET ACKNOWLEDGE and move to READY state. */ +static void iu_rnc_rx_reset(struct ranap_iu_rnc *rnc) +{ + struct msgb *reset_ack; + struct iu_grnc_id grnc_id; + sgsn_ranap_iu_grnc_id_compose(&grnc_id, &rnc->rnc_id); + + iu_rnc_discard_all_ue_ctx(rnc); + + reset_ack = ranap_new_msg_reset_ack(RANAP_CN_DomainIndicator_ps_domain, &grnc_id.grnc_id); + if (!reset_ack) { + LOG_RNC(rnc, LOGL_ERROR, "Failed to compose RESET ACKNOWLEDGE message\n"); + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return; + } + if (sgsn_ranap_iu_tx_cl(rnc->scu_iups, &rnc->sccp_addr, reset_ack) < 0) { + LOG_RNC(rnc, LOGL_ERROR, "Failed to send RESET ACKNOWLEDGE message\n"); + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return; + } + + LOG_RNC(rnc, LOGL_INFO, "Sent RESET ACKNOWLEDGE\n"); + iu_rnc_state_chg(rnc, IU_RNC_ST_READY); +} + +static void iu_rnc_reset(struct ranap_iu_rnc *rnc) +{ + struct msgb *reset; + const RANAP_Cause_t cause = { + .present = RANAP_Cause_PR_protocol, + .choice = { + .protocol = RANAP_CauseProtocol_message_not_compatible_with_receiver_state, + }, + }; + + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET_ACK); + iu_rnc_discard_all_ue_ctx(rnc); + + reset = ranap_new_msg_reset(RANAP_CN_DomainIndicator_ps_domain, &cause); + if (!reset) { + LOG_RNC(rnc, LOGL_ERROR, "Failed to compose RESET message\n"); + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return; + } + + if (sgsn_ranap_iu_tx_cl(rnc->scu_iups, &rnc->sccp_addr, reset) < 0) { + LOG_RNC(rnc, LOGL_ERROR, "Failed to send RESET message\n"); + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return; + } +} + +static void iu_rnc_st_wait_rx_reset(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct ranap_iu_rnc *rnc = fi->priv; + switch (event) { + + case IU_RNC_EV_MSG_UP_CO: + case IU_RNC_EV_MSG_UP_CO_INITIAL: + OSMO_ASSERT(data); + +#define LEGACY_BEHAVIOR +#ifdef LEGACY_BEHAVIOR + LOG_RNC(rnc, LOGL_ERROR, "Receiving CO message on RAN peer that has not done a proper RESET yet." + " Accepting RAN peer implicitly (legacy compat)\n"); + iu_rnc_state_chg(rnc, IU_RNC_ST_READY); + osmo_fsm_inst_dispatch(rnc->fi, event, data); + return; +#else + LOG_RNC(rnc, LOGL_ERROR, "Receiving CO message on RAN peer that has not done a proper RESET yet." + " Disconnecting on incoming message, sending RESET to RAN peer.\n"); + /* No valid RESET procedure has happened here yet. Usually, we're expecting the RAN peer (BSC, + * RNC) to first send a RESET message before sending Connection Oriented messages. So if we're + * getting a CO message, likely we've just restarted or something. Send a RESET to the peer. */ + + /* Make sure the MS / UE properly disconnects. */ + clear_and_disconnect(rnc, ctx->conn_id); + + iu_rnc_reset(rnc); + return; +#endif + + case IU_RNC_EV_RX_RESET: + iu_rnc_rx_reset(rnc); + return; + + case IU_RNC_EV_AVAILABLE: + /* Send a RESET to the peer. */ + iu_rnc_reset(rnc); + return; + + case IU_RNC_EV_UNAVAILABLE: + /* Do nothing, wait for peer to come up again. */ + return; + + default: + LOG_RNC(rnc, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&iu_rnc_fsm, event)); + return; + } +} + +static void iu_rnc_st_wait_rx_reset_ack(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct ranap_iu_rnc *rnc = fi->priv; + struct iu_rnc_ev_msg_up_co_initial_ctx *ev_msg_up_co_initial_ctx; + struct iu_rnc_ev_msg_up_co_ctx *ev_msg_up_co_ctx; + + switch (event) { + + case IU_RNC_EV_RX_RESET_ACK: + iu_rnc_state_chg(rnc, IU_RNC_ST_READY); + return; + + + case IU_RNC_EV_MSG_UP_CO_INITIAL: + ev_msg_up_co_initial_ctx = data; + OSMO_ASSERT(ev_msg_up_co_initial_ctx); + LOG_RNC(rnc, LOGL_ERROR, "Receiving CO Initial message on RAN peer that has not done a proper RESET yet." + " Disconnecting on incoming message, sending RESET to RAN peer.\n"); + osmo_sccp_tx_disconn(ev_msg_up_co_initial_ctx->rnc->scu_iups->scu, + ev_msg_up_co_initial_ctx->conn_id, NULL, 0); + /* No valid RESET procedure has happened here yet. */ + iu_rnc_reset(rnc); + return; + return; + case IU_RNC_EV_MSG_UP_CO: + ev_msg_up_co_ctx = data; + OSMO_ASSERT(ev_msg_up_co_ctx); + LOG_RNC(rnc, LOGL_ERROR, "Receiving CO message on RAN peer that has not done a proper RESET yet." + " Disconnecting on incoming message, sending RESET to RAN peer.\n"); + ue_conn_ctx_link_invalidated_free(ev_msg_up_co_ctx->ue_ctx); + /* No valid RESET procedure has happened here yet. */ + iu_rnc_reset(rnc); + return; + + case IU_RNC_EV_RX_RESET: + iu_rnc_rx_reset(rnc); + return; + + case IU_RNC_EV_AVAILABLE: + /* Send a RESET to the peer. */ + iu_rnc_reset(rnc); + return; + + case IU_RNC_EV_UNAVAILABLE: + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return; + + default: + LOG_RNC(rnc, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&iu_rnc_fsm, event)); + return; + } +} + +static void iu_rnc_st_ready(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct ranap_iu_rnc *rnc = fi->priv; + struct iu_rnc_ev_msg_up_co_initial_ctx *ev_msg_up_co_initial_ctx; + struct iu_rnc_ev_msg_up_co_ctx *ev_msg_up_co_ctx; + + switch (event) { + + case IU_RNC_EV_MSG_UP_CO_INITIAL: + ev_msg_up_co_initial_ctx = data; + OSMO_ASSERT(ev_msg_up_co_initial_ctx); + OSMO_ASSERT(ev_msg_up_co_initial_ctx->rnc); + + sgsn_ranap_iu_handle_co_initial(ev_msg_up_co_initial_ctx->rnc, + ev_msg_up_co_initial_ctx->conn_id, + &ev_msg_up_co_initial_ctx->message); + return; + + case IU_RNC_EV_MSG_UP_CO: + ev_msg_up_co_ctx = data; + OSMO_ASSERT(ev_msg_up_co_ctx); + OSMO_ASSERT(ev_msg_up_co_ctx->ue_ctx); + + sgsn_ranap_iu_handle_co(ev_msg_up_co_ctx->ue_ctx, &ev_msg_up_co_ctx->message); + return; + + case IU_RNC_EV_RX_RESET: + iu_rnc_rx_reset(rnc); + return; + + case IU_RNC_EV_AVAILABLE: + /* Do nothing, we were already up. */ + return; + + case IU_RNC_EV_UNAVAILABLE: + iu_rnc_discard_all_ue_ctx(rnc); + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return; + + default: + LOG_RNC(rnc, LOGL_ERROR, "Unhandled event: %s\n", osmo_fsm_event_name(&iu_rnc_fsm, event)); + return; + } +} + +static int iu_rnc_fsm_timer_cb(struct osmo_fsm_inst *fi) +{ + struct ranap_iu_rnc *rnc = fi->priv; + iu_rnc_state_chg(rnc, IU_RNC_ST_WAIT_RX_RESET); + return 0; +} + +static void iu_rnc_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + struct ranap_iu_rnc *rnc = fi->priv; + + iu_rnc_discard_all_ue_ctx(rnc); +} + +static const struct osmo_fsm_state iu_rnc_fsm_states[] = { + [IU_RNC_ST_WAIT_RX_RESET] = { + .name = "WAIT_RX_RESET", + .action = iu_rnc_st_wait_rx_reset, + .in_event_mask = 0 + | S(IU_RNC_EV_RX_RESET) + | S(IU_RNC_EV_MSG_UP_CO_INITIAL) + | S(IU_RNC_EV_MSG_UP_CO) + | S(IU_RNC_EV_AVAILABLE) + | S(IU_RNC_EV_UNAVAILABLE) + , + .out_state_mask = 0 + | S(IU_RNC_ST_WAIT_RX_RESET) + | S(IU_RNC_ST_WAIT_RX_RESET_ACK) + | S(IU_RNC_ST_READY) + | S(IU_RNC_ST_DISCARDING) + , + }, + [IU_RNC_ST_WAIT_RX_RESET_ACK] = { + .name = "WAIT_RX_RESET_ACK", + .action = iu_rnc_st_wait_rx_reset_ack, + .in_event_mask = 0 + | S(IU_RNC_EV_RX_RESET) + | S(IU_RNC_EV_RX_RESET_ACK) + | S(IU_RNC_EV_MSG_UP_CO_INITIAL) + | S(IU_RNC_EV_MSG_UP_CO) + | S(IU_RNC_EV_AVAILABLE) + | S(IU_RNC_EV_UNAVAILABLE) + , + .out_state_mask = 0 + | S(IU_RNC_ST_WAIT_RX_RESET) + | S(IU_RNC_ST_WAIT_RX_RESET_ACK) + | S(IU_RNC_ST_READY) + | S(IU_RNC_ST_DISCARDING) + , + }, + [IU_RNC_ST_READY] = { + .name = "READY", + .action = iu_rnc_st_ready, + .in_event_mask = 0 + | S(IU_RNC_EV_RX_RESET) + | S(IU_RNC_EV_MSG_UP_CO_INITIAL) + | S(IU_RNC_EV_MSG_UP_CO) + | S(IU_RNC_EV_AVAILABLE) + | S(IU_RNC_EV_UNAVAILABLE) + , + .out_state_mask = 0 + | S(IU_RNC_ST_WAIT_RX_RESET) + | S(IU_RNC_ST_WAIT_RX_RESET_ACK) + | S(IU_RNC_ST_READY) + | S(IU_RNC_ST_DISCARDING) + , + }, + [IU_RNC_ST_DISCARDING] = { + .name = "DISCARDING", + }, +}; + +struct osmo_fsm iu_rnc_fsm = { + .name = "iu_rnc", + .states = iu_rnc_fsm_states, + .num_states = ARRAY_SIZE(iu_rnc_fsm_states), + .log_subsys = DRANAP, + .event_names = iu_rnc_fsm_event_names, + .timer_cb = iu_rnc_fsm_timer_cb, + .cleanup = iu_rnc_fsm_cleanup, +}; + +static __attribute__((constructor)) void iu_rnc_init(void) +{ + OSMO_ASSERT(osmo_fsm_register(&iu_rnc_fsm) == 0); +} diff --git a/src/sgsn/sccp.c b/src/sgsn/sccp.c index 8b57dd4..83aa19f 100644 --- a/src/sgsn/sccp.c +++ b/src/sgsn/sccp.c @@ -33,6 +33,7 @@ #include <osmocom/sgsn/debug.h> #include <osmocom/sgsn/iu_client.h> #include <osmocom/sgsn/iu_rnc.h> +#include <osmocom/sgsn/iu_rnc_fsm.h> #include <osmocom/sgsn/gprs_ranap.h> #include <osmocom/sgsn/sccp.h> #include <osmocom/sgsn/sgsn.h> @@ -147,10 +148,21 @@ { struct ranap_iu_rnc *rnc; - LOGP(DSUA, LOGL_DEBUG, "(calling_addr=%s) N-NOTICE.ind cause=%u='%s' importance=%u\n", - osmo_sccp_addr_dump(&ni->calling_addr), - ni->cause, osmo_sccp_return_cause_name(ni->cause), - ni->importance); + 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: @@ -161,19 +173,8 @@ break; } - /* Messages are not arriving to RNC. Signal to user that all related ue_ctx are invalid. */ - llist_for_each_entry(rnc, &sgsn->rnc_list, entry) { - if (osmo_sccp_addr_ri_cmp(&rnc->sccp_addr, &ni->calling_addr)) - continue; - LOGP(DSUA, LOGL_NOTICE, - "RNC %s now unreachable: N-NOTICE.ind cause=%u='%s' importance=%u\n", - osmo_rnc_id_name(&rnc->rnc_id), - ni->cause, osmo_sccp_return_cause_name(ni->cause), - ni->importance); - iu_rnc_discard_all_ue_ctx(rnc); - /* TODO: ideally we'd have some event to submit to upper - * layer to inform about peer availability change... */ - } + /* 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) @@ -191,6 +192,13 @@ 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: @@ -245,38 +253,19 @@ } if (disconnected) { - /* A previously usable RNC has disconnected. Signal to user that all related ue_ctx are invalid. */ - llist_for_each_entry(rnc, &sgsn->rnc_list, entry) { - struct ranap_ue_conn_ctx *ue_ctx, *ue_ctx_tmp; - if (osmo_sccp_addr_cmp(&rnc->sccp_addr, &rem_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) - continue; - LOGP(DSUA, LOGL_NOTICE, - "RNC %s now unreachable: N-PCSTATE ind: pc=%u=%s sp_status=%s remote_sccp_status=%s\n", - osmo_rnc_id_name(&rnc->rnc_id), - 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)); - llist_for_each_entry_safe(ue_ctx, ue_ctx_tmp, &scu_iups->ue_conn_ctx_list, list) { - if (ue_ctx->rnc != rnc) - continue; - ue_conn_ctx_link_invalidated_free(ue_ctx); - } - /* TODO: ideally we'd have some event to submit to upper - * layer to inform about peer availability change... */ - } + 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) { - llist_for_each_entry(rnc, &sgsn->rnc_list, entry) { - if (osmo_sccp_addr_cmp(&rnc->sccp_addr, &rem_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC)) - continue; - LOGP(DSUA, LOGL_NOTICE, - "RNC %s now available: N-PCSTATE ind: pc=%u=%s sp_status=%s remote_sccp_status=%s\n", - osmo_rnc_id_name(&rnc->rnc_id), - 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)); - /* TODO: ideally we'd have some event to submit to upper - * layer to inform about peer availability change... */ - } + 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); } } diff --git a/src/sgsn/sgsn.c b/src/sgsn/sgsn.c index c78997e..1b02f76 100644 --- a/src/sgsn/sgsn.c +++ b/src/sgsn/sgsn.c @@ -61,6 +61,7 @@ #include <osmocom/sgsn/pdpctx.h> #include <osmocom/sgsn/gprs_routing_area.h> #if BUILD_IU +#include <osmocom/sgsn/iu_rnc_fsm.h> #include <osmocom/sgsn/sccp.h> #endif /* #if BUILD_IU */ @@ -68,8 +69,6 @@ #define GPRS_LLME_CHECK_TICK 30 -extern struct osmo_tdef sgsn_T_defs[]; - static const struct rate_ctr_desc sgsn_ctr_description[] = { { "llc:dl_bytes", "Count sent LLC bytes before giving it to the bssgp layer" }, { "llc:ul_bytes", "Count successful received LLC bytes (encrypt & fcs correct)" }, diff --git a/src/sgsn/sgsn_vty.c b/src/sgsn/sgsn_vty.c index 9721ee1..b4bfb2c 100644 --- a/src/sgsn/sgsn_vty.c +++ b/src/sgsn/sgsn_vty.c @@ -98,7 +98,7 @@ /* Non spec timer */ #define NONSPEC_X1001_SECS 5 /* wait for a RANAP Release Complete */ - +#define RANAP_TRafR_SECS 5 /* wait for a RANAP Release Complete */ struct osmo_tdef sgsn_T_defs[] = { { .T=3312, .default_val=GSM0408_T3312_SECS, .desc="Periodic RA Update timer (s)" }, @@ -116,6 +116,7 @@ /* non spec timers */ { .T=-1001, .default_val=NONSPEC_X1001_SECS, .desc="RANAP Release timeout. Wait for RANAP Release Complete." "On expiry release Iu connection (s)" }, + { .T=-1002, .default_val=RANAP_TRafR_SECS, .desc="TRafR, Maximum time for Reset procedure in the CN (s)" }, {} }; diff --git a/tests/gprs_routing_area/Makefile.am b/tests/gprs_routing_area/Makefile.am index ea2779a..380d946 100644 --- a/tests/gprs_routing_area/Makefile.am +++ b/tests/gprs_routing_area/Makefile.am @@ -89,6 +89,7 @@ $(top_builddir)/src/sgsn/gprs_mm_state_iu_fsm.o \ $(top_builddir)/src/sgsn/iu_client.o \ $(top_builddir)/src/sgsn/iu_rnc.o \ + $(top_builddir)/src/sgsn/iu_rnc_fsm.o \ $(top_builddir)/src/sgsn/sccp.o \ $(LIBOSMORANAP_LIBS) \ $(LIBOSMOSIGTRAN_LIBS) \ diff --git a/tests/osmo-sgsn_test-nodes.vty b/tests/osmo-sgsn_test-nodes.vty index 32ff8e7..3b914c8 100644 --- a/tests/osmo-sgsn_test-nodes.vty +++ b/tests/osmo-sgsn_test-nodes.vty @@ -13,6 +13,7 @@ T3395 = 8 s Wait for DEACT PDP CTX ACK timer (s) (default: 8 s) T3397 = 8 s Wait for DEACT AA PDP CTX ACK timer (s) (default: 8 s) X1001 = 5 s RANAP Release timeout. Wait for RANAP Release Complete.On expiry release Iu connection (s) (default: 5 s) +X1002 = 5 s TRafR, Maximum time for Reset procedure in the CN (s) (default: 5 s) OsmoSGSN# configure terminal OsmoSGSN(config)# list ... diff --git a/tests/sgsn/Makefile.am b/tests/sgsn/Makefile.am index 8bf78e7..77c1dd7 100644 --- a/tests/sgsn/Makefile.am +++ b/tests/sgsn/Makefile.am @@ -103,6 +103,7 @@ $(top_builddir)/src/sgsn/gprs_mm_state_iu_fsm.o \ $(top_builddir)/src/sgsn/iu_client.o \ $(top_builddir)/src/sgsn/iu_rnc.o \ + $(top_builddir)/src/sgsn/iu_rnc_fsm.o \ $(top_builddir)/src/sgsn/sccp.o \ $(LIBOSMORANAP_LIBS) \ $(LIBOSMOSIGTRAN_LIBS) \ -- To view, visit https://gerrit.osmocom.org/c/osmo-sgsn/+/40988?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: osmo-sgsn Gerrit-Branch: master Gerrit-Change-Id: I18b7803500163e78ff6a684095194174b0fb6ee1 Gerrit-Change-Number: 40988 Gerrit-PatchSet: 4 Gerrit-Owner: pespin <pes...@sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: fixeria <vyanits...@sysmocom.de> Gerrit-Reviewer: lynxis lazus <lyn...@fe80.eu> Gerrit-Reviewer: osmith <osm...@sysmocom.de>