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>
296 lines
8.1 KiB
Go
296 lines
8.1 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"housecarl/internal/api"
|
|
"housecarl/internal/engine"
|
|
"housecarl/internal/model"
|
|
)
|
|
|
|
// Every configuration change is an operation (model.Op): applied at once,
|
|
// or staged in a changeset and replayed when the changeset is applied.
|
|
// applyOp is the one place an operation becomes an edit.
|
|
|
|
// applyOp makes an operation's change to st. It returns the ID of the
|
|
// object it made or changed (0 for deletes and settings).
|
|
func applyOp(st *model.State, op *model.Op) (int64, error) {
|
|
switch op.Op {
|
|
case model.OpCreate:
|
|
return createOp(st, op)
|
|
case model.OpUpdate:
|
|
return updateOp(st, op)
|
|
case model.OpDelete:
|
|
return 0, deleteOp(st, op)
|
|
case model.OpMove:
|
|
return moveOp(st, op)
|
|
case model.OpSettings:
|
|
return 0, settingsOp(st, op)
|
|
case model.OpRevert:
|
|
var h model.HistoryEntry
|
|
if err := json.Unmarshal(op.Data, &h); err != nil {
|
|
return 0, badRequest("revert: %v", err)
|
|
}
|
|
return 0, engine.Undo(st, &h)
|
|
}
|
|
return 0, badRequest("unknown operation %q", op.Op)
|
|
}
|
|
|
|
func createOp(st *model.State, op *model.Op) (int64, error) {
|
|
o, err := decodeObject(op.Kind, op.Data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if svc, ok := o.(*model.Service); ok {
|
|
svc.Builtin = false
|
|
}
|
|
if rule, ok := o.(*model.Rule); ok && rule.Action == "" {
|
|
rule.Action = model.ActionAccept
|
|
}
|
|
if f, ok := o.(*model.PortForward); ok && len(f.Protocols) == 0 {
|
|
f.Protocols = []string{"tcp"}
|
|
}
|
|
if st.ByID(op.Kind, op.ID) != nil {
|
|
return 0, conflict("%s %d exists already", op.Kind, op.ID)
|
|
}
|
|
setID(o, op.ID)
|
|
if pos := position(o); pos != nil && *pos == 0 {
|
|
*pos = lastPosition(st, op.Kind, chainOf(o))
|
|
}
|
|
data, _ := json.Marshal(o)
|
|
if err := st.Put(op.Kind, op.ID, data); err != nil {
|
|
return 0, err
|
|
}
|
|
// Placed in the same change, so it is never in force anywhere else.
|
|
if len(op.Place) > 0 {
|
|
if _, err := moveOp(st, &model.Op{Op: model.OpMove, Kind: op.Kind, Ref: strconv.FormatInt(op.ID, 10), Data: op.Place}); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return op.ID, nil
|
|
}
|
|
|
|
func updateOp(st *model.State, op *model.Op) (int64, error) {
|
|
var patch map[string]json.RawMessage
|
|
if err := json.Unmarshal(op.Data, &patch); err != nil {
|
|
return 0, badRequest("%s: %v", op.Kind, err)
|
|
}
|
|
delete(patch, "id")
|
|
o, err := findObject(st, op.Kind, op.Ref)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
id := o.GetID()
|
|
// The patch's fields over the object's.
|
|
var cur map[string]json.RawMessage
|
|
b, _ := json.Marshal(o)
|
|
json.Unmarshal(b, &cur)
|
|
for k, v := range patch {
|
|
if string(v) == "null" {
|
|
delete(cur, k)
|
|
} else {
|
|
cur[k] = v
|
|
}
|
|
}
|
|
merged, _ := json.Marshal(cur)
|
|
next, err := decodeObject(op.Kind, merged)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
setID(next, id)
|
|
if old, now := o.Label(), next.Label(); old != now && op.Kind == model.KindBanList {
|
|
return 0, conflict("ban list %s can't be renamed: the API tokens and programs (Guard, fail2ban) that ban into it go by its name; make a new list instead", old)
|
|
}
|
|
if old, now := o.Label(), next.Label(); old != now && isNamed(op.Kind) {
|
|
if set, ok := o.(*model.AddressSet); ok && set.Dynamic {
|
|
return 0, conflict("set %s is dynamic: its entries, and the API tokens and programs that fill it, go by its name; make a new set instead", old)
|
|
}
|
|
renameRefs(st, op.Kind, old, now)
|
|
}
|
|
data, _ := json.Marshal(next)
|
|
return id, st.Put(op.Kind, id, data)
|
|
}
|
|
|
|
// isNamed reports whether other objects refer to a kind by name.
|
|
func isNamed(kind string) bool {
|
|
return kind == model.KindZone || kind == model.KindSet || kind == model.KindService
|
|
}
|
|
|
|
func deleteOp(st *model.State, op *model.Op) error {
|
|
o, err := findObject(st, op.Kind, op.Ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if used := usedBy(st, op.Kind, o); used != "" {
|
|
return conflict("%s %s is used by %s; change that first", op.Kind, o.Label(), used)
|
|
}
|
|
return st.Put(op.Kind, o.GetID(), nil)
|
|
}
|
|
|
|
// moveOp reorders a rule or NAT rule: before or after another, or to the
|
|
// start or end; a rule can change chain at the same time.
|
|
func moveOp(st *model.State, op *model.Op) (int64, error) {
|
|
var in api.MoveInput
|
|
if err := json.Unmarshal(op.Data, &in); err != nil {
|
|
return 0, badRequest("move: %v", err)
|
|
}
|
|
o, err := findObject(st, op.Kind, op.Ref)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
id := o.GetID()
|
|
pos := position(o)
|
|
if pos == nil {
|
|
return 0, badRequest("only rules, port forwards, source NAT rules and redirects have an order")
|
|
}
|
|
if rule, ok := o.(*model.Rule); ok && in.Chain != "" {
|
|
rule.Chain = in.Chain
|
|
}
|
|
chain := chainOf(o)
|
|
// The others in order.
|
|
var others []model.Object
|
|
for _, x := range st.Objects(op.Kind) {
|
|
if x.GetID() != id && chainOf(x) == chain {
|
|
others = append(others, x)
|
|
}
|
|
}
|
|
sort.SliceStable(others, func(i, j int) bool { return *position(others[i]) < *position(others[j]) })
|
|
place := func(i int) { // before others[i] (i == len: at the end)
|
|
switch {
|
|
case len(others) == 0:
|
|
*pos = 1
|
|
case i == 0:
|
|
*pos = *position(others[0]) - 1
|
|
case i == len(others):
|
|
*pos = *position(others[len(others)-1]) + 1
|
|
default:
|
|
*pos = (*position(others[i-1]) + *position(others[i])) / 2
|
|
}
|
|
}
|
|
index := func(ref int64) (int, error) {
|
|
for i, x := range others {
|
|
if x.GetID() == ref {
|
|
return i, nil
|
|
}
|
|
}
|
|
return 0, badRequest("%d isn't another %s of the same chain", ref, op.Kind)
|
|
}
|
|
switch {
|
|
case in.Before != 0:
|
|
i, err := index(in.Before)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
place(i)
|
|
case in.After != 0:
|
|
i, err := index(in.After)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
place(i + 1)
|
|
case in.To == "start":
|
|
place(0)
|
|
case in.To == "end" || in.Chain != "":
|
|
place(len(others))
|
|
default:
|
|
return 0, badRequest("say where: before, after, or to start or end")
|
|
}
|
|
data, _ := json.Marshal(o)
|
|
return id, st.Put(op.Kind, id, data)
|
|
}
|
|
|
|
func settingsOp(st *model.State, op *model.Op) error {
|
|
var patch map[string]json.RawMessage
|
|
if err := json.Unmarshal(op.Data, &patch); err != nil {
|
|
return badRequest("settings: %v", err)
|
|
}
|
|
var cur map[string]json.RawMessage
|
|
b, _ := json.Marshal(st.Settings)
|
|
json.Unmarshal(b, &cur)
|
|
for k, v := range patch {
|
|
// Built-ins merge one by one.
|
|
if k == "builtins" {
|
|
var cb, pb map[string]json.RawMessage
|
|
json.Unmarshal(cur[k], &cb)
|
|
if err := json.Unmarshal(v, &pb); err != nil {
|
|
return badRequest("builtins: %v", err)
|
|
}
|
|
for bk, bv := range pb {
|
|
cb[bk] = bv
|
|
}
|
|
v, _ = json.Marshal(cb)
|
|
}
|
|
cur[k] = v
|
|
}
|
|
merged, _ := json.Marshal(cur)
|
|
var next model.Settings
|
|
dec := json.NewDecoder(strings.NewReader(string(merged)))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&next); err != nil {
|
|
return badRequest("settings: %v", err)
|
|
}
|
|
st.Settings = next
|
|
return nil
|
|
}
|
|
|
|
// replay applies a changeset's operations to st, in order. An operation
|
|
// that fails is named in the error (opError).
|
|
func replay(st *model.State, ops []model.Op) error {
|
|
for i := range ops {
|
|
if _, err := applyOp(st, &ops[i]); err != nil {
|
|
return &opError{seq: ops[i].Seq, summary: ops[i].Summary, err: err}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// opError: an operation of a changeset that can't be applied (the
|
|
// objects it names changed since it was staged, say).
|
|
type opError struct {
|
|
seq int
|
|
summary string
|
|
err error
|
|
}
|
|
|
|
func (e *opError) Error() string {
|
|
return fmt.Sprintf("operation %d (%s) can't be applied: %v", e.seq, e.summary, e.err)
|
|
}
|
|
|
|
func (e *opError) Unwrap() error { return e.err }
|
|
|
|
// info is the error for the API, with the inner error's status.
|
|
func (e *opError) info() (api.ErrorInfo, int) {
|
|
info := api.ErrorInfo{Code: "conflict", Message: e.Error(), Op: e.seq}
|
|
status := http.StatusConflict
|
|
var he *httpError
|
|
if errors.As(e.err, &he) {
|
|
info.Code, status = he.code, he.status
|
|
}
|
|
if ee, ok := engine.AsError(e.err); ok {
|
|
info.Code, info.Problems, status = ee.Kind, ee.Problems, engineStatus(ee.Kind)
|
|
}
|
|
return info, status
|
|
}
|
|
|
|
// objectOf picks what an operation's answer returns from a state.
|
|
func objectOf(op *model.Op, id int64) func(st *model.State) any {
|
|
switch op.Op {
|
|
case model.OpDelete:
|
|
return nil
|
|
case model.OpSettings:
|
|
return func(st *model.State) any { return st.Settings }
|
|
}
|
|
return func(st *model.State) any {
|
|
if o := st.ByID(op.Kind, id); o != nil {
|
|
return o
|
|
}
|
|
return nil
|
|
}
|
|
}
|