From cdc884c34f5d7f22624d4e85a8bb61fd50560e5e Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 03 Nov 2026 10:44:13 +0000
Subject: [PATCH] [#1261] Close JMX RMI connector sockets on shutdown (#1264)
---
opendj-server-legacy/src/test/java/org/opends/server/protocols/jmx/JmxListenAddressTestCase.java | 80 ++++++++++++++++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/OpendsRmiServerSocketFactory.java | 7 +
opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/DirectoryRMIServerSocketFactory.java | 24 +++++++
opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/RmiConnector.java | 17 +++++
4 files changed, 122 insertions(+), 6 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/DirectoryRMIServerSocketFactory.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/DirectoryRMIServerSocketFactory.java
index 82081b9..c983616 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/DirectoryRMIServerSocketFactory.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/DirectoryRMIServerSocketFactory.java
@@ -19,6 +19,7 @@
import org.forgerock.i18n.slf4j.LocalizedLogger;
+import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
@@ -37,7 +38,7 @@
* Sockets Layer (SSL) or Transport Layer Security (TLS) protocols.
*/
public class DirectoryRMIServerSocketFactory implements
- RMIServerSocketFactory
+ RMIServerSocketFactory, Closeable
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -50,6 +51,9 @@
/** The address to listen on, which could be INADDR_ANY. */
private final InetAddress listenAddress;
+ /** The server socket owned by this factory. */
+ private ServerSocket serverSocket;
+
/**
* Constructs a new <code>DirectoryRMIServerSocketFactory</code> with the
* specified SSL socket configuration.
@@ -105,7 +109,7 @@
@Override
public ServerSocket createServerSocket(int port) throws IOException
{
- return new ServerSocket(port, 0, listenAddress)
+ serverSocket = new ServerSocket(port, 0, listenAddress)
{
@Override
public Socket accept() throws IOException
@@ -127,6 +131,22 @@
return sslSocket;
}
};
+ return serverSocket;
+ }
+
+ /**
+ * Closes the underlying server socket, including an anonymous RMI listener.
+ *
+ * @throws IOException if the socket cannot be closed
+ */
+ @Override
+ public void close() throws IOException
+ {
+ if (serverSocket != null)
+ {
+ serverSocket.close();
+ serverSocket = null;
+ }
}
/**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/OpendsRmiServerSocketFactory.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/OpendsRmiServerSocketFactory.java
index 8457ce4..1ed4621 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/OpendsRmiServerSocketFactory.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/OpendsRmiServerSocketFactory.java
@@ -13,9 +13,11 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2015 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.protocols.jmx;
+import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
@@ -38,7 +40,7 @@
* <p>
* Each connector is registered into the JMX MBean server.
*/
-public class OpendsRmiServerSocketFactory implements RMIServerSocketFactory
+public class OpendsRmiServerSocketFactory implements RMIServerSocketFactory, Closeable
{
/** The address to listen on, which could be INADDR_ANY. */
private final InetAddress listenAddress;
@@ -69,7 +71,8 @@
*
* @throws IOException If an I/O error occurs when closing the socket.
*/
- void close() throws IOException
+ @Override
+ public void close() throws IOException
{
if (serverSocket != null)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/RmiConnector.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/RmiConnector.java
index 626f94d..d7e0a9b 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/RmiConnector.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/jmx/RmiConnector.java
@@ -20,6 +20,7 @@
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.util.StaticUtils.*;
+import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -165,6 +166,9 @@
*/
private OpendsRmiServerSocketFactory rmiSsf;
+ /** The socket factory owned by the RMI connector, separate from the registry's. */
+ private Closeable connectorSocketFactory;
+
/**
* The RMI protocol version used by this connector.
*/
@@ -371,8 +375,10 @@
SSLSocketFactory ssf = ctx.getSocketFactory();
// set the Server socket factory in the JMX map
- rmiServerSockeyFactory = new DirectoryRMIServerSocketFactory(
+ DirectoryRMIServerSocketFactory factory = new DirectoryRMIServerSocketFactory(
ssf, false, jmxConnectionHandler.getListenAddress());
+ connectorSocketFactory = factory;
+ rmiServerSockeyFactory = factory;
env.put(
"jmx.remote.rmi.server.socket.factory",
rmiServerSockeyFactory);
@@ -393,8 +399,10 @@
{
logger.trace("UNSECURE CONNECTION");
}
- rmiServerSockeyFactory = new OpendsRmiServerSocketFactory(
+ OpendsRmiServerSocketFactory factory = new OpendsRmiServerSocketFactory(
jmxConnectionHandler.getListenAddress());
+ connectorSocketFactory = factory;
+ rmiServerSockeyFactory = factory;
}
// specify the rmi JMX authenticator to be used
@@ -553,6 +561,11 @@
jmxRmiConnectorNoClientCertificate = null;
jmxRmiConnectorClientCertificate = null;
+ // RMI keeps anonymous-port listeners open even after the last object is unexported.
+ // Close our socket explicitly, also when stop() failed or the registry is being kept.
+ close(connectorSocketFactory);
+ connectorSocketFactory = null;
+
// Unregister connectors and stop them.
try
{
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/jmx/JmxListenAddressTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/jmx/JmxListenAddressTestCase.java
index 815d886..b139a59 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/protocols/jmx/JmxListenAddressTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/protocols/jmx/JmxListenAddressTestCase.java
@@ -21,6 +21,7 @@
import java.io.IOException;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.registry.LocateRegistry;
@@ -28,6 +29,8 @@
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.management.remote.JMXConnector;
import javax.net.ssl.SSLContext;
@@ -201,6 +204,83 @@
}
}
+ /** Each handler restart must release the anonymous RMI port, with and without SSL. */
+ @Test(dataProvider = "useSSL")
+ public void releasesTheAnonymousRmiPortAfterEachStop(boolean useSSL) throws Exception
+ {
+ final int listenPort = TestCaseUtils.findFreePort();
+ for (int i = 0; i < 3; i++)
+ {
+ JmxConnectionHandler handler = newHandler(listenAddress, listenPort, 0, useSSL);
+ final int rmiPort;
+ try
+ {
+ handler.run();
+ rmiPort = anonymousRmiPort(listenAddress, listenPort);
+ assertOpensASession(listenAddress, listenPort);
+ }
+ finally
+ {
+ handler.finalizeConnectionHandler(STOP_REASON);
+ // Shutdown may be called both by processServerShutdown and by finalization.
+ handler.finalizeConnectionHandler(STOP_REASON);
+ }
+ assertPortReleased(listenAddress, rmiPort);
+ assertPortReleased(listenAddress, listenPort);
+ }
+ }
+
+ /** Restarting only the connector must close its old socket while keeping the registry usable. */
+ @Test(dataProvider = "useSSL")
+ public void releasesTheAnonymousRmiPortAfterAConfigurationChange(boolean useSSL) throws Exception
+ {
+ final int listenPort = TestCaseUtils.findFreePort();
+ JmxConnectionHandler handler = newHandler(listenAddress, listenPort, 0, useSSL);
+ try
+ {
+ handler.run();
+ final int oldRmiPort = anonymousRmiPort(listenAddress, listenPort);
+ assertOpensASession(listenAddress, listenPort);
+
+ // Select the replacement port while the old connector is listening: it cannot be the old port.
+ final int newRmiPort = TestCaseUtils.findFreePort();
+ applyConfigurationChange(handler, newConfig(listenAddress, listenPort, newRmiPort, useSSL));
+
+ assertPortReleased(listenAddress, oldRmiPort);
+ assertOpensASession(listenAddress, listenPort);
+ }
+ finally
+ {
+ handler.finalizeConnectionHandler(STOP_REASON);
+ }
+ }
+
+ /**
+ * The configured port is zero, so read the assigned port from the stub's endpoint description.
+ * This avoids opening JDK-internal RMI packages for reflection in the test JVM.
+ */
+ private static int anonymousRmiPort(InetAddress address, int listenPort) throws Exception
+ {
+ Registry registry = LocateRegistry.getRegistry(address.getHostAddress(), listenPort);
+ String stub = registry.lookup("org.opends.server.protocols.jmx.client-unknown").toString();
+ Matcher endpoint = Pattern.compile("endpoint:\\[[^\\]]*?:(\\d+)[,\\]]").matcher(stub);
+ assertTrue(endpoint.find(), "Cannot find the RMI endpoint in " + stub);
+ int port = Integer.parseInt(endpoint.group(1));
+ assertTrue(port > 0, "The RMI stub must advertise the assigned port");
+ assertTrue(isAcceptingConnections(address, port), "The RMI connector must listen before it is stopped");
+ return port;
+ }
+
+ private static void assertPortReleased(InetAddress address, int port) throws IOException
+ {
+ assertFalse(isAcceptingConnections(address, port), "The stopped RMI listener still accepts connections on " + port);
+ try (ServerSocket socket = new ServerSocket())
+ {
+ socket.setReuseAddress(true);
+ socket.bind(new InetSocketAddress(address, port));
+ }
+ }
+
/** A change that restarts the RMI connector keeps the listen address that is reported. */
@Test
public void reportsTheListenAddressAfterAChangeOfTheRmiPort() throws Exception
--
Gitblit v1.10.0