Documentation
¶
Index ¶
Constants ¶
const DefaultInitialClientTimeout = 10 * time.Second
DefaultInitialClientTimeout bounds how long a server told to await its initial client waits before giving up on the session.
const DefaultStopGrace = 5 * time.Second
DefaultStopGrace is how long each step of the teardown waits before the next, save the first: SIGHUP (bounded by hangupGrace instead), then close the pty master, then SIGTERM, then SIGKILL. Five seconds is long enough for a shell to hang up its jobs and a program to flush, and short enough that `session stop` against something that ignores everything is a worst case of about sixteen seconds -- hangupGrace plus three of these -- not a minute.
Variables ¶
var ErrJoinTimeout = errors.New("no guest joined within the join timeout")
ErrJoinTimeout marks the winning first-guest deadline. Host passes it as the server context cancellation cause so attached clients also exit zero.
var ErrNoInitialClient = errors.New("no client attached before the command could start")
ErrNoInitialClient is returned by ServeWithContext when AwaitInitialClient was set and nobody attached within InitialClientTimeout. The command never started; the caller records startup_abandoned.
Functions ¶
This section is empty.
Types ¶
type AdminServer ¶
type AdminServer struct {
Session *api.GetSessionResponse
ClientRepo *ClientRepo
// LaunchID is the launch this server speaks for, from the record the
// session claimed. StopSession refuses a request that names any other
// launch, so that a client whose session ended between reading the
// record and dialling the socket does not stop the session that took
// the name over. Empty means no name was claimed -- a caller that
// supplied its own admin socket -- and then no stop can be bound to a
// launch, so none is accepted.
LaunchID string
// OnListening, if set, is called once the socket is bound. It is the other
// half of readiness: a caller told "ready" must be able to connect, which
// registering the actor does not establish and binding does.
OnListening func()
// OnStop is called when a client asks the session to end. The daemon
// wires it to an explicit stop cause; the RPC returns at
// once and the teardown follows.
OnStop func()
// OnSetJoinTimeout sets the session's join timeout and reports what it
// found. Nil answers Unimplemented, as a daemon without the call would.
OnSetJoinTimeout func(time.Duration) *api.SetJoinTimeoutResponse
// JoinState reports the join timeout as the daemon holds it, for
// GetSession. Nil reports none.
JoinState func() *api.JoinState
sync.Mutex
// contains filtered or unexported fields
}
func (*AdminServer) Listen ¶ added in v0.30.0
func (s *AdminServer) Listen(sock string) error
Listen binds the admin socket. It is separate from Serve so the caller can bind synchronously, before any concurrent actor exists.
Binding inside the serving goroutine made a bind failure race the command's start: whichever lost the race decided how the run was classified, so the same broken socket path was reported as startup_failed on one run and signaled on the next. Bound up front, the failure is the caller's to return and there is nothing to race.
func (*AdminServer) Serve ¶
func (s *AdminServer) Serve(ctx context.Context) error
Serve serves the listener Listen bound. Listen must have succeeded first: this deliberately refuses to bind on the caller's behalf, because doing so is exactly the race Listen exists to remove.
func (*AdminServer) Shutdown ¶
func (s *AdminServer) Shutdown(ctx context.Context) error
Shutdown drains RPCs until ctx expires, then initiates forced transport closure. It cannot terminate a callback that ignores cancellation: gRPC Serve and its shutdown goroutines may still wait for that callback to return.
type ClientRepo ¶
type ClientRepo struct {
// contains filtered or unexported fields
}
func NewClientRepo ¶
func NewClientRepo() *ClientRepo
func (*ClientRepo) Clients ¶
func (c *ClientRepo) Clients() []*api.Client
func (*ClientRepo) Delete ¶
func (c *ClientRepo) Delete(clientId string)
type CommandResult ¶ added in v0.30.0
type CommandResult struct {
// Exited is true when the command terminated under its own control.
Exited bool
// Code is its status; meaningful only when Exited.
Code int
// Signal names the signal that killed it, when it was killed.
Signal string
// SignalNumber is the originating Unix terminating signal, when available.
SignalNumber *int
}
CommandResult is a command's own outcome, as distinct from the session's shutdown cause.
type ExitError ¶ added in v0.25.0
type ExitError struct {
// Code is the status GetExitCodeProcess reported, kept unsigned because
// that is what Windows produces and what SSH puts on the wire. On a
// 32-bit build, narrowing it to int would make every status with the high
// bit set negative -- an unhandled exception such as 0xC0000005, or a
// deliberate "exit -1" -- and exitCode would then read it as a process
// that never exited under its own control.
Code uint32
}
ExitError reports that a PTY's process exited with a non-zero status.
Unix gets this for free from exec.Cmd.Wait, which returns *exec.ExitError. Windows waits on a process handle rather than an exec.Cmd, so it has no such error to return and needs this to carry the code instead of burying it in a message string.
type PTY ¶ added in v0.18.0
type PTY interface {
io.ReadWriteCloser
// Setsize changes the terminal dimensions.
// On Unix, this sends a SIGWINCH to the slave process.
// On Windows, this resizes the ConPTY buffer.
Setsize(h, w int) error
// Redraw asks the process on the pty to repaint without changing its
// geometry: a replay repaints what the ring holds, and a full-screen
// program redraws the rest on SIGWINCH. On Windows there is no SIGWINCH,
// so the nudge is a resize, and unavailable under a pinned size.
Redraw() error
// Wait waits for the process associated with this PTY to exit.
// On Unix, this delegates to exec.Cmd.Wait().
// On Windows, this waits on the process handle.
Wait() error
// Kill terminates the process associated with this PTY.
// On Unix, this delegates to exec.Cmd.Process.Kill().
// On Windows, this calls TerminateProcess on the handle.
Kill() error
// Signal sends sig to the command's process group, so that a shell's
// hangup reaches its jobs. Unsupported on Windows, which returns
// errors.ErrUnsupported and lets the caller go straight to Kill.
Signal(sig syscall.Signal) error
}
PTY represents a pseudo-terminal abstraction that works across platforms. On Unix, it wraps a traditional PTY created via creack/pty. On Windows, it wraps a ConPTY (Console Pseudo Terminal).
The interface provides a common abstraction for:
- Reading/writing terminal I/O (via io.ReadWriteCloser)
- Resizing the terminal window
- Managing process lifecycle (Wait/Kill)
Platform-specific implementations:
- Unix: see pty_unix.go
- Windows: see pty_windows.go
type PermissionDeniedError ¶
type PermissionDeniedError struct {
// contains filtered or unexported fields
}
PermissionDeniedError is a relay that would not authenticate this host.
It says which of the two ways that happened, because they are different problems: a host that offered nothing has no identity for the relay to have rejected, while a host whose keys were all refused has keys this relay does not accept. x/crypto's own error stays reachable through Unwrap for anything that wants the handshake's words instead.
func (*PermissionDeniedError) Error ¶
func (e *PermissionDeniedError) Error() string
func (*PermissionDeniedError) Unwrap ¶
func (e *PermissionDeniedError) Unwrap() error
type RelayAuthority ¶ added in v0.33.0
type RelayAuthority struct {
// contains filtered or unexported fields
}
RelayAuthority is the relay key a host verified, and the only key whose signature makes a guest certificate's AuthRequest believable.
The relay mints every guest's credential, naming in it the key the guest authenticated with, and the host's sshd authorizes that name. For that name to mean anything the host has to know which key may sign it. That key is not a new thing to configure: it is the relay host key this host already decided to trust, in its own host key callback. Wrap records it there.
That bounds the damage to the relay the host chose. A host that skipped verification (--skip-host-key-check on a first connection) can still be lied to by whoever answered, which is the separate problem of publishing and pinning the public relay's key.
The zero value trusts nothing, which is what a host whose handshake has not happened yet should do.
func (*RelayAuthority) IsUserAuthority ¶ added in v0.33.0
func (r *RelayAuthority) IsUserAuthority(key ssh.PublicKey) bool
IsUserAuthority reports whether key is the relay key this host verified. It is the predicate server.UserCertChecker asks, and it is false until a handshake has recorded one.
The comparison is exact, on the marshalled key, and deliberately not utils.KeysEqual: that unwraps a certificate to the key it certifies, so a certificate that merely certified the relay's key would be accepted as the authority itself. An authority is one specific key. Production only ever asks about a cert.SignatureKey, which is always a plain key, so the strictness costs nothing.
Locked because Wrap runs on the goroutine dialing the relay while this runs on the embedded sshd's handler goroutines. They are ordered today -- the server is built after the tunnel is established -- but a redial would not be.
func (*RelayAuthority) Wrap ¶ added in v0.33.0
func (r *RelayAuthority) Wrap(cb ssh.HostKeyCallback) ssh.HostKeyCallback
Wrap returns cb with the accepted key recorded. Only a key cb accepted is recorded: the verdict belongs to the host's trust policy, and this only remembers what it decided.
A relay presents a host certificate when the client offers certificate algorithms, which ReverseTunnel does, so the key recorded is the certificate's SignatureKey -- the same key the relay signs guest certificates with, and the same key the prompt and known_hosts already reduce a host certificate to.
type ReverseTunnel ¶
type ReverseTunnel struct {
*ssh.Client
Host *url.URL
Signers []ssh.Signer
// HostKey is the key the embedded sshd presents. Its public half is what
// the relay is told to expect on every guest's upstream hop; Signers
// authenticate this tunnel and are used for nothing else.
HostKey ssh.Signer
AuthorizedKeys []ssh.PublicKey
KeepAliveDuration time.Duration
// ProxyURL, when non-nil, is the HTTP proxy to connect to Host through.
ProxyURL *url.URL
HostKeyCallback ssh.HostKeyCallback
Logger *slog.Logger
// contains filtered or unexported fields
}
func (*ReverseTunnel) Close ¶
func (c *ReverseTunnel) Close()
Close releases whatever Establish took, and works on a tunnel that never established anything. Establish gives up at three different points — the dial, the session request, the listen — and each leaves a different subset of these set; a Close that assumed the successful one would turn a failed dial into a nil dereference in the caller's teardown.
It is bounded: closing ln goes through the wrapper, which gives the relay listenerCloseGrace and then closes the client itself.
func (*ReverseTunnel) Establish ¶
func (c *ReverseTunnel) Establish(ctx context.Context) (*server.CreateSessionResponse, error)
func (*ReverseTunnel) Listener ¶
func (c *ReverseTunnel) Listener() net.Listener
Listener returns the forwarded listener the guest server serves on, wrapped so that whoever closes it — Shutdown, from an interrupt — cannot be left waiting on a relay that has stopped replying.
type SFTPSession ¶ added in v0.22.0
type SFTPSession struct {
// contains filtered or unexported fields
}
SFTPSession tracks permission state for a single SFTP session
type Server ¶
type Server struct {
Command []string
CommandEnv []string
ForceCommand []string
// HostKey is the key both doors present. A session's key, not the
// operator's identity: a key exchange signs with whatever is here, on
// every join, attach and rekey, and an identity held by a confirming
// agent would be asked each time.
HostKey ssh.Signer
AuthorizedKeys []ssh.PublicKey
// GuestCertAuthority reports whether a key may vouch for a guest
// certificate's AuthRequest -- in production, the relay key the host key
// callback accepted, via RelayAuthority. Nil admits no guest: the only
// legitimate guest credential is one the relay minted, and without knowing
// which relay, no claim in one can be believed.
GuestCertAuthority func(ssh.PublicKey) bool
EventEmitter *emitter.Emitter
KeepAliveDuration time.Duration
Logger *slog.Logger
ReadOnly bool
AllowLocalTCPForwarding bool
PtySize termsize.Size
PinPtySize bool
Term string
// AwaitInitialClient defers starting the command until the first host
// client's output subscription is installed, and defers serving guests
// until the command has started. Foreground use sets it: a command that
// exits at once — `upterm host -- false` — would otherwise race the local
// terminal's attach, and lose. Headless use leaves it off.
AwaitInitialClient bool
// InitialClientTimeout bounds that wait; zero means
// DefaultInitialClientTimeout.
InitialClientTimeout time.Duration
// StopGrace bounds each step of the command's teardown; zero means
// DefaultStopGrace.
StopGrace time.Duration
// OnCommandStarted, if set, is called once the hosted command is running.
// Readiness is a claim about facts, and this is one of the two facts it
// rests on: until this fires, "ready" would mean a command that may still
// fail to start.
OnCommandStarted func()
// OnGuestServerStopped is called when the guest listener stops serving,
// which in practice means the reverse tunnel is gone. The session does not
// end: the command keeps running and keeps its pty. Reporting it is the
// caller's job, because internal must not know about on-disk state.
OnGuestServerStopped func(error)
// SFTP configuration
SFTPDisabled bool // Disable SFTP subsystem entirely
SFTPPermissionChecker sftp.PermissionChecker // Optional: prompts user for SFTP permissions (nil = auto-allow)
// contains filtered or unexported fields
}
func (*Server) CommandResult ¶ added in v0.30.0
func (s *Server) CommandResult() CommandResult
CommandResult returns the hosted command's outcome. Valid after ServeWithContext returns.