feat(banned): allow ban by clientid/username regexps, peerhost cidrs
This commit is contained in:
parent
755b59b7fe
commit
90fd2b26d3
|
@ -88,10 +88,7 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-record(banned, {
|
-record(banned, {
|
||||||
who ::
|
who :: emqx_types:banned_who(),
|
||||||
{clientid, binary()}
|
|
||||||
| {peerhost, inet:ip_address()}
|
|
||||||
| {username, binary()},
|
|
||||||
by :: binary(),
|
by :: binary(),
|
||||||
reason :: binary(),
|
reason :: binary(),
|
||||||
at :: integer(),
|
at :: integer(),
|
||||||
|
|
|
@ -39,7 +39,9 @@
|
||||||
info/1,
|
info/1,
|
||||||
format/1,
|
format/1,
|
||||||
parse/1,
|
parse/1,
|
||||||
clear/0
|
clear/0,
|
||||||
|
who/2,
|
||||||
|
tables/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
|
@ -61,7 +63,8 @@
|
||||||
|
|
||||||
-elvis([{elvis_style, state_record_and_type, disable}]).
|
-elvis([{elvis_style, state_record_and_type, disable}]).
|
||||||
|
|
||||||
-define(BANNED_TAB, ?MODULE).
|
-define(BANNED_INDIVIDUAL_TAB, ?MODULE).
|
||||||
|
-define(BANNED_RULE_TAB, emqx_banned_rules).
|
||||||
|
|
||||||
%% The default expiration time should be infinite
|
%% The default expiration time should be infinite
|
||||||
%% but for compatibility, a large number (1 years) is used here to represent the 'infinite'
|
%% but for compatibility, a large number (1 years) is used here to represent the 'infinite'
|
||||||
|
@ -77,19 +80,24 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = mria:create_table(?BANNED_TAB, [
|
Options = [
|
||||||
{type, set},
|
{type, set},
|
||||||
{rlog_shard, ?COMMON_SHARD},
|
{rlog_shard, ?COMMON_SHARD},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{record_name, banned},
|
{record_name, banned},
|
||||||
{attributes, record_info(fields, banned)},
|
{attributes, record_info(fields, banned)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}
|
||||||
]).
|
],
|
||||||
|
ok = mria:create_table(?BANNED_INDIVIDUAL_TAB, Options),
|
||||||
|
ok = mria:create_table(?BANNED_RULE_TAB, Options).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Data backup
|
%% Data backup
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
backup_tables() -> [?BANNED_TAB].
|
backup_tables() -> tables().
|
||||||
|
|
||||||
|
-spec tables() -> [atom()].
|
||||||
|
tables() -> [?BANNED_RULE_TAB, ?BANNED_INDIVIDUAL_TAB].
|
||||||
|
|
||||||
%% @doc Start the banned server.
|
%% @doc Start the banned server.
|
||||||
-spec start_link() -> startlink_ret().
|
-spec start_link() -> startlink_ret().
|
||||||
|
@ -104,16 +112,10 @@ stop() -> gen_server:stop(?MODULE).
|
||||||
check(ClientInfo) ->
|
check(ClientInfo) ->
|
||||||
do_check({clientid, maps:get(clientid, ClientInfo, undefined)}) orelse
|
do_check({clientid, maps:get(clientid, ClientInfo, undefined)}) orelse
|
||||||
do_check({username, maps:get(username, ClientInfo, undefined)}) orelse
|
do_check({username, maps:get(username, ClientInfo, undefined)}) orelse
|
||||||
do_check({peerhost, maps:get(peerhost, ClientInfo, undefined)}).
|
do_check({peerhost, maps:get(peerhost, ClientInfo, undefined)}) orelse
|
||||||
|
do_check_rules(ClientInfo).
|
||||||
do_check({_, undefined}) ->
|
|
||||||
false;
|
|
||||||
do_check(Who) when is_tuple(Who) ->
|
|
||||||
case mnesia:dirty_read(?BANNED_TAB, Who) of
|
|
||||||
[] -> false;
|
|
||||||
[#banned{until = Until}] -> Until > erlang:system_time(second)
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
-spec format(emqx_types:banned()) -> map().
|
||||||
format(#banned{
|
format(#banned{
|
||||||
who = Who0,
|
who = Who0,
|
||||||
by = By,
|
by = By,
|
||||||
|
@ -121,7 +123,7 @@ format(#banned{
|
||||||
at = At,
|
at = At,
|
||||||
until = Until
|
until = Until
|
||||||
}) ->
|
}) ->
|
||||||
{As, Who} = maybe_format_host(Who0),
|
{As, Who} = format_who(Who0),
|
||||||
#{
|
#{
|
||||||
as => As,
|
as => As,
|
||||||
who => Who,
|
who => Who,
|
||||||
|
@ -131,6 +133,7 @@ format(#banned{
|
||||||
until => to_rfc3339(Until)
|
until => to_rfc3339(Until)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
-spec parse(map()) -> emqx_types:banned() | {error, term()}.
|
||||||
parse(Params) ->
|
parse(Params) ->
|
||||||
case parse_who(Params) of
|
case parse_who(Params) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -155,24 +158,6 @@ parse(Params) ->
|
||||||
{error, ErrorReason}
|
{error, ErrorReason}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
parse_who(#{as := As, who := Who}) ->
|
|
||||||
parse_who(#{<<"as">> => As, <<"who">> => Who});
|
|
||||||
parse_who(#{<<"as">> := peerhost, <<"who">> := Peerhost0}) ->
|
|
||||||
case inet:parse_address(binary_to_list(Peerhost0)) of
|
|
||||||
{ok, Peerhost} -> {peerhost, Peerhost};
|
|
||||||
{error, einval} -> {error, "bad peerhost"}
|
|
||||||
end;
|
|
||||||
parse_who(#{<<"as">> := As, <<"who">> := Who}) ->
|
|
||||||
{As, Who}.
|
|
||||||
|
|
||||||
maybe_format_host({peerhost, Host}) ->
|
|
||||||
AddrBinary = list_to_binary(inet:ntoa(Host)),
|
|
||||||
{peerhost, AddrBinary};
|
|
||||||
maybe_format_host({As, Who}) ->
|
|
||||||
{As, Who}.
|
|
||||||
|
|
||||||
to_rfc3339(Timestamp) ->
|
|
||||||
emqx_utils_calendar:epoch_to_rfc3339(Timestamp, second).
|
|
||||||
|
|
||||||
-spec create(emqx_types:banned() | map()) ->
|
-spec create(emqx_types:banned() | map()) ->
|
||||||
{ok, emqx_types:banned()} | {error, {already_exist, emqx_types:banned()}}.
|
{ok, emqx_types:banned()} | {error, {already_exist, emqx_types:banned()}}.
|
||||||
|
@ -194,7 +179,7 @@ create(#{
|
||||||
create(Banned = #banned{who = Who}) ->
|
create(Banned = #banned{who = Who}) ->
|
||||||
case look_up(Who) of
|
case look_up(Who) of
|
||||||
[] ->
|
[] ->
|
||||||
insert_banned(Banned),
|
insert_banned(table(Who), Banned),
|
||||||
{ok, Banned};
|
{ok, Banned};
|
||||||
[OldBanned = #banned{until = Until}] ->
|
[OldBanned = #banned{until = Until}] ->
|
||||||
%% Don't support shorten or extend the until time by overwrite.
|
%% Don't support shorten or extend the until time by overwrite.
|
||||||
|
@ -204,33 +189,52 @@ create(Banned = #banned{who = Who}) ->
|
||||||
{error, {already_exist, OldBanned}};
|
{error, {already_exist, OldBanned}};
|
||||||
%% overwrite expired one is ok.
|
%% overwrite expired one is ok.
|
||||||
false ->
|
false ->
|
||||||
insert_banned(Banned),
|
insert_banned(table(Who), Banned),
|
||||||
{ok, Banned}
|
{ok, Banned}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec look_up(emqx_types:banned_who() | map()) -> [emqx_types:banned()].
|
||||||
look_up(Who) when is_map(Who) ->
|
look_up(Who) when is_map(Who) ->
|
||||||
look_up(parse_who(Who));
|
look_up(parse_who(Who));
|
||||||
look_up(Who) ->
|
look_up(Who) ->
|
||||||
mnesia:dirty_read(?BANNED_TAB, Who).
|
mnesia:dirty_read(table(Who), Who).
|
||||||
|
|
||||||
-spec delete(
|
-spec delete(map() | emqx_types:banned_who()) -> ok.
|
||||||
{clientid, emqx_types:clientid()}
|
|
||||||
| {username, emqx_types:username()}
|
|
||||||
| {peerhost, emqx_types:peerhost()}
|
|
||||||
) -> ok.
|
|
||||||
delete(Who) when is_map(Who) ->
|
delete(Who) when is_map(Who) ->
|
||||||
delete(parse_who(Who));
|
delete(parse_who(Who));
|
||||||
delete(Who) ->
|
delete(Who) ->
|
||||||
mria:dirty_delete(?BANNED_TAB, Who).
|
mria:dirty_delete(table(Who), Who).
|
||||||
|
|
||||||
info(InfoKey) ->
|
-spec info(size) -> non_neg_integer().
|
||||||
mnesia:table_info(?BANNED_TAB, InfoKey).
|
info(size) ->
|
||||||
|
mnesia:table_info(?BANNED_INDIVIDUAL_TAB, size) + mnesia:table_info(?BANNED_RULE_TAB, size).
|
||||||
|
|
||||||
|
-spec clear() -> ok.
|
||||||
clear() ->
|
clear() ->
|
||||||
_ = mria:clear_table(?BANNED_TAB),
|
_ = mria:clear_table(?BANNED_INDIVIDUAL_TAB),
|
||||||
|
_ = mria:clear_table(?BANNED_RULE_TAB),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
%% Creating banned with `#banned{}` records is exposed as a public API
|
||||||
|
%% so we need helpers to create the `who` field of `#banned{}` records
|
||||||
|
-spec who(atom(), binary() | inet:ip_address() | esockd_cidr:cidr()) -> emqx_types:banned_who().
|
||||||
|
who(clientid, ClientId) when is_binary(ClientId) -> {clientid, ClientId};
|
||||||
|
who(username, Username) when is_binary(Username) -> {username, Username};
|
||||||
|
who(peerhost, Peerhost) when is_tuple(Peerhost) -> {peerhost, Peerhost};
|
||||||
|
who(peerhost, Peerhost) when is_binary(Peerhost) ->
|
||||||
|
{ok, Addr} = inet:parse_address(binary_to_list(Peerhost)),
|
||||||
|
{peerhost, Addr};
|
||||||
|
who(clientid_re, RE) when is_binary(RE) ->
|
||||||
|
{ok, RECompiled} = re:compile(RE),
|
||||||
|
{clientid_re, {RECompiled, RE}};
|
||||||
|
who(username_re, RE) when is_binary(RE) ->
|
||||||
|
{ok, RECompiled} = re:compile(RE),
|
||||||
|
{username_re, {RECompiled, RE}};
|
||||||
|
who(peerhost_net, CIDR) when is_tuple(CIDR) -> {peerhost_net, CIDR};
|
||||||
|
who(peerhost_net, CIDR) when is_binary(CIDR) ->
|
||||||
|
{peerhost_net, esockd_cidr:parse(binary_to_list(CIDR), true)}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -265,6 +269,81 @@ code_change(_OldVsn, State, _Extra) ->
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_check({_, undefined}) ->
|
||||||
|
false;
|
||||||
|
do_check(Who) when is_tuple(Who) ->
|
||||||
|
case mnesia:dirty_read(table(Who), Who) of
|
||||||
|
[] -> false;
|
||||||
|
[#banned{until = Until}] -> Until > erlang:system_time(second)
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_check_rules(ClientInfo) ->
|
||||||
|
Rules = all_rules(),
|
||||||
|
Now = erlang:system_time(second),
|
||||||
|
lists:any(
|
||||||
|
fun(Rule) -> is_rule_actual(Rule, Now) andalso do_check_rule(Rule, ClientInfo) end, Rules
|
||||||
|
).
|
||||||
|
|
||||||
|
is_rule_actual(#banned{until = Until}, Now) ->
|
||||||
|
Until > Now.
|
||||||
|
|
||||||
|
do_check_rule(#banned{who = {clientid_re, {RE, _}}}, #{clientid := ClientId}) ->
|
||||||
|
is_binary(ClientId) andalso re:run(ClientId, RE) =/= nomatch;
|
||||||
|
do_check_rule(#banned{who = {clientid_re, _}}, #{}) ->
|
||||||
|
false;
|
||||||
|
do_check_rule(#banned{who = {username_re, {RE, _}}}, #{username := Username}) ->
|
||||||
|
is_binary(Username) andalso re:run(Username, RE) =/= nomatch;
|
||||||
|
do_check_rule(#banned{who = {username_re, _}}, #{}) ->
|
||||||
|
false;
|
||||||
|
do_check_rule(#banned{who = {peerhost_net, CIDR}}, #{peerhost := Peerhost}) ->
|
||||||
|
esockd_cidr:match(Peerhost, CIDR);
|
||||||
|
do_check_rule(#banned{who = {peerhost_net, _}}, #{}) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
parse_who(#{as := As, who := Who}) ->
|
||||||
|
parse_who(#{<<"as">> => As, <<"who">> => Who});
|
||||||
|
parse_who(#{<<"as">> := peerhost, <<"who">> := Peerhost0}) ->
|
||||||
|
case inet:parse_address(binary_to_list(Peerhost0)) of
|
||||||
|
{ok, Peerhost} -> {peerhost, Peerhost};
|
||||||
|
{error, einval} -> {error, "bad peerhost"}
|
||||||
|
end;
|
||||||
|
parse_who(#{<<"as">> := peerhost_net, <<"who">> := CIDRString}) ->
|
||||||
|
try esockd_cidr:parse(binary_to_list(CIDRString), true) of
|
||||||
|
CIDR -> {peerhost_net, CIDR}
|
||||||
|
catch
|
||||||
|
error:Error -> {error, Error}
|
||||||
|
end;
|
||||||
|
parse_who(#{<<"as">> := AsRE, <<"who">> := Who}) when
|
||||||
|
AsRE =:= clientid_re orelse AsRE =:= username_re
|
||||||
|
->
|
||||||
|
case re:compile(Who) of
|
||||||
|
{ok, RE} -> {AsRE, {RE, Who}};
|
||||||
|
{error, _} = Error -> Error
|
||||||
|
end;
|
||||||
|
parse_who(#{<<"as">> := As, <<"who">> := Who}) when As =:= clientid orelse As =:= username ->
|
||||||
|
{As, Who}.
|
||||||
|
|
||||||
|
format_who({peerhost, Host}) ->
|
||||||
|
AddrBinary = list_to_binary(inet:ntoa(Host)),
|
||||||
|
{peerhost, AddrBinary};
|
||||||
|
format_who({peerhost_net, CIDR}) ->
|
||||||
|
CIDRBinary = list_to_binary(esockd_cidr:to_string(CIDR)),
|
||||||
|
{peerhost_net, CIDRBinary};
|
||||||
|
format_who({AsRE, {_RE, REOriginal}}) when AsRE =:= clientid_re orelse AsRE =:= username_re ->
|
||||||
|
{AsRE, REOriginal};
|
||||||
|
format_who({As, Who}) when As =:= clientid orelse As =:= username ->
|
||||||
|
{As, Who}.
|
||||||
|
|
||||||
|
to_rfc3339(Timestamp) ->
|
||||||
|
emqx_utils_calendar:epoch_to_rfc3339(Timestamp, second).
|
||||||
|
|
||||||
|
table({username, _Username}) -> ?BANNED_INDIVIDUAL_TAB;
|
||||||
|
table({clientid, _ClientId}) -> ?BANNED_INDIVIDUAL_TAB;
|
||||||
|
table({peerhost, _Peerhost}) -> ?BANNED_INDIVIDUAL_TAB;
|
||||||
|
table({username_re, _UsernameRE}) -> ?BANNED_RULE_TAB;
|
||||||
|
table({clientid_re, _ClientIdRE}) -> ?BANNED_RULE_TAB;
|
||||||
|
table({peerhost_net, _PeerhostNet}) -> ?BANNED_RULE_TAB.
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
ensure_expiry_timer(State) ->
|
ensure_expiry_timer(State) ->
|
||||||
State#{expiry_timer := emqx_utils:start_timer(10, expire)}.
|
State#{expiry_timer := emqx_utils:start_timer(10, expire)}.
|
||||||
|
@ -274,19 +353,27 @@ ensure_expiry_timer(State) ->
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
expire_banned_items(Now) ->
|
expire_banned_items(Now) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun(Tab) ->
|
||||||
|
expire_banned_items(Now, Tab)
|
||||||
|
end,
|
||||||
|
[?BANNED_INDIVIDUAL_TAB, ?BANNED_RULE_TAB]
|
||||||
|
).
|
||||||
|
|
||||||
|
expire_banned_items(Now, Tab) ->
|
||||||
mnesia:foldl(
|
mnesia:foldl(
|
||||||
fun
|
fun
|
||||||
(B = #banned{until = Until}, _Acc) when Until < Now ->
|
(B = #banned{until = Until}, _Acc) when Until < Now ->
|
||||||
mnesia:delete_object(?BANNED_TAB, B, sticky_write);
|
mnesia:delete_object(Tab, B, sticky_write);
|
||||||
(_, _Acc) ->
|
(_, _Acc) ->
|
||||||
ok
|
ok
|
||||||
end,
|
end,
|
||||||
ok,
|
ok,
|
||||||
?BANNED_TAB
|
Tab
|
||||||
).
|
).
|
||||||
|
|
||||||
insert_banned(Banned) ->
|
insert_banned(Tab, Banned) ->
|
||||||
mria:dirty_write(?BANNED_TAB, Banned),
|
mria:dirty_write(Tab, Banned),
|
||||||
on_banned(Banned).
|
on_banned(Banned).
|
||||||
|
|
||||||
on_banned(#banned{who = {clientid, ClientId}}) ->
|
on_banned(#banned{who = {clientid, ClientId}}) ->
|
||||||
|
@ -302,3 +389,6 @@ on_banned(#banned{who = {clientid, ClientId}}) ->
|
||||||
ok;
|
ok;
|
||||||
on_banned(_) ->
|
on_banned(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
all_rules() ->
|
||||||
|
ets:tab2list(?BANNED_RULE_TAB).
|
||||||
|
|
|
@ -150,7 +150,7 @@ handle_cast(
|
||||||
),
|
),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Banned = #banned{
|
Banned = #banned{
|
||||||
who = {clientid, ClientId},
|
who = emqx_banned:who(clientid, ClientId),
|
||||||
by = <<"flapping detector">>,
|
by = <<"flapping detector">>,
|
||||||
reason = <<"flapping is detected">>,
|
reason = <<"flapping is detected">>,
|
||||||
at = Now,
|
at = Now,
|
||||||
|
|
|
@ -100,6 +100,7 @@
|
||||||
|
|
||||||
-export_type([
|
-export_type([
|
||||||
banned/0,
|
banned/0,
|
||||||
|
banned_who/0,
|
||||||
command/0
|
command/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
@ -246,6 +247,14 @@
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-type banned() :: #banned{}.
|
-type banned() :: #banned{}.
|
||||||
|
-type banned_who() ::
|
||||||
|
{clientid, binary()}
|
||||||
|
| {peerhost, inet:ip_address()}
|
||||||
|
| {username, binary()}
|
||||||
|
| {clientid_re, {_RE :: tuple(), binary()}}
|
||||||
|
| {username_re, {_RE :: tuple(), binary()}}
|
||||||
|
| {peerhost_net, esockd_cidr:cidr()}.
|
||||||
|
|
||||||
-type deliver() :: {deliver, topic(), message()}.
|
-type deliver() :: {deliver, topic(), message()}.
|
||||||
-type delivery() :: #delivery{}.
|
-type delivery() :: #delivery{}.
|
||||||
-type deliver_result() :: ok | {ok, non_neg_integer()} | {error, term()}.
|
-type deliver_result() :: ok | {ok, non_neg_integer()} | {error, term()}.
|
||||||
|
|
|
@ -34,7 +34,7 @@ end_per_suite(Config) ->
|
||||||
|
|
||||||
t_add_delete(_) ->
|
t_add_delete(_) ->
|
||||||
Banned = #banned{
|
Banned = #banned{
|
||||||
who = {clientid, <<"TestClient">>},
|
who = emqx_banned:who(clientid, <<"TestClient">>),
|
||||||
by = <<"banned suite">>,
|
by = <<"banned suite">>,
|
||||||
reason = <<"test">>,
|
reason = <<"test">>,
|
||||||
at = erlang:system_time(second),
|
at = erlang:system_time(second),
|
||||||
|
@ -47,54 +47,91 @@ t_add_delete(_) ->
|
||||||
emqx_banned:create(Banned#banned{until = erlang:system_time(second) + 100}),
|
emqx_banned:create(Banned#banned{until = erlang:system_time(second) + 100}),
|
||||||
?assertEqual(1, emqx_banned:info(size)),
|
?assertEqual(1, emqx_banned:info(size)),
|
||||||
|
|
||||||
ok = emqx_banned:delete({clientid, <<"TestClient">>}),
|
ok = emqx_banned:delete(emqx_banned:who(clientid, <<"TestClient">>)),
|
||||||
?assertEqual(0, emqx_banned:info(size)).
|
?assertEqual(0, emqx_banned:info(size)).
|
||||||
|
|
||||||
t_check(_) ->
|
t_check(_) ->
|
||||||
{ok, _} = emqx_banned:create(#banned{who = {clientid, <<"BannedClient">>}}),
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(clientid, <<"BannedClient">>)}),
|
||||||
{ok, _} = emqx_banned:create(#banned{who = {username, <<"BannedUser">>}}),
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(username, <<"BannedUser">>)}),
|
||||||
{ok, _} = emqx_banned:create(#banned{who = {peerhost, {192, 168, 0, 1}}}),
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(peerhost, {192, 168, 0, 1})}),
|
||||||
?assertEqual(3, emqx_banned:info(size)),
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(peerhost, <<"192.168.0.2">>)}),
|
||||||
ClientInfo1 = #{
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(clientid_re, <<"BannedClientRE.*">>)}),
|
||||||
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(username_re, <<"BannedUserRE.*">>)}),
|
||||||
|
{ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(peerhost_net, <<"192.168.3.0/24">>)}),
|
||||||
|
|
||||||
|
?assertEqual(7, emqx_banned:info(size)),
|
||||||
|
ClientInfoBannedClientId = #{
|
||||||
clientid => <<"BannedClient">>,
|
clientid => <<"BannedClient">>,
|
||||||
username => <<"user">>,
|
username => <<"user">>,
|
||||||
peerhost => {127, 0, 0, 1}
|
peerhost => {127, 0, 0, 1}
|
||||||
},
|
},
|
||||||
ClientInfo2 = #{
|
ClientInfoBannedUsername = #{
|
||||||
clientid => <<"client">>,
|
clientid => <<"client">>,
|
||||||
username => <<"BannedUser">>,
|
username => <<"BannedUser">>,
|
||||||
peerhost => {127, 0, 0, 1}
|
peerhost => {127, 0, 0, 1}
|
||||||
},
|
},
|
||||||
ClientInfo3 = #{
|
ClientInfoBannedAddr1 = #{
|
||||||
clientid => <<"client">>,
|
clientid => <<"client">>,
|
||||||
username => <<"user">>,
|
username => <<"user">>,
|
||||||
peerhost => {192, 168, 0, 1}
|
peerhost => {192, 168, 0, 1}
|
||||||
},
|
},
|
||||||
ClientInfo4 = #{
|
ClientInfoBannedAddr2 = #{
|
||||||
|
clientid => <<"client">>,
|
||||||
|
username => <<"user">>,
|
||||||
|
peerhost => {192, 168, 0, 2}
|
||||||
|
},
|
||||||
|
ClientInfoBannedClientIdRE = #{
|
||||||
|
clientid => <<"BannedClientRE1">>,
|
||||||
|
username => <<"user">>,
|
||||||
|
peerhost => {127, 0, 0, 1}
|
||||||
|
},
|
||||||
|
ClientInfoBannedUsernameRE = #{
|
||||||
|
clientid => <<"client">>,
|
||||||
|
username => <<"BannedUserRE1">>,
|
||||||
|
peerhost => {127, 0, 0, 1}
|
||||||
|
},
|
||||||
|
ClientInfoBannedAddrNet = #{
|
||||||
|
clientid => <<"client">>,
|
||||||
|
username => <<"user">>,
|
||||||
|
peerhost => {192, 168, 3, 1}
|
||||||
|
},
|
||||||
|
ClientInfoValidFull = #{
|
||||||
clientid => <<"client">>,
|
clientid => <<"client">>,
|
||||||
username => <<"user">>,
|
username => <<"user">>,
|
||||||
peerhost => {127, 0, 0, 1}
|
peerhost => {127, 0, 0, 1}
|
||||||
},
|
},
|
||||||
ClientInfo5 = #{},
|
ClientInfoValidEmpty = #{},
|
||||||
ClientInfo6 = #{clientid => <<"client1">>},
|
ClientInfoValidOnlyClientId = #{clientid => <<"client1">>},
|
||||||
?assert(emqx_banned:check(ClientInfo1)),
|
?assert(emqx_banned:check(ClientInfoBannedClientId)),
|
||||||
?assert(emqx_banned:check(ClientInfo2)),
|
?assert(emqx_banned:check(ClientInfoBannedUsername)),
|
||||||
?assert(emqx_banned:check(ClientInfo3)),
|
?assert(emqx_banned:check(ClientInfoBannedAddr1)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo4)),
|
?assert(emqx_banned:check(ClientInfoBannedAddr2)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo5)),
|
?assert(emqx_banned:check(ClientInfoBannedClientIdRE)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo6)),
|
?assert(emqx_banned:check(ClientInfoBannedUsernameRE)),
|
||||||
ok = emqx_banned:delete({clientid, <<"BannedClient">>}),
|
?assert(emqx_banned:check(ClientInfoBannedAddrNet)),
|
||||||
ok = emqx_banned:delete({username, <<"BannedUser">>}),
|
?assertNot(emqx_banned:check(ClientInfoValidFull)),
|
||||||
ok = emqx_banned:delete({peerhost, {192, 168, 0, 1}}),
|
?assertNot(emqx_banned:check(ClientInfoValidEmpty)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo1)),
|
?assertNot(emqx_banned:check(ClientInfoValidOnlyClientId)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo2)),
|
ok = emqx_banned:delete(emqx_banned:who(clientid, <<"BannedClient">>)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo3)),
|
ok = emqx_banned:delete(emqx_banned:who(username, <<"BannedUser">>)),
|
||||||
?assertNot(emqx_banned:check(ClientInfo4)),
|
ok = emqx_banned:delete(emqx_banned:who(peerhost, {192, 168, 0, 1})),
|
||||||
|
ok = emqx_banned:delete(emqx_banned:who(peerhost, <<"192.168.0.2">>)),
|
||||||
|
ok = emqx_banned:delete(emqx_banned:who(clientid_re, <<"BannedClientRE.*">>)),
|
||||||
|
ok = emqx_banned:delete(emqx_banned:who(username_re, <<"BannedUserRE.*">>)),
|
||||||
|
ok = emqx_banned:delete(emqx_banned:who(peerhost_net, <<"192.168.3.0/24">>)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedClientId)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedUsername)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedAddr1)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedAddr2)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedClientIdRE)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedUsernameRE)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoBannedAddrNet)),
|
||||||
|
?assertNot(emqx_banned:check(ClientInfoValidFull)),
|
||||||
?assertEqual(0, emqx_banned:info(size)).
|
?assertEqual(0, emqx_banned:info(size)).
|
||||||
|
|
||||||
t_unused(_) ->
|
t_unused(_) ->
|
||||||
Who1 = {clientid, <<"BannedClient1">>},
|
Who1 = emqx_banned:who(clientid, <<"BannedClient1">>),
|
||||||
Who2 = {clientid, <<"BannedClient2">>},
|
Who2 = emqx_banned:who(clientid, <<"BannedClient2">>),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, _},
|
{ok, _},
|
||||||
|
@ -123,7 +160,7 @@ t_kick(_) ->
|
||||||
snabbkaffe:start_trace(),
|
snabbkaffe:start_trace(),
|
||||||
|
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Who = {clientid, ClientId},
|
Who = emqx_banned:who(clientid, ClientId),
|
||||||
|
|
||||||
emqx_banned:create(#{
|
emqx_banned:create(#{
|
||||||
who => Who,
|
who => Who,
|
||||||
|
@ -194,7 +231,7 @@ t_session_taken(_) ->
|
||||||
Publish(),
|
Publish(),
|
||||||
|
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Who = {clientid, ClientId2},
|
Who = emqx_banned:who(clientid, ClientId2),
|
||||||
emqx_banned:create(#{
|
emqx_banned:create(#{
|
||||||
who => Who,
|
who => Who,
|
||||||
by => <<"test">>,
|
by => <<"test">>,
|
||||||
|
|
|
@ -561,7 +561,7 @@ t_publish_last_will_testament_banned_client_connecting(_Config) ->
|
||||||
|
|
||||||
%% Now we ban the client while it is connected.
|
%% Now we ban the client while it is connected.
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Who = {username, Username},
|
Who = emqx_banned:who(username, Username),
|
||||||
emqx_banned:create(#{
|
emqx_banned:create(#{
|
||||||
who => Who,
|
who => Who,
|
||||||
by => <<"test">>,
|
by => <<"test">>,
|
||||||
|
|
|
@ -79,17 +79,19 @@
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-type query_return() :: #{meta := map(), data := [term()]}.
|
-type query_return() :: #{meta := map(), data := [term()]}.
|
||||||
|
-type table_name() :: atom().
|
||||||
|
-type table_names() :: [table_name()].
|
||||||
|
|
||||||
-export([do_query/2, apply_total_query/1]).
|
-export([do_query/2, apply_total_query/1]).
|
||||||
|
|
||||||
-spec paginate(atom(), map(), {atom(), atom()}) ->
|
-spec paginate(table_name() | table_names(), map(), {atom(), atom()}) ->
|
||||||
#{
|
#{
|
||||||
meta => #{page => pos_integer(), limit => pos_integer(), count => pos_integer()},
|
meta => #{page => pos_integer(), limit => pos_integer(), count => pos_integer()},
|
||||||
data => list(term())
|
data => list(term())
|
||||||
}.
|
}.
|
||||||
paginate(Table, Params, {Module, FormatFun}) ->
|
paginate(Tables, Params, {Module, FormatFun}) ->
|
||||||
Qh = query_handle(Table),
|
Qh = query_handle(Tables),
|
||||||
Count = count(Table),
|
Count = count(Tables),
|
||||||
do_paginate(Qh, Count, Params, {Module, FormatFun}).
|
do_paginate(Qh, Count, Params, {Module, FormatFun}).
|
||||||
|
|
||||||
do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
|
do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
|
||||||
|
@ -110,9 +112,13 @@ do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
|
||||||
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]
|
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
query_handle(Tables) when is_list(Tables) ->
|
||||||
|
qlc:append([query_handle(T) || T <- Tables]);
|
||||||
query_handle(Table) ->
|
query_handle(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table)]).
|
ets:table(Table).
|
||||||
|
|
||||||
|
count(Tables) when is_list(Tables) ->
|
||||||
|
lists:sum([count(T) || T <- Tables]);
|
||||||
count(Table) ->
|
count(Table) ->
|
||||||
ets:info(Table, size).
|
ets:info(Table, size).
|
||||||
|
|
||||||
|
|
|
@ -38,10 +38,9 @@
|
||||||
delete_banned/2
|
delete_banned/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-define(TAB, emqx_banned).
|
|
||||||
-define(TAGS, [<<"Banned">>]).
|
-define(TAGS, [<<"Banned">>]).
|
||||||
|
|
||||||
-define(BANNED_TYPES, [clientid, username, peerhost]).
|
-define(BANNED_TYPES, [clientid, username, peerhost, clientid_re, username_re, peerhost_net]).
|
||||||
|
|
||||||
-define(FORMAT_FUN, {?MODULE, format}).
|
-define(FORMAT_FUN, {?MODULE, format}).
|
||||||
|
|
||||||
|
@ -161,7 +160,7 @@ fields(ban) ->
|
||||||
].
|
].
|
||||||
|
|
||||||
banned(get, #{query_string := Params}) ->
|
banned(get, #{query_string := Params}) ->
|
||||||
Response = emqx_mgmt_api:paginate(?TAB, Params, ?FORMAT_FUN),
|
Response = emqx_mgmt_api:paginate(emqx_banned:tables(), Params, ?FORMAT_FUN),
|
||||||
{200, Response};
|
{200, Response};
|
||||||
banned(post, #{body := Body}) ->
|
banned(post, #{body := Body}) ->
|
||||||
case emqx_banned:parse(Body) of
|
case emqx_banned:parse(Body) of
|
||||||
|
|
|
@ -40,6 +40,8 @@ t_create(_Config) ->
|
||||||
By = <<"banned suite测试组"/utf8>>,
|
By = <<"banned suite测试组"/utf8>>,
|
||||||
Reason = <<"test测试"/utf8>>,
|
Reason = <<"test测试"/utf8>>,
|
||||||
As = <<"clientid">>,
|
As = <<"clientid">>,
|
||||||
|
|
||||||
|
%% ban by clientid
|
||||||
ClientIdBanned = #{
|
ClientIdBanned = #{
|
||||||
as => As,
|
as => As,
|
||||||
who => ClientId,
|
who => ClientId,
|
||||||
|
@ -60,6 +62,8 @@ t_create(_Config) ->
|
||||||
},
|
},
|
||||||
ClientIdBannedRes
|
ClientIdBannedRes
|
||||||
),
|
),
|
||||||
|
|
||||||
|
%% ban by peerhost
|
||||||
PeerHost = <<"192.168.2.13">>,
|
PeerHost = <<"192.168.2.13">>,
|
||||||
PeerHostBanned = #{
|
PeerHostBanned = #{
|
||||||
as => <<"peerhost">>,
|
as => <<"peerhost">>,
|
||||||
|
@ -81,9 +85,88 @@ t_create(_Config) ->
|
||||||
},
|
},
|
||||||
PeerHostBannedRes
|
PeerHostBannedRes
|
||||||
),
|
),
|
||||||
|
|
||||||
|
%% ban by username RE
|
||||||
|
UsernameRE = <<"BannedUser.*">>,
|
||||||
|
UsernameREBanned = #{
|
||||||
|
as => <<"username_re">>,
|
||||||
|
who => UsernameRE,
|
||||||
|
by => By,
|
||||||
|
reason => Reason,
|
||||||
|
at => At,
|
||||||
|
until => Until
|
||||||
|
},
|
||||||
|
{ok, UsernameREBannedRes} = create_banned(UsernameREBanned),
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
<<"as">> => <<"username_re">>,
|
||||||
|
<<"at">> => At,
|
||||||
|
<<"by">> => By,
|
||||||
|
<<"reason">> => Reason,
|
||||||
|
<<"until">> => Until,
|
||||||
|
<<"who">> => UsernameRE
|
||||||
|
},
|
||||||
|
UsernameREBannedRes
|
||||||
|
),
|
||||||
|
|
||||||
|
%% ban by clientid RE
|
||||||
|
ClientIdRE = <<"BannedClient.*">>,
|
||||||
|
ClientIdREBanned = #{
|
||||||
|
as => <<"clientid_re">>,
|
||||||
|
who => ClientIdRE,
|
||||||
|
by => By,
|
||||||
|
reason => Reason,
|
||||||
|
at => At,
|
||||||
|
until => Until
|
||||||
|
},
|
||||||
|
{ok, ClientIdREBannedRes} = create_banned(ClientIdREBanned),
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
<<"as">> => <<"clientid_re">>,
|
||||||
|
<<"at">> => At,
|
||||||
|
<<"by">> => By,
|
||||||
|
<<"reason">> => Reason,
|
||||||
|
<<"until">> => Until,
|
||||||
|
<<"who">> => ClientIdRE
|
||||||
|
},
|
||||||
|
ClientIdREBannedRes
|
||||||
|
),
|
||||||
|
|
||||||
|
%% ban by CIDR
|
||||||
|
PeerHostNet = <<"192.168.0.0/24">>,
|
||||||
|
PeerHostNetBanned = #{
|
||||||
|
as => <<"peerhost_net">>,
|
||||||
|
who => PeerHostNet,
|
||||||
|
by => By,
|
||||||
|
reason => Reason,
|
||||||
|
at => At,
|
||||||
|
until => Until
|
||||||
|
},
|
||||||
|
{ok, PeerHostNetBannedRes} = create_banned(PeerHostNetBanned),
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
<<"as">> => <<"peerhost_net">>,
|
||||||
|
<<"at">> => At,
|
||||||
|
<<"by">> => By,
|
||||||
|
<<"reason">> => Reason,
|
||||||
|
<<"until">> => Until,
|
||||||
|
<<"who">> => PeerHostNet
|
||||||
|
},
|
||||||
|
PeerHostNetBannedRes
|
||||||
|
),
|
||||||
|
|
||||||
{ok, #{<<"data">> := List}} = list_banned(),
|
{ok, #{<<"data">> := List}} = list_banned(),
|
||||||
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
||||||
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
|
?assertEqual(
|
||||||
|
[
|
||||||
|
{<<"clientid">>, ClientId},
|
||||||
|
{<<"clientid_re">>, ClientIdRE},
|
||||||
|
{<<"peerhost">>, PeerHost},
|
||||||
|
{<<"peerhost_net">>, PeerHostNet},
|
||||||
|
{<<"username_re">>, UsernameRE}
|
||||||
|
],
|
||||||
|
Bans
|
||||||
|
),
|
||||||
|
|
||||||
ClientId2 = <<"TestClient2"/utf8>>,
|
ClientId2 = <<"TestClient2"/utf8>>,
|
||||||
ClientIdBanned2 = #{
|
ClientIdBanned2 = #{
|
||||||
|
|
|
@ -217,7 +217,7 @@ t_banned_delayed(_) ->
|
||||||
ClientId2 = <<"bc2">>,
|
ClientId2 = <<"bc2">>,
|
||||||
|
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Who = {clientid, ClientId2},
|
Who = emqx_banned:who(clientid, ClientId2),
|
||||||
emqx_banned:create(#{
|
emqx_banned:create(#{
|
||||||
who => Who,
|
who => Who,
|
||||||
by => <<"test">>,
|
by => <<"test">>,
|
||||||
|
|
|
@ -698,7 +698,7 @@ t_deliver_when_banned(_) ->
|
||||||
),
|
),
|
||||||
|
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Who = {clientid, Client2},
|
Who = emqx_banned:who(clientid, Client2),
|
||||||
|
|
||||||
emqx_banned:create(#{
|
emqx_banned:create(#{
|
||||||
who => Who,
|
who => Who,
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Added ability to ban clients by extended rules:
|
||||||
|
* by matching `clientid`s to a regular expression;
|
||||||
|
* by matching client's `username` to a regular expression;
|
||||||
|
* by matching client's peer address to an CIDR range.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
emqx_mgmt_api_banned {
|
emqx_mgmt_api_banned {
|
||||||
|
|
||||||
as.desc:
|
as.desc:
|
||||||
"""Ban method, which can be client ID, username or IP address."""
|
"""Ban method, which can be exact client ID, client ID regular expression, exact username, username regular expression,
|
||||||
|
IP address or an IP address range."""
|
||||||
|
|
||||||
as.label:
|
as.label:
|
||||||
"""Ban Method"""
|
"""Ban Method"""
|
||||||
|
|
Loading…
Reference in New Issue