@@ -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