github.com/letsencrypt/boulder@v0.20251208.0/iana/ip.go (about)

     1  package iana
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/csv"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/netip"
    10  	"regexp"
    11  	"slices"
    12  	"strings"
    13  
    14  	_ "embed"
    15  )
    16  
    17  type reservedPrefix struct {
    18  	// addressFamily is "IPv4" or "IPv6".
    19  	addressFamily string
    20  	// The other fields are defined in:
    21  	// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
    22  	// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
    23  	addressBlock netip.Prefix
    24  	name         string
    25  	rfc          string
    26  	// The BRs' requirement that we not issue for Reserved IP Addresses only
    27  	// cares about presence in one of these registries, not any of the other
    28  	// metadata fields tracked by the registries. Therefore, we ignore the
    29  	// Allocation Date, Termination Date, Source, Destination, Forwardable,
    30  	// Globally Reachable, and Reserved By Protocol columns.
    31  }
    32  
    33  var (
    34  	reservedPrefixes []reservedPrefix
    35  
    36  	// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
    37  	//go:embed data/iana-ipv4-special-registry-1.csv
    38  	ipv4Registry []byte
    39  	// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
    40  	//go:embed data/iana-ipv6-special-registry-1.csv
    41  	ipv6Registry []byte
    42  )
    43  
    44  // init parses and loads the embedded IANA special-purpose address registry CSV
    45  // files for all address families, panicking if any one fails.
    46  func init() {
    47  	ipv4Prefixes, err := parseReservedPrefixFile(ipv4Registry, "IPv4")
    48  	if err != nil {
    49  		panic(err)
    50  	}
    51  
    52  	ipv6Prefixes, err := parseReservedPrefixFile(ipv6Registry, "IPv6")
    53  	if err != nil {
    54  		panic(err)
    55  	}
    56  
    57  	reservedPrefixes = slices.Concat(ipv4Prefixes, ipv6Prefixes)
    58  
    59  	// Sort the list of reserved prefixes in descending order of prefix size, so
    60  	// that checks will match the most-specific reserved prefix first.
    61  	slices.SortFunc(reservedPrefixes, func(a, b reservedPrefix) int {
    62  		if a.addressBlock.Bits() == b.addressBlock.Bits() {
    63  			return 0
    64  		}
    65  		if a.addressBlock.Bits() > b.addressBlock.Bits() {
    66  			return -1
    67  		}
    68  		return 1
    69  	})
    70  }
    71  
    72  // Define regexps we'll use to clean up poorly formatted registry entries.
    73  var (
    74  	// 2+ sequential whitespace characters. The csv package takes care of
    75  	// newlines automatically.
    76  	ianaWhitespacesRE = regexp.MustCompile(`\s{2,}`)
    77  	// Footnotes at the end, like `[2]`.
    78  	ianaFootnotesRE = regexp.MustCompile(`\[\d+\]$`)
    79  )
    80  
    81  // parseReservedPrefixFile parses and returns the IANA special-purpose address
    82  // registry CSV data for a single address family, or returns an error if parsing
    83  // fails.
    84  func parseReservedPrefixFile(registryData []byte, addressFamily string) ([]reservedPrefix, error) {
    85  	if addressFamily != "IPv4" && addressFamily != "IPv6" {
    86  		return nil, fmt.Errorf("failed to parse reserved address registry: invalid address family %q", addressFamily)
    87  	}
    88  	if registryData == nil {
    89  		return nil, fmt.Errorf("failed to parse reserved %s address registry: empty", addressFamily)
    90  	}
    91  
    92  	reader := csv.NewReader(bytes.NewReader(registryData))
    93  
    94  	// Parse the header row.
    95  	record, err := reader.Read()
    96  	if err != nil {
    97  		return nil, fmt.Errorf("failed to parse reserved %s address registry header: %w", addressFamily, err)
    98  	}
    99  	if record[0] != "Address Block" || record[1] != "Name" || record[2] != "RFC" {
   100  		return nil, fmt.Errorf("failed to parse reserved %s address registry header: must begin with \"Address Block\", \"Name\" and \"RFC\"", addressFamily)
   101  	}
   102  
   103  	// Parse the records.
   104  	var prefixes []reservedPrefix
   105  	for {
   106  		row, err := reader.Read()
   107  		if errors.Is(err, io.EOF) {
   108  			// Finished parsing the file.
   109  			if len(prefixes) < 1 {
   110  				return nil, fmt.Errorf("failed to parse reserved %s address registry: no rows after header", addressFamily)
   111  			}
   112  			break
   113  		} else if err != nil {
   114  			return nil, err
   115  		} else if len(row) < 3 {
   116  			return nil, fmt.Errorf("failed to parse reserved %s address registry: incomplete row", addressFamily)
   117  		}
   118  
   119  		// Remove any footnotes, then handle each comma-separated prefix.
   120  		for prefixStr := range strings.SplitSeq(ianaFootnotesRE.ReplaceAllLiteralString(row[0], ""), ",") {
   121  			prefix, err := netip.ParsePrefix(strings.TrimSpace(prefixStr))
   122  			if err != nil {
   123  				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)
   124  			}
   125  
   126  			prefixes = append(prefixes, reservedPrefix{
   127  				addressFamily: addressFamily,
   128  				addressBlock:  prefix,
   129  				name:          row[1],
   130  				// Replace any whitespace sequences with a single space.
   131  				rfc: ianaWhitespacesRE.ReplaceAllLiteralString(row[2], " "),
   132  			})
   133  		}
   134  	}
   135  
   136  	return prefixes, nil
   137  }
   138  
   139  // IsReservedAddr returns an error if an IP address is part of a reserved range.
   140  func IsReservedAddr(ip netip.Addr) error {
   141  	// Strip zone from IPv6 addresses before checking
   142  	ip = ip.WithZone("")
   143  	for _, rpx := range reservedPrefixes {
   144  		if rpx.addressBlock.Contains(ip) {
   145  			return fmt.Errorf("IP address is in a reserved address block: %s: %s", rpx.rfc, rpx.name)
   146  		}
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // IsReservedPrefix returns an error if an IP address prefix overlaps with a
   153  // reserved range.
   154  func IsReservedPrefix(prefix netip.Prefix) error {
   155  	for _, rpx := range reservedPrefixes {
   156  		if rpx.addressBlock.Overlaps(prefix) {
   157  			return fmt.Errorf("IP address is in a reserved address block: %s: %s", rpx.rfc, rpx.name)
   158  		}
   159  	}
   160  
   161  	return nil
   162  }