summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHans Nilsson <hans@erlang.org>2019-08-20 15:55:06 +0200
committerHans Nilsson <hans@erlang.org>2019-09-20 10:31:47 +0200
commit604499b2b752fe0c09c06dc2e7d10e767f4d5456 (patch)
tree0dbbd857843cd1ccd66376d83288c3e678044db6
parent33b9ea4ca66f63adae4679ae6351c121a588d4dd (diff)
downloaderlang-604499b2b752fe0c09c06dc2e7d10e767f4d5456.tar.gz
ssh: tcpip-forward, client part
-rw-r--r--lib/ssh/doc/src/ssh.xml22
-rw-r--r--lib/ssh/src/Makefile1
-rw-r--r--lib/ssh/src/ssh.app.src1
-rw-r--r--lib/ssh/src/ssh.erl70
-rw-r--r--lib/ssh/src/ssh_connection.erl60
-rw-r--r--lib/ssh/src/ssh_connection_handler.erl46
-rw-r--r--lib/ssh/src/ssh_tcpip_forward_client.erl64
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}.
+
+