// Package tablewatch tells when an nftables table is deleted: by // `nft flush ruleset` (which a ruleset file such as /etc/nftables.conf // usually starts with, and which nftables.service runs when it stops), or // by hand. It listens to nftables' netlink notifications, as `nft monitor` // does, and calls back once they have been quiet for a moment, since a // ruleset reload brings many. In shadow mode housecarld then puts its // table back (docs/shadow.md). package tablewatch import ( "context" "encoding/binary" "errors" "fmt" "os" "time" "golang.org/x/sys/unix" ) // Settle is how long the notifications must be quiet before the callback. const Settle = 2 * time.Second // Watch calls deleted after a table was deleted, until ctx ends. It // returns an error only if it can't listen at all. func Watch(ctx context.Context, deleted func()) error { fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC|unix.SOCK_NONBLOCK, unix.NETLINK_NETFILTER) if err != nil { return fmt.Errorf("opening a netlink socket: %w", err) } if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK, Groups: 1 << (unix.NFNLGRP_NFTABLES - 1)}); err != nil { unix.Close(fd) return fmt.Errorf("listening to nftables' 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), "nfnetlink") go func() { <-ctx.Done() f.Close() }() events := make(chan struct{}, 1) go func() { buf := make([]byte, 256<<10) for { n, err := f.Read(buf) switch { case err != nil && ctx.Err() != nil: close(events) return case errors.Is(err, unix.ENOBUFS): // Notifications were lost: one may have been a deletion. case err != nil: time.Sleep(time.Second) continue case !DeletesTable(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 deleted() } } } // DeletesTable reports whether a netlink datagram holds a table being // deleted. func DeletesTable(b []byte) bool { want := uint16(unix.NFNL_SUBSYS_NFTABLES<<8 | unix.NFT_MSG_DELTABLE) 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 == want { return true } size = (size + unix.NLMSG_ALIGNTO - 1) &^ (unix.NLMSG_ALIGNTO - 1) if size >= len(b) { return false } b = b[size:] } return false }