diff --git a/CHANGELOG.md b/CHANGELOG.md index fe805ae..f984f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ +## [2.14.0] - 2026-03-19 +- Fixed SSH connections through bastion hosts where the target server sends its version string immediately upon connection (which is standard behavior per RFC 4253) [#141]. Thanks @shihuili1218. +- Adds a new forwardLocalUnix() function, which is an equivalent of ssh -L localPort:remoteSocketPath [#140]. Thanks @isegal. + + ## [2.13.0] - 2025-06-22 -- docs: Update NoPorts naming [`#115`]. [`@XavierChanth`]. -- Add parameter disableHostkeyVerification [`#123`]. Thanks [`@alexander-irion`]. -- Add support for server-initiated re-keying [`#125`]. Thanks [`@MarBazuz`]. -- Add support for new algorithms "mac-sha2-256-96", "hmac-sha2-512-96", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com" [`#126`] [`#127`]. Thanks [`@reinbeumer`]. +- docs: Update NoPorts naming [#115]. [@XavierChanth]. +- Add parameter disableHostkeyVerification [#123]. Thanks [@alexander-irion]. +- Add support for server-initiated re-keying [#125]. Thanks [@MarBazuz]. +- Add support for new algorithms "mac-sha2-256-96", "hmac-sha2-512-96", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com" [#126] [#127]. Thanks [@reinbeumer]. ## [2.12.0] - 2025-02-08 - Fixed streams and channel not closing after receiving SSH_Message_Channel_Close [#116]. [@cbenhagen]. @@ -171,12 +176,14 @@ - Initial release. -[`#127`]: https://github.com/TerminalStudio/dartssh2/pull/127 -[`#126`]: https://github.com/TerminalStudio/dartssh2/pull/126 -[`#125`]: https://github.com/TerminalStudio/dartssh2/pull/125 -[`#123`]: https://github.com/TerminalStudio/dartssh2/pull/123 -[`#116`]: https://github.com/TerminalStudio/dartssh2/pull/116 -[`#115`]: https://github.com/TerminalStudio/dartssh2/pull/115 +[#141]: https://github.com/TerminalStudio/dartssh2/pull/141 +[#140]: https://github.com/TerminalStudio/dartssh2/pull/140 +[#127]: https://github.com/TerminalStudio/dartssh2/pull/127 +[#126]: https://github.com/TerminalStudio/dartssh2/pull/126 +[#125]: https://github.com/TerminalStudio/dartssh2/pull/125 +[#123]: https://github.com/TerminalStudio/dartssh2/pull/123 +[#116]: https://github.com/TerminalStudio/dartssh2/pull/116 +[#115]: https://github.com/TerminalStudio/dartssh2/pull/115 [#101]: https://github.com/TerminalStudio/dartssh2/pull/101 [#100]: https://github.com/TerminalStudio/dartssh2/issues/100 [#80]: https://github.com/TerminalStudio/dartssh2/issues/80 diff --git a/README.md b/README.md index ce8e698..85bbaa3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e DartShell + + + Naviterm + @@ -65,6 +69,12 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e dartShell displaying terminal and session information for SSH operations + + + + Your all-in-one SSH terminal, SFTP client, and port forwarding tool, built from the ground up for macOS, iPhone, and iPad. + + diff --git a/example/forward_local_unix.dart b/example/forward_local_unix.dart new file mode 100644 index 0000000..e8791a4 --- /dev/null +++ b/example/forward_local_unix.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:dartssh2/dartssh2.dart'; + +/// Example of forwarding a local TCP port to a remote Unix domain socket using `ssh -L localPort:remoteSocketPath`. +void main(List args) async { + final socket = await SSHSocket.connect('localhost', 22); + + final client = SSHClient( + socket, + username: 'root', + onPasswordRequest: () { + stdout.write('Password: '); + stdin.echoMode = false; + String? password; + try { + password = stdin.readLineSync(); + } finally { + stdin.echoMode = true; + } + if (password == null) exit(1); + return password; + }, + ); + + await client.authenticated; + + final serverSocket = await ServerSocket.bind('localhost', 8080); + + print('Listening on ${serverSocket.address.address}:${serverSocket.port}'); + + await for (final socket in serverSocket) { + final forward = await client.forwardLocalUnix('/var/run/docker.sock'); + forward.stream.cast>().pipe(socket); + socket.cast>().pipe(forward.sink); + } + + client.close(); + await client.done; +} diff --git a/lib/src/algorithm/ssh_mac_type.dart b/lib/src/algorithm/ssh_mac_type.dart index 7d84bea..de37ab3 100644 --- a/lib/src/algorithm/ssh_mac_type.dart +++ b/lib/src/algorithm/ssh_mac_type.dart @@ -57,6 +57,8 @@ class SSHMacType extends SSHAlgorithm { macFactory: _hmacSha512Factory, isEtm: true, ); + // end added by Rein + const SSHMacType._({ required this.name, required this.keySize, diff --git a/lib/src/message/msg_channel.dart b/lib/src/message/msg_channel.dart index db71026..bcd2802 100644 --- a/lib/src/message/msg_channel.dart +++ b/lib/src/message/msg_channel.dart @@ -22,6 +22,9 @@ class SSH_Message_Channel_Open implements SSHMessage { final String? originatorIP; final int? originatorPort; + /// "direct-streamlocal@openssh.com" channel specific data. + final String? socketPath; + SSH_Message_Channel_Open({ required this.channelType, required this.senderChannel, @@ -31,6 +34,7 @@ class SSH_Message_Channel_Open implements SSHMessage { this.port, this.originatorIP, this.originatorPort, + this.socketPath, }); factory SSH_Message_Channel_Open.session({ @@ -105,6 +109,23 @@ class SSH_Message_Channel_Open implements SSHMessage { ); } + /// Opens a channel to forward data to a Unix domain socket on the remote + /// side. See OpenSSH PROTOCOL, section 2.4. + factory SSH_Message_Channel_Open.directStreamLocal({ + required int senderChannel, + required int initialWindowSize, + required int maximumPacketSize, + required String socketPath, + }) { + return SSH_Message_Channel_Open( + channelType: 'direct-streamlocal@openssh.com', + senderChannel: senderChannel, + initialWindowSize: initialWindowSize, + maximumPacketSize: maximumPacketSize, + socketPath: socketPath, + ); + } + factory SSH_Message_Channel_Open.decode(Uint8List bytes) { final reader = SSHMessageReader(bytes); reader.skip(1); @@ -157,6 +178,17 @@ class SSH_Message_Channel_Open implements SSHMessage { originatorIP: originatorIP, originatorPort: originatorPort, ); + case 'direct-streamlocal@openssh.com': + final socketPath = reader.readUtf8(); + // reserved string and uint32 per OpenSSH PROTOCOL spec + reader.readUtf8(); + reader.readUint32(); + return SSH_Message_Channel_Open.directStreamLocal( + senderChannel: senderChannel, + initialWindowSize: initialWindowSize, + maximumPacketSize: maximumPacketSize, + socketPath: socketPath, + ); default: return SSH_Message_Channel_Open( @@ -195,6 +227,11 @@ class SSH_Message_Channel_Open implements SSHMessage { writer.writeUtf8(originatorIP!); writer.writeUint32(originatorPort!); break; + case 'direct-streamlocal@openssh.com': + writer.writeUtf8(socketPath!); + writer.writeUtf8(''); // reserved + writer.writeUint32(0); // reserved + break; } return writer.takeBytes(); } @@ -210,6 +247,8 @@ class SSH_Message_Channel_Open implements SSHMessage { return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize, host: $host, port: $port, originatorIP: $originatorIP, originatorPort: $originatorPort)'; case 'direct-tcpip': return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize, host: $host, port: $port, originatorIP: $originatorIP, originatorPort: $originatorPort)'; + case 'direct-streamlocal@openssh.com': + return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize, socketPath: $socketPath)'; default: return 'SSH_Message_Channel_Open(channelType: $channelType, senderChannel: $senderChannel, initialWindowSize: $initialWindowSize, maximumPacketSize: $maximumPacketSize)'; } diff --git a/lib/src/ssh_algorithm.dart b/lib/src/ssh_algorithm.dart index c941a94..652303f 100644 --- a/lib/src/ssh_algorithm.dart +++ b/lib/src/ssh_algorithm.dart @@ -130,7 +130,8 @@ class SSHAlgorithms { SSHCipherType.aes256cbc, SSHCipherType.aes128cbc, ], - // Prefer modern SHA-2 MACs by default; keep SHA-1 as fallback and MD5 last. + // Prefer modern SHA-2 MACs by default; full-length variants first, + // ETM variants for better security, truncated 96-bit as last-resort fallback. this.mac = const [ SSHMacType.hmacSha256Etm, SSHMacType.hmacSha512Etm, @@ -138,6 +139,8 @@ class SSHAlgorithms { SSHMacType.hmacSha512, SSHMacType.hmacSha1, SSHMacType.hmacMd5, + SSHMacType.hmacSha256_96, + SSHMacType.hmacSha512_96, ], }); } diff --git a/lib/src/ssh_client.dart b/lib/src/ssh_client.dart index 81fa6de..6e01249 100644 --- a/lib/src/ssh_client.dart +++ b/lib/src/ssh_client.dart @@ -357,6 +357,21 @@ class SSHClient { return SSHForwardChannel(channelController.channel); } + /// Forward local connections to a remote Unix domain socket at + /// [remoteSocketPath] on the remote side via a + /// `direct-streamlocal@openssh.com` channel. + /// + /// This is the equivalent of `ssh -L localPort:remoteSocketPath`. + Future forwardLocalUnix( + String remoteSocketPath, + ) async { + await _authenticated.future; + final channelController = await _openForwardLocalUnixChannel( + remoteSocketPath, + ); + return SSHForwardChannel(channelController.channel); + } + /// Execute [command] on the remote side. Returns a [SSHChannel] that can be /// used to read and write to the remote side. Future execute( @@ -1265,6 +1280,22 @@ class SSHClient { return await _waitChannelOpen(localChannelId); } + Future _openForwardLocalUnixChannel( + String socketPath, + ) async { + final localChannelId = _channelIdAllocator.allocate(); + + final request = SSH_Message_Channel_Open.directStreamLocal( + senderChannel: localChannelId, + initialWindowSize: _initialWindowSize, + maximumPacketSize: _maximumPacketSize, + socketPath: socketPath, + ); + _sendMessage(request); + + return await _waitChannelOpen(localChannelId); + } + Future _waitChannelOpen( SSHChannelId localChannelId, ) async { diff --git a/pubspec.yaml b/pubspec.yaml index 34f55fe..3cc6926 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dartssh2 -version: 2.13.0 +version: 2.14.0 description: SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use. homepage: https://github.com/TerminalStudio/dartssh2 diff --git a/test/src/algorithm/ssh_cipher_type_test.dart b/test/src/algorithm/ssh_cipher_type_test.dart index 565dae2..70755d9 100644 --- a/test/src/algorithm/ssh_cipher_type_test.dart +++ b/test/src/algorithm/ssh_cipher_type_test.dart @@ -88,14 +88,14 @@ void main() { expect( algorithms.mac, equals([ - SSHMacType.hmacSha256_96, - SSHMacType.hmacSha512_96, SSHMacType.hmacSha256Etm, SSHMacType.hmacSha512Etm, - SSHMacType.hmacSha1, SSHMacType.hmacSha256, SSHMacType.hmacSha512, + SSHMacType.hmacSha1, SSHMacType.hmacMd5, + SSHMacType.hmacSha256_96, + SSHMacType.hmacSha512_96, ])); }); } diff --git a/test/src/socket/ssh_socket_io_test.dart b/test/src/socket/ssh_socket_io_test.dart index 82c4be9..626f678 100644 --- a/test/src/socket/ssh_socket_io_test.dart +++ b/test/src/socket/ssh_socket_io_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('SSHSocket', () { test('can establish tcp connections', () async { - final socket = await SSHSocket.connect('time.nist.gov', 13); + final socket = await SSHSocket.connect('test.rebex.net', 22); final firstPacket = await socket.stream.first; expect(firstPacket, isNotEmpty); await socket.close();