fixeria has uploaded this change for review.

View Change

pfcp_peer: initial PFCP peer implementation

Change-Id: I7bad0069dca2111006039d12e744a2bd6d2cb86e
---
M config/sys.config
M src/osmo_s1gw.app.src
M src/osmo_s1gw_sup.erl
A src/pfcp_peer.erl
4 files changed, 253 insertions(+), 3 deletions(-)

git pull ssh://gerrit.osmocom.org:29418/erlang/osmo-s1gw refs/changes/68/37268/1
diff --git a/config/sys.config b/config/sys.config
index 5ccd43d..20d409d 100644
--- a/config/sys.config
+++ b/config/sys.config
@@ -13,7 +13,9 @@
{osmo_s1gw,
[{s1gw_bind_addr, "127.0.1.1"}, %% S1GW bind address for incoming eNB connections
{mme_loc_addr, "127.0.2.1"}, %% local address for outgoing connections to the MME
- {mme_rem_addr, "127.0.2.10"} %% remote address for outgoing connections to the MME
+ {mme_rem_addr, "127.0.2.10"}, %% remote address for outgoing connections to the MME
+ {upf_loc_addr, "127.0.1.1"}, %% local address for outgoing connections to the UPF
+ {upf_rem_addr, "127.0.1.2"} %% remote address for outgoing connections to the UPF
]},
%% ================================================================================
%% kernel config
diff --git a/src/osmo_s1gw.app.src b/src/osmo_s1gw.app.src
index 6fd8174..8bb89de 100644
--- a/src/osmo_s1gw.app.src
+++ b/src/osmo_s1gw.app.src
@@ -7,7 +7,8 @@
{applications, [
kernel,
stdlib,
- logger_color_formatter
+ logger_color_formatter,
+ pfcplib
]},
{modules, []},
{mod, {osmo_s1gw_app, []}},
diff --git a/src/osmo_s1gw_sup.erl b/src/osmo_s1gw_sup.erl
index e13cd1a..a0652c3 100644
--- a/src/osmo_s1gw_sup.erl
+++ b/src/osmo_s1gw_sup.erl
@@ -47,6 +47,8 @@
-define(ENV_DEFAULT_MME_LOC_ADDR, "127.0.2.1").
-define(ENV_DEFAULT_MME_REM_ADDR, "127.0.2.10").
-define(ENV_DEFAULT_MME_REM_PORT, ?S1AP_PORT).
+-define(ENV_DEFAULT_UPF_LOC_ADDR, "127.0.1.1").
+-define(ENV_DEFAULT_UPF_REM_ADDR, "127.0.1.2").

%% ------------------------------------------------------------------
%% supervisor API
@@ -62,6 +64,9 @@
MmeLocAddr = get_env(mme_loc_addr, ?ENV_DEFAULT_MME_LOC_ADDR),
MmeRemAddr = get_env(mme_rem_addr, ?ENV_DEFAULT_MME_REM_ADDR),
MmeRemPort = get_env(mme_rem_port, ?ENV_DEFAULT_MME_REM_PORT),
+ UpfLocAddr = get_env(upf_loc_addr, ?ENV_DEFAULT_UPF_LOC_ADDR),
+ UpfRemPort = get_env(upf_rem_addr, ?ENV_DEFAULT_UPF_REM_ADDR),
+
SctpServer = {sctp_server, {sctp_server, start_link,
[S1GWBindAddr, S1GWBindPort,
{{MmeLocAddr, MmeRemAddr}, MmeRemPort}]},
@@ -69,7 +74,14 @@
5000,
worker,
[sctp_server]},
- {ok, {{one_for_all, 5, 10}, [SctpServer]}}.
+ PfcpPeer = {pfcp_peer, {pfcp_peer, start_link,
+ [UpfLocAddr, UpfRemPort]},
+ permanent,
+ 5000,
+ worker,
+ [pfcp_peer]},
+
+ {ok, {{one_for_all, 5, 10}, [SctpServer, PfcpPeer]}}.


%% ------------------------------------------------------------------
diff --git a/src/pfcp_peer.erl b/src/pfcp_peer.erl
new file mode 100644
index 0000000..ced3a5e
--- /dev/null
+++ b/src/pfcp_peer.erl
@@ -0,0 +1,226 @@
+%% 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(pfcp_peer).
+-behaviour(gen_statem).
+
+-export([init/1,
+ callback_mode/0,
+ connecting/3,
+ connected/3,
+ code_change/4,
+ terminate/3]).
+-export([start_link/2,
+ shutdown/0]).
+
+-include_lib("kernel/include/logger.hrl").
+-include_lib("pfcplib/include/pfcp_packet.hrl").
+
+%% 3GPP TS 29.244, section 4.2 "UDP Header and Port Numbers"
+-define(PFCP_PORT, 8805).
+
+-type pfcp_seq_nr() :: pos_integer().
+-type pfcp_msg_type() :: atom().
+-type pfcp_msg_ies() :: [term()] | map() | binary().
+-type pfcp_msg() :: {pfcp_msg_type(), pfcp_msg_ies()}.
+
+-record(peer_state, {sock :: gen_udp:socket(),
+ loc_addr :: inet:ip_address(),
+ rem_addr :: inet:ip_address(),
+ tx_seq_nr :: pfcp_seq_nr()
+ }).
+
+-type peer_state() :: #peer_state{}.
+
+%% ------------------------------------------------------------------
+%% public API
+%% ------------------------------------------------------------------
+
+start_link(LocAddr, RemAddr) ->
+ gen_statem:start_link({local, ?MODULE}, ?MODULE,
+ [LocAddr, RemAddr],
+ []).
+
+
+shutdown() ->
+ gen_statem:stop(?MODULE).
+
+
+%% ------------------------------------------------------------------
+%% gen_server API
+%% ------------------------------------------------------------------
+
+init([LocAddrStr, RemAddr]) when is_list(LocAddrStr) ->
+ {ok, LocAddr} = inet:parse_address(LocAddrStr),
+ init([LocAddr, RemAddr]);
+
+init([LocAddr, RemAddrStr]) when is_list(RemAddrStr) ->
+ {ok, RemAddr} = inet:parse_address(RemAddrStr),
+ init([LocAddr, RemAddr]);
+
+init([LocAddr, RemAddr]) ->
+ process_flag(trap_exit, true),
+ {ok, Sock} = gen_udp:open(?PFCP_PORT, [binary,
+ {ip, LocAddr},
+ {reuseaddr, true},
+ {active, true}]),
+ ?LOG_INFO("PFCP peer @ ~p will talk to UPF @ ~p", [LocAddr, RemAddr]),
+ {ok, connecting, #peer_state{sock = Sock,
+ loc_addr = LocAddr,
+ rem_addr = RemAddr,
+ tx_seq_nr = 0}}.
+
+
+callback_mode() ->
+ [state_functions, state_enter].
+
+
+%% CONNECTING state
+connecting(enter, OldState,
+ #peer_state{tx_seq_nr = TxSeqNr} = S) ->
+ ?LOG_INFO("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
+ %% Tx PFCP Association Setup
+ ok = send_assoc_setup(S),
+ {next_state, connecting, S#peer_state{tx_seq_nr = TxSeqNr + 1},
+ [{state_timeout, 2_000, assoc_setup_timeout}]};
+
+%% Handle Association Setup timeout
+connecting(state_timeout, assoc_setup_timeout, _S) ->
+ {stop, {shutdown, assoc_setup_timeout}};
+
+%% Handle incoming PFCP PDU(s)
+connecting(info, {udp, Sock, _FromIp, _FromPort, Data},
+ #peer_state{sock = Sock} = S) ->
+ case decode_pdu(S, Data) of
+ {association_setup_response, #{pfcp_cause := 'Request accepted'}} ->
+ ?LOG_INFO("Rx Association Setup Response (Request accepted)"),
+ {next_state, connected, S};
+ {association_setup_response, #{pfcp_cause := Cause}} ->
+ ?LOG_ERROR("Rx Association Setup Response (~p)", [Cause]),
+ {stop, {shutdown, assoc_setup_nack}};
+ PDU ->
+ ?LOG_NOTICE("Rx unexpected PFCP PDU: ~p", [PDU]),
+ {keep_state, S}
+ end;
+
+connecting(Event, EventData, S) ->
+ ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]),
+ {keep_state, S}.
+
+
+%% CONNECTED state
+connected(enter, OldState, S) ->
+ ?LOG_INFO("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
+ {keep_state, S};
+
+%% Catch-all handler for this state
+connected(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]),
+ case State of
+ connected ->
+ send_assoc_release(S);
+ _ ->
+ nop
+ end,
+ gen_udp:close(S#peer_state.sock),
+ ok.
+
+
+%% ------------------------------------------------------------------
+%% private API
+%% ------------------------------------------------------------------
+
+-spec decode_pdu(peer_state(), binary()) -> pfcp_msg().
+decode_pdu(_S, Data) ->
+ ?LOG_DEBUG("Rx encoded PFCP PDU: ~p", [Data]),
+ PDU = pfcp_packet:decode(Data),
+ ?LOG_DEBUG("Rx PFCP PDU: ~p", [PDU]),
+ %% TODO: check PDU#pfcp.seq_no
+ %% TODO: there can be batched PDUs
+ {PDU#pfcp.type, PDU#pfcp.ie}.
+
+
+-spec send_data(peer_state(), binary()) -> ok | {error, term()}.
+send_data(#peer_state{sock = Sock, rem_addr = RemAddr}, Data) ->
+ ?LOG_DEBUG("Tx encoded PFCP PDU: ~p", [Data]),
+ gen_udp:send(Sock, RemAddr, ?PFCP_PORT, Data).
+
+
+-spec send_pdu(peer_state(), pfcp_msg()) -> ok | {error, term()}.
+send_pdu(S, {MsgType, IEs}) ->
+ TxSeqNr = S#peer_state.tx_seq_nr,
+ PDU = #pfcp{version = v1,
+ type = MsgType,
+ seid = undefined,
+ seq_no = TxSeqNr,
+ ie = IEs},
+ ?LOG_DEBUG("Tx PFCP PDU: ~p", [PDU]),
+ Data = pfcp_packet:encode(PDU),
+ send_data(S, Data).
+
+
+-spec get_node_id(peer_state()) -> binary().
+get_node_id(#peer_state{loc_addr = LocAddr}) ->
+ list_to_binary(tuple_to_list(LocAddr)).
+
+
+-spec get_recovery_timestamp() -> pos_integer().
+get_recovery_timestamp() ->
+ {NowMS, NowS, _NowUS} = erlang:timestamp(),
+ %% erlang:timestamp() returns time relative to the UNIX epoch (1970),
+ %% but for PFCP we need time relative to the NTP era 0 (1900).
+ %% 2208988800 is the diff between 1900 and 1970.
+ NowMS * 1_000_000 + NowS + 2208988800.
+
+
+send_assoc_setup(S) ->
+ IEs = #{node_id => #node_id{id = get_node_id(S)},
+ recovery_time_stamp => #recovery_time_stamp{time = get_recovery_timestamp()}},
+ send_pdu(S, {association_setup_request, IEs}).
+
+
+send_assoc_release(S) ->
+ IEs = #{node_id => #node_id{id = get_node_id(S)}},
+ send_pdu(S, {association_release_request, IEs}).
+
+%% vim:set ts=4 sw=4 et:

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

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