1
0

info_api_server.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. package nebula
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io/fs"
  8. "log/slog"
  9. "net"
  10. "net/http"
  11. "net/netip"
  12. "os"
  13. "path/filepath"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "sync/atomic"
  18. "time"
  19. "github.com/slackhq/nebula/cert"
  20. "github.com/slackhq/nebula/config"
  21. )
  22. // infoAPIServer is a small http+json listener on a unix socket that lets other
  23. // programs on this machine resolve a vpn address to its certificate identity (name, groups, networks)
  24. // for making authorization decisions. Lifecycle works like statsServer: the constructor wires the
  25. // reload callback, reload records config, Start runs the runtime, Stop tears it down
  26. type infoAPIServer struct {
  27. l *slog.Logger
  28. ctx context.Context
  29. hostMap *HostMap
  30. pki *PKI
  31. // enabled mirrors `info_api.enabled` so callers of Start don't need to know the gating rules
  32. enabled atomic.Bool
  33. runMu sync.Mutex
  34. runCfg *infoAPIConfig
  35. run *infoAPIRuntime // non-nil while a runtime is live
  36. }
  37. // infoAPIRuntime is the live state owned by a single Start invocation. Stop and Start's exit path
  38. // use pointer equality to tell "my runtime" apart from one that replaced it after a reload
  39. type infoAPIRuntime struct {
  40. server *http.Server
  41. listener net.Listener
  42. }
  43. // infoAPIConfig is a snapshot of the info_api config section, comparable with == so reload can
  44. // detect "no change" cheaply
  45. type infoAPIConfig struct {
  46. enabled bool
  47. listen string // raw config value, for error messages
  48. addr string // unix socket path
  49. // file mode applied to the unix socket after bind
  50. socketMode fs.FileMode
  51. }
  52. // newInfoAPIServerFromConfig builds a infoAPIServer and applies the initial config. The reload
  53. // callback is registered first so a SIGHUP can later enable, fix, or disable the listener even if
  54. // the initial config was bad. Nothing binds until Start, so config tests are side effect free.
  55. // A bad config is logged rather than returned: it must not stop nebula from starting, the feature
  56. // just stays disabled until a reload provides a valid config
  57. func newInfoAPIServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI, hostMap *HostMap, c *config.C) *infoAPIServer {
  58. h := &infoAPIServer{
  59. l: l,
  60. ctx: ctx,
  61. hostMap: hostMap,
  62. pki: pki,
  63. }
  64. c.RegisterReloadCallback(func(c *config.C) {
  65. if err := h.reload(c, false); err != nil {
  66. h.l.Warn("Failed to reload info API from config", "error", err)
  67. }
  68. })
  69. if err := h.reload(c, true); err != nil {
  70. h.l.Warn("Failed to apply info API config; it will stay disabled until the config is fixed and reloaded", "error", err)
  71. }
  72. return h
  73. }
  74. // reload records the latest config. The initial call only records it, Control.Start launches the
  75. // first runtime via infoAPIStart. Later calls reconcile the running listener with the new config:
  76. // enable, disable, or restart when the listen config changed
  77. func (h *infoAPIServer) reload(c *config.C, initial bool) error {
  78. newCfg, err := loadInfoAPIConfig(c)
  79. if err != nil {
  80. return err
  81. }
  82. h.runMu.Lock()
  83. sameCfg := h.runCfg != nil && *h.runCfg == newCfg
  84. h.runCfg = &newCfg
  85. running := h.run != nil
  86. h.runMu.Unlock()
  87. h.enabled.Store(newCfg.enabled)
  88. if initial || sameCfg {
  89. return nil
  90. }
  91. if running {
  92. h.Stop()
  93. }
  94. if newCfg.enabled {
  95. go h.Start()
  96. }
  97. return nil
  98. }
  99. // Start binds the listener from the latest config and serves until Stop is called or ctx fires.
  100. // Safe to call when disabled or already running (both no-op)
  101. func (h *infoAPIServer) Start() {
  102. if !h.enabled.Load() {
  103. return
  104. }
  105. h.runMu.Lock()
  106. if h.ctx.Err() != nil || h.run != nil || h.runCfg == nil {
  107. h.runMu.Unlock()
  108. return
  109. }
  110. cfg := *h.runCfg
  111. ln, err := h.listen(cfg)
  112. if err != nil {
  113. // drop the cached config so a SIGHUP with the same config retries the bind
  114. h.runCfg = nil
  115. h.runMu.Unlock()
  116. h.l.Error("Failed to start info API listener", "listen", cfg.listen, "error", err)
  117. return
  118. }
  119. mux := http.NewServeMux()
  120. mux.HandleFunc("GET /v1/host", h.handleHost)
  121. mux.HandleFunc("GET /v1/self", h.handleSelf)
  122. srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second}
  123. rt := &infoAPIRuntime{server: srv, listener: ln}
  124. h.run = rt
  125. h.runMu.Unlock()
  126. h.l.Info("Starting info API listener", "addr", ln.Addr())
  127. cleanExit := h.serve(srv, ln)
  128. // A Stop that raced our bind shut the server down before Serve could adopt the listener;
  129. // closing it again is harmless and guarantees a unix socket file gets unlinked
  130. _ = ln.Close()
  131. // Clear our runtime only if nothing has replaced it. Stop races through here too but leaves
  132. // h.run == nil, so the pointer check skips
  133. h.runMu.Lock()
  134. if h.run == rt {
  135. h.run = nil
  136. // an error exit leaves runCfg cached as if it were applied, drop it so a SIGHUP with the
  137. // same config re-triggers Start once the user fixes the underlying problem
  138. if !cleanExit {
  139. h.runCfg = nil
  140. }
  141. }
  142. h.runMu.Unlock()
  143. }
  144. // serve runs srv.Serve and ensures ctx cancellation unblocks it. Returns true if the listener
  145. // exited cleanly (Stop, ctx cancellation), false on an unexpected error
  146. func (h *infoAPIServer) serve(srv *http.Server, ln net.Listener) bool {
  147. // ctx cancellation triggers a server shutdown which in turn unblocks Serve, closing `done` on
  148. // exit keeps the watcher from outliving this call
  149. done := make(chan struct{})
  150. go func() {
  151. select {
  152. case <-h.ctx.Done():
  153. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  154. defer cancel()
  155. if err := srv.Shutdown(shutdownCtx); err != nil {
  156. h.l.Warn("Failed to shut down info API listener", "error", err)
  157. }
  158. case <-done:
  159. }
  160. }()
  161. defer close(done)
  162. err := srv.Serve(ln)
  163. if err == nil || errors.Is(err, http.ErrServerClosed) {
  164. return true
  165. }
  166. h.l.Error("Info API listener exited", "error", err)
  167. return false
  168. }
  169. // Stop tears down the active runtime, if any. Idempotent
  170. func (h *infoAPIServer) Stop() {
  171. h.runMu.Lock()
  172. rt := h.run
  173. h.run = nil
  174. h.runMu.Unlock()
  175. if rt == nil {
  176. return
  177. }
  178. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  179. defer cancel()
  180. if err := rt.server.Shutdown(shutdownCtx); err != nil {
  181. h.l.Warn("Failed to shut down info API listener", "error", err)
  182. }
  183. }
  184. // listen binds the configured unix socket. It also clears a stale socket file left by an unclean
  185. // exit and applies the configured file mode
  186. func (h *infoAPIServer) listen(cfg infoAPIConfig) (net.Listener, error) {
  187. if fi, err := os.Stat(cfg.addr); err == nil {
  188. if fi.Mode()&os.ModeSocket == 0 {
  189. return nil, fmt.Errorf("info_api.listen path %s exists and is not a socket, refusing to replace it", cfg.addr)
  190. }
  191. // a normal shutdown unlinks the socket, so a file here means a previous process exited
  192. // uncleanly, remove it so the bind below can succeed
  193. if err = os.Remove(cfg.addr); err != nil {
  194. return nil, fmt.Errorf("failed to remove stale socket %s: %w", cfg.addr, err)
  195. }
  196. }
  197. ln, err := net.Listen("unix", cfg.addr)
  198. if err != nil {
  199. return nil, err
  200. }
  201. // The socket is briefly live with umask-derived permissions before this chmod lands, tolerated
  202. // because connections accepted in that window still only reach this read-only API
  203. if err = os.Chmod(cfg.addr, cfg.socketMode); err != nil {
  204. _ = ln.Close()
  205. return nil, fmt.Errorf("failed to set mode on socket %s: %w", cfg.addr, err)
  206. }
  207. return ln, nil
  208. }
  209. func (h *infoAPIServer) certState() *CertState {
  210. if h.pki == nil {
  211. return nil
  212. }
  213. return h.pki.getCertState()
  214. }
  215. // handleHost serves GET /v1/host?addr=<vpn addr>, answering with the identity of the host that
  216. // owns the address: a peer with an active tunnel, or this node itself. addr may include a port,
  217. // which is ignored, so clients can pass a connection's remote address through without parsing it
  218. func (h *infoAPIServer) handleHost(w http.ResponseWriter, r *http.Request) {
  219. q := r.URL.Query().Get("addr")
  220. if q == "" {
  221. writeJSONError(w, http.StatusBadRequest, "missing addr parameter")
  222. return
  223. }
  224. ip, err := parseQueryAddrParam(q)
  225. if err != nil {
  226. writeJSONError(w, http.StatusBadRequest, "invalid address")
  227. return
  228. }
  229. crt := findCertificateForVpnAddr(h.certState(), h.hostMap, ip)
  230. if crt == nil {
  231. writeJSONError(w, http.StatusNotFound, "no active tunnel for address")
  232. return
  233. }
  234. h.writeHostIdentity(w, crt)
  235. }
  236. // handleSelf serves GET /v1/self, answering with this node's own identity
  237. func (h *infoAPIServer) handleSelf(w http.ResponseWriter, r *http.Request) {
  238. var crt cert.Certificate
  239. if cs := h.certState(); cs != nil {
  240. crt = cs.getCertificate(cs.initiatingVersion)
  241. }
  242. if crt == nil {
  243. writeJSONError(w, http.StatusInternalServerError, "no certificate available")
  244. return
  245. }
  246. h.writeHostIdentity(w, crt)
  247. }
  248. func (h *infoAPIServer) writeHostIdentity(w http.ResponseWriter, crt cert.Certificate) {
  249. id, err := newHostIdentity(crt)
  250. if err != nil {
  251. writeJSONError(w, http.StatusInternalServerError, "failed to fingerprint certificate")
  252. return
  253. }
  254. w.Header().Set("Content-Type", "application/json")
  255. if err = json.NewEncoder(w).Encode(id); err != nil {
  256. h.l.Debug("Failed to write info API response", "error", err)
  257. }
  258. }
  259. // findCertificateForVpnAddr answers "who owns this vpn address": ourselves (from local cert state,
  260. // the hostmap never carries an entry for this node) or a peer with an active tunnel. Returns nil
  261. // when the address is unknown or the tunnel is mid-teardown
  262. func findCertificateForVpnAddr(cs *CertState, hostMap *HostMap, ip netip.Addr) cert.Certificate {
  263. if cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
  264. return cs.getCertificate(cs.initiatingVersion)
  265. }
  266. hostinfo := hostMap.QueryVpnAddr(ip)
  267. if hostinfo == nil {
  268. return nil
  269. }
  270. cc := hostinfo.GetCert()
  271. if cc == nil {
  272. return nil
  273. }
  274. return cc.Certificate
  275. }
  276. // hostIdentity is the json document served for both /v1/host and /v1/self, every field is derived
  277. // from the authenticated certificate alone
  278. type hostIdentity struct {
  279. Name string `json:"name"`
  280. VpnAddrs []netip.Addr `json:"vpnAddrs"`
  281. Networks []netip.Prefix `json:"networks"`
  282. UnsafeNetworks []netip.Prefix `json:"unsafeNetworks"`
  283. Groups []string `json:"groups"`
  284. Fingerprint string `json:"fingerprint"`
  285. Issuer string `json:"issuer"`
  286. NotBefore time.Time `json:"notBefore"`
  287. NotAfter time.Time `json:"notAfter"`
  288. CertVersion int `json:"certVersion"`
  289. }
  290. func newHostIdentity(crt cert.Certificate) (hostIdentity, error) {
  291. fp, err := crt.Fingerprint()
  292. if err != nil {
  293. return hostIdentity{}, err
  294. }
  295. // slices are always allocated so they marshal as [] rather than null
  296. networks := crt.Networks()
  297. id := hostIdentity{
  298. Name: crt.Name(),
  299. VpnAddrs: make([]netip.Addr, 0, len(networks)),
  300. Networks: append(make([]netip.Prefix, 0, len(networks)), networks...),
  301. UnsafeNetworks: append(make([]netip.Prefix, 0, len(crt.UnsafeNetworks())), crt.UnsafeNetworks()...),
  302. Groups: append(make([]string, 0, len(crt.Groups())), crt.Groups()...),
  303. Fingerprint: fp,
  304. Issuer: crt.Issuer(),
  305. NotBefore: crt.NotBefore(),
  306. NotAfter: crt.NotAfter(),
  307. CertVersion: int(crt.Version()),
  308. }
  309. for _, n := range networks {
  310. id.VpnAddrs = append(id.VpnAddrs, n.Addr())
  311. }
  312. return id, nil
  313. }
  314. func writeJSONError(w http.ResponseWriter, status int, msg string) {
  315. w.Header().Set("Content-Type", "application/json")
  316. w.WriteHeader(status)
  317. _ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
  318. }
  319. // parseQueryAddrParam parses the addr query parameter, accepting a bare address or an address with
  320. // a port (`192.168.100.7:54321`, `[fd00::1]:443`) so callers can pass a connection's RemoteAddr
  321. // straight through. The result is unmapped, 4in6 addresses (::ffff:a.b.c.d) become ipv4
  322. func parseQueryAddrParam(s string) (netip.Addr, error) {
  323. if ip, err := netip.ParseAddr(s); err == nil {
  324. return ip.Unmap(), nil
  325. }
  326. ap, err := netip.ParseAddrPort(s)
  327. if err != nil {
  328. return netip.Addr{}, err
  329. }
  330. return ap.Addr().Unmap(), nil
  331. }
  332. func loadInfoAPIConfig(c *config.C) (infoAPIConfig, error) {
  333. cfg := infoAPIConfig{
  334. enabled: c.GetBool("info_api.enabled", false),
  335. listen: c.GetString("info_api.listen", ""),
  336. }
  337. if !cfg.enabled {
  338. return cfg, nil
  339. }
  340. if cfg.listen == "" {
  341. return cfg, errors.New("info_api.listen can not be empty when info_api is enabled")
  342. }
  343. addr, err := parseInfoAPIListen(cfg.listen)
  344. if err != nil {
  345. return cfg, err
  346. }
  347. cfg.addr = addr
  348. // read as a string so yaml can't reinterpret the octal literal
  349. modeStr := c.GetString("info_api.socket_mode", "0600")
  350. mode, err := strconv.ParseUint(modeStr, 8, 32)
  351. if err != nil || fs.FileMode(mode)&^fs.ModePerm != 0 {
  352. return cfg, fmt.Errorf("info_api.socket_mode was not a valid octal file mode: %s", modeStr)
  353. }
  354. cfg.socketMode = fs.FileMode(mode)
  355. return cfg, nil
  356. }
  357. // parseInfoAPIListen extracts the unix socket path from the info_api.listen config value, which
  358. // must be a `unix://` URL with an absolute path, e.g. `unix:///var/run/nebula.sock`
  359. func parseInfoAPIListen(listen string) (addr string, err error) {
  360. path, ok := strings.CutPrefix(listen, "unix://")
  361. if !ok {
  362. return "", fmt.Errorf("info_api.listen must be a unix:// socket path: %s", listen)
  363. } else if !filepath.IsAbs(path) {
  364. return "", fmt.Errorf("info_api.listen unix socket path must be absolute: %s", listen)
  365. }
  366. return path, nil
  367. }