transport

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 19 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func ParseTarget(s string) (userName, host string, port int, err error)

ParseTarget splits an ad-hoc "[user@]host[:port]" target. IPv6 literals use the bracketed form "[::1]:22".

func TrustKey

func TrustKey(hostname string, remoteAddr net.Addr, key ssh.PublicKey) error

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 New

func New(opts Options) (*Client, error)

New creates a Client. It does not dial; call Start.

func (*Client) Close

func (c *Client) Close() error

Close stops the supervisor and releases the connection.

func (*Client) Exec

func (c *Client) Exec(ctx context.Context, cmd string) (ExecResult, error)

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

func (c *Client) ExecInput(ctx context.Context, cmd string, input []byte) (ExecResult, error)

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) SSH

func (c *Client) SSH() (*ssh.Client, error)

SSH returns the current ssh client, or ErrNotConnected while down.

func (*Client) Start

func (c *Client) Start(ctx context.Context) error

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 Clock

type Clock interface {
	Now() time.Time
	After(d time.Duration) <-chan time.Time
}

Clock is the test seam for keepalive and reconnect timing.

type Dialer

type Dialer interface {
	DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}

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

type ExecResult struct {
	Stdout   []byte
	Stderr   []byte
	ExitCode int
}

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

func (p *HostKeyPolicy) HostKeyAlgorithms(hostname string, remote net.Addr) ([]string, error)

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

type KnownHostLocation struct {
	Filename string
	Line     int
}

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

type LookupEntry func(name string) (HostEntry, bool)

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) Addr

func (h ResolvedHost) Addr() string

Addr is the host:port dial string.

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
)

func (Status) String

func (s Status) String() string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL