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); }