Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## [2.17.0] - yyyy-mm-dd
- Improved Web/WASM compatibility by updating `SSHSocket` conditional imports so web runtimes consistently use the web socket shim and avoid incorrect native socket selection [#88]. Thanks [@vicajilau].
- Added local dynamic forwarding (`SSHClient.forwardDynamic`) with SOCKS5 `NO AUTH` + `CONNECT`, including configurable handshake/connect timeouts and connection limits.

## [2.16.0] - 2026-03-24
- **BREAKING**: Changed `SSHChannelController.sendEnv()` from `void` to `Future<bool>` to properly await environment variable setup responses and avoid race conditions with PTY requests [#102]. Thanks [@itzhoujun] and [@vicajilau].
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e
- **Pure Dart**: Working with both Dart VM and Flutter.
- **SSH Session**: Executing commands, spawning shells, setting environment variables, pseudo terminals, etc.
- **Authentication**: Supports password, private key and interactive authentication method.
- **Forwarding**: Supports local forwarding and remote forwarding.
- **Forwarding**: Supports local forwarding, remote forwarding, and dynamic forwarding (SOCKS5 CONNECT).
- **SFTP**: Supports all operations defined in [SFTPv3 protocol](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02) including upload, download, list, link, remove, rename, etc.

## 🧬 Built with dartssh2
Expand Down Expand Up @@ -296,6 +296,40 @@ void main() async {
}
```

### Start a local SOCKS5 proxy through SSH (`ssh -D` style)

```dart
void main() async {
final dynamicForward = await client.forwardDynamic(
bindHost: '127.0.0.1',
bindPort: 1080,
options: const SSHDynamicForwardOptions(
handshakeTimeout: Duration(seconds: 10),
connectTimeout: Duration(seconds: 15),
maxConnections: 128,
),
filter: (host, port) {
// Optional allow/deny policy.
return true;
},
);

print('SOCKS5 proxy at ${dynamicForward.host}:${dynamicForward.port}');
}
```

This currently supports SOCKS5 `NO AUTH` + `CONNECT`.
It requires `dart:io` and is not available on web runtimes.

Quick verification from your terminal:

```sh
curl --proxy socks5h://127.0.0.1:1080 https://ifconfig.me
```

If the proxy is working, this command returns the public egress IP seen through
the SSH tunnel.

### Authenticate with public keys

```dart
Expand Down
48 changes: 48 additions & 0 deletions example/forward_dynamic.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';

Future<void> main() async {
final host = Platform.environment['SSH_HOST'] ?? 'localhost';
final port = int.tryParse(Platform.environment['SSH_PORT'] ?? '') ?? 22;
final username = Platform.environment['SSH_USERNAME'] ?? 'root';
final password = Platform.environment['SSH_PASSWORD'];

final socket = await SSHSocket.connect(host, port);

final client = SSHClient(
socket,
username: username,
onPasswordRequest: () => password,
);

await client.authenticated;

final dynamicForward = await client.forwardDynamic(
bindHost: '127.0.0.1',
bindPort: 1080,
options: const SSHDynamicForwardOptions(
handshakeTimeout: Duration(seconds: 10),
connectTimeout: Duration(seconds: 15),
maxConnections: 64,
),
filter: (targetHost, targetPort) {
// Allow only web ports in this sample.
return targetPort == 80 || targetPort == 443;
},
);

print(
'SOCKS5 proxy ready on ${dynamicForward.host}:${dynamicForward.port}.',
);
print('Press Ctrl+C to stop.');

ProcessSignal.sigint.watch().listen((_) async {
await dynamicForward.close();
client.close();
await client.done;
exit(0);
});

await Future<void>.delayed(const Duration(days: 365));
}
24 changes: 24 additions & 0 deletions lib/src/dynamic_forward.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:dartssh2/src/dynamic_forward_stub.dart'
if (dart.library.io) 'package:dartssh2/src/dynamic_forward_io.dart' as impl;
import 'package:dartssh2/src/ssh_forward.dart';

typedef SSHDynamicDial = Future<SSHForwardChannel> Function(
String host,
int port,
);

Future<SSHDynamicForward> startDynamicForward({
required String bindHost,
required int? bindPort,
required SSHDynamicForwardOptions options,
SSHDynamicConnectionFilter? filter,
required SSHDynamicDial dial,
}) {
return impl.startDynamicForward(
bindHost: bindHost,
bindPort: bindPort,
options: options,
filter: filter,
dial: dial,
);
}
Loading
Loading