Merge pull request #6900 from zhongwencool/restricted-shell
feat(shell): add restricted shell and user_default
This commit is contained in:
commit
3b4eade1ad
|
@ -36,6 +36,11 @@
|
||||||
## Can be one of: inet_tcp, inet6_tcp, inet_tls
|
## Can be one of: inet_tcp, inet6_tcp, inet_tls
|
||||||
#-proto_dist inet_tcp
|
#-proto_dist inet_tcp
|
||||||
|
|
||||||
|
## The shell is started in a restricted mode.
|
||||||
|
## In this mode, the shell evaluates a function call only if allowed.
|
||||||
|
## Prevent user from accidentally calling a function from the prompt that could harm a running system.
|
||||||
|
-stdlib restricted_shell emqx_restricted_shell
|
||||||
|
|
||||||
## Specify SSL Options in the file if using SSL for Erlang Distribution.
|
## Specify SSL Options in the file if using SSL for Erlang Distribution.
|
||||||
## Used only when -proto_dist set to inet_tls
|
## Used only when -proto_dist set to inet_tls
|
||||||
#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf
|
#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf
|
||||||
|
|
|
@ -35,6 +35,11 @@
|
||||||
## Can be one of: inet_tcp, inet6_tcp, inet_tls
|
## Can be one of: inet_tcp, inet6_tcp, inet_tls
|
||||||
#-proto_dist inet_tcp
|
#-proto_dist inet_tcp
|
||||||
|
|
||||||
|
## The shell is started in a restricted mode.
|
||||||
|
## In this mode, the shell evaluates a function call only if allowed.
|
||||||
|
## Prevent user from accidentally calling a function from the prompt that could harm a running system.
|
||||||
|
-stdlib restricted_shell emqx_restricted_shell
|
||||||
|
|
||||||
## Specify SSL Options in the file if using SSL for Erlang Distribution.
|
## Specify SSL Options in the file if using SSL for Erlang Distribution.
|
||||||
## Used only when -proto_dist set to inet_tls
|
## Used only when -proto_dist set to inet_tls
|
||||||
#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf
|
#-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf
|
||||||
|
|
|
@ -48,6 +48,8 @@
|
||||||
|
|
||||||
-export([post_config_update/5]).
|
-export([post_config_update/5]).
|
||||||
|
|
||||||
|
-export([format_addr/1]).
|
||||||
|
|
||||||
-define(CONF_KEY_PATH, [listeners]).
|
-define(CONF_KEY_PATH, [listeners]).
|
||||||
-define(TYPES_STRING, ["tcp","ssl","ws","wss","quic"]).
|
-define(TYPES_STRING, ["tcp","ssl","ws","wss","quic"]).
|
||||||
|
|
||||||
|
@ -363,7 +365,12 @@ merge_default(Options) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
format_addr(Port) when is_integer(Port) ->
|
format_addr(Port) when is_integer(Port) ->
|
||||||
io_lib:format("0.0.0.0:~w", [Port]);
|
io_lib:format(":~w", [Port]);
|
||||||
|
%% Print only the port number when bound on all interfaces
|
||||||
|
format_addr({{0, 0, 0, 0}, Port}) ->
|
||||||
|
format_addr(Port);
|
||||||
|
format_addr({{0, 0, 0, 0, 0, 0, 0, 0}, Port}) ->
|
||||||
|
format_addr(Port);
|
||||||
format_addr({Addr, Port}) when is_list(Addr) ->
|
format_addr({Addr, Port}) when is_list(Addr) ->
|
||||||
io_lib:format("~ts:~w", [Addr, Port]);
|
io_lib:format("~ts:~w", [Addr, Port]);
|
||||||
format_addr({Addr, Port}) when is_tuple(Addr) ->
|
format_addr({Addr, Port}) when is_tuple(Addr) ->
|
||||||
|
|
|
@ -62,11 +62,11 @@ start_listeners() ->
|
||||||
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
|
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
|
||||||
},
|
},
|
||||||
Res =
|
Res =
|
||||||
lists:foldl(fun({Name, Protocol, Port, RanchOptions}, Acc) ->
|
lists:foldl(fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
|
||||||
Minirest = BaseMinirest#{protocol => Protocol},
|
Minirest = BaseMinirest#{protocol => Protocol},
|
||||||
case minirest:start(Name, RanchOptions, Minirest) of
|
case minirest:start(Name, RanchOptions, Minirest) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
?ULOG("Start listener ~ts on ~p successfully.~n", [Name, Port]),
|
?ULOG("Start listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Bind)]),
|
||||||
Acc;
|
Acc;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
%% Don't record the reason because minirest already does(too much logs noise).
|
%% Don't record the reason because minirest already does(too much logs noise).
|
||||||
|
@ -82,7 +82,7 @@ stop_listeners() ->
|
||||||
[begin
|
[begin
|
||||||
case minirest:stop(Name) of
|
case minirest:stop(Name) of
|
||||||
ok ->
|
ok ->
|
||||||
?ULOG("Stop listener ~ts on ~p successfully.~n", [Name, Port]);
|
?ULOG("Stop listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Port)]);
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
|
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
|
||||||
end
|
end
|
||||||
|
@ -101,17 +101,17 @@ apps() ->
|
||||||
listeners() ->
|
listeners() ->
|
||||||
[begin
|
[begin
|
||||||
Protocol = maps:get(protocol, ListenerOption0, http),
|
Protocol = maps:get(protocol, ListenerOption0, http),
|
||||||
{ListenerOption, Port} = ip_port(ListenerOption0),
|
{ListenerOption, Bind} = ip_port(ListenerOption0),
|
||||||
Name = listener_name(Protocol, ListenerOption),
|
Name = listener_name(Protocol, ListenerOption),
|
||||||
RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
|
RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
|
||||||
{Name, Protocol, Port, RanchOptions}
|
{Name, Protocol, Bind, RanchOptions}
|
||||||
end || ListenerOption0 <- emqx_conf:get([dashboard, listeners], [])].
|
end || ListenerOption0 <- emqx_conf:get([dashboard, listeners], [])].
|
||||||
|
|
||||||
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
||||||
|
|
||||||
ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
|
ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
|
||||||
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
||||||
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, Port}.
|
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
|
||||||
|
|
||||||
|
|
||||||
ranch_opts(RanchOptions) ->
|
ranch_opts(RanchOptions) ->
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
start(_Type, _Args) ->
|
start(_Type, _Args) ->
|
||||||
ok = emqx_machine:start(),
|
ok = emqx_machine:start(),
|
||||||
|
_ = emqx_restricted_shell:set_prompt_func(),
|
||||||
emqx_machine_sup:start_link().
|
emqx_machine_sup:start_link().
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_restricted_shell).
|
||||||
|
|
||||||
|
-export([local_allowed/3, non_local_allowed/3]).
|
||||||
|
-export([set_prompt_func/0, prompt_func/1]).
|
||||||
|
-export([lock/0, unlock/0, is_locked/0]).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-define(APP, 'emqx_machine').
|
||||||
|
-define(IS_LOCKED, 'restricted.is_locked').
|
||||||
|
-define(MAX_HEAP_SIZE, 1024 * 1024 * 1).
|
||||||
|
-define(MAX_ARGS_SIZE, 1024 * 10).
|
||||||
|
|
||||||
|
-define(RED_BG, "\e[48;2;184;0;0m").
|
||||||
|
-define(RESET, "\e[0m").
|
||||||
|
|
||||||
|
-define(LOCAL_PROHIBITED, [halt, q]).
|
||||||
|
-define(REMOTE_PROHIBITED, [{erlang, halt}, {c, q}, {init, stop}, {init, restart}, {init, reboot}]).
|
||||||
|
|
||||||
|
is_locked() ->
|
||||||
|
{ok, false} =/= application:get_env(?APP, ?IS_LOCKED).
|
||||||
|
|
||||||
|
lock() -> application:set_env(?APP, ?IS_LOCKED, true).
|
||||||
|
unlock() -> application:set_env(?APP, ?IS_LOCKED, false).
|
||||||
|
|
||||||
|
set_prompt_func() ->
|
||||||
|
shell:prompt_func({?MODULE, prompt_func}).
|
||||||
|
|
||||||
|
prompt_func(PropList) ->
|
||||||
|
Line = proplists:get_value(history, PropList, 1),
|
||||||
|
Version = emqx_release:version(),
|
||||||
|
case is_alive() of
|
||||||
|
true -> io_lib:format(<<"~ts(~s)~w> ">>, [Version, node(), Line]);
|
||||||
|
false -> io_lib:format(<<"~ts ~w> ">>, [Version, Line])
|
||||||
|
end.
|
||||||
|
|
||||||
|
local_allowed(MF, Args, State) ->
|
||||||
|
Allowed = check_allowed(MF, ?LOCAL_PROHIBITED),
|
||||||
|
log(Allowed, MF, Args),
|
||||||
|
{is_allowed(Allowed), State}.
|
||||||
|
|
||||||
|
non_local_allowed(MF, Args, State) ->
|
||||||
|
Allow = check_allowed(MF, ?REMOTE_PROHIBITED),
|
||||||
|
log(Allow, MF, Args),
|
||||||
|
{is_allowed(Allow), State}.
|
||||||
|
|
||||||
|
check_allowed(MF, NotAllowed) ->
|
||||||
|
case {lists:member(MF, NotAllowed), is_locked()} of
|
||||||
|
{true, false} -> exempted;
|
||||||
|
{true, true} -> prohibited;
|
||||||
|
{false, _} -> ignore
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_allowed(prohibited) -> false;
|
||||||
|
is_allowed(_) -> true.
|
||||||
|
|
||||||
|
limit_warning(MF, Args) ->
|
||||||
|
max_heap_size_warning(MF, Args),
|
||||||
|
max_args_warning(MF, Args).
|
||||||
|
|
||||||
|
max_args_warning(MF, Args) ->
|
||||||
|
ArgsSize = erts_debug:flat_size(Args),
|
||||||
|
case ArgsSize < ?MAX_ARGS_SIZE of
|
||||||
|
true -> ok;
|
||||||
|
false ->
|
||||||
|
warning("[WARNING] current_args_size:~w, max_args_size:~w", [ArgsSize, ?MAX_ARGS_SIZE]),
|
||||||
|
?SLOG(warning, #{msg => "execute_function_in_shell_max_args_size",
|
||||||
|
function => MF,
|
||||||
|
args => Args,
|
||||||
|
args_size => ArgsSize,
|
||||||
|
max_heap_size => ?MAX_ARGS_SIZE})
|
||||||
|
end.
|
||||||
|
|
||||||
|
max_heap_size_warning(MF, Args) ->
|
||||||
|
{heap_size, HeapSize} = erlang:process_info(self(), heap_size),
|
||||||
|
case HeapSize < ?MAX_HEAP_SIZE of
|
||||||
|
true -> ok;
|
||||||
|
false ->
|
||||||
|
warning("[WARNING] current_heap_size:~w, max_heap_size_warning:~w", [HeapSize, ?MAX_HEAP_SIZE]),
|
||||||
|
?SLOG(warning, #{msg => "shell_process_exceed_max_heap_size",
|
||||||
|
current_heap_size => HeapSize,
|
||||||
|
function => MF,
|
||||||
|
args => Args,
|
||||||
|
max_heap_size => ?MAX_HEAP_SIZE})
|
||||||
|
end.
|
||||||
|
|
||||||
|
log(prohibited, MF, Args) ->
|
||||||
|
warning("DANGEROUS FUNCTION: FORBIDDEN IN SHELL!!!!!", []),
|
||||||
|
?SLOG(error, #{msg => "execute_function_in_shell_prohibited", function => MF, args => Args});
|
||||||
|
log(exempted, MF, Args) ->
|
||||||
|
limit_warning(MF, Args),
|
||||||
|
?SLOG(error, #{msg => "execute_dangerous_function_in_shell_exempted", function => MF, args => Args});
|
||||||
|
log(ignore, MF, Args) -> limit_warning(MF, Args).
|
||||||
|
|
||||||
|
warning(Format, Args) ->
|
||||||
|
io:format(?RED_BG ++ Format ++ ?RESET ++ "~n", Args).
|
|
@ -0,0 +1,31 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(user_default).
|
||||||
|
|
||||||
|
%% INCLUDE BEGIN
|
||||||
|
%% Import all the record definitions from the header file into the erlang shell.
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
-include_lib("emqx_conf/include/emqx_conf.hrl").
|
||||||
|
-include_lib("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
%% INCLUDE END
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([lock/0, unlock/0]).
|
||||||
|
|
||||||
|
lock() -> emqx_restricted_shell:lock().
|
||||||
|
unlock() -> emqx_restricted_shell:unlock().
|
|
@ -0,0 +1,72 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_restricted_shell_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
emqx_common_test_helpers:start_apps([]),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_common_test_helpers:stop_apps([]).
|
||||||
|
|
||||||
|
t_local_allowed(_Config) ->
|
||||||
|
LocalProhibited = [halt, q],
|
||||||
|
State = undefined,
|
||||||
|
lists:foreach(fun(LocalFunc) ->
|
||||||
|
?assertEqual({false, State}, emqx_restricted_shell:local_allowed(LocalFunc, [], State))
|
||||||
|
end, LocalProhibited),
|
||||||
|
LocalAllowed = [ls, pwd],
|
||||||
|
lists:foreach(fun(LocalFunc) ->
|
||||||
|
?assertEqual({true, State},emqx_restricted_shell:local_allowed(LocalFunc, [], State))
|
||||||
|
end, LocalAllowed),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_non_local_allowed(_Config) ->
|
||||||
|
RemoteProhibited = [{erlang, halt}, {c, q}, {init, stop}, {init, restart}, {init, reboot}],
|
||||||
|
State = undefined,
|
||||||
|
lists:foreach(fun(RemoteFunc) ->
|
||||||
|
?assertEqual({false, State}, emqx_restricted_shell:non_local_allowed(RemoteFunc, [], State))
|
||||||
|
end, RemoteProhibited),
|
||||||
|
RemoteAllowed = [{erlang, date}, {erlang, system_time}],
|
||||||
|
lists:foreach(fun(RemoteFunc) ->
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:local_allowed(RemoteFunc, [], State))
|
||||||
|
end, RemoteAllowed),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_lock(_Config) ->
|
||||||
|
State = undefined,
|
||||||
|
emqx_restricted_shell:lock(),
|
||||||
|
?assertEqual({false, State}, emqx_restricted_shell:local_allowed(q, [], State)),
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:local_allowed(ls, [], State)),
|
||||||
|
?assertEqual({false, State}, emqx_restricted_shell:non_local_allowed({init, stop}, [], State)),
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:non_local_allowed({inet, getifaddrs}, [], State)),
|
||||||
|
emqx_restricted_shell:unlock(),
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:local_allowed(q, [], State)),
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:local_allowed(ls, [], State)),
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:non_local_allowed({init, stop}, [], State)),
|
||||||
|
?assertEqual({true, State}, emqx_restricted_shell:non_local_allowed({inet, getifaddrs}, [], State)),
|
||||||
|
emqx_restricted_shell:lock(),
|
||||||
|
ok.
|
Loading…
Reference in New Issue