DevToys Web Pro iconDevToys Web Proಬ್ಲಾಗ್
ಅನುವಾದ LocalePack logoLocalePack
ನಮಗೆ ರೇಟಿಂಗ್ ನೀಡಿ:
ಬ್ರೌಸರ್ ಎಕ್ಸ್ಟೆನ್ಶನ್ ಪ್ರಯತ್ನಿಸಿ:
← Back to Blog

IPv6 for Developers: Address Format, Subnetting, and Dual-Stack Code

9 min read

IPv4 exhaustion is not a future concern — it happened. Your services already receive IPv6 traffic, your cloud providers assign IPv6 addresses by default, and mobile networks prefer it. Yet most developers treat IPv6 as someone else's problem until the day a socket bind fails or a URL construction bug ships to production. Use the IPv6 Parser to inspect and normalize addresses as you work through this guide.

128 Bits: The Address Space

An IPv6 address is 128 bits, written as eight groups of four hexadecimal digits separated by colons:

2001:0db8:0000:0000:0000:ff00:0042:8329

That is 2128 possible addresses — roughly 3.4 × 1038. For perspective, IPv4 has 232 (about 4.3 billion). The IPv6 space is large enough to assign billions of addresses to every atom on Earth's surface. Scarcity is not the design constraint here; routing hierarchy and autoconfiguration are.

Shortening Rules

Typing 32 hex digits per address is impractical. Two rules compress the notation:

  • Leading zeros in each group are dropped. 0db8 becomes db8. 0042 becomes 42. 0000 becomes 0.
  • The longest consecutive run of all-zero groups is replaced with ::. This substitution may occur at most once per address. If two runs are equal length, the leftmost run is compressed.

Applying both rules to the example above:

2001:0db8:0000:0000:0000:ff00:0042:8329
 drop leading zeros per group
2001:db8:0:0:0:ff00:42:8329
 compress longest zero run with ::
2001:db8::ff00:42:8329

The double colon represents however many groups of zeros are needed to complete eight groups total. ::1 (loopback) expands to 0000:0000:0000:0000:0000:0000:0000:0001. :: alone is the all-zeros address.

Always normalize to the canonical compressed form before storing in a database or comparing strings. Two textual representations of the same address will not match a WHERE addr = ? query unless both are normalized.

Address Scopes

IPv6 addresses carry their scope in the prefix. Knowing the scope tells you immediately whether an address is routable on the public internet, confined to a local network, or only valid on a single link.

ScopePrefixIPv4 EquivalentNotes
Loopback::1/128127.0.0.1Single address, never leaves the host
Link-localfe80::/10169.254.0.0/16Auto-configured on every interface; not routed beyond the link
Unique localfc00::/710.0.0.0/8, 192.168.0.0/16Private range; routable within an organization, not on the public internet
Global unicast2000::/3Public IP addressesPublicly routable; assigned by RIRs and ISPs
Multicastff00::/8224.0.0.0/4One-to-many; used by NDP, routing protocols

Link-local addresses (fe80::/10) deserve special attention. Every IPv6 interface automatically generates one using the interface identifier derived from the MAC address or a random token. They are essential for neighbor discovery and router communication. When you see a link-local address in logs or socket output, remember it is only valid on that specific network interface — you must include a zone ID (%eth0, %en0) when specifying it in code, for example fe80::1%eth0.

Subnetting: /64 and ISP Allocations

IPv6 subnetting uses the same CIDR slash notation as IPv4, but the conventions differ significantly.

The standard LAN prefix length is /64. This is not arbitrary: stateless address autoconfiguration (SLAAC) requires exactly 64 bits for the interface identifier. Splitting a /64 into smaller subnets breaks SLAAC. You should treat /64 as the smallest unit you assign to a single network segment.

ISPs typically delegate larger blocks to customers: a /56 (256 subnets of /64) for residential connections, or a /48 (65,536 subnets of /64) for business customers. If you are writing infrastructure or provisioning code that allocates subnets, plan for /48 as the customer allocation unit.

The related article on IPv4 CIDR subnetting covers prefix math in depth — the binary logic is identical in IPv6, just applied to 128 bits instead of 32.

URL Syntax: Bracket Notation

IPv6 addresses contain colons. URLs use colons to separate the host from the port. This creates an ambiguity that RFC 2732 resolves with square brackets:

http://[2001:db8::1]:8080/path

The brackets are mandatory whenever an IPv6 address appears in the host portion of a URL. Omitting them causes parsers to misread the first colon as the host-port separator and produce a malformed URL.

// Correct
const url = new URL("http://[2001:db8::1]:8080/api/v1");
console.log(url.hostname); // "2001:db8::1"  (brackets stripped by URL API)
console.log(url.port);     // "8080"

// Wrong — throws TypeError: Failed to construct 'URL'
const bad = new URL("http://2001:db8::1:8080/api/v1");

The JavaScript URL constructor handles bracket parsing correctly. When you need to construct a URL from a raw IPv6 address string, wrap it yourself:

function buildUrl(host: string, port: number, path: string): string {
  const isIPv6 = host.includes(":");
  const hostPart = isIPv6 ? `[${host}]` : host;
  return `https://${hostPart}:${port}${path}`;
}

Dual-Stack Application Code

Dual-stack means your server accepts both IPv4 and IPv6 connections. The simplest approach is to bind your listening socket to :: (the IPv6 unspecified address) instead of 0.0.0.0. On most operating systems, a socket bound to :: also accepts IPv4 connections — the kernel maps incoming IPv4 traffic to IPv4-mapped IPv6 addresses automatically.

import socket

# Bind to :: — accepts both IPv4 and IPv6 on most OS defaults
srv = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
srv.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)  # 0 = dual-stack
srv.bind(("", 8080))   # empty string binds to ::
srv.listen()

The IPV6_V6ONLY socket option controls this behavior:

  • IPV6_V6ONLY = 0 — the socket accepts both IPv4 and IPv6 (dual-stack). IPv4 clients appear as IPv4-mapped IPv6 addresses.
  • IPV6_V6ONLY = 1 — the socket accepts only IPv6. You must create a separate IPv4 socket if you need both.

The OS default varies. Linux defaults to IPV6_V6ONLY = 0 (dual-stack). OpenBSD and some BSDs default to 1. Always set it explicitly for portable code.

IPv4-Mapped IPv6 Addresses

When a dual-stack socket receives a connection from an IPv4 client, the kernel represents the source address as an IPv4-mapped IPv6 address:

::ffff:192.0.2.1
::ffff:0:0/96 the /96 prefix that marks all IPv4-mapped addresses

If you log peer addresses, apply IP-based rate limiting, or parse source IPs for geolocation, you must handle this format. Strip the ::ffff: prefix to recover the original IPv4 address, or use a library that normalizes both forms.

import ipaddress

def normalize_peer(addr: str) -> str:
    ip = ipaddress.ip_address(addr)
    if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
        return str(ip.ipv4_mapped)  # returns "192.0.2.1"
    return str(ip)

Programming Language Support

Every major language ships standard-library support for IPv6 parsing and manipulation. Use the stdlib before reaching for a third-party package.

LanguageModule / APIKey Function
Pythonipaddressipaddress.ip_address(), IPv6Address.compressed
Gonetnet.ParseIP(), net.IP.String()
Node.js / BrowserURL (built-in)new URL() for host parsing; no built-in normalization
Javajava.net.InetAddressInetAddress.getByName()
Ruststd::net"addr".parse::<Ipv6Addr>()
import ipaddress

# Parse and normalize
addr = ipaddress.ip_address("2001:0db8:0000:0000:0000:ff00:0042:8329")
print(addr.compressed)   # "2001:db8::ff00:42:8329"
print(addr.exploded)     # "2001:0db8:0000:0000:0000:ff00:0042:8329"

# Check scope
print(addr.is_global)        # True
print(addr.is_link_local)    # False
print(addr.is_loopback)      # False

# Network membership
net = ipaddress.ip_network("2001:db8::/32")
print(addr in net)           # True
package main

import (
	"fmt"
	"net"
)

func main() {
	ip := net.ParseIP("2001:0db8:0000:0000:0000:ff00:0042:8329")
	if ip == nil {
		panic("invalid address")
	}
	// net.ParseIP normalizes automatically
	fmt.Println(ip.String()) // "2001:db8::ff00:42:8329"

	// Check if IPv4-mapped
	if ip4 := ip.To4(); ip4 != nil {
		fmt.Println("IPv4-mapped:", ip4)
	}
}

One practical rule: always normalize before storing. Accept whatever format the client or OS provides, run it through the standard parser, and persist the compressed (canonical) form. This prevents duplicate rows in your database and ensures consistent string comparisons.


Validate, expand, and compress IPv6 addresses directly in your browser with the IPv6 Parser — inspect scope, check prefix membership, and see both the compressed and exploded forms side by side.