Skip to content

Commit 1acefbe

Browse files
committed
small fixs
1 parent b2a8376 commit 1acefbe

2 files changed

Lines changed: 166 additions & 25 deletions

File tree

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

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,13 @@ public InetAddress[] resolve(final String host) throws UnknownHostException {
129129
return null;
130130
}
131131

132-
if (resolved.length <= 1) {
133-
if (LOG.isDebugEnabled()) {
134-
LOG.debug("resolved '{}' -> {}", host, fmt(resolved));
135-
}
136-
return resolved;
137-
}
138-
139132
if (LOG.isTraceEnabled()) {
140133
LOG.trace("resolving host '{}' via delegate {}", host, delegate.getClass().getName());
141134
LOG.trace("familyPreference={}", familyPreference);
142135
LOG.trace("delegate returned {} addresses for '{}': {}", resolved.length, host, fmt(resolved));
143136
}
144137

138+
// Always filter, even for a single address — the family preference may exclude it.
145139
final List<InetAddress> candidates = filterCandidates(resolved, familyPreference);
146140

147141
if (candidates.isEmpty()) {
@@ -151,6 +145,13 @@ public InetAddress[] resolve(final String host) throws UnknownHostException {
151145
return null;
152146
}
153147

148+
if (candidates.size() == 1) {
149+
if (LOG.isDebugEnabled()) {
150+
LOG.debug("resolved '{}' -> {}", host, fmt(candidates));
151+
}
152+
return candidates.toArray(new InetAddress[0]);
153+
}
154+
154155
final List<InetAddress> rfcSorted = sortByRfc6724(candidates);
155156
final List<InetAddress> ordered = applyFamilyPreference(rfcSorted, familyPreference);
156157

@@ -161,6 +162,20 @@ public InetAddress[] resolve(final String host) throws UnknownHostException {
161162
return ordered.toArray(new InetAddress[0]);
162163
}
163164

165+
@Override
166+
public List<InetSocketAddress> resolve(final String host, final int port) throws UnknownHostException {
167+
final InetAddress[] ordered = resolve(host);
168+
if (ordered == null) {
169+
// Do NOT fall back to createUnresolved: an unresolved address would let
170+
// downstream code re-resolve the hostname and bypass family filtering
171+
// (e.g. IPV4_ONLY / IPV6_ONLY).
172+
throw new UnknownHostException(host);
173+
}
174+
return Arrays.stream(ordered)
175+
.map(a -> new InetSocketAddress(a, port))
176+
.collect(Collectors.toList());
177+
}
178+
164179
@Override
165180
public String resolveCanonicalHostname(final String host) throws UnknownHostException {
166181
if (LOG.isTraceEnabled()) {
@@ -409,25 +424,48 @@ private static Attr ipAttrOf(final InetAddress ip) {
409424
}
410425

411426
// Package-private for unit testing.
427+
//
428+
// RFC 6724 §3.1 scope assignment:
429+
// IPv6 ::1 → link-local (interface-local is multicast-only per RFC 4291 §2.7)
430+
// IPv6 link-local → link-local
431+
// IPv6 site-local → site-local (deprecated but still defined)
432+
// IPv6 multicast → scope nibble from second byte (RFC 4291)
433+
// IPv6 ULA (fc00::/7) → global (handled via policy table, not scope)
434+
// IPv6 everything else → global
435+
//
436+
// IPv4 127/8 → link-local
437+
// IPv4 169.254/16 → link-local
438+
// IPv4 everything else → global (including RFC1918 and 100.64/10)
439+
//
412440
static Scope classifyScope(final InetAddress ip) {
441+
if (ip instanceof Inet4Address) {
442+
// IPv4 scope rules per RFC 6724 §3.1:
443+
// Only loopback (127/8) and link-local (169.254/16) get link-local scope;
444+
// all other IPv4 addresses (including RFC1918 private and 100.64/10) are global.
445+
if (ip.isLoopbackAddress() || ip.isLinkLocalAddress()) {
446+
return Scope.LINK_LOCAL;
447+
}
448+
return Scope.GLOBAL;
449+
}
450+
451+
// IPv6 scope rules.
413452
if (ip.isLoopbackAddress()) {
414-
return Scope.INTERFACE_LOCAL;
453+
// Interface-local scope (0x1) is a multicast-only concept (RFC 4291 §2.7).
454+
// For unicast, the smallest meaningful scope is link-local.
455+
return Scope.LINK_LOCAL;
415456
}
416457
if (ip.isLinkLocalAddress()) {
417458
return Scope.LINK_LOCAL;
418459
}
419460
if (ip.isMulticastAddress()) {
420-
if (ip instanceof Inet6Address) {
421-
// RFC 6724 §3.1 and RFC 4291: low 4 bits of second byte encode scope for IPv6 multicast.
422-
// Not all nibble values map to a known Scope constant; treat unknown values as GLOBAL.
423-
final int nibble = ip.getAddress()[1] & 0x0f;
424-
try {
425-
return Scope.fromValue(nibble);
426-
} catch (final IllegalArgumentException e) {
427-
return Scope.GLOBAL;
428-
}
461+
// RFC 6724 §3.1 and RFC 4291: low 4 bits of second byte encode scope for IPv6 multicast.
462+
// Not all nibble values map to a known Scope constant; treat unknown values as GLOBAL.
463+
final int nibble = ip.getAddress()[1] & 0x0f;
464+
try {
465+
return Scope.fromValue(nibble);
466+
} catch (final IllegalArgumentException e) {
467+
return Scope.GLOBAL;
429468
}
430-
return Scope.GLOBAL;
431469
}
432470
if (ip.isSiteLocalAddress()) {
433471
return Scope.SITE_LOCAL;
@@ -588,8 +626,16 @@ private static PolicyEntry classify(final InetAddress ip) {
588626
return preferB;
589627
}
590628

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

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

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.net.InetSocketAddress;
4242
import java.net.UnknownHostException;
4343
import java.util.Arrays;
44+
import java.util.List;
4445

4546
import org.apache.hc.client5.http.config.ProtocolFamilyPreference;
4647
import org.apache.hc.client5.http.impl.InMemoryDnsResolver;
@@ -180,20 +181,54 @@ void filtersOutMulticastDestinations() throws Exception {
180181
// -------------------------------------------------------------------------
181182

182183
@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,
187187
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+
}
188198

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

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,
195214
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+
}
196229

230+
@Test
231+
void classifyScope_global() throws Exception {
197232
assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL,
198233
AddressSelectingDnsResolver.classifyScope(inet("8.8.8.8")));
199234
assertEquals(AddressSelectingDnsResolver.Scope.GLOBAL,
@@ -389,6 +424,66 @@ private static AddressSelectingDnsResolver.SourceAddressResolver sourceMap(
389424
}
390425

391426

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+
392487
@Test
393488
void networkContains_ipv6Prefix32() throws Exception {
394489
final AddressSelectingDnsResolver.Network p32 =

0 commit comments

Comments
 (0)