netchange_darwin.go 5.3 KB

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