module UPF_Tests { /* Integration Tests for OsmoUPF * (C) 2022 by sysmocom - s.f.m.c. GmbH * 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 * * This test suite acts as a PFCP Control Plane Function to test OsmoUPF. */ import from Misc_Helpers all; import from General_Types all; import from Osmocom_Types all; import from IPL4asp_Types all; import from Native_Functions all; import from TCCConversion_Functions all; import from Osmocom_CTRL_Functions all; import from Osmocom_CTRL_Types all; import from Osmocom_CTRL_Adapter all; import from StatsD_Types all; import from StatsD_CodecPort all; import from StatsD_CodecPort_CtrlFunct all; import from StatsD_Checker all; import from Osmocom_VTY_Functions all; import from TELNETasp_PortType all; import from CPF_ConnectionHandler all; import from PFCP_Types all; import from PFCP_Emulation all; import from PFCP_Templates all; modulepar { /* IP address at which the UPF can be reached */ charstring mp_pfcp_ip_upf := "127.0.0.1"; charstring mp_pfcp_ip_local := "127.0.0.2"; charstring mp_netinst_access_ip_1 := "127.0.1.1"; charstring mp_netinst_access_ip_2 := "127.0.1.2"; charstring mp_netinst_core_ip_1 := "127.0.2.1"; charstring mp_netinst_core_ip_2 := "127.0.2.2"; /* When testing with gtp mockup, actions will not show. */ boolean mp_verify_gtp_actions := false; } type component test_CT extends CTRL_Adapter_CT { port TELNETasp_PT UPFVTY; /* global test case guard timer (actual timeout value is set in f_init()) */ timer T_guard := 15.0; } /* global altstep for global guard timer; */ altstep as_Tguard() runs on test_CT { [] T_guard.timeout { setverdict(fail, "Timeout of T_guard"); mtc.stop; } } private function f_get_name_val(out charstring val, charstring str, charstring name, charstring sep := ":", charstring delim := " ") return boolean { var charstring labl := name & sep; var integer namepos := f_strstr(str, labl); if (namepos < 0) { return false; } var integer valpos := namepos + lengthof(labl); var integer valend := f_strstr(str, delim, valpos); if (valend < 0) { valend := lengthof(str); } val := substr(str, valpos, valend - valpos); return true; } private function f_get_name_val_oct8(out OCT8 val, charstring str, charstring name) return boolean { var charstring token; if (not f_get_name_val(token, str, name, ":0x")) { return false; } if (lengthof(token) > 16) { log("token too long: ", name, " in ", str); return false; } var charstring padded := substr("0000000000000000", 0, 16 - lengthof(token)) & token; val := str2oct(padded); return true; } private function f_get_name_val_oct4(out OCT4 val, charstring str, charstring name) return boolean { var charstring token; if (not f_get_name_val(token, str, name, ":0x")) { return false; } if (lengthof(token) > 8) { log("token too long: ", name, " in ", str); return false; } var charstring padded := substr("00000000", 0, 8 - lengthof(token)) & token; val := str2oct(padded); return true; } private function f_get_name_val_int(out integer val, charstring str, charstring name) return boolean { var charstring token; if (not f_get_name_val(token, str, name)) { return false; } val := str2int(token); return true; } private function f_get_name_val_2int(out integer val1, out integer val2, charstring str, charstring name, charstring delim := ",") return boolean { var charstring token; if (not f_get_name_val(token, str, name)) { return false; } var Misc_Helpers.ro_charstring nrl := f_str_split(token, delim); if (lengthof(nrl) != 2) { return false; } val1 := str2int(nrl[0]); val2 := str2int(nrl[1]); return true; } /* A PFCP session as seen by the system under test, osmo-upf. up_seid is what osmo-upf sees as its local SEID * ("SEID-l"). cp_seid is this tester's side's SEID, which osmo-upf sees as the remote SEID. */ type record PFCP_session { OCT8 up_seid, OCT8 cp_seid, GTP_Action gtp } /* _r and _l means 'remote' and 'local', from the perspective of the osmo-upf process. */ type record GTP_Action_tunend { /* the PDR Id detecting packets from this side */ integer pdr_id, /* IP packets arriving from this side are arriving on ip_l */ charstring ip_l, /* the FAR Id forwarding packets to this side */ integer far_id } /* _r and _l means 'remote' and 'local', from the perspective of the osmo-upf process. */ type record GTP_Action_tun { /* the PDR Id detecting packets from this side */ integer pdr_id, /* GTP arriving from this side is arriving on gtp_ip_l with teid_l */ charstring gtp_ip_l, OCT4 teid_l, /* the FAR Id forwarding packets to this side */ integer far_id, /* GTP going out to this side should be sent to gtp_ip_r with teid_r */ charstring gtp_ip_r, OCT4 teid_r } type union GTP_Action_core { /* For kind = "tunend", the local IP that the UE has on 'the internet' */ GTP_Action_tunend tunend, /* For kind = "tunmap", the second GTP tunnel */ GTP_Action_tun tunmap } /* State of what GTP functionality osmo-upf should put in place, after a PFCP request was ACKed by it. * _r and _l means 'remote' and 'local', from the perspective of the osmo-upf process. * * tunend: * Access UPF Core * GTP-r:127.0.0.2,0x1 <-FAR-1-- | 192.168.0.1 <-PDR-1-- * --PDR-2-> GTP-l:127.0.0.1,0x2 | --FAR-2-> (IP destination is in each GTP payload) * * tunmap: * Access UPF Core * GTP-r:127.0.0.2,0x1 <-FAR-1-- | 127.0.0.1,0x1 <-PDR-1-- * --PDR-2-> GTP-l:127.0.0.1,0x2 | --FAR-2-> GTP-r:127.0.0.3,0x2 */ type record GTP_Action { /* kind = ("tunend"|"tunmap") */ charstring kind, /* The access side GTP tunnel. (The 'Access' side is always GTP.) */ GTP_Action_tun access, /* The core side GTP tunnel (tunmap) or local IP (tunend) */ GTP_Action_core core, /* Reference to the PFCP Session that created this GTP action: PFCP session's F-SEID as seen from osmo-upf */ charstring pfcp_peer, OCT8 seid_l }; type record of GTP_Action GTP_Action_List; private function f_parse_gtp_action(out GTP_Action ret, charstring str) return boolean { /* Parse a string like * "GTP:tunend GTP-access-r:127.0.0.2 TEID-access-r:0x94f0001 TEID-access-l:0x1 IP-core-l:192.168.44.241 PFCP-peer:127.0.0.2 SEID-l:0x1 PDR:1,2" */ var GTP_Action a; if (not f_get_name_val(a.kind, str, "GTP")) { return false; } if (not f_get_name_val(a.access.gtp_ip_r, str, "GTP-access-r")) { return false; } if (not f_get_name_val_oct4(a.access.teid_r, str, "TEID-access-r")) { return false; } if (not f_get_name_val(a.access.gtp_ip_l, str, "GTP-access-l")) { return false; } if (not f_get_name_val_oct4(a.access.teid_l, str, "TEID-access-l")) { return false; } if (not f_get_name_val_int(a.access.pdr_id, str, "PDR-access")) { return false; } if (not f_get_name_val(a.pfcp_peer, str, "PFCP-peer")) { return false; } if (not f_get_name_val_oct8(a.seid_l, str, "SEID-l")) { return false; } if (a.kind == "tunend") { if (not f_get_name_val_int(a.core.tunend.pdr_id, str, "PDR-core")) { return false; } if (not f_get_name_val(a.core.tunend.ip_l, str, "IP-core-l")) { return false; } /* in these tests, the PDR Id and its FAR Id are always the same: PDR for incoming on Access matches its * FAR that forwards to Core. */ a.core.tunend.far_id := a.access.pdr_id; a.access.far_id := a.core.tunend.pdr_id; } else if (a.kind == "tunmap") { if (not f_get_name_val(a.core.tunmap.gtp_ip_r, str, "GTP-core-r")) { return false; } if (not f_get_name_val_oct4(a.core.tunmap.teid_r, str, "TEID-core-r")) { return false; } if (not f_get_name_val(a.core.tunmap.gtp_ip_l, str, "GTP-core-l")) { return false; } if (not f_get_name_val_oct4(a.core.tunmap.teid_l, str, "TEID-core-l")) { return false; } if (not f_get_name_val_int(a.core.tunmap.pdr_id, str, "PDR-core")) { return false; } /* in these tests, the PDR Id and its FAR Id are always the same: PDR for incoming on Access matches its * FAR that forwards to Core. */ a.core.tunmap.far_id := a.access.pdr_id; a.access.far_id := a.core.tunmap.pdr_id; } ret := a; return true; } private function f_vty_get_gtp_actions(TELNETasp_PT vty_pt) return GTP_Action_List { var charstring gtp_str := f_vty_transceive_ret(vty_pt, "show gtp"); var Misc_Helpers.ro_charstring lines := f_str_split(gtp_str, "\n"); var GTP_Action_List gtps := {}; for (var integer i := 0; i < lengthof(lines); i := i + 1) { var charstring line := lines[i]; var GTP_Action a; if (not f_parse_gtp_action(a, line)) { continue; } gtps := gtps & { a }; } log("GTP-actions: ", gtps); return gtps; } private function f_find_gtp_action(GTP_Action_List actions, template GTP_Action find) return boolean { for (var integer i := 0; i < lengthof(actions); i := i + 1) { if (match(actions[i], find)) { return true; } } return false; } private function f_expect_gtp_action(GTP_Action_List actions, template GTP_Action expect) { if (f_find_gtp_action(actions, expect)) { log("VTY confirms: GTP action active: ", expect); setverdict(pass); return; } log("Expected to find ", expect, " in ", actions); setverdict(fail, "on VTY, a GTP action failed to show as active"); mtc.stop; } private function f_expect_no_gtp_action(GTP_Action_List actions, template GTP_Action expect) { if (f_find_gtp_action(actions, expect)) { log("Expected to *not* find ", expect, " in ", actions); setverdict(fail, "a GTP action failed to show as inactive"); mtc.stop; } log("VTY confirms: GTP action inactive: ", expect); setverdict(pass); return; } private function f_vty_expect_gtp_action(TELNETasp_PT vty_pt, template GTP_Action expect) { if (not mp_verify_gtp_actions) { /* In GTP mockup mode, GTP actions don't show on VTY. Cannot verify. */ setverdict(pass); return; } var GTP_Action_List actions := f_vty_get_gtp_actions(vty_pt); f_expect_gtp_action(actions, expect); } private function f_vty_expect_no_gtp_actions(TELNETasp_PT vty_pt) { var GTP_Action_List actions := f_vty_get_gtp_actions(vty_pt); if (lengthof(actions) > 0) { setverdict(fail, "VTY says that there are still active GTP actions"); mtc.stop; } setverdict(pass); } type record PFCP_Session_Status { charstring peer, OCT8 seid_r, OCT8 seid_l, charstring state, integer pdr_active_count, integer pdr_count, integer far_active_count, integer far_count, integer gtp_active_count }; template PFCP_Session_Status PFCP_session_active := { peer := ?, seid_r := ?, seid_l := ?, state := "ESTABLISHED", pdr_active_count := (1..99999), pdr_count := (1..99999), far_active_count := (1..99999), far_count := (1..99999), gtp_active_count := (1..99999) }; template PFCP_Session_Status PFCP_session_inactive := { peer := ?, seid_r := ?, seid_l := ?, state := "ESTABLISHED", pdr_active_count := 0, pdr_count := (1..99999), far_active_count := 0, far_count := (1..99999), gtp_active_count := 0 }; type record of PFCP_Session_Status PFCP_Session_Status_List; private function f_parse_session_status(out PFCP_Session_Status ret, charstring str) return boolean { var PFCP_Session_Status st; if (not f_get_name_val(st.peer, str, "peer")) { return false; } if (not f_get_name_val_oct8(st.seid_l, str, "SEID-l")) { return false; } f_get_name_val_oct8(st.seid_r, str, "SEID-r"); f_get_name_val(st.state, str, "state"); /* parse 'PDR-active:1/2' */ if (not f_get_name_val_2int(st.pdr_active_count, st.pdr_count, str, "PDR-active", "/")) { return false; } /* parse 'FAR-active:1/2' */ if (not f_get_name_val_2int(st.far_active_count, st.far_count, str, "FAR-active", "/")) { return false; } f_get_name_val_int(st.gtp_active_count, str, "GTP-active"); ret := st; return true; } private function f_vty_get_sessions(TELNETasp_PT vty_pt) return PFCP_Session_Status_List { var charstring sessions_str := f_vty_transceive_ret(vty_pt, "show session"); var Misc_Helpers.ro_charstring lines := f_str_split(sessions_str, "\n"); var PFCP_Session_Status_List sessions := {}; for (var integer i := 0; i < lengthof(lines); i := i + 1) { var charstring line := lines[i]; var PFCP_Session_Status st; if (not f_parse_session_status(st, line)) { continue; } sessions := sessions & { st }; } log("Sessions: ", sessions); return sessions; } private function f_vty_get_session_status(TELNETasp_PT vty_pt, PFCP_session s, out PFCP_Session_Status ret) return boolean { var PFCP_Session_Status_List sessions := f_vty_get_sessions(vty_pt); return f_get_session_status(sessions, s, ret); } private function f_get_session_status(PFCP_Session_Status_List sessions, PFCP_session s, out PFCP_Session_Status ret) return boolean { var PFCP_Session_Status_List matches := {}; for (var integer i := 0; i < lengthof(sessions); i := i + 1) { var PFCP_Session_Status st := sessions[i]; if (st.seid_l != s.up_seid) { continue; } if (st.seid_r != s.cp_seid) { continue; } matches := matches & { st }; } if (lengthof(matches) < 1) { log("no session with SEID-l = ", s.up_seid); return false; } if (lengthof(matches) > 1) { log("multiple sessions have ", s, ": ", matches); return false; } ret := matches[0]; return true; } private function f_vty_expect_session_status(TELNETasp_PT vty_pt, PFCP_session s, template PFCP_Session_Status expect_st) { var PFCP_Session_Status st; if (not f_vty_get_session_status(vty_pt, s, st)) { log("Session ", s, " not found in VTY session list"); setverdict(fail, "Session not found in VTY list"); mtc.stop; } log("Session ", s, " status: ", st); if (not match(st, expect_st)) { log("ERROR: Session ", st, " does not match ", expect_st); setverdict(fail, "VTY shows unexpected state of PFCP session"); mtc.stop; } setverdict(pass); } private function f_vty_expect_session_active(TELNETasp_PT vty_pt, PFCP_session s) { f_vty_expect_session_status(vty_pt, s, PFCP_session_active); f_vty_expect_gtp_action(vty_pt, s.gtp); setverdict(pass); } private function f_vty_expect_no_active_sessions(TELNETasp_PT vty_pt) { var PFCP_Session_Status_List stl := f_vty_get_sessions(vty_pt); var integer active := 0; for (var integer i := 0; i < lengthof(stl); i := i + 1) { if (match(stl[i], PFCP_session_active)) { log("Active session: ", stl[i]); active := active + 1; } } if (active > 0) { setverdict(fail, "There are still active sessions"); mtc.stop; } setverdict(pass); } private function f_vty_netinst_cfg(TELNETasp_PT vty_pt, Misc_Helpers.ro_charstring netinst_cmds) { f_vty_enter_config(vty_pt); f_vty_transceive(vty_pt, "netinst"); for (var integer i := 0; i < lengthof(netinst_cmds); i := i + 1) { f_vty_transceive(vty_pt, netinst_cmds[i]); } f_vty_transceive_ret(vty_pt, "end"); } function f_init_vty(charstring id := "foo") runs on test_CT { if (UPFVTY.checkstate("Mapped")) { /* skip initialization if already executed once */ return; } map(self:UPFVTY, system:UPFVTY); f_vty_set_prompts(UPFVTY); f_vty_transceive(UPFVTY, "enable"); } /* global initialization function */ function f_init(float guard_timeout := 30.0) runs on test_CT { var integer bssap_idx; T_guard.start(guard_timeout); activate(as_Tguard()); f_init_vty("VirtCPF"); /* Clear out and set up default network instance config */ f_vty_netinst_cfg(UPFVTY, { "clear", "add access " & mp_netinst_access_ip_1, "add access2 " & mp_netinst_access_ip_2, "add core " & mp_netinst_core_ip_1, "add core2 " & mp_netinst_core_ip_2 }); } friend function f_shutdown_helper() runs on test_CT { all component.stop; setverdict(pass); mtc.stop; } private function f_gen_test_hdlr_pars() runs on test_CT return TestHdlrParams { var TestHdlrParams pars := valueof(t_def_TestHdlrPars); pars.remote_upf_addr := mp_pfcp_ip_upf; pars.local_addr := mp_pfcp_ip_local; pars.local_node_id := valueof(ts_PFCP_Node_ID_ipv4(f_inet_addr(mp_pfcp_ip_local))); return pars; } type function void_fn(charstring id) runs on CPF_ConnHdlr; function f_start_handler_create(TestHdlrParams pars) runs on test_CT return CPF_ConnHdlr { var charstring id := testcasename(); var CPF_ConnHdlr vc_conn; vc_conn := CPF_ConnHdlr.create(id); return vc_conn; } function f_start_handler_run(CPF_ConnHdlr vc_conn, void_fn fn, TestHdlrParams pars) runs on test_CT return CPF_ConnHdlr { var charstring id := testcasename(); /* Emit a marker to appear in the SUT's own logging output */ f_logp(UPFVTY, id & "() start"); vc_conn.start(f_handler_init(fn, id, pars)); return vc_conn; } function f_start_handler(void_fn fn, template (omit) TestHdlrParams pars_tmpl := omit) runs on test_CT return CPF_ConnHdlr { var TestHdlrParams pars; if (isvalue(pars_tmpl)) { pars := valueof(pars_tmpl); } else { pars := valueof(f_gen_test_hdlr_pars()); } return f_start_handler_run(f_start_handler_create(pars), fn, pars); } /* first function inside ConnHdlr component; sets g_pars + starts function */ private function f_handler_init(void_fn fn, charstring id, TestHdlrParams pars) runs on CPF_ConnHdlr { f_CPF_ConnHdlr_init(id, pars); fn.apply(id); } /* Run a PFCP Association procedure */ private function f_assoc_setup() runs on CPF_ConnHdlr { PFCP.send(ts_PFCP_Assoc_Setup_Req(g_pars.local_node_id, g_recovery_timestamp)); PFCP.receive(tr_PFCP_Assoc_Setup_Resp(cause := tr_PFCP_Cause(REQUEST_ACCEPTED))); } /* Release a PFCP Association */ private function f_assoc_release() runs on CPF_ConnHdlr { PFCP.send(ts_PFCP_Assoc_Release_Req(g_pars.local_node_id)); PFCP.receive(tr_PFCP_Assoc_Release_Resp(cause := tr_PFCP_Cause(REQUEST_ACCEPTED))); } /* Collection of what a test intends to send to osmo-upf */ type record PFCP_Ruleset { Create_PDR_list pdr, Create_FAR_list far }; /* Add to r a rule set that does GTP decapsulation (half of encapsulation/decapsulation): * Receive GTP on src_iface = ACCESS by a local F-TEID to be chosen by osmo-upf. * Dispatch GTP payload as plain IP on dest_iface = CORE. */ private function f_ruleset_add_GTP_decaps(inout PFCP_Ruleset r, integer pdr_id, charstring src_netinst, integer far_id) { r.pdr := r.pdr & { valueof( ts_PFCP_Create_PDR( pdr_id, ts_PFCP_PDI( ACCESS, local_F_TEID := ts_PFCP_F_TEID_choose_v4(), network_instance := ts_PFCP_Network_Instance(src_netinst)), ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4), far_id ) ) }; r.far := r.far & { valueof( ts_PFCP_Create_FAR( far_id, ts_PFCP_Apply_Action_FORW, valueof(ts_PFCP_Forwarding_Parameters(CORE)) ) ) }; } /* Add to r a rule set that does GTP encapsulation (half of encapsulation/decapsulation) */ private function f_ruleset_add_GTP_encaps(inout PFCP_Ruleset r, integer pdr_id, charstring ue_addr_v4 := "192.168.23.42", integer far_id, OCT4 remote_teid, OCT4 gtp_dest_addr_v4) { r.pdr := r.pdr & { valueof( ts_PFCP_Create_PDR( pdr_id, ts_PFCP_PDI( CORE, ue_addr_v4 := ts_PFCP_UE_IP_Address_v4(f_inet_addr(ue_addr_v4), is_destination := true) ), far_id := far_id ) ) }; r.far := r.far & { valueof( ts_PFCP_Create_FAR( far_id, ts_PFCP_Apply_Action_FORW, valueof(ts_PFCP_Forwarding_Parameters( ACCESS, ts_PFCP_Outer_Header_Creation_GTP_ipv4( remote_teid, gtp_dest_addr_v4) )) ) ) }; } /* Add to r a rule set that forwards GTP from one tunnel to another, i.e. one direction of a tunmap */ private function f_ruleset_add_GTP_forw(inout PFCP_Ruleset r, integer pdr_id, e_PFCP_Src_Iface src_iface, charstring src_netinst, integer far_id, e_PFCP_Dest_Iface dest_iface, F_TEID dest_remote_f_teid) { r.pdr := r.pdr & { valueof( ts_PFCP_Create_PDR( pdr_id, ts_PFCP_PDI( src_iface, local_F_TEID := ts_PFCP_F_TEID_choose_v4(), network_instance := ts_PFCP_Network_Instance(src_netinst)), ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4), far_id ) ) }; r.far := r.far & { valueof( ts_PFCP_Create_FAR( far_id, ts_PFCP_Apply_Action_FORW, valueof(ts_PFCP_Forwarding_Parameters(dest_iface, ts_PFCP_Outer_Header_Creation_GTP_ipv4(dest_remote_f_teid.teid, dest_remote_f_teid.ipv4_address))) ) ) }; } /* Add to r a DROP rule from src_iface to dest_iface */ private function f_ruleset_add_GTP_drop(inout PFCP_Ruleset r, integer pdr_id, e_PFCP_Src_Iface src_iface, charstring src_netinst, integer far_id, e_PFCP_Dest_Iface dest_iface) { r.pdr := r.pdr & { valueof( ts_PFCP_Create_PDR( pdr_id, ts_PFCP_PDI( src_iface, local_F_TEID := ts_PFCP_F_TEID_choose_v4(), network_instance := ts_PFCP_Network_Instance(src_netinst)), ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4), far_id ) ) }; r.far := r.far & { valueof( ts_PFCP_Create_FAR( far_id, ts_PFCP_Apply_Action_DROP, fp := omit ) ) }; } private function f_tunmap_upd_far_to_core(GTP_Action gtp) return Update_FAR { return valueof( ts_PFCP_Update_FAR( gtp.core.tunmap.far_id, ts_PFCP_Apply_Action_FORW, valueof(ts_PFCP_Update_Forwarding_Parameters( ts_PFCP_Destination_Interface(CORE), ts_PFCP_Outer_Header_Creation_GTP_ipv4(gtp.core.tunmap.teid_r, f_inet_addr(gtp.core.tunmap.gtp_ip_r)) ) ) ) ); } /* Return two PDR+FAR rulesets that involve a src=CP-Function. Such rulesets are emitted by certain third party CPF, and * osmo-upf should ACK the creation but ignore the rules (no-op). This function models rulesets seen in the field, so we * can confirm that osmo-upf ACKs and ignores. */ private function f_ruleset_noop() return PFCP_Ruleset { var PFCP_Ruleset r := { {}, {} }; var integer pdr_id := lengthof(r.pdr) + 1; var integer far_id := lengthof(r.far) + 1; r.pdr := r.pdr & { valueof( ts_PFCP_Create_PDR( pdr_id, ts_PFCP_PDI( CP_FUNCTION, local_F_TEID := ts_PFCP_F_TEID_choose_v4('17'O)), ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4), far_id ) ) }; r.far := r.far & { valueof( ts_PFCP_Create_FAR( far_id, ts_PFCP_Apply_Action_FORW, valueof(ts_PFCP_Forwarding_Parameters(ACCESS)) ) ) }; /* And another one (sic) */ pdr_id := lengthof(r.pdr) + 1; far_id := lengthof(r.far) + 1; r.pdr := r.pdr & { valueof( ts_PFCP_Create_PDR( pdr_id, ts_PFCP_PDI( CP_FUNCTION, local_F_TEID := ts_PFCP_F_TEID_choose_v4('2a'O)), far_id := far_id ) ) }; r.far := r.far & { valueof( ts_PFCP_Create_FAR( far_id, ts_PFCP_Apply_Action_FORW, valueof(ts_PFCP_Forwarding_Parameters(ACCESS)) ) ) }; return r; } /* Return a rule set that does GTP encapsulation and decapsulation, in both directions. */ private function f_ruleset_tunend(GTP_Action gtp, charstring netinst_access := "access") return PFCP_Ruleset { var PFCP_Ruleset rules := { {}, {} }; f_ruleset_add_GTP_decaps(rules, pdr_id := gtp.access.pdr_id, src_netinst := netinst_access, far_id := gtp.core.tunend.far_id); f_ruleset_add_GTP_encaps(rules, gtp.core.tunend.pdr_id, gtp.core.tunend.ip_l, gtp.access.far_id, gtp.access.teid_r, f_inet_addr(gtp.access.gtp_ip_r)); return rules; } /* Return a rule set that does GTP tunnel forwarding in both directions. * If core_gtp_known == true, place full FORW rules in both directions. * If core_gtp_known == false, keep the Core side as DROP: this allows testing the usual/realistic case, where upon * Session Establishment, the core side PGW has not yet provided the destination GTP F-TEID, which will follow later in * a Session Modification. (This test suite has already configured which GTP F-TEID will be used on the core side, but * we're omitting it from Session Establishment, until it is time to use it in f_session_mod()). */ private function f_ruleset_tunmap(GTP_Action gtp, boolean core_gtp_known := true, charstring netinst_access := "access", charstring netinst_core := "core") return PFCP_Ruleset { var PFCP_Ruleset rules := { {}, {} }; /* Access to Core */ if (core_gtp_known) { f_ruleset_add_GTP_forw(rules, pdr_id := gtp.access.pdr_id, src_iface := ACCESS, src_netinst := netinst_access, far_id := gtp.core.tunmap.far_id, dest_iface := CORE, dest_remote_f_teid := valueof(ts_PFCP_F_TEID_ipv4(gtp.core.tunmap.teid_r, f_inet_addr(gtp.core.tunmap.gtp_ip_r)))); } else { /* The Core remote GTP will follow in a Session Modification, for now set Core->Access to DROP */ f_ruleset_add_GTP_drop(rules, pdr_id := gtp.access.pdr_id, src_iface := ACCESS, src_netinst := netinst_access, far_id := gtp.core.tunmap.far_id, dest_iface := CORE); } /* Core to Access */ f_ruleset_add_GTP_forw(rules, pdr_id := gtp.core.tunmap.pdr_id, src_iface := CORE, src_netinst := netinst_core, far_id := gtp.access.far_id, dest_iface := ACCESS, dest_remote_f_teid := valueof(ts_PFCP_F_TEID_ipv4(gtp.access.teid_r, f_inet_addr(gtp.access.gtp_ip_r)))); return rules; } /* From a PFCP Session Establishment Response, retrieve the F_TEID returned in the Created PDR IE for the given PDR Id */ private function f_get_created_local_f_teid(PDU_PFCP sess_est_resp, integer pdr_id) return F_TEID { for (var integer i := 0; i < lengthof(sess_est_resp.message_body.pfcp_session_establishment_response.created_PDR_list); i := i + 1) { var Created_PDR cpdr := sess_est_resp.message_body.pfcp_session_establishment_response.created_PDR_list[i]; if (oct2int(cpdr.grouped_ie.pdr_id.rule_id) != pdr_id) { continue; } log("osmo-upf has chosen local F-TEID: PDR-" & int2str(pdr_id) & " = ", cpdr.grouped_ie.local_F_TEID); return cpdr.grouped_ie.local_F_TEID; } setverdict(fail, "PDR Id " & int2str(pdr_id) & " not found in PFCP message"); mtc.stop; } /* Run a PFCP Session Establishment procedure */ private function f_session_est(inout PFCP_session s, PFCP_Ruleset rules) runs on CPF_ConnHdlr { log("f_session_est: rules = ", rules); PFCP.send(ts_PFCP_Session_Est_Req(ts_PFCP_Node_ID_ipv4(f_inet_addr(g_pars.local_addr)), ts_PFCP_F_SEID_ipv4(f_inet_addr(g_pars.local_addr), s.cp_seid), rules.pdr, rules.far)); var PDU_PFCP pfcp; PFCP.receive(tr_PFCP_Session_Est_Resp(seid := s.cp_seid)) -> value pfcp; s.up_seid := pfcp.message_body.pfcp_session_establishment_response.UP_F_SEID.seid; s.gtp.seid_l := s.up_seid; var F_TEID access_local_f_teid := f_get_created_local_f_teid(pfcp, s.gtp.access.pdr_id); s.gtp.access.gtp_ip_l := f_inet_ntoa(access_local_f_teid.ipv4_address); s.gtp.access.teid_l := access_local_f_teid.teid; if (ischosen(s.gtp.core.tunmap)) { var F_TEID core_local_f_teid := f_get_created_local_f_teid(pfcp, s.gtp.core.tunmap.pdr_id); s.gtp.core.tunmap.gtp_ip_l := f_inet_ntoa(core_local_f_teid.ipv4_address); s.gtp.core.tunmap.teid_l := core_local_f_teid.teid; } log("established PFCP session: ", s); } /* Run a PFCP Session Modification procedure */ private function f_session_mod(inout PFCP_session s) runs on CPF_ConnHdlr { PFCP.send(ts_PFCP_Session_Mod_Req(s.up_seid, f_tunmap_upd_far_to_core(s.gtp))); PFCP.receive(tr_PFCP_Session_Mod_Resp(s.cp_seid)); log("modified PFCP session: ", s); } private function f_create_PFCP_session_tunend() runs on CPF_ConnHdlr return PFCP_session { var PFCP_session s := { up_seid := -, cp_seid := f_next_seid(), gtp := { kind := "tunend", access := { pdr_id := 1, /* gtp_ip_l and teid_l will be returned by Session Establishment Response, Created PDR */ gtp_ip_l := "", teid_l := '00000000'O, far_id := 2, gtp_ip_r := "127.0.0.2", teid_r := f_next_remote_teid() }, core := { tunend := { pdr_id := 2, ip_l := f_next_ue_addr(), far_id := 1 } }, pfcp_peer := g_pars.local_addr, seid_l := '0000000000000000'O /* seid_l will be returned by Session Establishment Response */ } }; return s; } private function f_create_PFCP_session_tunmap() runs on CPF_ConnHdlr return PFCP_session { var PFCP_session s := { up_seid := -, cp_seid := f_next_seid(), gtp := { kind := "tunmap", access := { pdr_id := 1, /* gtp_ip_l and teid_l will be returned by Session Establishment Response, Created PDR */ gtp_ip_l := "", teid_l := '00000000'O, far_id := 2, gtp_ip_r := "127.0.0.2", teid_r := f_next_remote_teid() }, core := { tunmap := { pdr_id := 2, /* gtp_ip_l and teid_l will be returned by Session Establishment Response, Created PDR */ gtp_ip_l := "", teid_l := '00000000'O, far_id := 1, gtp_ip_r := "127.0.0.3", teid_r := f_next_remote_teid() } }, pfcp_peer := g_pars.local_addr, seid_l := '0000000000000000'O /* seid_l will be returned by Session Establishment Response */ } }; return s; } /* Do a PFCP Session Establishment with default values (see f_create_PFCP_session_tunend()) */ private function f_session_est_tunend() runs on CPF_ConnHdlr return PFCP_session { var PFCP_session s := f_create_PFCP_session_tunend(); f_session_est(s, f_ruleset_tunend(s.gtp)); return s; } private function f_session_del(PFCP_session s) runs on CPF_ConnHdlr { PFCP.send(ts_PFCP_Session_Del_Req(s.up_seid)); PFCP.receive(tr_PFCP_Session_Del_Resp(s.cp_seid)); } private function f_tc_assoc(charstring id) runs on CPF_ConnHdlr { f_assoc_setup(); f_assoc_release(); setverdict(pass); } /* Verify that the CPF can send a Node-ID of the IPv4 type */ testcase TC_assoc_node_id_v4() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 5.0); vc_conn := f_start_handler(refers(f_tc_assoc)); vc_conn.done; f_shutdown_helper(); } /* Verify that the CPF can send a Node-ID of the FQDN type */ testcase TC_assoc_node_id_fqdn() runs on test_CT { var CPF_ConnHdlr vc_conn; var TestHdlrParams pars := f_gen_test_hdlr_pars(); pars.local_node_id := valueof(ts_PFCP_Node_ID_fqdn("\7example\3com")); f_init(guard_timeout := 5.0); vc_conn := f_start_handler(refers(f_tc_assoc), pars); vc_conn.done; f_shutdown_helper(); } /* Verify PFCP Session Establishment and Deletion */ private function f_tc_session_est_tunend(charstring id) runs on CPF_ConnHdlr { f_assoc_setup(); var PFCP_session s := f_session_est_tunend(); f_sleep(1.0); f_vty_expect_session_active(UPFVTY, s); f_session_del(s); f_vty_expect_no_active_sessions(UPFVTY); f_vty_expect_no_gtp_actions(UPFVTY); f_assoc_release(); setverdict(pass); } testcase TC_session_est_tunend() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 15.0); vc_conn := f_start_handler(refers(f_tc_session_est_tunend)); vc_conn.done; f_shutdown_helper(); } /* Verify that releasing a PFCP Association also releases all its sessions and GTP actions. */ private function f_tc_session_term_by_assoc_rel(charstring id) runs on CPF_ConnHdlr { f_assoc_setup(); var PFCP_session s := f_session_est_tunend(); f_sleep(1.0); f_vty_expect_session_active(UPFVTY, s); f_assoc_release(); f_vty_expect_no_active_sessions(UPFVTY); f_vty_expect_no_gtp_actions(UPFVTY); setverdict(pass); } testcase TC_session_term_by_assoc_rel() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 15.0); vc_conn := f_start_handler(refers(f_tc_session_term_by_assoc_rel)); vc_conn.done; f_shutdown_helper(); } /* Verify that PFCP Sessions with a src-interface other than ACCESS or CORE are ACKed by osmo-upf but have no effect. */ private function f_tc_session_est_noop(charstring id) runs on CPF_ConnHdlr { f_assoc_setup(); var PFCP_session s := f_create_PFCP_session_tunend(); f_session_est(s, f_ruleset_noop()); f_sleep(1.0); f_vty_expect_session_status(UPFVTY, s, PFCP_session_inactive); f_session_del(s); f_vty_expect_no_active_sessions(UPFVTY); f_vty_expect_no_gtp_actions(UPFVTY); f_assoc_release(); setverdict(pass); } testcase TC_session_est_noop() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 15.0); vc_conn := f_start_handler(refers(f_tc_session_est_noop)); vc_conn.done; f_shutdown_helper(); } /* Verify that the Network Instance IE in Create PDR chooses the right local address for a tunmap session */ private function f_tc_session_est_tunmap(charstring id) runs on CPF_ConnHdlr { f_assoc_setup(); var PFCP_session s := f_create_PFCP_session_tunmap(); f_session_est(s, f_ruleset_tunmap(s.gtp)); f_sleep(1.0); f_vty_expect_session_active(UPFVTY, s); f_session_del(s); f_vty_expect_no_active_sessions(UPFVTY); f_vty_expect_no_gtp_actions(UPFVTY); f_assoc_release(); setverdict(pass); } testcase TC_session_est_tunmap() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 15.0); vc_conn := f_start_handler(refers(f_tc_session_est_tunmap)); vc_conn.done; f_shutdown_helper(); } /* Set up a tunmap session with a partial Session Establishment, followed by a Session Modification to complete it. */ private function f_session_est_mod_tunmap(charstring netinst_access, charstring expect_gtp_ip_access, charstring netinst_core, charstring expect_gtp_ip_core) runs on CPF_ConnHdlr { f_assoc_setup(); var PFCP_session s := f_create_PFCP_session_tunmap(); f_session_est(s, f_ruleset_tunmap(s.gtp, core_gtp_known := false, netinst_access := netinst_access, netinst_core := netinst_core)); /* The locally chosen GTP IP addresses where osmo-upf receives GTP traffic were chosen by netinst_access / * netinst_core and are returned in s.gtp.access.gtp_ip_l / s.gtp.core.tunmap.gtp_ip_l. Verify that the netinst * names have returned their matching IP addresses. */ if (s.gtp.access.gtp_ip_l != expect_gtp_ip_access) { setverdict(fail, "Network Instance '" & netinst_access & "' should have yielded GTP IP " & expect_gtp_ip_access & " but osmo-upf chose " & s.gtp.access.gtp_ip_l); mtc.stop; } if (s.gtp.core.tunmap.gtp_ip_l != expect_gtp_ip_core) { setverdict(fail, "Network Instance '" & netinst_core & "' should have yielded GTP IP " & expect_gtp_ip_core & " but osmo-upf chose " & s.gtp.core.tunmap.gtp_ip_l); mtc.stop; } f_sleep(1.0); f_vty_expect_session_status(UPFVTY, s, PFCP_session_inactive); f_session_mod(s); f_sleep(1.0); f_vty_expect_session_active(UPFVTY, s); f_session_del(s); f_vty_expect_no_active_sessions(UPFVTY); f_vty_expect_no_gtp_actions(UPFVTY); f_assoc_release(); setverdict(pass); } /* Run f_session_est_mod_tunmap() with the first Network Instances */ private function f_tc_session_est_mod_tunmap(charstring id) runs on CPF_ConnHdlr { f_session_est_mod_tunmap("access", mp_netinst_access_ip_1, "core", mp_netinst_core_ip_1); } /* Run f_session_est_mod_tunmap() with the second Network Instances */ private function f_tc_session_est_mod_tunmap2(charstring id) runs on CPF_ConnHdlr { f_session_est_mod_tunmap("access2", mp_netinst_access_ip_2, "core2", mp_netinst_core_ip_2); } testcase TC_session_est_mod_tunmap() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 15.0); vc_conn := f_start_handler(refers(f_tc_session_est_mod_tunmap)); vc_conn.done; f_shutdown_helper(); } testcase TC_session_est_mod_tunmap2() runs on test_CT { var CPF_ConnHdlr vc_conn; f_init(guard_timeout := 15.0); vc_conn := f_start_handler(refers(f_tc_session_est_mod_tunmap2)); vc_conn.done; f_shutdown_helper(); } control { execute( TC_assoc_node_id_v4() ); execute( TC_assoc_node_id_fqdn() ); execute( TC_session_est_tunend() ); execute( TC_session_term_by_assoc_rel() ); execute( TC_session_est_noop() ); execute( TC_session_est_tunmap() ); execute( TC_session_est_mod_tunmap() ); execute( TC_session_est_mod_tunmap2() ); } }