NAME

IO::Socket::HappyEyeballs - RFC 8305 Happy Eyeballs v2 connection algorithm

VERSION

version 0.002

SYNOPSIS

# Direct usage:
use IO::Socket::HappyEyeballs;

my $sock = IO::Socket::HappyEyeballs->new(
  PeerHost => 'www.example.com',
  PeerPort => 80,
);
die "Cannot connect: $@" unless $sock;
print $sock "GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n";

# Global override — makes ALL IO::Socket::IP connections use Happy Eyeballs:
use IO::Socket::HappyEyeballs -override;

# Now any code using IO::Socket::IP gets Happy Eyeballs automatically,
# including LWP::UserAgent, HTTP::Tiny, Net::Async::HTTP, etc.
use HTTP::Tiny;
my $response = HTTP::Tiny->new->get('http://www.example.com');

DESCRIPTION

This module implements the Happy Eyeballs algorithm for establishing TCP connections to dual-stack hosts (hosts reachable via both IPv4 and IPv6).

This module was created because David Leadbeater (DGL) needed it.

The problem

As the internet transitions from IPv4 to IPv6, many hosts are reachable via both protocols ("dual-stack"). A naive client that tries IPv6 first will experience long timeouts (typically 30-75 seconds) when IPv6 connectivity is broken — even though IPv4 would work instantly. This is a common situation: a host publishes AAAA records but the user's network path to that host over IPv6 is broken somewhere along the way.

The solution: Happy Eyeballs

The Happy Eyeballs algorithm (originally specified in RFC 6555, updated in RFC 8305) solves this by racing connection attempts in parallel:

1. Resolve the hostname to all available addresses (both AAAA and A records)
2. Sort the addresses with interleaving — IPv6 first, then alternate between families (e.g. IPv6, IPv4, IPv6, IPv4, ...)
3. Start connecting to the first address (typically IPv6)
4. Wait 250ms — if not connected yet, start connecting to the next address (typically IPv4) in parallel
5. Continue starting new attempts every 250ms while previous ones are still pending
6. Return the first socket that successfully connects, close all others
7. Cache the winning address family so future connections try it first

The 250ms delay is called the Connection Attempt Delay. It is short enough to avoid noticeable lag, but long enough to give the preferred address family (IPv6) a fair chance to connect first.

RFC compliance

This module implements RFC 8305 ("Happy Eyeballs Version 2: Better Connectivity Using Concurrency"), which supersedes the original RFC 6555 ("Happy Eyeballs: Success with Dual-Stack Hosts").

Key RFC 8305 features implemented:

  • Address interleaving (Section 4) — alternating address families

  • Connection attempt delay (Section 5) — 250ms default, configurable

  • First-wins connection racing — parallel non-blocking connects via select()

  • Address family caching (Section 5.2) — successful family is remembered

  • Last Resort Local Synthesis (Section 7.2) — handles broken AAAA records via NAT64 synthesis fallback

Using the -override import flag

The most powerful way to use this module is with the -override flag:

use IO::Socket::HappyEyeballs -override;

This transparently replaces IO::Socket::IP->new() with the Happy Eyeballs algorithm for all outgoing TCP connections in the entire process. Any library that uses IO::Socket::IP internally — including HTTP::Tiny, LWP::UserAgent, Net::Async::HTTP, IO::Async, and many others — will automatically benefit.

Only outgoing TCP connections (those with PeerHost/PeerAddr and PeerPort) are intercepted. Listening sockets, UDP sockets, and Unix domain sockets are passed through to the original IO::Socket::IP unchanged.

ConnectionAttemptDelay

Time in seconds to wait before starting the next connection attempt. Defaults to 0.250 (250ms) per RFC 8305 Section 5. Can be passed to new().

Timeout

Overall connection timeout in seconds. Defaults to 30.

new

my $sock = IO::Socket::HappyEyeballs->new(%args);

Creates a new socket connection using the Happy Eyeballs v2 algorithm (RFC 8305). Accepts the same arguments as IO::Socket::IP plus:

ConnectionAttemptDelay

Delay in seconds between connection attempts (default: 0.250).

Returns the connected socket on success, or undef on failure with $@ set to an error message.

clear_cache

IO::Socket::HappyEyeballs->clear_cache;

Clears the internal address family preference cache.

connection_attempt_delay

IO::Socket::HappyEyeballs->connection_attempt_delay(0.300);
my $delay = IO::Socket::HappyEyeballs->connection_attempt_delay;

Get/set the default connection attempt delay in seconds. The default is 0.250 (250ms) as recommended by RFC 8305 Section 5.

cache_ttl

IO::Socket::HappyEyeballs->cache_ttl(300);
my $ttl = IO::Socket::HappyEyeballs->cache_ttl;

Get/set the address family cache TTL in seconds. The default is 600 (10 minutes). When a successful connection is made, the winning address family (IPv4 or IPv6) is cached for this duration. Subsequent connections to the same host:port will try the cached family first.

last_resort_delay

IO::Socket::HappyEyeballs->last_resort_delay(3);
my $delay = IO::Socket::HappyEyeballs->last_resort_delay;

Get/set the Last Resort Local Synthesis Delay in seconds per RFC 8305 Section 7.2. The default is 2 seconds. This is the time to wait after the last connection attempt before falling back to A-record-only resolution with NAT64 address synthesis. This handles the case of hostnames with broken AAAA records on IPv6-only networks with NAT64/DNS64.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-io-socket-happyeyeballs/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudss.us>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.