|
41 | 41 | import java.net.InetSocketAddress; |
42 | 42 | import java.net.UnknownHostException; |
43 | 43 | import java.util.Arrays; |
| 44 | +import java.util.List; |
44 | 45 |
|
45 | 46 | import org.apache.hc.client5.http.config.ProtocolFamilyPreference; |
46 | 47 | import org.apache.hc.client5.http.impl.InMemoryDnsResolver; |
@@ -180,20 +181,54 @@ void filtersOutMulticastDestinations() throws Exception { |
180 | 181 | // ------------------------------------------------------------------------- |
181 | 182 |
|
182 | 183 | @Test |
183 | | - void classifyScope_loopback_linkLocal_siteLocal_global() throws Exception { |
184 | | - assertEquals(AddressSelectingDnsResolver.Scope.INTERFACE_LOCAL, |
185 | | - AddressSelectingDnsResolver.classifyScope(inet("127.0.0.1"))); |
186 | | - assertEquals(AddressSelectingDnsResolver.Scope.INTERFACE_LOCAL, |
| 184 | + void classifyScope_ipv6Loopback_isLinkLocal() throws Exception { |
| 185 | + // ::1 maps to link-local scope; interface-local (0x1) is multicast-only (RFC 4291 §2.7). |
| 186 | + assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL, |
187 | 187 | AddressSelectingDnsResolver.classifyScope(inet("::1"))); |
| 188 | + } |
| 189 | + |
| 190 | + @Test |
| 191 | + void classifyScope_ipv4Loopback_isLinkLocal() throws Exception { |
| 192 | + // IPv4 127/8 maps to link-local, NOT interface-local (RFC 6724 §3.1). |
| 193 | + assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL, |
| 194 | + AddressSelectingDnsResolver.classifyScope(inet("127.0.0.1"))); |
| 195 | + assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL, |
| 196 | + AddressSelectingDnsResolver.classifyScope(inet("127.255.255.255"))); |
| 197 | + } |
188 | 198 |
|
| 199 | + @Test |
| 200 | + void classifyScope_linkLocal() throws Exception { |
| 201 | + // IPv4 169.254/16 → link-local. |
189 | 202 | assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL, |
190 | 203 | AddressSelectingDnsResolver.classifyScope(inet("169.254.0.1"))); |
| 204 | + // IPv6 fe80::/10 → link-local. |
191 | 205 | assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL, |
192 | 206 | AddressSelectingDnsResolver.classifyScope(inet("fe80::1"))); |
| 207 | + } |
193 | 208 |
|
194 | | - assertEquals(AddressSelectingDnsResolver.Scope.SITE_LOCAL, |
| 209 | + @Test |
| 210 | + void classifyScope_ipv4Private_isGlobal() throws Exception { |
| 211 | + // RFC 6724: all IPv4 except 127/8 and 169.254/16 are global, |
| 212 | + // including RFC1918 (10/8, 172.16/12, 192.168/16) and 100.64/10. |
| 213 | + assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL, |
195 | 214 | AddressSelectingDnsResolver.classifyScope(inet("10.0.0.1"))); |
| 215 | + assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL, |
| 216 | + AddressSelectingDnsResolver.classifyScope(inet("172.16.0.1"))); |
| 217 | + assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL, |
| 218 | + AddressSelectingDnsResolver.classifyScope(inet("192.168.1.1"))); |
| 219 | + assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL, |
| 220 | + AddressSelectingDnsResolver.classifyScope(inet("100.64.0.1"))); |
| 221 | + } |
| 222 | + |
| 223 | + @Test |
| 224 | + void classifyScope_ipv6SiteLocal() throws Exception { |
| 225 | + // IPv6 deprecated site-local (fec0::/10) still classified as site-local per RFC 6724. |
| 226 | + assertEquals(AddressSelectingDnsResolver.Scope.SITE_LOCAL, |
| 227 | + AddressSelectingDnsResolver.classifyScope(inet("fec0::1"))); |
| 228 | + } |
196 | 229 |
|
| 230 | + @Test |
| 231 | + void classifyScope_global() throws Exception { |
197 | 232 | assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL, |
198 | 233 | AddressSelectingDnsResolver.classifyScope(inet("8.8.8.8"))); |
199 | 234 | assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL, |
@@ -389,6 +424,66 @@ private static AddressSelectingDnsResolver.SourceAddressResolver sourceMap( |
389 | 424 | } |
390 | 425 |
|
391 | 426 |
|
| 427 | + @Test |
| 428 | + void ipv4Only_filtersSingleV6Address() throws Exception { |
| 429 | + // Regression: a single IPv6 address must still be filtered when IPV4_ONLY is set. |
| 430 | + final InetAddress v6 = inet("2001:db8::1"); |
| 431 | + delegate.add("v6.example", v6); |
| 432 | + |
| 433 | + final AddressSelectingDnsResolver r = |
| 434 | + new AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV4_ONLY, NO_SOURCE_ADDR); |
| 435 | + |
| 436 | + assertNull(r.resolve("v6.example")); |
| 437 | + } |
| 438 | + |
| 439 | + @Test |
| 440 | + void resolveHostPort_appliesRfc6724Ordering() throws Exception { |
| 441 | + final InetAddress v4 = inet("192.0.2.1"); |
| 442 | + final InetAddress v6 = inet("2001:db8::1"); |
| 443 | + |
| 444 | + delegate.add("dual.example", v4, v6); |
| 445 | + |
| 446 | + final AddressSelectingDnsResolver r = |
| 447 | + new AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV4_ONLY, NO_SOURCE_ADDR); |
| 448 | + |
| 449 | + final List<InetSocketAddress> out = r.resolve("dual.example", 443); |
| 450 | + assertEquals(1, out.size()); |
| 451 | + assertEquals(new InetSocketAddress(v4, 443), out.get(0)); |
| 452 | + } |
| 453 | + |
| 454 | + @Test |
| 455 | + void resolveHostPort_throwsWhenAllFilteredOut() throws Exception { |
| 456 | + // Only IPv6 addresses, but IPV4_ONLY is requested. |
| 457 | + // Must throw rather than returning an unresolved address that bypasses filtering. |
| 458 | + delegate.add("v6.example", inet("2001:db8::1")); |
| 459 | + |
| 460 | + final AddressSelectingDnsResolver r = |
| 461 | + new AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV4_ONLY, NO_SOURCE_ADDR); |
| 462 | + |
| 463 | + assertThrows(UnknownHostException.class, () -> r.resolve("v6.example", 443)); |
| 464 | + } |
| 465 | + |
| 466 | + @Test |
| 467 | + void rfcRule9_appliesToIpv4Pairs() throws Exception { |
| 468 | + // Both IPv4, same policy (::ffff:0:0/96 → prec 35, label 4), same scope (GLOBAL). |
| 469 | + // Rule 9 should prefer the address whose source shares a longer prefix. |
| 470 | + final InetAddress aDst = inet("192.0.2.1"); |
| 471 | + final InetAddress bDst = inet("203.0.113.1"); |
| 472 | + |
| 473 | + // aSrc shares 24 bits with aDst (192.0.2.x); bSrc shares only 8 bits with bDst (203 vs 203). |
| 474 | + final InetAddress aSrc = inet("192.0.2.100"); |
| 475 | + final InetAddress bSrc = inet("203.0.114.1"); |
| 476 | + |
| 477 | + delegate.add("t.example", bDst, aDst); |
| 478 | + |
| 479 | + final AddressSelectingDnsResolver r = |
| 480 | + new AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, sourceMap(aDst, aSrc, bDst, bSrc)); |
| 481 | + |
| 482 | + final InetAddress[] out = r.resolve("t.example"); |
| 483 | + // aSrc-aDst share more prefix bits than bSrc-bDst, so aDst should come first. |
| 484 | + assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out)); |
| 485 | + } |
| 486 | + |
392 | 487 | @Test |
393 | 488 | void networkContains_ipv6Prefix32() throws Exception { |
394 | 489 | final AddressSelectingDnsResolver.Network p32 = |
|
0 commit comments