fixeria has uploaded this change for review. ( https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/41621?usp=email )
Change subject: s1ap_utils: add API for S1AP PDU parsing ......................................................................
s1ap_utils: add API for S1AP PDU parsing
This API will be used in a follow-up patch, in which handling of the S1 SETUP procedure will be fully migrated to `sctp_proxy` - a prerequisite for proper MME pooling support.
Change-Id: I0aa3847d0f0ae65f13fa6bbfdbcd6ed188dcb04f Related: SYS#7052 --- M src/enb_registry.erl M src/s1ap_proxy.erl M src/s1ap_utils.erl A test/s1ap_utils_test.erl 4 files changed, 188 insertions(+), 52 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/erlang/osmo-s1gw refs/changes/21/41621/1
diff --git a/src/enb_registry.erl b/src/enb_registry.erl index 0217836..90c3376 100644 --- a/src/enb_registry.erl +++ b/src/enb_registry.erl @@ -80,8 +80,8 @@ reg_time := integer(), %% registration time (monotonic) uptime := non_neg_integer(), %% seconds since reg_time genb_id_str => string(), %% Global-eNB-ID - enb_id => s1ap_proxy:enb_id(), %% eNB-ID - plmn_id => s1ap_proxy:plmn_id(), %% PLMN-ID + enb_id => s1ap_utils:enb_id(), %% eNB-ID + plmn_id => s1ap_utils:plmn_id(), %% PLMN-ID enb_conn_info => sctp_server:conn_info(), %% eNB -> S1GW connection info mme_conn_info => sctp_proxy:conn_info() %% S1GW -> MME connection info }. diff --git a/src/s1ap_proxy.erl b/src/s1ap_proxy.erl index cbd9b10..43624d2 100644 --- a/src/s1ap_proxy.erl +++ b/src/s1ap_proxy.erl @@ -60,19 +60,16 @@ -type s1ap_ie_id() :: non_neg_integer(). -type s1ap_ie_val() :: tuple().
--type enb_id() :: 0..16#fffffff. -type mme_ue_id() :: 0..16#ffffffff. -type enb_ue_id() :: 0..16#ffffff. -type erab_id() :: 0..16#ff. -type erab_uid() :: {mme_ue_id(), erab_id()}. --type plmn_id() :: {MCC :: nonempty_string(), - MNC :: nonempty_string()}.
-record(proxy_state, {owner :: pid(), erabs :: dict:dict(K :: erab_uid(), V :: pid()), - enb_id :: undefined | non_neg_integer(), - plmn_id :: undefined | plmn_id(), + enb_id :: undefined | s1ap_utils:enb_id(), + plmn_id :: undefined | s1ap_utils:plmn_id(), genb_id_str :: undefined | string(), mme_ue_id :: undefined | mme_ue_id(), enb_ue_id :: undefined | enb_ue_id(), @@ -83,15 +80,13 @@ -type proxy_state() :: #proxy_state{}. -type proxy_action() :: forward | reply | drop.
--type enb_info() :: #{enb_id => enb_id(), - plmn_id => plmn_id(), +-type enb_info() :: #{enb_id => s1ap_utils:enb_id(), + plmn_id => s1ap_utils:plmn_id(), genb_id_str => string() }.
-export_type([proxy_action/0, - enb_info/0, - enb_id/0, - plmn_id/0]). + enb_info/0]).
%% ------------------------------------------------------------------ @@ -203,41 +198,6 @@ ok.
-%% 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. - - --spec genb_id_str(proxy_state()) -> string(). -genb_id_str(#proxy_state{plmn_id = {MCC, MNC}, - enb_id = ENBId}) -> - MCC ++ "-" ++ MNC ++ "-" ++ integer_to_list(ENBId). - - -spec enb_info(proxy_state()) -> enb_info(). enb_info(S) -> Info = #{enb_id => S#proxy_state.enb_id, @@ -686,13 +646,14 @@ #'Global-ENB-ID'{'pLMNidentity' = PLMNId, 'eNB-ID' = ENBId} = C, S0) -> %% store PLMNId/ENBId - S1 = S0#proxy_state{plmn_id = parse_plmn_id(PLMNId), - enb_id = parse_enb_id(ENBId)}, + S1 = S0#proxy_state{plmn_id = s1ap_utils:parse_plmn_id(PLMNId), + enb_id = s1ap_utils:parse_enb_id(ENBId)}, ?LOG_INFO("Global-ENB-ID: PLMN-ID=~p, eNB-ID=~p", [S1#proxy_state.plmn_id, S1#proxy_state.enb_id]), %% use that as a context for logging - GlobalENBId = genb_id_str(S1), + GlobalENBId = s1ap_utils:genb_id_str(#{plmn_id => S1#proxy_state.plmn_id, + enb_id => S1#proxy_state.enb_id}), osmo_s1gw:set_log_prefix("eNB " ++ GlobalENBId), %% register per-eNB metrics ctr_reg_all(GlobalENBId), diff --git a/src/s1ap_utils.erl b/src/s1ap_utils.erl index ab7180c..6f7e6b6 100644 --- a/src/s1ap_utils.erl +++ b/src/s1ap_utils.erl @@ -35,11 +35,19 @@ -module(s1ap_utils).
-export([encode_pdu/1, - decode_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) @@ -47,7 +55,28 @@ {successfulOutcome, #'SuccessfulOutcome'{}} | {unsuccessfulOutcome, #'UnsuccessfulOutcome'{}}.
--export_type([s1ap_pdu/0]). +%% 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]).
%% ------------------------------------------------------------------ @@ -68,4 +97,114 @@ '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: diff --git a/test/s1ap_utils_test.erl b/test/s1ap_utils_test.erl new file mode 100644 index 0000000..1f3279b --- /dev/null +++ b/test/s1ap_utils_test.erl @@ -0,0 +1,36 @@ +-module(s1ap_utils_test). + +-include_lib("eunit/include/eunit.hrl"). +-include("S1AP-Constants.hrl"). +-include("S1AP-IEs.hrl"). + + +-define(_assertEqualIE(IEI, Values), + ?_assertEqual(Values, proplists:get_value(IEI, IEs))). + +-define(_assertMatchIE(IEI, Values), + ?_assertMatch(Values, proplists:get_value(IEI, IEs))). + + +%% ------------------------------------------------------------------ +%% actual testcases +%% ------------------------------------------------------------------ + +s1ap_setup_req_test_() -> + {MsgType, IEs} = s1ap_utils:parse_pdu(s1ap_samples:s1_setup_req_pdu()), + [?_assertEqual(MsgType, {?'id-S1Setup', initiatingMessage}), + ?_assertEqualIE(?'id-Global-ENB-ID', #{enb_id => 0, + plmn_id => {"001", "01"}}), + ?_assertEqualIE(?'id-eNBname', undefined), %% optional, not present + ?_assertMatchIE(?'id-SupportedTAs', [12345])]. %% we only parse the TACs + + +s1ap_setup_rsp_test_() -> + {MsgType, IEs} = s1ap_utils:parse_pdu(s1ap_samples:s1_setup_rsp_pdu()), + [?_assertEqual(MsgType, {?'id-S1Setup', successfulOutcome}), + ?_assertEqualIE(?'id-MMEname', "open5gs-mme0"), + ?_assertMatchIE(?'id-ServedGUMMEIs', [#'ServedGUMMEIsItem'{}]), + ?_assertEqualIE(?'id-RelativeMMECapacity', 16#ff)]. + + +%% vim:set ts=4 sw=4 et: