IPv6 is usually introduced as IPv4 with longer addresses. That framing causes most deployment mistakes. The address is 128 bits instead of 32, but the bigger changes are architectural: every interface carries several addresses at once, hosts configure themselves from router announcements, ARP is replaced by ICMPv6, routers never fragment, and the protocol assumes end-to-end addressing with no NAT. Networks and applications that copy their IPv4 habits across end up with broken path MTU, rogue routers, blocked ICMP and logs that cannot identify a user.

This article builds the architecture from the packet up: the header and MTU rules, address types, a worked address plan, Neighbor Discovery, the choice between SLAAC and DHCPv6, how hosts pick a source address, the coexistence options from dual stack to IPv6-only with NAT64, and what security and application code must change. Related mechanics are covered in Happy Eyeballs and Path MTU Discovery.

Advertisement

What changed at the packet level

The IPv6 header (RFC 8200) is a fixed 40 bytes: version, traffic class, flow label, payload length, next header, hop limit, and 128-bit source and destination addresses. There is no header checksum, because link layers and transport checksums already cover errors, and no options area in the base header. Optional functions live in extension headers chained through the next-header field, such as hop-by-hop options, routing, fragment and destination options.

Two rules matter most in practice. First, routers never fragment. If a packet is too big for the next link, the router drops it and sends an ICMPv6 Packet Too Big message back; only the source may fragment, using a fragment extension header. Second, every link must carry at least 1280 bytes. Together these make ICMPv6 a required part of the data path, not a diagnostic extra: block Packet Too Big and large transfers hang while small ones work. The full behaviour is in Path MTU Discovery.

Address types and scopes

Addresses are written as eight groups of four hex digits, with leading zeros dropped and one run of zero groups compressed to ::. RFC 5952 recommends lowercase and compressing the longest run, which matters when you compare addresses as strings in logs or allowlists: normalise before comparing.

TypePrefixScope and use
Loopback::1/128The host itself
Link-localfe80::/10Every interface has one; used by Neighbor Discovery and routing protocols; never routed
Unique local (ULA)fc00::/7, in practice fd00::/8Private addressing inside a site, with a random 40-bit global ID
Global unicast (GUA)2000::/3Publicly routable
Multicastff00::/8Replaces broadcast, for example ff02::1 all nodes and ff02::2 all routers on a link
Documentation2001:db8::/32For examples, never deployed

A host normally has a link-local address, one or more global addresses, often temporary privacy addresses, and possibly a ULA, all at once on one interface. Software that assumes one address per interface will pick the wrong one or log the wrong one.

The other structural rule is that a LAN subnet is a /64. The lower 64 bits are the interface identifier, and SLAAC depends on that boundary. Using longer prefixes on host subnets breaks autoconfiguration; reserve them for point-to-point links (a /127 is common) and loopbacks (/128).

Advertisement

Worked example: planning a /48

Suppose a company receives 2001:db8:4c00::/48 for its main site. A /48 holds 65,536 /64 subnets, so the plan is not about scarcity; it is about making prefixes readable and aggregatable. Allocate on nibble (4-bit) boundaries so each hex digit means something.

  • Digit 1 of the subnet field selects the function: 0 infrastructure, 1 servers, 2 user LANs, 3 guest, f lab.
  • Digit 2 selects the building or zone.
  • Digits 3 and 4 number the VLAN, giving 256 /64s per function and zone.
import ipaddress

site = ipaddress.ip_network("2001:db8:4c00::/48")
FUNCTION = {"infra": 0x0, "servers": 0x1, "users": 0x2, "guest": 0x3, "lab": 0xF}

def subnet(function: str, zone: int, vlan: int) -> ipaddress.IPv6Network:
    assert 0 <= zone <= 0xF and 0 <= vlan <= 0xFF
    index = (FUNCTION[function] << 12) | (zone << 8) | vlan   # 16-bit subnet ID
    return ipaddress.IPv6Network((int(site.network_address) | (index << 64), 64))

print(subnet("servers", 2, 0x10))   # 2001:db8:4c00:1210::/64
print(subnet("users", 0, 0x05))     # 2001:db8:4c00:2005::/64

An engineer reading 2001:db8:4c00:1210::/64 in a firewall log now knows it is a server VLAN in zone 2 without looking anything up, and a single 2001:db8:4c00:1000::/52 rule covers all servers. Keep the plan in source control and generate firewall objects, DHCP scopes and DNS reverse zones from it.

Neighbor Discovery replaces ARP

Neighbor Discovery (RFC 4861) is a set of ICMPv6 messages that do the jobs of ARP, ICMP router discovery and redirects.

MessageICMPv6 typePurpose
Router Solicitation (RS)133A host asks routers to announce themselves now
Router Advertisement (RA)134Routers announce prefixes, flags, default route, MTU and optionally DNS servers
Neighbor Solicitation (NS)135Resolve an address to a link-layer address; also duplicate address detection
Neighbor Advertisement (NA)136Answer to NS, or an unsolicited update
Redirect137Tell a host a better first hop

Address resolution uses multicast, not broadcast. Each address joins a solicited-node multicast group, ff02::1:ff followed by the last 24 bits of the address, so an NS for one neighbour reaches only the few hosts that share those bits. Before using any new address a host runs duplicate address detection by sending an NS for it; if anyone answers, the address is not used. Neighbour unreachability detection then tracks whether each neighbour is still reachable.

The weakness is trust. Any host on the link can send an RA and become a default router, or answer an NS for someone else's address. On shared access networks, enable RA Guard (RFC 6105) and ND inspection on switches so only router ports may send RAs.

SLAAC, DHCPv6 and the RA flags

The diagram shows a host joining a link and later reaching an IPv4-only server. Steps 1 to 3 are address configuration: the host forms a link-local address, solicits an RA, and builds global addresses from the advertised prefix.

An IPv6 host joining a link, then reaching an IPv4-only server through NAT64Hostfe80:: link-localRoutersends RA1. RS to ff02::22. RA: prefix, flagsHost2001:db8:...:/64 GUA3. SLAAC + DADDNS64 resolversynthesises AAAA4. AAAA example.net?64:ff9b::c000:221NAT64 gatewayv6 -> v4, stateful5. packets to 64:ff9b::/96IPv4-only server192.0.2.336. IPv4IPv6 servernative AAAAnative pathNative IPv6 destinations are reached directly; only IPv4-only ones pay for translation.
Steps 1-3: router discovery and SLAAC with duplicate address detection. Steps 4-6: a DNS64 resolver synthesises an AAAA record inside 64:ff9b::/96 for an IPv4-only name, and a NAT64 gateway translates the traffic to IPv4. Native IPv6 destinations bypass translation.

The RA tells hosts how to configure themselves through flags.

FlagWhereMeaning
A (autonomous)Prefix information optionHosts may form addresses from this prefix with SLAAC (RFC 4862)
M (managed)RA headerAddresses are available from stateful DHCPv6
O (other)RA headerOther configuration, such as DNS, is available from DHCPv6

Note what the RA always provides and DHCPv6 never does: the default route. DHCPv6 (RFC 8415) assigns addresses and options but not routes, so a host using DHCPv6 still depends on RAs. DNS servers can be delivered in the RA itself with the RDNSS option (RFC 8106), which lets SLAAC-only networks work without DHCPv6 at all.

The choice is mostly about clients and accountability. Android does not use stateful DHCPv6 for addresses, so a network that relies only on M-flag addressing leaves Android devices without global IPv6. SLAAC with RDNSS works on all major platforms. DHCPv6 gives a central record of which client had which address, which some audit regimes want. Many enterprises run SLAAC for addressing and log neighbour tables from switches or routers to get attribution. For routers that need a whole prefix, such as a home gateway or a container host, DHCPv6 prefix delegation hands out a /56 or /60.

Interface identifiers are no longer derived from MAC addresses by default on modern systems. RFC 7217 defines stable, opaque identifiers per network, and RFC 8981 defines temporary addresses that rotate for outbound privacy. That is good for users and awkward for operators: a single client may appear under several addresses a day.

How a host chooses a source address

With several addresses on an interface, the operating system picks a source for each connection using the rules in RFC 6724: prefer the same scope as the destination, avoid deprecated addresses, prefer temporary addresses for outbound connections where the policy says so, prefer a matching label and the longest matching prefix. Destination selection follows a similar policy table when DNS returns several addresses, and by default IPv6 is preferred over IPv4.

Applications should not reimplement this. Call getaddrinfo and try the results in order, preferably with Happy Eyeballs so a broken IPv6 path costs milliseconds rather than a timeout. If you must bind a specific source, for example to match a firewall rule, bind to a stable address explicitly instead of hoping selection picks it.

Coexistence: dual stack or IPv6-only

Dual stack runs IPv4 and IPv6 side by side on every interface. It is the easiest first step and the safest for compatibility, but you operate two of everything: two address plans, two firewall policies, two sets of monitoring, and IPv4 address shortage is not relieved.

IPv6-only with NAT64 removes IPv4 from the access network. A DNS64 resolver (RFC 6147) answers a query for a name that has only an A record by synthesising an AAAA record that embeds the IPv4 address in a translation prefix, commonly the well-known 64:ff9b::/96 (RFC 6052). The host sends IPv6 packets to that address, and a stateful NAT64 gateway (RFC 6146) translates them to IPv4. In the diagram, steps 4 to 6 show this path for 192.0.2.33, which appears to the host as 64:ff9b::c000:221.

NAT64 breaks software that uses IPv4 literals or IPv4-only sockets, because no DNS lookup happens. 464XLAT (RFC 6877) fixes this with a small translator on the client, called the CLAT, that presents a private IPv4 interface to applications and translates to IPv6 locally. Hosts learn the NAT64 prefix from DNS or from the PREF64 RA option (RFC 8781). On mixed networks, DHCPv4 option 108 (RFC 8925) lets a client that can live without IPv4 tell the server so, and the server then withholds an IPv4 lease. This is how large mobile and campus networks run IPv6-mostly today.

ModelStrengthCost
Dual stackMaximum compatibilityTwo stacks to run and secure; no IPv4 savings
IPv6-only + NAT64/DNS64One stack on the access network; IPv4 only at the edgeIPv4 literals break without 464XLAT; translation gateway to scale; DNSSEC validation by clients conflicts with synthesis
IPv6-mostly (option 108 + 464XLAT)Capable clients go IPv6-only, legacy ones keep IPv4Needs CLAT support on clients and careful testing of old devices

Security and firewalling

  • No NAT is not no firewall. A stateful firewall that permits outbound and related return traffic gives the same protection NAT gave by accident in IPv4. Write the IPv6 policy explicitly; a dual-stack host with an IPv4 firewall and an open IPv6 path is a common breach route.
  • Permit essential ICMPv6. Packet Too Big, destination unreachable, time exceeded, parameter problem and the Neighbor Discovery types must pass where relevant. RFC 4890 gives filtering recommendations.
  • Guard the link. RA Guard, DHCPv6 guard and ND inspection stop rogue routers and address spoofing on access ports.
  • Protect neighbour caches. Scanning a /64 makes a router send NS messages for addresses that do not exist and can fill its neighbour cache. Rate-limit ND resolution and prefer router implementations with cache protections.
  • Rate-limit by prefix. One client can use a whole /64 of addresses, so per-/128 rate limits and blocklists are trivially evaded. Aggregate to /64, and to /56 or /48 for abusive networks.

What applications must change

Most application bugs are representation bugs. Store addresses in a 16-byte column or a native inet type, not a 15-character string. Bracket literal addresses in URLs, as in http://[2001:db8::1]:8080/, and parse host:port with a library, since a bare IPv6 address contains colons. Normalise addresses before comparing or logging. Accept AAAA records everywhere you accept A records.

For servers, listen on both families. On Linux a socket bound to :: accepts IPv4 as mapped addresses unless IPV6_V6ONLY is set, while Windows sets it by default; being explicit avoids surprises.

import socket

srv = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)   # also accept IPv4 as ::ffff:a.b.c.d
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("::", 8080))
srv.listen()
conn, (addr, port, flowinfo, scope_id) = srv.accept()
client = addr.removeprefix("::ffff:")    # log the IPv4 form for mapped clients

For clients, use getaddrinfo with AF_UNSPEC and never hard-code IPv4 literals in configuration. The TCP/IP stack article covers how the transport layer above is unchanged.

Failure modes and how to diagnose them

  • Large transfers hang. ICMPv6 Packet Too Big is filtered somewhere on the path, often in a tunnel. Test with tracepath -6 and fix the filter or clamp MSS.
  • Slow first connections. A host has an IPv6 address and default route but the path is broken, so clients wait for IPv6 to fail. Check curl -6 -v and make sure applications use Happy Eyeballs.
  • Hosts lose connectivity at random. A rogue RA, often from a misconfigured VM or tethered phone, installs a second default route. Inspect with rdisc6 from the ndisc6 tools and enable RA Guard.
  • Android has no IPv6. The network offers addresses only through stateful DHCPv6. Enable SLAAC on the prefix.

The basic Linux toolkit is ip -6 addr, ip -6 route, ip -6 neigh, ping -6, tracepath -6 and dig AAAA.

What to do next

  1. Get a prefix (a /48 per site is the common allocation) and write a nibble-aligned address plan in source control.
  2. Enable IPv6 on one network segment with SLAAC and RDNSS, RA Guard on access ports and an explicit stateful firewall policy that permits essential ICMPv6.
  3. Audit applications for IPv4 assumptions: address storage, URL parsing, logging, rate limiting and allowlists, and fix them to handle 128-bit addresses and /64 aggregation.
  4. Make servers listen on both families and confirm clients use getaddrinfo with Happy Eyeballs.
  5. Test with an IPv6-only client, then pilot IPv6-only with NAT64, DNS64 and 464XLAT for one user group.
  6. Add IPv6 to monitoring: per-family traffic, ND table size, ICMPv6 error counts and the share of clients on each family.
Key takeaway: IPv6 is a different architecture, not a longer address. Subnets are /64, hosts hold several addresses and configure themselves from router advertisements, Neighbor Discovery replaces ARP and must be protected on shared links, routers never fragment so ICMPv6 must flow, and there is no NAT, so the firewall policy has to be explicit. Plan addresses on nibble boundaries, choose SLAAC or DHCPv6 with your clients in mind, move from dual stack towards IPv6-only with NAT64 and 464XLAT, and fix application code that assumes 32-bit addresses.