Skip to content

Commit b017c1b

Browse files
authored
bdns, policy: Move reserved IP checking from bdns to policy & refactor (#8196)
Move `IsReservedIP` and its supporting vars from `bdns` to `policy`. Rewrite `IsReservedIP` to: * Use `netip` because `netip.Prefix` can be used as a map key, allowing us to define prefix lists more elegantly. This will enable future work to import prefix lists from IANA's primary source data. * Return an error including the reserved network's name. Refactor `IsReservedIP` tests to be table-based. Fixes #8040
1 parent 103ffb0 commit b017c1b

File tree

12 files changed

+192
-177
lines changed

12 files changed

+192
-177
lines changed

bdns/dns.go

Lines changed: 15 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"net"
1111
"net/http"
12+
"net/netip"
1213
"net/url"
1314
"slices"
1415
"strconv"
@@ -23,83 +24,7 @@ import (
2324
"github.com/letsencrypt/boulder/features"
2425
blog "github.com/letsencrypt/boulder/log"
2526
"github.com/letsencrypt/boulder/metrics"
26-
)
27-
28-
func parseCidr(network string, comment string) net.IPNet {
29-
_, net, err := net.ParseCIDR(network)
30-
if err != nil {
31-
panic(fmt.Sprintf("error parsing %s (%s): %s", network, comment, err))
32-
}
33-
return *net
34-
}
35-
36-
var (
37-
// TODO(#8040): Rebuild these as structs that track the structure of IANA's
38-
// CSV files, for better automated handling.
39-
//
40-
// Private CIDRs to ignore. Sourced from:
41-
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
42-
privateV4Networks = []net.IPNet{
43-
parseCidr("0.0.0.0/8", "RFC 791, Section 3.2: This network"),
44-
parseCidr("0.0.0.0/32", "RFC 1122, Section 3.2.1.3: This host on this network"),
45-
parseCidr("10.0.0.0/8", "RFC 1918: Private-Use"),
46-
parseCidr("100.64.0.0/10", "RFC 6598: Shared Address Space"),
47-
parseCidr("127.0.0.0/8", "RFC 1122, Section 3.2.1.3: Loopback"),
48-
parseCidr("169.254.0.0/16", "RFC 3927: Link Local"),
49-
parseCidr("172.16.0.0/12", "RFC 1918: Private-Use"),
50-
parseCidr("192.0.0.0/24", "RFC 6890, Section 2.1: IETF Protocol Assignments"),
51-
parseCidr("192.0.0.0/29", "RFC 7335: IPv4 Service Continuity Prefix"),
52-
parseCidr("192.0.0.8/32", "RFC 7600: IPv4 dummy address"),
53-
parseCidr("192.0.0.9/32", "RFC 7723: Port Control Protocol Anycast"),
54-
parseCidr("192.0.0.10/32", "RFC 8155: Traversal Using Relays around NAT Anycast"),
55-
parseCidr("192.0.0.170/32", "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery"),
56-
parseCidr("192.0.0.171/32", "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery"),
57-
parseCidr("192.0.2.0/24", "RFC 5737: Documentation (TEST-NET-1)"),
58-
parseCidr("192.31.196.0/24", "RFC 7535: AS112-v4"),
59-
parseCidr("192.52.193.0/24", "RFC 7450: AMT"),
60-
parseCidr("192.88.99.0/24", "RFC 7526: Deprecated (6to4 Relay Anycast)"),
61-
parseCidr("192.168.0.0/16", "RFC 1918: Private-Use"),
62-
parseCidr("192.175.48.0/24", "RFC 7534: Direct Delegation AS112 Service"),
63-
parseCidr("198.18.0.0/15", "RFC 2544: Benchmarking"),
64-
parseCidr("198.51.100.0/24", "RFC 5737: Documentation (TEST-NET-2)"),
65-
parseCidr("203.0.113.0/24", "RFC 5737: Documentation (TEST-NET-3)"),
66-
parseCidr("240.0.0.0/4", "RFC1112, Section 4: Reserved"),
67-
parseCidr("255.255.255.255/32", "RFC 8190 & RFC 919, Section 7: Limited Broadcast"),
68-
// 224.0.0.0/4 are multicast addresses as per RFC 3171. They are not
69-
// present in the IANA registry.
70-
parseCidr("224.0.0.0/4", "RFC 3171: Multicast Addresses"),
71-
}
72-
// Sourced from:
73-
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
74-
privateV6Networks = []net.IPNet{
75-
parseCidr("::/128", "RFC 4291: Unspecified Address"),
76-
parseCidr("::1/128", "RFC 4291: Loopback Address"),
77-
parseCidr("::ffff:0:0/96", "RFC 4291: IPv4-mapped Address"),
78-
parseCidr("64:ff9b::/96", "RFC 6052: IPv4-IPv6 Translat."),
79-
parseCidr("64:ff9b:1::/48", "RFC 8215: IPv4-IPv6 Translat."),
80-
parseCidr("100::/64", "RFC 6666: Discard-Only Address Block"),
81-
parseCidr("2001::/23", "RFC 2928: IETF Protocol Assignments"),
82-
parseCidr("2001::/32", "RFC 4380 & RFC 8190: TEREDO"),
83-
parseCidr("2001:1::1/128", "RFC 7723: Port Control Protocol Anycast"),
84-
parseCidr("2001:1::2/128", "RFC 8155: Traversal Using Relays around NAT Anycast"),
85-
parseCidr("2001:1::3/128", "RFC-ietf-dnssd-srp-25: DNS-SD Service Registration Protocol Anycast"),
86-
parseCidr("2001:2::/48", "RFC 5180 & RFC Errata 1752: Benchmarking"),
87-
parseCidr("2001:3::/32", "RFC 7450: AMT"),
88-
parseCidr("2001:4:112::/48", "RFC 7535: AS112-v6"),
89-
parseCidr("2001:10::/28", "RFC 4843: Deprecated (previously ORCHID)"),
90-
parseCidr("2001:20::/28", "RFC 7343: ORCHIDv2"),
91-
parseCidr("2001:30::/28", "RFC 9374: Drone Remote ID Protocol Entity Tags (DETs) Prefix"),
92-
parseCidr("2001:db8::/32", "RFC 3849: Documentation"),
93-
parseCidr("2002::/16", "RFC 3056: 6to4"),
94-
parseCidr("2620:4f:8000::/48", "RFC 7534: Direct Delegation AS112 Service"),
95-
parseCidr("3fff::/20", "RFC 9637: Documentation"),
96-
parseCidr("5f00::/16", "RFC 9602: Segment Routing (SRv6) SIDs"),
97-
parseCidr("fc00::/7", "RFC 4193 & RFC 8190: Unique-Local"),
98-
parseCidr("fe80::/10", "RFC 4291: Link-Local Unicast"),
99-
// ff00::/8 are multicast addresses as per RFC 4291, Sections 2.4 & 2.7.
100-
// They are not present in the IANA registry.
101-
parseCidr("ff00::/8", "RFC 4291: Multicast Addresses"),
102-
}
27+
"github.com/letsencrypt/boulder/policy"
10328
)
10429

10530
// ResolverAddrs contains DNS resolver(s) that were chosen to perform a
@@ -432,24 +357,6 @@ func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string
432357
return txt, ResolverAddrs{resolver}, err
433358
}
434359

435-
func isPrivateV4(ip net.IP) bool {
436-
for _, net := range privateV4Networks {
437-
if net.Contains(ip) {
438-
return true
439-
}
440-
}
441-
return false
442-
}
443-
444-
func isPrivateV6(ip net.IP) bool {
445-
for _, net := range privateV6Networks {
446-
if net.Contains(ip) {
447-
return true
448-
}
449-
}
450-
return false
451-
}
452-
453360
func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uint16) ([]dns.RR, string, error) {
454361
resp, resolver, err := dnsClient.exchangeOne(ctx, hostname, ipType)
455362
switch ipType {
@@ -474,6 +381,9 @@ func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uin
474381
// chase CNAME/DNAME aliases and return relevant records. It will retry
475382
// requests in the case of temporary network errors. It returns an error if
476383
// both the A and AAAA lookups fail or are empty, but succeeds otherwise.
384+
//
385+
// TODO(#5925): Changing from net.IP to netip.Addr could start from here
386+
// outwards.
477387
func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.IP, ResolverAddrs, error) {
478388
var recordsA, recordsAAAA []dns.RR
479389
var errA, errAAAA error
@@ -502,8 +412,11 @@ func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.I
502412
for _, answer := range recordsA {
503413
if answer.Header().Rrtype == dns.TypeA {
504414
a, ok := answer.(*dns.A)
505-
if ok && a.A.To4() != nil && (!isPrivateV4(a.A) || dnsClient.allowRestrictedAddresses) {
506-
addrsA = append(addrsA, a.A)
415+
if ok && a.A.To4() != nil {
416+
netIP, ok := netip.AddrFromSlice(a.A)
417+
if ok && (policy.IsReservedIP(netIP) == nil || dnsClient.allowRestrictedAddresses) {
418+
addrsA = append(addrsA, a.A)
419+
}
507420
}
508421
}
509422
}
@@ -517,8 +430,11 @@ func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.I
517430
for _, answer := range recordsAAAA {
518431
if answer.Header().Rrtype == dns.TypeAAAA {
519432
aaaa, ok := answer.(*dns.AAAA)
520-
if ok && aaaa.AAAA.To16() != nil && (!isPrivateV6(aaaa.AAAA) || dnsClient.allowRestrictedAddresses) {
521-
addrsAAAA = append(addrsAAAA, aaaa.AAAA)
433+
if ok && aaaa.AAAA.To16() != nil {
434+
netIP, ok := netip.AddrFromSlice(aaaa.AAAA)
435+
if ok && (policy.IsReservedIP(netIP) == nil || dnsClient.allowRestrictedAddresses) {
436+
addrsAAAA = append(addrsAAAA, aaaa.AAAA)
437+
}
522438
}
523439
}
524440
}
@@ -686,20 +602,3 @@ func (d *dohExchanger) Exchange(query *dns.Msg, server string) (*dns.Msg, time.D
686602

687603
return response, d.clk.Since(start), nil
688604
}
689-
690-
// IsReservedIP reports whether an IP address is part of a reserved range.
691-
//
692-
// TODO(#7311): Once we're fully ready to issue for IP address identifiers, dev
693-
// environments should have a way to bypass this check for their own Private-Use
694-
// IP addresses. Maybe plumb the DNSAllowLoopbackAddresses feature flag through
695-
// to here.
696-
//
697-
// TODO(#8040): Move this and its dependencies into the policy package. As part
698-
// of this, consider changing it to return an error and/or the description of
699-
// the reserved network.
700-
func IsReservedIP(ip net.IP) bool {
701-
if ip.To4() == nil {
702-
return isPrivateV6(ip)
703-
}
704-
return isPrivateV4(ip)
705-
}

bdns/dns_test.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -487,37 +487,6 @@ caa.example.com. 0 IN CAA 1 issue "letsencrypt.org"
487487
test.AssertEquals(t, resolvers[0], "127.0.0.1:4053")
488488
}
489489

490-
func TestIsPrivateIP(t *testing.T) {
491-
test.Assert(t, isPrivateV4(net.ParseIP("127.0.0.1")), "should be private")
492-
test.Assert(t, isPrivateV4(net.ParseIP("192.168.254.254")), "should be private")
493-
test.Assert(t, isPrivateV4(net.ParseIP("10.255.0.3")), "should be private")
494-
test.Assert(t, isPrivateV4(net.ParseIP("172.16.255.255")), "should be private")
495-
test.Assert(t, isPrivateV4(net.ParseIP("172.31.255.255")), "should be private")
496-
test.Assert(t, !isPrivateV4(net.ParseIP("128.0.0.1")), "should be private")
497-
test.Assert(t, !isPrivateV4(net.ParseIP("192.169.255.255")), "should not be private")
498-
test.Assert(t, !isPrivateV4(net.ParseIP("9.255.0.255")), "should not be private")
499-
test.Assert(t, !isPrivateV4(net.ParseIP("172.32.255.255")), "should not be private")
500-
501-
test.Assert(t, isPrivateV6(net.ParseIP("::0")), "should be private")
502-
test.Assert(t, isPrivateV6(net.ParseIP("::1")), "should be private")
503-
test.Assert(t, !isPrivateV6(net.ParseIP("::2")), "should not be private")
504-
505-
test.Assert(t, isPrivateV6(net.ParseIP("fe80::1")), "should be private")
506-
test.Assert(t, isPrivateV6(net.ParseIP("febf::1")), "should be private")
507-
test.Assert(t, !isPrivateV6(net.ParseIP("fec0::1")), "should not be private")
508-
test.Assert(t, !isPrivateV6(net.ParseIP("feff::1")), "should not be private")
509-
510-
test.Assert(t, isPrivateV6(net.ParseIP("ff00::1")), "should be private")
511-
test.Assert(t, isPrivateV6(net.ParseIP("ff10::1")), "should be private")
512-
test.Assert(t, isPrivateV6(net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
513-
514-
test.Assert(t, isPrivateV6(net.ParseIP("2002::")), "should be private")
515-
test.Assert(t, isPrivateV6(net.ParseIP("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
516-
test.Assert(t, isPrivateV6(net.ParseIP("0100::")), "should be private")
517-
test.Assert(t, isPrivateV6(net.ParseIP("0100::0000:ffff:ffff:ffff:ffff")), "should be private")
518-
test.Assert(t, !isPrivateV6(net.ParseIP("0100::0001:0000:0000:0000:0000")), "should be private")
519-
}
520-
521490
type testExchanger struct {
522491
sync.Mutex
523492
count int

cmd/boulder-va/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/letsencrypt/boulder/cmd"
1111
"github.com/letsencrypt/boulder/features"
1212
bgrpc "github.com/letsencrypt/boulder/grpc"
13+
"github.com/letsencrypt/boulder/policy"
1314
"github.com/letsencrypt/boulder/va"
1415
vaConfig "github.com/letsencrypt/boulder/va/config"
1516
vapb "github.com/letsencrypt/boulder/va/proto"
@@ -152,7 +153,7 @@ func main() {
152153
c.VA.AccountURIPrefixes,
153154
va.PrimaryPerspective,
154155
"",
155-
bdns.IsReservedIP)
156+
policy.IsReservedIP)
156157
cmd.FailOnError(err, "Unable to create VA server")
157158

158159
start, err := bgrpc.NewServer(c.VA.GRPC, logger).Add(

cmd/remoteva/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/letsencrypt/boulder/cmd"
1212
"github.com/letsencrypt/boulder/features"
1313
bgrpc "github.com/letsencrypt/boulder/grpc"
14+
"github.com/letsencrypt/boulder/policy"
1415
"github.com/letsencrypt/boulder/va"
1516
vaConfig "github.com/letsencrypt/boulder/va/config"
1617
vapb "github.com/letsencrypt/boulder/va/proto"
@@ -141,7 +142,7 @@ func main() {
141142
c.RVA.AccountURIPrefixes,
142143
c.RVA.Perspective,
143144
c.RVA.RIR,
144-
bdns.IsReservedIP)
145+
policy.IsReservedIP)
145146
cmd.FailOnError(err, "Unable to create Remote-VA server")
146147

147148
start, err := bgrpc.NewServer(c.RVA.GRPC, logger).Add(

policy/ip.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package policy
2+
3+
import (
4+
"fmt"
5+
"net/netip"
6+
)
7+
8+
var (
9+
// TODO(#8080): Rebuild these as structs that track the structure of IANA's
10+
// CSV files, for better automated handling.
11+
//
12+
// Private CIDRs to ignore. Sourced from:
13+
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
14+
privateV4Prefixes = map[netip.Prefix]string{
15+
netip.MustParsePrefix("0.0.0.0/8"): "RFC 791, Section 3.2: This network",
16+
netip.MustParsePrefix("0.0.0.0/32"): "RFC 1122, Section 3.2.1.3: This host on this network",
17+
netip.MustParsePrefix("10.0.0.0/8"): "RFC 1918: Private-Use",
18+
netip.MustParsePrefix("100.64.0.0/10"): "RFC 6598: Shared Address Space",
19+
netip.MustParsePrefix("127.0.0.0/8"): "RFC 1122, Section 3.2.1.3: Loopback",
20+
netip.MustParsePrefix("169.254.0.0/16"): "RFC 3927: Link Local",
21+
netip.MustParsePrefix("172.16.0.0/12"): "RFC 1918: Private-Use",
22+
netip.MustParsePrefix("192.0.0.0/24"): "RFC 6890, Section 2.1: IETF Protocol Assignments",
23+
netip.MustParsePrefix("192.0.0.0/29"): "RFC 7335: IPv4 Service Continuity Prefix",
24+
netip.MustParsePrefix("192.0.0.8/32"): "RFC 7600: IPv4 dummy address",
25+
netip.MustParsePrefix("192.0.0.9/32"): "RFC 7723: Port Control Protocol Anycast",
26+
netip.MustParsePrefix("192.0.0.10/32"): "RFC 8155: Traversal Using Relays around NAT Anycast",
27+
netip.MustParsePrefix("192.0.0.170/32"): "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery",
28+
netip.MustParsePrefix("192.0.0.171/32"): "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery",
29+
netip.MustParsePrefix("192.0.2.0/24"): "RFC 5737: Documentation (TEST-NET-1)",
30+
netip.MustParsePrefix("192.31.196.0/24"): "RFC 7535: AS112-v4",
31+
netip.MustParsePrefix("192.52.193.0/24"): "RFC 7450: AMT",
32+
netip.MustParsePrefix("192.88.99.0/24"): "RFC 7526: Deprecated (6to4 Relay Anycast)",
33+
netip.MustParsePrefix("192.168.0.0/16"): "RFC 1918: Private-Use",
34+
netip.MustParsePrefix("192.175.48.0/24"): "RFC 7534: Direct Delegation AS112 Service",
35+
netip.MustParsePrefix("198.18.0.0/15"): "RFC 2544: Benchmarking",
36+
netip.MustParsePrefix("198.51.100.0/24"): "RFC 5737: Documentation (TEST-NET-2)",
37+
netip.MustParsePrefix("203.0.113.0/24"): "RFC 5737: Documentation (TEST-NET-3)",
38+
netip.MustParsePrefix("240.0.0.0/4"): "RFC1112, Section 4: Reserved",
39+
netip.MustParsePrefix("255.255.255.255/32"): "RFC 8190 & RFC 919, Section 7: Limited Broadcast",
40+
// 224.0.0.0/4 are multicast addresses as per RFC 3171. They are not
41+
// present in the IANA registry.
42+
netip.MustParsePrefix("224.0.0.0/4"): "RFC 3171: Multicast Addresses",
43+
}
44+
// Sourced from:
45+
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
46+
privateV6Prefixes = map[netip.Prefix]string{
47+
netip.MustParsePrefix("::/128"): "RFC 4291: Unspecified Address",
48+
netip.MustParsePrefix("::1/128"): "RFC 4291: Loopback Address",
49+
netip.MustParsePrefix("::ffff:0:0/96"): "RFC 4291: IPv4-mapped Address",
50+
netip.MustParsePrefix("64:ff9b::/96"): "RFC 6052: IPv4-IPv6 Translat.",
51+
netip.MustParsePrefix("64:ff9b:1::/48"): "RFC 8215: IPv4-IPv6 Translat.",
52+
netip.MustParsePrefix("100::/64"): "RFC 6666: Discard-Only Address Block",
53+
netip.MustParsePrefix("2001::/23"): "RFC 2928: IETF Protocol Assignments",
54+
netip.MustParsePrefix("2001::/32"): "RFC 4380 & RFC 8190: TEREDO",
55+
netip.MustParsePrefix("2001:1::1/128"): "RFC 7723: Port Control Protocol Anycast",
56+
netip.MustParsePrefix("2001:1::2/128"): "RFC 8155: Traversal Using Relays around NAT Anycast",
57+
netip.MustParsePrefix("2001:1::3/128"): "RFC-ietf-dnssd-srp-25: DNS-SD Service Registration Protocol Anycast",
58+
netip.MustParsePrefix("2001:2::/48"): "RFC 5180 & RFC Errata 1752: Benchmarking",
59+
netip.MustParsePrefix("2001:3::/32"): "RFC 7450: AMT",
60+
netip.MustParsePrefix("2001:4:112::/48"): "RFC 7535: AS112-v6",
61+
netip.MustParsePrefix("2001:10::/28"): "RFC 4843: Deprecated (previously ORCHID)",
62+
netip.MustParsePrefix("2001:20::/28"): "RFC 7343: ORCHIDv2",
63+
netip.MustParsePrefix("2001:30::/28"): "RFC 9374: Drone Remote ID Protocol Entity Tags (DETs) Prefix",
64+
netip.MustParsePrefix("2001:db8::/32"): "RFC 3849: Documentation",
65+
netip.MustParsePrefix("2002::/16"): "RFC 3056: 6to4",
66+
netip.MustParsePrefix("2620:4f:8000::/48"): "RFC 7534: Direct Delegation AS112 Service",
67+
netip.MustParsePrefix("3fff::/20"): "RFC 9637: Documentation",
68+
netip.MustParsePrefix("5f00::/16"): "RFC 9602: Segment Routing (SRv6) SIDs",
69+
netip.MustParsePrefix("fc00::/7"): "RFC 4193 & RFC 8190: Unique-Local",
70+
netip.MustParsePrefix("fe80::/10"): "RFC 4291: Link-Local Unicast",
71+
// ff00::/8 are multicast addresses as per RFC 4291, Sections 2.4 & 2.7.
72+
// They are not present in the IANA registry.
73+
netip.MustParsePrefix("ff00::/8"): "RFC 4291: Multicast Addresses",
74+
}
75+
)
76+
77+
// IsReservedIP returns an error if an IP address is part of a reserved range.
78+
func IsReservedIP(ip netip.Addr) error {
79+
var reservedPrefixes map[netip.Prefix]string
80+
if ip.Is4() {
81+
reservedPrefixes = privateV4Prefixes
82+
} else {
83+
reservedPrefixes = privateV6Prefixes
84+
}
85+
86+
for net, name := range reservedPrefixes {
87+
if net.Contains(ip) {
88+
return fmt.Errorf("%w: %s", errIPReserved, name)
89+
}
90+
}
91+
92+
return nil
93+
}

0 commit comments

Comments
 (0)