Documentation
¶
Overview ¶
Package connector provides PTY-based SSH connection management with credential injection and session recording support.
The connector spawns SSH as a child process in a pseudo-terminal, enabling interactive sessions with automatic password injection and host key handling.
Connection Flow ¶
The Connector handles the complete SSH session lifecycle:
- Spawns ssh in a PTY for interactive I/O
- Detects and responds to password prompts
- Handles host key verification (accept-once mode optional)
- Proxies terminal I/O between user and SSH process
- Propagates window resize events (SIGWINCH)
Credential Injection ¶
When credentials are provided, the connector monitors SSH output for password prompts and injects the password automatically. Passwords are held in secure memory via the secret package and wiped after use.
Host Key Handling ¶
The connector supports an "accept-once" mode where unknown host keys are automatically accepted for the current session only, without persisting to known_hosts. This is useful for temporary or ephemeral connections.
Recording Integration ¶
Sessions can be recorded via asciinema. Use MaybeWrapWithRecording to check recording configuration and spawn a recording wrapper if enabled. The recording wrapper re-executes nssh under asciinema.
Connection Testing ¶
The TestConnection function performs a non-interactive connection test to diagnose connectivity and compatibility issues. This is used by the automatic compatibility fix system.
Example ¶
conn := connector.NewConnector(hostname, username, password, sshArgs)
conn.SetTimeouts(&cfg.SSH.Connection)
if err := conn.Run(ctx); err != nil {
// Handle connection error
}
Package connector provides PTY-based SSH connection handling.
Index ¶
Constants ¶
const ( DefaultRingBufferSize = 2048 DefaultPasswordFilterWindow = 100 * time.Millisecond )
Default configuration values.
const ( // TimingConfigLoad is emitted after config file is loaded. TimingConfigLoad = "config_load" // TimingCredentialLookup is emitted after credential resolution completes. TimingCredentialLookup = "credential_lookup" // TimingPTYStart is emitted after PTY allocation completes. TimingPTYStart = "pty_start" // TimingFirstRead is emitted when first data is read from PTY (SSH banner/prompt). TimingFirstRead = "first_read" // TimingPasswordPrompt is emitted when password prompt is detected (time since session start). TimingPasswordPrompt = "password_prompt" // TimingPasswordSent is emitted after password is injected (duration of injection). TimingPasswordSent = "password_sent" // TimingSessionEnd is emitted when the session ends (total session duration). TimingSessionEnd = "session_end" // TimingTotal is emitted with the total connection time (from connector.Run() start to end). TimingTotal = "total" )
Timing event names used throughout the connector and CLI.
Variables ¶
This section is empty.
Functions ¶
func EmitWithValue ¶
EmitWithValue logs a timing event with a custom duration value. Useful when timing was measured elsewhere.
func MaybeWrapWithRecording ¶
MaybeWrapWithRecording checks if recording is enabled and wraps the connection. Returns true if recording was started (caller should exit), false to continue normally.
func TimingEnabled ¶
func TimingEnabled() bool
TimingEnabled returns true if detailed timing instrumentation is enabled. Timing is enabled when NSSH_DEBUG=1 environment variable is set.
Types ¶
type Connector ¶
type Connector struct {
// contains filtered or unexported fields
}
Connector manages a PTY-based SSH connection with credential injection.
func NewConnector ¶
NewConnector creates a new PTY connector for SSH connections.
func (*Connector) GetStdinReader ¶
GetStdinReader returns an io.Reader that reads from the connector's stdin channel. This allows diverting stdin to other consumers (like UI prompts) while ensuring no data is lost if ensureStdinReader has already consumed it.
func (*Connector) Run ¶
Run is the main entry point for the connector. It handles the full lifecycle including host key restart if needed.
func (*Connector) SetAcceptOnceMode ¶
SetAcceptOnceMode configures how AcceptOnce handles host keys: "pin" (default) pre-seeds a temp known_hosts with the observed key; "accept-new" uses TOFU.
func (*Connector) SetResolvedEndpoint ¶
SetResolvedEndpoint sets the concrete hostname and port derived from SSH config. Used for host-key pinning (keyscan) when the user connects via an alias. NOT used as the SSH command target - we use the alias for proper config matching.
func (*Connector) SetTimeouts ¶
func (c *Connector) SetTimeouts(cfg *config.SSHConnectionConfig)
SetTimeouts configures connection timeouts from config.
type HostKeyAction ¶
type HostKeyAction int
HostKeyAction represents user's choice for host key handling.
const ( HostKeyReject HostKeyAction = iota // Reject connection HostKeyAcceptOnce // Accept for this session only (temp known_hosts) HostKeyAcceptAlways // Answer "yes" - SSH adds to real known_hosts )
Host key actions for interactive verification prompts.
type HostKeyResult ¶
type HostKeyResult int
HostKeyResult indicates how the caller should proceed after host key handling.
const ( HostKeyResultContinue HostKeyResult = iota // Connection proceeds normally HostKeyResultAbort // User rejected, exit HostKeyResultRestart // Need to restart with temp known_hosts )
Host key result codes indicating how the caller should proceed.
type RingBuffer ¶
type RingBuffer struct {
// contains filtered or unexported fields
}
RingBuffer provides a fixed-size circular buffer for prompt detection. It maintains a sliding window of the most recent N bytes, ensuring password prompts are never split across truncation boundaries.
Thread-safety: RingBuffer is NOT thread-safe. Callers must synchronize access when used from multiple goroutines.
func NewRingBuffer ¶
func NewRingBuffer(size int) *RingBuffer
NewRingBuffer creates a ring buffer with the specified capacity.
func (*RingBuffer) Len ¶
func (r *RingBuffer) Len() int
Len returns the number of bytes currently in the buffer.
func (*RingBuffer) LinearBytes ¶
func (r *RingBuffer) LinearBytes() []byte
LinearBytes returns buffer contents as a contiguous slice. The returned slice is only valid until the next Write call.
Performance: Uses zero-copy fast path when data hasn't wrapped. When data wraps around, copies to pre-allocated linearBuf.
func (*RingBuffer) Write ¶
func (r *RingBuffer) Write(p []byte)
Write appends bytes to the buffer, overwriting oldest data if full. Uses bulk copy operations for performance in the PTY hot path.
type TestConfig ¶
type TestConfig struct {
Timeout time.Duration // Connection timeout (default 10s)
Password *secret.Secret // Password for auth testing (nil = BatchMode)
Port string // SSH port (empty = use SSH config default)
// UseSystemKnownHosts controls whether probes write to the user's real
// known_hosts file. Default false uses a temp file to avoid persistence.
UseSystemKnownHosts bool
// ConfigFile specifies a custom SSH config file to use (-F flag).
// If empty, uses the default SSH config.
ConfigFile string
}
TestConfig configures the connection test.
func DefaultTestConfig ¶
func DefaultTestConfig() TestConfig
DefaultTestConfig returns sensible defaults for connection testing.
type TestResult ¶
type TestResult struct {
Success bool // Whether the connection/auth succeeded
ExitCode int // SSH exit code
Stderr string // SSH verbose output (combined stdout/stderr from PTY)
AuthMethod string // Detected auth method from verbose output
}
TestResult holds the outcome of a connection test.
func TestConnection ¶
func TestConnection(ctx context.Context, hostname, username string, cfg TestConfig) (*TestResult, error)
TestConnection runs a non-interactive SSH connection test. It uses -vv for verbose output to capture negotiation details. If password is nil, uses BatchMode (compat-only testing). If password is provided, injects it via PTY (full auth testing).
type TimingEvent ¶
TimingEvent represents a named timing measurement point. Use StartTiming() to create a new event, then call Emit() when the operation completes. The duration and event name will be written to stderr in a parseable format.
func StartTiming ¶
func StartTiming(name string) *TimingEvent
StartTiming creates a new timing event with the given name. The event's start time is recorded immediately. Call Emit() when the operation completes to log the duration.
Example:
t := StartTiming("pty_start")
defer t.Emit()
// ... operation ...
func (*TimingEvent) Emit ¶
func (t *TimingEvent) Emit()
Emit logs the timing event to stderr if timing is enabled. The output format is: NSSH_TIMING:<event_name>:<duration_ms> This format is designed to be easily parsed by benchmark tools.
If timing is not enabled (NSSH_DEBUG != "1"), this is a no-op.