/* PFCP Emulation in TTCN-3 * * (C) 2022-2024 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 */ module PFCP_Emulation { import from IPL4asp_Types all; import from General_Types all; import from Osmocom_Types all; import from Misc_Helpers all; import from PFCP_Types all; import from PFCP_Templates all; import from PFCP_CodecPort all; import from PFCP_CodecPort_CtrlFunct all; /*********************************************************************** * Main Emulation Component ***********************************************************************/ const integer PFCP_PORT := 8805; type enumerated PFCP_Role { CPF, UPF }; type record PFCP_Emulation_Cfg { HostName pfcp_bind_ip, PortNumber pfcp_bind_port, HostName pfcp_remote_ip, PortNumber pfcp_remote_port, PFCP_Role role }; type component PFCP_Emulation_CT { /* Communication with underlying PFCP CodecPort */ port PFCP_PT PFCP; /* Communication with Clients */ port PFCPEM_PT CLIENT; port PFCPEM_PROC_PT CLIENT_PROC; /* Configuration by the user */ var PFCP_Emulation_Cfg g_pfcp_cfg; /* State */ var integer g_pfcp_conn_id; var integer g_recovery_timestamp; /* List of PFCP_ConnHdlr subscribed for broadcast */ var PFCPEM_conns g_conns_bcast; /* List of PFCP_ConnHdlr subscribed for SEID */ var PFCPEM_conns g_conns_seqnr; /* List of PFCP_ConnHdlr subscribed for SeqNr */ var PFCPEM_conns g_conns_seid; var integer g_next_sequence_nr_state; }; private function f_PFCPEM_next_sequence_nr() runs on PFCP_Emulation_CT return integer { g_next_sequence_nr_state := g_next_sequence_nr_state + 1; if (g_next_sequence_nr_state > 16777215) { g_next_sequence_nr_state := 1; } return g_next_sequence_nr_state; } type record PFCPEM_conn { PFCP_ConnHdlr vc_conn, OCT8 seid optional, LIN3_BO_LAST seqnr optional }; type record of PFCPEM_conn PFCPEM_conns; private function f_PFCPEM_conn_by_seqnr(LIN3_BO_LAST seqnr) runs on PFCP_Emulation_CT return PFCP_ConnHdlr { for (var integer i := 0; i < lengthof(g_conns_seqnr); i := i + 1) { if (seqnr == g_conns_seqnr[i].seqnr) { return g_conns_seqnr[i].vc_conn; } } return null; }; private function f_PFCPEM_conn_by_seid(OCT8 seid) runs on PFCP_Emulation_CT return PFCP_ConnHdlr { for (var integer i := 0; i < lengthof(g_conns_seid); i := i + 1) { if (seid == g_conns_seid[i].seid) { return g_conns_seid[i].vc_conn; } } return null; }; private function f_PFCPEM_conn_for_pdu(in PDU_PFCP pdu) runs on PFCP_Emulation_CT return PFCP_ConnHdlr { var PFCP_ConnHdlr vc_conn := null; vc_conn := f_PFCPEM_conn_by_seqnr(pdu.sequence_number); if (vc_conn != null) { f_PFCPEM_conns_seqnr_del(pdu.sequence_number); return vc_conn; } /* If there is a SEID, we can look it up */ if (pdu.s_flag == '1'B) { vc_conn := f_PFCPEM_conn_by_seid(pdu.seid); } return vc_conn; }; /* subscribe/unsubscribe for/from broadcast PDUs */ private function f_PFCPEM_conns_bcast_add(PFCP_ConnHdlr vc_conn) runs on PFCP_Emulation_CT return boolean { for (var integer i := 0; i < lengthof(g_conns_bcast); i := i + 1) { if (g_conns_bcast[i].vc_conn == vc_conn) { log(__SCOPE__, "(): vc_conn ", vc_conn, " is already in the list"); return false; } } g_conns_bcast := g_conns_bcast & {{vc_conn, omit, omit}}; log(__SCOPE__, "(): vc_conn ", vc_conn, " subscribed for broadcast"); return true; } private function f_PFCPEM_conns_bcast_del(PFCP_ConnHdlr vc_conn) runs on PFCP_Emulation_CT return boolean { var PFCPEM_conns conns := { }; for (var integer i := 0; i < lengthof(g_conns_bcast); i := i + 1) { if (g_conns_bcast[i].vc_conn != vc_conn) { conns := conns & { g_conns_bcast[i] }; } } if (lengthof(conns) == lengthof(g_conns_bcast)) { log(__SCOPE__, "(): vc_conn ", vc_conn, " was not in the list"); return false; } log(__SCOPE__, "(): vc_conn ", vc_conn, " unsubscribed from broadcast"); g_conns_bcast := conns; return true; } /* subscribe/unsubscribe for/from PDUs with the given SeqNr value */ private function f_PFCPEM_conns_seqnr_add(PFCP_ConnHdlr vc_conn, LIN3_BO_LAST seqnr) runs on PFCP_Emulation_CT return boolean { for (var integer i := 0; i < lengthof(g_conns_seqnr); i := i + 1) { if (g_conns_seqnr[i].seqnr == seqnr) { log(__SCOPE__, "(): SeqNr ", seqnr, " is already claimed", " by vc_conn ", g_conns_seqnr[i].vc_conn); return false; } } g_conns_seqnr := g_conns_seqnr & { {vc_conn, omit, seqnr} }; return true; } private function f_PFCPEM_conns_seqnr_del(LIN3_BO_LAST seqnr) runs on PFCP_Emulation_CT return boolean { var PFCPEM_conns conns := { }; for (var integer i := 0; i < lengthof(g_conns_seqnr); i := i + 1) { if (g_conns_seqnr[i].seqnr != seqnr) { conns := conns & { g_conns_seqnr[i] }; } } if (lengthof(conns) == lengthof(g_conns_seqnr)) { log(__SCOPE__, "(): SeqNr ", seqnr, " was not in the list"); return false; } g_conns_seqnr := conns; return true; } /* subscribe/unsubscribe for/from PDUs with the given SEID value */ private function f_PFCPEM_conns_seid_add(PFCP_ConnHdlr vc_conn, OCT8 seid) runs on PFCP_Emulation_CT return boolean { for (var integer i := 0; i < lengthof(g_conns_seid); i := i + 1) { if (g_conns_seid[i].seid == seid) { log(__SCOPE__, "(): SEID ", seid, " is already claimed", " by vc_conn ", g_conns_seid[i].vc_conn); return false; } } g_conns_seid := g_conns_seid & { {vc_conn, seid, omit} }; return true; } private function f_PFCPEM_conns_seid_del(OCT8 seid) runs on PFCP_Emulation_CT return boolean { var PFCPEM_conns conns := { }; for (var integer i := 0; i < lengthof(g_conns_seid); i := i + 1) { if (g_conns_seid[i].seid != seid) { conns := conns & { g_conns_seid[i] }; } } if (lengthof(conns) == lengthof(g_conns_seid)) { log(__SCOPE__, "(): SEID ", seid, " was not in the list"); return false; } g_conns_seid := conns; return true; } private function f_init(PFCP_Emulation_Cfg cfg) runs on PFCP_Emulation_CT { var Result res; map(self:PFCP, system:PFCP); res := PFCP_CodecPort_CtrlFunct.f_IPL4_listen(PFCP, cfg.pfcp_bind_ip, cfg.pfcp_bind_port, {udp:={}}); g_pfcp_conn_id := res.connId; g_recovery_timestamp := f_rnd_int(4294967296); g_pfcp_cfg := cfg; g_conns_bcast := { }; g_conns_seqnr := { }; g_conns_seid := { }; g_next_sequence_nr_state := (1 + f_rnd_int(1000)) * 10000; } function main(PFCP_Emulation_Cfg cfg) runs on PFCP_Emulation_CT { var PFCP_ConnHdlr vc_conn; var PFCP_Unitdata ud; var PDU_PFCP pdu; var OCT8 seid; f_init(cfg); while (true) { alt { [] PFCP.receive(tr_PFCP_UD(tr_PFCP_Heartbeat_Req)) -> value ud { log("Rx Heartbeat Req: ", ud.pdu); pdu := valueof(ts_PFCP_Heartbeat_Resp(g_recovery_timestamp)); pdu.sequence_number := ud.pdu.sequence_number; ud.pdu := pdu; PFCP.send(ud); } [] PFCP.receive(tr_PFCP_UD(?)) -> value ud { log("PFCP_Emulation main() PFCP.receive: ", ud); vc_conn := f_PFCPEM_conn_for_pdu(ud.pdu); if (vc_conn != null) { log("found destination ", vc_conn); CLIENT.send(ud.pdu) to vc_conn; } else { for (var integer i := 0; i < lengthof(g_conns_bcast); i := i + 1) { log("broadcasting to ", g_conns_bcast[i].vc_conn); CLIENT.send(ud.pdu) to g_conns_bcast[i].vc_conn; } } } [] CLIENT.receive(PDU_PFCP:?) -> value pdu sender vc_conn { log("PFCP_Emulation main() CLIENT.receive from ", vc_conn, ": ", pdu); if (pdu.sequence_number == 0) { pdu.sequence_number := f_PFCPEM_next_sequence_nr(); } ud := { peer := { conn_id := g_pfcp_conn_id, remote_name := g_pfcp_cfg.pfcp_remote_ip, remote_port := g_pfcp_cfg.pfcp_remote_port }, pdu := pdu }; /* route the response back to this vc_conn */ f_PFCPEM_conns_seqnr_add(vc_conn, pdu.sequence_number); PFCP.send(ud); } /* subscribe/unsubscribe for/from broadcast PDUs */ [] CLIENT_PROC.getcall(PFCPEM_subscribe_bcast:{?}) -> sender vc_conn { var boolean res := f_PFCPEM_conns_bcast_add(vc_conn); CLIENT_PROC.reply(PFCPEM_subscribe_bcast:{res}) to vc_conn; } [] CLIENT_PROC.getcall(PFCPEM_unsubscribe_bcast:{?}) -> sender vc_conn { var boolean res := f_PFCPEM_conns_bcast_del(vc_conn); CLIENT_PROC.reply(PFCPEM_unsubscribe_bcast:{res}) to vc_conn; } /* subscribe/unsubscribe for/from PDUs with the given SEID value */ [] CLIENT_PROC.getcall(PFCPEM_subscribe_seid:{?, ?}) -> param(seid := seid) sender vc_conn { var boolean res := f_PFCPEM_conns_seid_add(vc_conn, seid); CLIENT_PROC.reply(PFCPEM_subscribe_seid:{seid, res}) to vc_conn; } [] CLIENT_PROC.getcall(PFCPEM_unsubscribe_seid:{?, ?}) -> param(seid := seid) sender vc_conn { var boolean res := f_PFCPEM_conns_seid_del(seid); CLIENT_PROC.reply(PFCPEM_unsubscribe_seid:{seid, res}) to vc_conn; } [] CLIENT_PROC.getcall(PFCPEM_get_recovery_timestamp:{?}) -> sender vc_conn { CLIENT_PROC.reply(PFCPEM_get_recovery_timestamp:{g_recovery_timestamp}) to vc_conn; } } } } /*********************************************************************** * Interaction between Main and Client Components ***********************************************************************/ type port PFCPEM_PT message { inout PDU_PFCP; } with { extension "internal" }; signature PFCPEM_subscribe_bcast(out boolean success); signature PFCPEM_unsubscribe_bcast(out boolean success); signature PFCPEM_subscribe_seid(in OCT8 seid, out boolean success); signature PFCPEM_unsubscribe_seid(in OCT8 seid, out boolean success); signature PFCPEM_get_recovery_timestamp(out integer rts); type port PFCPEM_PROC_PT procedure { inout PFCPEM_subscribe_bcast; inout PFCPEM_unsubscribe_bcast; inout PFCPEM_subscribe_seid; inout PFCPEM_unsubscribe_seid; inout PFCPEM_get_recovery_timestamp; } with { extension "internal" }; /*********************************************************************** * Client Component ***********************************************************************/ type component PFCP_ConnHdlr { port PFCPEM_PT PFCP; port PFCPEM_PROC_PT PFCP_PROC; }; /* subscribe/unsubscribe for/from broadcast PDUs */ function f_PFCPEM_subscribe_bcast() runs on PFCP_ConnHdlr { PFCP_PROC.call(PFCPEM_subscribe_bcast:{-}) { [] PFCP_PROC.getreply(PFCPEM_subscribe_bcast:{true}); [] PFCP_PROC.getreply(PFCPEM_subscribe_bcast:{false}) { setverdict(fail, __SCOPE__, "() failed"); Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } } } function f_PFCPEM_unsubscribe_bcast() runs on PFCP_ConnHdlr { PFCP_PROC.call(PFCPEM_unsubscribe_bcast:{-}) { [] PFCP_PROC.getreply(PFCPEM_unsubscribe_bcast:{true}); [] PFCP_PROC.getreply(PFCPEM_unsubscribe_bcast:{false}) { setverdict(fail, __SCOPE__, "() failed"); Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } } } /* subscribe/unsubscribe for/from PDUs with the given SEID value */ function f_PFCPEM_subscribe_seid(in OCT8 seid) runs on PFCP_ConnHdlr { PFCP_PROC.call(PFCPEM_subscribe_seid:{seid, -}) { [] PFCP_PROC.getreply(PFCPEM_subscribe_seid:{seid, true}); [] PFCP_PROC.getreply(PFCPEM_subscribe_seid:{seid, false}) { setverdict(fail, __SCOPE__, "() failed"); Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } } } function f_PFCPEM_unsubscribe_seid(in OCT8 seid) runs on PFCP_ConnHdlr { PFCP_PROC.call(PFCPEM_unsubscribe_seid:{seid, -}) { [] PFCP_PROC.getreply(PFCPEM_unsubscribe_seid:{seid, true}); [] PFCP_PROC.getreply(PFCPEM_unsubscribe_seid:{seid, false}) { setverdict(fail, __SCOPE__, "() failed"); Misc_Helpers.f_shutdown(__BFILE__, __LINE__); } } } function f_PFCPEM_get_recovery_timestamp() runs on PFCP_ConnHdlr return integer { var integer rts; PFCP_PROC.call(PFCPEM_get_recovery_timestamp:{-}) { [] PFCP_PROC.getreply(PFCPEM_get_recovery_timestamp:{?}) -> param(rts) {}; } return rts; } altstep as_pfcp_ignore(PFCPEM_PT pt, template PDU_PFCP pfcp_expect := ?) { var PDU_PFCP pdu; [] pt.receive(pfcp_expect) -> value pdu { log("Ignoring PFCP PDU (SeqNr := ", pdu.sequence_number, ")"); repeat; } } }