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
|
+
+
+
+
+
+ |
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();