Skip to content

Commit 6eeab3a

Browse files
committed
add ipv6 ranges
Signed-off-by: neo <1100909+neowu@users.noreply.github.com>
1 parent bb2b31e commit 6eeab3a

File tree

3 files changed

+262
-14
lines changed

3 files changed

+262
-14
lines changed

core-ng/src/main/java/core/framework/internal/web/http/IPv4Ranges.java

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,28 @@ public IPv4Ranges(List<String> cidrs) {
7575
private int[] comparableIPRanges(String cidr) {
7676
int index = cidr.indexOf('/');
7777
if (index <= 0 || index >= cidr.length() - 1) throw new Error("invalid cidr, value=" + cidr);
78-
int address = toInteger(address(cidr.substring(0, index)));
78+
int address = toInt(address(cidr.substring(0, index)));
7979
int maskBits = Integer.parseInt(cidr.substring(index + 1));
80-
long mask = -1L << (32 - maskBits);
81-
int lowestIP = (int) (address & mask);
82-
int highestIP = (int) (lowestIP + ~mask);
83-
return new int[]{lowestIP ^ Integer.MIN_VALUE, highestIP ^ Integer.MIN_VALUE};
80+
81+
int rangeStart;
82+
int rangeEnd;
83+
// refer to https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.19
84+
// If the promoted type of the left-hand operand is int, then only the five lowest-order bits of the right-hand operand are used as the shift distance.
85+
// It is as if the right-hand operand were subjected to a bitwise logical AND operator & (§15.22.1) with the mask value 0x1f (0b11111).
86+
// The shift distance actually used is therefore always in the range 0 to 31, inclusive.
87+
if (maskBits == 0) {
88+
rangeStart = 0;
89+
rangeEnd = -1;
90+
} else {
91+
int mask = -1 << (32 - maskBits);
92+
rangeStart = address & mask;
93+
rangeEnd = rangeStart + ~mask;
94+
}
95+
return new int[]{toSortable(rangeStart), toSortable(rangeEnd)};
8496
}
8597

86-
private int toInteger(byte[] address) {
87-
if (address.length > 4) throw new Error("only support ipv4, address=" + Arrays.toString(address));
98+
private int toInt(byte[] address) {
99+
if (address.length != 4) throw new Error("not ipv4 address, address=" + Arrays.toString(address));
88100
int result = 0;
89101
result |= (address[0] & 0xFF) << 24;
90102
result |= (address[1] & 0xFF) << 16;
@@ -93,15 +105,18 @@ private int toInteger(byte[] address) {
93105
return result;
94106
}
95107

96-
/*
97-
* with address ^ Integer.MIN_VALUE, it converts binary presentation to sortable int form
98-
* where Integer.MIN_VALUE = 0.0.0.0
99-
* and 0 = 128.0.0.0
100-
* and Integer.MAX_VALUE = 255.255.255.255
101-
* */
102108
public boolean matches(byte[] address) {
103109
if (ranges.length == 0) return false;
104-
int comparableIP = toInteger(address) ^ Integer.MIN_VALUE;
110+
int comparableIP = toSortable(toInt(address));
105111
return withinRanges(ranges, comparableIP);
106112
}
113+
114+
// with address ^ MIN_VALUE, it converts binary presentation to sortable number form (due to java doesn't have unsigned number type)
115+
// 0.0.0.0 => MIN_VALUE (00000000.0.0.0 => 10000000.0.0.0)
116+
// 127.255.255.255 => -1 (01111111.11111111.11111111.11111111 => 11111111.11111111.11111111.11111111)
117+
// 128.0.0.0 => 0 (10000000.0.0.0 => 00000000.0.0.0)
118+
// 255.255.255.255 => MAX_VALUE (11111111.11111111.11111111.11111111 => 01111111.11111111.11111111.11111111)
119+
private int toSortable(int value) {
120+
return value ^ Integer.MIN_VALUE;
121+
}
107122
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package core.framework.internal.web.http;
2+
3+
import java.util.Arrays;
4+
import java.util.Comparator;
5+
import java.util.List;
6+
7+
public class IPv6Ranges {
8+
static boolean withinRanges(LongLong[] ranges, LongLong value) {
9+
int low = 0;
10+
int high = ranges.length - 1;
11+
12+
if (value.compareTo(ranges[low]) < 0 || value.compareTo(ranges[high]) > 0) return false;
13+
14+
while (high - low > 1) {
15+
int middle = (low + high) >>> 1;
16+
LongLong middleValue = ranges[middle];
17+
18+
int comparison = middleValue.compareTo(value);
19+
if (comparison < 0)
20+
low = middle;
21+
else if (comparison > 0)
22+
high = middle;
23+
else {
24+
return true;
25+
}
26+
}
27+
return low % 2 == 0;
28+
}
29+
30+
static LongLong[] mergeRanges(LongLong[][] ranges) {
31+
Arrays.sort(ranges, Comparator.comparing(a -> a[0]));
32+
LongLong[] results = new LongLong[ranges.length * 2];
33+
int index = 0;
34+
for (LongLong[] range : ranges) {
35+
if (index > 1 && results[index - 1].compareTo(range[0]) >= 0) {
36+
if (results[index - 1].compareTo(range[1]) < 0) {
37+
results[index - 1] = range[1];
38+
}
39+
} else {
40+
results[index++] = range[0];
41+
results[index++] = range[1];
42+
}
43+
}
44+
if (index < results.length) {
45+
return Arrays.copyOf(results, index);
46+
} else {
47+
return results;
48+
}
49+
}
50+
51+
static LongLong toLongLong(byte[] address) {
52+
if (address.length != 16) throw new Error("not ipv6 address, address=" + Arrays.toString(address));
53+
54+
long high = 0;
55+
high |= (long) (address[0] & 0xFF) << 56;
56+
high |= (long) (address[1] & 0xFF) << 48;
57+
high |= (long) (address[2] & 0xFF) << 40;
58+
high |= (long) (address[3] & 0xFF) << 32;
59+
high |= (long) (address[4] & 0xFF) << 24;
60+
high |= (long) (address[5] & 0xFF) << 16;
61+
high |= (long) (address[6] & 0xFF) << 8;
62+
high |= address[7] & 0xFF;
63+
64+
long low = 0;
65+
low |= (long) (address[8] & 0xFF) << 56;
66+
low |= (long) (address[9] & 0xFF) << 48;
67+
low |= (long) (address[10] & 0xFF) << 40;
68+
low |= (long) (address[11] & 0xFF) << 32;
69+
low |= (long) (address[12] & 0xFF) << 24;
70+
low |= (long) (address[13] & 0xFF) << 16;
71+
low |= (long) (address[14] & 0xFF) << 8;
72+
low |= address[15] & 0xFF;
73+
74+
return new LongLong(high, low);
75+
}
76+
77+
private final LongLong[] ranges;
78+
79+
public IPv6Ranges(List<String> cidrs) {
80+
LongLong[][] ranges = new LongLong[cidrs.size()][];
81+
int index = 0;
82+
for (String cidr : cidrs) {
83+
LongLong[] range = comparableIPRanges(cidr);
84+
ranges[index++] = range;
85+
}
86+
this.ranges = mergeRanges(ranges);
87+
}
88+
89+
private LongLong[] comparableIPRanges(String cidr) {
90+
int index = cidr.indexOf('/');
91+
if (index <= 0 || index >= cidr.length() - 1) throw new Error("invalid cidr, value=" + cidr);
92+
LongLong address = toLongLong(IPv4Ranges.address(cidr.substring(0, index)));
93+
int maskBits = Integer.parseInt(cidr.substring(index + 1));
94+
95+
LongLong rangeStart;
96+
LongLong rangeEnd;
97+
if (maskBits == 0) {
98+
rangeStart = new LongLong(0L, 0L);
99+
rangeEnd = new LongLong(-1L, -1L);
100+
} else if (maskBits <= 64) {
101+
long highMask = -1L << (64 - maskBits);
102+
rangeStart = new LongLong(address.high & highMask, 0L);
103+
rangeEnd = new LongLong(rangeStart.high | ~highMask, -1L);
104+
} else {
105+
long lowMask = -1L << (128 - maskBits);
106+
rangeStart = new LongLong(address.high, address.low & lowMask);
107+
rangeEnd = new LongLong(address.high, rangeStart.low | ~lowMask);
108+
}
109+
110+
return new LongLong[]{rangeStart.toSortable(), rangeEnd.toSortable()};
111+
}
112+
113+
public boolean matches(byte[] address) {
114+
if (ranges.length == 0) return false;
115+
if (address.length != 16) return false;
116+
117+
LongLong comparableIP = toLongLong(address).toSortable();
118+
return withinRanges(ranges, comparableIP);
119+
}
120+
121+
// high,low are used to represent a 128-bit IPv6 address as two 64-bit long values
122+
record LongLong(long high, long low) implements Comparable<LongLong> {
123+
// with address ^ MIN_VALUE, it converts binary presentation to sortable number form (due to java doesn't have unsigned number type)
124+
// 0.0.0.0 => MIN_VALUE (00000000.0.0.0 => 10000000.0.0.0)
125+
// 127.255.255.255 => -1 (01111111.11111111.11111111.11111111 => 11111111.11111111.11111111.11111111)
126+
// 128.0.0.0 => 0 (10000000.0.0.0 => 00000000.0.0.0)
127+
// 255.255.255.255 => MAX_VALUE (11111111.11111111.11111111.11111111 => 01111111.11111111.11111111.11111111)
128+
LongLong toSortable() {
129+
return new LongLong(high ^ Long.MIN_VALUE, low ^ Long.MIN_VALUE);
130+
}
131+
132+
@Override
133+
public int compareTo(LongLong other) {
134+
int result = Long.compare(high, other.high);
135+
return result != 0 ? result : Long.compare(low, other.low);
136+
}
137+
}
138+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package core.framework.internal.web.http;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
7+
import static core.framework.internal.web.http.IPv4Ranges.address;
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.junit.jupiter.api.Assertions.assertFalse;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
12+
/**
13+
* @author neo
14+
*/
15+
class IPv6RangesTest {
16+
@Test
17+
void matches() {
18+
var ranges = new IPv6Ranges(List.of("2001:db8::/32"));
19+
assertTrue(ranges.matches(address("2001:db8::1")));
20+
assertTrue(ranges.matches(address("2001:db8::ffff")));
21+
assertFalse(ranges.matches(address("2001:db9::1")));
22+
assertFalse(ranges.matches(address("2001:db7::1")));
23+
24+
ranges = new IPv6Ranges(List.of("2001:db8::1/128"));
25+
assertTrue(ranges.matches(address("2001:db8::1")));
26+
assertFalse(ranges.matches(address("2001:db8::2")));
27+
assertFalse(ranges.matches(address("2001:db8::3")));
28+
29+
ranges = new IPv6Ranges(List.of("2001:db8::/64"));
30+
assertTrue(ranges.matches(address("2001:db8::0")));
31+
assertTrue(ranges.matches(address("2001:db8::1")));
32+
assertTrue(ranges.matches(address("2001:db8::ffff")));
33+
assertTrue(ranges.matches(address("2001:db8::ffff:ffff")));
34+
assertTrue(ranges.matches(address("2001:0db8::ffff:ffff:ffff:ffff")));
35+
assertFalse(ranges.matches(address("2001:db9::0")));
36+
37+
ranges = new IPv6Ranges(List.of("2001::/16"));
38+
assertTrue(ranges.matches(address("2001:db8::0")));
39+
assertTrue(ranges.matches(address("2001:db8::ffff")));
40+
assertTrue(ranges.matches(address("2001:db9::0")));
41+
assertFalse(ranges.matches(address("2002::0")));
42+
}
43+
44+
@Test
45+
void matchesMultipleRanges() {
46+
var ranges = new IPv6Ranges(List.of("2001:db8::/32", "2001:db9::/32", "2001:dba::/32"));
47+
assertTrue(ranges.matches(address("2001:db8::1")));
48+
assertTrue(ranges.matches(address("2001:db9::1")));
49+
assertTrue(ranges.matches(address("2001:dba::1")));
50+
assertFalse(ranges.matches(address("2001:dbb::1")));
51+
assertFalse(ranges.matches(address("2001:db7::1")));
52+
}
53+
54+
@Test
55+
void matchesWithOverlappingRanges() {
56+
var ranges = new IPv6Ranges(List.of("2001:db8::/48", "2001:db8:1::/48"));
57+
assertTrue(ranges.matches(address("2001:db8::1")));
58+
assertTrue(ranges.matches(address("2001:db8:1::1")));
59+
assertFalse(ranges.matches(address("2001:db8:2::1")));
60+
}
61+
62+
@Test
63+
void matchesWithEmptyRanges() {
64+
var ranges = new IPv6Ranges(List.of());
65+
assertFalse(ranges.matches(address("2001:db8::1")));
66+
}
67+
68+
@Test
69+
void matchesAll() {
70+
var ranges = new IPv6Ranges(List.of("::/0"));
71+
assertThat(ranges.matches(address("2001:db8::1"))).isTrue();
72+
assertThat(ranges.matches(address("::1"))).isTrue();
73+
assertTrue(ranges.matches(address("fe80::1")));
74+
}
75+
76+
@Test
77+
void mergeRanges() {
78+
IPv6Ranges.LongLong[][] ranges1 = {{longLong(1), longLong(2)},
79+
{longLong(4), longLong(5)},
80+
{longLong(5), longLong(9)},
81+
{longLong(3), longLong(5)},
82+
{longLong(10), longLong(20)}};
83+
assertThat(IPv6Ranges.mergeRanges(ranges1)).containsExactly(longLong(1), longLong(2), longLong(3), longLong(9), longLong(10), longLong(20));
84+
85+
IPv6Ranges.LongLong[][] ranges2 = {{longLong(1), longLong(2)},
86+
{longLong(10), longLong(20)},
87+
{longLong(3), longLong(5)},
88+
{longLong(30), longLong(40)}};
89+
assertThat(IPv6Ranges.mergeRanges(ranges2)).containsExactly(longLong(1), longLong(2), longLong(3), longLong(5), longLong(10), longLong(20), longLong(30), longLong(40));
90+
}
91+
92+
IPv6Ranges.LongLong longLong(long value) {
93+
return new IPv6Ranges.LongLong(value, value);
94+
}
95+
}

0 commit comments

Comments
 (0)