From 3f61870ac6e5b18dbb74ce6f6cb2930ad8750a43 Mon Sep 17 00:00:00 2001 From: Google Java Core Libraries Date: Fri, 24 May 2024 09:39:45 -0700 Subject: [PATCH] Change `InetAddress`-`String` conversion methods to preserve the scope ID. This matches the behavior in https://bugs.openjdk.org/browse/JDK-8272215 (except still not supporting brackets []) RELNOTES=`net`: Changed `InetAddress`-`String` conversion methods to preserve the scope ID. This may lead to two kinds of problems: First, callers of those methods may be relying on the returned values _not_ to include the scope ID. For example, they might compensate for the old behavior of the methods by appending the scope ID to a returned string themselves. (If so, you can update your code to stop doing so at the same time as you upgrade Guava. Of, if your code might run against multiple versions of Guava, you can check whether Guava included a scope ID before adding one yourself.) Or they may pass the returned string to another system that does not understand scope IDs. (If so, you can strip the scope ID off, whether by truncating the string form at a `%` character (leaving behind any trailing `]` character in the case of `forUriString`) or by replacing the returned `InetAddress` with a new instance constructed by calling `InetAddress.getByAddress(addr)`. The other possible cause for problems is that `java.net.InetAddress` validates any provided scope ID against the interfaces available on the machine. As a result, methods in `InetAddresses` may now fail if the scope ID fails validation, including if the code runs in an Android app without networking permission. If this is not the behavior that you want, then you can strip off the scope ID from the input string before passing it to Guava, as discussed above. PiperOrigin-RevId: 636947430 --- .../google/common/net/InetAddressesTest.java | 130 +++++++++++++++--- .../com/google/common/net/InetAddresses.java | 124 +++++++++++++---- .../google/common/net/InetAddressesTest.java | 130 +++++++++++++++--- .../com/google/common/net/InetAddresses.java | 124 +++++++++++++---- 4 files changed, 420 insertions(+), 88 deletions(-) diff --git a/android/guava-tests/test/com/google/common/net/InetAddressesTest.java b/android/guava-tests/test/com/google/common/net/InetAddressesTest.java index 241c6bbf551a..c31c6e23fb40 100644 --- a/android/guava-tests/test/com/google/common/net/InetAddressesTest.java +++ b/android/guava-tests/test/com/google/common/net/InetAddressesTest.java @@ -25,7 +25,10 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.net.UnknownHostException; +import java.util.Enumeration; import junit.framework.TestCase; /** @@ -191,14 +194,10 @@ public void testConvertDottedQuadToHex() throws UnknownHostException { } } - // see https://github.com/google/guava/issues/2587 - private static final ImmutableSet SCOPE_IDS = - ImmutableSet.of("eno1", "en1", "eth0", "X", "1", "2", "14", "20"); - - public void testIPv4AddressWithScopeId() { + public void testIPv4AddressWithScopeId() throws SocketException { ImmutableSet ipStrings = ImmutableSet.of("1.2.3.4", "192.168.0.1"); for (String ipString : ipStrings) { - for (String scopeId : SCOPE_IDS) { + for (String scopeId : getMachineScopesAndInterfaces()) { String withScopeId = ipString + "%" + scopeId; assertFalse( "InetAddresses.isInetAddress(" + withScopeId + ") should be false but was true", @@ -207,11 +206,11 @@ public void testIPv4AddressWithScopeId() { } } - public void testDottedQuadAddressWithScopeId() { + public void testDottedQuadAddressWithScopeId() throws SocketException { ImmutableSet ipStrings = ImmutableSet.of("7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127"); for (String ipString : ipStrings) { - for (String scopeId : SCOPE_IDS) { + for (String scopeId : getMachineScopesAndInterfaces()) { String withScopeId = ipString + "%" + scopeId; assertFalse( "InetAddresses.isInetAddress(" + withScopeId + ") should be false but was true", @@ -220,29 +219,108 @@ public void testDottedQuadAddressWithScopeId() { } } - public void testIPv6AddressWithScopeId() { + public void testIPv6AddressWithScopeId() throws SocketException, UnknownHostException { + ImmutableSet ipStrings = + ImmutableSet.of( + "::1", + "1180::a", + "1180::1", + "1180::2", + "1180::42", + "1180::3dd0:7f8e:57b7:34d5", + "1180::71a3:2b00:ddd3:753f", + "1180::8b2:d61e:e5c:b333", + "1180::b059:65f4:e877:c40", + "fe80::34", + "fec0::34"); + boolean processedNamedInterface = false; + for (String ipString : ipStrings) { + for (String scopeId : getMachineScopesAndInterfaces()) { + String withScopeId = ipString + "%" + scopeId; + assertTrue( + "InetAddresses.isInetAddress(" + withScopeId + ") should be true but was false", + InetAddresses.isInetAddress(withScopeId)); + Inet6Address parsed; + boolean isNumeric = scopeId.matches("\\d+"); + try { + parsed = (Inet6Address) InetAddresses.forString(withScopeId); + } catch (IllegalArgumentException e) { + if (!isNumeric) { + // Android doesn't recognize %interface as valid + continue; + } + throw e; + } + processedNamedInterface |= !isNumeric; + assertThat(InetAddresses.toAddrString(parsed)).contains("%"); + if (isNumeric) { + assertEquals(Integer.parseInt(scopeId), parsed.getScopeId()); + } else { + assertEquals(scopeId, parsed.getScopedInterface().getName()); + } + Inet6Address reparsed = + (Inet6Address) InetAddresses.forString(InetAddresses.toAddrString(parsed)); + assertEquals(reparsed, parsed); + assertEquals(reparsed.getScopeId(), parsed.getScopeId()); + } + } + assertTrue(processedNamedInterface); + } + + public void testIPv6AddressWithScopeId_platformEquivalence() + throws SocketException, UnknownHostException { ImmutableSet ipStrings = ImmutableSet.of( - "0:0:0:0:0:0:0:1", - "fe80::a", - "fe80::1", - "fe80::2", - "fe80::42", - "fe80::3dd0:7f8e:57b7:34d5", - "fe80::71a3:2b00:ddd3:753f", - "fe80::8b2:d61e:e5c:b333", - "fe80::b059:65f4:e877:c40"); + "::1", + "1180::a", + "1180::1", + "1180::2", + "1180::42", + "1180::3dd0:7f8e:57b7:34d5", + "1180::71a3:2b00:ddd3:753f", + "1180::8b2:d61e:e5c:b333", + "1180::b059:65f4:e877:c40", + "fe80::34", + "fec0::34"); for (String ipString : ipStrings) { - for (String scopeId : SCOPE_IDS) { + for (String scopeId : getMachineScopesAndInterfaces()) { String withScopeId = ipString + "%" + scopeId; assertTrue( "InetAddresses.isInetAddress(" + withScopeId + ") should be true but was false", InetAddresses.isInetAddress(withScopeId)); - assertEquals(InetAddresses.forString(withScopeId), InetAddresses.forString(ipString)); + Inet6Address parsed; + boolean isNumeric = scopeId.matches("\\d+"); + try { + parsed = (Inet6Address) InetAddresses.forString(withScopeId); + } catch (IllegalArgumentException e) { + if (!isNumeric) { + // Android doesn't recognize %interface as valid + continue; + } + throw e; + } + Inet6Address platformValue; + try { + platformValue = (Inet6Address) InetAddress.getByName(withScopeId); + } catch (UnknownHostException e) { + // Android doesn't recognize %interface as valid + if (!isNumeric) { + continue; + } + throw e; + } + assertEquals(platformValue, parsed); + assertEquals(platformValue.getScopeId(), parsed.getScopeId()); } } } + public void testIPv6AddressWithBadScopeId() throws SocketException, UnknownHostException { + assertThrows( + IllegalArgumentException.class, + () -> InetAddresses.forString("1180::b059:65f4:e877:c40%eth9")); + } + public void testToAddrStringIPv4() { // Don't need to test IPv4 much; it just calls getHostAddress(). assertEquals("1.2.3.4", InetAddresses.toAddrString(InetAddresses.forString("1.2.3.4"))); @@ -792,6 +870,18 @@ public void testFromIpv6BigIntegerInputTooLarge() { expected.getMessage()); } + // see https://github.com/google/guava/issues/2587 + private static ImmutableSet getMachineScopesAndInterfaces() throws SocketException { + ImmutableSet.Builder builder = ImmutableSet.builder(); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + assertTrue(interfaces.hasMoreElements()); + while (interfaces.hasMoreElements()) { + NetworkInterface i = interfaces.nextElement(); + builder.add(i.getName()).add(String.valueOf(i.getIndex())); + } + return builder.build(); + } + /** Checks that the IP converts to the big integer and the big integer converts to the IP. */ private static void checkBigIntegerConversion(String ip, BigInteger bigIntegerIp) { InetAddress address = InetAddresses.forString(ip); diff --git a/android/guava/src/com/google/common/net/InetAddresses.java b/android/guava/src/com/google/common/net/InetAddresses.java index 540a21986a6f..8046c0f75cec 100644 --- a/android/guava/src/com/google/common/net/InetAddresses.java +++ b/android/guava/src/com/google/common/net/InetAddresses.java @@ -30,11 +30,14 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Locale; import javax.annotation.CheckForNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Static utility methods pertaining to {@link InetAddress} instances. @@ -126,7 +129,7 @@ private static Inet4Address getInet4Address(byte[] bytes) { bytes.length); // Given a 4-byte array, this cast should always succeed. - return (Inet4Address) bytesToInetAddress(bytes); + return (Inet4Address) bytesToInetAddress(bytes, null); } /** @@ -134,28 +137,32 @@ private static Inet4Address getInet4Address(byte[] bytes) { * *

This deliberately avoids all nameservice lookups (e.g. no DNS). * - *

Anything after a {@code %} in an IPv6 address is ignored (assumed to be a Scope ID). - * *

This method accepts non-ASCII digits, for example {@code "192.168.0.1"} (those are fullwidth * characters). That is consistent with {@link InetAddress}, but not with various RFCs. If you * want to accept ASCII digits only, you can use something like {@code * CharMatcher.ascii().matchesAllOf(ipString)}. * + *

The scope ID is validated against the interfaces on the machine, which requires permissions + * under Android. + * + *

Android users on API >= 29: Prefer {@code InetAddresses.parseNumericAddress}. + * * @param ipString {@code String} containing an IPv4 or IPv6 string literal, e.g. {@code - * "192.168.0.1"} or {@code "2001:db8::1"} + * "192.168.0.1"} or {@code "2001:db8::1"} or with a scope ID, e.g. {@code "2001:db8::1%eth0"} * @return {@link InetAddress} representing the argument * @throws IllegalArgumentException if the argument is not a valid IP string literal */ @CanIgnoreReturnValue // TODO(b/219820829): consider removing public static InetAddress forString(String ipString) { - byte[] addr = ipStringToBytes(ipString); + Scope scope = new Scope(); + byte[] addr = ipStringToBytes(ipString, scope); // The argument was malformed, i.e. not an IP string literal. if (addr == null) { throw formatIllegalArgumentException("'%s' is not an IP string literal.", ipString); } - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, scope.scope); } /** @@ -171,12 +178,16 @@ public static InetAddress forString(String ipString) { * @return {@code true} if the argument is a valid IP string literal */ public static boolean isInetAddress(String ipString) { - return ipStringToBytes(ipString) != null; + return ipStringToBytes(ipString, null) != null; + } + + private static final class Scope { + private String scope; } /** Returns {@code null} if unable to parse into a {@code byte[]}. */ @CheckForNull - private static byte[] ipStringToBytes(String ipStringParam) { + private static byte[] ipStringToBytes(String ipStringParam, @Nullable Scope scope) { String ipString = ipStringParam; // Make a first pass to categorize the characters in this string. boolean hasColon = false; @@ -193,7 +204,7 @@ private static byte[] ipStringToBytes(String ipStringParam) { hasColon = true; } else if (c == '%') { percentIndex = i; - break; // everything after a '%' is ignored (it's a Scope ID): http://superuser.com/a/99753 + break; } else if (Character.digit(c, 16) == -1) { return null; // Everything else must be a decimal or hex digit. } @@ -208,6 +219,9 @@ private static byte[] ipStringToBytes(String ipStringParam) { } } if (percentIndex != -1) { + if (scope != null) { + scope.scope = ipString.substring(percentIndex + 1); + } ipString = ipString.substring(0, percentIndex); } return textToNumericFormatV6(ipString); @@ -358,6 +372,24 @@ private static byte parseOctet(String ipString, int start, int end) { return (byte) octet; } + /** Returns a -1 if unable to parse */ + private static int tryParseDecimal(String string, int start, int end) { + int decimal = 0; + final int max = Integer.MAX_VALUE / 10; // for int overflow detection + for (int i = start; i < end; i++) { + if (decimal > max) { + return -1; + } + decimal *= 10; + int digit = Character.digit(string.charAt(i), 10); + if (digit < 0) { + return -1; + } + decimal += digit; + } + return decimal; + } + // Parse a hextet out of the ipString from start (inclusive) to end (exclusive) private static short parseHextet(String ipString, int start, int end) { // Note: we already verified that this string contains only hex digits. @@ -383,9 +415,30 @@ private static short parseHextet(String ipString, int start, int end) { * @param addr the raw 4-byte or 16-byte IP address in big-endian order * @return an InetAddress object created from the raw IP address */ - private static InetAddress bytesToInetAddress(byte[] addr) { + private static InetAddress bytesToInetAddress(byte[] addr, @Nullable String scope) { try { - return InetAddress.getByAddress(addr); + InetAddress address = InetAddress.getByAddress(addr); + if (scope == null) { + return address; + } + checkArgument( + address instanceof Inet6Address, "Unexpected state, scope should only appear for ipv6"); + Inet6Address v6Address = (Inet6Address) address; + int interfaceIndex = tryParseDecimal(scope, 0, scope.length()); + if (interfaceIndex != -1) { + return Inet6Address.getByAddress( + v6Address.getHostAddress(), v6Address.getAddress(), interfaceIndex); + } + try { + NetworkInterface asInterface = NetworkInterface.getByName(scope); + if (asInterface == null) { + throw formatIllegalArgumentException("No such interface: '%s'", scope); + } + return Inet6Address.getByAddress( + v6Address.getHostAddress(), v6Address.getAddress(), asInterface); + } catch (SocketException | UnknownHostException e) { + throw new IllegalArgumentException("No such interface: " + scope, e); + } } catch (UnknownHostException e) { throw new AssertionError(e); } @@ -397,10 +450,13 @@ private static InetAddress bytesToInetAddress(byte[] addr) { *

For IPv4 addresses, this is identical to {@link InetAddress#getHostAddress()}, but for IPv6 * addresses, the output follows RFC 5952 section * 4. The main difference is that this method uses "::" for zero compression, while Java's version - * uses the uncompressed form. + * uses the uncompressed form (except on Android, where the zero compression is also done). The + * other difference is that this method outputs any scope ID in the format that it was provided at + * creation time, while Android may always output it as an interface name, even if it was supplied + * as a numeric ID. * *

This method uses hexadecimal for all IPv6 addresses, including IPv4-mapped IPv6 addresses - * such as "::c000:201". The output does not include a Scope ID. + * such as "::c000:201". * * @param ip {@link InetAddress} to be converted to an address string * @return {@code String} containing the text-formatted IP address @@ -413,14 +469,29 @@ public static String toAddrString(InetAddress ip) { // requireNonNull accommodates Android's @RecentlyNullable annotation on getHostAddress return requireNonNull(ip.getHostAddress()); } - checkArgument(ip instanceof Inet6Address); byte[] bytes = ip.getAddress(); int[] hextets = new int[IPV6_PART_COUNT]; for (int i = 0; i < hextets.length; i++) { hextets[i] = Ints.fromBytes((byte) 0, (byte) 0, bytes[2 * i], bytes[2 * i + 1]); } compressLongestRunOfZeroes(hextets); - return hextetsToIPv6String(hextets); + + return hextetsToIPv6String(hextets) + scopeWithDelimiter((Inet6Address) ip); + } + + private static String scopeWithDelimiter(Inet6Address ip) { + // getHostAddress on android sometimes maps the scope id to an invalid interface name; if the + // mapped interface isn't present, fallback to use the scope id (which has no validation against + // present interfaces) + NetworkInterface scopedInterface = ip.getScopedInterface(); + if (scopedInterface != null) { + return "%" + scopedInterface.getName(); + } + int scope = ip.getScopeId(); + if (scope != 0) { + return "%" + scope; + } + return ""; } /** @@ -529,10 +600,11 @@ public static String toUriString(InetAddress ip) { * @param hostAddr an RFC 3986 section 3.2.2 encoded IPv4 or IPv6 address * @return an InetAddress representing the address in {@code hostAddr} * @throws IllegalArgumentException if {@code hostAddr} is not a valid IPv4 address, or IPv6 - * address surrounded by square brackets + * address surrounded by square brackets, or if the address has a scope id that fails + * validation against interfaces on the machine */ public static InetAddress forUriString(String hostAddr) { - InetAddress addr = forUriStringNoThrow(hostAddr); + InetAddress addr = forUriStringOrNull(hostAddr, /* parseScope= */ true); if (addr == null) { throw formatIllegalArgumentException("Not a valid URI IP literal: '%s'", hostAddr); } @@ -541,7 +613,7 @@ public static InetAddress forUriString(String hostAddr) { } @CheckForNull - private static InetAddress forUriStringNoThrow(String hostAddr) { + private static InetAddress forUriStringOrNull(String hostAddr, boolean parseScope) { checkNotNull(hostAddr); // Decide if this should be an IPv6 or IPv4 address. @@ -556,12 +628,13 @@ private static InetAddress forUriStringNoThrow(String hostAddr) { } // Parse the address, and make sure the length/version is correct. - byte[] addr = ipStringToBytes(ipString); + Scope scope = parseScope ? new Scope() : null; + byte[] addr = ipStringToBytes(ipString, scope); if (addr == null || addr.length != expectBytes) { return null; } - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, (scope != null) ? scope.scope : null); } /** @@ -573,11 +646,14 @@ private static InetAddress forUriStringNoThrow(String hostAddr) { * want to accept ASCII digits only, you can use something like {@code * CharMatcher.ascii().matchesAllOf(ipString)}. * + *

Note that if this method returns {@code true}, a call to {@link #forUriString(String)} can + * throw if the address has a scope id fails validation against interfaces on the machine. + * * @param ipString {@code String} to evaluated as an IP URI host string literal * @return {@code true} if the argument is a valid IP URI host */ public static boolean isUriInetAddress(String ipString) { - return forUriStringNoThrow(ipString) != null; + return forUriStringOrNull(ipString, /* parseScope= */ false) != null; } /** @@ -876,7 +952,7 @@ public static Inet4Address getEmbeddedIPv4ClientAddress(Inet6Address ip) { * @since 10.0 */ public static boolean isMappedIPv4Address(String ipString) { - byte[] bytes = ipStringToBytes(ipString); + byte[] bytes = ipStringToBytes(ipString, null); if (bytes != null && bytes.length == 16) { for (int i = 0; i < 10; i++) { if (bytes[i] != 0) { @@ -1108,7 +1184,7 @@ public static InetAddress decrement(InetAddress address) { checkArgument(i >= 0, "Decrementing %s would wrap.", address); addr[i]--; - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, null); } /** @@ -1131,7 +1207,7 @@ public static InetAddress increment(InetAddress address) { checkArgument(i >= 0, "Incrementing %s would wrap.", address); addr[i]++; - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, null); } /** diff --git a/guava-tests/test/com/google/common/net/InetAddressesTest.java b/guava-tests/test/com/google/common/net/InetAddressesTest.java index 241c6bbf551a..c31c6e23fb40 100644 --- a/guava-tests/test/com/google/common/net/InetAddressesTest.java +++ b/guava-tests/test/com/google/common/net/InetAddressesTest.java @@ -25,7 +25,10 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.net.UnknownHostException; +import java.util.Enumeration; import junit.framework.TestCase; /** @@ -191,14 +194,10 @@ public void testConvertDottedQuadToHex() throws UnknownHostException { } } - // see https://github.com/google/guava/issues/2587 - private static final ImmutableSet SCOPE_IDS = - ImmutableSet.of("eno1", "en1", "eth0", "X", "1", "2", "14", "20"); - - public void testIPv4AddressWithScopeId() { + public void testIPv4AddressWithScopeId() throws SocketException { ImmutableSet ipStrings = ImmutableSet.of("1.2.3.4", "192.168.0.1"); for (String ipString : ipStrings) { - for (String scopeId : SCOPE_IDS) { + for (String scopeId : getMachineScopesAndInterfaces()) { String withScopeId = ipString + "%" + scopeId; assertFalse( "InetAddresses.isInetAddress(" + withScopeId + ") should be false but was true", @@ -207,11 +206,11 @@ public void testIPv4AddressWithScopeId() { } } - public void testDottedQuadAddressWithScopeId() { + public void testDottedQuadAddressWithScopeId() throws SocketException { ImmutableSet ipStrings = ImmutableSet.of("7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127"); for (String ipString : ipStrings) { - for (String scopeId : SCOPE_IDS) { + for (String scopeId : getMachineScopesAndInterfaces()) { String withScopeId = ipString + "%" + scopeId; assertFalse( "InetAddresses.isInetAddress(" + withScopeId + ") should be false but was true", @@ -220,29 +219,108 @@ public void testDottedQuadAddressWithScopeId() { } } - public void testIPv6AddressWithScopeId() { + public void testIPv6AddressWithScopeId() throws SocketException, UnknownHostException { + ImmutableSet ipStrings = + ImmutableSet.of( + "::1", + "1180::a", + "1180::1", + "1180::2", + "1180::42", + "1180::3dd0:7f8e:57b7:34d5", + "1180::71a3:2b00:ddd3:753f", + "1180::8b2:d61e:e5c:b333", + "1180::b059:65f4:e877:c40", + "fe80::34", + "fec0::34"); + boolean processedNamedInterface = false; + for (String ipString : ipStrings) { + for (String scopeId : getMachineScopesAndInterfaces()) { + String withScopeId = ipString + "%" + scopeId; + assertTrue( + "InetAddresses.isInetAddress(" + withScopeId + ") should be true but was false", + InetAddresses.isInetAddress(withScopeId)); + Inet6Address parsed; + boolean isNumeric = scopeId.matches("\\d+"); + try { + parsed = (Inet6Address) InetAddresses.forString(withScopeId); + } catch (IllegalArgumentException e) { + if (!isNumeric) { + // Android doesn't recognize %interface as valid + continue; + } + throw e; + } + processedNamedInterface |= !isNumeric; + assertThat(InetAddresses.toAddrString(parsed)).contains("%"); + if (isNumeric) { + assertEquals(Integer.parseInt(scopeId), parsed.getScopeId()); + } else { + assertEquals(scopeId, parsed.getScopedInterface().getName()); + } + Inet6Address reparsed = + (Inet6Address) InetAddresses.forString(InetAddresses.toAddrString(parsed)); + assertEquals(reparsed, parsed); + assertEquals(reparsed.getScopeId(), parsed.getScopeId()); + } + } + assertTrue(processedNamedInterface); + } + + public void testIPv6AddressWithScopeId_platformEquivalence() + throws SocketException, UnknownHostException { ImmutableSet ipStrings = ImmutableSet.of( - "0:0:0:0:0:0:0:1", - "fe80::a", - "fe80::1", - "fe80::2", - "fe80::42", - "fe80::3dd0:7f8e:57b7:34d5", - "fe80::71a3:2b00:ddd3:753f", - "fe80::8b2:d61e:e5c:b333", - "fe80::b059:65f4:e877:c40"); + "::1", + "1180::a", + "1180::1", + "1180::2", + "1180::42", + "1180::3dd0:7f8e:57b7:34d5", + "1180::71a3:2b00:ddd3:753f", + "1180::8b2:d61e:e5c:b333", + "1180::b059:65f4:e877:c40", + "fe80::34", + "fec0::34"); for (String ipString : ipStrings) { - for (String scopeId : SCOPE_IDS) { + for (String scopeId : getMachineScopesAndInterfaces()) { String withScopeId = ipString + "%" + scopeId; assertTrue( "InetAddresses.isInetAddress(" + withScopeId + ") should be true but was false", InetAddresses.isInetAddress(withScopeId)); - assertEquals(InetAddresses.forString(withScopeId), InetAddresses.forString(ipString)); + Inet6Address parsed; + boolean isNumeric = scopeId.matches("\\d+"); + try { + parsed = (Inet6Address) InetAddresses.forString(withScopeId); + } catch (IllegalArgumentException e) { + if (!isNumeric) { + // Android doesn't recognize %interface as valid + continue; + } + throw e; + } + Inet6Address platformValue; + try { + platformValue = (Inet6Address) InetAddress.getByName(withScopeId); + } catch (UnknownHostException e) { + // Android doesn't recognize %interface as valid + if (!isNumeric) { + continue; + } + throw e; + } + assertEquals(platformValue, parsed); + assertEquals(platformValue.getScopeId(), parsed.getScopeId()); } } } + public void testIPv6AddressWithBadScopeId() throws SocketException, UnknownHostException { + assertThrows( + IllegalArgumentException.class, + () -> InetAddresses.forString("1180::b059:65f4:e877:c40%eth9")); + } + public void testToAddrStringIPv4() { // Don't need to test IPv4 much; it just calls getHostAddress(). assertEquals("1.2.3.4", InetAddresses.toAddrString(InetAddresses.forString("1.2.3.4"))); @@ -792,6 +870,18 @@ public void testFromIpv6BigIntegerInputTooLarge() { expected.getMessage()); } + // see https://github.com/google/guava/issues/2587 + private static ImmutableSet getMachineScopesAndInterfaces() throws SocketException { + ImmutableSet.Builder builder = ImmutableSet.builder(); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + assertTrue(interfaces.hasMoreElements()); + while (interfaces.hasMoreElements()) { + NetworkInterface i = interfaces.nextElement(); + builder.add(i.getName()).add(String.valueOf(i.getIndex())); + } + return builder.build(); + } + /** Checks that the IP converts to the big integer and the big integer converts to the IP. */ private static void checkBigIntegerConversion(String ip, BigInteger bigIntegerIp) { InetAddress address = InetAddresses.forString(ip); diff --git a/guava/src/com/google/common/net/InetAddresses.java b/guava/src/com/google/common/net/InetAddresses.java index 540a21986a6f..8046c0f75cec 100644 --- a/guava/src/com/google/common/net/InetAddresses.java +++ b/guava/src/com/google/common/net/InetAddresses.java @@ -30,11 +30,14 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Locale; import javax.annotation.CheckForNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Static utility methods pertaining to {@link InetAddress} instances. @@ -126,7 +129,7 @@ private static Inet4Address getInet4Address(byte[] bytes) { bytes.length); // Given a 4-byte array, this cast should always succeed. - return (Inet4Address) bytesToInetAddress(bytes); + return (Inet4Address) bytesToInetAddress(bytes, null); } /** @@ -134,28 +137,32 @@ private static Inet4Address getInet4Address(byte[] bytes) { * *

This deliberately avoids all nameservice lookups (e.g. no DNS). * - *

Anything after a {@code %} in an IPv6 address is ignored (assumed to be a Scope ID). - * *

This method accepts non-ASCII digits, for example {@code "192.168.0.1"} (those are fullwidth * characters). That is consistent with {@link InetAddress}, but not with various RFCs. If you * want to accept ASCII digits only, you can use something like {@code * CharMatcher.ascii().matchesAllOf(ipString)}. * + *

The scope ID is validated against the interfaces on the machine, which requires permissions + * under Android. + * + *

Android users on API >= 29: Prefer {@code InetAddresses.parseNumericAddress}. + * * @param ipString {@code String} containing an IPv4 or IPv6 string literal, e.g. {@code - * "192.168.0.1"} or {@code "2001:db8::1"} + * "192.168.0.1"} or {@code "2001:db8::1"} or with a scope ID, e.g. {@code "2001:db8::1%eth0"} * @return {@link InetAddress} representing the argument * @throws IllegalArgumentException if the argument is not a valid IP string literal */ @CanIgnoreReturnValue // TODO(b/219820829): consider removing public static InetAddress forString(String ipString) { - byte[] addr = ipStringToBytes(ipString); + Scope scope = new Scope(); + byte[] addr = ipStringToBytes(ipString, scope); // The argument was malformed, i.e. not an IP string literal. if (addr == null) { throw formatIllegalArgumentException("'%s' is not an IP string literal.", ipString); } - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, scope.scope); } /** @@ -171,12 +178,16 @@ public static InetAddress forString(String ipString) { * @return {@code true} if the argument is a valid IP string literal */ public static boolean isInetAddress(String ipString) { - return ipStringToBytes(ipString) != null; + return ipStringToBytes(ipString, null) != null; + } + + private static final class Scope { + private String scope; } /** Returns {@code null} if unable to parse into a {@code byte[]}. */ @CheckForNull - private static byte[] ipStringToBytes(String ipStringParam) { + private static byte[] ipStringToBytes(String ipStringParam, @Nullable Scope scope) { String ipString = ipStringParam; // Make a first pass to categorize the characters in this string. boolean hasColon = false; @@ -193,7 +204,7 @@ private static byte[] ipStringToBytes(String ipStringParam) { hasColon = true; } else if (c == '%') { percentIndex = i; - break; // everything after a '%' is ignored (it's a Scope ID): http://superuser.com/a/99753 + break; } else if (Character.digit(c, 16) == -1) { return null; // Everything else must be a decimal or hex digit. } @@ -208,6 +219,9 @@ private static byte[] ipStringToBytes(String ipStringParam) { } } if (percentIndex != -1) { + if (scope != null) { + scope.scope = ipString.substring(percentIndex + 1); + } ipString = ipString.substring(0, percentIndex); } return textToNumericFormatV6(ipString); @@ -358,6 +372,24 @@ private static byte parseOctet(String ipString, int start, int end) { return (byte) octet; } + /** Returns a -1 if unable to parse */ + private static int tryParseDecimal(String string, int start, int end) { + int decimal = 0; + final int max = Integer.MAX_VALUE / 10; // for int overflow detection + for (int i = start; i < end; i++) { + if (decimal > max) { + return -1; + } + decimal *= 10; + int digit = Character.digit(string.charAt(i), 10); + if (digit < 0) { + return -1; + } + decimal += digit; + } + return decimal; + } + // Parse a hextet out of the ipString from start (inclusive) to end (exclusive) private static short parseHextet(String ipString, int start, int end) { // Note: we already verified that this string contains only hex digits. @@ -383,9 +415,30 @@ private static short parseHextet(String ipString, int start, int end) { * @param addr the raw 4-byte or 16-byte IP address in big-endian order * @return an InetAddress object created from the raw IP address */ - private static InetAddress bytesToInetAddress(byte[] addr) { + private static InetAddress bytesToInetAddress(byte[] addr, @Nullable String scope) { try { - return InetAddress.getByAddress(addr); + InetAddress address = InetAddress.getByAddress(addr); + if (scope == null) { + return address; + } + checkArgument( + address instanceof Inet6Address, "Unexpected state, scope should only appear for ipv6"); + Inet6Address v6Address = (Inet6Address) address; + int interfaceIndex = tryParseDecimal(scope, 0, scope.length()); + if (interfaceIndex != -1) { + return Inet6Address.getByAddress( + v6Address.getHostAddress(), v6Address.getAddress(), interfaceIndex); + } + try { + NetworkInterface asInterface = NetworkInterface.getByName(scope); + if (asInterface == null) { + throw formatIllegalArgumentException("No such interface: '%s'", scope); + } + return Inet6Address.getByAddress( + v6Address.getHostAddress(), v6Address.getAddress(), asInterface); + } catch (SocketException | UnknownHostException e) { + throw new IllegalArgumentException("No such interface: " + scope, e); + } } catch (UnknownHostException e) { throw new AssertionError(e); } @@ -397,10 +450,13 @@ private static InetAddress bytesToInetAddress(byte[] addr) { *

For IPv4 addresses, this is identical to {@link InetAddress#getHostAddress()}, but for IPv6 * addresses, the output follows RFC 5952 section * 4. The main difference is that this method uses "::" for zero compression, while Java's version - * uses the uncompressed form. + * uses the uncompressed form (except on Android, where the zero compression is also done). The + * other difference is that this method outputs any scope ID in the format that it was provided at + * creation time, while Android may always output it as an interface name, even if it was supplied + * as a numeric ID. * *

This method uses hexadecimal for all IPv6 addresses, including IPv4-mapped IPv6 addresses - * such as "::c000:201". The output does not include a Scope ID. + * such as "::c000:201". * * @param ip {@link InetAddress} to be converted to an address string * @return {@code String} containing the text-formatted IP address @@ -413,14 +469,29 @@ public static String toAddrString(InetAddress ip) { // requireNonNull accommodates Android's @RecentlyNullable annotation on getHostAddress return requireNonNull(ip.getHostAddress()); } - checkArgument(ip instanceof Inet6Address); byte[] bytes = ip.getAddress(); int[] hextets = new int[IPV6_PART_COUNT]; for (int i = 0; i < hextets.length; i++) { hextets[i] = Ints.fromBytes((byte) 0, (byte) 0, bytes[2 * i], bytes[2 * i + 1]); } compressLongestRunOfZeroes(hextets); - return hextetsToIPv6String(hextets); + + return hextetsToIPv6String(hextets) + scopeWithDelimiter((Inet6Address) ip); + } + + private static String scopeWithDelimiter(Inet6Address ip) { + // getHostAddress on android sometimes maps the scope id to an invalid interface name; if the + // mapped interface isn't present, fallback to use the scope id (which has no validation against + // present interfaces) + NetworkInterface scopedInterface = ip.getScopedInterface(); + if (scopedInterface != null) { + return "%" + scopedInterface.getName(); + } + int scope = ip.getScopeId(); + if (scope != 0) { + return "%" + scope; + } + return ""; } /** @@ -529,10 +600,11 @@ public static String toUriString(InetAddress ip) { * @param hostAddr an RFC 3986 section 3.2.2 encoded IPv4 or IPv6 address * @return an InetAddress representing the address in {@code hostAddr} * @throws IllegalArgumentException if {@code hostAddr} is not a valid IPv4 address, or IPv6 - * address surrounded by square brackets + * address surrounded by square brackets, or if the address has a scope id that fails + * validation against interfaces on the machine */ public static InetAddress forUriString(String hostAddr) { - InetAddress addr = forUriStringNoThrow(hostAddr); + InetAddress addr = forUriStringOrNull(hostAddr, /* parseScope= */ true); if (addr == null) { throw formatIllegalArgumentException("Not a valid URI IP literal: '%s'", hostAddr); } @@ -541,7 +613,7 @@ public static InetAddress forUriString(String hostAddr) { } @CheckForNull - private static InetAddress forUriStringNoThrow(String hostAddr) { + private static InetAddress forUriStringOrNull(String hostAddr, boolean parseScope) { checkNotNull(hostAddr); // Decide if this should be an IPv6 or IPv4 address. @@ -556,12 +628,13 @@ private static InetAddress forUriStringNoThrow(String hostAddr) { } // Parse the address, and make sure the length/version is correct. - byte[] addr = ipStringToBytes(ipString); + Scope scope = parseScope ? new Scope() : null; + byte[] addr = ipStringToBytes(ipString, scope); if (addr == null || addr.length != expectBytes) { return null; } - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, (scope != null) ? scope.scope : null); } /** @@ -573,11 +646,14 @@ private static InetAddress forUriStringNoThrow(String hostAddr) { * want to accept ASCII digits only, you can use something like {@code * CharMatcher.ascii().matchesAllOf(ipString)}. * + *

Note that if this method returns {@code true}, a call to {@link #forUriString(String)} can + * throw if the address has a scope id fails validation against interfaces on the machine. + * * @param ipString {@code String} to evaluated as an IP URI host string literal * @return {@code true} if the argument is a valid IP URI host */ public static boolean isUriInetAddress(String ipString) { - return forUriStringNoThrow(ipString) != null; + return forUriStringOrNull(ipString, /* parseScope= */ false) != null; } /** @@ -876,7 +952,7 @@ public static Inet4Address getEmbeddedIPv4ClientAddress(Inet6Address ip) { * @since 10.0 */ public static boolean isMappedIPv4Address(String ipString) { - byte[] bytes = ipStringToBytes(ipString); + byte[] bytes = ipStringToBytes(ipString, null); if (bytes != null && bytes.length == 16) { for (int i = 0; i < 10; i++) { if (bytes[i] != 0) { @@ -1108,7 +1184,7 @@ public static InetAddress decrement(InetAddress address) { checkArgument(i >= 0, "Decrementing %s would wrap.", address); addr[i]--; - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, null); } /** @@ -1131,7 +1207,7 @@ public static InetAddress increment(InetAddress address) { checkArgument(i >= 0, "Incrementing %s would wrap.", address); addr[i]++; - return bytesToInetAddress(addr); + return bytesToInetAddress(addr, null); } /**