CrackedRuby CrackedRuby

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