ctl.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. package ctl
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "flag"
  7. "fmt"
  8. "io"
  9. "log/slog"
  10. "os"
  11. "strings"
  12. "github.com/slackhq/nebula/commands"
  13. "github.com/slackhq/nebula/config"
  14. "github.com/slackhq/nebula/localapi"
  15. )
  16. // Word is the argument that selects the control client instead of the daemon.
  17. const Word = "ctl"
  18. // Subcommand handles the one bare word a daemon binary accepts before its flags. `nebula
  19. // ctl <command>` runs the client. Any other first argument that is not a flag is a
  20. // mistyped subcommand, reported with a usage line: the daemon's flag parser would ignore
  21. // the stray word and start nebula, so `nebula clt` would bring up a second daemon
  22. // instead of reporting a typo. It returns the exit code and true when the process is
  23. // done, and false when argv is the daemon's to parse. Both daemon binaries, cmd/nebula
  24. // and cmd/nebula-service, call it before anything else, so each has a nebula ctl and
  25. // neither has the hazard. A word that follows the daemon's flags instead is Trailing's.
  26. func Subcommand(argv []string) (code int, done bool) {
  27. if len(argv) < 2 || strings.HasPrefix(argv[1], "-") {
  28. return 0, false
  29. }
  30. if argv[1] == Word {
  31. return Run(argv[2:]), true
  32. }
  33. return unknownCommand(argv[1]), true
  34. }
  35. // Trailing handles the words the daemon's flag parser leaves unparsed: flag.Parse stops
  36. // at the first argument that is not a flag and hands the rest back. rest is flag.Args()
  37. // after the daemon has parsed, and configPath is the config the daemon's -config named,
  38. // "" when it did not. Both daemon binaries call it right after flag.Parse, so that
  39. // `nebula -config x ctl identity` runs the client and `nebula -config x clt` reports a
  40. // typo, as Subcommand does for a word at the front of the line; a daemon that ignored
  41. // the words would start in their place.
  42. //
  43. // A leading ctl word runs the client on what follows, with the daemon's -config carried
  44. // over: an operator who typed it meant the daemon running on that config, and the client
  45. // derives the same socket from it. Any other word is a mistake, reported as Subcommand
  46. // reports one.
  47. func Trailing(rest []string, configPath string) (code int, done bool) {
  48. if len(rest) == 0 {
  49. return 0, false
  50. }
  51. if rest[0] != Word {
  52. return unknownCommand(rest[0]), true
  53. }
  54. args := rest[1:]
  55. if configPath != "" {
  56. // First, so a -config on the ctl side of the line still wins: Parse applies
  57. // flags in order and the last value set is the one read.
  58. args = append([]string{"-" + commands.FlagConfig, configPath}, args...)
  59. }
  60. return Run(args), true
  61. }
  62. // unknownCommand reports a bare word that is neither a flag nor ctl, with the one usage
  63. // line that says what the two forms of the command line are.
  64. func unknownCommand(word string) int {
  65. fmt.Fprintf(os.Stderr, "nebula: unknown command %q\n\n", word)
  66. fmt.Fprintf(os.Stderr, "Run nebula with flags to start it, or `nebula %s <command>` to control one that is already running.\n", Word)
  67. return 1
  68. }
  69. // Run implements `nebula ctl <command> [args]`
  70. //
  71. // args is everything after the `ctl` word.
  72. func Run(args []string) int {
  73. fs := flag.NewFlagSet("nebula ctl", flag.ContinueOnError)
  74. fs.SetOutput(os.Stderr)
  75. configPath := fs.String(commands.FlagConfig, "", "Path to the config of the running nebula, used to locate its control socket")
  76. socketPath := fs.String(commands.FlagSocket, "", "Path to the control socket, instead of the one the config implies")
  77. fs.Usage = func() {
  78. fmt.Fprintf(os.Stderr, "Usage: nebula ctl [flags] <command> [command flags]\n\n")
  79. fmt.Fprintf(os.Stderr, "Inspects and controls a nebula already running on this machine.\n")
  80. fmt.Fprintf(os.Stderr, "Run it as the user who ran nebula, or as a member of the group %s names.\n\n", localapi.CtlGroupKey)
  81. fmt.Fprintf(os.Stderr, "Flags, which may come before or after the command:\n")
  82. fs.PrintDefaults()
  83. fmt.Fprintf(os.Stderr, "\nRun `nebula ctl help` for the available commands.\n")
  84. }
  85. own, cmdArgs := splitLine(fs, args)
  86. if err := fs.Parse(own); err != nil {
  87. // fs has already printed the usage, or the problem. Asking for help is not a
  88. // failure, and a script that chains on `nebula ctl -h` should not see one.
  89. if errors.Is(err, flag.ErrHelp) {
  90. return 0
  91. }
  92. return 1
  93. }
  94. if len(cmdArgs) == 0 {
  95. cmdArgs = []string{"help"}
  96. }
  97. path, err := resolveSocket(*socketPath, *configPath)
  98. if err != nil {
  99. fmt.Fprintf(os.Stderr, "%s\n", err)
  100. return 1
  101. }
  102. client := &localapi.Client{Path: path}
  103. // Ask the daemon what it serves before parsing anything. The flags belong to the
  104. // command, which lives over there, so this is what lets the CLI keep its usual shape
  105. // while the wire protocol stays a named-parameter API rather than a shipped argv.
  106. schema, err := client.Describe()
  107. if err != nil {
  108. // A daemon that answered was reached, and what it said, or that it could not be
  109. // read, is the whole message.
  110. var refused *localapi.DaemonError
  111. var unread *localapi.ReplyError
  112. if errors.As(err, &refused) || errors.As(err, &unread) {
  113. fmt.Fprintf(os.Stderr, "nebula: %s\n", err)
  114. return 1
  115. }
  116. // Otherwise the operating system said why, and it is passed on as it came: a
  117. // permission denied or a missing socket says more about what to do than a
  118. // guess at the operator's setup would.
  119. fmt.Fprintf(os.Stderr, "nebula: could not reach the control socket at %s: %s\n", path, err)
  120. return 1
  121. }
  122. req, printedHelp, err := parseCommand(schema, cmdArgs)
  123. if err != nil {
  124. fmt.Fprintf(os.Stderr, "%s\n", err)
  125. return 1
  126. }
  127. if printedHelp {
  128. return 0
  129. }
  130. res, err := client.Invoke(string(req.name), req.Params, req.Args, wireFormat(req.Format))
  131. if err != nil {
  132. fmt.Fprintf(os.Stderr, "nebula: %s\n", err)
  133. return 1
  134. }
  135. out := output(res, req.Format)
  136. if out != "" {
  137. fmt.Fprint(os.Stdout, out)
  138. if !strings.HasSuffix(out, "\n") {
  139. fmt.Fprintln(os.Stdout)
  140. }
  141. }
  142. // Error set is the one failure signal, whether the request never ran or the command
  143. // ran and refused. A refusal asked for in JSON has already gone to stdout as the
  144. // {ok, reason} object, and is not said again on stderr; the exit status carries it.
  145. if res.Error == "" {
  146. return 0
  147. }
  148. if len(res.Result) == 0 && len(res.Text) == 0 {
  149. msg := res.Error
  150. if !strings.HasSuffix(msg, "\n") {
  151. msg += "\n"
  152. }
  153. fmt.Fprint(os.Stderr, msg)
  154. }
  155. return 1
  156. }
  157. // output renders what the daemon answered for the format the line asked for. A text
  158. // request is answered in Text with the bytes the command wrote, printed as they are; a
  159. // JSON request is answered in Result with the value itself, printed as it came, since a
  160. // value that happens to be a string is still JSON to the script reading it. Indentation
  161. // is the client's business: whitespace does not survive a JSON envelope, and the daemon
  162. // has no reason to care how its answer is laid out. The bytes are re-indented rather
  163. // than decoded and encoded again, which would sort the keys and round the numbers.
  164. func output(res *localapi.Response, format commands.Format) string {
  165. switch format {
  166. case commands.FormatJSON:
  167. return string(res.Result)
  168. case commands.FormatPrettyJSON:
  169. var buf bytes.Buffer
  170. if err := json.Indent(&buf, res.Result, "", " "); err != nil {
  171. return string(res.Result)
  172. }
  173. return buf.String()
  174. default:
  175. return string(res.Text)
  176. }
  177. }
  178. // ctlRequest is one parsed nebula ctl command line: the command's name, which only the
  179. // daemon can resolve, and the line as every surface parses it. The Request's Command is
  180. // nil here, since the CLI holds a schema rather than the command, and its Format is
  181. // FormatUnset when the line named none, which to nebula ctl means text: its default
  182. // differs from the API's.
  183. type ctlRequest struct {
  184. name commands.Name
  185. commands.Request
  186. }
  187. // wireFormat is what a request asks the daemon for. Pretty is the client's business,
  188. // since whitespace does not survive a JSON envelope, so it travels as json. No
  189. // preference is text, nebula ctl's default: a person gets the human rendering wherever
  190. // one exists.
  191. func wireFormat(f commands.Format) string {
  192. switch f {
  193. case commands.FormatJSON, commands.FormatPrettyJSON:
  194. return "json"
  195. default:
  196. return "text"
  197. }
  198. }
  199. // parseCommand turns the user's command line into a ctlRequest, using the schema the
  200. // daemon supplied. The line is parsed with the same rules the daemon applies to an ssh
  201. // line, through ParseArgs, and ParseLine reads what was set into the Request to send, so
  202. // a line means the same on both.
  203. //
  204. // printedHelp reports that help was written for the command instead, and there is
  205. // nothing to send.
  206. func parseCommand(schema []commands.CommandSchema, argv []string) (req ctlRequest, printedHelp bool, err error) {
  207. name := commands.Name(argv[0])
  208. var found *commands.CommandSchema
  209. for i := range schema {
  210. if schema[i].Name == name {
  211. found = &schema[i]
  212. break
  213. }
  214. }
  215. if found == nil {
  216. // Let the daemon answer. It knows whether this is a typo or a command it holds
  217. // back, and its message for the second case is better than anything guessable
  218. // from the schema alone.
  219. return ctlRequest{name: name, Request: commands.Request{Args: argv[1:]}}, false, nil
  220. }
  221. // The flag package's own report is a bare message followed by a usage dump in a layout
  222. // that does not match WriteCommandHelp, so it is silenced and the error is reported
  223. // once, below, with a pointer at the help this command actually prints.
  224. fs := flag.NewFlagSet(string(name), flag.ContinueOnError)
  225. fs.SetOutput(io.Discard)
  226. for _, f := range found.Flags {
  227. if f.Bool {
  228. fs.Bool(f.Name, f.Default == "true", f.Usage)
  229. continue
  230. }
  231. fs.String(f.Name, f.Default, f.Usage)
  232. }
  233. operands, err := commands.ParseArgs(fs, argv[1:])
  234. if err != nil {
  235. if errors.Is(err, flag.ErrHelp) {
  236. // The same rendering the daemon uses for `help <command>`, so -h and help agree.
  237. _ = commands.WriteCommandHelp(*found, os.Stdout)
  238. return ctlRequest{}, true, nil
  239. }
  240. // Say what flag did: an unknown flag, a missing value and a malformed value are
  241. // different mistakes, and "does not take" would be wrong for two of them.
  242. return ctlRequest{}, false, fmt.Errorf("nebula: %s: %s (see 'nebula ctl %s -h')", name, err, name)
  243. }
  244. // Only what the user actually set travels. A parameter left alone is the daemon's
  245. // default to apply, not the client's to assert, and the presentation flags are the
  246. // client's to interpret, never sent.
  247. //
  248. // Where the command has no human form the daemon says so, and only once the command
  249. // has run: a refusal ("No vpn address was provided") always has a human form, so
  250. // telling the user to pass -json before finding out whether the command would even
  251. // accept the line sends them to fix the wrong thing. That is why the schema does not
  252. // say which commands have a human form: nothing should decide on it before running.
  253. return ctlRequest{name: name, Request: commands.ParseLine(fs, operands)}, false, nil
  254. }
  255. // splitLine divides a nebula ctl line into the client's own flags, for fs to parse, and
  256. // the command line to send, which begins with the command word. The client's flags may
  257. // come before the command or after it: `nebula ctl -config x identity` and
  258. // `nebula ctl identity -config x` mean the same, so an operator who has typed a command
  259. // and then remembers which daemon they meant does not have to retype the line.
  260. //
  261. // Before the command word every flag is the client's, whatever it is, so an unknown one
  262. // is reported by fs, and -h is the client's usage. After it only the
  263. // flags fs defines are lifted out, and only those: the command's own flags are unknown
  264. // here, since they come from the daemon the line has yet to name, and -h after the
  265. // command asks for that command's help. Which command flags take a value is unknown too,
  266. // so the client's names are reserved in the commands package and no command can give
  267. // one a value that would be lifted from here. A "--" before the command is consumed, as
  268. // fs.Parse would have; one after it belongs to the command line and ends the lifting,
  269. // so an operand spelled like a client flag can still be passed.
  270. func splitLine(fs *flag.FlagSet, args []string) (own, cmd []string) {
  271. seenCommand := false
  272. for i := 0; i < len(args); i++ {
  273. a := args[i]
  274. if a == "--" {
  275. if seenCommand {
  276. return own, append(cmd, args[i:]...)
  277. }
  278. seenCommand = true
  279. continue
  280. }
  281. if !seenCommand {
  282. if len(a) < 2 || a[0] != '-' {
  283. seenCommand = true
  284. cmd = append(cmd, a)
  285. continue
  286. }
  287. own = append(own, a)
  288. if f, hasValue := commands.LookupArg(fs, a); f != nil && !hasValue && !commands.IsBoolFlag(f) && i+1 < len(args) {
  289. i++
  290. own = append(own, args[i])
  291. }
  292. continue
  293. }
  294. if len(a) >= 2 && a[0] == '-' {
  295. // The same reading of the argument ParseArgs makes, so a word this lifts as a
  296. // client flag's value is one the daemon's parser would have read the same way.
  297. if f, hasValue := commands.LookupArg(fs, a); f != nil {
  298. own = append(own, a)
  299. if !hasValue && !commands.IsBoolFlag(f) && i+1 < len(args) {
  300. i++
  301. own = append(own, args[i])
  302. }
  303. continue
  304. }
  305. }
  306. cmd = append(cmd, a)
  307. }
  308. return own, cmd
  309. }
  310. // resolveSocket decides which socket to talk to. It never searches: the answer is the
  311. // socket the operator named, or the one the config implies, and nothing else.
  312. //
  313. // An explicit flag wins. Otherwise the config is read, the one the operator named or the
  314. // one found next to the executable, and the path is the socket that config implies: its
  315. // local_api.control.socket if set, else the same derivation the daemon applies.
  316. func resolveSocket(socketFlag, configFlag string) (string, error) {
  317. if socketFlag != "" {
  318. return socketFlag, nil
  319. }
  320. return pathFromConfig(configFlag)
  321. }
  322. // pathFromConfig returns the socket path the daemon would be serving: the one
  323. // local_api.control.socket names in the config, so an operator who moved the socket does
  324. // not have to repeat themselves on every invocation, else the default.
  325. //
  326. // A config the operator did not name is a guess: config.DefaultPath looks next to the
  327. // executable, and a package that installs a default config there while systemd starts
  328. // the daemon with another will make it the wrong guess. It is still the only answer
  329. // that involves no searching. A guess that cannot be read is not an error, it is no
  330. // guess: a member of the socket's group cannot read a root daemon's config, and is
  331. // answered by the default path, where the daemon serves unless that config moved it.
  332. // A config the operator named is theirs to make readable.
  333. func pathFromConfig(configFlag string) (string, error) {
  334. return socketFromConfigFile(configFlag, configFlag != "")
  335. }
  336. func socketFromConfigFile(configPath string, named bool) (string, error) {
  337. if !named {
  338. p, err := config.DefaultPath()
  339. if err != nil {
  340. return localapi.DefaultSocketPath, nil
  341. }
  342. configPath = p
  343. }
  344. // The config is loaded only to find the socket path. Loading logs lines that belong
  345. // to the daemon, not to someone running a read only command, and mixing them into
  346. // the CLI's output would corrupt a -json result, so they are discarded.
  347. c := config.NewC(slog.New(slog.DiscardHandler))
  348. if err := c.Load(configPath); err != nil {
  349. if !named {
  350. return localapi.DefaultSocketPath, nil
  351. }
  352. // The operating system's error, as it came: permission denied on a root daemon's
  353. // config says what to do.
  354. return "", fmt.Errorf("nebula: could not read %s: %w", configPath, err)
  355. }
  356. // A daemon running this config serves no socket; dialing the path would only say
  357. // that nothing is there.
  358. if !localapi.ControlEnabled(c) {
  359. return "", fmt.Errorf("nebula: the control socket is disabled in %s by %s", configPath, localapi.CtlEnabledKey)
  360. }
  361. // The same call the daemon makes, so the two agree on the path and on what makes a
  362. // path unusable.
  363. p, err := localapi.SocketPathFromConfig(c)
  364. if err != nil {
  365. return "", fmt.Errorf("nebula: %w", err)
  366. }
  367. return p, nil
  368. }