fixeria has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/42495?usp=email )
Change subject: s1gw: add more PFCP Heartbeat test cases ......................................................................
s1gw: add more PFCP Heartbeat test cases
TC_pfcp_heartbeat_periodic: verify that the IUT sends periodic PFCP Heartbeat Requests and that the heartbeat timer re-arms correctly after each successful response (at least 2 periodic HBs must be observed).
TC_pfcp_heartbeat_miss_threshold: verify that the IUT resets the PFCP association after mp_pfcp_heartbeat_miss_count consecutive unanswered Heartbeat Requests (a new Association Setup Request is expected).
TC_pfcp_heartbeat_miss_reset: verify that a successful Heartbeat Response resets the miss counter; run 2 cycles of missing (miss_count - 1) HBs then responding to one, expecting no association reset throughout.
TC_pfcp_heartbeat_rts_mismatch: verify that the IUT detects a UPF restart when a Heartbeat Response carries a different Recovery Timestamp than the one observed during Association Setup, and that it promptly resets and re-establishes the PFCP association.
Also add module parameters for the heartbeat configuration (mp_pfcp_heartbeat_interval, mp_pfcp_heartbeat_req_timeout, mp_pfcp_heartbeat_miss_count) and update osmo-s1gw.config accordingly.
Change-Id: Ie5ac25b1ca4bb11e61bff220449397c271b11464 Related: osmo-s1gw.git Iba954746fe20e6b9eeaec3196e1f83e3fc3e7fc2 Related: osmo-s1gw.git I306324f8eca325202a3fa23125854db9d5eaab38 Related: osmo-s1gw.git I00a8384db1ea9c38d0ad9ff90b3d45ad86c3a020 --- M s1gw/S1GW_ConnHdlr.ttcn M s1gw/S1GW_Tests.ttcn M s1gw/expected-results.xml M s1gw/osmo-s1gw.config 4 files changed, 278 insertions(+), 8 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/osmo-ttcn3-hacks refs/changes/95/42495/1
diff --git a/s1gw/S1GW_ConnHdlr.ttcn b/s1gw/S1GW_ConnHdlr.ttcn index 297eccd..aeda4c5 100644 --- a/s1gw/S1GW_ConnHdlr.ttcn +++ b/s1gw/S1GW_ConnHdlr.ttcn @@ -398,6 +398,15 @@ Misc_Helpers.f_shutdown(__BFILE__, __LINE__); }
+function f_PFCPEM_tx_assoc_setup_resp(LIN3_BO_LAST seq_nr) +runs on PFCP_ConnHdlr +{ + PFCP.send(ts_PFCP_Assoc_Setup_Resp(seq_nr, + ts_PFCP_Node_ID_fqdn("\07osmocom\03org"), + ts_PFCP_Cause(REQUEST_ACCEPTED), + f_PFCPEM_get_recovery_timestamp())); +} + private function f_ConnHdlr_pfcp_assoc_setup() runs on ConnHdlr { @@ -406,10 +415,7 @@ f_PFCPEM_subscribe_bcast(); /* ask PFCPEM to route all PDUs to us */ rx := f_ConnHdlr_pfcp_expect(tr_PFCP_Assoc_Setup_Req, Tval := 10.0); f_PFCPEM_unsubscribe_bcast(); /* ask PFCPEM to *not* route all PDUs to us */ - PFCP.send(ts_PFCP_Assoc_Setup_Resp(rx.sequence_number, - ts_PFCP_Node_ID_fqdn("\07osmocom\03org"), - ts_PFCP_Cause(REQUEST_ACCEPTED), - f_PFCPEM_get_recovery_timestamp())); + f_PFCPEM_tx_assoc_setup_resp(rx.sequence_number); }
function f_ConnHdlr_pfcp_expect(template (present) PDU_PFCP exp_rx := ?, diff --git a/s1gw/S1GW_Tests.ttcn b/s1gw/S1GW_Tests.ttcn index 38a5e7d..198bc7b 100644 --- a/s1gw/S1GW_Tests.ttcn +++ b/s1gw/S1GW_Tests.ttcn @@ -68,6 +68,11 @@
charstring mp_rest_host := "127.0.0.1"; integer mp_rest_port := 8080; + + /* PFCP heartbeat test parameters (must match IUT's pfcp_peer configuration) */ + float mp_pfcp_heartbeat_interval := 4.0; + float mp_pfcp_heartbeat_req_timeout := 2.0; + integer mp_pfcp_heartbeat_miss_count := 3; }
type component test_CT extends StatsD_Checker_CT, http_CT { @@ -87,6 +92,7 @@ function f_init(boolean s1apsrv_start := true, integer num_mmes := 1, boolean upf_start := true, + boolean pfcp_answer_heartbeat := true, float Tval := 20.0) runs on test_CT { g_Tguard.start(Tval); activate(as_Tguard()); @@ -100,7 +106,7 @@ } f_init_rest(); if (upf_start) { - f_init_pfcp(); + f_init_pfcp(pfcp_answer_heartbeat); f_pfcp_assoc(); } } @@ -121,14 +127,14 @@ } }
-function f_init_pfcp() runs on test_CT { +function f_init_pfcp(boolean answer_heartbeat_req := true) 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, - answer_heartbeat_req := true + answer_heartbeat_req := answer_heartbeat_req };
vc_PFCP := PFCP_Emulation_CT.create("PFCPEM-" & testcasename()) alive; @@ -1028,6 +1034,29 @@ f_TC_exec(refers(f_TC_handover_res_alloc_fail), 1, 6); }
+/* Like f_TC_exec() but without S1AP server and with answer_heartbeat_req := false, + * allowing test functions to observe and control IUT-initiated HB requests via bcast. + * The PFCP association is established via f_pfcp_assoc() before the ConnHdlr starts. */ +private function f_TC_exec_pfcp_hb(void_fn fn, float Tval := 60.0) runs on test_CT { + var ConnHdlr vc_conn; + + f_init(s1apsrv_start := false, pfcp_answer_heartbeat := false, Tval := Tval); + vc_conn := f_ConnHdlr_spawn(fn, valueof(t_ConnHdlrPars)); + vc_conn.done; +} + +/* Send a PFCP Heartbeat Response with an explicit RTS value */ +private function f_pfcp_send_hb_resp_rts(PDU_PFCP req, integer rts) runs on ConnHdlr { + var PDU_PFCP resp := valueof(ts_PFCP_Heartbeat_Resp(rts)); + resp.sequence_number := req.sequence_number; + PFCP.send(resp); +} + +/* Send a PFCP Heartbeat Response with the PFCPEM's own recovery timestamp */ +private function f_pfcp_send_hb_resp(PDU_PFCP req) runs on ConnHdlr { + f_pfcp_send_hb_resp_rts(req, f_PFCPEM_get_recovery_timestamp()); +} + function f_TC_pfcp_heartbeat(charstring id) runs on ConnHdlr { var integer rts := f_PFCPEM_get_recovery_timestamp(); var PfcpAssocInfo assoc_info; @@ -1062,6 +1091,225 @@ f_TC_exec(refers(f_TC_pfcp_heartbeat)); }
+/* Verify that the IUT sends periodic PFCP Heartbeat Requests when heartbeat_interval > 0. + * The test subscribes to PFCPEM broadcast, responds to each HB request, and verifies + * that at least 2 periodic HBs are received (proving the timer re-arms correctly). + * The PFCP association is assumed to be already established (done by f_TC_exec_pfcp_hb). */ +function f_TC_pfcp_heartbeat_periodic(charstring id) runs on ConnHdlr { + var PDU_PFCP pfcp_pdu; + timer T; + + if (mp_pfcp_heartbeat_interval == 0.0) { + setverdict(inconc, "periodic HB is disabled, skipped"); + return; + } + + /* Subscribe to bcast so we receive IUT-initiated HB requests */ + f_PFCPEM_subscribe_bcast(); + + /* Expect at least 2 periodic HB requests, responding to each */ + for (var integer i := 0; i < 2; i := i + 1) { + T.start(mp_pfcp_heartbeat_interval + + mp_pfcp_heartbeat_req_timeout + 2.0); + alt { + [] PFCP.receive(tr_PFCP_Heartbeat_Req) -> value pfcp_pdu { + f_pfcp_send_hb_resp(pfcp_pdu); + T.stop; + } + [] T.timeout { + setverdict(fail, "Timeout waiting for periodic PFCP Heartbeat Request"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + } + + f_PFCPEM_unsubscribe_bcast(); + setverdict(pass); +} +testcase TC_pfcp_heartbeat_periodic() runs on test_CT { + const float Tval := (mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout) * 3.0 + 10.0; + f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_periodic), Tval := Tval); +} + +/* Verify that the IUT resets the PFCP association after miss_count consecutive unanswered + * Heartbeat Requests. The test ignores all HB requests and waits for the IUT to initiate + * a new PFCP Association Setup (proving the association was reset). + * The PFCP association is assumed to be already established (done by f_TC_exec_pfcp_hb). */ +function f_TC_pfcp_heartbeat_miss_threshold(charstring id) runs on ConnHdlr { + var PDU_PFCP pfcp_pdu; + var float Tval := (mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout) + * int2float(mp_pfcp_heartbeat_miss_count) + 10.0; + timer T; + + if (mp_pfcp_heartbeat_interval == 0.0) { + setverdict(inconc, "periodic HB is disabled, skipped"); + return; + } + + /* Subscribe to bcast: we'll receive both HB requests and the new assoc setup req */ + f_PFCPEM_subscribe_bcast(); + + /* Do NOT respond to any HB requests; wait for the IUT to reset and re-associate */ + T.start(Tval); + alt { + [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) -> value pfcp_pdu { + /* IUT reset the association and started a new one - success */ + f_PFCPEM_tx_assoc_setup_resp(pfcp_pdu.sequence_number); + } + [] PFCP.receive(tr_PFCP_Heartbeat_Req) { + /* Discard unanswered HB requests and keep waiting */ + repeat; + } + [] T.timeout { + setverdict(fail, "Timeout: IUT did not reset PFCP association after ", + mp_pfcp_heartbeat_miss_count, " missed heartbeats"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + + f_PFCPEM_unsubscribe_bcast(); + setverdict(pass); +} +testcase TC_pfcp_heartbeat_miss_threshold() runs on test_CT { + var float Tval := (mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout) + * int2float(mp_pfcp_heartbeat_miss_count + 1) + 15.0; + f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_miss_threshold), Tval := Tval); +} + +/* Verify that a successful HB response resets the miss counter. The test misses + * (miss_count - 1) HBs, then responds to one, then misses (miss_count - 1) again + * and responds to one. If the miss counter is properly reset on success, the IUT + * should NOT reset the association during this sequence. + * The PFCP association is assumed to be already established (done by f_TC_exec_pfcp_hb). */ +function f_TC_pfcp_heartbeat_miss_reset(charstring id) runs on ConnHdlr { + var PDU_PFCP pfcp_pdu; + var float Tval := mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout + 2.0; + timer T; + + if (mp_pfcp_heartbeat_interval == 0.0) { + setverdict(inconc, "periodic HB is disabled, skipped"); + return; + } + + /* Subscribe to bcast: we'll receive HB requests (and assoc setup if reset occurs) */ + f_PFCPEM_subscribe_bcast(); + + /* Run 2 full miss-then-recover cycles */ + for (var integer i := 0; i < 2; i := i + 1) { + /* Miss (mp_pfcp_heartbeat_miss_count - 1) heartbeats */ + for (var integer j := 0; j < mp_pfcp_heartbeat_miss_count - 1; j := j + 1) { + T.start(Tval); + alt { + [] PFCP.receive(tr_PFCP_Heartbeat_Req) { + /* Discard without responding */ + T.stop; + } + [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) { + setverdict(fail, "IUT reset association unexpectedly"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + [] T.timeout { + setverdict(fail, "Timeout waiting for periodic PFCP Heartbeat Request"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + } + + /* Respond to the next HB to reset the miss counter */ + T.start(Tval); + alt { + [] PFCP.receive(tr_PFCP_Heartbeat_Req) -> value pfcp_pdu { + T.stop; + f_pfcp_send_hb_resp(pfcp_pdu); + } + [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) { + setverdict(fail, "IUT reset association unexpectedly (miss counter not reset?)"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + [] T.timeout { + setverdict(fail, "Timeout waiting for periodic PFCP Heartbeat Request"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + } + + f_PFCPEM_unsubscribe_bcast(); + setverdict(pass); +} +testcase TC_pfcp_heartbeat_miss_reset() runs on test_CT { + var float Tval := (mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout) + * int2float(mp_pfcp_heartbeat_miss_count) * 2.0 + 15.0; + f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_miss_reset), Tval := Tval); +} + +/* Verify that the IUT detects a UPF restart when the Recovery Timestamp in a Heartbeat + * Response differs from the value stored during Association Setup, and resets the + * PFCP association as a result. + * The PFCP association is assumed to be already established (done by f_TC_exec_pfcp_hb). */ +function f_TC_pfcp_heartbeat_rts_mismatch(charstring id) runs on ConnHdlr { + var PDU_PFCP pfcp_pdu; + timer T; + + if (mp_pfcp_heartbeat_interval == 0.0) { + setverdict(inconc, "periodic HB is disabled, skipped"); + return; + } + + /* Subscribe to bcast so we receive IUT-initiated HB requests */ + f_PFCPEM_subscribe_bcast(); + + /* Wait for the first periodic HB request */ + T.start(mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout + 2.0); + alt { + [] PFCP.receive(tr_PFCP_Heartbeat_Req) -> value pfcp_pdu { + T.stop; + /* Respond with a mismatched RTS to simulate a UPF restart */ + f_pfcp_send_hb_resp_rts(pfcp_pdu, f_PFCPEM_get_recovery_timestamp() + 1); + } + [] T.timeout { + setverdict(fail, "Timeout waiting for periodic PFCP Heartbeat Request"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + + /* IUT should detect the RTS mismatch and reset the association immediately */ + T.start(2.0); + alt { + [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) -> value pfcp_pdu { + T.stop; + /* Verify via REST that the IUT is in connecting state (association was reset) */ + var PfcpAssocInfo assoc := f_REST_PfcpAssocState(); + if (assoc.state != connecting) { + setverdict(fail, "Unexpected PFCP assoc state ", assoc.state); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + /* Complete the new association setup */ + f_PFCPEM_tx_assoc_setup_resp(pfcp_pdu.sequence_number); + /* Allow the IUT to process the response, then verify it re-associated */ + f_sleep(0.5); + assoc := f_REST_PfcpAssocState(); + if (assoc.state != connected) { + setverdict(fail, "Unexpected PFCP assoc state ", assoc.state); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + [] PFCP.receive(tr_PFCP_Heartbeat_Req) { + repeat; /* discard any further HBs while waiting for the assoc reset */ + } + [] T.timeout { + setverdict(fail, "Timeout: IUT did not reset PFCP association after RTS mismatch"); + Misc_Helpers.f_shutdown(__BFILE__, __LINE__); + } + } + + f_PFCPEM_unsubscribe_bcast(); + setverdict(pass); +} +testcase TC_pfcp_heartbeat_rts_mismatch() runs on test_CT { + var float Tval := mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout + 20.0; + f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_rts_mismatch), Tval := Tval); +} + /* MME pool test: eNB connects, the first MME rejects, the second one accepts */ function f_TC_mme_pool_reject_fallback(charstring id) runs on ConnHdlr { f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip); @@ -1410,6 +1658,10 @@ execute( TC_handover_res_alloc() ); execute( TC_handover_res_alloc_fail() ); execute( TC_pfcp_heartbeat() ); + execute( TC_pfcp_heartbeat_periodic() ); + execute( TC_pfcp_heartbeat_miss_threshold() ); + execute( TC_pfcp_heartbeat_miss_reset() ); + execute( TC_pfcp_heartbeat_rts_mismatch() ); execute( TC_mme_pool_reject_fallback() ); execute( TC_mme_pool_timeout_fallback() ); execute( TC_mme_pool_all_reject() ); diff --git a/s1gw/expected-results.xml b/s1gw/expected-results.xml index b2b6cdb..d5fa997 100644 --- a/s1gw/expected-results.xml +++ b/s1gw/expected-results.xml @@ -1,5 +1,5 @@ <?xml version="1.0"?> -<testsuite name='S1GW_Tests' tests='41' failures='0' errors='0' skipped='0' inconc='0' time='MASKED'> +<testsuite name='S1GW_Tests' tests='45' failures='0' errors='0' skipped='0' inconc='0' time='MASKED'> <testcase classname='S1GW_Tests' name='TC_setup' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_setup_multi' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_conn_term_by_mme' time='MASKED'/> @@ -35,6 +35,10 @@ <testcase classname='S1GW_Tests' name='TC_handover_res_alloc' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_handover_res_alloc_fail' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat' time='MASKED'/> + <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_periodic' time='MASKED'/> + <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_miss_threshold' time='MASKED'/> + <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_miss_reset' time='MASKED'/> + <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_rts_mismatch' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_mme_pool_reject_fallback' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_mme_pool_timeout_fallback' time='MASKED'/> <testcase classname='S1GW_Tests' name='TC_mme_pool_all_reject' time='MASKED'/> diff --git a/s1gw/osmo-s1gw.config b/s1gw/osmo-s1gw.config index 5788446..f7fad64 100644 --- a/s1gw/osmo-s1gw.config +++ b/s1gw/osmo-s1gw.config @@ -30,6 +30,14 @@ #{name => "mme1", laddr => "127.0.2.1", raddr => "127.0.2.11"}, #{name => "mme2", laddr => "127.0.2.1", raddr => "127.0.2.12"} ]}, + {pfcp_peer, #{ + laddr => "127.0.3.1", + raddr => "127.0.3.10", + heartbeat_interval => 4_000, + heartbeat_req_timeout => 2_000, + heartbeat_miss_count => 3 + }}, + %% XXX: pfcp_{loc,rem}_addr kept for compatibility with the -latest (<= 0.4.0) {pfcp_loc_addr, "127.0.3.1"}, %% local address for incoming PFCP PDUs from the UPF {pfcp_rem_addr, "127.0.3.10"} %% remote address for outgoing PFCP PDUs to the UPF ]},