Skip to content

Commit 7231ce8

Browse files
committed
update branch
1 parent 73aafc7 commit 7231ce8

2 files changed

Lines changed: 116 additions & 18 deletions

File tree

httpclient5/src/main/java/org/apache/hc/client5/http/AddressSelectingDnsResolver.java

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -421,25 +421,48 @@ private static Attr ipAttrOf(final InetAddress ip) {
421421
}
422422

423423
// Package-private for unit testing.
424+
//
425+
// RFC 6724 §3.1 scope assignment:
426+
// IPv6 ::1 → link-local (interface-local is multicast-only per RFC 4291 §2.7)
427+
// IPv6 link-local → link-local
428+
// IPv6 site-local → site-local (deprecated but still defined)
429+
// IPv6 multicast → scope nibble from second byte (RFC 4291)
430+
// IPv6 ULA (fc00::/7) → global (handled via policy table, not scope)
431+
// IPv6 everything else → global
432+
//
433+
// IPv4 127/8 → link-local
434+
// IPv4 169.254/16 → link-local
435+
// IPv4 everything else → global (including RFC1918 and 100.64/10)
436+
//
424437
static Scope classifyScope(final InetAddress ip) {
438+
if (ip instanceof Inet4Address) {
439+
// IPv4 scope rules per RFC 6724 §3.1:
440+
// Only loopback (127/8) and link-local (169.254/16) get link-local scope;
441+
// all other IPv4 addresses (including RFC1918 private and 100.64/10) are global.
442+
if (ip.isLoopbackAddress() || ip.isLinkLocalAddress()) {
443+
return Scope.LINK_LOCAL;
444+
}
445+
return Scope.GLOBAL;
446+
}
447+
448+
// IPv6 scope rules.
425449
if (ip.isLoopbackAddress()) {
426-
return Scope.INTERFACE_LOCAL;
450+
// Interface-local scope (0x1) is a multicast-only concept (RFC 4291 §2.7).
451+
// For unicast, the smallest meaningful scope is link-local.
452+
return Scope.LINK_LOCAL;
427453
}
428454
if (ip.isLinkLocalAddress()) {
429455
return Scope.LINK_LOCAL;
430456
}
431457
if (ip.isMulticastAddress()) {
432-
if (ip instanceof Inet6Address) {
433-
// IPv6 multicast: low 4 bits of second byte encode scope.
434-
// Not all nibble values map to a known Scope constant; treat unknown values as GLOBAL.
435-
final int nibble = ip.getAddress()[1] & 0x0f;
436-
try {
437-
return Scope.fromValue(nibble);
438-
} catch (final IllegalArgumentException e) {
439-
return Scope.GLOBAL;
440-
}
458+
// RFC 6724 §3.1 and RFC 4291: low 4 bits of second byte encode scope for IPv6 multicast.
459+
// Not all nibble values map to a known Scope constant; treat unknown values as GLOBAL.
460+
final int nibble = ip.getAddress()[1] & 0x0f;
461+
try {
462+
return Scope.fromValue(nibble);
463+
} catch (final IllegalArgumentException e) {
464+
return Scope.GLOBAL;
441465
}
442-
return Scope.GLOBAL;
443466
}
444467
if (ip.isSiteLocalAddress()) {
445468
return Scope.SITE_LOCAL;
@@ -600,8 +623,16 @@ private static PolicyEntry classify(final InetAddress ip) {
600623
return preferB;
601624
}
602625

603-
// Rule 9: Longest matching prefix (IPv6 only).
604-
if (aDst instanceof Inet6Address && bDst instanceof Inet6Address) {
626+
// Rule 9: Use longest matching prefix.
627+
// Applies when DA and DB belong to the same address family.
628+
//
629+
// Note: this is an approximation. A fully correct implementation would
630+
// use the source address's on-link prefix length (from the interface
631+
// configuration / routing table), not the bit-wise common-prefix of
632+
// source and destination. The JDK does not expose interface prefix
633+
// lengths for source address selection, so we fall back to
634+
// CommonPrefixLen(Source(D), D) as a reasonable heuristic.
635+
if (aDst.getClass() == bDst.getClass()) {
605636
final int commonA = commonPrefixLen(aSrc, aDst);
606637
final int commonB = commonPrefixLen(bSrc, bDst);
607638
if (commonA > commonB) {

httpclient5/src/test/java/org/apache/hc/client5/http/AddressSelectingDnsResolverTest.java

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,20 +181,54 @@ void filtersOutMulticastDestinations() throws Exception {
181181
// -------------------------------------------------------------------------
182182

183183
@Test
184-
void classifyScope_loopback_linkLocal_siteLocal_global() throws Exception {
185-
assertEquals(AddressSelectingDnsResolver.Scope.INTERFACE_LOCAL,
186-
AddressSelectingDnsResolver.classifyScope(inet("127.0.0.1")));
187-
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,
188187
AddressSelectingDnsResolver.classifyScope(inet("::1")));
188+
}
189+
190+
@Test
191+
void classifyScope_ipv4Loopback_isLinkLocal() throws Exception {
192+
// IPv4 127/8 maps to link-local per 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+
}
189198

199+
@Test
200+
void classifyScope_linkLocal() throws Exception {
201+
// IPv4 169.254/16 → link-local.
190202
assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL,
191203
AddressSelectingDnsResolver.classifyScope(inet("169.254.0.1")));
204+
// IPv6 fe80::/10 → link-local.
192205
assertEquals(AddressSelectingDnsResolver.Scope.LINK_LOCAL,
193206
AddressSelectingDnsResolver.classifyScope(inet("fe80::1")));
207+
}
194208

195-
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,
196214
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+
}
197222

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+
}
229+
230+
@Test
231+
void classifyScope_global() throws Exception {
198232
assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL,
199233
AddressSelectingDnsResolver.classifyScope(inet("8.8.8.8")));
200234
assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL,
@@ -341,6 +375,39 @@ void rfcRule9_prefersLongestMatchingPrefix_ipv6Only() throws Exception {
341375
assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out));
342376
}
343377

378+
@Test
379+
void rfcRule9_appliesToIpv4Pairs() throws Exception {
380+
// Both IPv4, same policy (::ffff:0:0/96 → prec 35, label 4), same scope (GLOBAL).
381+
// Rule 9 should prefer the address whose source shares a longer prefix.
382+
final InetAddress aDst = inet("192.0.2.1");
383+
final InetAddress bDst = inet("203.0.113.1");
384+
385+
// aSrc shares 24 bits with aDst (192.0.2.x); bSrc shares fewer bits with bDst.
386+
final InetAddress aSrc = inet("192.0.2.100");
387+
final InetAddress bSrc = inet("203.0.114.1");
388+
389+
delegate.add("t.example", bDst, aDst);
390+
391+
final AddressSelectingDnsResolver r =
392+
new AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, sourceMap(aDst, aSrc, bDst, bSrc));
393+
394+
final InetAddress[] out = r.resolve("t.example");
395+
// aSrc-aDst share more prefix bits than bSrc-bDst, so aDst should come first.
396+
assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out));
397+
}
398+
399+
@Test
400+
void ipv4Only_filtersSingleV6Address() throws Exception {
401+
// Regression: a single IPv6 address must still be filtered when IPV4_ONLY is set.
402+
final InetAddress v6 = inet("2001:db8::1");
403+
delegate.add("v6.example", v6);
404+
405+
final AddressSelectingDnsResolver r =
406+
new AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV4_ONLY, NO_SOURCE_ADDR);
407+
408+
assertNull(r.resolve("v6.example"));
409+
}
410+
344411
@Test
345412
void classifyScope_unknownMulticastNibble_fallsBackToGlobal() throws Exception {
346413
// ff03::1 -> scope nibble 0x3, which is not a known Scope constant.

0 commit comments

Comments
 (0)