wire.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. // Endpoints is every local API Wire brought up, with the one Stop the daemon's exit
  10. // path calls.
  11. type Endpoints struct {
  12. Control *Server
  13. Info *InfoServer
  14. }
  15. // Stop closes both endpoints and removes their sockets.
  16. func (e *Endpoints) Stop() {
  17. e.Info.Stop()
  18. e.Control.Stop()
  19. }
  20. // Wire brings up the local APIs for a running nebula and keeps them following the
  21. // config across reloads: the control socket unless the config turns it off, and the
  22. // info API when the config asks for it.
  23. //
  24. // Every entry point that runs nebula as a daemon calls this, so the endpoints behave the
  25. // same whether nebula was started from a shell or by a service manager. It deliberately
  26. // does not live in nebula.Main: the control socket is on by default, so an embedder
  27. // inside an app sandbox would have to know to turn off a socket it cannot use. Wiring it
  28. // at the daemon entry points means embedders get the command registry and no listener.
  29. //
  30. // Every failure below is a warning, bar one. Nebula is expected to keep forwarding
  31. // packets whether or not anyone can reach it; but a name that another process holds is
  32. // not a misconfiguration, it is a process that will be answering nebula ctl in the
  33. // daemon's place, and that is logged as an error.
  34. //
  35. // Call it after Control.Start, so the listeners only exist once the interface is up.
  36. func Wire(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *Endpoints {
  37. l = l.With("subsystem", "localapi")
  38. return &Endpoints{
  39. Control: wireControl(ctx, l, c, reg),
  40. Info: wireInfo(ctx, l, c, reg),
  41. }
  42. }
  43. // sharedSocketPath reports the path the config gives to both the control socket and the
  44. // info API, when it gives them one path. The two speak different protocols, so neither
  45. // binds it: each side says so and leaves it, and the operator fixes the config. The
  46. // configured paths are compared, not the ones being served, so the case is caught on
  47. // whichever side reconciles first, when the other side's bind has failed, and when a
  48. // reload moves one socket onto the other's path.
  49. func sharedSocketPath(c *config.C) (string, bool) {
  50. if !ControlEnabled(c) {
  51. return "", false
  52. }
  53. ctl, err := SocketPathFromConfig(c)
  54. if err != nil {
  55. return "", false
  56. }
  57. info, err := infoConfigFrom(c)
  58. if err != nil || !info.Enabled || info.Path != ctl {
  59. return "", false
  60. }
  61. return ctl, true
  62. }
  63. // wireControl brings up the control socket and keeps it pointed at the configured path.
  64. func wireControl(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *Server {
  65. srv := NewServer(l, reg)
  66. // Registered before the first attempt, so that a path or bind that is bad at startup
  67. // can be fixed with a config edit and a reload, the same as one that goes bad later,
  68. // and so that a HUP landing in between is harmless: Rebind on a server already at
  69. // the path is a no-op.
  70. reconcile := followControlConfig(ctx, l, c, srv)
  71. reconcile(c)
  72. if !ControlEnabled(c) {
  73. l.Info("Local control socket is disabled", "key", CtlEnabledKey)
  74. }
  75. return srv
  76. }
  77. // followControlConfig registers the reload callback that keeps the socket where
  78. // local_api.control.socket says it is, moving it when the key changes and bringing one
  79. // up while there is none, and returns it for the first attempt to call.
  80. //
  81. // The key is reloadable because the client finds the socket by reading the config: if the
  82. // two could disagree, the file on disk would name a socket that is not there.
  83. //
  84. // Every reload is reconciled rather than gated on the key having changed. Rebind on a
  85. // server already at the path is a no-op, so that costs nothing; and a bind that failed
  86. // for a reason outside the config, a runtime directory that was not there yet, is fixed
  87. // outside the config too, so the reload that follows the fix is a plain one. That holds
  88. // for a move as much as for the first bind: a daemon left on its old path by a move that
  89. // could not bind is one the client, reading the new path from the same file, cannot
  90. // reach, and the operator's fix is the directory and a reload, not an edit to an
  91. // unrelated line to make the key read as changed.
  92. func followControlConfig(ctx context.Context, l *slog.Logger, c *config.C, srv *Server) func(*config.C) {
  93. reconcile := func(c *config.C) {
  94. if !ControlEnabled(c) {
  95. if srv.Path() != "" {
  96. srv.Stop()
  97. l.Info("Stopped the local control socket, it is disabled in the reloaded config")
  98. }
  99. return
  100. }
  101. want, err := SocketPathFromConfig(c)
  102. if err != nil {
  103. if srv.Path() == "" {
  104. l.Warn("Could not determine the local control socket path, control commands will not be available", "error", err)
  105. } else {
  106. l.Warn("Ignoring unusable local control socket path from reloaded config", "error", err)
  107. }
  108. return
  109. }
  110. acc, err := controlAccessFrom(c)
  111. if err != nil {
  112. if srv.Path() == "" {
  113. l.Warn("Local control socket is not available, its group is unusable", "error", err)
  114. } else {
  115. l.Warn("Ignoring unusable local control socket group from reloaded config, keeping the current socket", "error", err)
  116. }
  117. return
  118. }
  119. if p, shared := sharedSocketPath(c); shared {
  120. l.Warn("Local control socket is configured on the info API's socket path and will not bind it",
  121. "path", p, "current", srv.Path(), "fix", "set "+InfoSocketKey+" to another path")
  122. return
  123. }
  124. reportRebind(l, "local control socket", srv.Path(), want, srv.Rebind(ctx, want, acc))
  125. }
  126. onReload(l, c, "local control socket", reconcile)
  127. return reconcile
  128. }
  129. // onReload registers fn to run on every reload. The callback recovers from its own
  130. // panics rather than relying on the config package, whose callback loop has no isolation
  131. // of its own: a panic there would take the daemon down and skip every callback queued
  132. // behind this one.
  133. func onReload(l *slog.Logger, c *config.C, what string, fn func(*config.C)) {
  134. c.RegisterReloadCallback(func(c *config.C, _ *config.Reload) {
  135. defer func() {
  136. if r := recover(); r != nil {
  137. l.Error("Recovered from panic reloading the "+what, "panic", r)
  138. }
  139. }()
  140. fn(c)
  141. })
  142. }
  143. // reportRebind logs what a Rebind did beyond the listening line the bind itself writes:
  144. // nothing for an endpoint left where it was, a move with both paths, and a failure with
  145. // what the operator is left with. old is the path served before the call, "" for none,
  146. // and want the one asked for.
  147. func reportRebind(l *slog.Logger, what, old, want string, err error) {
  148. switch {
  149. case err == nil:
  150. if old != "" && old != want {
  151. l.Info("Moved the "+what, "from", old, "to", want)
  152. }
  153. case old == "":
  154. bindFailure(l, err)("Could not start the "+what+", it will not be available until a reload binds it", "error", err, "path", want)
  155. default:
  156. bindFailure(l, err)("Could not move the "+what+", keeping the current one",
  157. "error", err, "current", old, "requested", want,
  158. )
  159. }
  160. }
  161. // wireInfo brings up the info API when the config asks for one and follows the config
  162. // across reloads: enabling it, disabling it, moving its socket and changing its mode.
  163. //
  164. // Every reload is reconciled rather than gated on the section having changed. Rebind on
  165. // a server already where the config says is a no-op, so that costs nothing, and a bind
  166. // that failed for a reason outside the config, a runtime directory that was not there
  167. // yet, is retried by the plain reload that follows the fix, the same as the control
  168. // socket's. A section that does not parse keeps whatever is running: the operator is
  169. // told, and the programs relying on the endpoint are not cut off by a typo.
  170. func wireInfo(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *InfoServer {
  171. srv := NewInfoServer(l, reg)
  172. // Said once per process rather than on every reload: it is a platform limit, not
  173. // something the next reload could fix.
  174. saidUnsupported := false
  175. reconcile := func(c *config.C) {
  176. cfg, err := infoConfigFrom(c)
  177. if err != nil {
  178. if srv.Path() == "" {
  179. l.Warn("Local info API is not available, its config is unusable", "error", err)
  180. } else {
  181. l.Warn("Ignoring unusable local info API config, keeping the current endpoint", "error", err)
  182. }
  183. return
  184. }
  185. if !cfg.Enabled {
  186. if srv.Path() != "" {
  187. srv.Stop()
  188. l.Info("Stopped the local info API, it is disabled in the reloaded config")
  189. }
  190. return
  191. }
  192. if !infoSupported {
  193. if !saidUnsupported {
  194. l.Warn("Local info API is enabled in the config but is not served on this platform yet", "key", InfoEnabledKey)
  195. saidUnsupported = true
  196. }
  197. return
  198. }
  199. // The two endpoints speak different protocols; one socket cannot be both. Caught
  200. // here, from the configured paths, because listen's own answer, that the socket is
  201. // in use by another nebula, would send the operator looking for a second process.
  202. if p, shared := sharedSocketPath(c); shared {
  203. if cur := srv.Path(); cur != "" {
  204. l.Warn("Ignoring the local info API's new socket path, it is also the control socket's; keeping the current endpoint",
  205. "path", p, "current", cur, "fix", "set "+InfoSocketKey+" to another path")
  206. } else {
  207. 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")
  208. }
  209. return
  210. }
  211. old := srv.Path()
  212. reportRebind(l, "local info API", old, cfg.Path, srv.Rebind(ctx, cfg.Path, cfg.Access))
  213. }
  214. // Registered before the first attempt, as the control socket's is; see wireControl.
  215. onReload(l, c, "local info API", reconcile)
  216. reconcile(c)
  217. return srv
  218. }
  219. // errEndpointHeld is returned by listen when the endpoint's name is already held by
  220. // some other process, which nebula will not serve alongside: whoever holds it would be
  221. // answering nebula ctl. It is only raised on windows, where the default name can be
  222. // held by another administrator's process and an override outside the protected
  223. // namespace by any account's; on unix a path that is still being served is an earlier
  224. // nebula, which clearStale reports as in use.
  225. var errEndpointHeld = errors.New("the local control socket's name is held by another process")
  226. // bindFailure picks the level a failed bind is logged at. Nearly every failure is the
  227. // operator's environment, a missing runtime directory or a bad path, and a warning; a
  228. // name held by another process is logged as an error, since nothing else will say that
  229. // something is sitting where the daemon should be.
  230. func bindFailure(l *slog.Logger, err error) func(msg string, args ...any) {
  231. if errors.Is(err, errEndpointHeld) {
  232. return l.Error
  233. }
  234. return l.Warn
  235. }