From b67d89bf9eb91a1a74f5c58d0649c66590fb0a53 Mon Sep 17 00:00:00 2001 From: "Tak Lon (Stephen) Wu" Date: Thu, 25 Jun 2026 06:57:49 +0800 Subject: [PATCH 1/2] HBASE-30257 Fix case-sensitive hostname check in region server --- .../org/apache/hadoop/hbase/util/Strings.java | 24 +++++++++++++++++++ .../apache/hadoop/hbase/util/TestStrings.java | 14 +++++++++++ .../hbase/regionserver/HRegionServer.java | 5 ++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java index 6759603f3aa5..3df06e2db008 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; @@ -30,6 +31,7 @@ import org.apache.hbase.thirdparty.com.google.common.base.Joiner; import org.apache.hbase.thirdparty.com.google.common.base.Splitter; +import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses; /** * Utility for Strings. @@ -88,6 +90,28 @@ public static String domainNamePointerToHostName(String dnPtr) { return dnPtr.endsWith(".") ? dnPtr.substring(0, dnPtr.length() - 1) : dnPtr; } + /** + * Compare two host identifiers for equality. DNS hostnames are compared case-insensitively + * because DNS labels are case-insensitive. IP address literals are compared by numeric address. + * @param left first hostname or IP + * @param right second hostname or IP + * @return {@code true} if both refer to the same host identifier + * @throws NullPointerException if either argument is {@code null} + */ + public static boolean hostnamesEqual(String left, String right) { + Objects.requireNonNull(left, "Hostname or IP cannot be null"); + Objects.requireNonNull(right, "Hostname or IP cannot be null"); + boolean leftIsIp = InetAddresses.isInetAddress(left); + boolean rightIsIp = InetAddresses.isInetAddress(right); + if (leftIsIp != rightIsIp) { + return false; + } + if (leftIsIp) { + return InetAddresses.forString(left).equals(InetAddresses.forString(right)); + } + return left.equalsIgnoreCase(right); + } + /** * Push the input string to the right by appending a character before it, usually a space. * @param input the string to pad diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java index da388fda9b26..ff08662a518c 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java @@ -18,6 +18,7 @@ package org.apache.hadoop.hbase.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -57,6 +58,19 @@ public void testDomainNamePointerToHostName() { assertEquals("foo.bar.com", Strings.domainNamePointerToHostName("foo.bar.com.")); } + @Test + public void testHostnamesEqual() { + assertTrue(Strings.hostnamesEqual("HOST.example.com", "host.example.com")); + assertTrue(Strings.hostnamesEqual("rs1", "RS1")); + assertFalse(Strings.hostnamesEqual("host-a.example.com", "host-b.example.com")); + assertTrue(Strings.hostnamesEqual("10.0.0.1", "10.0.0.1")); + assertFalse(Strings.hostnamesEqual("10.0.0.1", "10.0.0.2")); + assertFalse(Strings.hostnamesEqual("HOST.example.com", "10.0.0.1")); + assertTrue(Strings.hostnamesEqual("::1", "0:0:0:0:0:0:0:1")); + assertThrows(NullPointerException.class, () -> Strings.hostnamesEqual(null, "host")); + assertThrows(NullPointerException.class, () -> Strings.hostnamesEqual("host", null)); + } + @Test public void testPadFront() { assertEquals("ddfoo", Strings.padFront("foo", 'd', 5)); diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index 3d7df098a37e..b4faaa5cfb30 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -168,6 +168,7 @@ import org.apache.hadoop.hbase.util.RetryCounter; import org.apache.hadoop.hbase.util.RetryCounterFactory; import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil; +import org.apache.hadoop.hbase.util.Strings; import org.apache.hadoop.hbase.util.Threads; import org.apache.hadoop.hbase.util.VersionInfo; import org.apache.hadoop.hbase.wal.AbstractFSWALProvider; @@ -1432,8 +1433,8 @@ protected void handleReportForDutyResponse(final RegionServerStartupResponse c) expectedHostName = rpcServices.getSocketAddress().getAddress().getHostAddress(); } boolean isHostnameConsist = StringUtils.isBlank(useThisHostnameInstead) - ? hostnameFromMasterPOV.equals(expectedHostName) - : hostnameFromMasterPOV.equals(useThisHostnameInstead); + ? Strings.hostnamesEqual(hostnameFromMasterPOV, expectedHostName) + : Strings.hostnamesEqual(hostnameFromMasterPOV, useThisHostnameInstead); if (!isHostnameConsist) { String msg = "Master passed us a different hostname to use; was=" + (StringUtils.isBlank(useThisHostnameInstead) From ac9de4c9947f96ad81f45ed2b7c182f74cf87fdf Mon Sep 17 00:00:00 2001 From: "Tak Lon (Stephen) Wu" Date: Fri, 26 Jun 2026 04:33:55 +0800 Subject: [PATCH 2/2] address comments and fix spotless check --- .../io/crypto/tls/HBaseHostnameVerifier.java | 50 ++++--------------- .../org/apache/hadoop/hbase/util/Strings.java | 32 ++++++++++-- .../apache/hadoop/hbase/util/TestStrings.java | 2 + .../hbase/regionserver/HRegionServer.java | 1 + 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java index a703f5ff630e..60ec927cfcf3 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java @@ -17,7 +17,6 @@ */ package org.apache.hadoop.hbase.io.crypto.tls; -import java.net.InetAddress; import java.security.cert.Certificate; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -28,7 +27,6 @@ import java.util.Locale; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; import javax.naming.InvalidNameException; import javax.naming.NamingException; import javax.naming.directory.Attribute; @@ -40,12 +38,11 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.security.auth.x500.X500Principal; +import org.apache.hadoop.hbase.util.Strings; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses; - /** * When enabled in {@link X509Util}, handles verifying that the hostname of a peer matches the * certificate it presents. @@ -112,9 +109,8 @@ public boolean verify(final String host, final SSLSession session) { void verify(final String host, final X509Certificate cert) throws SSLException { final List subjectAlts = getSubjectAltNames(cert); if (subjectAlts != null && !subjectAlts.isEmpty()) { - Optional inetAddress = parseIpAddress(host); - if (inetAddress.isPresent()) { - matchIPAddress(host, inetAddress.get(), subjectAlts); + if (Strings.isInetAddress(host)) { + matchIPAddress(host, subjectAlts); } else { matchDNSName(host, subjectAlts); } @@ -131,14 +127,14 @@ void verify(final String host, final X509Certificate cert) throws SSLException { } } - private static void matchIPAddress(final String host, final InetAddress inetAddress, - final List subjectAlts) throws SSLException { + private static void matchIPAddress(final String host, final List subjectAlts) + throws SSLException { for (final SubjectName subjectAlt : subjectAlts) { - if (subjectAlt.getType() == SubjectName.IP) { - Optional parsed = parseIpAddress(subjectAlt.getValue()); - if (parsed.filter(altAddr -> altAddr.equals(inetAddress)).isPresent()) { - return; - } + if ( + subjectAlt.getType() == SubjectName.IP + && Strings.hostnamesEqual(host, subjectAlt.getValue()) + ) { + return; } } throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " @@ -230,32 +226,6 @@ private static String extractCN(final String subjectPrincipal) throws SSLExcepti } } - private static Optional parseIpAddress(String host) { - host = host.trim(); - // Uri strings only work for ipv6 and are wrapped with brackets - // Unfortunately InetAddresses can't handle a mixed input, so we - // check here and choose which parse method to use. - if (host.startsWith("[") && host.endsWith("]")) { - return parseIpAddressUriString(host); - } else { - return parseIpAddressString(host); - } - } - - private static Optional parseIpAddressUriString(String host) { - if (InetAddresses.isUriInetAddress(host)) { - return Optional.of(InetAddresses.forUriString(host)); - } - return Optional.empty(); - } - - private static Optional parseIpAddressString(String host) { - if (InetAddresses.isInetAddress(host)) { - return Optional.of(InetAddresses.forString(host)); - } - return Optional.empty(); - } - @SuppressWarnings("MixedMutabilityReturnType") private static List getSubjectAltNames(final X509Certificate cert) { try { diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java index 3df06e2db008..647aec34a920 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Strings.java @@ -18,6 +18,7 @@ package org.apache.hadoop.hbase.util; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -90,10 +91,33 @@ public static String domainNamePointerToHostName(String dnPtr) { return dnPtr.endsWith(".") ? dnPtr.substring(0, dnPtr.length() - 1) : dnPtr; } + /** + * Returns whether the given string is an IP address, including bracketed IPv6 URI form. + * @param host hostname or IP + * @return {@code true} if {@code host} is an IP address + * @throws NullPointerException if {@code host} is {@code null} + */ + public static boolean isInetAddress(String host) { + Objects.requireNonNull(host, "Hostname or IP cannot be null"); + host = host.trim(); + if (host.startsWith("[") && host.endsWith("]")) { + return InetAddresses.isUriInetAddress(host); + } + return InetAddresses.isInetAddress(host); + } + + private static InetAddress parseInetAddress(String host) { + host = host.trim(); + if (host.startsWith("[") && host.endsWith("]")) { + return InetAddresses.forUriString(host); + } + return InetAddresses.forString(host); + } + /** * Compare two host identifiers for equality. DNS hostnames are compared case-insensitively * because DNS labels are case-insensitive. IP address literals are compared by numeric address. - * @param left first hostname or IP + * @param left first hostname or IP * @param right second hostname or IP * @return {@code true} if both refer to the same host identifier * @throws NullPointerException if either argument is {@code null} @@ -101,13 +125,13 @@ public static String domainNamePointerToHostName(String dnPtr) { public static boolean hostnamesEqual(String left, String right) { Objects.requireNonNull(left, "Hostname or IP cannot be null"); Objects.requireNonNull(right, "Hostname or IP cannot be null"); - boolean leftIsIp = InetAddresses.isInetAddress(left); - boolean rightIsIp = InetAddresses.isInetAddress(right); + boolean leftIsIp = isInetAddress(left); + boolean rightIsIp = isInetAddress(right); if (leftIsIp != rightIsIp) { return false; } if (leftIsIp) { - return InetAddresses.forString(left).equals(InetAddresses.forString(right)); + return parseInetAddress(left).equals(parseInetAddress(right)); } return left.equalsIgnoreCase(right); } diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java index ff08662a518c..c428e7a0a571 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestStrings.java @@ -67,6 +67,8 @@ public void testHostnamesEqual() { assertFalse(Strings.hostnamesEqual("10.0.0.1", "10.0.0.2")); assertFalse(Strings.hostnamesEqual("HOST.example.com", "10.0.0.1")); assertTrue(Strings.hostnamesEqual("::1", "0:0:0:0:0:0:0:1")); + assertTrue(Strings.hostnamesEqual("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334")); assertThrows(NullPointerException.class, () -> Strings.hostnamesEqual(null, "host")); assertThrows(NullPointerException.class, () -> Strings.hostnamesEqual("host", null)); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index b4faaa5cfb30..33a2717b07eb 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -1435,6 +1435,7 @@ protected void handleReportForDutyResponse(final RegionServerStartupResponse c) boolean isHostnameConsist = StringUtils.isBlank(useThisHostnameInstead) ? Strings.hostnamesEqual(hostnameFromMasterPOV, expectedHostName) : Strings.hostnamesEqual(hostnameFromMasterPOV, useThisHostnameInstead); + if (!isHostnameConsist) { String msg = "Master passed us a different hostname to use; was=" + (StringUtils.isBlank(useThisHostnameInstead)