diff options
author | Hans Nilsson <hans@erlang.org> | 2019-08-20 15:55:06 +0200 |
---|---|---|
committer | Hans Nilsson <hans@erlang.org> | 2019-09-20 10:31:47 +0200 |
commit | 604499b2b752fe0c09c06dc2e7d10e767f4d5456 (patch) | |
tree | 0dbbd857843cd1ccd66376d83288c3e678044db6 | |
parent | 33b9ea4ca66f63adae4679ae6351c121a588d4dd (diff) | |
download | erlang-604499b2b752fe0c09c06dc2e7d10e767f4d5456.tar.gz |
ssh: tcpip-forward, client part
-rw-r--r-- | lib/ssh/doc/src/ssh.xml | 22 | ||||
-rw-r--r-- | lib/ssh/src/Makefile | 1 | ||||
-rw-r--r-- | lib/ssh/src/ssh.app.src | 1 | ||||
-rw-r--r-- | lib/ssh/src/ssh.erl | 70 | ||||
-rw-r--r-- | lib/ssh/src/ssh_connection.erl | 60 | ||||
-rw-r--r-- | lib/ssh/src/ssh_connection_handler.erl | 46 | ||||
-rw-r--r-- | lib/ssh/src/ssh_tcpip_forward_client.erl | 64 |
7 files changed, 253 insertions, 11 deletions
diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml index 2314474c36..b38ab2ce5f 100644 --- a/lib/ssh/doc/src/ssh.xml +++ b/lib/ssh/doc/src/ssh.xml @@ -1302,6 +1302,28 @@ </desc> </func> + <func> + <name name="tcpip_tunnel_from_server" arity="5" since=""/> + <name name="tcpip_tunnel_from_server" arity="6" since=""/> + <fsummary>TCP/IP tunneling from a server to a client ("tcpip-forward")</fsummary> + <desc> + <p>Asks the remote server of <c>ConnectionRef</c> to listen to <c>ListenHost:ListenPort</c>. + When someone connects that address, the connection is forwarded in an encrypted channel from + the server to the client. The client (that is, at the node that calls this function) then + connects to <c>ConnectToHost:ConnectToPort</c>. + </p> + <p>The returned <c>TrueListenPort</c> is the port that is listened to. It is the same as + <c>ListenPort</c>, except when <c>ListenPort = 0</c>. In that case a free port is selected + by the underlying OS. + </p> + <p>Note that in case of an Erlang/OTP SSH server (daemon) as peer, that server must have been + started with the option + <seealso marker="#type-tcpip_tunnel_out_daemon_option">tcpip_tunnel_out</seealso> + to allow the connection. + </p> + </desc> + </func> + </funcs> </erlref> diff --git a/lib/ssh/src/Makefile b/lib/ssh/src/Makefile index f1b11bc5b9..13203cf1bb 100644 --- a/lib/ssh/src/Makefile +++ b/lib/ssh/src/Makefile @@ -75,6 +75,7 @@ MODULES= \ ssh_sup \ ssh_system_sup \ ssh_tcpip_forward_srv \ + ssh_tcpip_forward_client \ ssh_tcpip_forward_acceptor_sup \ ssh_tcpip_forward_acceptor \ ssh_transport \ diff --git a/lib/ssh/src/ssh.app.src b/lib/ssh/src/ssh.app.src index 0c70399d3f..54cf97f2ce 100644 --- a/lib/ssh/src/ssh.app.src +++ b/lib/ssh/src/ssh.app.src @@ -36,6 +36,7 @@ ssh_sftpd_file_api, ssh_subsystem_sup, ssh_sup, + ssh_tcpip_forward_client, ssh_tcpip_forward_srv, ssh_tcpip_forward_acceptor_sup, ssh_tcpip_forward_acceptor, diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl index 874c545e61..1471654a89 100644 --- a/lib/ssh/src/ssh.erl +++ b/lib/ssh/src/ssh.erl @@ -39,7 +39,8 @@ chk_algos_opts/1, stop_listener/1, stop_listener/2, stop_listener/3, stop_daemon/1, stop_daemon/2, stop_daemon/3, - shell/1, shell/2, shell/3 + shell/1, shell/2, shell/3, + tcpip_tunnel_from_server/5, tcpip_tunnel_from_server/6 ]). %%% "Deprecated" types export: @@ -565,6 +566,60 @@ chk_algos_opts(Opts) -> end. %%-------------------------------------------------------------------- +%% Ask remote server to listen to ListenHost:ListenPort. When someone +%% connects that address, connect to ConnectToHost:ConnectToPort from +%% the client. +%%-------------------------------------------------------------------- +-spec tcpip_tunnel_from_server(ConnectionRef, + ListenHost, ListenPort, + ConnectToHost, ConnectToPort + ) -> + {ok,TrueListenPort} | {error, term()} when + ConnectionRef :: connection_ref(), + ListenHost :: host(), + ListenPort :: inet:port_number(), + ConnectToHost :: host(), + ConnectToPort :: inet:port_number(), + TrueListenPort :: inet:port_number(). + +tcpip_tunnel_from_server(ConnectionRef, ListenHost, ListenPort, ConnectToHost, ConnectToPort) -> + tcpip_tunnel_from_server(ConnectionRef, ListenHost, ListenPort, ConnectToHost, ConnectToPort, infinity). + +-spec tcpip_tunnel_from_server(ConnectionRef, + ListenHost, ListenPort, + ConnectToHost, ConnectToPort, + Timeout) -> + {ok,TrueListenPort} | {error, term()} when + ConnectionRef :: connection_ref(), + ListenHost :: host(), + ListenPort :: inet:port_number(), + ConnectToHost :: host(), + ConnectToPort :: inet:port_number(), + Timeout :: timeout(), + TrueListenPort :: inet:port_number(). + +tcpip_tunnel_from_server(ConnectionRef, ListenHost0, ListenPort, ConnectToHost0, ConnectToPort, Timeout) -> + SockOpts = [], + ListenHost = mangle_tunnel_address(ListenHost0), + ConnectToHost = mangle_connect_address(ConnectToHost0, SockOpts), + case ssh_connection_handler:global_request(ConnectionRef, "tcpip-forward", true, + {ListenHost,ListenPort,ConnectToHost,ConnectToPort}, + Timeout) of + {success,<<>>} -> + {ok, ListenPort}; + {success,<<TruePort:32/unsigned-integer>>} when ListenPort==0 -> + {ok, TruePort}; + {success,_} = Res -> + {error, {bad_result,Res}}; + {failure,<<>>} -> + {error,not_accepted}; + {failure,Error} -> + {error,Error}; + Other -> + Other + end. + +%%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- %% The handle_daemon_args/2 function basically only sets the ip-option in Opts @@ -681,3 +736,16 @@ mangle_connect_address1(A, _) -> {ok, {0,0,0,0,0,0,0,0}} -> loopback(true); _ -> A end. + +%%%---------------------------------------------------------------- +mangle_tunnel_address(any) -> <<"">>; +mangle_tunnel_address(loopback) -> <<"localhost">>; +mangle_tunnel_address({0,0,0,0}) -> <<"">>; +mangle_tunnel_address({0,0,0,0,0,0,0,0}) -> <<"">>; +mangle_tunnel_address(IP) when is_tuple(IP) -> list_to_binary(inet_parse:ntoa(IP)); +mangle_tunnel_address(A) when is_atom(A) -> mangle_tunnel_address(atom_to_list(A)); +mangle_tunnel_address(X) when is_list(X) -> case catch inet:parse_address(X) of + {ok, {0,0,0,0}} -> <<"">>; + {ok, {0,0,0,0,0,0,0,0}} -> <<"">>; + _ -> list_to_binary(X) + end. diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index 318e0ca35d..1c22b095e8 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -391,6 +391,7 @@ ptty_alloc(ConnectionHandler, Channel, Options0, TimeOut) -> proplists:get_value(pixel_height, TermData, PixHeight), proplists:get_value(pty_opts, TermData, []), TimeOut ). + %%-------------------------------------------------------------------- %% Not yet officialy supported! The following functions are part of the %% initial contributed ssh application. They are untested. Do we want them? @@ -583,6 +584,64 @@ handle_msg(#ssh_msg_channel_open{channel_type = "session" = Type, {[{connection_reply, FailMsg}], Connection0} end; +handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip", + sender_channel = RemoteId, + initial_window_size = WindowSize, + maximum_packet_size = PacketSize, + data = <<?DEC_BIN(ConnectedHost,_L1), ?UINT32(ConnectedPort), + ?DEC_BIN(_OriginHost,_L2), ?UINT32(_OriginPort) + >> + }, + #connection{channel_cache = Cache, + channel_id_seed = ChId, + options = Options, + sub_system_supervisor = SubSysSup + } = C, + client) -> + {ReplyMsg, NextChId} = + case ssh_connection_handler:retrieve(C, {tcpip_forward,ConnectedHost,ConnectedPort}) of + {ok, {ConnectToHost,ConnectToPort}} -> + case gen_tcp:connect(ConnectToHost, ConnectToPort, [{active,false}, binary]) of + {ok,Sock} -> + {ok,Pid} = ssh_subsystem_sup:start_channel(client, SubSysSup, self(), + ssh_tcpip_forward_client, ChId, + [Sock], undefined, Options), + ssh_client_channel:cache_update(Cache, + #channel{type = "forwarded-tcpip", + sys = "none", + local_id = ChId, + remote_id = RemoteId, + user = Pid, + recv_window_size = ?DEFAULT_WINDOW_SIZE, + recv_packet_size = ?DEFAULT_PACKET_SIZE, + send_window_size = WindowSize, + send_packet_size = PacketSize, + send_buf = queue:new() + }), + gen_tcp:controlling_process(Sock, Pid), + inet:setopts(Sock, [{active,once}]), + {channel_open_confirmation_msg(RemoteId, ChId, + ?DEFAULT_WINDOW_SIZE, + ?DEFAULT_PACKET_SIZE), + ChId + 1}; + + {error,Error} -> + {channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + io_lib:format("Forwarded connection refused: ~p",[Error]), + "en"), + ChId} + end; + + undefined -> + {channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + io_lib:format("No forwarding ordered",[]), + "en"), + ChId} + end, + {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}}; + handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip", sender_channel = RemoteId, initial_window_size = WindowSize, @@ -597,7 +656,6 @@ handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip", sub_system_supervisor = SubSysSup } = C, server) -> - {ReplyMsg, NextChId} = case ?GET_OPT(tcpip_tunnel_in, Options) of %% May add more to the option, like allowed ip/port pairs to connect to diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index ae59e8b610..bae27da330 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -343,6 +343,14 @@ close(ConnectionHandler, ChannelId) -> store(ConnectionHandler, Key, Value) -> cast(ConnectionHandler, {store,Key,Value}). +retrieve(#connection{options=Opts}, Key) -> + try ?GET_INTERNAL_OPT(Key, Opts) of + Value -> + {ok,Value} + catch + error:{badkey,Key} -> + undefined + end; retrieve(ConnectionHandler, Key) -> call(ConnectionHandler, {retrieve,Key}). @@ -1277,10 +1285,31 @@ handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, StateName, {keep_state, D, cond_set_idle_timer(D)} end; -handle_event({call,From}, {global_request, Type, Data, Timeout}, StateName, D) when ?CONNECTED(StateName) -> +handle_event({call,From}, {global_request, "tcpip-forward" = Type, + {ListenHost,ListenPort,ConnectToHost,ConnectToPort}, + Timeout}, StateName, D0) when ?CONNECTED(StateName) -> Id = make_ref(), + Data = <<?STRING(ListenHost), ?Euint32(ListenPort)>>, + Fun = fun({success, <<Port:32/unsigned-integer>>}, C) -> + Key = {tcpip_forward,ListenHost,Port}, + Value = {ConnectToHost,ConnectToPort}, + C#connection{options = ?PUT_INTERNAL_OPT({Key,Value}, C#connection.options)}; + ({success, <<>>}, C) -> + Key = {tcpip_forward,ListenHost,ListenPort}, + Value = {ConnectToHost,ConnectToPort}, + C#connection{options = ?PUT_INTERNAL_OPT({Key,Value}, C#connection.options)}; + (_, C) -> + C + end, D = send_msg(ssh_connection:request_global_msg(Type, true, Data), - add_request(true, Id, From, D)), + add_request(Fun, Id, From, D0)), + start_channel_request_timer(Id, From, Timeout), + {keep_state, D, cond_set_idle_timer(D)}; + +handle_event({call,From}, {global_request, Type, Data, Timeout}, StateName, D0) when ?CONNECTED(StateName) -> + Id = make_ref(), + D = send_msg(ssh_connection:request_global_msg(Type, true, Data), + add_request(true, Id, From, D0)), start_channel_request_timer(Id, From, Timeout), {keep_state, D, cond_set_idle_timer(D)}; @@ -1365,15 +1394,14 @@ handle_event({call,From}, {close, ChannelId}, StateName, D0) end; handle_event(cast, {store,Key,Value}, _StateName, #data{connection_state=C0} = D) -> - C = #connection{options = ?PUT_INTERNAL_OPT({Key,Value}, C0#connection.options)}, + C = C0#connection{options = ?PUT_INTERNAL_OPT({Key,Value}, C0#connection.options)}, {keep_state, D#data{connection_state = C}}; -handle_event({call,From}, {retrieve,Key}, _StateName, #data{connection_state=C0}) -> - try ?GET_INTERNAL_OPT(Key, C0#connection.options) of - Value -> - {keep_state_and_data, [{reply,From,{ok,Value}}]} - catch - error:{badkey,Key} -> +handle_event({call,From}, {retrieve,Key}, _StateName, #data{connection_state=C}) -> + case retrieve(C, Key) of + {ok,Value} -> + {keep_state_and_data, [{reply,From,{ok,Value}}]}; + _ -> {keep_state_and_data, [{reply,From,undefined}]} end; diff --git a/lib/ssh/src/ssh_tcpip_forward_client.erl b/lib/ssh/src/ssh_tcpip_forward_client.erl new file mode 100644 index 0000000000..60939f6263 --- /dev/null +++ b/lib/ssh/src/ssh_tcpip_forward_client.erl @@ -0,0 +1,64 @@ +-module(ssh_tcpip_forward_client). + +-behaviour(ssh_client_channel). + +-record(state, { + id, cm, + fwd_socket + }). + +-export([init/1, handle_call/3, handle_cast/2, handle_msg/2, handle_ssh_msg/2, terminate/2, code_change/3]). + +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. + + +handle_call(Req, _, S) -> {reply, {unknown,Req}, S}. + +handle_cast(_, S) -> {noreply, S}. + +code_change(_, S, _) -> {ok, S}. + + |