/* 5GC (55G Core) test suite in TTCN-3, ConnHdlr
 * (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * All rights reserved.
 * Author: Pau Espin Pedrol <pespin@sysmocom.de>
 *
 * 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 ConnHdlr {

import from General_Types all;
import from Native_Functions all;
import from IPL4asp_Types all;
import from Misc_Helpers all;

import from Osmocom_Types all;
import from GSM_Types all;

import from DNS_Helpers all;

import from Milenage_Functions all;

import from NGAP_PDU_Descriptions all;
import from NGAP_IEs all;
import from NGAP_PDU_Contents all;
import from NGAP_Constants all;

import from NGAP_Types all;
import from NGAP_Pixits all;
import from NGAP_Templates all;
import from NGAP_Functions all;
import from NGAP_Emulation all;

import from NAS_CommonTypeDefs all;
import from NAS_CommonTemplates all;
import from NG_NAS_Common all;
import from NG_NAS_TypeDefs all;
import from NG_NAS_MsgContainers all;
import from NG_NAS_Templates all;

import from NG_NAS_Osmo_Types all;
import from NG_NAS_Osmo_Templates all;
import from NG_NAS_Functions all;
import from NG_CryptoFunctions all;

import from GTPv1U_Emulation all;
import from UECUPS_Types all;

/* (maximum) number of emulated eNBs */
const integer NUM_NGRAN := 1;

/* parameters of emulated gNB / ng-eNB */
type record NGRANParams {
	GlobalRANNodeID global_ngran_id,
	NGRAN_CGI cell_identity,
	SupportedTAList supported_ta_list
}

type record UeDerivedKeys {
	OCT32 kausf,
	OCT32 kseaf,
	OCT32 kamf
};

type record PDUSessionParams {
	PDUSessionID id,
	RunProgParams run_prog_pars,
	charstring ran_gtpu_ip,
	OCT4 ran_gtpu_teid,
	charstring cn_gtpu_ip optional,
	OCT4 cn_gtpu_teid optional,
	QoS_RuleList qos_rules optional,
	charstring ue_ip optional
};

/* parameters of emulated UE */
type record UeParams {
	integer idx,
	HEX15n imsi,
	HEX16n imeisv,
	octetstring usim_key,
	octetstring usim_opc,
	charstring apn,
	RAN_UE_NGAP_ID ran_id,
	AMF_UE_NGAP_ID amf_id optional,
	octetstring guti optional,
	ProcedureTransactionIdentifier pti,
	PDUSessionParams sess_pars
};

type record RunProgParams {
	charstring run_as_user,
	charstring log_path_prefix,
	charstring tun_netns_name,
	charstring tun_dev_name,
	charstring ping_hostname
};

type record ConnHdlrPars {
	/* copied over from MTC_CT on start of component */
	NGRANParams ngran_pars[NUM_NGRAN],
	/* copied over from MTC_CT on start of component */
	UeParams ue_pars,
	/* currently used 5GC (index into ngran_pars, NGAP, ...) */
	integer c5g_idx,
	/* Currently set KSI */
	NAS_KeySetIdentifier kset_id
}

type component ConnHdlr extends NGAP_ConnHdlr, GTP1U_ConnHdlr {
	var ConnHdlrPars g_pars;
	var UeDerivedKeys g_keys;

	/* number of programs started, used as identifier. */
	var integer g_start_prog_count := 0;
}

type function void_fn() runs on ConnHdlr;

private template (value) NGAP_IEs.TAI ts_ngran_NGAP_TAI(NGRANParams ngran_pars) := {
	pLMNIdentity := ngran_pars.global_ngran_id.globalGNB_ID.pLMNIdentity,
	tAC := ngran_pars.supported_ta_list[0].tAC,
	iE_Extensions := omit
}

private function f_imsi_plmn_id()  runs on ConnHdlr return PLMNIdentity {
	var hexstring imsi := g_pars.ue_pars.imsi;
	var GsmMcc mcc := substr(imsi, 0, 3);
	var GsmMnc mnc := substr(imsi, 3, 2);
	return f_enc_mcc_mnc(mcc, mnc);
}

private function f_SUCI_IMSI() runs on ConnHdlr return octetstring {
	var hexstring imsi := g_pars.ue_pars.imsi;
	var PLMNIdentity plmn_id := f_imsi_plmn_id();
	var octetstring imsi_suffix := imsi_hex2oct(substr(imsi, lengthof(imsi) - 10, 10));
	return plmn_id & '21430001'O & imsi_suffix;
}

private function f_UE_SecurityCapability() runs on ConnHdlr return NG_UE_SecurityCapability {
	var template (value) NG_UE_SecurityCapability ue_sec_cap;
	ue_sec_cap := cs_NG_UE_SecurityCapabilityTLV(ngeaCap := '80'O, /* ONLY NEA0 (no encryption) for now */
						     ngiaCap := '40'O /* ONLY NIA1 supported */);
	return valueof(ue_sec_cap);
}

private function f_ULI() runs on ConnHdlr return UserLocationInformation {
	var template (value) UserLocationInformation p_ueLocInf
	p_ueLocInf := m_uPTransportLayerInformation_userLocationInformationNR(
			m_userLocationInformationNR(
				g_pars.ngran_pars[g_pars.c5g_idx].cell_identity.nR_CGI,
				ts_ngran_NGAP_TAI(g_pars.ngran_pars[g_pars.c5g_idx])

	));
	return valueof(p_ueLocInf);
}

private function f_tr_ConnHdlr_kset_id() runs on ConnHdlr return template (present) NAS_KeySetIdentifier {
	/* KSI not yet set, expect whatever assignment from network: */
	if (g_pars.kset_id.nasKeySetId == tsc_NasKsi_NoKey) {
		return cr_NAS_KeySetIdentifier(?, ?);
	} else {
		/* Expect specific kset_id. Upon transmitting it UE->Network, f_attach()
		 * has updated it to the expected value. */
		return g_pars.kset_id;
	}
}

/* 3GPP TS 24.501 9.6, 3GPP TS 24.007 11.2.3.1a.
 * 0 = "No procedure transaction identity assigned", 255 = "Reserved". */
private function f_next_pti() runs on ConnHdlr return ProcedureTransactionIdentifier
{
	var integer i := oct2int(g_pars.ue_pars.pti);
	if (i >= 254) {
		g_pars.ue_pars.pti := '01'O;
	} else {
		g_pars.ue_pars.pti := int2oct(i + 1, 1);
	}
	return g_pars.ue_pars.pti;
}

/* 3GPP TS 24.501 5.4.1.3.2, 3GPP TS 33.501 6.1.3.2 */
private altstep as_ngap_handle_auth(boolean allow_resync := true) runs on ConnHdlr {
	var NG_NAS_DL_Message_Type rx_nas;
	var template (present) NAS_KeySetIdentifier kset_id := f_tr_ConnHdlr_kset_id();
	[] NGAP.receive(cr_NG_AUTHENTICATION_REQUEST) -> value rx_nas {
		var integer ret;
		var integer my_sqn := 0; /* TODO: move to a ConnHdlr state attribute? */
		var OCT16 rand := bit2oct(rx_nas.authentication_Request.rand.randValue);
		var OCT16 autn := bit2oct(rx_nas.authentication_Request.autn.aUTN);
		var OCT2 abba := rx_nas.authentication_Request.abba.abbaValue;
		var OCT16 ik;
		var OCT16 ck;
		var OCT8 res;
		var OCT14 auts;

		g_pars.kset_id := rx_nas.authentication_Request.ngNasKeySetId;

		ret := f_milenage_check(g_pars.ue_pars.usim_key,
					g_pars.ue_pars.usim_opc,
					int2oct(my_sqn, 8),
					rand, autn, ik, ck, res, auts);
		if (ret == -1) {
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
						log2str("fx_milenage_check() failed! -1"));
		}
		if (ret == -2) {
			if (not allow_resync) {
				Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
							log2str("fx_milenage_check() unexpected Resync!"));
			}
			log("Milenage: RX-SQN differs from SIM SQN: Re-Sync! AUTS=", auts);
			/* 3GPP TS 24.501 5.4.1.3.6 d) */
			NGAP.send(cs_NG_AUTHENTICATION_FAILURE(cs_GMM_GSM_Cause(omit, int2bit(21, 8)),
							       cs_AuthFailParam(oct2bit(auts))));
			as_ngap_handle_auth(allow_resync := false);
			return;
		}

		var octetstring ssn := f_NG_NAS_ServingNetworkName_OCT(f_imsi_plmn_id(), omit);
		g_keys.kausf := f_kdf_kausf(ck, ik, ssn, autn);
		g_keys.kseaf := f_kdf_kseaf(g_keys.kausf, ssn);
		g_keys.kamf := f_kdf_kamf(g_keys.kseaf, char2oct(hex2str(g_pars.ue_pars.imsi)), abba);

		var NGAPEM_Config cfg := {
			set_nas_keys := {
				k_nas_int := f_kdf_ng_nas_int(g_keys.kamf, NG_NAS_ALG_IP_NIA1),
				k_nas_enc := f_kdf_ng_nas_enc(g_keys.kamf, NG_NAS_ALG_ENC_NEA0)
			}
		};
		NGAP.send(cfg);

		/* Derive (X)RES* from (X)RES, 3GPP TS 33.501 A.4  */
		var OCT16 res_star := f_kdf_xres_star(ssn, ck, ik, rand, res);
		NGAP.send(cs_NG_AUTHENTICATION_RESPONSE(cs_AuthenticationResponseParameter(oct2bit(res_star))));
	}
}

private altstep as_ngap_handle_sec_mode() runs on ConnHdlr {
	var NGAPEM_Config cfg;
	var NG_NAS_DL_Message_Type rx_nas;
	/* Make sure network selected specific algos we requested: */
	var template (present) NG_UE_SecurityCapability ue_sec_cap :=
		cr_NG_UE_SecurityCapabilityLV(ngeaCap := '80'O, /* ONLY NEA0 (no encryption) for now */
					      ngiaCap := '40'O /* ONLY NIA1 supported */);

	[] NGAP.receive(cr_NG_SECURITY_MODE_COMMAND(p_UECap := ue_sec_cap)) -> value rx_nas {
		/* Configure integrity protection: */
		cfg := {
			set_nas_alg_int := NG_NAS_ALG_IP_NIA1
		};
		NGAP.send(cfg);
		/* Configure Ciphering: */
		cfg := {
			set_nas_alg_enc := NG_NAS_ALG_ENC_NEA0
		};
		NGAP.send(cfg);

		var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
		var NAS_PDU nas_pdu;
		nas_ul_msg := cs_NG_REGISTRATION_REQUEST(cs_RegistrationType(tsc_NG_RegistrationInitial, '1'B),
							g_pars.kset_id.nasKeySetId,
							g_pars.kset_id.tsc,
							cs_NG_MobileIdentitySUCI('0000'B /* Type IMSI */,
										 f_SUCI_IMSI()),
							p_UESecurityCap := f_UE_SecurityCapability());
		nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
		NGAP.send(cs_NG_SECURITY_MODE_COMPLETE(cs_NG_MobileIdentityTLV_IMEISV(hex2oct(g_pars.ue_pars.imeisv)),
										      cs_ReplayedNASMessageContainerTLV(nas_pdu)));
	}
}

private altstep as_ngap_handle_configuration_update() runs on ConnHdlr {
	var NG_NAS_DL_Message_Type rx_nas;

	[] NGAP.receive(cr_NG_CONFIGURATION_UPDATE_COMMAND) -> value rx_nas {
		NGAP.send(cs_NG_CONFIGURATION_UPDATE_COMPLETE);
	}
}

/* Handle a PDUSessionResourceSetupRequestTransfer contained inside NGAP InitialContextSetupRequest and return a Result for InitialContextSetupResponse */
private function f_pdu_handle_session_resource_setup_item(PDUSessionResourceSetupItemCxtReq req) runs on ConnHdlr return PDUSessionResourceSetupItemCxtRes
{
	var PDUSessionResourceSetupItemCxtRes resp;
	var octetstring setup_trans_enc;
	var NGAP_IEs.PDUSessionResourceSetupRequestTransfer setup_req_transf;
	var NGAP_IEs.PDUSessionResourceSetupResponseTransfer setup_resp_transf;
	var template (value) UPTransportLayerInformation utla;
	var template (value) QosFlowPerTNLInformation qos;

	/* Parse PDUSessionResourceSetupRequestTransfer contained inside InitialContextSetupRequest's PDUSessionResourceSetup Item: */
	setup_req_transf := dec_NGAP_PDUSessionResourceSetupRequestTransfer(req.pDUSessionResourceSetupRequestTransfer);
	for (var integer i := 0; i < lengthof(setup_req_transf.protocolIEs); i := i + 1) {
		if (setup_req_transf.protocolIEs[i].id == id_UL_NGU_UP_TNLInformation) {
			var UPTransportLayerInformation utli := setup_req_transf.protocolIEs[i].value_.uPTransportLayerInformation;
			g_pars.ue_pars.sess_pars.cn_gtpu_ip := f_inet_ntoa(bit2oct(utli.gTPTunnel.transportLayerAddress));
			g_pars.ue_pars.sess_pars.cn_gtpu_teid := utli.gTPTunnel.gTP_TEID;
		}
	}

	/* Prepare Response for it: */
	utla := m_uPTransportLayerInformation_gTPTunnel(
			m_gTPTunnel(p_tla := oct2bit(f_inet_addr(g_pars.ue_pars.sess_pars.ran_gtpu_ip)),
					p_gtp_teid := g_pars.ue_pars.sess_pars.ran_gtpu_teid));
	qos := m_qosFlowPerTNLInformation(utla, { m_associatedQosFlowItem(1) });
	setup_resp_transf := valueof(m_pDUSessionResourceSetupResponseTransfer(qos));
	setup_trans_enc := enc_NGAP_PDUSessionResourceSetupResponseTransfer(setup_resp_transf)
	resp := valueof(m_pDUSessionResourceSetupItemCxtRes(req.pDUSessionID, setup_trans_enc));
	return resp;
}

private function f_pdu_handle_session_resource_setup_list(PDUSessionResourceSetupListCxtReq li_req) runs on ConnHdlr return PDUSessionResourceSetupListCxtRes
{
	var PDUSessionResourceSetupListCxtRes li_resp;
	for (var integer i := 0; i < lengthof(li_req); i := i + 1) {
		var PDUSessionResourceSetupItemCxtRes it_resp;
		it_resp := f_pdu_handle_session_resource_setup_item(li_req[i]);
		if (i == 0) { /* min 1 item in list doesn't allow previously allocating an empty list */
			li_resp := { it_resp };
		} else {
			li_resp := li_resp & { it_resp };
		}
	}
	return li_resp;
}

/* 3GPP TS 23.502 Figure 4.3.2.2.1-1 steps 12-14. */
private altstep as_ngap_handle_InitialCtxReq_withPDUSessionList() runs on ConnHdlr {
	var NGAP_PDU rx_ngap;
	var template (present) NGAP_PDU exp_ngap :=
		mw_ngap_initMsg(f_mw_n2_InitialContextSetupRequest(g_pars.ue_pars.amf_id,
								   g_pars.ue_pars.ran_id,
								   p_uEAggregateMaximumBitRate := ?,
								   p_pDUSessionResourceSetupListCxtReq := ?,
								   p_nextHopNH := ?,
								   p_maskedIMEISV := ?));

	[] NGAP.receive(exp_ngap) -> value rx_ngap {
		var InitialContextSetupRequest ctx_setup_req := rx_ngap.initiatingMessage.value_.initialContextSetupRequest;
		var PDUSessionResourceSetupListCxtRes resources;

		for (var integer i := 0; i < lengthof(ctx_setup_req.protocolIEs); i := i + 1) {
			if (ctx_setup_req.protocolIEs[i].id != id_PDUSessionResourceSetupListCxtReq) {
				continue;
			}
			var PDUSessionResourceSetupListCxtReq li := ctx_setup_req.protocolIEs[i].value_.pDUSessionResourceSetupListCxtReq;
			resources := f_pdu_handle_session_resource_setup_list(li);
		}
		NGAP.send(m_ngap_succMsg(m_n2_InitialContextSetupResponse(g_pars.ue_pars.amf_id,
									  g_pars.ue_pars.ran_id,
									  resources)));
	}
}

/* 3GPP TS 23.502 4.3.2.2 UE Requested PDU Session Establishment */
function f_pdu_sess_establish(boolean configure_userplane := true) runs on ConnHdlr {
	var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
	var NAS_PDU nas_pdu;
	var NG_NAS_DL_Message_Type rx_nas, inner_nas;
	var ProcedureTransactionIdentifier pti := f_next_pti();
	var OCT1 pdu_sess_id := int2oct(g_pars.ue_pars.sess_pars.id, 1);

	nas_ul_msg := cs_NG_PDU_SESSION_ESTABLISHMENT_REQUEST(cs_NG_PDU_SessionIdV(pdu_sess_id), pti,
							      p_IntegrityProtMaxDataRate := cs_IntegrityProtMaxDataRateV('FF'O, 'FF'O),
							      p_PDU_SessionType := cs_PDU_SessionTypeTV('001'B), /* IPv4 */
							      p_SSC_Mode := cs_SSC_ModeTV('001'B));
	nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
	nas_ul_msg := cs_NG_UL_NAS_TRANSPORT(cs_PayloadContainerType(tsc_PayloadContainerESMMsg),
					     cs_PayloadContainerLV(nas_pdu),
					     p_PDU_SessionId := cs_NG_PDU_SessionIdTV(pdu_sess_id),
					     p_RequestType := cs_NG_Request_TypeTV('001'B),
					     p_DNN := cs_DNN_TLV(f_enc_dns_hostname(g_pars.ue_pars.apn)));
	NGAP.send(nas_ul_msg);
	as_ngap_handle_InitialCtxReq_withPDUSessionList();

	/* PDU Session Establishment Accept: This DL NAS arrives contained in InitialCtxReq handled above: */
	NGAP.receive(cr_NG_DL_NAS_TRANSPORT) -> value rx_nas;
	inner_nas := f_dec_NG_NAS_DL_Message_Payload_Container(rx_nas.dl_Nas_Transport.payloadContainerType.container,
							       rx_nas.dl_Nas_Transport.payload.payload);
	g_pars.ue_pars.sess_pars.qos_rules := inner_nas.pdu_Session_Establishment_Accept.authorizedQoSRules.listofQoSRules;
	g_pars.ue_pars.sess_pars.ue_ip := f_inet_ntoa(inner_nas.pdu_Session_Establishment_Accept.pduAddress.adressInfo);
	log("5GC assigned CN GTPU Address: ", g_pars.ue_pars.sess_pars.cn_gtpu_ip, " TEID: ", g_pars.ue_pars.sess_pars.cn_gtpu_teid);
	log("5GC assigned UE IP address: ", g_pars.ue_pars.sess_pars.ue_ip);

	if (configure_userplane) {
		f_pdu_sess_create_tun();
	}
}

private function f_pdu_handle_session_resource_modify_item_qos_flow_list(QosFlowAddOrModifyRequestList qos_flow_req_li) runs on ConnHdlr return QosFlowAddOrModifyResponseList
{
	var QosFlowAddOrModifyResponseList qos_flow_resp_li;
	for (var integer i := 0; i < lengthof(qos_flow_req_li); i := i + 1) {
		var QosFlowAddOrModifyRequestItem qos_flow_req_it := qos_flow_req_li[i];
		var QosFlowAddOrModifyResponseItem qos_flow_resp_it := valueof(m_qosFlowAddOrModifyResponseItem(qos_flow_req_it.qosFlowIdentifier));
		qos_flow_resp_li[i] := qos_flow_resp_it;
	}
	return qos_flow_resp_li;
}

/* Handle a PDUSessionResourceModifyRequestTransfer contained inside NGAP PDUSessionResourceModifyRequest and return a Result for PDUSessionResourceModifyResponse */
private function f_pdu_handle_session_resource_modify_item(PDUSessionResourceModifyItemModReq req) runs on ConnHdlr return PDUSessionResourceModifyItemModRes
{
	var PDUSessionResourceModifyItemModRes resp;
	var NGAP_IEs.PDUSessionResourceModifyRequestTransfer mod_req_transf;
	var NGAP_IEs.PDUSessionResourceModifyResponseTransfer mod_resp_transf;
	var octetstring mod_resp_transf_enc;
	var template (omit) QosFlowAddOrModifyResponseList qos_flow_resp_li := omit;

	mod_req_transf := dec_NGAP_PDUSessionResourceModifyRequestTransfer(req.pDUSessionResourceModifyRequestTransfer);

	for (var integer i := 0; i < lengthof(mod_req_transf.protocolIEs); i := i + 1) {
		select(mod_req_transf.protocolIEs[i].id) {
		case (id_QosFlowAddOrModifyRequestList) {
			var QosFlowAddOrModifyRequestList qos_flow_req_li := mod_req_transf.protocolIEs[i].value_.qosFlowAddOrModifyRequestList;
			qos_flow_resp_li := f_pdu_handle_session_resource_modify_item_qos_flow_list(qos_flow_req_li);
			}
		case (id_UL_NGU_UP_TNLModifyList) {
			// TODO: assign the fields below, see how it's done in f_pdu_handle_session_resource_setup_item()
			// g_pars.ue_pars.sess_pars.cn_gtpu_ip := ;
			// g_pars.ue_pars.sess_pars.cn_gtpu_teid := ;
			// TODO: Update IP+TEID in usperlane osmo-uecups if any of them changed.
			}
		}
	}

	/* Prepare Response for it: */
	mod_resp_transf := valueof(m_pDUSessionResourceModifyResponseTransfer(p_qosFlowAddOrModifyResponseList := qos_flow_resp_li));
	mod_resp_transf_enc := enc_NGAP_PDUSessionResourceModifyResponseTransfer(mod_resp_transf);
	resp := valueof(m_pDUSessionResourceModifyItemModRes(req.pDUSessionID, mod_resp_transf_enc));
	return resp;
}

private function f_pdu_handle_session_resource_modify_list(PDUSessionResourceModifyListModReq li_cmd) runs on ConnHdlr return PDUSessionResourceModifyListModRes
{
	var PDUSessionResourceModifyListModRes li_resp;
	for (var integer i := 0; i < lengthof(li_cmd); i := i + 1) {
		var PDUSessionResourceModifyItemModRes it_resp;
		it_resp := f_pdu_handle_session_resource_modify_item(li_cmd[i]);
		li_resp[i] := it_resp;
	}
	return li_resp;
}

private altstep as_ngap_handle_PDUSessionResourceModifyRequest() runs on ConnHdlr {
	var NGAP_PDU rx_ngap;
	var template (present) NGAP_PDU exp_ngap :=
		mw_ngap_initMsg(mw_n2_PDUSessionResourceModify(g_pars.ue_pars.amf_id,
							       g_pars.ue_pars.ran_id,
							       ?));

	[] NGAP.receive(exp_ngap) -> value rx_ngap {
		var PDUSessionResourceModifyRequest mod_req := rx_ngap.initiatingMessage.value_.PDUSessionResourceModifyRequest;
		var PDUSessionResourceModifyListModRes resources;

		for (var integer i := 0; i < lengthof(mod_req.protocolIEs); i := i + 1) {
			if (mod_req.protocolIEs[i].id != id_PDUSessionResourceModifyListModReq) {
				continue;
			}
			var PDUSessionResourceModifyListModReq li := mod_req.protocolIEs[i].value_.pDUSessionResourceModifyListModReq;
			resources := f_pdu_handle_session_resource_modify_list(li);
		}
		NGAP.send(m_ngap_succMsg(m_n2_PDUSessionResourceModifyResponse(g_pars.ue_pars.amf_id,
									       g_pars.ue_pars.ran_id,
									       resources)));
	}
}

/* 3GPP TS 24.501 8.3.7 PDU session modification request,
 * 3GPP TS 23.502 4.3.3.2 UE requested PDU Session Modification */
function f_pdu_sess_modify() runs on ConnHdlr {
	var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
	var NAS_PDU nas_pdu;
	var NG_NAS_DL_Message_Type rx_nas, inner_nas;
	var ProcedureTransactionIdentifier pti := f_next_pti();
	var OCT1 pdu_sess_id := int2oct(g_pars.ue_pars.sess_pars.id, 1);

	var template (value) QoS_RuleU qos_rule_u := cs_QoS_RuleU_Create(
		g_pars.ue_pars.sess_pars.qos_rules[0].u.creat.flowIdentifier,
		'09'O,
		{cs_PacketFilter_MediaUDP('0110'B, 60350)},
		'0'B,
		g_pars.ue_pars.sess_pars.qos_rules[0].u.creat.spare,
		'1'B
	);
	var template (value) QoS_RuleList qos_rules := {
		cs_QoS_Rule(g_pars.ue_pars.sess_pars.qos_rules[0].identifier,
			    qos_rule_u)
	};

	/* 3GPP TS 24.501 Table 8.3.7.1. */
	nas_ul_msg := cs_NG_PDU_SESSION_MODIFICATION_REQUEST(cs_NG_PDU_SessionIdV(pdu_sess_id), pti,
							     p_IntegrityProtMaxDataRate := cs_IntegrityProtMaxDataRateTV('FF'O, 'FF'O),
							     p_QoS_Rules := cs_QoS_Rules(qos_rules, p_IEI := '7A'O));
	nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
	nas_ul_msg := cs_NG_UL_NAS_TRANSPORT(cs_PayloadContainerType(tsc_PayloadContainerESMMsg),
					     cs_PayloadContainerLV(nas_pdu),
					     p_PDU_SessionId := cs_NG_PDU_SessionIdTV(pdu_sess_id),
					     p_RequestType := cs_NG_Request_TypeTV('001'B),
					     p_DNN := cs_DNN_TLV(f_enc_dns_hostname(g_pars.ue_pars.apn)));
	NGAP.send(nas_ul_msg);
	as_ngap_handle_PDUSessionResourceModifyRequest();

	/* 5G-NAS PDU Session Modify Request: This DL NAS arrives contained in NGAP PDUSessionModifyRequest handled above: */
	NGAP.receive(cr_NG_DL_NAS_TRANSPORT) -> value rx_nas;
	inner_nas := f_dec_NG_NAS_DL_Message_Payload_Container(rx_nas.dl_Nas_Transport.payloadContainerType.container,
							       rx_nas.dl_Nas_Transport.payload.payload);
	if (not match(inner_nas, cr_NG_PDU_SESSION_MODIFICATION_COMMAND(cs_NG_PDU_SessionIdV(pdu_sess_id),
									pti))) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Unexpected decoded NAS Payload Container ", inner_nas));
	}

	nas_ul_msg := cs_NG_PDU_SESSION_MODIFICATION_COMPLETE(cs_NG_PDU_SessionIdV(pdu_sess_id),
							      pti);
	nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
	nas_ul_msg := cs_NG_UL_NAS_TRANSPORT(cs_PayloadContainerType(tsc_PayloadContainerESMMsg),
					     cs_PayloadContainerLV(nas_pdu),
					     p_PDU_SessionId := cs_NG_PDU_SessionIdTV(pdu_sess_id));
	NGAP.send(nas_ul_msg);
}

function f_register() runs on ConnHdlr {
	var template (value) NGAP_PDU tx_pdu;
	var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
	var NAS_PDU nas_pdu;
	var NG_NAS_DL_Message_Type rx_nas;

	nas_ul_msg := cs_NG_REGISTRATION_REQUEST(cs_RegistrationType(tsc_NG_RegistrationInitial, '1'B),
						 g_pars.kset_id.nasKeySetId,
						 g_pars.kset_id.tsc,
						 cs_NG_MobileIdentitySUCI('0000'B /* Type IMSI */,
									  f_SUCI_IMSI()),
						 p_UESecurityCap := f_UE_SecurityCapability());
	nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
	tx_pdu := m_ngap_initMsg(m_n2_initialUeMessage(g_pars.ue_pars.ran_id,
						       nas_pdu, /* Registration request */
						       f_ULI(),
						       mo_Signalling));
	NGAP.send(tx_pdu);

	/* Expect updated KSI from network following 5G Core standards
	*(open5gs.git 70310979c58fe186e9eaa06bec9d9a31f24ff7a1): */
	if (g_pars.kset_id.nasKeySetId != tsc_NasKsi_NoKey) {
		if (g_pars.kset_id.nasKeySetId == '110'B) {
			g_pars.kset_id.nasKeySetId := '000'B;
		} else {
			g_pars.kset_id.nasKeySetId := int2bit(bit2int(g_pars.kset_id.nasKeySetId) + 1, 3);
		}
	}

	as_ngap_handle_auth();
	as_ngap_handle_sec_mode();

	/* After receiving DL NAS Transport above, the UE-AMF-Id became known in NGAP_Emulation: */
	g_pars.ue_pars.amf_id := f_ngap_obtain_amf_id();

	NGAP.receive(cr_NG_REGISTRATION_ACCEPT) -> value rx_nas;
	g_pars.ue_pars.guti := rx_nas.registration_Accept.guti.otherDigits;
	NGAP.send(cs_NG_REGISTRATION_COMPLETE);

	as_ngap_handle_configuration_update();
}

function f_periodic_register_update() runs on ConnHdlr {
	var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
	var NG_NAS_DL_Message_Type rx_nas;

	nas_ul_msg := cs_NG_REGISTRATION_REQUEST(cs_RegistrationType(tsc_NG_RegistrationPeriodic, '1'B),
						 g_pars.kset_id.nasKeySetId,
						 g_pars.kset_id.tsc,
						 cs_NG_MobileIdentity_Guti(omit, g_pars.ue_pars.guti),
						 p_UESecurityCap := f_UE_SecurityCapability());
	NGAP.send(nas_ul_msg);

	/* Expect updated KSI from network following 5G Core standards
	* (open5gs.git 70310979c58fe186e9eaa06bec9d9a31f24ff7a1): */
	if (g_pars.kset_id.nasKeySetId != tsc_NasKsi_NoKey) {
		if (g_pars.kset_id.nasKeySetId == '110'B) {
			g_pars.kset_id.nasKeySetId := '000'B;
		} else {
			g_pars.kset_id.nasKeySetId := int2bit(bit2int(g_pars.kset_id.nasKeySetId) + 1, 3);
		}
	}

	NGAP.receive(cr_NG_REGISTRATION_ACCEPT) -> value rx_nas;
	g_pars.ue_pars.guti := rx_nas.registration_Accept.guti.otherDigits;
	NGAP.send(cs_NG_REGISTRATION_COMPLETE);

	as_ngap_handle_configuration_update();
}

/* 3GPP TS 24.501 5.5.2.2 UE-initiated de-registration procedure,
 * 3GPP TS 23.502 4.2.2.3.2 UE-initiated Deregistration */
function f_deregister() runs on ConnHdlr {
	var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
	var NG_NAS_DL_Message_Type rx_nas;

	nas_ul_msg := cs_NG_DEREGISTRATION_REQUEST_MO(crs_DeregisterType ('0'B,/* p_SwitchOff */
									  '0'B,/* p_ReReg */
									  '01'B/* p_Access */),
						      g_pars.kset_id,
						      cs_NG_MobileIdentity_Guti(omit, g_pars.ue_pars.guti));
	NGAP.send(nas_ul_msg);

	NGAP.receive(cr_NG_DEREGISTRATION_ACCEPT_MO);
	as_ngap_handle_UeContextReleaseCmd();
}

/* 3GPP TS 38.413 8.3.3 UE Context Release (AMF initiated) */
private altstep as_ngap_handle_UeContextReleaseCmd(template (present) Cause exp_cause := ?) runs on ConnHdlr {
	var NGAP_PDU rx_ngap;
	var template (present) NGAP_PDU exp_ngap :=
		mw_ngap_initMsg(mw_n2_UEContextReleaseCommand(
					mw_uE_NGAP_IDs_uE_NGAP_ID_pair(
						mw_uE_NGAP_ID_pair(g_pars.ue_pars.amf_id,
								   g_pars.ue_pars.ran_id)),
					p_cause := exp_cause));

	[] NGAP.receive(exp_ngap) -> value rx_ngap {
		NGAP.send(m_ngap_succMsg(f_ts_n2_UEContextReleaseComplete(g_pars.ue_pars.amf_id,
									  g_pars.ue_pars.ran_id,
									  f_ULI())));
	}
}

/* 3GPP TS 38.413 8.3.2 UE Context Release Request (NG-RAN node initiated),
 * 3GPP TS 23.502 4.2.6 AN Release */
function f_ue_context_release(template (value) Cause cause := m_cause_radioNetwork(unspecified)) runs on ConnHdlr {
	var template (value) NGAP_PDU tx_pdu;

	if (ispresent(g_pars.ue_pars.sess_pars.cn_gtpu_teid)) {
		var template (value) PDUSessionResourceListCxtRelReq li_req;
		var template (value) PDUSessionResourceItemCxtRelReq it_req;
		it_req := m_pDUSessionResourceItemCxtRelReq(g_pars.ue_pars.sess_pars.id);
		li_req := { it_req };
		tx_pdu := m_ngap_initMsg(m_n2_UEContextReleaseRequest_withPDUSessionList(
						g_pars.ue_pars.amf_id,
						g_pars.ue_pars.ran_id,
						li_req,
						cause));
	} else {
		tx_pdu := m_ngap_initMsg(m_n2_UEContextReleaseRequest(
						g_pars.ue_pars.amf_id,
						g_pars.ue_pars.ran_id,
						cause));
	}
	NGAP.send(tx_pdu);
	as_ngap_handle_UeContextReleaseCmd(exp_cause := cause);
}

/* Handle a PDUSessionResourceSetupRequestTransfer contained inside NGAP InitialContextSetupRequest and return a Result for InitialContextSetupResponse */
private function f_pdu_handle_session_resource_released_item(PDUSessionResourceToReleaseItemRelCmd cmd) runs on ConnHdlr return PDUSessionResourceReleasedItemRelRes
{
	var PDUSessionResourceReleasedItemRelRes resp;
	var NAS_CauseValue_Type nas_cause;

	nas_cause := oct2bit(cmd.pDUSessionResourceReleaseCommandTransfer);

	g_pars.ue_pars.sess_pars.cn_gtpu_ip := omit;
	g_pars.ue_pars.sess_pars.cn_gtpu_teid := omit;

	/* Prepare Response for it: */
	resp := valueof(m_pDUSessionResourceReleasedItemRelRes(cmd.pDUSessionID, bit2oct(nas_cause)));
	return resp;
}

private function f_pdu_handle_session_resource_released_list(PDUSessionResourceToReleaseListRelCmd li_cmd) runs on ConnHdlr return PDUSessionResourceReleasedListRelRes
{
	var PDUSessionResourceReleasedListRelRes li_resp;
	for (var integer i := 0; i < lengthof(li_cmd); i := i + 1) {
		var PDUSessionResourceReleasedItemRelRes it_resp;
		it_resp := f_pdu_handle_session_resource_released_item(li_cmd[i]);
		if (i == 0) { /* min 1 item in list doesn't allow previously allocating an empty list */
			li_resp := { it_resp };
		} else {
			li_resp := li_resp & { it_resp };
		}
	}
	return li_resp;
}

private altstep as_ngap_handle_PDUSessionReleaseCommand() runs on ConnHdlr {
	var NGAP_PDU rx_ngap;
	var template (present) NGAP_PDU exp_ngap :=
		mw_ngap_initMsg(f_tr_n2_PDUSessionResourceReleaseCommand(g_pars.ue_pars.amf_id,
									 g_pars.ue_pars.ran_id,
									 ?, ?));

	[] NGAP.receive(exp_ngap) -> value rx_ngap {
		var PDUSessionResourceReleaseCommand rel_cmd := rx_ngap.initiatingMessage.value_.PDUSessionResourceReleaseCommand;
		var PDUSessionResourceReleasedListRelRes resources;

		for (var integer i := 0; i < lengthof(rel_cmd.protocolIEs); i := i + 1) {
			if (rel_cmd.protocolIEs[i].id != id_PDUSessionResourceToReleaseListRelCmd) {
				continue;
			}
			var PDUSessionResourceToReleaseListRelCmd li := rel_cmd.protocolIEs[i].value_.pDUSessionResourceToReleaseListRelCmd;
			resources := f_pdu_handle_session_resource_released_list(li);
		}
		NGAP.send(m_ngap_succMsg(m_n2_PDUSessionResourceReleaseResponse(g_pars.ue_pars.amf_id,
										g_pars.ue_pars.ran_id,
										resources)));
	}
}

function f_pdu_sess_release(boolean configure_userplane := true) runs on ConnHdlr {
	var template (value) NG_NAS_UL_Message_Type nas_ul_msg;
	var NAS_PDU nas_pdu;
	var NG_NAS_DL_Message_Type rx_nas, inner_nas;
	var ProcedureTransactionIdentifier pti := f_next_pti();
	var NAS_CauseValue_Type cause_val := c_5GSM_Cause_RegularDeactivation;
	var OCT1 pdu_sess_id := int2oct(g_pars.ue_pars.sess_pars.id, 1);

	nas_ul_msg := cs_NG_PDU_SESSION_RELEASE_REQUEST(cs_NG_PDU_SessionIdV(pdu_sess_id), pti,
							p_Cause := cs_GMM_GSM_Cause('59'O, cause_val));
	nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
	nas_ul_msg := cs_NG_UL_NAS_TRANSPORT(cs_PayloadContainerType(tsc_PayloadContainerESMMsg),
					     cs_PayloadContainerLV(nas_pdu),
					     p_PDU_SessionId := cs_NG_PDU_SessionIdTV(pdu_sess_id),
					     p_RequestType := cs_NG_Request_TypeTV('001'B),
					     p_DNN := cs_DNN_TLV(f_enc_dns_hostname(g_pars.ue_pars.apn)));
	NGAP.send(nas_ul_msg);
	as_ngap_handle_PDUSessionReleaseCommand();

	/* 5G-NAS PDU Session Release Command: This DL NAS arrives contained in NGAP PDUSessionReleaseCommand handled above: */
	NGAP.receive(cr_NG_DL_NAS_TRANSPORT) -> value rx_nas;
	inner_nas := f_dec_NG_NAS_DL_Message_Payload_Container(rx_nas.dl_Nas_Transport.payloadContainerType.container,
							       rx_nas.dl_Nas_Transport.payload.payload);
	if (not match(inner_nas, cr_NG_PDU_SESSION_RELEASE_COMMAND(cs_NG_PDU_SessionIdV(pdu_sess_id), pti,
							           cs_GMM_GSM_Cause(omit, cause_val)))) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Unexpected decoded NAS Payload Container ", inner_nas));
	}

	g_pars.ue_pars.sess_pars.qos_rules := omit;
	g_pars.ue_pars.sess_pars.ue_ip := omit;

	nas_ul_msg := cs_NG_PDU_SESSION_RELEASE_COMPLETE(cs_NG_PDU_SessionIdV(pdu_sess_id), pti,
							 p_Cause := cs_GMM_GSM_Cause('59'O, cause_val));
	nas_pdu := enc_NG_NAS_UL_Message_Type(valueof(nas_ul_msg));
	nas_ul_msg := cs_NG_UL_NAS_TRANSPORT(cs_PayloadContainerType(tsc_PayloadContainerESMMsg),
					     cs_PayloadContainerLV(nas_pdu),
					     p_PDU_SessionId := cs_NG_PDU_SessionIdTV(pdu_sess_id));
	NGAP.send(nas_ul_msg);

	if (configure_userplane) {
		f_pdu_sess_delete_tun();
	}
}

function f_pdu_sess_create_tun() runs on ConnHdlr {
	var template (value) UECUPS_GtpExtHdr gtp_ext_hdr;
	var template (value) UECUPS_CreateTun uecups_create;
	var template (value) UECUPS_GtpExtHdr_PduSessContainer pdu_sess_cont;

	pdu_sess_cont := ts_UECUPS_GtpExtHdr_PduSessContainer(UECUPS_GtpExtHdr_PduSessContainer_Type_ul_pdu_sess_info,
							      oct2int(g_pars.ue_pars.sess_pars.qos_rules[0].identifier));
	gtp_ext_hdr := ts_UECUPS_GtpExtHdr(pdu_session_container := pdu_sess_cont);

	uecups_create := ts_UECUPS_CreateTun(
		tx_teid := oct2int(g_pars.ue_pars.sess_pars.cn_gtpu_teid),
		rx_teid := oct2int(g_pars.ue_pars.sess_pars.ran_gtpu_teid),
		user_addr_type := IPV4,
		user_addr := f_inet_addr(g_pars.ue_pars.sess_pars.ue_ip),
		local_gtp_ep := valueof(ts_UECUPS_SockAddr(f_inet_addr(g_pars.ue_pars.sess_pars.ran_gtpu_ip))),
		remote_gtp_ep := valueof(ts_UECUPS_SockAddr(f_inet_addr(g_pars.ue_pars.sess_pars.cn_gtpu_ip))),
		tun_dev_name := g_pars.ue_pars.sess_pars.run_prog_pars.tun_dev_name,
		tun_netns_name := g_pars.ue_pars.sess_pars.run_prog_pars.tun_netns_name,
		gtp_ext_hdr := gtp_ext_hdr
	);

	f_gtp1u_create_tunnel(uecups_create);
}

/* destroy tunnel in daemon */
function f_pdu_sess_delete_tun() runs on ConnHdlr {
	var template (value) UECUPS_DestroyTun uecups_destroy;
	uecups_destroy := ts_UECUPS_DestroyTun(
		local_gtp_ep := valueof(ts_UECUPS_SockAddr(f_inet_addr(g_pars.ue_pars.sess_pars.ran_gtpu_ip))),
		rx_teid := oct2int(g_pars.ue_pars.sess_pars.ran_gtpu_teid)
	);
	f_gtp1u_destroy_tunnel(uecups_destroy);
}

private function f_run_prog_init(charstring command) runs on ConnHdlr return UECUPS_StartProgram
{
	var template (value) UECUPS_StartProgram sprog := ts_UECUPS_StartProgram(
		command := command,
		environment := {},
		run_as_user := g_pars.ue_pars.sess_pars.run_prog_pars.run_as_user,
		tun_netns_name := g_pars.ue_pars.sess_pars.run_prog_pars.tun_netns_name
	);
	return valueof(sprog);
}

private function f_run_prog_unique_log_path() runs on ConnHdlr return charstring
{
	var charstring id := testcasename() & "-" & hex2str(g_pars.ue_pars.imsi) & "-" & int2str(g_start_prog_count);
	var charstring prefix :=  g_pars.ue_pars.sess_pars.run_prog_pars.log_path_prefix & "/" & id;
	g_start_prog_count := g_start_prog_count + 1;
	return prefix;
}

/* execute ping command and wait for result */
function f_ping4(charstring host, integer interval := 1, integer count := 10) runs on ConnHdlr
{
	/* command will be filled in by f_gtp1u_ping4() below: */
	var UECUPS_StartProgram sprog := f_run_prog_init("");
	f_gtp1u_ping4(sprog, host, interval, count, g_pars.ue_pars.sess_pars.ue_ip,
		      redirect_output := true,
		      redirect_output_path_prefix := f_run_prog_unique_log_path());
}

}
