fixeria has uploaded this change for review.
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
]},
To view, visit change 42495. To unsubscribe, or for help writing mail filters, visit settings.