Surfus.Shell is an SSH client library for .NET.
- Fully asynchronous API
- Terminal sessions, command execution, and SCP file transfers
- Configurable terminal modes (echo, newline translation, raw modes)
- 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
awaitusingvarclient=newSshClient("192.168.1.1");awaitclient.ConnectAsync(cancellationToken);awaitclient.AuthenticateAsync("admin","password",cancellationToken);varterminal=awaitclient.CreateTerminalAsync(cancellationToken);awaitterminal.StandardInput.WriteAsync("show version\n"u8.ToArray(),cancellationToken);awaitusingvarclient=newSshClient("host",22);awaitclient.ConnectAsync(ct);// Password authenticationawaitclient.AuthenticateAsync("user","pass",ct);// Keyboard-interactiveawaitclient.AuthenticateAsync("user",(prompt,ct)=>Task.FromResult("password"),ct);// SSH agent (via SSH_AUTH_SOCK)varagent=awaitSshAgentClient.ConnectAsync(ct);awaitclient.AuthenticateAsync("user",agent,ct);// SSH agent over a custom streamusingvaragent2=newSshAgentClient(myAgentStream);awaitclient.AuthenticateAsync("user",agent2,ct);// Private keyvarkey=PrivateKeyAuth.FromFile("/path/to/key");awaitclient.AuthenticateAsync("user",key,ct);varterminal=awaitclient.CreateTerminalAsync(ct);// Write to stdinawaitterminal.StandardInput.WriteAsync("ls -la\n"u8.ToArray(),ct);// Read from stdoutvarbuf=newbyte[4096];varn=awaitterminal.StandardOutput.ReadAsync(buf,ct);varoutput=Encoding.UTF8.GetString(buf,0,n);// Disable echo — useful for automation to avoid reading back sent commandsvarterminal=awaitclient.CreateTerminalAsync(ct,newTerminalOptions{Echo=false,OutputNewlineTranslation=false,});// Custom terminal sizevarterminal=awaitclient.CreateTerminalAsync(ct,newTerminalOptions{Columns=200,Rows=50,TerminalType="xterm-256color",});// Full control with explicit mode list (RFC 4254 §8)varterminal=awaitclient.CreateTerminalAsync(ct,newTerminalOptions{RawTerminalModes=new[]{TerminalMode.Echo(false),TerminalMode.Onlcr(false),newTerminalMode(70,0),// any opcode by number},});varcommand=awaitclient.CreateCommandAsync(ct);awaitcommand.StartAsync("echo hello",ct);varresult=newMemoryStream();awaitcommand.StandardOutput.CopyToAsync(result,ct);// result contains "hello\n"// Exit code is available after the channel closesvarexitCode=command.ExitCode;// Forward local port 8080 through SSH to remote-db:5432awaitclient.ForwardLocalPortAsync(8080,"remote-db",5432,ct);// Open a single forwarded connectionawaitusingvarchannel=awaitclient.CreateDirectTcpIpChannelAsync("10.0.0.5",80,ct);awaitchannel.StandardInput.WriteAsync("GET / HTTP/1.0\r\n\r\n"u8.ToArray(),ct);varresponse=newMemoryStream();awaitchannel.StandardOutput.CopyToAsync(response,ct);varscp=newScpClient(client);// Uploadawaitscp.UploadAsync("/local/file.txt","/remote/file.txt",cancellationToken:ct);// Downloadawaitscp.DownloadAsync("/remote/file.txt","/local/file.txt",ct);varclient=newSshClient("host"){HostKeyCallback=async(hostKey,ct)=>{// Verify the host key and return true to acceptreturntrue;}};varclient=newSshClient("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 channelvarchannel=awaitclient.OpenChannelAsync(newChannelOpenSession(),ct);// Send custom requestsawaitchannel.RequestAsync(newChannelRequestSubsystem(channel.ServerId,true,"sftp"),ct);// Use the channel streamsawaitchannel.StandardInput.WriteAsync(data,ct);// Or construct higher-level wrappers manuallyvarterminal=newSshTerminal(channel,newTerminalOptions{Columns=120,Rows=40});awaitterminal.RequestAsync(ct);// Use an existing stream (e.g., over a proxy)awaitusingvarclient=newSshClient(myStream);// Or use a factory for reconnectable transportsawaitusingvarclient=newSshClient(async ct =>{vartcp=newTcpClient();awaittcp.ConnectAsync("host",22,ct);return(tcp.GetStream(),()=>{tcp.Dispose();returnValueTask.CompletedTask;});});