osmo-epdg/src/epdg_gtpc_s2b.erl

367 lines
14 KiB
Erlang

% S2b: GTPv2C towards PGW
%
% 3GPP TS 29.274
%
% (C) 2023 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
% Author: Pau Espin Pedrol <pespin@sysmocom.de>
%
% All Rights Reserved
%
% 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 <http://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 http://www.erlang.org (or a modified version of these
% libraries), containing parts covered by the terms of the Erlang Public
% License (http://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(epdg_gtpc_s2b).
-author('Pau Espin Pedrol <pespin@sysmocom.de>').
-behaviour(gen_server).
-include_lib("gtplib/include/gtp_packet.hrl").
%% API Function Exports
-export([start_link/5]).
-export([terminate/2]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-export([code_change/3]).
-export([create_session_req/1]).
%% Application Definitions
-define(SERVER, ?MODULE).
-define(SVC_NAME, ?MODULE).
-define(APP_ALIAS, ?MODULE).
-define(CALLBACK_MOD, epdg_gtpc_s2b_cb).
-define(ENV_APP_NAME, osmo_epdg).
%% TODO: make APN configurable? get it from HSS?
-define(APN, <<"internet">>).
-define(MCC, 901).
-define(MNC, 42).
-record(gtp_state, {
socket,
laddr_str,
laddr :: inet:ip_address(),
lport :: non_neg_integer(),
raddr_str,
raddr :: inet:ip_address(),
rport :: non_neg_integer(),
restart_counter :: 0..255,
seq_no :: 0..16#ffffffff,
next_local_control_tei = 1 :: 0..16#ffffffff,
next_local_data_tei = 1 :: 0..16#ffffffff,
sessions = sets:new()
}).
-record(gtp_bearer, {
ebi :: non_neg_integer(),
local_data_tei = 0 :: non_neg_integer(),
remote_data_tei = 0 :: non_neg_integer()
}).
-record(gtp_session, {
imsi :: binary(),
apn :: binary(),
ue_ip :: inet:ip_address(),
local_control_tei = 0 :: non_neg_integer(),
remote_control_tei = 0 :: non_neg_integer(),
bearer :: gtp_bearer %% FIXME: only one bearer for now
}).
start_link(LocalAddr, LocalPort, RemoteAddr, RemotePort, Options) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [LocalAddr, LocalPort, RemoteAddr, RemotePort, Options], []).
peer_down(API, SvcName, {PeerRef, _} = Peer) ->
% fixme: why do we still have ets here?
(catch ets:delete(?MODULE, {API, PeerRef})),
gen_server:cast(?SERVER, {peer_down, SvcName, Peer}),
ok.
% from travelping
ip_to_bin({A, B, C, D}) ->
<<A, B, C, D>>.
plmn(CC, NC, NCSize) ->
MCC = iolist_to_binary(io_lib:format("~3..0b", [CC])),
MNC = iolist_to_binary(io_lib:format("~*..0b", [NCSize, NC])),
{MCC, MNC}.
init(State) ->
lager:info("epdg_gtpc_s2b: init(): ~p", [State]),
[LocalAddr | [LocalPort | [RemoteAddr | [RemotePort | _]]]] = State,
lager:info("epdg_gtpc_s2b: Binding to IP ~s port ~p~n", [LocalAddr, LocalPort]),
{ok, LocalAddrInet} = inet_parse:address(LocalAddr),
{ok, RemoteAddrInet} = inet_parse:address(RemoteAddr),
Opts = [
binary,
{ip, LocalAddrInet},
{active, true},
{reuseaddr, true}
],
Ret = gen_udp:open(LocalPort, Opts),
case Ret of
{ok, Socket} ->
lager:info("epdg_gtpc_s2b: Socket is ~p~n", [Socket]),
ok = connect({Socket, RemoteAddr, RemotePort}),
St = #gtp_state{
socket = Socket,
laddr_str = LocalAddr,
laddr = LocalAddrInet,
lport = LocalPort,
raddr_str = RemoteAddr,
raddr = RemoteAddrInet,
rport = RemotePort,
restart_counter = 0,
seq_no = 0
},
{ok, St};
{error, Reason} ->
lager:error("GTPv2C UDP socket open error: ~w~n", [Reason])
end.
create_session_req(Imsi) ->
gen_server:call(?SERVER,
{gtpc_create_session_req, {Imsi}}).
handle_call({gtpc_create_session_req, {Imsi}}, _From, State0) ->
{Sess0, State1} = find_or_new_gtp_session(Imsi, State0),
Req = gen_create_session_request(Sess0, State1),
%TODO: increment State.seq_no.
tx_gtp(Req, State1),
lager:debug("Waiting for CreateSessionResponse~n", []),
receive
{udp, _Socket, IP, InPortNo, RxMsg} ->
try
Resp = gtp_packet:decode(RxMsg),
lager:info("s2b: Rx from IP ~p port ~p ~p~n", [IP, InPortNo, Resp]),
Sess1 = update_gtp_session_from_create_session_response(Resp, Sess0),
lager:info("s2b: Updated Session after create_session_response: ~p~n", [Sess1]),
State2 = update_gtp_session(Sess0, Sess1, State1),
{reply, {ok, Resp}, State2}
catch Any ->
lager:error("Error sending message to receiver, ERROR: ~p~n", [Any]),
{reply, {error, decode_failure}, State1}
end
after 5000 ->
lager:error("Timeout waiting for CreateSessionResponse for ~p~n", [Req]),
{reply, timeout, State1}
end.
%% @callback gen_server
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(Req, State) ->
lager:info("S2b handle_cast: ~p ~n", [Req]),
{noreply, State}.
%% @callback gen_server
handle_info({udp, _Socket, IP, InPortNo, RxMsg}, State) ->
lager:info("S2b: Rx from IP ~p port ~p: ~p~n", [IP, InPortNo, RxMsg]),
Req = gtp_packet:decode(RxMsg),
lager:info("S2b: Rx from IP ~p port ~p: ~p~n", [IP, InPortNo, Req]),
rx_gtp(Req, State);
handle_info(Info, State) ->
lager:info("S2b handle_info: ~p ~n", [Info]),
{noreply, State}.
%% @callback gen_server
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% @callback gen_server
terminate(normal, State) ->
udp_gen:close(State#gtp_state.socket),
ok;
terminate(shutdown, _State) ->
ok;
terminate({shutdown, _Reason}, _State) ->
ok;
terminate(_Reason, _State) ->
ok.
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
new_gtp_session(Imsi, State) ->
% TODO: find non-used local TEI inside State
Bearer = #gtp_bearer{
ebi = 5,
local_data_tei = State#gtp_state.next_local_data_tei
},
Sess = #gtp_session{imsi = Imsi,
apn = ?APN,
local_control_tei = State#gtp_state.next_local_control_tei,
bearer = Bearer
},
NewSt = State#gtp_state{next_local_control_tei = State#gtp_state.next_local_control_tei + 1,
next_local_data_tei = State#gtp_state.next_local_data_tei + 1,
sessions = sets:add_element(Sess, State#gtp_state.sessions)},
{Sess, NewSt}.
% returns Sess if found, undefined it not
find_gtp_session_by_imsi(Imsi, State) ->
sets:fold(
fun(SessIt = #gtp_session{imsi = Imsi}, _AccIn) -> SessIt;
(_, AccIn) -> AccIn
end,
undefined,
State#gtp_state.sessions).
find_or_new_gtp_session(Imsi, State) ->
Sess = find_gtp_session_by_imsi(Imsi, State),
case Sess of
#gtp_session{imsi = Imsi} ->
{Sess, State};
undefined ->
new_gtp_session(Imsi, State)
end.
update_gtp_session(OldSess, NewSess, State) ->
SetRemoved = sets:del_element(OldSess, State#gtp_state.sessions),
SetUpdated = sets:add_element(NewSess, SetRemoved),
State#gtp_state{sessions = SetUpdated}.
delete_gtp_session(Sess, State) ->
SetRemoved = sets:del_element(Sess, State#gtp_state.sessions),
State#gtp_state{sessions = SetRemoved}.
update_gtp_session_from_create_session_response_ie(none, Sess) ->
Sess;
update_gtp_session_from_create_session_response_ie({_,
#v2_fully_qualified_tunnel_endpoint_identifier{
interface_type = _Interface,
key = TEI, ipv4 = _IP4, ipv6 = _IP6},
Next}, Sess) ->
update_gtp_session_from_create_session_response_ie(maps:next(Next), Sess#gtp_session{remote_control_tei = TEI});
update_gtp_session_from_create_session_response_ie({_, _, Next},
Sess) ->
update_gtp_session_from_create_session_response_ie(maps:next(Next), Sess).
update_gtp_session_from_create_session_response_ies(#gtp{ie = IEs}, Sess) ->
update_gtp_session_from_create_session_response_ie(maps:next(maps:iterator(IEs)), Sess).
update_gtp_session_from_create_session_response(Resp = #gtp{version = v2, type = create_session_response}, Sess) ->
update_gtp_session_from_create_session_response_ies(#gtp{ie = Resp#gtp.ie}, Sess).
% returns Sess if found, undefined it not
find_gtp_session_by_local_teic(LocalControlTei, State) ->
sets:fold(
fun(SessIt = #gtp_session{local_control_tei = LocalControlTei}, _AccIn) -> SessIt;
(_, AccIn) -> AccIn
end,
undefined,
State#gtp_state.sessions).
%% connect/2
connect(Name, {Socket, RemoteAddr, RemotePort}) ->
lager:info("~s connecting to IP ~s port ~p~n", [Name, RemoteAddr, RemotePort]),
gen_udp:connect(Socket, RemoteAddr, RemotePort).
connect(Address) ->
connect(?SVC_NAME, Address).
rx_gtp(Req = #gtp{version = v2, type = delete_bearer_request}, State) ->
Sess = find_gtp_session_by_local_teic(Req#gtp.tei, State),
case Sess of
undefined ->
lager:error("Rx unknown TEI ~p: ~p~n", [Req#gtp.tei, Req]),
{noreply, State};
Sess ->
Resp = gen_delete_bearer_response(Req, Sess, request_accepted, State),
tx_gtp(Resp, State),
State1 = delete_gtp_session(Sess, State),
{noreply, State1}
end;
rx_gtp(Req, State) ->
lager:error("S2b: UNIMPLEMENTED Rx: ~p~n", [Req]),
{noreply, State}.
tx_gtp(Req, State) ->
lager:info("s2b: Tx ~p~n", [Req]),
Msg = gtp_packet:encode(Req),
gen_udp:send(State#gtp_state.socket, State#gtp_state.raddr, State#gtp_state.rport, Msg).
%% 7.2.1 Create Session Request
gen_create_session_request(#gtp_session{imsi = Imsi,
apn = Apn,
local_control_tei = LocalCtlTEI,
bearer = Bearer},
#gtp_state{laddr = LocalAddr,
restart_counter = RCnt,
seq_no = SeqNo}) ->
BearersIE = [#v2_bearer_level_quality_of_service{
pci = 1, pl = 10, pvi = 0, label = 8,
maximum_bit_rate_for_uplink = 0,
maximum_bit_rate_for_downlink = 0,
guaranteed_bit_rate_for_uplink = 0,
guaranteed_bit_rate_for_downlink = 0
},
#v2_eps_bearer_id{eps_bearer_id = Bearer#gtp_bearer.ebi},
#v2_fully_qualified_tunnel_endpoint_identifier{
instance = Bearer#gtp_bearer.ebi,
interface_type = 31, %% "S2b-U ePDG GTP-U"
key = Bearer#gtp_bearer.local_data_tei,
ipv4 = ip_to_bin(LocalAddr)
}
],
IEs = [#v2_recovery{restart_counter = RCnt},
#v2_international_mobile_subscriber_identity{imsi = Imsi},
#v2_rat_type{rat_type = 3}, %% 3 = WLAN
#v2_fully_qualified_tunnel_endpoint_identifier{
instance = 0,
interface_type = 30, %% "S2b ePDG GTP-C"
key = LocalCtlTEI,
ipv4 = ip_to_bin(LocalAddr)
},
#v2_access_point_name{instance = 0, apn = [Apn]},
#v2_selection_mode{mode = 0},
#v2_pdn_address_allocation{type = ipv4, address = <<0,0,0,0>>},
#v2_bearer_context{group = BearersIE},
#v2_serving_network{
instance = 0,
plmn_id = plmn(?MCC, ?MNC, 3)
}
],
#gtp{version = v2, type = create_session_request, tei = 0, seq_no = SeqNo, ie = IEs}.
gen_delete_bearer_response(Req = #gtp{version = v2, type = delete_bearer_request},
Sess = #gtp_session{remote_control_tei = RemoteCtlTEI},
GtpCause,
#gtp_state{restart_counter = RCnt}) ->
IEs = [#v2_recovery{restart_counter = RCnt},
#v2_cause{v2_cause = GtpCause}
],
#gtp{version = v2,
type = delete_bearer_response,
tei = RemoteCtlTEI,
seq_no = Req#gtp.seq_no,
ie = IEs}.