fixeria has uploaded this change for review. ( https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/37268?usp=email )
Change subject: pfcp_peer: initial PFCP peer implementation ......................................................................
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: