From 6da0f5865a30a266a1f2c6a1be53a5d79d0bba74 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 07:24:43 -0500 Subject: [PATCH 01/11] add some characterization tests to prove the address conversion output can be converted back to its input --- .../elasticsearch/common/network/InetAddressesTests.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index f323494b987e5..d256250eb03f5 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -189,12 +189,18 @@ public void testToAddrStringIPv6() { assertEquals("1::4:0:0:7:8", InetAddresses.toAddrString( InetAddresses.forString("1:0:0:4:0:0:7:8"))); + assertEquals("::", + InetAddresses.toAddrString( + InetAddresses.forString("::"))); assertEquals("::", InetAddresses.toAddrString( InetAddresses.forString("0:0:0:0:0:0:0:0"))); assertEquals("::1", InetAddresses.toAddrString( InetAddresses.forString("0:0:0:0:0:0:0:1"))); + assertEquals("::1", + InetAddresses.toAddrString( + InetAddresses.forString("::1"))); assertEquals("2001:658:22a:cafe::", InetAddresses.toAddrString( InetAddresses.forString("2001:0658:022a:cafe::"))); From 4e71f6f2ca28cc99ac3687b625b35f0e03e67ad1 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 07:25:38 -0500 Subject: [PATCH 02/11] create a test to reproduce the error from issue #37107 and https://github.com/elastic/beats/issues/9836 --- .../org/elasticsearch/common/network/InetAddressesTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index d256250eb03f5..ea7ce770bef4b 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -201,6 +201,9 @@ public void testToAddrStringIPv6() { assertEquals("::1", InetAddresses.toAddrString( InetAddresses.forString("::1"))); + assertEquals("::1", + InetAddresses.toAddrString( + InetAddresses.forString("::1%0"))); assertEquals("2001:658:22a:cafe::", InetAddresses.toAddrString( InetAddresses.forString("2001:0658:022a:cafe::"))); From 2a8913f463f9ce8734e56327e105f993ffc9f2d6 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 07:30:19 -0500 Subject: [PATCH 03/11] better to create a separate test for the failing scenarion --- .../elasticsearch/common/network/InetAddressesTests.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index ea7ce770bef4b..e2b434cc89c29 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -201,9 +201,6 @@ public void testToAddrStringIPv6() { assertEquals("::1", InetAddresses.toAddrString( InetAddresses.forString("::1"))); - assertEquals("::1", - InetAddresses.toAddrString( - InetAddresses.forString("::1%0"))); assertEquals("2001:658:22a:cafe::", InetAddresses.toAddrString( InetAddresses.forString("2001:0658:022a:cafe::"))); @@ -212,6 +209,12 @@ public void testToAddrStringIPv6() { InetAddresses.forString("::1.2.3.4"))); } + public void testToAddrStringIPv6WithZoneId(){ + assertEquals("::1", + InetAddresses.toAddrString( + InetAddresses.forString("::1%0"))); + } + public void testToUriStringIPv4() { String ipStr = "1.2.3.4"; InetAddress ip = InetAddresses.forString(ipStr); From 46104dfbc8bd6621fdfbdc7a81d9dd97f931ea76 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 08:20:09 -0500 Subject: [PATCH 04/11] extract an overload of ipStringToBytes that builds the bytes --- .../java/org/elasticsearch/common/network/InetAddresses.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 2e68d8358f0b2..c8cd19faf7459 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -52,6 +52,10 @@ private static byte[] ipStringToBytes(String ipString) { } } + return ipStringToBytes(ipString, hasColon, hasDot); + } + + private static byte[] ipStringToBytes(String ipString, boolean hasColon, boolean hasDot) { // Now decide which address family to parse. if (hasColon) { if (hasDot) { From 9ec96f3ec674ec76ca8d5e5a2e1eb1d4959d33e9 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 08:25:44 -0500 Subject: [PATCH 05/11] change ipStringToBytes to ignore the zoneId further commits will treat it accordingly --- .../elasticsearch/common/network/InetAddresses.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index c8cd19faf7459..91eca4f79d523 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -38,6 +38,8 @@ private static byte[] ipStringToBytes(String ipString) { // Make a first pass to categorize the characters in this string. boolean hasColon = false; boolean hasDot = false; + boolean hasPercent = false; + int percentIndex = -1; for (int i = 0; i < ipString.length(); i++) { char c = ipString.charAt(i); if (c == '.') { @@ -47,11 +49,20 @@ private static byte[] ipStringToBytes(String ipString) { return null; // Colons must not appear after dots. } hasColon = true; + } else if (c == '%'){ + hasPercent = true; + percentIndex = i; } else if (Character.digit(c, 16) == -1) { return null; // Everything else must be a decimal or hex digit. } } + // string zoneId from the address + if (hasPercent){ + String ipStringWithoutZoneId = ipString.substring(0, percentIndex); + return ipStringToBytes(ipStringWithoutZoneId, hasColon, hasDot); + } + return ipStringToBytes(ipString, hasColon, hasDot); } From d1516dd25f3c492b4430b6f191bedbb5dd185e3e Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 09:31:06 -0500 Subject: [PATCH 06/11] correct typo that completely changes the meaning of the comment --- .../java/org/elasticsearch/common/network/InetAddresses.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 91eca4f79d523..1868597db78de 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -57,7 +57,7 @@ private static byte[] ipStringToBytes(String ipString) { } } - // string zoneId from the address + // strip zoneId from the address if (hasPercent){ String ipStringWithoutZoneId = ipString.substring(0, percentIndex); return ipStringToBytes(ipStringWithoutZoneId, hasColon, hasDot); From b6ae23cc80b2a351eba4333cbb9dd5db250ef7aa Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 09:57:49 -0500 Subject: [PATCH 07/11] make sure IPv6 addresses with IPv4 components can contain a zoneId --- .../org/elasticsearch/common/network/InetAddressesTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index e2b434cc89c29..4e7beb2009369 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -213,6 +213,9 @@ public void testToAddrStringIPv6WithZoneId(){ assertEquals("::1", InetAddresses.toAddrString( InetAddresses.forString("::1%0"))); + assertEquals("::102:304", + InetAddresses.toAddrString( + InetAddresses.forString("::1.2.3.4%0"))); } public void testToUriStringIPv4() { From 1345cc4954f2494098e9ffbf022f18bc03a5b4a6 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 10:10:54 -0500 Subject: [PATCH 08/11] add some validations to the zoneId delimiter parsing --- .../common/network/InetAddresses.java | 18 +++++++++- .../common/network/InetAddressesTests.java | 36 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 1868597db78de..8332d4e4470f5 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -43,13 +43,29 @@ private static byte[] ipStringToBytes(String ipString) { for (int i = 0; i < ipString.length(); i++) { char c = ipString.charAt(i); if (c == '.') { + if (hasPercent){ + return null; // Dots must not appear after percents. + } + hasDot = true; } else if (c == ':') { + if (hasPercent){ + return null; // Colons must not appear after percents. + } + if (hasDot) { return null; // Colons must not appear after dots. } hasColon = true; } else if (c == '%'){ + if (hasPercent){ + return null; // There can only be one percent. + } + + if (!hasColon){ + return null; // Percents can only appear if there are colons. + } + hasPercent = true; percentIndex = i; } else if (Character.digit(c, 16) == -1) { @@ -60,7 +76,7 @@ private static byte[] ipStringToBytes(String ipString) { // strip zoneId from the address if (hasPercent){ String ipStringWithoutZoneId = ipString.substring(0, percentIndex); - return ipStringToBytes(ipStringWithoutZoneId, hasColon, hasDot); + return ipStringToBytes(ipStringWithoutZoneId); } return ipStringToBytes(ipString, hasColon, hasDot); diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 4e7beb2009369..70f4b5abe1108 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -218,6 +218,42 @@ public void testToAddrStringIPv6WithZoneId(){ InetAddresses.forString("::1.2.3.4%0"))); } + public void testToAddrStringIPv6WithInvalidZoneId(){ + IllegalArgumentException e = null; + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1%fred")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1%%")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("%::1")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("0::0:0:0:0:0:0%:1")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1%1.2.3.4")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + } + + public void testToAddrStringZoneIdDelimiterCannotAppearRightAfterOtherDelimiters(){ + IllegalArgumentException e = null; + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1:%0")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("::1:1.2.3.%0")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + } + + public void testToAddrStringIPv4DoesNotAllowZoneId(){ + IllegalArgumentException e = null; + + e = expectThrows(IllegalArgumentException.class, () -> InetAddresses.forString("1.2.3.4%0")); + assertThat(e.getMessage(), Matchers.containsString("is not an IP string literal")); + } + public void testToUriStringIPv4() { String ipStr = "1.2.3.4"; InetAddress ip = InetAddresses.forString(ipStr); From 9cd2c5ee6b60f79ec28e61ea4d6ebb0135eef24d Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 13:38:41 -0500 Subject: [PATCH 09/11] start slowly building up the InetAddresses capability to read the zone ID --- .../common/network/InetAddresses.java | 24 +++++++++++++++++-- .../common/network/InetAddressesTests.java | 8 +++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index 8332d4e4470f5..dd632d3cacc2c 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -29,11 +29,23 @@ public class InetAddresses { private static int IPV4_PART_COUNT = 4; private static int IPV6_PART_COUNT = 8; + private static final int DEFAULT_ZONE_ID = 0; public static boolean isInetAddress(String ipString) { return ipStringToBytes(ipString) != null; } + private static int ipStringToZoneId(String ipString){ + int percentIndex = ipString.lastIndexOf('%'); + + if (percentIndex >= 0 && (percentIndex + 1) < ipString.length()){ + String zoneId = ipString.substring(percentIndex + 1); + return Integer.parseInt(zoneId, 16); + } + + return DEFAULT_ZONE_ID; + } + private static byte[] ipStringToBytes(String ipString) { // Make a first pass to categorize the characters in this string. boolean hasColon = false; @@ -366,7 +378,9 @@ public static InetAddress forString(String ipString) { throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString)); } - return bytesToInetAddress(addr); + int zoneId = ipStringToZoneId(ipString); + + return bytesToInetAddress(addr, zoneId); } /** @@ -378,10 +392,16 @@ public static InetAddress forString(String ipString) { * is an array of length 4 or 16. * * @param addr the raw 4-byte or 16-byte IP address in big-endian order + * @param zoneId the IPv6 zone ID. Zero by default. + * Uses {@code Inet6Address.getByAddress} for non-zero values. * @return an InetAddress object created from the raw IP address */ - private static InetAddress bytesToInetAddress(byte[] addr) { + private static InetAddress bytesToInetAddress(byte[] addr, int zoneId) { try { + if (zoneId != DEFAULT_ZONE_ID){ + return Inet6Address.getByAddress(null, addr, zoneId); + } + return InetAddress.getByAddress(addr); } catch (UnknownHostException e) { throw new AssertionError(e); diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 70f4b5abe1108..23d229e938cb6 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -20,6 +20,7 @@ import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; @@ -218,6 +219,13 @@ public void testToAddrStringIPv6WithZoneId(){ InetAddresses.forString("::1.2.3.4%0"))); } + public void testToAddrStringIPv6ReadsZoneId(){ + assertEquals(1, + ((Inet6Address)InetAddresses.forString("::1%1")).getScopeId()); + assertEquals(10, + ((Inet6Address)InetAddresses.forString("::1%A")).getScopeId()); + } + public void testToAddrStringIPv6WithInvalidZoneId(){ IllegalArgumentException e = null; From d199991e306aad3f35676c03c2cc011de7b1a328 Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 13:53:40 -0500 Subject: [PATCH 10/11] make sure the zone ID can be displayed --- .../elasticsearch/common/network/InetAddresses.java | 12 ++++++++++-- .../common/network/InetAddressesTests.java | 9 +++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java index dd632d3cacc2c..1963da85a8507 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -286,12 +286,13 @@ public static String toAddrString(InetAddress ip) { throw new IllegalArgumentException("ip"); } byte[] bytes = ip.getAddress(); + int zoneId = ((Inet6Address)ip).getScopeId(); int[] hextets = new int[IPV6_PART_COUNT]; for (int i = 0; i < hextets.length; i++) { hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255; } compressLongestRunOfZeroes(hextets); - return hextetsToIPv6String(hextets); + return hextetsToIPv6String(hextets, zoneId); } /** @@ -333,8 +334,9 @@ private static void compressLongestRunOfZeroes(int[] hextets) { * sentinel values in place of the elided zeroes. * * @param hextets {@code int[]} array of eight 16-bit hextets, or -1s + * @param zoneId {@code int} the zone ID of an IPv6 address. Ignored from output if zero. */ - private static String hextetsToIPv6String(int[] hextets) { + private static String hextetsToIPv6String(int[] hextets, int zoneId) { /* * While scanning the array, handle these state transitions: * start->num => "num" start->gap => "::" @@ -357,6 +359,12 @@ private static String hextetsToIPv6String(int[] hextets) { } lastWasNumber = thisIsNumber; } + + if (zoneId != DEFAULT_ZONE_ID){ + buf.append('%'); + buf.append(Integer.toString(zoneId, 16)); + } + return buf.toString(); } diff --git a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java index 23d229e938cb6..4b29cca2b9fbc 100644 --- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java @@ -214,9 +214,18 @@ public void testToAddrStringIPv6WithZoneId(){ assertEquals("::1", InetAddresses.toAddrString( InetAddresses.forString("::1%0"))); + assertEquals("::1%1", + InetAddresses.toAddrString( + InetAddresses.forString("::1%1"))); + assertEquals("::1%ff", + InetAddresses.toAddrString( + InetAddresses.forString("::1%ff"))); assertEquals("::102:304", InetAddresses.toAddrString( InetAddresses.forString("::1.2.3.4%0"))); + assertEquals("::102:304%b", + InetAddresses.toAddrString( + InetAddresses.forString("::1.2.3.4%b"))); } public void testToAddrStringIPv6ReadsZoneId(){ From a744c39b99e5b3f2bdf1eb395d23b2b9ccff9e0b Mon Sep 17 00:00:00 2001 From: Camilo Sanchez Date: Wed, 16 Jan 2019 21:40:54 -0500 Subject: [PATCH 11/11] correct failing tests in NetworkAddressTests that don't expect the zone ID to be returned --- .../elasticsearch/common/network/NetworkAddressTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/common/network/NetworkAddressTests.java b/server/src/test/java/org/elasticsearch/common/network/NetworkAddressTests.java index 4f08eee9b777d..253bb198af9b8 100644 --- a/server/src/test/java/org/elasticsearch/common/network/NetworkAddressTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/NetworkAddressTests.java @@ -53,11 +53,11 @@ public void testFormatPortV6() throws Exception { } public void testNoScopeID() throws Exception { - assertEquals("::1", NetworkAddress.format(forgeScoped(null, "::1", 5))); - assertEquals("::1", NetworkAddress.format(forgeScoped("localhost", "::1", 5))); + assertEquals("::1%5", NetworkAddress.format(forgeScoped(null, "::1", 5))); + assertEquals("::1%5", NetworkAddress.format(forgeScoped("localhost", "::1", 5))); - assertEquals("[::1]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped(null, "::1", 5), 1234))); - assertEquals("[::1]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped("localhost", "::1", 5), 1234))); + assertEquals("[::1%5]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped(null, "::1", 5), 1234))); + assertEquals("[::1%5]:1234", NetworkAddress.format(new InetSocketAddress(forgeScoped("localhost", "::1", 5), 1234))); } /** Test that ipv4 address formatting round trips */