feat(banned): allow ban by clientid/username regexps, peerhost cidrs

This commit is contained in:
Ilya Averyanov 2024-02-09 20:59:34 +03:00
parent 755b59b7fe
commit 90fd2b26d3
13 changed files with 323 additions and 96 deletions

View File

@ -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(),

View File

@ -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).

View File

@ -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,

View File

@ -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()}.

View File

@ -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">>,

View File

@ -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">>,

View File

@ -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).

View File

@ -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

View File

@ -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 = #{

View File

@ -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">>,

View File

@ -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,

View File

@ -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.

View File

@ -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"""