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

host

package
v0.31.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 43 Imported by: 1

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ClaimTimeout = 10 * time.Second

ClaimTimeout bounds how long Run waits for the session registry when it takes a name.

Claim waits on two lock files, either of which can be held by a process that is stopped rather than slow — a wait no amount of patience resolves. The caller's context is not a bound in practice: the CLI runs Run on context.Background(), so without this a stuck registry hangs `upterm host` at startup forever while every read path already gives up after sessionQueryTimeout. Ten seconds matches those read paths, since they wait on the same locks.

A var, not a const, so an embedder on a slower filesystem and a test that wants to observe the timeout can both move it.

View Source
var ErrNoInitialClient = internal.ErrNoInitialClient

ErrNoInitialClient is what a host told to await its initial client returns when nobody attached in time. Nothing ran, so it is recorded as startup_abandoned rather than a failure.

View Source
var ErrSessionAbandoned = errors.New("session abandoned before the command started")

ErrSessionAbandoned marks a session given up before its command started, as opposed to one that failed to start. A SessionCreatedCallback error that wraps it is recorded as startup_abandoned rather than startup_failed.

Declining the interactive confirmation is the case it exists for: the operator was shown the session and said no, nothing went wrong, and a record saying otherwise sends whoever reads it looking for a fault that never happened.

Functions

func AdminClient

func AdminClient(socket string) (api.AdminServiceClient, error)

func DisplayVersionWarning added in v0.30.0

func DisplayVersionWarning(out io.Writer, logger *slog.Logger, result *version.CompatibilityResult)

DisplayVersionWarning prints a formatted version mismatch warning to the given writer. Run no longer prints it — a daemon has no terminal — so this is for a caller that does, from VersionWarningCallback.

func InstallSignalPolicy added in v0.30.0

func InstallSignalPolicy()

InstallSignalPolicy sets the process-wide signal dispositions a host needs: SIGPIPE converted to EPIPE, and SIGTTIN and SIGTTOU ignored. Starting a host installs it before it touches its terminal; an embedder that writes to the same stdout or reads the same stdin before then may call it earlier, and calling it twice is free.

SIGPIPE is fatal for writes to fd 1 and 2 and only for those; Go turns it into EPIPE everywhere else. So `upterm host … | head` would kill the whole session the instant head exits, taking the hosted command with it — the one failure mode headless operation must not have. Calling Notify converts it to EPIPE, and the error then travels the ordinary path: the stdout sink fails and is dropped.

SIGTTIN and SIGTTOU get SIG_IGN, not a handler. A backgrounded process reading its controlling terminal is sent SIGTTIN, and the default disposition stops it — so `upterm host … &` would suspend. With a handler installed the read returns EINTR and Go retries, which spins. Ignored, it returns EIO, which the host's stdin actor survives. SIGTTOU is the same story for writes, and it is why the ignores are installed here rather than beside the signal actor, where they used to be: Run assembles that only after the reverse tunnel is up, the version warning is printed and SessionCreatedCallback has written the banner, and from a terminal with `stty tostop` set the first of those writes stopped the host before the ignore existed. The prompting host-key callback's read of stdin was sent SIGTTIN the same way.

Ignoring SIGTTOU also means a background tcsetattr would succeed, which is why command.Run checks foreground ownership before touching terminal modes at all.

It belongs to this package rather than to the CLI, which is where the SIGPIPE conversion used to live and still is: an application that embeds Host.Run with Stdout set to its own os.Stdout never runs upterm's main, so it inherited the fatal disposition and died on its reader's close — before the sink could report the broken pipe and before the final session record was published.

Process-wide and permanent is the point. Scoping any of it to a session would leave the startup banner unprotected at one end and the shutdown drain unprotected at the other, and nothing here may ever call signal.Stop or signal.Reset: that would strip the policy from every other session in the same process. An embedder that calls Host.Run in-process inherits all three for the rest of the process's life, and so does everything else in that process — a library of its own that reads a background terminal would find the read returning EIO instead of the caller being stopped.

func NewAutoAcceptingHostKeyCallback added in v0.19.0

func NewAutoAcceptingHostKeyCallback(stdout io.Writer, knownHostsFilename string) (ssh.HostKeyCallback, error)

NewAutoAcceptingHostKeyCallback creates a host key callback that automatically accepts unknown host keys and adds them to the known_hosts file without prompting. This is similar to SSH's StrictHostKeyChecking=accept-new behavior: - Unknown host keys are automatically accepted and added to known_hosts - Known host keys are still validated (preventing MITM attacks on subsequent connections)

It takes no proxied flag: its message names the hostname and the key type, never the peer address.

func NewHostKey added in v0.30.0

func NewHostKey() (ssh.Signer, error)

NewHostKey generates the key the embedded sshd presents on both its doors and the key the relay is told to expect from this host. It is a session's key, not the operator's: fresh per run, never read from disk, never offered to the relay as an identity.

func NewPromptingHostKeyCallback

func NewPromptingHostKeyCallback(stdin io.Reader, stdout io.Writer, knownHostsFilename string, proxied bool) (ssh.HostKeyCallback, error)

NewPromptingHostKeyCallback creates a host key callback that asks before accepting an unknown host key.

Set proxied when the transport passes through an HTTP proxy. x/crypto hands the callback the socket's peer, which is then the proxy rather than the server, and presenting it as the server's address is misleading — OpenSSH prints a placeholder in the same situation. It is display-only: knownhosts matches on the hostname argument and appendHostLine stores only that, so verification is unaffected either way.

func ProviderList added in v0.27.0

func ProviderList() string

ProviderList names every supported provider and marks the ones that need an explicit host. It is the single source for both parse errors and the --authorized-user flag's help text, so the two cannot drift apart.

func Signers

func Signers(privateKeys []string, identitiesOnly bool) ([]ssh.Signer, func(), error)

Signers returns the identities upterm host offers to the server, and a cleanup that releases the agent connection they may sign through.

With identitiesOnly, privateKeys is the whole set and must be non-empty: each entry must resolve to a signer, a public key file selects the agent key it names, and the agent is otherwise used only to sign a key it already holds that the file itself cannot supply — encrypted, or otherwise unparseable with a `.pub` sibling. Without it, the agent's keys are preferred when it has any, then the files that load, then a generated key.

Passphrases are prompted for on this process's terminal; a process that has none calls SignersWith instead.

func SignersFromFiles

func SignersFromFiles(privateKeys []string) ([]ssh.Signer, error)

func SignersFromFilesWith added in v0.30.0

func SignersFromFilesWith(privateKeys []string, passphrase func(string) ([]byte, error), onSkip func(string, error)) ([]ssh.Signer, error)

SignersFromFilesWith reads every key it can and skips the rest. A key that cannot be read never fails the set: the default key list is every file in ~/.ssh that exists, and one unreadable file must not stop a session.

func SignersWith added in v0.30.0

func SignersWith(opts SignerOptions) ([]ssh.Signer, func(), error)

SignersWith is Signers with the passphrase prompt and the skip report injected. The prompt reaches both paths — identitySigners takes one too — so a daemon, which has no terminal to prompt on, resolves an encrypted key the same way whether or not the identity list is the whole set.

Types

type AuthorizedKey added in v0.13.0

type AuthorizedKey struct {
	PublicKeys []ssh.PublicKey
	Comment    string
}

func AuthorizedKeysFromFile added in v0.13.0

func AuthorizedKeysFromFile(file string) (*AuthorizedKey, error)

func AuthorizedKeysFromUserRefs added in v0.27.0

func AuthorizedKeysFromUserRefs(ctx context.Context, refs []UserRef, proxyURL *url.URL, logger *slog.Logger) ([]*AuthorizedKey, error)

AuthorizedKeysFromUserRefs resolves refs using a default Fetcher. A non-nil proxyURL sends every fetch through that HTTP proxy.

type CredentialMode added in v0.27.0

type CredentialMode int

CredentialMode describes how a reference selects an authentication token.

const (
	// CredentialNone never sends a credential. Every provider but GitHub.
	CredentialNone CredentialMode = iota
	// CredentialDefault defers to go-gh's own host and token resolution,
	// preserving GH_HOST and the enterprise environment variables. Used when a
	// GitHub reference names no explicit host.
	CredentialDefault
	// CredentialHostScoped uses only credentials stored for the named host.
	// GH_ENTERPRISE_TOKEN and GITHUB_ENTERPRISE_TOKEN are not host-scoped, so
	// honoring them here would send one instance's token to another.
	CredentialHostScoped
)

type Fetcher added in v0.27.0

type Fetcher struct {
	Logger *slog.Logger
	// Transport, when set, is the base round tripper for every request. Tests
	// use it to trust httptest certificates; production leaves it nil.
	Transport http.RoundTripper
}

Fetcher resolves user references to authorized keys.

func (*Fetcher) AuthorizedKeys added in v0.27.0

func (f *Fetcher) AuthorizedKeys(ctx context.Context, refs []UserRef) ([]*AuthorizedKey, error)

AuthorizedKeys fetches every reference, reporting all failures together. Any failure fails the whole call: continuing with a partial set can, in the limit, degrade "only alice may join" into "anyone may join".

type Host

type Host struct {
	Host              string
	KeepAliveDuration time.Duration
	Command           []string
	ForceCommand      []string
	Signers           []ssh.Signer
	// HostKey is the key the embedded sshd presents on both its doors and
	// the key the relay is told to expect from this host. A session's key,
	// not the operator's: Signers authenticate the tunnel and are used for
	// nothing else. Left nil, Run generates one for that run and does not
	// keep it, so a Host run twice presents two keys; a caller that needs
	// the public half before Run — the CLI, to pin its own attach — sets it
	// and it is used as given.
	HostKey                 ssh.Signer
	HostKeyCallback         ssh.HostKeyCallback
	AuthorizedKeys          []*AuthorizedKey
	AdminSocketFile         string
	SessionCreatedCallback  func(context.Context, *api.GetSessionResponse) error
	ClientJoinedCallback    func(*api.Client)
	ClientLeftCallback      func(*api.Client)
	Logger                  *slog.Logger
	ReadOnly                bool
	AllowLocalTCPForwarding bool
	// ProxyURL, when non-nil, routes the connection to the upterm server
	// through an HTTP proxy.
	ProxyURL   *url.URL
	PtySize    termsize.Size
	PinPtySize bool
	Term       string

	// AttachSocketFile is where the local attach door is bound. Like
	// AdminSocketFile, supplying it means the caller manages the path and
	// no name is claimed; left empty with AdminSocketFile set, no attach
	// door is served, which is how an embedder that will never attach a
	// terminal says so. Claimed with the name otherwise.
	AttachSocketFile string
	// AttachListeningCallback is called once the attach socket is bound,
	// before the command starts, with the path a client should dial. With
	// AwaitInitialClient set, this is the moment to attach.
	//
	// Called on Run's own goroutine, before the run.Group exists, so it must
	// not block: everything after it — the command, the guest door, the
	// readiness record — waits behind it.
	AttachListeningCallback func(attachSocket string)
	// CommandStartedCallback is called once the hosted command is running,
	// which with AwaitInitialClient is also the news that a client got
	// through the door. A caller whose own attach failed asks this whether
	// there is a session to leave alone.
	//
	// Called on the server's own goroutine, in front of everything the
	// command's start releases, so it must not block.
	CommandStartedCallback func()
	// SessionReadyCallback is called once the session's record has been
	// published: the admin socket is bound, the command is running, and the
	// record a reader would consult already carries the session ID and the
	// status handed to this callback. A caller that tells anyone else the
	// session is up says it here, not from CommandStartedCallback — the
	// command starting is one of the two facts readiness is made of, and the
	// record is written after both. A script that runs `upterm session info`
	// the instant it is told "ready" would otherwise be told "starting" by
	// the record.
	//
	// The status is what the record ended on, which is not always ready:
	// advanceStatus refuses to move a status backwards, so a tunnel lost
	// between the command starting and this write leaves "disconnected"
	// standing and the ready write is a no-op. The caller is told what the
	// record says rather than what this actor asked for, because the two
	// agreeing is the whole point of the callback.
	//
	// Not called when the record could not be published: the run fails
	// instead, so that nobody is told about a record that was never written.
	// With no session directory — an embedder that supplied its own admin
	// socket — there is no record, the two facts alone are it, and the
	// status is ready.
	//
	// Called once, on the readiness actor's own goroutine, after the write;
	// it must not block.
	SessionReadyCallback func(status string)
	// SessionClaimedCallback is called once the session's name is claimed,
	// with the directory that holds it, before the tunnel is dialled: the
	// first thing a caller can know about a session is its name and its
	// paths, and a caller that relays prompts wants them before any prompt.
	// Not called when AdminSocketFile was supplied, since nothing is claimed.
	//
	// Called on Run's own goroutine; it must not block.
	SessionClaimedCallback func(*sessiondir.Dir)
	// VersionWarningCallback is called when the server's version is
	// incompatible with this host's. Nil logs the mismatch and nothing more:
	// the daemon has no terminal to print to.
	VersionWarningCallback func(*version.CompatibilityResult)

	// 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
	// internal.DefaultInitialClientTimeout.
	InitialClientTimeout time.Duration

	// StopGrace bounds each step of the command's teardown; zero means
	// internal.DefaultStopGrace.
	StopGrace time.Duration

	// JoinTimeout bounds the wait for the first guest, starting at readiness.
	// Zero waits forever. The session enforces it independently of its launcher;
	// a guest joining disarms it permanently, even after that guest leaves.
	JoinTimeout time.Duration

	// SFTP configuration
	SFTPDisabled          bool                   // Disable SFTP subsystem entirely (--no-sftp)
	SFTPPermissionChecker sftp.PermissionChecker // Optional: prompts user for SFTP permissions (nil = auto-allow)

	// Name is the session's local name. It determines the socket paths, so
	// they are known before the server is ever contacted. It is unrelated to
	// the server-assigned session ID in the connect string.
	//
	// Ignored when AdminSocketFile is set: supplying a socket is how a caller
	// says it is managing the paths itself, so Run claims no name and there is
	// nothing for this to name. SessionDir stays nil in that case, and no
	// record is published.
	Name string

	// SessionDir is claimed by Run and readable for as long as Run is
	// running. Run clears it on the way out: the directory is released by
	// then, a released Dir may not be touched again, and leaving it here
	// would be leaving a handle to a name that now belongs to whoever claimed
	// it next. Nil when AdminSocketFile was supplied, which is how tests
	// drive Host without taking a name.
	SessionDir *sessiondir.Dir
	// contains filtered or unexported fields
}

func (*Host) Run

func (c *Host) Run(ctx context.Context) (runErr error)

Run hosts one session and returns when it ends.

It may be called again afterwards: everything Run claims, it gives back before returning, including the two fields it fills in on the Host itself. A caller that supplied AdminSocketFile keeps it across runs, since managing the path is what supplying it means.

type SignerOptions added in v0.30.0

type SignerOptions struct {
	PrivateKeys []string
	// IdentitiesOnly makes PrivateKeys the whole identity set, as OpenSSH's
	// IdentitiesOnly does: every entry must resolve to a signer, and no
	// other key the agent holds is offered.
	IdentitiesOnly bool
	// Passphrase is asked for an encrypted key's passphrase. Nil prompts on
	// this process's terminal, which a daemon does not have.
	Passphrase func(file string) ([]byte, error)
	// OnSkip is told about a key file that was skipped and why. Nil is
	// silent, as SignersFromFiles always was.
	OnSkip func(file string, err error)
}

SignerOptions configures SignersWith.

type UserRef added in v0.27.0

type UserRef struct {
	Provider string // "" for the raw-URL form
	User     string
	Host     string // host[:port] as written; "" means "provider default"
	URL      string // raw-URL form only: the complete .keys URL
	Raw      string // the original string, for error messages
	Mode     CredentialMode
}

UserRef is one parsed --authorized-user value.

func ParseUserRef added in v0.27.0

func ParseUserRef(s string) (UserRef, error)

ParseUserRef parses a reference of the form provider:user, provider:user@host, or an https:// URL. It performs no I/O.

func (UserRef) Display added in v0.27.0

func (r UserRef) Display() string

Display is the resolved identity used as AuthorizedKey.Comment. It names the host actually contacted, which the verbatim input does not when GH_HOST is in play.

func (UserRef) KeysURL added in v0.27.0

func (r UserRef) KeysURL() string

KeysURL returns the endpoint serving this user's authorized_keys body.

func (UserRef) ResolveHost added in v0.27.0

func (r UserRef) ResolveHost() string

ResolveHost returns the host[:port] this reference targets.

Directories

Path Synopsis
Package sessiondir owns the on-disk identity of a session: which name it holds, where its sockets live, whether the process that claimed it is still alive, and how it ended.
Package sessiondir owns the on-disk identity of a session: which name it holds, where its sockets live, whether the process that claimed it is still alive, and how it ended.

Jump to

Keyboard shortcuts

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