Documentation
¶
Overview ¶
Package transport is the netdev SSH connection layer: host resolution (configured entries + ~/.ssh/config layering), authentication, host-key verification (system known_hosts read-only + a netdev-managed TOFU file), and a supervised connection with keepalive and exponential-backoff reconnect.
It is ported from DeepSeek-Reasonix's internal/remote (MIT, same origin as fairpeer) with the remote-workspace concerns removed: no bootstrap, no serve, no workbench, no SFTP, no port forwards. netdev talks CLI/NETCONF to network devices; the supervision surface is connect/exec/watch.
The package is frontend-agnostic: all interactivity flows through callbacks (HostKeyPrompt, SecretPrompt) and status subscriptions.
Index ¶
- Variables
- func ParseTarget(s string) (userName, host string, port int, err error)
- func TrustKey(hostname string, remoteAddr net.Addr, key ssh.PublicKey) error
- type AuthOptions
- type BackoffPolicy
- type Client
- func (c *Client) Close() error
- func (c *Client) Exec(ctx context.Context, cmd string) (ExecResult, error)
- func (c *Client) ExecInput(ctx context.Context, cmd string, input []byte) (ExecResult, error)
- func (c *Client) SSH() (*ssh.Client, error)
- func (c *Client) Start(ctx context.Context) error
- func (c *Client) Status() StatusEvent
- func (c *Client) Subscribe(fn func(StatusEvent)) (cancel func())
- type Clock
- type Dialer
- type EffectiveSSHConfig
- type ExecResult
- type HostEntry
- type HostKeyMismatchError
- type HostKeyPolicy
- type HostKeyPrompt
- type HostKeyQuestion
- type ImportedHost
- type JumpHostOptions
- type KeepalivePolicy
- type KnownHostLocation
- type LookupEntry
- type Options
- type ResolvedHost
- type SSHConfigSource
- func (s *SSHConfigSource) Aliases() []ImportedHost
- func (s *SSHConfigSource) Effective(alias string) EffectiveSSHConfig
- func (s *SSHConfigSource) EffectiveWithError(alias string) (EffectiveSSHConfig, error)
- func (s *SSHConfigSource) HasAlias(alias string) bool
- func (s *SSHConfigSource) HostName(alias string) string
- func (s *SSHConfigSource) IdentitiesOnly(alias string) bool
- func (s *SSHConfigSource) IdentityFile(alias string) string
- func (s *SSHConfigSource) IdentityFileNone(alias string) bool
- func (s *SSHConfigSource) IdentityFiles(alias string) []string
- func (s *SSHConfigSource) Path() string
- func (s *SSHConfigSource) Port(alias string) int
- func (s *SSHConfigSource) ProxyJump(alias string) string
- func (s *SSHConfigSource) User(alias string) string
- type SecretKind
- type SecretPrompt
- type Status
- type StatusEvent
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotConnected: the client is not currently connected (SSH access // while down, or Exec during a reconnect window). ErrNotConnected = errors.New("netdev: not connected") // ErrAuthFailed: every configured auth method was rejected; reconnects // stop rather than re-prompting in the background. ErrAuthFailed = errors.New("netdev: authentication failed") // ErrHostKeyMismatch: the presented host key contradicts a recorded one. // Never promptable — the user must inspect the named known_hosts line. ErrHostKeyMismatch = errors.New("netdev: host key mismatch") // ErrHostKeyRejected: the user declined a first-seen (TOFU) fingerprint. ErrHostKeyRejected = errors.New("netdev: host key rejected") )
Typed errors surfaced by dial/auth/host-key verification and the client.
var ManagedKnownHostsOverride string
ManagedKnownHostsOverride redirects the default managed known_hosts file for the whole package (tests isolate here instead of touching the user's real state tree). Empty = the fairpeer state dir default.
Functions ¶
func ParseTarget ¶
ParseTarget splits an ad-hoc "[user@]host[:port]" target. IPv6 literals use the bracketed form "[::1]:22".
func TrustKey ¶
TrustKey appends key to the netdev-managed known_hosts (the default ManagedPath) for hostname/remoteAddr. This is the programmatic half of the two-step TOFU flow: the UI captured a first-seen question, the human confirmed the fingerprint, and now the key is durably recorded so the next dial verifies instead of prompting.
Types ¶
type AuthOptions ¶
type AuthOptions struct {
Passphrase func() (string, error)
Password func() (string, error)
SecretPrompt SecretPrompt
DisableAgent bool
// contains filtered or unexported fields
}
AuthOptions supplies credential resolution for a dial. Passphrase and Password return already-resolved credential-store values (nil when none is configured). SecretPrompt is the interactive fallback — a terminal prompt in the CLI, a dialog in the desktop — and is only ever called on the first connect; reconnects reuse in-memory-cached secrets and never prompt.
type BackoffPolicy ¶
type BackoffPolicy struct {
Initial time.Duration // 0 => 1s
Factor float64 // 0 => 2
Max time.Duration // 0 => 60s
}
BackoffPolicy controls reconnect pacing: full-jitter exponential backoff.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a supervised SSH connection: it dials, verifies the host key, keeps the link alive, and reconnects with backoff. netdev sessions are on-demand (spec: devices have scarce VTY lines), so the supervisor exists to keep an ACTIVE session usable across link drops — not to hold idle sessions open.
func (*Client) Exec ¶
Exec runs cmd via `sh -c` on a fresh session and collects its output. (Network devices usually need a PTY-driven interactive CLI session instead — that is the driver layer's job, built on SSH(); Exec serves Linux hops.)
func (*Client) ExecInput ¶
ExecInput is Exec with bytes fed to the session's stdin (closed after the write). The proposal executor's file-upload path (§7.1 file-upload / §6.2 upload-only-in-proposals) uses it to stream file content through `base64 -d > path`; the read-only diagnostic surface never calls it.
func (*Client) Start ¶
Start dials and blocks until the first Connected (returns nil) or an unrecoverable error / ctx cancellation (returns the error). The supervisor keeps running after a successful Start; call Close to stop it.
func (*Client) Status ¶
func (c *Client) Status() StatusEvent
Status returns the last published status event.
func (*Client) Subscribe ¶
func (c *Client) Subscribe(fn func(StatusEvent)) (cancel func())
Subscribe registers a status callback; it receives the current event immediately and every subsequent transition. Callbacks must not block.
type Dialer ¶
Dialer is the first-hop transport. nil means a direct net.Dialer; the netdev config intentionally defaults to NOT routing device traffic through the shared HTTP proxy (spec §9.2 proxy_device_traffic).
type EffectiveSSHConfig ¶
type EffectiveSSHConfig struct {
HostName string
User string
Port int
IdentityFiles []string
IdentityFileNone bool
ProxyJump string
IdentitiesOnly bool
}
EffectiveSSHConfig is the subset of `ssh -G` output consumed by netdev. Keeping every IdentityFile is important: OpenSSH permits the directive to be repeated and probes the resulting identities in order.
type ExecResult ¶
ExecResult is the outcome of a one-shot remote command.
type HostEntry ¶
type HostEntry struct {
Name string
Host string
Port int // 0 => 22 (or ssh_config value)
User string
IdentityFile string
PassphraseEnv string
PasswordEnv string
ProxyJump string // OpenSSH ProxyJump syntax, comma-separated chain
UseSSHConfig bool // layer ~/.ssh/config values under unset fields
}
HostEntry is one configured netdev host target (device or hop). It is the transport-level shape of a [[netdev.devices]] / [[netdev.hops]] entry; the netdev config layer maps its TOML entries into this struct. Secrets follow the fairpeer idiom: the entry names credential env vars (passphrase_env / password_env); values live in the secret store, never in TOML.
type HostKeyMismatchError ¶
type HostKeyMismatchError struct {
Host string
PresentedFingerprint string
Locations []KnownHostLocation
}
HostKeyMismatchError describes a presented key that contradicts an existing known_hosts record. It unwraps to ErrHostKeyMismatch so callers can retain the existing fail-closed classification without parsing error strings.
func (*HostKeyMismatchError) Error ¶
func (e *HostKeyMismatchError) Error() string
func (*HostKeyMismatchError) Unwrap ¶
func (e *HostKeyMismatchError) Unwrap() error
type HostKeyPolicy ¶
type HostKeyPolicy struct {
// SystemKnownHosts are OpenSSH known_hosts files consulted read-only.
// Empty => [~/.ssh/known_hosts, ~/.ssh/known_hosts2] when they exist.
SystemKnownHosts []string
// ManagedPath is the netdev-managed known_hosts file that accepted TOFU
// keys are appended to. Empty => defaultNetdevKnownHosts().
ManagedPath string
// Prompt decides unknown (first-seen) keys. Nil => strict reject.
Prompt HostKeyPrompt
// Verified observes a key only after the known_hosts check (and, for TOFU,
// the user's acceptance and durable append) succeeded. It lets an assembly
// layer bind higher-level capabilities to the peer actually authenticated by
// this transport without weakening HostKeyCallback authority.
Verified func(HostKeyQuestion)
// Capture observes a FIRST-SEEN key together with its raw material the
// moment TOFU would prompt — before any decision. The two-step UI trust
// flow uses this: capture the key, reject, let the human confirm the
// fingerprint, then TrustKey() the captured material.
Capture func(hostname string, remote net.Addr, key ssh.PublicKey)
// contains filtered or unexported fields
}
HostKeyPolicy verifies presented host keys against the user's OpenSSH known_hosts files (read-only) and a netdev-managed file (read-write, TOFU).
func (*HostKeyPolicy) Callback ¶
func (p *HostKeyPolicy) Callback(ctx context.Context, host string) (ssh.HostKeyCallback, error)
Callback builds an ssh.HostKeyCallback enforcing this policy for host (the display label used in prompts). ctx bounds any interactive prompt.
func (*HostKeyPolicy) HostKeyAlgorithms ¶
HostKeyAlgorithms returns host-key algorithms in negotiation order, preferring algorithms compatible with ordinary host identities already recorded for hostname. Certificate-authority records are deliberately not treated as host keys: the CA algorithm does not describe the certified host key. The strict callback remains the authority for every negotiated key.
type HostKeyPrompt ¶
type HostKeyPrompt func(ctx context.Context, q HostKeyQuestion) (accept bool, err error)
HostKeyPrompt is called for an unknown host key. Returning (true, nil) accepts and persists it (trust on first use); (false, nil) rejects; a non-nil error aborts the dial. A nil prompt means strict mode: unknown hosts are rejected.
type HostKeyQuestion ¶
type HostKeyQuestion struct {
Host string // display label (user@host:port or alias)
Address string // the network address that presented the key
KeyType string // e.g. "ssh-ed25519"
Fingerprint string // ssh.FingerprintSHA256(key)
}
HostKeyQuestion describes a first-seen (TOFU) host key awaiting the user's decision.
type ImportedHost ¶
type ImportedHost struct {
Alias string
HostName string
User string
Port int
IdentityFile string
ProxyJump string
}
ImportedHost is one concrete Host alias surfaced by the inventory import.
type JumpHostOptions ¶
type JumpHostOptions struct {
Host ResolvedHost
Auth AuthOptions
}
JumpHostOptions binds one resolved ProxyJump host to credentials owned by that hop. Target credentials are never inherited implicitly.
type KeepalivePolicy ¶
type KeepalivePolicy struct {
Interval time.Duration // 0 => 30s; <0 disables keepalive
MaxMisses int // consecutive failures before declaring the link dead; 0 => 3
Timeout time.Duration // per-probe reply timeout; 0 => 10s
}
KeepalivePolicy controls liveness probing of an established connection.
type KnownHostLocation ¶
KnownHostLocation identifies the OpenSSH record that conflicts with a presented host key. It is intentionally structured so desktop clients can keep machine-local paths out of the primary error message while still exposing the exact record in an explicit security-details view.
type LookupEntry ¶
LookupEntry resolves a configured host name to its entry. The config layer supplies this (user-global inventory); transport stays config-agnostic.
type Options ¶
type Options struct {
Host ResolvedHost
Auth AuthOptions
JumpHosts []JumpHostOptions // resolved ProxyJump hosts in chain order
HostKeys *HostKeyPolicy
Dialer Dialer // first-hop transport; nil => direct
DialTimeout time.Duration // default 15s
Keepalive KeepalivePolicy
Backoff BackoffPolicy
Clock Clock // nil => real clock
Rand *rand.Rand
}
Options configures a Client. Host, Auth, and HostKeys are required; the rest default sensibly.
type ResolvedHost ¶
type ResolvedHost struct {
Name string // entry name, or the raw target for ad-hoc dials
HostName string // network address to dial
Port int
User string
IdentityFile string // explicit key path; empty => agent/default identities
IdentityFiles []string // ordered effective ssh_config identities
IdentityFileNone bool // ssh_config explicitly suppresses default identity files
IdentitiesOnly bool // ssh_config IdentitiesOnly: never offer unrelated agent keys
PassphraseEnv string // credential env var name for the key passphrase
PasswordEnv string // credential env var name for password auth
ProxyJump []string // resolved jump chain, in dial order
}
ResolvedHost is a fully resolved dial target: explicit entry fields layered over ~/.ssh/config values (when use_ssh_config) over defaults.
func ResolveHost ¶
func ResolveHost(lookup LookupEntry, nameOrTarget string, sshCfg *SSHConfigSource) (ResolvedHost, error)
ResolveHost builds the dial target for a configured host name or an ad-hoc "[user@]host[:port]" target. Field precedence: explicit entry value → ~/.ssh/config value (only when the entry sets use_ssh_config, or for ad-hoc targets when sshCfg is non-nil) → default (port 22, current OS user).
func ResolveJumpHosts ¶
func ResolveJumpHosts(lookup LookupEntry, chain []string, sshCfg *SSHConfigSource) ([]ResolvedHost, error)
ResolveJumpHosts resolves every ProxyJump token through the same host table and ~/.ssh/config layers as the final target. A jump entry's own ProxyJump is deliberately cleared: the caller-provided chain is already the complete left-to-right route, and recursively expanding nested chains would make ordering and credential ownership ambiguous.
func (ResolvedHost) Label ¶
func (h ResolvedHost) Label() string
Label is the display form user@host:port.
type SSHConfigSource ¶
type SSHConfigSource struct {
// contains filtered or unexported fields
}
SSHConfigSource discovers aliases from a parsed OpenSSH client config and resolves their effective values through the installed `ssh -G`. The embedded parser remains a compatibility fallback when the OpenSSH executable is not available.
func LoadSSHConfig ¶
func LoadSSHConfig(path string) (*SSHConfigSource, error)
LoadSSHConfig parses one OpenSSH client config file.
func LoadUserSSHConfig ¶
func LoadUserSSHConfig() (*SSHConfigSource, error)
LoadUserSSHConfig parses ~/.ssh/config. A missing file yields an empty source (all lookups return zero values), not an error.
func (*SSHConfigSource) Aliases ¶
func (s *SSHConfigSource) Aliases() []ImportedHost
Aliases lists concrete (non-wildcard, non-negated) Host aliases in file order without executing ssh -G or Match exec. Effective values are resolved only for a selected connection target.
func (*SSHConfigSource) Effective ¶
func (s *SSHConfigSource) Effective(alias string) EffectiveSSHConfig
Effective resolves alias through the user's installed OpenSSH client. This is the same source of truth used by VS Code Remote-SSH and covers Include, Host wildcards, Match rules, token expansion, and OpenSSH's precedence. If ssh is unavailable, netdev falls back to its embedded parser so existing installations without the executable keep working.
func (*SSHConfigSource) EffectiveWithError ¶
func (s *SSHConfigSource) EffectiveWithError(alias string) (EffectiveSSHConfig, error)
EffectiveWithError resolves alias without hiding an installed OpenSSH client's timeout or configuration error. The embedded parser is used only when ssh is genuinely unavailable (or a test explicitly disables it).
func (*SSHConfigSource) HasAlias ¶
func (s *SSHConfigSource) HasAlias(alias string) bool
HasAlias reports whether alias was declared as a concrete Host entry. It is intentionally stricter than `ssh -G`: OpenSSH returns defaults for arbitrary host names, which must not make a user-facing label override an older saved Host lookup key.
func (*SSHConfigSource) HostName ¶
func (s *SSHConfigSource) HostName(alias string) string
HostName returns the ssh_config HostName for alias, or "" when it would just echo the default/alias back.
func (*SSHConfigSource) IdentitiesOnly ¶
func (s *SSHConfigSource) IdentitiesOnly(alias string) bool
func (*SSHConfigSource) IdentityFile ¶
func (s *SSHConfigSource) IdentityFile(alias string) string
IdentityFile returns the first non-default identity file, ~-expanded.
func (*SSHConfigSource) IdentityFileNone ¶
func (s *SSHConfigSource) IdentityFileNone(alias string) bool
func (*SSHConfigSource) IdentityFiles ¶
func (s *SSHConfigSource) IdentityFiles(alias string) []string
func (*SSHConfigSource) Path ¶
func (s *SSHConfigSource) Path() string
Path is the file this source was parsed from (may not exist).
func (*SSHConfigSource) Port ¶
func (s *SSHConfigSource) Port(alias string) int
func (*SSHConfigSource) ProxyJump ¶
func (s *SSHConfigSource) ProxyJump(alias string) string
func (*SSHConfigSource) User ¶
func (s *SSHConfigSource) User(alias string) string
type SecretKind ¶
type SecretKind int
SecretKind identifies which interactive secret is being requested.
const ( SecretPassphrase SecretKind = iota // private-key passphrase SecretPassword // password auth )
func (SecretKind) String ¶
func (k SecretKind) String() string
type SecretPrompt ¶
type SecretPrompt func(ctx context.Context, kind SecretKind, host, identityFile string) (string, error)
SecretPrompt obtains a one-shot credential without persisting or publishing it. Implementations should respect ctx cancellation when the connection is stopped or superseded.
type Status ¶
type Status int
Status is the supervised connection state.
const ( // StatusIdle: created, Start not yet called. StatusIdle Status = iota // StatusConnecting: first dial in progress. StatusConnecting // StatusConnected: SSH established. StatusConnected // StatusReconnecting: connection lost, supervisor is backing off/redialing. StatusReconnecting // StatusStopped: Close was called, the context ended, or auth became // unrecoverable. Terminal. StatusStopped )
type StatusEvent ¶
type StatusEvent struct {
Host string // configured host name (or user@host target)
Status Status
Attempt int // reconnect attempt counter; 0 on the first connect
Err error // last error for Reconnecting/Stopped; nil otherwise
At time.Time
}
StatusEvent is one supervisor state transition, delivered to subscribers and returned by Client.Status.