exposure

package
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package exposure decides which address `cloop ui` listens on, and refuses the one combination that turns a hub into an open door: no sign-in, and a listener the network can reach (Task 20393).

Why the bind address is a security decision here

A hub with no browser credential — no OIDC single sign-on and no static token — answers every request from anyone who can reach it: it lists the projects, queues tasks and starts agent runs on its host. Whether that is acceptable depends entirely on who can reach it, and the hub decides that with one value: the address it binds. Until this package, `cloop ui` bound every interface whatever its authentication, so an open hub on a machine with a public address was an open hub on the Internet.

The rule

  • A hub with a browser credential binds every interface by default, as it always has: the credential is what protects it.
  • A hub without one binds 127.0.0.1 by default, so only this machine reaches it.
  • An address the operator names (--listen, ui.listen) is honoured as given — except that an open hub refuses to start on one beyond loopback unless ui.allow_unauthenticated_network acknowledges it, and then warns at every start. The acknowledgement never widens the default: it lets an address an operator named take effect, and nothing else.

How "loopback" is decided

From the address as written, never by resolving it. An IP literal is loopback by its value; "localhost" is loopback by RFC 6761 and is bound as 127.0.0.1, so no resolver configuration can turn it into anything else; and every other hostname counts as reaching beyond loopback, whatever it resolves to today. A refusal that costs an operator typing 127.0.0.1 is the cheap side of that trade.

Index

Constants

View Source
const AckKey = "ui.allow_unauthenticated_network"

AckKey is the configuration key that lets a hub without sign-in listen beyond loopback.

View Source
const LoopbackHost = "127.0.0.1"

LoopbackHost is what a hub without sign-in binds when no address is named.

Variables

View Source
var ErrInvalidListen = errors.New("not a listen address")

ErrInvalidListen is wrapped by Decide when the address does not parse.

View Source
var ErrOpenToNetwork = errors.New("a hub without sign-in may not listen beyond loopback")

ErrOpenToNetwork is wrapped by Decide's refusal, so a caller can tell it from a malformed address.

Functions

func HasSignIn

func HasSignIn(sso, staticToken bool) bool

HasSignIn is the one definition of "this hub can tell people apart": SSO or a static token. API tokens do not count — a hub whose only credentials are API tokens serves every caller that presents none. Decide asks it about a configuration, and a running hub asks it about itself before it guards a request against DNS rebinding (Task 20394), so the bind address and the Host check cannot disagree about which hubs are open.

Types

type Plan

type Plan struct {
	// Host is what net.Listen is given: "" for every interface, otherwise an
	// address or hostname. Never "localhost", which is bound as 127.0.0.1.
	Host string
	// Port is the TCP port.
	Port int
	// Scope is how far Host reaches.
	Scope Scope
	// Explicit reports that an operator named the address; false means it is
	// the default for this hub's authentication.
	Explicit bool
	// Authenticated reports a browser credential: SSO or a static token.
	Authenticated bool
	// Acknowledged reports a hub without sign-in that listens beyond loopback
	// because ui.allow_unauthenticated_network says it may.
	Acknowledged bool
	// TLS reports that the listener serves HTTPS itself.
	TLS bool
	// ExternalHTTPS reports that the URL browsers reach the hub at is https.
	ExternalHTTPS bool
	// ExternalURL is that URL, and ExternalURLKey the setting it came from.
	ExternalURL    string
	ExternalURLKey string
}

Plan is where a hub listens, and the facts the decision was made on.

func Decide

func Decide(r Request) (Plan, error)

Decide works out where a hub listens, or refuses.

The only refusal for a well-formed address is ErrOpenToNetwork: a hub without sign-in, an address beyond loopback the operator named, and no acknowledgement. Every other combination is a Plan, and the caller prints Notice for it.

func (Plan) Addr

func (p Plan) Addr() string

Addr is the host:port to listen on: ":8080" for every interface.

func (Plan) BeyondLoopback

func (p Plan) BeyondLoopback() bool

BeyondLoopback reports a listener another machine may reach.

func (Plan) LocalHost

func (p Plan) LocalHost() string

LocalHost is the host this machine reaches the listener at: the bound address itself, or 127.0.0.1 for every interface (a dual-stack wildcard accepts IPv4 loopback). It is what the hub-cluster advertise URL defaults to, so members on one machine keep reaching each other whatever the bind.

func (Plan) Notice

func (p Plan) Notice() (text string, warning bool)

Notice is what a starting hub prints about its exposure, or "" when there is nothing worth saying. Warning reports that it must go to stderr, every start: an open hub serving the network by acknowledgement.

func (Plan) Open

func (p Plan) Open() bool

Open reports a hub with no browser credential.

func (Plan) PlaintextBehindHTTPS

func (p Plan) PlaintextBehindHTTPS() bool

PlaintextBehindHTTPS reports a hub that serves plaintext beyond loopback while its external URL is https: TLS is terminated in front of it, and anything that reaches the port directly skips the terminator — a sign-in, a session cookie or an API token sent there crosses the network in the clear.

func (Plan) String

func (p Plan) String() string

String renders the bind address for a person: "127.0.0.1:8080", "*:8080" for every interface, "[::1]:8080". Without a port it is the host alone.

func (Plan) URL

func (p Plan) URL() string

URL is how a browser on this machine reaches the dashboard: localhost for every interface, the bound address otherwise.

type RefusalError

type RefusalError struct {
	// Plan is what would have been bound.
	Plan Plan
	// Source names the setting that asked for it.
	Source string
}

RefusalError is Decide's refusal: an open hub asked to listen beyond loopback. It wraps ErrOpenToNetwork.

func (*RefusalError) Error

func (e *RefusalError) Error() string

func (*RefusalError) Unwrap

func (e *RefusalError) Unwrap() error

type Request

type Request struct {
	// Listen is the bind host as the operator gave it: --listen, else
	// ui.listen. Empty asks for the default. It carries no port.
	Listen string
	// Source names where Listen came from ("--listen", "ui.listen"), for
	// messages. Empty reads as "--listen / ui.listen".
	Source string
	// Port is the TCP port (--port). Zero is allowed: it is how a caller
	// asks about a configuration without a port in mind.
	Port int

	// SSO reports ui.oidc.enabled.
	SSO bool
	// StaticToken reports a --token or CLOOP_UI_TOKEN.
	StaticToken bool
	// AllowUnauthenticatedNetwork is ui.allow_unauthenticated_network.
	AllowUnauthenticatedNetwork bool

	// TLS reports that the listener itself serves HTTPS (ui.tls, --tls-cert).
	TLS bool
	// ExternalURL is the URL browsers reach the hub at — ui.external_url, or
	// the SSO callback when only that names it (config.UIConfig.PublicURL).
	ExternalURL string
	// ExternalURLKey names the setting ExternalURL came from, for messages.
	// Empty reads as "ui.external_url".
	ExternalURLKey string
}

Request is what Decide needs to know about one hub.

type Scope

type Scope int

Scope is how far a bind address reaches.

const (
	// ScopeLoopback reaches this machine only: 127.0.0.0/8, ::1, localhost.
	ScopeLoopback Scope = iota
	// ScopeEvery is every interface: 0.0.0.0, ::, or no host at all. Go binds
	// one dual-stack socket for each of them, so all three reach IPv4 and
	// IPv6 alike.
	ScopeEvery
	// ScopeAddress is one address beyond loopback: an interface's IP, or a
	// hostname.
	ScopeAddress
)

func ParseHost

func ParseHost(s string) (host string, scope Scope, err error)

ParseHost validates a listen host and classifies it. It returns the host to give net.Listen — "localhost" becomes 127.0.0.1, an IPv4-mapped address its IPv4 form, an unspecified address "" — and its scope.

A port is refused rather than split off: the port is --port, and a second place to set it is a second answer to "which port is this hub on".

func (Scope) String

func (s Scope) String() string

String names the scope for messages.

Jump to

Keyboard shortcuts

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