diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java new file mode 100644 index 0000000000000..7888aa4c9978a --- /dev/null +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/common/network/IpAddressesBenchmarks.java @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.benchmark.common.network; + +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.xcontent.Text; +import org.elasticsearch.xcontent.XContentString; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Warmup(iterations = 2) +@Measurement(iterations = 3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +@Fork(1) +public class IpAddressesBenchmarks { + + @Param("1000") + private int size; + private String[] ipV6Addresses; + private String[] ipV4Addresses; + private XContentString[] ipV6AddressesBytes; + private XContentString[] ipV4AddressesBytes; + + @Setup + public void setup() throws UnknownHostException { + Random random = new Random(); + ipV6Addresses = new String[size]; + ipV4Addresses = new String[size]; + ipV6AddressesBytes = new XContentString[size]; + ipV4AddressesBytes = new XContentString[size]; + byte[] ipv6Bytes = new byte[16]; + byte[] ipv4Bytes = new byte[4]; + for (int i = 0; i < size; i++) { + random.nextBytes(ipv6Bytes); + random.nextBytes(ipv4Bytes); + String ipv6String = InetAddresses.toAddrString(InetAddress.getByAddress(ipv6Bytes)); + String ipv4String = InetAddresses.toAddrString(InetAddress.getByAddress(ipv4Bytes)); + ipV6Addresses[i] = ipv6String; + ipV4Addresses[i] = ipv4String; + ipV6AddressesBytes[i] = new Text(ipv6String); + ipV4AddressesBytes[i] = new Text(ipv4String); + } + } + + @Benchmark + public boolean isInetAddressIpv6() { + boolean b = true; + for (int i = 0; i < size; i++) { + b ^= InetAddresses.isInetAddress(ipV6Addresses[i]); + } + return b; + } + + @Benchmark + public boolean isInetAddressIpv4() { + boolean b = true; + for (int i = 0; i < size; i++) { + b ^= InetAddresses.isInetAddress(ipV4Addresses[i]); + } + return b; + } + + @Benchmark + public void getIpOrHostIpv6(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.getIpOrHost(ipV6Addresses[i])); + } + } + + @Benchmark + public void getIpOrHostIpv4(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV4Addresses[i])); + } + } + + @Benchmark + public void forStringIpv6String(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV6Addresses[i])); + } + } + + @Benchmark + public void forStringIpv4String(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV4Addresses[i])); + } + } + + @Benchmark + public void forStringIpv6Bytes(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV6AddressesBytes[i].bytes())); + } + } + + @Benchmark + public void forStringIpv4Bytes(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.forString(ipV4AddressesBytes[i].bytes())); + } + } + + @Benchmark + public void encodeAsIpv6WithIpv6(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.encodeAsIpv6(ipV6AddressesBytes[i])); + } + } + + @Benchmark + public void encodeAsIpv6WithIpv4(Blackhole blackhole) { + for (int i = 0; i < size; i++) { + blackhole.consume(InetAddresses.encodeAsIpv6(ipV4AddressesBytes[i])); + } + } +} 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 292dba566d343..e315984b6e668 100644 --- a/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java +++ b/server/src/main/java/org/elasticsearch/common/network/InetAddresses.java @@ -19,183 +19,265 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; +import org.elasticsearch.xcontent.XContentString; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; public class InetAddresses { - private static int IPV4_PART_COUNT = 4; - private static int IPV6_PART_COUNT = 8; + private static final int IPV4_PART_COUNT = 4; + private static final int IPV6_PART_COUNT = 8; + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); public static boolean isInetAddress(String ipString) { - return ipStringToBytes(ipString) != null; + byte[] utf8Bytes = ipString.getBytes(StandardCharsets.UTF_8); + return ipStringToBytes(utf8Bytes, 0, utf8Bytes.length, false) != null; } public static String getIpOrHost(String ipString) { - byte[] bytes = ipStringToBytes(ipString); + byte[] utf8Bytes = ipString.getBytes(StandardCharsets.UTF_8); + byte[] bytes = ipStringToBytes(utf8Bytes, 0, utf8Bytes.length, false); if (bytes == null) { // is not InetAddress return ipString; } return NetworkAddress.format(bytesToInetAddress(bytes)); } - private static byte[] ipStringToBytes(String ipString) { + /** + * Encodes the given {@link XContentString} in binary encoding, always using 16 bytes for both IPv4 and IPv6 addresses. + * This is how Lucene encodes IP addresses in {@link org.apache.lucene.document.InetAddressPoint}. + * + * @param ipString the IP address as a string + * @return a byte array containing the binary representation of the IP address + * @throws IllegalArgumentException if the argument is not a valid IP string literal + */ + public static byte[] encodeAsIpv6(XContentString ipString) { + XContentString.UTF8Bytes uft8Bytes = ipString.bytes(); + byte[] address = ipStringToBytes(uft8Bytes.bytes(), uft8Bytes.offset(), uft8Bytes.length(), true); + // The argument was malformed, i.e. not an IP string literal. + if (address == null) { + throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString.string())); + } + return address; + } + + /** + * Converts an IP address string to a byte array. + *
+ * This method supports both IPv4 and IPv6 addresses, including dotted quad notation for IPv6. + * + * @param ipUtf8 the IP address as a byte array in UTF-8 encoding + * @param offset the starting index in the byte array + * @param length the length of the IP address string + * @param asIpv6 if true, always returns a 16-byte array (IPv6 format), otherwise returns a 4-byte array for IPv4 + * @return a byte array representing the IP address, or null if the input is invalid + */ + private static byte[] ipStringToBytes(byte[] ipUtf8, int offset, int length, boolean asIpv6) { // Make a first pass to categorize the characters in this string. - boolean hasColon = false; + int indexOfLastColon = -1; boolean hasDot = false; - int percentIndex = -1; - for (int i = 0; i < ipString.length(); i++) { - char c = ipString.charAt(i); - if (c == '.') { + for (int i = offset; i < offset + length; i++) { + byte c = ipUtf8[i]; + if ((c & 0x80) != 0) { + return null; // Only allow ASCII characters. + } else if (c == '.') { hasDot = true; } else if (c == ':') { if (hasDot) { return null; // Colons must not appear after dots. } - hasColon = true; + indexOfLastColon = i; } else if (c == '%') { - percentIndex = i; + if (i == offset + length - 1) { + return null; // Filter out strings that end in % and have an empty scope ID. + } + length = i; break; // Everything after a '%' is ignored (it's a Scope ID) - } else if (Character.digit(c, 16) == -1) { - return null; // Everything else must be a decimal or hex digit. } } // Now decide which address family to parse. - if (hasColon) { + if (indexOfLastColon >= 0) { if (hasDot) { - ipString = convertDottedQuadToHex(ipString); - if (ipString == null) { + ipUtf8 = convertDottedQuadToHex(ipUtf8, offset, length, indexOfLastColon); + if (ipUtf8 == null) { return null; } + offset = 0; + length = ipUtf8.length; } - if (percentIndex == ipString.length() - 1) { - return null; // Filter out strings that end in % and have an empty scope ID. - } - if (percentIndex != -1) { - ipString = ipString.substring(0, percentIndex); - } - return textToNumericFormatV6(ipString); + return textToNumericFormatV6(ipUtf8, offset, length); } else if (hasDot) { - return textToNumericFormatV4(ipString); + return textToNumericFormatV4(ipUtf8, offset, length, asIpv6); } return null; } - private static String convertDottedQuadToHex(String ipString) { - int lastColon = ipString.lastIndexOf(':'); - String initialPart = ipString.substring(0, lastColon + 1); - String dottedQuad = ipString.substring(lastColon + 1); - byte[] quad = textToNumericFormatV4(dottedQuad); + private static byte[] convertDottedQuadToHex(byte[] ipUtf8, int offset, int length, int indexOfLastColon) { + int quadOffset = indexOfLastColon - offset + 1; // +1 to include the colon + assert quadOffset >= 0 : "Expected at least one colon in dotted quad IPv6 address"; + byte[] quad = textToNumericFormatV4(ipUtf8, offset + quadOffset, length - quadOffset, false); if (quad == null) { return null; } - String penultimate = Integer.toHexString(((quad[0] & 0xff) << 8) | (quad[1] & 0xff)); - String ultimate = Integer.toHexString(((quad[2] & 0xff) << 8) | (quad[3] & 0xff)); - return initialPart + penultimate + ":" + ultimate; + // initialPart(quadOffset) + penultimate(4) + ":"(1) + ultimate(4) + byte[] result = new byte[quadOffset + 9]; + System.arraycopy(ipUtf8, offset, result, 0, quadOffset); + appendHexBytes(result, quadOffset, quad[0], quad[1]); // penultimate part + result[quadOffset + 4] = ':'; + appendHexBytes(result, quadOffset + 5, quad[2], quad[3]); // ultimate part + return result; + } + + static void appendHexBytes(byte[] result, int offset, byte b1, byte b2) { + result[offset] = (byte) HEX_DIGITS[((b1 & 0xf0) >> 4)]; + result[offset + 1] = (byte) HEX_DIGITS[(b1 & 0x0f)]; + result[offset + 2] = (byte) HEX_DIGITS[((b2 & 0xf0) >> 4)]; + result[offset + 3] = (byte) HEX_DIGITS[(b2 & 0x0f)]; } - private static byte[] textToNumericFormatV4(String ipString) { - byte[] bytes = new byte[IPV4_PART_COUNT]; - byte octet = 0; + private static byte[] textToNumericFormatV4(byte[] ipUtf8, int offset, int length, boolean asIpv6) { + byte[] bytes; + byte octet; + if (asIpv6) { + bytes = new byte[IPV6_PART_COUNT * 2]; + System.arraycopy(CIDRUtils.IPV4_PREFIX, 0, bytes, 0, CIDRUtils.IPV4_PREFIX.length); + octet = (byte) CIDRUtils.IPV4_PREFIX.length; + } else { + bytes = new byte[IPV4_PART_COUNT]; + octet = 0; + } byte digits = 0; - for (int i = 0; i < ipString.length(); i++) { - char c = ipString.charAt(i); + int current = 0; + for (int i = offset; i < offset + length; i++) { + byte c = ipUtf8[i]; if (c == '.') { - octet++; - if (octet > 3 /* too many octets */ || digits == 0 /* empty octet */) { + if (octet >= bytes.length /* too many octets */ + || digits == 0 /* empty octet */ + || current > 255 /* octet is outside a byte range */) { return null; } + bytes[octet++] = (byte) current; + current = 0; digits = 0; } else if (c >= '0' && c <= '9') { - digits++; - var next = bytes[octet] * 10 + (c - '0'); - if (next > 255 /* octet is outside a byte range */ || (digits > 1 && bytes[octet] == 0) /* octet contains leading 0 */) { + if (digits != 0 && current == 0 /* octet contains leading 0 */) { return null; } - bytes[octet] = (byte) next; + current = current * 10 + (c - '0'); + digits++; } else { return null; } } - return octet != 3 ? null : bytes; + if (octet != bytes.length - 1 /* too many or too few octets */ + || digits == 0 /* empty octet */ + || current > 255 /* octet is outside a byte range */) { + return null; + } + bytes[octet] = (byte) current; + return bytes; } - private static byte[] textToNumericFormatV6(String ipString) { - // An address can have [2..8] colons, and N colons make N+1 parts. - String[] parts = ipString.split(":", IPV6_PART_COUNT + 2); - if (parts.length < 3 || parts.length > IPV6_PART_COUNT + 1) { + private static byte[] textToNumericFormatV6(byte[] ipUtf8, int offset, int length) { + if (length < 2) { + // IPv6 addresses must be at least 2 characters long (e.g., "::") + return null; + } + if (ipUtf8[offset] == ':' && ipUtf8[offset + 1] != ':') { + // Addresses can't start with a single colon + return null; + } + if (ipUtf8[offset + length - 1] == ':' && ipUtf8[offset + length - 2] != ':') { + // Addresses can't end with a single colon return null; } - // Disregarding the endpoints, find "::" with nothing in between. - // This indicates that a run of zeroes has been skipped. - int skipIndex = -1; - for (int i = 1; i < parts.length - 1; i++) { - if (parts[i].length() == 0) { - if (skipIndex >= 0) { - return null; // Can't have more than one :: + // An IPv6 address has 8 hextets (16-bit pieces), each represented by 1-4 hex digits + // Total size: 16 bytes (128 bits) + ByteBuffer bytes = ByteBuffer.allocate(IPV6_PART_COUNT * 2); + + // Find position of :: abbreviation if present + int compressedHextetIndex = -1; + int hextetIndex = 0; + int currentHextetStart = 0; + int currentHextet = 0; + for (int i = offset; i < offset + length; i++) { + byte c = ipUtf8[i]; + if (c == ':') { + if (currentHextetStart == i) { + // Two colons in a row, indicating a compressed section + if (compressedHextetIndex >= 0 && i != 1) { + // We've already seen a ::, can't have another + return null; + } + compressedHextetIndex = hextetIndex; // Mark the position of the compressed section + } else { + if (putHextet(bytes, currentHextet) == false) { + return null; + } + currentHextet = 0; + hextetIndex++; } - skipIndex = i; + currentHextetStart = i + 1; + } else if (c >= '0' && c <= '9') { + // Valid hex digit + currentHextet = currentHextet * 16 + (c - '0'); + } else if (c >= 'a' && c <= 'f') { + // Valid hex digit in lowercase + currentHextet = currentHextet * 16 + (c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + // Valid hex digit in uppercase + currentHextet = currentHextet * 16 + (c - 'A' + 10); + } else { + return null; // Invalid character } } - - int partsHi; // Number of parts to copy from above/before the "::" - int partsLo; // Number of parts to copy from below/after the "::" - if (skipIndex >= 0) { - // If we found a "::", then check if it also covers the endpoints. - partsHi = skipIndex; - partsLo = parts.length - skipIndex - 1; - if (parts[0].length() == 0 && --partsHi != 0) { - return null; // ^: requires ^:: - } - if (parts[parts.length - 1].length() == 0 && --partsLo != 0) { - return null; // :$ requires ::$ + if (currentHextetStart != length) { + // Handle the last hextet + if (putHextet(bytes, currentHextet) == false) { + return null; } - } else { - // Otherwise, allocate the entire address to partsHi. The endpoints - // could still be empty, but parseHextet() will check for that. - partsHi = parts.length; - partsLo = 0; + hextetIndex++; } - // If we found a ::, then we must have skipped at least one part. - // Otherwise, we must have exactly the right number of parts. - int partsSkipped = IPV6_PART_COUNT - (partsHi + partsLo); - if ((skipIndex >= 0 ? partsSkipped >= 1 : partsSkipped == 0) == false) { - return null; + if (compressedHextetIndex >= 0) { + if (hextetIndex >= IPV6_PART_COUNT) { + return null; // Invalid, too many hextets + } + shiftHextetsRight(bytes, compressedHextetIndex, hextetIndex); + } else if (hextetIndex != IPV6_PART_COUNT) { + return null; // Invalid, not enough hextets } - // Now parse the hextets into a byte array. - ByteBuffer rawBytes = ByteBuffer.allocate(2 * IPV6_PART_COUNT); - try { - for (int i = 0; i < partsHi; i++) { - rawBytes.putShort(parseHextet(parts[i])); - } - for (int i = 0; i < partsSkipped; i++) { - rawBytes.putShort((short) 0); - } - for (int i = partsLo; i > 0; i--) { - rawBytes.putShort(parseHextet(parts[parts.length - i])); - } - } catch (NumberFormatException ex) { - return null; + return bytes.array(); + } + + private static void shiftHextetsRight(ByteBuffer bytes, int start, int end) { + int shift = IPV6_PART_COUNT - end; + for (int hextetIndexToShift = end - 1; hextetIndexToShift >= start; hextetIndexToShift--) { + int bytesIndexBeforeShift = hextetIndexToShift * Short.BYTES; + short hextetToShift = bytes.getShort(bytesIndexBeforeShift); + bytes.putShort(bytesIndexBeforeShift, (short) 0); + bytes.putShort(bytesIndexBeforeShift + shift * Short.BYTES, hextetToShift); } - return rawBytes.array(); } - private static short parseHextet(String ipPart) { - // Note: we already verified that this string contains only hex digits. - int hextet = Integer.parseInt(ipPart, 16); + private static boolean putHextet(ByteBuffer buf, int hextet) { + if (buf.remaining() < 2) { + return false; + } if (hextet > 0xffff) { - throw new NumberFormatException(); + return false; } - return (short) hextet; + buf.putShort((short) hextet); + return true; } /** @@ -345,11 +427,30 @@ private static String hextetsToIPv6String(int[] hextets) { * @throws IllegalArgumentException if the argument is not a valid IP string literal */ public static InetAddress forString(String ipString) { - byte[] addr = ipStringToBytes(ipString); + byte[] utf8Bytes = ipString.getBytes(StandardCharsets.UTF_8); + return forString(utf8Bytes, 0, utf8Bytes.length); + } + + /** + * A variant of {@link #forString(String)} that accepts an {@link XContentString.UTF8Bytes} object, + * which utilizes a more efficient implementation for parsing the IP address. + */ + public static InetAddress forString(XContentString.UTF8Bytes bytes) { + return forString(bytes.bytes(), bytes.offset(), bytes.length()); + } + + /** + * A variant of {@link #forString(String)} that accepts a byte array, + * which utilizes a more efficient implementation for parsing the IP address. + */ + public static InetAddress forString(byte[] ipUtf8, int offset, int length) { + byte[] addr = ipStringToBytes(ipUtf8, offset, length, false); // The argument was malformed, i.e. not an IP string literal. if (addr == null) { - throw new IllegalArgumentException(String.format(Locale.ROOT, "'%s' is not an IP string literal.", ipString)); + throw new IllegalArgumentException( + String.format(Locale.ROOT, "'%s' is not an IP string literal.", new String(ipUtf8, offset, length, StandardCharsets.UTF_8)) + ); } return bytesToInetAddress(addr); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java b/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java new file mode 100644 index 0000000000000..4471dfcd842e2 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/ESInetAddressPoint.java @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.mapper; + +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FieldType; +import org.apache.lucene.document.InetAddressPoint; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.network.CIDRUtils; +import org.elasticsearch.common.network.InetAddresses; +import org.elasticsearch.common.network.NetworkAddress; +import org.elasticsearch.xcontent.XContentString; + +import java.net.InetAddress; + +/** + * A Lucene {@link Field} that stores an IP address as a point. + * This is similar to {@link InetAddressPoint} but uses a more efficient way to parse IP addresses + * that doesn't require the address to be an {@link InetAddress} object. + * Otherwise, it behaves just like the {@link InetAddressPoint} field. + */ +class ESInetAddressPoint extends Field { + private static final FieldType TYPE; + + static { + TYPE = new FieldType(); + TYPE.setDimensions(1, InetAddressPoint.BYTES); + TYPE.freeze(); + } + + private final XContentString ipString; + private final InetAddress inetAddress; + + /** + * Creates a new ESInetAddressPoint, indexing the provided address. + *
+ * This is the difference compared to {@link #ESInetAddressPoint(String, InetAddress)} + * and {@link InetAddressPoint#InetAddressPoint(String, InetAddress)} + * is that this constructor uses a more efficient way to parse the IP address that avoids the need to create + * a {@link String} and an {@link InetAddress} object for the IP address. + * + * @param name the name of the field + * @param value the IP address as a string + * @throws IllegalArgumentException if the field name or value is null or if the IP address is invalid + */ + protected ESInetAddressPoint(String name, XContentString value) { + super(name, TYPE); + if (value == null) { + throw new IllegalArgumentException("point must not be null"); + } + this.fieldsData = new BytesRef(InetAddresses.encodeAsIpv6(value)); + this.ipString = value; + this.inetAddress = null; + } + + /** + * Creates a new ESInetAddressPoint, indexing the provided address. + *
+ * This constructor is similar to Lucene's InetAddressPoint.
+ * For performance reasons, it is recommended to use the constructor that accepts
+ * an {@link XContentString} representation of the IP address instead.
+ *
+ * @param name field name
+ * @param value InetAddress value
+ * @throws IllegalArgumentException if the field name or value is null.
+ */
+ protected ESInetAddressPoint(String name, InetAddress value) {
+ super(name, TYPE);
+ if (value == null) {
+ throw new IllegalArgumentException("point must not be null");
+ }
+ this.fieldsData = new BytesRef(CIDRUtils.encode(value.getAddress()));
+ this.inetAddress = value;
+ this.ipString = null;
+ }
+
+ public InetAddress getInetAddress() {
+ if (ipString != null) {
+ return InetAddresses.forString(ipString.bytes());
+ }
+ if (inetAddress != null) {
+ return inetAddress;
+ }
+ throw new IllegalStateException("Neither ipString nor inetAddress is set");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(getClass().getSimpleName());
+ result.append(" <");
+ result.append(name);
+ result.append(':');
+
+ // IPv6 addresses are bracketed, to not cause confusion with historic field:value representation
+ BytesRef bytes = (BytesRef) fieldsData;
+ InetAddress address = InetAddressPoint.decode(BytesRef.deepCopyOf(bytes).bytes);
+ if (address.getAddress().length == 16) {
+ result.append('[');
+ result.append(NetworkAddress.format(address));
+ result.append(']');
+ } else {
+ result.append(NetworkAddress.format(address));
+ }
+
+ result.append('>');
+ return result.toString();
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java
index 0e309e3084878..ebf4fb0d11cc6 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java
@@ -9,7 +9,6 @@
package org.elasticsearch.index.mapper;
-import org.apache.lucene.document.Field;
import org.apache.lucene.document.InetAddressPoint;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.document.StoredField;
@@ -44,6 +43,7 @@
import org.elasticsearch.search.lookup.FieldValues;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentString;
import java.io.IOException;
import java.net.InetAddress;
@@ -648,10 +648,12 @@ protected String contentType() {
@Override
protected void parseCreateField(DocumentParserContext context) throws IOException {
- InetAddress address;
- String value = context.parser().textOrNull();
+ ESInetAddressPoint address;
+ XContentString value = context.parser().optimizedTextOrNull();
try {
- address = value == null ? nullValue : InetAddresses.forString(value);
+ address = value == null
+ ? nullValue == null ? null : new ESInetAddressPoint(fieldType().name(), nullValue)
+ : new ESInetAddressPoint(fieldType().name(), value);
} catch (IllegalArgumentException e) {
if (ignoreMalformed) {
context.addIgnoredField(fieldType().name());
@@ -669,7 +671,7 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
}
if (offsetsFieldName != null && context.isImmediateParentAnArray() && context.canAddIgnoredField()) {
if (address != null) {
- BytesRef sortableValue = new BytesRef(InetAddressPoint.encode(address));
+ BytesRef sortableValue = address.binaryValue();
context.getOffSetContext().recordOffset(offsetsFieldName, sortableValue);
} else {
context.getOffSetContext().recordNull(offsetsFieldName);
@@ -677,21 +679,21 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
}
}
- private void indexValue(DocumentParserContext context, InetAddress address) {
+ private void indexValue(DocumentParserContext context, ESInetAddressPoint address) {
if (dimension) {
- context.getRoutingFields().addIp(fieldType().name(), address);
+ context.getRoutingFields().addIp(fieldType().name(), address.getInetAddress());
}
+ LuceneDocument doc = context.doc();
if (indexed) {
- Field field = new InetAddressPoint(fieldType().name(), address);
- context.doc().add(field);
+ doc.add(address);
}
if (hasDocValues) {
- context.doc().add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address))));
+ doc.add(new SortedSetDocValuesField(fieldType().name(), address.binaryValue()));
} else if (stored || indexed) {
context.addToFieldNames(fieldType().name());
}
if (stored) {
- context.doc().add(new StoredField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address))));
+ doc.add(new StoredField(fieldType().name(), address.binaryValue()));
}
}
@@ -702,7 +704,12 @@ protected void indexScriptValues(
int doc,
DocumentParserContext documentParserContext
) {
- this.scriptValues.valuesForDoc(searchLookup, readerContext, doc, value -> indexValue(documentParserContext, value));
+ this.scriptValues.valuesForDoc(
+ searchLookup,
+ readerContext,
+ doc,
+ value -> indexValue(documentParserContext, new ESInetAddressPoint(fieldType().name(), value))
+ );
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java b/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java
index 0cec1bb9b6a4a..2782446e904c1 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/RangeType.java
@@ -54,14 +54,14 @@ public Field getRangeField(String name, RangeFieldMapper.Range r) {
@Override
public InetAddress parseFrom(RangeFieldMapper.RangeFieldType fieldType, XContentParser parser, boolean coerce, boolean included)
throws IOException {
- InetAddress address = InetAddresses.forString(parser.text());
+ InetAddress address = InetAddresses.forString(parser.optimizedText().bytes());
return included ? address : nextUp(address);
}
@Override
public InetAddress parseTo(RangeFieldMapper.RangeFieldType fieldType, XContentParser parser, boolean coerce, boolean included)
throws IOException {
- InetAddress address = InetAddresses.forString(parser.text());
+ InetAddress address = InetAddresses.forString(parser.optimizedText().bytes());
return included ? address : nextDown(address);
}
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 0f5b7746bea43..9d544c00f15ff 100644
--- a/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java
+++ b/server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java
@@ -19,12 +19,15 @@
import org.elasticsearch.core.Tuple;
import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xcontent.Text;
import org.hamcrest.Matchers;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
+import java.util.Locale;
import static org.hamcrest.Matchers.equalTo;
@@ -171,12 +174,16 @@ public void testForStringIPv6EightColons() throws UnknownHostException {
}
public void testConvertDottedQuadToHex() throws UnknownHostException {
- String[] ipStrings = { "7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127" };
+ String[] ipStrings = { "7::0.128.0.127", "7::0.128.0.128", "7::128.128.0.127", "7::0.128.128.127", "::ffff:10.10.1.1" };
for (String ipString : ipStrings) {
// Shouldn't hit DNS, because it's an IP string literal.
InetAddress ipv6Addr = InetAddress.getByName(ipString);
assertEquals(ipv6Addr, InetAddresses.forString(ipString));
+ byte[] asBytes = ipString.getBytes(StandardCharsets.UTF_8);
+ byte[] bytes = new byte[32];
+ System.arraycopy(asBytes, 0, bytes, 8, asBytes.length);
+ assertEquals(ipv6Addr, InetAddresses.forString(bytes, 8, asBytes.length));
assertTrue(InetAddresses.isInetAddress(ipString));
}
}
@@ -244,4 +251,26 @@ public void testParseCidr() {
assertEquals(InetAddresses.forString("::fffe:0:0"), cidr.v1());
assertEquals(Integer.valueOf(128), cidr.v2());
}
+
+ public void testEncodeAsIpv6() throws Exception {
+ assertEquals(16, InetAddresses.encodeAsIpv6(new Text("::1")).length);
+ assertEquals(16, InetAddresses.encodeAsIpv6(new Text("192.168.0.0")).length);
+ assertEquals(
+ "192.168.0.0",
+ InetAddresses.toAddrString(InetAddress.getByAddress(InetAddresses.encodeAsIpv6(new Text("192.168.0.0"))))
+ );
+ }
+
+ public void testAppendHexBytes() {
+ for (int i = 0; i < 256; i++) {
+ byte b1 = randomByte();
+ byte b2 = randomByte();
+ // The expected string is the hex representation of the two bytes, padded to 4 characters
+ String expected = String.format(Locale.ROOT, "%1$04x", (b1 & 0xFF) << 8 | b2 & 0xFF);
+ byte[] hex = new byte[4];
+ InetAddresses.appendHexBytes(hex, 0, b1, b2);
+ String actual = new String(hex, StandardCharsets.US_ASCII);
+ assertEquals(expected, actual);
+ }
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java
index c7055c9ff438a..a771746fde1db 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java
@@ -9,7 +9,6 @@
package org.elasticsearch.index.mapper;
-import org.apache.lucene.document.InetAddressPoint;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.LongField;
import org.apache.lucene.index.IndexOptions;
@@ -2205,22 +2204,22 @@ public void testMatchAndUnmatchWithArrayOfFieldNamesMapToIpType() throws IOExcep
merge(mapperService, dynamicMapping(parsedDoc.dynamicMappingsUpdate()));
LuceneDocument doc = parsedDoc.rootDoc();
- assertNotEquals(InetAddressPoint.class, doc.getField("one_ip").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("one_ip").getClass());
Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("one_ip");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
- assertNotEquals(InetAddressPoint.class, doc.getField("ip_two").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("ip_two").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("ip_two");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
- assertEquals(InetAddressPoint.class, doc.getField("three_ip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("three_ip").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("three_ip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
- assertEquals(InetAddressPoint.class, doc.getField("ip_four").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("ip_four").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("ip_four");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
@@ -2257,17 +2256,17 @@ public void testMatchWithArrayOfFieldNamesUsingRegex() throws IOException {
merge(mapperService, dynamicMapping(parsedDoc.dynamicMappingsUpdate()));
LuceneDocument doc = parsedDoc.rootDoc();
- assertEquals(InetAddressPoint.class, doc.getField("one100_ip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("one100_ip").getClass());
Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("one100_ip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
- assertEquals(InetAddressPoint.class, doc.getField("iptwo").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("iptwo").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("iptwo");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
- assertNotEquals(InetAddressPoint.class, doc.getField("threeip").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("threeip").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("threeip");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
@@ -2303,18 +2302,18 @@ public void testSimpleMatchWithArrayOfFieldNamesMixingGlobsAndRegex() throws IOE
merge(mapperService, dynamicMapping(parsedDoc.dynamicMappingsUpdate()));
LuceneDocument doc = parsedDoc.rootDoc();
- assertEquals(InetAddressPoint.class, doc.getField("oneip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("oneip").getClass());
Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("oneip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
// this one will not match and be an IP field because it was specified with a regex but match_pattern is implicit "simple"
- assertNotEquals(InetAddressPoint.class, doc.getField("iptwo").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("iptwo").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("iptwo");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
- assertNotEquals(InetAddressPoint.class, doc.getField("threeip").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("threeip").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("threeip");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
@@ -2362,18 +2361,18 @@ public void testDefaultMatchTypeWithArrayOfFieldNamesMixingGlobsAndRegexInPathMa
LuceneDocument doc = parsedDoc.rootDoc();
- assertEquals(InetAddressPoint.class, doc.getField("outer.oneip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("outer.oneip").getClass());
Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.oneip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
// this one will not match and be an IP field because it was specified with a regex but match_pattern is implicit "simple"
- assertNotEquals(InetAddressPoint.class, doc.getField("outer.iptwo").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("outer.iptwo").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.iptwo");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
- assertEquals(InetAddressPoint.class, doc.getField("outer.threeip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("outer.threeip").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.threeip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
@@ -2421,18 +2420,18 @@ public void testSimpleMatchTypeWithArrayOfFieldNamesMixingGlobsAndRegexInPathMat
LuceneDocument doc = parsedDoc.rootDoc();
- assertEquals(InetAddressPoint.class, doc.getField("outer.oneip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("outer.oneip").getClass());
Mapper fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.oneip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
// this one will not match and be an IP field because it was specified with a regex but match_pattern is implicit "simple"
- assertNotEquals(InetAddressPoint.class, doc.getField("outer.iptwo").getClass());
+ assertNotEquals(ESInetAddressPoint.class, doc.getField("outer.iptwo").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.iptwo");
assertNotNull(fieldMapper);
assertEquals("text", fieldMapper.typeName());
- assertEquals(InetAddressPoint.class, doc.getField("outer.threeip").getClass());
+ assertEquals(ESInetAddressPoint.class, doc.getField("outer.threeip").getClass());
fieldMapper = mapperService.documentMapper().mappers().getMapper("outer.threeip");
assertNotNull(fieldMapper);
assertEquals("ip", fieldMapper.typeName());
diff --git a/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java
index 7de761d3197ca..b1e0eea500505 100644
--- a/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java
+++ b/server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java
@@ -75,16 +75,16 @@ protected IpFieldScript.Factory multipleValuesScript() {
@Override
protected void assertMultipleValues(List