Files
abrown38andClaude Opus 5.5 500bfc7bdd 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>
2026-10-22 16:29:37 +01:00

355 lines
8.4 KiB
Go

// Package model holds Housecarl's data: the firewall configuration
// (State), users and tokens, the history of changes and the audit log.
package model
import (
"encoding/json"
"sort"
"strings"
)
// State is the whole firewall configuration. It is small and changed as a
// whole (internal/engine); the ruleset is rendered from it
// (internal/render).
type State struct {
Settings Settings `json:"settings"`
Zones []*Zone `json:"zones"`
Sets []*AddressSet `json:"sets"`
Services []*Service `json:"services"`
Rules []*Rule `json:"rules"`
Forwards []*PortForward `json:"forwards"`
SNATs []*SourceNAT `json:"snats"`
Redirects []*Redirect `json:"redirects"`
BanLists []*BanList `json:"ban_lists"`
}
// Object kinds, for changes and the history.
const (
KindSettings = "settings"
KindZone = "zone"
KindSet = "set"
KindService = "service"
KindRule = "rule"
KindForward = "forward"
KindSNAT = "snat"
KindRedirect = "redirect"
KindBanList = "banlist"
)
// Kinds lists the object kinds (not settings) in display order.
var Kinds = []string{KindZone, KindSet, KindService, KindRule, KindForward, KindSNAT, KindRedirect, KindBanList}
// Object is anything in the State with an ID.
type Object interface {
GetID() int64
Label() string
}
// NewState is an empty configuration with the default settings.
func NewState() *State {
st := &State{Settings: DefaultSettings()}
st.Fill()
return st
}
// Fill replaces nil lists with empty ones (for JSON and comparisons).
func (st *State) Fill() {
if st.Zones == nil {
st.Zones = []*Zone{}
}
if st.Sets == nil {
st.Sets = []*AddressSet{}
}
if st.Services == nil {
st.Services = []*Service{}
}
if st.Rules == nil {
st.Rules = []*Rule{}
}
if st.Forwards == nil {
st.Forwards = []*PortForward{}
}
if st.SNATs == nil {
st.SNATs = []*SourceNAT{}
}
if st.Redirects == nil {
st.Redirects = []*Redirect{}
}
if st.BanLists == nil {
st.BanLists = []*BanList{}
}
}
// Objects returns the objects of a kind.
func (st *State) Objects(kind string) []Object {
var out []Object
switch kind {
case KindZone:
for _, o := range st.Zones {
out = append(out, o)
}
case KindSet:
for _, o := range st.Sets {
out = append(out, o)
}
case KindService:
for _, o := range st.Services {
out = append(out, o)
}
case KindRule:
for _, o := range st.Rules {
out = append(out, o)
}
case KindForward:
for _, o := range st.Forwards {
out = append(out, o)
}
case KindSNAT:
for _, o := range st.SNATs {
out = append(out, o)
}
case KindRedirect:
for _, o := range st.Redirects {
out = append(out, o)
}
case KindBanList:
for _, o := range st.BanLists {
out = append(out, o)
}
}
return out
}
// Put replaces (or adds, or with data nil deletes) the object of a kind
// with an ID, from its JSON.
// Empty reports whether nothing has been configured: no objects of any
// kind. housecarld applies no ruleset for an empty configuration, so
// installing and starting it doesn't close the machine.
func (st *State) Empty() bool {
for _, k := range Kinds {
if len(st.Objects(k)) > 0 {
return false
}
}
return true
}
func (st *State) Put(kind string, id int64, data json.RawMessage) error {
switch kind {
case KindZone:
return put(&st.Zones, id, data)
case KindSet:
return put(&st.Sets, id, data)
case KindService:
return put(&st.Services, id, data)
case KindRule:
return put(&st.Rules, id, data)
case KindForward:
return put(&st.Forwards, id, data)
case KindSNAT:
return put(&st.SNATs, id, data)
case KindRedirect:
return put(&st.Redirects, id, data)
case KindBanList:
return put(&st.BanLists, id, data)
}
return &unknownKind{kind}
}
type unknownKind struct{ kind string }
func (e *unknownKind) Error() string { return "unknown kind " + e.kind }
func put[T Object](list *[]T, id int64, data json.RawMessage) error {
keep := (*list)[:0:0]
for _, o := range *list {
if o.GetID() != id {
keep = append(keep, o)
}
}
if data != nil {
var o T
if err := json.Unmarshal(data, &o); err != nil {
return err
}
keep = append(keep, o)
}
*list = keep
return nil
}
// Clone returns a deep copy.
func (st *State) Clone() *State {
b, err := json.Marshal(st)
if err != nil {
panic(err)
}
var out State
if err := json.Unmarshal(b, &out); err != nil {
panic(err)
}
out.Fill()
return &out
}
// Sort puts the objects in display order: named objects by name, rules by
// chain and position, NAT rules by position.
func (st *State) Sort() {
sort.SliceStable(st.Zones, func(i, j int) bool { return st.Zones[i].Name < st.Zones[j].Name })
sort.SliceStable(st.Sets, func(i, j int) bool { return st.Sets[i].Name < st.Sets[j].Name })
sort.SliceStable(st.Services, func(i, j int) bool { return st.Services[i].Name < st.Services[j].Name })
sort.SliceStable(st.BanLists, func(i, j int) bool { return st.BanLists[i].Name < st.BanLists[j].Name })
sort.SliceStable(st.Rules, func(i, j int) bool {
a, b := st.Rules[i], st.Rules[j]
if a.Chain != b.Chain {
return ChainOrder(a.Chain) < ChainOrder(b.Chain)
}
return a.Position < b.Position
})
sort.SliceStable(st.Forwards, func(i, j int) bool { return st.Forwards[i].Position < st.Forwards[j].Position })
sort.SliceStable(st.SNATs, func(i, j int) bool { return st.SNATs[i].Position < st.SNATs[j].Position })
sort.SliceStable(st.Redirects, func(i, j int) bool { return st.Redirects[i].Position < st.Redirects[j].Position })
}
// Renumber gives the rules of each chain, and the NAT rules of each kind,
// positions 1, 2, 3, … in their current order (after an insertion at a
// fractional position, or a deletion).
func (st *State) Renumber() {
st.Sort()
n := map[string]float64{}
for _, r := range st.Rules {
n[r.Chain]++
r.Position = n[r.Chain]
}
for i, f := range st.Forwards {
f.Position = float64(i + 1)
}
for i, s := range st.SNATs {
s.Position = float64(i + 1)
}
for i, r := range st.Redirects {
r.Position = float64(i + 1)
}
}
// Zone finds a zone by name.
func (st *State) Zone(name string) *Zone {
for _, z := range st.Zones {
if strings.EqualFold(z.Name, name) {
return z
}
}
return nil
}
// Set finds an address set by name.
func (st *State) Set(name string) *AddressSet {
for _, s := range st.Sets {
if strings.EqualFold(s.Name, name) {
return s
}
}
return nil
}
// Service finds a service by name: the configuration's, else the built-in
// catalog's.
func (st *State) Service(name string) *Service {
for _, s := range st.Services {
if strings.EqualFold(s.Name, name) {
return s
}
}
return BuiltinService(name)
}
// ByID finds an object of a kind by ID (nil if there is none).
func (st *State) ByID(kind string, id int64) Object {
for _, o := range st.Objects(kind) {
if o.GetID() == id {
return o
}
}
return nil
}
// Find finds an object of a kind by ID or name (zones, sets, services,
// port forwards, ban lists) or by ID alone (rules, source NAT, redirects).
func (st *State) Find(kind, ref string) Object {
for _, o := range st.Objects(kind) {
if itoa(o.GetID()) == ref {
return o
}
}
switch kind {
case KindZone:
if z := st.Zone(ref); z != nil {
return z
}
case KindSet:
if s := st.Set(ref); s != nil {
return s
}
case KindService:
for _, s := range st.Services {
if strings.EqualFold(s.Name, ref) {
return s
}
}
case KindForward:
for _, f := range st.Forwards {
if f.Name != "" && strings.EqualFold(f.Name, ref) {
return f
}
}
case KindBanList:
if b := st.BanList(ref); b != nil {
return b
}
}
return nil
}
// BanList finds a ban list by name.
func (st *State) BanList(name string) *BanList {
for _, b := range st.BanLists {
if strings.EqualFold(b.Name, name) {
return b
}
}
return nil
}
func itoa(n int64) string {
b, _ := json.Marshal(n)
return string(b)
}
// ExternalInterfaces lists the interfaces of the external zones.
func (st *State) ExternalInterfaces() []string {
return st.zoneInterfaces(func(z *Zone) bool { return z.External })
}
// InternalInterfaces lists the interfaces of the internal zones.
func (st *State) InternalInterfaces() []string {
return st.zoneInterfaces(func(z *Zone) bool { return !z.External })
}
func (st *State) zoneInterfaces(keep func(*Zone) bool) []string {
seen := map[string]bool{}
var out []string
for _, z := range st.Zones {
if !keep(z) {
continue
}
for _, i := range z.Interfaces {
if !seen[i] {
seen[i] = true
out = append(out, i)
}
}
}
sort.Strings(out)
return out
}