/* OsmoS1GW (S1AP Gateway) ConnHdlr
 *
 * (C) 2024 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * Author: Vadim Yanitskiy <vyanitskiy@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
 */

module S1GW_ConnHdlr {

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

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

import from S1AP_CodecPort all;
import from S1AP_CodecPort_CtrlFunct all;
import from S1AP_Types all;
import from S1AP_Templates all;
import from S1AP_PDU_Descriptions all;
import from S1AP_IEs all;
import from S1AP_PDU_Contents all;
import from S1AP_Constants all;

import from SCTP_Templates all;

import from StatsD_Types all;
import from StatsD_Checker all;

import from S1AP_Server all;

type component ConnHdlr extends MutexCT, S1APSRV_ConnHdlr, PFCP_ConnHdlr, StatsD_ConnHdlr {
	var ConnHdlrPars g_pars;
	port S1AP_CODEC_PT S1AP_ENB;
	var ConnectionId g_s1ap_conn_id := -1;
};
type record of ConnHdlr ConnHdlrList;

type record ConnHdlrPars {
	integer idx,
	Global_ENB_ID genb_id,
	charstring statsd_prefix,
	charstring pfcp_loc_addr,
	charstring pfcp_rem_addr,
	MME_UE_S1AP_ID mme_ue_id,
	ERabList erabs
};

template Global_ENB_ID
ts_Global_ENB_ID(integer enb_id := 0,
		 OCT3 plmn_id := '00f110'O) := {
	pLMNidentity := plmn_id,
	eNB_ID := {
		macroENB_ID := int2bit(enb_id, 20)
	},
	iE_Extensions := omit
}

type function void_fn(charstring id) runs on ConnHdlr;

function f_ConnHdlr_init(void_fn fn, charstring id, ConnHdlrPars pars)
runs on ConnHdlr {
	g_pars := pars;
	fn.apply(id);
}


function f_ConnHdlr_s1ap_connect(charstring local_addr, charstring remote_addr) runs on ConnHdlr {
	var Result res;
	timer T;

	map(self:S1AP_ENB, system:S1AP_CODEC_PT);

	/* initiate SCTP connection establishment */
	res := S1AP_CodecPort_CtrlFunct.f_IPL4_connect(S1AP_ENB,
						       remote_addr, 36412,
						       local_addr, 0, -1,
						       { sctp := c_SctpTuple_S1AP });
	if (not ispresent(res.connId)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					"Could not create an S1AP socket, check your configuration");
	}
	g_s1ap_conn_id := res.connId;

	/* wait for the establishment confirmation */
	T.start(2.0);
	alt {
	[] S1AP_ENB.receive(tr_SctpAssocChange(SCTP_COMM_UP, g_s1ap_conn_id)) {
		log("eNB connection established");
		}
	[] S1AP_ENB.receive(PortEvent:{sctpEvent := ?}) { repeat; }
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					"eNB connection establishment timeout");
		}
	}
}

function f_ConnHdlr_s1ap_disconnect() runs on ConnHdlr {
	var Result res;

	S1AP_CodecPort_CtrlFunct.f_IPL4_close(S1AP_ENB, g_s1ap_conn_id,
					      { sctp := c_SctpTuple_S1AP });
	g_s1ap_conn_id := -1;
	unmap(self:S1AP_ENB, system:S1AP_CODEC_PT);

	S1AP_CONN.receive(S1APSRV_Event:S1APSRV_EVENT_CONN_DOWN);

	log("eNB connection closed");
}

function f_ConnHdlr_s1ap_expect_shutdown() runs on ConnHdlr {
	S1AP_ENB.receive(tr_SctpShutDownEvent(g_s1ap_conn_id));
	S1AP_ENB.receive(tr_SctpAssocChange(SCTP_SHUTDOWN_COMP, g_s1ap_conn_id));
	S1AP_ENB.receive(PortEvent:{connClosed := ?});
}

function f_ConnHdlr_tx_s1ap_from_enb(template (value) S1AP_PDU pdu)
runs on ConnHdlr {
	S1AP_ENB.send(t_S1AP_Send(g_s1ap_conn_id, pdu));
}

function f_ConnHdlr_tx_s1ap_from_mme(template (value) S1AP_PDU pdu)
runs on ConnHdlr {
	S1AP_CONN.send(pdu);
}

function f_ConnHdlr_rx_s1ap_from_enb(out S1AP_PDU pdu,
				     template (present) S1AP_PDU tr_pdu := ?,
				     float Tval := 3.0)
runs on ConnHdlr {
	timer T := Tval;

	T.start;
	alt {
	[] S1AP_CONN.receive(tr_pdu) -> value pdu {
		setverdict(pass);
		T.stop;
		}
	[] S1AP_CONN.receive(S1AP_PDU:?) -> value pdu {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Rx unexpected S1AP PDU from eNB: ", pdu));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for S1AP PDU from eNB: ", tr_pdu));
		}
	}
}

function f_ConnHdlr_rx_s1ap_from_mme(out S1AP_PDU pdu,
				     template (present) S1AP_PDU tr_pdu := ?,
				     float Tval := 3.0)
runs on ConnHdlr {
	var S1AP_RecvFrom recv;
	timer T := Tval;

	T.start;
	alt {
	[] S1AP_ENB.receive(t_S1AP_RecvFrom(tr_pdu)) -> value recv {
		pdu := recv.msg;
		setverdict(pass);
		T.stop;
		}
	[] S1AP_ENB.receive(t_S1AP_RecvFrom(?)) -> value recv {
		pdu := recv.msg;
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Rx unexpected S1AP PDU from MME: ", pdu));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for S1AP PDU from MME: ", tr_pdu));
		}
	}
}

function f_ConnHdlr_s1ap_setup(Global_ENB_ID genb_id, float Tval := 5.0) runs on ConnHdlr {
	var S1AP_PDU pdu;
	timer T;

	var SupportedTAs supported_tas_dummy := {
		{
			tAC := '0000'O,
			broadcastPLMNs := { '00f000'O },
			iE_Extensions := omit
		}
	};

	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_SetupReq(genb_id,
						     supported_tas_dummy,
						     v32));
	T.start(Tval);
	alt {
	[] S1AP_CONN.receive(S1APSRV_Event:S1APSRV_EVENT_CONN_UP) { repeat; }
	[] S1AP_CONN.receive(tr_S1AP_SetupReq) {
		var template (value) PLMNidentity plmn_id := '00f110'O;
		var template (value) MME_Group_ID mme_group_id := '0011'O;
		var template (value) MME_Code mme_code := '55'O;
		var template (value) ServedGUMMEIsItem gummei := ts_S1AP_ServedGUMMEIsItem(
			{ plmn_id },
			{ mme_group_id },
			{ mme_code }
		);
		f_ConnHdlr_tx_s1ap_from_mme(ts_S1AP_SetupResp({ gummei }, 1));
		f_ConnHdlr_rx_s1ap_from_mme(pdu, tr_S1AP_SetupResp({ gummei }, 1));
		T.stop;
		}
	[] S1AP_CONN.receive(S1AP_PDU:?) -> value pdu {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Rx unexpected S1AP PDU: ", pdu));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for S1AP SetupReq, idx = ", g_pars.idx));
		}
	}
}

private function f_ConnHdlr_pfcp_assoc_setup()
runs on ConnHdlr
{
	var PDU_PFCP rx;

	f_PFCPEM_subscribe_bcast(); /* ask PFCPEM to route all PDUs to us */
	rx := f_ConnHdlr_pfcp_expect(tr_PFCP_Assoc_Setup_Req, Tval := 10.0);
	f_PFCPEM_unsubscribe_bcast(); /* ask PFCPEM to *not* route all PDUs to us */
	PFCP.send(ts_PFCP_Assoc_Setup_Resp(rx.sequence_number,
					   ts_PFCP_Node_ID_fqdn("\07osmocom\03org"),
					   ts_PFCP_Cause(REQUEST_ACCEPTED),
					   f_PFCPEM_get_recovery_timestamp()));
}

function f_ConnHdlr_pfcp_expect(template (present) PDU_PFCP exp_rx := ?,
				float Tval := 2.0)
runs on ConnHdlr return PDU_PFCP
{
	var PDU_PFCP pdu;
	timer T;

	T.start(Tval);
	alt {
	[] PFCP.receive(exp_rx) -> value pdu { T.stop; }
	[] PFCP.receive(PDU_PFCP:?) -> value pdu {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Got an unexpected PFCP PDU: ", pdu));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for PFCP ", exp_rx));
		}
	}

	return pdu;
}

function f_ConnHdlr_pfcp_assoc_handler(charstring id)
runs on ConnHdlr {
	var charstring key_name := g_pars.statsd_prefix & "gauge.pfcp.associated.value";
	var StatsDMetricKeys statsd_keys := { valueof(ts_StatsDMetricKey(key_name, "g")) };
	var StatsDMetrics statsd_snapshot := f_statsd_snapshot(statsd_keys, since_last_snapshot := false);
	var boolean pfcp_associated := statsd_snapshot[0].val == 1;

	if (not pfcp_associated) {
		log("Waiting for IUT to associate over PFCP");
		f_ConnHdlr_pfcp_assoc_setup();
	}

	setverdict(pass);
}

/* 256 is maxnoofE-RABs (see S1AP_Constants.asn) */
type record length(1..256) of ERab ERabList;
type record ERab {
	E_RAB_ID erab_id,	/* ::= INTEGER (0..15, ...) */
	ERabParams u2c,		/* UPF -> Core params */
	ERabParams c2u,		/* Core -> UPF params */
	ERabParams a2u,		/* Access -> UPF params */
	ERabParams u2a,		/* UPF -> Access params */
	OCT8 pfcp_loc_seid,
	OCT8 pfcp_rem_seid optional
};

type record ERabParams {
	GTP_TEID teid,		/* ::= OCTET STRING (SIZE (4)) */
	charstring tla		/* Transport Layer Address */
};

private const S1AP_IEs.Cause c_REL_CMD_CAUSE := { nas := normal_release };

private const LIN4_BO_LAST c_PFCP_FAR_ID_C2A := 1; /* Core -> Access */
private const LIN4_BO_LAST c_PFCP_FAR_ID_A2C := 2; /* Access -> Core */

function f_ConnHdlr_tx_erab_setup_req(in ERabList erabs)
runs on ConnHdlr {
	var template (value) E_RABToBeSetupListBearerSUReq items;
	var template (value) E_RABLevelQoSParameters qos_params;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	qos_params := valueof(ts_E_RABLevelQoSParameters);

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (value) E_RABToBeSetupItemBearerSUReq item;
		var template (value) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].u2c;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := ts_S1AP_RABToBeSetupItemBearerSUReq(rab_id := erabs[i].erab_id,
							    qos_params := qos_params,
							    tla := tla,
							    gtp_teid := epars.teid,
							    nas_pdu := ''O);
		items[i] := ts_S1AP_RABToBeSetupListBearerSUReq(item)[0];
	}

	f_ConnHdlr_tx_s1ap_from_mme(ts_S1AP_RABSetupReq(g_pars.mme_ue_id, enb_ue_id, items));
}

function f_ConnHdlr_rx_erab_setup_req(in ERabList erabs)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABToBeSetupListBearerSUReq items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (present) E_RABToBeSetupItemBearerSUReq item;
		var template (present) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].a2u;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := tr_S1AP_RABToBeSetupItemBearerSUReq(rab_id := erabs[i].erab_id,
							    qos_params := ?,
							    tla := tla,
							    gtp_teid := epars.teid,
							    nas_pdu := ''O);
		items[i] := tr_S1AP_RABToBeSetupListBearerSUReq(item)[0];
	}

	f_ConnHdlr_rx_s1ap_from_mme(pdu, tr_S1AP_RABSetupReq(g_pars.mme_ue_id, enb_ue_id, items));
	return pdu;
}

function f_ConnHdlr_tx_erab_setup_rsp(in ERabList erabs)
runs on ConnHdlr {
	var template (value) E_RABSetupListBearerSURes items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (value) E_RABSetupItemBearerSURes item;
		var template (value) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].u2a;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := ts_S1AP_RABSetupItemBearerSURes(rab_id := erabs[i].erab_id,
							tla := tla,
							gtp_teid := epars.teid);
		items[i] := ts_S1AP_RABSetupListBearerSURes(item)[0];
	}

	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_RABSetupRsp(g_pars.mme_ue_id, enb_ue_id, items));
}

function f_ConnHdlr_rx_erab_setup_rsp(in ERabList erabs)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABSetupListBearerSURes items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (present) E_RABSetupItemBearerSURes item;
		var template (present) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].c2u;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := tr_S1AP_RABSetupItemBearerSURes(rab_id := erabs[i].erab_id,
							tla := tla,
							gtp_teid := epars.teid);
		items[i] := tr_S1AP_RABSetupListBearerSURes(item)[0];
	}

	f_ConnHdlr_rx_s1ap_from_enb(pdu, tr_S1AP_RABSetupRsp(g_pars.mme_ue_id, enb_ue_id, items));
	return pdu;
}

private function f_ts_E_RABList(in ERabList erabs,
				S1AP_IEs.Cause cause)
return template (value) E_RABList {
	var template (value) E_RABList items;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (value) E_RABItem item;

		item := ts_E_RABItem(erabs[i].erab_id, cause);
		items[i] := ts_E_RABList(item)[0];
	}

	return items;
}
private function f_tr_E_RABList(in ERabList erabs,
				S1AP_IEs.Cause cause)
return template (present) E_RABList {
	var template (present) E_RABList items;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (present) E_RABItem item;

		item := tr_E_RABItem(erabs[i].erab_id, cause);
		items[i] := tr_E_RABList(item)[0];
	}

	return items;
}

function f_ConnHdlr_tx_erab_release_cmd(in ERabList erabs,
					S1AP_IEs.Cause cause := c_REL_CMD_CAUSE)
runs on ConnHdlr {
	var template (value) E_RABList items := f_ts_E_RABList(erabs, cause);
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	f_ConnHdlr_tx_s1ap_from_mme(ts_S1AP_RABReleaseCmd(g_pars.mme_ue_id, enb_ue_id, items));
}

function f_ConnHdlr_rx_erab_release_cmd(in ERabList erabs,
					S1AP_IEs.Cause cause := c_REL_CMD_CAUSE)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABList items := f_tr_E_RABList(erabs, cause);
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	f_ConnHdlr_rx_s1ap_from_mme(pdu, tr_S1AP_RABReleaseCmd(g_pars.mme_ue_id, enb_ue_id, items));
	return pdu;
}

function f_ConnHdlr_tx_erab_release_rsp(in ERabList erabs)
runs on ConnHdlr {
	var template (value) E_RABReleaseListBearerRelComp items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (value) E_RABReleaseItemBearerRelComp item;

		item := ts_E_RABReleaseItemBearerRelComp(erabs[i].erab_id);
		items[i] := ts_E_RABReleaseListBearerRelComp(item)[0];
	}

	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_RABReleaseRsp(g_pars.mme_ue_id, enb_ue_id, items));
}

function f_ConnHdlr_rx_erab_release_rsp(in ERabList erabs)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABReleaseListBearerRelComp items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (present) E_RABReleaseItemBearerRelComp item;

		item := tr_E_RABReleaseItemBearerRelComp(erabs[i].erab_id);
		items[i] := tr_E_RABReleaseListBearerRelComp(item)[0];
	}

	f_ConnHdlr_rx_s1ap_from_enb(pdu, tr_S1AP_RABReleaseRsp(g_pars.mme_ue_id, enb_ue_id, items));
	return pdu;
}

function f_ConnHdlr_tx_erab_release_ind(in ERabList erabs,
					S1AP_IEs.Cause cause := c_REL_CMD_CAUSE)
runs on ConnHdlr {
	var template (value) E_RABList items := f_ts_E_RABList(erabs, cause);
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_RABReleaseInd(g_pars.mme_ue_id, enb_ue_id, items));
}

function f_ConnHdlr_rx_erab_release_ind(in ERabList erabs,
					S1AP_IEs.Cause cause := c_REL_CMD_CAUSE)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABList items := f_tr_E_RABList(erabs, cause);
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	f_ConnHdlr_rx_s1ap_from_enb(pdu, tr_S1AP_RABReleaseInd(g_pars.mme_ue_id, enb_ue_id, items));
	return pdu;
}

function f_ConnHdlr_tx_initial_ctx_setup_req(in ERabList erabs)
runs on ConnHdlr {
	var template (value) E_RABToBeSetupListCtxtSUReq items;
	var template (value) E_RABLevelQoSParameters qos_params;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	qos_params := valueof(ts_E_RABLevelQoSParameters);

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (value) E_RABToBeSetupItemCtxtSUReq item;
		var template (value) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].u2c;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := ts_E_RABToBeSetupItemCtxtSUReq(rab_id := erabs[i].erab_id,
						       qos_params := qos_params,
						       tla := tla,
						       gtp_teid := epars.teid,
						       nas_pdu := omit);
		items[i] := ts_E_RABToBeSetupListCtxtSUReq(item)[0];
	}

	f_ConnHdlr_tx_s1ap_from_mme(ts_S1AP_IntialCtxSetupReq(mme_id := g_pars.mme_ue_id,
							      enb_id := enb_ue_id,
							      max_br := ts_UEAggregateMaximumBitrate,
							      rab_setup_items := items,
							      ue_sec_par := ts_UESecurityCapabilities,
							      sec_key := f_rnd_bitstring(256)));
}

function f_ConnHdlr_rx_initial_ctx_setup_req(in ERabList erabs)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABToBeSetupListCtxtSUReq items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (present) E_RABToBeSetupItemCtxtSUReq item;
		var template (present) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].a2u;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := tr_E_RABToBeSetupItemCtxtSUReq(rab_id := erabs[i].erab_id,
						       qos_params := ?,
						       tla := tla,
						       gtp_teid := epars.teid,
						       nas_pdu := omit);
		items[i] := tr_E_RABToBeSetupListCtxtSUReq(item)[0];
	}

	f_ConnHdlr_rx_s1ap_from_mme(pdu, tr_S1AP_IntialCtxSetupReq(mme_id := g_pars.mme_ue_id,
								   enb_id := enb_ue_id,
								   max_br := tr_UEAggregateMaximumBitrate,
								   rab_setup_items := items,
								   ue_sec_par := tr_UESecurityCapabilities));
	return pdu;
}

function f_ConnHdlr_tx_initial_ctx_setup_rsp(in ERabList erabs)
runs on ConnHdlr {
	var template (value) E_RABSetupListCtxtSURes items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (value) E_RABSetupItemCtxtSURes item;
		var template (value) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].u2a;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := ts_S1AP_RABSetupItemCtxtSURes(rab_id := erabs[i].erab_id,
						      tla := tla,
						      gtp_teid := epars.teid);
		items[i] := ts_S1AP_RABSetupListCtxtSURes(item)[0];
	}

	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_InitialCtxSetupResp(g_pars.mme_ue_id, enb_ue_id, items));
}

function f_ConnHdlr_rx_initial_ctx_setup_rsp(in ERabList erabs)
runs on ConnHdlr return S1AP_PDU {
	var template (present) E_RABSetupListCtxtSURes items;
	var ENB_UE_S1AP_ID enb_ue_id := g_pars.idx;
	var S1AP_PDU pdu;

	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		var template (present) E_RABSetupItemCtxtSURes item;
		var template (present) TransportLayerAddress tla;
		var ERabParams epars := erabs[i].c2u;

		tla := oct2bit(f_inet_addr(epars.tla));
		item := tr_S1AP_RABSetupItemCtxtSURes(rab_id := erabs[i].erab_id,
						      tla := tla,
						      gtp_teid := epars.teid);
		items[i] := tr_S1AP_RABSetupListCtxtSURes(item)[0];
	}

	f_ConnHdlr_rx_s1ap_from_enb(pdu, tr_S1AP_InitialCtxSetupResp(g_pars.mme_ue_id, enb_ue_id, items));
	return pdu;
}

function f_ConnHdlr_rx_session_establish_req(in ERab erab)
runs on ConnHdlr return PDU_PFCP {
	var OCT4 addr := f_inet_addr(g_pars.pfcp_rem_addr);

	/* Packet Detection Rules */
	var template (present) Outer_Header_Removal ohr;
	var template (present) Create_PDR pdr1;
	var template (present) Create_PDR pdr2;

	ohr := tr_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4);
	pdr1 := tr_PFCP_Create_PDR(pdr_id := 1, /* -- for Core -> Access */
				   pdi := tr_PFCP_PDI(CORE),
				   ohr := ohr,
				   far_id := c_PFCP_FAR_ID_C2A);
	pdr2 := tr_PFCP_Create_PDR(pdr_id := 2, /* -- for Access -> Core */
				   pdi := tr_PFCP_PDI(ACCESS),
				   ohr := ohr,
				   far_id := c_PFCP_FAR_ID_A2C);

	/* Forwarding Action Rules */
	var template (present) Outer_Header_Creation ohc2;
	var template (present) Forwarding_Parameters fpars2;
	var template (present) Create_FAR far1;
	var template (present) Create_FAR far2;

	ohc2 := tr_PFCP_Outer_Header_Creation_GTP_ipv4(erab.u2c.teid,
						       f_inet_addr(erab.u2c.tla));
	fpars2 := tr_PFCP_Forwarding_Parameters(CORE, ohc2);

	far1 := tr_PFCP_Create_FAR(far_id := c_PFCP_FAR_ID_C2A, /* -- for Core -> Access */
				   aa := tr_PFCP_Apply_Action_DROP,
				   fp := omit);
	far2 := tr_PFCP_Create_FAR(far_id := c_PFCP_FAR_ID_A2C, /* -- for Access -> Core */
				   aa := tr_PFCP_Apply_Action_FORW,
				   fp := fpars2);

	/* The final Session Establishment Request PDU */
	var template (present) PDU_PFCP tr_pdu;
	tr_pdu := tr_PFCP_Session_Est_Req(node_id := tr_PFCP_Node_ID_ipv4(addr),
					  cp_f_seid := tr_PFCP_F_SEID_ipv4(addr),
					  create_pdr := {pdr1, pdr2},
					  create_far := {far1, far2});
	return f_ConnHdlr_pfcp_expect(tr_pdu);
}

function f_ConnHdlr_tx_session_establish_resp(in ERab erab,
					      in PDU_PFCP req)
runs on ConnHdlr {
	var OCT4 addr := f_inet_addr(g_pars.pfcp_loc_addr);
	var PFCP_Session_Establishment_Request serq;

	serq := req.message_body.pfcp_session_establishment_request;

	/* Created Packet Detection Rules */
	var template (value) Created_PDR pdr1;
	var template (value) Created_PDR pdr2;
	pdr1 := ts_PFCP_Created_PDR(pdr_id := serq.create_PDR_list[0].grouped_ie.pdr_id,
				    local_F_TEID := ts_PFCP_F_TEID_ipv4(erab.c2u.teid,
									f_inet_addr(erab.c2u.tla)));
	pdr2 := ts_PFCP_Created_PDR(pdr_id := serq.create_PDR_list[1].grouped_ie.pdr_id,
				    local_F_TEID := ts_PFCP_F_TEID_ipv4(erab.a2u.teid,
									f_inet_addr(erab.a2u.tla)));

	/* The final Session Establishment Response PDU */
	var template (value) PDU_PFCP resp;
	resp := ts_PFCP_Session_Est_Resp(seq_nr := req.sequence_number,
					 node_id := ts_PFCP_Node_ID_ipv4(addr),
					 seid := erab.pfcp_rem_seid,
					 f_seid := ts_PFCP_F_SEID_ipv4(addr, erab.pfcp_loc_seid),
					 created_pdr := {pdr1, pdr2});
	PFCP.send(resp);
}

function f_ConnHdlr_rx_session_modify_req(in ERabParams epars,
					  template (present) OCT8 seid := ?,
					  template (present) LIN4_BO_LAST far_id := ?)
runs on ConnHdlr return PDU_PFCP {
	/* Forwarding Action Rules */
	var template (present) Outer_Header_Creation ohc;
	var template (present) Update_Forwarding_Parameters fpars;
	var template (present) Update_FAR far;

	ohc := tr_PFCP_Outer_Header_Creation_GTP_ipv4(epars.teid,
						      f_inet_addr(epars.tla));
	fpars := tr_PFCP_Update_Forwarding_Parameters(ohc := ohc);

	far := tr_PFCP_Update_FAR(far_id := far_id,
				  aa := tr_PFCP_Apply_Action_FORW,
				  fp := fpars);

	/* The final Session Modification Request PDU */
	var template (present) PDU_PFCP tr_pdu;
	tr_pdu := tr_PFCP_Session_Mod_Req(seid := seid,
					  update_far := far);
	return f_ConnHdlr_pfcp_expect(tr_pdu);
}

function f_ConnHdlr_tx_session_modify_resp(in ERab erab,
					   in PDU_PFCP req)
runs on ConnHdlr {
	/* The final Session Modification Response PDU */
	var template (value) PDU_PFCP resp;
	resp := ts_PFCP_Session_Mod_Resp(seq_nr := req.sequence_number,
					 seid := erab.pfcp_rem_seid);
	PFCP.send(resp);
}

function f_ConnHdlr_rx_session_delete_req(in ERab erab)
runs on ConnHdlr return PDU_PFCP {
	/* The final Session Deletion Request PDU */
	var template (present) PDU_PFCP tr_pdu;
	tr_pdu := tr_PFCP_Session_Del_Req(erab.pfcp_loc_seid);
	return f_ConnHdlr_pfcp_expect(tr_pdu);
}

function f_ConnHdlr_tx_session_delete_resp(in ERab erab,
					   in PDU_PFCP req)
runs on ConnHdlr {
	/* The final Session Deletion Response PDU */
	var template (value) PDU_PFCP resp;
	resp := ts_PFCP_Session_Del_Resp(seq_nr := req.sequence_number,
					 seid := erab.pfcp_rem_seid);
	PFCP.send(resp);
}

function f_ConnHdlr_session_establish(inout ERabList erabs)
runs on ConnHdlr {
	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		log("UPF <- S1GW: PFCP Session Establishment Request for E-RAB ID ", erabs[i].erab_id);
		var PDU_PFCP pdu := f_ConnHdlr_rx_session_establish_req(erabs[i]);
		/* store peer's SEID, so that it can be used in outgoing PDUs later */
		erabs[i].pfcp_rem_seid := pdu.message_body.pfcp_session_establishment_request.CP_F_SEID.seid;
		/* ask PFCPEM to route PDUs with the local SEID to us */
		f_PFCPEM_subscribe_seid(erabs[i].pfcp_loc_seid);
		log("UPF -> S1GW: PFCP Session Establishment Response for E-RAB ID ", erabs[i].erab_id);
		f_ConnHdlr_tx_session_establish_resp(erabs[i], pdu);
	}
}

function f_ConnHdlr_session_modify(in ERabList erabs)
runs on ConnHdlr {
	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		log("UPF <- S1GW: PFCP Session Modification Request for E-RAB ID ", erabs[i].erab_id);
		var PDU_PFCP pdu := f_ConnHdlr_rx_session_modify_req(epars := erabs[i].u2a,
								     seid := erabs[i].pfcp_loc_seid,
								     far_id := c_PFCP_FAR_ID_C2A);
		log("UPF -> S1GW: PFCP Session Modification Response for E-RAB ID ", erabs[i].erab_id);
		f_ConnHdlr_tx_session_modify_resp(erabs[i], pdu);
	}
}

function f_ConnHdlr_session_delete(in ERabList erabs)
runs on ConnHdlr {
	for (var integer i := 0; i < lengthof(erabs); i := i + 1) {
		log("UPF <- S1GW: PFCP Session Deletion Request for E-RAB ID ", erabs[i].erab_id);
		var PDU_PFCP pdu := f_ConnHdlr_rx_session_delete_req(erabs[i]);
		log("UPF -> S1GW: PFCP Session Deletion Response for E-RAB ID ", erabs[i].erab_id);
		f_ConnHdlr_tx_session_delete_resp(erabs[i], pdu);
		/* ask PFCPEM to *not* route PDUs with this SEID to us */
		f_PFCPEM_unsubscribe_seid(erabs[i].pfcp_loc_seid);
	}
}

function f_ConnHdlr_erab_setup_req(inout ERabList erabs)
runs on ConnHdlr {
	const OCT8 c_SEID0 := '0000000000000000'O;

	/* This code block cannot be executed by more than one component at a time because
	 * the S1AP E-RAB SETUP REQUEST triggers the IUT to send PFCP Session Establishment
	 * Request PDU(s), with need 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__);
	f_PFCPEM_subscribe_seid(c_SEID0);

	log("eNB <- [S1GW <- MME]: E-RAB SETUP REQUEST");
	f_ConnHdlr_tx_erab_setup_req(erabs);
	f_ConnHdlr_session_establish(erabs);

	/* 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__);

	log("[eNB <- S1GW] <- MME: E-RAB SETUP REQUEST");
	f_ConnHdlr_rx_erab_setup_req(erabs);
}

function f_ConnHdlr_erab_setup_rsp(in ERabList erabs)
runs on ConnHdlr {
	log("[eNB -> S1GW] -> MME: E-RAB SETUP RESPONSE");
	f_ConnHdlr_tx_erab_setup_rsp(erabs);
	f_ConnHdlr_session_modify(erabs);
	log("eNB -> [S1GW -> MME]: E-RAB SETUP RESPONSE");
	f_ConnHdlr_rx_erab_setup_rsp(erabs);
}

function f_ConnHdlr_erab_release_cmd(inout ERabList erabs,
				     S1AP_IEs.Cause cause := c_REL_CMD_CAUSE)
runs on ConnHdlr {
	log("eNB <- [S1GW <- MME]: E-RAB RELEASE COMMAND");
	f_ConnHdlr_tx_erab_release_cmd(erabs, cause);
	f_ConnHdlr_session_delete(erabs);
	log("[eNB <- S1GW] <- MME: E-RAB RELEASE COMMAND");
	f_ConnHdlr_rx_erab_release_cmd(erabs, cause);
}

function f_ConnHdlr_erab_release_rsp(inout ERabList erabs)
runs on ConnHdlr {
	log("[eNB -> S1GW] -> MME: E-RAB RELEASE RESPONSE");
	f_ConnHdlr_tx_erab_release_rsp(erabs);
	log("eNB -> [S1GW -> MME]: E-RAB RELEASE RESPONSE");
	f_ConnHdlr_rx_erab_release_rsp(erabs);
}

function f_ConnHdlr_erab_release_ind(inout ERabList erabs,
				     S1AP_IEs.Cause cause := c_REL_CMD_CAUSE)
runs on ConnHdlr {
	log("[eNB -> S1GW] -> MME: E-RAB RELEASE INDICATION");
	f_ConnHdlr_tx_erab_release_ind(erabs, cause);
	f_ConnHdlr_session_delete(erabs);
	log("eNB -> [S1GW -> MME]: E-RAB RELEASE INDICATION");
	f_ConnHdlr_rx_erab_release_ind(erabs, cause);
}

function f_ConnHdlr_initial_ctx_setup_req(inout ERabList erabs)
runs on ConnHdlr {
	const OCT8 c_SEID0 := '0000000000000000'O;

	/* This code block cannot be executed by more than one component at a time because
	 * the S1AP E-RAB SETUP REQUEST triggers the IUT to send PFCP Session Establishment
	 * Request PDU(s), with need 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__);
	f_PFCPEM_subscribe_seid(c_SEID0);

	log("eNB <- [S1GW <- MME]: INITIAL CONTEXT SETUP REQUEST");
	f_ConnHdlr_tx_initial_ctx_setup_req(erabs);
	f_ConnHdlr_session_establish(erabs);

	/* 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__);

	log("[eNB <- S1GW] <- MME: INITIAL CONTEXT SETUP REQUEST");
	f_ConnHdlr_rx_initial_ctx_setup_req(erabs);
}

function f_ConnHdlr_initial_ctx_setup_rsp(inout ERabList erabs)
runs on ConnHdlr {
	log("[eNB -> S1GW] -> MME: INITIAL CONTEXT SETUP RESPONSE");
	f_ConnHdlr_tx_initial_ctx_setup_rsp(erabs);
	f_ConnHdlr_session_modify(erabs);
	log("eNB -> [S1GW -> MME]: INITIAL CONTEXT SETUP RESPONSE");
	f_ConnHdlr_rx_initial_ctx_setup_rsp(erabs);
}

}