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 }