mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

18 files modified
2 files added
810 ■■■■■ changed files
opendj-embedded/pom.xml 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/BindRule.java 31 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/AckMsg.java 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ExpectedAcksInfo.java 29 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/MessageHandler.java 27 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServer.java 11 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServerDomain.java 23 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/SafeDataExpectedAcksInfo.java 15 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/SafeReadExpectedAcksInfo.java 15 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerHandler.java 56 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java 11 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java 58 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/util/StaticUtils.java 21 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/BindRuleOperandTest.java 122 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/core/BindOperationTestCase.java 41 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/InitOnLineTest.java 72 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/ReplicationTestCase.java 9 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/server/AssuredReplicationServerTest.java 121 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ExpectedAcksInfoTest.java 85 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/util/TestStaticUtils.java 54 ●●●●● patch | view | raw | blame | history
opendj-embedded/pom.xml
@@ -38,7 +38,7 @@
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.5.33</version>
            <version>1.5.35</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/BindRule.java
@@ -142,7 +142,9 @@
    /**
     * Decode an ACI bind rule string representation.
     * @param input The string representation of the bind rule.
     * @return A BindRule class representing the bind rule.
     * @return A BindRule class representing the bind rule, or {@code null} if
     * {@code input} is null or blank. This null is the recursion base case for
     * an empty group; callers must reject it rather than dereference it.
     * @throws AciException If the string is an invalid bind rule.
     */
    public static BindRule decode (String input) throws AciException {
@@ -153,8 +155,10 @@
        String bindruleStr = input.trim();
        if (bindruleStr.isEmpty())
        {
          // A blank bind rule (e.g. "(          )") must be rejected as a
          // syntax error rather than throwing StringIndexOutOfBoundsException.
          // A blank bind rule (e.g. "(          )") decodes to null rather than
          // throwing StringIndexOutOfBoundsException on charAt(0). This null is
          // the recursion base case for an empty group; the caller rejects it as
          // a syntax error (see the bindrule_1 == null check below).
          return null;
        }
        char firstChar = bindruleStr.charAt(0);
@@ -203,6 +207,17 @@
              throw new AciException(WARN_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input));
          }
          /*
           * An empty group such as "()" or "(   )" decodes to a null bind rule.
           * Such a group is not a valid bind rule on its own, and using it as the
           * left operand of an "and"/"or" (e.g. "()or userdn=...") would build a
           * complex rule with a null left side that is later dereferenced during
           * evaluation. Reject both cases here, before the remaining-chars test,
           * so the message names the full offending input rather than a fragment.
           */
          if (bindrule_1 == null) {
            throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input));
          }
          /*
           * If there are remaining chars => there MUST be an operand (AND / OR)
           * otherwise there is a syntax error
           */
@@ -318,6 +333,16 @@
            boolean negate=determineNegation(ruleExpr);
            remainingBindrule=ruleExpr.toString();
            BindRule bindrule_2 = BindRule.decode(remainingBindrule);
            /*
             * The right operand of an "and"/"or" bind rule must exist. It is null
             * when the boolean operator is not followed by a bind rule, e.g. the
             * "or" in "userdn=\"ldap:///self\" or" has nothing to its right
             * (issue #719).
             */
            if (bindrule_2 == null) {
                throw new AciException(
                    WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr));
            }
            bindrule_2.setNegate(negate);
            return new BindRule(bindrule, bindrule_2, operand);
        }
opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/AckMsg.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.protocol;
@@ -254,5 +255,11 @@
      "concerned server ids: " + idList;
  }
  /** {@inheritDoc} */
  @Override
  public String toString()
  {
    return getClass().getSimpleName() + " csn: " + csn + ", " + errorsToString();
  }
}
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ExpectedAcksInfo.java
@@ -13,13 +13,17 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.CSN;
@@ -69,6 +73,15 @@
  protected Map<Integer,Boolean> expectedServersAckStatus = new HashMap<>();
  /**
   * Immutable snapshot of the ids of the servers we expect an ack from, taken
   * at construction time. Used for lock-free membership checks: the key set of
   * {@link #expectedServersAckStatus} never changes after construction, but
   * that map has its values mutated under lock by {@code processReceivedAck()},
   * so it must not be read concurrently without synchronization.
   */
  private final Set<Integer> expectedServerIds;
  /**
   * Facility for monitoring:
   * If the timeout occurs for the original update, we call createAck(true)
   * in the timeout code for sending back an error ack to the original server.
@@ -99,6 +112,22 @@
    {
      expectedServersAckStatus.put(serverId, false);
    }
    this.expectedServerIds =
        Collections.unmodifiableSet(new HashSet<>(expectedServers));
  }
  /**
   * Indicates whether the provided server is one of the servers an ack is
   * expected from for the matching update message. Reads an immutable snapshot
   * taken at construction time, so it is safe to call without holding the lock
   * on this object.
   *
   * @param serverId The serverId of the server.
   * @return true if an ack is expected from the provided server.
   */
  public boolean isExpectedServer(int serverId)
  {
    return expectedServerIds.contains(serverId);
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/replication/server/MessageHandler.java
@@ -80,6 +80,13 @@
  private final int maxQueueBytesSize;
  /** Specifies whether the consumer is following the producer (is not late). */
  private boolean following;
  /**
   * Specifies whether the last update message returned by
   * {@link #getNextMessage()} was re-read from the changelog DB (catch-up
   * path) rather than taken from the in-memory {@link #msgQueue}. Only ever
   * accessed by the single consumer thread calling {@link #getNextMessage()}.
   */
  private boolean lastMessageFromLateQueue;
  /** Specifies the current serverState of this handler. */
  private ServerState serverState;
  /** Specifies the baseDN of the domain. */
@@ -226,6 +233,21 @@
  }
  /**
   * Indicates whether the last update message returned by
   * {@link #getNextMessage()} was re-read from the changelog DB (catch-up
   * path) rather than taken from the in-memory queue.
   * <p>
   * Must only be called from the consumer thread calling
   * {@link #getNextMessage()}.
   *
   * @return true if the last returned update message came from the late queue
   */
  protected boolean isLastMessageFromLateQueue()
  {
    return lastMessageFromLateQueue;
  }
  /**
   * Retrieves the name of this monitor provider.  It should be unique among all
   * monitor providers, including all instances of the same monitor provider.
   *
@@ -319,6 +341,9 @@
                msgQueue.consumeUpTo(msg);
                if (updateServerState(msg))
                {
                  // the returned instance is the one re-read from the
                  // changelog DB, not its msgQueue equivalent
                  lastMessageFromLateQueue = true;
                  return msg;
                }
              }
@@ -347,6 +372,7 @@
          }
          if (updateServerState(msg))
          {
            lastMessageFromLateQueue = true;
            return msg;
          }
          continue;
@@ -379,6 +405,7 @@
             * by the other server.
             * Otherwise just loop to select the next message.
             */
            lastMessageFromLateQueue = false;
            return msg;
          }
        }
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServer.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyrighted 2026 3A Systems, LLC.
 */
package org.opends.server.replication.server;
@@ -21,6 +22,7 @@
import static org.opends.server.util.StaticUtils.*;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
@@ -414,6 +416,15 @@
      }
      int timeoutMS = MultimasterReplication.getConnectionTimeoutMS();
      socket.connect(remoteServerAddress.toInetSocketAddress(), timeoutMS);
      if (isSelfConnection(socket))
      {
        // While the remote RS is down, the kernel may pick its port as the
        // local port of this connecting socket (TCP simultaneous open),
        // "connecting" it to itself. Keeping such a socket open would hold
        // the port and prevent the RS from binding it on restart.
        throw new ConnectException("Connection to " + remoteServerAddress
            + " is a TCP self-connect, no replication server is listening");
      }
      session = replSessionSecurity.createClientSession(socket, timeoutMS);
      ReplicationServerHandler rsHandler = new ReplicationServerHandler(
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServerDomain.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
@@ -789,6 +790,20 @@
  }
  /**
   * Indicates whether the ack window for the provided CSN is still open and
   * the provided server is one of the servers an ack is expected from.
   *
   * @param csn The CSN of the update message.
   * @param serverId The serverId of the candidate acknowledging server.
   * @return true if an ack from the provided server would be accounted for.
   */
  boolean isExpectedAck(CSN csn, int serverId)
  {
    final ExpectedAcksInfo expectedAcksInfo = waitingAcks.get(csn);
    return expectedAcksInfo != null && expectedAcksInfo.isExpectedServer(serverId);
  }
  /**
   * Process an ack received from a given server.
   *
   * @param ack The ack message received.
@@ -1722,6 +1737,14 @@
        this.generationId = generationId;
        this.generationIdSavedStatus = false;
        // generationId gossip is purely event-driven: it only travels in the
        // topology messages sent on connect/disconnect/status events. Re-advertise
        // on every real transition so a peer that missed one converges on the next.
        if (generationId > 0)
        {
          sendTopoInfoToAll();
        }
      }
      return oldGenerationId;
    }
opendj-server-legacy/src/main/java/org/opends/server/replication/server/SafeDataExpectedAcksInfo.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
@@ -84,7 +85,19 @@
    // Get the ack status for the matching server
    int ackingServerId = ackingServer.getServerId();
    boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
    Boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
    if (ackReceived == null)
    {
      // Ack from a server we were not expecting an ack from, for instance
      // because the update was delivered to it with the assured flag of the
      // original sender through the changelog catch-up path: ignore it.
      if (logger.isTraceEnabled())
      {
        logger.trace("Received ack from not expected server id: " +
          ackingServerId + " ack message: " + ackMsg);
      }
      return false;
    }
    if (ackReceived)
    {
      // Sanity check: this should never happen
opendj-server-legacy/src/main/java/org/opends/server/replication/server/SafeReadExpectedAcksInfo.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
@@ -146,7 +147,19 @@
  {
    // Get the ack status for the matching server
    int ackingServerId = ackingServer.getServerId();
    boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
    Boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
    if (ackReceived == null)
    {
      // Ack from a server we were not expecting an ack from, for instance
      // because the update was delivered to it with the assured flag of the
      // original sender through the changelog catch-up path: ignore it.
      if (logger.isTraceEnabled())
      {
        logger.trace("Received ack from not expected server id: "
          + ackingServerId + " ack message: " + ackMsg);
      }
      return false;
    }
    if (ackReceived)
    {
      // Sanity check: this should never happen
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerHandler.java
@@ -13,12 +13,15 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@@ -924,12 +927,32 @@
   */
  public UpdateMsg take() throws ChangelogException
  {
    final UpdateMsg msg = getNextMessage();
    UpdateMsg msg = getNextMessage();
    final boolean fromLateQueue = isLastMessageFromLateQueue();
    acquirePermitInSendWindow();
    if (msg != null)
    {
      // Updates re-read from the changelog DB (catch-up path) carry the
      // assured flag, mode and safe data level of their original sender:
      // the NotAssuredUpdateMsg substitution performed at publish time by
      // ReplicationServerDomain.addUpdate() only exists on the in-memory
      // queue path. Normalize them here: keep the assured flag only while
      // the ack window is still open and this server is expected to ack.
      // Messages taken from the in-memory queue already carry the
      // publish-time decision and must NOT be revisited: the ack window may
      // legitimately close (timeout, or enough acks already received)
      // before a slow peer gets here - e.g. when acquirePermitInSendWindow()
      // above blocks on a closed send window - and such a peer must still
      // receive the assured flag it was deemed eligible for. Its late ack is
      // then safely ignored by ReplicationServerDomain.processAck() and
      // ExpectedAcksInfo.processReceivedAck().
      if (fromLateQueue && msg.isAssured()
          && !replicationServerDomain.isExpectedAck(msg.getCSN(), serverId))
      {
        msg = toNotAssuredUpdateMsg(msg);
      }
      incrementOutCount();
      if (msg.isAssured())
      {
@@ -940,6 +963,37 @@
    return null;
  }
  /**
   * Substitutes a not assured version of the provided update message so that a
   * peer not expected to acknowledge it does not receive it with the assured
   * flag.
   * <p>
   * This is the counterpart, for the changelog catch-up path, of the
   * {@link NotAssuredUpdateMsg} substitution performed on the in-memory queue
   * path by ReplicationServerDomain.addUpdate(): updates re-read from the
   * changelog DB keep the assured flag of their original sender and must be
   * normalized in {@link #take()} before being handed to the ServerWriter.
   */
  private UpdateMsg toNotAssuredUpdateMsg(UpdateMsg msg)
  {
    try
    {
      return new NotAssuredUpdateMsg(msg);
    }
    catch (UnsupportedEncodingException e)
    {
      // Could not build the not assured form (unexpected message encoding).
      // Deliver the original message rather than dropping it: losing the
      // update would break replication consistency, which is worse than a
      // spurious ack - and such an ack is now safely ignored by
      // ExpectedAcksInfo.processReceivedAck().
      logger.error(LocalizableMessage.raw(
          "Could not substitute a not assured version of update message %s: %s",
          msg, stackTraceToSingleLineString(e)));
      return msg;
    }
  }
  private void acquirePermitInSendWindow()
  {
    boolean acquired = false;
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2023-2025 3A Systems LLC.
 * Portions Copyright 2023-2026 3A Systems LLC.
 * Portions Copyright 2025 Wren Security.
 */
package org.opends.server.replication.service;
@@ -1083,6 +1083,15 @@
      }
      int timeoutMS = MultimasterReplication.getConnectionTimeoutMS();
      socket.connect(HostPort.valueOf(serverURL).toInetSocketAddress(), timeoutMS);
      if (isSelfConnection(socket))
      {
        // While the RS is down, the kernel may pick the RS port as the local
        // port of this reconnecting socket (TCP simultaneous open),
        // "connecting" it to itself. Keeping such a socket open would hold
        // the RS port and prevent the RS from binding it on restart.
        throw new ConnectException("Connection to " + serverURL
            + " is a TCP self-connect, no replication server is listening");
      }
      newSession = replSessionSecurity.createClientSession(socket, timeoutMS);
      boolean isSslEncryption = replSessionSecurity.isSslEncryption();
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java
@@ -1442,8 +1442,6 @@
      int serverRunningTheTask, Task initTask, int initWindow)
  throws DirectoryException
  {
    final ImportExportContext ieCtx = acquireIEContext(false);
    /*
    We manage the list of servers to initialize in order :
    - to test at the end that all expected servers have reconnected
@@ -1451,7 +1449,12 @@
    - to update the task with the server(s) where this test failed
    */
    Map<Integer, DSInfo> replicaInfos = getReplicaInfos();
    // Validate the request before acquiring the import/export context: a
    // validation failure must not leave the context acquired, otherwise
    // ieRunning() would remain true forever and the domain would reject every
    // subsequent total update as a simultaneous import/export.
    final Map<Integer, DSInfo> replicaInfos = getReplicaInfos();
    final DSInfo targetDsi;
    if (serverToInitialize == RoutableMsg.ALL_SERVERS)
    {
      if (replicaInfos.isEmpty())
@@ -1459,7 +1462,44 @@
        throw new DirectoryException(UNWILLING_TO_PERFORM,
            ERR_FULL_UPDATE_NO_REMOTES.get(getBaseDN(), getServerId()));
      }
      targetDsi = null;
    }
    else
    {
      targetDsi = getDsInfoOrNull(replicaInfos.values(), serverToInitialize);
      if (targetDsi == null)
      {
        throw new DirectoryException(UNWILLING_TO_PERFORM,
            ERR_FULL_UPDATE_MISSING_REMOTE.get(getBaseDN(), getServerId(), serverToInitialize));
      }
    }
    final ImportExportContext ieCtx = acquireIEContext(false);
    try
    {
      initializeRemote(ieCtx, replicaInfos, targetDsi, serverToInitialize,
          serverRunningTheTask, initTask, initWindow);
    }
    finally
    {
      // Release the context whatever the outcome, otherwise ieRunning() would
      // remain true forever (resolves the historical "FIXME should not this
      // be in a finally?").
      releaseIEContext();
    }
  }
  /**
   * Performs the remote initialization with the import/export context already
   * acquired - and released - by the caller.
   */
  private void initializeRemote(ImportExportContext ieCtx,
      Map<Integer, DSInfo> replicaInfos, DSInfo targetDsi,
      int serverToInitialize, int serverRunningTheTask, Task initTask,
      int initWindow) throws DirectoryException
  {
    if (serverToInitialize == RoutableMsg.ALL_SERVERS)
    {
      logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START_ALL,
          countEntries(), getBaseDN(), getServerId());
@@ -1475,18 +1515,11 @@
    }
    else
    {
      DSInfo dsi = getDsInfoOrNull(replicaInfos.values(), serverToInitialize);
      if (dsi == null)
      {
        throw new DirectoryException(UNWILLING_TO_PERFORM,
            ERR_FULL_UPDATE_MISSING_REMOTE.get(getBaseDN(), getServerId(), serverToInitialize));
      }
      logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START, countEntries(),
          getBaseDN(), getServerId(), serverToInitialize);
      ieCtx.startList.add(serverToInitialize);
      ieCtx.setAckVal(dsi.getDsId(), 0);
      ieCtx.setAckVal(targetDsi.getDsId(), 0);
    }
    DirectoryException exportRootException = null;
@@ -1625,9 +1658,6 @@
              ERR_INIT_NO_SUCCESS_END_FROM_SERVERS.get(getGenerationID(), ieCtx.failureList));
    }
    // Don't forget to release IEcontext acquired at beginning.
    releaseIEContext(); // FIXME should not this be in a finally?
    final String cause = exportRootException == null ? ""
        : exportRootException.getLocalizedMessage();
    if (serverToInitialize == RoutableMsg.ALL_SERVERS)
opendj-server-legacy/src/main/java/org/opends/server/util/StaticUtils.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyrighted 2026 3A Systems, LLC.
 */
package org.opends.server.util;
@@ -1319,7 +1320,25 @@
    return true;
  }
  /**
   * Indicates whether the provided connected socket is connected to itself,
   * i.e. its local and remote endpoints are identical.
   * <p>
   * Connecting to a local port from the TCP ephemeral range while nothing
   * listens on it can make the kernel pick that very port as the local port
   * of the connecting socket: TCP simultaneous open then "establishes" the
   * connection to itself (observed on Linux). Such a socket occupies the
   * listen port its target service is about to bind, so callers retrying
   * connections to a temporarily stopped service must detect and close it.
   *
   * @param socket a connected socket
   * @return true if the socket is connected to itself
   */
  public static boolean isSelfConnection(Socket socket)
  {
    return socket.getLocalPort() == socket.getPort()
        && socket.getLocalAddress().equals(socket.getInetAddress());
  }
  /**
   * Returns a lower-case string representation of a given string, verifying for null input string.
opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/BindRuleOperandTest.java
New file
@@ -0,0 +1,122 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.authorization.dseecompat;
import static org.assertj.core.api.Assertions.*;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
 * Verifies that {@link BindRule#decode(String)} rejects a boolean ("and"/"or")
 * bind rule that is missing one of its operands, instead of throwing a
 * {@link NullPointerException}. An empty group such as "()" decodes to a null
 * bind rule; before this fix, using such a group as the left side, or leaving
 * the right side of the boolean empty (e.g. "(()or)"), dereferenced that null
 * while parsing and failed with an NPE rather than a diagnosable
 * {@link AciException} (issue #719).
 */
@SuppressWarnings("javadoc")
public class BindRuleOperandTest extends DirectoryServerTestCase
{
  @BeforeClass
  public void setUp() throws Exception
  {
    TestCaseUtils.startFakeServer();
  }
  @AfterClass
  public void tearDown() throws DirectoryException
  {
    TestCaseUtils.shutdownFakeServer();
  }
  @DataProvider(name = "bindRulesWithMissingOperand")
  public Object[][] bindRulesWithMissingOperand()
  {
    return new Object[][] {
      // the exact bind rule from issue #719: "or" with no right operand and an
      // empty left group
      { "(()or)" },
      // empty right operand after a valid left operand
      { "(userdn=\"ldap:///self\" or)" },
      { "(userdn=\"ldap:///self\" and)" },
      // empty left operand before a valid right operand
      { "()or userdn=\"ldap:///self\"" },
      // an empty group is not a bind rule on its own
      { "()" },
      // a nested empty group is still empty
      { "(())" },
      // a whitespace-only group is empty once trimmed
      { "(   )" },
      // empty right *group*: reaches the decode-level guard from createBindRule
      { "(userdn=\"ldap:///self\" or ())" },
      // "not" applied to an empty group
      { "not ()" },
    };
  }
  @Test(dataProvider = "bindRulesWithMissingOperand")
  public void rejectsMissingOperand(String bindRule)
  {
    assertThatThrownBy(() -> BindRule.decode(bindRule)).isInstanceOf(AciException.class);
  }
  @DataProvider(name = "validBindRules")
  public Object[][] validBindRules()
  {
    return new Object[][] {
      // a valid rule inside a nested group must still decode: guards must not
      // start over-rejecting non-empty groups
      { "((userdn=\"ldap:///self\"))" },
      // a valid complex rule with both operands present
      { "(userdn=\"ldap:///self\" or userdn=\"ldap:///anyone\")" },
    };
  }
  @Test(dataProvider = "validBindRules")
  public void acceptsValidBindRule(String bindRule) throws Exception
  {
    assertThat(BindRule.decode(bindRule)).isNotNull();
  }
  @DataProvider(name = "acisWithMissingOperand")
  public Object[][] acisWithMissingOperand()
  {
    return new Object[][] {
      // the full ACI from issue #719
      { "(version 3.0; acl \"ac\"; allow (search)(()or) (userdn=\"ldap:///self\"); )" },
      // missing *left* operand: previously accepted and stored with left == null,
      // then NPEd during evaluation. Must now be rejected at decode time.
      { "(version 3.0; acl \"ac\"; allow (search) ()or userdn=\"ldap:///self\"; )" },
    };
  }
  /** Reproduces missing-operand ACIs from issue #719 through {@link Aci#decode}. */
  @Test(dataProvider = "acisWithMissingOperand")
  public void decodeOfAciWithMissingOperandThrowsAciException(String aci)
  {
    assertThatThrownBy(() -> Aci.decode(ByteString.valueOfUtf8(aci), DN.rootDN()))
        .isInstanceOf(AciException.class);
  }
}
opendj-server-legacy/src/test/java/org/opends/server/core/BindOperationTestCase.java
@@ -45,9 +45,11 @@
import org.opends.server.types.Control;
import org.opends.server.types.Operation;
import org.opends.server.types.OperationType;
import org.opends.server.util.TestTimer;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.util.concurrent.TimeUnit.*;
import static org.assertj.core.api.Assertions.*;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
@@ -1527,7 +1529,16 @@
        };
      assertEquals(LDAPDelete.run(nullPrintStream(), System.err, args), 0);
      assertNull(DirectoryServer.getAuthenticatedUsers().get(userDN));
      // AuthenticatedUsers is a POST_RESPONSE plugin: the registry is updated
      // after the client already received the response, so poll briefly.
      newAuthInfoTimer().repeatUntilSuccess(new TestTimer.CallableVoid()
      {
        @Override
        public void call() throws Exception
        {
          assertNull(DirectoryServer.getAuthenticatedUsers().get(userDN));
        }
      });
    }
    finally
    {
@@ -1591,9 +1602,18 @@
        };
      assertEquals(LDAPModify.run(nullPrintStream(), System.err, args), 0);
      DN newUserDN = DN.valueOf("uid=test,ou=users,dc=example,dc=com");
      assertNotNull(DirectoryServer.getAuthenticatedUsers().get(newUserDN));
      assertEquals(DirectoryServer.getAuthenticatedUsers().get(newUserDN).size(), 1);
      final DN newUserDN = DN.valueOf("uid=test,ou=users,dc=example,dc=com");
      // AuthenticatedUsers is a POST_RESPONSE plugin: the registry is updated
      // after the client already received the response, so poll briefly.
      newAuthInfoTimer().repeatUntilSuccess(new TestTimer.CallableVoid()
      {
        @Override
        public void call() throws Exception
        {
          assertNotNull(DirectoryServer.getAuthenticatedUsers().get(newUserDN));
          assertEquals(DirectoryServer.getAuthenticatedUsers().get(newUserDN).size(), 1);
        }
      });
    }
    finally
    {
@@ -1602,6 +1622,19 @@
  }
  /**
   * Timer to await the asynchronous {@link AuthenticatedUsers} registry
   * update: it is performed by a POST_RESPONSE plugin, i.e. after the client
   * already received the operation response.
   */
  private TestTimer newAuthInfoTimer()
  {
    return new TestTimer.Builder()
        .maxSleep(10, SECONDS)
        .sleepTimes(100, MILLISECONDS)
        .toTimer();
  }
  /**
   * Tests to ensure that the "ignore" password policy state update policy
   * works as expected.
   *
opendj-server-legacy/src/test/java/org/opends/server/replication/InitOnLineTest.java
@@ -656,6 +656,7 @@
      }
      // Launch in S1 the task that will initialize S2
      waitForRemoteReplicas(server2ID);
      addTask(taskInitTargetS2, ResultCode.SUCCESS, null);
      // Signal RS we just entered the full update status
@@ -714,6 +715,7 @@
      }
      // Launch in S1 the task that will initialize S2
      waitForRemoteReplicas(server2ID, server3ID);
      addTask(taskInitTargetAll, ResultCode.SUCCESS, null);
      // Tests that entries have been received by S2
@@ -1004,6 +1006,7 @@
      // Launch in S1 the task that will initialize S2
      log(testCase + " add task " + Thread.currentThread());
      waitForRemoteReplicas(server2ID);
      addTask(taskInitTargetS2, ResultCode.SUCCESS, null);
      log(testCase + " " + server2.getServerId() + " wait target " + Thread.currentThread());
@@ -1027,6 +1030,47 @@
    }
  }
  /**
   * Tests that an InitializeTarget task failing its validation (the remote
   * replica is unknown to the domain) does not leave the import/export
   * context acquired (issue #730): the domain used to reject every subsequent
   * total update as a simultaneous import/export.
   */
  @Test(enabled=true)
  public void initializeTargetUnknownRemote() throws Exception
  {
    String testCase = "initializeTargetUnknownRemote";
    log("Starting " + testCase);
    try
    {
      replServer1 = createReplicationServer(replServer1ID, testCase);
      connectServer1ToReplServer(replServer1ID);
      addTestEntriesToDB();
      // DS(42424) is unknown in the topology: the task must fail to start...
      Entry taskInitTargetUnknown = TestCaseUtils.makeEntry(
          "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
          "objectclass: top",
          "objectclass: ds-task",
          "objectclass: ds-task-initialize-remote-replica",
          "ds-task-class-name: org.opends.server.tasks.InitializeTargetTask",
          "ds-task-initialize-domain-dn: " + EXAMPLE_DN,
          "ds-task-initialize-replica-server-id: " + 42424);
      addTask(taskInitTargetUnknown, ResultCode.SUCCESS, null);
      waitTaskState(taskInitTargetUnknown, STOPPED_BY_ERROR, 20000, null);
      // ...but must not leave the import/export context acquired
      assertFalse(replDomain.ieRunning(),
          "ReplicationDomain: Import/Export is not expected to be running after a failed InitializeTarget");
      log("Successfully ending " + testCase);
    }
    finally
    {
      afterTest(testCase);
    }
  }
  private void waitForInitializeTargetMsg(String testCase,
      ReplicationBroker server) throws Exception
  {
@@ -1293,9 +1337,30 @@
   * Disconnect broker and remove entries from the local DB
   * @param testCase The name of the test case.
   */
  /**
   * Waits until the local replication domain sees the provided replicas in
   * its topology view, so that an InitializeTarget task does not race the
   * topology propagation and fail to start with "the remote directory server
   * DS(x) is unknown" - which, combined with the import/export context leak
   * (issue #730), used to poison the domain for the rest of the test class.
   */
  private void waitForRemoteReplicas(Integer... serverIds) throws Exception
  {
    for (int serverId : serverIds)
    {
      for (int i = 0; i < 100 && !replDomain.getReplicaInfos().containsKey(serverId); i++)
      {
        sleep(100);
      }
      assertTrue(replDomain.getReplicaInfos().containsKey(serverId),
          "DS(" + serverId + ") is not known to the local replication domain");
    }
  }
  private void afterTest(String testCase) throws Exception
  {
    // Check that the domain has completed the import/export task.
    boolean ieStillRunning = false;
    if (replDomain != null)
    {
      // race condition could cause the main thread to reach
@@ -1309,7 +1374,10 @@
        }
        sleep(500);
      }
      assertFalse(replDomain.ieRunning(), "ReplicationDomain: Import/Export is not expected to be running");
      // asserted only after the cleanup below: failing before it would leak
      // the domain config, the brokers and the replication servers into the
      // following tests and cascade the failure over the whole class
      ieStillRunning = replDomain.ieRunning();
    }
    // Remove domain config
    super.cleanConfigEntries();
@@ -1330,6 +1398,8 @@
    Arrays.fill(replServerPort, 0);
    log("Successfully cleaned " + testCase);
    assertFalse(ieStillRunning, "ReplicationDomain: Import/Export is not expected to be running");
  }
  /**
opendj-server-legacy/src/test/java/org/opends/server/replication/ReplicationTestCase.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyrighted 2026 3A Systems, LLC.
 */
package org.opends.server.replication;
@@ -726,11 +727,13 @@
      }
    }
    if (expectedTaskState == RUNNING && taskState == COMPLETED_SUCCESSFULLY)
    if (expectedTaskState == RUNNING
        && (taskState == COMPLETED_SUCCESSFULLY || taskState == STOPPED_BY_ERROR))
    {
      // We usually wait the running state after adding the task
      // and if the task is fast enough then it may be already done
      // and we can go on.
      // and if the task is fast enough then it may already be done
      // (successfully or not) and we can go on: callers interested in the
      // final state wait for it explicitly afterwards.
    }
    else
    {
opendj-server-legacy/src/test/java/org/opends/server/replication/server/AssuredReplicationServerTest.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC
 * Portions Copyright 2023-2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
@@ -51,6 +51,7 @@
import org.opends.server.TestCaseUtils;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.CSNGenerator;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
@@ -581,7 +582,7 @@
     * Sends a new update from this DS.
     * @throws TimeoutException If timeout waiting for an assured ack
     */
    private void sendNewFakeUpdate() throws TimeoutException
    private CSN sendNewFakeUpdate() throws TimeoutException
    {
      // Create a new delete update message (the simplest to create)
      DeleteMsg delMsg = new DeleteMsg(getBaseDN(), gen.newCSN(), UUID.randomUUID().toString());
@@ -590,6 +591,7 @@
      prepareWaitForAckIfAssuredEnabled(delMsg);
      publish(delMsg);
      waitForAckIfAssuredEnabled(delMsg);
      return delMsg.getCSN();
    }
    private void assertReceivedWrongUpdates(int expectedNbUpdates, int expectedNbWrongUpdates)
@@ -751,9 +753,14 @@
    private CSNGenerator gen;
    /** False if a received update had assured parameters not as expected. */
    private boolean everyUpdatesAreOk = true;
    /** Number of received updates. */
    private int nReceivedUpdates;
    private volatile boolean everyUpdatesAreOk = true;
    /**
     * Number of received updates. Volatile as it is incremented by the listen
     * thread and polled from the test thread (waitForReceivedUpdates()); a read
     * observing the incremented value also publishes everyUpdatesAreOk, which
     * is written before the increment.
     */
    private volatile int nReceivedUpdates;
    /**
     * True if an ack has been replied to a received assured update (in assured
@@ -1076,6 +1083,110 @@
  }
  /**
   * Parameters for {@link #testCatchUpClearsAssuredFlag}: assured mode and
   * safe data level of the update a peer catches up from the changelog.
   */
  @DataProvider(name = "catchUpAssuredModeProvider")
  private Object[][] catchUpAssuredModeProvider()
  {
    return new Object[][]
    {
      { AssuredMode.SAFE_DATA_MODE, 1 },
      { AssuredMode.SAFE_DATA_MODE, 2 },
      { AssuredMode.SAFE_READ_MODE, 1 },
    };
  }
  /**
   * Regression test for issue #710: an update served to a peer through the
   * changelog catch-up path must not keep the assured flag of its original
   * sender.
   * <p>
   * A fake RS connects with an empty server state after an assured update has
   * already been received and persisted by the real RS. As the fake RS was not
   * connected when the update was received, the update is re-read from the
   * changelog DB (catch-up path) rather than served from the in-memory queue,
   * and the fake RS is not an expected ack server for it. The update must
   * therefore be forwarded with its assured flag cleared (while its assured
   * mode and safe data level are preserved). This used to be delivered with
   * assured=true, making the fake RS reply a spurious ack. Exercised across
   * safe data (level 1 and level > 1) and safe read modes.
   */
  @Test(dataProvider = "catchUpAssuredModeProvider", enabled = true)
  public void testCatchUpClearsAssuredFlag(AssuredMode assuredMode,
      int safeDataLevel) throws Exception
  {
    String testCase = "testCatchUpClearsAssuredFlag";
    debugInfo("Starting " + testCase + " mode=" + assuredMode + " sdl=" + safeDataLevel);
    initTest();
    try
    {
      // Real RS to be tested
      rs1 = createReplicationServer(RS1_ID, DEFAULT_GID, SMALL_TIMEOUT, testCase, 0);
      // Main DS sends an assured update, acknowledged by the RS. No other peer
      // is connected yet, so the update only lands in the changelog DB, not in
      // any peer message queue.
      fakeRDs[1] = createFakeReplicationDomain(FDS1_ID, DEFAULT_GID, RS1_ID,
          DEFAULT_GENID, assuredMode, safeDataLevel, LONG_TIMEOUT, TIMEOUT_DS_SCENARIO);
      CSN csn = fakeRDs[1].sendNewFakeUpdate();
      // Ensure the update is persisted before the peer connects, so the peer is
      // served through the changelog catch-up path (the RS acks the DS before
      // persisting, so returning from sendNewFakeUpdate() does not guarantee it).
      waitForChangePersisted(rs1, csn);
      // A fake RS connects with an empty state: it must catch up the historical
      // update from the changelog DB. It expects a non-assured forward and
      // never replies an ack (TIMEOUT_RS_SCENARIO).
      fakeRs1 = createFakeReplicationServer(FRS1_ID, DEFAULT_GID, DEFAULT_GENID,
          false, assuredMode, safeDataLevel, TIMEOUT_RS_SCENARIO);
      // The historical update must have been received, with assured flag off
      waitForReceivedUpdates(fakeRs1, 1);
      fakeRs1.assertReceivedUpdates(1);
    }
    finally
    {
      endTest();
    }
  }
  /**
   * Waits until the provided change has been persisted in the changelog of the
   * provided replication server, i.e. is retrievable through the catch-up path.
   */
  private void waitForChangePersisted(ReplicationServer rs, CSN csn) throws Exception
  {
    ReplicationServerDomain domain =
        rs.getReplicationServerDomain(DN.valueOf(TEST_ROOT_DN_STRING));
    assertNotNull(domain);
    int i = 0;
    while (!domain.getLatestServerState().cover(csn))
    {
      if (i++ > 50)
      {
        Assert.fail("Change " + csn + " was not persisted in the changelog in time.");
      }
      Thread.sleep(100);
    }
  }
  /**
   * Waits until the provided fake RS has received at least the expected number
   * of updates, so the assertion on the updates does not race with delivery.
   */
  private void waitForReceivedUpdates(FakeReplicationServer fakeRs, int expected)
      throws Exception
  {
    int i = 0;
    while (fakeRs.nReceivedUpdates < expected && i++ <= 50)
    {
      Thread.sleep(100);
    }
  }
  /**
   * Returns possible combinations of parameters for testSafeDataLevelOne test.
   */
  @DataProvider(name = "testSafeDataLevelOneProvider")
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ExpectedAcksInfoTest.java
New file
@@ -0,0 +1,85 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Portions Copyright 2026 3A Systems, LLC
 */
package org.opends.server.replication.server;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.protocol.AckMsg;
import org.testng.annotations.Test;
/**
 * Test the handling of acks received from servers that were not expected to
 * acknowledge an assured update.
 * <p>
 * A peer served through the changelog catch-up path receives the update with
 * the assured flag of the original sender (see issue #710), so it may send
 * back an ack for a server id that is not part of the expected servers of the
 * matching {@link ExpectedAcksInfo}. That ack must be ignored instead of
 * raising a {@link NullPointerException} while auto-unboxing the missing map
 * entry, which would close the connection to the acking peer.
 */
@SuppressWarnings("javadoc")
public class ExpectedAcksInfoTest extends DirectoryServerTestCase
{
  private static final CSN CSN1 = new CSN(1, 1, 10);
  private static ServerHandler mockServerHandler(int serverId, boolean isDataServer)
  {
    ServerHandler handler = mock(ServerHandler.class);
    when(handler.getServerId()).thenReturn(serverId);
    when(handler.isDataServer()).thenReturn(isDataServer);
    return handler;
  }
  @Test
  public void safeReadAckFromNotExpectedServerIsIgnored()
  {
    ServerHandler requester = mockServerHandler(1, true);
    List<Integer> expectedServers = Arrays.asList(2, 3);
    SafeReadExpectedAcksInfo acksInfo = new SafeReadExpectedAcksInfo(
        CSN1, requester, expectedServers, Collections.<Integer> emptyList());
    // Server id 99 is not in the expected servers list
    ServerHandler notExpected = mockServerHandler(99, false);
    assertFalse(acksInfo.processReceivedAck(notExpected, new AckMsg(CSN1)),
        "An ack from a not expected server must not complete the ack info");
    assertFalse(acksInfo.isExpectedServer(99));
    assertTrue(acksInfo.isExpectedServer(2));
  }
  @Test
  public void safeDataAckFromNotExpectedServerIsIgnored()
  {
    ServerHandler requester = mockServerHandler(1, true);
    List<Integer> expectedServers = Arrays.asList(2, 3);
    SafeDataExpectedAcksInfo acksInfo = new SafeDataExpectedAcksInfo(
        CSN1, requester, (byte) 3, expectedServers);
    // Server id 99 is a RS not in the expected servers list
    ServerHandler notExpected = mockServerHandler(99, false);
    assertFalse(acksInfo.processReceivedAck(notExpected, new AckMsg(CSN1)),
        "An ack from a not expected server must not complete the ack info");
    assertFalse(acksInfo.isExpectedServer(99));
    assertTrue(acksInfo.isExpectedServer(3));
  }
}
opendj-server-legacy/src/test/java/org/opends/server/util/TestStaticUtils.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyrighted 2026 3A Systems, LLC.
 */
package org.opends.server.util;
@@ -26,6 +27,9 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
@@ -36,6 +40,7 @@
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.TestCaseUtils;
import org.testng.Assert;
import org.testng.SkipException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -1046,4 +1051,53 @@
        new RuntimeException(new IllegalThreadStateException()), IllegalArgumentException.class);
    Assert.assertTrue(hasCause, "Third case : IllegalThreadStateException should be detected as a cause");
  }
  @Test
  public void testIsSelfConnectionFalseForNormalConnection() throws Exception
  {
    try (ServerSocket listener = new ServerSocket())
    {
      listener.bind(new InetSocketAddress("127.0.0.1", 0));
      try (Socket client = new Socket())
      {
        client.connect(listener.getLocalSocketAddress(), 2000);
        try (Socket accepted = listener.accept())
        {
          Assert.assertFalse(StaticUtils.isSelfConnection(client));
          Assert.assertFalse(StaticUtils.isSelfConnection(accepted));
        }
      }
    }
  }
  @Test
  public void testIsSelfConnectionTrueForSelfConnectedSocket() throws Exception
  {
    // find a free port
    final int port;
    try (ServerSocket tmp = new ServerSocket())
    {
      tmp.bind(new InetSocketAddress("127.0.0.1", 0));
      port = tmp.getLocalPort();
    }
    // Force the TCP self-connect (simultaneous open) that the kernel can
    // produce spontaneously when connecting to an unbound port from the
    // ephemeral range: bind the local end to the very port being connected.
    try (Socket socket = new Socket())
    {
      socket.setReuseAddress(true);
      socket.bind(new InetSocketAddress("127.0.0.1", port));
      try
      {
        socket.connect(new InetSocketAddress("127.0.0.1", port), 2000);
      }
      catch (IOException e)
      {
        // BSD-based stacks (macOS) refuse an explicit self-connect
        throw new SkipException("TCP self-connect not supported by this OS: " + e);
      }
      Assert.assertTrue(StaticUtils.isSelfConnection(socket));
    }
  }
}