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 fields) { assertEquals(4, fields.size()); - assertEquals("InetAddressPoint ", fields.get(0).toString()); + assertEquals("ESInetAddressPoint ", fields.get(0).toString()); assertEquals("docValuesType=SORTED_SET", fields.get(1).toString()); - assertEquals("InetAddressPoint ", fields.get(2).toString()); + assertEquals("ESInetAddressPoint ", fields.get(2).toString()); assertEquals("docValuesType=SORTED_SET", fields.get(3).toString()); } @Override protected void assertDocValuesDisabled(List fields) { assertEquals(1, fields.size()); - assertEquals("InetAddressPoint ", fields.get(0).toString()); + assertEquals("ESInetAddressPoint ", fields.get(0).toString()); } @Override