This is an automated email from the ASF dual-hosted git repository.
szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git
The following commit(s) were added to refs/heads/master by this push:
new d7f71c7ad RATIS-2592. Ratis CLI and example peer-address parsers fail
on IPv6 literal addresses (#1509)
d7f71c7ad is described below
commit d7f71c7ad33b467e70c5dfb83afc39c41d7468b7
Author: Siyao Meng <[email protected]>
AuthorDate: Wed Jul 8 21:07:17 2026 -0700
RATIS-2592. Ratis CLI and example peer-address parsers fail on IPv6 literal
addresses (#1509)
---
.../ratis/examples/common/SubCommandBase.java | 77 +++++++++++++++++-----
.../ratis/examples/common/TestSubCommand.java | 43 ++++++++++++
.../java/org/apache/ratis/shell/cli/CliUtils.java | 10 +--
.../shell/cli/sh/local/RaftMetaConfCommand.java | 4 +-
.../org/apache/ratis/shell/cli/TestCliUtils.java | 64 ++++++++++++++++++
.../shell/cli/sh/LocalCommandIntegrationTest.java | 35 ++++++++++
6 files changed, 210 insertions(+), 23 deletions(-)
diff --git
a/ratis-examples/src/main/java/org/apache/ratis/examples/common/SubCommandBase.java
b/ratis-examples/src/main/java/org/apache/ratis/examples/common/SubCommandBase.java
index d650a5cfe..0755cbd8b 100644
---
a/ratis-examples/src/main/java/org/apache/ratis/examples/common/SubCommandBase.java
+++
b/ratis-examples/src/main/java/org/apache/ratis/examples/common/SubCommandBase.java
@@ -41,26 +41,67 @@ public abstract class SubCommandBase {
private String peers;
public static RaftPeer[] parsePeers(String peers) {
- return Stream.of(peers.split(",")).map(address -> {
- String[] addressParts = address.split(":");
- if (addressParts.length < 3) {
- throw new IllegalArgumentException(
- "Raft peer " + address + " is not a legitimate format. "
- + "(format:
name:host:port:dataStreamPort:clientPort:adminPort)");
+ return
Stream.of(peers.split(",")).map(SubCommandBase::parsePeer).toArray(RaftPeer[]::new);
+ }
+
+ /**
+ * Parse a single peer definition in the format
+ * {@code name:host:port:dataStreamPort:clientPort:adminPort}, where the
trailing
+ * ports are optional. The host may be an IPv6 literal enclosed in brackets,
+ * e.g. {@code n0:[::1]:9000:9001:9002:9003}.
+ */
+ private static RaftPeer parsePeer(String address) {
+ final int idEnd = address.indexOf(':');
+ if (idEnd < 0) {
+ throw illegalFormat(address);
+ }
+ final String id = address.substring(0, idEnd);
+ final String hostAndPorts = address.substring(idEnd + 1);
+
+ // Separate the host from the port list, honoring IPv6 bracketed literals.
+ final String host;
+ final String portList;
+ if (hostAndPorts.startsWith("[")) {
+ final int bracketEnd = hostAndPorts.indexOf("]:");
+ if (bracketEnd < 0) {
+ throw illegalFormat(address);
}
- RaftPeer.Builder builder = RaftPeer.newBuilder();
- builder.setId(addressParts[0]).setAddress(addressParts[1] + ":" +
addressParts[2]);
- if (addressParts.length >= 4) {
- builder.setDataStreamAddress(addressParts[1] + ":" + addressParts[3]);
- if (addressParts.length >= 5) {
- builder.setClientAddress(addressParts[1] + ":" + addressParts[4]);
- if (addressParts.length >= 6) {
- builder.setAdminAddress(addressParts[1] + ":" + addressParts[5]);
- }
- }
+ host = hostAndPorts.substring(0, bracketEnd + 1); // include the closing
']'
+ portList = hostAndPorts.substring(bracketEnd + 2);
+ } else {
+ final int hostEnd = hostAndPorts.indexOf(':');
+ if (hostEnd < 0) {
+ throw illegalFormat(address);
+ }
+ host = hostAndPorts.substring(0, hostEnd);
+ portList = hostAndPorts.substring(hostEnd + 1);
+ }
+
+ // Use -1 limit so trailing/embedded empty segments (e.g. "6000:" or
"6000::6002")
+ // are preserved and can be rejected rather than silently building "host:".
+ final String[] ports = portList.split(":", -1);
+ for (String port : ports) {
+ if (port.isEmpty()) {
+ throw illegalFormat(address);
}
- return builder.build();
- }).toArray(RaftPeer[]::new);
+ }
+ final RaftPeer.Builder builder = RaftPeer.newBuilder();
+ builder.setId(id).setAddress(host + ":" + ports[0]);
+ if (ports.length >= 2) {
+ builder.setDataStreamAddress(host + ":" + ports[1]);
+ }
+ if (ports.length >= 3) {
+ builder.setClientAddress(host + ":" + ports[2]);
+ }
+ if (ports.length >= 4) {
+ builder.setAdminAddress(host + ":" + ports[3]);
+ }
+ return builder.build();
+ }
+
+ private static IllegalArgumentException illegalFormat(String address) {
+ return new IllegalArgumentException("Raft peer " + address + " is not a
legitimate format. "
+ + "(format: name:host:port:dataStreamPort:clientPort:adminPort)");
}
public RaftPeer[] getPeers() {
diff --git
a/ratis-examples/src/test/java/org/apache/ratis/examples/common/TestSubCommand.java
b/ratis-examples/src/test/java/org/apache/ratis/examples/common/TestSubCommand.java
index c47dbb82a..59806b71b 100644
---
a/ratis-examples/src/test/java/org/apache/ratis/examples/common/TestSubCommand.java
+++
b/ratis-examples/src/test/java/org/apache/ratis/examples/common/TestSubCommand.java
@@ -21,7 +21,9 @@ package org.apache.ratis.examples.common;
import java.util.Collection;
import java.util.Collections;
+import org.apache.ratis.protocol.RaftPeer;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -38,4 +40,45 @@ public class TestSubCommand {
() -> SubCommandBase.parsePeers(peers));
}
+ @Test
+ public void testParseIpv4Peer() {
+ final RaftPeer[] peers =
SubCommandBase.parsePeers("n0:127.0.0.1:6000:6001:6002:6003");
+ Assertions.assertEquals(1, peers.length);
+ Assertions.assertEquals("n0", peers[0].getId().toString());
+ Assertions.assertEquals("127.0.0.1:6000", peers[0].getAddress());
+ Assertions.assertEquals("127.0.0.1:6001", peers[0].getDataStreamAddress());
+ Assertions.assertEquals("127.0.0.1:6002", peers[0].getClientAddress());
+ Assertions.assertEquals("127.0.0.1:6003", peers[0].getAdminAddress());
+ }
+
+ @Test
+ public void testParseIpv6Peer() {
+ final RaftPeer[] peers =
SubCommandBase.parsePeers("n0:[::1]:6000:6001:6002:6003");
+ Assertions.assertEquals(1, peers.length);
+ Assertions.assertEquals("n0", peers[0].getId().toString());
+ Assertions.assertEquals("[::1]:6000", peers[0].getAddress());
+ Assertions.assertEquals("[::1]:6001", peers[0].getDataStreamAddress());
+ Assertions.assertEquals("[::1]:6002", peers[0].getClientAddress());
+ Assertions.assertEquals("[::1]:6003", peers[0].getAdminAddress());
+ }
+
+ @Test
+ public void testParseMixedPeers() {
+ final RaftPeer[] peers =
SubCommandBase.parsePeers("n0:[::1]:6000,n1:127.0.0.1:6001");
+ Assertions.assertEquals(2, peers.length);
+ Assertions.assertEquals("[::1]:6000", peers[0].getAddress());
+ Assertions.assertEquals("127.0.0.1:6001", peers[1].getAddress());
+ }
+
+ @Test
+ public void testParseEmptyEmbeddedPortRejected() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> SubCommandBase.parsePeers("n0:127.0.0.1:6000::6002"));
+ }
+
+ @Test
+ public void testParseEmptyTrailingPortRejected() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> SubCommandBase.parsePeers("n0:[::1]:6000:"));
+ }
}
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/CliUtils.java
b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/CliUtils.java
index 1cecc665c..a4a30ae77 100644
--- a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/CliUtils.java
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/CliUtils.java
@@ -24,6 +24,7 @@ import org.apache.ratis.protocol.RaftGroupId;
import org.apache.ratis.protocol.RaftPeer;
import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.protocol.exceptions.RaftException;
+import org.apache.ratis.util.NetUtils;
import org.apache.ratis.util.function.CheckedFunction;
import java.io.IOException;
@@ -164,11 +165,12 @@ public final class CliUtils {
/** Parse the given string as a {@link InetSocketAddress}. */
public static InetSocketAddress parseInetSocketAddress(String address) {
try {
- final String[] hostPortPair = address.split(":");
- if (hostPortPair.length < 2) {
- throw new IllegalArgumentException("Unexpected address format
<HOST:PORT>.");
+ // NetUtils.createSocketAddr also accepts scheme://host:port; the shell
only
+ // expects <HOST:PORT>, so reject a scheme to avoid masking invalid
input.
+ if (address.contains("://")) {
+ throw new IllegalArgumentException("Unexpected scheme in \"" + address
+ "\"; expected format <HOST:PORT>.");
}
- return new InetSocketAddress(hostPortPair[0],
Integer.parseInt(hostPortPair[1]));
+ return NetUtils.createSocketAddr(address);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to parse the server address
parameter \"" + address + "\".", e);
}
diff --git
a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/local/RaftMetaConfCommand.java
b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/local/RaftMetaConfCommand.java
index a63b65937..fd1ef78e6 100644
---
a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/local/RaftMetaConfCommand.java
+++
b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/local/RaftMetaConfCommand.java
@@ -29,6 +29,7 @@ import org.apache.ratis.shell.cli.CliUtils;
import org.apache.ratis.shell.cli.sh.command.AbstractCommand;
import org.apache.ratis.shell.cli.sh.command.Context;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+import org.apache.ratis.util.NetUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -91,7 +92,8 @@ public class RaftMetaConfCommand extends AbstractCommand {
}
InetSocketAddress inetSocketAddress = CliUtils.parseInetSocketAddress(
peerIdWithAddressArray[peerIdWithAddressArray.length - 1]);
- String addressString = inetSocketAddress.getHostString() + ":" +
inetSocketAddress.getPort();
+ // Use address2String so that IPv6 literals are surrounded with '[', ']'.
+ String addressString = NetUtils.address2String(inetSocketAddress);
if (addresses.contains(addressString)) {
printf("Found duplicated address: %s. Please make sure the address of
peer have no duplicated value.",
addressString);
diff --git
a/ratis-test/src/test/java/org/apache/ratis/shell/cli/TestCliUtils.java
b/ratis-test/src/test/java/org/apache/ratis/shell/cli/TestCliUtils.java
new file mode 100644
index 000000000..151914b64
--- /dev/null
+++ b/ratis-test/src/test/java/org/apache/ratis/shell/cli/TestCliUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ratis.shell.cli;
+
+import org.apache.ratis.protocol.RaftPeer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+public class TestCliUtils {
+
+ @Test
+ public void testParseIpv4Address() {
+ final InetSocketAddress addr =
CliUtils.parseInetSocketAddress("127.0.0.1:6000");
+ Assertions.assertEquals(6000, addr.getPort());
+ Assertions.assertTrue(addr.getAddress() instanceof Inet4Address);
+ }
+
+ @Test
+ public void testParseIpv6Address() {
+ final InetSocketAddress addr =
CliUtils.parseInetSocketAddress("[::1]:6000");
+ Assertions.assertEquals(6000, addr.getPort());
+ Assertions.assertTrue(addr.getAddress() instanceof Inet6Address);
+ }
+
+ @Test
+ public void testParseIpv6Peers() {
+ final List<RaftPeer> peers =
CliUtils.parseRaftPeers("[::1]:6000,[::1]:6001");
+ Assertions.assertEquals(2, peers.size());
+ Assertions.assertEquals(6000,
CliUtils.parseInetSocketAddress(peers.get(0).getAddress()).getPort());
+ Assertions.assertEquals(6001,
CliUtils.parseInetSocketAddress(peers.get(1).getAddress()).getPort());
+ }
+
+ @Test
+ public void testParseMissingPort() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> CliUtils.parseInetSocketAddress("127.0.0.1"));
+ }
+
+ @Test
+ public void testParseRejectsScheme() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> CliUtils.parseInetSocketAddress("http://127.0.0.1:6000"));
+ }
+}
diff --git
a/ratis-test/src/test/java/org/apache/ratis/shell/cli/sh/LocalCommandIntegrationTest.java
b/ratis-test/src/test/java/org/apache/ratis/shell/cli/sh/LocalCommandIntegrationTest.java
index afc13837c..b4c1cd4cc 100644
---
a/ratis-test/src/test/java/org/apache/ratis/shell/cli/sh/LocalCommandIntegrationTest.java
+++
b/ratis-test/src/test/java/org/apache/ratis/shell/cli/sh/LocalCommandIntegrationTest.java
@@ -22,6 +22,7 @@ import org.apache.ratis.proto.RaftProtos.LogEntryProto;
import org.apache.ratis.proto.RaftProtos.RaftConfigurationProto;
import org.apache.ratis.proto.RaftProtos.RaftPeerProto;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+import org.apache.ratis.util.NetUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -29,6 +30,8 @@ import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -121,6 +124,38 @@ public class LocalCommandIntegrationTest {
}
+ @Test
+ public void testRunMethodWithIpv6(@TempDir Path tempDir) throws Exception {
+ final int index = 1;
+ generateRaftConf(tempDir.resolve(RAFT_META_CONF), index);
+
+ // IPv6 literals must be enclosed in brackets, e.g. [2001:db8::1]:9872.
+ final String peersListStr =
"peer1_ID|[2001:db8::1]:9872,peer2_ID|[2001:db8::2]:9873";
+ StringPrintStream out = new StringPrintStream();
+ RatisShell shell = new RatisShell(out.getPrintStream());
+ int ret = shell.run("local", "raftMetaConf", "-peers", peersListStr,
"-path", tempDir.toString());
+ Assertions.assertEquals(0, ret);
+
+ final List<RaftPeerProto> peers;
+ try (InputStream in =
Files.newInputStream(tempDir.resolve(NEW_RAFT_META_CONF))) {
+ peers = LogEntryProto.newBuilder().mergeFrom(in).build()
+ .getConfigurationEntry().getPeersList();
+ }
+ Assertions.assertEquals(2, peers.size());
+
+ // Each written address must stay bracketed and round-trip to an IPv6
address with the same port.
+ final int[] expectedPorts = {9872, 9873};
+ for (int i = 0; i < peers.size(); i++) {
+ final String address = peers.get(i).getAddress();
+ Assertions.assertTrue(address.startsWith("[") && address.contains("]:"),
+ () -> "IPv6 address is not bracketed: " + address);
+ final InetSocketAddress parsed = NetUtils.createSocketAddr(address);
+ Assertions.assertTrue(parsed.getAddress() instanceof Inet6Address,
+ () -> "Not an IPv6 address: " + address);
+ Assertions.assertEquals(expectedPorts[i], parsed.getPort());
+ }
+ }
+
private void generateRaftConf(Path path, int index) throws IOException {
Map<String, String> map = new HashMap<>();
map.put("peer1_ID", "host1:9872");