Files
housecarl/internal/model/addr.go
T
abrown38andClaude Opus 5.5 e9a24b14e3 Refuse link-local addresses in the antispoof lists
fe80::/10 in the IPv6 antispoof list (or your prefixes) dropped the
ISP's router adverts and DHCPv6 replies in the early chain, before the
ICMPv6 and DHCPv6 client built-ins could accept them: the router lost
its IPv6 default route. Validation now refuses it
(antispoof_link_local), the settings page says so, and the importer
leaves it out with a note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:00:00 +00:00

123 lines
3.5 KiB
Go

package model
import (
"fmt"
"net/netip"
"strings"
)
// LinkLocal6 is IPv6's link-local prefix.
var LinkLocal6 = netip.MustParsePrefix("fe80::/10")
// 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)
}
if p.Addr().Is4In6() {
// Unmapping would leave an IPv4 address with IPv6 bits
// (::ffff:0:0/96 as 0.0.0.0/96).
return Address{}, fmt.Errorf("%q is an IPv4-mapped prefix, which can't be used: write IPv4 prefixes as IPv4 (10.0.0.0/8)", s)
}
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
}