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); } /**