Help us shape the future of Go! Take the 2026 Go Developer Survey

pty

package
v1.3.103 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: AGPL-3.0 Imports: 42 Imported by: 0

Documentation

Overview

Package pty pkg/pty/cli.go c3-vis-pty

Package pty pkg/pty/cli_tcp.go c3-vis-pty for the dmsgpty service. Counterpart to host_tcp.go: dials a raw TCP listener exposed by the remote visor's pty.Host, runs an XK noise handshake pinning the remote PK, and drives the same PtyClient surface used over the dmsg-overlay path.

Identity model: the local PK/SK passed to each method is the caller's stable identity, which is what the remote whitelist gates on. Operators wanting ssh-equivalent semantics pass a pinned --sk so the remote can authorize repeated connections from the same client across runs.

Package pty pkg/pty/cli_unix.go c3-vis-pty

Package pty pkg/pty/conf.go c3-vis-pty

Package pty pkg/pty/const.go c3-vis-pty

Package pty pkg/pty/const_unix.go c3-vis-pty

Package pty pkg/pty/dialer.go c3-vis-pty

StreamDialer abstracts the outbound dial that Host uses to proxy CLI requests to a remote dmsgpty endpoint. Pre-refactor, Host reached into its bound *dmsg.Client directly (h.dmsgC.DialStream) at every call site — pinning the entire dmsgpty stack to dmsg as the only outbound transport, even though the wire-level PtyClient only needs an io.ReadWriteCloser. This file introduces a small interface around just the outbound dial so a transport-aware adapter can be swapped in without touching the rest of Host.

Phase 1 (#2671): NewHost wires the default dmsg adapter, behavior is unchanged. Phase 2 (this PR): MultiDialer composes strategies, visor init goes through NewHostWithDialer with a single-strategy chain (still dmsg-only). Phase 3 (next): a transport-aware adapter backed by SkywireNetworker / transport.Manager prepends to the chain so `cli dmsg pty start` rides the visor's already-negotiated transports first and falls through to dmsg only on miss.

The listening side (Host.ListenAndServe → h.dmsgC.Listen) and the logger plumbing stay on *dmsg.Client; only the proxy-out dial is pluggable. dmsgpty-hosts are still reached over dmsg by remote peers — only this host's outgoing proxy gets the transport choice.

Package pty pkg/pty/exec_builtin_other.go c3-vis-pty

Package pty pkg/pty/exec_http.go c3-vis-pty Streaming one-shot exec, served as HTTP over the dmsgpty stream.

The capability is the same as PtyGateway.Exec — run one command on a peer visor and learn its exit code — but the output arrives as it is produced instead of being buffered until the command exits. Exec cannot do that because it is dispatched over net/rpc (see host_mux.go), and net/rpc is strictly one call / one reply: there is no way to hand the caller bytes mid-call. That is what forces exec_gateway.go to accumulate stdout and stderr in memory, and what forces execMaxOutputBytes (16 MiB) on them — buffering a peer's unbounded output is a memory-exhaustion vector. Neither limit is inherent to the capability; both are inherited from the dispatch.

Why HTTP rather than a bespoke protocol: chunked transfer-encoding is streaming, and HTTP trailers exist for precisely the problem this subsystem has — a status that is only known AFTER an unbounded body has been sent. Declaring "Trailer: X-Exec-Exit-Code, ..." up front and setting those headers once the command has exited is the protocol-level answer to "where does the exit code go", so this file does not have to invent one. The response status line also gives every pre-stream failure a place to live: a rejected request is a 400 with a plain-text body, which the client always has something to read from. A hand-rolled protocol has to remember to say something on every error path or it deadlocks the reader.

Dispatch: this rides hostMux.HandleConn against ExecStreamURI, the same raw-conn path sftp uses. The mux does URI matching and the accept response, then hands the stream over; from that byte onward the conn speaks HTTP/1.1. It is NOT reachable through pkg/dmsg/dmsghttp's ListenAndServe — that binds its own dmsg port with no dmsgpty handshake and no whitelist gate. Only the wire format is shared, and it is stdlib net/http on both ends, so nothing new is vendored.

Exec is NOT deprecated by this. For bounded reads — a unit file, a config, a status probe — one round trip returning a structured CommandExecResult is a better shape than a stream the caller has to reassemble.

Trust model: identical to Exec, Start and sftp. The conn arriving here was already authorized by h.authorize() against the dmsgpty whitelist in the listening loop. A peer trusted to open an interactive shell is already trusted to run one command, and this is strictly less powerful than the shell. There is deliberately no second ACL — see the header of sftp_subsystem.go for the same reasoning applied to filesystem access.

Package pty pkg/pty/exec_http_client.go c3-vis-pty Client half of the streaming exec subsystem. See exec_http.go for why it is HTTP and what the framed body looks like.

This does not use pkg/dmsg/dmsghttp's HTTPTransport. That transport dials its own dmsg stream and pools it, which skips the dmsgpty mux handshake (and therefore the whitelist gate) and would keep a finished exec stream in an idle pool for a subsystem that is one-command-per-conn. What is wanted here is one HTTP exchange over a conn somebody else already authorized, and that is exactly http.Request.Write + http.ReadResponse.

Package pty pkg/pty/exec_pool.go c3-vis-pty

Session reuse for one-shot remote Exec. Establishing a dmsgpty session (dial + noise + the RPC writeRequest/readResponse handshake + the remote's proxy dial) costs ~10-15s and was paid fresh on EVERY `cli pty exec`. The remote Exec gateway is STATELESS — each Exec spawns its own os/exec.Cmd and touches no gateway state (see exec_gateway.go) — so one established session can serve unlimited sequential Exec calls. This pool caches one session per (remote PK, port, dialer) so only the first exec to a peer pays setup; the rest are an RPC round-trip.

Correctness over a flaky overlay: a failed Exec (an RPC/connection error, distinct from a non-zero command exit which returns err==nil) retires the session, so the caller's next exec re-dials. A dead pooled conn is never silently reused — the trap that bit the RSN pool (a TTL outliving the underlying conn → handing out shut-down connections).

Locking discipline: execMu guards the pool map and every pooledExec field, but is NEVER held across a session Close. *PtyClient.Close() is a synchronous, un-timeouted RPC round-trip (Stop() + rpcC.Close()) over a possibly-dead stream; since execMu is Host-global, closing under it would stall every peer's pooled-exec ops behind one dead peer's slow Close. retireLocked therefore RETURNS the session to close and each public method closes it after releasing the lock.

Package pty pkg/pty/host.go c3-vis-pty

Package pty pkg/pty/host_mux.go c3-vis-pty

Package pty pkg/pty/host_tcp.go c3-vis-pty point for the dmsgpty / dmsgscp / dmsgcat protocols, with an XK noise handshake gating the dmsgpty whitelist.

The dmsg-overlay path (ListenAndServe on a dmsg port) is the primary entry. This file adds a parallel listener that operators can enable via Dmsgpty.SshListen in the visor config. The handshake model intentionally mirrors ssh's:

  • The CLIENT pins the SERVER's PK (passed as RemotePK on the XK-Initiator side). A wrong PK fails the handshake — the equivalent of "host key check failed".
  • The SERVER learns the CLIENT's PK from the completed handshake (noise.RemoteStatic()). That PK is then checked against the dmsgpty Whitelist — the equivalent of authorized_keys but indexed by visor PK instead of an SSH pubkey.

The Stream that comes out of the noise wrapper satisfies net.Conn and is fed to the same serveConn path that the dmsg-overlay listener uses, so dmsgpty / dmsgscp / dmsgcat handlers are transport-agnostic at the protocol layer.

Package pty pkg/pty/pty_client.go c3-vis-pty

Package pty pkg/pty/pty_gateway.go c3-vis-pty

Package pty pkg/pty/pty_gateway_unix.go c3-vis-pty

Package pty pkg/pty/pty_session.go c3-vis-pty

ptySession is the host-side persistence primitive for detach/reattach pty sessions (Option B, phase 1). A single pump goroutine is the ONLY reader of the underlying *Pty: it copies output into a bounded ring buffer and signals waiters. Clients read by following a monotonic logical offset into that ring, so there is never a reader handoff to race on — attach/detach is just whether a follower exists, and a detached shell keeps running because the pump keeps draining it (a kernel pty buffer is tiny; without a constant reader the shell blocks on write).

Phase 1 lands the session + ring + pump (so a shell survives a dropped stream) and the registry + GC (so abandoned shells don't leak). The attach-by-id protocol, gateway Read rework, and client reconnect are later phases; this file is self-contained and unit-tested in isolation.

Package pty pkg/pty/pty_session_gateway.go c3-vis-pty

sessionPtyGateway is the persistent-session variant of LocalPtyGateway. The classic gateway owns a *Pty directly and the host kills it when the serving connection ends; this gateway instead drives a registry-tracked *ptySession whose single pump keeps the shell alive across a dropped stream. On Start it creates and registers a session; on connection teardown the host calls detach() (not Stop), leaving the shell running for a later reattach until the GC reaps it.

The wire protocol is identical to LocalPtyGateway — same RPC method set — so an unmodified client works unchanged; it simply can't reattach yet (it never learns the session id). The id-return + Attach RPC that enables reconnect is a later phase; this gateway is the host-side seam it plugs into.

Package pty pkg/pty/pty_session_registry.go c3-vis-pty

sessionRegistry tracks live ptySessions by id so the host side of the pty can survive a dropped stream: when a client's connection tears down the gateway detaches its follower (instead of killing the shell), leaving the session in the registry for a later reattach. A GC goroutine reaps sessions that have either exited (the shell quit) or sat idle (no follower) past a TTL, so an abandoned shell can't leak indefinitely.

This is the host-side mechanism for detach/reattach (Option B). The attach protocol that lets a reconnecting client rebind to an id is a later phase; the registry + GC + id allocation live here.

Package pty pkg/pty/pty_unix.go c3-vis-pty

Package pty pkg/pty/rpc_util.go c3-vis-pty

Package pty pkg/pty/sftp_client.go c3-vis-pty dmsgpty sftp subsystem.

Two surfaces: DialSftpTCP for the direct-TCP path (same noise-XK pin-by-PK model used by `skywire cli pty shell`), and DialSftpDmsg for the dmsg-overlay path. Both return an io.ReadWriteCloser ready to hand to github.com/pkg/sftp.NewClientPipe — the dmsgpty mux handshake (writeRequest(SftpURI) + readResponse) has already been performed.

Callers own the returned conn; close it via the resulting *sftp.Client.Close (which closes the conn underneath).

Package pty pkg/pty/sftp_subsystem.go c3-vis-pty that rides the same noise-protected stream and whitelist auth as the pty subsystem, dispatched by SftpURI.

Trust model: the conn arriving here has ALREADY been authorized by h.authorize() in the listening loop (ListenAndServe / ListenAndServeNet / ListenAndServeTCP). The remote PK is on the dmsgpty whitelist; that is the same gate as the interactive-pty path. There is no separate sftp ACL by design — operators who trust a peer for shell access implicitly trust them for filesystem access, mirroring OpenSSH's default where sshd grants both unless explicitly subsystem-restricted.

Mount-side concerns (read-only, chroot, allow-list of paths) are out of scope for v1 and belong in a follow-up that adds subsystem-level policy hooks shared between exec/pty/sftp. Until then the served root is the host process's filesystem view as it would be over ssh; the dmsgpty_whitelist gate is the operator's lever.

Package pty pkg/pty/ui.go c3-vis-pty

Package pty pkg/pty/ui_dialer.go c3-vis-pty

Package pty pkg/pty/ui_page.go c3-vis-pty

Package pty pkg/pty/ui_socket.go c2-app-pty

Package pty pkg/pty/ui_target.go c3-vis-pty

Package pty pkg/pty/ui_unix.go c3-vis-pty

Package pty pkg/pty/whitelist.go c3-vis-pty

Package pty pkg/pty/whitelist_client.go c3-vis-pty

Package pty pkg/pty/whitelist_gateway.go c3-vis-pty

Index

Constants

View Source
const (
	PtyRPCName  = "pty"
	PtyURI      = "dmsgpty/pty"
	PtyProxyURI = "dmsgpty/proxy"
)

Constants related to pty.

View Source
const (
	DefaultPort     = uint16(22)
	DefaultCmd      = "/bin/bash"
	DefaultFlagExec = "-c"
)

Constants related to dmsg.

View Source
const (
	WhitelistRPCName = "whitelist"
	WhitelistURI     = "dmsgpty/whitelist"
)

Constants related to whitelist.

View Source
const (
	// SftpURI is the dispatch key for the sftp subsystem. Server-side
	// hands the conn to github.com/pkg/sftp.NewServer instead of the
	// net/rpc-driven pty gateway. Whitelist gating is identical to PtyURI.
	SftpURI = "dmsgpty/sftp"

	// ExecStreamURI is the dispatch key for streaming one-shot exec. Separate
	// from the net/rpc exec gateway because net/rpc cannot stream: this rides
	// the raw-conn path, and the conn then speaks ordinary HTTP/1.1 so that
	// chunked encoding carries the output live and HTTP trailers carry the
	// exit code once it is known. No cap, no truncation.
	ExecStreamURI = "dmsgpty/exec-stream"
)

Subsystem URIs share the dmsgpty mux + noise-protected stream + whitelist auth. The mux dispatches on URI alone — new subsystems are added by registering a new URI rather than bumping a wire-protocol version, so older peers either serve the URIs they know (pty) and reject the rest (preserving backwards-compat) or, on the client side, simply never ask for what they don't implement.

View Source
const (
	// DefaultCLINet for windows
	DefaultCLINet = "unix"
)

Variables

View Source
var (
	ErrSessionNotFound = errors.New("dmsgpty: session not found")
	ErrSessionNotOwned = errors.New("dmsgpty: session owned by a different public key")
)

Reattach errors. ErrSessionNotFound means the id is unknown — the session was never created, already Stop'd, or GC-reaped after its idle TTL. Both are surfaced to a reconnecting client so it can fall back to starting fresh.

View Source
var (
	ErrPtyAlreadyRunning = errors.New("a pty session is already running")
	ErrPtyNotRunning     = errors.New("no active pty session")
)

Pty errors.

Functions

func DefaultCLIAddr

func DefaultCLIAddr() string

DefaultCLIAddr gets the default cli address (temp address)

func DialSftpDmsg

func DialSftpDmsg(
	ctx context.Context,
	c *dmsg.Client,
	rPK cipher.PubKey,
	rPort uint16,
) (io.ReadWriteCloser, error)

DialSftpDmsg opens an sftp-subsystem stream to (rPK, rPort) over the supplied dmsg.Client. rPort==0 falls back to DefaultPort (22) to match the rest of the pty surface.

func DialSftpTCP

func DialSftpTCP(
	ctx context.Context,
	addr string,
	localPK cipher.PubKey,
	localSK cipher.SecKey,
	rPK cipher.PubKey,
) (io.ReadWriteCloser, error)

DialSftpTCP opens an sftp-subsystem stream to a remote dmsgpty-host over its direct-TCP entry point (Dmsgpty.SshListen). The noise-XK handshake pins rPK; localPK/localSK identify this client to the remote whitelist. Returns a conn whose stream is positioned at the start of the sftp byte protocol — pass to sftp.NewClientPipe.

Error surfaces: dial / handshake errors propagate verbatim; an "invalid request" response from the remote means the server lacks SftpURI registration (older host binary) — surface this as a clear "remote dmsgpty-host has no sftp subsystem" so operators don't chase it as a transport bug.

func OpenSftpConn

func OpenSftpConn(conn net.Conn) (io.ReadWriteCloser, error)

OpenSftpConn performs the dmsgpty sftp-subsystem handshake on a stream that is ALREADY connected to a remote dmsgpty host — e.g. a conn bridged through the local visor's authorized dmsg client (the `cli pty fs mount` via-visor path). The dial + whitelist auth has happened on the visor side using its own identity; here we only run the mux handshake (writeRequest(SftpURI) + readResponse). On success the returned conn is positioned at the start of the sftp byte protocol — pass to sftp.NewClientPipe. The caller owns conn.

func ParseWindowsEnv

func ParseWindowsEnv(cliAddr string) string

ParseWindowsEnv finds '%'-enclosed windows env in json string

func WriteConfig

func WriteConfig(conf Config, path string) error

WriteConfig write the config struct to the provided path

Types

type CLI

type CLI struct {
	Log  logrus.FieldLogger `json:"-"`
	Net  string             `json:"cli_network"`
	Addr string             `json:"cli_address"`
}

CLI connects with and has ownership over a pty.Host.

func DefaultCLI

func DefaultCLI() CLI

DefaultCLI returns a CLI with default values.

func (*CLI) ExecLocal

func (cli *CLI) ExecLocal(ctx context.Context, name string, args []string, env []string, stdin []byte, timeout time.Duration) (*CommandExecResult, error)

ExecLocal runs a one-shot command on the local host (no PTY) via the same dmsgpty client surface. Useful for self-checks where the caller wants the same cap + timeout semantics as the remote path.

func (*CLI) ExecRemote

func (cli *CLI) ExecRemote(ctx context.Context, rPK cipher.PubKey, rPort uint16, name string, args []string, env []string, stdin []byte, timeout time.Duration) (*CommandExecResult, error)

ExecRemote runs a one-shot command on a remote host (no PTY). Whitelist gating is identical to StartRemotePty — the same dmsgpty host trust check applies. Returns the captured result; a non-nil error here is an RPC-layer failure, not a remote command exit-non- zero (callers must inspect resp.ExitCode and resp.TimedOut).

rPort = 0 falls back to the host's default dmsgpty port (22). stdin may be nil for commands that don't read it.

func (*CLI) ExecRemoteTCP

func (cli *CLI) ExecRemoteTCP(
	ctx context.Context,
	rPK cipher.PubKey,
	addr string,
	localPK cipher.PubKey,
	localSK cipher.SecKey,
	name string,
	args []string,
	env []string,
	stdin []byte,
	timeout time.Duration,
) (*CommandExecResult, error)

ExecRemoteTCP runs a one-shot command on a remote visor via the dmsgpty-host's direct-TCP entry point (Dmsgpty.SshListen). rPK is pinned during the noise handshake — equivalent to ssh's host-key check. addr is `host:port` (any net.Dial-acceptable form). localPK/localSK is the caller's identity; the remote's whitelist gates on it.

Semantics match ExecRemote (dmsg-overlay path): returns the command's stdout / stderr / exit code on success; an err here is dial / handshake / RPC-layer failure, not a non-zero command exit.

func (*CLI) StartLocalPty

func (cli *CLI) StartLocalPty(ctx context.Context, cmd string, args ...string) error

StartLocalPty starts a pty on the host.

func (*CLI) StartRemotePty

func (cli *CLI) StartRemotePty(ctx context.Context, rPK cipher.PubKey, rPort uint16, cmd string, args ...string) error

StartRemotePty starts a pty on a remote host, proxied via the local pty.

func (*CLI) StartRemotePtyTCP

func (cli *CLI) StartRemotePtyTCP(
	ctx context.Context,
	rPK cipher.PubKey,
	addr string,
	localPK cipher.PubKey,
	localSK cipher.SecKey,
	cmd string,
	args ...string,
) error

StartRemotePtyTCP opens an interactive pty session against a remote dmsgpty-host over the direct-TCP entry point. Mirrors StartRemotePty over the dmsg-overlay path.

func (*CLI) WhitelistClient

func (cli *CLI) WhitelistClient() (*WhitelistClient, error)

WhitelistClient returns a client that interacts with the Host's whitelist.

type CommandExecReq

type CommandExecReq struct {
	Name      string   // command, looked up in $PATH on the host
	Arg       []string // command arguments
	Env       []string // additional env vars; merged on top of host env
	Stdin     []byte   // optional stdin; capped at execMaxStdinBytes
	TimeoutMS int64    // wall-clock cap; 0 = execDefaultTimeout
}

CommandExecReq is the input to PtyGateway.Exec. Mirrors CommandReq for the interactive Start path but adds Stdin and TimeoutMS.

type CommandExecResult

type CommandExecResult struct {
	Stdout          []byte
	Stderr          []byte
	StdoutTruncated bool
	StderrTruncated bool
	ExitCode        int
	TimedOut        bool
	// DurationMS reports the time from spawn to exit/timeout, useful
	// for callers measuring command latency without round-tripping
	// their own timestamps.
	DurationMS int64
}

CommandExecResult is the output of PtyGateway.Exec.

Stdout / Stderr are captured to memory and may be truncated when the command produces more than execMaxOutputBytes; the StdoutTruncated / StderrTruncated flags signal it. ExitCode follows Go's os/exec conventions (-1 on signal-kill, 0 on success, non-zero on exit). TimedOut is set when the deadline fired before the command exited.

type CommandReq

type CommandReq struct {
	Name string
	Arg  []string
	Size *WinSize
	Env  []string // Environment variables to pass to the PTY (e.g., TERM=xterm-256color)
}

CommandReq represents a pty command.

type Config

type Config struct {
	DmsgDisc     string   `json:"dmsgdisc"`
	DmsgSessions int      `json:"dmsgsessions"`
	DmsgPort     uint16   `json:"dmsgport"`
	CLINet       string   `json:"clinet"`
	CLIAddr      string   `json:"cliaddr"`
	SK           string   `json:"sk"`
	PK           string   `json:"pk"`
	WL           []string `json:"wl"`

	// SshListen, when non-empty, brings up the direct-TCP entry point
	// alongside the dmsg-overlay listener — same shape and semantics
	// as the visor-embedded version (see pkg/visor/visorconfig:
	// Dmsgpty.SshListen). net.Listen syntax: ":2022",
	// "0.0.0.0:2022", "127.0.0.1:2022", IPv6 forms all work. Empty
	// (default) disables the TCP entry point: only the dmsg-overlay
	// path is served, matching legacy behavior.
	//
	// This is the surface that `skywire cli pty shell` (client) and
	// `skywire cli pty host` (server) expose as the OpenSSH-equivalent
	// shell over skywire.
	//
	// Auth flow per accepted TCP connection:
	//   1. XK noise handshake using this host's PK/SK. The client
	//      pins the server's PK; mismatched pin fails handshake
	//      (ssh host-key-check equivalent).
	//   2. The handshake-derived client PK is checked against the
	//      same whitelist the dmsg-overlay accept loop uses. No
	//      separate ACL.
	SshListen string `json:"ssh_listen,omitempty"`
}

Config struct is used to read the values from the config.json file

func DefaultConfig

func DefaultConfig() Config

DefaultConfig is used to populate the config struct with its default values

type ErrorJSON

type ErrorJSON struct {
	ErrorCode int    `json:"error_code"`
	ErrorMsg  string `json:"error_msg"`
}

ErrorJSON displays errors in JSON format.

type ExecStreamReq

type ExecStreamReq struct {
	Name      string   `json:"name"`
	Arg       []string `json:"arg,omitempty"`
	Env       []string `json:"env,omitempty"`
	Stdin     []byte   `json:"stdin,omitempty"`
	TimeoutMS int64    `json:"timeout_ms,omitempty"`
}

ExecStreamReq is the request body, JSON. It mirrors CommandExecReq so the buffered and streaming paths take the same shape of input.

Stdin stays request-side and capped, as in the buffered path: streaming stdin would make this a bidirectional session, which is what Start already is. A command that needs interactive input wants a TTY, not this.

type ExecStreamResult

type ExecStreamResult struct {
	ExitCode   int
	TimedOut   bool
	DurationMS int64
	Err        string
}

ExecStreamResult is what the caller gets once the command has ended. It carries the same facts CommandExecResult does, minus the output — that already went to the writers as it arrived.

func ExecStreamDmsg

func ExecStreamDmsg(
	ctx context.Context,
	dmsgC *dmsg.Client,
	rPK cipher.PubKey,
	rPort uint16,
	req ExecStreamReq,
	stdout, stderr io.Writer,
) (*ExecStreamResult, error)

ExecStreamDmsg runs a command on rPK over the dmsg overlay, streaming its output to stdout/stderr as it is produced.

func ExecStreamTCP

func ExecStreamTCP(
	ctx context.Context,
	addr string,
	localPK cipher.PubKey,
	localSK cipher.SecKey,
	rPK cipher.PubKey,
	req ExecStreamReq,
	stdout, stderr io.Writer,
) (*ExecStreamResult, error)

ExecStreamTCP is the direct-TCP equivalent, for hosts reachable on Dmsgpty.SshListen without going over the overlay.

type Host

type Host struct {
	// contains filtered or unexported fields
}

Host represents the main instance of pty.

dmsgC is kept for the listening side (ListenAndServe) and logger access; the outbound proxy-dial side now goes through `dialer` so callers can plug in a transport-aware StreamDialer. NewHost wires the default dmsg adapter so behavior is unchanged when callers don't opt in.

func NewHost

func NewHost(dmsgC *dmsg.Client, wl Whitelist) *Host

NewHost creates a new pty.Host with a given dmsg.Client and whitelist. Outbound proxy dials go over dmsg via the default adapter (pre-refactor behavior). Callers that want the outbound side to use a different transport — typically the visor with its transport.Manager — should use NewHostWithDialer instead.

func NewHostWithDialer

func NewHostWithDialer(dmsgC *dmsg.Client, wl Whitelist, dialer StreamDialer) *Host

NewHostWithDialer constructs a Host with an explicit outbound StreamDialer. dmsgC is still required for the listening side (ListenAndServe binds the dmsg port for incoming proxy requests) and for logger plumbing — only the OUTBOUND proxy dial flows through the supplied dialer.

Pass dmsgDialer{c: dmsgC} to keep dmsg-only outbound (equivalent to NewHost). Pass a transport-aware adapter (Phase 2) to let `cli dmsg pty start` ride the visor's existing transports.

func (*Host) ExecRemote

func (h *Host) ExecRemote(ctx context.Context, rPK cipher.PubKey, rPort uint16, req *CommandExecReq) (*CommandExecResult, error)

ExecRemote runs a one-shot command on the remote dmsgpty host at (rPK, rPort) using this host's dmsg client. Equivalent to what a CLI client would obtain via the proxy path, but skips the local CLI socket entirely — callers that already hold a Host (notably the visor's own RPC layer) can drive Exec without the intermediate unix-socket-or-tcp control listener. Same trust model: the remote's whitelist gates the connection on its own PK admission.

func (*Host) ExecRemoteVia

func (h *Host) ExecRemoteVia(ctx context.Context, dialer StreamDialer, rPK cipher.PubKey, rPort uint16, req *CommandExecReq) (*CommandExecResult, error)

ExecRemoteVia runs ExecRemote against the supplied dialer instead of the Host's configured one. Used when the caller wants to pin the transport (e.g. dmsg-only / skynet-only) rather than ride the Host's MultiDialer chain. Pass pty.NewDmsgDialer(c) to force dmsg, or the visor's skywireDialer to force skynet.

Same protocol, same wire shape — the dialer only changes the underlying transport that carries the dmsgpty stream.

func (*Host) ListenAndServe

func (h *Host) ListenAndServe(ctx context.Context, port uint16) error

ListenAndServe serves the host over the dmsg network via the given dmsg port.

func (*Host) ListenAndServeNet

func (h *Host) ListenAndServeNet(ctx context.Context, lis net.Listener, extractPK PKExtractor) error

ListenAndServeNet serves dmsgpty over an arbitrary net.Listener. Generic counterpart of ListenAndServe (which is hard-bound to dmsg.Listener for AcceptStream + dmsg-specific error sentinels); this method handles only the generic surface: accept → extract PK → authorize → fan out to serveConn.

Use case: visor wires a parallel listener on appnet.SkywireNetworker so dmsgpty traffic can ride skynet routes in addition to dmsg. Each transport gets its own listener call and they run in parallel — accepted conns are served via the same mux as the dmsg path, so the wire protocol is identical regardless of transport.

extractPK must be non-nil; nil short-circuits to a rejected conn because skipping whitelist enforcement would silently weaken the dmsgpty trust model.

func (*Host) ListenAndServeTCP

func (h *Host) ListenAndServeTCP(ctx context.Context, addr string, localPK cipher.PubKey, localSK cipher.SecKey) error

ListenAndServeTCP brings up a raw-TCP entry point for the dmsgpty protocol on addr (net.Listen format — `:2022`, `0.0.0.0:2022`, etc.). Each accepted connection runs an XK noise handshake with the visor's identity keypair before being authorized against the dmsgpty whitelist and handed to the standard dmsgpty mux.

Blocks until ctx is canceled or the listener fails permanently. Returns the underlying listen error in the latter case; ctx-driven shutdown returns nil.

Whitelist sharing: the wl injected at NewHost time is the same wl the dmsg-overlay path uses. There is no separate TCP ACL by design — trusting a peer for dmsg-pty implicitly trusts them over the TCP entry point, since the protocol semantics are identical.

func (*Host) RunSessionGC

func (h *Host) RunSessionGC(ctx context.Context)

RunSessionGC periodically reaps detached/exited persistent sessions until ctx is done. No-op (returns immediately) when persistence isn't enabled. The visor runs this in a goroutine for the lifetime of the pty listeners.

func (*Host) ServeAcceptedTCP

func (h *Host) ServeAcceptedTCP(
	ctx context.Context,
	conn net.Conn,
	localPK cipher.PubKey,
	localSK cipher.SecKey,
)

ServeAcceptedTCP runs the per-connection noise + authorize + serve pipeline on a TCP conn that the caller has already accepted (e.g. from a systemd-socket-activation handoff: net.FileConn on FD 0).

Synchronous and one-shot: returns when the session ends. Designed for the "one dmsgpty-host process per active session" model that matches sshd's fork-per-session behavior — the parent socket- activated unit can be restarted without disturbing this process's running session.

localPK / localSK identify the server side of the noise handshake. In --sk-from-visor mode these come from skywire-config.json's top-level sk/pk fields so the host shares the visor's identity without contending for a dmsg-discovery entry.

Logging: uses a fresh package logger if h has no dmsg.Client master logger (the standalone --no-dmsg path); otherwise reuses the master logger for parity with ListenAndServeTCP.

func (*Host) ServeCLI

func (h *Host) ServeCLI(ctx context.Context, lis net.Listener) error

ServeCLI listens for CLI connections via the provided listener.

func (*Host) SetPersistentSessions

func (h *Host) SetPersistentSessions(enable bool, ttl time.Duration)

SetPersistentSessions enables (or disables) host-side pty session persistence: when on, an interactive pty's shell survives a dropped stream (the host detaches the follower instead of killing the shell) and lingers in the registry until reattached or GC'd. ttl bounds how long a detached, follower-less session lives before the GC reaps it; ttl <= 0 uses defaultSessionTTL. Off by default — the caller (the visor, from config) opts in, then runs RunSessionGC to start the sweeper.

type LocalPtyGateway

type LocalPtyGateway struct {
	// contains filtered or unexported fields
}

LocalPtyGateway is the gateway to a local pty.

func (*LocalPtyGateway) Exec

Exec runs Name + Arg on the host without a PTY. Implements the LocalPtyGateway side of the new RPC method. The session is one-shot — there's no separate Start / Stop / Read / Write protocol; the whole command lifetime is inside this one call.

Concurrency: callers can run multiple Exec RPCs in flight; each spawns its own os/exec.Cmd. The local LocalPtyGateway state is NOT touched by Exec, so a concurrent interactive Start session on the same gateway works unaffected.

func (*LocalPtyGateway) Ping

func (g *LocalPtyGateway) Ping(_, _ *struct{}) error

Ping is a no-op keepalive round-trip. The interactive web terminal calls it periodically so an idle pty stream keeps refreshing its StreamIdleTimeout read deadline and isn't torn down after 2 minutes of inactivity (the dmsg analog of SSH's ServerAliveInterval).

func (*LocalPtyGateway) Read

func (g *LocalPtyGateway) Read(reqN *int, respB *[]byte) error

Read reads from the local pty.

func (*LocalPtyGateway) SetPtySize

func (g *LocalPtyGateway) SetPtySize(size *WinSize, _ *struct{}) error

SetPtySize sets the local pty's window size.

func (*LocalPtyGateway) Start

func (g *LocalPtyGateway) Start(req *CommandReq, _ *struct{}) error

Start starts the local pty.

func (*LocalPtyGateway) Stop

func (g *LocalPtyGateway) Stop(_, _ *struct{}) error

Stop stops the local pty.

func (*LocalPtyGateway) Write

func (g *LocalPtyGateway) Write(wb *[]byte, n *int) error

Write writes to the local pty.

type MultiDialer

type MultiDialer []StreamDialer

MultiDialer tries each StreamDialer in order and returns the first success. Errors are accumulated; if every strategy fails, the joined error surfaces so operators can see which transports were attempted. Empty list errors immediately rather than panicking.

Strategy order is caller-controlled — typically transport-aware adapters first (skynet routes / stcpr) with a dmsg fallback last so working dmsg behavior survives a misconfigured upstream strategy. Each strategy gets the full ctx; a strategy that hangs will block fallback until ctx fires, so callers should bound ctx per overall attempt, not per strategy.

func (MultiDialer) DialStream

func (m MultiDialer) DialStream(ctx context.Context, pk cipher.PubKey, port uint16) (net.Conn, error)

DialStream walks the strategies in order. Returns the first conn to succeed; otherwise joins every strategy's error so the caller gets the full attempt list for diagnostics.

type PKExtractor

type PKExtractor func(conn net.Conn) (cipher.PubKey, bool)

PKExtractor pulls the remote public key from an accepted conn so ListenAndServeNet can drive whitelist authorization on transports that aren't dmsg. Returns (pk, true) on success; (zero, false) causes the conn to be rejected before any pty handshake happens — the same defense-in-depth that the dmsg path's whitelist check provides. Callers wrap their listener's conn type (typically appnet.SkywireConn) and read the embedded address.

type ProxiedPtyGateway

type ProxiedPtyGateway struct {
	// contains filtered or unexported fields
}

ProxiedPtyGateway is an RPC gateway for a remote pty.

func (*ProxiedPtyGateway) Exec

Exec on the proxied gateway forwards to the underlying PtyClient. Same one-shot semantics; the proxy adds nothing but the RPC hop.

func (*ProxiedPtyGateway) Ping

func (g *ProxiedPtyGateway) Ping(_, _ *struct{}) error

Ping forwards the keepalive round-trip to the remote pty.

func (*ProxiedPtyGateway) Read

func (g *ProxiedPtyGateway) Read(reqN *int, respB *[]byte) error

Read reads from the remote pty.

func (*ProxiedPtyGateway) SetPtySize

func (g *ProxiedPtyGateway) SetPtySize(size *WinSize, _ *struct{}) error

SetPtySize sets the remote pty's window size.

func (*ProxiedPtyGateway) Start

func (g *ProxiedPtyGateway) Start(req *CommandReq, _ *struct{}) error

Start starts the remote pty.

func (*ProxiedPtyGateway) Stop

func (g *ProxiedPtyGateway) Stop(_, _ *struct{}) error

Stop stops the remote pty.

func (*ProxiedPtyGateway) Write

func (g *ProxiedPtyGateway) Write(reqB *[]byte, respN *int) error

Write writes to the remote pty.

type Pty

type Pty struct {
	// contains filtered or unexported fields
}

Pty runs a local pty.

func NewPty

func NewPty() *Pty

NewPty creates a new Pty.

func (*Pty) Read

func (s *Pty) Read(b []byte) (int, error)

Read reads any stdout or stderr outputs from the pty.

The lock is held only to capture the *os.File, NOT across the blocking read: the read can block indefinitely (waiting for shell output), and holding the RLock that whole time would deadlock Stop, which needs the write lock to close the fd. Capturing then reading outside the lock lets Stop close the fd, which unblocks this read with an error. Safe against fd reuse because os.File.Read returns ErrClosed (without touching the raw fd) once the file is closed.

func (*Pty) SetPtySize

func (s *Pty) SetPtySize(size *WinSize) error

SetPtySize sets the pty size.

func (*Pty) Start

func (s *Pty) Start(name string, args []string, size *WinSize, env []string) error

Start runs a command with the given command name, args, optional window size, and optional environment variables. If env is provided, those variables will be merged with (and override) the host's environment.

func (*Pty) Stop

func (s *Pty) Stop() error

Stop stops the running command and closes the pty.

func (*Pty) Write

func (s *Pty) Write(b []byte) (int, error)

Write writes to the stdin of the pty. Like Read, it captures the *os.File under the lock and writes outside it — a write can block when the pty buffer is full, and holding the lock would deadlock Stop.

type PtyClient

type PtyClient struct {
	// contains filtered or unexported fields
}

PtyClient represents the client end of a dmsgpty session.

func NewProxyClient

func NewProxyClient(conn io.ReadWriteCloser, rPK cipher.PubKey, rPort uint16) (*PtyClient, error)

NewProxyClient creates a new pty client that interacts with a remote pty hosted on the given dmsg pk and port. Interactions are proxied via the local pty.Host

func NewPtyClient

func NewPtyClient(conn io.ReadWriteCloser) (*PtyClient, error)

NewPtyClient creates a new pty client that interacts with a local pty.

func (*PtyClient) Attach

func (sc *PtyClient) Attach(sid string) error

Attach rebinds to an existing persistent session by id (from a prior StartSession), replaying output produced while the client was disconnected. Returns an error if the session was reaped or is owned by another key, in which case the caller should start a fresh session.

func (*PtyClient) Close

func (sc *PtyClient) Close() error

Close closes the pty and closes the connection to the remote.

func (*PtyClient) Detach

func (sc *PtyClient) Detach() error

Detach closes the connection without stopping the pty. A persistent session keeps its shell for a later Attach, until the host reaps it; a classic one ends with its connection either way.

func (*PtyClient) Exec

func (sc *PtyClient) Exec(req *CommandExecReq) (*CommandExecResult, error)

Exec runs a one-shot command on the remote host without a PTY. Same trust model as Start; see exec_gateway.go for the cap + timeout semantics. Returns the captured stdout/stderr/exit-code in the response; a non-nil error here means the RPC itself failed, NOT that the remote command exited non-zero (check resp.ExitCode and resp.TimedOut for that).

func (*PtyClient) Ping

func (sc *PtyClient) Ping() error

Ping issues a no-op RPC round-trip. The interactive web terminal calls it periodically as a keepalive: the response read refreshes the dmsg stream's idle read deadline, so a terminal left idle isn't dropped after StreamIdleTimeout (2m). The dmsg analog of SSH ServerAliveInterval.

func (*PtyClient) Read

func (sc *PtyClient) Read(b []byte) (int, error)

Read reads from the pty.

func (*PtyClient) SetPtySize

func (sc *PtyClient) SetPtySize(size *WinSize) error

SetPtySize sets the pty size.

func (*PtyClient) Start

func (sc *PtyClient) Start(name string, env []string, arg ...string) error

Start starts the pty with optional environment variables.

func (*PtyClient) StartSession

func (sc *PtyClient) StartSession(name string, arg []string, size *WinSize, env []string) (string, error)

StartSession starts the pty like StartWithSize but returns a persistent-session id the caller can stash and later Attach to after a reconnect. Only persistence-aware hosts expose it; on an older host the RPC returns a "method not found" error (see isRPCMethodNotFound) and the caller should fall back to StartWithSize, giving up reconnect support against that host.

func (*PtyClient) StartWithSize

func (sc *PtyClient) StartWithSize(name string, arg []string, c *WinSize, env []string) error

StartWithSize starts the pty with a specified size and optional environment variables.

func (*PtyClient) Stop

func (sc *PtyClient) Stop() error

Stop stops the pty.

func (*PtyClient) Write

func (sc *PtyClient) Write(b []byte) (int, error)

Write writes to the pty.

type PtyGateway

type PtyGateway interface {
	Start(req *CommandReq, _ *struct{}) error
	Stop(_, _ *struct{}) error
	Read(reqN *int, respB *[]byte) error
	Write(reqB *[]byte, respN *int) error
	SetPtySize(size *WinSize, _ *struct{}) error
	Exec(req *CommandExecReq, resp *CommandExecResult) error
	// Ping is a no-op round-trip used as a dmsg-stream keepalive for the
	// interactive terminal — its response read refreshes the stream's
	// idle read deadline. See pkg/pty/ui.go's keepalive goroutine.
	Ping(_, _ *struct{}) error
}

PtyGateway represents a pty gateway, hosted by the pty.SessionServer.

Exec is the non-interactive variant of Start: one command, capture stdout/stderr/exit-code, return. Uses the same whitelist as Start — a host that trusts a PK to spawn a shell implicitly trusts it to run one command (strictly less powerful surface). See exec_gateway.go for the impl + caps.

func NewProxyGateway

func NewProxyGateway(ptyC *PtyClient) PtyGateway

NewProxyGateway creates a new pty-proxy gateway

func NewPtyGateway

func NewPtyGateway(ses *Pty) PtyGateway

NewPtyGateway creates a new gateway to a local pty.

type SchemeUIDialer

type SchemeUIDialer interface {
	UIDialer
	DialScheme(scheme string) (net.Conn, error)
}

SchemeUIDialer is an optional extension of UIDialer for dialers that can reach the peer over more than one transport scheme. When the pty handler receives ?scheme=dmsg or ?scheme=skynet it calls DialScheme, letting an operator force a transport — e.g. fall back to dmsg when the skynet route to a peer is wedged. Dialers that don't implement this just use Dial().

type StreamDialer

type StreamDialer interface {
	// DialStream opens an outbound stream to pk:port. The returned conn
	// is owned by the caller — close on cleanup. ctx bounds dial-side
	// work only; in-flight reads/writes on the returned conn are
	// independent of ctx cancellation.
	DialStream(ctx context.Context, pk cipher.PubKey, port uint16) (net.Conn, error)
}

StreamDialer is the outbound-dial side of pty.Host. Implementations open a bidirectional stream to (pk, port) on whatever transport they're backed by. The returned conn carries the dmsgpty wire protocol that NewPtyClient consumes, so any net.Conn-shaped transport (dmsg.Stream, skynet RouteGroup, raw TCP, …) is a valid backing.

func NewDmsgDialer

func NewDmsgDialer(c *dmsg.Client) StreamDialer

NewDmsgDialer constructs the dmsg-backed StreamDialer used by NewHost's default wiring and by callers that want to compose it into a MultiDialer alongside non-dmsg strategies. Exposed (vs keeping dmsgDialer private) so the visor's init_dmsg.go can build the same wrapping that NewHost uses internally — both call sites produce identical wire behavior.

func NewTCPDialer added in v1.3.101

func NewTCPDialer(addr string, localPK cipher.PubKey, localSK cipher.SecKey) StreamDialer

NewTCPDialer dials the direct-TCP pty listener at addr as localPK.

type TCPNoiseAddr

type TCPNoiseAddr struct {
	TCP net.Addr
	PK  cipher.PubKey
}

TCPNoiseAddr is the RemoteAddr type returned from a dmsgpty-tcp accepted connection. The underlying TCP address is preserved so log lines + audit trails look familiar; PK is the peer's noise-handshake-verified static key.

func (TCPNoiseAddr) Network

func (a TCPNoiseAddr) Network() string

Network returns the underlying TCP network ("tcp" / "tcp4" / "tcp6").

func (TCPNoiseAddr) String

func (a TCPNoiseAddr) String() string

String returns a `<pk>@<ip:port>` form so log lines uniquely identify the peer without the reader having to cross-reference the dmsg discovery.

type UI

type UI struct {
	// contains filtered or unexported fields
}

UI connects to a dmsgpty-host and exposes a pty via a web UI.

func NewUI

func NewUI(dialer UIDialer, conf UIConfig) *UI

NewUI creates a new dmsgpty-ui was a given dailer and config.

func (*UI) Handler

func (ui *UI) Handler(customCommands map[string][]string) http.HandlerFunc

Handler returns a http handler that serves the dmsgpty-ui.

func (*UI) Logger

func (ui *UI) Logger() logrus.FieldLogger

Logger returns the internal logger.

func (*UI) SetLogger

func (ui *UI) SetLogger(log logrus.FieldLogger)

SetLogger sets the internal logger. This should be called before serving .Handler()

type UIConfig

type UIConfig struct {
	CmdName string
	CmdArgs []string
}

UIConfig configures the dmsgpty-ui.

func DefaultUIConfig

func DefaultUIConfig() UIConfig

DefaultUIConfig returns the default UI config.

type UIDialer

type UIDialer interface {
	Dial() (net.Conn, error)
	AddrString() string
}

UIDialer represents a dialer for dmsgpty-ui.

func DmsgUIDialer

func DmsgUIDialer(dmsgC *dmsg.Client, rAddr dmsg.Addr) UIDialer

DmsgUIDialer returns a UIDialer that dials with dmsg.

func NetUIDialer

func NetUIDialer(network, address string) UIDialer

NetUIDialer returns a UIDialer that dials with stdlib net.

type Whitelist

type Whitelist interface {
	Get(pk cipher.PubKey) (bool, error)
	All() (map[cipher.PubKey]bool, error)
	Add(pks ...cipher.PubKey) error
	Remove(pks ...cipher.PubKey) error
}

Whitelist represents a whitelist of public keys.

func NewConfigWhitelist

func NewConfigWhitelist(confPath string) (Whitelist, error)

NewConfigWhitelist creates a config file implementation of a whitelist.

func NewMemoryWhitelist

func NewMemoryWhitelist() Whitelist

NewMemoryWhitelist creates a memory implementation of a whitelist.

type WhitelistClient

type WhitelistClient struct {
	// contains filtered or unexported fields
}

WhitelistClient interacts with a whitelist's API.

func NewWhitelistClient

func NewWhitelistClient(conn io.ReadWriteCloser) (*WhitelistClient, error)

NewWhitelistClient creates a new whitelist client.

func (WhitelistClient) ViewWhitelist

func (wc WhitelistClient) ViewWhitelist() ([]cipher.PubKey, error)

ViewWhitelist obtains the whitelist entries from host.

func (WhitelistClient) WhitelistAdd

func (wc WhitelistClient) WhitelistAdd(pks ...cipher.PubKey) error

WhitelistAdd adds a whitelist entry to host.

func (WhitelistClient) WhitelistRemove

func (wc WhitelistClient) WhitelistRemove(pks ...cipher.PubKey) error

WhitelistRemove removes a whitelist entry from host.

type WhitelistGateway

type WhitelistGateway struct {
	// contains filtered or unexported fields
}

WhitelistGateway is the configuration gateway.

func NewWhitelistGateway

func NewWhitelistGateway(auth Whitelist) *WhitelistGateway

NewWhitelistGateway creates a new configuration gateway.

func (*WhitelistGateway) Whitelist

func (g *WhitelistGateway) Whitelist(_ *struct{}, out *[]cipher.PubKey) error

Whitelist obtains the whitelist entries.

func (*WhitelistGateway) WhitelistAdd

func (g *WhitelistGateway) WhitelistAdd(in *[]cipher.PubKey, _ *struct{}) error

WhitelistAdd adds a whitelist entry.

func (*WhitelistGateway) WhitelistRemove

func (g *WhitelistGateway) WhitelistRemove(in *[]cipher.PubKey, _ *struct{}) error

WhitelistRemove removes a whitelist entry.

type WinSize

type WinSize struct {
	X    uint16
	Y    uint16
	Rows uint16
	Cols uint16
}

WinSize wraps around pty.Winsize and *windows.Coord

func NewWinSize

func NewWinSize(w *pty.Winsize) (*WinSize, error)

NewWinSize creates a new WinSize wrapper object

func (*WinSize) PtySize

func (w *WinSize) PtySize() *pty.Winsize

PtySize returns *pty.Winsize

Jump to

Keyboard shortcuts

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