| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- //go:build darwin && !ios && !e2e_testing
- package udp
- import (
- "context"
- "encoding/binary"
- "errors"
- "log/slog"
- "os"
- "time"
- "golang.org/x/sys/unix"
- )
- const (
- // netChangeSettleWindow is how long we keep swallowing routing messages after the first interesting one. A
- // single network change is never a single message, it is a burst: the link drops, addresses go away, new ones
- // arrive, routes get rewritten. Reporting part way through that just means reporting again.
- netChangeSettleWindow = time.Second
- // netChangeReadBuffer is sized well past any rt_msghdr plus its addresses. A short read would be discarded by
- // the kernel, so being generous here is how we avoid missing a message.
- netChangeReadBuffer = 4096
- )
- // watchNetworkChanges reports when the local network moves out from under us, so the listener can be rebound.
- //
- // Darwin scopes a udp socket to whatever interface it came up on. Move between networks and we keep sending out an
- // interface that no longer has a route, which surfaces as an instant "no route to host" with no packet ever leaving
- // the box. Rebind clears that, but only if something notices the change and calls it. iOS has always been told by
- // the host app off NWPathMonitor. This is the equivalent for everything else that runs on darwin.
- //
- // The returned channel is buffered and coalescing: a send is dropped if one is already pending, since both mean the
- // same thing to a reader. It is closed when ctx is cancelled or the routing socket fails, so a caller can simply
- // range over it. Platforms whose sockets do not need rebinding return a nil channel and no error.
- func watchNetworkChanges(ctx context.Context, l *slog.Logger) (<-chan struct{}, error) {
- sock, err := openRouteSocket()
- if err != nil {
- return nil, err
- }
- changes := make(chan struct{}, 1)
- go func() {
- defer close(changes)
- defer func() { _ = sock.Close() }()
- // Closing the socket is what unblocks the read in watchRouteSocket, so this turns cancellation into a
- // close. It is scoped to this call so it cannot outlive the watch it belongs to.
- done := make(chan struct{})
- defer close(done)
- go func() {
- select {
- case <-ctx.Done():
- _ = sock.Close()
- case <-done:
- }
- }()
- watchRouteSocket(l, sock, changes)
- }()
- return changes, nil
- }
- // watchRouteSocket blocks reading the routing socket, reporting once per settled burst of changes. It returns when
- // the socket is closed, which is how cancellation gets us out of here.
- func watchRouteSocket(l *slog.Logger, sock *os.File, changes chan<- struct{}) {
- buf := make([]byte, netChangeReadBuffer)
- for {
- n, err := sock.Read(buf)
- if err != nil {
- logRouteSocketError(l, err)
- return
- }
- if !isNetworkChange(buf[:n]) {
- continue
- }
- // Swallow the rest of the burst. The deadline is absolute and not extended by what arrives, so this always
- // ends after the settle window no matter how chatty the socket is. Changes that land after the window
- // simply produce another report, which is the correct outcome anyway.
- deadline := time.Now().Add(netChangeSettleWindow)
- for {
- if err = sock.SetReadDeadline(deadline); err != nil {
- logRouteSocketError(l, err)
- return
- }
- if _, err = sock.Read(buf); err != nil {
- if os.IsTimeout(err) {
- break
- }
- logRouteSocketError(l, err)
- return
- }
- }
- if err = sock.SetReadDeadline(time.Time{}); err != nil {
- logRouteSocketError(l, err)
- return
- }
- select {
- case changes <- struct{}{}:
- default:
- // One already pending, and a second "the network moved" tells the reader nothing new.
- }
- }
- }
- // logRouteSocketError reports a routing socket failure unless it is just us shutting the socket down.
- func logRouteSocketError(l *slog.Logger, err error) {
- if errors.Is(err, os.ErrClosed) {
- return
- }
- l.Error("Error reading the routing socket, will no longer notice local network changes", "error", err)
- }
- // openRouteSocket returns the routing socket as a non blocking os.File. Going through os.File puts reads on the go
- // poller, which buys us both a working read deadline and a Close that unblocks a read in progress.
- func openRouteSocket() (*os.File, error) {
- fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
- if err != nil {
- return nil, err
- }
- if err = unix.SetNonblock(fd, true); err != nil {
- _ = unix.Close(fd)
- return nil, err
- }
- return os.NewFile(uintptr(fd), "route"), nil
- }
- // isNetworkChange reports whether a routing message means our local addressing may have moved out from under us.
- //
- // We read the header instead of parsing the message because the type is the only part we need, and a full parse can
- // fail on shapes we don't care about, which would turn "a message I can't parse" into "a change I missed".
- // rt_msghdr, if_msghdr and ifa_msghdr all begin with the same three fields, so this is the same for every type.
- func isNetworkChange(msg []byte) bool {
- if len(msg) < 4 {
- return false
- }
- // u_short msglen, u_char version, u_char type
- if int(binary.NativeEndian.Uint16(msg[0:2])) > len(msg) || msg[2] != unix.RTM_VERSION {
- return false
- }
- switch msg[3] {
- case unix.RTM_NEWADDR, unix.RTM_DELADDR, unix.RTM_IFINFO:
- // An address arrived or left, or a link changed state. Anything else on this socket is either a route
- // churning underneath us, which a rebind doesn't help with, or unrelated traffic.
- return true
- default:
- return false
- }
- }
|