Networking in C · beginner · ~15 min
Bind, connect, and parse IPv6 alongside IPv4 cleanly.
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.
The modern internet is dual-stack: most hosts speak both IPv4 and IPv6.
This has two practical consequences:
IPv6 reserves several ranges for specific uses:
::1 — the host itself (like 127.0.0.1).fe80::/10 — valid only on a single network link.2001:db8::/32 — reserved for examples, never routed.fc00::/7 — Unique Local Addresses, the IPv6 equivalent of private ranges like 10.0.0.0/8.ff00::/8 — one-to-many delivery.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.
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.
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).
When you bind a server, be explicit:
::, orIPV6_V6ONLY deliberately, so you always know whether IPv4 traffic is allowed.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;
};
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.
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);
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);
AF_INET. Pass AF_UNSPEC to getaddrinfo and handle whichever family it returns.IPV6_V6ONLY when you want to listen on IPv4 and IPv6 as two separate sockets.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.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.
IPv6 support is everywhere in modern infrastructure:
::1 on port 8080 and accept a connection from nc -6.sockaddr_in6, but the same sockets API.AF_UNSPEC in getaddrinfo plus the IPV6_V6ONLY toggle.inet_pton / inet_ntop — sin6_addr is 16 bytes.