Surfus.Shell is an SSH client library for .NET.
- Fully asynchronous API
- Terminal sessions, command execution, and SCP file transfers
- Local port forwarding (direct-tcpip)
- SSH agent authentication
- Private key authentication
- Keyboard-interactive authentication
- Host key verification callback
- Low-level channel API for custom SSH extensions
dotnet add package Surfus.Shell
await using var client = new SshClient("192.168.1.1");
await client.ConnectAsync(cancellationToken);
await client.AuthenticateAsync("admin", "password", cancellationToken);
var terminal = await client.CreateTerminalAsync(cancellationToken);
await terminal.StandardInput.WriteAsync("show version\n"u8.ToArray(), cancellationToken);await using var client = new SshClient("host", 22);
await client.ConnectAsync(ct);
// Password authentication
await client.AuthenticateAsync("user", "pass", ct);
// Keyboard-interactive
await client.AuthenticateAsync("user", (prompt, ct) => Task.FromResult("password"), ct);
// SSH agent (via SSH_AUTH_SOCK)
var agent = await SshAgentClient.ConnectAsync(ct);
await client.AuthenticateAsync("user", agent, ct);
// SSH agent over a custom stream
using var agent2 = new SshAgentClient(myAgentStream);
await client.AuthenticateAsync("user", agent2, ct);
// Private key
var key = PrivateKeyAuth.FromFile("/path/to/key");
await client.AuthenticateAsync("user", key, ct);var terminal = await client.CreateTerminalAsync(ct);
// Write to stdin
await terminal.StandardInput.WriteAsync("ls -la\n"u8.ToArray(), ct);
// Read from stdout
var buf = new byte[4096];
var n = await terminal.StandardOutput.ReadAsync(buf, ct);
var output = Encoding.UTF8.GetString(buf, 0, n);var command = await client.CreateCommandAsync(ct);
await command.StartAsync("echo hello", ct);
var result = new MemoryStream();
await command.StandardOutput.CopyToAsync(result, ct);
// result contains "hello\n"
// Exit code is available after the channel closes
var exitCode = command.ExitCode;// Forward local port 8080 through SSH to remote-db:5432
await client.ForwardLocalPortAsync(8080, "remote-db", 5432, ct);// Open a single forwarded connection
await using var channel = await client.CreateDirectTcpIpChannelAsync("10.0.0.5", 80, ct);
await channel.StandardInput.WriteAsync("GET / HTTP/1.0\r\n\r\n"u8.ToArray(), ct);
var response = new MemoryStream();
await channel.StandardOutput.CopyToAsync(response, ct);var scp = new ScpClient(client);
// Upload
await scp.UploadAsync("/local/file.txt", "/remote/file.txt", cancellationToken: ct);
// Download
await scp.DownloadAsync("/remote/file.txt", "/local/file.txt", ct);var client = new SshClient("host")
{
HostKeyCallback = async (hostKey, ct) =>
{
// Verify the host key and return true to accept
return true;
}
};var client = new SshClient("host")
{
// Called for each line the server sends before its version string (RFC 4253 §4.2)
OnConnectionBanner = line => Console.WriteLine($"Connection: {line}"),
// Called when the server sends a banner during authentication (RFC 4252)
OnAuthenticationBanner = banner => Console.WriteLine($"Auth: {banner}")
};For custom channel types or advanced use cases:
// Open a raw session channel
var channel = await client.OpenChannelAsync(new ChannelOpenSession(), ct);
// Send custom requests
await channel.RequestAsync(new ChannelRequestSubsystem(channel.ServerId, true, "sftp"), ct);
// Use the channel streams
await channel.StandardInput.WriteAsync(data, ct);
// Or construct higher-level wrappers manually
var terminal = new SshTerminal(channel, new TerminalOptions { Columns = 120, Rows = 40 });
await terminal.RequestAsync(ct);// Use an existing stream (e.g., over a proxy)
await using var client = new SshClient(myStream);
// Or use a factory for reconnectable transports
await using var client = new SshClient(async ct =>
{
var tcp = new TcpClient();
await tcp.ConnectAsync("host", 22, ct);
return (tcp.GetStream(), () => { tcp.Dispose(); return ValueTask.CompletedTask; });
});