IPv4 and IPv6 run side by side on the same laptop. Below are seven checks that show it, each one a command and its output.
Everything ran on macOS 26.6 with curl 8.7 and Python 3.14, on a home network that has both IPv4 and IPv6. My own addresses are replaced with example addresses (192.168.1.x, 198.51.100.x, 2001:db8:…). Addresses of Google and GitHub are the real ones, and they change, so yours will differ.
The seven checks, in one table
| Question | What I saw |
|---|---|
Is 216.58.198.110 the only way to write that address? | No. 3627730542 and 0xd83ac66e reach the same host |
| Who answers when the TTL is 1, and when it is 2? | Two different routers, one step apart |
| How big can one packet be? | 1,472 bytes of ping data. One more byte is refused |
| How many private addresses are there? | About 17.9 million, in three ranges, plus a fourth range for providers |
| Why does one laptop have six IPv6 addresses? | Three kinds of address, and the public ones are replaced regularly |
| How short can an IPv6 address be written? | There is one official short form, and Python prints it |
| What happens when a site has no IPv6? | The system quietly uses IPv4 |
Check 1: one address, four spellings
An IPv4 address is a 32-bit number. The dotted form is one way to print it, and the tools accept others.
python3 -c "import ipaddress; n = int(ipaddress.ip_address('216.58.198.110')); print(n, hex(n))"
ping -n -c 1 3627730542 | head -1
ping -n -c 1 0xd83ac66e | head -1
ping -n -c 1 127.1 | head -2
3627730542 0xd83ac66e
PING 3627730542 (216.58.198.110): 56 data bytes
PING 0xd83ac66e (216.58.198.110): 56 data bytes
PING 127.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.059 msoutput
Decimal and hexadecimal both land on the same host. The last line is an old rule of the address parser: when fewer than four parts are given, the final part fills all the bytes that are left. So 127.1 is 127.0.0.1, and 10.1 would be 10.0.0.1.
This matters outside of party tricks. A filter that looks for the text 127.0.0.1 in a URL does not match 127.1 or 2130706433. Parse the address first, then compare numbers.
Check 2: walking the path with the TTL
Every router lowers the TTL by one and drops the packet at zero, then reports back. -m sets the starting TTL, so I can decide which router has to drop it.
ping -n -c 1 -m 1 8.8.8.8 | sed -n 2p
ping -n -c 1 -m 2 8.8.8.8 | sed -n 2p
92 bytes from 192.168.1.254: Time to live exceeded
36 bytes from 198.51.100.1: Time to live exceededoutput
With a TTL of 1 the answer comes from the router in the house. With 2 it comes from the next machine, which belongs to the provider. With 3 I got no answer at all: that router drops the packet and stays silent, which it is allowed to do.
This is the whole idea behind traceroute. It sends packets with TTL 1, 2, 3 and so on, and writes down who complains.
Check 3: the 1500 byte limit
A link has a maximum packet size, the MTU. On Wi-Fi and Ethernet it is normally 1500 bytes.
ifconfig en0 | grep -o 'mtu [0-9]*'
mtu 1500output
-s sets how many data bytes ping sends, and -D sets the “don’t fragment” bit in the IPv4 header, so nobody on the way may cut the packet in pieces.
ping -n -c 1 -D -s 1472 216.58.198.110 | head -2
ping -n -c 1 -D -s 1473 216.58.198.110
PING 216.58.198.110 (216.58.198.110): 1472 data bytes
1480 bytes from 216.58.198.110: icmp_seq=0 ttl=119 time=6.626 ms
ping: sendto: Message too longoutput
1,472 is the limit, and the arithmetic explains it: 1,472 bytes of data, 8 bytes of ICMP header and 20 bytes of IPv4 header make exactly 1,500. One more byte and the laptop refuses to send.
IPv6 has a larger header, so the same link carries less data. ping6 prints the sum in its first line:
ping6 -n -c 1 -s 1452 2a00:1450:400e:80b::200e | head -1
PING6(1500=40+8+1452 bytes) 2001:db8:4a6f:1c2::1a2e --> 2a00:1450:400e:80b::200eoutput
40 bytes of header against 20. In IPv4 a router may split a packet that is too large for the next link. In IPv6 routers never do that. They drop the packet and send a “packet too big” message, and the sender has to make it smaller. Every IPv6 link must carry at least 1,280 bytes.
Check 4: how large the private ranges are
Python’s ipaddress module knows the special ranges, so it can answer this without a table from the web.
import ipaddress
for n in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10"):
net = ipaddress.ip_network(n)
print(f"{n:<16} {net.num_addresses:>12,} private={net.is_private} global={net.is_global}")ranges.py
10.0.0.0/8 16,777,216 private=True global=False
172.16.0.0/12 1,048,576 private=True global=False
192.168.0.0/16 65,536 private=True global=False
100.64.0.0/10 4,194,304 private=False global=Falseoutput
The first three are the private ranges from 1996: 17,891,328 addresses that any network may reuse. The middle one ends at 172.31.255.255, a detail that is easy to get wrong. 172.32.0.1 is a normal public address.
The fourth line is the interesting one. 100.64.0.0/10 is neither private nor global. It was reserved in 2012 for providers that ran out of public addresses and put their customers behind a second NAT. If the “WAN” address of your router starts with a number between 100.64 and 100.127, your home is behind two layers of translation, and opening a port on your router will not make anything reachable from outside.
Check 5: why a laptop has six IPv6 addresses
ifconfig en0 | grep inet6
inet6 fe80::1c2a:5b7e:9d04:6f31%en0 prefixlen 64 secured scopeid 0xf
inet6 fd4c:8a21:77e0:1:c3e:9a52:4d18:70b6 prefixlen 64 autoconf secured
inet6 2001:db8:4a6f:1c2:83d:6f0a:b215:9c47 prefixlen 64 autoconf secured
inet6 2001:db8:4a6f:1c2:f1a9:3c7e:52d0:8b64 prefixlen 64 deprecated autoconf temporary
inet6 2001:db8:4a6f:1c2:6d02:e8b1:47f3:a95c prefixlen 64 deprecated autoconf temporary
inet6 2001:db8:4a6f:1c2:1a2e:d470:c938:5eb7 prefixlen 64 autoconf temporaryoutput
The addresses are replaced, the flags are what my laptop printed. Reading it line by line:
fe80::is the link-local address. Every IPv6 interface has one. It only works on the local network, and routers do not forward it.fd…is a unique local address, the IPv6 relative of192.168. My router hands out this prefix for traffic that stays in the house.- The third line is the stable public address.
autoconfmeans the laptop built it on its own from the prefix the router announced. No DHCP server gave it out. - The last three are temporary addresses. The laptop makes a new one regularly and uses it for outgoing connections, so a website cannot follow the same address for months. The two marked
deprecatedare older ones that stay alive only until their open connections finish.
So the count changes during the day. On an earlier day I counted four public ones. Today there are four again, but not the same four.
Check 6: the one official short form
An IPv6 address can be shortened in more than one way, which is bad for logs and for grep. There is a recommended form, and ipaddress prints it.
import ipaddress
a = ipaddress.ip_address("2a00:1450:400e:080a:0000:0000:0000:200e")
print(a.compressed)
print(a.exploded)
print(ipaddress.ip_address("2001:db8:0:0:1:0:0:1"))
print(a.reverse_pointer)notation.py
2a00:1450:400e:80a::200e
2a00:1450:400e:080a:0000:0000:0000:200e
2001:db8::1:0:0:1
e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.a.0.8.0.e.0.0.4.0.5.4.1.0.0.a.2.ip6.arpaoutput
The rules, as far as these lines show them: leading zeros in a group are dropped, one run of zero groups becomes ::, and letters are lower case. The third line has two runs of the same length, and only the first one is shortened, because :: may appear once. Otherwise nobody could tell how many zeros each one hides.
The last line is the name used for reverse DNS: all 32 hexadecimal digits, backwards, one label each.
The sizes are worth one more look.
python3 -c "print(f'{2**64:,}'); print(f'{2**64 // 2**32:,}')"
18,446,744,073,709,551,616
4,294,967,296output
The prefixlen 64 in check 5 means the house got a /64: 18 quintillion addresses for one network. That is the whole IPv4 internet, 4.29 billion times over.
Check 7: which one does your machine pick?
When a name has both kinds of address, the client decides. curl can print the address it ended up using.
for i in 1 2 3; do curl -s -o /dev/null -w '%{remote_ip}\n' https://www.google.com/; done
2001:4860:482b:7700::
2001:4860:482b:7700::
2001:4860:482b:7700::output
IPv6, three times out of three. Modern clients try IPv6 first and fall back to IPv4 quickly if it does not answer, so a broken IPv6 path costs milliseconds and not a timeout.
Is one of them faster? I forced each one three times.
for f in 4 6; do for i in 1 2 3; do
curl -$f -s -o /dev/null -w "IPv$f connect %{time_connect}s total %{time_total}s\n" https://www.google.com/
done; done
IPv4 connect 0.009436s total 0.081064s
IPv4 connect 0.009625s total 0.086635s
IPv4 connect 0.010267s total 0.092395s
IPv6 connect 0.010162s total 0.095515s
IPv6 connect 0.009833s total 0.088751s
IPv6 connect 0.011100s total 0.089786soutput
About 10 milliseconds to connect on both, and the totals overlap. From this desk, to this site, there is no difference I can measure. That is three requests each from one network, so it says nothing about yours.
And a site that has no IPv6 address at all:
dig +short github.com AAAA | wc -l
dig +short github.com A
curl -6 -s -o /dev/null -w '%{remote_ip} HTTP %{http_code}\n' https://github.com/
0
140.82.121.3
::ffff:140.82.121.3 HTTP 200output
DNS has no AAAA record for github.com today. I expected curl -6 to fail. On this Mac it did not: the system resolver handed curl the address ::ffff:140.82.121.3. That is an IPv4-mapped address, the IPv4 address wrapped in IPv6 notation, and the connection behind it is plain IPv4. On another system the same command may end with “could not resolve host”.
It is a small picture of the whole situation. IPv6 is preferred where it exists, and wherever it does not, IPv4 is still doing the work underneath.
The adoption numbers, yourself
Google publishes how many of its users reach it over IPv6. This script reads those statistics, so the numbers can be checked on any later day.
import datetime, re, urllib.request
URL = "https://www.google.com/intl/en/ipv6/statistics/data/adoption.js"
text = urllib.request.urlopen(URL, timeout=30).read().decode()
rows = [(datetime.date(int(y), int(m) + 1, int(d)), float(p)) # months in the file start at 0
for y, m, d, p in re.findall(r"\[(\d+),\s*(\d+),\s*(\d+),\s*([\d.]+),", text)]
for level in (1, 10, 25, 40, 50):
first = next((day for day, pct in rows if pct >= level), None)
print(f"first day at {level:>2}%: {first if first else 'not yet'}")
print()
for day, pct in rows[-7:]:
print(f"{day} {day:%a} {pct:5.1f}% " + "#" * round(pct / 2))adoption.py
One note on what it measures: the share of Google’s users who reach Google over IPv6. It is a good sample of the internet, and it is still a sample.
What I would take from this
- Compare addresses as numbers. The same IPv4 address has several valid spellings.
- 1,500 bytes is the size to remember. IPv4 leaves 1,480 of it after its header, IPv6 leaves 1,460.
- A router address in
100.64.0.0/10means a second NAT at the provider. - An IPv6 host has several addresses with different jobs, and the public ones rotate. Do not put them in an allow list.
- Print IPv6 addresses in the compressed form before you store or search them.
Sources
- RFC 791, Internet Protocol
- RFC 1918, Address Allocation for Private Internets
- RFC 6598, Shared Address Space (100.64.0.0/10)
- RFC 8200, IPv6
- RFC 4291, IPv6 Addressing Architecture
- RFC 5952, A Recommendation for IPv6 Address Text Representation
- RFC 8981, Temporary Address Extensions in IPv6
- RFC 8305, Happy Eyeballs Version 2
- Python
ipaddressmodule - Google IPv6 statistics