// Package iprange handles address ranges such as 10.0.10.101-10.0.10.199 // or 2001:db8::100-2001:db8::1ff. BIND matches only addresses and // prefixes, so Jarl keeps a range as it was written and turns it into the // exact set of prefixes that covers it when it writes named.conf. package iprange import ( "fmt" "math/big" "net/netip" "strings" ) // Range is an inclusive range of addresses of one family. type Range struct { Start, End netip.Addr } // String returns the range as Jarl writes it: START-END. func (r Range) String() string { return r.Start.String() + "-" + r.End.String() } // Parse parses START-END, both full addresses of the same family. ok is // false when s isn't meant as a range (no "-", or text before it that // isn't an address, such as the ACL name "branch-office"); err is set when // it is meant as one but is wrong. func Parse(s string) (r Range, ok bool, err error) { left, right, found := strings.Cut(s, "-") if !found { return Range{}, false, nil } left, right = strings.TrimSpace(left), strings.TrimSpace(right) if !looksLikeAddr(left) { return Range{}, false, nil } start, err := netip.ParseAddr(left) if err != nil { return Range{}, true, fmt.Errorf("range %q: %q is not a full IP address", s, left) } end, err := netip.ParseAddr(right) if err != nil { if right == "" { return Range{}, true, fmt.Errorf("range %q has no end address", s) } return Range{}, true, fmt.Errorf("range %q: %q is not a full IP address (write both ends in full, e.g. 10.0.10.101-10.0.10.199)", s, right) } if start.Zone() != "" || end.Zone() != "" { return Range{}, true, fmt.Errorf("range %q: addresses with a zone (%%interface) can't be matched", s) } if start.Is4() != end.Is4() { return Range{}, true, fmt.Errorf("range %q mixes IPv4 and IPv6", s) } if end.Less(start) { return Range{}, true, fmt.Errorf("range %q: the start is after the end", s) } return Range{Start: start, End: end}, true, nil } // looksLikeAddr reports whether s could only be meant as an address: it // has a colon, or it is digits and dots with at least one dot. func looksLikeAddr(s string) bool { if strings.Contains(s, ":") { return true } if !strings.Contains(s, ".") { return false } for i := 0; i < len(s); i++ { if c := s[i]; !(c >= '0' && c <= '9' || c == '.') { return false } } return true } // Prefixes returns the fewest prefixes that together cover exactly the // range, in address order: 10.0.10.101-10.0.10.199 is 10.0.10.101/32, // 10.0.10.102/31, 10.0.10.104/29, 10.0.10.112/28, 10.0.10.128/26 and // 10.0.10.192/29. That is at most 62 prefixes for IPv4 and 254 for IPv6. func (r Range) Prefixes() []netip.Prefix { var out []netip.Prefix a := r.Start for { // The largest prefix that starts at a and ends at or before End. p := netip.PrefixFrom(a, a.BitLen()) for bits := a.BitLen() - 1; bits >= 0; bits-- { q := netip.PrefixFrom(a, bits) if q.Masked().Addr() != a || r.End.Less(last(q)) { break } p = q } out = append(out, p) end := last(p) if end == r.End { return out } a = end.Next() } } // last returns the last address of a prefix. func last(p netip.Prefix) netip.Addr { b := p.Addr().AsSlice() for i := p.Bits(); i < len(b)*8; i++ { b[i/8] |= 0x80 >> (i % 8) } a, _ := netip.AddrFromSlice(b) return a } // Size returns the number of addresses in the range. func (r Range) Size() *big.Int { s := new(big.Int).SetBytes(r.Start.AsSlice()) e := new(big.Int).SetBytes(r.End.AsSlice()) return e.Sub(e, s).Add(e, big.NewInt(1)) } // SizeText describes the size for people: "99 addresses", or a power of // two for very large IPv6 ranges ("about 2^80 addresses"). func (r Range) SizeText() string { n := r.Size() if n.BitLen() <= 40 { if n.Int64() == 1 { return "1 address" } return n.String() + " addresses" } return fmt.Sprintf("about 2^%d addresses", n.BitLen()-1) } // Mapped4 is the IPv4-mapped IPv6 space; BIND can see IPv4 clients as // addresses in it. var Mapped4 = netip.MustParsePrefix("::ffff:0:0/96") // TouchesMapped4 reports whether an IPv6 range overlaps ::ffff:0:0/96. func (r Range) TouchesMapped4() bool { if r.Start.Is4() { return false } lo, hi := Mapped4.Addr(), last(Mapped4) return !(r.End.Less(lo) || hi.Less(r.Start)) } // FromPrefix returns the addresses of a prefix as a range. func FromPrefix(p netip.Prefix) Range { p = p.Masked() return Range{Start: p.Addr(), End: last(p)} }