Skip to content

Commit 1b60ec0

Browse files
authored
Add CIDR processing class (#69630)
Adapted logic from CIDRUtils to create a new class for CIDR processing. Relates #60668
1 parent 5c374e8 commit 1b60ec0

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

  • modules/lang-painless/src
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.painless.api;
10+
11+
import org.elasticsearch.common.collect.Tuple;
12+
import org.elasticsearch.common.network.InetAddresses;
13+
14+
import java.net.InetAddress;
15+
import java.util.Arrays;
16+
17+
/**
18+
* The intent of this class is to provide a more efficient way of matching multiple IP addresses against a single CIDR.
19+
* The logic comes from CIDRUtils.
20+
* @see org.elasticsearch.common.network.CIDRUtils
21+
*/
22+
public class CIDR {
23+
private final byte[] lower;
24+
private final byte[] upper;
25+
26+
/**
27+
* @param cidr an IPv4 or IPv6 address, which may or may not contain a suffix.
28+
*/
29+
public CIDR(String cidr) {
30+
if (cidr.contains("/")) {
31+
final Tuple<byte[], byte[]> range = getLowerUpper(InetAddresses.parseCidr(cidr));
32+
lower = range.v1();
33+
upper = range.v2();
34+
} else {
35+
lower = InetAddresses.forString(cidr).getAddress();
36+
upper = lower;
37+
}
38+
}
39+
40+
/**
41+
* Checks if a given IP address belongs to the range of the CIDR object.
42+
* @param addressToCheck an IPv4 or IPv6 address without a suffix.
43+
* @return whether the IP is in the object's range or not.
44+
*/
45+
public boolean contains(String addressToCheck) {
46+
if (addressToCheck == null) {
47+
return false;
48+
}
49+
50+
byte[] parsedAddress = InetAddresses.forString(addressToCheck).getAddress();
51+
return isBetween(parsedAddress, lower, upper);
52+
}
53+
54+
private static Tuple<byte[], byte[]> getLowerUpper(Tuple<InetAddress, Integer> cidr) {
55+
final InetAddress value = cidr.v1();
56+
final Integer prefixLength = cidr.v2();
57+
58+
if (prefixLength < 0 || prefixLength > 8 * value.getAddress().length) {
59+
throw new IllegalArgumentException("illegal prefixLength '" + prefixLength +
60+
"'. Must be 0-32 for IPv4 ranges, 0-128 for IPv6 ranges");
61+
}
62+
63+
byte[] lower = value.getAddress();
64+
byte[] upper = value.getAddress();
65+
// Borrowed from Lucene
66+
for (int i = prefixLength; i < 8 * lower.length; i++) {
67+
int m = 1 << (7 - (i & 7));
68+
lower[i >> 3] &= ~m;
69+
upper[i >> 3] |= m;
70+
}
71+
return new Tuple<>(lower, upper);
72+
}
73+
74+
private static boolean isBetween(byte[] addr, byte[] lower, byte[] upper) {
75+
if (addr.length != lower.length) {
76+
addr = encode(addr);
77+
lower = encode(lower);
78+
upper = encode(upper);
79+
}
80+
return Arrays.compareUnsigned(lower, addr) <= 0 &&
81+
Arrays.compareUnsigned(upper, addr) >= 0;
82+
}
83+
84+
// Borrowed from Lucene to make this consistent IP fields matching for the mix of IPv4 and IPv6 values
85+
// Modified signature to avoid extra conversions
86+
private static byte[] encode(byte[] address) {
87+
final byte[] IPV4_PREFIX = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1};
88+
if (address.length == 4) {
89+
byte[] mapped = new byte[16];
90+
System.arraycopy(IPV4_PREFIX, 0, mapped, 0, IPV4_PREFIX.length);
91+
System.arraycopy(address, 0, mapped, IPV4_PREFIX.length, address.length);
92+
address = mapped;
93+
} else if (address.length != 16) {
94+
throw new UnsupportedOperationException("Only IPv4 and IPv6 addresses are supported");
95+
}
96+
return address;
97+
}
98+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.painless.api;
10+
11+
import org.elasticsearch.test.ESTestCase;
12+
13+
public class CIDRTests extends ESTestCase {
14+
public void testCIDR() {
15+
CIDR cidr = new CIDR("192.168.8.0/24");
16+
assertTrue(cidr.contains("192.168.8.0"));
17+
assertTrue(cidr.contains("192.168.8.255"));
18+
assertFalse(cidr.contains("192.168.9.0"));
19+
assertFalse(cidr.contains("192.168.7.255"));
20+
assertFalse(cidr.contains(null));
21+
22+
CIDR cidrNoRange = new CIDR("169.254.0.0");
23+
assertTrue(cidrNoRange.contains("169.254.0.0"));
24+
assertFalse(cidrNoRange.contains("169.254.0.1"));
25+
26+
CIDR cidrIPv6 = new CIDR("b181:3a88:339c:97f5:2b40:5175:bf3d:f77d/64");
27+
assertTrue(cidrIPv6.contains("b181:3a88:339c:97f5:2b40:5175:bf3d:f77e"));
28+
assertFalse(cidrIPv6.contains("254.120.25.32"));
29+
30+
expectThrows(IllegalArgumentException.class, () -> new CIDR("10.2.0.0/36"));
31+
}
32+
}

0 commit comments

Comments
 (0)