Ban lists: where Guard and fail2ban ban addresses
A program bans an address (every port, or a protocol's ports) for a time; the ban list says what it is kept from: the firewall, some hosts behind it, or everything. Bans drop ahead of every rule, close the address's connections, group IPv6 by /64, are kept in the database across restarts and audited. Never banned: the inside, the firewall's own addresses, the caller, the list's never. bans grant (bans:write:LIST), hc banlist/bans/ban/unban, Traffic -> Bans, a fail2ban action in contrib/, and failed logins and refused tokens logged for Guard and fail2ban. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f56eefae2
commit
500bfc7bdd
44 files changed
+2541
-39
No files matched your search
@@ -3,6 +3,7 @@ package nft
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -543,3 +544,66 @@ func TestPruneCounters(t *testing.T) {
|
||||
t.Errorf("kept %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBansInContainer: a ban goes in, a second one renews its time, a
|
||||
// reload keeps the bans the database has, and lifting one already gone
|
||||
// isn't an error.
|
||||
func TestBansInContainer(t *testing.T) {
|
||||
image := os.Getenv("HOUSECARL_NFT_IMAGE")
|
||||
if image == "" {
|
||||
t.Skip("HOUSECARL_NFT_IMAGE not set")
|
||||
}
|
||||
name := "housecarl-nft-bans"
|
||||
exec.Command("docker", "rm", "-f", name).Run()
|
||||
if out, err := exec.Command("docker", "run", "-d", "--name", name, "--cap-add", "NET_ADMIN", image, "sleep", "300").CombinedOutput(); err != nil {
|
||||
t.Fatalf("starting the container: %v %s", err, out)
|
||||
}
|
||||
t.Cleanup(func() { exec.Command("docker", "rm", "-f", name).Run() })
|
||||
var active []render.Ban
|
||||
fw := &Firewall{Runner: &Runner{Command: []string{"docker", "exec", "-i", name, "nft"}}, Bans: func() []render.Ban { return active }}
|
||||
ctx := context.Background()
|
||||
st := model.NewState()
|
||||
st.Zones = []*model.Zone{{ID: 1, Name: "wan", Interfaces: []string{"eth0"}, External: true}}
|
||||
st.BanLists = []*model.BanList{{ID: 2, Name: "router", Scope: model.BanScopeFirewall}}
|
||||
if err := fw.Apply(ctx, st); err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
list := func(set string) string {
|
||||
out, _ := exec.Command("docker", "exec", name, "nft", "list", "set", "inet", "housecarl", set).CombinedOutput()
|
||||
return string(out)
|
||||
}
|
||||
b := render.Ban{List: 2, Addr: netip.MustParsePrefix("203.0.113.9/32"), Timeout: 60}
|
||||
if err := fw.AddBan(ctx, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.Timeout = 7200
|
||||
if err := fw.AddBan(ctx, b); err != nil {
|
||||
t.Fatalf("renewing: %v", err)
|
||||
}
|
||||
if got := list("ban_2_v4"); !strings.Contains(got, "203.0.113.9 timeout 2h") {
|
||||
t.Errorf("renewed ban: %s", got)
|
||||
}
|
||||
svc := render.Ban{List: 2, Addr: netip.MustParsePrefix("2001:db8:1:2::/64"), Proto: "tcp", Port: 22}
|
||||
if err := fw.AddBan(ctx, svc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
active = []render.Ban{svc}
|
||||
if err := fw.Apply(ctx, st); err != nil {
|
||||
t.Fatalf("reapply: %v", err)
|
||||
}
|
||||
if got := list("ban_2_v6_svc"); !strings.Contains(got, "2001:db8:1:2:: . 6 . 22") {
|
||||
t.Errorf("after a reload, the database's ban: %s", got)
|
||||
}
|
||||
if got := list("ban_2_v4"); strings.Contains(got, "203.0.113.9") {
|
||||
t.Errorf("after a reload, a ban the database doesn't have: %s", got)
|
||||
}
|
||||
if err := fw.DeleteBan(ctx, svc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := fw.DeleteBan(ctx, svc); err != nil {
|
||||
t.Errorf("lifting a ban already gone: %v", err)
|
||||
}
|
||||
if got := list("ban_2_v6_svc"); strings.Contains(got, "2001:db8") {
|
||||
t.Errorf("lifted ban: %s", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user