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:
abrown38andClaude Opus 5.5 committed 2026-10-22 16:29:37 +01:00
1 parent 6f56eefae2
commit 500bfc7bdd
44 files changed
+2541 -39

No files matched your search

+48 -1
View File
@@ -190,6 +190,30 @@ func (r *Runner) DeleteElement(ctx context.Context, set, addr string) error {
return err
}
// AddBan puts a ban in force: added, or if it is there already, given
// its new time (nftables keeps an element's old timeout when it is added
// again, so it is added, deleted and added in one transaction).
func (r *Runner) AddBan(ctx context.Context, b render.Ban) error {
set := render.BanSet(b.List, b.Addr.Addr().Is6(), b.Proto != "")
e := render.BanElement(b)
t := ""
if b.Timeout > 0 {
t = fmt.Sprintf(" timeout %ds", b.Timeout)
}
script := fmt.Sprintf("add element inet %[1]s %[2]s { %[3]s }\ndelete element inet %[1]s %[2]s { %[3]s }\nadd element inet %[1]s %[2]s { %[3]s%[4]s }\n", render.Table, set, e, t)
_, err := r.run(ctx, script, "-f", "/dev/stdin")
return err
}
// DeleteBan lifts a ban. One already gone (it ran out) isn't an error.
func (r *Runner) DeleteBan(ctx context.Context, b render.Ban) error {
set := render.BanSet(b.List, b.Addr.Addr().Is6(), b.Proto != "")
e := render.BanElement(b)
script := fmt.Sprintf("add element inet %[1]s %[2]s { %[3]s }\ndelete element inet %[1]s %[2]s { %[3]s }\n", render.Table, set, e)
_, err := r.run(ctx, script, "-f", "/dev/stdin")
return err
}
// ---- reading JSON
// Table is what Housecarl reads back from its table: counters by mark
@@ -367,7 +391,7 @@ func ParseTable(data []byte) (*Table, error) {
continue
}
name := strings.TrimSuffix(strings.TrimSuffix(s.Name, "_v4"), "_v6")
if name == s.Name || strings.HasPrefix(s.Name, "meter_") || strings.HasPrefix(s.Name, "conn_") {
if name == s.Name || strings.HasPrefix(s.Name, "meter_") || strings.HasPrefix(s.Name, "conn_") || render.IsBanSet(s.Name) {
continue
}
for _, e := range s.Elem {
@@ -428,6 +452,9 @@ type Firewall struct {
// quotaRules are the rules with a quota in the ruleset in force, by
// ID: what their usage was counted by (nil before the first apply).
quotaRules map[int64]*model.Rule
// Bans returns the active bans, which every apply puts back (the
// database keeps them; nil: none).
Bans func() []render.Ban
// flowDevs are the flowtable's devices in the ruleset in force, by
// name, with their interface index then (0: left out, missing).
flowDevs map[string]int
@@ -654,6 +681,9 @@ func (f *Firewall) Apply(ctx context.Context, st *model.State) error {
usage.apply(f.adjust)
f.adjust = nil
opt.Quotas, opt.QuotaSets = usage.Quotas, usage.QuotaSets
if f.Bans != nil {
opt.Bans = f.Bans()
}
if old != nil {
opt.DeviceSets = old.DeviceSets
}
@@ -708,7 +738,16 @@ func pruneCounters(base map[string]Counter, st *model.State) {
for _, x := range st.Redirects {
ids[fmt.Sprintf("d%d", x.ID)] = true
}
for _, x := range st.BanLists {
ids[render.BanMark(x.ID)] = true
}
for mark := range base {
if strings.HasPrefix(mark, "ban") && strings.Trim(mark[3:], "0123456789") == "" {
if !ids[mark] {
delete(base, mark)
}
continue
}
obj, _, _ := strings.Cut(mark, "-") // f3-hairpin, f3-6 are f3's
if len(obj) < 2 || !strings.ContainsRune("rfsd", rune(obj[0])) || strings.Trim(obj[1:], "0123456789") != "" {
continue // b-established, p-input
@@ -791,6 +830,14 @@ func (f *Firewall) AddElement(ctx context.Context, set, addr string, timeout int
return f.Runner.AddElement(ctx, set, addr, timeout)
}
// AddBan puts a ban in force (Runner.AddBan).
func (f *Firewall) AddBan(ctx context.Context, b render.Ban) error { return f.Runner.AddBan(ctx, b) }
// DeleteBan lifts a ban (Runner.DeleteBan).
func (f *Firewall) DeleteBan(ctx context.Context, b render.Ban) error {
return f.Runner.DeleteBan(ctx, b)
}
// DeleteElement removes an entry from a dynamic set.
func (f *Firewall) DeleteElement(ctx context.Context, set, addr string) error {
return f.Runner.DeleteElement(ctx, set, addr)