Build the mental model
IPv4 has 32 bits, about 4.3 billion addresses, and a large fraction of those were never usable. The regional registries ran out years ago. NAT bought the internet an extra two decades by letting an entire network hide behind one public address, but it did so by breaking the end-to-end assumption: a host behind NAT cannot be connected to unless something outside holds a mapping open, which is why peer-to-peer, VoIP, and games all carry elaborate traversal machinery that exists purely to work around NAT.
IPv6 uses 128 bits, written as eight groups of four hex digits separated by colons. Two rules shorten it: leading zeros in a group may be dropped, and exactly one run of consecutive all-zero groups may be replaced with a double colon. The exactly one is not a style preference, it is a parsing requirement. Two double colons in the same address would be ambiguous, because there would be no way to know how many zero groups belong on each side. One is solvable; two is not.
Every IPv6 interface also holds a link-local address starting fe80::, generated automatically and valid only on its own segment. It is never routed, and it is what neighbour discovery and router advertisements actually run over, so an interface can have a perfectly working link-local address and still have no internet connectivity at all.
Hosts get global addresses two ways. With SLAAC the router advertises a prefix and the host builds its own address, no server involved. DHCPv6 works more like IPv4's DHCP, with a server handing out specific addresses. Many networks run both.
Nobody flips a switch. The migration path is dual-stack: run both protocols, publish A and AAAA records, and let clients prefer IPv6 when it works.
IPV4 VS IPV6 ADDRESS STRUCTURE AND COMPRESSION
----------------------------------------------
IPv4 -- 32 bits, four decimal octets
203 . 0 . 113 . 42
+-----------------+---------+
| network | host | boundary set by the mask
+-----------------+---------+ (here: /24)
IPv6 -- 128 bits, eight hex groups of 16 bits
2001 : 0db8 : abcd : 1234 : 0000 : 0000 : 0000 : 0010
+--------------------------+--------------------------+
| /64 prefix | interface identifier |
+--------------------------+--------------------------+
routing prefix + subnet the host's own 64 bits
COMPRESSION, STEP BY STEP
full 2001:0db8:0000:0000:0000:ff00:0042:8329
drop zeros 2001:db8:0:0:0:ff00:42:8329
collapse 2001:db8::ff00:42:8329
^^
one run of zero groups, ONE time
INVALID 2001:db8::1::2
how many zero groups go on the left, and
how many on the right? unanswerable, so
a second '::' is forbidden outright.
ADDRESS KINDS ON ONE INTERFACE
fe80::... link-local auto-made, never routed off-link
2001:db8:... global routable, from SLAAC or DHCPv6
::1 loopback the IPv6 127.0.0.1
ff02::1 multicast all nodes on this linkConnect it to a real scenario
The dual-stack failure mode you will actually meet is a service that is slow for some users after someone adds an AAAA record.
Here is the mechanism. Publishing an AAAA record tells every client that IPv6 is available. Clients following Happy Eyeballs try IPv6 first and fall back to IPv4 if the path is broken, but that fallback costs a timeout. So users on a partially broken IPv6 path see multi-second delays on their first connection while everyone else is fine. Adding an AAAA record is therefore a commitment: the address must work from the public internet, not merely exist on the interface.
Test it as a client, not as the server. From an outside network, resolve the AAAA record and connect to that address with curl -6. If that hangs while curl -4 succeeds, your IPv6 path is broken and the record should come down until it is fixed.
The other habit worth building early is writing code that does not assume 32 bits. Parse addresses with a library rather than a regex, size database columns for 45 characters, and remember that an IPv6 address in a URL must be bracketed, as in http://[2001:db8::1]:8080/, or the colons read as a port separator. The example below uses Python's ipaddress module for compression, expansion, prefix math, and a dual-stack check.
Try the working example
import ipaddress
# Every one of these is the SAME address written differently. The rules:
# drop leading zeros in a group, and collapse ONE run of all-zero groups
# to '::'. Python always prints the canonical shortest form.
FORMS = [
"2001:0db8:0000:0000:0000:ff00:0042:8329",
"2001:db8:0:0:0:ff00:42:8329",
"2001:db8::ff00:42:8329",
]
print("all three of these are one address:")
for text in FORMS:
addr = ipaddress.IPv6Address(text)
print(" " + text.ljust(40) + " -> " + str(addr))
print("")
# exploded shows every group padded back out -- useful when you need to
# eyeball a prefix boundary.
one = ipaddress.IPv6Address("2001:db8::ff00:42:8329")
print("compressed: " + str(one))
print("exploded: " + one.exploded)
print("128 bits as an integer: " + str(int(one)))
print("")
# Why only ONE '::' is allowed: two of them would be ambiguous.
print("why only one '::':")
print(" 2001:db8::1::2 would be unparseable -- 'how many zero groups")
print(" went on the left, and how many on the right?' has no answer.")
try:
ipaddress.IPv6Address("2001:db8::1::2")
except ipaddress.AddressValueError:
print(" python agrees: AddressValueError")
print("")
# Prefix math. /64 is the near-universal subnet size, which is why the
# second half of an address is called the interface identifier.
net = ipaddress.IPv6Network("2001:db8:abcd:1234::/64")
print("network: " + str(net))
print("first addr: " + str(net[0]))
print("host bits: " + str(net.max_prefixlen - net.prefixlen))
print("addresses: " + str(net.num_addresses))
print("")
# Address categories a host actually holds at once.
CANDIDATES = [
"fe80::1c2b:3aff:fe4d:5e6f",
"2001:db8:abcd:1234::10",
"::1",
"ff02::1",
]
for text in CANDIDATES:
a = ipaddress.IPv6Address(text)
kind = "global unicast"
if a.is_link_local:
kind = "link-local (SLAAC, never routed off-link)"
elif a.is_loopback:
kind = "loopback"
elif a.is_multicast:
kind = "multicast (all-nodes)"
print(" " + text.ljust(26) + kind)
print("")
# Dual-stack: one name, two families. Code must handle both.
print("dual-stack host:")
for text in ["203.0.113.42", "2001:db8:abcd:1234::10"]:
a = ipaddress.ip_address(text)
print(" " + text.ljust(26) + "IPv" + str(a.version) +
" " + str(a.max_prefixlen) + " bits")all three of these are one address:
2001:0db8:0000:0000:0000:ff00:0042:8329 -> 2001:db8::ff00:42:8329
2001:db8:0:0:0:ff00:42:8329 -> 2001:db8::ff00:42:8329
2001:db8::ff00:42:8329 -> 2001:db8::ff00:42:8329
compressed: 2001:db8::ff00:42:8329
exploded: 2001:0db8:0000:0000:0000:ff00:0042:8329
128 bits as an integer: 42540766411282592856904265327123268393
why only one '::':
2001:db8::1::2 would be unparseable -- 'how many zero groups
went on the left, and how many on the right?' has no answer.
python agrees: AddressValueError
network: 2001:db8:abcd:1234::/64
first addr: 2001:db8:abcd:1234::
host bits: 64
addresses: 18446744073709551616
fe80::1c2b:3aff:fe4d:5e6f link-local (SLAAC, never routed off-link)
2001:db8:abcd:1234::10 global unicast
::1 loopback
ff02::1 multicast (all-nodes)
dual-stack host:
203.0.113.42 IPv4 32 bits
2001:db8:abcd:1234::10 IPv6 128 bits5-minute try-it
Add 2001:db8:0:0:1:0:0:1 to FORMS and see which run of zeros Python collapses: when there are two equal runs, which one wins? Then split the /64 network with subnets(new_prefix=68) and print how many subnets you get.
One important caution
Publishing an AAAA record before the IPv6 path actually works end-to-end: clients try IPv6 first, so users eat a fallback timeout.
Validating addresses with a hand-written regex or sizing a database column at 15 characters: IPv6 text form needs up to 45 characters and has many valid spellings.
RFC 4291 - IP Version 6 Addressing Architecture — Computer Networking