wire.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package localapi
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "github.com/slackhq/nebula/commands"
  7. "github.com/slackhq/nebula/config"
  8. )
  9. // Wire brings up the local control socket for a running nebula, unless the config turns
  10. // it off, and keeps it following the config across reloads.
  11. //
  12. // Every entry point that runs nebula as a daemon calls this, so the endpoint behaves the
  13. // same whether nebula was started from a shell or by a service manager. It deliberately
  14. // does not live in nebula.Main: the endpoint is on by default, so an embedder inside an
  15. // app sandbox would have to know to turn off a socket it cannot use. Wiring it at the
  16. // daemon entry points means embedders get the command registry and no listener.
  17. //
  18. // Every failure below is a warning, bar one. Nebula is expected to keep forwarding
  19. // packets whether or not anyone can reach it; but a name that another process holds is
  20. // not a misconfiguration, it is a process that will be answering nebula ctl in the
  21. // daemon's place, and that is logged as an error.
  22. //
  23. // Call it after Control.Start, so the listener only exists once the interface is up.
  24. func Wire(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *Server {
  25. l = l.With("subsystem", "localapi")
  26. srv := NewServer(l, reg)
  27. // Registered before the first attempt, so that a path or bind that is bad at startup
  28. // can be fixed with a config edit and a reload, the same as one that goes bad later,
  29. // and so that a HUP landing in between is harmless: Rebind on a server already at
  30. // the path is a no-op.
  31. reconcile := followControlConfig(ctx, l, c, srv)
  32. reconcile(c)
  33. if !ControlEnabled(c) {
  34. l.Info("Local control socket is disabled", "key", CtlEnabledKey)
  35. }
  36. return srv
  37. }
  38. // followControlConfig registers the reload callback that keeps the socket where
  39. // local_api.control.socket says it is, moving it when the key changes and bringing one
  40. // up while there is none, and returns it for the first attempt to call.
  41. //
  42. // The key is reloadable because the client finds the socket by reading the config: if the
  43. // two could disagree, the file on disk would name a socket that is not there.
  44. //
  45. // Every reload is reconciled rather than gated on the key having changed. Rebind on a
  46. // server already at the path is a no-op, so that costs nothing; and a bind that failed
  47. // for a reason outside the config, a runtime directory that was not there yet, is fixed
  48. // outside the config too, so the reload that follows the fix is a plain one. That holds
  49. // for a move as much as for the first bind: a daemon left on its old path by a move that
  50. // could not bind is one the client, reading the new path from the same file, cannot
  51. // reach, and the operator's fix is the directory and a reload, not an edit to an
  52. // unrelated line to make the key read as changed.
  53. //
  54. // The callback recovers from its own panics rather than relying on the config package,
  55. // whose callback loop has no isolation of its own. A panic there would take the daemon
  56. // down and skip every callback queued behind this one.
  57. func followControlConfig(ctx context.Context, l *slog.Logger, c *config.C, srv *Server) func(*config.C) {
  58. reconcile := func(c *config.C) {
  59. if !ControlEnabled(c) {
  60. if srv.Path() != "" {
  61. srv.Stop()
  62. l.Info("Stopped the local control socket, it is disabled in the reloaded config")
  63. }
  64. return
  65. }
  66. want, err := SocketPathFromConfig(c)
  67. if err != nil {
  68. if srv.Path() == "" {
  69. l.Warn("Could not determine the local control socket path, control commands will not be available", "error", err)
  70. } else {
  71. l.Warn("Ignoring unusable local control socket path from reloaded config", "error", err)
  72. }
  73. return
  74. }
  75. acc, err := controlAccessFrom(c)
  76. if err != nil {
  77. if srv.Path() == "" {
  78. l.Warn("Local control socket is not available, its group is unusable", "error", err)
  79. } else {
  80. l.Warn("Ignoring unusable local control socket group from reloaded config, keeping the current socket", "error", err)
  81. }
  82. return
  83. }
  84. reportRebind(l, srv.Path(), want, srv.Rebind(ctx, want, acc))
  85. }
  86. c.RegisterReloadCallback(func(c *config.C, _ *config.Reload) {
  87. defer func() {
  88. if r := recover(); r != nil {
  89. l.Error("Recovered from panic reloading the local control socket", "panic", r)
  90. }
  91. }()
  92. reconcile(c)
  93. })
  94. return reconcile
  95. }
  96. // reportRebind logs what a Rebind did beyond the listening line the bind itself writes:
  97. // nothing for a server left where it was, a move with both paths, and a failure with
  98. // what the operator is left with. old is the path served before the call, "" for none,
  99. // and want the one asked for.
  100. func reportRebind(l *slog.Logger, old, want string, err error) {
  101. switch {
  102. case err == nil:
  103. if old != "" && old != want {
  104. l.Info("Moved the local control socket", "from", old, "to", want)
  105. }
  106. case old == "":
  107. bindFailure(l, err)("Could not start the local control socket, control commands will not be available until a reload binds it", "error", err, "path", want)
  108. default:
  109. bindFailure(l, err)("Could not move the local control socket, keeping the current one",
  110. "error", err, "current", old, "requested", want,
  111. )
  112. }
  113. }
  114. // errEndpointHeld is returned by listen when the endpoint's name is already held by
  115. // some other process, which nebula will not serve alongside: whoever holds it would be
  116. // answering nebula ctl. It is only raised on windows, where the default name can be
  117. // held by another administrator's process and an override outside the protected
  118. // namespace by any account's; on unix a path that is still being served is an earlier
  119. // nebula, which clearStale reports as in use.
  120. var errEndpointHeld = errors.New("the local control socket's name is held by another process")
  121. // bindFailure picks the level a failed bind is logged at. Nearly every failure is the
  122. // operator's environment, a missing runtime directory or a bad path, and a warning; a
  123. // name held by another process is logged as an error, since nothing else will say that
  124. // something is sitting where the daemon should be.
  125. func bindFailure(l *slog.Logger, err error) func(msg string, args ...any) {
  126. if errors.Is(err, errEndpointHeld) {
  127. return l.Error
  128. }
  129. return l.Warn
  130. }