---
title: IPv6
source: 'https://academia.sh/en/courses/network-models/ipv6'
course: 'Network Models and Protocols'
language: en
updated: '2026-08-17T18:07:09+00:00'
license: 'CC BY-SA 4.0'
---

# IPv6

The notation rules of the 128-bit address, address types, autoconfiguration, header differences, and how the two versions interoperate.

The previous two lessons built methods for coping with the scarcity of the 32-bit
address space: splitting private addresses carefully, giving each section exactly what
it needs. These methods manage the scarcity; they do not remove it.

The solution aimed at the scarcity itself is expanding the address space. This lesson's
question is what decisions come bundled with quadrupling the address width — because
address width sits at the neck of the hourglass introduced in the previous topic, and
touching it affects everything.

## 128 Bits

An IPv6 address is 128 bits. The total number of addresses is $2^{128}$, roughly
$3.4 \times 10^{38}$. For comparison: IPv4's $2^{32}$ addresses are one $2^{96}$th of
IPv6's space.

Choosing such an extravagant width was not meant to maximize the address count. The goal
is to **ease hierarchical distribution**: when addresses are abundant, every level can
be given generous blocks, blocks can be kept contiguous, and prefixes can be summarized.
In IPv4, the effort to conserve addresses had led to fragmented routing tables.

The visible consequence of this is that IPv6 subnets are **always `/64`**. Regardless of
how many hosts a subnet has — even if it is only two devices — it is given a 64-bit host
portion. Viewed with IPv4 habits, this is wasteful; in IPv6's design, it is the
precondition for autoconfiguration.

## Notation Rules

The address is written as eight hexadecimal groups separated by colons. There are three
shortening rules:

1. Leading zeros in each group can be dropped: `0db8` → `db8`, `0000` → `0`.
2. Consecutive zero groups can be dropped once with `::`.
3. `::` can be used **at most once** in an address; using it twice would leave it
   ambiguous how many groups each gap stands for.

```python
import ipaddress

for text in ("2001:0db8:0010:0003:0000:0000:0000:0001",
             "fe80:0000:0000:0000:0200:5eff:fe00:5301",
             "ff02:0000:0000:0000:0000:0000:0000:0001",
             "0000:0000:0000:0000:0000:0000:0000:0001",
             "2001:0db8:0000:0000:0001:0000:0000:0001"):
    a = ipaddress.ip_address(text)
    print(f"{text} -> {a.compressed}")
```

```
2001:0db8:0010:0003:0000:0000:0000:0001 -> 2001:db8:10:3::1
fe80:0000:0000:0000:0200:5eff:fe00:5301 -> fe80::200:5eff:fe00:5301
ff02:0000:0000:0000:0000:0000:0000:0001 -> ff02::1
0000:0000:0000:0000:0000:0000:0000:0001 -> ::1
2001:0db8:0000:0000:0001:0000:0000:0001 -> 2001:db8::1:0:0:1
```

The last line shows the third rule: the address has two separate runs of zeros, and only
the **longer one** is compressed. The shorter one must be written as `0:0`. If the two
runs are equal in length, the left one is compressed.

When written together with a port number, the address is enclosed in square brackets:
`[2001:db8:10:3::1]:443`. Otherwise it cannot be told whether the final colon belongs to
the address or the port.

## Address Types

IPv6 has **no broadcast address**. Broadcast's function is met by multicast instead;
this gives an "ask those who care" behavior in place of "ask everyone," and it reduces
unnecessary processing.

| Prefix | Type | Scope |
|---|---|---|
| `2000::/3` | Global unicast address | Global Internet |
| `fe80::/10` | Link-local | Same link only |
| `fc00::/7` | Unique local address | Within the organization |
| `ff00::/8` | Multicast | Embedded in prefix |
| `::1/128` | Loopback | The machine itself |
| `::/128` | Unspecified address | "I have no address" in the source field |
| `2001:db8::/32` | Documentation | Not routed |

A **link-local address** is present on every IPv6 interface and is mandatory; it exists
even without a global address. Router advertisements and neighbor discovery run over
these addresses. Because the address is only meaningful within a single link, the same
link-local address can repeat on different interfaces; this is why the interface must be
specified when it is used.

A **unique local address** is the counterpart of IPv4's private-use blocks: it is used
within the organization and is not routed on the public Internet.

In multicast addresses, the prefix also carries the scope. `ff02::1` denotes all nodes
within link scope, `ff02::2` all routers within link scope.

The **solicited-node multicast address** is the mechanism that solves ARP's broadcast
problem: the target address's last 24 bits are appended to the `ff02::1:ff00:0/104`
prefix. The neighbor discovery query is sent to this address, and only the small number
of interfaces whose last 24 bits match process the message. This prevents every device
in the broadcast domain from having to process every query.

## Autoconfiguration

IPv6 interfaces can configure an address without a server. **Stateless address
autoconfiguration (SLAAC)** is four steps:

1. The interface generates itself a **link-local address** (`fe80::` prefix + interface
   identifier).
2. It performs **duplicate address detection**: it sends a neighbor request to the
   solicited-node group of the address it generated. If a reply arrives, the address is
   in use.
3. It sends a **router solicitation** to `ff02::2`. The router replies with a **router
   advertisement** containing the `/64` prefix used on the link.
4. The interface combines the advertised prefix with its own interface identifier to
   build its global address.

One way to generate the interface identifier is the **EUI-64** conversion: `ff:fe` is
inserted into the middle of the 48-bit MAC address, and the universal/local bit is
flipped.

```python
import ipaddress


def eui64(mac: str) -> str:
    b = [int(x, 16) for x in mac.split(":")]
    b[0] ^= 0x02                       # flip the universal/local bit
    raw = bytes(b[:3] + [0xFF, 0xFE] + b[3:])
    return ":".join(raw[i:i + 2].hex() for i in range(0, 8, 2))


interface_id = eui64("00:00:5e:00:53:01")
print("interface identifier:", interface_id)
print("link-local address  :", ipaddress.ip_address("fe80::" + interface_id))
print("global address       :", ipaddress.ip_address("2001:db8:10:3:" + interface_id))
```

```
interface identifier: 0200:5eff:fe00:5301
link-local address  : fe80::200:5eff:fe00:5301
global address       : 2001:db8:10:3:200:5eff:fe00:5301
```

The example network's client had MAC address `00:00:5e:00:53:01`; the first byte's
second bit was flipped to make it `02`, and `ff:fe` was inserted in the middle. The
reason for the `/64` subnet requirement is visible here: the interface identifier is 64
bits.

The EUI-64 conversion has a privacy problem: because the address embeds the hardware
address, the machine can be tracked even as it moves from network to network. For this
reason, end hosts mostly generate the interface identifier randomly and rotate it
periodically. Servers, by contrast, use fixed and readable identifiers — manually
assigned addresses like `2001:db8:10:4::20` are common.

SLAAC does not carry name server information. That information is delivered either
through an option in the router advertisement or through the stateless leasing protocol
defined for IPv6.

## Header Differences

The IPv6 header is a fixed 40 bytes; it is larger than IPv4's 20-byte header, but it has
fewer fields and no variable length.

| Change | Rationale |
|---|---|
| Header checksum removed | The link layer and transport layer already check; recomputing at every hop is redundant |
| Fragmentation at routers removed | The cost of fragmentation is pushed to the endpoints; an intermediate node drops the packet and reports it |
| Header length fixed | The processing pipeline is simplified |
| Options moved to extension headers | They are appended as a chain, not bloating the main header |
| Minimum MTU raised to 1280 bytes | Reduces the need for path MTU discovery |

Removing the checksum is a direct application of the end-to-end principle: the check
performed at the intermediate node was not necessary for correctness — it only added
cost.

## The Example Network's IPv6 Plan

The organization has received a `2001:db8:10::/48` allocation from its provider. This
allocation contains 65,536 `/64` subnets — far more than enough for the organization's
five networks.

```python
import ipaddress

allocation = ipaddress.ip_network("2001:db8:10::/48")
plan = {
    "Guest wireless": "2001:db8:10:1::/64",
    "Lab": "2001:db8:10:2::/64",
    "Administration": "2001:db8:10:3::/64",
    "Server room": "2001:db8:10:4::/64",
    "Link": "2001:db8:10:ffff::/64",
}
print("allocation:", allocation, "— /64 subnets inside:", 2 ** (64 - allocation.prefixlen))
for name, cidr in plan.items():
    net = ipaddress.ip_network(cidr)
    print(f"{name:18s} {str(net):24s} inside allocation={net.subnet_of(allocation)}")
```

```
allocation: 2001:db8:10::/48 — /64 subnets inside: 65536
Guest wireless     2001:db8:10:1::/64       inside allocation=True
Lab                2001:db8:10:2::/64       inside allocation=True
Administration     2001:db8:10:3::/64       inside allocation=True
Server room        2001:db8:10:4::/64       inside allocation=True
Link               2001:db8:10:ffff::/64    inside allocation=True
```

The contrast between the two plans is instructive. In the IPv4 plan, every bit was
counted, spare addresses were noted one by one, and growth headroom was a constraint. In
the IPv6 plan, host count never entered the calculation; the only decision was which
subnet number to assign to which section. The subnet numbers were chosen for
readability — giving the point-to-point link `ffff` is a stylistic choice, starting from
the end of the allocation.

## Interoperability of the Two Versions

IPv4 and IPv6 **do not talk to each other directly**: different address widths,
different headers, different neighbor discovery. There are three approaches to
interoperability.

**Dual stack:** Endpoints and routers run both protocols. The result of name resolution
decides which version to use for a given destination. This is the simplest method; its
cost is that everything is configured and monitored twice.

**Tunneling:** An IPv6 packet is carried as the payload of an IPv4 packet. It is used
when the network in between does not know IPv6. The tunnel header lowers the MTU; this
is why the fragmentation and path MTU problems covered in the previous topic are common
in tunnels.

**Translation:** An intermediate node converts IPv6 addresses to IPv4 addresses. It
breaks end-to-end transparency and inherits the address-translation problems covered in
the next lesson.

If an endpoint has both versions, which one it picks depends on a preference rule; the
general tendency is to try IPv6 first and fall back to IPv4 on failure. This behavior
can increase latency when there is a silent failure along the IPv6 path.

## Summary

- An IPv6 address is 128 bits; the point of the width is less about raising the address
  count and more about easing hierarchical distribution and prefix summarization.
- Subnets are always `/64`; the 64-bit interface identifier is the precondition for
  autoconfiguration.
- In notation, leading zeros are dropped, and the longest run of zeros is compressed
  once with `::`.
- There is no broadcast address; its function is met by multicast and the
  solicited-node group.
- Stateless autoconfiguration consists of generating a link-local address, duplicate
  address detection, router advertisement, and prefix combination.
- The header checksum and router fragmentation have been removed; the two versions
  interoperate through dual stack, tunneling, or translation.

## Next Step

This lesson used the phrase "the router drops the packet and reports it" several times.
The reporting itself is also a protocol, and it does more than announce errors:
reachability testing, path discovery, and MTU notification all run over it too. The next
lesson covers these control messages, how the time-to-live field turns into
traceroute, and why blocking these messages leads to silent failures.
