%% Copyright (C) 2024 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(erab_fsm). -behaviour(gen_statem). -export([init/1, callback_mode/0, erab_wait_setup_req/3, session_establish/3, erab_wait_setup_rsp/3, session_modify/3, erab_setup/3, session_delete/3, erab_wait_release_rsp/3, code_change/4, terminate/3]). -export([start_link/1, erab_setup_req/2, erab_setup_rsp/2, erab_release/2, erab_release_cmd/1, erab_release_rsp/1, erab_release_ind/1, fetch_info/1, shutdown/1]). -include_lib("kernel/include/logger.hrl"). -include_lib("pfcplib/include/pfcp_packet.hrl"). -define(ERAB_T_WAIT_SETUP_RSP, 5_000). -define(ERAB_T_WAIT_RELEASE_RSP, 5_000). -define(ERAB_T_SESSION_ESTABLISH, 2_000). -define(ERAB_T_SESSION_MODIFY, 2_000). -define(ERAB_T_SESSION_DELETE, 2_000). -type teid() :: 0..16#ffffffff. -type addr() :: << _:32 >> | << _:128 >>. -type teid_addr() :: {teid(), %% GTP-U TEID addr() %% GTP-U Transport Layer Address }. -type rel_kind() :: cmd | ind. -record(erab_state, {from :: undefined | gen_statem:from(), %% destination to use when replying u2c :: undefined | teid_addr(), %% GTP-U params for UPF -> Core c2u :: undefined | teid_addr(), %% GTP-U params for UPF <- Core a2u :: undefined | teid_addr(), %% GTP-U params for UPF <- Access u2a :: undefined | teid_addr(), %% GTP-U params for UPF -> Access seid_loc :: undefined | pfcp_peer:pfcp_seid(), %% local SEID, chosen by us seid_rem :: undefined | pfcp_peer:pfcp_seid(), %% remote SEID, chosen by the UPF rel_kind :: undefined | rel_kind() %% E-RAB RELEASE kind }). -type erab_state() :: #erab_state{}. -type iface() :: 'Core' | 'Access'. -type pfcp_ie() :: map(). %% ------------------------------------------------------------------ %% public API %% ------------------------------------------------------------------ %% @doc Allocate and start an E-RAB FSM process %% @param UID *unique* E-RAB identifier. %% @returns process ID on success; an error otherwise. %% @end -spec start_link(term()) -> gen_statem:start_ret(). start_link(UID) -> gen_statem:start_link(?MODULE, [UID], []). %% @doc Indicate reception of E-RAB setup request (from core). %% @param Pid PID of an erab_fsm. %% @param F_TEID TEID and bind address indicated by the MME. %% @returns TEID and Addr to be sent to the eNB; an error otherwise. %% @end -spec erab_setup_req(pid(), teid_addr()) -> {ok, teid_addr()} | {error, term()}. erab_setup_req(Pid, F_TEID) -> gen_statem:call(Pid, {?FUNCTION_NAME, F_TEID}). %% @doc Indicate reception of E-RAB setup response (from access). %% @param Pid PID of an erab_fsm. %% @param F_TEID TEID and bind address indicated by the eNB. %% @returns TEID and Addr to be sent to the MME; an error otherwise. %% @end -spec erab_setup_rsp(pid(), teid_addr()) -> {ok, teid_addr()} | {error, term()}. erab_setup_rsp(Pid, F_TEID) -> gen_statem:call(Pid, {?FUNCTION_NAME, F_TEID}). -spec erab_release(pid(), rel_kind()) -> ok. erab_release(Pid, cmd) -> erab_release_cmd(Pid); erab_release(Pid, ind) -> erab_release_ind(Pid). -spec erab_release_cmd(pid()) -> ok. erab_release_cmd(Pid) -> gen_statem:call(Pid, ?FUNCTION_NAME). -spec erab_release_rsp(pid()) -> ok. erab_release_rsp(Pid) -> gen_statem:cast(Pid, ?FUNCTION_NAME). -spec erab_release_ind(pid()) -> ok. erab_release_ind(Pid) -> gen_statem:call(Pid, ?FUNCTION_NAME). -spec fetch_info(pid()) -> proplists:proplist(). fetch_info(Pid) -> gen_statem:call(Pid, ?FUNCTION_NAME). -spec shutdown(pid()) -> ok. shutdown(Pid) -> gen_statem:stop(Pid). %% ------------------------------------------------------------------ %% gen_statem API %% ------------------------------------------------------------------ init([UID]) -> set_logging_prefix(UID), %% request a unieue SEID for this E-RAB FSM {ok, SEID} = pfcp_peer:seid_alloc(), {ok, erab_wait_setup_req, #erab_state{seid_loc = SEID}}. callback_mode() -> [state_functions, state_enter]. %% state WAIT_SETUP_REQ :: wait E-RAB SETUP Req from core erab_wait_setup_req(enter, ?FUNCTION_NAME, S) -> ?LOG_DEBUG("State enter: ~p", [?FUNCTION_NAME]), {keep_state, S}; erab_wait_setup_req({call, From}, {erab_setup_req, U2C}, #erab_state{} = S) -> ?LOG_DEBUG("Rx E-RAB SETUP Req (U2C ~p)", [U2C]), {next_state, session_establish, S#erab_state{from = From, u2c = U2C}}; erab_wait_setup_req(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% state SESSION_ESTABLISH :: PFCP session establishment session_establish(enter, OldState, #erab_state{from = From} = S) -> ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), case session_establish_req(S) of ok -> {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout [{state_timeout, ?ERAB_T_SESSION_ESTABLISH, ?FUNCTION_NAME}]}; {error, Reason} -> {stop_and_reply, {shutdown, Reason}, {reply, From, {error, Reason}}} end; session_establish(state_timeout, _Timeout, #erab_state{from = From}) -> ?LOG_ERROR("PFCP session establishment timeout"), {stop_and_reply, {shutdown, timeout}, {reply, From, {error, {timeout, ?FUNCTION_NAME}}}}; session_establish(info, #pfcp{} = PDU, #erab_state{from = From, seid_loc = SEID_Rsp} = S) -> case PDU of #pfcp{type = session_establishment_response, seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req ie = #{pfcp_cause := 'Request accepted', f_seid := #f_seid{seid = F_SEID}, created_pdr := [#{f_teid := F_TEID_C2U}, #{f_teid := F_TEID_A2U}]}} -> C2U = {F_TEID_C2U#f_teid.teid, f_teid_addr(F_TEID_C2U)}, A2U = {F_TEID_A2U#f_teid.teid, f_teid_addr(F_TEID_A2U)}, ?LOG_DEBUG("PFCP session established (C2U ~p, A2U ~p)", [C2U, A2U]), {next_state, erab_wait_setup_rsp, S#erab_state{from = undefined, seid_rem = F_SEID, %% SEID to be used in further requests from us c2u = C2U, a2u = A2U}, [{reply, From, {ok, A2U}}]}; _ -> ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), {stop_and_reply, {shutdown, unexp_pdu}, {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}} end; session_establish(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% state WAIT_SETUP_RSP :: wait E-RAB SETUP Rsp from access erab_wait_setup_rsp(enter, OldState, S) -> ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout [{state_timeout, ?ERAB_T_WAIT_SETUP_RSP, ?FUNCTION_NAME}]}; erab_wait_setup_rsp(state_timeout, _Timeout, _S) -> {stop, {shutdown, timeout}}; erab_wait_setup_rsp({call, From}, {erab_setup_rsp, U2A}, #erab_state{} = S) -> ?LOG_DEBUG("Rx E-RAB SETUP Rsp (U2A ~p)", [U2A]), {next_state, session_modify, S#erab_state{from = From, u2a = U2A}}; erab_wait_setup_rsp(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% state SESSION_MODIFY :: PFCP session modification session_modify(enter, OldState, S) -> ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), ok = session_modify_req(S), {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout [{state_timeout, ?ERAB_T_SESSION_MODIFY, ?FUNCTION_NAME}]}; session_modify(state_timeout, _Timeout, #erab_state{from = From}) -> ?LOG_ERROR("PFCP session modification timeout"), {stop_and_reply, {shutdown, timeout}, {reply, From, {error, {timeout, ?FUNCTION_NAME}}}}; session_modify(info, #pfcp{} = PDU, #erab_state{from = From, seid_loc = SEID_Rsp, c2u = C2U} = S) -> case PDU of #pfcp{type = session_modification_response, seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req ie = #{pfcp_cause := 'Request accepted'}} -> ?LOG_DEBUG("PFCP session modified"), {next_state, erab_setup, S#erab_state{from = undefined}, [{reply, From, {ok, C2U}}]}; _ -> ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), {stop_and_reply, {shutdown, unexp_pdu}, {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}} end; session_modify(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% state SETUP :: E-RAB is fully setup erab_setup(enter, OldState, S) -> ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), {keep_state, S}; erab_setup({call, From}, erab_release_cmd, #erab_state{} = S) -> ?LOG_DEBUG("Rx E-RAB RELEASE Cmd"), {next_state, session_delete, S#erab_state{from = From, rel_kind = cmd}}; erab_setup({call, From}, erab_release_ind, #erab_state{} = S) -> ?LOG_DEBUG("Rx E-RAB RELEASE Ind"), {next_state, session_delete, S#erab_state{from = From, rel_kind = ind}}; erab_setup(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% state SESSION_DELETE :: PFCP session deletion session_delete(enter, OldState, #erab_state{seid_rem = SEID_Req} = S) -> ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), ok = pfcp_peer:session_delete_req(SEID_Req), {next_state, ?FUNCTION_NAME, %% loop transition to enable state_timeout %% unset seid_rem to prevent terminate/3 from deleting the session again S#erab_state{seid_rem = undefined}, [{state_timeout, ?ERAB_T_SESSION_DELETE, ?FUNCTION_NAME}]}; session_delete(state_timeout, _Timeout, #erab_state{from = From}) -> ?LOG_ERROR("PFCP session modification timeout"), {stop_and_reply, {shutdown, timeout}, {reply, From, {error, {timeout, ?FUNCTION_NAME}}}}; session_delete(info, #pfcp{} = PDU, #erab_state{from = From, seid_loc = SEID_Rsp, rel_kind = RelKind} = S) -> case PDU of #pfcp{type = session_deletion_response, seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req ie = #{pfcp_cause := 'Request accepted'}} -> ?LOG_DEBUG("PFCP session deleted"), Reply = {reply, From, ok}, case RelKind of cmd -> %% E-RAB RELEASE CMD => wait for the RSP {next_state, erab_wait_release_rsp, S#erab_state{from = undefined}, [Reply]}; ind -> %% E-RAB RELEASE IND => terminate immediately {stop_and_reply, normal, [Reply]} end; _ -> ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), {stop_and_reply, {shutdown, unexp_pdu}, {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}} end; session_delete(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% state WAIT_RELEASE_RSP :: wait E-RAB RELEASE Rsp erab_wait_release_rsp(enter, OldState, S) -> ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout [{state_timeout, ?ERAB_T_WAIT_RELEASE_RSP, ?FUNCTION_NAME}]}; erab_wait_release_rsp(state_timeout, _Timeout, _S) -> {stop, {shutdown, timeout}}; erab_wait_release_rsp(cast, erab_release_rsp, #erab_state{}) -> ?LOG_DEBUG("Rx E-RAB RELEASE Rsp, we're done"), {stop, normal}; %% we're done! erab_wait_release_rsp(Event, EventData, S) -> handle_event(?FUNCTION_NAME, Event, EventData, S). %% Event handler for all states handle_event(State, {call, From}, fetch_info, #erab_state{} = S) -> Info = [{state, State}, {f_teid_u2c, S#erab_state.u2c}, {f_teid_c2u, S#erab_state.c2u}, {f_teid_a2u, S#erab_state.a2u}, {f_teid_u2a, S#erab_state.u2a}, {seid_loc, S#erab_state.seid_loc}, {seid_rem, S#erab_state.seid_rem}], %% omit props with Value =:= undefined Reply = lists:filter(fun({_, V}) -> V =/= undefined end, Info), {keep_state_and_data, {reply, From, Reply}}; handle_event(State, Event, EventData, _S) -> ?LOG_ERROR("Unexpected event ~p in state ~p: ~p", [Event, State, EventData]), {keep_state_and_data}. code_change(_Vsn, State, S, _Extra) -> {ok, State, S}. terminate(Reason, State, S) -> ?LOG_NOTICE("Terminating in state ~p, reason ~p", [State, Reason]), case S of %% PFCP session is not established or was deleted #erab_state{seid_rem = undefined} -> ok; %% PFCP session is established, so we terminate it #erab_state{seid_rem = SEID_Req} -> ?LOG_INFO("Sending Session Deletion Req"), pfcp_peer:session_delete_req(SEID_Req) end. %% ------------------------------------------------------------------ %% private API %% ------------------------------------------------------------------ %% set process metadata for the logger set_logging_prefix(UID) -> Prefix = io_lib:format("E-RAB ~p", [UID]), logger:set_process_metadata(#{prefix => Prefix}). -spec session_establish_req(erab_state()) -> pfcp_peer:pfcp_session_rsp(). session_establish_req(#erab_state{seid_loc = F_SEID, %% used as F-SEID u2c = U2C}) -> %% Packet Detection Rules OHR = #outer_header_removal{header = 'GTP-U/UDP/IPv4'}, PDRs = [#{pdr_id => {pdr_id, 1}, %% -- for Core -> Access far_id => {far_id, 1}, %% see FARs below precedence => {precedence, 255}, outer_header_removal => OHR, pdi => pdi('Core')}, #{pdr_id => {pdr_id, 2}, %% -- for Access -> Core far_id => {far_id, 2}, %% see FARs below precedence => {precedence, 255}, outer_header_removal => OHR, pdi => pdi('Access')}], %% Forwarding Action Rules FPars = #{outer_header_creation => ohc(U2C), destination_interface => {destination_interface, 'Core'}}, FParsNI = add_net_inst(FPars, 'Core'), %% optional Network Instance IE FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access %% We don't know the Access side TEID / GTP-U address yet, so we set %% this FAR to DROP and modify it when we get E-RAB SETUP RESPONSE. apply_action => #{'DROP' => []}}, #{far_id => {far_id, 2}, %% -- for Access -> Core apply_action => #{'FORW' => []}, forwarding_parameters => FParsNI}], pfcp_peer:session_establish_req(F_SEID, PDRs, FARs). -spec session_modify_req(erab_state()) -> pfcp_peer:pfcp_session_rsp(). session_modify_req(#erab_state{seid_rem = SEID, %% SEID allocated to us u2a = U2A}) -> %% Forwarding Action Rules FPars = #{outer_header_creation => ohc(U2A)}, FParsNI = add_net_inst(FPars, 'Access'), %% optional Network Instance IE FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access %% Now we know the Access side TEID / GTP-U address, so we modify %% this FAR (which was previously set to DROP) to FORW. apply_action => #{'FORW' => []}, update_forwarding_parameters => FParsNI}], pfcp_peer:session_modify_req(SEID, [], FARs). %% Network Instance IE env parameter name -spec net_inst_param(iface()) -> atom(). net_inst_param('Core') -> pfcp_net_inst_core; net_inst_param('Access') -> pfcp_net_inst_access. %% if configured, add Network Instance IE (optional) -spec add_net_inst(pfcp_ie(), iface()) -> pfcp_ie(). add_net_inst(IE, Iface) -> Param = net_inst_param(Iface), case application:get_env(osmo_s1gw, Param) of {ok, NI} -> IE#{network_instance => NI}; undefined -> IE end. %% Packet Detection Information IE generator -spec pdi(iface()) -> pfcp_ie(). pdi(SrcIface) -> IE = #{f_teid => #f_teid{teid = choose, ipv4 = choose}, source_interface => {source_interface, SrcIface}}, %% if configured, add Network Instance IE (optional) add_net_inst(IE, SrcIface). %% Outer Header Creation IE generator ohc({TEID, Addr}) -> OHC = #outer_header_creation{n6 = false, n19 = false, type = 'GTP-U', teid = TEID}, case Addr of << _:32 >> -> OHC#outer_header_creation{ipv4 = Addr}; << _:128 >> -> OHC#outer_header_creation{ipv6 = Addr} end. %% select an address from F-TEID (prefer IPv6 over IPv4) f_teid_addr(#f_teid{ipv6 = << IPv6:16/bytes >>}) -> IPv6; f_teid_addr(#f_teid{ipv4 = << IPv4:4/bytes >>}) -> IPv4; f_teid_addr(#f_teid{}) -> erlang:error("F-TEID IE contains no IPv4/IPv6"). %% vim:set ts=4 sw=4 et: