module MGCP_Emulation { /* MGCP Emulation, runs on top of MGCP_CodecPort. It multiplexes/demultiplexes * the individual connections, so there can be separate TTCN-3 components handling * each of the connections. * * The MGCP_Emulation.main() function processes MGCP primitives from the MGCP * socket via the MGCP_CodecPort, and dispatches them to the per-connection components. * * For each new inbound connection, the MgcpOps.create_cb() is called. It can create * or resolve a TTCN-3 component, and returns a component reference to which that inbound * connection is routed/dispatched. * * If a pre-existing component wants to register to handle a future inbound call, it can * do so by registering an "expect" with the expected destination phone number. This is e.g. useful * if you are simulating BSC + MGCP, and first trigger a connection from BSC side in a * component which then subsequently should also handle the MGCP emulation. * * Inbound Unit Data messages (such as are dispatched to the MgcpOps.unitdata_cb() callback, * which is registered with an argument to the main() function below. * * (C) 2017-2018 by Harald Welte * (C) 2018 by sysmocom - s.f.m.c. GmbH, Author: Daniel Willmann * 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 MGCP_CodecPort all; import from MGCP_CodecPort_CtrlFunct all; import from MGCP_Types all; import from MGCP_Templates all; import from Osmocom_Types all; import from IPL4asp_Types all; type component MGCP_ConnHdlr { /* Simple send/recv without caring about peer addr+port. Used with multi_conn_mode=false. */ port MGCP_Conn_PT MGCP; /* Handle multiple connections concurrently. Used with multi_conn_mode=true. */ port MGCP_Conn_Multi_PT MGCP_MULTI; /* procedure based port to register for incoming connections. */ port MGCPEM_PROC_PT MGCP_PROC; } /* port between individual per-connection components and this dispatcher */ type port MGCP_Conn_PT message { inout MgcpCommand, MgcpResponse; } with { extension "internal" }; /* port between individual per-connection components and this dispatcher */ type port MGCP_Conn_Multi_PT message { inout MGCP_RecvFrom, MGCP_SendTo; } with { extension "internal" }; /* represents a single MGCP Endpoint */ type record EndpointData { MGCP_ConnHdlr comp_ref, MgcpEndpoint endpoint optional }; /* pending CRCX with their transaction ID */ type set of MgcpTransId MgcpTransIds; type component MGCP_Emulation_CT { /* Port facing to the UDP SUT */ port MGCP_CODEC_PT MGCP; /* All MGCP_ConnHdlr MGCP ports connect here * MGCP_Emulation_CT.main needs to figure out what messages * to send where with CLIENT.send() to vc_conn */ port MGCP_Conn_PT MGCP_CLIENT; /* This one is used with multi_conn_mode=true and allows differentiating UDP sockets */ port MGCP_Conn_Multi_PT MGCP_CLIENT_MULTI; /* currently tracked connections */ var EndpointData MgcpEndpointTable[16]; var MgcpTransIds MgcpPendingTrans := {}; /* pending expected CRCX */ var ExpectData MgcpExpectTable[8]; /* procedure based port to register for incoming connections */ port MGCPEM_PROC_PT MGCP_PROC; var charstring g_mgcp_id; var integer g_mgcp_conn_id := -1; var MGCP_conn_parameters g_pars; } type function MGCPCreateCallback(MgcpCommand cmd, charstring id) runs on MGCP_Emulation_CT return MGCP_ConnHdlr; type function MGCPUnitdataCallback(MgcpMessage msg) runs on MGCP_Emulation_CT return template MgcpMessage; type record MGCPOps { MGCPCreateCallback create_cb, MGCPUnitdataCallback unitdata_cb } type record MGCP_conn_parameters { HostName callagent_ip, PortNumber callagent_udp_port, HostName mgw_ip, PortNumber mgw_udp_port, boolean multi_conn_mode } function tr_MGCP_RecvFrom_R(template MgcpMessage msg) runs on MGCP_Emulation_CT return template MGCP_RecvFrom { var template MGCP_RecvFrom mrf := { connId := g_mgcp_conn_id, remName := ?, remPort := ?, locName := ?, locPort := ?, msg := msg } return mrf; } private function f_ep_known(MgcpEndpoint ep) runs on MGCP_Emulation_CT return boolean { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (MgcpEndpointTable[i].endpoint == ep) { return true; } } return false; } private function f_comp_known(MGCP_ConnHdlr client) runs on MGCP_Emulation_CT return boolean { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (MgcpEndpointTable[i].comp_ref == client) { return true; } } return false; } private function f_comp_by_ep(MgcpEndpoint ep) runs on MGCP_Emulation_CT return MGCP_ConnHdlr { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (MgcpEndpointTable[i].endpoint == ep) { return MgcpEndpointTable[i].comp_ref; } } setverdict(fail, "MGCP Endpoint Table not found by Endpoint", ep); mtc.stop; } private function f_ep_by_comp(MGCP_ConnHdlr client) runs on MGCP_Emulation_CT return MgcpEndpoint { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (MgcpEndpointTable[i].comp_ref == client) { return MgcpEndpointTable[i].endpoint; } } setverdict(fail, "MGCP Endpoint Table not found by component ", client); mtc.stop; } private function f_ep_table_add(MGCP_ConnHdlr comp_ref, MgcpEndpoint ep) runs on MGCP_Emulation_CT { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (not isvalue(MgcpEndpointTable[i].endpoint)) { MgcpEndpointTable[i].endpoint := ep; MgcpEndpointTable[i].comp_ref := comp_ref; return; } } testcase.stop("MGCP Endpoint Table full!"); } private function f_ep_table_del(MGCP_ConnHdlr comp_ref, MgcpEndpoint ep) runs on MGCP_Emulation_CT { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (MgcpEndpointTable[i].comp_ref == comp_ref and MgcpEndpointTable[i].endpoint == ep) { MgcpEndpointTable[i].endpoint := omit; MgcpEndpointTable[i].comp_ref := null; return; } } setverdict(fail, "MGCP Endpoint Table: Couldn't find to-be-deleted entry!"); mtc.stop; } private function f_ep_table_change_connhdlr(MGCP_ConnHdlr comp_ref, MgcpEndpoint ep) runs on MGCP_Emulation_CT { var integer i; for (i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { if (MgcpEndpointTable[i].endpoint == ep) { MgcpEndpointTable[i].comp_ref := comp_ref; log("MGCP_Emulation_CT: MgcpEndpointTable[", i, "] now sends to ", comp_ref); return; } } setverdict(fail, "MGCP Endpoint Table: Couldn't find entry to move to ", comp_ref); mtc.stop; } /* Check if the given transaction ID is a pending CRCX. If yes, return true + remove */ private function f_trans_id_was_pending(MgcpTransId trans_id) runs on MGCP_Emulation_CT return boolean { for (var integer i := 0; i < lengthof(MgcpPendingTrans); i := i+1) { if (MgcpPendingTrans[i] == trans_id) { /* Remove from list */ var MgcpTransIds OldPendingTrans := MgcpPendingTrans; MgcpPendingTrans := {} for (var integer j := 0; j < lengthof(OldPendingTrans); j := j+1) { if (j != i) { MgcpPendingTrans := MgcpPendingTrans & {OldPendingTrans[j]}; } } return true; } } return false; } /* TODO: move this to MGCP_Types? */ function f_mgcp_ep(MgcpMessage msg) return MgcpEndpoint { var MgcpParameterList params; var integer i; if (ischosen(msg.command)) { return msg.command.line.ep; } else { var MgcpEndpoint ep; if (f_mgcp_find_param(msg, "Z", ep) == false) { setverdict(fail, "No SpecificEndpointName in MGCP response", msg); mtc.stop; } return ep; } } private function f_ep_table_init() runs on MGCP_Emulation_CT { for (var integer i := 0; i < sizeof(MgcpEndpointTable); i := i+1) { MgcpEndpointTable[i].comp_ref := null; MgcpEndpointTable[i].endpoint := omit; } } private function f_forward_to_client(MGCP_RecvFrom mrf, MGCP_ConnHdlr vc_conn) runs on MGCP_Emulation_CT { if (g_pars.multi_conn_mode) { MGCP_CLIENT_MULTI.send(mrf) to vc_conn; } else { MGCP_CLIENT.send(mrf.msg.command) to vc_conn; } } function main(MGCPOps ops, MGCP_conn_parameters p, charstring id) runs on MGCP_Emulation_CT { var Result res; g_pars := p; g_mgcp_id := id; f_ep_table_init(); f_expect_table_init(); map(self:MGCP, system:MGCP_CODEC_PT); if (p.multi_conn_mode or p.callagent_udp_port == -1) { res := MGCP_CodecPort_CtrlFunct.f_IPL4_listen(MGCP, p.mgw_ip, p.mgw_udp_port, { udp:={} }); } else { res := MGCP_CodecPort_CtrlFunct.f_IPL4_connect(MGCP, p.callagent_ip, p.callagent_udp_port, p.mgw_ip, p.mgw_udp_port, -1, { udp:={} }); } if (not ispresent(res.connId)) { setverdict(fail, "Could not connect MGCP socket, check your configuration"); mtc.stop; } g_mgcp_conn_id := res.connId; while (true) { var MGCP_ConnHdlr vc_conn; var ExpectCriteria crit; var MGCP_RecvFrom mrf; var MGCP_SendTo mst; var MgcpMessage msg; var MgcpCommand cmd; var MgcpResponse resp; var MgcpEndpoint ep; alt { /* MGCP from client */ [not p.multi_conn_mode] MGCP_CLIENT.receive(MgcpResponse:?) -> value resp sender vc_conn { msg := { response := resp }; /* If this is the response to a pending CRCX, extract Endpoint and store in table */ if (f_trans_id_was_pending(resp.line.trans_id)) { f_ep_table_add(vc_conn, f_mgcp_ep(msg)); } /* Pass message through */ /* TODO: check which ConnectionID client has allocated + store in table? */ MGCP.send(t_MGCP_Send(g_mgcp_conn_id, msg)); } /* MGCP from client in Multi Conn mode */ [p.multi_conn_mode] MGCP_CLIENT_MULTI.receive(MGCP_SendTo:?) -> value mst sender vc_conn { /* If this is the response to a pending CRCX, extract Endpoint and store in table */ if (f_trans_id_was_pending(mst.msg.response.line.trans_id)) { f_ep_table_add(vc_conn, f_mgcp_ep(mst.msg)); } /* Pass message through */ /* TODO: check which ConnectionID client has allocated + store in table? */ MGCP.send(mst); } [] MGCP.receive(tr_MGCP_RecvFrom_R(?)) -> value mrf { if (not p.multi_conn_mode and p.callagent_udp_port == -1) { /* we aren't yet connected to the remote side port, let's fix this. This way upper layers can use Send/Recv without caring about UDP src/dst addr + port */ p.callagent_udp_port := mrf.remPort; res := MGCP_CodecPort_CtrlFunct.f_IPL4_connect(MGCP, p.callagent_ip, p.callagent_udp_port, p.mgw_ip, p.mgw_udp_port, g_mgcp_conn_id, { udp:={} }); if (not ispresent(res.connId)) { setverdict(fail, "Could not connect MGCP socket, check your configuration"); mtc.stop; } } if (ischosen(mrf.msg.command)) { cmd := mrf.msg.command; if (f_ep_known(cmd.line.ep)) { vc_conn := f_comp_by_ep(cmd.line.ep); f_forward_to_client(mrf, vc_conn); } else { if (cmd.line.verb == "CRCX") { vc_conn := ops.create_cb.apply(cmd, id); if (not match(cmd.line.ep, t_MGCP_EP_wildcard)) { /* non-wildcard EP, use directly */ f_ep_table_add(vc_conn, cmd.line.ep); } else { /* add this transaction to list of pending transactions */ MgcpPendingTrans := MgcpPendingTrans & {cmd.line.trans_id}; } f_forward_to_client(mrf, vc_conn); } else { /* connectionless MGCP, i.e. messages without ConnectionId */ var template MgcpMessage r := ops.unitdata_cb.apply(mrf.msg); if (isvalue(r)) { MGCP.send(t_MGCP_SendToMrf(mrf, r)); } } } } else { setverdict(fail, "Received unexpected MGCP response: ", mrf.msg.response); mtc.stop; } } [] MGCP_PROC.getcall(MGCPEM_register:{?,?}) -> param(crit, vc_conn) { f_create_expect(crit, vc_conn); MGCP_PROC.reply(MGCPEM_register:{crit, vc_conn}) to vc_conn; } [] MGCP_PROC.getcall(MGCPEM_delete_ep:{?,?}) -> param(ep, vc_conn) { f_ep_table_del(vc_conn, ep); MGCP_PROC.reply(MGCPEM_delete_ep:{ep, vc_conn}) to vc_conn; } [] MGCP_PROC.getcall(MGCPEM_change_connhdlr:{?,?}) -> param(ep, vc_conn) { f_ep_table_change_connhdlr(vc_conn, ep); MGCP_PROC.reply(MGCPEM_change_connhdlr:{ep, vc_conn}) to vc_conn; } } } } /* "Expect" Handling */ /* */ type record ExpectCriteria { MgcpConnectionId connid optional, MgcpEndpoint endpoint optional, MgcpTransId transid optional } type record ExpectData { ExpectCriteria crit optional, MGCP_ConnHdlr vc_conn } signature MGCPEM_register(in ExpectCriteria cmd, in MGCP_ConnHdlr hdlr); signature MGCPEM_delete_ep(in MgcpEndpoint ep, in MGCP_ConnHdlr hdlr); signature MGCPEM_change_connhdlr(in MgcpEndpoint ep, in MGCP_ConnHdlr hdlr); type port MGCPEM_PROC_PT procedure { inout MGCPEM_register, MGCPEM_delete_ep, MGCPEM_change_connhdlr; } with { extension "internal" }; function f_get_mgcp_by_crit(ExpectCriteria crit) return template MgcpCommand { var template MgcpCommand ret := { line := { verb := ?, trans_id := ?, ep := ?, ver := ? }, params := *, sdp := * } if (ispresent(crit.connid)) { ret.params := { *, ts_MgcpParConnectionId(crit.connid), * }; } if (ispresent(crit.endpoint)) { ret.line.ep := crit.endpoint; } if (ispresent(crit.transid)) { ret.line.trans_id := crit.transid; } return ret; } /* Function that can be used as create_cb and will use the expect table */ function ExpectedCreateCallback(MgcpCommand cmd, charstring id) runs on MGCP_Emulation_CT return MGCP_ConnHdlr { var MGCP_ConnHdlr ret := null; var template MgcpCommand mgcpcmd; var integer i; for (i := 0; i < sizeof(MgcpExpectTable); i := i+1) { if (not ispresent(MgcpExpectTable[i].crit)) { continue; } /* FIXME: Ignore criteria for now */ mgcpcmd := f_get_mgcp_by_crit(MgcpExpectTable[i].crit); if (match(cmd, mgcpcmd)) { ret := MgcpExpectTable[i].vc_conn; /* Release this entry */ MgcpExpectTable[i].crit := omit; MgcpExpectTable[i].vc_conn := null; log("Found Expect[", i, "] for ", cmd, " handled at ", ret); return ret; } } setverdict(fail, "Couldn't find Expect for CRCX", cmd); mtc.stop; } private function f_create_expect(ExpectCriteria crit, MGCP_ConnHdlr hdlr) runs on MGCP_Emulation_CT { var integer i; /* Check an entry like this is not already present */ for (i := 0; i < sizeof(MgcpExpectTable); i := i+1) { if (crit == MgcpExpectTable[i].crit) { setverdict(fail, "Crit already present", crit); mtc.stop; } } for (i := 0; i < sizeof(MgcpExpectTable); i := i+1) { if (not ispresent(MgcpExpectTable[i].crit)) { MgcpExpectTable[i].crit := crit; MgcpExpectTable[i].vc_conn := hdlr; log("Created Expect[", i, "] for ", crit, " to be handled at ", hdlr); return; } } testcase.stop("No space left in MgcpExpectTable") } /* client/conn_hdlr side function to use procedure port to create expect in emulation */ function f_create_mgcp_expect(ExpectCriteria dest_number) runs on MGCP_ConnHdlr { MGCP_PROC.call(MGCPEM_register:{dest_number, self}) { [] MGCP_PROC.getreply(MGCPEM_register:{?,?}) {}; } } /* client/conn_hdlr side function to use procedure port to delete expect in emulation */ function f_create_mgcp_delete_ep(MgcpEndpoint ep) runs on MGCP_ConnHdlr { MGCP_PROC.call(MGCPEM_delete_ep:{ep, self}) { [] MGCP_PROC.getreply(MGCPEM_delete_ep:{?,?}) {}; } } /* Move MGCP handling for a given MGW endpoint to a different MGCP_ConnHdlr component. */ function f_mgcp_change_connhdlr(MgcpEndpoint ep) runs on MGCP_ConnHdlr { MGCP_PROC.call(MGCPEM_change_connhdlr:{ep, self}) { [] MGCP_PROC.getreply(MGCPEM_change_connhdlr:{?,?}) {}; } } private function f_expect_table_init() runs on MGCP_Emulation_CT { var integer i; for (i := 0; i < sizeof(MgcpExpectTable); i := i + 1) { MgcpExpectTable[i].crit := omit; } } function DummyUnitdataCallback(MgcpMessage msg) runs on MGCP_Emulation_CT return template MgcpMessage { log("Ignoring MGCP ", msg); return omit; } /* Determine encoding name for a specified payload type number */ function f_encoding_name_from_pt(SDP_FIELD_PayloadType pt) return charstring { if (pt == PT_PCMU) { return "PCMU"; } else if (pt == PT_GSM) { return "GSM"; } else if (pt == PT_PCMA) { return "PCMA"; } else if (pt == PT_GSMEFR) { return "GSM-EFR"; } else if (pt == PT_GSMHR) { return "GSM-HR-08"; } else if (pt == PT_AMR) { return "AMR"; } else if (pt == PT_AMRWB) { return "AMR-WB"; } setverdict(fail, "Unknown payload type ", pt); mtc.stop; } }