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>
140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
package render
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"housecarl/internal/model"
|
|
)
|
|
|
|
// Ban is an active ban, for rendering (Options.Bans): its list, the
|
|
// address banned (an IPv4 address, or an IPv6 prefix of the list's
|
|
// grouping), and for a ban of one port its protocol and port. Timeout is
|
|
// the seconds it has left (0: it doesn't end).
|
|
type Ban struct {
|
|
List int64
|
|
Addr netip.Prefix
|
|
Proto string // "tcp", "udp", or "" for every port
|
|
Port int
|
|
Timeout int
|
|
}
|
|
|
|
// BanSet is the name of a ban list's set for a family, and with svc
|
|
// true its set of bans of one port.
|
|
func BanSet(list int64, v6, svc bool) string {
|
|
name := fmt.Sprintf("ban_%d_v4", list)
|
|
if v6 {
|
|
name = fmt.Sprintf("ban_%d_v6", list)
|
|
}
|
|
if svc {
|
|
name += "_svc"
|
|
}
|
|
return name
|
|
}
|
|
|
|
// BanMark is the mark (comment "hc:ban3") of a ban list's rules.
|
|
func BanMark(list int64) string { return fmt.Sprintf("ban%d", list) }
|
|
|
|
// BanElement is the set element of a ban: "203.0.113.9", or
|
|
// "203.0.113.9 . tcp . 22".
|
|
func BanElement(b Ban) string {
|
|
e := b.Addr.Addr().String()
|
|
if b.Proto != "" {
|
|
e += fmt.Sprintf(" . %s . %d", b.Proto, b.Port)
|
|
}
|
|
return e
|
|
}
|
|
|
|
// IsBanSet reports whether a set is a ban list's.
|
|
func IsBanSet(name string) bool { return strings.HasPrefix(name, "ban_") }
|
|
|
|
// banSets declares the ban lists' sets.
|
|
func (r *renderer) banSets() {
|
|
for _, b := range r.st.BanLists {
|
|
if b.Disabled {
|
|
continue
|
|
}
|
|
obj := "ban list " + b.Name
|
|
for _, v6 := range []bool{false, true} {
|
|
r.w.emit(obj, "\tset %s { type %s; flags timeout; }", BanSet(b.ID, v6, false), addrType(v6))
|
|
r.w.emit(obj, "\tset %s { type %s . inet_proto . inet_service; flags timeout; }", BanSet(b.ID, v6, true), addrType(v6))
|
|
}
|
|
}
|
|
}
|
|
|
|
// banRules writes the drops of the ban lists that cover a chain, ahead
|
|
// of everything that accepts: a banned address's established connections
|
|
// are cut too.
|
|
func (r *renderer) banRules(chain string) {
|
|
for _, b := range r.st.BanLists {
|
|
if b.Disabled || !b.Covers(chain) {
|
|
continue
|
|
}
|
|
obj := "ban list " + b.Name
|
|
d4, d6 := r.st.SplitRefs(b.Hosts)
|
|
for _, fam := range []string{"v4", "v6"} {
|
|
dsts := []string{""}
|
|
if b.Scope == model.BanScopeHosts {
|
|
refs := d4
|
|
if fam == "v6" {
|
|
refs = d6
|
|
}
|
|
var ok bool
|
|
if dsts, ok = addrAlts(fam, "daddr", refs, true, false); !ok {
|
|
continue
|
|
}
|
|
}
|
|
key := ipKeyword(fam) + " saddr"
|
|
if fam == "v6" && b.V6Group() < 128 {
|
|
key += " and " + v6Mask(b.V6Group())
|
|
}
|
|
v6 := fam == "v6"
|
|
for _, d := range dsts {
|
|
r.banDrop(obj, b.ID, strings.Join(nonEmpty([]string{d, key + " @" + BanSet(b.ID, v6, false)}), " "))
|
|
r.banDrop(obj, b.ID, strings.Join(nonEmpty([]string{d, key + " . meta l4proto . th dport @" + BanSet(b.ID, v6, true)}), " "))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// banDrop writes one drop of a ban list, or in shadow mode counts it.
|
|
func (r *renderer) banDrop(obj string, list int64, match string) {
|
|
if r.opt.Shadow {
|
|
r.w.emit(obj, "\t\t%s counter accept comment \"hc:%s %s\"", match, BanMark(list), Would("drop"))
|
|
return
|
|
}
|
|
r.w.emit(obj, "\t\t%s counter drop comment \"hc:%s\"", match, BanMark(list))
|
|
}
|
|
|
|
// v6Mask is the mask of an IPv6 prefix length: 64 → ffff:ffff:ffff:ffff::.
|
|
func v6Mask(bits int) string {
|
|
mask := make([]byte, 16)
|
|
for i := 0; i < bits; i++ {
|
|
mask[i/8] |= 0x80 >> (i % 8)
|
|
}
|
|
m, _ := netip.AddrFromSlice(mask)
|
|
return m.String()
|
|
}
|
|
|
|
// banElements adds the active bans, with the time each has left, so a
|
|
// reload keeps them.
|
|
func (r *renderer) banElements() {
|
|
for _, ban := range r.opt.Bans {
|
|
var list *model.BanList
|
|
for _, b := range r.st.BanLists {
|
|
if b.ID == ban.List && !b.Disabled {
|
|
list = b
|
|
}
|
|
}
|
|
if list == nil || !ban.Addr.IsValid() {
|
|
continue
|
|
}
|
|
timeout := ""
|
|
if ban.Timeout > 0 {
|
|
timeout = fmt.Sprintf(" timeout %ds", ban.Timeout)
|
|
}
|
|
r.w.emit("ban list "+list.Name, "add element inet %s %s { %s%s }", Table, BanSet(list.ID, ban.Addr.Addr().Is6(), ban.Proto != ""), BanElement(ban), timeout)
|
|
}
|
|
}
|