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>
This commit is contained in:
1 parent
835a9ba2cc
commit
9d766b9823
38 files changed
+1335
-84
No files matched your search
@@ -0,0 +1,84 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"housecarl/internal/api"
|
||||
)
|
||||
|
||||
// TestRename: renaming a zone, set or service changes what refers to it,
|
||||
// in the same change, which undoes as one.
|
||||
func TestRename(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
e.srv.probationFor = 0
|
||||
post := func(path string, body map[string]any) {
|
||||
t.Helper()
|
||||
if code := e.call(e.token, "POST", path, body, nil); code != 201 {
|
||||
t.Fatalf("POST %s: %d", path, code)
|
||||
}
|
||||
}
|
||||
post("/zones", map[string]any{"name": "wan", "interfaces": []string{"wan"}, "external": true})
|
||||
post("/zones", map[string]any{"name": "dhcp", "interfaces": []string{"eth1"}})
|
||||
post("/sets", map[string]any{"name": "trusted_ips", "entries": []string{"10.0.10.0/24"}})
|
||||
post("/sets", map[string]any{"name": "guests", "dynamic": true})
|
||||
post("/services", map[string]any{"name": "jarl", "entries": []string{"tcp/8053"}})
|
||||
post("/rules", map[string]any{"chain": "forward", "in_zones": []string{"dhcp"}, "out_zones": []string{"wan"}, "sources": []string{"trusted_ips"}})
|
||||
post("/rules", map[string]any{"chain": "input", "in_zones": []string{"dhcp"}, "services": []string{"jarl"}})
|
||||
post("/snats", map[string]any{"out_zones": []string{"wan"}, "sources": []string{"trusted_ips"}})
|
||||
post("/forwards", map[string]any{"name": "jf", "ports": "8096", "to": "10.0.10.57", "public_addresses": []string{"217.155.8.194"}, "sources": []string{"trusted_ips"}})
|
||||
|
||||
var res api.MutationResponse
|
||||
if code := e.call(e.token, "PATCH", "/zones/dhcp", map[string]any{"name": "lan"}, &res); code != 200 {
|
||||
t.Fatalf("renaming the zone: %d %+v", code, res)
|
||||
}
|
||||
zoneRename := res.HistoryID
|
||||
e.call(e.token, "PATCH", "/sets/trusted_ips", map[string]any{"name": "trusted"}, &res)
|
||||
setRename := res.HistoryID
|
||||
e.call(e.token, "PATCH", "/services/jarl", map[string]any{"name": "jarl-api"}, &res)
|
||||
serviceRename := res.HistoryID
|
||||
st := e.eng.State()
|
||||
if st.Zone("dhcp") != nil || st.Zone("lan") == nil {
|
||||
t.Fatalf("zones %+v", st.Zones)
|
||||
}
|
||||
for _, r := range st.Rules {
|
||||
if strings.Join(r.InZones, ",") != "lan" {
|
||||
t.Errorf("rule %d in zones %v", r.ID, r.InZones)
|
||||
}
|
||||
if r.Chain == "forward" && strings.Join(r.Sources, ",") != "trusted" {
|
||||
t.Errorf("rule %d sources %v", r.ID, r.Sources)
|
||||
}
|
||||
if r.Chain == "input" && strings.Join(r.Services, ",") != "jarl-api" {
|
||||
t.Errorf("rule %d services %v", r.ID, r.Services)
|
||||
}
|
||||
}
|
||||
if s := st.SNATs[0].Sources; strings.Join(s, ",") != "trusted" {
|
||||
t.Errorf("snat sources %v", s)
|
||||
}
|
||||
if s := st.Forwards[0].Sources; strings.Join(s, ",") != "trusted" {
|
||||
t.Errorf("forward sources %v", s)
|
||||
}
|
||||
if ps := e.eng.Problems(); len(ps) != 0 {
|
||||
for _, p := range ps {
|
||||
if p.Severity == "error" {
|
||||
t.Errorf("problem after renaming: %+v", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Undoing the renames (newest first) puts the references back too.
|
||||
for _, id := range []int64{serviceRename, setRename, zoneRename} {
|
||||
if code := e.call(e.token, "POST", "/history/"+itoa(id)+"/revert", nil, nil); code != 200 {
|
||||
t.Fatalf("undo %d: %d", id, code)
|
||||
}
|
||||
}
|
||||
st = e.eng.State()
|
||||
for _, r := range st.Rules {
|
||||
if strings.Join(r.InZones, ",") != "dhcp" || r.Chain == "forward" && r.Sources[0] != "trusted_ips" || r.Chain == "input" && r.Services[0] != "jarl" {
|
||||
t.Errorf("after undo: rule %+v", r.Match)
|
||||
}
|
||||
}
|
||||
// A dynamic set keeps its name: tokens and programs go by it.
|
||||
if code := e.call(e.token, "PATCH", "/sets/guests", map[string]any{"name": "visitors"}, nil); code != 409 {
|
||||
t.Errorf("renaming a dynamic set: %d", code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user