This is an automated email from the ASF dual-hosted git repository. kezhuw pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/master by this push: new 641cf0085 ZOOKEEPER-4240: Add IPV6 support for ZooKeeper ACL 641cf0085 is described below commit 641cf0085d0bb84ed8f23214d1c2ce201c8f7f11 Author: Abhishek Kothalikar <99398985+kabhish...@users.noreply.github.com> AuthorDate: Tue Aug 19 23:29:36 2025 +0530 ZOOKEEPER-4240: Add IPV6 support for ZooKeeper ACL Reviewers: anmolnar, anmolnar, kezhuw Author: kabhishek4 Closes #2280 from kabhishek4/ZOOKEEPER-4240 --- .../server/auth/IPAuthenticationProvider.java | 104 ++++++++++++- .../server/auth/IPAuthenticationProviderTest.java | 90 +++++++++++ .../java/org/apache/zookeeper/test/ACLTest.java | 164 +++++++++++++++++++++ 3 files changed, 355 insertions(+), 3 deletions(-) diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/IPAuthenticationProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/IPAuthenticationProvider.java index 26c14a4e8..0b4ef9a70 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/IPAuthenticationProvider.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/IPAuthenticationProvider.java @@ -22,15 +22,26 @@ import java.util.Collections; import java.util.List; import java.util.StringTokenizer; +import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.data.Id; import org.apache.zookeeper.server.ServerCnxn; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IPAuthenticationProvider implements AuthenticationProvider { + private static final Logger LOG = LoggerFactory.getLogger(IPAuthenticationProvider.class); public static final String X_FORWARDED_FOR_HEADER_NAME = "X-Forwarded-For"; public static final String USE_X_FORWARDED_FOR_KEY = "zookeeper.IPAuthenticationProvider.usexforwardedfor"; + private static final int IPV6_BYTE_LENGTH = 16; // IPv6 address is 128 bits = 16 bytes + private static final int IPV6_SEGMENT_COUNT = 8; // IPv6 address has 8 segments + private static final int IPV6_SEGMENT_BYTE_LENGTH = 2; // Each segment has up to two bytes + private static final int IPV6_SEGMENT_HEX_LENGTH = 4; // Each segment has up to 4 hex digits + + private static final Pattern IPV6_PATTERN = Pattern.compile(":"); + private static final Pattern IPV4_PATTERN = Pattern.compile("\\."); public String getScheme() { return "ip"; @@ -55,9 +66,14 @@ public List<Id> handleAuthentication(HttpServletRequest request, byte[] authData // This is a bit weird but we need to return the address and the number of // bytes (to distinguish between IPv4 and IPv6 private byte[] addr2Bytes(String addr) { - byte[] b = v4addr2Bytes(addr); - // TODO Write the v6addr2Bytes - return b; + if (IPV6_PATTERN.matcher(addr).find()) { + return v6addr2Bytes(addr); + } else if (IPV4_PATTERN.matcher(addr).find()) { + return v4addr2Bytes(addr); + } else { + LOG.warn("Input string does not resemble an IPv4 or IPv6 address: {}", addr); + return null; + } } private byte[] v4addr2Bytes(String addr) { @@ -81,6 +97,83 @@ private byte[] v4addr2Bytes(String addr) { return b; } + /** + * Validates an IPv6 address string and converts it into a byte array. + * + * @param ipv6Addr The IPv6 address string to validate. + * @return A byte array representing the IPv6 address if valid, or null if the address + * is invalid or cannot be parsed. + */ + static byte[] v6addr2Bytes(String ipv6Addr) { + try { + return parseV6addr(ipv6Addr); + } catch (IllegalArgumentException e) { + LOG.warn("Fail to parse {} as IPv6 address: {}", ipv6Addr, e.getMessage()); + return null; + } + } + + static byte[] parseV6addr(String ipv6Addr) { + // Split the address by "::" to handle zero compression, -1 to keep trailing empty strings + String[] parts = ipv6Addr.split("::", -1); + + String[] segments1 = new String[0]; + String[] segments2 = new String[0]; + + // Case 1: No "::" (full address) + if (parts.length == 1) { + segments1 = parts[0].split(":", -1); + if (segments1.length != IPV6_SEGMENT_COUNT) { + String reason = "wrong number of segments"; + throw new IllegalArgumentException(reason); + } + } else if (parts.length == 2) { + // Case 2: "::" is present + // Handle cases like "::1" or "1::" + if (!parts[0].isEmpty()) { + segments1 = parts[0].split(":", -1); + } + if (!parts[1].isEmpty()) { + segments2 = parts[1].split(":", -1); + } + + // Check if the total number of explicit segments exceeds 8 + if (segments1.length + segments2.length >= IPV6_SEGMENT_COUNT) { + String reason = "too many segments"; + throw new IllegalArgumentException(reason); + } + } else { + // Case 3: Invalid number of parts after splitting by "::" (should be 1 or 2) + String reason = "too many '::'"; + throw new IllegalArgumentException(reason); + } + + byte[] result = new byte[IPV6_BYTE_LENGTH]; + // Process segments before "::" + parseV6Segment(result, 0, segments1); + // Process segments after "::" + parseV6Segment(result, IPV6_BYTE_LENGTH - segments2.length * IPV6_SEGMENT_BYTE_LENGTH, segments2); + + return result; + } + + private static void parseV6Segment(byte[] addr, int i, String[] segments) { + for (String segment : segments) { + if (segment.isEmpty()) { + throw new IllegalArgumentException("empty segment"); + } else if (segment.length() > IPV6_SEGMENT_HEX_LENGTH) { + throw new IllegalArgumentException("segment too long"); + } + try { + int value = Integer.parseInt(segment, 16); + addr[i++] = (byte) ((value >> 8) & 0xFF); + addr[i++] = (byte) (value & 0xFF); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid hexadecimal characters in segment: " + segment); + } + } + } + private void mask(byte[] b, int bits) { int start = bits / 8; int startMask = (1 << (8 - (bits % 8))) - 1; @@ -93,6 +186,7 @@ private void mask(byte[] b, int bits) { } public boolean matches(String id, String aclExpr) { + LOG.trace("id: '{}' aclExpr: {}", id, aclExpr); String[] parts = aclExpr.split("/", 2); byte[] aclAddr = addr2Bytes(parts[0]); if (aclAddr == null) { @@ -115,6 +209,10 @@ public boolean matches(String id, String aclExpr) { return false; } mask(remoteAddr, bits); + // Check if id and acl expression are of different formats (ipv6 or iv4) return false + if (remoteAddr.length != aclAddr.length) { + return false; + } for (int i = 0; i < remoteAddr.length; i++) { if (remoteAddr[i] != aclAddr[i]) { return false; diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/auth/IPAuthenticationProviderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/auth/IPAuthenticationProviderTest.java index c1a1d1f52..1586cac39 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/auth/IPAuthenticationProviderTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/auth/IPAuthenticationProviderTest.java @@ -19,13 +19,23 @@ import static org.apache.zookeeper.server.auth.IPAuthenticationProvider.USE_X_FORWARDED_FOR_KEY; import static org.apache.zookeeper.server.auth.IPAuthenticationProvider.X_FORWARDED_FOR_HEADER_NAME; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class IPAuthenticationProviderTest { @@ -96,4 +106,84 @@ public void testGetClientIPAddressMissingXForwardedFor() { // Assert assertEquals("192.168.1.1", clientIp); } + + @Test + public void testParsingOfIPv6Address() { + //Full IPv6 address + String ipv6Full = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + byte[] expectedFull = { + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0x85, (byte) 0xa3, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x8a, (byte) 0x2e, + (byte) 0x03, (byte) 0x70, (byte) 0x73, (byte) 0x34 + }; + byte[] actualFull = IPAuthenticationProvider.v6addr2Bytes(ipv6Full); + assertNotNull(actualFull, "Full IPv6 address should not return null"); + assertArrayEquals(expectedFull, actualFull, "Full IPv6 address conversion mismatch"); + + //Compressed IPv6 address (double colon) + String ipv6Compressed = "2001:db8::8a2e:370:7334"; + byte[] expectedCompressed = { + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x8a, (byte) 0x2e, + (byte) 0x03, (byte) 0x70, (byte) 0x73, (byte) 0x34 + }; + byte[] actualCompressed = IPAuthenticationProvider.v6addr2Bytes(ipv6Compressed); + assertNotNull(actualCompressed, "Compressed IPv6 address should not return null"); + assertArrayEquals(expectedCompressed, actualCompressed, "Compressed IPv6 address conversion mismatch"); + + //Shortened IPv6 address + String ipv6Shortened = "2001:db8::1"; + byte[] expectedShortened = { + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 + }; + byte[] actualShortened = IPAuthenticationProvider.v6addr2Bytes(ipv6Shortened); + assertNotNull(actualShortened, "Shortened IPv6 address should not return null"); + assertArrayEquals(expectedShortened, actualShortened, "Shortened IPv6 address conversion mismatch"); + + //Loopback address + String ipv6Loopback = "::1"; + byte[] expectedLoopback = { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01 + }; + byte[] actualLoopback = IPAuthenticationProvider.v6addr2Bytes(ipv6Loopback); + assertNotNull(actualLoopback, "Loopback IPv6 address should not return null"); + assertArrayEquals(expectedLoopback, actualLoopback, "Loopback IPv6 address conversion mismatch"); + } + + private static Stream<Arguments> invalidIPv6Addresses() { + return Stream.of( + Arguments.of("1", "wrong number of segments"), + Arguments.of("1:2", "wrong number of segments"), + Arguments.of("1::2:", "empty segment"), + Arguments.of(":1::2:", "empty segment"), + Arguments.of("1:2:3:4:5:6:7:8:", "wrong number of segments"), + Arguments.of("1:2:3:4:5:6:7:8:9", "wrong number of segments"), + Arguments.of("1:2::3:4:5:6:7:8", "too many segments"), + Arguments.of("1::2::", "too many '::'"), + Arguments.of("1:abcdf::", "segment too long"), + Arguments.of("efgh::", "invalid hexadecimal characters in segment"), + Arguments.of("1:: ", "invalid hexadecimal characters in segment"), + Arguments.of(" 1::", "invalid hexadecimal characters in segment") + ); + } + + @ParameterizedTest(name = "address = \"{0}\"") + @MethodSource("invalidIPv6Addresses") + public void testParsingOfInvalidIPv6Address(String ipv6Address, String expectedMessage) { + try { + IPAuthenticationProvider.parseV6addr(ipv6Address); + fail("expect failure"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString(expectedMessage)); + } + assertNull(IPAuthenticationProvider.v6addr2Bytes(ipv6Address)); + } } diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ACLTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ACLTest.java index e66592e49..d1f7c00de 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ACLTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ACLTest.java @@ -23,11 +23,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Properties; import java.util.concurrent.CountDownLatch; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; @@ -39,6 +43,7 @@ import org.apache.zookeeper.ZKTestCase; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooDefs.Ids; +import org.apache.zookeeper.ZooDefs.Perms; import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Id; @@ -48,6 +53,7 @@ import org.apache.zookeeper.server.SyncRequestProcessor; import org.apache.zookeeper.server.ZooKeeperServer; import org.apache.zookeeper.server.auth.IPAuthenticationProvider; +import org.apache.zookeeper.server.embedded.ZooKeeperServerEmbedded; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; @@ -69,6 +75,38 @@ public void testIPAuthenticationIsValidCIDR() throws Exception { assertFalse(prov.isValid("10.0.0.1/-1"), "testing netmask too low"); } + @Test + public void testIPAuthenticationIsValidIpv6CIDR() throws Exception { + IPAuthenticationProvider prov = new IPAuthenticationProvider(); + assertTrue(prov.isValid("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), "full address no netmask"); + assertTrue(prov.isValid("2001:db8:85a3::8a2e:370:7334"), "compressed zeros"); + assertTrue(prov.isValid("::1"), "loopback with compression"); + assertTrue(prov.isValid("1::"), "Start with compression"); + assertTrue(prov.isValid("2001:db8::/4"), "end with compression"); + assertTrue(prov.isValid("0:0:0:0:0:0:0::/8"), "all zeros"); + assertTrue(prov.isValid("2001:db8:85a3:0:0:0:0::/32"), "Explicit zeros"); + assertTrue(prov.isValid("1234:5678:9abc:def0:1234:5678:9abc:def0"), "max hex value"); + assertFalse(prov.isValid("2001:db8:85a3:0000:0000:8a2e:0370:7334:extra"), "too many address segments"); + assertFalse(prov.isValid("2001:db8:85a3:0000:0000:8a2e:0370"), "too few address segments"); + assertFalse(prov.isValid("2001:db8:85a3::8a2e::0370:7334"), "multiple '::' not valid"); + assertFalse(prov.isValid("2001:db8:85a3:G::8a2e:0370:7334"), "Invalid hex character"); + assertFalse(prov.isValid(""), "empty string"); + assertFalse(prov.isValid("2001:db8:85a3:0:0:0:0:1:2"), "too many segments post compression"); + assertFalse(prov.isValid("2001:db8:85a3::8a2e:0370:7334:"), "trailing colon"); + assertFalse(prov.isValid(":2001:db8:85a3::8a2e:0370:7334"), "Leading colon"); + assertFalse(prov.isValid("::FFFF:192.168.1.1"), "IPv4-mapped"); + assertTrue(prov.isValid("2001:db8:1234::/64"), "IPv6 address for multiple clients"); + } + + @Test + public void testIPAuthenticationIsValidIpv6Mask() throws Exception { + IPAuthenticationProvider prov = new IPAuthenticationProvider(); + assertTrue(prov.matches("2001:db8:1234::", "2001:db8:1234::/64")); + assertTrue(prov.matches("2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370::/2")); + assertFalse(prov.matches("22001:db8:85a3:0:0:0:0::0", "2001:db8:85a3:0:0:0:0::/32")); + assertFalse(prov.matches("2001:db8::/4", "2001:db8::/4")); + } + @Test public void testNettyIpAuthDefault(@TempDir File tmpDir) throws Exception { String HOSTPORT = "127.0.0.1:" + PortAssignment.unique(); @@ -101,6 +139,132 @@ public void testNettyIpAuthDefault(@TempDir File tmpDir) throws Exception { } } + @Test + public void testAuthWithIPV6Server(@TempDir File tmpDir) throws Exception { + Properties properties = new Properties(); + properties.setProperty("clientPortAddress", "::1"); + properties.setProperty("clientPort", "0"); + + ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded.builder() + .baseDir(tmpDir.toPath()) + .configuration(properties) + .build(); + server.start(); + + String connectionString = server.getConnectionString(); + String port = connectionString.substring(connectionString.lastIndexOf(':') + 1); + + String hostport = String.format("[::1]:%s", port); + assertTrue(ClientBase.waitForServerUp(hostport, CONNECTION_TIMEOUT), "waiting for server being up"); + + // given: ipv6 client + ZooKeeper zk = ClientBase.createZKClient(hostport); + + // when: invalid ipv6 network acl + // then: InvalidACL + assertThrows(KeeperException.InvalidACLException.class, () -> + zk.create("/invalid-ipv6-network-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "::1/256"))), CreateMode.PERSISTENT)); + + // given: ipv4 network acl + zk.create("/unmatched-ipv4-network-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "127.0.0.1/16"))), CreateMode.PERSISTENT); + // when: access with v6 ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/unmatched-ipv4-network-acl", null, -1)); + + // given: prefix matched ipv4 acl + zk.create("/prefix-matched-ipv4-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "0.0.0.1/16"))), CreateMode.PERSISTENT); + // when: access with v6 ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/prefix-matched-ipv4-acl", null, -1)); + + // given: ipv6 with network acl + zk.create("/ipv6-network-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "::1/64"))), CreateMode.PERSISTENT); + // when: access with valid ip + // then: ok + zk.setData("/ipv6-network-acl", null, -1); + + // given: ipv6 acl + zk.create("/ipv6-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "::1"))), CreateMode.PERSISTENT); + // when: access with valid ip + // then: ok + zk.setData("/ipv6-acl", null, -1); + + // given: mismatched ipv6 with network acl + zk.create("/mismatched-ipv6-network-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "0000:0001::/32"))), CreateMode.PERSISTENT); + // when: access with invalid ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/mismatched-ipv6-network-acl", null, -1)); + + // given: mismatched ipv6 acl + zk.create("/mismatched-ipv6-acl", null, Collections.singletonList(new ACL(Perms.ALL, new Id("ip", "::2"))), CreateMode.PERSISTENT); + // when: access with invalid ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/mismatched-ipv6-acl", null, -1)); + + server.close(); + } + + @Test + public void testAuthWithIPV4Server(@TempDir File tmpDir) throws Exception { + Properties properties = new Properties(); + properties.setProperty("clientPortAddress", "127.0.0.1"); + properties.setProperty("clientPort", "0"); + + ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded.builder() + .baseDir(tmpDir.toPath()) + .configuration(properties) + .build(); + server.start(); + + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), CONNECTION_TIMEOUT), "waiting for server being up"); + + // given: ipv4 client + ZooKeeper zk = ClientBase.createZKClient(server.getConnectionString()); + + // when: invalid ipv4 network acl + // then: InvalidACL + assertThrows(KeeperException.InvalidACLException.class, () -> + zk.create("/invalid-ipv4-network-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "127.0.0.1/64"))), CreateMode.PERSISTENT)); + + // given: ipv6 acl + zk.create("/mismatched-ipv6-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "::1"))), CreateMode.PERSISTENT); + // when: access with v4 ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/mismatched-ipv6-acl", null, -1)); + + // given: prefix matched ipv6 network acl + zk.create("/prefix-matched-ipv6-network-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "7f::/16"))), CreateMode.PERSISTENT); + // when: access with v4 ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/prefix-matched-ipv6-network-acl", null, -1)); + + // given: ipv4 with network acl + zk.create("/matched-ipv4-network-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "127.0.0.1/16"))), CreateMode.PERSISTENT); + // when: access with valid ip + // then: ok + zk.setData("/matched-ipv4-network-acl", null, -1); + + // given: matched ipv4 acl + zk.create("/matched-ipv4-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "127.0.0.1"))), CreateMode.PERSISTENT); + // when: access with valid ip + // then: ok + zk.setData("/matched-ipv4-acl", null, -1); + + // given: mismatched ipv4 network acl + zk.create("/mismatched-ipv4-network-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "192.168.0.2/16"))), CreateMode.PERSISTENT); + // when: access with invalid ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/mismatched-ipv4-network-acl", null, -1)); + + // given: mismatched ipv4 acl + zk.create("/mismatched-ipv4-acl", new byte[]{}, Arrays.asList(new ACL(Perms.ALL, new Id("ip", "127.0.0.2"))), CreateMode.PERSISTENT); + // when: access with invalid ip + // then: NoAuth + assertThrows(KeeperException.NoAuthException.class, () -> zk.setData("/mismatched-ipv4-acl", null, -1)); + + server.close(); + } + @Test public void testDisconnectedAddAuth(@TempDir File tmpDir) throws Exception { ClientBase.setupTestEnv();