module S1GW_UEMux { /* UE Multiplexer, runs on top of ConnHdlr component, dispatches S1AP PDUs * to/from the UE components. In order to receive S1AP PDUs, a UE component * needs to subscribe for MME_UE_S1AP_ID and/or ENB_UE_S1AP_ID. * * (C) 2025 by sysmocom - s.f.m.c. GmbH * Author: Vadim Yanitskiy * * All rights reserved. * * Released under the terms of GNU General Public License, Version 2 or * (at your option) any later version. * * SPDX-License-Identifier: GPL-2.0-or-later */ import from General_Types all; import from Osmocom_Types all; import from Misc_Helpers all; import from S1AP_IEs all; import from S1AP_Types all; import from S1AP_PDU_Contents all; import from S1AP_PDU_Descriptions all; import from S1AP_Functions all; import from S1GW_ConnHdlr all; private type enumerated S1AP_PDU_DIR { S1AP_PDU_DIR_FROM_MME, S1AP_PDU_DIR_FROM_ENB }; private type record S1AP_dPDU { S1AP_PDU_DIR dir, S1AP_PDU pdu }; private altstep as_s1ap_dpdu(out S1AP_PDU pdu, template (present) S1AP_PDU_DIR dir := ?, template (present) S1AP_PDU tr_pdu := ?) runs on UEMuxUE { var S1AP_dPDU dpdu; [] UEMUX_CONN.receive(S1AP_dPDU:{dir, tr_pdu}) -> value dpdu { pdu := dpdu.pdu; } } private function f_trx_s1ap_pdu(out S1AP_PDU pdu, S1AP_PDU_DIR dir, template (value) S1AP_PDU ts_pdu, template (present) S1AP_PDU tr_pdu, float Tval) runs on UEMuxUE { timer T := Tval; UEMUX_CONN.send(S1AP_dPDU:{dir, ts_pdu}); T.start; alt { [] as_s1ap_dpdu(pdu, dir, tr_pdu); [] as_s1ap_dpdu(pdu, dir) { setverdict(fail, "Rx unexpected S1AP PDU (", dir, "): ", pdu); Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } [] T.timeout { setverdict(fail, "Timeout waiting for S1AP PDU (", dir, "): ", pdu); Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } } } type port UEMUX_CONN_PT message { inout S1AP_dPDU; } with { extension "internal" }; type record UEMuxUE_IEs { S1AP_IEs.EUTRAN_CGI eutran_cgi, S1AP_IEs.TAI tai }; type component UEMuxUE { port UEMUX_CONN_PT UEMUX_CONN; port UEMUX_PROC_PT UEMUX_PROC; var MME_UE_S1AP_ID g_mme_ue_id; var ENB_UE_S1AP_ID g_enb_ue_id; var UEMuxUE_IEs g_ies; }; type record of UEMuxUE UEMuxUEList; type component UEMux_CT extends ConnHdlr, UEMuxUE { /* registered UE components */ var UE_ConnList g_uemux_list := { }; }; /* represents a single UE connection */ private type record UE_ConnData { UEMuxUE vc_conn, MME_UE_S1AP_ID mme_ue_id optional, ENB_UE_S1AP_ID enb_ue_id optional }; private type record of UE_ConnData UE_ConnList; /*********************************************************************************** * UE connection management API **********************************************************************************/ /* find a UE connection [index] by an MME_UE_S1AP_ID */ private function f_UEMux_find_ue_by_mme_ue_id(MME_UE_S1AP_ID mme_ue_id) runs on UEMux_CT return integer { for (var integer i := 0; i < lengthof(g_uemux_list); i := i + 1) { if (g_uemux_list[i].mme_ue_id == mme_ue_id) { return i; } } return -1; } /* find a UE connection [index] by an ENB_UE_S1AP_ID */ private function f_UEMux_find_ue_by_enb_ue_id(ENB_UE_S1AP_ID enb_ue_id) runs on UEMux_CT return integer { for (var integer i := 0; i < lengthof(g_uemux_list); i := i + 1) { if (g_uemux_list[i].enb_ue_id == enb_ue_id) { return i; } } return -1; } /* find a UE connection [index] for the given S1AP PDU */ private function f_UEMux_find_ue_for_pdu(in S1AP_PDU pdu) runs on UEMux_CT return integer { var template (omit) MME_UE_S1AP_ID mme_ue_id; var template (omit) ENB_UE_S1AP_ID enb_ue_id; var integer idx; mme_ue_id := f_S1AP_get_MME_UE_S1AP_ID(pdu); if (isvalue(mme_ue_id)) { idx := f_UEMux_find_ue_by_mme_ue_id(valueof(mme_ue_id)); if (idx != -1) { return idx; } } enb_ue_id := f_S1AP_get_ENB_UE_S1AP_ID(pdu); if (isvalue(enb_ue_id)) { idx := f_UEMux_find_ue_by_enb_ue_id(valueof(enb_ue_id)); if (idx != -1) { return idx; } } return -1; } /* find a UE connection [index] by component reference */ private function f_UEMux_find_ue_by_vc_conn(UEMuxUE vc_conn) runs on UEMux_CT return integer { for (var integer i := 0; i < lengthof(g_uemux_list); i := i + 1) { if (g_uemux_list[i].vc_conn == vc_conn) { return i; } } return -1; } private function f_UEMux_route_pdu(S1AP_dPDU dpdu) runs on UEMux_CT { var integer idx; idx := f_UEMux_find_ue_for_pdu(dpdu.pdu); if (idx >= 0) { UEMUX_CONN.send(dpdu) to g_uemux_list[idx].vc_conn; } else { log("S1AP PDU cannot be routed: ", dpdu.pdu); } } /* add a new UE connection, return its index */ private function f_UEMux_ue_add(UEMuxUE vc_conn) runs on UEMux_CT return integer { var integer idx; idx := f_UEMux_find_ue_by_vc_conn(vc_conn); if (idx != -1) { /* already registered */ return idx; } idx := lengthof(g_uemux_list); g_uemux_list[idx] := { vc_conn, omit, omit }; log("UE ", vc_conn, " is registered (idx=", idx, ")"); return idx; } /* subscribe a UE component for S1AP PDUs with the given MME-UE-ID */ private function f_UEMux_subscribe_for_mme_ue_id(UEMuxUE vc_conn, MME_UE_S1AP_ID mme_ue_id) runs on UEMux_CT { var integer idx; if (f_UEMux_find_ue_by_mme_ue_id(mme_ue_id) != -1) { setverdict(fail, "MME-UE-ID ", mme_ue_id," is already used"); mtc.stop; } idx := f_UEMux_ue_add(vc_conn); g_uemux_list[idx].mme_ue_id := mme_ue_id; } /* subscribe a UE component for S1AP PDUs with the given ENB-UE-ID */ private function f_UEMux_subscribe_for_enb_ue_id(UEMuxUE vc_conn, ENB_UE_S1AP_ID enb_ue_id) runs on UEMux_CT { var integer idx; if (f_UEMux_find_ue_by_enb_ue_id(enb_ue_id) != -1) { setverdict(fail, "ENB-UE-ID ", enb_ue_id," is already used"); mtc.stop; } idx := f_UEMux_ue_add(vc_conn); g_uemux_list[idx].enb_ue_id := enb_ue_id; } /* unsubscribe a UE component */ private function f_UEMux_unsubscribe(UEMuxUE vc_conn) runs on UEMux_CT { var UE_ConnList ue_list := { }; for (var integer i := 0; i < lengthof(g_uemux_list); i := i + 1) { if (g_uemux_list[i].vc_conn != vc_conn) { ue_list := ue_list & { g_uemux_list[i] }; } } if (lengthof(ue_list) == lengthof(g_uemux_list)) { setverdict(fail, "UE ", vc_conn, " is not known"); mtc.stop; } g_uemux_list := ue_list; log("UE ", vc_conn, " is unregistered"); } signature UEMux_subscribe_for_mme_ue_id(in MME_UE_S1AP_ID mme_ue_id); signature UEMux_subscribe_for_enb_ue_id(in ENB_UE_S1AP_ID enb_ue_id); signature UEMux_unsubscribe(); signature UEMux_erab_setup(inout ERabList erabs); signature UEMux_erab_release_cmd(inout ERabList erabs); signature UEMux_erab_release_ind(inout ERabList erabs); signature UEMux_initial_ctx_setup(inout ERabList erabs); signature UEMux_ue_ctx_release_req(inout ERabList erabs); signature UEMux_ue_ctx_release_cmd(inout ERabList erabs); type port UEMUX_PROC_PT procedure { inout UEMux_subscribe_for_mme_ue_id; inout UEMux_subscribe_for_enb_ue_id; inout UEMux_unsubscribe; inout UEMux_erab_setup; inout UEMux_erab_release_cmd; inout UEMux_erab_release_ind; inout UEMux_initial_ctx_setup; inout UEMux_ue_ctx_release_req; inout UEMux_ue_ctx_release_cmd; } with { extension "internal" }; /*********************************************************************************** * public API **********************************************************************************/ function f_UEMuxUE_subscribe_for_mme_ue_id(MME_UE_S1AP_ID mme_ue_id) runs on UEMuxUE { UEMUX_PROC.call(UEMux_subscribe_for_mme_ue_id:{mme_ue_id}) { [] UEMUX_PROC.getreply(UEMux_subscribe_for_mme_ue_id:{mme_ue_id}); } } function f_UEMuxUE_subscribe_for_enb_ue_id(ENB_UE_S1AP_ID enb_ue_id) runs on UEMuxUE { UEMUX_PROC.call(UEMux_subscribe_for_enb_ue_id:{enb_ue_id}) { [] UEMUX_PROC.getreply(UEMux_subscribe_for_enb_ue_id:{enb_ue_id}); } } function f_UEMuxUE_unsubscribe() runs on UEMuxUE { UEMUX_PROC.call(UEMux_unsubscribe:{}) { [] UEMUX_PROC.getreply(UEMux_unsubscribe:{}); } } function f_UEMux_erab_setup(inout ERabList erabs) runs on UEMuxUE { UEMUX_PROC.call(UEMux_erab_setup:{erabs}) { [] UEMUX_PROC.getreply(UEMux_erab_setup:{?}) -> param(erabs); } } function f_UEMux_erab_release_cmd(inout ERabList erabs) runs on UEMuxUE { UEMUX_PROC.call(UEMux_erab_release_cmd:{erabs}) { [] UEMUX_PROC.getreply(UEMux_erab_release_cmd:{?}) -> param(erabs); } } function f_UEMux_erab_release_ind(inout ERabList erabs) runs on UEMuxUE { UEMUX_PROC.call(UEMux_erab_release_ind:{erabs}) { [] UEMUX_PROC.getreply(UEMux_erab_release_ind:{?}) -> param(erabs); } } function f_UEMux_initial_ctx_setup(inout ERabList erabs) runs on UEMuxUE { UEMUX_PROC.call(UEMux_initial_ctx_setup:{erabs}) { [] UEMUX_PROC.getreply(UEMux_initial_ctx_setup:{?}) -> param(erabs); } } function f_UEMux_ue_ctx_release_req(inout ERabList erabs) runs on UEMuxUE { UEMUX_PROC.call(UEMux_ue_ctx_release_req:{erabs}) { [] UEMUX_PROC.getreply(UEMux_ue_ctx_release_req:{?}) -> param(erabs); } } function f_UEMux_ue_ctx_release_cmd(inout ERabList erabs) runs on UEMuxUE { UEMUX_PROC.call(UEMux_ue_ctx_release_cmd:{erabs}) { [] UEMUX_PROC.getreply(UEMux_ue_ctx_release_cmd:{?}) -> param(erabs); } } function f_UEMuxUE_tx_s1ap_from_mme(template (value) S1AP_PDU pdu) runs on UEMuxUE { var S1AP_dPDU dpdu := { S1AP_PDU_DIR_FROM_MME, valueof(pdu) }; UEMUX_CONN.send(dpdu); } function f_UEMuxUE_tx_s1ap_from_enb(template (value) S1AP_PDU pdu) runs on UEMuxUE { var S1AP_dPDU dpdu := { S1AP_PDU_DIR_FROM_ENB, valueof(pdu) }; UEMUX_CONN.send(dpdu); } altstep as_UEMuxUE_s1ap_from_mme(out S1AP_PDU pdu, template (present) S1AP_PDU tr_pdu := ?) runs on UEMuxUE { [] as_s1ap_dpdu(pdu, S1AP_PDU_DIR_FROM_MME, tr_pdu); } altstep as_UEMuxUE_s1ap_from_enb(out S1AP_PDU pdu, template (present) S1AP_PDU tr_pdu := ?) runs on UEMuxUE { [] as_s1ap_dpdu(pdu, S1AP_PDU_DIR_FROM_ENB, tr_pdu); } function f_UEMuxUE_trx_s1ap_from_mme(template (value) S1AP_PDU ts_pdu, template (present) S1AP_PDU tr_pdu := ?, float Tval := 1.0) runs on UEMuxUE return S1AP_PDU { var S1AP_PDU pdu; f_trx_s1ap_pdu(pdu, S1AP_PDU_DIR_FROM_MME, ts_pdu, tr_pdu, Tval); return pdu; } function f_UEMuxUE_trx_s1ap_from_enb(template (value) S1AP_PDU ts_pdu, template (present) S1AP_PDU tr_pdu := ?, float Tval := 1.0) runs on UEMuxUE return S1AP_PDU { var S1AP_PDU pdu; f_trx_s1ap_pdu(pdu, S1AP_PDU_DIR_FROM_ENB, ts_pdu, tr_pdu, Tval); return pdu; } type function void_fn2(charstring id) runs on UEMux_CT; function f_UEMux_init(void_fn2 fn, charstring id, ConnHdlrPars pars) runs on UEMux_CT { g_pars := pars; fn.apply(id); } function main() runs on UEMux_CT { while (true) { var MME_UE_S1AP_ID mme_ue_id; var ENB_UE_S1AP_ID enb_ue_id; var UEMuxUE vc_conn; var ERabList erabs; var S1AP_PDU pdu; alt { /* MME originated S1AP PDU to S1GW */ [] as_s1ap_dpdu(pdu, S1AP_PDU_DIR_FROM_MME) { f_ConnHdlr_tx_s1ap_from_mme(pdu); } /* eNB originated S1AP PDU to S1GW */ [] as_s1ap_dpdu(pdu, S1AP_PDU_DIR_FROM_ENB) { f_ConnHdlr_tx_s1ap_from_enb(pdu); } /* MME originated S1AP PDU from S1GW */ [] as_ConnHdlr_s1ap_from_mme(pdu) { f_UEMux_route_pdu({S1AP_PDU_DIR_FROM_MME, pdu}); } /* eNB originated S1AP PDU from S1GW */ [] as_ConnHdlr_s1ap_from_enb(pdu) { f_UEMux_route_pdu({S1AP_PDU_DIR_FROM_ENB, pdu}); } /* UE subsciption procedures */ [] UEMUX_PROC.getcall(UEMux_subscribe_for_mme_ue_id:{?}) -> param(mme_ue_id) sender vc_conn { f_UEMux_subscribe_for_mme_ue_id(vc_conn, mme_ue_id); UEMUX_PROC.reply(UEMux_subscribe_for_mme_ue_id:{mme_ue_id}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_subscribe_for_enb_ue_id:{?}) -> param(enb_ue_id) sender vc_conn { f_UEMux_subscribe_for_enb_ue_id(vc_conn, enb_ue_id); UEMUX_PROC.reply(UEMux_subscribe_for_enb_ue_id:{enb_ue_id}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_unsubscribe:{}) -> sender vc_conn { f_UEMux_unsubscribe(vc_conn); UEMUX_PROC.reply(UEMux_unsubscribe:{}) to vc_conn; } /* E-RAB and context related procedures */ [] UEMUX_PROC.getcall(UEMux_erab_setup:{?}) -> param(erabs) sender vc_conn { var integer idx := f_UEMux_find_ue_by_vc_conn(vc_conn); f_ConnHdlr_erab_setup_req(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); f_ConnHdlr_erab_setup_rsp(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); UEMUX_PROC.reply(UEMux_erab_setup:{erabs}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_erab_release_cmd:{?}) -> param(erabs) sender vc_conn { var integer idx := f_UEMux_find_ue_by_vc_conn(vc_conn); f_ConnHdlr_erab_release_cmd(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); f_ConnHdlr_erab_release_rsp(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); UEMUX_PROC.reply(UEMux_erab_release_cmd:{erabs}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_erab_release_ind:{?}) -> param(erabs) sender vc_conn { var integer idx := f_UEMux_find_ue_by_vc_conn(vc_conn); f_ConnHdlr_erab_release_ind(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); UEMUX_PROC.reply(UEMux_erab_release_ind:{erabs}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_initial_ctx_setup:{?}) -> param(erabs) sender vc_conn { var integer idx := f_UEMux_find_ue_by_vc_conn(vc_conn); f_ConnHdlr_initial_ctx_setup_req(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); f_ConnHdlr_initial_ctx_setup_rsp(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); UEMUX_PROC.reply(UEMux_initial_ctx_setup:{erabs}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_ue_ctx_release_req:{?}) -> param(erabs) sender vc_conn { var integer idx := f_UEMux_find_ue_by_vc_conn(vc_conn); f_ConnHdlr_ue_ctx_release_req(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); UEMUX_PROC.reply(UEMux_ue_ctx_release_req:{erabs}) to vc_conn; } [] UEMUX_PROC.getcall(UEMux_ue_ctx_release_cmd:{?}) -> param(erabs) sender vc_conn { var integer idx := f_UEMux_find_ue_by_vc_conn(vc_conn); f_ConnHdlr_ue_ctx_release_cmd(erabs, g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); f_ConnHdlr_ue_ctx_release_compl(g_uemux_list[idx].mme_ue_id, g_uemux_list[idx].enb_ue_id); UEMUX_PROC.reply(UEMux_ue_ctx_release_cmd:{erabs}) to vc_conn; } } } } }