fixeria has uploaded this change for review.

View Change

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}.



To view, visit change 37317. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: erlang/osmo-s1gw
Gerrit-Branch: master
Gerrit-Change-Id: Ic57e14675339b7cadb0cdd7cbc5d3a56288d7297
Gerrit-Change-Number: 37317
Gerrit-PatchSet: 1
Gerrit-Owner: fixeria <vyanitskiy@sysmocom.de>
Gerrit-MessageType: newchange