1
0

netchange_darwin.go 5.4 KB

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