From 2d29274f3227ada3c4cafc43cb2d7a0f6834829c Mon Sep 17 00:00:00 2001 From: Neels Hofmeyr Date: Tue, 7 Jun 2022 23:58:05 +0200 Subject: [PATCH] add upf/ to test osmo-upf So far testing only PFCP interaction without real GTP traffic. Related: SYS#5599 Change-Id: If67819dea785597841f21d8c444cb4866cfde571 --- Makefile | 1 + upf/CPF_ConnectionHandler.ttcn | 123 ++++++ upf/README.md | 21 + upf/README.txt | 4 + upf/UPF_Tests.cfg | 19 + upf/UPF_Tests.default | 28 ++ upf/UPF_Tests.ttcn | 785 +++++++++++++++++++++++++++++++++ upf/expected-results.xml | 4 + upf/gen_links.sh | 36 ++ upf/osmo-upf.cfg | 16 + upf/regen_makefile.sh | 24 + 11 files changed, 1061 insertions(+) create mode 100644 upf/CPF_ConnectionHandler.ttcn create mode 100644 upf/README.md create mode 100644 upf/README.txt create mode 100644 upf/UPF_Tests.cfg create mode 100644 upf/UPF_Tests.default create mode 100644 upf/UPF_Tests.ttcn create mode 100644 upf/expected-results.xml create mode 100755 upf/gen_links.sh create mode 100644 upf/osmo-upf.cfg create mode 100755 upf/regen_makefile.sh diff --git a/Makefile b/Makefile index fbd380cbc..4b6a5e235 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ SUBDIRS= \ smlc \ stp \ sysinfo \ + upf \ $(NULL) NPROC=$(shell nproc 2>/dev/null) diff --git a/upf/CPF_ConnectionHandler.ttcn b/upf/CPF_ConnectionHandler.ttcn new file mode 100644 index 000000000..25f99b947 --- /dev/null +++ b/upf/CPF_ConnectionHandler.ttcn @@ -0,0 +1,123 @@ +module CPF_ConnectionHandler { + +/* CPF Connection Handler of UPF_Tests in TTCN-3 + * (C) 2022 by sysmocom - s.m.f.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 + */ + +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 StatsD_Checker all; + +import from TELNETasp_PortType all; +import from Osmocom_VTY_Functions all; + +import from PFCP_Types all; +import from PFCP_Emulation all; + +/* The system under test is a UPF (User Plane Function). This component represents a Control Plane Function, with a + * single association via PFCP to the UPF (User Plane Function). */ +type component CPF_ConnHdlr extends StatsD_ConnHdlr { + port PFCPEM_PT PFCP; + port TELNETasp_PT UPFVTY; + + var PFCP_Emulation_CT vc_PFCP; + + var TestHdlrParams g_pars; + + var boolean g_vty_initialized := false; + var integer g_recovery_timestamp; + var integer g_next_seid_state; + var integer g_next_local_teid_state; + var integer g_next_remote_teid_state; + var integer g_next_ue_addr_state; +} + +function f_next_seid() runs on CPF_ConnHdlr return OCT8 { + g_next_seid_state := g_next_seid_state + 1; + return int2oct(g_next_seid_state, 8); +} + +function f_next_local_teid() runs on CPF_ConnHdlr return OCT4 { + g_next_local_teid_state := g_next_local_teid_state + 1; + return int2oct(g_next_local_teid_state, 4); +} + +function f_next_remote_teid() runs on CPF_ConnHdlr return OCT4 { + g_next_remote_teid_state := g_next_remote_teid_state + 1; + return int2oct(g_next_remote_teid_state, 4); +} + +function f_next_ue_addr() runs on CPF_ConnHdlr return charstring { + g_next_ue_addr_state := g_next_ue_addr_state + 1; + if (g_next_ue_addr_state > 254) { + g_next_ue_addr_state := 16; + } + return "192.168.44." & int2str(g_next_ue_addr_state); +} + +function f_CPF_ConnHdlr_init_vty() runs on CPF_ConnHdlr { + if (not g_vty_initialized) { + map(self:UPFVTY, system:UPFVTY); + f_vty_set_prompts(UPFVTY); + f_vty_transceive(UPFVTY, "enable"); + g_vty_initialized := true; + } +} + +private function f_CPF_ConnHdlr_init_pfcp(charstring id) runs on CPF_ConnHdlr { + id := id & "-PFCP"; + + g_recovery_timestamp := f_rnd_int(4294967295); + g_next_seid_state := (1 + f_rnd_int(65534)) * 65536; + g_next_local_teid_state := (1 + f_rnd_int(65534)) * 65536; + g_next_remote_teid_state := (1 + f_rnd_int(65534)) * 65536; + g_next_ue_addr_state := (1 + f_rnd_int(15)) * 16; + + var PFCP_Emulation_Cfg pfcp_cfg := { + pfcp_bind_ip := g_pars.local_addr, + pfcp_bind_port := g_pars.local_port, + pfcp_remote_ip := g_pars.remote_upf_addr, + pfcp_remote_port := g_pars.remote_upf_port, + role := CPF + }; + + vc_PFCP := PFCP_Emulation_CT.create(id) alive; + connect(self:PFCP, vc_PFCP:CLIENT); + vc_PFCP.start(PFCP_Emulation.main(pfcp_cfg)); +} + +/* initialize all parameters */ +function f_CPF_ConnHdlr_init(charstring id, TestHdlrParams pars) runs on CPF_ConnHdlr { + g_pars := valueof(pars); + f_CPF_ConnHdlr_init_pfcp(id); + f_CPF_ConnHdlr_init_vty(); +} + +type record TestHdlrParams { + charstring remote_upf_addr, + integer remote_upf_port, + charstring local_addr, + integer local_port, + Node_ID local_node_id +}; + +/* Note: Do not use valueof() to get a value of this template, use + * f_gen_test_hdlr_pars() instead in order to get a configuration. */ +template (value) TestHdlrParams t_def_TestHdlrPars := { + remote_upf_addr := "127.0.0.1", + remote_upf_port := 8805, + local_addr := "127.0.0.2", + local_port := 8805 +} + +} diff --git a/upf/README.md b/upf/README.md new file mode 100644 index 000000000..09d8199bf --- /dev/null +++ b/upf/README.md @@ -0,0 +1,21 @@ +# UPF_Tests.ttcn + +* external interfaces + * PFCP + * VTY + * CTRL + * StatsD + +{% dot upf_tests.svg +digraph G { + graph [label="UPF_Tests", labelloc=t, fontsize=30]; + rankdir=LR; + UPF [label="IUT\nosmo-upf",shape="box"]; + ATS [label="ATS\nUPF_Tests.ttcn"]; + + UPF <- ATS [label="PFCP"]; + UPF <- ATS [label="CTRL"]; + UPF <- ATS [label="VTY"]; + UPF -> ATS [label="StatsD"]; +} +%} diff --git a/upf/README.txt b/upf/README.txt new file mode 100644 index 000000000..9f1eced26 --- /dev/null +++ b/upf/README.txt @@ -0,0 +1,4 @@ +Integration Tests for OsmoUPF +----------------------------- + +This test suite tests OsmoUPF, emulating a Control Plane Function. diff --git a/upf/UPF_Tests.cfg b/upf/UPF_Tests.cfg new file mode 100644 index 000000000..fc410b643 --- /dev/null +++ b/upf/UPF_Tests.cfg @@ -0,0 +1,19 @@ +[ORDERED_INCLUDE] +# Common configuration, shared between test suites +"../Common.cfg" +# testsuite specific configuration, not expected to change +"./UPF_Tests.default" + +# Local configuration below + +[LOGGING] + +[TESTPORT_PARAMETERS] + +[MODULE_PARAMETERS] +UPF_Tests.mp_verify_gtp := false; + +[MAIN_CONTROLLER] + +[EXECUTE] +UPF_Tests.control diff --git a/upf/UPF_Tests.default b/upf/UPF_Tests.default new file mode 100644 index 000000000..2175e9f26 --- /dev/null +++ b/upf/UPF_Tests.default @@ -0,0 +1,28 @@ +[LOGGING] +mtc.FileMask := LOG_ALL | TTCN_DEBUG | TTCN_MATCHING | DEBUG_ENCDEC; + +[TESTPORT_PARAMETERS] +*.UPFVTY.CTRL_MODE := "client" +*.UPFVTY.CTRL_HOSTNAME := "127.0.0.1" +*.UPFVTY.CTRL_PORTNUM := "4275" +*.UPFVTY.CTRL_LOGIN_SKIPPED := "yes" +*.UPFVTY.CTRL_DETECT_SERVER_DISCONNECTED := "yes" +*.UPFVTY.CTRL_READMODE := "buffered" +*.UPFVTY.CTRL_CLIENT_CLEANUP_LINEFEED := "yes" +*.UPFVTY.CTRL_DETECT_CONNECTION_ESTABLISHMENT_RESULT := "yes" +*.UPFVTY.PROMPT1 := "OsmoUPF> " +*.STATSVTY.CTRL_MODE := "client" +*.STATSVTY.CTRL_HOSTNAME := "127.0.0.1" +*.STATSVTY.CTRL_PORTNUM := "4276" +*.STATSVTY.CTRL_LOGIN_SKIPPED := "yes" +*.STATSVTY.CTRL_DETECT_SERVER_DISCONNECTED := "yes" +*.STATSVTY.CTRL_READMODE := "buffered" +*.STATSVTY.CTRL_CLIENT_CLEANUP_LINEFEED := "yes" +*.STATSVTY.CTRL_DETECT_CONNECTION_ESTABLISHMENT_RESULT := "yes" +*.STATSVTY.PROMPT1 := "OsmoUPF> " +*.LLSK.socket_type := "SEQPACKET" + +[MODULE_PARAMETERS] +Osmocom_VTY_Functions.mp_prompt_prefix := "OsmoUPF"; + +[EXECUTE] diff --git a/upf/UPF_Tests.ttcn b/upf/UPF_Tests.ttcn new file mode 100644 index 000000000..ca1a0f615 --- /dev/null +++ b/upf/UPF_Tests.ttcn @@ -0,0 +1,785 @@ +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"; + + /* When testing with gtp mockup, actions will not show. */ + boolean mp_verify_gtp_actions := false; +} + +type component test_CT extends CTRL_Adapter_CT { + port PFCPEM_PT PFCP; + + 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; + } +} + +friend function f_logp(TELNETasp_PT pt, charstring log_msg) +{ + // log on TTCN3 log output + log(log_msg); + // log in stderr log + f_vty_transceive(pt, "logp lglobal notice TTCN3 f_logp(): " & log_msg); +} + +private function f_str_split(charstring str, charstring delim := "\n") return ro_charstring +{ + var integer pos := 0; + var ro_charstring parts := {}; + var integer delim_pos; + var integer end := lengthof(str); + while (pos < end) { + delim_pos := f_strstr(str, delim, pos); + if (delim_pos < 0) { + delim_pos := end; + } + parts := parts & { substr(str, pos, delim_pos - pos) }; + pos := delim_pos + 1; + } + return parts; +} + +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 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 +} + +type record GTP_Action { + charstring kind, + charstring gtp_access_ip, + OCT4 teid_access_r, + OCT4 teid_access_l, + charstring core_ip, + 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 { + var GTP_Action a; + if (not f_get_name_val(a.kind, str, "GTP")) { + return false; + } + if (not f_get_name_val(a.gtp_access_ip, str, "GTP-access")) { + return false; + } + if (not f_get_name_val_oct4(a.teid_access_r, str, "TEID-r")) { + return false; + } + if (not f_get_name_val_oct4(a.teid_access_l, str, "TEID-l")) { + 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 (not f_get_name_val(a.core_ip, str, "IP-core")) { + return false; + } + 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 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 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); +} + +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"); +} + +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))); +} + +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) */ +private function f_ruleset_add_GTP_decaps(inout PFCP_Ruleset r, + template F_TEID local_f_teid := omit) { + 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( + ACCESS, + local_F_TEID := local_f_teid), + 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, + charstring ue_addr_v4 := "192.168.23.42", + OCT4 remote_teid, + charstring gtp_dest_addr_v4) { + + 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( + CORE, + ue_addr_v4 := ts_PFCP_UE_IP_Address_v4(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) + )) + ) + ) + }; +} + +/* 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/decapsulation */ +private function f_ruleset_endecaps(GTP_Action gtp) return PFCP_Ruleset +{ + var PFCP_Ruleset rules := { {}, {} }; + f_ruleset_add_GTP_decaps(rules, ts_PFCP_F_TEID_ipv4(gtp.teid_access_l, gtp.gtp_access_ip)); + f_ruleset_add_GTP_encaps(rules, gtp.core_ip, gtp.teid_access_r, gtp.gtp_access_ip); + return rules; +} + +/* Run a PFCP Session Establishment procedure */ +private function f_session_est(inout PFCP_session s, PFCP_Ruleset rules) runs on CPF_ConnHdlr { + + PFCP.send(ts_PFCP_Session_Est_Req(g_pars.local_addr, s.cp_seid, rules.pdr, rules.far)); + + var PDU_PFCP pfcp; + PFCP.receive(tr_PFCP_Session_Est_Resp(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; + log("established PFCP session: ", s); +} + +private function f_create_PFCP_session() runs on CPF_ConnHdlr return PFCP_session +{ + var PFCP_session s := { + up_seid := -, + cp_seid := f_next_seid(), + gtp := { + kind := "endecaps", + gtp_access_ip := "127.0.0.2", + teid_access_r := f_next_remote_teid(), + teid_access_l := f_next_local_teid(), + core_ip := f_next_ue_addr(), + pfcp_peer := g_pars.local_addr, + seid_l := '0000000000000000'O + } + }; + return s; +} + +/* Do a PFCP Session Establishment with default values (see f_create_PFCP_session()) */ +private function f_session_est_default() runs on CPF_ConnHdlr return PFCP_session +{ + var PFCP_session s := f_create_PFCP_session(); + f_session_est(s, f_ruleset_endecaps(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(charstring id) runs on CPF_ConnHdlr { + f_assoc_setup(); + var PFCP_session s := f_session_est_default(); + 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() 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)); + 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_default(); + 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(); + 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(); +} + +control { + execute( TC_assoc_node_id_v4() ); + execute( TC_assoc_node_id_fqdn() ); + execute( TC_session_est() ); + execute( TC_session_term_by_assoc_rel() ); + execute( TC_session_est_noop() ); +} + +} diff --git a/upf/expected-results.xml b/upf/expected-results.xml new file mode 100644 index 000000000..3504126e8 --- /dev/null +++ b/upf/expected-results.xml @@ -0,0 +1,4 @@ + + + + diff --git a/upf/gen_links.sh b/upf/gen_links.sh new file mode 100755 index 000000000..386511034 --- /dev/null +++ b/upf/gen_links.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +BASEDIR=../deps + +. ../gen_links.sh.inc + +DIR=$BASEDIR/titan.Libraries.TCCUsefulFunctions/src +FILES="TCCInterface_Functions.ttcn TCCConversion_Functions.ttcn TCCConversion.cc TCCInterface.cc TCCInterface_ip.h" +FILES+=" TCCEncoding_Functions.ttcn TCCEncoding.cc " # GSM 7-bit coding +gen_links $DIR $FILES + +DIR=$BASEDIR/titan.TestPorts.Common_Components.Socket-API/src +FILES="Socket_API_Definitions.ttcn" +gen_links $DIR $FILES + +# Required by PFCP/UDP +DIR=$BASEDIR/titan.TestPorts.IPL4asp/src +FILES="IPL4asp_Functions.ttcn IPL4asp_PT.cc IPL4asp_PT.hh IPL4asp_PortType.ttcn IPL4asp_Types.ttcn IPL4asp_discovery.cc IPL4asp_protocol_L234.hh" +gen_links $DIR $FILES + +DIR=$BASEDIR/titan.ProtocolModules.PFCP_v15.1.0/src +FILES="PFCP_Types.ttcn" +gen_links $DIR $FILES + +gen_links $DIR $FILES +DIR=$BASEDIR/titan.TestPorts.TELNETasp/src +FILES="TELNETasp_PT.cc TELNETasp_PT.hh TELNETasp_PortType.ttcn" +gen_links $DIR $FILES + +DIR=../library +FILES="Misc_Helpers.ttcn General_Types.ttcn Osmocom_Types.ttcn Osmocom_VTY_Functions.ttcn Native_Functions.ttcn Native_FunctionDefs.cc IPA_Types.ttcn IPA_CodecPort.ttcn IPA_CodecPort_CtrlFunct.ttcn IPA_CodecPort_CtrlFunctDef.cc IPA_Emulation.ttcnpp Osmocom_CTRL_Types.ttcn Osmocom_CTRL_Functions.ttcn Osmocom_CTRL_Adapter.ttcn " +FILES+="StatsD_Types.ttcn StatsD_CodecPort.ttcn StatsD_CodecPort_CtrlFunct.ttcn StatsD_CodecPort_CtrlFunctdef.cc StatsD_Checker.ttcn " +FILES+="PFCP_CodecPort.ttcn PFCP_CodecPort_CtrlFunct.ttcn PFCP_CodecPort_CtrlFunctDef.cc PFCP_Emulation.ttcn PFCP_Templates.ttcn" +gen_links $DIR $FILES + +ignore_pp_results diff --git a/upf/osmo-upf.cfg b/upf/osmo-upf.cfg new file mode 100644 index 000000000..f84a4aecb --- /dev/null +++ b/upf/osmo-upf.cfg @@ -0,0 +1,16 @@ +log stderr + logging filter all 1 + logging color 1 + logging print category-hex 0 + logging print category 1 + logging print thread-id 0 + logging print extended-timestamp 1 + logging print level 1 + logging print file basename last + logging level set-all debug +pfcp + local-addr 127.0.0.1 +gtp + mockup +nft + mockup diff --git a/upf/regen_makefile.sh b/upf/regen_makefile.sh new file mode 100755 index 000000000..953e10f2b --- /dev/null +++ b/upf/regen_makefile.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +NAME=UPF_Tests + +FILES=" + *.ttcn + *.ttcnpp + *.cc + IPA_CodecPort_CtrlFunctDef.cc + IPL4asp_PT.cc + IPL4asp_discovery.cc + Native_FunctionDefs.cc + StatsD_CodecPort_CtrlFunctdef.cc + TCCConversion.cc + TCCEncoding.cc + TCCInterface.cc + TELNETasp_PT.cc +" + +export CPPFLAGS_TTCN3=" + -DIPA_EMULATION_CTRL +" + +../regen-makefile.sh -e $NAME $FILES