Просмотр исходного кода

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

Ilya Averyanov 2 лет назад
Родитель
Сommit
90fd2b26d3

+ 1 - 4
apps/emqx/include/emqx.hrl

@@ -88,10 +88,7 @@
 %%--------------------------------------------------------------------
 
 -record(banned, {
-    who ::
-        {clientid, binary()}
-        | {peerhost, inet:ip_address()}
-        | {username, binary()},
+    who :: emqx_types:banned_who(),
     by :: binary(),
     reason :: binary(),
     at :: integer(),

+ 139 - 49
apps/emqx/src/emqx_banned.erl

@@ -39,7 +39,9 @@
     info/1,
     format/1,
     parse/1,
-    clear/0
+    clear/0,
+    who/2,
+    tables/0
 ]).
 
 %% gen_server callbacks
@@ -61,7 +63,8 @@
 
 -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
 %% but for compatibility, a large number (1 years) is used here to represent the 'infinite'
@@ -77,19 +80,24 @@
 %%--------------------------------------------------------------------
 
 mnesia(boot) ->
-    ok = mria:create_table(?BANNED_TAB, [
+    Options = [
         {type, set},
         {rlog_shard, ?COMMON_SHARD},
         {storage, disc_copies},
         {record_name, banned},
         {attributes, record_info(fields, banned)},
         {storage_properties, [{ets, [{read_concurrency, true}]}]}
-    ]).
+    ],
+    ok = mria:create_table(?BANNED_INDIVIDUAL_TAB, Options),
+    ok = mria:create_table(?BANNED_RULE_TAB, Options).
 
 %%--------------------------------------------------------------------
 %% Data backup
 %%--------------------------------------------------------------------
-backup_tables() -> [?BANNED_TAB].
+backup_tables() -> tables().
+
+-spec tables() -> [atom()].
+tables() -> [?BANNED_RULE_TAB, ?BANNED_INDIVIDUAL_TAB].
 
 %% @doc Start the banned server.
 -spec start_link() -> startlink_ret().
@@ -104,16 +112,10 @@ stop() -> gen_server:stop(?MODULE).
 check(ClientInfo) ->
     do_check({clientid, maps:get(clientid, ClientInfo, undefined)}) orelse
         do_check({username, maps:get(username, ClientInfo, undefined)}) orelse
-        do_check({peerhost, maps:get(peerhost, ClientInfo, undefined)}).
-
-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.
+        do_check({peerhost, maps:get(peerhost, ClientInfo, undefined)}) orelse
+        do_check_rules(ClientInfo).
 
+-spec format(emqx_types:banned()) -> map().
 format(#banned{
     who = Who0,
     by = By,
@@ -121,7 +123,7 @@ format(#banned{
     at = At,
     until = Until
 }) ->
-    {As, Who} = maybe_format_host(Who0),
+    {As, Who} = format_who(Who0),
     #{
         as => As,
         who => Who,
@@ -131,6 +133,7 @@ format(#banned{
         until => to_rfc3339(Until)
     }.
 
+-spec parse(map()) -> emqx_types:banned() | {error, term()}.
 parse(Params) ->
     case parse_who(Params) of
         {error, Reason} ->
@@ -155,24 +158,6 @@ parse(Params) ->
                     {error, ErrorReason}
             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()) ->
     {ok, emqx_types:banned()} | {error, {already_exist, emqx_types:banned()}}.
@@ -194,7 +179,7 @@ create(#{
 create(Banned = #banned{who = Who}) ->
     case look_up(Who) of
         [] ->
-            insert_banned(Banned),
+            insert_banned(table(Who), Banned),
             {ok, Banned};
         [OldBanned = #banned{until = Until}] ->
             %% Don't support shorten or extend the until time by overwrite.
@@ -204,33 +189,52 @@ create(Banned = #banned{who = Who}) ->
                     {error, {already_exist, OldBanned}};
                 %% overwrite expired one is ok.
                 false ->
-                    insert_banned(Banned),
+                    insert_banned(table(Who), Banned),
                     {ok, Banned}
             end
     end.
 
+-spec look_up(emqx_types:banned_who() | map()) -> [emqx_types:banned()].
 look_up(Who) when is_map(Who) ->
     look_up(parse_who(Who));
 look_up(Who) ->
-    mnesia:dirty_read(?BANNED_TAB, Who).
+    mnesia:dirty_read(table(Who), Who).
 
--spec delete(
-    {clientid, emqx_types:clientid()}
-    | {username, emqx_types:username()}
-    | {peerhost, emqx_types:peerhost()}
-) -> ok.
+-spec delete(map() | emqx_types:banned_who()) -> ok.
 delete(Who) when is_map(Who) ->
     delete(parse_who(Who));
 delete(Who) ->
-    mria:dirty_delete(?BANNED_TAB, Who).
+    mria:dirty_delete(table(Who), Who).
 
-info(InfoKey) ->
-    mnesia:table_info(?BANNED_TAB, InfoKey).
+-spec info(size) -> non_neg_integer().
+info(size) ->
+    mnesia:table_info(?BANNED_INDIVIDUAL_TAB, size) + mnesia:table_info(?BANNED_RULE_TAB, size).
 
+-spec clear() -> ok.
 clear() ->
-    _ = mria:clear_table(?BANNED_TAB),
+    _ = mria:clear_table(?BANNED_INDIVIDUAL_TAB),
+    _ = mria:clear_table(?BANNED_RULE_TAB),
     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
 %%--------------------------------------------------------------------
@@ -265,6 +269,81 @@ code_change(_OldVsn, State, _Extra) ->
 %% 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).
 ensure_expiry_timer(State) ->
     State#{expiry_timer := emqx_utils:start_timer(10, expire)}.
@@ -274,19 +353,27 @@ ensure_expiry_timer(State) ->
 -endif.
 
 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(
         fun
             (B = #banned{until = Until}, _Acc) when Until < Now ->
-                mnesia:delete_object(?BANNED_TAB, B, sticky_write);
+                mnesia:delete_object(Tab, B, sticky_write);
             (_, _Acc) ->
                 ok
         end,
         ok,
-        ?BANNED_TAB
+        Tab
     ).
 
-insert_banned(Banned) ->
-    mria:dirty_write(?BANNED_TAB, Banned),
+insert_banned(Tab, Banned) ->
+    mria:dirty_write(Tab, Banned),
     on_banned(Banned).
 
 on_banned(#banned{who = {clientid, ClientId}}) ->
@@ -302,3 +389,6 @@ on_banned(#banned{who = {clientid, ClientId}}) ->
     ok;
 on_banned(_) ->
     ok.
+
+all_rules() ->
+    ets:tab2list(?BANNED_RULE_TAB).

+ 1 - 1
apps/emqx/src/emqx_flapping.erl

@@ -150,7 +150,7 @@ handle_cast(
             ),
             Now = erlang:system_time(second),
             Banned = #banned{
-                who = {clientid, ClientId},
+                who = emqx_banned:who(clientid, ClientId),
                 by = <<"flapping detector">>,
                 reason = <<"flapping is detected">>,
                 at = Now,

+ 9 - 0
apps/emqx/src/emqx_types.erl

@@ -100,6 +100,7 @@
 
 -export_type([
     banned/0,
+    banned_who/0,
     command/0
 ]).
 
@@ -246,6 +247,14 @@
 }.
 
 -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 delivery() :: #delivery{}.
 -type deliver_result() :: ok | {ok, non_neg_integer()} | {error, term()}.

+ 66 - 29
apps/emqx/test/emqx_banned_SUITE.erl

@@ -34,7 +34,7 @@ end_per_suite(Config) ->
 
 t_add_delete(_) ->
     Banned = #banned{
-        who = {clientid, <<"TestClient">>},
+        who = emqx_banned:who(clientid, <<"TestClient">>),
         by = <<"banned suite">>,
         reason = <<"test">>,
         at = erlang:system_time(second),
@@ -47,54 +47,91 @@ t_add_delete(_) ->
         emqx_banned:create(Banned#banned{until = erlang:system_time(second) + 100}),
     ?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)).
 
 t_check(_) ->
-    {ok, _} = emqx_banned:create(#banned{who = {clientid, <<"BannedClient">>}}),
-    {ok, _} = emqx_banned:create(#banned{who = {username, <<"BannedUser">>}}),
-    {ok, _} = emqx_banned:create(#banned{who = {peerhost, {192, 168, 0, 1}}}),
-    ?assertEqual(3, emqx_banned:info(size)),
-    ClientInfo1 = #{
+    {ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(clientid, <<"BannedClient">>)}),
+    {ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(username, <<"BannedUser">>)}),
+    {ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(peerhost, {192, 168, 0, 1})}),
+    {ok, _} = emqx_banned:create(#banned{who = emqx_banned:who(peerhost, <<"192.168.0.2">>)}),
+    {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">>,
         username => <<"user">>,
         peerhost => {127, 0, 0, 1}
     },
-    ClientInfo2 = #{
+    ClientInfoBannedUsername = #{
         clientid => <<"client">>,
         username => <<"BannedUser">>,
         peerhost => {127, 0, 0, 1}
     },
-    ClientInfo3 = #{
+    ClientInfoBannedAddr1 = #{
         clientid => <<"client">>,
         username => <<"user">>,
         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">>,
         username => <<"user">>,
         peerhost => {127, 0, 0, 1}
     },
-    ClientInfo5 = #{},
-    ClientInfo6 = #{clientid => <<"client1">>},
-    ?assert(emqx_banned:check(ClientInfo1)),
-    ?assert(emqx_banned:check(ClientInfo2)),
-    ?assert(emqx_banned:check(ClientInfo3)),
-    ?assertNot(emqx_banned:check(ClientInfo4)),
-    ?assertNot(emqx_banned:check(ClientInfo5)),
-    ?assertNot(emqx_banned:check(ClientInfo6)),
-    ok = emqx_banned:delete({clientid, <<"BannedClient">>}),
-    ok = emqx_banned:delete({username, <<"BannedUser">>}),
-    ok = emqx_banned:delete({peerhost, {192, 168, 0, 1}}),
-    ?assertNot(emqx_banned:check(ClientInfo1)),
-    ?assertNot(emqx_banned:check(ClientInfo2)),
-    ?assertNot(emqx_banned:check(ClientInfo3)),
-    ?assertNot(emqx_banned:check(ClientInfo4)),
+    ClientInfoValidEmpty = #{},
+    ClientInfoValidOnlyClientId = #{clientid => <<"client1">>},
+    ?assert(emqx_banned:check(ClientInfoBannedClientId)),
+    ?assert(emqx_banned:check(ClientInfoBannedUsername)),
+    ?assert(emqx_banned:check(ClientInfoBannedAddr1)),
+    ?assert(emqx_banned:check(ClientInfoBannedAddr2)),
+    ?assert(emqx_banned:check(ClientInfoBannedClientIdRE)),
+    ?assert(emqx_banned:check(ClientInfoBannedUsernameRE)),
+    ?assert(emqx_banned:check(ClientInfoBannedAddrNet)),
+    ?assertNot(emqx_banned:check(ClientInfoValidFull)),
+    ?assertNot(emqx_banned:check(ClientInfoValidEmpty)),
+    ?assertNot(emqx_banned:check(ClientInfoValidOnlyClientId)),
+    ok = emqx_banned:delete(emqx_banned:who(clientid, <<"BannedClient">>)),
+    ok = emqx_banned:delete(emqx_banned:who(username, <<"BannedUser">>)),
+    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)).
 
 t_unused(_) ->
-    Who1 = {clientid, <<"BannedClient1">>},
-    Who2 = {clientid, <<"BannedClient2">>},
+    Who1 = emqx_banned:who(clientid, <<"BannedClient1">>),
+    Who2 = emqx_banned:who(clientid, <<"BannedClient2">>),
 
     ?assertMatch(
         {ok, _},
@@ -123,7 +160,7 @@ t_kick(_) ->
     snabbkaffe:start_trace(),
 
     Now = erlang:system_time(second),
-    Who = {clientid, ClientId},
+    Who = emqx_banned:who(clientid, ClientId),
 
     emqx_banned:create(#{
         who => Who,
@@ -194,7 +231,7 @@ t_session_taken(_) ->
     Publish(),
 
     Now = erlang:system_time(second),
-    Who = {clientid, ClientId2},
+    Who = emqx_banned:who(clientid, ClientId2),
     emqx_banned:create(#{
         who => Who,
         by => <<"test">>,

+ 1 - 1
apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl

@@ -561,7 +561,7 @@ t_publish_last_will_testament_banned_client_connecting(_Config) ->
 
     %% Now we ban the client while it is connected.
     Now = erlang:system_time(second),
-    Who = {username, Username},
+    Who = emqx_banned:who(username, Username),
     emqx_banned:create(#{
         who => Who,
         by => <<"test">>,

+ 11 - 5
apps/emqx_management/src/emqx_mgmt_api.erl

@@ -79,17 +79,19 @@
 }.
 
 -type query_return() :: #{meta := map(), data := [term()]}.
+-type table_name() :: atom().
+-type table_names() :: [table_name()].
 
 -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()},
         data => list(term())
     }.
-paginate(Table, Params, {Module, FormatFun}) ->
-    Qh = query_handle(Table),
-    Count = count(Table),
+paginate(Tables, Params, {Module, FormatFun}) ->
+    Qh = query_handle(Tables),
+    Count = count(Tables),
     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]
     }.
 
+query_handle(Tables) when is_list(Tables) ->
+    qlc:append([query_handle(T) || T <- Tables]);
 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) ->
     ets:info(Table, size).
 

+ 2 - 3
apps/emqx_management/src/emqx_mgmt_api_banned.erl

@@ -38,10 +38,9 @@
     delete_banned/2
 ]).
 
--define(TAB, emqx_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}).
 
@@ -161,7 +160,7 @@ fields(ban) ->
     ].
 
 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};
 banned(post, #{body := Body}) ->
     case emqx_banned:parse(Body) of

+ 84 - 1
apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl

@@ -40,6 +40,8 @@ t_create(_Config) ->
     By = <<"banned suite测试组"/utf8>>,
     Reason = <<"test测试"/utf8>>,
     As = <<"clientid">>,
+
+    %% ban by clientid
     ClientIdBanned = #{
         as => As,
         who => ClientId,
@@ -60,6 +62,8 @@ t_create(_Config) ->
         },
         ClientIdBannedRes
     ),
+
+    %% ban by peerhost
     PeerHost = <<"192.168.2.13">>,
     PeerHostBanned = #{
         as => <<"peerhost">>,
@@ -81,9 +85,88 @@ t_create(_Config) ->
         },
         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(),
     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>>,
     ClientIdBanned2 = #{

+ 1 - 1
apps/emqx_modules/test/emqx_delayed_SUITE.erl

@@ -217,7 +217,7 @@ t_banned_delayed(_) ->
     ClientId2 = <<"bc2">>,
 
     Now = erlang:system_time(second),
-    Who = {clientid, ClientId2},
+    Who = emqx_banned:who(clientid, ClientId2),
     emqx_banned:create(#{
         who => Who,
         by => <<"test">>,

+ 1 - 1
apps/emqx_retainer/test/emqx_retainer_SUITE.erl

@@ -698,7 +698,7 @@ t_deliver_when_banned(_) ->
     ),
 
     Now = erlang:system_time(second),
-    Who = {clientid, Client2},
+    Who = emqx_banned:who(clientid, Client2),
 
     emqx_banned:create(#{
         who => Who,

+ 5 - 0
changes/ce/feat-12499.en.md

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

+ 2 - 1
rel/i18n/emqx_mgmt_api_banned.hocon

@@ -1,7 +1,8 @@
 emqx_mgmt_api_banned {
 
 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:
 """Ban Method"""