Networking in C · beginner · ~15 min

IPv6 mechanics — addresses and dual-stack

Bind, connect, and parse IPv6 alongside IPv4 cleanly.

Overview

IPv6 addresses are 128 bits wide. This is the main difference from IPv4, which uses 32 bits.

IPv6 also uses a slightly different address structure (sockaddr). Apart from that, the Berkeley sockets API works the same way.

One helper covers both protocols: inet_pton(AF_INET6, ...) parses IPv6 text into binary form.

Why it matters

The modern internet is dual-stack: most hosts speak both IPv4 and IPv6.

This has two practical consequences:

  • A server that listens on IPv4 only is unreachable for clients that have only IPv6. You miss part of your audience.
  • A parser that breaks on IPv6 input is a security bug waiting to happen (a future CVE).

Core concepts

Address types

IPv6 reserves several ranges for specific uses:

  • Loopback ::1 — the host itself (like 127.0.0.1).
  • Link-local fe80::/10 — valid only on a single network link.
  • Documentation 2001:db8::/32 — reserved for examples, never routed.
  • ULA fc00::/7 — Unique Local Addresses, the IPv6 equivalent of private ranges like 10.0.0.0/8.
  • Multicast ff00::/8 — one-to-many delivery.

Scope IDs

Link-local addresses are ambiguous on their own, because the same address can exist on several interfaces. A scope ID removes the ambiguity by naming the interface.

Example: fe80::1%eth0. Here %eth0 is the scope — an interface name or index.

Dual-stack

A single AF_INET6 socket can also accept IPv4 connections. They arrive as v4-mapped addresses, written ::ffff:1.2.3.4.

This happens unless you turn it off with the socket option IPV6_V6ONLY=1.

Pentester mindset

IPv6 adds new private and internal ranges. Allow-lists and block-lists must account for them.

Blocking 10.0.0.0/8 is not enough. Also block fc00::/7 (ULA) and fe80::/10 (link-local).

Defensive coding habit

When you bind a server, be explicit:

  • Listen on a specific address rather than the wildcard ::, or
  • Set IPV6_V6ONLY deliberately, so you always know whether IPv4 traffic is allowed.

Syntax notes

The IPv6 address structure mirrors the IPv4 one, with extra fields for flow info and scope:

struct sockaddr_in6 {
    sa_family_t      sin6_family;
    in_port_t        sin6_port;
    uint32_t         sin6_flowinfo;
    struct in6_addr  sin6_addr;
    uint32_t         sin6_scope_id;
};

Lesson

An IPv6 address is 128 bits long. It is written as eight groups of 16-bit hexadecimal values, joined by colons.

In code, struct sockaddr_in6 mirrors struct sockaddr_in. The conversion helpers inet_pton and inet_ntop handle both address families through the same interface.

Code examples

struct sockaddr_in6 a6 = { .sin6_family = AF_INET6, .sin6_port = htons(8080) };
inet_pton(AF_INET6, "::1", &a6.sin6_addr);
int s = socket(AF_INET6, SOCK_STREAM, 0);
int v6only = 1; setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof v6only);
bind(s, (struct sockaddr *)&a6, sizeof a6);

Line by line

struct sockaddr_in6 a = { .sin6_family = AF_INET6 };
a.sin6_port = htons(8080);
inet_pton(AF_INET6, "::1", &a.sin6_addr);    /* parse loopback */
int s = socket(AF_INET6, SOCK_STREAM, 0);
int yes = 1;
setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof yes);
bind(s, (struct sockaddr *)&a, sizeof a);

Common mistakes

  • Hardcoding AF_INET. Pass AF_UNSPEC to getaddrinfo and handle whichever family it returns.
  • Forgetting IPV6_V6ONLY when you want to listen on IPv4 and IPv6 as two separate sockets.

Debugging tips

Useful commands from the shell:

  • ip -6 addr — list the host's IPv6 addresses.
  • ping6 ::1 — verify the loopback address responds.
  • nc -6 ::1 8080 — open a test connection over IPv6.

Memory safety

sin6_addr is 16 bytes (versus 4 bytes for IPv4).

Always convert with inet_pton / inet_ntop. Do not use inet_aton, which is IPv4-only and will not handle 16-byte addresses.

Real-world uses

IPv6 support is everywhere in modern infrastructure:

  • Every modern web server.
  • Every cloud provider's load balancer.
  • Every operating system released since 2010.

Practice tasks

  1. Bind to ::1 on port 8080 and accept a connection from nc -6.
  2. Parse an address string into a discriminated union that holds either a v4 or a v6 address.
  3. Reject any IPv6 result that falls in the ULA, link-local, or loopback ranges.

Summary

  • IPv6 uses 128-bit addresses, a separate sockaddr_in6, but the same sockets API.
  • Get dual-stack support with AF_UNSPEC in getaddrinfo plus the IPV6_V6ONLY toggle.
  • Convert addresses with inet_pton / inet_ntopsin6_addr is 16 bytes.
  • Do not let your parser break on IPv6 input.

Practice with these exercises