Skip to content

Commit 641cf00

Browse files
authored
ZOOKEEPER-4240: Add IPV6 support for ZooKeeper ACL
Reviewers: anmolnar, anmolnar, kezhuw Author: kabhishek4 Closes #2280 from kabhishek4/ZOOKEEPER-4240
1 parent c21d37f commit 641cf00

File tree

3 files changed

+355
-3
lines changed

3 files changed

+355
-3
lines changed

zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/IPAuthenticationProvider.java

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,26 @@
2222
import java.util.Collections;
2323
import java.util.List;
2424
import java.util.StringTokenizer;
25+
import java.util.regex.Pattern;
2526
import javax.servlet.http.HttpServletRequest;
2627
import org.apache.zookeeper.KeeperException;
2728
import org.apache.zookeeper.data.Id;
2829
import org.apache.zookeeper.server.ServerCnxn;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
2932

3033
public class IPAuthenticationProvider implements AuthenticationProvider {
34+
private static final Logger LOG = LoggerFactory.getLogger(IPAuthenticationProvider.class);
3135
public static final String X_FORWARDED_FOR_HEADER_NAME = "X-Forwarded-For";
3236

3337
public static final String USE_X_FORWARDED_FOR_KEY = "zookeeper.IPAuthenticationProvider.usexforwardedfor";
38+
private static final int IPV6_BYTE_LENGTH = 16; // IPv6 address is 128 bits = 16 bytes
39+
private static final int IPV6_SEGMENT_COUNT = 8; // IPv6 address has 8 segments
40+
private static final int IPV6_SEGMENT_BYTE_LENGTH = 2; // Each segment has up to two bytes
41+
private static final int IPV6_SEGMENT_HEX_LENGTH = 4; // Each segment has up to 4 hex digits
42+
43+
private static final Pattern IPV6_PATTERN = Pattern.compile(":");
44+
private static final Pattern IPV4_PATTERN = Pattern.compile("\\.");
3445

3546
public String getScheme() {
3647
return "ip";
@@ -55,9 +66,14 @@ public List<Id> handleAuthentication(HttpServletRequest request, byte[] authData
5566
// This is a bit weird but we need to return the address and the number of
5667
// bytes (to distinguish between IPv4 and IPv6
5768
private byte[] addr2Bytes(String addr) {
58-
byte[] b = v4addr2Bytes(addr);
59-
// TODO Write the v6addr2Bytes
60-
return b;
69+
if (IPV6_PATTERN.matcher(addr).find()) {
70+
return v6addr2Bytes(addr);
71+
} else if (IPV4_PATTERN.matcher(addr).find()) {
72+
return v4addr2Bytes(addr);
73+
} else {
74+
LOG.warn("Input string does not resemble an IPv4 or IPv6 address: {}", addr);
75+
return null;
76+
}
6177
}
6278

6379
private byte[] v4addr2Bytes(String addr) {
@@ -81,6 +97,83 @@ private byte[] v4addr2Bytes(String addr) {
8197
return b;
8298
}
8399

100+
/**
101+
* Validates an IPv6 address string and converts it into a byte array.
102+
*
103+
* @param ipv6Addr The IPv6 address string to validate.
104+
* @return A byte array representing the IPv6 address if valid, or null if the address
105+
* is invalid or cannot be parsed.
106+
*/
107+
static byte[] v6addr2Bytes(String ipv6Addr) {
108+
try {
109+
return parseV6addr(ipv6Addr);
110+
} catch (IllegalArgumentException e) {
111+
LOG.warn("Fail to parse {} as IPv6 address: {}", ipv6Addr, e.getMessage());
112+
return null;
113+
}
114+
}
115+
116+
static byte[] parseV6addr(String ipv6Addr) {
117+
// Split the address by "::" to handle zero compression, -1 to keep trailing empty strings
118+
String[] parts = ipv6Addr.split("::", -1);
119+
120+
String[] segments1 = new String[0];
121+
String[] segments2 = new String[0];
122+
123+
// Case 1: No "::" (full address)
124+
if (parts.length == 1) {
125+
segments1 = parts[0].split(":", -1);
126+
if (segments1.length != IPV6_SEGMENT_COUNT) {
127+
String reason = "wrong number of segments";
128+
throw new IllegalArgumentException(reason);
129+
}
130+
} else if (parts.length == 2) {
131+
// Case 2: "::" is present
132+
// Handle cases like "::1" or "1::"
133+
if (!parts[0].isEmpty()) {
134+
segments1 = parts[0].split(":", -1);
135+
}
136+
if (!parts[1].isEmpty()) {
137+
segments2 = parts[1].split(":", -1);
138+
}
139+
140+
// Check if the total number of explicit segments exceeds 8
141+
if (segments1.length + segments2.length >= IPV6_SEGMENT_COUNT) {
142+
String reason = "too many segments";
143+
throw new IllegalArgumentException(reason);
144+
}
145+
} else {
146+
// Case 3: Invalid number of parts after splitting by "::" (should be 1 or 2)
147+
String reason = "too many '::'";
148+
throw new IllegalArgumentException(reason);
149+
}
150+
151+
byte[] result = new byte[IPV6_BYTE_LENGTH];
152+
// Process segments before "::"
153+
parseV6Segment(result, 0, segments1);
154+
// Process segments after "::"
155+
parseV6Segment(result, IPV6_BYTE_LENGTH - segments2.length * IPV6_SEGMENT_BYTE_LENGTH, segments2);
156+
157+
return result;
158+
}
159+
160+
private static void parseV6Segment(byte[] addr, int i, String[] segments) {
161+
for (String segment : segments) {
162+
if (segment.isEmpty()) {
163+
throw new IllegalArgumentException("empty segment");
164+
} else if (segment.length() > IPV6_SEGMENT_HEX_LENGTH) {
165+
throw new IllegalArgumentException("segment too long");
166+
}
167+
try {
168+
int value = Integer.parseInt(segment, 16);
169+
addr[i++] = (byte) ((value >> 8) & 0xFF);
170+
addr[i++] = (byte) (value & 0xFF);
171+
} catch (NumberFormatException e) {
172+
throw new IllegalArgumentException("invalid hexadecimal characters in segment: " + segment);
173+
}
174+
}
175+
}
176+
84177
private void mask(byte[] b, int bits) {
85178
int start = bits / 8;
86179
int startMask = (1 << (8 - (bits % 8))) - 1;
@@ -93,6 +186,7 @@ private void mask(byte[] b, int bits) {
93186
}
94187

95188
public boolean matches(String id, String aclExpr) {
189+
LOG.trace("id: '{}' aclExpr: {}", id, aclExpr);
96190
String[] parts = aclExpr.split("/", 2);
97191
byte[] aclAddr = addr2Bytes(parts[0]);
98192
if (aclAddr == null) {
@@ -115,6 +209,10 @@ public boolean matches(String id, String aclExpr) {
115209
return false;
116210
}
117211
mask(remoteAddr, bits);
212+
// Check if id and acl expression are of different formats (ipv6 or iv4) return false
213+
if (remoteAddr.length != aclAddr.length) {
214+
return false;
215+
}
118216
for (int i = 0; i < remoteAddr.length; i++) {
119217
if (remoteAddr[i] != aclAddr[i]) {
120218
return false;

zookeeper-server/src/test/java/org/apache/zookeeper/server/auth/IPAuthenticationProviderTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,23 @@
1919

2020
import static org.apache.zookeeper.server.auth.IPAuthenticationProvider.USE_X_FORWARDED_FOR_KEY;
2121
import static org.apache.zookeeper.server.auth.IPAuthenticationProvider.X_FORWARDED_FOR_HEADER_NAME;
22+
import static org.hamcrest.CoreMatchers.containsString;
23+
import static org.hamcrest.MatcherAssert.assertThat;
2224
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.assertNull;
26+
import static org.junit.Assert.fail;
27+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
28+
import static org.junit.jupiter.api.Assertions.assertNotNull;
2329
import static org.mockito.Mockito.doReturn;
2430
import static org.mockito.Mockito.mock;
31+
import java.util.stream.Stream;
2532
import javax.servlet.http.HttpServletRequest;
2633
import org.junit.After;
2734
import org.junit.Before;
2835
import org.junit.Test;
36+
import org.junit.jupiter.params.ParameterizedTest;
37+
import org.junit.jupiter.params.provider.Arguments;
38+
import org.junit.jupiter.params.provider.MethodSource;
2939

3040
public class IPAuthenticationProviderTest {
3141

@@ -96,4 +106,84 @@ public void testGetClientIPAddressMissingXForwardedFor() {
96106
// Assert
97107
assertEquals("192.168.1.1", clientIp);
98108
}
109+
110+
@Test
111+
public void testParsingOfIPv6Address() {
112+
//Full IPv6 address
113+
String ipv6Full = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
114+
byte[] expectedFull = {
115+
(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
116+
(byte) 0x85, (byte) 0xa3, (byte) 0x00, (byte) 0x00,
117+
(byte) 0x00, (byte) 0x00, (byte) 0x8a, (byte) 0x2e,
118+
(byte) 0x03, (byte) 0x70, (byte) 0x73, (byte) 0x34
119+
};
120+
byte[] actualFull = IPAuthenticationProvider.v6addr2Bytes(ipv6Full);
121+
assertNotNull(actualFull, "Full IPv6 address should not return null");
122+
assertArrayEquals(expectedFull, actualFull, "Full IPv6 address conversion mismatch");
123+
124+
//Compressed IPv6 address (double colon)
125+
String ipv6Compressed = "2001:db8::8a2e:370:7334";
126+
byte[] expectedCompressed = {
127+
(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
128+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
129+
(byte) 0x00, (byte) 0x00, (byte) 0x8a, (byte) 0x2e,
130+
(byte) 0x03, (byte) 0x70, (byte) 0x73, (byte) 0x34
131+
};
132+
byte[] actualCompressed = IPAuthenticationProvider.v6addr2Bytes(ipv6Compressed);
133+
assertNotNull(actualCompressed, "Compressed IPv6 address should not return null");
134+
assertArrayEquals(expectedCompressed, actualCompressed, "Compressed IPv6 address conversion mismatch");
135+
136+
//Shortened IPv6 address
137+
String ipv6Shortened = "2001:db8::1";
138+
byte[] expectedShortened = {
139+
(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
140+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
141+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
142+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01
143+
};
144+
byte[] actualShortened = IPAuthenticationProvider.v6addr2Bytes(ipv6Shortened);
145+
assertNotNull(actualShortened, "Shortened IPv6 address should not return null");
146+
assertArrayEquals(expectedShortened, actualShortened, "Shortened IPv6 address conversion mismatch");
147+
148+
//Loopback address
149+
String ipv6Loopback = "::1";
150+
byte[] expectedLoopback = {
151+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
152+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
153+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
154+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01
155+
};
156+
byte[] actualLoopback = IPAuthenticationProvider.v6addr2Bytes(ipv6Loopback);
157+
assertNotNull(actualLoopback, "Loopback IPv6 address should not return null");
158+
assertArrayEquals(expectedLoopback, actualLoopback, "Loopback IPv6 address conversion mismatch");
159+
}
160+
161+
private static Stream<Arguments> invalidIPv6Addresses() {
162+
return Stream.of(
163+
Arguments.of("1", "wrong number of segments"),
164+
Arguments.of("1:2", "wrong number of segments"),
165+
Arguments.of("1::2:", "empty segment"),
166+
Arguments.of(":1::2:", "empty segment"),
167+
Arguments.of("1:2:3:4:5:6:7:8:", "wrong number of segments"),
168+
Arguments.of("1:2:3:4:5:6:7:8:9", "wrong number of segments"),
169+
Arguments.of("1:2::3:4:5:6:7:8", "too many segments"),
170+
Arguments.of("1::2::", "too many '::'"),
171+
Arguments.of("1:abcdf::", "segment too long"),
172+
Arguments.of("efgh::", "invalid hexadecimal characters in segment"),
173+
Arguments.of("1:: ", "invalid hexadecimal characters in segment"),
174+
Arguments.of(" 1::", "invalid hexadecimal characters in segment")
175+
);
176+
}
177+
178+
@ParameterizedTest(name = "address = \"{0}\"")
179+
@MethodSource("invalidIPv6Addresses")
180+
public void testParsingOfInvalidIPv6Address(String ipv6Address, String expectedMessage) {
181+
try {
182+
IPAuthenticationProvider.parseV6addr(ipv6Address);
183+
fail("expect failure");
184+
} catch (IllegalArgumentException e) {
185+
assertThat(e.getMessage(), containsString(expectedMessage));
186+
}
187+
assertNull(IPAuthenticationProvider.v6addr2Bytes(ipv6Address));
188+
}
99189
}

0 commit comments

Comments
 (0)