From 0bb391db6e31d5c2672a865cfae4b7051e1cc88c Mon Sep 17 00:00:00 2001 From: Hans Nilsson Date: Tue, 13 Aug 2019 11:57:42 +0200 Subject: ssh: tcpip-forward, server part Added handling of SSH_MSG_GLOBAL_REQUEST(tcpip-forward) and a new boolean option tcpip_tunnel_out with default false. --- lib/ssh/doc/src/ssh.xml | 9 ++ lib/ssh/src/Makefile | 3 + lib/ssh/src/ssh.app.src | 3 + lib/ssh/src/ssh.hrl | 3 + lib/ssh/src/ssh_connection.erl | 72 ++++++++++++++-- lib/ssh/src/ssh_connection_handler.erl | 11 +++ lib/ssh/src/ssh_options.erl | 6 ++ lib/ssh/src/ssh_subsystem_sup.erl | 21 ++++- lib/ssh/src/ssh_tcpip_forward_acceptor.erl | 113 +++++++++++++++++++++++++ lib/ssh/src/ssh_tcpip_forward_acceptor_sup.erl | 62 ++++++++++++++ lib/ssh/src/ssh_tcpip_forward_srv.erl | 55 ++++++++++++ lib/ssh/test/ssh_sup_SUITE.erl | 9 +- 12 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 lib/ssh/src/ssh_tcpip_forward_acceptor.erl create mode 100644 lib/ssh/src/ssh_tcpip_forward_acceptor_sup.erl create mode 100644 lib/ssh/src/ssh_tcpip_forward_srv.erl diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml index afcae71418..7f5be7b17d 100644 --- a/lib/ssh/doc/src/ssh.xml +++ b/lib/ssh/doc/src/ssh.xml @@ -693,6 +693,15 @@ + + + +

Enables (true) or disables (false) the possibility to tunnel a TCP/IP connection out of a + server. + Disabled per default. +

+
+
Options common to clients and daemons diff --git a/lib/ssh/src/Makefile b/lib/ssh/src/Makefile index 46bf6332e5..f1b11bc5b9 100644 --- a/lib/ssh/src/Makefile +++ b/lib/ssh/src/Makefile @@ -74,6 +74,9 @@ MODULES= \ ssh_subsystem_sup \ ssh_sup \ ssh_system_sup \ + ssh_tcpip_forward_srv \ + ssh_tcpip_forward_acceptor_sup \ + ssh_tcpip_forward_acceptor \ ssh_transport \ ssh_xfer \ sshc_sup \ diff --git a/lib/ssh/src/ssh.app.src b/lib/ssh/src/ssh.app.src index 379cde23e3..0c70399d3f 100644 --- a/lib/ssh/src/ssh.app.src +++ b/lib/ssh/src/ssh.app.src @@ -36,6 +36,9 @@ ssh_sftpd_file_api, ssh_subsystem_sup, ssh_sup, + ssh_tcpip_forward_srv, + ssh_tcpip_forward_acceptor_sup, + ssh_tcpip_forward_acceptor, ssh_system_sup, ssh_transport, ssh_xfer]}, diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index a991f72cf2..301e0d91e5 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -302,6 +302,7 @@ | shell_daemon_option() | exec_daemon_option() | ssh_cli_daemon_option() + | tcpip_tunnel_out_daemon_option() | authentication_daemon_options() | diffie_hellman_group_exchange_daemon_option() | negotiation_timeout_daemon_option() @@ -329,6 +330,8 @@ -type ssh_cli_daemon_option() :: {ssh_cli, mod_args() | no_cli }. +-type tcpip_tunnel_out_daemon_option() :: {tcpip_tunnel_out, boolean()} . + -type send_ext_info_daemon_option() :: {send_ext_info, boolean()} . -type authentication_daemon_options() :: diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index bf9e178cce..0caf5cd819 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -45,6 +45,8 @@ handle_msg/3, handle_stop/1, + open_channel/4, + channel_adjust_window_msg/2, channel_close_msg/1, channel_open_failure_msg/4, @@ -57,6 +59,7 @@ channel_request_msg/4, channel_success_msg/1, + request_global_msg/3, request_failure_msg/0, request_success_msg/1, @@ -202,10 +205,22 @@ session_channel(ConnectionHandler, Timeout) -> Result :: {ok, ssh:channel_id()} | {error, reason()} . session_channel(ConnectionHandler, InitialWindowSize, MaxPacketSize, Timeout) -> - case ssh_connection_handler:open_channel(ConnectionHandler, "session", <<>>, - InitialWindowSize, - MaxPacketSize, Timeout) of - {open, Channel} -> + open_channel(ConnectionHandler, "session", <<>>, + InitialWindowSize, + MaxPacketSize, + Timeout). + +%%-------------------------------------------------------------------- +%% Description: Opens a channel for the given type. +%% -------------------------------------------------------------------- +open_channel(ConnectionHandler, Type, ChanData, Timeout) -> + open_channel(ConnectionHandler, Type, ChanData, ?DEFAULT_WINDOW_SIZE, ?DEFAULT_PACKET_SIZE, Timeout). + +open_channel(ConnectionHandler, Type, ChanData, InitialWindowSize, MaxPacketSize, Timeout) -> + case ssh_connection_handler:open_channel(ConnectionHandler, Type, ChanData, + InitialWindowSize, MaxPacketSize, + Timeout) of + {open, Channel} -> {ok, Channel}; Error -> Error @@ -740,9 +755,45 @@ handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, {[], Connection} end; +handle_msg(#ssh_msg_global_request{name = <<"tcpip-forward">>, + want_reply = WantReply, + data = <>}, + #connection{options = Opts} = Connection, server) -> + case ?GET_OPT(tcpip_tunnel_out, Opts) of + false -> + %% This daemon instance has not enabled tcpip_forwarding + {[{connection_reply, request_failure_msg()}], Connection}; + + true -> + Sups = ?GET_INTERNAL_OPT(supervisors, Opts), + SubSysSup = proplists:get_value(subsystem_sup, Sups), + FwdSup = ssh_subsystem_sup:tcpip_fwd_supervisor(SubSysSup), + ConnPid = self(), + case ssh_tcpip_forward_acceptor:supervised_start(FwdSup, + {ListenAddrStr, ListenPort}, + undefined, + "forwarded-tcpip", ssh_tcpip_forward_srv, + ConnPid) of + {ok,ListenPort} when WantReply==true -> + {[{connection_reply, request_success_msg(<<>>)}], Connection}; + + {ok,LPort} when WantReply==true -> + {[{connection_reply, request_success_msg(<>)}], Connection}; + + {error,_} when WantReply==true -> + {[{connection_reply, request_failure_msg()}], Connection}; + + _ when WantReply==true -> + {[{connection_reply, request_failure_msg()}], Connection}; + + _ -> + {[], Connection} + end + end; + handle_msg(#ssh_msg_global_request{name = _Type, want_reply = WantReply, - data = _Data}, Connection, _) -> + data = _Data}, Connection, _Role) -> if WantReply == true -> FailMsg = request_failure_msg(), {[{connection_reply, FailMsg}], Connection}; @@ -855,8 +906,13 @@ channel_success_msg(ChannelId) -> %%%---------------------------------------------------------------- %%% request_*_msg(...) -%%% Returns a #ssh_msg_....{} for request responses. +%%% Returns a #ssh_msg_....{} %%% +request_global_msg(Name, WantReply, Data) -> + #ssh_msg_global_request{name = Name, + want_reply = WantReply, + data = Data}. + request_failure_msg() -> #ssh_msg_request_failure{}. @@ -1299,6 +1355,10 @@ handle_cli_msg(C0, ChId, Reply0) -> reply_msg(Ch0, C0, Reply0) end. +%%%---------------------------------------------------------------- +%%% +%%% TCP/IP forwarding + %%%---------------------------------------------------------------- %%% %%% Request response handling on return to the calling ssh_connection_handler diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 991348b156..ae59e8b610 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -1544,6 +1544,17 @@ handle_event(info, {'EXIT', _Sup, Reason}, StateName, _) -> handle_event(info, check_cache, _, D) -> {keep_state, D, cond_set_idle_timer(D)}; +handle_event(info, {fwd_connect_received, Sock, ChId, ChanCB}, StateName, #data{connection_state = Connection}) -> + #connection{options = Options, + channel_cache = Cache, + sub_system_supervisor = SubSysSup} = Connection, + Channel = ssh_client_channel:cache_lookup(Cache, ChId), + {ok,Pid} = ssh_subsystem_sup:start_channel(role(StateName), SubSysSup, self(), ChanCB, ChId, [Sock], undefined, Options), + ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), + gen_tcp:controlling_process(Sock, Pid), + inet:setopts(Sock, [{active,once}]), + keep_state_and_data; + handle_event(info, UnexpectedMessage, StateName, D = #data{ssh_params = Ssh}) -> case unexpected_fun(UnexpectedMessage, D) of report -> diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index 39f23a8b8a..7a2bb95298 100644 --- a/lib/ssh/src/ssh_options.erl +++ b/lib/ssh/src/ssh_options.erl @@ -329,6 +329,12 @@ default(server) -> class => user_option }, + tcpip_tunnel_out => + #{default => false, + chk => fun erlang:is_boolean/1, + class => user_option + }, + system_dir => #{default => "/etc/ssh", chk => fun(V) -> check_string(V) andalso check_dir(V) end, diff --git a/lib/ssh/src/ssh_subsystem_sup.erl b/lib/ssh/src/ssh_subsystem_sup.erl index db34735e1b..140b219b32 100644 --- a/lib/ssh/src/ssh_subsystem_sup.erl +++ b/lib/ssh/src/ssh_subsystem_sup.erl @@ -31,6 +31,7 @@ -export([start_link/5, connection_supervisor/1, channel_supervisor/1, + tcpip_fwd_supervisor/1, start_channel/8 ]). @@ -51,6 +52,10 @@ channel_supervisor(SupPid) when is_pid(SupPid) -> Children = supervisor:which_children(SupPid), ssh_channel_sup(Children). +tcpip_fwd_supervisor(SupPid) when is_pid(SupPid) -> + Children = supervisor:which_children(SupPid), + tcpip_fwd_sup(Children). + start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec, Opts) -> ChannelSup = channel_supervisor(SupPid), ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts). @@ -71,7 +76,8 @@ init([Role, Address, Port, Profile, Options]) -> %%%========================================================================= child_specs(Role, Address, Port, Profile, Options) -> [ssh_channel_child_spec(Role, Address, Port, Profile, Options), - ssh_connection_child_spec(Role, Address, Port, Profile, Options)]. + ssh_connection_child_spec(Role, Address, Port, Profile, Options), + ssh_tcpip_forward_acceptor_child_spec()]. ssh_connection_child_spec(Role, Address, Port, _Profile, Options) -> #{id => id(Role, ssh_connection_sup, Address, Port), @@ -87,6 +93,14 @@ ssh_channel_child_spec(Role, Address, Port, _Profile, Options) -> type => supervisor }. +ssh_tcpip_forward_acceptor_child_spec() -> + #{id => make_ref(), + start => {ssh_tcpip_forward_acceptor_sup, start_link, []}, + restart => temporary, + type => supervisor + }. + + id(Role, Sup, Address, Port) -> {Role, Sup, Address, Port}. @@ -100,5 +114,8 @@ ssh_channel_sup([{_, Child, _, [ssh_channel_sup]} | _]) -> ssh_channel_sup([_ | Rest]) -> ssh_channel_sup(Rest). - +tcpip_fwd_sup([{_, Child, _, [ssh_tcpip_forward_acceptor_sup]} | _]) -> + Child; +tcpip_fwd_sup([_ | Rest]) -> + tcpip_fwd_sup(Rest). diff --git a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl new file mode 100644 index 0000000000..60a0a7a0e1 --- /dev/null +++ b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl @@ -0,0 +1,113 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2018. 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. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: The supervisor for tcpip-forwarding acceptor +%%---------------------------------------------------------------------- + +-module(ssh_tcpip_forward_acceptor). + +-export([supervised_start/6, + start_link/6]). + +-include("ssh.hrl"). + +%%%---------------------------------------------------------------- +supervised_start(FwdSup, {ListenAddrStr, ListenPort}, ConnectToAddr, ChanType, ChanCB, ConnPid) -> + case get_fwd_listen_opts(ListenAddrStr) of + {ok,Opts} -> + %% start listening on Addr:BoundPort + case gen_tcp:listen(ListenPort, [binary, + {reuseaddr,true}, + {active,false} | Opts]) of + {ok,LSock} -> + {ok,{_, TrueListenPort}} = inet:sockname(LSock), + ssh_tcpip_forward_acceptor_sup:start_child(FwdSup, + LSock, + {ListenAddrStr,TrueListenPort}, + ConnectToAddr, + ChanType, + ChanCB, + ConnPid), + {ok, TrueListenPort}; + + {error,Error} -> + {error,Error} + end; + + {error,Error} -> + {error,Error} + end. + + +%%%---------------------------------------------------------------- +start_link(LSock, {ListenAddrStr,ListenPort}, ConnectToAddr, ChanType, ChanCB, ConnPid) -> + Pid = proc_lib:spawn_link( + fun() -> + acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, ConnPid) + end), + {ok, Pid}. + +%%%================================================================ +%%% +%%% Internal +%%% +acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, ConnPid) -> + case gen_tcp:accept(LSock) of + {ok, Sock} -> + {ok, {RemHost,RemPort}} = inet:peername(Sock), + RemHostBin = list_to_binary(encode_ip(RemHost)), + Data = + case ConnectToAddr of + undefined -> + <> + end, + case ssh_connection:open_channel(ConnPid, ChanType, Data, infinity) of + {ok,ChId} -> + gen_tcp:controlling_process(Sock, ConnPid), + ConnPid ! {fwd_connect_received, Sock, ChId, ChanCB}; + _ -> + gen_tcp:close(Sock) + end, + acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, ConnPid); + + {error,closed} -> + ok + end. + +%%%---------------------------------------------------------------- +get_fwd_listen_opts(<<"">> ) -> {ok, []}; +get_fwd_listen_opts(<<"0.0.0.0">> ) -> {ok, [inet]}; +get_fwd_listen_opts(<<"::">> ) -> {ok, [inet6]}; +get_fwd_listen_opts(<<"localhost">>) -> {ok, [{ip,loopback}]}; +get_fwd_listen_opts(AddrStr) -> + case inet:getaddr(binary_to_list(AddrStr), inet) of + {ok, Addr} -> {ok, [{ip,Addr}]}; + {error,Error} -> {error,Error} + end. + +%%%---------------------------------------------------------------- +encode_ip(Addr) when is_tuple(Addr) -> + case catch inet_parse:ntoa(Addr) of + {'EXIT',_} -> false; + A -> A + end. diff --git a/lib/ssh/src/ssh_tcpip_forward_acceptor_sup.erl b/lib/ssh/src/ssh_tcpip_forward_acceptor_sup.erl new file mode 100644 index 0000000000..522ce650ff --- /dev/null +++ b/lib/ssh/src/ssh_tcpip_forward_acceptor_sup.erl @@ -0,0 +1,62 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2018. 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. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: The supervisor for tcpip-forwarding acceptor +%%---------------------------------------------------------------------- + +-module(ssh_tcpip_forward_acceptor_sup). +-behaviour(supervisor). + +-include("ssh.hrl"). + +-export([start_link/0, start_child/7]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link() -> + supervisor:start_link(?MODULE, []). + +start_child(Sup, LSock, ListenAddr, ConnectToAddr, ChanType, ChanCB, ConnPid) -> + Args = [LSock, ListenAddr, ConnectToAddr, ChanType, ChanCB, ConnPid], + supervisor:start_child(Sup, Args). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([]) -> + SupFlags = #{strategy => simple_one_for_one, + intensity => 10, + period => 3600 + }, + ChildSpecs = [#{id => undefined, % As simple_one_for_one is used. + start => {ssh_tcpip_forward_acceptor, start_link, []} + } + ], + {ok, {SupFlags,ChildSpecs}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= diff --git a/lib/ssh/src/ssh_tcpip_forward_srv.erl b/lib/ssh/src/ssh_tcpip_forward_srv.erl new file mode 100644 index 0000000000..0ce8e30732 --- /dev/null +++ b/lib/ssh/src/ssh_tcpip_forward_srv.erl @@ -0,0 +1,55 @@ +-module(ssh_tcpip_forward_srv). + +-behaviour(ssh_server_channel). + +-record(state, { + id, cm, + fwd_socket + }). + +-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]). + +init([FwdSocket]) -> + {ok, #state{fwd_socket=FwdSocket}}. + + +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, State) -> + {ok, State#state{id = ChannelId, + cm = ConnectionManager}}; + +handle_msg({tcp,Sock,Data}, #state{fwd_socket = Sock, + cm = CM, + id = ChId} = State) -> + ssh_connection:send(CM, ChId, Data), + inet:setopts(Sock, [{active,once}]), + {ok, State}; + +handle_msg({tcp_closed,Sock}, #state{fwd_socket = Sock, + cm = CM, + id = ChId} = State) -> + ssh_connection:send_eof(CM, ChId), + {stop, ChId, State#state{fwd_socket=undefined}}. + + + +handle_ssh_msg({ssh_cm, _CM, {data, _ChannelId, _Type, Data}}, #state{fwd_socket=Sock} = State) -> + gen_tcp:send(Sock, Data), + {ok, State}; + +handle_ssh_msg({ssh_cm, _CM, {eof, ChId}}, State) -> + {stop, ChId, State}; + +handle_ssh_msg({ssh_cm, _CM, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _CM, {exit_signal, ChId, _, _Error, _}}, State) -> + {stop, ChId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChId, _Status}}, State) -> + {stop, ChId, State}. + + +terminate(_Reason, #state{fwd_socket=Sock}) -> + gen_tcp:close(Sock), + ok. diff --git a/lib/ssh/test/ssh_sup_SUITE.erl b/lib/ssh/test/ssh_sup_SUITE.erl index 0bf4d87f0a..554fb63ad8 100644 --- a/lib/ssh/test/ssh_sup_SUITE.erl +++ b/lib/ssh/test/ssh_sup_SUITE.erl @@ -342,7 +342,9 @@ chk_empty_con_daemon(Daemon) -> {{ssh_acceptor_sup,_,_,_}, AccSup, supervisor,[ssh_acceptor_sup]}], supervisor:which_children(Daemon), [SubSysSup,AccSup]), - ?wait_match([{{server,ssh_connection_sup, _,_}, + ?wait_match([{_,_, supervisor, + [ssh_tcpip_forward_acceptor_sup]}, + {{server,ssh_connection_sup, _,_}, ConnectionSup, supervisor, [ssh_connection_sup]}, {{server,ssh_channel_sup,_ ,_}, @@ -375,7 +377,10 @@ check_sshd_system_tree(Daemon, Config) -> supervisor:which_children(Daemon), [SubSysSup,AccSup]), - ?wait_match([{{server,ssh_connection_sup, _,_}, + ?wait_match([{_, + _AcceptorSup, supervisor, + [ssh_tcpip_forward_acceptor_sup]}, + {{server,ssh_connection_sup, _,_}, ConnectionSup, supervisor, [ssh_connection_sup]}, {{server,ssh_channel_sup,_ ,_}, -- cgit v1.2.1