fixeria has uploaded this change for review. ( https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/37317?usp=email )
Change subject: erab_fsm: E-RAB FSM implementation ......................................................................
erab_fsm: E-RAB FSM implementation
Change-Id: Ic57e14675339b7cadb0cdd7cbc5d3a56288d7297 --- A src/erab_fsm.erl M src/pfcp_peer.erl 2 files changed, 350 insertions(+), 5 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/erlang/osmo-s1gw refs/changes/17/37317/1
diff --git a/src/erab_fsm.erl b/src/erab_fsm.erl new file mode 100644 index 0000000..eecd3cf --- /dev/null +++ b/src/erab_fsm.erl @@ -0,0 +1,316 @@ +%% Copyright (C) 2024 by sysmocom - s.f.m.c. GmbH info@sysmocom.de +%% Author: Vadim Yanitskiy vyanitskiy@sysmocom.de +%% +%% 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 https://www.gnu.org/licenses/. +%% +%% 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, + erab_wait_setup_rsp/3, + erab_setup/3, + erab_wait_release_rsp/3, + code_change/4, + terminate/3]). +-export([start_link/0, + erab_setup_req/3, + erab_setup_rsp/2, + erab_release_req/1, + erab_release_rsp/1, + shutdown/1]). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("pfcplib/include/pfcp_packet.hrl"). + + +-type ue_id() :: 0..16#ffff. +-type mme_id() :: 0..16#ffff. +-type erab_id() :: 0..16#ff. +-type erab_uid() :: {mme_id(), ue_id(), erab_id()}. +-type teid() :: 0..16#ffffffff. + +-record(erab_state, {uid :: erab_uid(), %% *unique* E-REB ID (for logging) + teid_from_access :: teid(), %% TEID to be used in eNB -> UPF + teid_to_access :: teid(), %% TEID to be used in UPF -> eNB + teid_from_core :: teid(), %% TEID to be used in PGW -> UPF + teid_to_core :: teid(), %% TEID to be used in UPF -> PGW + seid_loc :: pfcp_peer:pfcp_seid(), %% local SEID, which we transmit + seid_rem :: pfcp_peer:pfcp_seid() %% remote SEID, which we receive + }). + +-type erab_state() :: #erab_state{}. + +%% ------------------------------------------------------------------ +%% public API +%% ------------------------------------------------------------------ + +start_link() -> + gen_statem:start_link(?MODULE, [], []). + + +%% @doc Indicate reception of E-RAB setup request (from the eNB). +%% @param Pid PID of an erab_fsm. +%% @param UID *unique* E-RAB identifier. +%% @param TEID TEID choosen by the eNB. +%% @returns TEID to be sent to the core; an error otherwise. +%% @end +-spec erab_setup_req(pid(), erab_uid(), teid()) -> {ok, teid()} | + {error, term()}. +erab_setup_req(Pid, UID, TEID) -> + gen_statem:call(Pid, {?FUNCTION_NAME, {UID, TEID}}). + + +%% @doc Indicate reception of E-RAB setup response (from the core). +%% @param Pid PID of an erab_fsm. +%% @param TEID TEID choosen by the core. +%% @returns TEID to be sent to the eNB; an error otherwise. +%% @end +-spec erab_setup_rsp(pid(), teid()) -> {ok, teid()} | + {error, term()}. +erab_setup_rsp(Pid, TEID) -> + gen_statem:call(Pid, {?FUNCTION_NAME, {TEID}}). + + +-spec erab_release_req(pid()) -> ok. +erab_release_req(Pid) -> + gen_statem:cast(Pid, ?FUNCTION_NAME). + + +-spec erab_release_rsp(pid()) -> ok. +erab_release_rsp(Pid) -> + gen_statem:cast(Pid, ?FUNCTION_NAME). + + +-spec shutdown(pid()) -> ok. +shutdown(Pid) -> + gen_statem:stop(Pid). + + +%% ------------------------------------------------------------------ +%% gen_statem API +%% ------------------------------------------------------------------ + +init([]) -> + %% 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]. + + +erab_wait_setup_req(enter, OldState, S) -> + ?LOG_INFO("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {keep_state, S}; + +erab_wait_setup_req({call, From}, + {erab_setup_req, {UID, TEID_FromAcc}}, + #erab_state{seid_loc = SEID} = S0) -> + S1 = S0#erab_state{uid = UID, + teid_from_access = TEID_FromAcc}, + ?LOG_DEBUG("Rx E-RAB SETUP Req: TEID (Access -> UPF) ~p", [TEID_FromAcc]), + case session_establish_req(S1) of + {ok, #pfcp{type = session_establishment_response, + seid = SEID, %% 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{teid = TEID_ToCore}}, + #{f_teid := #f_teid{teid = TEID_ToAcc}}]}}} -> + ?LOG_DEBUG("PFCP session established"), + {next_state, + erab_wait_setup_rsp, + S1#erab_state{seid_rem = F_SEID, + teid_to_core = TEID_ToCore, + teid_to_access = TEID_ToAcc}, + [{reply, From, {ok, TEID_ToCore}}]}; + {ok, PDU} -> + ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), + {stop_and_reply, + {shutdown, {?FUNCTION_NAME, unexp_pdu}}, + [{reply, From, {error, unexp_pdu}}]}; + {error, Cause} -> + ?LOG_ERROR("Could not establish PFCP session (~p)", [Cause]), + {stop_and_reply, + {shutdown, {?FUNCTION_NAME, Cause}}, + [{reply, From, {error, Cause}}]} + end; + +%% Catch-all handler for this state +erab_wait_setup_req(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +erab_wait_setup_rsp(enter, OldState, S) -> + ?LOG_INFO("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {keep_state, S}; %% TODO: start a timer here + +erab_wait_setup_rsp({call, From}, + {erab_setup_rsp, {TEID_FromCore}}, + #erab_state{seid_loc = SEID} = S0) -> + S1 = S0#erab_state{teid_from_core = TEID_FromCore}, + ?LOG_DEBUG("Rx E-RAB SETUP Rsp: TEID (Core -> UPF) ~p", [TEID_FromCore]), + case session_modify_req(S1) of + {ok, #pfcp{type = session_modification_response, + seid = SEID, %% matches F-SEID we sent in the ESTABLISH Req + ie = #{pfcp_cause := 'Request accepted'}}} -> + {next_state, + erab_setup, S1, + [{reply, From, {ok, S1#erab_state.teid_to_access}}]}; + {ok, PDU} -> + ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), + {stop_and_reply, + {shutdown, {?FUNCTION_NAME, unexp_pdu}}, + [{reply, From, {error, unexp_pdu}}]}; + {error, Cause} -> + ?LOG_ERROR("Could not modify PFCP session (~p)", [Cause]), + %% TODO: send a PFCP session deletion request + {stop_and_reply, + {shutdown, {?FUNCTION_NAME, Cause}}, + [{reply, From, {error, Cause}}]} + end; + +%% Catch-all handler for this state +erab_wait_setup_rsp(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +erab_setup(enter, OldState, S) -> + ?LOG_INFO("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {keep_state, S}; + +erab_setup(cast, erab_release_req, + #erab_state{seid_loc = SEID} = S) -> + case pfcp_peer:session_delete_req(SEID) of + {ok, Resp} -> %% TODO: parse Resp + {next_state, %% TODO: start a timer? + erab_wait_release_rsp, S}; + {error, Cause} -> + ?LOG_ERROR("Could not delete a PFCP session (~p)", [Cause]), + {stop, {shutdown, {?FUNCTION_NAME, Cause}}} + end; + +%% Catch-all handler for this state +erab_setup(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +erab_wait_release_rsp(enter, OldState, S) -> + ?LOG_INFO("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {keep_state, S}; + +erab_wait_release_rsp(cast, erab_release_rsp, + #erab_state{}) -> + %% we're done! + {stop, normal}; + +%% Catch-all handler for this state +erab_wait_release_rsp(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +code_change(_Vsn, State, S, _Extra) -> + {ok, State, S}. + + +terminate(Reason, State, _S) -> + ?LOG_NOTICE("Terminating in state ~p, reason ~p", [State, Reason]), + ok. + + +%% ------------------------------------------------------------------ +%% private API +%% ------------------------------------------------------------------ + +-spec session_establish_req(erab_state()) -> pfcp_peer:session_rsp(). +session_establish_req(#erab_state{seid_loc = F_SEID, %% used as F-SEID + teid_from_access = TEID_FromAcc}) -> + %% 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 => #{f_teid => #f_teid{teid = choose, + ipv4 = choose}, + source_interface => {source_interface, '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 => #{f_teid => #f_teid{teid = choose, + ipv4 = choose}, + source_interface => {source_interface, 'Access'}}}], + %% Forwarding Action Rules + OHC = #outer_header_creation{n6 = false, + n19 = false, + type = 'GTP-U', + teid = TEID_FromAcc, + ipv4 = <<127,0,0,1>>}, %% XXX: Core facing addr of the UPF + FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access + %% We don't know the Core 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 => + #{outer_header_creation => OHC, + destination_interface => {destination_interface, 'Core'}}}], + pfcp_peer:session_establish_req(F_SEID, PDRs, FARs). + + +-spec session_modify_req(erab_state()) -> pfcp_peer:session_rsp(). +session_modify_req(#erab_state{seid_rem = SEID, + teid_from_core = TEID_FromCore}) -> + %% Forwarding Action Rules + OHC = #outer_header_creation{n6 = false, + n19 = false, + type = 'GTP-U', + teid = TEID_FromCore, + ipv4 = <<127,0,0,1>>}, %% XXX: Access facing addr of the UPF + FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access + %% Now we know the Core side TEID / GTP-U address, so we modify + %% this FAR (which was previously set to DROP) to FORW. + apply_action => #{'FORW' => []}, + forwarding_parameters => + #{outer_header_creation => OHC, + destination_interface => {destination_interface, 'Access'}}}], + pfcp_peer:session_modify_req(SEID, [], FARs). + +%% vim:set ts=4 sw=4 et: diff --git a/src/pfcp_peer.erl b/src/pfcp_peer.erl index 1db6271..e04cd11 100644 --- a/src/pfcp_peer.erl +++ b/src/pfcp_peer.erl @@ -42,6 +42,7 @@ code_change/4, terminate/3]). -export([start_link/2, + seid_alloc/0, session_establish_req/3, session_modify_req/3, session_delete_req/1, @@ -70,7 +71,8 @@ pfcp_pdu/0, session_rsp/0]).
--record(peer_state, {sock :: gen_udp:socket(), +-record(peer_state, {seid :: pfcp_seid(), + sock :: gen_udp:socket(), loc_addr :: inet:ip_address(), rem_addr :: inet:ip_address(), tx_seq_nr :: pfcp_seq_nr(), @@ -89,6 +91,13 @@ []).
+%% Request to allocate a unique SEID +-spec seid_alloc() -> {ok, pfcp_seid()} | + {error, term()}. +seid_alloc() -> + gen_statem:call(?MODULE, ?FUNCTION_NAME). + + -spec session_establish_req(pfcp_seid(), list(), list()) -> session_rsp(). session_establish_req(SEID, PDRs, FARs) -> gen_statem:call(?MODULE, {?FUNCTION_NAME, SEID, PDRs, FARs}). @@ -127,7 +136,8 @@ {reuseaddr, true}, {active, true}]), ?LOG_INFO("PFCP peer @ ~p will talk to UPF @ ~p", [LocAddr, RemAddr]), - {ok, connecting, #peer_state{sock = Sock, + {ok, connecting, #peer_state{seid = 0, + sock = Sock, loc_addr = LocAddr, rem_addr = RemAddr, tx_seq_nr = 0, @@ -169,8 +179,7 @@ end;
connecting(Event, EventData, S) -> - ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), - {keep_state, S}. + handle_event(?FUNCTION_NAME, Event, EventData, S).
%% CONNECTED state @@ -213,7 +222,18 @@
%% Catch-all handler for this state connected(Event, EventData, S) -> - ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + handle_event(?FUNCTION_NAME, Event, EventData, S). + + +%% Event handler for all states +handle_event(_, {call, From}, seid_alloc, + #peer_state{seid = SEID} = S) -> + NewSEID = SEID + 1, + {keep_state, S#peer_state{seid = NewSEID}, + [{reply, From, {ok, NewSEID}}]}; + +handle_event(State, Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p in state ~p: ~p", [Event, State, EventData]), {keep_state, S}.