mirror of
https://github.com/slackhq/nebula.git
synced 2026-09-30 03:16:38 +02:00
Every diagnostic command nebula has was reachable through exactly one door:
the built-in ssh debug server. That server is off by default, and turning it
on means generating a host key, writing an sshd block with authorized public
keys, and SIGHUPing the daemon. That is a lot of ceremony to answer "what
version is this node running".
Nebula now serves the same commands over a local unix socket, enabled by
default, and `nebula ctl <command>` runs them. The socket lives in a 0700
directory so filesystem permissions are the access control; no keys, nothing
on the network. Failing to create it is logged and never blocks startup.
The command registry was already transport neutral, so this is mostly new
transport rather than new commands:
- diag/ holds the registry, dispatch, writer and wire protocol, moved out
of sshd because none of it was ever about ssh. sshd and ctl.go dispatch
against one shared registry.
- commands.go holds every command implementation, moved out of ssh.go
(which was 85% not ssh) and renamed off the ssh prefix. Adding a command
there makes it available over both transports.
- ssh.go keeps only host keys, authorized users, and the listen address.
- ctl.go supervises the socket, following the statsServer lifecycle shape.
The wire protocol frames the response rather than terminating it, because
print-cert -raw and list-hostmap -json both emit arbitrary bytes that no
sentinel could safely delimit. argv travels as a list so quoting survives.
Exit statuses are real: 0, 2 for usage, 127 for an unknown command.
Two things fall out. The ssh console now reports a real exit status instead
of a hardcoded zero, so `ssh host list-hostmap` is scriptable too. And eight
command callbacks that silently returned nil on a flags type mismatch now
report it, which the exit status makes visible.
Windows is a stub returning a clear "not supported" until it gets a named
pipe with a security descriptor; iOS and Android are never enabled, having no
daemon for a CLI to attach to.
Breaking for embedders of the sshd package: NewSSHServer takes a
*diag.Registry, SSHServer.RegisterCommand is gone in favor of registering on
that registry, and the command types live in diag rather than sshd.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fya5fTXGiwX72FUmoL9y3
175 lines
4.1 KiB
Go
175 lines
4.1 KiB
Go
package diag
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/armon/go-radix"
|
|
)
|
|
|
|
var (
|
|
// ErrUnknownCommand is returned by the Registry when the first argument names no
|
|
// registered command. The user has already been told so on their writer.
|
|
ErrUnknownCommand = errors.New("unknown command")
|
|
|
|
// ErrUsage wraps a flag parsing failure. The flag package has already written the
|
|
// details to the caller's writer by the time this is returned, so a transport should
|
|
// use it only to pick an exit status.
|
|
ErrUsage = errors.New("usage")
|
|
)
|
|
|
|
// CommandFlags is a function called before help or command execution to parse command line flags
|
|
// It should return a flag.FlagSet instance and a pointer to the struct that will contain parsed flags
|
|
type CommandFlags func() (*flag.FlagSet, any)
|
|
|
|
// CommandCallback is the function called when your command should execute.
|
|
// fs will be a a pointer to the struct provided by Command.Flags callback, if there was one. -h and -help are reserved
|
|
// and handled automatically for you.
|
|
// a will be any unconsumed arguments, if no Command.Flags was available this will be all the flags passed in.
|
|
// w is the writer to use when sending messages back to the client.
|
|
// If an error is returned by the callback it is logged locally, the callback should handle messaging errors to the user
|
|
// where appropriate
|
|
type CommandCallback func(fs any, a []string, w StringWriter) error
|
|
|
|
type Command struct {
|
|
Name string
|
|
ShortDescription string
|
|
Help string
|
|
Flags CommandFlags
|
|
Callback CommandCallback
|
|
}
|
|
|
|
func execCommand(c *Command, args []string, w StringWriter) error {
|
|
var (
|
|
fl *flag.FlagSet
|
|
fs any
|
|
)
|
|
|
|
if c.Flags != nil {
|
|
fl, fs = c.Flags()
|
|
if fl != nil {
|
|
// SetOutput() here in case fl.Parse dumps usage.
|
|
fl.SetOutput(w.GetWriter())
|
|
err := fl.Parse(args)
|
|
if err != nil {
|
|
// fl.Parse has dumped error information to the user via the w writer, so
|
|
// the wrapper exists purely so a transport can tell a usage problem from a
|
|
// command that ran and failed.
|
|
return fmt.Errorf("%w: %w", ErrUsage, err)
|
|
}
|
|
args = fl.Args()
|
|
}
|
|
}
|
|
|
|
return c.Callback(fs, args, w)
|
|
}
|
|
|
|
func dumpCommands(c *radix.Tree, w StringWriter) {
|
|
err := w.WriteLine("Available commands:")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cmds := make([]string, 0)
|
|
for _, l := range allCommands(c) {
|
|
cmds = append(cmds, fmt.Sprintf("%s - %s", l.Name, l.ShortDescription))
|
|
}
|
|
|
|
sort.Strings(cmds)
|
|
_ = w.Write(strings.Join(cmds, "\n") + "\n\n")
|
|
}
|
|
|
|
func lookupCommand(c *radix.Tree, sCmd string) (*Command, error) {
|
|
cmd, ok := c.Get(sCmd)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
command, ok := cmd.(*Command)
|
|
if !ok {
|
|
return nil, errors.New("failed to cast command")
|
|
}
|
|
|
|
return command, nil
|
|
}
|
|
|
|
func matchCommand(c *radix.Tree, cmd string) []string {
|
|
cmds := make([]string, 0)
|
|
c.WalkPrefix(cmd, func(found string, v any) bool {
|
|
cmds = append(cmds, found)
|
|
return false
|
|
})
|
|
sort.Strings(cmds)
|
|
return cmds
|
|
}
|
|
|
|
func allCommands(c *radix.Tree) []*Command {
|
|
cmds := make([]*Command, 0)
|
|
c.WalkPrefix("", func(found string, v any) bool {
|
|
cmd, ok := v.(*Command)
|
|
if ok {
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
return false
|
|
})
|
|
return cmds
|
|
}
|
|
|
|
func helpCallback(commands *radix.Tree, a []string, w StringWriter) (err error) {
|
|
// Just typed help
|
|
if len(a) == 0 {
|
|
dumpCommands(commands, w)
|
|
return nil
|
|
}
|
|
|
|
// We are printing a specific commands help text
|
|
cmd, err := lookupCommand(commands, a[0])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if cmd != nil {
|
|
err = w.WriteLine(fmt.Sprintf("%s - %s", cmd.Name, cmd.ShortDescription))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cmd.Help != "" {
|
|
err = w.WriteLine(fmt.Sprintf(" %s", cmd.Help))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if cmd.Flags != nil {
|
|
fs, _ := cmd.Flags()
|
|
if fs != nil {
|
|
fs.SetOutput(w.GetWriter())
|
|
fs.PrintDefaults()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
err = w.WriteLine("Command not available " + a[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkHelpArgs(args []string) bool {
|
|
for _, a := range args {
|
|
if a == "-h" || a == "-help" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|