1
0
Felix Barnsteiner 1 сар өмнө
parent
commit
6dae0112ee

+ 142 - 0
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]));
+        }
+    }
+}

+ 202 - 101
server/src/main/java/org/elasticsearch/common/network/InetAddresses.java

@@ -19,183 +19,265 @@ package org.elasticsearch.common.network;
 
 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.
+     * <p>
+     * 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 @@ public class InetAddresses {
      * @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);

+ 116 - 0
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.
+     * <p>
+     * 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.
+     * <p>
+     * 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();
+    }
+}

+ 19 - 12
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.aggregations.support.CoreValuesSourceType;
 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 @@ public class IpFieldMapper extends FieldMapper {
 
     @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 @@ public class IpFieldMapper extends FieldMapper {
         }
         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 @@ public class IpFieldMapper extends FieldMapper {
         }
     }
 
-    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 @@ public class IpFieldMapper extends FieldMapper {
         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

+ 2 - 2
server/src/main/java/org/elasticsearch/index/mapper/RangeType.java

@@ -54,14 +54,14 @@ public enum RangeType {
         @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);
         }
 

+ 30 - 1
server/src/test/java/org/elasticsearch/common/network/InetAddressesTests.java

@@ -19,12 +19,15 @@ package org.elasticsearch.common.network;
 
 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 class InetAddressesTests extends ESTestCase {
     }
 
     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 class InetAddressesTests extends ESTestCase {
         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);
+        }
+    }
 }

+ 16 - 17
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 class DynamicTemplatesTests extends MapperServiceTestCase {
         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 class DynamicTemplatesTests extends MapperServiceTestCase {
         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 class DynamicTemplatesTests extends MapperServiceTestCase {
         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 class DynamicTemplatesTests extends MapperServiceTestCase {
 
         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 class DynamicTemplatesTests extends MapperServiceTestCase {
 
         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());

+ 3 - 3
server/src/test/java/org/elasticsearch/index/mapper/IpScriptMapperTests.java

@@ -75,16 +75,16 @@ public class IpScriptMapperTests extends MapperScriptTestCase<IpFieldScript.Fact
     @Override
     protected void assertMultipleValues(List<IndexableField> fields) {
         assertEquals(4, fields.size());
-        assertEquals("InetAddressPoint <field:[0:0:0:0:0:0:0:1]>", fields.get(0).toString());
+        assertEquals("ESInetAddressPoint <field:[::1]>", fields.get(0).toString());
         assertEquals("docValuesType=SORTED_SET<field:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]>", fields.get(1).toString());
-        assertEquals("InetAddressPoint <field:[0:0:0:0:0:0:0:2]>", fields.get(2).toString());
+        assertEquals("ESInetAddressPoint <field:[::2]>", fields.get(2).toString());
         assertEquals("docValuesType=SORTED_SET<field:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2]>", fields.get(3).toString());
     }
 
     @Override
     protected void assertDocValuesDisabled(List<IndexableField> fields) {
         assertEquals(1, fields.size());
-        assertEquals("InetAddressPoint <field:[0:0:0:0:0:0:0:1]>", fields.get(0).toString());
+        assertEquals("ESInetAddressPoint <field:[::1]>", fields.get(0).toString());
     }
 
     @Override