Skip to content
Huseyin Babal
Go back

IPv4 and IPv6 on your own laptop: seven checks you can run

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

QuestionWhat 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:

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

Sources


Share this post:

Previous Post
A Redis cache in Node.js after the first request: five checks you can run
Next Post
Repeating MotherDuck's DuckDB 2.0 benchmarks on my own laptop