// Package linkwatch tells when the machine's network interfaces change: // one appears, goes, or comes back (ppp0 when PPPoE reconnects). It // listens to the kernel's rtnetlink link messages and calls back once // they have been quiet for a moment, since one change brings several. package linkwatch import ( "context" "encoding/binary" "errors" "fmt" "os" "time" "golang.org/x/sys/unix" ) // Settle is how long the link messages must be quiet before the callback. const Settle = 2 * time.Second // Watch calls changed after interfaces changed, until ctx ends. It returns // an error only if it can't listen at all. func Watch(ctx context.Context, changed func()) error { fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC|unix.SOCK_NONBLOCK, unix.NETLINK_ROUTE) if err != nil { return fmt.Errorf("opening a netlink socket: %w", err) } if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK, Groups: unix.RTMGRP_LINK}); err != nil { unix.Close(fd) return fmt.Errorf("listening to link changes: %w", err) } // A non-blocking descriptor goes through Go's poller, so closing the // file ends a Read in progress. f := os.NewFile(uintptr(fd), "rtnetlink") go func() { <-ctx.Done() f.Close() }() events := make(chan struct{}, 1) go func() { buf := make([]byte, 64<<10) for { n, err := f.Read(buf) switch { case err != nil && ctx.Err() != nil: close(events) return case errors.Is(err, unix.ENOBUFS): // Messages were lost: something changed anyway. case err != nil: time.Sleep(time.Second) continue case !links(buf[:n]): continue } select { case events <- struct{}{}: default: } } }() var timer <-chan time.Time for { select { case _, ok := <-events: if !ok { return nil } timer = time.After(Settle) case <-timer: timer = nil changed() } } } // links reports whether a netlink datagram holds a link being added, // changed or deleted. func links(b []byte) bool { for len(b) >= unix.NLMSG_HDRLEN { size := int(binary.NativeEndian.Uint32(b[0:4])) typ := binary.NativeEndian.Uint16(b[4:6]) if size < unix.NLMSG_HDRLEN || size > len(b) { return false } if typ == unix.RTM_NEWLINK || typ == unix.RTM_DELLINK { return true } size = (size + unix.NLMSG_ALIGNTO - 1) &^ (unix.NLMSG_ALIGNTO - 1) if size >= len(b) { return false } b = b[size:] } return false }