|
|
@@ -27,7 +27,7 @@
|
|
|
-export([start_link/0, stop/0]).
|
|
|
|
|
|
%% API
|
|
|
--export([check/1, detect/1]).
|
|
|
+-export([detect/1]).
|
|
|
|
|
|
%% gen_server callbacks
|
|
|
-export([ init/1
|
|
|
@@ -54,8 +54,7 @@
|
|
|
clientid :: emqx_types:clientid(),
|
|
|
peerhost :: emqx_types:peerhost(),
|
|
|
started_at :: pos_integer(),
|
|
|
- detect_cnt :: pos_integer(),
|
|
|
- banned_at :: pos_integer()
|
|
|
+ detect_cnt :: pos_integer()
|
|
|
}).
|
|
|
|
|
|
-opaque(flapping() :: #flapping{}).
|
|
|
@@ -68,27 +67,14 @@ start_link() ->
|
|
|
|
|
|
stop() -> gen_server:stop(?MODULE).
|
|
|
|
|
|
-%% @doc Check flapping when a MQTT client connected.
|
|
|
--spec(check(emqx_types:clientinfo()) -> boolean()).
|
|
|
-check(#{clientid := ClientId}) ->
|
|
|
- check(ClientId, get_policy()).
|
|
|
-
|
|
|
-check(ClientId, #{banned_interval := Interval}) ->
|
|
|
- case ets:lookup(?FLAPPING_TAB, {banned, ClientId}) of
|
|
|
- [] -> false;
|
|
|
- [#flapping{banned_at = BannedAt}] ->
|
|
|
- now_diff(BannedAt) < Interval
|
|
|
- end.
|
|
|
-
|
|
|
%% @doc Detect flapping when a MQTT client disconnected.
|
|
|
-spec(detect(emqx_types:clientinfo()) -> boolean()).
|
|
|
detect(Client) -> detect(Client, get_policy()).
|
|
|
|
|
|
-detect(#{clientid := ClientId, peerhost := PeerHost},
|
|
|
- Policy = #{threshold := Threshold}) ->
|
|
|
+detect(#{clientid := ClientId, peerhost := PeerHost}, Policy = #{threshold := Threshold}) ->
|
|
|
try ets:update_counter(?FLAPPING_TAB, ClientId, {#flapping.detect_cnt, 1}) of
|
|
|
Cnt when Cnt < Threshold -> false;
|
|
|
- _Cnt -> case ets:lookup(?FLAPPING_TAB, ClientId) of
|
|
|
+ _Cnt -> case ets:take(?FLAPPING_TAB, ClientId) of
|
|
|
[Flapping] ->
|
|
|
ok = gen_server:cast(?MODULE, {detected, Flapping, Policy}),
|
|
|
true;
|
|
|
@@ -118,52 +104,44 @@ now_diff(TS) -> erlang:system_time(millisecond) - TS.
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
|
|
init([]) ->
|
|
|
- #{duration := Duration, banned_interval := Interval} = get_policy(),
|
|
|
ok = emqx_tables:new(?FLAPPING_TAB, [public, set,
|
|
|
{keypos, 2},
|
|
|
{read_concurrency, true},
|
|
|
{write_concurrency, true}
|
|
|
]),
|
|
|
- State = #{time => max(Duration, Interval) + 1, tref => undefined},
|
|
|
- {ok, ensure_timer(State), hibernate}.
|
|
|
+ {ok, #{}, hibernate}.
|
|
|
|
|
|
handle_call(Req, _From, State) ->
|
|
|
?LOG(error, "Unexpected call: ~p", [Req]),
|
|
|
{reply, ignored, State}.
|
|
|
|
|
|
-handle_cast({detected, Flapping = #flapping{clientid = ClientId,
|
|
|
- peerhost = PeerHost,
|
|
|
- started_at = StartedAt,
|
|
|
- detect_cnt = DetectCnt},
|
|
|
- #{duration := Duration}}, State) ->
|
|
|
- case (Interval = now_diff(StartedAt)) < Duration of
|
|
|
+handle_cast({detected, #flapping{clientid = ClientId,
|
|
|
+ peerhost = PeerHost,
|
|
|
+ started_at = StartedAt,
|
|
|
+ detect_cnt = DetectCnt},
|
|
|
+ #{duration := Duration, banned_interval := Interval}}, State) ->
|
|
|
+ case now_diff(StartedAt) < Duration of
|
|
|
true -> %% Flapping happened:(
|
|
|
- %% Log first
|
|
|
?LOG(error, "Flapping detected: ~s(~s) disconnected ~w times in ~wms",
|
|
|
[ClientId, esockd_net:ntoa(PeerHost), DetectCnt, Duration]),
|
|
|
- %% Banned.
|
|
|
- BannedFlapping = Flapping#flapping{clientid = {banned, ClientId},
|
|
|
- banned_at = erlang:system_time(millisecond)
|
|
|
- },
|
|
|
- alarm_handler:set_alarm({{flapping_detected, ClientId}, BannedFlapping}),
|
|
|
- ets:insert(?FLAPPING_TAB, BannedFlapping);
|
|
|
+ Now = erlang:system_time(millisecond),
|
|
|
+ Banned = #banned{who = {clientid, ClientId},
|
|
|
+ by = <<"flapping detector">>,
|
|
|
+ reason = <<"flapping is detected">>,
|
|
|
+ at = Now,
|
|
|
+ until = Now + Interval},
|
|
|
+ alarm_handler:set_alarm({{flapping_detected, ClientId}, Banned}),
|
|
|
+ emqx_banned:create(Banned);
|
|
|
false ->
|
|
|
?LOG(warning, "~s(~s) disconnected ~w times in ~wms",
|
|
|
[ClientId, esockd_net:ntoa(PeerHost), DetectCnt, Interval])
|
|
|
end,
|
|
|
- ets:delete_object(?FLAPPING_TAB, Flapping),
|
|
|
{noreply, State};
|
|
|
|
|
|
handle_cast(Msg, State) ->
|
|
|
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
|
|
{noreply, State}.
|
|
|
|
|
|
-handle_info({timeout, TRef, expire_flapping}, State = #{tref := TRef}) ->
|
|
|
- with_flapping_tab(fun expire_flapping/2,
|
|
|
- [erlang:system_time(millisecond),
|
|
|
- get_policy()]),
|
|
|
- {noreply, ensure_timer(State#{tref => undefined}), hibernate};
|
|
|
-
|
|
|
handle_info(Info, State) ->
|
|
|
?LOG(error, "Unexpected info: ~p", [Info]),
|
|
|
{noreply, State}.
|
|
|
@@ -173,34 +151,3 @@ terminate(_Reason, _State) ->
|
|
|
|
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
|
{ok, State}.
|
|
|
-
|
|
|
-%%--------------------------------------------------------------------
|
|
|
-%% Internal functions
|
|
|
-%%--------------------------------------------------------------------
|
|
|
-
|
|
|
-ensure_timer(State = #{time := Time, tref := undefined}) ->
|
|
|
- State#{tref => emqx_misc:start_timer(Time, expire_flapping)};
|
|
|
-ensure_timer(State) -> State.
|
|
|
-
|
|
|
-with_flapping_tab(Fun, Args) ->
|
|
|
- case ets:info(?FLAPPING_TAB, size) of
|
|
|
- undefined -> ok;
|
|
|
- 0 -> ok;
|
|
|
- _Size -> erlang:apply(Fun, Args)
|
|
|
- end.
|
|
|
-
|
|
|
-expire_flapping(NowTime, #{duration := Duration, banned_interval := Interval}) ->
|
|
|
- case ets:select(?FLAPPING_TAB,
|
|
|
- [{#flapping{started_at = '$1', banned_at = undefined, _ = '_'},
|
|
|
- [{'<', '$1', NowTime-Duration}], ['$_']},
|
|
|
- {#flapping{clientid = {banned, '_'}, banned_at = '$1', _ = '_'},
|
|
|
- [{'<', '$1', NowTime-Interval}], ['$_']}]) of
|
|
|
- [] -> ok;
|
|
|
- Flappings ->
|
|
|
- lists:foreach(fun(Flapping = #flapping{clientid = {banned, ClientId}}) ->
|
|
|
- ets:delete_object(?FLAPPING_TAB, Flapping),
|
|
|
- alarm_handler:clear_alarm({flapping_detected, ClientId});
|
|
|
- (_) -> ok
|
|
|
- end, Flappings)
|
|
|
- end.
|
|
|
-
|