This is an automated email from the ASF dual-hosted git repository.
sarankk pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git
The following commit(s) were added to refs/heads/trunk by this push:
new 37735805c CASSSIDECAR-503: Fix IPV6 regex to recognize bracketed
addresses in gossip info host headers (#388)
37735805c is described below
commit 37735805cf4250f31309864ae69a92cbdfc84883
Author: Saranya Krishnakumar <[email protected]>
AuthorDate: Thu Oct 1 16:04:42 2026 -0700
CASSSIDECAR-503: Fix IPV6 regex to recognize bracketed addresses in gossip
info host headers (#388)
Patch by Saranya Krishnakumar; Reviewed by Francisco Guerrero, Yifan Cai
for CASSSIDECAR-503
---
CHANGES.txt | 1 +
.../common/server/utils/GossipInfoParser.java | 13 +-
.../common/server/utils/GossipInfoParserTest.java | 53 +++++
.../ClusterLeaseElectorateIPv6Test.java | 225 +++++++++++++++++++++
4 files changed, 289 insertions(+), 3 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index f3e2203f5..255a597d7 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
0.5.0
-----
+ * Fix IPV6 regex to recognize bracketed addresses in gossip info host headers
(CASSSIDECAR-503)
* CDC batch-write mixing a CDC-enabled and CDC-disabled table drops the CDC
table's mutation (CASSSIDECAR-499)
* Disable Netty wire logging on the HTTP server (CASSSIDECAR-496)
* Tighten path containment in Live Migration file route (CASSSIDECAR-479)
diff --git
a/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParser.java
b/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParser.java
index a89b40be5..b26a906fb 100644
---
a/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParser.java
+++
b/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParser.java
@@ -36,10 +36,15 @@ public class GossipInfoParser
{
} // util, do not instantiate
- private static final String IPV4_PATTERN = "(?:[0-9]{1,3}.){3}[0-9]{1,3}";
+ private static final String IPV4_PATTERN =
"(?:[0-9]{1,3}\\.){3}[0-9]{1,3}";
private static final String IPV6_PATTERN =
"(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}";
+ // Cassandra wraps an IPv6 literal in square brackets before appending the
port, so that the address and
+ // the port can be told apart, e.g. '/[2001:db8:0:0:0:0:0:1]:7000'. The
brackets are absent when the
+ // gossip info is retrieved without ports. See
org.apache.cassandra.locator.InetAddressAndPort#toString(InetAddress, int)
+ private static final String BRACKETED_IPV6_PATTERN =
String.format("\\[%s\\]", IPV6_PATTERN);
// Pattern text matching both IPv4 and IPv6
- private static final String IP_PATTERN = String.format("((%s)|(%s))",
IPV4_PATTERN, IPV6_PATTERN);
+ private static final String IP_PATTERN = String.format("((%s)|(%s)|(%s))",
+ IPV4_PATTERN,
BRACKETED_IPV6_PATTERN, IPV6_PATTERN);
// Pattern text matching IP with port optionally present.
// Technically, [0-9]{1,5} represents a larger range for valid ports. It
is fine for parsing the gossip info output.
private static final String IP_WITH_PORT_PATTERN =
String.format("%s(:[0-9]{1,5})?", IP_PATTERN);
@@ -78,7 +83,9 @@ public class GossipInfoParser
}
else
{
- assert gossipInfo != null; // the host line appears before the
rest. gossipInfo map must be initialized
+ Preconditions.checkState(gossipInfo != null,
+ "Expected a gossip host header before
the gossip fields. " +
+ "Unrecognized line: %s", line);
final List<String> splitLine =
GOSSIP_INFO_FIELD_SPLITTER.splitToList(line);
Preconditions.checkState(splitLine.size() == 2 ||
splitLine.size() == 3,
"A gossip field should be split into
two or three parts. %s",
diff --git
a/server-common/src/test/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParserTest.java
b/server-common/src/test/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParserTest.java
index b9c53e6f5..c2a3a08e8 100644
---
a/server-common/src/test/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParserTest.java
+++
b/server-common/src/test/java/org/apache/cassandra/sidecar/common/server/utils/GossipInfoParserTest.java
@@ -62,6 +62,36 @@ class GossipInfoParserTest
.isFalse();
}
+ @Test
+ void testIpv6GossipInfoHostHeaderCheck()
+ {
+
assertThat(GossipInfoParser.isGossipInfoHostHeader("/[0:0:0:0:0:0:0:1]:7000"))
+ .as("should parse format /[IPV6]:PORT")
+ .isTrue();
+
assertThat(GossipInfoParser.isGossipInfoHostHeader("optional_hostname/[fe80:0:0:0:0:0:0:1]:7000"))
+ .as("should parse format HOSTNAME/[IPV6]:PORT")
+ .isTrue();
+ assertThat(GossipInfoParser.isGossipInfoHostHeader("/0:0:0:0:0:0:0:1"))
+ .as("should parse format /IPV6")
+ .isTrue();
+ }
+
+ @Test
+ void testParseIpv6GossipInfo()
+ {
+ GossipInfoResponse result =
GossipInfoParser.parse(SAMPLE_IPV6_GOSSIP_INFO);
+ assertThat(result).containsKey("/[0:0:0:0:0:0:0:1]:7000")
+ .containsKey("localhost2/[0:0:0:0:0:0:0:2]:7000")
+ .containsKey("/[fe80:0:0:0:0:0:0:1]:7000");
+ GossipInfoResponse.GossipInfo gossipInfo =
result.get("[0:0:0:0:0:0:0:1]:7000");
+ assertThat(gossipInfo).isNotNull();
+ assertThat(gossipInfo.generation()).isEqualTo("1668100877");
+ assertThat(gossipInfo.heartbeat()).isEqualTo("242");
+
assertThat(gossipInfo.statusWithPort()).isEqualTo("NORMAL,-9223372036854775808");
+ assertThat(result.get("[fe80:0:0:0:0:0:0:1]:7000").statusWithPort())
+ .isEqualTo("BOOT_REPLACE,-3074457345618258603");
+ }
+
private static final String SAMPLE_GOSSIP_INFO =
"/127.0.0.3\n" +
" generation:1668100877\n" +
@@ -85,4 +115,27 @@ class GossipInfoParserTest
" SSTABLE_VERSIONS:6:big-nb\n" +
" TOKENS:18:<hidden>\n" +
" INDEX_STATUS:2198:{\"ks.tbl_idx\":\"BUILD_SUCCEEDED\"}";
+
+ private static final String SAMPLE_IPV6_GOSSIP_INFO =
+ "/[fe80:0:0:0:0:0:0:1]:7000\n" +
+ " generation:1668100877\n" +
+ " heartbeat:248\n" +
+ " LOAD:217:88883.0\n" +
+ " STATUS_WITH_PORT:71:BOOT_REPLACE,-3074457345618258603\n" +
+ " SSTABLE_VERSIONS:6:big-nb\n" +
+ " TOKENS:70:<hidden>\n" +
+ "localhost2/[0:0:0:0:0:0:0:2]:7000\n" +
+ " generation:1668100877\n" +
+ " heartbeat:243\n" +
+ " LOAD:211:83702.0\n" +
+ " STATUS_WITH_PORT:19:NORMAL,-3074457345618258603\n" +
+ " SSTABLE_VERSIONS:6:big-nb\n" +
+ " TOKENS:18:<hidden>\n" +
+ "/[0:0:0:0:0:0:0:1]:7000\n" +
+ " generation:1668100877\n" +
+ " heartbeat:242\n" +
+ " LOAD:211:88971.0\n" +
+ " STATUS_WITH_PORT:19:NORMAL,-9223372036854775808\n" +
+ " SSTABLE_VERSIONS:6:big-nb\n" +
+ " TOKENS:18:<hidden>";
}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/coordination/ClusterLeaseElectorateIPv6Test.java
b/server/src/test/java/org/apache/cassandra/sidecar/coordination/ClusterLeaseElectorateIPv6Test.java
new file mode 100644
index 000000000..ad1c6101f
--- /dev/null
+++
b/server/src/test/java/org/apache/cassandra/sidecar/coordination/ClusterLeaseElectorateIPv6Test.java
@@ -0,0 +1,225 @@
+/*
+ * 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.cassandra.sidecar.coordination;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import org.apache.cassandra.sidecar.adapters.base.CassandraStorageOperations;
+import
org.apache.cassandra.sidecar.adapters.base.jmx.ClusterMembershipJmxOperations;
+import
org.apache.cassandra.sidecar.adapters.base.jmx.EndpointSnitchJmxOperations;
+import org.apache.cassandra.sidecar.adapters.base.jmx.StorageJmxOperations;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.response.NodeSettings;
+import org.apache.cassandra.sidecar.common.server.JmxClient;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.common.server.dns.DnsResolvers;
+import org.apache.cassandra.sidecar.common.server.utils.GossipInfoParser;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+
+import static
org.apache.cassandra.sidecar.adapters.base.jmx.ClusterMembershipJmxOperations.FAILURE_DETECTOR_OBJ_NAME;
+import static
org.apache.cassandra.sidecar.adapters.base.jmx.EndpointSnitchJmxOperations.ENDPOINT_SNITCH_INFO_OBJ_NAME;
+import static
org.apache.cassandra.sidecar.adapters.base.jmx.StorageJmxOperations.STORAGE_SERVICE_OBJ_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the cluster lease electorate for nodes with IPV6 address.
+ */
+class ClusterLeaseElectorateIPv6Test
+{
+ private static final String DATACENTER = "datacenter1";
+ private static final String PARTITIONER =
"org.apache.cassandra.dht.Murmur3Partitioner";
+ private static final List<String> TOKEN_RANGE_OWNING_TOKEN_ZERO =
Arrays.asList("-100", "100");
+
+ static Stream<Arguments> clusters() throws UnknownHostException
+ {
+ return Stream.of(Arguments.of(new Fixture("IPv4",
+ new
InetSocketAddress(InetAddress.getByName("127.0.0.1"), 7000),
+
Arrays.asList("127.0.0.1:7000",
+
"127.0.0.2:7000",
+
"127.0.0.3:7000"))),
+ Arguments.of(new Fixture("IPv6",
+ new
InetSocketAddress(InetAddress.getByName("2001:db8::1"), 7000),
+
Arrays.asList("[2001:db8:0:0:0:0:0:1]:7000",
+
"[2001:db8:0:0:0:0:0:2]:7000",
+
"[2001:db8:0:0:0:0:0:3]:7000"))));
+ }
+
+ @ParameterizedTest(name = "{index} => {0}")
+ @MethodSource("clusters")
+ void testGossipHostHeaderIsRecognized(Fixture fixture)
+ {
+ String header = "/" + fixture.endpoints.get(0);
+ assertThat(GossipInfoParser.isGossipInfoHostHeader(header))
+ .as("'%s' is a host header emitted by
InetAddressAndPort#toString(true)", header)
+ .isTrue();
+ }
+
+ @ParameterizedTest(name = "{index} => {0}")
+ @MethodSource("clusters")
+ void testIsMemberOfElectorateWhenOwningTokenZero(Fixture fixture)
+ {
+ SidecarConfiguration sidecarConfiguration = new
SidecarConfigurationImpl();
+ ElectorateMembership membership = electorateMembership(fixture,
sidecarConfiguration);
+
+ assertThat(membership.isMember())
+ .as("The local instance owns token zero of %s, so it is part of the
electorate",
+
sidecarConfiguration.serviceConfiguration().schemaKeyspaceConfiguration().keyspace())
+ .isTrue();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "/[2001:db8::1]:7000",
+ "/2001:db8::1",
+ "/[2001:db8:0:0:0:0:0:1:7000",
+ "/[2001:db8:0:0:0:1]:7000",
+ "/[gggg:db8:0:0:0:0:0:1]:7000",
+ })
+ void testUnrecognizedIpv6HostHeaderIsRejected(String header)
+ {
+ assertThat(GossipInfoParser.isGossipInfoHostHeader(header))
+ .as("'%s' is not a host header Cassandra emits", header)
+ .isFalse();
+
+ String newLine = System.lineSeparator();
+ String rawGossipInfo = header + newLine + " generation:1683666630" +
newLine + " heartbeat:275" + newLine;
+ assertThatThrownBy(() -> GossipInfoParser.parse(rawGossipInfo))
+ .as("Parsing must fail loudly instead of misreading the fields of an
unrecognized host")
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining(header);
+ }
+
+ private ElectorateMembership electorateMembership(Fixture fixture,
SidecarConfiguration sidecarConfiguration)
+ {
+ String keyspace =
sidecarConfiguration.serviceConfiguration().schemaKeyspaceConfiguration().keyspace();
+
+ NodeSettings nodeSettings = mock(NodeSettings.class);
+ when(nodeSettings.partitioner()).thenReturn(PARTITIONER);
+
+ CassandraAdapterDelegate delegate =
mock(CassandraAdapterDelegate.class);
+
when(delegate.localStorageBroadcastAddress()).thenReturn(fixture.localBroadcastAddress);
+ when(delegate.nodeSettings()).thenReturn(nodeSettings);
+ StorageOperations storageOperations = new
CassandraStorageOperations(jmxClient(fixture.endpoints, keyspace),
+
DnsResolvers.RESOLVE_TO_IP);
+ when(delegate.storageOperations()).thenReturn(storageOperations);
+
+ InstanceMetadata instanceMetadata = mock(InstanceMetadata.class);
+ when(instanceMetadata.delegate()).thenReturn(delegate);
+ InstancesMetadata instancesMetadata = mock(InstancesMetadata.class);
+
when(instancesMetadata.instances()).thenReturn(Collections.singletonList(instanceMetadata));
+
+ return new SidecarInternalTokenZeroElectorateMembership(new
InstanceMetadataFetcher(instancesMetadata),
+
sidecarConfiguration);
+ }
+
+ private static JmxClient jmxClient(List<String> endpoints, String keyspace)
+ {
+ Map<List<String>, List<String>> rangeToEndpoints = new HashMap<>();
+ rangeToEndpoints.put(TOKEN_RANGE_OWNING_TOKEN_ZERO, endpoints);
+
+ StorageJmxOperations storage = mock(StorageJmxOperations.class);
+ when(storage.isGossipRunning()).thenReturn(true);
+
when(storage.getRangeToEndpointWithPortMap(keyspace)).thenReturn(rangeToEndpoints);
+
when(storage.getPendingRangeToEndpointWithPortMap(keyspace)).thenReturn(Collections.emptyMap());
+ when(storage.getLiveNodesWithPort()).thenReturn(endpoints);
+
when(storage.getUnreachableNodesWithPort()).thenReturn(Collections.emptyList());
+
when(storage.getJoiningNodesWithPort()).thenReturn(Collections.emptyList());
+
when(storage.getLeavingNodesWithPort()).thenReturn(Collections.emptyList());
+
when(storage.getMovingNodesWithPort()).thenReturn(Collections.emptyList());
+
+ EndpointSnitchJmxOperations endpointSnitch =
mock(EndpointSnitchJmxOperations.class);
+ try
+ {
+
when(endpointSnitch.getDatacenter(anyString())).thenReturn(DATACENTER);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new AssertionError(e); // stubbing only, cannot be thrown
+ }
+
+ ClusterMembershipJmxOperations clusterMembership =
mock(ClusterMembershipJmxOperations.class);
+
when(clusterMembership.getAllEndpointStatesWithPort()).thenReturn(rawGossipInfo(endpoints));
+
+ JmxClient jmxClient = mock(JmxClient.class);
+ when(jmxClient.proxy(StorageJmxOperations.class,
STORAGE_SERVICE_OBJ_NAME)).thenReturn(storage);
+ when(jmxClient.proxy(EndpointSnitchJmxOperations.class,
ENDPOINT_SNITCH_INFO_OBJ_NAME)).thenReturn(endpointSnitch);
+ when(jmxClient.proxy(ClusterMembershipJmxOperations.class,
FAILURE_DETECTOR_OBJ_NAME)).thenReturn(clusterMembership);
+ return jmxClient;
+ }
+
+ private static String rawGossipInfo(List<String> endpoints)
+ {
+ String newLine = System.lineSeparator();
+ StringBuilder sb = new StringBuilder();
+ for (String endpoint : endpoints)
+ {
+ sb.append('/').append(endpoint).append(newLine)
+ .append(" generation:1683666630").append(newLine)
+ .append(" heartbeat:275").append(newLine)
+ .append(" LOAD:210:88971.0").append(newLine)
+ .append(" DC:8:").append(DATACENTER).append(newLine)
+ .append(" RACK:10:rack1").append(newLine)
+ .append(" STATUS_WITH_PORT:16:NORMAL,-1").append(newLine)
+ .append(" SSTABLE_VERSIONS:6:big-nb").append(newLine)
+ .append(" TOKENS:15:<hidden>").append(newLine);
+ }
+ return sb.toString();
+ }
+
+ static class Fixture
+ {
+ final String name;
+ final InetSocketAddress localBroadcastAddress;
+ final List<String> endpoints;
+
+ Fixture(String name, InetSocketAddress localBroadcastAddress,
List<String> endpoints)
+ {
+ this.name = name;
+ this.localBroadcastAddress = localBroadcastAddress;
+ this.endpoints = endpoints;
+ }
+
+ @Override
+ public String toString()
+ {
+ return name;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]