diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 654d96d8c..13b3373f1 100644 --- a/apps/emqx/include/emqx.hrl +++ b/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(), diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index 1568bf103..fcb6edc00 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/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). diff --git a/apps/emqx/src/emqx_flapping.erl b/apps/emqx/src/emqx_flapping.erl index 7e8b8f9fc..1615c8aba 100644 --- a/apps/emqx/src/emqx_flapping.erl +++ b/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, diff --git a/apps/emqx/src/emqx_types.erl b/apps/emqx/src/emqx_types.erl index 087bcaebe..c99ddbe13 100644 --- a/apps/emqx/src/emqx_types.erl +++ b/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()}. diff --git a/apps/emqx/test/emqx_banned_SUITE.erl b/apps/emqx/test/emqx_banned_SUITE.erl index 8c86e17f6..b4bd3d444 100644 --- a/apps/emqx/test/emqx_banned_SUITE.erl +++ b/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">>, diff --git a/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl b/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl index e0ef906ad..cee120acb 100644 --- a/apps/emqx_auth/test/emqx_authz/emqx_authz_SUITE.erl +++ b/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">>, diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 4db37a7dc..645b701f0 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/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). diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl index 6c1d407b5..cf1ab3c49 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl +++ b/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 diff --git a/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl index 3167a5621..9ef4f1b7d 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl +++ b/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 = #{ diff --git a/apps/emqx_modules/test/emqx_delayed_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_SUITE.erl index 5085aa2da..631e7c1aa 100644 --- a/apps/emqx_modules/test/emqx_delayed_SUITE.erl +++ b/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">>, diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index c76ba90c6..71b1ef900 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/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, diff --git a/changes/ce/feat-12499.en.md b/changes/ce/feat-12499.en.md new file mode 100644 index 000000000..8724b66e7 --- /dev/null +++ b/changes/ce/feat-12499.en.md @@ -0,0 +1,7 @@ +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. + +Warning: large number of matching rules (not tied to a concrete clientid, username or host) will impact performance. + diff --git a/rel/i18n/emqx_mgmt_api_banned.hocon b/rel/i18n/emqx_mgmt_api_banned.hocon index 4bf72103f..6d5470bc9 100644 --- a/rel/i18n/emqx_mgmt_api_banned.hocon +++ b/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"""