%% Copyright (C) 2025 by sysmocom - s.f.m.c. GmbH %% Author: Vadim Yanitskiy %% %% All Rights Reserved %% %% SPDX-License-Identifier: AGPL-3.0-or-later %% %% This program is free software; you can redistribute it and/or modify %% it under the terms of the GNU Affero General Public License as %% published by the Free Software Foundation; either version 3 of the %% License, or (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU General Public License for more details. %% %% You should have received a copy of the GNU Affero General Public License %% along with this program. If not, see . %% %% Additional Permission under GNU AGPL version 3 section 7: %% %% If you modify this Program, or any covered work, by linking or %% combining it with runtime libraries of Erlang/OTP as released by %% Ericsson on https://www.erlang.org (or a modified version of these %% libraries), containing parts covered by the terms of the Erlang Public %% License (https://www.erlang.org/EPLICENSE), the licensors of this %% Program grant you additional permission to convey the resulting work %% without the need to license the runtime libraries of Erlang/OTP under %% the GNU Affero General Public License. Corresponding Source for a %% non-source form of such a combination shall include the source code %% for the parts of the runtime libraries of Erlang/OTP used as well as %% that of the covered work. -module(s1ap_utils). -export([encode_pdu/1, decode_pdu/1, parse_pdu/1, parse_plmn_id/1, parse_enb_id/1, genb_id_str/1]). -include_lib("kernel/include/logger.hrl"). -include("S1AP-PDU-Descriptions.hrl"). -include("S1AP-PDU-Contents.hrl"). -include("S1AP-Containers.hrl"). -include("S1AP-Constants.hrl"). -include("S1AP-IEs.hrl"). %% S1AP PDU (decoded) -type s1ap_pdu() :: {initiatingMessage, #'InitiatingMessage'{}} | {successfulOutcome, #'SuccessfulOutcome'{}} | {unsuccessfulOutcome, #'UnsuccessfulOutcome'{}}. %% 9.2.1.1 Message Type -type s1ap_msg_type() :: {Proc :: non_neg_integer(), Type :: initiatingMessage | successfulOutcome | unsuccessfulOutcome}. %% S1AP PDU (decoded, unrolled) -type s1ap_pdu_info() :: {MsgType :: s1ap_msg_type(), Content :: proplists:proplist()}. -export_type([s1ap_pdu/0, s1ap_msg_type/0, s1ap_pdu_info/0]). -type enb_id() :: 0..16#fffffff. -type plmn_id() :: {MCC :: nonempty_string(), MNC :: nonempty_string()}. -type genb_id() :: #{enb_id => enb_id(), plmn_id => plmn_id()}. -export_type([enb_id/0, plmn_id/0, genb_id/0]). %% ------------------------------------------------------------------ %% public API %% ------------------------------------------------------------------ %% Encode an S1AP PDU -spec encode_pdu(s1ap_pdu()) -> {ok, binary()} | {error, {asn1, tuple()}}. encode_pdu(PDU) -> 'S1AP-PDU-Descriptions':encode('S1AP-PDU', PDU). %% Decode an S1AP PDU -spec decode_pdu(binary()) -> {ok, s1ap_pdu()} | {error, {asn1, tuple()}}. decode_pdu(Data) -> 'S1AP-PDU-Descriptions':decode('S1AP-PDU', Data). %% Parse an S1AP PDU -spec parse_pdu(binary()) -> s1ap_pdu_info() | {error, term()}. parse_pdu(Data) -> try decode_pdu(Data) of {ok, PDU} -> {MsgType, IEs} = unroll_pdu(PDU), {MsgType, parse_ies(IEs)}; {error, Error} -> ?LOG_ERROR("S1AP PDU decoding failed: ~p", [Error]), {error, {decode_pdu, Error}} catch Exception:Reason:StackTrace -> ?LOG_ERROR("An exception occurred: ~p, ~p, ~p", [Exception, Reason, StackTrace]), {error, decode_pdu} end. -spec genb_id_str(genb_id()) -> string(). genb_id_str(#{plmn_id := {MCC, MNC}, enb_id := ENBId}) -> MCC ++ "-" ++ MNC ++ "-" ++ integer_to_list(ENBId). %% ------------------------------------------------------------------ %% private API %% ------------------------------------------------------------------ %% Unroll a decoded S1AP PDU (procedure code, type, IEs) -spec unroll_pdu(s1ap_pdu()) -> s1ap_pdu_info(). unroll_pdu({Type = initiatingMessage, #'InitiatingMessage'{procedureCode = PC, value = {_, IEs}}}) -> {{PC, Type}, IEs}; unroll_pdu({Type = successfulOutcome, #'SuccessfulOutcome'{procedureCode = PC, value = {_, IEs}}}) -> {{PC, Type}, IEs}; unroll_pdu({Type = unsuccessfulOutcome, #'UnsuccessfulOutcome'{procedureCode = PC, value = {_, IEs}}}) -> {{PC, Type}, IEs}. %% Parse PLMN-ID as per 3GPP TS 24.008, Figure 10.5.13 %% | MCC digit 2 | MCC digit 1 | octet 1 %% | MNC digit 3 | MCC digit 3 | octet 2 %% | MNC digit 2 | MNC digit 1 | octet 3 -spec parse_plmn_id(<< _:24 >>) -> plmn_id(). parse_plmn_id(<< MCC2:4, MCC1:4, MNC3:4, MCC3:4, MNC2:4, MNC1:4 >>) -> MCC = parse_mcc_mnc(MCC1, MCC2, MCC3), MNC = parse_mcc_mnc(MNC1, MNC2, MNC3), {MCC, MNC}. -define(UNHEX(H), H + 48). parse_mcc_mnc(D1, D2, 16#f) -> [?UNHEX(D1), ?UNHEX(D2)]; parse_mcc_mnc(D1, D2, D3) -> [?UNHEX(D1), ?UNHEX(D2), ?UNHEX(D3)]. -spec parse_enb_id(tuple()) -> enb_id(). parse_enb_id({'macroENB-ID', << ID:20 >>}) -> ID; parse_enb_id({'homeENB-ID', << ID:28 >>}) -> ID; parse_enb_id({'short-macroENB-ID', << ID:18 >>}) -> ID; parse_enb_id({'long-macroENB-ID', << ID:21 >>}) -> ID. -type s1ap_ie_id() :: non_neg_integer(). -type s1ap_ie_val() :: tuple(). -spec parse_ie(tuple()) -> proplists:property(). -spec parse_ie(s1ap_ie_id(), s1ap_ie_val()) -> term(). parse_ie(#'ProtocolIE-Field'{id = IEI, value = C}) -> {IEI, parse_ie(IEI, C)}; parse_ie(#'ProtocolExtensionField'{id = IEI, extensionValue = C}) -> {IEI, parse_ie(IEI, C)}; parse_ie(IE) -> ?LOG_ERROR("Unknown IE format: ~p", [IE]), {unknown, IE}. %% 9.2.1.37 Global eNB ID parse_ie(?'id-Global-ENB-ID', #'Global-ENB-ID'{'pLMNidentity' = PLMNId, 'eNB-ID' = EnbId}) -> #{plmn_id => parse_plmn_id(PLMNId), enb_id => parse_enb_id(EnbId)}; %% 9.1.8.4 Supported TAs %% 9.2.3.7 Broadcast TAC parse_ie(?'id-SupportedTAs', TAs) -> %% TODO: Broadcast PLMNs [TAC || #'SupportedTAs-Item'{tAC = << TAC:16 >>} <- TAs]; %% For all other IEIs return the contents as-is. parse_ie(_IEI, C) -> C. %% Iterate over the given list of S1AP IEs, calling parse_ie/2 for each. %% The result is a proplist containing parsed IEs. -spec parse_ies(list()) -> proplists:proplist(). parse_ies(IEs) -> lists:map(fun parse_ie/1, IEs). %% vim:set ts=4 sw=4 et: