Files
housecarl/internal/model/addr.go
T
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

116 lines
3.2 KiB
Go

package model
import (
"fmt"
"net/netip"
"strings"
)
// Address is a parsed address entry: one address, a prefix or a range.
type Address struct {
Prefix netip.Prefix // a single address is a /32 or /128
First, End netip.Addr // a range (First valid only for ranges)
}
// IsV6 reports whether the entry is IPv6.
func (a Address) IsV6() bool {
if a.First.IsValid() {
return a.First.Is6()
}
return a.Prefix.Addr().Is6()
}
// String writes the entry as nftables and Housecarl write it:
// "10.0.0.5", "10.0.0.0/24", "10.0.0.1-10.0.0.9".
func (a Address) String() string {
if a.First.IsValid() {
return a.First.String() + "-" + a.End.String()
}
if a.Prefix.IsSingleIP() {
return a.Prefix.Addr().String()
}
return a.Prefix.String()
}
// Contains reports whether the entry holds an address.
func (a Address) Contains(x netip.Addr) bool {
x = x.Unmap()
if a.First.IsValid() {
return x.Is4() == a.First.Is4() && !x.Less(a.First) && !a.End.Less(x)
}
return a.Prefix.Contains(x)
}
// Overlaps reports whether the entry and a prefix share an address.
func (a Address) Overlaps(p netip.Prefix) bool {
if !a.First.IsValid() {
return a.Prefix.Overlaps(p)
}
if a.First.Is4() != p.Addr().Is4() {
return false
}
// The prefix's first and last addresses against the range.
first := p.Masked().Addr()
last := first
b := last.AsSlice()
for i := p.Bits(); i < len(b)*8; i++ {
b[i/8] |= 0x80 >> (i % 8)
}
last, _ = netip.AddrFromSlice(b)
return !last.Less(a.First) && !a.End.Less(first)
}
// ParseAddress reads an address, a prefix (host bits must be zero) or a
// range, IPv4 or IPv6.
func ParseAddress(s string) (Address, error) {
s = strings.TrimSpace(s)
if a, b, ok := strings.Cut(s, "-"); ok {
first, err1 := netip.ParseAddr(strings.TrimSpace(a))
end, err2 := netip.ParseAddr(strings.TrimSpace(b))
if err1 != nil || err2 != nil || first.Is4() != end.Is4() {
return Address{}, fmt.Errorf("%q is not an address range (write it as 10.0.0.1-10.0.0.9)", s)
}
first, end = first.Unmap(), end.Unmap()
if end.Less(first) {
return Address{}, fmt.Errorf("the range %q ends before it starts", s)
}
if first == end {
return Address{Prefix: netip.PrefixFrom(first, first.BitLen())}, nil
}
return Address{First: first, End: end}, nil
}
if strings.Contains(s, "/") {
p, err := netip.ParsePrefix(s)
if err != nil {
return Address{}, fmt.Errorf("%q is not an address or prefix", s)
}
p = netip.PrefixFrom(p.Addr().Unmap(), p.Bits())
if p.Masked() != p {
return Address{}, fmt.Errorf("%q has host bits set; the prefix is %s", s, p.Masked())
}
return Address{Prefix: p}, nil
}
a, err := netip.ParseAddr(s)
if err != nil || a.Zone() != "" {
return Address{}, fmt.Errorf("%q is not an address, prefix, range or set name", s)
}
a = a.Unmap()
return Address{Prefix: netip.PrefixFrom(a, a.BitLen())}, nil
}
// IsLiteral reports whether a source/destination reference is an
// address, prefix or range rather than a set name.
func IsLiteral(ref string) bool {
_, err := ParseAddress(ref)
return err == nil
}
// NormalizeAddress rewrites an address entry in its canonical form.
func NormalizeAddress(s string) (string, error) {
a, err := ParseAddress(s)
if err != nil {
return "", err
}
return a.String(), nil
}