| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package IO::Socket::HappyEyeballs; |
|
2
|
|
|
|
|
|
|
# ABSTRACT: RFC 8305 Happy Eyeballs v2 connection algorithm |
|
3
|
|
|
|
|
|
|
|
|
4
|
11
|
|
|
11
|
|
2177044
|
use strict; |
|
|
11
|
|
|
|
|
25
|
|
|
|
11
|
|
|
|
|
469
|
|
|
5
|
11
|
|
|
11
|
|
54
|
use warnings; |
|
|
11
|
|
|
|
|
26
|
|
|
|
11
|
|
|
|
|
883
|
|
|
6
|
11
|
|
|
11
|
|
68
|
use Carp; |
|
|
11
|
|
|
|
|
25
|
|
|
|
11
|
|
|
|
|
981
|
|
|
7
|
11
|
|
|
11
|
|
2815
|
use Errno qw( EINPROGRESS ECONNREFUSED EISCONN ); |
|
|
11
|
|
|
|
|
14193
|
|
|
|
11
|
|
|
|
|
5391
|
|
|
8
|
11
|
|
|
11
|
|
6528
|
use IO::Socket::IP; |
|
|
11
|
|
|
|
|
388771
|
|
|
|
11
|
|
|
|
|
62
|
|
|
9
|
11
|
|
|
|
|
1811
|
use Socket qw( |
|
10
|
|
|
|
|
|
|
getaddrinfo getnameinfo pack_sockaddr_in6 unpack_sockaddr_in6 |
|
11
|
|
|
|
|
|
|
AF_INET AF_INET6 AF_UNSPEC |
|
12
|
|
|
|
|
|
|
SOCK_STREAM IPPROTO_TCP |
|
13
|
|
|
|
|
|
|
NI_NUMERICHOST NI_NUMERICSERV |
|
14
|
|
|
|
|
|
|
SOL_SOCKET SO_ERROR |
|
15
|
|
|
|
|
|
|
AI_ADDRCONFIG |
|
16
|
|
|
|
|
|
|
inet_pton inet_ntop |
|
17
|
11
|
|
|
11
|
|
8912
|
); |
|
|
11
|
|
|
|
|
24
|
|
|
18
|
11
|
|
|
11
|
|
6623
|
use IO::Select; |
|
|
11
|
|
|
|
|
21273
|
|
|
|
11
|
|
|
|
|
745
|
|
|
19
|
|
|
|
|
|
|
|
|
20
|
11
|
|
|
11
|
|
4518
|
use parent qw(IO::Socket::IP); |
|
|
11
|
|
|
|
|
3190
|
|
|
|
11
|
|
|
|
|
87
|
|
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
our $VERSION = '0.002'; |
|
23
|
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
# Class-level cache: { "host:port" => { family => AF_INET6|AF_INET, expires => time } } |
|
25
|
|
|
|
|
|
|
my %_cache; |
|
26
|
|
|
|
|
|
|
my $CACHE_TTL = 600; # 10 minutes |
|
27
|
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
# Default connection attempt delay per RFC 8305 §5: 250ms |
|
29
|
|
|
|
|
|
|
my $DEFAULT_DELAY = 0.250; |
|
30
|
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# Last Resort Local Synthesis Delay per RFC 8305 §7.2: 2 seconds |
|
32
|
|
|
|
|
|
|
my $LAST_RESORT_DELAY = 2; |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
my $_override_active; |
|
35
|
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
sub import { |
|
37
|
11
|
|
|
11
|
|
168
|
my ($class, @args) = @_; |
|
38
|
11
|
|
|
|
|
847407
|
for my $arg (@args) { |
|
39
|
1
|
50
|
|
|
|
5
|
if ($arg eq '-override') { |
|
40
|
1
|
50
|
|
|
|
6
|
$class->_install_override unless $_override_active; |
|
41
|
|
|
|
|
|
|
} |
|
42
|
|
|
|
|
|
|
} |
|
43
|
|
|
|
|
|
|
} |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
sub _install_override { |
|
46
|
1
|
|
|
1
|
|
3
|
my ($class) = @_; |
|
47
|
1
|
|
|
|
|
16
|
my $original_new = IO::Socket::IP->can('new'); |
|
48
|
1
|
|
|
|
|
4
|
my $original_configure = IO::Socket::IP->can('configure'); |
|
49
|
11
|
|
|
11
|
|
2505
|
no warnings 'redefine'; |
|
|
11
|
|
|
|
|
28
|
|
|
|
11
|
|
|
|
|
33530
|
|
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
# Override configure to skip connect on already-connected sockets. |
|
52
|
|
|
|
|
|
|
# When a subclass (e.g. Net::HTTP) calls configure after Happy Eyeballs |
|
53
|
|
|
|
|
|
|
# has already connected, IO::Socket::IP::configure must not reconnect. |
|
54
|
|
|
|
|
|
|
*IO::Socket::IP::configure = sub { |
|
55
|
3
|
|
|
3
|
|
11940
|
my ($self, $cnf) = @_; |
|
56
|
3
|
100
|
|
|
|
33
|
if (delete ${*$self}{io_socket_happyeyeballs_connected}) { |
|
|
3
|
|
|
|
|
26
|
|
|
57
|
1
|
|
|
|
|
14
|
return $self; |
|
58
|
|
|
|
|
|
|
} |
|
59
|
2
|
|
|
|
|
10
|
return $original_configure->($self, $cnf); |
|
60
|
1
|
|
|
|
|
22
|
}; |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
*IO::Socket::IP::new = sub { |
|
63
|
11
|
|
|
11
|
|
1243119
|
my ($ip_class, %args) = @_; |
|
64
|
|
|
|
|
|
|
# Only intercept TCP connections with a peer |
|
65
|
11
|
50
|
100
|
|
|
269
|
if (($args{PeerHost} || $args{PeerAddr}) && $args{PeerPort}) { |
|
|
|
|
66
|
|
|
|
|
|
66
|
4
|
|
100
|
|
|
91
|
my $proto = $args{Proto} || ''; |
|
67
|
4
|
|
50
|
|
|
23
|
my $type = $args{Type} || 0; |
|
68
|
4
|
0
|
66
|
|
|
29
|
if (!$proto || $proto eq 'tcp' || $type == SOCK_STREAM || !$type) { |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
69
|
4
|
|
|
|
|
135
|
my $sock = IO::Socket::HappyEyeballs->_happy_connect(\%args); |
|
70
|
4
|
100
|
|
|
|
18
|
if ($sock) { |
|
71
|
|
|
|
|
|
|
# For subclasses (e.g. Net::HTTP): rebless and let their |
|
72
|
|
|
|
|
|
|
# configure run for protocol-specific setup, while our |
|
73
|
|
|
|
|
|
|
# IO::Socket::IP::configure override skips reconnecting. |
|
74
|
3
|
50
|
66
|
|
|
112
|
if ($ip_class ne 'IO::Socket::IP' |
|
|
|
|
66
|
|
|
|
|
|
75
|
|
|
|
|
|
|
&& $ip_class ne 'IO::Socket::HappyEyeballs' |
|
76
|
|
|
|
|
|
|
&& $ip_class->isa('IO::Socket::IP')) { |
|
77
|
1
|
|
|
|
|
53
|
bless $sock, $ip_class; |
|
78
|
1
|
|
|
|
|
7
|
${*$sock}{io_socket_happyeyeballs_connected} = 1; |
|
|
1
|
|
|
|
|
27
|
|
|
79
|
1
|
50
|
|
|
|
21
|
$sock->configure(\%args) if $sock->can('configure'); |
|
80
|
|
|
|
|
|
|
} |
|
81
|
3
|
|
|
|
|
135
|
return $sock; |
|
82
|
|
|
|
|
|
|
} |
|
83
|
1
|
|
|
|
|
6
|
return; |
|
84
|
|
|
|
|
|
|
} |
|
85
|
|
|
|
|
|
|
} |
|
86
|
7
|
|
|
|
|
73
|
return $original_new->($ip_class, %args); |
|
87
|
1
|
|
|
|
|
8
|
}; |
|
88
|
1
|
|
|
|
|
116109
|
$_override_active = 1; |
|
89
|
|
|
|
|
|
|
} |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
sub _happy_connect { |
|
92
|
4
|
|
|
4
|
|
22
|
my ($class, $args) = @_; |
|
93
|
|
|
|
|
|
|
# Use our Happy Eyeballs algorithm |
|
94
|
4
|
|
66
|
|
|
26
|
my $peer_host = $args->{PeerHost} || $args->{PeerAddr}; |
|
95
|
4
|
|
|
|
|
24
|
my $peer_port = $args->{PeerPort}; |
|
96
|
|
|
|
|
|
|
|
|
97
|
4
|
|
33
|
|
|
48
|
my $delay = delete $args->{ConnectionAttemptDelay} // $DEFAULT_DELAY; |
|
98
|
4
|
|
100
|
|
|
15
|
my $timeout = $args->{Timeout} // 30; |
|
99
|
|
|
|
|
|
|
|
|
100
|
4
|
|
|
|
|
44
|
my @addresses = _resolve($peer_host, $peer_port, $args); |
|
101
|
4
|
50
|
|
|
|
11
|
unless (@addresses) { |
|
102
|
0
|
|
|
|
|
0
|
$@ = "Cannot resolve host '$peer_host': no addresses found"; |
|
103
|
0
|
|
|
|
|
0
|
return; |
|
104
|
|
|
|
|
|
|
} |
|
105
|
|
|
|
|
|
|
|
|
106
|
4
|
|
|
|
|
17
|
@addresses = _sort_addresses(\@addresses, $peer_host, $peer_port); |
|
107
|
|
|
|
|
|
|
|
|
108
|
4
|
|
|
|
|
17
|
my $sock = _attempt_connections(\@addresses, $delay, $timeout, $args, |
|
109
|
|
|
|
|
|
|
$peer_host, $peer_port); |
|
110
|
|
|
|
|
|
|
|
|
111
|
4
|
100
|
|
|
|
16
|
if ($sock) { |
|
112
|
3
|
|
|
|
|
139
|
_cache_result($peer_host, $peer_port, $sock->sockdomain); |
|
113
|
3
|
|
|
|
|
33
|
return $sock; |
|
114
|
|
|
|
|
|
|
} |
|
115
|
|
|
|
|
|
|
|
|
116
|
1
|
|
|
|
|
3
|
$@ = "Cannot connect to $peer_host:$peer_port: all attempts failed"; |
|
117
|
1
|
|
|
|
|
5
|
return; |
|
118
|
|
|
|
|
|
|
} |
|
119
|
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
sub new { |
|
121
|
6
|
|
|
6
|
1
|
3112934
|
my ($class, %args) = @_; |
|
122
|
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
my $peer_host = $args{PeerHost} || $args{PeerAddr} |
|
124
|
6
|
50
|
33
|
|
|
257
|
or croak "PeerHost or PeerAddr is required"; |
|
125
|
|
|
|
|
|
|
my $peer_port = $args{PeerPort} |
|
126
|
6
|
50
|
|
|
|
129
|
or croak "PeerPort is required"; |
|
127
|
|
|
|
|
|
|
|
|
128
|
6
|
|
33
|
|
|
245
|
my $delay = delete $args{ConnectionAttemptDelay} // $DEFAULT_DELAY; |
|
129
|
6
|
|
50
|
|
|
165
|
my $timeout = $args{Timeout} // 30; |
|
130
|
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
# Resolve addresses |
|
132
|
6
|
|
|
|
|
280
|
my @addresses = _resolve($peer_host, $peer_port, \%args); |
|
133
|
6
|
100
|
|
|
|
47
|
unless (@addresses) { |
|
134
|
1
|
|
|
|
|
5
|
$@ = "Cannot resolve host '$peer_host': no addresses found"; |
|
135
|
1
|
|
|
|
|
16
|
return; |
|
136
|
|
|
|
|
|
|
} |
|
137
|
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
# Sort with interleaving per RFC 8305 §4 |
|
139
|
5
|
|
|
|
|
45
|
@addresses = _sort_addresses(\@addresses, $peer_host, $peer_port); |
|
140
|
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
# Attempt connections with Happy Eyeballs algorithm |
|
142
|
5
|
|
|
|
|
82
|
my $sock = _attempt_connections(\@addresses, $delay, $timeout, \%args, |
|
143
|
|
|
|
|
|
|
$peer_host, $peer_port); |
|
144
|
|
|
|
|
|
|
|
|
145
|
5
|
100
|
|
|
|
23
|
if ($sock) { |
|
146
|
4
|
|
|
|
|
134
|
_cache_result($peer_host, $peer_port, $sock->sockdomain); |
|
147
|
4
|
|
|
|
|
39
|
return $sock; |
|
148
|
|
|
|
|
|
|
} |
|
149
|
|
|
|
|
|
|
|
|
150
|
1
|
|
|
|
|
3
|
$@ = "Cannot connect to $peer_host:$peer_port: all attempts failed"; |
|
151
|
1
|
|
|
|
|
7
|
return; |
|
152
|
|
|
|
|
|
|
} |
|
153
|
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
sub _resolve { |
|
155
|
10
|
|
|
10
|
|
91
|
my ($host, $port, $args) = @_; |
|
156
|
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
# Respect GetAddrInfoFlags from IO::Socket::IP compatibility, |
|
158
|
|
|
|
|
|
|
# default to AI_ADDRCONFIG per RFC 8305 recommendation. |
|
159
|
|
|
|
|
|
|
my $flags = exists $args->{GetAddrInfoFlags} |
|
160
|
|
|
|
|
|
|
? $args->{GetAddrInfoFlags} |
|
161
|
10
|
50
|
|
|
|
120
|
: AI_ADDRCONFIG; |
|
162
|
|
|
|
|
|
|
|
|
163
|
10
|
50
|
|
|
|
326
|
my %hints = ( |
|
164
|
|
|
|
|
|
|
socktype => SOCK_STREAM, |
|
165
|
|
|
|
|
|
|
protocol => IPPROTO_TCP, |
|
166
|
|
|
|
|
|
|
family => AF_UNSPEC, |
|
167
|
|
|
|
|
|
|
($flags ? (flags => $flags) : ()), |
|
168
|
|
|
|
|
|
|
); |
|
169
|
|
|
|
|
|
|
|
|
170
|
10
|
|
|
|
|
36435
|
my ($err, @results) = getaddrinfo($host, $port, \%hints); |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
# If AI_ADDRCONFIG returned nothing (e.g. loopback-only interfaces), |
|
173
|
|
|
|
|
|
|
# retry without it to avoid filtering out valid addresses. |
|
174
|
10
|
100
|
66
|
|
|
343
|
if (($err || !@results) && $flags) { |
|
|
|
|
66
|
|
|
|
|
|
175
|
1
|
|
|
|
|
8
|
delete $hints{flags}; |
|
176
|
1
|
|
|
|
|
88239
|
($err, @results) = getaddrinfo($host, $port, \%hints); |
|
177
|
|
|
|
|
|
|
} |
|
178
|
|
|
|
|
|
|
|
|
179
|
10
|
100
|
|
|
|
76
|
if ($err) { |
|
180
|
1
|
|
|
|
|
4
|
$@ = "getaddrinfo failed: $err"; |
|
181
|
1
|
|
|
|
|
12
|
return; |
|
182
|
|
|
|
|
|
|
} |
|
183
|
|
|
|
|
|
|
|
|
184
|
9
|
|
|
|
|
62
|
return @results; |
|
185
|
|
|
|
|
|
|
} |
|
186
|
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
sub _sort_addresses { |
|
188
|
17
|
|
|
17
|
|
4179
|
my ($addresses, $host, $port) = @_; |
|
189
|
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
# Check cache for preferred family |
|
191
|
17
|
|
|
|
|
71
|
my $cache_key = "$host:$port"; |
|
192
|
17
|
|
|
|
|
35
|
my $preferred_family; |
|
193
|
17
|
100
|
|
|
|
106
|
if (my $cached = $_cache{$cache_key}) { |
|
194
|
8
|
100
|
|
|
|
68
|
if ($cached->{expires} > time()) { |
|
195
|
7
|
|
|
|
|
27
|
$preferred_family = $cached->{family}; |
|
196
|
|
|
|
|
|
|
} else { |
|
197
|
1
|
|
|
|
|
14
|
delete $_cache{$cache_key}; |
|
198
|
|
|
|
|
|
|
} |
|
199
|
|
|
|
|
|
|
} |
|
200
|
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
# Separate by address family |
|
202
|
17
|
|
|
|
|
40
|
my (@ipv6, @ipv4); |
|
203
|
17
|
|
|
|
|
95
|
for my $addr (@$addresses) { |
|
204
|
29
|
100
|
|
|
|
102
|
if ($addr->{family} == AF_INET6) { |
|
205
|
10
|
|
|
|
|
15
|
push @ipv6, $addr; |
|
206
|
|
|
|
|
|
|
} else { |
|
207
|
19
|
|
|
|
|
60
|
push @ipv4, $addr; |
|
208
|
|
|
|
|
|
|
} |
|
209
|
|
|
|
|
|
|
} |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
# Interleave: preferred family first, then alternate |
|
212
|
|
|
|
|
|
|
# Per RFC 8305 §4 |
|
213
|
17
|
|
|
|
|
74
|
my @sorted; |
|
214
|
17
|
|
|
|
|
26
|
my ($primary, $secondary); |
|
215
|
17
|
100
|
100
|
|
|
125
|
if ($preferred_family && $preferred_family == AF_INET) { |
|
216
|
6
|
|
|
|
|
13
|
$primary = \@ipv4; |
|
217
|
6
|
|
|
|
|
24
|
$secondary = \@ipv6; |
|
218
|
|
|
|
|
|
|
} else { |
|
219
|
11
|
100
|
|
|
|
30
|
$primary = @ipv6 ? \@ipv6 : \@ipv4; |
|
220
|
11
|
100
|
|
|
|
64
|
$secondary = @ipv6 ? \@ipv4 : \@ipv6; |
|
221
|
|
|
|
|
|
|
} |
|
222
|
|
|
|
|
|
|
|
|
223
|
17
|
|
66
|
|
|
110
|
while (@$primary || @$secondary) { |
|
224
|
21
|
50
|
|
|
|
76
|
push @sorted, shift @$primary if @$primary; |
|
225
|
21
|
100
|
|
|
|
166
|
push @sorted, shift @$secondary if @$secondary; |
|
226
|
|
|
|
|
|
|
} |
|
227
|
|
|
|
|
|
|
|
|
228
|
17
|
|
|
|
|
81
|
return @sorted; |
|
229
|
|
|
|
|
|
|
} |
|
230
|
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
sub _attempt_connections { |
|
232
|
9
|
|
|
9
|
|
47
|
my ($addresses, $delay, $timeout, $args, $host, $port) = @_; |
|
233
|
|
|
|
|
|
|
|
|
234
|
9
|
|
|
|
|
30
|
my @pending; # [ socket, addrinfo ] pairs |
|
235
|
9
|
|
|
|
|
227
|
my $select = IO::Select->new; |
|
236
|
9
|
|
|
|
|
218
|
my $deadline = time() + $timeout; |
|
237
|
9
|
|
|
|
|
26
|
my $next_attempt_time = 0; # start first attempt immediately |
|
238
|
|
|
|
|
|
|
|
|
239
|
9
|
|
|
|
|
26
|
my $addr_idx = 0; |
|
240
|
9
|
|
|
|
|
15
|
my $last_attempt_time; |
|
241
|
9
|
|
|
|
|
19
|
my $last_resort_done = 0; |
|
242
|
|
|
|
|
|
|
|
|
243
|
9
|
|
66
|
|
|
52
|
while ($addr_idx < @$addresses || @pending) { |
|
244
|
9
|
|
|
|
|
54
|
my $now = time(); |
|
245
|
9
|
50
|
|
|
|
32
|
last if $now >= $deadline; |
|
246
|
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
# RFC 8305 §7.2: Last Resort Local Synthesis |
|
248
|
|
|
|
|
|
|
# When all initial addresses are exhausted and all pending have failed, |
|
249
|
|
|
|
|
|
|
# wait until $LAST_RESORT_DELAY seconds after last attempt, then try |
|
250
|
|
|
|
|
|
|
# A-record-only resolution with NAT64 synthesis. |
|
251
|
9
|
0
|
33
|
|
|
122
|
if (!$last_resort_done && $addr_idx >= @$addresses && !@pending |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
252
|
|
|
|
|
|
|
&& $last_attempt_time) { |
|
253
|
0
|
|
|
|
|
0
|
my $synth_addresses = _last_resort_synthesis( |
|
254
|
|
|
|
|
|
|
$host, $port, $args, $last_attempt_time, $deadline); |
|
255
|
0
|
0
|
0
|
|
|
0
|
if ($synth_addresses && @$synth_addresses) { |
|
256
|
0
|
|
|
|
|
0
|
push @$addresses, @$synth_addresses; |
|
257
|
0
|
|
|
|
|
0
|
$last_resort_done = 1; |
|
258
|
0
|
|
|
|
|
0
|
next; # re-enter loop to process new addresses |
|
259
|
|
|
|
|
|
|
} |
|
260
|
0
|
|
|
|
|
0
|
$last_resort_done = 1; |
|
261
|
|
|
|
|
|
|
} |
|
262
|
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
# Start a new connection attempt if it's time |
|
264
|
9
|
50
|
33
|
|
|
127
|
if ($addr_idx < @$addresses && $now >= $next_attempt_time) { |
|
265
|
9
|
|
|
|
|
30
|
my $addr = $addresses->[$addr_idx++]; |
|
266
|
9
|
|
|
|
|
72
|
my $sock = _start_connect($addr, $args); |
|
267
|
|
|
|
|
|
|
|
|
268
|
9
|
50
|
|
|
|
54
|
if ($sock) { |
|
269
|
|
|
|
|
|
|
# Check if already connected (localhost etc.) |
|
270
|
9
|
100
|
|
|
|
114
|
if ($sock->connected) { |
|
271
|
7
|
|
|
|
|
336
|
_cleanup_pending(\@pending); |
|
272
|
7
|
|
|
|
|
57
|
_restore_blocking($sock, $args); |
|
273
|
7
|
|
|
|
|
255
|
return $sock; |
|
274
|
|
|
|
|
|
|
} |
|
275
|
2
|
|
|
|
|
67
|
push @pending, [ $sock, $addr ]; |
|
276
|
2
|
|
|
|
|
13
|
$select->add($sock); |
|
277
|
|
|
|
|
|
|
} |
|
278
|
|
|
|
|
|
|
|
|
279
|
2
|
|
|
|
|
141
|
$last_attempt_time = time(); |
|
280
|
2
|
|
|
|
|
7
|
$next_attempt_time = $last_attempt_time + $delay; |
|
281
|
|
|
|
|
|
|
} |
|
282
|
|
|
|
|
|
|
|
|
283
|
2
|
50
|
|
|
|
8
|
next unless @pending; |
|
284
|
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
# Calculate how long to wait |
|
286
|
2
|
|
|
|
|
5
|
my $wait_time; |
|
287
|
2
|
50
|
|
|
|
7
|
if ($addr_idx < @$addresses) { |
|
288
|
|
|
|
|
|
|
# Wait until either a connection succeeds or it's time for the next attempt |
|
289
|
0
|
|
|
|
|
0
|
$wait_time = $next_attempt_time - time(); |
|
290
|
0
|
0
|
|
|
|
0
|
$wait_time = 0 if $wait_time < 0; |
|
291
|
|
|
|
|
|
|
} else { |
|
292
|
|
|
|
|
|
|
# No more addresses to try, wait for remaining connections |
|
293
|
2
|
|
|
|
|
6
|
$wait_time = $deadline - time(); |
|
294
|
2
|
50
|
|
|
|
7
|
$wait_time = 0 if $wait_time < 0; |
|
295
|
|
|
|
|
|
|
} |
|
296
|
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
# select() for writable (connected) sockets |
|
298
|
2
|
|
|
|
|
18
|
my @ready = IO::Select->select(undef, $select, undef, $wait_time); |
|
299
|
|
|
|
|
|
|
|
|
300
|
2
|
50
|
33
|
|
|
189
|
if (@ready && $ready[1]) { |
|
301
|
2
|
|
|
|
|
5
|
for my $sock (@{$ready[1]}) { |
|
|
2
|
|
|
|
|
7
|
|
|
302
|
|
|
|
|
|
|
# Check if the connection actually succeeded |
|
303
|
2
|
|
|
|
|
20
|
my $err = $sock->sockopt(SO_ERROR); |
|
304
|
2
|
50
|
|
|
|
61
|
if ($err == 0) { |
|
305
|
|
|
|
|
|
|
# Success! Clean up all other pending sockets |
|
306
|
0
|
|
|
|
|
0
|
my @others = grep { $_->[0] != $sock } @pending; |
|
|
0
|
|
|
|
|
0
|
|
|
307
|
0
|
|
|
|
|
0
|
_cleanup_pending(\@others); |
|
308
|
0
|
|
|
|
|
0
|
_restore_blocking($sock, $args); |
|
309
|
|
|
|
|
|
|
|
|
310
|
0
|
|
|
|
|
0
|
return $sock; |
|
311
|
|
|
|
|
|
|
} else { |
|
312
|
|
|
|
|
|
|
# This connection failed, remove it |
|
313
|
2
|
|
|
|
|
21
|
$select->remove($sock); |
|
314
|
2
|
|
|
|
|
121
|
$sock->close; |
|
315
|
2
|
|
|
|
|
79
|
@pending = grep { $_->[0] != $sock } @pending; |
|
|
2
|
|
|
|
|
59
|
|
|
316
|
|
|
|
|
|
|
} |
|
317
|
|
|
|
|
|
|
} |
|
318
|
|
|
|
|
|
|
} |
|
319
|
|
|
|
|
|
|
} |
|
320
|
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
# All failed |
|
322
|
2
|
|
|
|
|
15
|
_cleanup_pending(\@pending); |
|
323
|
2
|
|
|
|
|
19
|
return; |
|
324
|
|
|
|
|
|
|
} |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
sub _start_connect { |
|
327
|
9
|
|
|
9
|
|
31
|
my ($addr, $args) = @_; |
|
328
|
|
|
|
|
|
|
|
|
329
|
9
|
|
|
|
|
212
|
my $sock = IO::Socket::IP->new; |
|
330
|
|
|
|
|
|
|
$sock->socket($addr->{family}, $addr->{socktype}, $addr->{protocol}) |
|
331
|
9
|
50
|
|
|
|
1958
|
or return; |
|
332
|
|
|
|
|
|
|
|
|
333
|
9
|
|
|
|
|
1093
|
$sock->blocking(0); |
|
334
|
|
|
|
|
|
|
|
|
335
|
9
|
|
|
|
|
2002
|
my $rv = CORE::connect($sock, $addr->{addr}); |
|
336
|
9
|
50
|
|
|
|
124
|
if ($rv) { |
|
337
|
|
|
|
|
|
|
# Connected immediately |
|
338
|
0
|
|
|
|
|
0
|
return $sock; |
|
339
|
|
|
|
|
|
|
} |
|
340
|
9
|
50
|
|
|
|
263
|
if ($! == EINPROGRESS) { |
|
341
|
|
|
|
|
|
|
# Connection in progress — this is the normal non-blocking case |
|
342
|
9
|
|
|
|
|
49
|
return $sock; |
|
343
|
|
|
|
|
|
|
} |
|
344
|
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
# Immediate failure |
|
346
|
0
|
|
|
|
|
0
|
$sock->close; |
|
347
|
0
|
|
|
|
|
0
|
return; |
|
348
|
|
|
|
|
|
|
} |
|
349
|
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
sub _restore_blocking { |
|
351
|
7
|
|
|
7
|
|
30
|
my ($sock, $args) = @_; |
|
352
|
7
|
100
|
66
|
|
|
118
|
if (exists $args->{Blocking} && !$args->{Blocking}) { |
|
353
|
1
|
|
|
|
|
6
|
$sock->blocking(0); |
|
354
|
|
|
|
|
|
|
} else { |
|
355
|
6
|
|
|
|
|
64
|
$sock->blocking(1); |
|
356
|
|
|
|
|
|
|
} |
|
357
|
|
|
|
|
|
|
} |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
sub _cleanup_pending { |
|
360
|
9
|
|
|
9
|
|
30
|
my ($pending) = @_; |
|
361
|
9
|
|
|
|
|
87
|
for my $p (@$pending) { |
|
362
|
0
|
0
|
|
|
|
0
|
$p->[0]->close if $p->[0]; |
|
363
|
|
|
|
|
|
|
} |
|
364
|
|
|
|
|
|
|
} |
|
365
|
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
# RFC 8305 §7.2: Last Resort Local Synthesis |
|
367
|
|
|
|
|
|
|
# After all AAAA-based attempts fail, wait for the synthesis delay, |
|
368
|
|
|
|
|
|
|
# then query A records and synthesize IPv6 via NAT64 if available. |
|
369
|
|
|
|
|
|
|
sub _last_resort_synthesis { |
|
370
|
2
|
|
|
2
|
|
19
|
my ($host, $port, $args, $last_attempt_time, $deadline) = @_; |
|
371
|
|
|
|
|
|
|
|
|
372
|
2
|
50
|
33
|
|
|
17
|
return unless defined $host && defined $port; |
|
373
|
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
# Wait until $LAST_RESORT_DELAY after last attempt fired |
|
375
|
2
|
|
|
|
|
7
|
my $synthesis_time = $last_attempt_time + $LAST_RESORT_DELAY; |
|
376
|
2
|
|
|
|
|
6
|
my $now = time(); |
|
377
|
2
|
100
|
|
|
|
9
|
if ($now < $synthesis_time) { |
|
378
|
1
|
|
|
|
|
5
|
my $wait = $synthesis_time - $now; |
|
379
|
1
|
50
|
|
|
|
7
|
return if $now + $wait > $deadline; |
|
380
|
0
|
|
|
|
|
0
|
select(undef, undef, undef, $wait); |
|
381
|
|
|
|
|
|
|
} |
|
382
|
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
# Query A records only (IPv4) |
|
384
|
1
|
|
|
|
|
82954
|
my ($err, @ipv4_results) = getaddrinfo($host, $port, { |
|
385
|
|
|
|
|
|
|
socktype => SOCK_STREAM, |
|
386
|
|
|
|
|
|
|
protocol => IPPROTO_TCP, |
|
387
|
|
|
|
|
|
|
family => AF_INET, |
|
388
|
|
|
|
|
|
|
}); |
|
389
|
1
|
50
|
|
|
|
22
|
return unless @ipv4_results; |
|
390
|
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
# Detect NAT64 prefix via RFC 7050 (ipv4only.arpa) |
|
392
|
0
|
|
|
|
|
0
|
my $nat64_prefix = _detect_nat64_prefix(); |
|
393
|
|
|
|
|
|
|
|
|
394
|
0
|
0
|
|
|
|
0
|
if ($nat64_prefix) { |
|
395
|
|
|
|
|
|
|
# Synthesize IPv6 addresses from IPv4 using NAT64 prefix |
|
396
|
0
|
|
|
|
|
0
|
my @synthesized; |
|
397
|
0
|
|
|
|
|
0
|
for my $r (@ipv4_results) { |
|
398
|
0
|
|
|
|
|
0
|
my $synth = _synthesize_nat64_addr($r, $nat64_prefix, $port); |
|
399
|
0
|
0
|
|
|
|
0
|
push @synthesized, $synth if $synth; |
|
400
|
|
|
|
|
|
|
} |
|
401
|
0
|
0
|
|
|
|
0
|
return \@synthesized if @synthesized; |
|
402
|
|
|
|
|
|
|
} |
|
403
|
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
# No NAT64: return IPv4 addresses directly as fallback |
|
405
|
0
|
|
|
|
|
0
|
return \@ipv4_results; |
|
406
|
|
|
|
|
|
|
} |
|
407
|
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
# RFC 7050: Discovery of the IPv6 Prefix Used for IPv6 Address Synthesis |
|
409
|
|
|
|
|
|
|
# Resolve ipv4only.arpa AAAA — if we get results, NAT64 is present |
|
410
|
|
|
|
|
|
|
# and the prefix can be extracted from the response. |
|
411
|
|
|
|
|
|
|
my $_nat64_prefix_cache; |
|
412
|
|
|
|
|
|
|
my $_nat64_prefix_expires = 0; |
|
413
|
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
sub _detect_nat64_prefix { |
|
415
|
1
|
|
|
1
|
|
5
|
my $now = time(); |
|
416
|
1
|
50
|
|
|
|
4
|
if ($now < $_nat64_prefix_expires) { |
|
417
|
0
|
|
|
|
|
0
|
return $_nat64_prefix_cache; |
|
418
|
|
|
|
|
|
|
} |
|
419
|
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
# The well-known IPv4 addresses for ipv4only.arpa are |
|
421
|
|
|
|
|
|
|
# 192.0.0.170 and 192.0.0.171. If AAAA resolution returns |
|
422
|
|
|
|
|
|
|
# results, the NAT64 prefix is the first 96 bits. |
|
423
|
1
|
|
|
|
|
72155
|
my ($err, @results) = getaddrinfo('ipv4only.arpa', '443', { |
|
424
|
|
|
|
|
|
|
socktype => SOCK_STREAM, |
|
425
|
|
|
|
|
|
|
family => AF_INET6, |
|
426
|
|
|
|
|
|
|
}); |
|
427
|
|
|
|
|
|
|
|
|
428
|
1
|
50
|
33
|
|
|
20
|
if (!$err && @results) { |
|
429
|
0
|
|
|
|
|
0
|
my ($synth_port, $synth_addr) = unpack_sockaddr_in6($results[0]{addr}); |
|
430
|
|
|
|
|
|
|
# The well-known address 192.0.0.170 = 0xC0000AA in the last 32 bits |
|
431
|
|
|
|
|
|
|
# Extract the first 12 bytes as the NAT64 prefix |
|
432
|
0
|
|
|
|
|
0
|
my $prefix = substr($synth_addr, 0, 12); |
|
433
|
0
|
|
|
|
|
0
|
$_nat64_prefix_cache = $prefix; |
|
434
|
0
|
|
|
|
|
0
|
$_nat64_prefix_expires = $now + 600; # cache for 10 minutes |
|
435
|
0
|
|
|
|
|
0
|
return $prefix; |
|
436
|
|
|
|
|
|
|
} |
|
437
|
|
|
|
|
|
|
|
|
438
|
1
|
|
|
|
|
3
|
$_nat64_prefix_cache = undef; |
|
439
|
1
|
|
|
|
|
3
|
$_nat64_prefix_expires = $now + 60; # negative cache for 1 minute |
|
440
|
1
|
|
|
|
|
9
|
return; |
|
441
|
|
|
|
|
|
|
} |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
# Synthesize an IPv6 address by combining NAT64 prefix with IPv4 address |
|
444
|
|
|
|
|
|
|
sub _synthesize_nat64_addr { |
|
445
|
2
|
|
|
2
|
|
2451
|
my ($ipv4_addrinfo, $nat64_prefix, $port) = @_; |
|
446
|
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
# Extract the IPv4 address from the sockaddr |
|
448
|
2
|
|
|
|
|
5
|
my $family = $ipv4_addrinfo->{family}; |
|
449
|
2
|
100
|
|
|
|
6
|
return unless $family == AF_INET; |
|
450
|
|
|
|
|
|
|
|
|
451
|
1
|
|
|
|
|
5
|
my ($ipv4_port, $ipv4_packed) = Socket::unpack_sockaddr_in($ipv4_addrinfo->{addr}); |
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
# Build synthesized IPv6 address: 96-bit prefix + 32-bit IPv4 |
|
454
|
1
|
|
|
|
|
2
|
my $synth_ipv6 = $nat64_prefix . $ipv4_packed; |
|
455
|
1
|
|
|
|
|
4
|
my $synth_sockaddr = pack_sockaddr_in6($ipv4_port, $synth_ipv6); |
|
456
|
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
return { |
|
458
|
1
|
|
|
|
|
9
|
family => AF_INET6, |
|
459
|
|
|
|
|
|
|
socktype => SOCK_STREAM, |
|
460
|
|
|
|
|
|
|
protocol => IPPROTO_TCP, |
|
461
|
|
|
|
|
|
|
addr => $synth_sockaddr, |
|
462
|
|
|
|
|
|
|
}; |
|
463
|
|
|
|
|
|
|
} |
|
464
|
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
sub _cache_result { |
|
466
|
11
|
|
|
11
|
|
1954
|
my ($host, $port, $family) = @_; |
|
467
|
11
|
|
|
|
|
205
|
$_cache{"$host:$port"} = { |
|
468
|
|
|
|
|
|
|
family => $family, |
|
469
|
|
|
|
|
|
|
expires => time() + $CACHE_TTL, |
|
470
|
|
|
|
|
|
|
}; |
|
471
|
|
|
|
|
|
|
} |
|
472
|
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
sub clear_cache { |
|
474
|
6
|
|
|
6
|
1
|
263452
|
%_cache = (); |
|
475
|
6
|
|
|
|
|
12
|
$_nat64_prefix_cache = undef; |
|
476
|
6
|
|
|
|
|
14
|
$_nat64_prefix_expires = 0; |
|
477
|
|
|
|
|
|
|
} |
|
478
|
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
sub last_resort_delay { |
|
480
|
8
|
|
|
8
|
1
|
248571
|
my ($class, $new_delay) = @_; |
|
481
|
8
|
100
|
|
|
|
22
|
if (defined $new_delay) { |
|
482
|
6
|
|
|
|
|
15
|
$LAST_RESORT_DELAY = $new_delay; |
|
483
|
|
|
|
|
|
|
} |
|
484
|
8
|
|
|
|
|
21
|
return $LAST_RESORT_DELAY; |
|
485
|
|
|
|
|
|
|
} |
|
486
|
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
sub connection_attempt_delay { |
|
488
|
4
|
|
|
4
|
1
|
294530
|
my ($class, $new_delay) = @_; |
|
489
|
4
|
100
|
|
|
|
13
|
if (defined $new_delay) { |
|
490
|
2
|
|
|
|
|
6
|
$DEFAULT_DELAY = $new_delay; |
|
491
|
|
|
|
|
|
|
} |
|
492
|
4
|
|
|
|
|
13
|
return $DEFAULT_DELAY; |
|
493
|
|
|
|
|
|
|
} |
|
494
|
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
sub cache_ttl { |
|
496
|
4
|
|
|
4
|
1
|
1895
|
my ($class, $new_ttl) = @_; |
|
497
|
4
|
100
|
|
|
|
11
|
if (defined $new_ttl) { |
|
498
|
2
|
|
|
|
|
3
|
$CACHE_TTL = $new_ttl; |
|
499
|
|
|
|
|
|
|
} |
|
500
|
4
|
|
|
|
|
10
|
return $CACHE_TTL; |
|
501
|
|
|
|
|
|
|
} |
|
502
|
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
1; |
|
504
|
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
__END__ |