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>
355 lines
8.4 KiB
Go
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
|
|
}
|