out = new ArrayList<>(arr.size());
+ for (final InetAddress a : arr) {
+ out.add(addr(a));
+ }
+ return out;
+ }
+
+ private static String simpleName() {
+ return "Rfc6724Resolver";
+ }
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/config/ProtocolFamilyPreference.java b/httpclient5/src/main/java/org/apache/hc/client5/http/config/ProtocolFamilyPreference.java
new file mode 100644
index 0000000000..7ad272e3f6
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/config/ProtocolFamilyPreference.java
@@ -0,0 +1,72 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.config;
+
+/**
+ * Protocol family preference for outbound connections.
+ *
+ * Used by connection initiation code to filter or order destination
+ * addresses and, when enabled, to interleave families during staggered attempts.
+ *
+ * @since 5.7
+ */
+public enum ProtocolFamilyPreference {
+
+ /**
+ * No family bias. Preserve RFC 6724 order.
+ */
+ DEFAULT,
+
+ /**
+ * Prefer IPv4 addresses (stable: preserves RFC order within each family).
+ */
+ PREFER_IPV4,
+
+ /**
+ * Prefer IPv6 addresses (stable: preserves RFC order within each family).
+ */
+ PREFER_IPV6,
+
+ /**
+ * Filter out all non-IPv4 addresses.
+ */
+ IPV4_ONLY,
+
+ /**
+ * Filter out all non-IPv6 addresses.
+ */
+ IPV6_ONLY,
+
+ /**
+ * Interleave address families (v6, then v4, then v6, …) when multiple
+ * addresses are available, preserving the relative order within each family
+ * as produced by RFC 6724 sorting.
+ */
+ INTERLEAVE
+
+}
+
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/Rfc6724AddressSelectingDnsResolverTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/Rfc6724AddressSelectingDnsResolverTest.java
new file mode 100644
index 0000000000..3690a99aeb
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/Rfc6724AddressSelectingDnsResolverTest.java
@@ -0,0 +1,385 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.hc.client5.http.config.ProtocolFamilyPreference;
+import org.apache.hc.client5.http.impl.InMemoryDnsResolver;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class Rfc6724AddressSelectingDnsResolverTest {
+
+ private static final Rfc6724AddressSelectingDnsResolver.SourceAddressResolver NO_SOURCE_ADDR =
+ (final InetSocketAddress dest) -> null;
+
+ private InMemoryDnsResolver delegate;
+
+ @BeforeEach
+ void setUp() {
+ delegate = new InMemoryDnsResolver();
+ }
+
+ @Test
+ void ipv4Only_filtersOutIPv6() throws Exception {
+ final InetAddress v4 = inet("203.0.113.10"); // TEST-NET-3
+ final InetAddress v6 = inet("2001:db8::10"); // documentation prefix
+
+ delegate.add("dual.example", v6, v4);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV4_ONLY, NO_SOURCE_ADDR);
+
+ final InetAddress[] ordered = r.resolve("dual.example");
+ assertEquals(1, ordered.length);
+ assertInstanceOf(Inet4Address.class, ordered[0]);
+ assertEquals(v4, ordered[0]);
+ }
+
+ @Test
+ void ipv6Only_filtersOutIPv4() throws Exception {
+ final InetAddress v4 = inet("192.0.2.1"); // TEST-NET-1
+ final InetAddress v6 = inet("2001:db8::1");
+
+ delegate.add("dual.example", v4, v6);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV6_ONLY, NO_SOURCE_ADDR);
+
+ final InetAddress[] ordered = r.resolve("dual.example");
+ assertEquals(1, ordered.length);
+ assertInstanceOf(Inet6Address.class, ordered[0]);
+ assertEquals(v6, ordered[0]);
+ }
+
+ @Test
+ void ipv4Only_emptyWhenNoIPv4Candidates() throws Exception {
+ final InetAddress v6a = inet("2001:db8::1");
+ final InetAddress v6b = inet("2001:db8::2");
+
+ delegate.add("v6only.example", v6a, v6b);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.IPV4_ONLY, NO_SOURCE_ADDR);
+
+ final InetAddress[] ordered = r.resolve("v6only.example");
+ assertEquals(0, ordered.length);
+ }
+
+ @Test
+ void default_hasNoFamilyBias() throws Exception {
+ final InetAddress v6a = inet("2001:db8::1");
+ final InetAddress v6b = inet("2001:db8::2");
+ final InetAddress v4a = inet("192.0.2.1");
+ final InetAddress v4b = inet("203.0.113.10");
+
+ delegate.add("dual.example", v6a, v6b, v4a, v4b);
+
+ final Rfc6724AddressSelectingDnsResolver r1 =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, NO_SOURCE_ADDR);
+ final Rfc6724AddressSelectingDnsResolver r2 =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, NO_SOURCE_ADDR);
+
+ final InetAddress[] out1 = r1.resolve("dual.example");
+ final InetAddress[] out2 = r2.resolve("dual.example");
+
+ assertArrayEquals(out1, out2);
+ assertEquals(4, out1.length);
+ }
+
+ @Test
+ void interleave_alternatesFamilies_preservingRelativeOrder_whenRfcSortIsNoop() throws Exception {
+ final InetAddress v6a = inet("2001:db8::1");
+ final InetAddress v6b = inet("2001:db8::2");
+ final InetAddress v4a = inet("192.0.2.1");
+ final InetAddress v4b = inet("203.0.113.10");
+
+ // With NO_SOURCE_ADDR, RFC sort becomes a stable no-op; deterministic interleave.
+ delegate.add("dual.example", v6a, v6b, v4a, v4b);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.INTERLEAVE, NO_SOURCE_ADDR);
+
+ final InetAddress[] out = r.resolve("dual.example");
+ assertEquals(Arrays.asList(v6a, v4a, v6b, v4b), Arrays.asList(out));
+ }
+
+ @Test
+ void preferIpv6_groupsAllV6First_preservingRelativeOrder_whenRfcSortIsNoop() throws Exception {
+ final InetAddress v4a = inet("192.0.2.1");
+ final InetAddress v6a = inet("2001:db8::1");
+ final InetAddress v4b = inet("203.0.113.10");
+ final InetAddress v6b = inet("2001:db8::2");
+
+ delegate.add("dual.example", v4a, v6a, v4b, v6b);
+
+ final Rfc6724AddressSelectingDnsResolver preferV6 =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.PREFER_IPV6, NO_SOURCE_ADDR);
+
+ final InetAddress[] out = preferV6.resolve("dual.example");
+ assertEquals(Arrays.asList(v6a, v6b, v4a, v4b), Arrays.asList(out));
+ assertInstanceOf(Inet6Address.class, out[0]);
+ }
+
+ @Test
+ void filtersOutMulticastDestinations() throws Exception {
+ final InetAddress multicastV6 = inet("ff02::1");
+ final InetAddress v6 = inet("2001:db8::1");
+
+ delegate.add("mcast.example", multicastV6, v6);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, NO_SOURCE_ADDR);
+
+ final InetAddress[] out = r.resolve("mcast.example");
+ assertEquals(1, out.length);
+ assertEquals(v6, out[0]);
+ }
+
+ // -------------------------------------------------------------------------
+ // New: direct tests for classifyScope(..) and Scope.fromValue(..) via reflection
+ // (Scope and classifyScope are private in the resolver).
+ // -------------------------------------------------------------------------
+
+ @Test
+ void classifyScope_loopback_linkLocal_siteLocal_global() throws Exception {
+ final Class> resolverClass = Rfc6724AddressSelectingDnsResolver.class;
+
+ assertEquals("INTERFACE_LOCAL", classifyScope(resolverClass, inet("127.0.0.1")));
+ assertEquals("INTERFACE_LOCAL", classifyScope(resolverClass, inet("::1")));
+
+ assertEquals("LINK_LOCAL", classifyScope(resolverClass, inet("169.254.0.1")));
+ assertEquals("LINK_LOCAL", classifyScope(resolverClass, inet("fe80::1")));
+
+ assertEquals("SITE_LOCAL", classifyScope(resolverClass, inet("10.0.0.1")));
+
+ assertEquals("GLOBAL", classifyScope(resolverClass, inet("8.8.8.8")));
+ assertEquals("GLOBAL", classifyScope(resolverClass, inet("2003::1")));
+ }
+
+ @Test
+ void classifyScope_ipv6Multicast_usesLowNibbleScope() throws Exception {
+ final Class> resolverClass = Rfc6724AddressSelectingDnsResolver.class;
+
+ // ff01::1 -> scope 0x1 -> INTERFACE_LOCAL
+ assertEquals("INTERFACE_LOCAL", classifyScope(resolverClass, inet("ff01::1")));
+ // ff02::1 -> scope 0x2 -> LINK_LOCAL
+ assertEquals("LINK_LOCAL", classifyScope(resolverClass, inet("ff02::1")));
+ // ff04::1 -> scope 0x4 -> ADMIN_LOCAL
+ assertEquals("ADMIN_LOCAL", classifyScope(resolverClass, inet("ff04::1")));
+ // ff05::1 -> scope 0x5 -> SITE_LOCAL
+ assertEquals("SITE_LOCAL", classifyScope(resolverClass, inet("ff05::1")));
+ // ff08::1 -> scope 0x8 -> ORG_LOCAL
+ assertEquals("ORG_LOCAL", classifyScope(resolverClass, inet("ff08::1")));
+ // ff0e::1 -> scope 0xe -> GLOBAL (default branch)
+ assertEquals("GLOBAL", classifyScope(resolverClass, inet("ff0e::1")));
+ }
+
+ @Test
+ void scopeFromValue_mapsKnownConstants_andDefaultsToGlobal() throws Exception {
+ final Class> resolverClass = Rfc6724AddressSelectingDnsResolver.class;
+ final Class> scopeClass = findDeclaredClass(resolverClass, "Scope");
+ assertNotNull(scopeClass);
+
+ assertEquals("INTERFACE_LOCAL", scopeFromValue(scopeClass, 0x1));
+ assertEquals("LINK_LOCAL", scopeFromValue(scopeClass, 0x2));
+ assertEquals("ADMIN_LOCAL", scopeFromValue(scopeClass, 0x4));
+ assertEquals("SITE_LOCAL", scopeFromValue(scopeClass, 0x5));
+ assertEquals("ORG_LOCAL", scopeFromValue(scopeClass, 0x8));
+
+ assertEquals("GLOBAL", scopeFromValue(scopeClass, 0x0));
+ assertEquals("GLOBAL", scopeFromValue(scopeClass, 0xe));
+ assertEquals("GLOBAL", scopeFromValue(scopeClass, 0xf));
+ }
+
+ @Test
+ void rfcRule2_prefersMatchingScope() throws Exception {
+ final InetAddress aDst = inet("2001:db8::1");
+ final InetAddress bDst = inet("2001:db8::2");
+
+ // A matches scope (GLOBAL == GLOBAL); B mismatches (GLOBAL != LINK_LOCAL)
+ final InetAddress aSrc = inet("2001:db8::abcd");
+ final InetAddress bSrc = inet("fe80::1");
+
+ delegate.add("t.example", bDst, aDst);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, sourceMap(aDst, aSrc, bDst, bSrc));
+
+ final InetAddress[] out = r.resolve("t.example");
+ assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out));
+ }
+
+ @Test
+ void rfcRule5_prefersMatchingLabel() throws Exception {
+ final InetAddress aDst = inet("2001:db8::1"); // label 5 (2001::/32)
+ final InetAddress bDst = inet("2001:db8::2"); // label 5
+
+ final InetAddress aSrc = inet("2001:db8::abcd"); // label 5 -> matches A
+ final InetAddress bSrc = inet("::ffff:192.0.2.1"); // label 4 -> does not match B
+
+ delegate.add("t.example", bDst, aDst);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, sourceMap(aDst, aSrc, bDst, bSrc));
+
+ final InetAddress[] out = r.resolve("t.example");
+ assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out));
+ }
+
+ @Test
+ void rfcRule6_prefersHigherPrecedence() throws Exception {
+ final InetAddress aDst = inet("::1"); // precedence 50 (policy ::1)
+ final InetAddress bDst = inet("2001:db8::1"); // precedence 5 (policy 2001::/32)
+
+ final InetAddress aSrc = inet("::1");
+ final InetAddress bSrc = inet("2001:db8::abcd");
+
+ delegate.add("t.example", bDst, aDst);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, sourceMap(aDst, aSrc, bDst, bSrc));
+
+ final InetAddress[] out = r.resolve("t.example");
+ assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out));
+ }
+
+ @Test
+ void rfcRule8_prefersSmallerScope_whenPrecedenceAndLabelTie() throws Exception {
+ // Both fall to ::/0 policy -> precedence 40, label 1, but different scopes.
+ final InetAddress aDst = inet("fe80::1"); // LINK_LOCAL scope (0x2)
+ final InetAddress bDst = inet("2003::1"); // GLOBAL scope (0xe)
+
+ final InetAddress aSrc = inet("fe80::2"); // LINK_LOCAL, label 1
+ final InetAddress bSrc = inet("2003::2"); // GLOBAL, label 1
+
+ delegate.add("t.example", bDst, aDst);
+
+ final Rfc6724AddressSelectingDnsResolver r =
+ new Rfc6724AddressSelectingDnsResolver(delegate, ProtocolFamilyPreference.DEFAULT, sourceMap(aDst, aSrc, bDst, bSrc));
+
+ final InetAddress[] out = r.resolve("t.example");
+ assertEquals(Arrays.asList(aDst, bDst), Arrays.asList(out));
+ }
+
+ @Test
+ void addr_fmt_simpleName() throws Exception {
+ final Class> resolverClass = Rfc6724AddressSelectingDnsResolver.class;
+
+ final Method addr = resolverClass.getDeclaredMethod("addr", InetAddress.class);
+ addr.setAccessible(true);
+
+ final Method fmtArr = resolverClass.getDeclaredMethod("fmt", InetAddress[].class);
+ fmtArr.setAccessible(true);
+
+ final Method fmtList = resolverClass.getDeclaredMethod("fmt", List.class);
+ fmtList.setAccessible(true);
+
+ final Method simpleName = resolverClass.getDeclaredMethod("simpleName");
+ simpleName.setAccessible(true);
+
+ assertEquals("null", (String) addr.invoke(null, new Object[]{null}));
+
+ final InetAddress v4 = inet("192.0.2.1");
+ final InetAddress v6 = inet("2001:db8::1");
+
+ final String s4 = (String) addr.invoke(null, v4);
+ final String s6 = (String) addr.invoke(null, v6);
+
+ assertEquals("IPv4(" + v4.getHostAddress() + ")", s4);
+ assertEquals("IPv6(" + v6.getHostAddress() + ")", s6);
+
+ @SuppressWarnings("unchecked") final List arrOut = (List) fmtArr.invoke(null, new Object[]{new InetAddress[]{v6, v4}});
+ assertEquals(Arrays.asList("IPv6(" + v6.getHostAddress() + ")", "IPv4(" + v4.getHostAddress() + ")"), arrOut);
+
+ @SuppressWarnings("unchecked") final List listOut = (List) fmtList.invoke(null, Arrays.asList(v4, v6));
+ assertEquals(Arrays.asList("IPv4(" + v4.getHostAddress() + ")", "IPv6(" + v6.getHostAddress() + ")"), listOut);
+
+ assertNotNull((String) simpleName.invoke(null));
+ assertEquals("Rfc6724Resolver", (String) simpleName.invoke(null));
+ }
+
+ private static InetAddress inet(final String s) {
+ try {
+ return InetAddress.getByName(s);
+ } catch (final UnknownHostException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ private static Rfc6724AddressSelectingDnsResolver.SourceAddressResolver sourceMap(
+ final InetAddress aDst, final InetAddress aSrc,
+ final InetAddress bDst, final InetAddress bSrc) {
+ return (final InetSocketAddress dest) -> {
+ final InetAddress d = dest.getAddress();
+ if (aDst.equals(d)) {
+ return aSrc;
+ }
+ if (bDst.equals(d)) {
+ return bSrc;
+ }
+ return null;
+ };
+ }
+
+ private static String classifyScope(final Class> resolverClass, final InetAddress ip) throws Exception {
+ final Method m = resolverClass.getDeclaredMethod("classifyScope", InetAddress.class);
+ m.setAccessible(true);
+ final Object scope = m.invoke(null, ip);
+ return scope != null ? scope.toString() : null;
+ }
+
+ private static String scopeFromValue(final Class> scopeClass, final int v) throws Exception {
+ final Method m = scopeClass.getDeclaredMethod("fromValue", int.class);
+ m.setAccessible(true);
+ final Object scope = m.invoke(null, v);
+ return scope != null ? scope.toString() : null;
+ }
+
+ private static Class> findDeclaredClass(final Class> outer, final String simpleName) {
+ for (final Class> c : outer.getDeclaredClasses()) {
+ if (simpleName.equals(c.getSimpleName())) {
+ return c;
+ }
+ }
+ return null;
+ }
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/Rfc6724ResolverExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/Rfc6724ResolverExample.java
new file mode 100644
index 0000000000..4113bcc4b8
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/Rfc6724ResolverExample.java
@@ -0,0 +1,67 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.examples;
+
+import java.net.InetAddress;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.config.ProtocolFamilyPreference;
+
+public final class Rfc6724ResolverExample {
+
+ public static void main(final String[] args) throws Exception {
+ final String host = args.length > 0 ? args[0] : "localhost";
+ final ProtocolFamilyPreference pref = args.length > 1
+ ? ProtocolFamilyPreference.valueOf(args[1])
+ : ProtocolFamilyPreference.DEFAULT;
+
+ final DnsResolver resolver = new Rfc6724AddressSelectingDnsResolver(SystemDefaultDnsResolver.INSTANCE, pref);
+
+ final InetAddress[] out = resolver.resolve(host);
+
+ System.out.println("Host: " + host);
+ System.out.println("Preference: " + pref);
+ if (out == null) {
+ System.out.println("Result: null");
+ return;
+ }
+ if (out.length == 0) {
+ System.out.println("Result: []");
+ return;
+ }
+ for (final InetAddress a : out) {
+ final String family = a instanceof java.net.Inet6Address ? "IPv6" : "IPv4";
+ System.out.println(" " + family + " " + a.getHostAddress());
+ }
+ }
+
+ private Rfc6724ResolverExample() {
+ }
+}