Skip to content

Commit 8563101

Browse files
authored
[#728] Reject TCP self-connects in replication connect paths (#729)
1 parent 5106bad commit 8563101

4 files changed

Lines changed: 95 additions & 2 deletions

File tree

opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2006-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16+
* Portions Copyrighted 2026 3A Systems, LLC.
1617
*/
1718
package org.opends.server.replication.server;
1819

@@ -21,6 +22,7 @@
2122
import static org.opends.server.util.StaticUtils.*;
2223

2324
import java.io.IOException;
25+
import java.net.ConnectException;
2426
import java.net.InetAddress;
2527
import java.net.InetSocketAddress;
2628
import java.net.ServerSocket;
@@ -414,6 +416,15 @@ private boolean connect(HostPort remoteServerAddress, DN baseDN)
414416
}
415417
int timeoutMS = MultimasterReplication.getConnectionTimeoutMS();
416418
socket.connect(remoteServerAddress.toInetSocketAddress(), timeoutMS);
419+
if (isSelfConnection(socket))
420+
{
421+
// While the remote RS is down, the kernel may pick its port as the
422+
// local port of this connecting socket (TCP simultaneous open),
423+
// "connecting" it to itself. Keeping such a socket open would hold
424+
// the port and prevent the RS from binding it on restart.
425+
throw new ConnectException("Connection to " + remoteServerAddress
426+
+ " is a TCP self-connect, no replication server is listening");
427+
}
417428
session = replSessionSecurity.createClientSession(socket, timeoutMS);
418429

419430
ReplicationServerHandler rsHandler = new ReplicationServerHandler(

opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*
1414
* Copyright 2006-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16-
* Portions Copyright 2023-2025 3A Systems LLC.
16+
* Portions Copyright 2023-2026 3A Systems LLC.
1717
* Portions Copyright 2025 Wren Security.
1818
*/
1919
package org.opends.server.replication.service;
@@ -1083,6 +1083,15 @@ private ConnectedRS performPhaseOneHandshake(String serverURL, boolean keepSessi
10831083
}
10841084
int timeoutMS = MultimasterReplication.getConnectionTimeoutMS();
10851085
socket.connect(HostPort.valueOf(serverURL).toInetSocketAddress(), timeoutMS);
1086+
if (isSelfConnection(socket))
1087+
{
1088+
// While the RS is down, the kernel may pick the RS port as the local
1089+
// port of this reconnecting socket (TCP simultaneous open),
1090+
// "connecting" it to itself. Keeping such a socket open would hold
1091+
// the RS port and prevent the RS from binding it on restart.
1092+
throw new ConnectException("Connection to " + serverURL
1093+
+ " is a TCP self-connect, no replication server is listening");
1094+
}
10861095
newSession = replSessionSecurity.createClientSession(socket, timeoutMS);
10871096
boolean isSslEncryption = replSessionSecurity.isSslEncryption();
10881097

opendj-server-legacy/src/main/java/org/opends/server/util/StaticUtils.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2006-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16+
* Portions Copyrighted 2026 3A Systems, LLC.
1617
*/
1718
package org.opends.server.util;
1819

@@ -1319,7 +1320,25 @@ public static boolean isAddressInUse(
13191320
return true;
13201321
}
13211322

1322-
1323+
/**
1324+
* Indicates whether the provided connected socket is connected to itself,
1325+
* i.e. its local and remote endpoints are identical.
1326+
* <p>
1327+
* Connecting to a local port from the TCP ephemeral range while nothing
1328+
* listens on it can make the kernel pick that very port as the local port
1329+
* of the connecting socket: TCP simultaneous open then "establishes" the
1330+
* connection to itself (observed on Linux). Such a socket occupies the
1331+
* listen port its target service is about to bind, so callers retrying
1332+
* connections to a temporarily stopped service must detect and close it.
1333+
*
1334+
* @param socket a connected socket
1335+
* @return true if the socket is connected to itself
1336+
*/
1337+
public static boolean isSelfConnection(Socket socket)
1338+
{
1339+
return socket.getLocalPort() == socket.getPort()
1340+
&& socket.getLocalAddress().equals(socket.getInetAddress());
1341+
}
13231342

13241343
/**
13251344
* Returns a lower-case string representation of a given string, verifying for null input string.

opendj-server-legacy/src/test/java/org/opends/server/util/TestStaticUtils.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2006-2009 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2016 ForgeRock AS.
16+
* Portions Copyrighted 2026 3A Systems, LLC.
1617
*/
1718
package org.opends.server.util;
1819

@@ -26,6 +27,9 @@
2627
import java.io.IOException;
2728
import java.io.PrintWriter;
2829
import java.io.RandomAccessFile;
30+
import java.net.InetSocketAddress;
31+
import java.net.ServerSocket;
32+
import java.net.Socket;
2933
import java.nio.ByteBuffer;
3034
import java.nio.channels.FileChannel;
3135
import java.nio.channels.FileLock;
@@ -36,6 +40,7 @@
3640
import org.forgerock.opendj.ldap.ByteString;
3741
import org.opends.server.TestCaseUtils;
3842
import org.testng.Assert;
43+
import org.testng.SkipException;
3944
import org.testng.annotations.BeforeClass;
4045
import org.testng.annotations.DataProvider;
4146
import org.testng.annotations.Test;
@@ -1046,4 +1051,53 @@ public void testStackTraceHasCause() throws Exception
10461051
new RuntimeException(new IllegalThreadStateException()), IllegalArgumentException.class);
10471052
Assert.assertTrue(hasCause, "Third case : IllegalThreadStateException should be detected as a cause");
10481053
}
1054+
1055+
@Test
1056+
public void testIsSelfConnectionFalseForNormalConnection() throws Exception
1057+
{
1058+
try (ServerSocket listener = new ServerSocket())
1059+
{
1060+
listener.bind(new InetSocketAddress("127.0.0.1", 0));
1061+
try (Socket client = new Socket())
1062+
{
1063+
client.connect(listener.getLocalSocketAddress(), 2000);
1064+
try (Socket accepted = listener.accept())
1065+
{
1066+
Assert.assertFalse(StaticUtils.isSelfConnection(client));
1067+
Assert.assertFalse(StaticUtils.isSelfConnection(accepted));
1068+
}
1069+
}
1070+
}
1071+
}
1072+
1073+
@Test
1074+
public void testIsSelfConnectionTrueForSelfConnectedSocket() throws Exception
1075+
{
1076+
// find a free port
1077+
final int port;
1078+
try (ServerSocket tmp = new ServerSocket())
1079+
{
1080+
tmp.bind(new InetSocketAddress("127.0.0.1", 0));
1081+
port = tmp.getLocalPort();
1082+
}
1083+
1084+
// Force the TCP self-connect (simultaneous open) that the kernel can
1085+
// produce spontaneously when connecting to an unbound port from the
1086+
// ephemeral range: bind the local end to the very port being connected.
1087+
try (Socket socket = new Socket())
1088+
{
1089+
socket.setReuseAddress(true);
1090+
socket.bind(new InetSocketAddress("127.0.0.1", port));
1091+
try
1092+
{
1093+
socket.connect(new InetSocketAddress("127.0.0.1", port), 2000);
1094+
}
1095+
catch (IOException e)
1096+
{
1097+
// BSD-based stacks (macOS) refuse an explicit self-connect
1098+
throw new SkipException("TCP self-connect not supported by this OS: " + e);
1099+
}
1100+
Assert.assertTrue(StaticUtils.isSelfConnection(socket));
1101+
}
1102+
}
10491103
}

0 commit comments

Comments
 (0)