netchange_darwin.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. //go:build darwin && !ios && !e2e_testing
  2. // +build darwin,!ios,!e2e_testing
  3. package udp
  4. import (
  5. "context"
  6. "encoding/binary"
  7. "errors"
  8. "log/slog"
  9. "os"
  10. "time"
  11. "golang.org/x/sys/unix"
  12. )
  13. const (
  14. // netChangeSettleWindow is how long we keep swallowing routing messages after the first interesting one. A
  15. // single network change is never a single message, it is a burst: the link drops, addresses go away, new ones
  16. // arrive, routes get rewritten. Reporting part way through that just means reporting again.
  17. netChangeSettleWindow = time.Second
  18. // netChangeReadBuffer is sized well past any rt_msghdr plus its addresses. A short read would be discarded by
  19. // the kernel, so being generous here is how we avoid missing a message.
  20. netChangeReadBuffer = 4096
  21. )
  22. // watchNetworkChanges reports when the local network moves out from under us, so the listener can be rebound.
  23. //
  24. // Darwin scopes a udp socket to whatever interface it came up on. Move between networks and we keep sending out an
  25. // interface that no longer has a route, which surfaces as an instant "no route to host" with no packet ever leaving
  26. // the box. Rebind clears that, but only if something notices the change and calls it. iOS has always been told by
  27. // the host app off NWPathMonitor. This is the equivalent for everything else that runs on darwin.
  28. //
  29. // The returned channel is buffered and coalescing: a send is dropped if one is already pending, since both mean the
  30. // same thing to a reader. It is closed when ctx is cancelled or the routing socket fails, so a caller can simply
  31. // range over it. Platforms whose sockets do not need rebinding return a nil channel and no error.
  32. func watchNetworkChanges(ctx context.Context, l *slog.Logger) (<-chan struct{}, error) {
  33. sock, err := openRouteSocket()
  34. if err != nil {
  35. return nil, err
  36. }
  37. changes := make(chan struct{}, 1)
  38. go func() {
  39. defer close(changes)
  40. defer func() { _ = sock.Close() }()
  41. // Closing the socket is what unblocks the read in watchRouteSocket, so this turns cancellation into a
  42. // close. It is scoped to this call so it cannot outlive the watch it belongs to.
  43. done := make(chan struct{})
  44. defer close(done)
  45. go func() {
  46. select {
  47. case <-ctx.Done():
  48. _ = sock.Close()
  49. case <-done:
  50. }
  51. }()
  52. watchRouteSocket(l, sock, changes)
  53. }()
  54. return changes, nil
  55. }
  56. // watchRouteSocket blocks reading the routing socket, reporting once per settled burst of changes. It returns when
  57. // the socket is closed, which is how cancellation gets us out of here.
  58. func watchRouteSocket(l *slog.Logger, sock *os.File, changes chan<- struct{}) {
  59. buf := make([]byte, netChangeReadBuffer)
  60. for {
  61. n, err := sock.Read(buf)
  62. if err != nil {
  63. logRouteSocketError(l, err)
  64. return
  65. }
  66. if !isNetworkChange(buf[:n]) {
  67. continue
  68. }
  69. // Swallow the rest of the burst. The deadline is absolute and not extended by what arrives, so this always
  70. // ends after the settle window no matter how chatty the socket is. Changes that land after the window
  71. // simply produce another report, which is the correct outcome anyway.
  72. deadline := time.Now().Add(netChangeSettleWindow)
  73. for {
  74. if err = sock.SetReadDeadline(deadline); err != nil {
  75. logRouteSocketError(l, err)
  76. return
  77. }
  78. if _, err = sock.Read(buf); err != nil {
  79. if os.IsTimeout(err) {
  80. break
  81. }
  82. logRouteSocketError(l, err)
  83. return
  84. }
  85. }
  86. if err = sock.SetReadDeadline(time.Time{}); err != nil {
  87. logRouteSocketError(l, err)
  88. return
  89. }
  90. select {
  91. case changes <- struct{}{}:
  92. default:
  93. // One already pending, and a second "the network moved" tells the reader nothing new.
  94. }
  95. }
  96. }
  97. // logRouteSocketError reports a routing socket failure unless it is just us shutting the socket down.
  98. func logRouteSocketError(l *slog.Logger, err error) {
  99. if errors.Is(err, os.ErrClosed) {
  100. return
  101. }
  102. l.Error("Error reading the routing socket, will no longer notice local network changes", "error", err)
  103. }
  104. // openRouteSocket returns the routing socket as a non blocking os.File. Going through os.File puts reads on the go
  105. // poller, which buys us both a working read deadline and a Close that unblocks a read in progress.
  106. func openRouteSocket() (*os.File, error) {
  107. fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
  108. if err != nil {
  109. return nil, err
  110. }
  111. if err = unix.SetNonblock(fd, true); err != nil {
  112. _ = unix.Close(fd)
  113. return nil, err
  114. }
  115. return os.NewFile(uintptr(fd), "route"), nil
  116. }
  117. // isNetworkChange reports whether a routing message means our local addressing may have moved out from under us.
  118. //
  119. // We read the header instead of parsing the message because the type is the only part we need, and a full parse can
  120. // fail on shapes we don't care about, which would turn "a message I can't parse" into "a change I missed".
  121. // rt_msghdr, if_msghdr and ifa_msghdr all begin with the same three fields, so this is the same for every type.
  122. func isNetworkChange(msg []byte) bool {
  123. if len(msg) < 4 {
  124. return false
  125. }
  126. // u_short msglen, u_char version, u_char type
  127. if int(binary.NativeEndian.Uint16(msg[0:2])) > len(msg) || msg[2] != unix.RTM_VERSION {
  128. return false
  129. }
  130. switch msg[3] {
  131. case unix.RTM_NEWADDR, unix.RTM_DELADDR, unix.RTM_IFINFO:
  132. // An address arrived or left, or a link changed state. Anything else on this socket is either a route
  133. // churning underneath us, which a rebind doesn't help with, or unrelated traffic.
  134. return true
  135. default:
  136. return false
  137. }
  138. }