Module: DomainSanity::IP
- Defined in:
- lib/domain_sanity/ip.rb
Overview
IP address helpers.
A certificate authority has to tell three things apart: a public IP that may be certifiable, a private or otherwise reserved IP that the CA/Browser Forum Baseline Requirements forbid issuing for, and a reverse-DNS zone name (in-addr.arpa / ip6.arpa) that is not an issuable host name at all. These helpers answer all three from the public IANA special-purpose registries.
Only single host addresses count as IPs here: prefix / CIDR notation such as "10.0.0.0/8" is deliberately rejected, since a network block is not a host name or a certificate subject.
Constant Summary collapse
- RESERVED_IPV4 =
IPv4 special-purpose ranges. These are not globally routable public addresses and must not appear in a public certificate.
Source: IANA IPv4 Special-Purpose Address Registry (https://www.iana.org/assignments/iana-ipv4-special-registry). This list is maintained by hand and should be re-synced with the registry when IANA adds or removes a block.
[ "0.0.0.0/8", # "This host on this network" (RFC 1122) "10.0.0.0/8", # Private-use (RFC 1918) "100.64.0.0/10", # Shared address space / CGN (RFC 6598) "127.0.0.0/8", # Loopback (RFC 1122) "169.254.0.0/16", # Link-local (RFC 3927) "172.16.0.0/12", # Private-use (RFC 1918) "192.0.0.0/24", # IETF protocol assignments (RFC 6890) "192.0.2.0/24", # Documentation TEST-NET-1 (RFC 5737) "192.31.196.0/24", # AS112-v4 (RFC 7535) "192.52.193.0/24", # AMT (RFC 7450) "192.88.99.0/24", # 6to4 relay anycast (RFC 3068, deprecated) "192.168.0.0/16", # Private-use (RFC 1918) "192.175.48.0/24", # Direct Delegation AS112 Service (RFC 7534) "198.18.0.0/15", # Benchmarking (RFC 2544) "198.51.100.0/24", # Documentation TEST-NET-2 (RFC 5737) "203.0.113.0/24", # Documentation TEST-NET-3 (RFC 5737) "224.0.0.0/4", # Multicast (RFC 5771) "240.0.0.0/4", # Reserved for future use (RFC 1112) "255.255.255.255/32" # Limited broadcast (RFC 8190) ].freeze
- RESERVED_IPV6 =
IPv6 special-purpose ranges.
Source: IANA IPv6 Special-Purpose Address Registry (https://www.iana.org/assignments/iana-ipv6-special-registry). Re-sync with the registry when IANA adds or removes a block. Note that 2001::/23 is a superset covering several individually registered protocol blocks (Teredo, benchmarking, AMT, ORCHIDv2, and friends).
[ "::/128", # Unspecified address (RFC 4291) "::1/128", # Loopback (RFC 4291) "::ffff:0:0/96", # IPv4-mapped (RFC 4291) "64:ff9b::/96", # IPv4/IPv6 translation (RFC 6052) "64:ff9b:1::/48", # Local-use IPv4/IPv6 translation (RFC 8215) "100::/64", # Discard-only (RFC 6666) "2001::/23", # IETF protocol assignments (RFC 2928) "2001:db8::/32", # Documentation (RFC 3849) "2002::/16", # 6to4 (RFC 3056, deprecated) "2620:4f:8000::/48", # Direct Delegation AS112 Service (RFC 7534) "3fff::/20", # Documentation (RFC 9637) "5f00::/16", # Segment Routing (SRv6) SIDs (RFC 9602) "fc00::/7", # Unique local addresses (RFC 4193) "fe80::/10", # Link-local (RFC 4291) "ff00::/8" # Multicast (RFC 4291) ].freeze
- RESERVED_IPV4_NETS =
Parse the CIDR strings once so membership checks don't reparse them on every call.
RESERVED_IPV4.map { |cidr| IPAddr.new(cidr) }.freeze
- RESERVED_IPV6_NETS =
RESERVED_IPV6.map { |cidr| IPAddr.new(cidr) }.freeze
- IP_CHARS =
Characters that can appear in an IPv4 or IPv6 literal. Anything else means the string can't be an address, so we skip IPAddr.new (which signals a non-address by raising) - most host names contain a letter outside a-f and never reach that hot-path exception.
/\A[0-9a-f:.]+\z/i
Class Method Summary collapse
-
.ip?(subject) ⇒ Boolean
True when subject parses as a single IPv4 or IPv6 host address.
-
.parse(subject) ⇒ Object
Parse to an IPAddr for a single host address, or nil when the input is not one (including CIDR / prefix notation and non-strings).
-
.public?(subject) ⇒ Boolean
True when subject is a routable, non-reserved public IP.
-
.reserved?(subject) ⇒ Boolean
True when subject is a private or otherwise reserved (non-public) IP.
-
.reserved_address?(addr) ⇒ Boolean
Whether an already-parsed IPAddr falls in any reserved range.
-
.reverse_zone?(subject) ⇒ Boolean
True when subject is a reverse-DNS zone name rather than a host name.
Class Method Details
.ip?(subject) ⇒ Boolean
True when subject parses as a single IPv4 or IPv6 host address. Prefix / CIDR notation and non-strings return false.
87 88 89 |
# File 'lib/domain_sanity/ip.rb', line 87 def ip?(subject) !parse(subject).nil? end |
.parse(subject) ⇒ Object
Parse to an IPAddr for a single host address, or nil when the input is not one (including CIDR / prefix notation and non-strings).
120 121 122 123 124 125 126 127 128 |
# File 'lib/domain_sanity/ip.rb', line 120 def parse(subject) return nil unless subject.is_a?(String) return nil if subject.include?("/") return nil unless subject.match?(IP_CHARS) IPAddr.new(subject) rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError nil end |
.public?(subject) ⇒ Boolean
True when subject is a routable, non-reserved public IP. Non-IP input returns false. Parses once, unlike ip? && !reserved?.
102 103 104 105 106 107 |
# File 'lib/domain_sanity/ip.rb', line 102 def public?(subject) addr = parse(subject) return false if addr.nil? !reserved_address?(addr) end |
.reserved?(subject) ⇒ Boolean
True when subject is a private or otherwise reserved (non-public) IP. Non-IP input returns false.
93 94 95 96 97 98 |
# File 'lib/domain_sanity/ip.rb', line 93 def reserved?(subject) addr = parse(subject) return false if addr.nil? reserved_address?(addr) end |
.reserved_address?(addr) ⇒ Boolean
Whether an already-parsed IPAddr falls in any reserved range.
131 132 133 134 |
# File 'lib/domain_sanity/ip.rb', line 131 def reserved_address?(addr) nets = addr.ipv4? ? RESERVED_IPV4_NETS : RESERVED_IPV6_NETS nets.any? { |net| net.include?(addr) } end |
.reverse_zone?(subject) ⇒ Boolean
True when subject is a reverse-DNS zone name rather than a host name.
110 111 112 113 114 115 116 |
# File 'lib/domain_sanity/ip.rb', line 110 def reverse_zone?(subject) return false unless subject.is_a?(String) normalized = subject.downcase.chomp(".") normalized == "in-addr.arpa" || normalized == "ip6.arpa" || normalized.end_with?(".in-addr.arpa") || normalized.end_with?(".ip6.arpa") end |