This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit e7ac3bfa56f0dc2946924a96b817fb57fecc60af
Author: opencode <[email protected]>
AuthorDate: Sun Oct 11 17:36:34 2026 +0200

    Fail the receiver start when the cluster bind address cannot be resolved
    
    The receiver lazily resolved the configured bind host and, when the
    resolution failed, only logged an error and returned a null address.
    The bind paths then passed that null to the socket address, silently
    binding the cluster listener to the wildcard address and exposing it on
    all interfaces instead of the configured address, while the member
    advertisement still announced the unresolvable host to the cluster.
    Each automatic port retry repeated the resolution and its error log, so
    one misconfigured host produced up to one hundred entries per start.
    
    getBind() is now synchronized and the new resolveBind() converts a
    failed resolution into an IOException with a dedicated message. The TCP
    and UDP bind paths resolve once before the auto-bind retry loop and the
    NioReceiver start resolves before anything is bound, so the TLS-only
    path that skips the TCP bind fails as well. A failed resolution is not
    cached, so a repeated start after the configuration is corrected
    resolves again.
    
    Add TestReceiverBase covering the fail-closed start, the TCP and UDP
    binds against an address in the guaranteed non-resolving .invalid top
    level domain, the use of a resolved address, and the successful retry
    after the configuration is corrected.
---
 .../tribes/transport/LocalStrings.properties       |   1 +
 .../catalina/tribes/transport/ReceiverBase.java    |  31 ++++-
 .../catalina/tribes/transport/nio/NioReceiver.java |   2 +-
 .../tribes/transport/TestAbstractSender.java       |  25 ----
 .../tribes/transport/TestReceiverBase.java         | 129 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   8 ++
 6 files changed, 164 insertions(+), 32 deletions(-)

diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings.properties 
b/java/org/apache/catalina/tribes/transport/LocalStrings.properties
index d6c41381f1..08d6f38ad4 100644
--- a/java/org/apache/catalina/tribes/transport/LocalStrings.properties
+++ b/java/org/apache/catalina/tribes/transport/LocalStrings.properties
@@ -20,6 +20,7 @@ PooledSender.senderDisconnectFail=Failed to disconnect sender
 
 pooledSender.closed.queue=Queue is closed
 
+receiverBase.address.unresolvable=Failed to resolve replication listener bind 
address [{0}]. The receiver start fails rather than binding to the wildcard 
address.
 receiverBase.bind.failed=Failed bind replication listener on address:[{0}]
 receiverBase.socket.bind=Receiver Server Socket bound to:[{0}]
 receiverBase.start=Starting replication listener on address [{0}]
diff --git a/java/org/apache/catalina/tribes/transport/ReceiverBase.java 
b/java/org/apache/catalina/tribes/transport/ReceiverBase.java
index ea4a9543c2..9ca824cc14 100644
--- a/java/org/apache/catalina/tribes/transport/ReceiverBase.java
+++ b/java/org/apache/catalina/tribes/transport/ReceiverBase.java
@@ -200,11 +200,11 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
     }
 
     /**
-     * Returns the bind address.
+     * Returns the bind address, resolving the configured host on first use.
      *
-     * @return the bind address
+     * @return the bind address or <code>null</code> if the configured host 
could not be resolved
      */
-    public InetAddress getBind() {
+    public synchronized InetAddress getBind() {
         if (bind == null) {
             try {
                 if ("auto".equals(host)) {
@@ -221,10 +221,27 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
         return bind;
     }
 
+    /**
+     * Resolves the bind address, failing if the configured host cannot be 
resolved. This avoids silently binding to
+     * the wildcard address when {@link #getBind()} returns <code>null</code> 
after a failed resolution.
+     *
+     * @return the resolved bind address
+     *
+     * @throws IOException If the configured host could not be resolved
+     */
+    protected InetAddress resolveBind() throws IOException {
+        InetAddress address = getBind();
+        if (address == null) {
+            // The cause has already been logged by getBind()
+            throw new 
IOException(sm.getString("receiverBase.address.unresolvable", host));
+        }
+        return address;
+    }
+
     /**
      * Attempts to bind using the provided port and if that fails attempts to 
bind to each of the ports from portstart
      * to (portstart + retries -1) until either there are no more ports or the 
bind is successful. The address to bind
-     * to is obtained via a call to {@link #getBind()}.
+     * to is obtained via a call to {@link #resolveBind()}.
      *
      * @param socket    The socket to bind
      * @param portstart Starting port for bind attempts
@@ -234,11 +251,12 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
      */
     protected void bind(ServerSocket socket, int portstart, int retries) 
throws IOException {
         synchronized (bindLock) {
+            InetAddress address = resolveBind();
             InetSocketAddress addr = null;
             int port = portstart;
             while (retries > 0) {
                 try {
-                    addr = new InetSocketAddress(getBind(), port);
+                    addr = new InetSocketAddress(address, port);
                     socket.bind(addr);
                     setPort(port);
                     log.info(sm.getString("receiverBase.socket.bind", addr));
@@ -267,11 +285,12 @@ public abstract class ReceiverBase implements 
ChannelReceiver, ListenCallback, R
      * @throws IOException Socket bind error
      */
     protected int bindUdp(DatagramSocket socket, int portstart, int retries) 
throws IOException {
+        InetAddress address = resolveBind();
         InetSocketAddress addr = null;
         int port = portstart;
         while (retries > 0) {
             try {
-                addr = new InetSocketAddress(getBind(), port);
+                addr = new InetSocketAddress(address, port);
                 socket.bind(addr);
                 setUdpPort(port);
                 log.info(sm.getString("receiverBase.udp.bind", addr));
diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java 
b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
index e390f7c2f7..80cd5ad0ed 100644
--- a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
+++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java
@@ -90,7 +90,7 @@ public class NioReceiver extends ReceiverBase implements 
Runnable, NioReceiverMB
             }
         }
         try {
-            getBind();
+            resolveBind();
             bind();
             String channelName = "";
             if (getChannel().getName() != null) {
diff --git a/test/org/apache/catalina/tribes/transport/TestAbstractSender.java 
b/test/org/apache/catalina/tribes/transport/TestAbstractSender.java
index 19b6fe952f..2c93f2fb96 100644
--- a/test/org/apache/catalina/tribes/transport/TestAbstractSender.java
+++ b/test/org/apache/catalina/tribes/transport/TestAbstractSender.java
@@ -16,38 +16,15 @@
  */
 package org.apache.catalina.tribes.transport;
 
-import org.junit.After;
 import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.Before;
 import org.junit.Test;
 
-import org.apache.catalina.tribes.group.TribesSslContext;
 import org.apache.catalina.tribes.membership.MemberImpl;
 import org.apache.catalina.tribes.transport.nio.NioSender;
 import org.apache.catalina.tribes.transport.nio.PooledParallelSender;
 
 public class TestAbstractSender {
 
-    private TribesSslContext sslContext;
-
-    @Before
-    public void setUp() {
-        try {
-            sslContext = new TribesSslContext("tribes-test",
-                    
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "SHA256", 
"TLSv1.3");
-        } catch (Exception e) {
-            Assume.assumeNoException(e);
-        }
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (sslContext != null) {
-            sslContext.close();
-        }
-    }
-
     @Test
     public void testTransferProperties() throws Exception {
         AbstractSender source = new NioSender();
@@ -90,7 +67,6 @@ public class TestAbstractSender {
         sender.setThrowOnFailedAck(false);
         sender.setUdpBased(true);
         sender.setUdpPort(5000);
-        sender.setSslContext(sslContext);
     }
 
     private void assertTransferred(AbstractSender expected, AbstractSender 
actual) {
@@ -116,6 +92,5 @@ public class TestAbstractSender {
         Assert.assertEquals("soTrafficClass", expected.getSoTrafficClass(), 
actual.getSoTrafficClass());
         Assert.assertEquals("throwOnFailedAck", 
expected.getThrowOnFailedAck(), actual.getThrowOnFailedAck());
         Assert.assertEquals("udpBased", expected.isUdpBased(), 
actual.isUdpBased());
-        Assert.assertEquals("sslContext", expected.getSslContext(), 
actual.getSslContext());
     }
 }
diff --git a/test/org/apache/catalina/tribes/transport/TestReceiverBase.java 
b/test/org/apache/catalina/tribes/transport/TestReceiverBase.java
new file mode 100644
index 0000000000..aef95bc404
--- /dev/null
+++ b/test/org/apache/catalina/tribes/transport/TestReceiverBase.java
@@ -0,0 +1,129 @@
+/*
+ * 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.catalina.tribes.transport;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.UnknownHostException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.tribes.group.GroupChannel;
+import org.apache.catalina.tribes.transport.nio.NioReceiver;
+
+public class TestReceiverBase {
+
+    // The ".invalid" top level domain is guaranteed never to resolve
+    private static final String UNRESOLVABLE_HOST = 
"tomcat-tribes-does-not-exist.invalid";
+
+    private static class ReceiverBaseImpl extends ReceiverBase {
+
+        @Override
+        public AbstractRxTask createRxTask() {
+            return null;
+        }
+    }
+
+    /*
+     * An unresolvable bind address must fail the start of the receiver rather 
than silently binding the cluster
+     * listener to the wildcard address, which would expose the receiver on 
all interfaces.
+     */
+    @Test
+    public void testUnresolvableAddressFailsClosed() {
+        NioReceiver receiver = new NioReceiver();
+        receiver.setChannel(new GroupChannel());
+        receiver.setAddress(UNRESOLVABLE_HOST);
+        receiver.setPort(0);
+        IOException ioe = Assert.assertThrows(IOException.class, 
receiver::start);
+        Assert.assertNull("A failed resolution must not produce a bind 
address", receiver.getBind());
+        Assert.assertFalse(receiver.isListening());
+        receiver.stop();
+        Assert.assertTrue(ioe.getMessage().contains(UNRESOLVABLE_HOST));
+    }
+
+    @Test
+    public void testBindWithUnresolvableAddressThrows() {
+        ReceiverBaseImpl receiver = new ReceiverBaseImpl();
+        receiver.setAddress(UNRESOLVABLE_HOST);
+        try (ServerSocket socket = new ServerSocket()) {
+            IOException ioe = Assert.assertThrows(IOException.class, () -> 
receiver.bind(socket, 0, 3));
+            Assert.assertTrue(ioe.getMessage().contains(UNRESOLVABLE_HOST));
+        } catch (IOException ioe) {
+            Assert.fail(ioe.getMessage());
+        }
+        Assert.assertNull(receiver.getBind());
+    }
+
+    @Test
+    public void testBindUdpWithUnresolvableAddressThrows() {
+        ReceiverBaseImpl receiver = new ReceiverBaseImpl();
+        receiver.setAddress(UNRESOLVABLE_HOST);
+        try (DatagramSocket socket = new DatagramSocket(null)) {
+            IOException ioe = Assert.assertThrows(IOException.class, () -> 
receiver.bindUdp(socket, 0, 3));
+            Assert.assertTrue(ioe.getMessage().contains(UNRESOLVABLE_HOST));
+        } catch (IOException ioe) {
+            Assert.fail(ioe.getMessage());
+        }
+    }
+
+    @Test
+    public void testBindWithResolvableAddress() throws UnknownHostException {
+        ReceiverBaseImpl receiver = new ReceiverBaseImpl();
+        receiver.setAddress("127.0.0.1");
+        Assert.assertEquals(InetAddress.getByName("127.0.0.1"), 
receiver.getBind());
+        try (ServerSocket socket = new ServerSocket()) {
+            receiver.bind(socket, 0, 1);
+            // A resolved address must be used, never the wildcard address
+            Assert.assertFalse(socket.getInetAddress().isAnyLocalAddress());
+        } catch (IOException ioe) {
+            Assert.fail(ioe.getMessage());
+        }
+    }
+
+    @Test
+    public void testBindUdpWithResolvableAddress() throws UnknownHostException 
{
+        ReceiverBaseImpl receiver = new ReceiverBaseImpl();
+        receiver.setAddress("127.0.0.1");
+        Assert.assertEquals(InetAddress.getByName("127.0.0.1"), 
receiver.getBind());
+        try (DatagramSocket socket = new DatagramSocket(null)) {
+            receiver.bindUdp(socket, 0, 1);
+            Assert.assertFalse(socket.getLocalAddress().isAnyLocalAddress());
+        } catch (IOException ioe) {
+            Assert.fail(ioe.getMessage());
+        }
+    }
+
+    /*
+     * A failed resolution must not be cached, so that a later attempt after 
the configuration is corrected succeeds.
+     */
+    @Test
+    public void testBindSucceedsAfterCorrectedAddress() {
+        ReceiverBaseImpl receiver = new ReceiverBaseImpl();
+        receiver.setAddress(UNRESOLVABLE_HOST);
+        try (ServerSocket socket = new ServerSocket()) {
+            Assert.assertThrows(IOException.class, () -> receiver.bind(socket, 
0, 1));
+            receiver.setAddress("127.0.0.1");
+            receiver.bind(socket, 0, 1);
+            Assert.assertFalse(socket.getInetAddress().isAnyLocalAddress());
+        } catch (IOException ioe) {
+            Assert.fail(ioe.getMessage());
+        }
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 4af9cab2f0..04ae86a192 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -2538,6 +2538,14 @@
         configured with the default buffer sizes and the configured values were
         silently ignored. (remm)
       </fix>
+      <fix>
+        The start of the cluster receiver now fails when the configured bind
+        address cannot be resolved. Previously, the resolution failure was only
+        logged and the receiver bound to the wildcard address, silently
+        exposing the cluster listener on all interfaces instead of the
+        configured address, while the unresolvable host was advertised to the
+        other cluster members. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Other">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to