forked from pool/erlang27
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
5685 lines
230 KiB
Diff
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)"
|