Overview
Internet Protocol version 4 (IPv4) and Internet Protocol version 6 (IPv6) represent two generations of the network layer protocol that enables communication across the internet. IPv4, standardized in 1981, uses 32-bit addresses providing approximately 4.3 billion unique addresses. IPv6, standardized in 1998, uses 128-bit addresses providing 340 undecillion unique addresses—a number so large it exceeds practical exhaustion scenarios.
The transition from IPv4 to IPv6 stems from address exhaustion rather than technical limitations of IPv4 itself. As internet-connected devices proliferated beyond the original design assumptions, techniques like Network Address Translation (NAT) temporarily extended IPv4's lifespan. However, NAT introduces complexity in peer-to-peer communications, requires stateful middleboxes, and complicates end-to-end connectivity principles.
IPv6 redesigns the protocol to address multiple concerns beyond address space. The protocol eliminates broadcast traffic in favor of multicast, simplifies header structures for more efficient routing, mandates IPsec support at the protocol level, and removes fragmentation from router responsibilities. These changes reflect lessons learned from decades of IPv4 deployment and accommodate modern network architectures.
require 'socket'
# IPv4 address structure
ipv4 = IPAddr.new('192.168.1.100')
puts ipv4.ipv4? # => true
puts ipv4.to_i # => 3232235876 (32-bit integer)
# IPv6 address structure
ipv6 = IPAddr.new('2001:0db8:85a3:0000:0000:8a2e:0370:7334')
puts ipv6.ipv6? # => true
puts ipv6.to_i # => 42540766452641154071740215577757643572 (128-bit integer)
Software developers encounter IPv4/IPv6 considerations when building networked applications, configuring server infrastructure, implementing protocol-aware routing logic, and ensuring application compatibility across heterogeneous networks. Many production systems operate dual-stack configurations, simultaneously supporting both protocols while managing the complexity this introduces.
Key Principles
Address Structure and Representation
IPv4 addresses consist of 32 bits divided into four 8-bit octets, represented in dotted-decimal notation. Each octet ranges from 0 to 255, producing addresses like 192.168.1.1 or 10.0.0.0. This representation makes addresses human-readable while clearly showing the four-byte structure.
IPv6 addresses consist of 128 bits divided into eight 16-bit groups, represented in hexadecimal colon-separated notation. Each group contains four hexadecimal digits, producing addresses like 2001:0db8:85a3:0000:0000:8a2e:0370:7334. IPv6 permits compression rules: consecutive groups of zeros compress to :: (used once per address), and leading zeros within groups may be omitted. The address 2001:0db8:0000:0000:0000:0000:0000:0001 compresses to 2001:0db8::1.
Address Space Organization
IPv4 divides its address space into classes (classful addressing, largely obsolete) and CIDR blocks. Private address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) enable internal networks to reuse addresses through NAT. Special-purpose ranges include loopback (127.0.0.0/8), link-local (169.254.0.0/16), and multicast (224.0.0.0/4).
IPv6 organizes address space hierarchically. Global Unicast Addresses (GUA) use the 2000::/3 prefix for internet-routable addresses. Unique Local Addresses (ULA) use fc00::/7 for private networks without NAT requirements. Link-Local Addresses use fe80::/10 for local network segment communication. IPv6 reserves significantly larger address space for special purposes while maintaining a more structured allocation policy.
Packet Header Architecture
IPv4 headers contain variable length fields spanning 20-60 bytes. The header includes version (4 bits), header length (4 bits), type of service (8 bits), total length (16 bits), identification (16 bits), flags (3 bits), fragment offset (13 bits), TTL (8 bits), protocol (8 bits), header checksum (16 bits), source address (32 bits), destination address (32 bits), and optional options field (0-40 bytes).
IPv6 simplifies the header to fixed 40 bytes with eight fields: version (4 bits), traffic class (8 bits), flow label (20 bits), payload length (16 bits), next header (8 bits), hop limit (8 bits), source address (128 bits), and destination address (128 bits). Optional headers chain through the next header field rather than embedding in the main header, improving routing efficiency.
Address Configuration Mechanisms
IPv4 typically requires DHCP for automatic address assignment or manual static configuration. DHCP servers maintain address pools, lease addresses to clients, and manage address-to-hostname mappings. This centralized approach requires DHCP infrastructure and introduces a single point of failure unless redundant servers deploy.
IPv6 introduces Stateless Address Autoconfiguration (SLAAC) where hosts generate addresses from router advertisements and their interface identifiers. Hosts construct link-local addresses independently, listen for router advertisements containing network prefixes, and combine prefixes with interface identifiers (derived from MAC addresses or privacy extensions) to create global addresses. DHCPv6 remains available for stateful configuration or additional parameters like DNS servers.
Network Address Translation Philosophy
IPv4 deployments extensively use NAT to map private addresses to public addresses, conserving scarce IPv4 space. NAT creates a stateful mapping between internal and external address/port combinations, allowing multiple internal hosts to share a single public address. This breaks end-to-end connectivity principles and complicates protocols embedding IP addresses in application data.
IPv6 eliminates NAT requirements through abundant address space. Each device receives globally routable addresses, restoring end-to-end connectivity. Firewall rules provide security boundaries without requiring address translation. Some organizations deploy NAT66 for internal policy reasons, though this contradicts IPv6 design principles and reintroduces complexity IPv6 aimed to eliminate.
Fragmentation Handling
IPv4 permits routers to fragment packets exceeding link MTU (Maximum Transmission Unit). Routers split packets into smaller fragments, each carrying fragment identification and offset information. The destination host reassembles fragments, and if any fragment fails to arrive, the entire packet discards. This router-based fragmentation increases processing overhead and complicates network equipment.
IPv6 prohibits routers from performing fragmentation. Source hosts must perform Path MTU Discovery (PMTUD) to determine the minimum MTU along the path and fragment at the source if necessary. Routers encountering oversized packets return ICMPv6 "Packet Too Big" messages. This design shifts complexity to endpoints, simplifying router operations and improving throughput.
Ruby Implementation
Ruby's socket library provides native support for both IPv4 and IPv6 through the Socket class and related abstractions. The IPAddr class handles address parsing, manipulation, and validation for both protocol versions.
Address Parsing and Manipulation
Ruby distinguishes IPv4 and IPv6 addresses through the IPAddr class, which automatically detects address format and provides version-specific operations.
require 'ipaddr'
# Parse and validate IPv4
ipv4 = IPAddr.new('192.168.1.100')
puts ipv4.ipv4? # => true
puts ipv4.ipv6? # => false
puts ipv4.to_s # => "192.168.1.100"
puts ipv4.to_range # => 192.168.1.100..192.168.1.100
# Parse and validate IPv6
ipv6 = IPAddr.new('2001:db8::1')
puts ipv6.ipv6? # => true
puts ipv6.to_s # => "2001:db8::1"
puts ipv6.to_string # => "2001:0db8:0000:0000:0000:0000:0000:0001"
# CIDR operations work identically for both versions
ipv4_network = IPAddr.new('192.168.1.0/24')
puts ipv4_network.include?(IPAddr.new('192.168.1.50')) # => true
ipv6_network = IPAddr.new('2001:db8::/32')
puts ipv6_network.include?(IPAddr.new('2001:db8::1')) # => true
Socket Creation and Binding
Ruby creates protocol-specific sockets through address family constants. The Socket.getaddrinfo method returns address information for both IPv4 and IPv6, enabling protocol-agnostic connection logic.
require 'socket'
# Explicitly create IPv4 socket
ipv4_socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
ipv4_addr = Socket.pack_sockaddr_in(8080, '0.0.0.0')
ipv4_socket.bind(ipv4_addr)
ipv4_socket.listen(5)
# Explicitly create IPv6 socket
ipv6_socket = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM)
ipv6_addr = Socket.pack_sockaddr_in(8080, '::')
ipv6_socket.bind(ipv6_addr)
ipv6_socket.listen(5)
# Protocol-agnostic address resolution
Socket.getaddrinfo('example.com', 'http', nil, :STREAM).each do |addr|
family, port, hostname, ip_address, protocol_family, socket_type = addr
puts "#{protocol_family == Socket::AF_INET6 ? 'IPv6' : 'IPv4'}: #{ip_address}"
end
Dual-Stack Server Implementation
Ruby servers commonly bind to both IPv4 and IPv6 addresses simultaneously, handling both protocol versions in a single application. This requires managing multiple server sockets or using the IPV6_V6ONLY socket option.
require 'socket'
# Dual-stack server with separate sockets
def create_dual_stack_server(port)
servers = []
# IPv4 socket
begin
ipv4 = TCPServer.new('0.0.0.0', port)
servers << ipv4
puts "IPv4 server listening on 0.0.0.0:#{port}"
rescue Errno::EADDRINUSE
puts "IPv4 port #{port} already in use"
end
# IPv6 socket
begin
ipv6 = TCPServer.new('::', port)
servers << ipv6
puts "IPv6 server listening on :::#{port}"
rescue Errno::EADDRINUSE
puts "IPv6 port #{port} already in use"
end
servers
end
servers = create_dual_stack_server(9090)
# Accept connections from both protocols
loop do
readable, = IO.select(servers)
readable.each do |server|
client = server.accept
client_addr = client.remote_address
if client_addr.ipv6?
puts "IPv6 connection from #{client_addr.ip_address}"
else
puts "IPv4 connection from #{client_addr.ip_address}"
end
client.close
end
end
Client Connection with Protocol Fallback
Ruby clients should attempt connections using available address families, implementing fallback mechanisms when one protocol fails.
require 'socket'
def connect_with_fallback(hostname, port, timeout: 5)
addresses = Socket.getaddrinfo(hostname, port, nil, :STREAM)
# Separate IPv4 and IPv6 addresses
ipv4_addrs = addresses.select { |a| a[4] == Socket::AF_INET }
ipv6_addrs = addresses.select { |a| a[4] == Socket::AF_INET6 }
# Try IPv6 first (Happy Eyeballs approach)
ipv6_addrs.each do |addr|
begin
socket = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM)
sockaddr = Socket.pack_sockaddr_in(addr[1], addr[3])
socket.connect_nonblock(sockaddr)
return socket
rescue Errno::EINPROGRESS
if IO.select(nil, [socket], nil, timeout)
return socket
end
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
socket.close rescue nil
next
end
end
# Fallback to IPv4
ipv4_addrs.each do |addr|
begin
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
sockaddr = Socket.pack_sockaddr_in(addr[1], addr[3])
socket.connect_nonblock(sockaddr)
return socket
rescue Errno::EINPROGRESS
if IO.select(nil, [socket], nil, timeout)
return socket
end
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
socket.close rescue nil
next
end
end
raise "Failed to connect to #{hostname}:#{port} via IPv4 or IPv6"
end
Address Type Detection and Validation
Ruby applications frequently need to detect address types for logging, routing decisions, or protocol-specific handling.
require 'ipaddr'
def analyze_address(addr_string)
addr = IPAddr.new(addr_string)
info = {
version: addr.ipv4? ? 4 : 6,
address: addr.to_s,
network: addr.to_range.first.to_s,
broadcast: addr.to_range.last.to_s
}
if addr.ipv4?
info[:private] = addr.private?
info[:loopback] = addr.loopback?
info[:link_local] = addr.to_s.start_with?('169.254')
else
info[:link_local] = addr.to_s.start_with?('fe80')
info[:unique_local] = addr.to_s.start_with?('fc', 'fd')
info[:loopback] = addr.to_s == '::1'
info[:multicast] = addr.to_s.start_with?('ff')
end
info
rescue IPAddr::InvalidAddressError
{ error: "Invalid IP address: #{addr_string}" }
end
# Example usage
puts analyze_address('192.168.1.100')
# => {:version=>4, :address=>"192.168.1.100", :network=>"192.168.1.100",
# :broadcast=>"192.168.1.100", :private=>true, :loopback=>false, :link_local=>false}
puts analyze_address('2001:db8::1')
# => {:version=>6, :address=>"2001:db8::1", :network=>"2001:db8::1",
# :broadcast=>"2001:db8::1", :link_local=>false, :unique_local=>false,
# :loopback=>false, :multicast=>false}
Practical Examples
DNS Resolution Comparison
DNS resolution behaves differently for IPv4 and IPv6, with AAAA records serving IPv6 addresses and A records serving IPv4 addresses. Modern resolvers query both record types.
require 'resolv'
def resolve_all_addresses(hostname)
resolver = Resolv::DNS.new
results = { ipv4: [], ipv6: [] }
# Query A records (IPv4)
begin
results[:ipv4] = resolver.getresources(hostname, Resolv::DNS::Resource::IN::A)
.map(&:address).map(&:to_s)
rescue Resolv::ResolvError
results[:ipv4] = []
end
# Query AAAA records (IPv6)
begin
results[:ipv6] = resolver.getresources(hostname, Resolv::DNS::Resource::IN::AAAA)
.map(&:address).map(&:to_s)
rescue Resolv::ResolvError
results[:ipv6] = []
end
results
end
# Resolve a dual-stack hostname
addresses = resolve_all_addresses('google.com')
puts "IPv4 addresses: #{addresses[:ipv4].join(', ')}"
puts "IPv6 addresses: #{addresses[:ipv6].join(', ')}"
# Determine preferred protocol
if addresses[:ipv6].any?
puts "Prefer IPv6 connection"
elsif addresses[:ipv4].any?
puts "Use IPv4 connection"
else
puts "No addresses resolved"
end
HTTP Client with Protocol Selection
HTTP clients must handle both IPv4 and IPv6 server endpoints, respecting DNS responses and connection availability.
require 'net/http'
require 'uri'
def http_request_dual_stack(url_string)
uri = URI.parse(url_string)
# Resolve hostname to all addresses
addresses = Socket.getaddrinfo(uri.host, uri.port, nil, :STREAM)
# Group by protocol family
ipv6_addrs = addresses.select { |a| a[4] == Socket::AF_INET6 }
ipv4_addrs = addresses.select { |a| a[4] == Socket::AF_INET }
# Try IPv6 first
ipv6_addrs.each do |addr|
begin
# Create HTTP connection with specific IP
http = Net::HTTP.new(addr[3], uri.port)
http.read_timeout = 5
http.open_timeout = 5
response = http.request_get(uri.path.empty? ? '/' : uri.path)
return { protocol: 'IPv6', address: addr[3], status: response.code, body: response.body }
rescue StandardError => e
# Connection failed, try next address
next
end
end
# Fallback to IPv4
ipv4_addrs.each do |addr|
begin
http = Net::HTTP.new(addr[3], uri.port)
http.read_timeout = 5
http.open_timeout = 5
response = http.request_get(uri.path.empty? ? '/' : uri.path)
return { protocol: 'IPv4', address: addr[3], status: response.code, body: response.body }
rescue StandardError => e
next
end
end
{ error: "Failed to connect via IPv4 or IPv6" }
end
result = http_request_dual_stack('http://example.com')
puts "Connected via #{result[:protocol]} to #{result[:address]}"
puts "Status: #{result[:status]}"
Network Interface Enumeration
Ruby applications may need to enumerate network interfaces to determine available IPv4 and IPv6 addresses for binding or configuration.
require 'socket'
def enumerate_network_interfaces
interfaces = {}
Socket.ip_address_list.each do |addrinfo|
iface_name = addrinfo.inspect_sockaddr.match(/\w+$/)[0] rescue 'unknown'
interfaces[iface_name] ||= { ipv4: [], ipv6: [] }
if addrinfo.ipv4?
interfaces[iface_name][:ipv4] << {
address: addrinfo.ip_address,
private: addrinfo.ipv4_private?,
loopback: addrinfo.ipv4_loopback?
}
elsif addrinfo.ipv6?
interfaces[iface_name][:ipv6] << {
address: addrinfo.ip_address,
link_local: addrinfo.ipv6_linklocal?,
loopback: addrinfo.ipv6_loopback?,
unique_local: addrinfo.ipv6_unique_local?
}
end
end
interfaces
end
# Display all network interfaces
enumerate_network_interfaces.each do |iface, addrs|
puts "\nInterface: #{iface}"
if addrs[:ipv4].any?
puts " IPv4 addresses:"
addrs[:ipv4].each do |addr|
type = addr[:loopback] ? 'loopback' : (addr[:private] ? 'private' : 'public')
puts " #{addr[:address]} (#{type})"
end
end
if addrs[:ipv6].any?
puts " IPv6 addresses:"
addrs[:ipv6].each do |addr|
type = addr[:loopback] ? 'loopback' : (addr[:link_local] ? 'link-local' : (addr[:unique_local] ? 'unique-local' : 'global'))
puts " #{addr[:address]} (#{type})"
end
end
end
Subnet Operations and CIDR Notation
CIDR notation applies to both IPv4 and IPv6, though IPv6 networks typically use larger prefixes. Ruby's IPAddr handles subnet calculations identically for both protocols.
require 'ipaddr'
def subnet_analysis(cidr_string)
network = IPAddr.new(cidr_string)
analysis = {
version: network.ipv4? ? 4 : 6,
network: network.to_s,
prefix_length: cidr_string.split('/').last.to_i,
first_address: network.to_range.first.to_s,
last_address: network.to_range.last.to_s
}
if network.ipv4?
# Calculate usable IPv4 addresses
total_hosts = 2 ** (32 - analysis[:prefix_length])
analysis[:total_addresses] = total_hosts
analysis[:usable_addresses] = total_hosts - 2 if analysis[:prefix_length] < 31
else
# IPv6 subnet size
total_hosts = 2 ** (128 - analysis[:prefix_length])
analysis[:total_addresses] = total_hosts
end
analysis
end
# IPv4 subnet
ipv4_subnet = subnet_analysis('192.168.1.0/24')
puts "IPv4 /24 network:"
puts " Range: #{ipv4_subnet[:first_address]} - #{ipv4_subnet[:last_address]}"
puts " Total addresses: #{ipv4_subnet[:total_addresses]}"
puts " Usable: #{ipv4_subnet[:usable_addresses]}"
# IPv6 subnet
ipv6_subnet = subnet_analysis('2001:db8::/32')
puts "\nIPv6 /32 network:"
puts " Range: #{ipv6_subnet[:first_address]} - #{ipv6_subnet[:last_address]}"
puts " Total addresses: #{ipv6_subnet[:total_addresses]}"
Integration & Interoperability
Transition Mechanisms
Multiple transition mechanisms enable IPv4 and IPv6 to coexist during the multi-decade migration period. Dual-stack, tunneling, and translation each address specific deployment scenarios.
Dual-stack networks run both protocols simultaneously. Hosts maintain IPv4 and IPv6 addresses, routes, and DNS records. Applications query both A and AAAA records, preferring IPv6 when available. This approach requires no protocol translation but doubles addressing overhead and configuration complexity.
Tunneling encapsulates IPv6 packets within IPv4 packets (or vice versa) to traverse networks supporting only one protocol. 6to4, Teredo, and ISATAP represent common tunneling protocols. Tunnels introduce latency and complicate troubleshooting but enable connectivity across protocol boundaries without requiring intermediate network upgrades.
Translation mechanisms convert between IPv4 and IPv6 at the network edge. NAT64 translates IPv6-only client traffic to IPv4 destinations, while DNS64 synthesizes AAAA records for IPv4-only services. Translation maintains connectivity but breaks end-to-end principles and prevents applications embedding addresses in payload data.
Happy Eyeballs Algorithm
RFC 8305 defines the Happy Eyeballs algorithm for initiating connections across dual-stack networks. The algorithm attempts IPv6 and IPv4 connections in parallel with staggered timing to optimize for both speed and IPv6 preference.
require 'socket'
require 'timeout'
def happy_eyeballs_connect(hostname, port, resolution_delay: 0.05, connection_attempt_delay: 0.25)
addresses = Socket.getaddrinfo(hostname, port, nil, :STREAM)
ipv6_addrs = addresses.select { |a| a[4] == Socket::AF_INET6 }
ipv4_addrs = addresses.select { |a| a[4] == Socket::AF_INET }
sockets = []
connected = nil
begin
# Start IPv6 attempt
unless ipv6_addrs.empty?
addr = ipv6_addrs.first
socket = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM)
sockaddr = Socket.pack_sockaddr_in(addr[1], addr[3])
begin
socket.connect_nonblock(sockaddr)
return socket # Immediate success
rescue IO::WaitWritable
sockets << { socket: socket, family: 'IPv6', started_at: Time.now }
rescue StandardError => e
socket.close rescue nil
end
end
# Wait resolution delay before starting IPv4
sleep(resolution_delay)
# Start IPv4 attempt
unless ipv4_addrs.empty?
addr = ipv4_addrs.first
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
sockaddr = Socket.pack_sockaddr_in(addr[1], addr[3])
begin
socket.connect_nonblock(sockaddr)
sockets.each { |s| s[:socket].close rescue nil }
return socket
rescue IO::WaitWritable
sockets << { socket: socket, family: 'IPv4', started_at: Time.now }
rescue StandardError => e
socket.close rescue nil
end
end
# Wait for first successful connection
timeout = 5
start_time = Time.now
while Time.now - start_time < timeout && !sockets.empty?
writable = IO.select(nil, sockets.map { |s| s[:socket] }, nil, 0.1)
if writable && writable[1]
writable[1].each do |sock|
begin
sock.connect_nonblock(Socket.pack_sockaddr_in(0, '0.0.0.0'))
rescue Errno::EISCONN
# Connection succeeded
connected = sockets.find { |s| s[:socket] == sock }
sockets.reject! { |s| s == connected }
sockets.each { |s| s[:socket].close rescue nil }
return connected[:socket]
rescue StandardError
# Connection failed
sockets.reject! { |s| s[:socket] == sock }
sock.close rescue nil
end
end
end
end
sockets.each { |s| s[:socket].close rescue nil }
raise "Failed to connect to #{hostname}:#{port}"
end
end
Dual-Stack Web Server Configuration
Web servers must bind to both IPv4 and IPv6 addresses to serve dual-stack clients. Ruby's WEBrick and other HTTP servers can be configured for dual-stack operation.
require 'webrick'
require 'socket'
def create_dual_stack_webserver(port: 8080)
servers = []
# IPv4 server
ipv4_server = WEBrick::HTTPServer.new(
BindAddress: '0.0.0.0',
Port: port,
Logger: WEBrick::Log.new('/dev/null'),
AccessLog: []
)
ipv4_server.mount_proc '/' do |req, res|
client_addr = req.peeraddr[3]
res.body = "IPv4 server response from #{client_addr}"
res['Content-Type'] = 'text/plain'
end
servers << Thread.new { ipv4_server.start }
# IPv6 server
ipv6_server = WEBrick::HTTPServer.new(
BindAddress: '::',
Port: port + 1, # Different port to avoid conflict
Logger: WEBrick::Log.new('/dev/null'),
AccessLog: []
)
ipv6_server.mount_proc '/' do |req, res|
client_addr = req.peeraddr[3]
res.body = "IPv6 server response from #{client_addr}"
res['Content-Type'] = 'text/plain'
end
servers << Thread.new { ipv6_server.start }
trap('INT') {
ipv4_server.shutdown
ipv6_server.shutdown
}
servers.each(&:join)
end
IPv6 Address Scoping
IPv6 link-local addresses require scope identifiers to specify which network interface to use. Ruby handles scope identifiers through the sockaddr structure.
require 'socket'
def connect_with_scope(ipv6_address, scope_id, port)
# Link-local addresses require scope ID
if ipv6_address.start_with?('fe80:')
# Format: fe80::1%eth0 or fe80::1%2
scoped_address = "#{ipv6_address}%#{scope_id}"
addrinfo = Addrinfo.tcp(scoped_address, port)
socket = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM)
socket.connect(addrinfo.to_sockaddr)
return socket
else
# Global addresses don't need scope
socket = TCPSocket.new(ipv6_address, port)
return socket
end
end
# List interfaces with their scope IDs
Socket.ip_address_list.each do |addrinfo|
if addrinfo.ipv6_linklocal?
puts "Link-local: #{addrinfo.ip_address}"
puts "Scope ID: #{addrinfo.ifindex}"
end
end
Security Implications
Address Scanning and Discovery
IPv4's small address space enables complete network scanning. Attackers systematically probe all addresses in a subnet to discover active hosts. Common /24 networks contain only 254 usable addresses, scanned in seconds.
IPv6's vast address space renders traditional scanning impractical. A /64 subnet—the standard allocation for a network segment—contains 18,446,744,073,709,551,616 addresses. Scanning at one million addresses per second requires 584,942 years to complete. Attackers must employ targeted discovery methods like DNS enumeration, monitoring multicast traffic, or exploiting predictable address patterns.
However, many IPv6 deployments use predictable addressing schemes. SLAAC with EUI-64 derives the interface identifier from the MAC address, exposing hardware information. Sequential numbering (::1, ::2, ::3) creates easily guessable addresses. Privacy extensions (RFC 4941) generate random interface identifiers periodically, complicating tracking and scanning.
require 'ipaddr'
require 'securerandom'
def generate_ipv6_addresses(prefix, strategy: :random)
network = IPAddr.new(prefix)
case strategy
when :sequential
# Predictable sequential addressing (avoid in production)
(1..10).map do |i|
"#{prefix.split('/').first}#{i.to_s(16)}"
end
when :eui64
# Simulate EUI-64 derived from MAC (exposes hardware info)
mac = '00:11:22:33:44:55'
mac_parts = mac.split(':').map { |h| h.to_i(16) }
# Modify first octet for EUI-64
mac_parts[0] ^= 0x02
interface_id = sprintf('%02x%02x:%02xff:fe%02x:%02x%02x',
mac_parts[0], mac_parts[1], mac_parts[2],
mac_parts[3], mac_parts[4], mac_parts[5])
["#{prefix.split('/').first}#{interface_id}"]
when :random
# Privacy extensions - random interface identifiers
(1..10).map do
random_suffix = SecureRandom.hex(8).scan(/.{4}/).join(':')
"#{prefix.split('/').first}#{random_suffix}"
end
end
end
puts "Sequential (predictable):"
puts generate_ipv6_addresses('2001:db8::/64', strategy: :sequential)
puts "\nEUI-64 (exposes MAC):"
puts generate_ipv6_addresses('2001:db8::/64', strategy: :eui64)
puts "\nRandom (privacy-preserving):"
puts generate_ipv6_addresses('2001:db8::/64', strategy: :random)
IPsec and Encryption
IPv4 treats IPsec as optional, requiring explicit configuration. Most IPv4 traffic traverses the internet unencrypted unless application-layer security (TLS/SSL) provides protection.
IPv6 mandates IPsec support in all implementations, though actual deployment remains optional. IPsec provides authentication (AH) and encryption (ESP) at the network layer. Authentication Header (AH) verifies packet integrity and origin. Encapsulating Security Payload (ESP) encrypts packet contents, preventing eavesdropping.
Despite mandatory IPsec support, most IPv6 deployments rely on application-layer security rather than network-layer IPsec. TLS/SSL remains the dominant encryption mechanism for web traffic. IPsec deployment requires complex key management, policy configuration, and coordination between endpoints.
Filtering and Firewall Rules
IPv4 firewalls typically implement stateful inspection with rules based on source/destination addresses and ports. NAT inherently provides a security boundary, blocking unsolicited inbound connections.
IPv6 eliminates NAT's implicit filtering. Stateful firewalls remain critical for security boundaries. Rules must explicitly permit or deny traffic based on IPv6 addresses, protocols, and ports. The expanded address space complicates rule management—address-based filtering requires careful prefix matching rather than simple range checks.
ICMPv6 requires special consideration in IPv6 firewalls. IPv6 depends on ICMPv6 for essential functions like Neighbor Discovery, Path MTU Discovery, and router advertisements. Blocking ICMPv6 entirely breaks IPv6 operation. Firewalls must permit specific ICMPv6 types while blocking potentially dangerous messages.
require 'ipaddr'
class IPv6Firewall
ALLOWED_ICMPV6_TYPES = [
1, # Destination Unreachable
2, # Packet Too Big
3, # Time Exceeded
4, # Parameter Problem
128, # Echo Request
129, # Echo Reply
133, # Router Solicitation
134, # Router Advertisement
135, # Neighbor Solicitation
136, # Neighbor Advertisement
].freeze
def initialize
@rules = []
end
def allow_subnet(subnet_cidr, port: nil)
network = IPAddr.new(subnet_cidr)
@rules << {
action: :allow,
network: network,
port: port,
priority: 100
}
end
def block_subnet(subnet_cidr)
network = IPAddr.new(subnet_cidr)
@rules << {
action: :block,
network: network,
priority: 50
}
end
def evaluate(source_addr, dest_port, protocol: :tcp)
addr = IPAddr.new(source_addr)
# ICMPv6 special handling
if protocol == :icmpv6
return :allow if ALLOWED_ICMPV6_TYPES.include?(dest_port)
return :block
end
# Evaluate rules by priority
matching_rules = @rules.select { |r| r[:network].include?(addr) }
matching_rules.sort_by! { |r| -r[:priority] }
matching_rules.each do |rule|
next if rule[:port] && rule[:port] != dest_port
return rule[:action]
end
:block # Default deny
end
end
firewall = IPv6Firewall.new
firewall.allow_subnet('2001:db8:1000::/48', port: 443)
firewall.block_subnet('2001:db8:bad::/48')
puts firewall.evaluate('2001:db8:1000::1', 443) # => :allow
puts firewall.evaluate('2001:db8:bad::1', 80) # => :block
puts firewall.evaluate('fe80::1', 135, protocol: :icmpv6) # => :allow (Neighbor Solicitation)
Privacy and Tracking Concerns
IPv4 NAT provides privacy through address masking. Multiple internal hosts share a single public address, complicating individual device tracking. Dynamic address assignment from ISP pools further obscures device identity over time.
IPv6 global unicast addresses enable end-to-end tracking. Persistent addresses tied to specific devices facilitate long-term monitoring across networks and services. Combined with predictable SLAAC addressing using EUI-64, IPv6 addresses potentially expose device hardware identifiers and location information.
Privacy extensions mitigate tracking concerns by generating temporary addresses with limited lifetimes. Devices create random interface identifiers, use them for outbound connections for a configurable period (typically hours or days), then deprecate them and generate new addresses. Incoming connections use stable addresses, while outbound connections rotate through temporary addresses.
Reference
Protocol Comparison
| Feature | IPv4 | IPv6 |
|---|---|---|
| Address Length | 32 bits | 128 bits |
| Address Notation | Dotted decimal (192.168.1.1) | Hexadecimal colon-separated (2001:db8::1) |
| Address Space | 4.3 billion addresses | 340 undecillion addresses |
| Header Size | Variable (20-60 bytes) | Fixed (40 bytes) |
| Header Fields | 13 fields + options | 8 fields + extension headers |
| Fragmentation | Routers and hosts | Hosts only |
| Checksum | Header checksum required | No header checksum |
| Broadcast | Supported | Replaced with multicast |
| Configuration | DHCP or manual | SLAAC, DHCPv6, or manual |
| IPsec | Optional | Mandatory support |
| NAT | Common | Unnecessary |
| QoS | Type of Service field | Traffic Class and Flow Label |
Address Type Classification
| Address Type | IPv4 Example | IPv6 Example | Purpose |
|---|---|---|---|
| Loopback | 127.0.0.1 | ::1 | Local host communication |
| Link-Local | 169.254.0.0/16 | fe80::/10 | Local network segment |
| Private/Unique Local | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | fc00::/7 | Internal networks |
| Global Unicast | Public addresses | 2000::/3 | Internet-routable |
| Multicast | 224.0.0.0/4 | ff00::/8 | One-to-many communication |
| Broadcast | 255.255.255.255 | N/A | Replaced by multicast in IPv6 |
| Documentation | 192.0.2.0/24, 198.51.100.0/24 | 2001:db8::/32 | Examples and documentation |
| Unspecified | 0.0.0.0 | :: | No address assigned |
Ruby Socket Constants
| Constant | Value | Description |
|---|---|---|
| Socket::AF_INET | 2 | IPv4 address family |
| Socket::AF_INET6 | 30 | IPv6 address family |
| Socket::IPPROTO_TCP | 6 | TCP protocol |
| Socket::IPPROTO_UDP | 17 | UDP protocol |
| Socket::IPPROTO_ICMPV6 | 58 | ICMPv6 protocol |
| Socket::IPV6_V6ONLY | 27 | Socket option for IPv6-only |
| Socket::IPV6_UNICAST_HOPS | 4 | IPv6 hop limit |
| Socket::IPV6_MULTICAST_HOPS | 10 | IPv6 multicast hop limit |
Common ICMPv6 Message Types
| Type | Name | Purpose | Firewall Action |
|---|---|---|---|
| 1 | Destination Unreachable | Path unavailable | Allow |
| 2 | Packet Too Big | MTU discovery | Allow |
| 3 | Time Exceeded | Hop limit reached | Allow |
| 4 | Parameter Problem | Header error | Allow |
| 128 | Echo Request | Ping request | Allow selectively |
| 129 | Echo Reply | Ping response | Allow |
| 133 | Router Solicitation | Discover routers | Allow on local |
| 134 | Router Advertisement | Router info | Allow on local |
| 135 | Neighbor Solicitation | Address resolution | Allow on local |
| 136 | Neighbor Advertisement | Address resolution | Allow on local |
Transition Mechanism Comparison
| Mechanism | Type | Use Case | Complexity |
|---|---|---|---|
| Dual Stack | Native | Full IPv4 and IPv6 support | Medium |
| 6to4 | Tunneling | Automatic IPv6 over IPv4 | Low |
| Teredo | Tunneling | IPv6 behind NAT | Medium |
| ISATAP | Tunneling | IPv6 over IPv4 infrastructure | Medium |
| 6rd | Tunneling | ISP IPv6 deployment | Low |
| NAT64/DNS64 | Translation | IPv6-only to IPv4 services | High |
| 464XLAT | Translation | IPv4 apps on IPv6 networks | High |
Address Scoping
| Scope | IPv4 Equivalent | IPv6 Prefix | Description |
|---|---|---|---|
| Interface-local | 127.0.0.0/8 | ::1/128 | Single interface |
| Link-local | 169.254.0.0/16 | fe80::/10 | Single network link |
| Site-local | Private RFC 1918 | Deprecated | Organization (deprecated) |
| Unique-local | Private RFC 1918 | fc00::/7 | Organization |
| Global | Public addresses | 2000::/3 | Internet-routable |
Ruby IPAddr Methods
| Method | Return Type | Description |
|---|---|---|
| ipv4? | Boolean | Tests if address is IPv4 |
| ipv6? | Boolean | Tests if address is IPv6 |
| private? | Boolean | Tests if address is private (IPv4 only) |
| loopback? | Boolean | Tests if address is loopback |
| to_i | Integer | Converts address to integer |
| to_s | String | Converts to string representation |
| to_range | Range | Returns address range for network |
| include? | Boolean | Tests if address is in network |
| mask | IPAddr | Applies subnet mask |
| & | IPAddr | Bitwise AND operation |
| pipe | IPAddr | Bitwise OR operation |