namespace DodoSSH.Client.Ssh;
/// How to authenticate to a host.
///
/// Always decrypted from the vault immediately before use and never persisted outside it. Because
/// SSH terminates on the client, using a credential requires its plaintext here — which is exactly
/// why the Connect permission is a UI hint and not an enforceable boundary. See ADR 0001.
///
public abstract record SshCredential;
/// Password authentication, and keyboard-interactive where the server prefers it.
public sealed record SshPasswordCredential(string Password) : SshCredential;
/// Public-key authentication.
/// The private key in PEM form, as stored in the vault.
/// Passphrase protecting the key, when it has one.
public sealed record SshPrivateKeyCredential(byte[] PrivateKeyPem, string? Passphrase) : SshCredential;
///
/// A SOCKS5 proxy on this machine's loopback interface, through which a host is reached.
///
/// The port it is listening on.
///
///
/// ◆ A port and nothing else, so a proxy anywhere but loopback cannot be expressed. Both things
/// that will produce one of these listen on 127.0.0.1 — SSH.NET's own dynamic forward over a
/// bastion, and the bridge that will front the server relay — and a SOCKS proxy bound to any other
/// interface is an open proxy into whatever network the machine is on, for as long as the shell is up.
/// Leaving the host out of this type is what makes that unrepresentable rather than merely unlikely; it is
/// the same reason AuthenticationChoice carries a kind beside its id.
///
///
/// SOCKS5 rather than a plain pipe, and that is what keeps host key pinning honest. The target's
/// real name and port stay in and
/// and travel to the proxy in the CONNECT request, so the connection is *about* the target throughout —
/// nothing downstream has to be told that the address dialled is not the address being spoken to. A dumb
/// pipe would have meant handing SSH.NET 127.0.0.1 and remembering, everywhere else, that it was a
/// stand-in. See the gate in SshNetConnectionFactory, which pins what this request names.
///
///
/// Nothing in this assembly opens one. The proxy is somebody else's — a forward on a bastion connection,
/// or the relay bridge — and its lifetime belongs to whoever opened it, which must outlast the connection
/// made through it. See docs/reaching-a-host-you-cannot-dial.md.
///
///
public sealed record SshLoopbackProxy(int Port);
/// Everything needed to reach one host.
/// Hostname or address.
/// Port.
/// Remote account.
/// How to authenticate.
/// How long to wait for the transport and handshake.
///
/// A loopback SOCKS5 proxy to reach through, or null to dial it directly.
///
/// Last and optional, so that every existing caller — which is every connection this product makes today —
/// keeps meaning exactly what it did. A host that can be dialled is still dialled.
///
///
public sealed record SshConnectionRequest(
string Host,
int Port,
string Username,
SshCredential Credential,
TimeSpan? ConnectTimeout = null,
SshLoopbackProxy? Proxy = null);
/// An interactive shell over a pseudo-terminal.
public interface ISshShellSession : IAsyncDisposable
{
/// Whether the channel is still usable.
bool IsOpen { get; }
/// Reads whatever output is available, blocking until at least one byte arrives.
/// Bytes read, or 0 once the remote closes the channel.
ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken);
/// Sends keystrokes to the remote.
ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken);
///
/// Tells the remote the terminal has been resized.
///
///
/// Verified to reach the remote against a real sshd; see PtyAndResizeSpikeTests. Sizes
/// that are not are dropped rather than forwarded.
///
void Resize(TerminalSize size);
}
/// An authenticated connection to one host.
public interface ISshConnection : IAsyncDisposable
{
/// Whether the transport is still up.
bool IsConnected { get; }
/// The host key that was accepted for this connection.
HostKeyPresentation HostKey { get; }
///
/// The negotiated server-to-client encryption algorithm, e.g. aes256-gcm@openssh.com.
///
///
///
/// Read once, immediately after the handshake, off SSH.NET's own ConnectionInfo.CurrentServerEncryption.
/// The only event that could make this stale is a rekey, and SSH.NET raises no event for one and exposes no
/// way to ask again — there is nothing behind this property to go and re-read. A captured value is therefore
/// not a snapshot that might drift; it is the only value there has ever been a moment to observe.
///
///
/// Server-to-client, not client-to-server. SSH negotiates the two directions independently and a
/// server is free to choose differently for each, so the two can in principle disagree. This is the
/// direction the bytes drawn on a terminal pane travelled in, which is the fact a status bar showing what
/// the screen is made of should be naming.
///
///
string Cipher { get; }
/// Opens an interactive shell with a pseudo-terminal.
Task OpenShellAsync(TerminalSize size, CancellationToken cancellationToken);
}
///
/// How far a connection being made has got.
///
///
///
/// These are the boundaries a client can actually observe, and there are deliberately no others. SSH.NET
/// runs the whole handshake inside one ConnectAsync and raises exactly one event from the middle of
/// it — HostKeyReceived, once the key exchange has produced a key to show. That event is the only
/// interior moment there is, so it is the only interior phase named here: everything before it is
/// and everything after it is .
///
///
/// ◆ Nothing here is a guess about elapsed time or a fraction of the way through. Each value is
/// reported at the instant the thing it names actually starts, which is what makes it safe for a screen to
/// draw as fact. A phase that took no measurable time is reported anyway and simply passes at once — that
/// is a true account of a fast handshake, not a step that was skipped. See the transfer strip's own remark
/// in TransfersScreen.axaml for why this design does not invent furniture for states it cannot measure.
///
///
public enum SshConnectionPhase
{
/// Resolving the name, opening the socket, and exchanging keys. Before any key is known.
Reaching = 0,
/// The server has offered a host key, and its trust is being decided.
CheckingHostKey = 1,
/// The key was accepted. The credential is being offered.
Authenticating = 2,
/// Authenticated. A pseudo-terminal and a shell channel are being opened.
OpeningShell = 3,
}
/// Opens connections, enforcing host key trust before authenticating.
public interface ISshConnectionFactory
{
///
/// Connects and authenticates.
///
/// What to connect to, as whom, and with what.
///
/// Told each phase as it begins, or null to report nothing. Called from whichever thread the handshake
/// is on — SSH.NET raises host key verification on its own — so an implementation that touches a UI must
/// marshal for itself.
///
/// Abandons the attempt.
///
/// The host has no pinned key. The caller must show the fingerprint, and only on explicit
/// confirmation record it via and retry.
///
///
/// The presented key differs from the pin. There is no retry path: this is a hard block.
///
Task ConnectAsync(
SshConnectionRequest request,
IProgress? progress,
CancellationToken cancellationToken);
}