Update 2022-03-23: Matt Layher created a Go issue about this.
Update 2022-04-14: In response to that issue, two weeks ago a change was committed to Go that makes
netip.ParsePrefix behave like
net.ParseCIDR: they both return an error when a zone is present. It wasn’t released in 1.18.1, but I’m guessing it’ll be in 1.18.2. So that’s great!
Does this surprise you? (Try it in the playground.)
prefix := netip.MustParsePrefix("fe80::%zone/10") addr := netip.MustParseAddr("fe80::1%zone") fmt.Println(prefix.Contains(addr)) // ==> false
netip package is better in every way than the previous
net.IP, etc., but this one design decision will probably burn someone, somewhere, sometime.
If you pass a prefix with a zone to the older
net.ParseCIDR it returns an error. If you pass a prefix with a zone to the newer
netip.ParsePrefix, it succeeds but silently discards the zone. If you then pass an IP address that is clearly contained by the original prefix – including the zone – to
netip.Prefix.Contains… it returns false!
## Why is it like this?
For what it’s worth, I helped work on the library that ultimately became Go’s net/netip and we decided we would remove zones in our CIDR prefix parser because we didn’t find any documented usage of a a CIDR like “fe80::%eth0/64” in the wild.
Which is fair, but I don’t think the resulting behaviour is ideal.
## What do the docs say?
The documentation for
netip.Prefix.Contains does make clear the behaviour (emphasis added):
Contains reports whether the network p includes ip.
An IPv4 address will not match an IPv6 prefix. A v6-mapped IPv6 address will not match an IPv4 prefix. A zero-value IP will not match any prefix. If ip has an IPv6 zone, Contains returns false, because Prefixes strip zones.
It’s good that it’s documented, but… how many people are going to read the doc for that method? Most people who use it are going to know what it means for a prefix (or CIDR) to “contain” an IP address. And many of us will already be familiar with the older
net.IPMask.Contains, which has the one-sentence documentation: “Contains reports whether the network includes ip.” And the doc for
netip.ParsePrefix says nothing about discarding the zone.
## Why do I care about this fringe thing that no one uses?
I’m writing a library that will take a configured list of prefixes/CIDRs/ranges, parse them, and then later check if incoming IPs are contained by them. And whether the IP is contained or not could lead to security-relevant decisions, so the accuracy is important.
With the older
net package, if the user tried to configure the library to use
"fe80::/10%zone", the parsing would fail and there would be an immediate error. If I switch to using
netip, the parsing will succeed but then the
Contains checks will return false and the resulting behaviour will be wrong. (The ramifications of that will depend on how the library is being used. It could mean rate-limiting a link-local IP. It could mean using a link-local IP for an access control check where it should instead be an external IP.)
So even though the Go/netip/netaddr team didn’t find any instance of a link-local-with-zone-prefix “in the wild”, I still need to code (defensively) for the possibility of it.
To be safe I’m going to have to force the
netip code to behave like the
net code: return an error from the prefix parsing code if there’s a percent sign.
## Bonus: IPv4-mapped IPv6 handling has also changed
As hinted at in the
netip.Prefix.Contains doc I quoted above…
prefix := netip.MustParsePrefix("188.8.131.52/8") // Let's check that it's working as expected addr := netip.MustParseAddr("184.108.40.206") fmt.Println(prefix.Contains(addr)) // ==> true // Now let's try the "IPv4-mapped IPv6" representation of the same address addr = netip.MustParseAddr("::ffff:220.127.116.11") fmt.Println(addr) // ==> "::ffff:18.104.22.168" fmt.Println(prefix.Contains(addr)) // ==> false! // But with the older net.IP and net.NetIP... _, cidr, _ := net.ParseCIDR("22.214.171.124/8") ip := net.ParseIP("::ffff:126.96.36.199") fmt.Println(ip) // ==> "188.8.131.52" fmt.Println(cidr.Contains(ip)) // ==> true!
(Try it in the playground.)
net code would convert IPv4-mapped IPv6 addresses to IPv4 addresses, with the result that they would be contained by IPv4 CIDRs. The new
netip code does not convert to IPv4, and the resulting address is not contained by an IPv4 prefix.
I haven’t yet thought about this enough to form a strong opinion, but it’s good to know.
Who super helpfully answered my Reddit question and I’m totally not taking a swipe at him. To be clear, I still think
netipis great and will be using it wherever I can make 1.18 the minimum Go version. ↩︎