endpoint.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package localapi
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "net"
  9. "sync"
  10. )
  11. // endpoint is the lifecycle the two local APIs share: one socket at a path, bound and
  12. // given up under a mutex, moved by binding the new path before the old one is released,
  13. // and stopped when the context that brought it up ends. What is served on the socket,
  14. // and by what, is the embedding server's: it hands rebindLocked a serve function and
  15. // gets back the thing to close.
  16. type endpoint struct {
  17. l *slog.Logger
  18. // name is what the endpoint is called in its own log lines and errors, so the two
  19. // read apart.
  20. name string
  21. mu sync.Mutex
  22. // closer stops the serving when closed: the listener itself, or the server that owns
  23. // it. It is nil while the endpoint is stopped, which is what stopped means here.
  24. closer io.Closer
  25. path string
  26. access Access
  27. unwatch func() bool
  28. // id identifies the node listen created at path, so a reload can tell whether the
  29. // socket is still there and Stop whether the node it would unlink is still ours.
  30. id socketID
  31. }
  32. // socketID identifies a socket's node on the filesystem, by device and inode on unix.
  33. // The zero value is no identity: a platform with no node to identify, or one that could
  34. // not be read, and removeSocket then unlinks without looking.
  35. type socketID struct {
  36. dev, ino uint64
  37. }
  38. // Path returns the socket path currently being served, or "" when stopped.
  39. func (e *endpoint) Path() string {
  40. e.mu.Lock()
  41. defer e.mu.Unlock()
  42. return e.path
  43. }
  44. // rebindLocked brings the endpoint up at path with acc, moves it there, or, when it is
  45. // already there, applies a changed access; with nothing changed it does nothing, which
  46. // is what lets Wire's first attempt and its reload callback be the same call.
  47. //
  48. // The new socket is bound before the old one is given up: a bad path in a reloaded
  49. // config leaves the operator with the endpoint they already had rather than none. serve
  50. // is handed the new listener once it is certain to be kept and returns what to close to
  51. // stop serving it; what becomes of the old socket's connections is that closer's.
  52. //
  53. // The caller holds e.mu for the whole operation, bind included. Releasing it around
  54. // listen would let a Stop land in between, and this would then install a fresh socket on
  55. // an endpoint that had just shut down, leaving a live socket on disk until the process
  56. // exits. listen does not take e.mu, so holding it costs the other methods one bind's
  57. // wait, at most the stale socket probe, once per reload.
  58. func (e *endpoint) rebindLocked(ctx context.Context, path string, acc Access, serve func(net.Listener) io.Closer) error {
  59. reopening := false
  60. if e.closer != nil && e.path == path {
  61. if cur, ok := identifySocket(path); ok && cur == e.id {
  62. if e.access == acc {
  63. return nil
  64. }
  65. err := applyAccess(path, acc)
  66. if err == nil {
  67. e.access = acc
  68. e.l.Info("Changed the "+e.name+"'s socket access", "path", path, "access", acc.String())
  69. return nil
  70. }
  71. if !errors.Is(err, errReopenForAccess) {
  72. return fmt.Errorf("could not change the access of %s: %w", path, err)
  73. }
  74. // The platform fixes access when the endpoint is created, so it is created
  75. // again. The name cannot be held twice, so the old one goes first, and a bind
  76. // that then fails leaves the endpoint down rather than on the old access the
  77. // operator asked to change; that is reported below.
  78. reopening = true
  79. old := e.closer
  80. e.closer, e.path, e.access, e.id = nil, "", Access{}, socketID{}
  81. _ = old.Close()
  82. } else {
  83. // The node at the path is not the one this endpoint bound: something outside
  84. // nebula removed it, logind clearing /run/user/<uid> on logout or a unit
  85. // cleaning a RuntimeDirectory= it shares, or replaced it. The listener is still
  86. // open and nothing can reach it, so it is given up now and the path is bound
  87. // afresh below; listen's stale check decides whether whatever is there now may
  88. // be cleared. Given up first, rather than after a successful bind as a move
  89. // does, so that a bind that fails leaves the endpoint reported as down, which
  90. // it is, instead of serving a path nothing can reach.
  91. e.l.Warn("The "+e.name+"'s socket is no longer at its path, binding it again", "path", path)
  92. old := e.closer
  93. e.closer, e.path, e.access, e.id = nil, "", Access{}, socketID{}
  94. _ = old.Close()
  95. }
  96. }
  97. ln, err := listen(path, acc)
  98. if err != nil {
  99. if reopening {
  100. return fmt.Errorf("the %s was closed to change its access and could not be created again: %w", e.name, err)
  101. }
  102. return err
  103. }
  104. id, _ := identifySocket(path)
  105. // A reload that arrives during shutdown must not bring the endpoint back. Stop is
  106. // not terminal (a Rebind after Stop is allowed, and is how an endpoint whose first
  107. // bind failed comes up), so whether it is running is no guide; the context is what
  108. // shutdown cancels, and it is checked here, while nothing else can change the state.
  109. if err := ctx.Err(); err != nil {
  110. release(ln, path, id)
  111. return fmt.Errorf("not starting the %s, nebula is shutting down: %w", e.name, err)
  112. }
  113. old, oldPath, oldID := e.closer, e.path, e.id
  114. e.closer = serve(ln)
  115. e.path = path
  116. e.access = acc
  117. e.id = id
  118. e.watchLocked(ctx)
  119. // Stop serving the old socket. When it was on this same path its node is already
  120. // gone, and the node there now is the new socket's, so only the closer goes.
  121. if old != nil {
  122. if oldPath == path {
  123. _ = old.Close()
  124. } else {
  125. release(old, oldPath, oldID)
  126. }
  127. }
  128. return nil
  129. }
  130. // watchLocked stops the endpoint when ctx is cancelled. One watcher covers a run of the
  131. // endpoint, from the first bind that succeeds until Stop: a daemon whose initial listen
  132. // failed and which later Rebinds onto a good path from a reloaded config still needs its
  133. // socket removed on shutdown, and a Rebind on a running endpoint must not stack a second
  134. // watcher on the same ctx. Stop detaches the watcher, so an endpoint started again
  135. // afterwards is watched on the ctx that started it, and the ctx of an earlier run no
  136. // longer reaches it whenever that one ends. Callers hold e.mu.
  137. func (e *endpoint) watchLocked(ctx context.Context) {
  138. if e.unwatch != nil {
  139. return
  140. }
  141. e.unwatch = context.AfterFunc(ctx, e.Stop)
  142. }
  143. // Stop closes the endpoint and removes its socket. Work in flight is the closer's
  144. // business: a listener leaves the connections it already accepted to finish, a server
  145. // cuts them off.
  146. //
  147. // The whole of it runs under e.mu, closer and socket included, so a Rebind cannot land
  148. // between the state change and the close and find a path that is still bound by the
  149. // socket it is replacing.
  150. func (e *endpoint) Stop() {
  151. e.mu.Lock()
  152. defer e.mu.Unlock()
  153. if e.closer == nil {
  154. return
  155. }
  156. release(e.closer, e.path, e.id)
  157. e.closer = nil
  158. e.path = ""
  159. e.access = Access{}
  160. e.id = socketID{}
  161. // Set whenever closer is. When this Stop is the watcher's own, stopping a watcher
  162. // that has already fired is a no-op.
  163. e.unwatch()
  164. e.unwatch = nil
  165. }
  166. // release gives up a socket's closer and the node at its path. The node goes first,
  167. // while the socket is still open: for as long as it is, the node is this endpoint's,
  168. // since a starting daemon's stale check refuses to remove a socket that answers. The
  169. // other order opens a window between the close and the unlink in which a new daemon can
  170. // find the node stale, clear it and bind its own at the same path, which the unlink then
  171. // takes from under it, leaving that daemon serving a socket nothing can reach. listen
  172. // turns off the listener's own unlink on close so this is the only unlink. id is the
  173. // node listen created, and the only node removeSocket will remove: one that replaced it,
  174. // another daemon's socket bound after ours was taken from under us, is left alone.
  175. func release(c io.Closer, path string, id socketID) {
  176. removeSocket(path, id)
  177. _ = c.Close()
  178. }