The boot unit fails closed: if ruleset.nft doesn't load, it loads ruleset-fallback.nft (render.Fallback), saved beside it from the same configuration: nothing new in from external zones, established, the ICMPv6 IPv6 needs and DHCPv6 replies let in, the inside left alone and masqueraded, and the unit marked failed with a critical message. housecarld replaces it when it applies; status, the dashboard and /healthz say while it is in force. no_external_zone now also says the fallback closes everything without an external zone. The boot ruleset has no flowtable: nftables on Debian 13's kernel refuses one naming a device that doesn't exist yet (ppp0 before PPPoE), and with it the whole file. internal/linkwatch listens to rtnetlink link messages; housecarld applies again when a flowtable device appears or comes back as a new interface (by ifindex), under the engine's lock. docs/flowtable.md: what it does, when to use it and when not, how. GET /healthz reports firewall (applied, unconfigured, fallback, missing), changes on probation and the machine's warnings, with 503 unless in force, read at most every five seconds. Renaming a zone, set or service changes what names it in the same change (one history entry, one undo); dynamic sets keep their names. The importer keeps a chain's head in order: once a would-be built-in stays a rule, the ones after it do too, so the maintainer's ICMPv6 accepts stay behind the invalid drop as written. hc settings set --flowtable none empties the list. make test (nft 1.1.3) and make e2e (242 checks, with VLAN sub-interfaces, the fallback and the flowtable) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
2.4 KiB
Go
97 lines
2.4 KiB
Go
// 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
|
|
}
|