osmo-epdg/src/aaa_ue_fsm.erl

373 lines
16 KiB
Erlang

% UE FSM
% (C) 2023 by sysmocom
%
% 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(aaa_ue_fsm).
-behaviour(gen_statem).
-define(NAME, aaa_ue_fsm).
-include_lib("diameter/include/diameter.hrl").
-include_lib("diameter_3gpp_ts29_229.hrl").
-include_lib("diameter_3gpp_ts29_273_s6b.hrl").
-include("conv.hrl").
-export([start/1, stop/1]).
-export([init/1,callback_mode/0,terminate/3]).
-export([get_server_name_by_imsi/1, get_pid_by_imsi/1]).
-export([ev_swm_auth_req/2, ev_swm_auth_compl/2, ev_rx_swm_str/1, ev_rx_swm_asa/1,
ev_rx_swx_maa/2, ev_rx_swx_saa/2, ev_rx_swx_rtr/1,
ev_rx_s6b_aar/2, ev_rx_s6b_str/1, ev_rx_s6b_asa/2]).
-export([state_new/3,
state_wait_swx_maa/3,
state_wait_swx_saa/3,
state_authenticated/3,
state_authenticated_wait_swx_saa/3,
state_dereg_net_initiated_wait_s6b_asa/3,
state_dereg_net_initiated_wait_swm_asa/3]).
-define(TIMEOUT_VAL_WAIT_S6b_ANSWER, 10000).
-define(TIMEOUT_VAL_WAIT_SWm_ANSWER, 10000).
-record(ue_fsm_data, {
imsi = unknown :: string(),
apn :: string(),
epdg_sess_active = false :: boolean(),
pgw_sess_active = false :: boolean(),
s6b_resp_pid :: pid()
}).
get_server_name_by_imsi(Imsi) ->
ServerName = lists:concat([?NAME, "_", Imsi]),
list_to_atom(ServerName).
get_pid_by_imsi(Imsi) ->
ServerName = get_server_name_by_imsi(Imsi),
whereis(ServerName).
start(Imsi) ->
ServerName = get_server_name_by_imsi(Imsi),
lager:info("ue_fsm start_link(~p)~n", [ServerName]),
gen_statem:start({local, ServerName}, ?MODULE, Imsi, [{debug, [trace]}]).
stop(SrvRef) ->
try
gen_statem:stop(SrvRef)
catch
exit:Err ->
{error, Err}
end.
ev_swm_auth_req(Pid, {PdpTypeNr, Apn, EAP}) ->
lager:info("ue_fsm ev_swm_auth_req~n", []),
try
gen_statem:call(Pid, {swm_auth_req, PdpTypeNr, Apn, EAP})
catch
exit:Err ->
{error, Err}
end.
ev_swm_auth_compl(Pid, Apn) ->
lager:info("ue_fsm ev_swm_auth_compl~n", []),
try
gen_statem:call(Pid, {swm_auth_compl, Apn})
catch
exit:Err ->
{error, Err}
end.
ev_rx_swm_str(Pid) ->
lager:info("ue_fsm ev_rx_swm_str~n", []),
try
gen_statem:call(Pid, rx_swm_str)
catch
exit:Err ->
{error, Err}
end.
ev_rx_swm_asa(Pid) ->
lager:info("ue_fsm ev_rx_swm_asa~n", []),
try
gen_statem:call(Pid, rx_swm_asa)
catch
exit:Err ->
{error, Err}
end.
ev_rx_swx_maa(Pid, Result) ->
lager:info("ue_fsm ev_rx_swx_maa~n", []),
try
gen_statem:call(Pid, {rx_swx_maa, Result})
catch
exit:Err ->
{error, Err}
end.
ev_rx_swx_saa(Pid, Result) ->
lager:info("ue_fsm ev_rx_swx_saa~n", []),
try
gen_statem:call(Pid, {rx_swx_saa, Result})
catch
exit:Err ->
{error, Err}
end.
ev_rx_swx_rtr(Pid) ->
lager:info("ue_fsm ev_rx_swx_rtr~n", []),
try
gen_statem:call(Pid, rx_swx_rtr)
catch
exit:Err ->
{error, Err}
end.
ev_rx_s6b_aar(Pid, {Apn, AgentInfoOpt}) ->
lager:info("ue_fsm ev_rx_s6b_aar: ~p ~p~n", [Apn, AgentInfoOpt]),
try
gen_statem:call(Pid, {rx_s6b_aar, Apn, AgentInfoOpt})
catch
exit:Err ->
{error, Err}
end.
ev_rx_s6b_asa(Pid, Result) ->
lager:info("ue_fsm ev_rx_s6b_asa: ~p~n", [Result]),
try
gen_statem:call(Pid, {rx_s6b_asa, Result})
catch
exit:Err ->
{error, Err}
end.
ev_rx_s6b_str(Pid) ->
lager:info("ue_fsm ev_rx_s6b_str~n", []),
try
gen_statem:call(Pid, rx_s6b_str)
catch
exit:Err ->
{error, Err}
end.
%% ------------------------------------------------------------------
%% Internal helpers
%% ------------------------------------------------------------------
%% ------------------------------------------------------------------
%% gen_statem Function Definitions
%% ------------------------------------------------------------------
init(Imsi) ->
lager:info("ue_fsm init(~p)~n", [Imsi]),
Data = #ue_fsm_data{imsi = Imsi},
{ok, state_new, Data}.
callback_mode() ->
[state_functions, state_enter].
terminate(Reason, State, Data) ->
lager:info("terminating ~p with reason ~p state=~p, ~p~n", [?MODULE, Reason, State, Data]),
ok.
state_new(enter, _OldState, Data) ->
{keep_state, Data};
state_new({call, From}, {swm_auth_req, PdpTypeNr, Apn, EAP}, Data) ->
lager:info("ue_fsm state_new event=swm_auth_req {~p, ~p, ~p}, ~p~n", [PdpTypeNr, Apn, EAP, Data]),
case maps:find(authorization, EAP) of
{ok, Authorization} when is_binary(Authorization) -> Authorization;
error -> Authorization = []
end,
case aaa_diameter_swx:multimedia_auth_request(Data#ue_fsm_data.imsi, 1, 1, "EAP-AKA", PdpTypeNr, Authorization) of
ok -> {next_state, state_wait_swx_maa, Data, [{reply,From,ok}]};
{error, Err} -> {keep_state, Data, [{reply,From,{error, Err}}]}
end;
state_new({call, From}, {swm_auth_compl, Apn}, Data) ->
lager:info("ue_fsm state_new event=swm_auth_compl, ~p~n", [Data]),
case aaa_diameter_swx:server_assignment_request(Data#ue_fsm_data.imsi, 1, Apn, []) of
ok -> {next_state, state_wait_swx_saa, Data, [{reply,From,ok}]};
{error, Err} -> {keep_state, Data, [{reply,From,{error, Err}}]}
end.
state_wait_swx_maa(enter, _OldState, Data) ->
{keep_state, Data};
state_wait_swx_maa({call, From}, {rx_swx_maa, Result}, Data) ->
lager:info("ue_fsm state_wait_swx_maa event=rx_swx_maa, ~p~n", [Data]),
aaa_diameter_swm:auth_response(Data#ue_fsm_data.imsi, Result),
{next_state, state_new, Data, [{reply,From,ok}]}.
state_wait_swx_saa(enter, _OldState, Data) ->
{keep_state, Data};
state_wait_swx_saa({call, From}, {rx_swx_saa, Result}, Data) ->
lager:info("ue_fsm state_wait_swx_saa event=rx_swx_saa ~p, ~p~n", [Result, Data]),
case Result of
{error, _SAType, DiaRC} ->
aaa_diameter_swm:auth_compl_response(Data#ue_fsm_data.imsi, {error, DiaRC}),
{next_state, state_new, Data, [{reply,From,ok}]};
{ok, _SAType, ResInfo} ->
aaa_diameter_swm:auth_compl_response(Data#ue_fsm_data.imsi, {ok, ResInfo}),
{next_state, state_authenticated, Data, [{reply,From,ok}]}
end.
state_authenticated(enter, _OldState, Data) ->
% Mark ePDG session as active:
Data1 = Data#ue_fsm_data{epdg_sess_active = true},
{keep_state, Data1};
state_authenticated({call, {Pid, _Tag} = From}, {rx_s6b_aar, Apn, AgentInfoOpt}, Data) ->
lager:info("ue_fsm state_authenticated event=rx_s6b_aar Apn=~p AgentInfo=~p, ~p~n", [Apn, AgentInfoOpt, Data]),
case aaa_diameter_swx:server_assignment_request(Data#ue_fsm_data.imsi,
?'DIAMETER_CX_SERVER-ASSIGNMENT-TYPE_PGW_UPDATE',
Apn, AgentInfoOpt) of
ok -> Data1 = Data#ue_fsm_data{s6b_resp_pid = Pid, apn = Apn},
{next_state, state_authenticated_wait_swx_saa, Data1, [{reply,From,ok}]};
{error, Err} -> {keep_state, Data, [{reply,From,{error, Err}}]}
end;
state_authenticated({call, From}, rx_swm_str, Data) ->
lager:info("ue_fsm state_authenticated event=rx_swm_str, ~p~n", [Data]),
case {Data#ue_fsm_data.epdg_sess_active, Data#ue_fsm_data.pgw_sess_active} of
{false, _} -> %% The SWm session is not active...
DiaRC = 5002, %% UNKNOWN_SESSION_ID
{keep_state, Data, [{reply,From,{error, DiaRC}}]};
{true, true} -> %% The other session is still active, no need to send SAR Type=USER_DEREGISTRATION
lager:info("ue_fsm state_authenticated event=rx_swm_str: PGW session still active, skip updating the HSS~n", []),
Data1 = Data#ue_fsm_data{epdg_sess_active = false},
{keep_state, Data1, [{reply,From,{ok, 2001}}]};
{true, false} -> %% All sessions will now be gone, trigger SAR Type=USER_DEREGISTRATION
case aaa_diameter_swx:server_assignment_request(Data#ue_fsm_data.imsi,
?'DIAMETER_CX_SERVER-ASSIGNMENT-TYPE_USER_DEREGISTRATION',
Data#ue_fsm_data.apn, []) of
ok -> {next_state, state_authenticated_wait_swx_saa, Data, [{reply,From,ok}]};
{error, _Err} ->
DiaRC = 5002, %% UNKNOWN_SESSION_ID
{keep_state, Data, [{reply,From,{error, DiaRC}}]}
end
end;
state_authenticated({call, {Pid, _Tag} = From}, rx_s6b_str, Data) ->
lager:info("ue_fsm state_authenticated event=rx_s6b_str, ~p~n", [Data]),
case {Data#ue_fsm_data.pgw_sess_active, Data#ue_fsm_data.epdg_sess_active} of
{false, _} -> %% The S6b session is not active...
DiaRC = #epdg_dia_rc{result_code = 5002}, %% UNKNOWN_SESSION_ID
{keep_state, Data, [{reply,From,{error, DiaRC}}]};
{true, true} -> %% The other session is still active, no need to send SAR Type=USER_DEREGISTRATION
lager:info("ue_fsm state_authenticated event=rx_s6b_str: ePDG session still active, skip updating the HSS~n", []),
Data1 = Data#ue_fsm_data{pgw_sess_active = false},
DiaRC = #epdg_dia_rc{result_code = 2001}, %% SUCCESS
{keep_state, Data1, [{reply,From,{ok, DiaRC}}]};
{true, false} -> %% All sessions will now be gone, trigger SAR Type=USER_DEREGISTRATION
case aaa_diameter_swx:server_assignment_request(Data#ue_fsm_data.imsi,
?'DIAMETER_CX_SERVER-ASSIGNMENT-TYPE_USER_DEREGISTRATION',
Data#ue_fsm_data.apn, []) of
ok -> Data1 = Data#ue_fsm_data{s6b_resp_pid = Pid},
{next_state, state_authenticated_wait_swx_saa, Data1, [{reply,From,ok}]};
{error, _Err} ->
DiaRC = #epdg_dia_rc{result_code = 5002}, %% UNKNOWN_SESSION_ID
{keep_state, Data, [{reply,From,{error, DiaRC}}]}
end
end;
state_authenticated({call, _From}, {swm_auth_req, PdpTypeNr, Apn, EAP}, Data) ->
lager:info("ue_fsm state_authenticated event=swm_auth_req {~p, ~p, ~p}, ~p~n", [PdpTypeNr, Apn, EAP, Data]),
{next_state, state_new, Data, [postpone]};
state_authenticated({call, From}, rx_swx_rtr, Data) ->
lager:info("ue_fsm state_authenticated event=rx_swx_rtr ~p~n", [Data]),
case {Data#ue_fsm_data.pgw_sess_active, Data#ue_fsm_data.epdg_sess_active} of
{true, _} -> {next_state, state_dereg_net_initiated_wait_s6b_asa, Data, [{reply,From,ok}]};
{false, _} -> {next_state, state_dereg_net_initiated_wait_s6b_asa, Data, [{reply,From,ok}]} %% TODO: proper state for s6b
end;
state_authenticated({call, From}, Ev, Data) ->
lager:info("ue_fsm state_authenticated: Unexpected call event ~p, ~p~n", [Ev, Data]),
{keep_state, Data, [{reply,From,ok}]}.
state_authenticated_wait_swx_saa(enter, _OldState, Data) ->
{keep_state, Data};
state_authenticated_wait_swx_saa({call, From}, {rx_swx_saa, Result}, Data) ->
case Result of
{error, SAType, DiaRC} -> DiaRC;
{ok, SAType, _ResInfo} -> DiaRC = #epdg_dia_rc{result_code = 2001}
end,
lager:info("ue_fsm state_authenticated_wait_swx_saa event=rx_swx_saa SAType=~p ResulCode=~p, ~p~n", [SAType, DiaRC, Data]),
case SAType of
?'DIAMETER_CX_SERVER-ASSIGNMENT-TYPE_PGW_UPDATE' ->
aaa_diameter_s6b:tx_aa_answer(Data#ue_fsm_data.s6b_resp_pid, DiaRC),
Data1 = Data#ue_fsm_data{pgw_sess_active = true, s6b_resp_pid = undefined},
{next_state, state_authenticated, Data1, [{reply,From,ok}]};
?'DIAMETER_CX_SERVER-ASSIGNMENT-TYPE_USER_DEREGISTRATION' ->
case Data#ue_fsm_data.s6b_resp_pid of
undefined -> %% SWm initiated
aaa_diameter_swm:session_termination_answer(Data#ue_fsm_data.imsi, DiaRC),
Data1 = Data#ue_fsm_data{epdg_sess_active = false},
{next_state, state_new, Data1, [{reply,From,ok}]};
_ -> %% S6b initiated
aaa_diameter_s6b:tx_st_answer(Data#ue_fsm_data.s6b_resp_pid, DiaRC),
Data1 = Data#ue_fsm_data{pgw_sess_active = false, s6b_resp_pid = undefined},
{next_state, state_new, Data1, [{reply,From,ok}]}
end
end.
%% HSS asked us to do deregistration towards the user.
%% Transmit S6b ASR towards PGW and wait for ASA back.
state_dereg_net_initiated_wait_s6b_asa(enter, _OldState, Data) ->
aaa_diameter_s6b:tx_as_request(Data#ue_fsm_data.imsi),
{keep_state, Data, {state_timeout,?TIMEOUT_VAL_WAIT_S6b_ANSWER,s6b_asa_timeout}};
state_dereg_net_initiated_wait_s6b_asa({call, From}, {rx_s6b_asa, _Result}, Data) ->
{next_state, state_dereg_net_initiated_wait_swm_asa, Data, [{reply,From,ok}]};
state_dereg_net_initiated_wait_s6b_asa({call, From}, Ev, Data) ->
lager:info("ue_fsm state_dereg_net_initiated_wait_s6b_asa: Unexpected call event ~p, ~p~n", [Ev, Data]),
{keep_state, Data, [{reply,From,ok}]};
state_dereg_net_initiated_wait_s6b_asa(state_timeout, s6b_asa_timeout, Data) ->
{next_state, state_dereg_net_initiated_wait_swm_asa, Data}.
%% HSS asked us to do deregistration towards the user.
%% S6b (PGW) was already torn down. Now transmit SWm ASR towards ePDG and wait for ASA back.
state_dereg_net_initiated_wait_swm_asa(enter, _OldState, Data) ->
aaa_diameter_swm:tx_as_request(Data#ue_fsm_data.imsi),
{keep_state, Data, {state_timeout,?TIMEOUT_VAL_WAIT_SWm_ANSWER,swm_asa_timeout}};
state_dereg_net_initiated_wait_swm_asa({call, From}, {rx_swm_asa, _Result}, Data) ->
{stop_and_reply, normal, [{reply,From,ok}], Data};
state_dereg_net_initiated_wait_swm_asa({call, From}, Ev, Data) ->
lager:info("ue_fsm state_dereg_net_initiated_wait_swm_asa: Unexpected call event ~p, ~p~n", [Ev, Data]),
{keep_state, Data, [{reply,From,ok}]};
state_dereg_net_initiated_wait_swm_asa(state_timeout, swm_asa_timeout, _Data) ->
{stop, normal}.