Architecture and connection lifecycle
This page explains what happens between SshClient.ConnectAsync and DisposeAsync: the protocol layers, the handshake, the
objects you get, and how a connection ends. None of it is needed to use the library, but it explains much of its behavior.
Three protocol layers, one object
SSH is three protocols stacked on one byte stream (RFC 4251):
| Layer | RFC | What it does | In SshSharp |
|---|---|---|---|
| Transport | 4253 | Version exchange, key exchange, host key proof, encryption, integrity, rekeying | inside SshClient; client.Algorithms, client.HostKey |
| User authentication | 4252 | Proves who you are: public key, keyboard-interactive, password, GSSAPI | SshClientOptions credentials; done before ConnectAsync returns |
| Connection | 4254 | Multiplexes channels: sessions, tunnels, forwarded connections, agent forwarding | SshSession, SftpClient, tunnels and forwarders |
SshClient is the whole stack for one connection. Everything else (sessions, SFTP clients, forwarders, tunnel streams) is a
channel on it, and lives no longer than it.
The handshake
ConnectAsync returns only after the whole handshake has succeeded, so a client you hold is always verified and authenticated:
sequenceDiagram
autonumber
participant App as Your code
participant C as SshClient
participant S as SSH server
App->>C: ConnectAsync(host, port, options)
C->>S: TCP connect (or proxy, ProxyJump tunnel, ProxyCommand, your Stream)
C->>S: identification "SSH-2.0-SshSharp_1.0.0"
S-->>C: identification, for example "SSH-2.0-OpenSSH_9.9"
C->>S: KEXINIT (algorithm lists, strict-kex and ext-info markers)
S-->>C: KEXINIT
Note over C,S: both pick the first client algorithm the server also supports
C->>S: key exchange init (for example mlkem768x25519-sha256)
S-->>C: reply: host key and its signature over the exchange
C->>C: check the signature, then HostKeyVerifier.VerifyAsync
C->>S: NEWKEYS (everything is encrypted from here)
S-->>C: NEWKEYS, EXT_INFO (server-sig-algs)
C->>S: request the ssh-userauth service
loop until success or nothing left to try
C->>S: USERAUTH_REQUEST (none, then GSSAPI, keys, keyboard-interactive, password)
S-->>C: FAILURE (methods that can continue) or SUCCESS
end
C-->>App: connected SshClient
- The host key is verified before any credential is sent. A rejected key ends the connection with
SshHostKeyException, and the server never sees a password or a signature. - Strict key exchange (OpenSSH's fix for the Terrapin attack) is always on; with a server that supports it, the packet sequence is
reset at each
NEWKEYS. - The order in which credentials are tried is described in Authentication.
The lifecycle
flowchart LR
hs["Handshake<br/>(inside ConnectAsync)"] -- "verified and<br/>authenticated" --> up["Connected<br/>channels, keep-alives"]
hs -- "failure" --> ex(["ConnectAsync throws"])
up -- "byte or time limit" --> rk["Rekeying<br/>channels keep running"]
rk --> up
up -- "DisposeAsync, server disconnect,<br/>network error, missed keep-alives" --> closed(["Closed<br/>ConnectionClosed cancelled,<br/>IsConnected false"])
- Rekeying renews the keys after
RekeyAfterBytes(1 GiB) orRekeyAfterTime(1 hour), and whenever the server asks. It is transparent: transfers and sessions continue. - Closing. However the connection ends,
client.ConnectionClosed(aCancellationToken) is cancelled once,IsConnectedturns false andConnectionErrorsays why the connection failed. Every channel ends with it: pending reads end, and calls fail withSshException(ConnectionClosed). - No automatic reconnect. A closed client stays closed; open a new one. See Keep-alive and reconnecting.
Ways in
All three ConnectAsync overloads run the same handshake; they differ in how the byte stream is made and where the settings come
from.
| Overload | Transport | Settings |
|---|---|---|
ConnectAsync(host, port, SshClientOptions) |
TCP, optionally through Proxy (HTTP/SOCKS) from LocalEndPoint |
the options you pass |
ConnectAsync(Stream, SshClientOptions, leaveOpen) |
any duplex stream you provide | the options you pass; HostKeyAlias names the host |
ConnectAsync(destination, SshConfigConnectOptions) |
TCP, ProxyJump chain or ProxyCommand, as ssh_config says |
ssh_config, plus what only code can supply (passphrases, prompts, policies) |
Ownership
| Object | Owned by | Disposing it |
|---|---|---|
SshClient |
you | closes every channel and the connection; for a ProxyJump chain, every hop |
SshSession |
you | closes its channel; a running program loses its streams |
SftpClient |
you | ends the SFTP session; the connection stays |
Forwarders (SshLocalPortForwarder, ...) |
you | stops listening and closes their connections |
| Tunnel streams | you | closes that one channel |
SshPrivateKey |
you (the client never disposes keys you pass) | clears the private key material |
Clients from ISshConnectionProvider |
the provider | do not dispose |