Files
housecarl/internal/hostif/hostif.go
T
abrown38andClaude Opus 5.5 9d766b9823 Boot fallback, flowtable devices that come and go, healthz, renames
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>
2026-10-19 23:16:46 +01:00

88 lines
1.9 KiB
Go

// Package hostif reads the machine's network interfaces: their IPv4 and
// IPv6 addresses and whether they are up. Results are cached for a few
// seconds, since validation and the status page ask often.
package hostif
import (
"net"
"net/netip"
"sync"
"time"
)
// Host reads this machine's interfaces.
type Host struct {
mu sync.Mutex
at time.Time
addrs map[string][]netip.Prefix
up map[string]bool
err error
}
func (h *Host) refresh() {
if time.Since(h.at) < 5*time.Second && (h.addrs != nil || h.err != nil) {
return
}
h.at = time.Now()
ifs, err := net.Interfaces()
if err != nil {
h.addrs, h.up, h.err = nil, nil, err
return
}
addrs, up := map[string][]netip.Prefix{}, map[string]bool{}
for _, i := range ifs {
var ps []netip.Prefix
as, _ := i.Addrs()
for _, a := range as {
if n, ok := a.(*net.IPNet); ok {
if ip, ok := netip.AddrFromSlice(n.IP); ok {
bits, _ := n.Mask.Size()
ps = append(ps, netip.PrefixFrom(ip.Unmap(), bits))
}
}
}
addrs[i.Name] = ps
up[i.Name] = i.Flags&net.FlagUp != 0
}
h.addrs, h.up, h.err = addrs, up, nil
}
// All returns every interface with its addresses.
func (h *Host) All() (map[string][]netip.Prefix, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.refresh()
return h.addrs, h.err
}
// Up reports whether an interface is up.
func (h *Host) Up(name string) bool {
h.mu.Lock()
defer h.mu.Unlock()
h.refresh()
return h.up[name]
}
// Forget drops what was read, so the next question reads the interfaces
// again (after the kernel said one appeared or went).
func (h *Host) Forget() {
h.mu.Lock()
h.at = time.Time{}
h.mu.Unlock()
}
// Exists returns the set of interface names (nil if they can't be read).
func (h *Host) Exists() map[string]bool {
h.mu.Lock()
defer h.mu.Unlock()
h.refresh()
if h.err != nil {
return nil
}
out := make(map[string]bool, len(h.addrs))
for n := range h.addrs {
out[n] = true
}
return out
}