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");

Reply via email to