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_html.go c3-vis-pty
Package pty pkg/pty/ui_socket.go c2-app-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
- Variables
- func DefaultCLIAddr() string
- func DialSftpDmsg(ctx context.Context, c *dmsg.Client, rPK cipher.PubKey, rPort uint16) (io.ReadWriteCloser, error)
- func DialSftpTCP(ctx context.Context, addr string, localPK cipher.PubKey, localSK cipher.SecKey, ...) (io.ReadWriteCloser, error)
- func OpenSftpConn(conn net.Conn) (io.ReadWriteCloser, error)
- func ParseWindowsEnv(cliAddr string) string
- func WriteConfig(conf Config, path string) error
- type CLI
- func (cli *CLI) ExecLocal(ctx context.Context, name string, args []string, env []string, stdin []byte, ...) (*CommandExecResult, error)
- func (cli *CLI) ExecRemote(ctx context.Context, rPK cipher.PubKey, rPort uint16, name string, ...) (*CommandExecResult, error)
- func (cli *CLI) ExecRemoteTCP(ctx context.Context, rPK cipher.PubKey, addr string, localPK cipher.PubKey, ...) (*CommandExecResult, error)
- func (cli *CLI) StartLocalPty(ctx context.Context, cmd string, args ...string) error
- func (cli *CLI) StartRemotePty(ctx context.Context, rPK cipher.PubKey, rPort uint16, cmd string, ...) error
- func (cli *CLI) StartRemotePtyTCP(ctx context.Context, rPK cipher.PubKey, addr string, localPK cipher.PubKey, ...) error
- func (cli *CLI) WhitelistClient() (*WhitelistClient, error)
- type CommandExecReq
- type CommandExecResult
- type CommandReq
- type Config
- type ErrorJSON
- type ExecStreamReq
- type ExecStreamResult
- type Host
- func (h *Host) ExecRemote(ctx context.Context, rPK cipher.PubKey, rPort uint16, req *CommandExecReq) (*CommandExecResult, error)
- func (h *Host) ExecRemoteVia(ctx context.Context, dialer StreamDialer, rPK cipher.PubKey, rPort uint16, ...) (*CommandExecResult, error)
- func (h *Host) ListenAndServe(ctx context.Context, port uint16) error
- func (h *Host) ListenAndServeNet(ctx context.Context, lis net.Listener, extractPK PKExtractor) error
- func (h *Host) ListenAndServeTCP(ctx context.Context, addr string, localPK cipher.PubKey, localSK cipher.SecKey) error
- func (h *Host) RunSessionGC(ctx context.Context)
- func (h *Host) ServeAcceptedTCP(ctx context.Context, conn net.Conn, localPK cipher.PubKey, ...)
- func (h *Host) ServeCLI(ctx context.Context, lis net.Listener) error
- func (h *Host) SetPersistentSessions(enable bool, ttl time.Duration)
- type LocalPtyGateway
- func (g *LocalPtyGateway) Exec(req *CommandExecReq, resp *CommandExecResult) error
- func (g *LocalPtyGateway) Ping(_, _ *struct{}) error
- func (g *LocalPtyGateway) Read(reqN *int, respB *[]byte) error
- func (g *LocalPtyGateway) SetPtySize(size *WinSize, _ *struct{}) error
- func (g *LocalPtyGateway) Start(req *CommandReq, _ *struct{}) error
- func (g *LocalPtyGateway) Stop(_, _ *struct{}) error
- func (g *LocalPtyGateway) Write(wb *[]byte, n *int) error
- type MultiDialer
- type PKExtractor
- type ProxiedPtyGateway
- func (g *ProxiedPtyGateway) Exec(req *CommandExecReq, resp *CommandExecResult) error
- func (g *ProxiedPtyGateway) Ping(_, _ *struct{}) error
- func (g *ProxiedPtyGateway) Read(reqN *int, respB *[]byte) error
- func (g *ProxiedPtyGateway) SetPtySize(size *WinSize, _ *struct{}) error
- func (g *ProxiedPtyGateway) Start(req *CommandReq, _ *struct{}) error
- func (g *ProxiedPtyGateway) Stop(_, _ *struct{}) error
- func (g *ProxiedPtyGateway) Write(reqB *[]byte, respN *int) error
- type Pty
- type PtyClient
- func (sc *PtyClient) Attach(sid string) error
- func (sc *PtyClient) Close() error
- func (sc *PtyClient) Exec(req *CommandExecReq) (*CommandExecResult, error)
- func (sc *PtyClient) Ping() error
- func (sc *PtyClient) Read(b []byte) (int, error)
- func (sc *PtyClient) SetPtySize(size *WinSize) error
- func (sc *PtyClient) Start(name string, env []string, arg ...string) error
- func (sc *PtyClient) StartSession(name string, arg []string, size *WinSize, env []string) (string, error)
- func (sc *PtyClient) StartWithSize(name string, arg []string, c *WinSize, env []string) error
- func (sc *PtyClient) Stop() error
- func (sc *PtyClient) Write(b []byte) (int, error)
- func (sc *PtyClient) WriteAsync(b []byte) error
- type PtyGateway
- type SchemeUIDialer
- type StreamDialer
- type TCPNoiseAddr
- type UI
- type UIConfig
- type UIDialer
- type Whitelist
- type WhitelistClient
- type WhitelistGateway
- type WinSize
Constants ¶
const ( PtyRPCName = "pty" PtyURI = "dmsgpty/pty" PtyProxyURI = "dmsgpty/proxy" )
Constants related to pty.
const ( DefaultPort = uint16(22) DefaultCmd = "/bin/bash" DefaultFlagExec = "-c" )
Constants related to dmsg.
const ( WhitelistRPCName = "whitelist" WhitelistURI = "dmsgpty/whitelist" )
Constants related to whitelist.
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.
const (
// DefaultCLINet for windows
DefaultCLINet = "unix"
)
Variables ¶
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.
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 ¶
ParseWindowsEnv finds '%'-enclosed windows env in json string
func WriteConfig ¶
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 (*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 ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
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) SetPersistentSessions ¶
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 ¶
func (g *LocalPtyGateway) Exec(req *CommandExecReq, resp *CommandExecResult) error
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.
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 ¶
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 ¶
func (g *ProxiedPtyGateway) Exec(req *CommandExecReq, resp *CommandExecResult) error
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.
type Pty ¶
type Pty struct {
// contains filtered or unexported fields
}
Pty runs a local pty.
func (*Pty) Read ¶
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 ¶
SetPtySize sets the pty size.
type PtyClient ¶
type PtyClient struct {
// contains filtered or unexported fields
}
PtyClient represents the client end of a dmsgpty session.
func NewProxyClient ¶
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 ¶
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) 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 ¶
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) SetPtySize ¶
SetPtySize sets the pty size.
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 ¶
StartWithSize starts the pty with a specified size and optional environment variables.
func (*PtyClient) WriteAsync ¶
WriteAsync writes to the pty WITHOUT waiting for the RPC reply, so an interactive caller (the web terminal's input loop) isn't blocked a full round-trip per keystroke. Ordering is preserved — net/rpc serializes request sends, and the send encodes b before returning, so the caller may reuse the buffer.
It returns an error if the client is closed OR if a PRIOR async write has since failed: completed Write calls land on sc.writeDone, and each call reaps them first, so a broken conn surfaces on the input path within a keystroke or two instead of only when the Read path notices it.
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 ¶
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
NewTCPDialer dials the direct-TCP pty listener at addr as localPK.
type TCPNoiseAddr ¶
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 (*UI) Handler ¶
func (ui *UI) Handler(customCommands map[string][]string) http.HandlerFunc
Handler returns a http handler that serves the dmsgpty-ui.
func (*UI) SetLogger ¶
func (ui *UI) SetLogger(log logrus.FieldLogger)
SetLogger sets the internal logger. This should be called before serving .Handler()
type UIConfig ¶
UIConfig configures the dmsgpty-ui.
func DefaultUIConfig ¶
func DefaultUIConfig() UIConfig
DefaultUIConfig returns the default UI config.
type UIDialer ¶
UIDialer represents a dialer for dmsgpty-ui.
func DmsgUIDialer ¶
DmsgUIDialer returns a UIDialer that dials with dmsg.
func NetUIDialer ¶
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 ¶
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 ¶
WinSize wraps around pty.Winsize and *windows.Coord
func NewWinSize ¶
NewWinSize creates a new WinSize wrapper object
Source Files
¶
- cli.go
- cli_tcp.go
- cli_unix.go
- conf.go
- const.go
- const_unix.go
- dialer.go
- exec_builtin_other.go
- exec_gateway.go
- exec_http.go
- exec_http_client.go
- exec_pool.go
- exec_unix.go
- host.go
- host_mux.go
- host_tcp.go
- pty_client.go
- pty_gateway.go
- pty_gateway_unix.go
- pty_session.go
- pty_session_gateway.go
- pty_session_registry.go
- pty_unix.go
- rpc_register_native.go
- rpc_util.go
- sftp_client.go
- sftp_subsystem.go
- ui.go
- ui_dialer.go
- ui_html.go
- ui_socket.go
- ui_unix.go
- whitelist.go
- whitelist_client.go
- whitelist_gateway.go