osmo-epdg/src/epdg_ue_fsm.erl

473 lines
22 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(epdg_ue_fsm).
-behaviour(gen_statem).
-define(NAME, epdg_ue_fsm).
-include_lib("osmo_gsup/include/gsup_protocol.hrl").
-include_lib("gtplib/include/gtp_packet.hrl").
-include_lib("gtp_utils.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([auth_request/2, lu_request/1, tunnel_request/2, purge_ms_request/1]).
-export([received_swm_auth_response/2, received_swm_auth_compl_response/2,
received_swm_session_termination_answer/2, received_swm_abort_session_request/1]).
-export([received_gtpc_create_session_response/2, received_gtpc_delete_session_response/2, received_gtpc_delete_bearer_request/1]).
-export([state_new/3, state_wait_auth_resp/3, state_authenticating/3, state_authenticated/3,
state_wait_create_session_resp/3, state_wait_delete_session_resp/3,
state_wait_swm_session_termination_answer/3, state_active/3,
state_dereg_net_initiated_wait_s2b_delete_session_resp/3]).
-define(TIMEOUT_VAL_WAIT_GTP_ANSWER, 10000).
-record(ue_fsm_data, {
imsi,
apn = "internet" :: string(),
pgw_rem_addr_list = [] :: list(),
tun_pdp_ctx :: epdg_tun_pdp_ctx,
tear_down_gsup_needed = false :: boolean(), %% need to send GSUP PurgeMSResp after STR+STA?
tear_down_gsup_cause = 0 :: integer()
}).
get_server_name_by_imsi(Imsi) ->
ServerName = lists:concat([?NAME, "_", binary_to_list(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(~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.
auth_request(Pid, {PdpTypeNr, Apn, EAP}) ->
lager:info("ue_fsm auth_request~n", []),
try
gen_statem:call(Pid, {auth_request, PdpTypeNr, Apn, EAP})
catch
exit:Err ->
{error, Err}
end.
lu_request(Pid) ->
lager:info("ue_fsm lu_request~n", []),
try
gen_statem:call(Pid, lu_request)
catch
exit:Err ->
{error, Err}
end.
tunnel_request(Pid, PCO) ->
lager:info("ue_fsm tunnel_request(~p)~n", [PCO]),
try
gen_statem:call(Pid, {tunnel_request, PCO})
catch
exit:Err ->
{error, Err}
end.
purge_ms_request(Pid) ->
lager:info("ue_fsm purge_ms_request~n", []),
try
gen_statem:call(Pid, purge_ms_request)
catch
exit:Err ->
{error, Err}
end.
received_swm_auth_response(Pid, Result) ->
lager:info("ue_fsm received_swm_auth_response ~p~n", [Result]),
try
gen_statem:call(Pid, {received_swm_auth_response, Result})
catch
exit:Err ->
{error, Err}
end.
received_swm_auth_compl_response(Pid, Result) ->
lager:info("ue_fsm received_swm_auth_compl_response ~p~n", [Result]),
try
gen_statem:call(Pid, {received_swm_auth_compl_response, Result})
catch
exit:Err ->
{error, Err}
end.
received_swm_session_termination_answer(Pid, Result) ->
lager:info("ue_fsm received_swm_session_termination_answer ~p~n", [Result]),
try
gen_statem:call(Pid, {received_swm_sta, Result})
catch
exit:Err ->
{error, Err}
end.
received_swm_abort_session_request(Pid) ->
lager:info("ue_fsm received_swm_abort_session_request~n", []),
try
gen_statem:call(Pid, received_swm_asr)
catch
exit:Err ->
{error, Err}
end.
received_gtpc_create_session_response(Pid, Result) ->
lager:info("ue_fsm received_gtpc_create_session_response ~p~n", [Result]),
try
gen_statem:call(Pid, {received_gtpc_create_session_response, Result})
catch
exit:Err ->
{error, Err}
end.
received_gtpc_delete_session_response(Pid, Msg) ->
lager:info("ue_fsm received_gtpc_delete_session_response ~p~n", [Msg]),
try
gen_statem:call(Pid, {received_gtpc_delete_session_response, Msg})
catch
exit:Err ->
{error, Err}
end.
received_gtpc_delete_bearer_request(Pid) ->
lager:info("ue_fsm received_gtpc_delete_bearer_request~n", []),
try
gen_statem:call(Pid, received_gtpc_delete_bearer_request)
catch
exit:Err ->
{error, Err}
end.
%% ------------------------------------------------------------------
%% Internal helpers
%% ------------------------------------------------------------------
ev_handle({call, From}, {auth_request, PdpTypeNr, Apn, EAP}, Data) ->
case epdg_diameter_swm:auth_request(Data#ue_fsm_data.imsi, PdpTypeNr, Apn, EAP) of
ok -> {next_state, state_wait_auth_resp, Data, [{reply,From,ok}]};
{error, Err} -> {stop_and_reply, Err, [{reply,From,{error,Err}}], Data}
end.
%% ------------------------------------------------------------------
%% 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]),
case Data#ue_fsm_data.tun_pdp_ctx of
undefined -> ok;
_ -> gtp_u_tun:delete_pdp_context(Data#ue_fsm_data.tun_pdp_ctx)
end,
ok.
state_new(enter, _OldState, Data) ->
{keep_state, Data};
state_new({call, _From} = EvType, {auth_request, PdpTypeNr, Apn, EAP} = EvContent, Data) ->
lager:info("ue_fsm state_new event=auth_request {~p, ~p, ~p}, ~p~n", [PdpTypeNr, Apn, EAP, Data]),
ev_handle(EvType, EvContent, Data);
state_new({call, From}, purge_ms_request, Data) ->
lager:info("ue_fsm state_new event=purge_ms_request, ~p~n", [Data]),
{stop_and_reply, purge_ms_request, [{reply,From,ok}], Data}.
state_wait_auth_resp(enter, _OldState, Data) ->
{keep_state, Data};
state_wait_auth_resp({call, From}, {received_swm_auth_response, Result}, Data) ->
lager:info("ue_fsm state_wait_auth_resp event=received_swm_auth_response Result=~p, ~p~n", [Result, Data]),
case Result of
{ok, _AuthTuples} ->
gsup_server:auth_response(Data#ue_fsm_data.imsi, Result),
{next_state, state_authenticating, Data, [{reply,From,ok}]};
{error, DiaRC} ->
GsupCause = conv:dia_rc_to_gsup_cause(DiaRC),
gsup_server:auth_response(Data#ue_fsm_data.imsi, {error, GsupCause}),
{next_state, state_new, Data, [{reply,From,ok}]};
_ ->
{next_state, state_new, Data, [{reply,From,{error,unknown}}]}
end.
state_authenticating(enter, _OldState, Data) ->
{keep_state, Data};
state_authenticating({call, _From} = EvType, {auth_request, PdpTypeNr, Apn, EAP} = EvContent, Data) ->
lager:info("ue_fsm state_authenticating event=auth_request {~p, ~p, ~p}, ~p~n", [PdpTypeNr, Apn, EAP, Data]),
ev_handle(EvType, EvContent, Data);
state_authenticating({call, From}, lu_request, Data) ->
lager:info("ue_fsm state_authenticating event=lu_request, ~p~n", [Data]),
% Rx "GSUP CEAI LU Req" is our way of saying Rx "Swm Diameter-EAP REQ (DER) with EAP AVP containing successuful auth":
case epdg_diameter_swm:auth_compl_request(Data#ue_fsm_data.imsi, Data#ue_fsm_data.apn) of
ok -> {keep_state, Data, [{reply,From,ok}]};
{error, Err} -> {stop_and_reply, Err, [{reply,From,{error,Err}}], Data}
end;
% Rx Swm Diameter-EAP Answer (DEA) containing APN-Configuration, triggered by
% earlier Tx DER EAP AVP containing successuful auth", when we received GSUP LU Req:
state_authenticating({call, From}, {received_swm_auth_compl_response, Result}, Data) ->
lager:info("ue_fsm state_authenticating event=lu_request, ~p, ~p~n", [Result, Data]),
% Rx "GSUP CEAI LU Req" is our way of saying Rx "Swm Diameter-EAP REQ (DER) with EAP AVP containing successuful auth":
case Result of
{ok, ResInfo} ->
% Store PGW Remote address if AAA/HSS signalled them to us:
case maps:find(pdp_info_list, ResInfo) of
error ->
Data1 = Data;
PGWAddrCandidateList ->
Data1 = Data#ue_fsm_data{pgw_rem_addr_list = PGWAddrCandidateList}
end,
gsup_server:lu_response(Data1#ue_fsm_data.imsi, ok),
{next_state, state_authenticated, Data1, [{reply,From,ok}]};
{error, DiaRC} ->
GsupCause = conv:dia_rc_to_gsup_cause(DiaRC),
gsup_server:lu_response(Data#ue_fsm_data.imsi, {error, GsupCause}),
{next_state, state_new, Data, [{reply,From,ok}]}
end.
state_authenticated(enter, _OldState, Data) ->
{keep_state, Data};
state_authenticated({call, _From}, {auth_request, PdpTypeNr, Apn, EAP}, Data) ->
lager:info("ue_fsm state_authenticated event=auth_request {~p, ~p, ~p}, ~p~n", [PdpTypeNr, Apn, EAP, Data]),
{next_state, state_new, Data, [postpone]};
state_authenticated({call, From}, {tunnel_request, PCO}, Data) ->
lager:info("ue_fsm state_authenticated event=tunnel_request, ~p~n", [Data]),
epdg_gtpc_s2b:create_session_req(Data#ue_fsm_data.imsi,
Data#ue_fsm_data.apn,
PCO,
Data#ue_fsm_data.pgw_rem_addr_list),
{next_state, state_wait_create_session_resp, Data, [{reply,From,ok}]};
state_authenticated({call, From}, purge_ms_request, Data) ->
lager:info("ue_fsm state_authenticated event=purge_ms_request, ~p~n", [Data]),
Data1 = Data#ue_fsm_data{tear_down_gsup_needed = true},
{next_state, state_wait_swm_session_termination_answer, Data1, [{reply,From,ok}]};
state_authenticated({call, From}, received_gtpc_delete_bearer_request, Data) ->
lager:info("ue_fsm state_authenticated event=received_gtpc_delete_bearer_request, ~p~n", [Data]),
gsup_server:cancel_location_request(Data#ue_fsm_data.imsi),
Data1 = Data#ue_fsm_data{tear_down_gsup_needed = false},
{next_state, state_wait_swm_session_termination_answer, Data1, [{reply,From,ok}]};
state_authenticated({call, From}, Event, Data) ->
lager:error("ue_fsm state_authenticated: Unexpected call event ~p, ~p~n", [Event, Data]),
{keep_state, Data, [{reply,From,ok}]};
state_authenticated(cast, Event, Data) ->
lager:error("ue_fsm state_authenticated: Unexpected cast event ~p, ~p~n", [Event, Data]),
{keep_state, Data}.
state_wait_create_session_resp(enter, _OldState, Data) ->
{keep_state, Data, [{state_timeout,?TIMEOUT_VAL_WAIT_GTP_ANSWER,create_session_timeout}]};
state_wait_create_session_resp({call, From}, {received_gtpc_create_session_response, Result}, Data) ->
lager:info("ue_fsm state_authenticated event=received_gtpc_create_session_response, ~p~n", [Data]),
case Result of
{ok, ResInfo} ->
#{eua := EUA,
local_teid := LocalTEID,
remote_teid := RemoteTEID,
remote_ipv4 := RemoteIPv4 % TODO: remote_ipv6
} = ResInfo,
TunPdpCtx = #epdg_tun_pdp_ctx{local_teid = LocalTEID, remote_teid = RemoteTEID,
eua = EUA, peer_addr = RemoteIPv4},
Ret = gtp_u_tun:create_pdp_context(TunPdpCtx),
lager:debug("gtp_u_tun:create_pdp_context(~p) returned ~p~n", [ResInfo, Ret]),
Data1 = Data#ue_fsm_data{tun_pdp_ctx = TunPdpCtx},
gsup_server:tunnel_response(Data1#ue_fsm_data.imsi, Result),
{next_state, state_active, Data1, [{reply,From,ok}]};
{error, GtpCause} ->
GsupCause = conv:cause_gtp2gsup(GtpCause),
gsup_server:tunnel_response(Data#ue_fsm_data.imsi, {error, GsupCause}),
{next_state, state_authenticated, Data, [{reply,From,ok}]}
end;
state_wait_create_session_resp({call, From}, Event, Data) ->
lager:error("ue_fsm state_wait_delete_session_resp: Unexpected call event ~p, ~p~n", [Event, Data]),
{keep_state, Data, [{reply,From,{error,unexpected_event}}]};
state_wait_create_session_resp(state_timeout, create_session_timeout, Data) ->
lager:error("ue_fsm state_wait_create_session_resp: Timeout ~p, ~p~n", [create_session_timeout, Data]),
gsup_server:tunnel_response(Data#ue_fsm_data.imsi, {error, ?GSUP_CAUSE_CONGESTION}),
{next_state, state_authenticated, Data}.
state_active(enter, _OldState, Data) ->
{keep_state, Data};
state_active({call, _From}, {auth_request, PdpTypeNr, Apn, EAP}, Data) ->
lager:info("ue_fsm state_active event=auth_request {~p, ~p, ~p}, ~p~n", [PdpTypeNr, Apn, EAP, Data]),
gtp_u_tun:delete_pdp_context(Data#ue_fsm_data.tun_pdp_ctx),
Data1 = Data#ue_fsm_data{tun_pdp_ctx = undefined},
{next_state, state_new, Data1, [postpone]};
state_active({call, From}, purge_ms_request, Data) ->
lager:info("ue_fsm state_active event=purge_ms_request, ~p~n", [Data]),
gtp_u_tun:delete_pdp_context(Data#ue_fsm_data.tun_pdp_ctx),
Data1 = Data#ue_fsm_data{tun_pdp_ctx = undefined},
case epdg_gtpc_s2b:delete_session_req(Data1#ue_fsm_data.imsi) of
ok -> {next_state, state_wait_delete_session_resp, Data1, [{reply,From,ok}]};
{error, Err} -> {keep_state, Data1, [{reply,From,{error, Err}}]}
end;
state_active({call, From}, received_gtpc_delete_bearer_request, Data) ->
lager:info("ue_fsm state_active event=received_gtpc_delete_bearer_request, ~p~n", [Data]),
gtp_u_tun:delete_pdp_context(Data#ue_fsm_data.tun_pdp_ctx),
gsup_server:cancel_location_request(Data#ue_fsm_data.imsi),
Data1 = Data#ue_fsm_data{tun_pdp_ctx = undefined, tear_down_gsup_needed = false},
{next_state, state_wait_swm_session_termination_answer, Data1, [{reply,From,ok}]};
%%% network (HSS/AAA) initiated de-registation requested:
state_active({call, From}, received_swm_asr, Data) ->
lager:info("ue_fsm state_active event=received_swm_asr, ~p~n", [Data]),
gsup_server:cancel_location_request(Data#ue_fsm_data.imsi),
gtp_u_tun:delete_pdp_context(Data#ue_fsm_data.tun_pdp_ctx),
Data1 = Data#ue_fsm_data{tun_pdp_ctx = undefined, tear_down_gsup_needed = false},
{next_state, state_dereg_net_initiated_wait_s2b_delete_session_resp, Data1, [{reply,From,ok}]};
state_active({call, From}, Event, Data) ->
lager:error("ue_fsm state_active: Unexpected call event ~p, ~p~n", [Event, Data]),
{keep_state, Data, [{reply,From,ok}]};
state_active(cast, Event, Data) ->
lager:error("ue_fsm state_active: Unexpected cast event ~p, ~p~n", [Event, Data]),
{keep_state, Data}.
state_wait_delete_session_resp(enter, _OldState, Data) ->
{keep_state, Data};
state_wait_delete_session_resp({call, From}, {received_gtpc_delete_session_response, _Resp = #gtp{version = v2, type = delete_session_response, ie = IEs}}, Data) ->
lager:info("ue_fsm state_wait_delete_session_resp event=received_gtpc_delete_session_response, ~p~n", [Data]),
#{{v2_cause,0} := CauseIE} = IEs,
GtpCause = gtp_utils:enum_v2_cause(CauseIE#v2_cause.v2_cause),
GsupCause = conv:cause_gtp2gsup(GtpCause),
lager:debug("Cause: GTP_atom=~p -> GTP_int=~p -> GSUP_int=~p~n", [CauseIE#v2_cause.v2_cause, GtpCause, GsupCause]),
Data1 = Data#ue_fsm_data{tear_down_gsup_needed = true},
case GsupCause of
0 -> Data2 = Data1;
_ -> Data2 = Data1#ue_fsm_data{tear_down_gsup_cause = GsupCause}
end,
{next_state, state_wait_swm_session_termination_answer, Data2, [{reply,From,ok}]};
state_wait_delete_session_resp({call, From}, Event, Data) ->
lager:error("ue_fsm state_wait_delete_session_resp: Unexpected call event ~p, ~p~n", [Event, Data]),
{keep_state, Data, [{reply,From,{error,unexpected_event}}]}.
state_wait_swm_session_termination_answer(enter, _OldState, Data) ->
% Send STR towards AAA-Server
% % 3GPP TS 29.273 7.1.2.3
lager:info("ue_fsm state_wait_swm_session_termination_answer event=enter, ~p~n", [Data]),
case epdg_diameter_swm:session_termination_request(Data#ue_fsm_data.imsi) of
ok -> {keep_state, Data};
{error, _Err} ->
case Data#ue_fsm_data.tear_down_gsup_needed of
true -> gsup_server:purge_ms_response(Data#ue_fsm_data.imsi, {error, ?GSUP_CAUSE_NET_FAIL});
false -> ok
end,
{keep_state, Data}
end;
state_wait_swm_session_termination_answer({call, From}, {received_swm_sta, DiaRC}, Data) ->
lager:info("ue_fsm state_wait_swm_session_termination_answer event=received_swm_sta, ~p~n", [Data]),
case Data#ue_fsm_data.tear_down_gsup_needed of
true ->
case {DiaRC#epdg_dia_rc.result_code, Data#ue_fsm_data.tear_down_gsup_cause} of
{2001, 0} -> gsup_server:purge_ms_response(Data#ue_fsm_data.imsi, ok);
{2001, _} -> gsup_server:purge_ms_response(Data#ue_fsm_data.imsi, {error, Data#ue_fsm_data.tear_down_gsup_cause});
_ -> gsup_server:purge_ms_response(Data#ue_fsm_data.imsi, {error, ?GSUP_CAUSE_NET_FAIL})
end;
false -> ok
end,
{stop_and_reply, normal, [{reply,From,ok}], Data};
state_wait_swm_session_termination_answer({call, From}, Event, Data) ->
lager:error("ue_fsm state_wait_delete_session_resp: Unexpected call event ~p, ~p~n", [Event, Data]),
{keep_state, Data, [{reply,From,{error,unexpected_event}}]}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% state_dereg_net_initiated_wait_s2b_delete_session_resp:
%% Network (AAA/HSS) initiated de-registration: We have informed UE (GSUP), and
%% have triggered GTPCv1 Delete Session Req against PGW.
%% Wait for GTPCv1 Delete Session Response, ssend SWm ASA to AAAA and terminate FSM.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
state_dereg_net_initiated_wait_s2b_delete_session_resp(enter, _OldState, Data) ->
case epdg_gtpc_s2b:delete_session_req(Data#ue_fsm_data.imsi) of
ok ->
{keep_state, Data, {state_timeout,?TIMEOUT_VAL_WAIT_GTP_ANSWER,s2b_delete_session_timeout}};
{error, Err} ->
epdg_diameter_swm:abort_session_answer(Data#ue_fsm_data.imsi),
{stop, {error,Err}}
end;
state_dereg_net_initiated_wait_s2b_delete_session_resp({call, From}, {received_gtpc_delete_session_response, _Resp = #gtp{version = v2, type = delete_session_response, ie = IEs}}, Data) ->
lager:info("ue_fsm state_wait_delete_session_resp event=state_dereg_net_initiated_wait_s2b_delete_session_resp, ~p~n", [Data]),
#{{v2_cause,0} := CauseIE} = IEs,
GtpCause = gtp_utils:enum_v2_cause(CauseIE#v2_cause.v2_cause),
lager:debug("Cause: GTP_atom=~p -> GTP_int=~p~n", [CauseIE#v2_cause.v2_cause, GtpCause]),
epdg_diameter_swm:abort_session_answer(Data#ue_fsm_data.imsi),
{stop_and_reply, normal, [{reply,From,ok}], Data};
state_dereg_net_initiated_wait_s2b_delete_session_resp({call, From}, Event, Data) ->
lager:error("ue_fsm state_dereg_net_initiated_wait_s2b_delete_session_resp: Unexpected call event ~p, ~p~n", [Event, Data]),
{keep_state, Data, [{reply,From,ok}]};
state_dereg_net_initiated_wait_s2b_delete_session_resp(state_timeout, s2b_delete_session_timeout, Data) ->
lager:error("ue_fsm state_dereg_net_initiated_wait_s2b_delete_session_resp: Timeout ~p, ~p~n", [s2b_delete_session_timeout, Data]),
epdg_diameter_swm:abort_session_answer(Data#ue_fsm_data.imsi),
{stop, normal}.