info.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. package localapi
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "log/slog"
  10. "maps"
  11. "net"
  12. "net/http"
  13. "net/url"
  14. "os"
  15. "path"
  16. "runtime/debug"
  17. "slices"
  18. "strconv"
  19. "time"
  20. "github.com/slackhq/nebula/commands"
  21. "github.com/slackhq/nebula/config"
  22. "golang.org/x/net/netutil"
  23. )
  24. // The info API's config keys. It is off until InfoEnabledKey is true and InfoSocketKey
  25. // names a socket; nothing is derived, because the programs that will dial it have to be
  26. // told where it is anyway.
  27. const (
  28. InfoEnabledKey = "local_api.info.enabled"
  29. InfoSocketKey = "local_api.info.socket"
  30. InfoModeKey = "local_api.info.socket_mode"
  31. InfoGroupKey = "local_api.info.group"
  32. )
  33. const (
  34. // defaultInfoMode is the socket's mode when the config names neither a mode nor a
  35. // group: the daemon's user alone, so that enabling the API does not open it to
  36. // anyone until the operator says who. Naming a group makes it groupSocketMode.
  37. defaultInfoMode os.FileMode = 0600
  38. // infoMaxConns bounds the connections held open at once. Beyond it the kernel
  39. // queues, which is what an HTTP client expects of a busy server; the control socket
  40. // refuses instead because its clients are people at a prompt.
  41. infoMaxConns = 64
  42. infoWriteTimeout = 30 * time.Second
  43. infoIdleTimeout = 60 * time.Second
  44. infoMaxHeaderBytes = 64 * 1024
  45. )
  46. // The read deadlines are variables so a test can shorten them. infoReadTimeout bounds
  47. // the whole request, not only its headers: a request is a GET with a query string and
  48. // the handlers never read a body, but net/http drains an unread body after the handler
  49. // returns, and without a deadline on that read a client sending a body a byte at a time
  50. // would hold its connection slot for as long as it liked, and sixty-four of them would
  51. // hold every slot. A body is refused as well, see refuseBody, but net/http drains it
  52. // before it closes, so this deadline is what ends a client that announced a body and
  53. // never sent it.
  54. var (
  55. infoReadHeaderTimeout = 10 * time.Second
  56. infoReadTimeout = 15 * time.Second
  57. )
  58. // servedOnInfo decides what the info API will run: the commands that report identity
  59. // and nothing else, which the commands declare about themselves. Filter adds help to
  60. // the result, so a client can ask what is served; help on a filtered registry describes
  61. // only that registry.
  62. func servedOnInfo(c *commands.Command) bool {
  63. return c.Identity
  64. }
  65. // notServedOnInfo explains a refusal, naming the surfaces that will run the command.
  66. const notServedOnInfo = "the info API serves identity lookups only; the ssh debug server and nebula ctl serve the rest"
  67. // InfoServer serves the identity commands over HTTP, on a socket an operator may open to
  68. // other users on the host.
  69. //
  70. // It is the second local API, and the opposite of the control socket in who it is for.
  71. // The control socket is for the user who runs nebula, serves nearly everything and needs
  72. // no configuration. This one is for every other program on the machine, a web server
  73. // that wants to know who is on the other end of a connection arriving over the overlay,
  74. // say, and it is off until an operator names a socket and a mode for it. What it serves
  75. // is decided by the commands themselves, Command.Identity: the identity a certificate
  76. // asserts and nothing operational, so opening it to a group of services, or to everyone
  77. // on the host, gives away nothing the overlay did not already tell them.
  78. //
  79. // HTTP, so that no client library is needed. GET /v1/<command>?<flag>=<value> runs a
  80. // command with its options as the query string and answers with the command's JSON, the
  81. // same value the control socket and -json produce. GET /v1 describes what is served. A
  82. // refusal is a 4xx with {"error": reason}: 404 when the command said the thing asked
  83. // about does not exist (commands.NotFound), 400 for a parameter or an argument the
  84. // command would not take, 403 for a command this surface withholds.
  85. type InfoServer struct {
  86. endpoint
  87. src *commands.Registry
  88. }
  89. // NewInfoServer returns an InfoServer that will serve the identity commands in reg. reg
  90. // is kept rather than filtered here, for the same reason the control socket keeps its
  91. // registry: a command registered on it later is served too.
  92. func NewInfoServer(l *slog.Logger, reg *commands.Registry) *InfoServer {
  93. return &InfoServer{endpoint: endpoint{l: l, name: "local info API"}, src: reg}
  94. }
  95. // registry returns the commands this surface serves, filtered at the time of the call.
  96. func (s *InfoServer) registry() *commands.Registry {
  97. return s.src.Filter(servedOnInfo, notServedOnInfo)
  98. }
  99. // Rebind brings the server up at path with mode, moves it there, or, when it is already
  100. // there, applies a changed mode; endpoint.rebindLocked says what that promises. It is
  101. // the one way the server comes up, so Wire's reload callback and its first attempt are
  102. // the same call.
  103. func (s *InfoServer) Rebind(ctx context.Context, path string, acc Access) error {
  104. s.mu.Lock()
  105. defer s.mu.Unlock()
  106. return s.rebindLocked(ctx, path, acc, func(ln net.Listener) io.Closer {
  107. srv := &http.Server{
  108. Handler: s.handler(),
  109. ReadHeaderTimeout: infoReadHeaderTimeout,
  110. ReadTimeout: infoReadTimeout,
  111. WriteTimeout: infoWriteTimeout,
  112. IdleTimeout: infoIdleTimeout,
  113. MaxHeaderBytes: infoMaxHeaderBytes,
  114. ErrorLog: slog.NewLogLogger(s.l.Handler(), slog.LevelDebug),
  115. }
  116. s.l.Info("Local info API listening", "path", path, "access", acc.String())
  117. go s.serve(srv, ln)
  118. // Closing the server closes the listener Serve was handed, and a Serve that has
  119. // not started yet closes it on its way out, so the node is never left bound by a
  120. // listener nothing is accepting from. Requests in flight are cut off rather than
  121. // waited for: a reload must not hang on a slow client, and the answer is a lookup
  122. // the client can simply repeat.
  123. return srv
  124. })
  125. }
  126. // serve runs the HTTP server on ln until it is closed. Only an error other than the
  127. // close is worth a line: Stop and Rebind are the two things that close it.
  128. func (s *InfoServer) serve(srv *http.Server, ln net.Listener) {
  129. err := srv.Serve(netutil.LimitListener(ln, infoMaxConns))
  130. if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) {
  131. s.l.Error("Local info API stopped serving", "error", err)
  132. }
  133. }
  134. // handler builds the routes. Every answer is JSON, the misses included, so a client
  135. // never has to parse net/http's text for a path or method it did not expect. That
  136. // includes a path ServeMux would clean and redirect, /v1/../v1/x or //v1/x, which is
  137. // refused outright: nothing legitimate sends one, and a redirect is one more thing a
  138. // client would have to follow.
  139. func (s *InfoServer) handler() http.Handler {
  140. mux := http.NewServeMux()
  141. mux.HandleFunc("GET /v1", s.handleDescribe)
  142. mux.HandleFunc("GET /v1/{command}", s.handleCommand)
  143. mux.HandleFunc("/v1", methodNotAllowed)
  144. mux.HandleFunc("/v1/{command}", methodNotAllowed)
  145. mux.HandleFunc("/", notFoundRoute)
  146. return s.recovering(refuseBody(mux))
  147. }
  148. // refuseBody answers a request that carries a body with 400, before any handler sees
  149. // it, and has the connection closed after the reply rather than kept for another
  150. // request. Nothing here reads a body; one is a mistake, and the client is told what to
  151. // do instead. net/http still drains what the body it announced it would send, up to the
  152. // read deadline, so the deadline is what bounds a client that never sends it.
  153. func refuseBody(next http.Handler) http.Handler {
  154. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  155. if r.ContentLength != 0 || len(r.TransferEncoding) > 0 {
  156. w.Header().Set("Connection", "close")
  157. writeJSONError(w, http.StatusBadRequest, "the info API takes no request body; put the command's parameters in the query string")
  158. return
  159. }
  160. next.ServeHTTP(w, r)
  161. })
  162. }
  163. // recovering turns a panic in a command into a 500 and a log line rather than a dropped
  164. // connection, which is all net/http would offer, and keeps the daemon up, which is what
  165. // matters: this surface is reachable by whoever the operator opened it to. The handlers
  166. // produce their whole answer before writing any of it, so a panic in Run always finds
  167. // the response unwritten.
  168. func (s *InfoServer) recovering(next http.Handler) http.Handler {
  169. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  170. defer func() {
  171. if rec := recover(); rec != nil {
  172. s.l.Error("Recovered from panic serving a local info API request",
  173. "panic", rec,
  174. "path", r.URL.Path,
  175. "query", r.URL.RawQuery,
  176. "stack", string(debug.Stack()),
  177. )
  178. writeJSONError(w, http.StatusInternalServerError, "command failed")
  179. }
  180. }()
  181. if path.Clean(r.URL.Path) != r.URL.Path {
  182. notFoundRoute(w, r)
  183. return
  184. }
  185. next.ServeHTTP(w, r)
  186. })
  187. }
  188. // handleDescribe answers GET /v1 with the schema of every command served here, the same
  189. // document the control socket's describe returns for its own surface.
  190. func (s *InfoServer) handleDescribe(w http.ResponseWriter, _ *http.Request) {
  191. body, err := json.Marshal(s.registry().Schema())
  192. if err != nil {
  193. writeJSONError(w, http.StatusInternalServerError, err.Error())
  194. return
  195. }
  196. writeJSON(w, http.StatusOK, body)
  197. }
  198. // handleCommand answers GET /v1/<command>: the query string carries the command's
  199. // parameters, and the response body is the command's JSON. A request body is never
  200. // read. It is the control socket's serve with HTTP's vocabulary for the outcomes.
  201. func (s *InfoServer) handleCommand(w http.ResponseWriter, r *http.Request) {
  202. name := commands.Name(r.PathValue("command"))
  203. cmd, err := s.registry().Resolve(name)
  204. if err != nil {
  205. // A command this surface withholds is a different answer from one that does not
  206. // exist, and Resolve says which; only the status is picked here.
  207. status := http.StatusNotFound
  208. var withheld *commands.WithheldError
  209. if errors.As(err, &withheld) {
  210. status = http.StatusForbidden
  211. }
  212. writeJSONError(w, status, err.Error())
  213. return
  214. }
  215. params, format, err := queryParams(r.URL.Query())
  216. if err != nil {
  217. writeJSONError(w, http.StatusBadRequest, err.Error())
  218. return
  219. }
  220. res, err := commands.Invoke(commands.Request{Command: cmd, Params: params})
  221. if err != nil {
  222. var pe *commands.ParamError
  223. if errors.As(err, &pe) {
  224. writeJSONError(w, http.StatusBadRequest, err.Error())
  225. return
  226. }
  227. // Run failed. The sentence is for the operator's log; the caller, who may be
  228. // anyone the operator admitted, is told that it failed and no more.
  229. s.l.Warn("Local info API command failed", "command", name, "error", err)
  230. writeJSONError(w, http.StatusInternalServerError, "command failed")
  231. return
  232. }
  233. // A command that acts does so before this surface's one reply, whose body is the
  234. // outcome; see Command.Then.
  235. if cmd.Then != nil {
  236. res = cmd.Then()
  237. }
  238. if res.Empty() {
  239. w.WriteHeader(http.StatusNoContent)
  240. return
  241. }
  242. if !res.OK {
  243. status := http.StatusBadRequest
  244. if res.NotFound {
  245. status = http.StatusNotFound
  246. }
  247. writeJSONError(w, status, res.Reason)
  248. return
  249. }
  250. // The same rendering function every surface uses, so the value here is byte for
  251. // byte what -json prints at an ssh prompt.
  252. buf := &bytes.Buffer{}
  253. if err := commands.Render(cmd, res, format, buf); err != nil {
  254. s.l.Warn("Local info API could not render a result", "command", name, "error", err)
  255. writeJSONError(w, http.StatusInternalServerError, "command failed")
  256. return
  257. }
  258. writeJSON(w, http.StatusOK, buf.Bytes())
  259. }
  260. // queryParams reads a query string as a command's parameters. Each key is a flag of the
  261. // command, by name, in the form the control socket takes them; one given twice is
  262. // refused rather than silently picking either. The presentation flags are read as a
  263. // command line reads them, and set apart: the answer is JSON regardless, and pretty
  264. // indents it.
  265. func queryParams(q url.Values) (map[string]string, commands.Format, error) {
  266. var params, presentation map[string]string
  267. for _, k := range slices.Sorted(maps.Keys(q)) {
  268. vs := q[k]
  269. if len(vs) > 1 {
  270. return nil, commands.FormatUnset, fmt.Errorf("parameter %q was given more than once", k)
  271. }
  272. into := &params
  273. if k == commands.FlagJSON || k == commands.FlagPretty {
  274. into = &presentation
  275. }
  276. if *into == nil {
  277. *into = map[string]string{}
  278. }
  279. (*into)[k] = vs[0]
  280. }
  281. format, err := commands.PresentationFormat(presentation)
  282. if err != nil {
  283. return nil, commands.FormatUnset, err
  284. }
  285. if format == commands.FormatUnset {
  286. format = commands.FormatJSON
  287. }
  288. return params, format, nil
  289. }
  290. func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
  291. w.Header().Set("Allow", http.MethodGet)
  292. writeJSONError(w, http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed; the info API is read only, use GET", r.Method))
  293. }
  294. func notFoundRoute(w http.ResponseWriter, r *http.Request) {
  295. writeJSONError(w, http.StatusNotFound, fmt.Sprintf("no such path %s; GET /v1 lists the commands and GET /v1/<command> runs one", r.URL.Path))
  296. }
  297. func writeJSON(w http.ResponseWriter, status int, body []byte) {
  298. w.Header().Set("Content-Type", "application/json")
  299. w.Header().Set("X-Content-Type-Options", "nosniff")
  300. w.Header().Set("Content-Length", strconv.Itoa(len(body)))
  301. w.WriteHeader(status)
  302. _, _ = w.Write(body)
  303. }
  304. func writeJSONError(w http.ResponseWriter, status int, msg string) {
  305. body, _ := json.Marshal(map[string]string{"error": msg})
  306. writeJSON(w, status, append(body, '\n'))
  307. }
  308. // infoConfig is the info API section of a config, read and checked.
  309. type infoConfig struct {
  310. Enabled bool
  311. Path string
  312. Access Access
  313. }
  314. // infoConfigFrom reads the info API section. A disabled section needs nothing else to be
  315. // right; an enabled one needs a socket path, a group that exists if it names one, and
  316. // a mode that parses if it names one. A group with no mode means groupSocketMode, and a
  317. // mode that gives a named group nothing is refused: it would be a group that cannot
  318. // connect, which is not what naming one asks for.
  319. func infoConfigFrom(c *config.C) (infoConfig, error) {
  320. cfg := infoConfig{Enabled: c.GetBool(InfoEnabledKey, false)}
  321. if !cfg.Enabled {
  322. return cfg, nil
  323. }
  324. p := c.GetString(InfoSocketKey, "")
  325. if p == "" {
  326. return cfg, fmt.Errorf("%s is true but %s is not set; the info API needs a socket path", InfoEnabledKey, InfoSocketKey)
  327. }
  328. path, err := configuredSocketPath(InfoSocketKey, p)
  329. if err != nil {
  330. return cfg, err
  331. }
  332. cfg.Path = path
  333. group, err := groupFromConfig(c, InfoGroupKey)
  334. if err != nil {
  335. return cfg, err
  336. }
  337. def := defaultInfoMode
  338. if group != nil {
  339. def = groupSocketMode
  340. }
  341. mode, err := parseSocketMode(c.Get(InfoModeKey), def)
  342. if err != nil {
  343. return cfg, fmt.Errorf("%s: %w", InfoModeKey, err)
  344. }
  345. if group != nil && mode&0070 == 0 {
  346. return cfg, fmt.Errorf("%s %s gives group %s no access to the socket; use \"%04o\" or leave the mode unset", InfoModeKey, modeString(mode), group.Name, mode|0060)
  347. }
  348. cfg.Access = Access{Mode: mode, Group: group}
  349. return cfg, nil
  350. }
  351. // parseSocketMode reads an octal mode written as a string. A bare 0660 in yaml is an
  352. // integer, and which integer depends on the parser (octal 432 under YAML 1.1, decimal
  353. // 660 under 1.2), so a number is refused with the fix rather than quietly applied as
  354. // whichever mode its digits happen to spell in octal.
  355. func parseSocketMode(raw any, def os.FileMode) (os.FileMode, error) {
  356. switch v := raw.(type) {
  357. case nil:
  358. return def, nil
  359. case string:
  360. mode, err := strconv.ParseUint(v, 8, 32)
  361. if err != nil || mode&^0777 != 0 {
  362. return 0, fmt.Errorf("%q is not an octal file mode such as \"0660\"", v)
  363. }
  364. return os.FileMode(mode), nil
  365. default:
  366. return 0, fmt.Errorf("must be a quoted octal string such as \"0660\", not the number %v; yaml reads a bare mode as a decimal or octal integer depending on the parser", v)
  367. }
  368. }