Files
erlang27/feature-fix-update-ssh-stack.patch
Simon Lees b1c2c86f54 - Update the ssh component
Take the latest update to the ssh component from the maint-27
branch, this contains multiple new fixes and no features as
well as making the backport process easier
  * Multiple fixes for Excessive Resource Consumption
    (bsc#1249469, bsc#1249470 bsc#1249472, CVE-2025-48038,
    CVE-2025-48039, CVE-2025-48040)
  * Other Minor fixes
  * feature-fix-update-ssh-stack.patch
2025-12-19 11:02:50 +10:30

5685 lines
230 KiB
Diff

diff -ruN a/lib/ssh/doc/docs.exs b/lib/ssh/doc/docs.exs
--- a/lib/ssh/doc/docs.exs 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/doc/docs.exs 2025-12-17 17:25:02.074061554 +1030
@@ -1,4 +1,12 @@
[
+ annotations_for_docs: fn
+ md ->
+ if md[:rfc] do
+ [md[:rfc]]
+ else
+ []
+ end
+ end,
## The order of these items determine
## how they are listed in the docs
extras: [
diff -ruN a/lib/ssh/doc/notes.md b/lib/ssh/doc/notes.md
--- a/lib/ssh/doc/notes.md 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/doc/notes.md 2025-12-17 17:25:02.074275138 +1030
@@ -1,7 +1,7 @@
<!--
%CopyrightBegin%
-Copyright Ericsson AB 2023-2024. All Rights Reserved.
+Copyright Ericsson AB 2023-2025. 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.
@@ -19,6 +19,181 @@
-->
# SSH Release Notes
+## Ssh 5.2.11.4
+
+### Fixed Bugs and Malfunctions
+
+- With this change user space buffers are used to limit ssh hello message size instead of kernel buffers
+
+ Own Id: OTP-19839 Aux Id: ERIERL-1273, [PR-10350]
+
+[PR-10350]: https://github.com/erlang/otp/pull/10350
+
+## Ssh 5.2.11.3
+
+### Fixed Bugs and Malfunctions
+
+- Option max_handles can be configured for sshd running SFTP. The positive integer value limits amount of file handles opened for a connection (by default 4096 is used).
+
+ *** POTENTIAL INCOMPATIBILITY ***
+
+ Own Id: OTP-19701 Aux Id: [CVE-2025-48041], [PR-10157]
+
+- Avoid decoding KEX messages providing too many algorithms. This change does not introduce new limitation but assures it is enforced earlier in processing chain. Adjustments in error logging during handshake.
+
+ *** POTENTIAL INCOMPATIBILITY ***
+
+ Own Id: OTP-19741 Aux Id: [CVE-2025-48040], [PR-10162]
+
+- A new 'max_path' option is now available in the sshd configuration, allowing administrators to set the maximum allowable path length. By default, this value is set to 4096 characters.
+
+ *** POTENTIAL INCOMPATIBILITY ***
+
+ Own Id: OTP-19742 Aux Id: [CVE-2025-48039], [PR-10155]
+
+- Reject file handles exceeding size specified in RFCs (256 bytes).
+
+ *** POTENTIAL INCOMPATIBILITY ***
+
+ Own Id: OTP-19748 Aux Id: [CVE-2025-48038], [PR-10156]
+
+[CVE-2025-48041]: https://nvd.nist.gov/vuln/detail/2025-48041
+[PR-10157]: https://github.com/erlang/otp/pull/10157
+[CVE-2025-48040]: https://nvd.nist.gov/vuln/detail/2025-48040
+[PR-10162]: https://github.com/erlang/otp/pull/10162
+[CVE-2025-48039]: https://nvd.nist.gov/vuln/detail/2025-48039
+[PR-10155]: https://github.com/erlang/otp/pull/10155
+[CVE-2025-48038]: https://nvd.nist.gov/vuln/detail/2025-48038
+[PR-10156]: https://github.com/erlang/otp/pull/10156
+
+## Ssh 5.2.11.2
+
+### Fixed Bugs and Malfunctions
+
+- Fix file handle id generation.
+
+ Own Id: OTP-19691 Aux Id: [PR-10003]
+
+- Fixes a badmatch error, when SFTP operation cannot be processed due to channel closed in parallel.
+
+ Own Id: OTP-19707 Aux Id: [GH-9655], [PR-10035], [PR-10036]
+
+[PR-10003]: https://github.com/erlang/otp/pull/10003
+[GH-9655]: https://github.com/erlang/otp/issues/9655
+[PR-10035]: https://github.com/erlang/otp/pull/10035
+[PR-10036]: https://github.com/erlang/otp/pull/10036
+
+## Ssh 5.2.11.1
+
+### Fixed Bugs and Malfunctions
+
+- Various channel closing robustness improvements. Avoid crashes when channel handling process closes channel and immediately exits. Avoid breaking the protocol by sending duplicated channel-close messages. Cleanup channels which timeout during closing procedure.
+
+ Own Id: OTP-19634 Aux Id: [GH-9102], [PR-9103]
+
+- Improved interoperability with clients acting as Paramiko.
+
+ Own Id: OTP-19637 Aux Id: [GH-6463], [PR-9838]
+
+[GH-9102]: https://github.com/erlang/otp/issues/9102
+[PR-9103]: https://github.com/erlang/otp/pull/9103
+[GH-6463]: https://github.com/erlang/otp/issues/6463
+[PR-9838]: https://github.com/erlang/otp/pull/9838
+
+## Ssh 5.2.11
+
+### Fixed Bugs and Malfunctions
+
+- Fix KEX strict implementation according to draft-miller-sshm-strict-kex-01 document.
+
+ Own Id: OTP-19625 Aux Id: CVE-2025-46712
+
+## Ssh 5.2.10
+
+### Fixed Bugs and Malfunctions
+
+- Reception of wrong Unicode does not cause unnecessary processing. US-ASCII fields are not decoded as Unicode.
+
+ Own Id: OTP-19582 Aux Id: [PR-9679]
+
+- SSH daemon disconnects upon receiving connection protocol message for unauthenticated used.
+
+ Thanks to Fabian Bäumer, Marcel Maehren, Marcus Brinkmann, Nurullah Erinola, Jörg Schwenk (Ruhr University Bochum).
+
+ Own Id: OTP-19595 Aux Id: CVE-2025-32433
+
+[PR-9679]: https://github.com/erlang/otp/pull/9679
+
+## Ssh 5.2.9
+
+### Fixed Bugs and Malfunctions
+
+- Reception of malicious KEX init message does not result with ssh daemon excessive memory usage.
+
+ Own Id: OTP-19543 Aux Id: CVE-2025-30211
+
+- Call to ssh:daemon_replace_options does not crash when argument is not a valid daemon ref.
+
+ Own Id: OTP-19559 Aux Id: [GH-9554], [PR-9545]
+
+[GH-9554]: https://github.com/erlang/otp/issues/9554
+[PR-9545]: https://github.com/erlang/otp/pull/9545
+
+## Ssh 5.2.8
+
+### Fixed Bugs and Malfunctions
+
+- Minor documentation improvements.
+
+ Own Id: OTP-19410 Aux Id: [PR-9188]
+
+- Function specification for `ssh_sftp:start_channel/2` is fixed.
+
+ Own Id: OTP-19475 Aux Id: [PR-9368], [GH-9359]
+
+[PR-9188]: https://github.com/erlang/otp/pull/9188
+[PR-9368]: https://github.com/erlang/otp/pull/9368
+[GH-9359]: https://github.com/erlang/otp/issues/9359
+
+## Ssh 5.2.7
+
+### Fixed Bugs and Malfunctions
+
+- SFTP packets exceeding max packet size are not processed and dropped.
+
+ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618
+
+## Ssh 5.2.6
+
+### Fixed Bugs and Malfunctions
+
+- With this change, type specs for ssh:connection_info/1,2 functions are fixed so they include \{error, term()\} return value.
+
+ Own Id: OTP-19388 Aux Id: ERIERL-1165, [PR-9161]
+
+- With this change, ssh client accepts a banner sent during processing keyboard interactive user authentication.
+
+ Own Id: OTP-19392 Aux Id: [PR-9139], [GH-9065]
+
+- With this change, large sftp transfers does not hang. Redundant window adjustment are not requested.
+
+ Own Id: OTP-19435 Aux Id: [PR-9309]
+
+[PR-9161]: https://github.com/erlang/otp/pull/9161
+[PR-9139]: https://github.com/erlang/otp/pull/9139
+[GH-9065]: https://github.com/erlang/otp/issues/9065
+[PR-9309]: https://github.com/erlang/otp/pull/9309
+
+## Ssh 5.2.5
+
+### Fixed Bugs and Malfunctions
+
+- Documentation is polished after OTP-27 migration to markdown.
+
+ Own Id: OTP-19335 Aux Id: [PR-9021]
+
+[PR-9021]: https://github.com/erlang/otp/pull/9021
+
## Ssh 5.2.4
### Fixed Bugs and Malfunctions
@@ -110,6 +285,50 @@
[PR-7845]: https://github.com/erlang/otp/pull/7845
[PR-8026]: https://github.com/erlang/otp/pull/8026
+## Ssh 5.1.4.6
+
+### Fixed Bugs and Malfunctions
+
+* SFTP packets exceeding max packet size are not processed and dropped.
+
+ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618
+
+## Ssh 5.1.4.5
+
+### Fixed Bugs and Malfunctions
+
+* With this change, type specs for ssh:connection_info/1,2 functions are fixed so they include \{error, term()\} return value.
+
+ Own Id: OTP-19388 Aux Id: ERIERL-1165, PR-9161
+* With this change, ssh client accepts a banner sent during processing keyboard interactive user authentication.
+
+ Own Id: OTP-19392 Aux Id: PR-9139, GH-9065
+* With this change, large sftp transfers does not hang. Redundant window adjustment are not requested.
+
+ Own Id: OTP-19435 Aux Id: PR-9309
+
+## Ssh 5.1.4.4
+
+### Fixed Bugs and Malfunctions
+
+* With this change, ssh connection does not crash upon receiving exit-signal message for an already terminated channel.
+
+ Own Id: OTP-19326 Aux Id: PR-8995, GH-8929
+
+## Ssh 5.1.4.3
+
+### Fixed Bugs and Malfunctions
+
+* With this change, a race condition is removed from ssh client connection setup procedure.
+
+ Own Id: OTP-19124 Aux Id: GH-7550, PR-8766
+* With this change, ssh:connect is not affected by presence of EXIT message in queue.
+
+ Own Id: OTP-19246 Aux Id: GH-8223, PR-8854
+* With this change, ssh appends \{active, false\} option after socket options received from user - so that false value is always used.
+
+ Own Id: OTP-19247 Aux Id: PR-8226
+
## Ssh 5.1.4.2
### Fixed Bugs and Malfunctions
@@ -179,6 +398,8 @@
cost of affecting interoperability. See
[Configuring algorithms in SSH](configure_algos.md).
+ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk.
+
\*** POTENTIAL INCOMPATIBILITY \***
Own Id: OTP-18897
@@ -247,6 +468,61 @@
Own Id: OTP-18490 Aux Id: OTP-18471, GH-6339, PR-6843
+## Ssh 4.15.3.10
+
+### Fixed Bugs and Malfunctions
+
+* SFTP packets exceeding max packet size are not processed and dropped.
+
+ Own Id: OTP-19466 Aux Id: ERIERL-1173, CVE-2025-26618
+
+## Ssh 4.15.3.9
+
+### Fixed Bugs and Malfunctions
+
+* With this change, type specs for ssh:connection_info/1,2 functions are fixed so they include \{error, term()\} return value.
+
+ Own Id: OTP-19388 Aux Id: ERIERL-1165, PR-9161
+* With this change, ssh client accepts a banner sent during processing keyboard interactive user authentication.
+
+ Own Id: OTP-19392 Aux Id: PR-9139, GH-9065
+* With this change, large sftp transfers does not hang. Redundant window adjustment are not requested.
+
+ Own Id: OTP-19435 Aux Id: PR-9309
+
+## Ssh 4.15.3.8
+
+### Fixed Bugs and Malfunctions
+
+* With this change, ssh connection does not crash upon receiving exit-signal message for an already terminated channel.
+
+ Own Id: OTP-19326 Aux Id: PR-8995, GH-8929
+
+## Ssh 4.15.3.7
+
+### Fixed Bugs and Malfunctions
+
+* With this change, a race condition is removed from ssh client connection setup procedure.
+
+ Own Id: OTP-19124 Aux Id: GH-7550, PR-8766
+* With this change, ssh:connect is not affected by presence of EXIT message in queue.
+
+ Own Id: OTP-19246 Aux Id: GH-8223, PR-8854
+* With this change, ssh appends \{active, false\} option after socket options received from user - so that false value is always used.
+
+ Own Id: OTP-19247 Aux Id: PR-8226
+
+## Ssh 4.15.3.6
+
+### Fixed Bugs and Malfunctions
+
+* The SSh daemon started with a TCP port number argument will now re-try obtaining a listen socket before returning an error to the user.
+
+ Own Id: OTP-19170 Aux Id: GH-7746
+* Robustness has been improved by monitoring the connection handler process before casting the socket control notification.
+
+ Own Id: OTP-19173 Aux Id: PR-8310
+
## Ssh 4.15.3.5
### Fixed Bugs and Malfunctions
@@ -305,6 +581,8 @@
cost of affecting interoperability. See
[Configuring algorithms in SSH](configure_algos.md).
+ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk.
+
\*** POTENTIAL INCOMPATIBILITY \***
Own Id: OTP-18897
@@ -471,6 +749,8 @@
cost of affecting interoperability. See
[Configuring algorithms in SSH](configure_algos.md).
+ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk.
+
\*** POTENTIAL INCOMPATIBILITY \***
Own Id: OTP-18897
@@ -697,6 +977,8 @@
If strict KEX availability cannot be ensured on both connection sides, affected encryption modes(CHACHA and CBC) can be disabled with standard ssh configuration. This will provide protection against vulnerability, but at a cost of affecting interoperability. See Configuring algorithms in SSH User's Guide.
+ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk.
+
\*** POTENTIAL INCOMPATIBILITY ***
Own Id: OTP-18897
@@ -1081,6 +1363,8 @@
If strict KEX availability cannot be ensured on both connection sides, affected encryption modes(CHACHA and CBC) can be disabled with standard ssh configuration. This will provide protection against vulnerability, but at a cost of affecting interoperability. See Configuring algorithms in SSH User's Guide.
+ Thanks to Fabian Bäumer, Marcus Brinkmann and Jörg Schwenk.
+
\*** POTENTIAL INCOMPATIBILITY ***
Own Id: OTP-18897
diff -ruN a/lib/ssh/src/.gitignore b/lib/ssh/src/.gitignore
--- a/lib/ssh/src/.gitignore 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/src/.gitignore 2025-12-17 17:25:02.074275138 +1030
@@ -0,0 +1 @@
+deps
diff -ruN a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl
--- a/lib/ssh/src/ssh_acceptor.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_acceptor.erl 2025-12-17 17:25:02.074556391 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -206,39 +206,63 @@
handle_error(Reason, ToAddress, ToPort, FromAddress, FromPort) ->
case Reason of
{max_sessions, MaxSessions} ->
- error_logger:info_report(
- lists:concat(["Ssh login attempt to ",ssh_lib:format_address_port(ToAddress,ToPort),
- " from ",ssh_lib:format_address_port(FromAddress,FromPort),
- " denied due to option max_sessions limits to ",
- MaxSessions, " sessions."
- ])
- );
-
+ MsgFun =
+ fun(debug) ->
+ lists:concat(["Ssh login attempt to ",
+ ssh_lib:format_address_port(ToAddress,ToPort),
+ " from ",
+ ssh_lib:format_address_port(FromAddress,FromPort),
+ " denied due to option max_sessions limits to ",
+ MaxSessions, " sessions."]);
+ (_) ->
+ ["Ssh login attempt denied max_session limits"]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun));
Limit when Limit==enfile ; Limit==emfile ->
%% Out of sockets...
- error_logger:info_report([atom_to_list(Limit),": out of accept sockets on ",
- ssh_lib:format_address_port(ToAddress, ToPort),
- " - retrying"]),
+ MsgFun =
+ fun(debug) ->
+ [atom_to_list(Limit),": out of accept sockets on ",
+ ssh_lib:format_address_port(ToAddress, ToPort),
+ " - retrying"];
+ (_) ->
+ ["Out of accept sockets on - retrying"]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun)),
timer:sleep(?SLEEP_TIME);
-
closed ->
- error_logger:info_report(["The ssh accept socket on ",ssh_lib:format_address_port(ToAddress,ToPort),
- "was closed by a third party."]
- );
-
+ MsgFun =
+ fun(debug) ->
+ ["The ssh accept socket on ", ssh_lib:format_address_port(ToAddress,ToPort),
+ "was closed by a third party."];
+ (_) ->
+ ["The ssh accept socket on was closed by a third party"]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun));
timeout ->
ok;
-
Error when is_list(Error) ->
ok;
Error when FromAddress=/=undefined,
FromPort=/=undefined ->
- error_logger:info_report(["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
- " for connect from ",ssh_lib:format_address_port(FromAddress,FromPort),
- io_lib:format(": ~p", [Error])]);
+ MsgFun =
+ fun(debug) ->
+ ["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
+ " for connect from ",ssh_lib:format_address_port(FromAddress,FromPort),
+ io_lib:format(": ~p", [Error])];
+ (_) ->
+ [io_lib:format("Accept failed on for connection: ~p", [Error])]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun));
Error ->
- error_logger:info_report(["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
- io_lib:format(": ~p", [Error])])
+ MsgFun =
+ fun(debug) ->
+ ["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort),
+ io_lib:format(": ~p", [Error])];
+ (_) ->
+ [io_lib:format("Accept failed on for connection: ~p", [Error])]
+ end,
+ error_logger:info_report(?SELECT_MSG(MsgFun))
end.
%%%----------------------------------------------------------------
diff -ruN a/lib/ssh/src/ssh_agent.erl b/lib/ssh/src/ssh_agent.erl
--- a/lib/ssh/src/ssh_agent.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_agent.erl 2025-12-17 17:25:02.074556391 +1030
@@ -58,7 +58,7 @@
""".
-moduledoc(#{since => "OTP 23.0",
titles =>
- [{type,<<"Options for the ssh_agent callback module">>}]}).
+ [{type,<<"Options">>}]}).
-behaviour(ssh_client_key_api).
@@ -72,19 +72,19 @@
Sets the [socket path](`m:ssh_agent#SOCKET_PATH`) for the communication with the
agent.
""".
--doc(#{title => <<"Options for the ssh_agent callback module">>}).
+-doc(#{title => <<"Options">>}).
-type socket_path_option() :: {socket_path, string()}.
-doc """
Sets the time-out in milliseconds when communicating with the agent via the
socket. The default value is `1000`.
""".
--doc(#{title => <<"Options for the ssh_agent callback module">>}).
+-doc(#{title => <<"Options">>}).
-type timeout_option() :: {timeout, integer()}.
-doc """
The module which the `add_host_key` and `is_host_key` callbacks are delegated
to. Defaults to the `m:ssh_file` module.
""".
--doc(#{title => <<"Options for the ssh_agent callback module">>}).
+-doc(#{title => <<"Options">>}).
-type call_ssh_file_option() :: {call_ssh_file, atom()}.
%% ssh_client_key_api implementation
diff -ruN a/lib/ssh/src/ssh_client_channel.erl b/lib/ssh/src/ssh_client_channel.erl
--- a/lib/ssh/src/ssh_client_channel.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_client_channel.erl 2025-12-17 17:25:02.075556404 +1030
@@ -22,16 +22,6 @@
-module(ssh_client_channel).
-moduledoc """
-\-behaviour(ssh_client_channel). (Replaces ssh_channel)
-
-> #### Note {: .info }
->
-> This module replaces ssh_channel.
->
-> The old module is still available for compatibility, but should not be used
-> for new programs. The old module will not be maintained except for some error
-> corrections
-
SSH services (clients and servers) are implemented as channels that are
multiplexed over an SSH connection and communicates over the
[SSH Connection Protocol](http://www.ietf.org/rfc/rfc4254.txt). This module
@@ -45,6 +35,14 @@
> #### Note {: .info }
>
+> This module replaces ssh_channel.
+>
+> The old module is still available for compatibility, but should not be used
+> for new programs. The old module will not be maintained except for some error
+> corrections
+
+> #### Note {: .info }
+>
> When implementing a `ssh` subsystem for daemons, use
> [\-behaviour(ssh_server_channel)](`m:ssh_server_channel`) (Replaces
> ssh_daemon_channel) instead.
@@ -60,8 +58,7 @@
semantics as in a `m:gen_server`. If the time-out occurs, `c:handle_msg/2` is called as
handle_msg(timeout, State).
""".
--moduledoc(#{since => "OTP 21.0",
- titles => [{callback,<<"Callback Functions">>}]}).
+-moduledoc(#{since => "OTP 21.0"}).
-include("ssh.hrl").
-include("ssh_connect.hrl").
@@ -73,7 +70,7 @@
For more detailed information on time-outs, see Section
[Callback timeouts](`m:ssh_client_channel#module-callback-timeouts`).
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback init(Args :: term()) ->
{ok, State :: term()} | {ok, State :: term(), timeout() | hibernate} |
{stop, Reason :: term()} | ignore.
@@ -83,7 +80,7 @@
For more detailed information on time-outs,, see Section
[Callback timeouts](`m:ssh_client_channel#module-callback-timeouts`).
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: term()) ->
{reply, Reply :: term(), NewState :: term()} |
@@ -98,7 +95,7 @@
For more detailed information on time-outs, see Section
[Callback timeouts](`m:ssh_client_channel#module-callback-timeouts`).
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback handle_cast(Request :: term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), timeout() | hibernate} |
@@ -112,7 +109,7 @@
the channel process terminates with reason `Reason`. The return value is
ignored.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback terminate(Reason :: (normal | shutdown | {shutdown, term()} |
term()),
State :: term()) ->
@@ -136,7 +133,7 @@
> handle two versions of the state, but this function cannot be used in the
> normal way.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback code_change(OldVsn :: (term() | {down, term()}), State :: term(),
Extra :: term()) ->
{ok, NewState :: term()} | {error, Reason :: term()}.
@@ -148,14 +145,14 @@
Possible Erlang 'EXIT' messages is to be handled by this function and all
channels are to handle the following message.
-- **`{ssh_channel_up, ``t:ssh:channel_id/0``, ``t:ssh:connection_ref/0``}`** -
+- **`{ssh_channel_up,` `t:ssh:channel_id/0` `,` `t:ssh:connection_ref/0` `}`** -
This is the first message that the channel receives. It is sent just before
the `init/1` function returns successfully. This is especially useful if the
server wants to send a message to the client without first receiving a message
from it. If the message is not useful for your particular scenario, ignore it
by immediately returning `{ok, State}`.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback handle_msg(Msg ::term(), State :: term()) ->
{ok, State::term()} | {stop, ChannelId::ssh:channel_id(), State::term()}.
@@ -165,11 +162,11 @@
The following message is taken care of by the `ssh_client_channel` behavior.
-- **`{closed, ``t:ssh:channel_id/0``}`** - The channel behavior sends a close
+- **`{closed,` `t:ssh:channel_id/0` `}`** - The channel behavior sends a close
message to the other side, if such a message has not already been sent. Then
it terminates the channel with reason `normal`.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback handle_ssh_msg(ssh_connection:event(),
State::term()) -> {ok, State::term()} |
{stop, ChannelId::ssh:channel_id(),
@@ -216,8 +213,6 @@
call(ChannelPid, Msg, infinity).
-doc """
-call(ChannelRef, Msg, Timeout) -> Reply | {error, Reason}
-
Makes a synchronous call to the channel process by sending a message and waiting
until a reply arrives, or a time-out occurs. The channel calls
[Module:handle_call/3](`c:handle_call/3`) to handle the message. If the channel
@@ -248,8 +243,6 @@
end.
-doc """
-cast(ChannelRef, Msg) -> ok
-
Sends an asynchronous message to the channel process and returns ok immediately,
ignoring if the destination node or channel process does not exist. The channel
calls [Module:handle_cast/2](`c:handle_cast/2`) to handle the message.
@@ -263,8 +256,6 @@
-opaque client() :: term().
-doc """
-reply(Client, Reply) -> \_
-
This function can be used by a channel to send a reply to a client that called
`call/[2,3]` when the reply cannot be defined in the return value of
[Module:handle_call/3](`c:handle_call/3`).
@@ -307,9 +298,6 @@
gen_server:start(?MODULE, [Options], []).
-doc """
-start_link(SshConnection, ChannelId, ChannelCb, CbInitArgs) -> {ok, ChannelRef}
-| {error, Reason}
-
Starts a process that handles an SSH channel. It is called internally, by the
`ssh` daemon, or explicitly by the `ssh` client implementations. The behavior
sets the `trap_exit` flag to `true`.
@@ -336,10 +324,10 @@
gen_server:start_link(?MODULE, [Options], []).
-doc """
-enter*loop(State) -> *
-
Makes an existing process an `ssh_client_channel` (replaces ssh_channel)
-process. Does not return, instead the calling process enters the
+process.
+
+Does not return, instead the calling process enters the
`ssh_client_channel` (replaces ssh_channel) process receive loop and become an
`ssh_client_channel` process. The process must have been started using one of
the start functions in `proc_lib`, see the `m:proc_lib` manual page in STDLIB.
@@ -363,7 +351,7 @@
%% Description: Initiates the server
%%--------------------------------------------------------------------
-doc """
-init(Options) -> {ok, State} | {ok, State, Timeout} | {stop, Reason}
+Initiates a client channel.
The following options must be present:
@@ -372,10 +360,10 @@
- **`{init_args(), list()}`** - The list of arguments to the `init` function of
the callback module.
-- **`{cm, ``t:ssh:connection_ref/0``}`** - Reference to the `ssh` connection as
+- **`{cm,` `t:ssh:connection_ref/0` `}`** - Reference to the `ssh` connection as
returned by `ssh:connect/3`.
-- **`{channel_id, ``t:ssh:channel_id/0``}`** - Id of the `ssh` channel as
+- **`{channel_id,` `t:ssh:channel_id/0` `}`** - Id of the `ssh` channel as
returned by
[ssh_connection:session_channel/2,4](`ssh_connection:session_channel/2`).
diff -ruN a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
--- a/lib/ssh/src/ssh_connection.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_connection.erl 2025-12-17 17:25:02.075556404 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -29,29 +29,31 @@
This module provides API functions to send SSH Connection Protocol events to the
other side of an SSH channel.
-The [SSH Connection Protocol](http://www.ietf.org/rfc/rfc4254.txt) is used by
+The [SSH Connection Protocol (RFC 4254)](http://www.ietf.org/rfc/rfc4254.txt) is used by
clients and servers, that is, SSH channels, to communicate over the SSH
connection. The API functions in this module send SSH Connection Protocol
events, which are received as messages by the remote channel handling the remote
channel. The Erlang format of thoose messages is (see also
[below](`t:event/0`)):
-`{ssh_cm, ``t:ssh:connection_ref/0``, ``t:channel_msg/0``}`
+`{ssh_cm,` `t:ssh:connection_ref/0` `,` `t:channel_msg/0` `}`
If the `m:ssh_client_channel` behavior is used to implement the channel process,
these messages are handled by
[handle_ssh_msg/2](`c:ssh_client_channel:handle_ssh_msg/2`).
""".
-moduledoc(#{titles =>
- [{type,<<"SSH Connection Protocol: General">>},
- {type,<<"Data Transfer (RFC 4254, section 5.2)">>},
- {type,<<"Closing a Channel (RFC 4254, section 5.3)">>},
- {type,<<"Requesting a Pseudo-Terminal (RFC 4254, section 6.2)">>},
- {type,<<"Environment Variable Passing (RFC 4254, section 6.4)">>},
- {type,<<"Starting a Shell or Command (RFC 4254, section 6.5)">>},
- {type,<<"Window Dimension Change Message (RFC 4254, section 6.7)">>},
- {type,<<"Signals (RFC 4254, section 6.9)">>},
- {type,<<"Returning Exit Status (RFC 4254, section 6.10)">>}]}).
+ [{type,<<"General">>},
+ {type,<<"Data Transfer">>},
+ {type,<<"Closing a Channel">>},
+ {type,<<"Pseudo-Terminal">>},
+ {type,<<"Environment Variable">>},
+ {type,<<"Shell or Command">>},
+ {type,<<"Window Change">>},
+ {type,<<"Signals">>},
+ {type,<<"Exit Status">>}]}).
+
+-include_lib("kernel/include/logger.hrl").
-include("ssh.hrl").
-include("ssh_connect.hrl").
@@ -122,7 +124,7 @@
""".
-type reason() :: closed | timeout .
--doc(#{equiv => reason/0}).
+-doc(#{}).
-type result() :: req_status() | {error, reason()} .
-doc """
@@ -150,8 +152,7 @@
exec_ch_msg/0
]).
--doc(#{title => <<"SSH Connection Protocol: General">>,
- equiv => channel_msg/0}).
+-doc(#{title => <<"General">>}).
-type event() :: {ssh_cm, ssh:connection_ref(), channel_msg()}.
-doc """
As mentioned in the introduction, the
@@ -160,7 +161,7 @@
support by the `m:ssh_client_channel` behavior the process must handle thoose
messages.
""".
--doc(#{title => <<"SSH Connection Protocol: General">>}).
+-doc(#{title => <<"General">>}).
-type channel_msg() :: data_ch_msg()
| eof_ch_msg()
| closed_ch_msg()
@@ -179,14 +180,15 @@
[ssh_connection:reply_request/4](`reply_request/4`) with the boolean value of
`WantReply` as the second argument.
""".
--doc(#{title => <<"SSH Connection Protocol: General">>}).
+-doc(#{title => <<"General">>}).
-type want_reply() :: boolean().
-doc """
Data has arrived on the channel. This event is sent as a result of calling
[ssh_connection:send/3,4,5](`send/3`).
""".
--doc(#{title => <<"Data Transfer (RFC 4254, section 5.2)">>}).
+-doc(#{title => <<"Data Transfer">>,
+ rfc => ~"RFC 4254, section 5.2"}).
-type data_ch_msg() :: {data,
ssh:channel_id(),
ssh_data_type_code(),
@@ -196,7 +198,8 @@
Indicates that the other side sends no more data. This event is sent as a result
of calling [ssh_connection:send_eof/2](`send_eof/2`).
""".
--doc(#{title => <<"Closing a Channel (RFC 4254, section 5.3)">>}).
+-doc(#{title => <<"Closing a Channel">>,
+ rfc => ~"RFC 4254, section 5.3"}).
-type eof_ch_msg() :: {eof,
ssh:channel_id()
} .
@@ -207,7 +210,8 @@
signals referred to are on OS-level and not something generated by an Erlang
program.
""".
--doc(#{title => <<"Signals (RFC 4254, section 6.9)">>}).
+-doc(#{title => <<"Signals">>,
+ rfc => ~"RFC 4254, section 6.9"}).
-type signal_ch_msg() :: {signal,
ssh:channel_id(),
SignalName :: string()
@@ -218,7 +222,8 @@
[RFC 4254](https://tools.ietf.org/html/rfc4254#section-6.10) Section 6.10, which
shows a special case of these signals.
""".
--doc(#{title => <<"Returning Exit Status (RFC 4254, section 6.10)">>}).
+-doc(#{title => <<"Exit Status">>,
+ rfc => ~"RFC 4254, section 6.10"}).
-type exit_signal_ch_msg() :: {exit_signal, ssh:channel_id(),
ExitSignal :: string(),
ErrorMsg :: string(),
@@ -229,7 +234,8 @@
means that the command terminated successfully. This event is sent as a result
of calling [ssh_connection:exit_status/3](`exit_status/3`).
""".
--doc(#{title => <<"Returning Exit Status (RFC 4254, section 6.10)">>}).
+-doc(#{title => <<"Exit Status">>,
+ rfc => ~"RFC 4254, section 6.10"}).
-type exit_status_ch_msg() :: {exit_status,
ssh:channel_id(),
ExitStatus :: non_neg_integer()
@@ -239,7 +245,8 @@
Both the handling of this event and sending it are taken care of by the
`m:ssh_client_channel` behavior.
""".
--doc(#{title => <<"Closing a Channel (RFC 4254, section 5.3)">>}).
+-doc(#{title => <<"Closing a Channel">>,
+ rfc => ~"RFC 4254, section 5.3"}).
-type closed_ch_msg() :: {closed,
ssh:channel_id()
} .
@@ -247,15 +254,16 @@
Environment variables can be passed to the shell/command to be started later.
This event is sent as a result of calling [ssh_connection:setenv/5](`setenv/5`).
""".
--doc(#{title => <<"Environment Variable Passing (RFC 4254, section 6.4)">>}).
+-doc(#{title => <<"Environment Variable">>,
+ rfc => ~"RFC 4254, section 6.4"}).
-type env_ch_msg() :: {env,
ssh:channel_id(),
want_reply(),
Var :: string(),
Value :: string()
} .
--doc(#{title => <<"Requesting a Pseudo-Terminal (RFC 4254, section 6.2)">>,
- equiv => term_mode/0}).
+-doc(#{title => <<"Pseudo-Terminal">>,
+ rfc => ~"RFC 4254, section 6.2"}).
-type pty_ch_msg() :: {pty,
ssh:channel_id(),
want_reply(),
@@ -280,7 +288,8 @@
`OP code: 53, mnemonic name ECHO erlang atom: echo`. This event is sent as a
result of calling [ssh_connection:ptty_alloc/4](`ptty_alloc/4`).
""".
--doc(#{title => <<"Requesting a Pseudo-Terminal (RFC 4254, section 6.2)">>}).
+-doc(#{title => <<"Pseudo-Terminal">>,
+ rfc => ~"RFC 4254, section 6.2"}).
-type term_mode() :: {Opcode :: atom() | byte(),
Value :: non_neg_integer()} .
@@ -288,7 +297,8 @@
This message requests that the user default shell is started at the other end.
This event is sent as a result of calling [ssh_connection:shell/2](`shell/2`).
""".
--doc(#{title => <<"Starting a Shell or Command (RFC 4254, section 6.5)">>}).
+-doc(#{title => <<"Shell or Command">>,
+ rfc => ~"RFC 4254, section 6.5"}).
-type shell_ch_msg() :: {shell,
ssh:channel_id(),
want_reply()
@@ -298,7 +308,8 @@
message to the server side to inform it of the new dimensions. No API function
generates this event.
""".
--doc(#{title => <<"Window Dimension Change Message (RFC 4254, section 6.7)">>}).
+-doc(#{title => <<"Window Change">>,
+ rfc => ~"RFC 4254, section 6.7"}).
-type window_change_ch_msg() :: {window_change,
ssh:channel_id(),
CharWidth :: non_neg_integer(),
@@ -310,7 +321,8 @@
This message requests that the server starts execution of the given command.
This event is sent as a result of calling [ssh_connection:exec/4 ](`exec/4`).
""".
--doc(#{title => <<"Starting a Shell or Command (RFC 4254, section 6.5)">>}).
+-doc(#{title => <<"Shell or Command">>,
+ rfc => ~"RFC 4254, section 6.5"}).
-type exec_ch_msg() :: {exec,
ssh:channel_id(),
want_reply(),
@@ -340,8 +352,8 @@
Timeout :: timeout(),
Result :: {ok, ssh:channel_id()} | {error, reason()} .
-session_channel(ConnectionHandler, Timeout) ->
- session_channel(ConnectionHandler, undefined, undefined, Timeout).
+session_channel(ConnectionRef, Timeout) ->
+ session_channel(ConnectionRef, undefined, undefined, Timeout).
-doc """
@@ -355,8 +367,8 @@
Timeout :: timeout(),
Result :: {ok, ssh:channel_id()} | {error, reason()} .
-session_channel(ConnectionHandler, InitialWindowSize, MaxPacketSize, Timeout) ->
- open_channel(ConnectionHandler, "session", <<>>,
+session_channel(ConnectionRef, InitialWindowSize, MaxPacketSize, Timeout) ->
+ open_channel(ConnectionRef, "session", <<>>,
InitialWindowSize,
MaxPacketSize,
Timeout).
@@ -365,11 +377,11 @@
%% Description: Opens a channel for the given type.
%% --------------------------------------------------------------------
-doc false.
-open_channel(ConnectionHandler, Type, ChanData, Timeout) ->
- open_channel(ConnectionHandler, Type, ChanData, undefined, undefined, Timeout).
+open_channel(ConnectionRef, Type, ChanData, Timeout) ->
+ open_channel(ConnectionRef, Type, ChanData, undefined, undefined, Timeout).
-open_channel(ConnectionHandler, Type, ChanData, InitialWindowSize, MaxPacketSize, Timeout) ->
- case ssh_connection_handler:open_channel(ConnectionHandler, Type, ChanData,
+open_channel(ConnectionRef, Type, ChanData, InitialWindowSize, MaxPacketSize, Timeout) ->
+ case ssh_connection_handler:open_channel(ConnectionRef, Type, ChanData,
InitialWindowSize, MaxPacketSize,
Timeout) of
{open, Channel} ->
@@ -414,8 +426,8 @@
Command :: string(),
Timeout :: timeout().
-exec(ConnectionHandler, ChannelId, Command, TimeOut) ->
- ssh_connection_handler:request(ConnectionHandler, self(), ChannelId, "exec",
+exec(ConnectionRef, ChannelId, Command, TimeOut) ->
+ ssh_connection_handler:request(ConnectionRef, self(), ChannelId, "exec",
true, [?string(Command)], TimeOut).
%%--------------------------------------------------------------------
@@ -437,8 +449,8 @@
ChannelId :: ssh:channel_id(),
Result :: ok | success | failure | {error, timeout} .
-shell(ConnectionHandler, ChannelId) ->
- ssh_connection_handler:request(ConnectionHandler, self(), ChannelId,
+shell(ConnectionRef, ChannelId) ->
+ ssh_connection_handler:request(ConnectionRef, self(), ChannelId,
"shell", false, <<>>, 0).
%%--------------------------------------------------------------------
%%
@@ -457,8 +469,8 @@
Subsystem :: string(),
Timeout :: timeout().
-subsystem(ConnectionHandler, ChannelId, SubSystem, TimeOut) ->
- ssh_connection_handler:request(ConnectionHandler, self(),
+subsystem(ConnectionRef, ChannelId, SubSystem, TimeOut) ->
+ ssh_connection_handler:request(ConnectionRef, self(),
ChannelId, "subsystem",
true, [?string(SubSystem)], TimeOut).
%%--------------------------------------------------------------------
@@ -466,51 +478,51 @@
%%--------------------------------------------------------------------
-doc(#{equiv => send/5}).
-spec send(connection_ref(), channel_id(), iodata()) ->
- ok | {error, timeout | closed}.
+ ok | {error, reason()}.
-send(ConnectionHandler, ChannelId, Data) ->
- send(ConnectionHandler, ChannelId, 0, Data, infinity).
+send(ConnectionRef, ChannelId, Data) ->
+ send(ConnectionRef, ChannelId, 0, Data, infinity).
-doc """
send(ConnectionRef, ChannelId, Type, Data)
+Depending on input arguments equivalent to one of `send/5` calls specified below.
+
Equivalent to [send(ConnectionRef, ChannelId, 0, Data, TimeOut)](`send/5`) if
called with TimeOut being integer.
Equivalent to [send(ConnectionRef, ChannelId, 0, Data, infinity)](`send/5`) if
called with TimeOut being infinity atom.
-Equivalent to [send(ConnectionHandler, ChannelId, Type, Data, infinity)](`send/5`) if
+Equivalent to [send(ConnectionRef, ChannelId, Type, Data, infinity)](`send/5`) if
called with last argument which is not integer or infinity atom.
""".
--spec send(connection_ref(), channel_id(), iodata(), timeout()) -> ok | {error, reason()};
- (connection_ref(), channel_id(), ssh_data_type_code(), iodata()) -> ok | {error, reason()}.
+-spec send(connection_ref(), channel_id(), iodata(), timeout()) -> ok | {error, reason()};
+ (connection_ref(), channel_id(), ssh_data_type_code(), iodata()) -> ok | {error, reason()}.
-send(ConnectionHandler, ChannelId, Data, TimeOut) when is_integer(TimeOut) ->
- send(ConnectionHandler, ChannelId, 0, Data, TimeOut);
+send(ConnectionRef, ChannelId, Data, TimeOut) when is_integer(TimeOut) ->
+ send(ConnectionRef, ChannelId, 0, Data, TimeOut);
-send(ConnectionHandler, ChannelId, Data, infinity) ->
- send(ConnectionHandler, ChannelId, 0, Data, infinity);
+send(ConnectionRef, ChannelId, Data, infinity) ->
+ send(ConnectionRef, ChannelId, 0, Data, infinity);
-send(ConnectionHandler, ChannelId, Type, Data) ->
- send(ConnectionHandler, ChannelId, Type, Data, infinity).
+send(ConnectionRef, ChannelId, Type, Data) ->
+ send(ConnectionRef, ChannelId, Type, Data, infinity).
-doc """
-send(ConnectionRef, ChannelId, Type, Data, TimeOut) -> ok | Error
-
Is to be called by client- and server-channel processes to send data to each
other.
The function `subsystem/4` and subsequent calls of `send/3,4,5` must be executed
in the same process.
""".
--spec send(connection_ref(), channel_id(), ssh_data_type_code(), iodata(), timeout()) -> ok | {error, reason()}.
+-spec send(connection_ref(), channel_id(), ssh_data_type_code(), iodata(), timeout()) -> ok | {error, reason()}.
-send(ConnectionHandler, ChannelId, Type, Data, TimeOut) ->
- ssh_connection_handler:send(ConnectionHandler, ChannelId,
+send(ConnectionRef, ChannelId, Type, Data, TimeOut) ->
+ ssh_connection_handler:send(ConnectionRef, ChannelId,
Type, Data, TimeOut).
%%--------------------------------------------------------------------
-doc "Sends EOF on channel `ChannelId`.".
@@ -521,8 +533,8 @@
%%
%% Description: Sends eof on the channel <ChannelId>.
%%--------------------------------------------------------------------
-send_eof(ConnectionHandler, Channel) ->
- ssh_connection_handler:send_eof(ConnectionHandler, Channel).
+send_eof(ConnectionRef, Channel) ->
+ ssh_connection_handler:send_eof(ConnectionRef, Channel).
%%--------------------------------------------------------------------
-doc """
@@ -545,8 +557,8 @@
%%
%% Description: Adjusts the ssh flowcontrol window.
%%--------------------------------------------------------------------
-adjust_window(ConnectionHandler, Channel, Bytes) ->
- ssh_connection_handler:adjust_window(ConnectionHandler, Channel, Bytes).
+adjust_window(ConnectionRef, Channel, Bytes) ->
+ ssh_connection_handler:adjust_window(ConnectionRef, Channel, Bytes).
%%--------------------------------------------------------------------
-doc """
@@ -563,11 +575,11 @@
%%
%% Description: Environment variables may be passed to the shell/command to be
%% started later.
-setenv(ConnectionHandler, ChannelId, Var, Value, TimeOut) ->
- setenv(ConnectionHandler, ChannelId, true, Var, Value, TimeOut).
+setenv(ConnectionRef, ChannelId, Var, Value, TimeOut) ->
+ setenv(ConnectionRef, ChannelId, true, Var, Value, TimeOut).
-setenv(ConnectionHandler, ChannelId, WantReply, Var, Value, TimeOut) ->
- case ssh_connection_handler:request(ConnectionHandler, ChannelId,
+setenv(ConnectionRef, ChannelId, WantReply, Var, Value, TimeOut) ->
+ case ssh_connection_handler:request(ConnectionRef, ChannelId,
"env", WantReply,
[?string(Var), ?string(Value)], TimeOut) of
ok when WantReply == false ->
@@ -593,8 +605,8 @@
%%
%% Description: Sends a close message on the channel <ChannelId>.
%%--------------------------------------------------------------------
-close(ConnectionHandler, ChannelId) ->
- ssh_connection_handler:close(ConnectionHandler, ChannelId).
+close(ConnectionRef, ChannelId) ->
+ ssh_connection_handler:close(ConnectionRef, ChannelId).
%%--------------------------------------------------------------------
-doc """
@@ -612,8 +624,8 @@
%%
%% Description: Send status replies to requests that want such replies.
%%--------------------------------------------------------------------
-reply_request(ConnectionHandler, true, Status, ChannelId) ->
- ssh_connection_handler:reply_request(ConnectionHandler, Status, ChannelId);
+reply_request(ConnectionRef, true, Status, ChannelId) ->
+ ssh_connection_handler:reply_request(ConnectionRef, Status, ChannelId);
reply_request(_,false, _, _) ->
ok.
@@ -627,8 +639,8 @@
ChannelId :: ssh:channel_id(),
Options :: proplists:proplist().
-ptty_alloc(ConnectionHandler, Channel, Options) ->
- ptty_alloc(ConnectionHandler, Channel, Options, infinity).
+ptty_alloc(ConnectionRef, Channel, Options) ->
+ ptty_alloc(ConnectionRef, Channel, Options, infinity).
-doc """
@@ -659,11 +671,11 @@
Options :: proplists:proplist(),
Timeout :: timeout().
-ptty_alloc(ConnectionHandler, Channel, Options0, TimeOut) ->
+ptty_alloc(ConnectionRef, Channel, Options0, TimeOut) ->
TermData = backwards_compatible(Options0, []), % FIXME
{Width, PixWidth} = pty_default_dimensions(width, TermData),
{Height, PixHeight} = pty_default_dimensions(height, TermData),
- pty_req(ConnectionHandler, Channel,
+ pty_req(ConnectionRef, Channel,
proplists:get_value(term, TermData, os:getenv("TERM", ?DEFAULT_TERMINAL)),
proplists:get_value(width, TermData, Width),
proplists:get_value(height, TermData, Height),
@@ -678,19 +690,19 @@
%% Should they be documented and tested?
%%--------------------------------------------------------------------
-doc false.
-window_change(ConnectionHandler, Channel, Width, Height) ->
- window_change(ConnectionHandler, Channel, Width, Height, 0, 0).
+window_change(ConnectionRef, Channel, Width, Height) ->
+ window_change(ConnectionRef, Channel, Width, Height, 0, 0).
-doc false.
-window_change(ConnectionHandler, Channel, Width, Height,
+window_change(ConnectionRef, Channel, Width, Height,
PixWidth, PixHeight) ->
- ssh_connection_handler:request(ConnectionHandler, Channel,
+ ssh_connection_handler:request(ConnectionRef, Channel,
"window-change", false,
[?uint32(Width), ?uint32(Height),
?uint32(PixWidth), ?uint32(PixHeight)], 0).
-doc false.
-signal(ConnectionHandler, Channel, Sig) ->
- ssh_connection_handler:request(ConnectionHandler, Channel,
+signal(ConnectionRef, Channel, Sig) ->
+ ssh_connection_handler:request(ConnectionRef, Channel,
"signal", false, [?string(Sig)], 0).
@@ -702,8 +714,8 @@
ConnectionRef :: ssh:connection_ref(),
ChannelId :: ssh:channel_id(),
Status :: integer().
-exit_status(ConnectionHandler, Channel, Status) ->
- ssh_connection_handler:request(ConnectionHandler, Channel,
+exit_status(ConnectionRef, Channel, Status) ->
+ ssh_connection_handler:request(ConnectionRef, Channel,
"exit-status", false, [?uint32(Status)], 0).
%%--------------------------------------------------------------------
@@ -746,23 +758,50 @@
%%%
-doc false.
+handle_msg(#ssh_msg_disconnect{code = Code, description = Description}, Connection, _, _SSH) ->
+ {disconnect, {Code, Description}, handle_stop(Connection)};
+
+handle_msg(Msg, Connection, server, Ssh = #ssh{authenticated = false}) ->
+ %% See RFC4252 6.
+ %% Message numbers of 80 and higher are reserved for protocols running
+ %% after this authentication protocol, so receiving one of them before
+ %% authentication is complete is an error, to which the server MUST
+ %% respond by disconnecting, preferably with a proper disconnect message
+ %% sent to ease troubleshooting.
+ MsgFun = fun(M) ->
+ io_lib:format("Connection terminated. Unexpected message for unauthenticated user."
+ " Message: ~w", [M],
+ [{chars_limit, ssh_lib:max_log_len(Ssh)}])
+ end,
+ ?LOG_DEBUG(MsgFun, [Msg]),
+ {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR, "Connection refused"}, handle_stop(Connection)};
+
handle_msg(#ssh_msg_channel_open_confirmation{recipient_channel = ChannelId,
sender_channel = RemoteId,
initial_window_size = WindowSz,
maximum_packet_size = PacketSz},
#connection{channel_cache = Cache} = Connection0, _, _SSH) ->
- #channel{remote_id = undefined} = Channel =
+ #channel{remote_id = undefined, user = U} = Channel =
ssh_client_channel:cache_lookup(Cache, ChannelId),
- ssh_client_channel:cache_update(Cache, Channel#channel{
- remote_id = RemoteId,
- recv_packet_size = max(32768, % rfc4254/5.2
- min(PacketSz, Channel#channel.recv_packet_size)
- ),
- send_window_size = WindowSz,
- send_packet_size = PacketSz}),
- reply_msg(Channel, Connection0, {open, ChannelId});
+ if U /= undefined ->
+ ssh_client_channel:cache_update(Cache, Channel#channel{
+ remote_id = RemoteId,
+ recv_packet_size = max(32768, % rfc4254/5.2
+ min(PacketSz, Channel#channel.recv_packet_size)
+ ),
+ send_window_size = WindowSz,
+ send_packet_size = PacketSz}),
+ reply_msg(Channel, Connection0, {open, ChannelId});
+ true ->
+ %% There is no user process so nobody cares about the channel
+ %% close it and remove from the cache, reply from the peer will be
+ %% ignored
+ CloseMsg = channel_close_msg(RemoteId),
+ ssh_client_channel:cache_delete(Cache, ChannelId),
+ {[{connection_reply, CloseMsg}], Connection0}
+ end;
handle_msg(#ssh_msg_channel_open_failure{recipient_channel = ChannelId,
reason = Reason,
@@ -811,6 +850,10 @@
{Replies, Connection};
undefined ->
+ %% This may happen among other reasons
+ %% - we sent 'channel-close' %% and the peer failed to respond in time
+ %% - we tried to open a channel but the handler died prematurely
+ %% and the channel entry was removed from the cache
{[], Connection0}
end;
@@ -1026,14 +1069,24 @@
?DEC_BIN(Err, _ErrLen),
?DEC_BIN(Lang, _LangLen)>> = Data,
case ssh_client_channel:cache_lookup(Cache, ChannelId) of
- #channel{remote_id = RemoteId} = Channel ->
+ #channel{remote_id = RemoteId, sent_close = SentClose} = Channel ->
{Reply, Connection} = reply_msg(Channel, Connection0,
{exit_signal, ChannelId,
binary_to_list(SigName),
binary_to_list(Err),
binary_to_list(Lang)}),
- ChannelCloseMsg = channel_close_msg(RemoteId),
- {[{connection_reply, ChannelCloseMsg}|Reply], Connection};
+ %% Send 'channel-close' only if it has not been sent yet
+ %% by e.g. our side also closing the channel or going down
+ %% and(!) update the cache
+ %% so that the 'channel-close' is not sent twice
+ if not SentClose ->
+ CloseMsg = channel_close_msg(RemoteId),
+ ssh_client_channel:cache_update(Cache,
+ Channel#channel{sent_close = true}),
+ {[{connection_reply, CloseMsg}|Reply], Connection};
+ true ->
+ {Reply, Connection}
+ end;
_ ->
%% Channel already closed by peer
{[], Connection0}
@@ -1250,12 +1303,7 @@
#connection{requests = [{_, From, Fun} | Rest]} = Connection0, _, _SSH) ->
Connection = Fun({success,Data}, Connection0),
{[{channel_request_reply, From, {success, Data}}],
- Connection#connection{requests = Rest}};
-
-handle_msg(#ssh_msg_disconnect{code = Code,
- description = Description},
- Connection, _, _SSH) ->
- {disconnect, {Code, Description}, handle_stop(Connection)}.
+ Connection#connection{requests = Rest}}.
%%%----------------------------------------------------------------
@@ -1555,9 +1603,9 @@
%%% Pseudo terminal stuff
%%%
-pty_req(ConnectionHandler, Channel, Term, Width, Height,
+pty_req(ConnectionRef, Channel, Term, Width, Height,
PixWidth, PixHeight, PtyOpts, TimeOut) ->
- ssh_connection_handler:request(ConnectionHandler,
+ ssh_connection_handler:request(ConnectionRef,
Channel, "pty-req", true,
[?string(Term),
?uint32(Width), ?uint32(Height),
@@ -1825,7 +1873,6 @@
WantedSize = Size - byte_size(Data),
ssh_client_channel:cache_update(Connection#connection.channel_cache,
Channel#channel{recv_window_size = WantedSize}),
- adjust_window(self(), ChannelId, byte_size(Data)),
reply_msg(Channel, Connection, {data, ChannelId, DataType, Data});
undefined ->
{[], Connection}
@@ -1867,14 +1914,14 @@
%%%----------------------------------------------------------------
-doc false.
-send_environment_vars(ConnectionHandler, Channel, VarNames) ->
+send_environment_vars(ConnectionRef, Channel, VarNames) ->
lists:foldl(
fun(Var, success) ->
case os:getenv(Var) of
false ->
success;
Value ->
- setenv(ConnectionHandler, Channel, false,
+ setenv(ConnectionRef, Channel, false,
Var, Value, infinity)
end
end, success, VarNames).
diff -ruN a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
--- a/lib/ssh/src/ssh_connection_handler.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_connection_handler.erl 2025-12-17 17:25:02.075556404 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -35,7 +35,6 @@
-include("ssh_transport.hrl").
-include("ssh_auth.hrl").
-include("ssh_connect.hrl").
-
-include("ssh_fsm.hrl").
%%====================================================================
@@ -614,17 +613,18 @@
handle_event(internal, socket_ready, {hello,_}=StateName, #data{ssh_params = Ssh0} = D) ->
VsnMsg = ssh_transport:hello_version_msg(string_version(Ssh0)),
send_bytes(VsnMsg, D),
- case inet:getopts(Socket=D#data.socket, [recbuf]) of
- {ok, [{recbuf,Size}]} ->
+ case inet:getopts(Socket=D#data.socket, [buffer]) of
+ {ok, [{buffer,Size}]} ->
%% Set the socket to the hello text line handling mode:
inet:setopts(Socket, [{packet, line},
{active, once},
% Expecting the version string which might
% be max ?MAX_PROTO_VERSION bytes:
- {recbuf, ?MAX_PROTO_VERSION},
+ {buffer, ?MAX_PROTO_VERSION},
+ {packet_size, ?MAX_PROTO_VERSION},
{nodelay,true}]),
Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts, infinity),
- {keep_state, D#data{inet_initial_recbuf_size=Size}, [{state_timeout,Time,no_hello_received}] };
+ {keep_state, D#data{inet_initial_buffer_size=Size}, [{state_timeout,Time,no_hello_received}] };
Other ->
?call_disconnectfun_and_log_cond("Option return",
@@ -653,11 +653,12 @@
case handle_version(NumVsn, StrVsn, D0#data.ssh_params) of
{ok, Ssh1} ->
%% Since the hello part is finished correctly, we set the
- %% socket to the packet handling mode (including recbuf size):
+ %% socket to the packet handling mode (including buffer size):
inet:setopts(D0#data.socket, [{packet,0},
{mode,binary},
{active, once},
- {recbuf, D0#data.inet_initial_recbuf_size}]),
+ {buffer, D0#data.inet_initial_buffer_size},
+ {packet_size, 0}]),
{KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh1),
send_bytes(SshPacket, D0),
D = D0#data{ssh_params = Ssh,
@@ -675,17 +676,24 @@
%%% timeout after tcp:connect but then nothing arrives
handle_event(state_timeout, no_hello_received, {hello,_Role}=StateName, D0 = #data{ssh_params = Ssh0}) ->
- Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts),
+ MsgFun =
+ fun (debug) ->
+ Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts),
+ lists:concat(["No HELLO received within ",ssh_lib:format_time_ms(Time)]);
+ (_) ->
+ ["No HELLO received within hello_timeout"]
+ end,
{Shutdown, D} =
- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
- lists:concat(["No HELLO received within ",ssh_lib:format_time_ms(Time)]),
- StateName, D0),
+ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun), StateName, D0),
{stop, Shutdown, D};
-%%% ######## {service_request, client|server} ####
-
-handle_event(internal, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D0) ->
+%%% ######## {service_request, client|server} #### StateName ==
+%% {userauth,server} guard added due to interoperability with clients
+%% sending extra ssh_msg_service_request (e.g. Paramiko for Python,
+%% see GH-6463)
+handle_event(internal, Msg = #ssh_msg_service_request{name=ServiceName}, StateName, D0)
+ when StateName == {service_request,server}; StateName == {userauth,server} ->
case ServiceName of
"ssh-userauth" ->
Ssh0 = #ssh{session_id=SessionId} = D0#data.ssh_params,
@@ -728,16 +736,6 @@
disconnect_fun("Received disconnect: "++Desc, D),
{stop_and_reply, {shutdown,Desc}, Actions, D};
-handle_event(internal, #ssh_msg_ignore{}, {_StateName, _Role, init},
- #data{ssh_params = #ssh{kex_strict_negotiated = true,
- send_sequence = SendSeq,
- recv_sequence = RecvSeq}}) ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("strict KEX violation: unexpected SSH_MSG_IGNORE "
- "send_sequence = ~p recv_sequence = ~p",
- [SendSeq, RecvSeq])
- );
-
handle_event(internal, #ssh_msg_ignore{}, _StateName, _) ->
keep_state_and_data;
@@ -1096,12 +1094,22 @@
handle_event({call,From}, {close, ChannelId}, StateName, D0)
when ?CONNECTED(StateName) ->
+ %% Send 'channel-close' only if it has not been sent yet
+ %% e.g. when 'exit-signal' was received from the peer
+ %% and(!) we update the cache so that we remember what we've done
case ssh_client_channel:cache_lookup(cache(D0), ChannelId) of
- #channel{remote_id = Id} = Channel ->
+ #channel{remote_id = Id, sent_close = false} = Channel ->
D1 = send_msg(ssh_connection:channel_close_msg(Id), D0),
- ssh_client_channel:cache_update(cache(D1), Channel#channel{sent_close = true}),
- {keep_state, D1, [cond_set_idle_timer(D1), {reply,From,ok}]};
- undefined ->
+ ssh_client_channel:cache_update(cache(D1),
+ Channel#channel{sent_close = true}),
+ {keep_state, D1, [cond_set_idle_timer(D1),
+ channel_close_timer(D1, Id),
+ {reply,From,ok}]};
+ _ ->
+ %% Here we match a channel which has already sent 'channel-close'
+ %% AND possible cases of 'broken cache' i.e. when a channel
+ %% disappeared from the cache, but has not been properly shut down
+ %% The latter would be a bug, but hard to chase
{keep_state_and_data, [{reply,From,ok}]}
end;
@@ -1141,11 +1149,14 @@
of
{packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} ->
D1 = D0#data{ssh_params =
- Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
- decrypted_data_buffer = <<>>,
- undecrypted_packet_length = undefined,
- aead_data = <<>>,
- encrypted_data_buffer = EncryptedDataRest},
+ Ssh1#ssh{recv_sequence =
+ ssh_transport:next_seqnum(StateName,
+ Ssh1#ssh.recv_sequence,
+ SshParams)},
+ decrypted_data_buffer = <<>>,
+ undecrypted_packet_length = undefined,
+ aead_data = <<>>,
+ encrypted_data_buffer = EncryptedDataRest},
try
ssh_message:decode(set_kex_overload_prefix(DecryptedBytes,D1))
of
@@ -1177,12 +1188,21 @@
{next_event, internal, Msg}
]}
catch
- C:E:ST ->
- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts),
+ Class:Reason0:Stacktrace ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ MsgFun =
+ fun(debug) ->
+ io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p",
+ [Class,Reason,Stacktrace],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}]);
+ (_) ->
+ io_lib:format("Bad packet: Decrypted, but can't decode ~p:~p",
+ [Class, Reason],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}])
+ end,
{Shutdown, D} =
?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
- io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~P",
- [C,E,ST,MaxLogItemLen]),
+ ?SELECT_MSG(MsgFun),
StateName, D1),
{stop, Shutdown, D}
end;
@@ -1212,12 +1232,20 @@
StateName, D0),
{stop, Shutdown, D}
catch
- C:E:ST ->
- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts),
+ Class:Reason0:Stacktrace ->
+ MsgFun =
+ fun(debug) ->
+ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p",
+ [Class,Reason0,Stacktrace],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}]);
+ (_) ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p",
+ [Class,Reason],
+ [{chars_limit, ssh_lib:max_log_len(SshParams)}])
+ end,
{Shutdown, D} =
- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
- io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~P",
- [C,E,ST,MaxLogItemLen]),
+ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun),
StateName, D0),
{stop, Shutdown, D}
end;
@@ -1259,15 +1287,33 @@
%%% Handle that ssh channels user process goes down
handle_event(info, {'DOWN', _Ref, process, ChannelPid, _Reason}, _, D) ->
Cache = cache(D),
- ssh_client_channel:cache_foldl(
- fun(#channel{user=U,
- local_id=Id}, Acc) when U == ChannelPid ->
- ssh_client_channel:cache_delete(Cache, Id),
- Acc;
- (_,Acc) ->
- Acc
- end, [], Cache),
- {keep_state, D, cond_set_idle_timer(D)};
+ %% Here we first collect the list of channel id's handled by the process
+ %% Do NOT remove them from the cache - they are not closed yet!
+ Channels = ssh_client_channel:cache_foldl(
+ fun(#channel{user=U} = Channel, Acc) when U == ChannelPid ->
+ [Channel | Acc];
+ (_,Acc) ->
+ Acc
+ end, [], Cache),
+ %% Then for each channel where 'channel-close' has not been sent yet
+ %% we send 'channel-close' and(!) update the cache so that we remember
+ %% what we've done.
+ %% Also set user as 'undefined' as there is no such process anyway
+ {D2, NewTimers} = lists:foldl(
+ fun(#channel{remote_id = Id, sent_close = false} = Channel,
+ {D0, Timers}) when Id /= undefined ->
+ D1 = send_msg(ssh_connection:channel_close_msg(Id), D0),
+ ssh_client_channel:cache_update(cache(D1),
+ Channel#channel{sent_close = true,
+ user = undefined}),
+ ChannelTimer = channel_close_timer(D1, Id),
+ {D1, [ChannelTimer | Timers]};
+ (Channel, {D0, _} = Acc) ->
+ ssh_client_channel:cache_update(cache(D0),
+ Channel#channel{user = undefined}),
+ Acc
+ end, {D, []}, Channels),
+ {keep_state, D2, [cond_set_idle_timer(D2) | NewTimers]};
handle_event({timeout,idle_time}, _Data, _StateName, D) ->
case ssh_client_channel:cache_info(num_entries, cache(D)) of
@@ -1280,6 +1326,16 @@
handle_event({timeout,max_initial_idle_time}, _Data, _StateName, _D) ->
{stop, {shutdown, "Timeout"}};
+handle_event({timeout, {channel_close, ChannelId}}, _Data, _StateName, D) ->
+ Cache = cache(D),
+ case ssh_client_channel:cache_lookup(Cache, ChannelId) of
+ #channel{sent_close = true} ->
+ ssh_client_channel:cache_delete(Cache, ChannelId),
+ {keep_state, D, cond_set_idle_timer(D)};
+ _ ->
+ keep_state_and_data
+ end;
+
%%% So that terminate will be run when supervisor is shutdown
handle_event(info, {'EXIT', _Sup, Reason}, StateName, _D) ->
Role = ?role(StateName),
@@ -2052,6 +2108,10 @@
_ -> {{timeout,idle_time}, infinity, none}
end.
+channel_close_timer(D, ChannelId) ->
+ {{timeout, {channel_close, ChannelId}},
+ ?GET_OPT(channel_close_timeout, (D#data.ssh_params)#ssh.opts), none}.
+
%%%----------------------------------------------------------------
start_channel_request_timer(_,_, infinity) ->
ok;
diff -ruN a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
--- a/lib/ssh/src/ssh.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh.erl 2025-12-17 17:25:02.074556391 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -61,7 +61,7 @@
`m:ssh_client_channel`. For server channel handlers use `m:ssh_server_channel`
behaviour (replaces ssh_daemon_channel).
-Both clients and daemons accepts options that controls the exact behaviour. Some
+Both clients and daemons accept options that control the exact behaviour. Some
options are common to both. The three sets are called
[Client Options](`t:client_options/0`), [Daemon Options](`t:daemon_options/0`)
and [Common Options](`t:common_options/0`).
@@ -133,7 +133,7 @@
""".
-moduledoc(#{titles =>
[{type,<<"Client Options">>},
- {type,<<"Daemon Options (Server Options)">>},
+ {type,<<"Daemon Options">>},
{type,<<"Common Options">>},
{type,<<"Other data types">>}]}).
@@ -322,8 +322,6 @@
bad_arg(PortOrOptions, OptionsOrTimeout).
-doc """
-connect(Host, Port, Options, NegotiationTimeout) -> Result
-
Connects to an SSH server at the `Host` on `Port`.
As an alternative, an already open TCP socket could be passed to the function in
@@ -429,13 +427,13 @@
%%--------------------------------------------------------------------
%% Description: Retrieves information about a connection.
%%---------------------------------------------------------------------
--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}).
+-doc(#{title => <<"Other data types">>}).
-type version() :: {protocol_version(), software_version()}.
--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}).
+-doc(#{title => <<"Other data types">>}).
-type protocol_version() :: {Major::pos_integer(), Minor::non_neg_integer()}.
--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}).
+-doc(#{title => <<"Other data types">>}).
-type software_version() :: string().
--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}).
+-doc(#{title => <<"Other data types">>}).
-type conn_info_algs() :: [{kex, kex_alg()}
| {hkey, pubkey_alg()}
| {encrypt, cipher_alg()}
@@ -456,7 +454,7 @@
-doc(#{title => <<"Other data types">>}).
-type conn_info_channels() :: [proplists:proplist()].
--doc(#{title => <<"Other data types">>,equiv => conn_info_channels/0}).
+-doc(#{title => <<"Other data types">>}).
-type connection_info_tuple() ::
{client_version, version()}
| {server_version, version()}
@@ -466,15 +464,16 @@
| {options, client_options()}
| {algorithms, conn_info_algs()}
| {channels, conn_info_channels()}.
-
+
-doc(#{equiv => connection_info/2}).
-doc(#{since => <<"OTP 22.1">>}).
--spec connection_info(ConnectionRef) -> InfoTupleList when
+-spec connection_info(ConnectionRef) ->
+ InfoTupleList | {error, term()} when
ConnectionRef :: connection_ref(),
InfoTupleList :: [InfoTuple],
InfoTuple :: connection_info_tuple().
-connection_info(ConnectionRef) ->
+connection_info(ConnectionRef) ->
connection_info(ConnectionRef, []).
-doc """
@@ -482,7 +481,8 @@
When the `Key` is a single `Item`, the result is a single `InfoTuple`
""".
--spec connection_info(ConnectionRef, ItemList|Item) -> InfoTupleList|InfoTuple when
+-spec connection_info(ConnectionRef, ItemList|Item) ->
+ InfoTupleList | InfoTuple | {error, term()} when
ConnectionRef :: connection_ref(),
ItemList :: [Item],
Item :: client_version | server_version | user | peer | sockname | options | algorithms | sockname,
@@ -560,7 +560,7 @@
-doc """
-daemon(HostAddress, Port, Options) -> Result
+daemon(HostAddress, Port, Options)
Starts a server listening for SSH connections on the given port. If the `Port`
is 0, a random free port is selected. See `daemon_info/1` about how to find the
@@ -682,9 +682,13 @@
NewUserOptions :: daemon_options().
daemon_replace_options(DaemonRef, NewUserOptions) ->
- {ok,Os0} = ssh_system_sup:get_acceptor_options(DaemonRef),
- Os1 = ssh_options:merge_options(server, NewUserOptions, Os0),
- ssh_system_sup:replace_acceptor_options(DaemonRef, Os1).
+ case ssh_system_sup:get_acceptor_options(DaemonRef) of
+ {ok, Os0} ->
+ Os1 = ssh_options:merge_options(server, NewUserOptions, Os0),
+ ssh_system_sup:replace_acceptor_options(DaemonRef, Os1);
+ {error, _Reason} = Error ->
+ Error
+ end.
%%--------------------------------------------------------------------
-doc """
@@ -894,8 +898,6 @@
-doc """
-shell(Host, Port, Options) -> Result
-
Connects to an SSH server at `Host` and `Port` (defaults to 22) and starts an
interactive shell on that remote host.
diff -ruN a/lib/ssh/src/ssh_file.erl b/lib/ssh/src/ssh_file.erl
--- a/lib/ssh/src/ssh_file.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_file.erl 2025-12-17 17:25:02.075556404 +1030
@@ -170,7 +170,7 @@
""".
-moduledoc(#{since => "OTP 21.2",
titles =>
- [{type,<<"Options for the default ssh_file callback module">>}]}).
+ [{type,<<"Options">>}]}).
-include_lib("public_key/include/public_key.hrl").
-include_lib("kernel/include/file.hrl").
@@ -186,7 +186,7 @@
-export([host_key/2, is_auth_key/3]).
-export_type([system_dir_daemon_option/0]).
-doc "Sets the [system directory](`m:ssh_file#SYSDIR`).".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type system_dir_daemon_option() :: {system_dir, string()}.
%%%--------------------- client exports ---------------------------
@@ -199,7 +199,7 @@
Note that EdDSA passhrases (Curves 25519 and 448) are not implemented.
""".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type pubkey_passphrase_client_options() :: {dsa_pass_phrase, string()}
| {rsa_pass_phrase, string()}
%% Not yet implemented: | {ed25519_pass_phrase, string()}
@@ -218,16 +218,15 @@
]).
-doc "Sets the [user directory](`m:ssh_file#USERDIR`).".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type user_dir_common_option() :: {user_dir, string()}.
--doc(#{title => <<"Options for the default ssh_file callback module">>,
- equiv => user2dir/0}).
+-doc(#{title => <<"Options">>}).
-type user_dir_fun_common_option() :: {user_dir_fun, user2dir()}.
-doc """
Sets the [user directory](`m:ssh_file#USERDIR`) dynamically by evaluating the
`user2dir` function.
""".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type user2dir() :: fun((RemoteUserName::string()) -> UserDir :: string()) .
-doc """
@@ -239,17 +238,16 @@
call of ["ssh:connect/3](`ssh:connect/3`), `ssh:daemon/2` or similar function
call that initiates an ssh connection.
""".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type optimize_key_lookup() :: {optimize, time|space} .
-doc "The key representation".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type key() :: public_key:public_key() | public_key:private_key() .
--doc(#{title => <<"Options for the default ssh_file callback module">>,
- equiv => openssh_key_v1_attributes/0}).
+-doc(#{title => <<"Options">>}).
-type experimental_openssh_key_v1() :: [{key(), openssh_key_v1_attributes()}].
-doc "Types for the experimental implementaition of the `openssh_key_v1` format.".
--doc(#{title => <<"Options for the default ssh_file callback module">>}).
+-doc(#{title => <<"Options">>}).
-type openssh_key_v1_attributes() :: [{atom(),term()}].
%%%================================================================
diff -ruN a/lib/ssh/src/ssh_fsm.hrl b/lib/ssh/src/ssh_fsm.hrl
--- a/lib/ssh/src/ssh_fsm.hrl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_fsm.hrl 2025-12-17 17:25:02.076556417 +1030
@@ -37,7 +37,7 @@
| undefined,
last_size_rekey = 0 :: non_neg_integer(),
event_queue = [] :: list(),
- inet_initial_recbuf_size :: pos_integer()
+ inet_initial_buffer_size :: pos_integer()
| undefined
}).
diff -ruN a/lib/ssh/src/ssh_fsm_kexinit.erl b/lib/ssh/src/ssh_fsm_kexinit.erl
--- a/lib/ssh/src/ssh_fsm_kexinit.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_fsm_kexinit.erl 2025-12-17 17:25:02.076556417 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -44,6 +44,11 @@
-export([callback_mode/0, handle_event/4, terminate/3,
format_status/2, code_change/4]).
+-behaviour(ssh_dbg).
+-export([ssh_dbg_trace_points/0, ssh_dbg_flags/1,
+ ssh_dbg_on/1, ssh_dbg_off/1,
+ ssh_dbg_format/2]).
+
%%====================================================================
%% gen_statem callbacks
%%====================================================================
@@ -54,8 +59,13 @@
%%--------------------------------------------------------------------
-%%% ######## {kexinit, client|server, init|renegotiate} ####
+handle_event(Type, Event = prepare_next_packet, StateName, D) ->
+ ssh_connection_handler:handle_event(Type, Event, StateName, D);
+handle_event(Type, Event = {send_disconnect, _, _, _, _}, StateName, D) ->
+ ssh_connection_handler:handle_event(Type, Event, StateName, D);
+
+%%% ######## {kexinit, client|server, init|renegotiate} ####
handle_event(internal, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg},
D = #data{key_exchange_init_msg = OwnKex}) ->
Ssh1 = ssh_transport:key_init(peer_role(Role), D#data.ssh_params, Payload),
@@ -68,11 +78,10 @@
end,
{next_state, {key_exchange,Role,ReNeg}, D#data{ssh_params=Ssh}};
-
%%% ######## {key_exchange, client|server, init|renegotiate} ####
-
%%%---- diffie-hellman
handle_event(internal, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexdhReply, D),
{ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
@@ -82,6 +91,7 @@
{next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(NewKeys, D),
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
@@ -90,24 +100,28 @@
%%%---- diffie-hellman group exchange
handle_event(internal, #ssh_msg_kex_dh_gex_request{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(GexGroup, D),
Ssh = ssh_transport:parallell_gen_key(Ssh1),
{next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kex_dh_gex_request_old{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(GexGroup, D),
Ssh = ssh_transport:parallell_gen_key(Ssh1),
{next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexGexInit, D),
{next_state, {key_exchange_dh_gex_reply,client,ReNeg}, D#data{ssh_params=Ssh}};
%%%---- elliptic curve diffie-hellman
handle_event(internal, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexEcdhReply, D),
{ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
@@ -117,16 +131,25 @@
{next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
handle_event(internal, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(NewKeys, D),
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}};
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {key_exchange,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]));
%%% ######## {key_exchange_dh_gex_init, server, init|renegotiate} ####
-
handle_event(internal, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(KexGexReply, D),
{ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
@@ -134,20 +157,33 @@
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2),
ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
-
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {key_exchange_dh_gex_init,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]));
%%% ######## {key_exchange_dh_gex_reply, client, init|renegotiate} ####
-
handle_event(internal, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) ->
+ ok = check_kex_strict(Msg, D),
{ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params),
ssh_connection_handler:send_bytes(NewKeys, D),
{ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}};
-
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {key_exchange_dh_gex_reply,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]));
%%% ######## {new_keys, client|server} ####
-
%% First key exchange round:
handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D0) ->
{ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D0#data.ssh_params),
@@ -163,6 +199,15 @@
%% ssh_connection_handler:send_bytes(ExtInfo, D),
{next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}};
+%%% ######## handle KEX strict
+handle_event(internal, _Event, {new_keys,_Role,init},
+ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation (send_sequence = ~p recv_sequence = ~p)",
+ [SendSeq, RecvSeq]));
+
%% Subsequent key exchange rounds (renegotiation):
handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) ->
{ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params),
@@ -184,7 +229,6 @@
handle_event(internal, #ssh_msg_newkeys{}=Msg, {ext_info,_Role,renegotiate}, D) ->
{ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params),
{keep_state, D#data{ssh_params = Ssh}};
-
handle_event(internal, Msg, {ext_info,Role,init}, D) when is_tuple(Msg) ->
%% If something else arrives, goto next state and handle the event in that one
@@ -218,3 +262,70 @@
peer_role(client) -> server;
peer_role(server) -> client.
+check_kex_strict(Msg,
+ #data{ssh_params =
+ #ssh{algorithms =
+ #alg{
+ kex = Kex,
+ kex_strict_negotiated = KexStrictNegotiated},
+ send_sequence = SendSeq,
+ recv_sequence = RecvSeq}}) ->
+ case check_msg_group(Msg, get_alg_group(Kex), KexStrictNegotiated) of
+ ok ->
+ ok;
+ error ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
+ [SendSeq, RecvSeq]))
+ end.
+
+get_alg_group(Kex) when Kex == 'diffie-hellman-group16-sha512';
+ Kex == 'diffie-hellman-group18-sha512';
+ Kex == 'diffie-hellman-group14-sha256';
+ Kex == 'diffie-hellman-group14-sha1';
+ Kex == 'diffie-hellman-group1-sha1' ->
+ dh_alg;
+get_alg_group(Kex) when Kex == 'diffie-hellman-group-exchange-sha256';
+ Kex == 'diffie-hellman-group-exchange-sha1' ->
+ dh_gex_alg;
+get_alg_group(Kex) when Kex == 'curve25519-sha256';
+ Kex == 'curve25519-sha256@libssh.org';
+ Kex == 'curve448-sha512';
+ Kex == 'ecdh-sha2-nistp521';
+ Kex == 'ecdh-sha2-nistp384';
+ Kex == 'ecdh-sha2-nistp256' ->
+ ecdh_alg.
+
+check_msg_group(_Msg, _AlgGroup, false) -> ok;
+check_msg_group(#ssh_msg_kexdh_init{}, dh_alg, true) -> ok;
+check_msg_group(#ssh_msg_kexdh_reply{}, dh_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_request_old{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_request{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_group{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_init{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_dh_gex_reply{}, dh_gex_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_ecdh_init{}, ecdh_alg, true) -> ok;
+check_msg_group(#ssh_msg_kex_ecdh_reply{}, ecdh_alg, true) -> ok;
+check_msg_group(_Msg, _AlgGroup, _) -> error.
+
+%%%################################################################
+%%%#
+%%%# Tracing
+%%%#
+
+ssh_dbg_trace_points() -> [connection_events].
+
+ssh_dbg_flags(connection_events) -> [c].
+
+ssh_dbg_on(connection_events) -> dbg:tp(?MODULE, handle_event, 4, x).
+
+ssh_dbg_off(connection_events) -> dbg:ctpg(?MODULE, handle_event, 4).
+
+ssh_dbg_format(connection_events, {call, {?MODULE,handle_event, [EventType, EventContent, State, _Data]}}) ->
+ ["Connection event\n",
+ io_lib:format("[~w] EventType: ~p~nEventContent: ~p~nState: ~p~n", [?MODULE, EventType, EventContent, State])
+ ];
+ssh_dbg_format(connection_events, {return_from, {?MODULE,handle_event,4}, Ret}) ->
+ ["Connection event result\n",
+ io_lib:format("[~w] ~p~n", [?MODULE, ssh_dbg:reduce_state(Ret, #data{})])
+ ].
diff -ruN a/lib/ssh/src/ssh_fsm_userauth_client.erl b/lib/ssh/src/ssh_fsm_userauth_client.erl
--- a/lib/ssh/src/ssh_fsm_userauth_client.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_fsm_userauth_client.erl 2025-12-17 17:25:02.076556417 +1030
@@ -106,7 +106,10 @@
end;
%%---- banner to client
-handle_event(internal, #ssh_msg_userauth_banner{message = Msg}, {userauth,client}, D) ->
+handle_event(internal, #ssh_msg_userauth_banner{message = Msg}, {S,client}, D)
+ when S == userauth; S == userauth_keyboard_interactive;
+ S == userauth_keyboard_interactive_extra;
+ S == userauth_keyboard_interactive_info_response ->
case D#data.ssh_params#ssh.userauth_quiet_mode of
false -> io:format("~s", [Msg]);
true -> ok
diff -ruN a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl
--- a/lib/ssh/src/ssh.hrl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh.hrl 2025-12-17 17:25:02.074556391 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -150,22 +150,19 @@
default. The option can be set to the empty list if you do not want the daemon
to run any subsystems.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type subsystem_spec() :: {Name::string(), mod_args()} .
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type algs_list() :: list( alg_entry() ).
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type alg_entry() :: {kex, [kex_alg()]}
| {public_key, [pubkey_alg()]}
| {cipher, double_algs(cipher_alg())}
| {mac, double_algs(mac_alg())}
| {compression, double_algs(compression_alg())} .
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type kex_alg() :: 'curve25519-sha256' |
'curve25519-sha256@libssh.org' |
'curve448-sha512' |
@@ -181,8 +178,7 @@
'diffie-hellman-group1-sha1'
.
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type pubkey_alg() :: 'ssh-ed25519' |
'ssh-ed448' |
'ecdsa-sha2-nistp521' |
@@ -194,8 +190,7 @@
'ssh-dss'
.
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type cipher_alg() :: 'aes256-gcm@openssh.com' |
'aes256-ctr' |
'aes192-ctr' |
@@ -210,8 +205,7 @@
'3des-cbc'
.
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type mac_alg() :: 'hmac-sha2-512-etm@openssh.com' |
'hmac-sha2-256-etm@openssh.com' |
'hmac-sha2-512' |
@@ -223,8 +217,7 @@
'AEAD_AES_128_GCM'
.
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type compression_alg() :: 'none' |
'zlib' |
'zlib@openssh.com'
@@ -324,23 +317,20 @@
-type internal_options() :: ssh_options:private_options().
-type socket_options() :: [gen_tcp:connect_option() | gen_tcp:listen_option()].
-
--doc(#{title => <<"Client Options">>,equiv => client_option/0}).
+
+-doc(#{title => <<"Client Options">>}).
-type client_options() :: [ client_option() ] .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => daemon_option/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type daemon_options() :: [ daemon_option() ].
-
--doc(#{title => <<"Common Options">>,
- equiv => common_option/0}).
+-doc(#{title => <<"Common Options">>}).
-type common_options() :: [ common_option() ].
-doc """
The options above can be used both in clients and in daemons (servers). They are
further explained below.
""".
-doc(#{title => <<"Common Options">>}).
--type common_option() ::
+-type common_option() ::
ssh_file:user_dir_common_option()
| profile_common_option()
| max_idle_time_common_option()
@@ -383,8 +373,7 @@
""".
-doc(#{title => <<"Common Options">>}).
-type max_idle_time_common_option() :: {idle_time, timeout()}.
--doc(#{title => <<"Common Options">>,
- equiv => limit_time/0}).
+-doc(#{title => <<"Common Options">>}).
-type rekey_limit_common_option() :: {rekey_limit, Bytes::limit_bytes() |
{Minutes::limit_time(), Bytes::limit_bytes()}
}.
@@ -395,8 +384,7 @@
-doc(#{title => <<"Common Options">>}).
-type max_log_item_len_common_option() :: {max_log_item_len, limit_bytes()} .
--doc(#{title => <<"Common Options">>,
- equiv => limit_time/0}).
+-doc(#{title => <<"Common Options">>}).
-type limit_bytes() :: non_neg_integer() | infinity . % non_neg_integer due to compatibility
-doc """
Sets the limit when rekeying is to be initiated. Both the max time and max
@@ -497,11 +485,9 @@
""".
-doc(#{title => <<"Common Options">>}).
-type pref_public_key_algs_common_option() :: {pref_public_key_algs, [pubkey_alg()] } .
--doc(#{title => <<"Common Options">>,
- equiv => double_algs/1}).
+-doc(#{title => <<"Common Options">>}).
-type preferred_algorithms_common_option():: {preferred_algorithms, algs_list()}.
--doc(#{title => <<"Common Options">>,
- equiv => modify_algs_list/0}).
+-doc(#{title => <<"Common Options">>}).
-type modify_algorithms_common_option() :: {modify_algorithms, modify_algs_list()}.
-doc """
Comma-separated string that determines which authentication methods that the
@@ -561,8 +547,7 @@
| gen_tcp:connect_option()
| ?COMMON_OPTION .
--doc(#{title => <<"Other data types">>,
- equiv => opaque_common_options/0}).
+-doc(#{title => <<"Other data types">>}).
-type opaque_client_options() ::
{keyboard_interact_fun, fun((Name::iodata(),
Instruction::iodata(),
@@ -572,25 +557,6 @@
)}
| opaque_common_options().
--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}).
--type host_accepting_client_options() ::
- {silently_accept_hosts, accept_hosts()}
- | {user_interaction, boolean()}
- | {save_accepted_host, boolean()}
- | {quiet_mode, boolean()} .
-
--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}).
--type accept_hosts() :: boolean()
- | accept_callback()
- | {HashAlgoSpec::fp_digest_alg(), accept_callback()}.
-
--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}).
--type fp_digest_alg() :: 'md5' | crypto:sha1() | crypto:sha2() .
-
--doc(#{title => <<"Client Options">>,equiv => fingerprint/0}).
--type accept_callback() :: fun((PeerName::string(), fingerprint() ) -> boolean()) % Old style
- | fun((PeerName::string(), Port::inet:port_number(), fingerprint() ) -> boolean()) % New style
- .
-doc """
- **`silently_accept_hosts`{: #hardening_client_options-silently_accept_hosts
}** - This option guides the `connect` function on how to act when the
@@ -651,6 +617,26 @@
Defaults to `false`
""".
-doc(#{title => <<"Client Options">>}).
+-type host_accepting_client_options() ::
+ {silently_accept_hosts, accept_hosts()}
+ | {user_interaction, boolean()}
+ | {save_accepted_host, boolean()}
+ | {quiet_mode, boolean()} .
+
+-doc(#{title => <<"Client Options">>}).
+-type accept_hosts() :: boolean()
+ | accept_callback()
+ | {HashAlgoSpec::fp_digest_alg(), accept_callback()}.
+
+-doc(#{title => <<"Client Options">>}).
+-type fp_digest_alg() :: 'md5' | crypto:sha1() | crypto:sha2() .
+
+-doc(#{title => <<"Client Options">>}).
+-type accept_callback() :: fun((PeerName::string(), fingerprint() ) -> boolean()) % Old style
+ | fun((PeerName::string(), Port::inet:port_number(), fingerprint() ) -> boolean()) % New style
+ .
+
+-doc(#{title => <<"Client Options">>}).
-type fingerprint() :: string() | [string()].
-doc """
@@ -712,7 +698,7 @@
a way that impacts the ssh deamon's behaviour negatively. You use it on your own
risk.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type daemon_option() ::
subsystem_daemon_option()
| shell_daemon_option()
@@ -732,23 +718,20 @@
| gen_tcp:listen_option()
| ?COMMON_OPTION .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => subsystem_spec/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type subsystem_daemon_option() :: {subsystems, subsystem_specs()}.
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => subsystem_spec/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type subsystem_specs() :: [ subsystem_spec() ].
--doc(#{title => <<"Daemon Options (Server Options)">>,
+-doc(#{title => <<"Daemon Options">>,
equiv => 'shell_fun/2'/0}).
-type shell_daemon_option() :: {shell, shell_spec()} .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => 'shell_fun/2'/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type shell_spec() :: mod_fun_args() | shell_fun() | disabled .
--doc(#{title => <<"Daemon Options (Server Options)">>,
+-doc(#{title => <<"Daemon Options">>,
equiv => 'shell_fun/2'/0}).
-type shell_fun() :: 'shell_fun/1'() | 'shell_fun/2'() .
--doc(#{title => <<"Daemon Options (Server Options)">>,
+-doc(#{title => <<"Daemon Options">>,
equiv => 'shell_fun/2'/0}).
-type 'shell_fun/1'() :: fun((User::string()) -> pid()) .
-doc """
@@ -759,23 +742,20 @@
how the daemon executes shell-requests and exec-requests depending on the shell-
and exec-options.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type 'shell_fun/2'() :: fun((User::string(), PeerAddr::inet:ip_address()) -> pid()).
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => exec_spec/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type exec_daemon_option() :: {exec, exec_spec()} .
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type exec_spec() :: {direct, exec_fun()} | disabled | deprecated_exec_opt().
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type exec_fun() :: 'exec_fun/1'() | 'exec_fun/2'() | 'exec_fun/3'().
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => 'exec_fun/3'/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type 'exec_fun/1'() :: fun((Cmd::string()) -> exec_result()) .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => 'exec_fun/3'/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type 'exec_fun/2'() :: fun((Cmd::string(), User::string()) -> exec_result()) .
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type 'exec_fun/3'() :: fun((Cmd::string(), User::string(), ClientAddr::ip_port()) -> exec_result()) .
-doc """
This option changes how the daemon executes exec-requests from clients. The term
@@ -847,13 +827,13 @@
> retained but obey the rules 1-6 above if conflicting. The old and undocumented
> style should not be used in new programs.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type exec_result() :: {ok,Result::term()} | {error,Reason::term()} .
-doc """
Old-style exec specification that are kept for compatibility, but should not be
used in new programs
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type deprecated_exec_opt() :: fun() | mod_fun_args() .
-doc """
@@ -866,20 +846,20 @@
[`shell`](`t:shell_daemon_option/0`) and [`exec`](`t:exec_daemon_option/0`) are
disabled and only subsystem channels are allowed.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type ssh_cli_daemon_option() :: {ssh_cli, mod_args() | no_cli }.
-doc """
Enables (`true`) or disables (`false`) the possibility to tunnel a TCP/IP
connection out of a [server](`daemon/2`). Disabled per default.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type tcpip_tunnel_out_daemon_option() :: {tcpip_tunnel_out, boolean()} .
-doc """
Enables (`true`) or disables (`false`) the possibility to tunnel a TCP/IP
connection in to a [server](`daemon/2`). Disabled per default.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type tcpip_tunnel_in_daemon_option() :: {tcpip_tunnel_in, boolean()} .
-doc """
@@ -892,11 +872,10 @@
Default value is `true` which is compatible with other implementations not
supporting ext-info.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type send_ext_info_daemon_option() :: {send_ext_info, boolean()} .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => pwdfun_4/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type authentication_daemon_options() ::
ssh_file:system_dir_daemon_option()
| {auth_method_kb_interactive_data, prompt_texts() }
@@ -904,28 +883,22 @@
| {pk_check_user, boolean()}
| {password, string()}
| {pwdfun, pwdfun_2() | pwdfun_4()}
- | {no_auth_needed, boolean()}
- .
+ | {no_auth_needed, boolean()}.
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => pwdfun_4/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type prompt_texts() ::
kb_int_tuple()
| kb_int_fun_3()
- | kb_int_fun_4()
- .
+ | kb_int_fun_4().
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => pwdfun_4/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type kb_int_fun_3() :: fun((Peer::ip_port(), User::string(), Service::string()) -> kb_int_tuple()).
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => pwdfun_4/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type kb_int_fun_4() :: fun((Peer::ip_port(), User::string(), Service::string(), State::any()) -> kb_int_tuple()).
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => pwdfun_4/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type kb_int_tuple() :: {Name::string(), Instruction::string(), Prompt::string(), Echo::boolean()}.
--doc(#{title => <<"Daemon Options (Server Options)">>,
+-doc(#{title => <<"Daemon Options">>,
equiv => pwdfun_4/0}).
-type pwdfun_2() :: fun((User::string(), Password::string()|pubkey) -> boolean()) .
-doc """
@@ -1025,7 +998,7 @@
The default value is `false`.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type pwdfun_4() :: fun((User::string(),
Password::string()|pubkey,
PeerAddress::ip_port(),
@@ -1033,17 +1006,14 @@
boolean() | disconnect | {boolean(),NewState::any()}
) .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => ssh_moduli_file/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type diffie_hellman_group_exchange_daemon_option() ::
{dh_gex_groups, [explicit_group()] | explicit_group_file() | ssh_moduli_file()}
| {dh_gex_limits, {Min::pos_integer(), Max::pos_integer()} } .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => ssh_moduli_file/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type explicit_group() :: {Size::pos_integer(),G::pos_integer(),P::pos_integer()} .
--doc(#{title => <<"Daemon Options (Server Options)">>,
- equiv => ssh_moduli_file/0}).
+-doc(#{title => <<"Daemon Options">>}).
-type explicit_group_file() :: {file,string()} .
-doc """
- **`dh_gex_groups`** - Defines the groups the server may choose among when
@@ -1078,7 +1048,7 @@
See [RFC 4419](https://tools.ietf.org/html/rfc4419) for the function of the
Max and Min values.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type ssh_moduli_file() :: {ssh_moduli_file,string()}.
-doc """
@@ -1089,7 +1059,7 @@
[Timeouts section ](hardening.md#timeouts)in the User's Guide
[Hardening](hardening.md) chapter.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type max_initial_idle_time_daemon_option() :: {max_initial_idle_time, timeout()} .
-doc """
Maximum time in milliseconds for the authentication negotiation. Defaults to
@@ -1100,7 +1070,7 @@
[Timeouts section ](hardening.md#timeouts)in the User's Guide
[Hardening](hardening.md) chapter.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type negotiation_timeout_daemon_option() :: {negotiation_timeout, timeout()} .
-doc """
Maximum time in milliseconds for the first part of the ssh session setup, the
@@ -1111,7 +1081,7 @@
[Timeouts section ](hardening.md#timeouts)in the User's Guide
[Hardening](hardening.md) chapter.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type hello_timeout_daemon_option() :: {hello_timeout, timeout()} .
-doc """
@@ -1160,7 +1130,7 @@
maximum packet size that the daemon will accept in channel open requests from
the client. The default value is 0.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type hardening_daemon_options() ::
{max_sessions, pos_integer()}
| {max_channels, pos_integer()}
@@ -1174,13 +1144,12 @@
- **`failfun`** - Provides a fun to implement your own logging when a user fails
to authenticate.
""".
--doc(#{title => <<"Daemon Options (Server Options)">>}).
+-doc(#{title => <<"Daemon Options">>}).
-type callbacks_daemon_options() ::
- {failfun, fun((User::string(), PeerAddress::inet:ip_address(), Reason::term()) -> _)}
- | {connectfun, fun((User::string(), PeerAddress::inet:ip_address(), Method::string()) ->_)} .
+ {failfun, fun((User::string(), Peer::{inet:ip_address(), inet:port_number()}, Reason::term()) -> _)}
+ | {connectfun, fun((User::string(), Peer::{inet:ip_address(), inet:port_number()}, Method::string()) ->_)} .
--doc(#{title => <<"Other data types">>,
- equiv => opaque_common_options/0}).
+-doc(#{title => <<"Other data types">>}).
-type opaque_daemon_options() ::
{infofun, fun()}
| opaque_common_options().
@@ -1202,7 +1171,7 @@
-record(ssh,
{
- role :: client | role(),
+ role :: role(),
peer :: undefined |
{inet:hostname(),ip_port()}, %% string version of peer address
@@ -1338,5 +1307,11 @@
-define(CIRC_BUF_IN_ONCE(VALUE),
((fun(V) -> ?CIRC_BUF_IN(V), V end)(VALUE))
).
-
+
+-define(SELECT_MSG(__Fun),
+ (fun() ->
+ #{level := __Level} = logger:get_primary_config(),
+ __Fun(__Level)
+ end)()).
+
-endif. % SSH_HRL defined
diff -ruN a/lib/ssh/src/ssh_info.erl b/lib/ssh/src/ssh_info.erl
--- a/lib/ssh/src/ssh_info.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_info.erl 2025-12-17 17:25:02.076556417 +1030
@@ -274,6 +274,7 @@
_:_ -> "?"
end.
+
format_address(#address{address=Addr, port=Port, profile=Prof}) ->
io_lib:format("~s (profile ~p)", [ssh_lib:format_address_port({Addr,Port}),Prof]);
format_address(A) ->
diff -ruN a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl
--- a/lib/ssh/src/ssh_lib.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_lib.erl 2025-12-17 17:25:02.076556417 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -31,7 +31,9 @@
format_time_ms/1,
comp/2,
set_label/1,
- set_label/2
+ set_label/2,
+ trim_reason/1,
+ max_log_len/1
]).
-include("ssh.hrl").
@@ -97,3 +99,17 @@
proc_lib:set_label({sshc, Details});
set_label(server, Details) ->
proc_lib:set_label({sshd, Details}).
+
+%% We don't want to process badmatch details, potentially containing
+%% malicious data of unknown size
+trim_reason({badmatch, V}) when is_binary(V) ->
+ badmatch;
+trim_reason(E) ->
+ E.
+
+max_log_len(#ssh{opts = Opts}) ->
+ ?GET_OPT(max_log_item_len, Opts);
+max_log_len(Opts) when is_map(Opts) ->
+ ?GET_OPT(max_log_item_len, Opts).
+
+
diff -ruN a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl
--- a/lib/ssh/src/ssh_message.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_message.erl 2025-12-17 17:25:02.076556417 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2013-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2013-2025. 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.
@@ -25,6 +25,7 @@
-moduledoc false.
-include_lib("public_key/include/public_key.hrl").
+-include_lib("kernel/include/logger.hrl").
-include("ssh.hrl").
-include("ssh_connect.hrl").
@@ -43,11 +44,12 @@
-behaviour(ssh_dbg).
-export([ssh_dbg_trace_points/0, ssh_dbg_flags/1, ssh_dbg_on/1, ssh_dbg_off/1, ssh_dbg_format/2]).
+-define(ALG_NAME_LIMIT, 64). % RFC4251 sec6
ucl(B) ->
try unicode:characters_to_list(B) of
L when is_list(L) -> L;
- {error,_Matched,Rest} -> throw({error,{bad_unicode,Rest}})
+ {error,_Matched,_Rest} -> throw({error,bad_unicode})
catch
_:_ -> throw({error,bad_unicode})
end.
@@ -207,12 +209,12 @@
encode(#ssh_msg_service_request{
name = Service
}) ->
- <<?Ebyte(?SSH_MSG_SERVICE_REQUEST), ?Estring_utf8(Service)>>;
+ <<?Ebyte(?SSH_MSG_SERVICE_REQUEST), ?Estring(Service)>>;
encode(#ssh_msg_service_accept{
name = Service
}) ->
- <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring_utf8(Service)>>;
+ <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring(Service)>>;
encode(#ssh_msg_ext_info{
nr_extensions = N,
@@ -375,7 +377,7 @@
try
#ssh_msg_channel_request{
recipient_channel = Recipient,
- request_type = ?unicode_list(RequestType),
+ request_type = binary:bin_to_list(RequestType),
want_reply = erl_boolean(Bool),
data = Data
}
@@ -405,8 +407,8 @@
Data/binary>>) ->
#ssh_msg_userauth_request{
user = ?unicode_list(User),
- service = ?unicode_list(Service),
- method = ?unicode_list(Method),
+ service = binary:bin_to_list(Service),
+ method = binary:bin_to_list(Method),
data = Data
};
@@ -414,7 +416,7 @@
?DEC_BIN(Auths,__0),
?BYTE(Bool)>>) ->
#ssh_msg_userauth_failure {
- authentications = ?unicode_list(Auths),
+ authentications = binary:bin_to_list(Auths),
partial_success = erl_boolean(Bool)
};
@@ -528,12 +530,12 @@
decode(<<?SSH_MSG_SERVICE_REQUEST, ?DEC_BIN(Service,__0)>>) ->
#ssh_msg_service_request{
- name = ?unicode_list(Service)
+ name = binary:bin_to_list(Service)
};
decode(<<?SSH_MSG_SERVICE_ACCEPT, ?DEC_BIN(Service,__0)>>) ->
#ssh_msg_service_accept{
- name = ?unicode_list(Service)
+ name = binary:bin_to_list(Service)
};
decode(<<?BYTE(?SSH_MSG_DISCONNECT), ?UINT32(Code), ?DEC_BIN(Desc,__0), ?DEC_BIN(Lang,__1)>>) ->
@@ -820,9 +822,33 @@
%% See rfc 4253 7.1
X = 0,
list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc]));
-decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) ->
- Names = string:tokens(?unicode_list(Data), ","),
- decode_kex_init(Rest, [Names | Acc], N -1).
+decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) when
+ byte_size(Data) < ?MAX_NUM_ALGORITHMS * ?ALG_NAME_LIMIT ->
+ BinParts = binary:split(Data, <<$,>>, [global]),
+ AlgCount = length(BinParts),
+ case AlgCount =< ?MAX_NUM_ALGORITHMS of
+ true ->
+ Process =
+ fun(<<>>, PAcc) ->
+ PAcc;
+ (Part, PAcc) ->
+ case byte_size(Part) =< ?ALG_NAME_LIMIT of
+ true ->
+ Name = binary:bin_to_list(Part),
+ [Name | PAcc];
+ false ->
+ ?LOG_DEBUG("Ignoring too long name", []),
+ PAcc
+ end
+ end,
+ Names = lists:foldr(Process, [], BinParts),
+ decode_kex_init(Rest, [Names | Acc], N - 1);
+ false ->
+ throw({error, {kexinit_error, N, {alg_count, AlgCount}}})
+ end;
+decode_kex_init(<<?DEC_BIN(Data,__0), _Rest/binary>>, _Acc, N) ->
+ throw({error, {kexinit, N, {string_size, byte_size(Data)}}}).
+
%%%================================================================
diff -ruN a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl
--- a/lib/ssh/src/ssh_options.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_options.erl 2025-12-17 17:25:02.076556417 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -886,6 +886,12 @@
#{default => ?MAX_RND_PADDING_LEN,
chk => fun(V) -> check_non_neg_integer(V) end,
class => undoc_user_option
+ },
+
+ channel_close_timeout =>
+ #{default => 5 * 1000,
+ chk => fun(V) -> check_non_neg_integer(V) end,
+ class => undoc_user_option
}
}.
diff -ruN a/lib/ssh/src/ssh_server_channel.erl b/lib/ssh/src/ssh_server_channel.erl
--- a/lib/ssh/src/ssh_server_channel.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_server_channel.erl 2025-12-17 17:25:02.076556417 +1030
@@ -51,8 +51,7 @@
> When implementing a client subsystem handler, use
> [\-behaviour(ssh_client_channel)](`m:ssh_client_channel`) instead.
""".
--moduledoc(#{since => "OTP 21.0",
- titles => [{callback,<<"Callback Functions">>}]}).
+-moduledoc(#{since => "OTP 21.0"}).
%% API to server side channel that can be plugged into the erlang ssh daemeon
-doc """
@@ -63,7 +62,7 @@
`m:gen_server`. If the time-out occurs, `c:handle_msg/2` is called as
[`handle_msg(timeout, State)`](`c:handle_msg/2`).
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback init(Args :: term()) ->
{ok, State :: term()} | {ok, State :: term(), timeout() | hibernate} |
{stop, Reason :: term()} | ignore.
@@ -76,7 +75,7 @@
the channel process terminates with reason `Reason`. The return value is
ignored.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback terminate(Reason :: (normal | shutdown | {shutdown, term()} |
term()),
State :: term()) ->
@@ -89,13 +88,13 @@
Possible Erlang 'EXIT' messages is to be handled by this function and all
channels are to handle the following message.
-- **`{ssh_channel_up, ``t:ssh:channel_id/0``, ``t:ssh:connection_ref/0``}`** -
+- **`{ssh_channel_up,` `t:ssh:channel_id/0` `,` `t:ssh:connection_ref/0` `}`** -
This is the first message that the channel receives. This is especially useful
if the server wants to send a message to the client without first receiving a
message from it. If the message is not useful for your particular scenario,
ignore it by immediately returning `{ok, State}`.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback handle_msg(Msg ::term(), State :: term()) ->
{ok, State::term()} | {stop, ChannelId::ssh:channel_id(), State::term()}.
-doc """
@@ -104,11 +103,11 @@
The following message is taken care of by the `ssh_server_channel` behavior.
-- **`{closed, ``t:ssh:channel_id/0``}`** - The channel behavior sends a close
+- **`{closed,` `t:ssh:channel_id/0` `}`** - The channel behavior sends a close
message to the other side, if such a message has not already been sent. Then
it terminates the channel with reason `normal`.
""".
--doc(#{title => <<"Callback Functions">>,since => <<"OTP 21.0">>}).
+-doc(#{since => <<"OTP 21.0">>}).
-callback handle_ssh_msg(ssh_connection:event(),
State::term()) -> {ok, State::term()} |
{stop, ChannelId::ssh:channel_id(),
diff -ruN a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl
--- a/lib/ssh/src/ssh_sftpd.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_sftpd.erl 2025-12-17 17:25:02.077556430 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. 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.
@@ -32,7 +32,7 @@
-behaviour(ssh_server_channel).
-include_lib("kernel/include/file.hrl").
-
+-include_lib("kernel/include/logger.hrl").
-include("ssh.hrl").
-include("ssh_xfer.hrl").
-include("ssh_connect.hrl"). %% For ?DEFAULT_PACKET_SIZE and ?DEFAULT_WINDOW_SIZE
@@ -57,6 +57,8 @@
file_handler, % atom() - callback module
file_state, % state for the file callback module
max_files, % integer >= 0 max no files sent during READDIR
+ max_handles, % integer > 0 - max number of file handles
+ max_path, % integer > 0 - max length of path
options, % from the subsystem declaration
handles % list of open handles
%% handle is either {<int>, directory, {Path, unread|eof}} or
@@ -86,6 +88,18 @@
limit. If supplied, the number of filenames returned to the SFTP client per
`READDIR` request is limited to at most the given value.
+- **`max_handles`** - The default value is `1000`. Positive integer
+ value represents the maximum number of file handles allowed for a
+ connection.
+
+ (Note: separate limitation might be also enforced by underlying
+ operating system)
+
+- **`max_path`** - The default value is `4096`. Positive integer value
+ represents the maximum path length which cannot be exceeded in
+ data provided by the SFTP client. (Note: limitations might be also
+ enforced by underlying operating system)
+
- **`root`** - Sets the SFTP root directory. Then the user cannot see any files
above this root. If, for example, the root directory is set to `/tmp`, then
the user sees this directory as `/`. If the user then writes `cd /etc`, the
@@ -98,6 +112,8 @@
Options :: [ {cwd, string()} |
{file_handler, CbMod | {CbMod, FileState}} |
{max_files, integer()} |
+ {max_handles, integer()} |
+ {max_path, integer()} |
{root, string()} |
{sftpd_vsn, integer()}
],
@@ -149,8 +165,14 @@
{Root0, State0}
end,
MaxLength = proplists:get_value(max_files, Options, 0),
+ MaxHandles = proplists:get_value(max_handles, Options, 1000),
+ MaxPath = proplists:get_value(max_path, Options, 4096),
Vsn = proplists:get_value(sftpd_vsn, Options, 5),
- {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength,
+ {ok, State#state{cwd = CWD,
+ root = Root,
+ max_files = MaxLength,
+ max_handles = MaxHandles,
+ max_path = MaxPath,
options = Options,
handles = [], pending = <<>>,
xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
@@ -163,9 +185,8 @@
%%--------------------------------------------------------------------
-doc false.
handle_ssh_msg({ssh_cm, _ConnectionManager,
- {data, _ChannelId, Type, Data}}, State) ->
- State1 = handle_data(Type, Data, State),
- {ok, State1};
+ {data, ChannelId, Type, Data}}, State) ->
+ handle_data(Type, ChannelId, Data, State);
handle_ssh_msg({ssh_cm, _, {eof, ChannelId}}, State) ->
{stop, ChannelId, State};
@@ -224,24 +245,77 @@
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
-handle_data(0, <<?UINT32(Len), Msg:Len/binary, Rest/binary>>,
+handle_data(0, ChannelId, <<?UINT32(Len), Msg:Len/binary, Rest/binary>>,
State = #state{pending = <<>>}) ->
<<Op, ?UINT32(ReqId), Data/binary>> = Msg,
NewState = handle_op(Op, ReqId, Data, State),
case Rest of
<<>> ->
- NewState;
+ {ok, NewState};
_ ->
- handle_data(0, Rest, NewState)
+ handle_data(0, ChannelId, Rest, NewState)
end;
-
-handle_data(0, Data, State = #state{pending = <<>>}) ->
- State#state{pending = Data};
-
-handle_data(Type, Data, State = #state{pending = Pending}) ->
- handle_data(Type, <<Pending/binary, Data/binary>>,
- State#state{pending = <<>>}).
-
+handle_data(0, _ChannelId, Data, State = #state{pending = <<>>}) ->
+ {ok, State#state{pending = Data}};
+handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) ->
+ Data = <<Pending/binary, Data0/binary>>,
+ Size = byte_size(Data),
+ case Size > ?SSH_MAX_PACKET_SIZE of
+ true ->
+ ReportFun =
+ fun([S]) ->
+ Report =
+ #{label => {error_logger, error_report},
+ report =>
+ io_lib:format("SFTP packet size (~B) exceeds the limit!",
+ [S])},
+ Meta =
+ #{error_logger =>
+ #{tag => error_report,type => std_error},
+ report_cb => fun(#{report := Msg}) -> {Msg, []} end},
+ {Report, Meta}
+ end,
+ ?LOG_ERROR(ReportFun, [Size]),
+ {stop, ChannelId, State};
+ _ ->
+ handle_data(Type, ChannelId, Data, State#state{pending = <<>>})
+ end.
+
+%% From draft-ietf-secsh-filexfer-02 "The file handle strings MUST NOT be longer than 256 bytes."
+handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF})
+ when (Request == ?SSH_FXP_CLOSE orelse
+ Request == ?SSH_FXP_FSETSTAT orelse
+ Request == ?SSH_FXP_FSTAT orelse
+ Request == ?SSH_FXP_READ orelse
+ Request == ?SSH_FXP_READDIR orelse
+ Request == ?SSH_FXP_WRITE),
+ HLen > 256 ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"),
+ State;
+handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>,
+ State = #state{max_path = MaxPath, xf = XF})
+ when (Request == ?SSH_FXP_LSTAT orelse
+ Request == ?SSH_FXP_MKDIR orelse
+ Request == ?SSH_FXP_OPEN orelse
+ Request == ?SSH_FXP_OPENDIR orelse
+ Request == ?SSH_FXP_READLINK orelse
+ Request == ?SSH_FXP_REALPATH orelse
+ Request == ?SSH_FXP_REMOVE orelse
+ Request == ?SSH_FXP_RMDIR orelse
+ Request == ?SSH_FXP_SETSTAT orelse
+ Request == ?SSH_FXP_STAT),
+ PLen > MaxPath ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
+ "No such path"),
+ State;
+handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _/binary>>,
+ State = #state{max_path = MaxPath, xf = XF})
+ when (Request == ?SSH_FXP_RENAME orelse
+ Request == ?SSH_FXP_SYMLINK),
+ (PLen > MaxPath orelse PLen2 > MaxPath) ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
+ "No such path"),
+ State;
handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
XF = State#state.xf,
Vsn = lists:min([XF#ssh_xfer.vsn, Version]),
@@ -249,7 +323,7 @@
ssh_xfer:xf_send_reply(XF1, ?SSH_FXP_VERSION, <<?UINT32(Vsn)>>),
State#state{xf = XF1};
handle_op(?SSH_FXP_REALPATH, ReqId,
- <<?UINT32(Rlen), RPath:Rlen/binary>>,
+ <<?UINT32(RLen), RPath:RLen/binary>>,
State0) ->
RelPath = relate_file_name(RPath, State0, _Canonicalize=false),
{Res, State} = resolve_symlinks(RelPath, State0),
@@ -265,14 +339,16 @@
end;
handle_op(?SSH_FXP_OPENDIR, ReqId,
<<?UINT32(RLen), RPath:RLen/binary>>,
- State0 = #state{xf = #ssh_xfer{vsn = Vsn},
- file_handler = FileMod, file_state = FS0}) ->
+ State0 = #state{xf = #ssh_xfer{vsn = Vsn},
+ file_handler = FileMod, file_state = FS0,
+ max_handles = MaxHandles}) ->
RelPath = unicode:characters_to_list(RPath),
AbsPath = relate_file_name(RelPath, State0),
XF = State0#state.xf,
{IsDir, FS1} = FileMod:is_dir(AbsPath, FS0),
State1 = State0#state{file_state = FS1},
+ HandlesCnt = length(State0#state.handles),
case IsDir of
false when Vsn > 5 ->
ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY,
@@ -282,8 +358,12 @@
ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
"Not a directory"),
State1;
- true ->
- add_handle(State1, XF, ReqId, directory, {RelPath,unread})
+ true when HandlesCnt < MaxHandles ->
+ add_handle(State1, XF, ReqId, directory, {RelPath,unread});
+ true ->
+ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE,
+ "max_handles limit reached"),
+ State1
end;
handle_op(?SSH_FXP_READDIR, ReqId,
<<?UINT32(HLen), BinHandle:HLen/binary>>,
@@ -418,14 +498,12 @@
send_status(Status, ReqId, State1);
handle_op(?SSH_FXP_RENAME, ReqId,
- Bin = <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2),
- _:PLen2/binary>>,
+ Bin = <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _:PLen2/binary>>,
State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 ->
handle_op(?SSH_FXP_RENAME, ReqId, <<Bin/binary, 0:32>>, State);
handle_op(?SSH_FXP_RENAME, ReqId,
- <<?UINT32(PLen), BPath:PLen/binary, ?UINT32(PLen2),
- BPath2:PLen2/binary, ?UINT32(Flags)>>,
+ <<?UINT32(PLen), BPath:PLen/binary, ?UINT32(PLen2), BPath2:PLen2/binary, ?UINT32(Flags)>>,
State0 = #state{file_handler = FileMod, file_state = FS0}) ->
Path = relate_file_name(BPath, State0),
Path2 = relate_file_name(BPath2, State0),
@@ -463,23 +541,29 @@
State1 = State0#state{file_state = FS1},
send_status(Status, ReqId, State1).
-new_handle([], H) ->
- H;
-new_handle([{N, _,_} | Rest], H) when N =< H ->
- new_handle(Rest, N+1);
-new_handle([_ | Rest], H) ->
- new_handle(Rest, H).
+new_handle_id([]) -> 0;
+new_handle_id([{_, _, _} | _] = Handles) ->
+ {HandleIds, _, _} = lists:unzip3(Handles),
+ new_handle_id(lists:sort(HandleIds));
+new_handle_id(HandleIds) ->
+ find_gap(HandleIds).
+
+find_gap([Id]) -> % no gap found
+ Id + 1;
+find_gap([Id1, Id2 | _]) when Id2 - Id1 > 1 -> % gap found
+ Id1 + 1;
+find_gap([_, Id | Rest]) ->
+ find_gap([Id | Rest]).
add_handle(State, XF, ReqId, Type, DirFileTuple) ->
Handles = State#state.handles,
- Handle = new_handle(Handles, 0),
- ssh_xfer:xf_send_handle(XF, ReqId, integer_to_list(Handle)),
- %% OBS: If you change handles-tuple also change new_handle!
- %% Is this this the best way to implement new handle?
- State#state{handles = [{Handle, Type, DirFileTuple} | Handles]}.
+ HandleId = new_handle_id(Handles),
+ ssh_xfer:xf_send_handle(XF, ReqId, integer_to_list(HandleId)),
+ %% OBS: If you change handles-tuple also change new_handle_id!
+ State#state{handles = [{HandleId, Type, DirFileTuple} | Handles]}.
get_handle(Handles, BinHandle) ->
- case (catch list_to_integer(binary_to_list(BinHandle))) of
+ case (catch binary_to_integer(BinHandle)) of
I when is_integer(I) ->
case lists:keysearch(I, 1, Handles) of
{value, T} -> T;
@@ -734,7 +818,9 @@
do_open(ReqId, State, Path, Flags).
do_open(ReqId, State0, Path, Flags) ->
- #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}} = State0,
+ #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn},
+ max_handles = MaxHandles} = State0,
+ HandlesCnt = length(State0#state.handles),
AbsPath = relate_file_name(Path, State0),
{IsDir, _FS1} = FileMod:is_dir(AbsPath, FS0),
case IsDir of
@@ -746,7 +832,7 @@
ssh_xfer:xf_send_status(State0#state.xf, ReqId,
?SSH_FX_FAILURE, "File is a directory"),
State0;
- false ->
+ false when HandlesCnt < MaxHandles ->
OpenFlags = [binary | Flags],
{Res, FS1} = FileMod:open(AbsPath, OpenFlags, FS0),
State1 = State0#state{file_state = FS1},
@@ -757,7 +843,11 @@
ssh_xfer:xf_send_status(State1#state.xf, ReqId,
ssh_xfer:encode_erlang_status(Error)),
State1
- end
+ end;
+ false ->
+ ssh_xfer:xf_send_status(State0#state.xf, ReqId,
+ ?SSH_FX_FAILURE, "max_handles limit reached"),
+ State0
end.
%% resolve all symlinks in a path
diff -ruN a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl
--- a/lib/ssh/src/ssh_sftp.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_sftp.erl 2025-12-17 17:25:02.077556430 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. 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.
@@ -30,8 +30,7 @@
file transfer service available for SSH.
""".
-moduledoc(#{titles =>
- [{type,<<"Error cause">>},
- {type,<<"Crypto operations for open_tar">>}]}).
+ [{type,<<"Crypto open_tar">>}]}).
-behaviour(ssh_client_channel).
@@ -125,7 +124,7 @@
The `t:tuple/0` reason are other errors like for example `{exit_status,1}`.
""".
--doc(#{title => <<"Error cause">>}).
+-doc(#{}).
-type reason() :: atom() | string() | tuple() .
%%====================================================================
@@ -149,20 +148,12 @@
%%% -spec:s are as if Dialyzer handled signatures for separate
%%% function clauses.
-doc(#{equiv => start_channel/3}).
--spec start_channel(ssh:open_socket(),
- [ssh:client_option() | sftp_option()]
- )
- -> {ok,pid(),ssh:connection_ref()} | {error,reason()};
-
- (ssh:connection_ref(),
- [sftp_option()]
- )
- -> {ok,pid()} | {ok,pid(),ssh:connection_ref()} | {error,reason()};
-
- (ssh:host(),
- [ssh:client_option() | sftp_option()]
- )
- -> {ok,pid(),ssh:connection_ref()} | {error,reason()} .
+-spec start_channel(ssh:open_socket(), [ssh:client_option() | sftp_option()]) ->
+ {ok, pid(), ssh:connection_ref()} | {error,reason()};
+ (ssh:connection_ref(), [ssh:client_option() | sftp_option()]) ->
+ {ok, pid()} | {ok, pid(), ssh:connection_ref()} | {error, reason()};
+ (ssh:host(), [ssh:client_option() | sftp_option()]) ->
+ {ok, pid(), ssh:connection_ref()} | {error, reason()} .
start_channel(Cm, UserOptions0) when is_pid(Cm) ->
UserOptions = legacy_timeout(UserOptions0),
Timeout = proplists:get_value(timeout, UserOptions, infinity),
@@ -318,12 +309,10 @@
call(Pid, {open, false, File, Mode}, FileOpTimeout).
--doc(#{title => <<"Crypto operations for open_tar">>,
- equiv => decrypt_spec/0}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type tar_crypto_spec() :: encrypt_spec() | decrypt_spec() .
--doc(#{title => <<"Crypto operations for open_tar">>,
- equiv => decrypt_spec/0}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type encrypt_spec() :: {init_fun(), crypto_fun(), final_fun()} .
-doc """
Specifies the encryption or decryption applied to tar files when using
@@ -336,16 +325,14 @@
[Example with encryption](using_ssh.md#example-with-encryption) in the ssh Users
Guide.
""".
--doc(#{title => <<"Crypto operations for open_tar">>}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type decrypt_spec() :: {init_fun(), crypto_fun()} .
--doc(#{title => <<"Crypto operations for open_tar">>,
- equiv => crypto_state/0}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type init_fun() :: fun(() -> {ok,crypto_state()})
| fun(() -> {ok,crypto_state(),chunk_size()}) .
--doc(#{title => <<"Crypto operations for open_tar">>,
- equiv => crypto_result/0}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type crypto_fun() :: fun((TextIn::binary(), crypto_state()) -> crypto_result()) .
-doc """
The initial `t:crypto_state/0` returned from the `t:init_fun/0` is folded into
@@ -357,7 +344,7 @@
If the `t:crypto_fun/0` reurns a `t:chunk_size/0`, that value is as block size
for further blocks in calls to `t:crypto_fun/0`.
""".
--doc(#{title => <<"Crypto operations for open_tar">>}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type crypto_result() :: {ok,TextOut::binary(),crypto_state()}
| {ok,TextOut::binary(),crypto_state(),chunk_size()} .
@@ -367,11 +354,10 @@
The `t:final_fun/0` is responsible for padding (if needed) and encryption of
that last piece.
""".
--doc(#{title => <<"Crypto operations for open_tar">>}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type final_fun() :: fun((FinalTextIn::binary(),crypto_state()) -> {ok,FinalTextOut::binary()}) .
--doc(#{title => <<"Crypto operations for open_tar">>,
- equiv => crypto_state/0}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type chunk_size() :: undefined | pos_integer().
-doc """
The `t:init_fun/0` in the [tar_crypto_spec](`t:tar_crypto_spec/0`) is applied
@@ -388,7 +374,7 @@
`t:chunk_size/0` can be changed in the return from the `t:crypto_fun/0`. The
value can be changed between `t:pos_integer/0` and `undefined`.
""".
--doc(#{title => <<"Crypto operations for open_tar">>}).
+-doc(#{title => <<"Crypto open_tar">>}).
-type crypto_state() :: any() .
@@ -414,7 +400,7 @@
in the ssh Users Guide.
The `crypto` mode option is explained in the data types section above, see
-[Crypto operations for open_tar](`m:ssh_sftp#types-crypto-operations-for-open_tar`).
+[Crypto operations for open_tar](`m:ssh_sftp#types-crypto-open_tar`).
Encryption is assumed if the `Mode` contains `write`, and decryption if the
`Mode` contains `read`.
""".
@@ -1038,14 +1024,12 @@
Timeout :: timeout(),
Error :: {error, reason()}.
read_file(Pid, Name, FileOpTimeout) ->
- case open(Pid, Name, [read, binary], FileOpTimeout) of
- {ok, Handle} ->
- {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout),
- Res = read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, []),
- close(Pid, Handle),
- Res;
- Error ->
- Error
+ maybe
+ {ok, Handle} ?= open(Pid, Name, [read, binary], FileOpTimeout),
+ {ok, {_WindowSz, PacketSz}} ?= recv_window(Pid, FileOpTimeout),
+ Res = read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, []),
+ close(Pid, Handle),
+ Res
end.
read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, Acc) ->
@@ -1080,15 +1064,12 @@
write_file(Pid, Name, List, FileOpTimeout) when is_list(List) ->
write_file(Pid, Name, to_bin(List), FileOpTimeout);
write_file(Pid, Name, Bin, FileOpTimeout) ->
- case open(Pid, Name, [write, binary], FileOpTimeout) of
- {ok, Handle} ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- Res = write_file_loop(Pid, Handle, 0, Bin, byte_size(Bin), Packet,
- FileOpTimeout),
- close(Pid, Handle, FileOpTimeout),
- Res;
- Error ->
- Error
+ maybe
+ {ok, Handle} ?= open(Pid, Name, [write, binary], FileOpTimeout),
+ {ok, {_Window, Packet}} ?= send_window(Pid, FileOpTimeout),
+ Res = write_file_loop(Pid, Handle, 0, Bin, byte_size(Bin), Packet, FileOpTimeout),
+ close(Pid, Handle, FileOpTimeout),
+ Res
end.
write_file_loop(_Pid, _Handle, _Pos, _Bin, 0, _PacketSz,_FileOpTimeout) ->
@@ -1910,8 +1891,10 @@
read_repeat(Pid, Handle, Len, FileOpTimeout) ->
- {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout),
- read_rpt(Pid, Handle, Len, PacketSz, FileOpTimeout, <<>>).
+ maybe
+ {ok, {_WindowSz, PacketSz}} ?= recv_window(Pid, FileOpTimeout),
+ read_rpt(Pid, Handle, Len, PacketSz, FileOpTimeout, <<>>)
+ end.
read_rpt(Pid, Handle, WantedLen, PacketSz, FileOpTimeout, Acc) when WantedLen > 0 ->
case read(Pid, Handle, min(WantedLen,PacketSz), FileOpTimeout) of
@@ -1929,8 +1912,10 @@
write_to_remote_tar(_Pid, _SftpHandle, <<>>, _FileOpTimeout) ->
ok;
write_to_remote_tar(Pid, SftpHandle, Bin, FileOpTimeout) ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- write_file_loop(Pid, SftpHandle, 0, Bin, byte_size(Bin), Packet, FileOpTimeout).
+ maybe
+ {ok, {_Window, Packet}} ?= send_window(Pid, FileOpTimeout),
+ write_file_loop(Pid, SftpHandle, 0, Bin, byte_size(Bin), Packet, FileOpTimeout)
+ end.
position_buf(Pid, SftpHandle, BufHandle, Pos, FileOpTimeout) ->
{ok,#bufinf{mode = Mode,
@@ -1967,18 +1952,19 @@
end.
read_buf(Pid, SftpHandle, BufHandle, WantedLen, FileOpTimeout) ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- {ok,B0} = call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
- case do_the_read_buf(Pid, SftpHandle, WantedLen, Packet, FileOpTimeout, B0) of
- {ok,ResultBin,B} ->
- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
- {ok,ResultBin};
- {error,Error} ->
- {error,Error};
- {eof,B} ->
- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
- eof
- end.
+ maybe
+ {ok, {_Window, Packet}} ?= send_window(Pid, FileOpTimeout),
+ {ok, B0} ?= call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
+ {ok, ResultBin, B} ?= do_the_read_buf(Pid, SftpHandle, WantedLen, Packet, FileOpTimeout, B0),
+ call(Pid, {put_bufinf, BufHandle, B}, FileOpTimeout),
+ {ok,ResultBin}
+ else
+ {error, Error} ->
+ {error,Error};
+ {eof, BufInf} ->
+ call(Pid, {put_bufinf, BufHandle, BufInf}, FileOpTimeout),
+ eof
+ end.
do_the_read_buf(_Pid, _SftpHandle, WantedLen, _Packet, _FileOpTimeout,
B=#bufinf{plain_text_buf=PlainBuf0,
@@ -2030,15 +2016,14 @@
write_buf(Pid, SftpHandle, BufHandle, PlainBin, FileOpTimeout) ->
- {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout),
- {ok,B0=#bufinf{plain_text_buf=PTB}} = call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
- case do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout,
- B0#bufinf{plain_text_buf = <<PTB/binary,PlainBin/binary>>}) of
- {ok, B} ->
- call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
- ok;
- {error,Error} ->
- {error,Error}
+ maybe
+ {ok, {_Window, Packet}} ?= send_window(Pid, FileOpTimeout),
+ {ok, B0=#bufinf{plain_text_buf=PTB}} ?= call(Pid, {get_bufinf,BufHandle}, FileOpTimeout),
+ {ok, B} ?=
+ do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout,
+ B0#bufinf{plain_text_buf = <<PTB/binary,PlainBin/binary>>}),
+ call(Pid, {put_bufinf,BufHandle,B}, FileOpTimeout),
+ ok
end.
do_the_write_buf(Pid, SftpHandle, Packet, FileOpTimeout,
diff -ruN a/lib/ssh/src/ssh_system_sup.erl b/lib/ssh/src/ssh_system_sup.erl
--- a/lib/ssh/src/ssh_system_sup.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_system_sup.erl 2025-12-17 17:25:02.077556430 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -160,8 +160,8 @@
case get_daemon_listen_address(SysPid) of
{ok,Address} ->
get_options(SysPid, Address);
- {error,Error} ->
- {error,Error}
+ {error,not_found} ->
+ {error,bad_daemon_ref}
end.
replace_acceptor_options(SysPid, NewOpts) ->
diff -ruN a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl
--- a/lib/ssh/src/ssh_transport.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/src/ssh_transport.erl 2025-12-17 17:25:02.077556430 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -27,12 +27,11 @@
-include_lib("public_key/include/public_key.hrl").
-include_lib("kernel/include/inet.hrl").
-
-include("ssh_transport.hrl").
-include("ssh.hrl").
-export([versions/2, hello_version_msg/1]).
--export([next_seqnum/1,
+-export([next_seqnum/3,
supported_algorithms/0, supported_algorithms/1,
default_algorithms/0, default_algorithms/1,
clear_default_algorithms_env/0,
@@ -296,7 +295,12 @@
hello_version_msg(Data) ->
[Data,"\r\n"].
-next_seqnum(SeqNum) ->
+next_seqnum({State, _Role, init}, 16#ffffffff,
+ #ssh{algorithms = #alg{kex_strict_negotiated = true}})
+ when State == kexinit; State == key_exchange; State == new_keys ->
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ io_lib:format("KEX strict violation: recv_sequence = 16#ffffffff", []));
+next_seqnum(_State, SeqNum, _) ->
(SeqNum + 1) band 16#ffffffff.
is_valid_mac(_, _ , #ssh{recv_mac_size = 0}) ->
@@ -400,8 +404,9 @@
key_exchange_first_msg(Algos#alg.kex,
Ssh#ssh{algorithms = Algos})
catch
- Class:Error ->
- Msg = kexinit_error(Class, Error, client, Own, CounterPart),
+ Class:Reason0 ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ Msg = kexinit_error(Class, Reason, client, Own, CounterPart, Ssh),
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg)
end;
@@ -417,31 +422,38 @@
Algos ->
{ok, Ssh#ssh{algorithms = Algos}}
catch
- Class:Error ->
- Msg = kexinit_error(Class, Error, server, Own, CounterPart),
+ Class:Reason0 ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ Msg = kexinit_error(Class, Reason, server, Own, CounterPart, Ssh),
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg)
end.
-kexinit_error(Class, Error, Role, Own, CounterPart) ->
+kexinit_error(Class, Error, Role, Own, CounterPart, Ssh) ->
{Fmt,Args} =
case {Class,Error} of
{error, {badmatch,{false,Alg}}} ->
{Txt,W,C} = alg_info(Role, Alg),
- {"No common ~s algorithm,~n"
- " we have:~n ~s~n"
- " peer have:~n ~s~n",
- [Txt,
- lists:join(", ", element(W,Own)),
- lists:join(", ", element(C,CounterPart))
- ]};
+ MsgFun =
+ fun(debug) ->
+ {"No common ~s algorithm,~n"
+ " we have:~n ~s~n"
+ " peer have:~n ~s~n",
+ [Txt,
+ lists:join(", ", element(W,Own)),
+ lists:join(", ", element(C,CounterPart))]};
+ (_) ->
+ {"No common ~s algorithm", [Txt]}
+ end,
+ ?SELECT_MSG(MsgFun);
_ ->
{"Kexinit failed in ~p: ~p:~p", [Role,Class,Error]}
end,
- try io_lib:format(Fmt, Args) of
+ try io_lib:format(Fmt, Args, [{chars_limit, ssh_lib:max_log_len(Ssh)}]) of
R -> R
catch
_:_ ->
- io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error])
+ io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error],
+ [{chars_limit, ssh_lib:max_log_len(Ssh)}])
end.
alg_info(client, Alg) ->
@@ -593,14 +605,19 @@
session_id = sid(Ssh1, H)}};
{error,unsupported_sign_alg} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Unsupported algorithm ~p", [SignAlg])
- )
+ io_lib:format("Unsupported algorithm ~p", [SignAlg],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end;
true ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ MsgFun =
+ fun(debug) ->
io_lib:format("Kexdh init failed, received 'e' out of bounds~n E=~p~n P=~p",
- [E,P])
- )
+ [E,P], [{chars_limit, ssh_lib:max_log_len(Opts)}]);
+ (_) ->
+ io_lib:format("Kexdh init failed, received 'e' out of bounds", [],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}] )
+ end,
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
end.
handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey,
@@ -621,14 +638,15 @@
session_id = sid(Ssh, H)})};
Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Kexdh init failed. Verify host key: ~p",[Error])
+ io_lib:format("Kexdh init failed. Verify host key: ~p",[Error],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
)
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
io_lib:format("Kexdh init failed, received 'f' out of bounds~n F=~p~n P=~p",
- [F,P])
+ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
)
end.
@@ -654,7 +672,8 @@
}};
{error,_} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("No possible diffie-hellman-group-exchange group found",[])
+ io_lib:format("No possible diffie-hellman-group-exchange group found",[],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}])
)
end;
@@ -686,8 +705,8 @@
}};
{error,_} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("No possible diffie-hellman-group-exchange group found",[])
- )
+ io_lib:format("No possible diffie-hellman-group-exchange group found",[],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end;
handle_kex_dh_gex_request(_, _) ->
@@ -713,7 +732,6 @@
{Public, Private} = generate_key(dh, [P,G,2*Sz]),
{SshPacket, Ssh1} =
ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0), % Pub = G^Priv mod P (def)
-
{ok, SshPacket,
Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}.
@@ -744,19 +762,22 @@
}};
{error,unsupported_sign_alg} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Unsupported algorithm ~p", [SignAlg])
- )
+ io_lib:format("Unsupported algorithm ~p", [SignAlg],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- "Kexdh init failed, received 'k' out of bounds"
- )
+ "Kexdh init failed, received 'k' out of bounds")
end;
true ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n E=~p~n P=~p",
- [E,P])
- )
+ MsgFun =
+ fun(debug) ->
+ io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n"
+ " E=~p~n P=~p", [E,P]);
+ (_) ->
+ io_lib:format("Kexdh gex init failed, received 'e' out of bounds", [])
+ end,
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
end.
handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostKey,
@@ -781,20 +802,18 @@
session_id = sid(Ssh, H)})};
Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Kexdh gex reply failed. Verify host key: ~p",[Error])
- )
+ io_lib:format("Kexdh gex reply failed. Verify host key: ~p",
+ [Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- "Kexdh gex init failed, 'K' out of bounds"
- )
+ "Kexdh gex init failed, 'K' out of bounds")
end;
true ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
io_lib:format("Kexdh gex init failed, received 'f' out of bounds~n F=~p~n P=~p",
- [F,P])
- )
+ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end.
%%%----------------------------------------------------------------
@@ -828,17 +847,25 @@
session_id = sid(Ssh1, H)}};
{error,unsupported_sign_alg} ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("Unsupported algorithm ~p", [SignAlg])
- )
+ io_lib:format("Unsupported algorithm ~p", [SignAlg],
+ [{chars_limit, ssh_lib:max_log_len(Opts)}]))
end
catch
- Class:Error ->
- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
+ Class:Reason0 ->
+ Reason = ssh_lib:trim_reason(Reason0),
+ MsgFun =
+ fun(debug) ->
io_lib:format("ECDH compute key failed in server: ~p:~p~n"
"Kex: ~p, Curve: ~p~n"
"PeerPublic: ~p",
- [Class,Error,Kex,Curve,PeerPublic])
- )
+ [Class,Reason,Kex,Curve,PeerPublic],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]);
+ (_) ->
+ io_lib:format("ECDH compute key failed in server: ~p:~p",
+ [Class,Reason],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])
+ end,
+ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun))
end.
handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey,
@@ -861,15 +888,14 @@
session_id = sid(Ssh, H)})};
Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
- io_lib:format("ECDH reply failed. Verify host key: ~p",[Error])
- )
+ io_lib:format("ECDH reply failed. Verify host key: ~p",[Error],
+ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end
catch
Class:Error ->
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
io_lib:format("Peer ECDH public key seem invalid: ~p:~p",
- [Class,Error])
- )
+ [Class,Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]))
end.
@@ -1081,7 +1107,7 @@
%% algorithm. Each string MUST contain at least one algorithm name.
select_algorithm(Role, Client, Server,
#ssh{opts = Opts,
- kex_strict_negotiated = KexStrictNegotiated0},
+ kex_strict_negotiated = KexStrictNegotiated0},
ReNeg) ->
KexStrictNegotiated =
case ReNeg of
@@ -1106,7 +1132,6 @@
_ ->
KexStrictNegotiated0
end,
-
{Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server),
{SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server),
@@ -1120,14 +1145,9 @@
Server#ssh_msg_kexinit.languages_client_to_server),
S_Lng = select(Client#ssh_msg_kexinit.languages_server_to_client,
Server#ssh_msg_kexinit.languages_server_to_client),
- HKey = select_all(Client#ssh_msg_kexinit.server_host_key_algorithms,
- Server#ssh_msg_kexinit.server_host_key_algorithms),
- HK = case HKey of
- [] -> undefined;
- [HK0|_] -> HK0
- end,
- %% Fixme verify Kex against HKey list and algorithms
-
+ HKey = select(Client#ssh_msg_kexinit.server_host_key_algorithms,
+ Server#ssh_msg_kexinit.server_host_key_algorithms),
+ %% FIXME verify Kex against HKey list and algorithms (see RFC4253 sec 7.1)
Kex = select(Client#ssh_msg_kexinit.kex_algorithms,
Server#ssh_msg_kexinit.kex_algorithms),
@@ -1147,7 +1167,7 @@
?GET_OPT(recv_ext_info,Opts),
{ok, #alg{kex = Kex,
- hkey = HK,
+ hkey = HKey,
encrypt = Encrypt,
decrypt = Decrypt,
send_mac = SendMac,
@@ -1299,38 +1319,27 @@
{ok,SSH3} = decompress_final(SSH2),
SSH3.
-
-select_all(CL, SL) when length(CL) + length(SL) < ?MAX_NUM_ALGORITHMS ->
- %% algorithms only used by client
- %% NOTE: an algorithm occurring more than once in CL will still be present
- %% in CLonly. This is not a problem for nice clients.
- CLonly = CL -- SL,
-
- %% algorithms used by client and server (client pref)
- lists:foldr(fun(ALG, Acc) ->
- try [list_to_existing_atom(ALG) | Acc]
- catch
- %% If an malicious client uses the same non-existing algorithm twice,
- %% we will end up here
- _:_ -> Acc
- end
- end, [], (CL -- CLonly));
-
-select_all(CL, SL) ->
- Error = lists:concat(["Received too many algorithms (",length(CL),"+",length(SL)," >= ",?MAX_NUM_ALGORITHMS,")."]),
- ?DISCONNECT(?SSH_DISCONNECT_PROTOCOL_ERROR,
- Error).
-
-
select([], []) ->
none;
select(CL, SL) ->
- C = case select_all(CL,SL) of
- [] -> undefined;
- [ALG|_] -> ALG
- end,
- C.
-
+ select_first(CL, SL).
+
+select_first([ClientAlg | ClientRest], SL) ->
+ case lists:member(ClientAlg, SL) of
+ true ->
+ try list_to_existing_atom(ClientAlg) of
+ Alg when is_atom(Alg) ->
+ Alg
+ catch
+ error:badarg ->
+ select_first(ClientRest, SL)
+ end;
+ false ->
+ select_first(ClientRest, SL)
+ end;
+select_first([], _) ->
+ undefined.
+
ssh_packet(#ssh_msg_kexinit{} = Msg, Ssh0) ->
BinMsg = ssh_message:encode(Msg),
Ssh = key_init(Ssh0#ssh.role, Ssh0, BinMsg),
diff -ruN a/lib/ssh/test/.gitignore b/lib/ssh/test/.gitignore
--- a/lib/ssh/test/.gitignore 1970-01-01 09:30:00.000000000 +0930
+++ b/lib/ssh/test/.gitignore 2023-04-26 15:10:42.429802138 +0930
@@ -0,0 +1,7 @@
+*COVER.html
+
+ssh_sftp_SUITE_data/test_data*
+
+property_test/ssh_eqc_client_server_dirs/system
+property_test/ssh_eqc_client_server_dirs/user
+
diff -ruN a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl
--- a/lib/ssh/test/ssh_connection_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_connection_SUITE.erl 2025-12-17 17:25:02.079556456 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -109,6 +109,7 @@
stop_listener/1,
trap_exit_connect/1,
trap_exit_daemon/1,
+ handler_down_before_open/1,
ssh_exec_echo/2 % called as an MFA
]).
@@ -180,7 +181,8 @@
stop_listener,
no_sensitive_leak,
start_subsystem_on_closed_channel,
- max_channels_option
+ max_channels_option,
+ handler_down_before_open
].
groups() ->
[{openssh, [], payload() ++ ptty() ++ sock()}].
@@ -348,7 +350,7 @@
print_interesting_events([], Cnt) ->
{ok, Cnt};
print_interesting_events([#{level := Level} = Event | Tail], Cnt)
- when Level /= info, Level /= notice ->
+ when Level /= info, Level /= notice, Level /= debug ->
ct:log("------------~nInteresting event found:~n~p~n==========~n", [Event]),
print_interesting_events(Tail, Cnt + 1);
print_interesting_events([_|Tail], Cnt) ->
@@ -623,6 +625,9 @@
%% build 10MB binary
Data = << <<X:32>> || X <- lists:seq(1,2500000)>>,
+ %% pre-adjust receive window so the other end doesn't block
+ ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)),
+
ct:log("sending ~p byte binary~n",[size(Data)]),
ok = ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000),
ok = ssh_connection:send_eof(ConnectionRef, ChannelId0),
@@ -764,11 +769,8 @@
%% done with transferring data towards client and terminates the
%% channel (this results with {error, closed} return value from
%% ssh_connection:send on the client side)
-%%- interrupted_send used to be interrupted when ssh_echo_server ran
-%% out of data window and closed channel
-%%- but with automatic window adjustment, above condition is not taking
-%% place, so ssh_echo_server continues sending data until it is done
-%%- so ssh_connection:send returns 'ok'
+%%- interrupted_send is interrupted when ssh_echo_server ran
+%% out of ssh data window and closed channel
small_interrupted_send(Config) ->
K = 1024,
SendSize = 10 * K * K,
@@ -807,7 +809,7 @@
fun() ->
ct:log("~p:~p open channel",[?MODULE,?LINE]),
{ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
- ct:log("~p:~p start subsystem", [?MODULE,?LINE]),
+ ct:log("~p:~p start ssh subsystem", [?MODULE,?LINE]),
case ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", infinity) of
success ->
Parent ! {self(), channelId, ChannelId},
@@ -840,6 +842,7 @@
SenderPid = spawn(fun() ->
Parent ! {self(), ssh_connection:send(ConnectionRef, ChannelId, Data, 30000)}
end),
+ ct:log("SenderPid = ~p", [SenderPid]),
receive
{ResultPid, result, {fail, Fail}} ->
ct:log("~p:~p Listener failed: ~p", [?MODULE,?LINE,Fail]),
@@ -858,7 +861,7 @@
ct:log("~p:~p Not expected send result: ~p",[?MODULE,?LINE,Msg]),
{fail, "Not expected msg"}
end;
- {SenderPid, SenderResult} ->
+ {SenderPid, {error, closed}} ->
ct:log("~p:~p ~p - That's what we expect, "
"but client channel handler has not reported yet",
[?MODULE,?LINE, SenderResult]),
@@ -1293,7 +1296,7 @@
do_start_shell_exec_fun(Fun, Command, Expect, ExpectType, Config) ->
DefaultReceiveFun =
- fun(ConnectionRef, ChannelId, Expect, ExpectType) ->
+ fun(ConnectionRef, ChannelId, _Expect, _ExpectType) ->
receive
{ssh_cm, ConnectionRef, {data, ChannelId, ExpectType, Expect}} ->
ok
@@ -1630,6 +1633,8 @@
end.
kex_error(Config) ->
+ #{level := Level} = logger:get_primary_config(),
+ ok = logger:set_primary_config(level, debug),
PrivDir = proplists:get_value(priv_dir, Config),
UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
file:make_dir(UserDir),
@@ -1650,6 +1655,10 @@
ok % Other msg
end,
self()),
+ Cleanup = fun() ->
+ ok = logger:remove_handler(kex_error),
+ ok = logger:set_primary_config(level, Level)
+ end,
try
ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
{user, "foo"},
@@ -1667,7 +1676,7 @@
%% ok
receive
{Ref, ErrMsgTxt} ->
- ok = logger:remove_handler(kex_error),
+ Cleanup(),
ct:log("ErrMsgTxt = ~n~s", [ErrMsgTxt]),
Lines = lists:map(fun string:trim/1, string:tokens(ErrMsgTxt, "\n")),
OK = (lists:all(fun(S) -> lists:member(S,Lines) end,
@@ -1685,12 +1694,12 @@
ct:fail("unexpected error text msg", [])
end
after 20000 ->
- ok = logger:remove_handler(kex_error),
+ Cleanup(),
ct:fail("timeout", [])
end;
error:{badmatch,{error,_}} ->
- ok = logger:remove_handler(kex_error),
+ Cleanup(),
ct:fail("unexpected error msg", [])
end.
@@ -1942,6 +1951,138 @@
ssh:close(ConnectionRef),
ssh:stop_daemon(Pid).
+handler_down_before_open(Config) ->
+ %% Start echo subsystem with a delay in init() - until a signal is received
+ %% One client opens a channel on the connection
+ %% the other client requests the echo subsystem on the second channel and then immediately goes down
+ %% the test monitors the client and when receiving 'DOWN' signals 'echo' to proceed
+ %% a) there should be no crash after 'channel-open-confirmation'
+ %% b) there should be proper 'channel-close' exchange
+ %% c) the 'exec' channel should not be affected after the 'echo' channel goes down
+ PrivDir = proplists:get_value(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = proplists:get_value(data_dir, Config),
+ Parent = self(),
+ EchoSS_spec = {ssh_echo_server, [8, [{dbg, true}, {parent, Parent}]]},
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, UserDir},
+ {password, "morot"},
+ {exec, fun ssh_exec_echo/1},
+ {subsystems, [{"echo_n",EchoSS_spec}]}]),
+ ct:log("~p:~p connect", [?MODULE,?LINE]),
+ ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ct:log("~p:~p connected", [?MODULE,?LINE]),
+
+ ExecChannelPid =
+ spawn(
+ fun() ->
+ {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
+
+ %% This is to get peer's connection handler PID ({conn_peer ...} below) and suspend it
+ {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity),
+ ssh_connection:subsystem(ConnectionRef, ChannelId1, "echo_n", infinity),
+ ssh_connection:close(ConnectionRef, ChannelId1),
+ receive
+ {ssh_cm, ConnectionRef, {closed, 1}} -> ok
+ end,
+
+ Parent ! {self(), channelId, ChannelId0},
+ Result = receive
+ cmd ->
+ ct:log("~p:~p Channel ~p executing", [?MODULE, ?LINE, ChannelId0]),
+ success = ssh_connection:exec(ConnectionRef, ChannelId0, "testing", infinity),
+ Expect = <<"echo testing\n">>,
+ ExpSz = size(Expect),
+ receive
+ {ssh_cm, ConnectionRef, {data, ChannelId0, 0,
+ <<Expect:ExpSz/binary, _/binary>>}} = R ->
+ ct:log("~p:~p Got expected ~p",[?MODULE,?LINE, R]),
+ ok;
+ Other ->
+ ct:log("~p:~p Got unexpected ~p~nExpect: ~p~n",
+ [?MODULE,?LINE, Other, {ssh_cm, ConnectionRef,
+ {data, ChannelId0, 0, Expect}}]),
+ {fail, "Unexpected data"}
+ after 5000 ->
+ {fail, "Exec Timeout"}
+ end;
+ stop -> {fail, "Stopped"}
+ end,
+ Parent ! {self(), Result}
+ end),
+ try
+ receive
+ {ExecChannelPid, channelId, ExId} ->
+ ct:log("~p:~p Channel that should stay: ~p pid ~p",
+ [?MODULE, ?LINE, ExId, ExecChannelPid]),
+ %% This is sent by the echo subsystem as a reaction to channel1 above
+ ConnPeer = receive {conn_peer, CM} -> CM end,
+ %% The sole purpose of this channel is to go down
+ %% before the opening procedure is complete
+ DownChannelPid = spawn(
+ fun() ->
+ ct:log("~p:~p open channel (incomplete)",[?MODULE,?LINE]),
+ Parent ! {self(), channelId, ok},
+ %% This is to prevent the peer from answering our 'channel-open' in time
+ sys:suspend(ConnPeer),
+ {ok, _} = ssh_connection:session_channel(ConnectionRef, infinity)
+ end),
+ MonRef = erlang:monitor(process, DownChannelPid),
+ receive
+ {DownChannelPid, channelId, ok} ->
+ ct:log("~p:~p Channel handler that won't continue: pid ~p",
+ [?MODULE, ?LINE, DownChannelPid]),
+ ensure_channels(ConnectionRef, 2),
+ channel_down_sequence(DownChannelPid, ExecChannelPid,
+ ExId, MonRef, ConnectionRef, ConnPeer)
+ end
+ end,
+ ensure_channels(ConnectionRef, 0)
+ after
+ ssh:close(ConnectionRef),
+ ssh:stop_daemon(Pid)
+ end.
+
+ensure_channels(ConnRef, Expected) ->
+ {ok, ChannelList} = ssh_connection_handler:info(ConnRef),
+ do_ensure_channels(ConnRef, Expected, length(ChannelList)).
+
+do_ensure_channels(_ConnRef, NumExpected, NumExpected) ->
+ ok;
+do_ensure_channels(ConnRef, NumExpected, _ChannelListLen) ->
+ ct:sleep(100),
+ {ok, ChannelList} = ssh_connection_handler:info(ConnRef),
+ do_ensure_channels(ConnRef, NumExpected, length(ChannelList)).
+
+channel_down_sequence(DownChannelPid, ExecChannelPid, ExecChannelId, MonRef, ConnRef, Peer) ->
+ ct:log("~p:~p sending order to ~p to go down", [?MODULE, ?LINE, DownChannelPid]),
+ exit(DownChannelPid, die),
+ receive {'DOWN', MonRef, _, _, _} -> ok end,
+ ct:log("~p:~p order executed, sending order to ~p to proceed", [?MODULE, ?LINE, Peer]),
+ %% Resume the peer connection to let it clean up among its channels
+ sys:resume(Peer),
+ ensure_channels(ConnRef, 1),
+ ExecChannelPid ! cmd,
+ try
+ receive
+ {ExecChannelPid, ok} ->
+ ct:log("~p:~p expected exec result: ~p", [?MODULE, ?LINE, ok]),
+ ok;
+ {ExecChannelPid, Result} ->
+ ct:log("~p:~p Unexpected exec result: ~p", [?MODULE, ?LINE, Result]),
+ {fail, "Unexpected exec result"}
+ after 5000 ->
+ {fail, "Exec result timeout"}
+ end
+ after
+ ssh_connection:close(ConnRef, ExecChannelId)
+ end.
+
%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
@@ -1966,26 +2107,35 @@
_ ->
receive_bytes(ConnectionRef, ChannelId0, N * byte_size(ExpectedBin), 0)
end,
-
%% receive close messages
- receive
- {ssh_cm, ConnectionRef, {eof, ChannelId0}} ->
- ok
+ CloseMessages =
+ [{ssh_cm, ConnectionRef, {eof, ChannelId0}},
+ {ssh_cm, ConnectionRef, {closed, ChannelId0}}],
+ Timeout = 10000,
+ [receive
+ M ->
+ ct:log("Received M = ~w", [M]),
+ ok
+ after
+ Timeout ->
+ ct:log("M = ~w not found !", [M]),
+ ct:log("Messages in queue =~n~p", [process_info(self(), messages)]),
+ ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ end || M <- CloseMessages],
+ receive
+ %% 141 is exit status of `yes testing | head -n 1` on tcsh
+ %% other shells return 0
+ ExitMsg = {ssh_cm, ConnectionRef, {exit_status, ChannelId0, ExitStatus}}
+ when ExitStatus == 0; ExitStatus == 141 ->
+ ct:log("Received M = ~w", [ExitMsg]),
+ ok
after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
+ Timeout ->
+ ct:log("Acceptable exit status not received"),
+ ct:log("Messages in queue =~n~p", [process_info(self(), messages)]),
+ ct:fail("timeout ~p:~p",[?MODULE,?LINE])
end,
- receive
- {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} ->
- ok
- after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
- end,
- receive
- {ssh_cm, ConnectionRef,{closed, ChannelId0}} ->
- ok
- after
- 10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
- end.
+ ok.
%%--------------------------------------------------------------------
@@ -2122,6 +2272,7 @@
{ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} when is_binary(Data) ->
ct:log("~p:~p collect_data: received ~p bytes. total ~p bytes, want ~p more",
[?MODULE,?LINE,size(Data),Sum+size(Data),EchoSize-Sum]),
+ ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)),
collect_data(ConnectionRef, ChannelId, EchoSize, [Data | Acc], Sum+size(Data));
{ssh_cm, ConnectionRef, Msg={eof, ChannelId}} ->
collect_data_report_end(Acc, Msg, EchoSize);
@@ -2185,6 +2336,7 @@
"~p bytes Received/Total = ~p/~p bytes",
Args = [Budget, byte_size(D), AccSize + byte_size(D)],
ct:log(Fmt, Args),
+ ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(D)),
receive_bytes(ConnectionRef, ChannelId0,
Budget - byte_size(D), AccSize + byte_size(D))
after
diff -ruN a/lib/ssh/test/ssh_echo_server.erl b/lib/ssh/test/ssh_echo_server.erl
--- a/lib/ssh/test/ssh_echo_server.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_echo_server.erl 2025-12-17 17:25:02.079556456 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. 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.
@@ -27,7 +27,8 @@
n,
id,
cm,
- dbg = false
+ dbg = false,
+ parent
}).
-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]).
@@ -42,13 +43,19 @@
{ok, #state{n = N}};
init([N,Opts]) ->
State = #state{n = N,
- dbg = proplists:get_value(dbg,Opts,false)
+ dbg = proplists:get_value(dbg,Opts,false),
+ parent = proplists:get_value(parent, Opts)
},
?DBG(State, "init([~p])",[N]),
{ok, State}.
handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, State) ->
?DBG(State, "ssh_channel_up Cid=~p ConnMngr=~p",[ChannelId,ConnectionManager]),
+ Pid = State#state.parent,
+ if Pid /= undefined ->
+ Pid ! {conn_peer, ConnectionManager};
+ true -> ok
+ end,
{ok, State#state{id = ChannelId,
cm = ConnectionManager}}.
diff -ruN a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl
--- a/lib/ssh/test/ssh_options_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_options_SUITE.erl 2025-12-17 17:25:02.080556468 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -88,7 +88,8 @@
daemon_replace_options_simple/1,
daemon_replace_options_algs/1,
daemon_replace_options_algs_connect/1,
- daemon_replace_options_algs_conf_file/1
+ daemon_replace_options_algs_conf_file/1,
+ daemon_replace_options_not_found/1
]).
%%% Common test callbacks
@@ -110,7 +111,7 @@
suite() ->
[{ct_hooks,[ts_install_cth]},
- {timetrap,{seconds,60}}].
+ {timetrap,{seconds,15}}].
all() ->
[connectfun_disconnectfun_server,
@@ -159,6 +160,7 @@
daemon_replace_options_algs,
daemon_replace_options_algs_connect,
daemon_replace_options_algs_conf_file,
+ daemon_replace_options_not_found,
{group, hardening_tests}
].
@@ -1563,7 +1565,7 @@
%%--------------------------------------------------------------------
max_sessions_drops_tcp_connects() ->
- [{timetrap,{minutes,20}}].
+ [{timetrap,{minutes,2}}].
max_sessions_drops_tcp_connects(Config) ->
MaxSessions = 20,
@@ -2033,6 +2035,14 @@
end.
%%--------------------------------------------------------------------
+daemon_replace_options_not_found(_Config) ->
+ %% when the daemon doesn't exist the error should be the same
+ %% in daemon_info and daemon_replace_options
+ %% which is {error, bad_daemon_ref}
+ Error = ssh:daemon_info(self()),
+ Error = ssh:daemon_replace_options(self(), []).
+
+%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
diff -ruN a/lib/ssh/test/ssh_protocol_SUITE.erl b/lib/ssh/test/ssh_protocol_SUITE.erl
--- a/lib/ssh/test/ssh_protocol_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_protocol_SUITE.erl 2025-12-17 17:25:02.080556468 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2008-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2008-2025. 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.
@@ -26,6 +26,7 @@
-include_lib("kernel/include/inet.hrl").
-include("ssh.hrl"). % ?UINT32, ?BYTE, #ssh{} ...
-include("ssh_transport.hrl").
+-include("ssh_connect.hrl").
-include("ssh_auth.hrl").
-include("ssh_test_lib.hrl").
@@ -48,6 +49,7 @@
bad_service_name_then_correct/1,
bad_very_long_service_name/1,
client_handles_keyboard_interactive_0_pwds/1,
+ client_handles_banner_keyboard_interactive/1,
client_info_line/1,
do_gex_client_init/3,
do_gex_client_init_old/3,
@@ -55,7 +57,10 @@
ext_info_c/1,
ext_info_s/1,
kex_strict_negotiated/1,
- kex_strict_msg_ignore/1,
+ kex_strict_violation_key_exchange/1,
+ kex_strict_violation_new_keys/1,
+ kex_strict_violation/1,
+ kex_strict_violation_2/1,
kex_strict_msg_unknown/1,
gex_client_init_option_groups/1,
gex_client_init_option_groups_file/1,
@@ -73,6 +78,8 @@
modify_rm/1,
no_common_alg_client_disconnects/1,
no_common_alg_server_disconnects/1,
+ custom_kexinit/1,
+ early_rce/1,
no_ext_info_s1/1,
no_ext_info_s2/1,
packet_length_too_large/1,
@@ -80,7 +87,9 @@
preferred_algorithms/1,
service_name_length_too_large/1,
service_name_length_too_short/1,
- client_close_after_hello/1
+ client_close_after_hello/1,
+ channel_close_timeout/1,
+ extra_ssh_msg_service_request/1
]).
-define(NEWLINE, <<"\r\n">>).
@@ -94,11 +103,19 @@
[{client2server,Ciphs}, {server2client,Ciphs}]
end)()
).
-
-
-define(v(Key, Config), proplists:get_value(Key, Config)).
-define(v(Key, Config, Default), proplists:get_value(Key, Config, Default)).
-
+-define(HARDCODED_KEXDH_REPLY,
+ #ssh_msg_kexdh_reply{
+ public_host_key = {{{'ECPoint',<<73,72,235,162,96,101,154,59,217,114,123,192,96,105,250,29,214,76,60,63,167,21,221,118,246,168,152,2,7,172,137,125>>},
+ {namedCurve,{1,3,101,112}}},
+ 'ssh-ed25519'},
+ f = 18504393053016436370762156176197081926381112956345797067569792020930728564439992620494295053804030674742529174859108487694089045521619258420515443400605141150065440678508889060925968846155921972385560196703381004650914261218463420313738628465563288022895912907728767735629532940627575655703806353550720122093175255090704443612257683903495753071530605378193139909567971489952258218767352348904221407081210633467414579377014704081235998044497191940270966762124544755076128392259615566530695493013708460088312025006678879288856957348606386230195080105197251789635675011844976120745546472873505352732719507783227210178188,
+ h_sig = <<90,247,44,240,136,196,82,215,56,165,53,33,230,101,253,
+ 34,112,201,21,131,162,169,10,129,174,14,69,25,39,174,
+ 92,210,130,249,103,2,215,245,7,213,110,235,136,134,11,
+ 124,248,139,79,17,225,77,125,182,204,84,137,167,99,186,
+ 167,42,192,10>>}).
%%--------------------------------------------------------------------
%% Common Test interface functions -----------------------------------
@@ -111,6 +128,7 @@
all() ->
[{group,tool_tests},
client_info_line,
+ early_rce,
{group,kex},
{group,service_requests},
{group,authentication},
@@ -118,7 +136,8 @@
{group,field_size_error},
{group,ext_info},
{group,preferred_algorithms},
- {group,client_close_early}
+ {group,client_close_early},
+ {group,channel_close}
].
groups() ->
@@ -129,11 +148,10 @@
]},
{packet_size_error, [], [packet_length_too_large,
packet_length_too_short]},
-
{field_size_error, [], [service_name_length_too_large,
service_name_length_too_short]},
-
- {kex, [], [no_common_alg_server_disconnects,
+ {kex, [], [custom_kexinit,
+ no_common_alg_server_disconnects,
no_common_alg_client_disconnects,
gex_client_init_option_groups,
gex_server_gex_limit,
@@ -142,15 +160,20 @@
gex_client_old_request_exact,
gex_client_old_request_noexact,
kex_strict_negotiated,
- kex_strict_msg_ignore,
+ kex_strict_violation_key_exchange,
+ kex_strict_violation_new_keys,
+ kex_strict_violation,
+ kex_strict_violation_2,
kex_strict_msg_unknown]},
{service_requests, [], [bad_service_name,
bad_long_service_name,
bad_very_long_service_name,
empty_service_name,
- bad_service_name_then_correct
+ bad_service_name_then_correct,
+ extra_ssh_msg_service_request
]},
- {authentication, [], [client_handles_keyboard_interactive_0_pwds
+ {authentication, [], [client_handles_keyboard_interactive_0_pwds,
+ client_handles_banner_keyboard_interactive
]},
{ext_info, [], [no_ext_info_s1,
no_ext_info_s2,
@@ -163,8 +186,8 @@
modify_rm,
modify_combo
]},
- {client_close_early, [], [client_close_after_hello
- ]}
+ {client_close_early, [], [client_close_after_hello]},
+ {channel_close, [], [channel_close_timeout]}
].
@@ -174,7 +197,8 @@
end_per_suite(Config) ->
stop_apps(Config).
-init_per_testcase(no_common_alg_server_disconnects, Config) ->
+init_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects;
+ Tc == custom_kexinit ->
start_std_daemon(Config, [{preferred_algorithms,[{public_key,['ssh-rsa']},
{cipher,?DEFAULT_CIPHERS}
]}]);
@@ -220,7 +244,8 @@
init_per_testcase(_TestCase, Config) ->
check_std_daemon_works(Config, ?LINE).
-end_per_testcase(no_common_alg_server_disconnects, Config) ->
+end_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects;
+ Tc == custom_kexinit ->
stop_std_daemon(Config);
end_per_testcase(kex_strict_negotiated, Config) ->
Config;
@@ -381,6 +406,90 @@
]
).
+early_rce(Config) ->
+ {ok,InitialState} =
+ ssh_trpt_test_lib:exec([{set_options, [print_ops, print_seqnums, print_messages]}]),
+ TypeOpen = "session",
+ ChannelId = 0,
+ WinSz = 425984,
+ PktSz = 65536,
+ DataOpen = <<>>,
+ SshMsgChannelOpen = ssh_connection:channel_open_msg(TypeOpen, ChannelId, WinSz, PktSz, DataOpen),
+
+ Id = 0,
+ TypeReq = "exec",
+ WantReply = true,
+ DataReq = <<?STRING(<<"lists:seq(1,10).">>)>>,
+ SshMsgChannelRequest =
+ ssh_connection:channel_request_msg(Id, TypeReq, WantReply, DataReq),
+ {ok, _AfterKexState} =
+ ssh_trpt_test_lib:exec(
+ [{connect,
+ server_host(Config),server_port(Config),
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {silently_accept_hosts, true},
+ {recv_ext_info, false},
+ {user_dir, user_dir(Config)},
+ {user_interaction, false}
+ | proplists:get_value(extra_options,Config,[])]},
+ receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, SshMsgChannelOpen},
+ {send, SshMsgChannelRequest},
+ {match, disconnect(), receive_msg}
+ ], InitialState),
+ ok.
+
+custom_kexinit(Config) ->
+ %% 16#C0 value causes unicode:characters_to_list to return a big error value
+ Trash = lists:duplicate(260_000, 16#C0),
+ FunnyAlg = "curve25519-sha256",
+ KexInit =
+ #ssh_msg_kexinit{cookie = <<"Ã/Ï!9zñKá:ñÀv¿JÜ">>,
+ kex_algorithms =
+ [FunnyAlg ++ Trash],
+ server_host_key_algorithms = ["ssh-rsa"],
+ encryption_algorithms_client_to_server =
+ ["aes256-ctr","aes192-ctr","aes128-ctr","aes128-cbc","3des-cbc"],
+ encryption_algorithms_server_to_client =
+ ["aes256-ctr","aes192-ctr","aes128-ctr","aes128-cbc","3des-cbc"],
+ mac_algorithms_client_to_server =
+ ["hmac-sha2-512-etm@openssh.com","hmac-sha2-256-etm@openssh.com",
+ "hmac-sha2-512","hmac-sha2-256","hmac-sha1-etm@openssh.com","hmac-sha1"],
+ mac_algorithms_server_to_client =
+ ["hmac-sha2-512-etm@openssh.com","hmac-sha2-256-etm@openssh.com",
+ "hmac-sha2-512","hmac-sha2-256","hmac-sha1-etm@openssh.com","hmac-sha1"],
+ compression_algorithms_client_to_server = ["none","zlib@openssh.com","zlib"],
+ compression_algorithms_server_to_client = ["none","zlib@openssh.com","zlib"],
+ languages_client_to_server = [],
+ languages_server_to_client = [],
+ first_kex_packet_follows = false,
+ reserved = 0
+ },
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, {print_messages,detail}]},
+ {connect,
+ server_host(Config),server_port(Config),
+ [{silently_accept_hosts, true},
+ {user_dir, user_dir(Config)},
+ {user_interaction, false},
+ {preferred_algorithms,[{public_key,['ssh-rsa']},
+ {cipher,?DEFAULT_CIPHERS}
+ ]}
+ ]},
+ receive_hello,
+ {send, hello},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, KexInit}, % with server unsupported 'ssh-dss' !
+ {match, disconnect(), receive_msg}
+ ]
+ ).
+
%%--------------------------------------------------------------------
%%% Algo negotiation fail. This should result in a ssh_msg_disconnect
%%% being sent from the client.
@@ -689,7 +798,82 @@
]}]
).
+%%%--------------------------------------------------------------------
+%%% SSH_MSG_USERAUTH_BANNER can be sent at any time during user auth.
+%%% The following test mimics a SSH server implementation that sends the banner
+%%% immediately before sending SSH_MSG_USERAUTH_SUCCESS.
+client_handles_banner_keyboard_interactive(Config) ->
+ {User,_Pwd} = server_user_password(Config),
+
+ %% Create a listening socket as server socket:
+ {ok,InitialState} = ssh_trpt_test_lib:exec(listen),
+ HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+
+ %% Start a process handling one connection on the server side:
+ spawn_link(
+ fun() ->
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_messages]},
+ {accept, [{system_dir, system_dir(Config)},
+ {user_dir, user_dir(Config)}]},
+ receive_hello,
+ {send, hello},
+
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+
+ {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_reply},
+
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+
+ {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_service_accept{name="ssh-userauth"}},
+
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="none",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_failure{authentications = "keyboard-interactive",
+ partial_success = false}},
+
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="keyboard-interactive",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_info_request{name = "",
+ instruction = "",
+ language_tag = "",
+ num_prompts = 1,
+ data = <<0,0,0,10,80,97,115,115,119,111,114,100,58,32,0>>
+ }},
+ {match, #ssh_msg_userauth_info_response{num_responses = 1,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_info_request{name = "",
+ instruction = "",
+ language_tag = "",
+ num_prompts = 0,
+ data = <<>>
+ }},
+ {match, #ssh_msg_userauth_info_response{num_responses = 0,
+ data = <<>>,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_banner{message = "Banner\n"}},
+ {send, #ssh_msg_userauth_success{}},
+ close_socket,
+ print_state
+ ],
+ InitialState)
+ end),
+ %% and finally connect to it with a regular Erlang SSH client:
+ {ok,_} = std_connect(HostPort, Config,
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]}]
+ ).
%%%--------------------------------------------------------------------
client_info_line(Config) ->
@@ -843,22 +1027,173 @@
ssh_test_lib:rm_log_handler(),
ok.
-%% Connect to an erlang server and inject unexpected SSH ignore
-kex_strict_msg_ignore(Config) ->
- ct:log("START: ~p~n=================================", [?FUNCTION_NAME]),
- ExpectedReason = "strict KEX violation: unexpected SSH_MSG_IGNORE",
- TestMessages =
- [{send, ssh_msg_ignore},
- {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
- {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
- kex_strict_helper(Config, TestMessages, ExpectedReason).
+%% Connect to an erlang server and inject unexpected SSH message
+%% ssh_fsm_kexinit in key_exchange state
+kex_strict_violation_key_exchange(Config) ->
+ ExpectedReason = "KEX strict violation",
+ Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented],
+ TestProcedure =
+ fun(M) ->
+ ct:log(
+ "=================== START: ~p Message: ~p Expected Fail =================================",
+ [?FUNCTION_NAME, M]),
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, M},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]
+ end,
+ [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) ||
+ Msg <- Injections],
+ ct:log("========== END ========"),
+ ok.
+
+%% Connect to an erlang server and inject unexpected SSH message
+%% ssh_fsm_kexinit in new_keys state
+kex_strict_violation_new_keys(Config) ->
+ ExpectedReason = "KEX strict violation",
+ Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented],
+ TestProcedure =
+ fun(M) ->
+ ct:log(
+ "=================== START: ~p Message: ~p Expected Fail =================================",
+ [?FUNCTION_NAME, M]),
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {send, M},
+ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]
+ end,
+ [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) ||
+ Msg <- Injections],
+ ct:log("========== END ========"),
+ ok.
+
+%% Connect to an erlang server and inject unexpected SSH message
+%% duplicated KEXINIT
+kex_strict_violation(Config) ->
+ TestFlows =
+ [{kexinit, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexinit},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
+ {ssh_msg_kexdh_init, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init_dup},
+ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
+ {new_keys, "Message ssh_msg_newkeys in wrong state",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+ {send, #ssh_msg_newkeys{}},
+ {match, disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR), receive_msg}]},
+ {ssh_msg_unexpected_dh_gex, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ %% dh_alg is expected but dh_gex_alg is provided
+ {send, #ssh_msg_kex_dh_gex_request{min = 1000, n = 3000, max = 4000}},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
+ {wrong_role, "KEX strict violation",
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ %% client should not send message below
+ {send, ?HARDCODED_KEXDH_REPLY},
+ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}],
+ TestProcedure =
+ fun({Msg, _, P}) ->
+ ct:log(
+ "==== START: ~p (duplicated ~p) Expected Fail ====~n~p",
+ [?FUNCTION_NAME, Msg, P]),
+ P
+ end,
+ [kex_strict_helper(Config, TestProcedure(Procedure), Reason) ||
+ Procedure = {_, Reason, _} <- TestFlows],
+ ct:log("==== END ====="),
+ ok.
+
+kex_strict_violation_2(Config) ->
+ ExpectedReason = "KEX strict violation",
+ {ok, TestRef} = ssh_test_lib:add_log_handler(),
+ Level = ssh_test_lib:get_log_level(),
+ ssh_test_lib:set_log_level(debug),
+ %% Connect and negotiate keys
+ {ok, InitialState} = ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_seqnums, print_messages]}]),
+ {ok, UpToUnexpectedKexDHReply} =
+ ssh_trpt_test_lib:exec(
+ [{connect,
+ server_host(Config),server_port(Config),
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {silently_accept_hosts, true},
+ {recv_ext_info, false},
+ {user_dir, user_dir(Config)},
+ {user_interaction, false}
+ | proplists:get_value(extra_options,Config,[])
+ ]}] ++
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
+ %% client should not send message below
+ {send, ?HARDCODED_KEXDH_REPLY},
+ {match, {'or', [#ssh_msg_newkeys{_='_'},
+ disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED)]},
+ receive_msg}],
+ InitialState),
+ case ssh_trpt_test_lib:return_value(UpToUnexpectedKexDHReply) of
+ {ssh_msg_newkeys} ->
+ ct:log("1st flow - extra match for disconnect needed"),
+ ssh_trpt_test_lib:exec(
+ [{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
+ UpToUnexpectedKexDHReply);
+ _ ->
+ ct:log("2nd flow disconnect already received")
+ end,
+ ct:sleep(100),
+ {ok, Events} = ssh_test_lib:get_log_events(TestRef),
+ ssh_test_lib:rm_log_handler(),
+ ct:log("Events = ~p", [Events]),
+ true = ssh_test_lib:kex_strict_negotiated(client, Events),
+ true = ssh_test_lib:kex_strict_negotiated(server, Events),
+ true = ssh_test_lib:event_logged(server, Events, ExpectedReason),
+ ssh_test_lib:set_log_level(Level),
+ ok.
%% Connect to an erlang server and inject unexpected non-SSH binary
kex_strict_msg_unknown(Config) ->
ct:log("START: ~p~n=================================", [?FUNCTION_NAME]),
ExpectedReason = "Bad packet: Size",
TestMessages =
- [{send, ssh_msg_unknown},
+ [receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_init},
+ {send, ssh_msg_unknown},
{match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
kex_strict_helper(Config, TestMessages, ExpectedReason).
@@ -869,8 +1204,7 @@
ssh_test_lib:set_log_level(debug),
%% Connect and negotiate keys
{ok, InitialState} = ssh_trpt_test_lib:exec(
- [{set_options, [print_ops, print_seqnums, print_messages]}]
- ),
+ [{set_options, [print_ops, print_seqnums, print_messages]}]),
{ok, _AfterKexState} =
ssh_trpt_test_lib:exec(
[{connect,
@@ -883,12 +1217,7 @@
{user_dir, user_dir(Config)},
{user_interaction, false}
| proplists:get_value(extra_options,Config,[])
- ]},
- receive_hello,
- {send, hello},
- {send, ssh_msg_kexinit},
- {match, #ssh_msg_kexinit{_='_'}, receive_msg},
- {send, ssh_msg_kexdh_init}] ++
+ ]}] ++
TestMessages,
InitialState),
ct:sleep(100),
@@ -1055,6 +1384,44 @@
{fail, no_handshakers}
end.
+%%% Connect to an erlang server and pretend client sending extra
+%%% ssh_msg_service_request (Paramiko client behavior)
+extra_ssh_msg_service_request(Config) ->
+ %% Connect and negotiate keys
+ {ok,InitialState} = ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_seqnums, print_messages]}]
+ ),
+ {ok,AfterKexState} = connect_and_kex(Config, InitialState),
+ %% Do the authentcation
+ {User,Pwd} = server_user_password(Config),
+ UserAuthFlow =
+ fun(P) ->
+ [{send, #ssh_msg_service_request{name = "ssh-userauth"}},
+ {match, #ssh_msg_service_accept{name = "ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_userauth_request{user = User,
+ service = "ssh-connection",
+ method = "password",
+ data = <<?BOOLEAN(?FALSE),
+ ?STRING(unicode:characters_to_binary(P))>>
+ }}]
+ end,
+ {ok,EndState} =
+ ssh_trpt_test_lib:exec(
+ UserAuthFlow("WRONG") ++
+ [{match, #ssh_msg_userauth_failure{_='_'}, receive_msg}] ++
+ UserAuthFlow(Pwd) ++
+ [{match, #ssh_msg_userauth_success{_='_'}, receive_msg}],
+ AfterKexState),
+ %% Disconnect
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{send, #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION,
+ description = "End of the fun",
+ language = ""
+ }},
+ close_socket
+ ], EndState),
+ ok.
%%%================================================================
%%%==== Internal functions ========================================
@@ -1221,6 +1588,84 @@
],
InitialState).
+channel_close_timeout(Config) ->
+ {User,_Pwd} = server_user_password(Config),
+ %% Create a listening socket as server socket:
+ {ok,InitialState} = ssh_trpt_test_lib:exec(listen),
+ HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+ %% Start a process handling one connection on the server side:
+ spawn_link(
+ fun() ->
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_messages]},
+ {accept, [{system_dir, system_dir(Config)},
+ {user_dir, user_dir(Config)},
+ {idle_time, 50000}]},
+ receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_reply},
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+ {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_service_accept{name="ssh-userauth"}},
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="none",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_failure{authentications = "password",
+ partial_success = false}},
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="password",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_success{}},
+ {match, #ssh_msg_channel_open{channel_type="session",
+ sender_channel=0,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_channel_open_confirmation{recipient_channel= 0,
+ sender_channel = 0,
+ initial_window_size = 64*1024,
+ maximum_packet_size = 32*1024
+ }},
+ {match, #ssh_msg_channel_open{channel_type="session",
+ sender_channel=1,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_channel_open_confirmation{recipient_channel= 1,
+ sender_channel = 1,
+ initial_window_size = 64*1024,
+ maximum_packet_size = 32*1024}},
+ {match, #ssh_msg_channel_close{recipient_channel = 0}, receive_msg},
+ {match, disconnect(), receive_msg},
+ print_state],
+ InitialState)
+ end),
+ %% connect to it with a regular Erlang SSH client:
+ ChannelCloseTimeout = 3000,
+ {ok, ConnRef} = std_connect(HostPort, Config,
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {channel_close_timeout, ChannelCloseTimeout},
+ {idle_time, 50000}
+ ]
+ ),
+ {ok, Channel0} = ssh_connection:session_channel(ConnRef, 50000),
+ {ok, _Channel1} = ssh_connection:session_channel(ConnRef, 50000),
+ %% Close the channel from client side, the server does not reply with 'channel-close'
+ %% After the timeout, the client should drop the cache entry
+ _ = ssh_connection:close(ConnRef, Channel0),
+ receive
+ after ChannelCloseTimeout + 1000 ->
+ {channels, Channels} = ssh:connection_info(ConnRef, channels),
+ ct:log("Channel entries ~p", [Channels]),
+ %% Only one channel entry should be present, the other one should be dropped
+ 1 = length(Channels),
+ ssh:close(ConnRef)
+ end.
%%%----------------------------------------------------------------
%%% For matching peer disconnection
diff -ruN a/lib/ssh/test/ssh_pubkey_SUITE.erl b/lib/ssh/test/ssh_pubkey_SUITE.erl
--- a/lib/ssh/test/ssh_pubkey_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_pubkey_SUITE.erl 2025-12-17 17:25:02.080556468 +1030
@@ -99,7 +99,7 @@
suite() ->
[{ct_hooks,[ts_install_cth]},
- {timetrap,{seconds,40}}].
+ {timetrap,{seconds,20}}].
all() ->
[{group, old_format},
diff -ruN a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl
--- a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl 2025-12-17 17:25:02.082182205 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2007-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2007-2024. 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.
@@ -56,7 +56,7 @@
suite() ->
[{ct_hooks,[ts_install_cth]},
- {timetrap,{seconds,40}}].
+ {timetrap,{seconds,20}}].
all() ->
[close_file,
diff -ruN a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl
--- a/lib/ssh/test/ssh_sftpd_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_sftpd_SUITE.erl 2025-12-17 17:25:02.082182205 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2006-2022. All Rights Reserved.
+%% Copyright Ericsson AB 2006-2025. 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.
@@ -43,6 +43,7 @@
open_file_dir_v6/1,
read_dir/1,
read_file/1,
+ max_path/1,
real_path/1,
relative_path/1,
relpath/1,
@@ -51,7 +52,6 @@
retrieve_attributes/1,
root_with_cwd/1,
set_attributes/1,
- sshd_read_file/1,
ver3_open_flags/1,
ver3_rename/1,
ver6_basic/1,
@@ -60,6 +60,7 @@
-include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/file.hrl").
+-include_lib("stdlib/include/assert.hrl").
-include("ssh_xfer.hrl").
-include("ssh.hrl").
-include("ssh_test_lib.hrl").
@@ -68,24 +69,25 @@
-define(PASSWD, "Sesame").
%% -define(XFER_PACKET_SIZE, 32768).
%% -define(XFER_WINDOW_SIZE, 4*?XFER_PACKET_SIZE).
--define(SSH_TIMEOUT, 10000).
+-define(SSH_TIMEOUT, 5000).
-define(REG_ATTERS, <<0,0,0,0,1>>).
-define(UNIX_EPOCH, 62167219200).
-
--define(is_set(F, Bits),
- ((F) band (Bits)) == (F)).
+-define(MAX_HANDLES, 10).
+-define(MAX_PATH, 200).
+-define(is_set(F, Bits), ((F) band (Bits)) == (F)).
%%--------------------------------------------------------------------
%% Common Test interface functions -----------------------------------
%%--------------------------------------------------------------------
suite() ->
- [{timetrap,{seconds,40}}].
+ [{timetrap,{seconds,20}}].
all() ->
[open_close_file,
open_close_dir,
read_file,
+ max_path,
read_dir,
write_file,
rename_file,
@@ -97,8 +99,7 @@
links,
ver3_rename,
ver3_open_flags,
- relpath,
- sshd_read_file,
+ relpath,
ver6_basic,
access_outside_root,
root_with_cwd,
@@ -180,7 +181,9 @@
{sftpd_vsn, 6}])],
ssh:daemon(0, [{subsystems, SubSystems}|Options]);
_ ->
- SubSystems = [ssh_sftpd:subsystem_spec([])],
+ SubSystems = [ssh_sftpd:subsystem_spec(
+ [{max_handles, ?MAX_HANDLES},
+ {max_path, ?MAX_PATH}])],
ssh:daemon(0, [{subsystems, SubSystems}|Options])
end,
@@ -316,33 +319,62 @@
read_file(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
FileName = filename:join(PrivDir, "test.txt"),
+ {Cm, Channel} = proplists:get_value(sftp, Config),
+ [begin
+ R1 = req_id(),
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
+ open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ R2 = req_id(),
+ {ok, <<?SSH_FXP_DATA, ?UINT32(R2), ?UINT32(_Length), Data/binary>>, _} =
+ read_file(Handle, 100, 0, Cm, Channel, R2),
+ {ok, Data} = file:read_file(FileName)
+ end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
+ ReqId = req_id(),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
+ ?UINT32(MsgLen), Msg:MsgLen/binary,
+ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
+ open_file(FileName, Cm, Channel, ReqId, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ ct:log("Message: ~s", [Msg]),
+ ok.
- ReqId = 0,
+%%--------------------------------------------------------------------
+max_path(Config) when is_list(Config) ->
+ PrivDir = proplists:get_value(priv_dir, Config),
+ FileName = filename:join(PrivDir, "test.txt"),
{Cm, Channel} = proplists:get_value(sftp, Config),
-
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
- open_file(FileName, Cm, Channel, ReqId,
+ %% verify max_path limit
+ LongFileName =
+ filename:join(PrivDir,
+ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
+ {ok, _} = file:copy(FileName, LongFileName),
+ ReqId1 = req_id(),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH),
+ _/binary>>, _} =
+ open_file(LongFileName, Cm, Channel, ReqId1,
?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
-
- NewReqId = 1,
-
- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
- Data/binary>>, _} =
- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
-
- {ok, Data} = file:read_file(FileName).
+ ?SSH_FXF_OPEN_EXISTING).
%%--------------------------------------------------------------------
read_dir(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
{Cm, Channel} = proplists:get_value(sftp, Config),
- ReqId = 0,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
- open_dir(PrivDir, Cm, Channel, ReqId),
- ok = read_dir(Handle, Cm, Channel, ReqId).
+ [begin
+ R1 = req_id(),
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} =
+ open_dir(PrivDir, Cm, Channel, R1),
+ R2 = req_id(),
+ ok = read_dir(Handle, Cm, Channel, R2)
+ end || _I <- lists:seq(0, ?MAX_HANDLES-1)],
+ ReqId = req_id(),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE),
+ ?UINT32(MsgLen), Msg:MsgLen/binary,
+ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} =
+ open_dir(PrivDir, Cm, Channel, ReqId),
+ ct:log("Message: ~s", [Msg]),
+ ok.
-%%--------------------------------------------------------------------
write_file(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
FileName = filename:join(PrivDir, "test.txt"),
@@ -388,35 +420,33 @@
PrivDir = proplists:get_value(priv_dir, Config),
FileName = filename:join(PrivDir, "test.txt"),
NewFileName = filename:join(PrivDir, "test1.txt"),
- ReqId = 0,
+ LongFileName =
+ filename:join(PrivDir,
+ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
{Cm, Channel} = proplists:get_value(sftp, Config),
-
- {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId),
- ?UINT32(?SSH_FX_OK), _/binary>>, _} =
- rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0),
-
- NewReqId = ReqId + 1,
-
- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId),
- ?UINT32(?SSH_FX_OK), _/binary>>, _} =
- rename(NewFileName, FileName, Cm, Channel, NewReqId, 6,
- ?SSH_FXP_RENAME_OVERWRITE),
-
- NewReqId1 = NewReqId + 1,
- file:copy(FileName, NewFileName),
-
- %% No overwrite
- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1),
- ?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} =
- rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6,
- ?SSH_FXP_RENAME_NATIVE),
-
- NewReqId2 = NewReqId1 + 1,
-
- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2),
- ?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} =
- rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6,
- ?SSH_FXP_RENAME_ATOMIC).
+ Version = 6,
+ [begin
+ case Action of
+ {Code, AFile, BFile, Flags} ->
+ ReqId = req_id(),
+ ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = ~p,~nFlags = ~p",
+ [ReqId, Code, AFile, BFile, Flags]),
+ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), _/binary>>, _} =
+ rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags);
+ {file_copy, AFile, BFile} ->
+ {ok, _} = file:copy(AFile, BFile)
+ end
+ end ||
+ Action <-
+ [{?SSH_FX_OK, FileName, NewFileName, 0},
+ {?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE},
+ {file_copy, FileName, NewFileName},
+ %% no overwrite
+ {?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, ?SSH_FXP_RENAME_NATIVE},
+ {?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, ?SSH_FXP_RENAME_ATOMIC},
+ %% max_path
+ {?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]],
+ ok.
%%--------------------------------------------------------------------
mk_rm_dir(Config) when is_list(Config) ->
@@ -644,27 +674,6 @@
Root = Path
end.
-%%--------------------------------------------------------------------
-sshd_read_file(Config) when is_list(Config) ->
- PrivDir = proplists:get_value(priv_dir, Config),
- FileName = filename:join(PrivDir, "test.txt"),
-
- ReqId = 0,
- {Cm, Channel} = proplists:get_value(sftp, Config),
-
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
- open_file(FileName, Cm, Channel, ReqId,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
-
- NewReqId = 1,
-
- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length),
- Data/binary>>, _} =
- read_file(Handle, 100, 0, Cm, Channel, NewReqId),
-
- {ok, Data} = file:read_file(FileName).
-%%--------------------------------------------------------------------
ver6_basic(Config) when is_list(Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
%FileName = filename:join(PrivDir, "test.txt"),
@@ -728,25 +737,33 @@
FileName = "root_with_cwd.txt",
FilePath = filename:join(CWD, FileName),
ok = filelib:ensure_dir(FilePath),
- ok = file:write_file(FilePath ++ "0", <<>>),
- ok = file:write_file(FilePath ++ "1", <<>>),
- ok = file:write_file(FilePath ++ "2", <<>>),
{Cm, Channel} = proplists:get_value(sftp, Config),
- ReqId0 = 0,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId0), _Handle0/binary>>, _} =
- open_file(FileName ++ "0", Cm, Channel, ReqId0,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
- ReqId1 = 1,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId1), _Handle1/binary>>, _} =
- open_file("./" ++ FileName ++ "1", Cm, Channel, ReqId1,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING),
- ReqId2 = 2,
- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId2), _Handle2/binary>>, _} =
- open_file("/home/" ++ FileName ++ "2", Cm, Channel, ReqId2,
- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
- ?SSH_FXF_OPEN_EXISTING).
+
+ %% repeat procedure to make sure uniq file handles are generated
+ FileHandles =
+ [begin
+ ReqIdStr = integer_to_list(ReqId),
+ ok = file:write_file(FilePath ++ ReqIdStr, <<>>),
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} =
+ open_file(FileName ++ ReqIdStr, Cm, Channel, ReqId,
+ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ Handle
+ end ||
+ ReqId <- lists:seq(0,2)],
+ ?assertEqual(length(FileHandles),
+ length(lists:uniq(FileHandles))),
+ %% create a gap in file handles
+ [_, MiddleHandle, _] = FileHandles,
+ close(MiddleHandle, 3, Cm, Channel),
+
+ %% check that gap in file handles is is re-used
+ GapReqId = 4,
+ {ok, <<?SSH_FXP_HANDLE, ?UINT32(GapReqId), MiddleHandle/binary>>, _} =
+ open_file(FileName ++ integer_to_list(1), Cm, Channel, GapReqId,
+ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
+ ?SSH_FXF_OPEN_EXISTING),
+ ok.
%%--------------------------------------------------------------------
relative_path(Config) when is_list(Config) ->
@@ -1078,3 +1095,12 @@
not_default_permissions() ->
8#600. %% User read-write-only
+
+req_id() ->
+ ReqId =
+ case get(req_id) of
+ undefined -> 0;
+ I -> I
+ end,
+ put(req_id, ReqId + 1),
+ ReqId.
diff -ruN a/lib/ssh/test/ssh_sftp_SUITE.erl b/lib/ssh/test/ssh_sftp_SUITE.erl
--- a/lib/ssh/test/ssh_sftp_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_sftp_SUITE.erl 2025-12-17 17:25:02.082182205 +1030
@@ -1,7 +1,7 @@
%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. 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.
@@ -55,6 +55,7 @@
pos_read/1,
pos_write/1,
position/1,
+ read_6GB/1,
read_crypto_tar/1,
read_dir/1,
read_file/1,
@@ -82,8 +83,9 @@
-include_lib("kernel/include/file.hrl").
-include("ssh_test_lib.hrl").
-include_lib("stdlib/include/assert.hrl").
+
%% Default timetrap timeout
--define(default_timeout, test_server:minutes(1)).
+-define(default_timeout, test_server:minutes(0.5)).
%%--------------------------------------------------------------------
%% Common Test interface functions -----------------------------------
@@ -91,7 +93,7 @@
suite() ->
[{ct_hooks,[ts_install_cth]},
- {timetrap,{seconds,40}}].
+ {timetrap,{seconds,20}}].
all() ->
[{group, not_unicode},
@@ -120,6 +122,7 @@
{unicode, [], [{group,erlang_server},
{group,openssh_server},
+ read_6GB,
sftp_nonexistent_subsystem
]},
@@ -229,24 +232,7 @@
[{peer, {fmt_host(HostX),PortX}}, {group, erlang_server}, {sftpd, Sftpd} | Config];
init_per_group(openssh_server, Config) ->
- ct:comment("Begin ~p",[grps(Config)]),
- Host = ssh_test_lib:hostname(),
- case (catch ssh_sftp:start_channel(Host,
- [{user_interaction, false},
- {silently_accept_hosts, true},
- {save_accepted_host, false}
- ])) of
- {ok, _ChannelPid, Connection} ->
- [{peer, {_HostName,{IPx,Portx}}}] = ssh:connection_info(Connection,[peer]),
- ssh:close(Connection),
- [{w2l, fun w2l/1},
- {peer, {fmt_host(IPx),Portx}}, {group, openssh_server} | Config];
- {error,"Key exchange failed"} ->
- {skip, "openssh server doesn't support the tested kex algorithm"};
- Other ->
- ct:log("No openssh server. Cause:~n~p~n",[Other]),
- {skip, "No openssh daemon (see log in testcase)"}
- end;
+ verify_openssh(Config);
init_per_group(remote_tar, Config) ->
ct:comment("Begin ~p",[grps(Config)]),
@@ -288,7 +274,20 @@
Config.
%%--------------------------------------------------------------------
-
+init_per_testcase(read_6GB, Config) ->
+ case verify_openssh(Config) of
+ Result = {skip, _} ->
+ Result;
+ _ ->
+ case {os:type(), erlang:system_info(system_architecture)} of
+ {{win32, _}, _} ->
+ {skip, "/dev/zero not available on Windows"};
+ {_, "aarch64"} ->
+ {skip, "machine too slow for test"};
+ _ ->
+ init_per_testcase(read_6GB_prepare_openssh_server, Config)
+ end
+ end;
init_per_testcase(sftp_nonexistent_subsystem, Config) ->
PrivDir = proplists:get_value(priv_dir, Config),
SysDir = proplists:get_value(data_dir, Config),
@@ -301,7 +300,6 @@
[{User, Passwd}]}
]),
[{sftpd, Sftpd} | Config];
-
init_per_testcase(version_option, Config0) ->
Config = prepare(Config0),
TmpConfig0 = lists:keydelete(watchdog, 1, Config),
@@ -321,7 +319,6 @@
]),
Sftp = {ChannelPid, Connection},
[{sftp,Sftp}, {watchdog, Dog} | TmpConfig];
-
init_per_testcase(Case, Config00) ->
Config0 = prepare(Config00),
Config1 = lists:keydelete(watchdog, 1, Config0),
@@ -333,11 +330,24 @@
undefined -> [];
Sz -> [{packet_size,Sz}]
end,
+ PrepareOpenSSHServer =
+ fun() ->
+ Host = ssh_test_lib:hostname(),
+ {ok, ChannelPid, Connection} =
+ ssh_sftp:start_channel(Host,
+ [{user_interaction, false},
+ {silently_accept_hosts, true},
+ {save_accepted_host, false}
+ | PktSzOpt
+ ]),
+ Sftp = {ChannelPid, Connection},
+ [{sftp, Sftp}, {watchdog, Dog} | Config2]
+ end,
Config =
case proplists:get_value(group,Config2) of
erlang_server ->
- {_,Host, Port} = proplists:get_value(sftpd, Config2),
- {ok, ChannelPid, Connection} =
+ {_,Host, Port} = proplists:get_value(sftpd, Config2),
+ {ok, ChannelPid, Connection} =
ssh_sftp:start_channel(Host, Port,
[{user, User},
{password, Passwd},
@@ -352,18 +362,10 @@
openssh_server when Case == links ->
{skip, "known bug in openssh"};
openssh_server ->
- Host = ssh_test_lib:hostname(),
- {ok, ChannelPid, Connection} =
- ssh_sftp:start_channel(Host,
- [{user_interaction, false},
- {silently_accept_hosts, true},
- {save_accepted_host, false}
- | PktSzOpt
- ]),
- Sftp = {ChannelPid, Connection},
- [{sftp, Sftp}, {watchdog, Dog} | Config2]
+ PrepareOpenSSHServer();
+ _ when Case == read_6GB_prepare_openssh_server ->
+ PrepareOpenSSHServer()
end,
-
case catch proplists:get_value(remote_tar,Config) of
%% The 'catch' is for the case of Config={skip,...}
true ->
@@ -713,6 +715,29 @@
{ok, 1} = ssh_sftp:position(Sftp, Handle, cur),
{ok, "2"} = ssh_sftp:read(Sftp, Handle, 1).
+read_6GB(Config) when is_list(Config) ->
+ ct:timetrap(test_server:minutes(20)),
+ FileName = "/dev/zero",
+ SftpFileName = w2l(Config, FileName),
+ {SftpChannel, _ConnectionRef} = proplists:get_value(sftp, Config),
+ ChunkSize = 65535,
+ N = 100000,
+ {ok, Handle} = ssh_sftp:open(SftpChannel, SftpFileName, [read]),
+ ExpectedList = lists:duplicate(ChunkSize, 0),
+ [begin
+ MBTransferred = io_lib:format("~.2f", [I * ChunkSize / 1048576.0]),
+ case ssh_sftp:read(SftpChannel, Handle, ChunkSize, timer:minutes(1)) of
+ {ok, ExpectedList} ->
+ [ct:log("~n~s MB read~n", [MBTransferred]) || I rem 10000 == 0];
+ Result ->
+ ct:log("## After reading ~s MB~n## Unexpected result received = ~p",
+ [MBTransferred, Result]),
+ ct:fail(unexpected_reason)
+ end
+ end ||
+ I <- lists:seq(0, N)],
+ ok.
+
%%--------------------------------------------------------------------
pos_read(Config) when is_list(Config) ->
FileName = proplists:get_value(testfile, Config),
@@ -1271,4 +1296,22 @@
W2L = proplists:get_value(w2l, Config, fun(X) -> X end),
W2L(P).
-
+verify_openssh(Config) ->
+ ct:comment("Begin ~p",[grps(Config)]),
+ Host = ssh_test_lib:hostname(),
+ case (catch ssh_sftp:start_channel(Host,
+ [{user_interaction, false},
+ {silently_accept_hosts, true},
+ {save_accepted_host, false}
+ ])) of
+ {ok, _ChannelPid, Connection} ->
+ [{peer, {_HostName,{IPx,Portx}}}] = ssh:connection_info(Connection,[peer]),
+ ssh:close(Connection),
+ [{w2l, fun w2l/1},
+ {peer, {fmt_host(IPx),Portx}}, {group, openssh_server} | Config];
+ {error,"Key exchange failed"} ->
+ {skip, "openssh server doesn't support the tested kex algorithm"};
+ Other ->
+ ct:log("No openssh server. Cause:~n~p~n",[Other]),
+ {skip, "No openssh daemon (see log in testcase)"}
+ end.
diff -ruN a/lib/ssh/test/ssh_sup_SUITE.erl b/lib/ssh/test/ssh_sup_SUITE.erl
--- a/lib/ssh/test/ssh_sup_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_sup_SUITE.erl 2025-12-17 17:25:02.082182205 +1030
@@ -61,7 +61,7 @@
%%--------------------------------------------------------------------
suite() ->
[{ct_hooks,[ts_install_cth]},
- {timetrap,{seconds,100}}].
+ {timetrap,{seconds,50}}].
all() ->
[default_tree, sshc_subtree, sshd_subtree, sshd_subtree_profile,
diff -ruN a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl
--- a/lib/ssh/test/ssh_test_lib.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_test_lib.erl 2025-12-17 17:25:02.082182205 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -1344,6 +1344,7 @@
ok = logger:set_primary_config(level, Level).
add_log_handler() ->
+ logger:remove_handler(?MODULE),
TestRef = make_ref(),
ok = logger:add_handler(?MODULE, ?MODULE,
#{level => debug,
diff -ruN a/lib/ssh/test/ssh_test_lib.hrl b/lib/ssh/test/ssh_test_lib.hrl
--- a/lib/ssh/test/ssh_test_lib.hrl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_test_lib.hrl 2025-12-17 17:25:02.082182205 +1030
@@ -9,7 +9,7 @@
%%-------------------------------------------------------------------------
%% Timeout time in ms
%%-------------------------------------------------------------------------
--define(TIMEOUT, 27000).
+-define(TIMEOUT, 15000).
%%-------------------------------------------------------------------------
%% Check for usable crypto
diff -ruN a/lib/ssh/test/ssh_to_openssh_SUITE.erl b/lib/ssh/test/ssh_to_openssh_SUITE.erl
--- a/lib/ssh/test/ssh_to_openssh_SUITE.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl 2025-12-17 17:25:02.082182205 +1030
@@ -60,7 +60,7 @@
%%--------------------------------------------------------------------
suite() ->
- [{timetrap,{seconds,60}}].
+ [{timetrap,{seconds,30}}].
all() ->
case os:find_executable("ssh") of
diff -ruN a/lib/ssh/test/ssh_trpt_test_lib.erl b/lib/ssh/test/ssh_trpt_test_lib.erl
--- a/lib/ssh/test/ssh_trpt_test_lib.erl 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/test/ssh_trpt_test_lib.erl 2025-12-17 17:25:02.082556494 +1030
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. 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.
@@ -24,7 +24,8 @@
-export([exec/1, exec/2,
instantiate/2,
format_msg/1,
- server_host_port/1
+ server_host_port/1,
+ return_value/1
]
).
@@ -91,7 +92,8 @@
report_trace(throw, Term, S1),
throw({Term,Op});
- error:Error ->
+ error:Error:St ->
+ ct:log("Stacktrace=~n~p", [St]),
report_trace(error, Error, S1),
error({Error,Op});
@@ -336,6 +338,17 @@
Msg = #ssh_msg_ignore{data = "unexpected_ignore_message"},
send(S0, Msg);
+send(S0, ssh_msg_debug) ->
+ Msg = #ssh_msg_debug{
+ always_display = true,
+ message = "some debug message",
+ language = "en"},
+ send(S0, Msg);
+
+send(S0, ssh_msg_unimplemented) ->
+ Msg = #ssh_msg_unimplemented{sequence = 123},
+ send(S0, Msg);
+
send(S0, ssh_msg_unknown) ->
Msg = binary:encode_hex(<<"0000000C060900000000000000000000">>),
send(S0, Msg);
@@ -383,6 +396,26 @@
end),
send_bytes(NextKexMsgBin, S#s{ssh = C});
+send(S0, ssh_msg_kexdh_init_dup) when ?role(S0) == client ->
+ {OwnMsg, PeerMsg} = S0#s.alg_neg,
+ {ok, NextKexMsgBin, C} =
+ try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S0#s.ssh, init)
+ catch
+ Class:Exc ->
+ fail("Algorithm negotiation failed!",
+ {"Algorithm negotiation failed at line ~p:~p~n~p:~s~nPeer: ~s~n Own: ~s",
+ [?MODULE,?LINE,Class,format_msg(Exc),format_msg(PeerMsg),format_msg(OwnMsg)]},
+ S0)
+ end,
+ S = opt(print_messages, S0,
+ fun(X) when X==true;X==detail ->
+ #ssh{keyex_key = {{_Private, Public}, {_G, _P}}} = C,
+ Msg = #ssh_msg_kexdh_init{e = Public},
+ {"Send (reconstructed)~n~s~n",[format_msg(Msg)]}
+ end),
+ send_bytes(NextKexMsgBin, S#s{ssh = C}),
+ send_bytes(NextKexMsgBin, S#s{ssh = C});
+
send(S0, ssh_msg_kexdh_reply) ->
Bytes = proplists:get_value(ssh_msg_kexdh_reply, S0#s.reply),
S = opt(print_messages, S0,
@@ -532,7 +565,10 @@
S0#s.ssh)
of
{packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} ->
- S1 = S0#s{ssh = Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
+ S1 = S0#s{ssh = Ssh1#ssh{recv_sequence =
+ ssh_transport:next_seqnum(undefined,
+ Ssh1#ssh.recv_sequence,
+ false)},
decrypted_data_buffer = <<>>,
undecrypted_packet_length = undefined,
aead_data = <<>>,
@@ -779,3 +815,6 @@
save_prints({Fmt,Args}, S) ->
S#s{prints = [{Fmt,Args}|S#s.prints]}.
+
+return_value(#s{return_value = ReturnValue}) ->
+ ReturnValue.
diff -ruN a/lib/ssh/vsn.mk b/lib/ssh/vsn.mk
--- a/lib/ssh/vsn.mk 2024-12-05 21:56:22.000000000 +1030
+++ b/lib/ssh/vsn.mk 2025-12-17 17:25:02.082556494 +1030
@@ -1,4 +1,4 @@
#-*-makefile-*- ; force emacs to enter makefile-mode
-SSH_VSN = 5.2.4
+SSH_VSN = 5.2.11.4
APP_VSN = "ssh-$(SSH_VSN)"