mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 16:17:00 +02:00
7902ce674e
Co-authored-by: Jack Doan <me@jackdoan.com>
165 lines
5.3 KiB
Go
165 lines
5.3 KiB
Go
//go:build darwin && !ios && !e2e_testing
|
|
// +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
|
|
}
|
|
}
|