Skip to content

Commit 5e799a9

Browse files
Abhishek KothalikarAbhishek Kothalikar
authored andcommitted
ZOOKEEPER-4240 IPV6 support in ZooKeeper ACLs
1 parent 9d1d25c commit 5e799a9

File tree

2 files changed

+178
-3
lines changed

2 files changed

+178
-3
lines changed

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

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,18 @@
2626
import org.apache.zookeeper.KeeperException;
2727
import org.apache.zookeeper.data.Id;
2828
import org.apache.zookeeper.server.ServerCnxn;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
2931

3032
public class IPAuthenticationProvider implements AuthenticationProvider {
33+
private static final Logger LOG = LoggerFactory.getLogger(IPAuthenticationProvider.class);
3134
public static final String X_FORWARDED_FOR_HEADER_NAME = "X-Forwarded-For";
3235

3336
public static final String USE_X_FORWARDED_FOR_KEY = "zookeeper.IPAuthenticationProvider.usexforwardedfor";
37+
private static final int IPV6_BYTE_LENGTH = 16; // IPv6 address is 128 bits = 16 bytes
38+
private static final int IPV6_SEGMENT_COUNT = 8; // IPv6 address has 8 segments
39+
private static final int IPV6_SEGMENT_HEX_LENGTH = 4; // Each segment has up to 4 hex digits
40+
3441

3542
public String getScheme() {
3643
return "ip";
@@ -55,9 +62,16 @@ public List<Id> handleAuthentication(HttpServletRequest request, byte[] authData
5562
// This is a bit weird but we need to return the address and the number of
5663
// bytes (to distinguish between IPv4 and IPv6
5764
private byte[] addr2Bytes(String addr) {
58-
byte[] b = v4addr2Bytes(addr);
59-
// TODO Write the v6addr2Bytes
60-
return b;
65+
if (addr.contains(":")) {
66+
LOG.info("Attempting to parse as IPv6...");
67+
return v6addr2Bytes(addr);
68+
} else if (addr.contains(".")) {
69+
LOG.info("Attempting to parse as IPv4...");
70+
return v4addr2Bytes(addr);
71+
} else {
72+
LOG.info("Input string does not resemble an IPv4 or IPv6 address: {}", addr);
73+
return null;
74+
}
6175
}
6276

6377
private byte[] v4addr2Bytes(String addr) {
@@ -81,6 +95,132 @@ private byte[] v4addr2Bytes(String addr) {
8195
return b;
8296
}
8397

98+
/**
99+
* Validates an IPv6 address string and converts it into a byte array.
100+
*
101+
* @param ipv6Addr The IPv6 address string to validate.
102+
* @return A byte array representing the IPv6 address if valid, or null if the address
103+
* is invalid or cannot be parsed.
104+
*/
105+
public static byte[] v6addr2Bytes(String ipv6Addr) {
106+
if (ipv6Addr == null || ipv6Addr.trim().isEmpty()) {
107+
LOG.info("Input IPv6 address cannot be null or empty.");
108+
return null;
109+
}
110+
111+
// Check for multiple '::' which is invalid
112+
if (ipv6Addr.indexOf("::") != ipv6Addr.lastIndexOf("::")) {
113+
LOG.info("IPv6 address contains multiple '::' which is invalid: {}", ipv6Addr);
114+
return null;
115+
}
116+
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(":");
126+
if (segments1.length != IPV6_SEGMENT_COUNT) {
127+
LOG.info("IPv6 address without '::' must have " + IPV6_SEGMENT_COUNT + " segments: {}", ipv6Addr);
128+
return null;
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(":");
135+
}
136+
if (!parts[1].isEmpty()) {
137+
segments2 = parts[1].split(":");
138+
}
139+
140+
// Check if the total number of explicit segments exceeds 8
141+
if (segments1.length + segments2.length >= IPV6_SEGMENT_COUNT) {
142+
LOG.info("Too many segments in IPv6 address with '::': {}", ipv6Addr);
143+
return null;
144+
}
145+
} else {
146+
// Case 3: Invalid number of parts after splitting by "::" (should be 1 or 2)
147+
LOG.info("Invalid IPv6 address format (unexpected '::' split result): {}", ipv6Addr);
148+
return null;
149+
}
150+
151+
List<Byte> byteList = new ArrayList<>();
152+
153+
try {
154+
// Process segments before "::"
155+
for (String segment : segments1) {
156+
if (isInvalidSegment(segment)) {
157+
LOG.info("Invalid IPv6 segment: '{}' in address: {}", segment, ipv6Addr);
158+
return null;
159+
}
160+
int value = Integer.parseInt(segment, 16);
161+
byteList.add((byte) ((value >> 8) & 0xFF));
162+
byteList.add((byte) (value & 0xFF));
163+
}
164+
165+
// Add zero segments for "::" compression
166+
int missingSegments = IPV6_SEGMENT_COUNT - (segments1.length + segments2.length);
167+
for (int i = 0; i < missingSegments; i++) {
168+
byteList.add((byte) 0x00);
169+
byteList.add((byte) 0x00);
170+
}
171+
172+
// Process segments after "::"
173+
for (String segment : segments2) {
174+
if (isInvalidSegment(segment)) {
175+
LOG.info("Invalid IPv6 segment: '{}' in address: {}", segment, ipv6Addr);
176+
return null;
177+
}
178+
int value = Integer.parseInt(segment, 16);
179+
byteList.add((byte) ((value >> 8) & 0xFF));
180+
byteList.add((byte) (value & 0xFF));
181+
}
182+
183+
} catch (NumberFormatException e) {
184+
// 3. Catch NumberFormatException if String cannot be parsed
185+
LOG.info("Invalid hexadecimal format in IPv6 address: {} - {}", ipv6Addr, e.getMessage());
186+
return null;
187+
}
188+
189+
// 4. Return null if address in out of bounds (i.e., not exactly 16 bytes)
190+
if (byteList.size() != IPV6_BYTE_LENGTH) {
191+
LOG.info("Parsed IPv6 address byte length is incorrect. Expected " + IPV6_BYTE_LENGTH + ", got {}: {}", byteList.size(), ipv6Addr);
192+
return null;
193+
}
194+
195+
// Convert List<Byte> to byte[]
196+
byte[] result = new byte[IPV6_BYTE_LENGTH];
197+
for (int i = 0; i < byteList.size(); i++) {
198+
result[i] = byteList.get(i);
199+
}
200+
201+
return result;
202+
}
203+
204+
/**
205+
* Checks if a single IPv6 segment is valid.
206+
* A valid segment is a non-empty string of 1 to 4 hexadecimal characters.
207+
*
208+
* @param segment The segment string to validate.
209+
* @return true if the segment is valid, false otherwise.
210+
*/
211+
private static boolean isInvalidSegment(String segment) {
212+
if (segment == null || segment.isEmpty() || segment.length() > IPV6_SEGMENT_HEX_LENGTH) {
213+
return true;
214+
}
215+
// Check if all characters are valid hexadecimal digits
216+
for (char c : segment.toCharArray()) {
217+
if (!Character.isDigit(c) && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) {
218+
return true;
219+
}
220+
}
221+
return false;
222+
}
223+
84224
private void mask(byte[] b, int bits) {
85225
int start = bits / 8;
86226
int startMask = (1 << (8 - (bits % 8))) - 1;
@@ -93,6 +233,7 @@ private void mask(byte[] b, int bits) {
93233
}
94234

95235
public boolean matches(String id, String aclExpr) {
236+
LOG.trace("id: '{}' aclExpr: {}", id, aclExpr);
96237
String[] parts = aclExpr.split("/", 2);
97238
byte[] aclAddr = addr2Bytes(parts[0]);
98239
if (aclAddr == null) {
@@ -112,11 +253,13 @@ public boolean matches(String id, String aclExpr) {
112253
mask(aclAddr, bits);
113254
byte[] remoteAddr = addr2Bytes(id);
114255
if (remoteAddr == null) {
256+
LOG.info("Address is null");
115257
return false;
116258
}
117259
mask(remoteAddr, bits);
118260
for (int i = 0; i < remoteAddr.length; i++) {
119261
if (remoteAddr[i] != aclAddr[i]) {
262+
LOG.info("ACL Expr and id are not identical");
120263
return false;
121264
}
122265
}

zookeeper-server/src/test/java/org/apache/zookeeper/test/ACLTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,38 @@ public void testIPAuthenticationIsValidCIDR() throws Exception {
6969
assertFalse(prov.isValid("10.0.0.1/-1"), "testing netmask too low");
7070
}
7171

72+
@Test
73+
public void testIPAuthenticationIsValidIpv6CIDR() throws Exception {
74+
IPAuthenticationProvider prov = new IPAuthenticationProvider();
75+
assertTrue(prov.isValid("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), "full address no netmask");
76+
assertTrue(prov.isValid("2001:db8:85a3::8a2e:370:7334"), "compressed zeros");
77+
assertTrue(prov.isValid("::1"), "loopback with compression");
78+
assertTrue(prov.isValid("1::"), "Start with compression");
79+
assertTrue(prov.isValid("2001:db8::/4"), "end with compression");
80+
assertTrue(prov.isValid("0:0:0:0:0:0:0::/8"), "all zeros");
81+
assertTrue(prov.isValid("2001:db8:85a3:0:0:0:0::/32"), "Explicit zeros");
82+
assertTrue(prov.isValid("1234:5678:9abc:def0:1234:5678:9abc:def0"), "max hex value");
83+
assertFalse(prov.isValid("2001:db8:85a3:0000:0000:8a2e:0370:7334:extra"), "too many address segments");
84+
assertFalse(prov.isValid("2001:db8:85a3:0000:0000:8a2e:0370"), "too few address segments");
85+
assertFalse(prov.isValid("2001:db8:85a3::8a2e::0370:7334"), "multiple '::' not valid");
86+
assertFalse(prov.isValid("2001:db8:85a3:G::8a2e:0370:7334"), "Invalid hex character");
87+
assertFalse(prov.isValid(""), "empty string");
88+
assertFalse(prov.isValid("2001:db8:85a3:0:0:0:0:1:2"), "too many segments post compression");
89+
assertTrue(prov.isValid("2001:db8:85a3::8a2e:0370:7334:"), "trailing colon");
90+
assertFalse(prov.isValid(":2001:db8:85a3::8a2e:0370:7334"), "Leading colon");
91+
assertFalse(prov.isValid("::FFFF:192.168.1.1"), "IPv4-mapped");
92+
assertTrue(prov.isValid("2001:db8:1234::/64"), "IPv6 address for multiple clients");
93+
}
94+
95+
@Test
96+
public void testIPAuthenticationIsValidIpv6Mask() throws Exception {
97+
IPAuthenticationProvider prov = new IPAuthenticationProvider();
98+
assertTrue(prov.matches("2001:db8:1234::", "2001:db8:1234::/64"));
99+
assertTrue (prov.matches("2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370::/2"));
100+
assertFalse(prov.matches("22001:db8:85a3:0:0:0:0::0", "2001:db8:85a3:0:0:0:0::/32"));
101+
assertFalse(prov.matches("2001:db8::/4", "2001:db8::/4"));
102+
}
103+
72104
@Test
73105
public void testNettyIpAuthDefault(@TempDir File tmpDir) throws Exception {
74106
String HOSTPORT = "127.0.0.1:" + PortAssignment.unique();

0 commit comments

Comments
 (0)