mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 08:46:52 +01:00
It is served there now, on a pipe, so nothing set the flag any more. It was also a plain bool written by the first reconcile and by the reload callback, which was the review's point about it. - infoSupported, errInfoUnsupported, the Rebind check and the reload branch that warned once go Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
250 lines
9.9 KiB
Go
250 lines
9.9 KiB
Go
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"github.com/slackhq/nebula/commands"
|
|
"github.com/slackhq/nebula/config"
|
|
)
|
|
|
|
// Endpoints is every local API Wire brought up, with the one Stop the daemon's exit
|
|
// path calls.
|
|
type Endpoints struct {
|
|
Control *Server
|
|
Info *InfoServer
|
|
}
|
|
|
|
// Stop closes both endpoints and removes their sockets.
|
|
func (e *Endpoints) Stop() {
|
|
e.Info.Stop()
|
|
e.Control.Stop()
|
|
}
|
|
|
|
// Wire brings up the local APIs for a running nebula and keeps them following the
|
|
// config across reloads: the control socket unless the config turns it off, and the
|
|
// info API when the config asks for it.
|
|
//
|
|
// Every entry point that runs nebula as a daemon calls this, so the endpoints behave the
|
|
// same whether nebula was started from a shell or by a service manager. It deliberately
|
|
// does not live in nebula.Main: the control socket is on by default, so an embedder
|
|
// inside an app sandbox would have to know to turn off a socket it cannot use. Wiring it
|
|
// at the daemon entry points means embedders get the command registry and no listener.
|
|
//
|
|
// Every failure below is a warning, bar one. Nebula is expected to keep forwarding
|
|
// packets whether or not anyone can reach it; but a name that another process holds is
|
|
// not a misconfiguration, it is a process that will be answering nebula ctl in the
|
|
// daemon's place, and that is logged as an error.
|
|
//
|
|
// Call it after Control.Start, so the listeners only exist once the interface is up.
|
|
func Wire(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *Endpoints {
|
|
l = l.With("subsystem", "localapi")
|
|
return &Endpoints{
|
|
Control: wireControl(ctx, l, c, reg),
|
|
Info: wireInfo(ctx, l, c, reg),
|
|
}
|
|
}
|
|
|
|
// sharedSocketPath reports the path the config gives to both the control socket and the
|
|
// info API, when it gives them one path. The two speak different protocols, so neither
|
|
// binds it: each side says so and leaves it, and the operator fixes the config. The
|
|
// configured paths are compared, not the ones being served, so the case is caught on
|
|
// whichever side reconciles first, when the other side's bind has failed, and when a
|
|
// reload moves one socket onto the other's path.
|
|
func sharedSocketPath(c *config.C) (string, bool) {
|
|
if !ControlEnabled(c) {
|
|
return "", false
|
|
}
|
|
|
|
ctl, err := SocketPathFromConfig(c)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
info, err := infoConfigFrom(c)
|
|
if err != nil || !info.Enabled || info.Path != ctl {
|
|
return "", false
|
|
}
|
|
|
|
return ctl, true
|
|
}
|
|
|
|
// wireControl brings up the control socket and keeps it pointed at the configured path.
|
|
func wireControl(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *Server {
|
|
srv := NewServer(l, reg)
|
|
|
|
// Registered before the first attempt, so that a path or bind that is bad at startup
|
|
// can be fixed with a config edit and a reload, the same as one that goes bad later,
|
|
// and so that a HUP landing in between is harmless: Rebind on a server already at
|
|
// the path is a no-op.
|
|
reconcile := followControlConfig(ctx, l, c, srv)
|
|
reconcile(c)
|
|
if !ControlEnabled(c) {
|
|
l.Info("Local control socket is disabled", "key", CtlEnabledKey)
|
|
}
|
|
return srv
|
|
}
|
|
|
|
// followControlConfig registers the reload callback that keeps the socket where
|
|
// local_api.control.socket says it is, moving it when the key changes and bringing one
|
|
// up while there is none, and returns it for the first attempt to call.
|
|
//
|
|
// The key is reloadable because the client finds the socket by reading the config: if the
|
|
// two could disagree, the file on disk would name a socket that is not there.
|
|
//
|
|
// Every reload is reconciled rather than gated on the key having changed. Rebind on a
|
|
// server already at the path is a no-op, so that costs nothing; and a bind that failed
|
|
// for a reason outside the config, a runtime directory that was not there yet, is fixed
|
|
// outside the config too, so the reload that follows the fix is a plain one. That holds
|
|
// for a move as much as for the first bind: a daemon left on its old path by a move that
|
|
// could not bind is one the client, reading the new path from the same file, cannot
|
|
// reach, and the operator's fix is the directory and a reload, not an edit to an
|
|
// unrelated line to make the key read as changed.
|
|
func followControlConfig(ctx context.Context, l *slog.Logger, c *config.C, srv *Server) func(*config.C) {
|
|
reconcile := func(c *config.C) {
|
|
if !ControlEnabled(c) {
|
|
if srv.Path() != "" {
|
|
srv.Stop()
|
|
l.Info("Stopped the local control socket, it is disabled in the reloaded config")
|
|
}
|
|
return
|
|
}
|
|
|
|
want, err := SocketPathFromConfig(c)
|
|
if err != nil {
|
|
warnUnusable(l, "local control socket", srv.Path(), err)
|
|
return
|
|
}
|
|
|
|
acc, err := controlAccessFrom(c)
|
|
if err != nil {
|
|
warnUnusable(l, "local control socket", srv.Path(), err)
|
|
return
|
|
}
|
|
|
|
if p, shared := sharedSocketPath(c); shared {
|
|
l.Warn("Local control socket is configured on the info API's socket path and will not bind it",
|
|
"path", p, "current", srv.Path(), "fix", "set "+InfoSocketKey+" to another path")
|
|
return
|
|
}
|
|
|
|
old := srv.Path()
|
|
err = srv.Rebind(ctx, want, acc)
|
|
reportRebind(l, "local control socket", old, want, err, srv.Path())
|
|
}
|
|
|
|
onReload(l, c, "local control socket", reconcile)
|
|
return reconcile
|
|
}
|
|
|
|
// onReload registers fn to run on every reload. The callback recovers from its own
|
|
// panics rather than relying on the config package, whose callback loop has no isolation
|
|
// of its own: a panic there would take the daemon down and skip every callback queued
|
|
// behind this one.
|
|
func onReload(l *slog.Logger, c *config.C, what string, fn func(*config.C)) {
|
|
c.RegisterReloadCallback(func(c *config.C, _ *config.Reload) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
l.Error("Recovered from panic reloading the "+what, "panic", r)
|
|
}
|
|
}()
|
|
fn(c)
|
|
})
|
|
}
|
|
|
|
// warnUnusable logs a config the endpoint cannot follow: with nothing serving, the
|
|
// endpoint is not available; with something serving, it is kept as it is.
|
|
func warnUnusable(l *slog.Logger, what, current string, err error) {
|
|
if current == "" {
|
|
l.Warn("The "+what+" is not available, its config is unusable", "error", err)
|
|
return
|
|
}
|
|
l.Warn("Ignoring unusable "+what+" config from the reload, keeping the current endpoint", "error", err, "current", current)
|
|
}
|
|
|
|
// reportRebind logs what a Rebind did beyond the listening line the bind itself writes:
|
|
// nothing for an endpoint left where it was, a move with both paths, and a failure with
|
|
// what the operator is left with. old is the path served before the call, "" for none,
|
|
// want the one asked for, and now the path served after it: on unix a failed move keeps
|
|
// the old socket, and now is old; on windows a change of access closes the pipe before
|
|
// creating it again, and a failure there leaves nothing serving, so now is "".
|
|
//
|
|
// Nearly every failure is the operator's environment, a missing runtime directory or a
|
|
// bad path, and a warning; a name held by another process is an error, since nothing
|
|
// else will say that something is sitting where the daemon should be.
|
|
func reportRebind(l *slog.Logger, what, old, want string, err error, now string) {
|
|
level := l.Warn
|
|
if errors.Is(err, errEndpointHeld) {
|
|
level = l.Error
|
|
}
|
|
|
|
switch {
|
|
case err == nil:
|
|
if old != "" && old != want {
|
|
l.Info("Moved the "+what, "from", old, "to", want)
|
|
}
|
|
case old == "":
|
|
level("Could not start the "+what+", it will not be available until a reload binds it", "error", err, "path", want)
|
|
default:
|
|
level("Could not bind the "+what+" at the requested path", "error", err, "requested", want, "serving", now)
|
|
}
|
|
}
|
|
|
|
// wireInfo brings up the info API when the config asks for one and follows the config
|
|
// across reloads: enabling it, disabling it, moving its socket and changing its mode.
|
|
//
|
|
// Every reload is reconciled, as the control socket's is; see followControlConfig. A
|
|
// section that does not parse keeps whatever is running: the operator is told, and the
|
|
// programs relying on the endpoint are not cut off by a typo.
|
|
func wireInfo(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *InfoServer {
|
|
srv := NewInfoServer(l, reg)
|
|
|
|
reconcile := func(c *config.C) {
|
|
cfg, err := infoConfigFrom(c)
|
|
if err != nil {
|
|
warnUnusable(l, "local info API", srv.Path(), err)
|
|
return
|
|
}
|
|
|
|
if !cfg.Enabled {
|
|
if srv.Path() != "" {
|
|
srv.Stop()
|
|
l.Info("Stopped the local info API, it is disabled in the reloaded config")
|
|
}
|
|
return
|
|
}
|
|
|
|
// The two endpoints speak different protocols; one socket cannot be both. Caught
|
|
// here, from the configured paths, because listen's own answer, that the socket is
|
|
// in use by another nebula, would send the operator looking for a second process.
|
|
if p, shared := sharedSocketPath(c); shared {
|
|
if cur := srv.Path(); cur != "" {
|
|
l.Warn("Ignoring the local info API's new socket path, it is also the control socket's; keeping the current endpoint",
|
|
"path", p, "current", cur, "fix", "set "+InfoSocketKey+" to another path")
|
|
} else {
|
|
l.Warn("Local info API is not available, its socket path is also the control socket's", "path", p, "fix", "set "+InfoSocketKey+" to another path")
|
|
}
|
|
return
|
|
}
|
|
|
|
old := srv.Path()
|
|
err = srv.Rebind(ctx, cfg.Path, cfg.Access)
|
|
reportRebind(l, "local info API", old, cfg.Path, err, srv.Path())
|
|
}
|
|
|
|
// Registered before the first attempt, as the control socket's is; see wireControl.
|
|
onReload(l, c, "local info API", reconcile)
|
|
reconcile(c)
|
|
return srv
|
|
}
|
|
|
|
// errEndpointHeld is returned by listen when the endpoint's name is already held by
|
|
// some other process, which nebula will not serve alongside: whoever holds it would be
|
|
// answering the endpoint's clients. It is only raised on windows, where the default
|
|
// name can be held by another administrator's process and an override outside the
|
|
// protected namespace by any account's; on unix a path that is still being served is an
|
|
// earlier nebula, which clearStale reports as in use. The log line that carries it names
|
|
// the endpoint, so the sentence does not.
|
|
var errEndpointHeld = errors.New("the pipe's name is held by another process")
|