/* Connection Handler of HNBGW test suite in TTCN-3
 * (C) 2021-2024 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * 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 tests OsmoHNBGW while emulating the hNodeB as well as MSC, SGSN, MGW
 * See README for more details.
 */

module ConnHdlr {

import from Misc_Helpers all;
import from General_Types all;
import from GSM_Types all;
import from Osmocom_Types all;
import from IPL4asp_Types all;
import from Native_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 HNBAP_Templates all;
import from HNBAP_IEs all;
import from HNBAP_PDU_Descriptions all;

import from RUA_IEs all;
import from RUA_Templates all;
import from RUA_Emulation all;

import from Iuh_Emulation all;

import from RANAP_Types all;
import from RANAP_PDU_Descriptions all;
import from RANAP_PDU_Contents all;
import from RANAP_IEs all;
import from RANAP_Templates all;
import from RANAP_CodecPort all;

import from RAN_Adapter all;
import from RAN_Emulation all;

import from MGCP_Emulation all;
import from MGCP_Types all;
import from MGCP_Templates all;
import from MGCP_CodecPort all;
import from SDP_Types all;
import from SDP_Templates all;

import from PFCP_Types all;
import from PFCP_Emulation all;
import from PFCP_Templates all;
import from PFCP_CodecPort all;

import from TCCConversion_Functions all;
import from MobileL3_Types all;
import from MobileL3_CommonIE_Types all;
import from MobileL3_GMM_SM_Types all;
import from L3_Templates all;
import from L3_Common all;

import from SCCPasp_Types all;
import from SCCP_Templates all;

import from Mutex all;

/***********************************************************************
 * code running inside per-UE ConnHdlr
 ***********************************************************************/

/* We extend:
   * RUA_ConnHdlr (for the Iuh side, emulating the HNB)
   * RAN_ConnHdlr (for the Iu side, emulating the MSC)
   * MGCP_ConnHdlr (for the MGCP side, emulating the MGW)
   * StatsD_ConnHdlr (for the statsd interface, verifying counters)
 */
type component ConnHdlr
extends RAN_ConnHdlr, MGCP_ConnHdlr, RUA_ConnHdlr, PFCP_ConnHdlr, StatsD_ConnHdlr, MutexCT {
	var integer g_sccp_conn_id;
	var TestHdlrParams g_pars;
	timer g_Tguard;

	port TELNETasp_PT HNBGWVTY;
}
type record of ConnHdlr ConnHdlrList;

type record MgwResponse {
	integer resp,
	HostName mgw_rtp_ip,
	/* if set, used after first received MDCX: */
	HostName mgw_rtp_ip_mdcx optional,
	PortNumber mgw_rtp_port,
	MgcpConnectionId mgcp_connection_id
}

type record MgcpParameters {
	integer got_crcx_count,
	integer got_dlcx_count,
	MgcpCallId mgcp_call_id optional,
	MgcpEndpoint mgcp_ep,
	MgwResponse mgw_conn_ran,
	MgwResponse mgw_conn_cn,
	uint7_t rtp_payload_type,
	charstring rtp_sdp_format,
	HostName hnb_rtp_ip,
	PortNumber hnb_rtp_port,
	HostName cn_rtp_ip,
	PortNumber cn_rtp_port,
	boolean use_osmux,
	integer got_osmux_count
}

private function f_mgcp_endp_str(integer endp_idx := 1) return charstring
{
	return "rtpbridge/" & int2str(endp_idx) & "@mgw";
}

template (value) MgcpParameters
t_MgcpParams(integer endp_idx := 1) := {
	got_crcx_count := 0,
	got_dlcx_count := 0,
	mgcp_call_id := omit,
	mgcp_ep := f_mgcp_endp_str(endp_idx),
	mgw_conn_ran := {
		resp := 1,
		mgw_rtp_ip := "127.1.2.1",
		mgw_rtp_ip_mdcx := omit,
		mgw_rtp_port := 10000,
		mgcp_connection_id := '11111'H
	},
	mgw_conn_cn := {
		resp := 1,
		mgw_rtp_ip := "127.1.2.2",
		mgw_rtp_ip_mdcx := omit,
		mgw_rtp_port := 20000,
		mgcp_connection_id := '22222'H
	},
	rtp_payload_type := 112,
	rtp_sdp_format := "VND.3GPP.IUFP",
	hnb_rtp_ip := "127.1.1.1",
	hnb_rtp_port := 10001,
	cn_rtp_ip := "127.1.3.1",
	cn_rtp_port := 20001,
	use_osmux := false,
	got_osmux_count := 0
}

type record FTeid {
	HostName addr,
	OCT4 teid
}

type record FTeids {
	FTeid local,
	FTeid remote
}

/* 'local' and 'remote' refer to the GTP information from the UPF's point of view:
 * HNB                             UPF                 CN
 * access.remote <---> access.local | core.local <---> core.remote
 */
type record GtpParameters {
	FTeids core,
	FTeids access
}

/* HNB                             UPF                 CN
 * access.remote <---> access.local | core.local <---> core.remote
 * 127.0.0.4           127.0.0.3      127.0.0.2        127.0.0.1
 * 0x44004400          0x30303030     0x22002200       0x10101010
 */
template (value) GtpParameters t_GtpParams := {
	core := {
		local := {
			addr := "127.0.0.2",
			teid := '22002200'O
		},
		remote := {
			addr := "127.0.0.1",
			teid := '10101010'O
		}
	},
	access := {
		local := {
			addr := "127.0.0.3",
			teid := '30303030'O
		},
		remote := {
			addr := "127.0.0.4",
			teid := '44004400'O
		}
	}
}

const OCT8 c_SEID0 := '0000000000000000'O;
type record PfcpParameters {
	/* whether we expect HNBGW to manage a UPF through PFCP: */
	boolean pfcp_enabled,
	charstring pfcp_local_addr,
	Node_ID upf_node_id,
	F_SEID upf_f_seid,
	F_SEID hnbgw_f_seid optional
}

template (value) PfcpParameters t_PfcpParams(boolean pfcp_enabled := false,
					     charstring pfcp_local_addr := "127.0.0.1",
					     charstring pfcp_upf_node_id := "\07osmocom\03org",
					     uint64_t upf_f_seid := 1) := {
	pfcp_enabled := pfcp_enabled,
	pfcp_local_addr := pfcp_local_addr,
	upf_node_id := valueof(ts_PFCP_Node_ID_fqdn(pfcp_upf_node_id)),
	upf_f_seid := ts_PFCP_F_SEID_ipv4(f_inet_addr(pfcp_local_addr), int2oct(upf_f_seid, 8)),
	hnbgw_f_seid := omit
}

type record HnbConfig {
	LocationAreaIdentification lai,
	uint8_t rac,
	integer sac,
	integer cell_id
}

type record TestHdlrParams {
	integer hnb_idx,
	/* cn_idx indicates which CN link from g_cn[] to connect to the test component.
	 * See also f_cn_idx(). */
	integer cn_idx,
	hexstring imsi,
	boolean ps_domain,
	MgcpParameters mgcp_pars,
	GtpParameters gtp_pars,
	HnbConfig hnb optional,
	boolean expect_separate_sccp_cr,
	integer tx_sccp_cr_data_len,
	boolean expect_compl_l3_success,
	PfcpParameters pfcp_pars,
	octetstring nas_pdu optional,
	/* local and remote SCCP addresses, used to transmit conectionless
	 * (unitdata) messages: */
	SCCP_PAR_Address sccp_addr_msc optional,
	SCCP_PAR_Address sccp_addr_hnbgw optional,
	/* RAB release cause */
	RANAP_IEs.Cause rab_rel_cause,
	integer hnbgw_timer_x31,
	IuSignallingConnectionIdentifier sigc_id,
	float t_guard
}

template (value) TestHdlrParams
t_pars(integer imsi_suffix,
       boolean ps_domain := false,
       integer hnb_idx := 0,
       boolean expect_separate_sccp_cr := false,
       integer tx_sccp_cr_data_len := 0,
       boolean expect_compl_l3_success := true,
       integer cn_idx := 0,
       template (value) MgcpParameters mgcp_pars := t_MgcpParams(),
       template (value) PfcpParameters pfcp_pars := t_PfcpParams(),
       template (value) RANAP_IEs.Cause rab_rel_cause := ts_RanapCause_nas_normal,
       integer hnbgw_timer_x31 := 5,
       float t_guard := 20.0) := {
	hnb_idx := hnb_idx,
	cn_idx := cn_idx,
	imsi := f_gen_imsi(imsi_suffix),
	ps_domain := ps_domain,
	mgcp_pars := mgcp_pars,
	gtp_pars := t_GtpParams,
	hnb := omit,	/* filled in later */
	expect_separate_sccp_cr := expect_separate_sccp_cr,
	tx_sccp_cr_data_len := tx_sccp_cr_data_len,
	expect_compl_l3_success := expect_compl_l3_success,
	pfcp_pars := pfcp_pars,
	nas_pdu := omit,
	sccp_addr_msc := omit,
	sccp_addr_hnbgw := omit,
	rab_rel_cause := rab_rel_cause,
	hnbgw_timer_x31 := hnbgw_timer_x31,
	sigc_id := int2bit(f_rnd_int(1000), 24),
	t_guard := t_guard
}


function f_create_ranap_exp(octetstring l3_enc) runs on ConnHdlr {
	BSSAP_PROC.call(RAN_register:{l3_enc, self}) {
		[] BSSAP_PROC.getreply(RAN_register:{?, ?}) {};
	}
}

type function void_fn(charstring id) runs on ConnHdlr;

/* first function inside ConnHdlr component; sets g_pars + starts function */
function f_handler_init(void_fn fn, charstring id, TestHdlrParams pars) runs on ConnHdlr {
	/* make parameters available via component variable */
	g_pars := pars;

	/* start guard timer and activate it as default */
	g_Tguard.start(g_pars.t_guard);
	activate(as_Tguard_ConnHdlr());

	map(self:HNBGWVTY, system:HNBGWVTY);
	f_vty_set_prompts(HNBGWVTY);
	f_vty_transceive(HNBGWVTY, "enable");

	/* TODO: CTRL? */

	f_sleep(1.0);

	fn.apply(id);
}

/* global altstep for global guard timer; */
private altstep as_Tguard_ConnHdlr() runs on ConnHdlr {
	[] g_Tguard.timeout {
		setverdict(fail, "Timeout of T_guard");
		mtc.stop;
	}
}

function f_bssap_expect(template (present) RANAP_PDU exp_rx) runs on ConnHdlr return RANAP_PDU
{
	var RANAP_PDU rx;
	timer T := 5.0;
	T.start;
	alt {
	[] BSSAP.receive(exp_rx) -> value rx {
		setverdict(pass);
		}
	[] BSSAP.receive(RANAP_PDU:?) -> value rx {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Got an unexpected RANAP message on BSSAP port: ", rx, " vs exp ", exp_rx));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for RANAP on BSSAP port: ", exp_rx));
		}
	}
	T.stop;
	return rx;
}

/* send RANAP on Iuh and expect it to show up on Iu */
function f_iuh2iu(template (present) RANAP_PDU tx, template RANAP_PDU exp_rx := omit)
runs on ConnHdlr return RANAP_PDU {
	var RANAP_PDU rx;
	timer T := 5.0;

	if (istemplatekind(exp_rx, "omit")) {
		exp_rx := tx;
	}

	RUA.send(tx);

	return f_bssap_expect(exp_rx);
}

/* send RANAP on Iu and expect it to show up on Iuh */
function f_iu2iuh(template (present) RANAP_PDU tx, template RANAP_PDU exp_rx := omit)
runs on ConnHdlr return RANAP_PDU {
	if (istemplatekind(exp_rx, "omit")) {
		exp_rx := tx;
	}

	BSSAP.send(tx);

	return f_rua_expect(exp_rx)
}

/* expect to receive a specific RUA message on Iuh */
function f_rua_expect(template (present) RANAP_PDU exp_rx) runs on ConnHdlr return RANAP_PDU
{
	var RANAP_PDU rx;
	timer T := 5.0;
	T.start;
	alt {
	[] RUA.receive(exp_rx) -> value rx {
		setverdict(pass);
		}
	[] RUA.receive(RANAP_PDU:?) -> value rx {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Got an unexpected RUA message: ", rx, " vs exp ", exp_rx));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for Iuh ", exp_rx));
		}
	}
	T.stop;
	return rx;
}

/* send RANAP on Iuh and expect it to show up on Iu */
function f_iuh2iu_connect(template (value) RANAP_PDU tx, template RANAP_PDU exp_rx := omit)
runs on ConnHdlr return RANAP_PDU {
	var RANAP_PDU rx;
	timer T := 5.0;

	if (istemplatekind(exp_rx, "omit")) {
		exp_rx := tx;
	}

	/* create an expect on the Iu side for the random NAS portion */
	if (g_pars.expect_separate_sccp_cr) {
		f_ran_register_sccp_cr_without_payload();
	} else {
		var template (omit) octetstring nas := f_ranap_extract_l3(valueof(tx));
		f_ran_register_exp(valueof(nas));
	}

	/* send it via Iuh (creating a RUA connection) */
	RUA.send(RUA_Conn_Req:{g_pars.ps_domain, tx});

	if (g_pars.expect_separate_sccp_cr) {
		/* Acknowledge the empty SCCP CR. RAN_Emulation does the confirmation, no need to respond. */
		BSSAP.receive(tr_RANAP_Conn_Req());
	}

	/* expect to receive it on the Iu side */
	return f_bssap_expect(exp_rx);
}

/* 3GPP TS 48.006 9.2 Connection release:
 *
 *  The MSC sends a SCCP released message. This message shall not contain
 *  any user data field.
 *
 * So what we expect normally is:
 *
 *                            HNBGW                    MSC
 *  RUA --id-Disconnect-------> | ---Data-Form-1(!)---> |  Iu-ReleaseComplete
 *                              | <--Released---------- |  (no data)
 *
 * This function tests osmo-hnbgw behavior if the CN fails to send a RLSD:
 * after some timeout, osmo-hnbgw should send a RLSD to the CN.
 */
function f_iuh2iu_disconnect(template (present) RANAP_PDU tx, RUA_IEs.Cause cause,
			     template RANAP_PDU exp_rx := omit)
runs on ConnHdlr return RANAP_PDU {
	var RANAP_PDU rx
	timer T := int2float(g_pars.hnbgw_timer_x31) + 1.0;

	if (istemplatekind(exp_rx, "omit")) {
		exp_rx := tx;
	}

	/* send it via Iuh (creating a RUA connection) */
	RUA.send(RUA_Disc_Req:{cause, tx});

	/* expect to receive it on the Iu side */
	rx := f_bssap_expect(exp_rx);
	return rx;
}

private function f_build_initial_ue_with_nas(TestHdlrParams pars, octetstring nas)
  return RANAP_PDU {
	var RANAP_IEs.LAI lai := {
		pLMNidentity := hex2oct(pars.hnb.lai.mcc_mnc),
		lAC := int2oct(pars.hnb.lai.lac, 2),
		iE_Extensions := omit
	};
	var RANAP_IEs.SAI sai := {
		pLMNidentity := lai.pLMNidentity,
		lAC := lai.lAC,
		sAC := int2oct(pars.hnb.sac, 2),
		iE_Extensions := omit
	}
	var GlobalRNC_ID grnc_id := {
		pLMNidentity := lai.pLMNidentity,
		rNC_ID := 2342
	}
	var template RANAP_PDU ret;
	if (pars.ps_domain) {
		var RANAP_IEs.RAC rac := int2oct(pars.hnb.rac, 1);
		ret := ts_RANAP_initialUE_PS(lai, rac, sai, nas, pars.sigc_id, grnc_id);
	} else {
		ret := ts_RANAP_initialUE_CS(lai, sai, nas, pars.sigc_id, grnc_id);
	}
	return valueof(ret);
}

/* build a RANAP InitialUE based on the TestHdlrParams */
function f_build_initial_ue(TestHdlrParams pars) return RANAP_PDU {

	var octetstring nas;

	if (pars.tx_sccp_cr_data_len == 0) {
		var octetstring tmsi := f_gen_tmsi(suffix := 0);
		var MobileIdentityLV mi_lv := valueof(ts_MI_IMSI_LV(pars.imsi));
		if (pars.ps_domain) {
			var PDU_L3_MS_SGSN l3_pdu;
			l3_pdu := valueof(ts_GMM_ATTACH_REQ(mi_lv, f_RAI('001'H, '01'H, '2a2a'O, '17'O)));
			nas := enc_PDU_L3_MS_SGSN(l3_pdu);
		} else {
			var PDU_ML3_MS_NW l3_pdu;
			l3_pdu := valueof(ts_LU_REQ(LU_Type_IMSI_Attach, mi_lv, '00F110'O));
			nas := enc_PDU_ML3_MS_NW(l3_pdu);
		}
	} else {
		/* The test asks for an exact number of Optional Data bytes. */

		/* First see what size the RANAP part of the payload data is,
		 * to adjust the NAS PDU size to the size requested by the test (pars.tx_sccp_cr_data_len). */
		var RANAP_PDU initial_ue := f_build_initial_ue_with_nas(pars, '00'O);

		var octetstring ranap_plus_one_byte_nas := enc_RANAP_PDU(initial_ue);
		var integer ranap_length := lengthof(ranap_plus_one_byte_nas) - 1;

		log("ranap_plus_one_byte_nas = ", lengthof(ranap_plus_one_byte_nas), " bytes, ", initial_ue, " = ",
		    ranap_plus_one_byte_nas);
		log("ranap_length = ", ranap_length);

		/* SCCP CR has a payload length limit of 130 bytes. To trigger this limit, the RANAP + NAS PDU has to be
		 * > 130 bytes. It doesn't need to be 131 bytes in the NAS PDU alone, but let's just make it definitely
		 * large enough. To test for this limit, pars.tx_sccp_cr_data_len asks for a specific amount of data len. */
		nas := f_rnd_octstring(pars.tx_sccp_cr_data_len - ranap_length);
	}

	var RANAP_PDU ret := f_build_initial_ue_with_nas(pars, nas);

	if (pars.tx_sccp_cr_data_len != 0) {
		for (var integer attempts := 0; attempts < 2; attempts := attempts + 1) {
			var octetstring check_len := enc_RANAP_PDU(ret);
			log("final RANAP PDU length = ", lengthof(check_len));
			if (lengthof(check_len) == pars.tx_sccp_cr_data_len) {
				return ret;
			}
			nas := f_rnd_octstring(lengthof(nas) + (pars.tx_sccp_cr_data_len - lengthof(check_len)));
			log("that was off, changed NAS length to ", lengthof(nas), " and trying again");
			ret := f_build_initial_ue_with_nas(pars, nas);
		}
		setverdict(fail, "Ended up with wrong Optional Data length");
		mtc.stop;
	}
	return ret;
}

/* build a RANAP RAB AssignmentResponse based on the TestHdlrParams */
function f_build_rab_ass_resp(TestHdlrParams pars) return RANAP_PDU {
	var template RAB_SetupOrModifiedList rab_sml;

	rab_sml := ts_RAB_SMdL(t_RAB_id(23), f_ts_RAB_TLA("192.168.1.23"), t_RAB_binding_port(1234));

	return valueof(ts_RANAP_RabAssResp(rab_sml));
}

/* HNB <- CN: Iu-ReleaseCmd, HNB -> CN: Iu-ReleaseCompl */
function f_cn_iu_release_procedure(boolean mgcp_teardown := false, boolean pfcp_teardown := false) runs on ConnHdlr {
	var RANAP_PDU tx;
	timer T;

	tx := valueof(ts_RANAP_IuReleaseCommand(g_pars.rab_rel_cause));
	f_iu2iuh(tx);

	if (mgcp_teardown) {
		T.start(5.0);
		alt {
		[] as_mgcp_dlcx() {}
		[] T.timeout {
			setverdict(fail, "Timeout waiting for DLCX");
			}
		}
		T.stop;
	}
	if (pfcp_teardown) {
		T.start(5.0);
		alt {
		[] as_pfcp_handle_del_req() {}
		[] T.timeout {
			setverdict(fail, "Timeout waiting for PFCP Del Sess Req");
			}
		}
		T.stop;
	}

	tx := valueof(ts_RANAP_IuReleaseComplete());
	f_iuh2iu_disconnect(tx, RUA_IEs.Cause:{radioNetwork:=normal});

	/* Once CN receives Iu-ReleaseComplete, it tears down the SCCP conn under Iu: */
	BSSAP.send(ts_MSC_CONN_PRIM_DISC_REQ(sccp_par_reason_end_user_originated));
	/* There's no signalling from lower layers regarding when RLC is
	 * received, hence only make sure the IUT is not sending an RLSD on its own
	 * after a timeout, which would mean it didn't process our RLSD */
	T.start(2.0);
	alt {
	[] BSSAP.receive(tr_MSC_CONN_PRIM_DISC_IND) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Rx Unexpected RLSD to our RLSD");
		}
	[] T.timeout {
		setverdict(pass);
		}
	}
}

/* Reply to a received CRCX with an OK (or the reply configured in cpars), using the given parameters.
 * Return true when an OK reply was sent, false otherwise.
 * Count occurrence of Osmux, include Osmux parameters in the reply if necessary. */
function f_handle_crcx(inout MgcpParameters pars, MgcpCommand mgcp_cmd) return template MgcpResponse {
	var MgwResponse conn := pars.mgw_conn_ran;
	if (pars.got_crcx_count > 0) {
		conn := pars.mgw_conn_cn;
	}
	pars.got_crcx_count := pars.got_crcx_count + 1;

	var MgcpMessage mgcp_msg := {
		command := mgcp_cmd
	}
	var template MgcpResponse mgcp_resp;
	var MgcpOsmuxCID osmux_cid;
	var MgcpCallId call_id := f_MgcpCmd_extract_call_id(mgcp_cmd);
	if (ispresent(pars.mgcp_call_id)) {
		if (pars.mgcp_call_id != call_id) {
			setverdict(fail, "CRCX contained unexpected call id. Expected:", pars.mgcp_call_id, " got:", call_id);
			mtc.stop;
		}
	} else {
		pars.mgcp_call_id := call_id;
	}

	/* When the endpoint contains a wildcard we keep the endpoint
	 * identifier we have set up in pars. Otherwise we use the
	 * endpoint name that the call agent has supplied */
	if (match(mgcp_cmd.line.ep, t_MGCP_EP_wildcard) == false) {
		pars.mgcp_ep := mgcp_cmd.line.ep;
	}

	if (conn.resp == -1) {
		/* Reply with rror */
		var MgcpResponse mgcp_rsp := {
			line := {
				code := "542",
				trans_id := mgcp_cmd.line.trans_id,
				string := "FORCED_FAIL"
			},
			sdp := omit

		}
		var MgcpParameter mgcp_rsp_param := {
			code := "Z",
			val := pars.mgcp_ep
		};
		mgcp_rsp.params[0] := mgcp_rsp_param;
		return mgcp_rsp;
	}

	if (conn.resp == 0) {
		/* Do not reply at all */
		return omit;
	}

	if (conn.resp != 1) {
		setverdict(fail, "Unexpected value for pars.mgw_conn_*.resp, expect -1, 0 or 1");
		mtc.stop;
	}

	var SDP_Message sdp := valueof(ts_SDP(conn.mgw_rtp_ip, conn.mgw_rtp_ip,
						hex2str(pars.mgcp_call_id), "42",
						conn.mgw_rtp_port,
						{ int2str(pars.rtp_payload_type) },
						{ valueof(ts_SDP_rtpmap(pars.rtp_payload_type,
									pars.rtp_sdp_format)),
						  valueof(ts_SDP_ptime(20)) }));

	if (f_mgcp_contains_par(mgcp_msg, "X-OSMUX")) {
		if (not pars.use_osmux) {
			setverdict(fail, "MSC sent X-Osmux parameter in MGCP, but not expecting any Osmux");
			mtc.stop;
		}
		pars.got_osmux_count := pars.got_osmux_count + 1;
		/* we expect MSC to use wildcard here, i.e. osmux_cid == -1 */
		osmux_cid := f_MgcpCmd_extract_osmux_cid(mgcp_cmd);
		log("f_handle_crcx(): got Osmux CID: ", osmux_cid);
		if (osmux_cid != -1) {
			setverdict(fail, "MSC using unexpected CID " & int2str(osmux_cid) & " != -1");
			mtc.stop;
		}

		osmux_cid := 0;
		mgcp_resp := ts_CRCX_ACK_osmux(mgcp_cmd.line.trans_id, conn.mgcp_connection_id, osmux_cid, sdp);
	} else {
		mgcp_resp := ts_CRCX_ACK(mgcp_cmd.line.trans_id, conn.mgcp_connection_id, sdp);
	}

	f_mgcp_par_append(mgcp_resp.params, ts_MgcpParSpecEP(pars.mgcp_ep));

	return mgcp_resp;
}

function f_rab_ass_req_cs() runs on ConnHdlr {
	var MgcpCommand mgcp_cmd;
	var RANAP_PDU tx;
	var template RAB_SetupOrModifyList rab_sml;
	timer T := 5.0;

	/* This code block cannot be executed by more than one component at a time because the
	 * RANAP RAB Ass Request triggers the IUT to send MGCP CRCX PDU(s), which needs to be
	 * routed to the respective ConnHdlr component (us). This is why we need to ensure that
	 * one ConnHdlr is triggering MGCP CRCX wildcard at the given moment of time.
	 */
	f_Mutex_lock(__BFILE__, __LINE__);

	f_create_mgcp_expect(ExpectCriteria:{omit,omit,omit});

	/* Send RAB Assignment Request */
	rab_sml := ts_RAB_SML(t_RAB_id(23), f_ts_RAB_TLA(g_pars.mgcp_pars.cn_rtp_ip), t_RAB_binding_port(g_pars.mgcp_pars.cn_rtp_port));
	tx := valueof(ts_RANAP_RabAssReq(rab_sml));
	BSSAP.send(tx);
	T.start;

	/* Handle MGCP CRCX */
	alt {
	[] MGCP.receive(tr_CRCX) -> value mgcp_cmd {
		log("CRCX1", mgcp_cmd);
		/* Unregister and unlock the mutex, enabling other components to
		 * establish MGCP sessions after us. */
		f_Mutex_unlock(__BFILE__, __LINE__);
		var template MgcpResponse mgcp_rsp := f_handle_crcx(g_pars.mgcp_pars, mgcp_cmd);
		MGCP.send(valueof(mgcp_rsp));
		}
	[] T.timeout {
		/* No need to f_Mutex_unlock since we are exiting anyway... */
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Timeout waiting for MGCP");
		}
	}
	T.stop;

	/* Expect RAB Assignment Request with IP/port from CRCX ACK via Iuh */
	rab_sml := ts_RAB_SML(t_RAB_id(23), f_ts_RAB_TLA(g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip), t_RAB_binding_port(g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_port));
	tx := valueof(ts_RANAP_RabAssReq(rab_sml));

	f_rua_expect(tx);
}

friend function f_rab_ass_resp_cs() runs on ConnHdlr {
	var MgcpCommand mgcp_cmd;
	var RANAP_PDU tx;
	var template RAB_SetupOrModifiedList rab_smdl;

	/* Send back RAB Assignment Response via Iuh */
	rab_smdl := ts_RAB_SMdL(t_RAB_id(23), f_ts_RAB_TLA(g_pars.mgcp_pars.hnb_rtp_ip), t_RAB_binding_port(g_pars.mgcp_pars.hnb_rtp_port));
	tx := valueof(ts_RANAP_RabAssResp(rab_smdl));
	RUA.send(tx);

	interleave {
	/* Expect MDCX with IP/port from RAB Assignment Response */
	[] MGCP.receive(tr_MDCX(tr_SDP(g_pars.mgcp_pars.hnb_rtp_ip, g_pars.mgcp_pars.hnb_rtp_port))) -> value mgcp_cmd {
		var HostName mgw_rtp_ip;
		var boolean exp_rua_rab_reass := false;
		log("MDCX1", mgcp_cmd);
		if (ispresent(g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip_mdcx)) {
			mgw_rtp_ip := g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip_mdcx;
			if (g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip != g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip_mdcx) {
				exp_rua_rab_reass := true;
			}
		} else {
			mgw_rtp_ip := g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip;
		}

		/* Verify SDP of MDCX */
		var SDP_Message sdp := valueof(ts_SDP(mgw_rtp_ip, mgw_rtp_ip, hex2str(g_pars.mgcp_pars.mgcp_call_id), "42", g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_port,
			{ int2str(g_pars.mgcp_pars.rtp_payload_type) }, { valueof(ts_SDP_rtpmap(g_pars.mgcp_pars.rtp_payload_type, g_pars.mgcp_pars.rtp_sdp_format)), valueof(ts_SDP_ptime(20)) } ));
		var template MgcpResponse mgcp_rsp := ts_MDCX_ACK(mgcp_cmd.line.trans_id, g_pars.mgcp_pars.mgw_conn_ran.mgcp_connection_id, sdp);
		MGCP.send(valueof(mgcp_rsp));

		/* If IP address changed, we expect HNBGW to Modify the RAB through RAB-Ass-Req: */
		if (exp_rua_rab_reass) {
			var template RAB_SetupOrModifyList rab_sml;
			/* Expect RAB Assignment Request with IP/port from MDCX ACK via Iuh */
			rab_sml := ts_RAB_SML(t_RAB_id(23), f_ts_RAB_TLA(g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_ip_mdcx), t_RAB_binding_port(g_pars.mgcp_pars.mgw_conn_ran.mgw_rtp_port));
			tx := valueof(ts_RANAP_RabAssReq(rab_sml));

			f_rua_expect(tx);
			/* Send back RAB Assignment Response via Iuh */
			rab_smdl := ts_RAB_SMdL(t_RAB_id(23), f_ts_RAB_TLA(g_pars.mgcp_pars.hnb_rtp_ip), t_RAB_binding_port(g_pars.mgcp_pars.hnb_rtp_port));
			tx := valueof(ts_RANAP_RabAssResp(rab_smdl));
			RUA.send(tx);
			/* This shouldn't trigger any MGCP, since nothing changed on the HNB IP side. Continue below. */
		}
		}
	/* Handle CRCX for second leg of endpoint, answer with IP/port */
	[] MGCP.receive(tr_CRCX(g_pars.mgcp_pars.mgcp_ep, tr_SDP(g_pars.mgcp_pars.cn_rtp_ip, g_pars.mgcp_pars.cn_rtp_port))) -> value mgcp_cmd {
		log("CRCX2", mgcp_cmd);
		/* Verify SDP of CRCX */
		var template MgcpResponse mgcp_rsp := f_handle_crcx(g_pars.mgcp_pars, mgcp_cmd);
		MGCP.send(valueof(mgcp_rsp));
	}
	}

	/* Expect RAB Assignment Response with IP/port from second CRCX ACK */
	rab_smdl := ts_RAB_SMdL(t_RAB_id(23), f_ts_RAB_TLA(g_pars.mgcp_pars.mgw_conn_cn.mgw_rtp_ip), t_RAB_binding_port(g_pars.mgcp_pars.mgw_conn_cn.mgw_rtp_port));
	tx := valueof(ts_RANAP_RabAssResp(rab_smdl));

	f_bssap_expect(tx);
}

function f_create_rab_cs() runs on ConnHdlr {
	f_rab_ass_req_cs();
	f_rab_ass_resp_cs();
}

function f_rab_ass_req_ps_with_pfcp() runs on ConnHdlr {
	var RANAP_PDU tx;
	var RANAP_PDU rx;
	var PDU_PFCP m;

	/* This code block cannot be executed by more than one component at a time because
	 * the RANAP RAB Ass Request triggers the IUT to send PFCP Session Establishment
	 * Request PDU(s), which needs to be routed to the respective ConnHdlr component (us).
	 * As per 3GPP TS 29.244, section 7.2.2.4.2, these PFCP PDUs shall all have SEID=0,
	 * so that the PFCPEM component cannot route them unambiguously.   This is why we
	 * need to ensure that only one ConnHdlr is triggering PFCP session establishment
	 * at the given moment of time. */
	f_Mutex_lock(__BFILE__, __LINE__);

	/* Subscribe for PFCP Session Establishment Request PDU(s), which are
	 * expected to have SEID set to 0, as per 3GPP TS 29.244, section 7.2.2.4.2. */
	f_PFCPEM_subscribe_seid(c_SEID0);

	/* Send RAB Assignment Request */
	var template RAB_SetupOrModifyList rab_sml;
	rab_sml := ts_RAB_SML_ps(t_RAB_id(23), f_ts_RAB_TLA(g_pars.gtp_pars.core.remote.addr), g_pars.gtp_pars.core.remote.teid);
	tx := valueof(ts_RANAP_RabAssReq(rab_sml));
	BSSAP.send(tx);

	/* Expect PFCP Session Establishment Request. */
	m := f_pfcp_expect(tr_PFCP_Session_Est_Req());
	/* Ask PFCPEM to route PDUs with to be indicated F-SEID to us. */
	f_PFCPEM_subscribe_seid(g_pars.pfcp_pars.upf_f_seid.seid);
	/* We're done establishing PFCP sessions, so at this point we no longer expect to
	 * receive Session Establishment Request PDUs with SEID=0.  Unregister and unlock
	 * the mutex, enabling other components to establish PFCP sessions after us. */
	f_PFCPEM_unsubscribe_seid(c_SEID0);
	f_Mutex_unlock(__BFILE__, __LINE__);

	var PFCP_Session_Establishment_Request serq := m.message_body.pfcp_session_establishment_request;
	/* Store HNBGW F-SEID for later: */
	g_pars.pfcp_pars.hnbgw_f_seid := serq.CP_F_SEID;

	/* Acting as UPF, invent a new PFCP SEID to send to HNBGW. Respond to the Session Establishment.
	 * The PFCP response must have the same sequence_number as the request. */
	var template F_TEID f_teid1 := ts_PFCP_F_TEID_ipv4(g_pars.gtp_pars.core.local.teid,
							   f_inet_addr(g_pars.gtp_pars.core.local.addr));
	var template F_TEID f_teid2 := ts_PFCP_F_TEID_ipv4(g_pars.gtp_pars.access.local.teid,
							   f_inet_addr(g_pars.gtp_pars.access.local.addr));
	var template Created_PDR pdr1 := ts_PFCP_Created_PDR(pdr_id := serq.create_PDR_list[0].grouped_ie.pdr_id,
							     local_F_TEID := f_teid1);
	var template Created_PDR pdr2 := ts_PFCP_Created_PDR(pdr_id := serq.create_PDR_list[1].grouped_ie.pdr_id,
							     local_F_TEID := f_teid2);
	var template PDU_PFCP r := ts_PFCP_Session_Est_Resp(seq_nr := m.sequence_number,
							    node_id := g_pars.pfcp_pars.upf_node_id,
							    seid := g_pars.pfcp_pars.hnbgw_f_seid.seid,
							    f_seid := g_pars.pfcp_pars.upf_f_seid,
							    created_pdr := {pdr1, pdr2});
	PFCP.send(r);

	/* Expect on Iuh: RAB Assignment Request with IP/port from PFCP Session Est Resp */
	rab_sml := ts_RAB_SML_ps(t_RAB_id(23), f_ts_RAB_TLA(g_pars.gtp_pars.access.local.addr),
				 g_pars.gtp_pars.access.local.teid);
	rx := valueof(ts_RANAP_RabAssReq(rab_sml));
	f_rua_expect(rx);
}

function f_rab_ass_resp_ps_with_pfcp() runs on ConnHdlr {
	var RANAP_PDU tx;
	var RANAP_PDU rx;
	var template PDU_PFCP r;
	var PDU_PFCP m;

	/* Send back RAB Assignment Response via Iuh */
	var template RAB_SetupOrModifiedList rab_smdl;
	rab_smdl := ts_RAB_SMdL_ps(t_RAB_id(23), f_ts_RAB_TLA(g_pars.gtp_pars.access.remote.addr),
				   g_pars.gtp_pars.access.remote.teid);
	tx := valueof(ts_RANAP_RabAssResp(rab_smdl));
	RUA.send(tx);

	m := f_pfcp_expect(tr_PFCP_Session_Mod_Req(g_pars.pfcp_pars.upf_f_seid.seid));
	r := ts_PFCP_Session_Mod_Resp(m.sequence_number, g_pars.pfcp_pars.hnbgw_f_seid.seid);
	PFCP.send(r);

	rab_smdl := ts_RAB_SMdL_ps(t_RAB_id(23), f_ts_RAB_TLA(g_pars.gtp_pars.core.local.addr), g_pars.gtp_pars.core.local.teid);
	f_bssap_expect(tr_RANAP_RabAssResp(rab_smdl));
}

function f_create_rab_ps_with_pfcp() runs on ConnHdlr {
	f_rab_ass_req_ps_with_pfcp();
	f_rab_ass_resp_ps_with_pfcp();
}

function f_rab_ass_req_ps_without_pfcp() runs on ConnHdlr {
	var RANAP_PDU tx;
	var RANAP_PDU rx;

	/* ask PFCPEM to route all PDUs to us */
	f_PFCPEM_subscribe_bcast();
	activate(as_disallow_pfcp());

	var template RAB_SetupOrModifyList rab_sml;
	rab_sml := ts_RAB_SML_ps(t_RAB_id(23), f_ts_RAB_TLA(g_pars.gtp_pars.core.remote.addr), g_pars.gtp_pars.core.remote.teid);
	tx := valueof(ts_RANAP_RabAssReq(rab_sml));
	BSSAP.send(tx);

	/* Expect on Iuh: unmodified RAB Assignment Request */
	rx := valueof(ts_RANAP_RabAssReq(rab_sml));
	f_rua_expect(rx);
}

function f_rab_ass_resp_ps_without_pfcp() runs on ConnHdlr {
	var RANAP_PDU tx;
	var RANAP_PDU rx;

	/* Send back RAB Assignment Response via Iuh */
	var template RAB_SetupOrModifiedList rab_smdl;
	rab_smdl := ts_RAB_SMdL_ps(t_RAB_id(23), f_ts_RAB_TLA(g_pars.gtp_pars.access.remote.addr),
				   g_pars.gtp_pars.access.remote.teid);
	tx := valueof(ts_RANAP_RabAssResp(rab_smdl));
	RUA.send(tx);

	/* Expect on IuPS: unmodified RAB Assignment Response */
	f_bssap_expect(tr_RANAP_RabAssResp(rab_smdl));
}

function f_create_rab_ps_without_pfcp() runs on ConnHdlr {
	f_rab_ass_req_ps_without_pfcp();
	f_rab_ass_resp_ps_without_pfcp();
}

function f_create_rab_ps() runs on ConnHdlr {
	if (g_pars.pfcp_pars.pfcp_enabled) {
		f_create_rab_ps_with_pfcp();
	} else {
		f_create_rab_ps_without_pfcp();
	}
}

altstep as_mgcp_dlcx() runs on ConnHdlr {
	var MgcpCommand mgcp_cmd;

	[] MGCP.receive(tr_DLCX(g_pars.mgcp_pars.mgcp_ep)) -> value mgcp_cmd {
		log("DLCX", mgcp_cmd);
		MGCP.send(ts_DLCX_ACK2(mgcp_cmd.line.trans_id));
		g_pars.mgcp_pars.got_dlcx_count := g_pars.mgcp_pars.got_dlcx_count + 1;
		if (g_pars.mgcp_pars.got_dlcx_count != g_pars.mgcp_pars.got_crcx_count) {
			repeat;
		}
		setverdict(pass);
	}
}

function f_pfcp_expect(template (present) PDU_PFCP exp_rx, float wait_time := 5.0) runs on ConnHdlr return PDU_PFCP
{
	var PDU_PFCP rx;
	timer T := wait_time;
	T.start;
	alt {
	[] PFCP.receive(exp_rx) -> value rx {
			setverdict(pass);
		}
	[] PFCP.receive(PDU_PFCP:?) {
			setverdict(fail, "Got an unexpected PFCP message, was waiting for ", exp_rx);
			mtc.stop;
		}
	[] T.timeout {
			setverdict(fail, "Timeout waiting for PFCP ", exp_rx);
			mtc.stop;
		}
	}
	T.stop;
	return rx;
}

altstep as_disallow_pfcp() runs on ConnHdlr {
	[] PFCP.receive(PDU_PFCP:?) {
			setverdict(fail, "Received PFCP message, but no PFCP communication expected");
			mtc.stop;
		}
}

altstep as_pfcp_handle_del_req() runs on ConnHdlr
{
	var template (present) PDU_PFCP exp_rx := tr_PFCP_Session_Del_Req(g_pars.pfcp_pars.upf_f_seid.seid);
	var PDU_PFCP m;

	[] PFCP.receive(exp_rx) -> value m {
		PFCP.send(ts_PFCP_Session_Del_Resp(m.sequence_number, g_pars.pfcp_pars.hnbgw_f_seid.seid));
		/* ask PFCPEM to *not* route PDUs with this specific SEID to us */
		f_PFCPEM_unsubscribe_seid(g_pars.pfcp_pars.upf_f_seid.seid);
	}
	[] PFCP.receive(PDU_PFCP:?) -> value m {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Got an unexpected PFCP message: ", m, " vs exp ", exp_rx));
	}
}

function f_perform_compl_l3(octetstring nas, boolean do_clear := true, boolean expect_success := true)
runs on ConnHdlr {
	timer T := 10.0;

	if (expect_success) {
		/* create an expect on the Iu side for the random NAS portion */
		if (g_pars.expect_separate_sccp_cr) {
			f_ran_register_sccp_cr_without_payload();
		} else {
			f_ran_register_exp(nas);
		}
	}

	/* send Connect via Iuh (creating a RUA connection) */
	var RANAP_PDU tx := f_build_initial_ue_with_nas(g_pars, nas);
	RUA.send(RUA_Conn_Req:{g_pars.ps_domain, tx});

	if (expect_success) {
		/* Expect same message to arrive at CN */
		f_bssap_expect(tx);
	} else {
		RUA.receive(RUA_Disc_Ind:?);
	}
}

} /* module BSC_ConnectionHandler */
