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>
88 lines
1.9 KiB
Go
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
|
|
}
|