180 lines
5.7 KiB
Go
180 lines
5.7 KiB
Go
package iana
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
_ "embed"
|
|
)
|
|
|
|
type reservedPrefix struct {
|
|
// addressFamily is "IPv4" or "IPv6".
|
|
addressFamily string
|
|
// The other fields are defined in:
|
|
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
|
|
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
|
|
addressBlock netip.Prefix
|
|
name string
|
|
rfc string
|
|
// The BRs' requirement that we not issue for Reserved IP Addresses only
|
|
// cares about presence in one of these registries, not any of the other
|
|
// metadata fields tracked by the registries. Therefore, we ignore the
|
|
// Allocation Date, Termination Date, Source, Destination, Forwardable,
|
|
// Globally Reachable, and Reserved By Protocol columns.
|
|
}
|
|
|
|
var (
|
|
reservedPrefixes []reservedPrefix
|
|
|
|
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
|
|
//go:embed data/iana-ipv4-special-registry-1.csv
|
|
ipv4Registry []byte
|
|
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
|
|
//go:embed data/iana-ipv6-special-registry-1.csv
|
|
ipv6Registry []byte
|
|
)
|
|
|
|
// init parses and loads the embedded IANA special-purpose address registry CSV
|
|
// files for all address families, panicking if any one fails.
|
|
func init() {
|
|
ipv4Prefixes, err := parseReservedPrefixFile(ipv4Registry, "IPv4")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ipv6Prefixes, err := parseReservedPrefixFile(ipv6Registry, "IPv6")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Add multicast addresses, which aren't in the IANA registries.
|
|
//
|
|
// TODO(#8237): Move these entries to IP address blocklists once they're
|
|
// implemented.
|
|
additionalPrefixes := []reservedPrefix{
|
|
{
|
|
addressFamily: "IPv4",
|
|
addressBlock: netip.MustParsePrefix("224.0.0.0/4"),
|
|
name: "Multicast Addresses",
|
|
rfc: "[RFC3171]",
|
|
},
|
|
{
|
|
addressFamily: "IPv6",
|
|
addressBlock: netip.MustParsePrefix("ff00::/8"),
|
|
name: "Multicast Addresses",
|
|
rfc: "[RFC4291]",
|
|
},
|
|
}
|
|
|
|
reservedPrefixes = slices.Concat(ipv4Prefixes, ipv6Prefixes, additionalPrefixes)
|
|
|
|
// Sort the list of reserved prefixes in descending order of prefix size, so
|
|
// that checks will match the most-specific reserved prefix first.
|
|
slices.SortFunc(reservedPrefixes, func(a, b reservedPrefix) int {
|
|
if a.addressBlock.Bits() == b.addressBlock.Bits() {
|
|
return 0
|
|
}
|
|
if a.addressBlock.Bits() > b.addressBlock.Bits() {
|
|
return -1
|
|
}
|
|
return 1
|
|
})
|
|
}
|
|
|
|
// Define regexps we'll use to clean up poorly formatted registry entries.
|
|
var (
|
|
// 2+ sequential whitespace characters. The csv package takes care of
|
|
// newlines automatically.
|
|
ianaWhitespacesRE = regexp.MustCompile(`\s{2,}`)
|
|
// Footnotes at the end, like `[2]`.
|
|
ianaFootnotesRE = regexp.MustCompile(`\[\d+\]$`)
|
|
)
|
|
|
|
// parseReservedPrefixFile parses and returns the IANA special-purpose address
|
|
// registry CSV data for a single address family, or returns an error if parsing
|
|
// fails.
|
|
func parseReservedPrefixFile(registryData []byte, addressFamily string) ([]reservedPrefix, error) {
|
|
if addressFamily != "IPv4" && addressFamily != "IPv6" {
|
|
return nil, fmt.Errorf("failed to parse reserved address registry: invalid address family %q", addressFamily)
|
|
}
|
|
if registryData == nil {
|
|
return nil, fmt.Errorf("failed to parse reserved %s address registry: empty", addressFamily)
|
|
}
|
|
|
|
reader := csv.NewReader(bytes.NewReader(registryData))
|
|
|
|
// Parse the header row.
|
|
record, err := reader.Read()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse reserved %s address registry header: %w", addressFamily, err)
|
|
}
|
|
if record[0] != "Address Block" || record[1] != "Name" || record[2] != "RFC" {
|
|
return nil, fmt.Errorf("failed to parse reserved %s address registry header: must begin with \"Address Block\", \"Name\" and \"RFC\"", addressFamily)
|
|
}
|
|
|
|
// Parse the records.
|
|
var prefixes []reservedPrefix
|
|
for {
|
|
row, err := reader.Read()
|
|
if errors.Is(err, io.EOF) {
|
|
// Finished parsing the file.
|
|
if len(prefixes) < 1 {
|
|
return nil, fmt.Errorf("failed to parse reserved %s address registry: no rows after header", addressFamily)
|
|
}
|
|
break
|
|
} else if err != nil {
|
|
return nil, err
|
|
} else if len(row) < 3 {
|
|
return nil, fmt.Errorf("failed to parse reserved %s address registry: incomplete row", addressFamily)
|
|
}
|
|
|
|
// Remove any footnotes, then handle each comma-separated prefix.
|
|
for _, prefixStr := range strings.Split(ianaFootnotesRE.ReplaceAllLiteralString(row[0], ""), ",") {
|
|
prefix, err := netip.ParsePrefix(strings.TrimSpace(prefixStr))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse reserved %s address registry: couldn't parse entry %q as an IP address prefix: %s", addressFamily, prefixStr, err)
|
|
}
|
|
|
|
prefixes = append(prefixes, reservedPrefix{
|
|
addressFamily: addressFamily,
|
|
addressBlock: prefix,
|
|
name: row[1],
|
|
// Replace any whitespace sequences with a single space.
|
|
rfc: ianaWhitespacesRE.ReplaceAllLiteralString(row[2], " "),
|
|
})
|
|
}
|
|
}
|
|
|
|
return prefixes, nil
|
|
}
|
|
|
|
// IsReservedAddr returns an error if an IP address is part of a reserved range.
|
|
func IsReservedAddr(ip netip.Addr) error {
|
|
for _, rpx := range reservedPrefixes {
|
|
if rpx.addressBlock.Contains(ip) {
|
|
return fmt.Errorf("IP address is in a reserved address block: %s: %s", rpx.rfc, rpx.name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsReservedPrefix returns an error if an IP address prefix overlaps with a
|
|
// reserved range.
|
|
func IsReservedPrefix(prefix netip.Prefix) error {
|
|
for _, rpx := range reservedPrefixes {
|
|
if rpx.addressBlock.Overlaps(prefix) {
|
|
return fmt.Errorf("IP address is in a reserved address block: %s: %s", rpx.rfc, rpx.name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|