/* OsmoS1GW (S1AP Gateway) test suite in TTCN-3 * * (C) 2024 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 */ module S1GW_Tests { import from General_Types all; import from Osmocom_Types all; import from Native_Functions all; import from IPL4asp_Types all; import from Misc_Helpers all; import from Mutex all; import from S1AP_CodecPort all; import from S1AP_CodecPort_CtrlFunct all; import from S1AP_Types all; import from S1AP_Templates all; import from S1AP_PDU_Descriptions all; import from S1AP_IEs all; import from S1AP_PDU_Contents all; import from S1AP_Constants all; import from PFCP_Types all; import from PFCP_Emulation all; import from PFCP_Templates all; import from PFCP_CodecPort all; import from SCTP_Templates all; import from StatsD_Types all; import from StatsD_CodecPort all; import from StatsD_CodecPort_CtrlFunct all; import from StatsD_Checker all; import from S1AP_Server all; import from S1GW_ConnHdlr all; modulepar { charstring mp_s1gw_enb_ip := "127.0.1.1"; /* eNB facing address of the S1GW */ charstring mp_enb_bind_ip := "127.0.1.10"; /* eNB address to use locally when connecting to S1GW */ charstring mp_s1gw_mme_ip := "127.0.2.1"; /* MME facing address of the S1GW */ charstring mp_mme_bind_ip := "127.0.2.10"; /* MME address on which we get connections from S1GW */ charstring mp_s1gw_upf_ip := "127.0.3.1"; /* UPF facing address of the S1GW */ charstring mp_upf_bind_ip := "127.0.3.10"; /* UPF address on which we get connections from S1GW */ /* Our emulated StatsD server: */ charstring mp_local_statsd_ip := "127.0.4.10"; integer mp_local_statsd_port := 8125; charstring mp_statsd_prefix := "s1gw."; integer mp_multi_enb_num := 42; /* number of eNBs in _multi TCs */ } type component test_CT extends StatsD_Checker_CT { timer g_Tguard; var MutexDispCT vc_mutex_disp; var S1AP_Server_CT vc_S1APSRV; var PFCP_Emulation_CT vc_PFCP; var StatsD_Checker_CT vc_STATSD; }; private altstep as_Tguard() runs on test_CT { [] g_Tguard.timeout { Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Tguard timeout"); } } function f_init(boolean s1apsrv_start := true, boolean upf_start := true, float Tval := 20.0) runs on test_CT { g_Tguard.start(Tval); activate(as_Tguard()); vc_mutex_disp := f_MutexDisp_start(); f_init_statsd("StatsDSRV", vc_STATSD, mp_local_statsd_ip, mp_local_statsd_port); if (s1apsrv_start) { f_init_s1ap_srv(); } if (upf_start) { f_init_pfcp(); f_pfcp_assoc(); } } function f_init_s1ap_srv() runs on test_CT { var S1APSRV_ConnParams cpars := { local_ip := mp_mme_bind_ip, local_port := 36412 }; vc_S1APSRV := S1AP_Server_CT.create("S1APSRV-" & testcasename()) alive; vc_S1APSRV.start(S1AP_Server.main(cpars)); } function f_init_pfcp() runs on test_CT { var PFCP_Emulation_Cfg pfcp_cfg := { pfcp_bind_ip := mp_upf_bind_ip, pfcp_bind_port := PFCP_PORT, pfcp_remote_ip := mp_s1gw_upf_ip, pfcp_remote_port := PFCP_PORT, role := UPF }; vc_PFCP := PFCP_Emulation_CT.create("PFCPEM-" & testcasename()) alive; vc_PFCP.start(PFCP_Emulation.main(pfcp_cfg)); } /* ensure that the PFCP association is set up */ function f_pfcp_assoc() runs on test_CT { var verdicttype verdict; var ConnHdlr vc_conn; vc_conn := f_ConnHdlr_spawn(refers(f_ConnHdlr_pfcp_assoc_handler), pars := valueof(t_ConnHdlrPars)); vc_conn.done -> value verdict; if (verdict != pass) { Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } } template (value) ConnHdlrPars t_ConnHdlrPars(integer idx := 0, integer num_erabs := 1, charstring statsd_prefix := mp_statsd_prefix, charstring pfcp_loc_addr := mp_upf_bind_ip, charstring pfcp_rem_addr := mp_s1gw_upf_ip) := { idx := idx, genb_id := ts_Global_ENB_ID(idx), statsd_prefix := statsd_prefix, pfcp_loc_addr := pfcp_loc_addr, pfcp_rem_addr := pfcp_rem_addr, mme_ue_id := 4242, erabs := f_gen_erab_list(idx, num_erabs) } private function f_gen_erab_list(integer idx, integer num_erabs) return ERabList { var OCT6 seid_prefix := char2oct("TTCN-3"); var ERabList erabs; for (var integer id := 0; id < num_erabs; id := id + 1) { var OCT2 uid := int2oct(idx, 1) & int2oct(id, 1); erabs[id] := { erab_id := id, /* sequentially assign E-RAB IDs */ u2c := {'0001'O & uid, "127.0.0.1"}, c2u := {'0101'O & uid, "127.0.1.1"}, a2u := {'0202'O & uid, "127.0.2.2"}, u2a := {'0002'O & uid, "127.0.0.2"}, pfcp_loc_seid := seid_prefix & uid, pfcp_rem_seid := omit /* assigned by S1GW */ }; } return erabs; } function f_ConnHdlr_spawn(void_fn fn, ConnHdlrPars pars) runs on test_CT return ConnHdlr { var ConnHdlr vc_conn; var charstring id := "ConnHdlr-" & testcasename() & "-" & int2str(pars.idx); vc_conn := ConnHdlr.create(id) alive; if (isbound(vc_STATSD) and vc_STATSD.running) { connect(vc_conn:STATSD_PROC, vc_STATSD:STATSD_PROC); } if (isbound(vc_S1APSRV) and vc_S1APSRV.running) { connect(vc_conn:S1AP_CONN, vc_S1APSRV:S1AP_CLIENT); connect(vc_conn:S1AP_PROC, vc_S1APSRV:S1AP_PROC); } if (isbound(vc_PFCP) and vc_PFCP.running) { connect(vc_conn:PFCP, vc_PFCP:CLIENT); connect(vc_conn:PFCP_PROC, vc_PFCP:CLIENT_PROC); } f_MutexDisp_connect(vc_mutex_disp, vc_conn); vc_conn.start(f_ConnHdlr_init(fn, id, pars)); return vc_conn; } /* wait for all ConnHdlr in the given list to be .done() */ function f_ConnHdlrList_all_done(in ConnHdlrList vc_conns) runs on test_CT { for (var integer i := 0; i < lengthof(vc_conns); i := i + 1) { vc_conns[i].done; } } function f_TC_setup(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_register(g_pars.genb_id); /* Expected values relative to snapshot: */ var StatsDExpects statsd_exp := { {name := mp_statsd_prefix & "gauge.s1ap.enb.num_sctp_connections.value", mtype := "g", min := 1, max := 1}, {name := mp_statsd_prefix & "ctr.s1ap.proxy.out_pkt.forward.unmodified.value", mtype := "c", min := 2, max := 2} } var StatsDMetrics statsd_snapshot := f_statsd_snapshot(f_statsd_keys_from_expect(statsd_exp)); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); f_sleep(0.5); /* keep the connection idle for some time */ f_statsd_expect_from_snapshot(statsd_exp, wait_converge := true, snapshot := statsd_snapshot); f_ConnHdlr_s1ap_disconnect(); /* Validate gauge decreases when we disconnect: */ f_statsd_expect({{name := mp_statsd_prefix & "gauge.s1ap.enb.num_sctp_connections.value", mtype := "g", min := 0, max := 0}}, wait_converge := true); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } testcase TC_setup() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(); vc_conn := f_ConnHdlr_spawn(refers(f_TC_setup), pars); vc_conn.done; } function f_TC_setup_multi(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); f_sleep(0.5); /* keep the connection idle for some time */ f_ConnHdlr_s1ap_disconnect(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } testcase TC_setup_multi() runs on test_CT { var ConnHdlrList vc_conns; f_init(); for (var integer i := 0; i < mp_multi_enb_num; i := i + 1) { var ConnHdlrPars pars := valueof(t_ConnHdlrPars(i)); vc_conns[i] := f_ConnHdlr_spawn(refers(f_TC_setup_multi), pars); } f_ConnHdlrList_all_done(vc_conns); } /* MME terminates connection, expect S1GW to terminate the eNB connection */ function f_TC_conn_term_by_mme(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); f_sleep(0.5); /* keep the connection idle for some time */ /* MME (S1AP_Server_CT) terminates connection */ f_ConnHdlr_s1ap_close_conn(g_pars.genb_id); /* expect our eNB connection to be released gracefully */ f_ConnHdlr_s1ap_expect_shutdown(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } testcase TC_conn_term_by_mme() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(); vc_conn := f_ConnHdlr_spawn(refers(f_TC_conn_term_by_mme), pars); vc_conn.done; } /* MME is not available, expect S1GW to terminate the eNB connection */ function f_TC_conn_term_mme_unavail(charstring id) runs on ConnHdlr { /* establish an eNB connection to the S1GW */ f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); /* expect our eNB connection to be released gracefully */ f_ConnHdlr_s1ap_expect_shutdown(); setverdict(pass); } testcase TC_conn_term_mme_unavail() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(s1apsrv_start := false); vc_conn := f_ConnHdlr_spawn(refers(f_TC_conn_term_mme_unavail), pars); vc_conn.done; } /* Test E-RAB SETUP and RELEASE.cmd procedures */ function f_TC_e_rab_setup(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); f_ConnHdlr_erab_setup_req(g_pars.erabs); f_ConnHdlr_erab_setup_rsp(g_pars.erabs); f_ConnHdlr_erab_release_cmd(g_pars.erabs); f_ConnHdlr_erab_release_rsp(g_pars.erabs); f_ConnHdlr_s1ap_disconnect(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } private function f_TC_e_rab_setup_exec(integer num_enbs, integer num_erabs) runs on test_CT { var ConnHdlrList vc_conns; f_init(); for (var integer i := 0; i < num_enbs; i := i + 1) { var ConnHdlrPars pars := valueof(t_ConnHdlrPars(i, num_erabs)); vc_conns[i] := f_ConnHdlr_spawn(refers(f_TC_e_rab_setup), pars); } f_ConnHdlrList_all_done(vc_conns); } /* 1 E-RAB at a time, single eNB */ testcase TC_e_rab_setup() runs on test_CT { f_TC_e_rab_setup_exec(num_enbs := 1, num_erabs := 1); } /* 1 E-RAB at a time, multiple eNB connections */ testcase TC_e_rab_setup_multi() runs on test_CT { f_TC_e_rab_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 1); } /* 3 E-RABs at a time, single eNB */ testcase TC_e_rab_setup3() runs on test_CT { f_TC_e_rab_setup_exec(num_enbs := 1, num_erabs := 3); } /* 3 E-RABs at a time, multiple eNB connections */ testcase TC_e_rab_setup3_multi() runs on test_CT { f_TC_e_rab_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 3); } /* Test E-RAB SETUP and RELEASE.ind procedures */ function f_TC_e_rab_release_ind(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); f_ConnHdlr_erab_setup_req(g_pars.erabs); f_ConnHdlr_erab_setup_rsp(g_pars.erabs); f_ConnHdlr_erab_release_ind(g_pars.erabs); f_ConnHdlr_s1ap_disconnect(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } testcase TC_e_rab_release_ind() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(); vc_conn := f_ConnHdlr_spawn(refers(f_TC_e_rab_release_ind), pars); vc_conn.done; } /* Test E-RAB SETUP procedure being aborted by the S1GW due to * PFCP Session Establishment failure (cause=REQUEST_REJECTED). */ function f_TC_e_rab_setup_failure(charstring id) runs on ConnHdlr { var OCT4 addr := f_inet_addr(g_pars.pfcp_loc_addr); var ERab erab := g_pars.erabs[0]; f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); /* see comments in f_ConnHdlr_erab_setup_req() */ f_PFCPEM_subscribe_seid('0000000000000000'O); f_PFCPEM_subscribe_seid(erab.pfcp_loc_seid); log("eNB <- [S1GW <- MME]: E-RAB SETUP REQUEST"); f_ConnHdlr_tx_erab_setup_req({erab}); log("UPF <- S1GW: PFCP Session Establishment Request"); var PDU_PFCP req := f_ConnHdlr_rx_session_establish_req(erab); /* store peer's SEID, so that it can be used in outgoing PDUs later */ erab.pfcp_rem_seid := req.message_body.pfcp_session_establishment_request.CP_F_SEID.seid; log("UPF -> S1GW: PFCP Session Establishment Response (failure)"); var template (value) PDU_PFCP resp; resp := ts_PFCP_Session_Est_Resp(seq_nr := req.sequence_number, node_id := ts_PFCP_Node_ID_ipv4(addr), seid := erab.pfcp_rem_seid, f_seid := ts_PFCP_F_SEID_ipv4(addr, erab.pfcp_loc_seid), created_pdr := {}, cause := ts_PFCP_Cause(REQUEST_REJECTED)); PFCP.send(resp); /* see comments in f_ConnHdlr_erab_setup_req() */ f_PFCPEM_unsubscribe_seid('0000000000000000'O); f_PFCPEM_unsubscribe_seid(erab.pfcp_loc_seid); /* expect E-RAB SETUP RESPONSE replied by the S1GW */ var template (present) S1AP_PDU setup_rsp; var template (present) E_RABItem item; var S1AP_PDU s1ap_pdu; log("eNB -- [S1GW -> MME]: E-RAB SETUP RESPONSE (failure)"); item := tr_E_RABItem(erab.erab_id, {transport := transport_resource_unavailable}); setup_rsp := tr_S1AP_RABSetupRsp(g_pars.mme_ue_id, g_pars.idx, rab_setup_items := omit, rab_failed_items := tr_E_RABList(item)); f_ConnHdlr_rx_s1ap_from_enb(s1ap_pdu); f_ConnHdlr_s1ap_disconnect(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } testcase TC_e_rab_setup_failure() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(); vc_conn := f_ConnHdlr_spawn(refers(f_TC_e_rab_setup_failure), pars); vc_conn.done; } /* Test INITIAL CONTEXT SETUP procedure (successful case) */ function f_TC_initial_ctx_setup(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); f_ConnHdlr_initial_ctx_setup_req(g_pars.erabs); f_ConnHdlr_initial_ctx_setup_rsp(g_pars.erabs); f_ConnHdlr_s1ap_disconnect(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } private function f_TC_initial_ctx_setup_exec(integer num_enbs, integer num_erabs) runs on test_CT { var ConnHdlrList vc_conns; f_init(); for (var integer i := 0; i < num_enbs; i := i + 1) { var ConnHdlrPars pars := valueof(t_ConnHdlrPars(i, num_erabs)); vc_conns[i] := f_ConnHdlr_spawn(refers(f_TC_initial_ctx_setup), pars); } f_ConnHdlrList_all_done(vc_conns); } /* 1 E-RAB at a time, single eNB */ testcase TC_initial_ctx_setup() runs on test_CT { f_TC_initial_ctx_setup_exec(num_enbs := 1, num_erabs := 1); } /* 1 E-RAB at a time, multiple eNB connections */ testcase TC_initial_ctx_setup_multi() runs on test_CT { f_TC_initial_ctx_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 1); } /* 3 E-RABs at a time, single eNB */ testcase TC_initial_ctx_setup3() runs on test_CT { f_TC_initial_ctx_setup_exec(num_enbs := 1, num_erabs := 3); } /* 3 E-RABs at a time, multiple eNB connections */ testcase TC_initial_ctx_setup3_multi() runs on test_CT { f_TC_initial_ctx_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 3); } /* Test INITIAL CONTEXT SETUP procedure (failure) */ function f_TC_initial_ctx_setup_failure(charstring id) runs on ConnHdlr { var template (present) PDU_PFCP pfcp_pdu; var template (value) S1AP_PDU s1ap_pdu; var S1AP_PDU unused; f_ConnHdlr_s1ap_register(g_pars.genb_id); f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); f_ConnHdlr_s1ap_setup(g_pars.genb_id); /* Initiate E-RAB establishment by sending INITIAL CONTEXT SETUP */ f_ConnHdlr_initial_ctx_setup_req(g_pars.erabs); /* At this point, the associated PFCP session is created, but not modified yet. * The E-RAB FSM in the IUT is waiting for INITIAL CONTEXT SETUP RESPONSE. */ /* INITIAL CONTEXT SETUP FAILURE does not immediately trigger deletion of PFCP * sessions. It's forwarded as-is without any action. */ s1ap_pdu := ts_S1AP_InitialCtxSetupFail(mme_id := g_pars.mme_ue_id, enb_id := g_pars.idx, cause := { misc := unspecified }); f_ConnHdlr_tx_s1ap_from_enb(s1ap_pdu); f_ConnHdlr_rx_s1ap_from_enb(unused, s1ap_pdu); /* TODO: Ideally, the IUT should terminate PFCP session(s) immediately. */ /* E-RAB FSM in the IUT terminates due to a timeout and deletes PFCP session. */ pfcp_pdu := tr_PFCP_Session_Del_Req(g_pars.erabs[0].pfcp_loc_seid); f_ConnHdlr_pfcp_expect(pfcp_pdu, Tval := 8.0); /* > ERAB_T_WAIT_RELEASE_RSP */ f_ConnHdlr_s1ap_disconnect(); f_ConnHdlr_s1ap_unregister(g_pars.genb_id); } testcase TC_initial_ctx_setup_failure() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(); vc_conn := f_ConnHdlr_spawn(refers(f_TC_initial_ctx_setup_failure), pars); vc_conn.done; } function f_TC_pfcp_heartbeat(charstring id) runs on ConnHdlr { var integer rts := f_PFCPEM_get_recovery_timestamp(); var PDU_PFCP pfcp_pdu; /* Tx Heartbeat Request */ PFCP.send(ts_PFCP_Heartbeat_Req(rts)); /* Expect Heartbeat Response * TODO: validate the indicated Recovery Time Stamp against * the one that we received in the PFCP Association Setup. */ pfcp_pdu := f_ConnHdlr_pfcp_expect(tr_PFCP_Heartbeat_Resp); setverdict(pass); } testcase TC_pfcp_heartbeat() runs on test_CT { var ConnHdlrPars pars := valueof(t_ConnHdlrPars); var ConnHdlr vc_conn; f_init(); vc_conn := f_ConnHdlr_spawn(refers(f_TC_pfcp_heartbeat), pars); vc_conn.done; } control { execute( TC_setup() ); execute( TC_setup_multi() ); execute( TC_conn_term_by_mme() ); execute( TC_conn_term_mme_unavail() ); execute( TC_e_rab_setup() ); execute( TC_e_rab_setup3() ); execute( TC_e_rab_setup_multi() ); execute( TC_e_rab_setup3_multi() ); execute( TC_e_rab_release_ind() ); execute( TC_e_rab_setup_failure() ); execute( TC_initial_ctx_setup() ); execute( TC_initial_ctx_setup3() ); execute( TC_initial_ctx_setup_multi() ); execute( TC_initial_ctx_setup3_multi() ); execute( TC_initial_ctx_setup_failure() ); execute( TC_pfcp_heartbeat() ); } }