package catalogue import ( "strings" "testing" ) // A node's fail2ban jails are composed from the modules it runs (novox/hq to-be 31): the holder // (jailing) gathers every module's declared jail into one jail file and a filter file per jail. func TestJailsAreComposedFromTheNodesModules(t *testing.T) { modules := []Manifest{ {Module: "fail2ban", Jailing: &Jailing{Into: "/etc/fail2ban/jail.d/mesh-composed.conf", FilterInto: "/etc/fail2ban/filter.d"}}, {Module: "postgres", Jails: []Jail{{Name: "postgres-auth", Failregex: "auth failed from ", Jail: "port = 5432\nmaxretry = 5"}}}, } files := jailsInto(modules, modules[0].Jailing) by := map[string]map[string]any{} for _, f := range files { by[f["id"].(string)] = f } jail := by[ComposedJailsID()] if jail == nil || jail["path"] != "/etc/fail2ban/jail.d/mesh-composed.conf" { t.Fatalf("the composed jail file was not written: %v", jail) } body := jail["content"].(string) if !strings.Contains(body, "[postgres-auth]") || !strings.Contains(body, "filter = postgres-auth") || !strings.Contains(body, "port = 5432") { t.Fatalf("the postgres jail stanza was not composed in:\n%s", body) } filter := by["filter-postgres-auth"] if filter == nil || filter["path"] != "/etc/fail2ban/filter.d/postgres-auth.conf" { t.Fatalf("the jail's filter file was not written: %v", filter) } if !strings.Contains(filter["content"].(string), "failregex = auth failed from ") { t.Fatalf("the failregex was not written: %v", filter["content"]) } } // A holder whose node runs no jail-declaring module still gets the file, empty — so removing the // last jail is a change the service restarts on, not a file that vanishes. func TestTheComposedJailFileIsWrittenEvenWhenEmpty(t *testing.T) { files := jailsInto([]Manifest{{Module: "fail2ban"}}, &Jailing{Into: "/x", FilterInto: "/f"}) if len(files) != 1 || files[0]["id"] != ComposedJailsID() { t.Fatalf("the empty composed jail file was not written alone: %v", files) } } // A changed pattern restarts fail2ban (novox/hq issue 191's rollout): the service restarts when the // composed jail file changes, and the filter is a file of its own, so the jail file names the // filter's digest. Changing only the failregex must change the jail file; the same pattern must not. func TestAChangedFilterChangesTheJailFile(t *testing.T) { jailFile := func(failregex string) string { modules := []Manifest{ {Module: "fail2ban", Jailing: &Jailing{Into: "/etc/fail2ban/jail.d/mesh.conf", FilterInto: "/etc/fail2ban/filter.d"}}, {Module: "gitea", Jails: []Jail{{Name: "gitea", Failregex: failregex, Jail: "port = 222"}}}, } for _, f := range jailsInto(modules, modules[0].Jailing) { if f["id"] == ComposedJailsID() { return f["content"].(string) } } t.Fatal("no composed jail file") return "" } before := jailFile("web login failed from ") if again := jailFile("web login failed from "); again != before { t.Errorf("the same pattern composed a different jail file, which would restart fail2ban for nothing") } if after := jailFile("web login failed from \n Invalid user .* from "); after == before { t.Errorf("a changed pattern left the jail file as it was, so fail2ban keeps the old filter:\n%s", after) } }