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

Valery Kharseko
2 days ago 41f5692c778b797fe09b5a658af37e4bc1da83ad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
/*
 * 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.replication.server;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.util.CollectionUtils.newArrayList;
import static org.opends.server.util.CollectionUtils.newTreeSet;
import static org.opends.server.util.StaticUtils.close;
 
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
 
import org.forgerock.opendj.ldap.DN;
import org.opends.server.TestCaseUtils;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.ReplServerStartMsg;
import org.opends.server.replication.protocol.ReplSessionSecurity;
import org.opends.server.replication.protocol.Session;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.types.HostPort;
import org.testng.annotations.Test;
 
/**
 * Reproducer for issue #1017: a replication server which is reachable at more than one
 * address -- a multi homed host, or a NAT where the address a peer connects <i>from</i> is
 * not the address it is configured <i>as</i> -- used to be recognised by the address the
 * socket of one of its sessions happened to carry.
 * <p>
 * {@code ReplicationServerHandler.toServerAddressURL()}, which this change removes, took
 * the host of a handler from {@code session.getRemoteAddress()} and its port from the start
 * message that handler received, so the address a peer was registered under was an artefact
 * of which interface its connection used, not an identity. Two things followed, and the
 * tests below drive both:
 * the already connected test of {@code runConnect()} compared a configured address against
 * one which never matches it, so the peer was dialled again on every pass, about once a
 * second, for as long as it stayed where it was; and the handshake offered to that same
 * peer ran into a handler holding its server id under another address URL, which is
 * {@code ERR_DUPLICATE_REPLICATION_SERVER_ID} -- an error logged with no throttle for a
 * topology which is merely multi homed.
 * <p>
 * The fixture is the mechanism itself rather than a stand in for it: the peer dials the
 * server under test over the loopback interface and names another address in its start
 * message, which is what a peer behind a NAT looks like to the server it dials, and the
 * session the server dials that other address on reports the address it dialled, which is
 * what reaching the same peer at its configured address gives. The addresses named are
 * documentation addresses (RFC 5737): nothing answers at them, which is what the connect
 * thread of the server under test finds when it dials one -- a host stack sends that SYN to
 * its default route, and the connect fails, once per six passes -- a failed dial is
 * blacklisted for the five passes after it -- until the peer has registered.
 */
@SuppressWarnings("javadoc")
public class MultiHomedPeerTest extends ReplicationTestCase
{
  private static final int SOCKET_TIMEOUT_MS = 30000;
  /** How long the registration of the fake peer is waited for, in milliseconds. */
  private static final long REGISTRATION_TIMEOUT_MS = 30000;
 
  private static final int RS_ID = 8251;
  private static final int PEER_RS_ID = 8252;
  /** The server id of the second fake peer, which only the removal case registers. */
  private static final int SECOND_PEER_RS_ID = 8253;
 
  /** TEST-NET-1: the address the peer is configured under and answers on. */
  private static final byte[] PEER_ADDRESS = { (byte) 192, 0, 2, 1 };
  /** TEST-NET-2: the address of a second, genuinely different server. */
  private static final byte[] OTHER_ADDRESS = { (byte) 198, 51, 100, 1 };
  /**
   * A name under the .invalid top level domain (RFC 6761), which resolvers answer does not
   * exist: the host name of a peer which this server has no way to resolve, and what
   * {@code ReplicationServer.setServerURL()} falls back to naming.
   */
  private static final String UNRESOLVABLE_HOST = "nonexistent.invalid";
 
  /**
   * Tests that a peer which dialled this server from an address it is not configured under
   * is still found by the address it <i>is</i> configured under.
   * <p>
   * This is what stops the connect thread from dialling it on every pass: the address the
   * handler is registered under is the loopback address the peer's own connection came
   * from, and the only address which can match the configured one is the address the peer
   * named in its start message.
   * <p>
   * Both halves are asserted, because the predicate and the call site which reads it fail
   * apart: what the connect thread does with a peer it finds connected is reported by the
   * outage it closes, and an outage is recorded here for it to close.
   */
  @Test
  public void aPeerRegisteredUnderTheAddressItDialledFromIsFoundByItsConfiguredAddress()
      throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = documentationAddress(PEER_ADDRESS, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerSkipDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
 
      /*
       * An outage for the configured address, which nothing but the already connected
       * branch of runConnect() closes: a peer that branch skips is dialled by no one, so a
       * connection is reported for it nowhere else. Recorded from this thread rather than
       * waited for, because whether the connect thread has dialled that address before the
       * peer registers is a race and what is asserted below is not.
       */
      assertThat(rs.connect(peerAddress, baseDN))
          .as("nothing answers at " + peerAddress).isFalse();
 
      inbound = registerPeerFrom(ports[0], peerAddress, baseDN);
      final ReplicationServerHandler registered = waitForRegistration(domain);
 
      // The precondition of the case rather than an assumption: a peer registered under the
      // address it is configured under is a peer this test says nothing about.
      assertThat(registered.getServerAddressURL())
          .as("the peer should be registered under the loopback address it dialled from")
          .isEqualTo(loopbackAt(ports[1]).toString());
      assertThat(registered.getServerURL())
          .as("the peer should name the address it is configured under in its start message")
          .isEqualTo(peerAddress.toString());
 
      assertThat(domain.isConnectedToServerAt(peerAddress))
          .as("the peer is connected, so its configured address must not be dialled again")
          .isTrue();
      assertThat(waitForConnectRestored(rs, peerAddress, baseDN))
          .as("the connect thread must find the peer at its configured address and close the"
              + " outage reported for it, rather than dial it again")
          .isTrue();
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that a second session with a peer already connected under another address is
   * dropped as the duplicate connection it is, and not reported as two servers sharing a
   * server id.
   * <p>
   * The server under test dials the peer at its configured address while holding the
   * session that same peer dialled it on. Both sessions are with one server, which names
   * one address in both of its start messages; only the addresses the two sockets carry
   * differ, which is what being reachable at more than one address means.
   */
  @Test
  public void aPeerReachedAtItsConfiguredAddressIsNotReportedAsADuplicateServerId()
      throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = documentationAddress(PEER_ADDRESS, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerDuplicateDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      inbound = registerPeerFrom(ports[0], peerAddress, baseDN);
      final ReplicationServerHandler registered = waitForRegistration(domain);
 
      final List<String> records =
          errorLogRecordsOfHandshakeWith(rs, baseDN, peerAddress, peerAddress);
 
      assertThat(duplicateServerIdRecords(records, rs, loopbackAt(ports[1]), peerAddress))
          .as("a peer reachable at more than one address is not two servers sharing a"
              + " server id, and every attempt to reach it would log this again")
          .isEmpty();
      assertThat(domain.getConnectedRSs().get(PEER_RS_ID))
          .as("the session the peer dialled must outlive the duplicate connection")
          .isSameAs(registered);
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that a peer which names an address this configuration does not use is found by
   * the address its own connection came from, which is the only one known of it.
   * <p>
   * The peer of the first case names the address it is configured under, and is found by
   * it. This one is the other way round: it is configured at the address it dials this
   * server from, and names one this topology does not configure it at -- the host name of
   * its machine under another of its addresses, which {@code setServerURL()} falls back to.
   * The address it names matches nothing here, so the address its session came from is what
   * has to match.
   */
  @Test
  public void aPeerWhichNamesAnAddressThisConfigurationDoesNotUseIsFoundByTheOneItDialledFrom()
      throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = loopbackAt(ports[1]);
    final HostPort namedAddress = documentationAddress(OTHER_ADDRESS, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerNamedDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      inbound = registerPeerFrom(ports[0], namedAddress, baseDN);
      final ReplicationServerHandler registered = waitForRegistration(domain);
 
      // The precondition of the case rather than an assumption, as in the first case: a
      // peer which names the address it is configured under is the peer of that one.
      assertThat(registered.getServerURL())
          .as("the peer should name the address this configuration does not use")
          .isEqualTo(namedAddress.toString());
      assertThat(registered.getServerAddressURL())
          .as("the peer should be registered under the loopback address it dialled from")
          .isEqualTo(peerAddress.toString());
 
      assertThat(domain.isConnectedToServerAt(peerAddress))
          .as("the address the connection of such a peer came from is the only one which"
              + " can match the address it is configured at")
          .isTrue();
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that a peer which names a host this server cannot resolve is recognised by that
   * name all the same, rather than reported as two servers sharing a server id.
   * <p>
   * {@code setServerURL()} falls back to the host name of the machine when none of the
   * addresses a peer is configured with is local to it, and the rest of the topology has no
   * reason to resolve that name. Both handlers of such a peer name it, so it is what tells
   * this server they are one -- but a comparison which resolves both sides answers that a
   * name it cannot resolve is equivalent to nothing at all, not even to itself, and the
   * handshake this server offers such a peer would abort on a duplicate server id on every
   * pass which reaches it.
   */
  @Test
  public void aPeerWhichNamesAnUnresolvableHostIsNotReportedAsADuplicateServerId()
      throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = documentationAddress(PEER_ADDRESS, ports[1]);
    final HostPort namedAddress = new HostPort(UNRESOLVABLE_HOST, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerUnresolvedDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      inbound = registerPeerFrom(ports[0], namedAddress, baseDN);
      final ReplicationServerHandler registered = waitForRegistration(domain);
 
      /*
       * The premise of this case rather than an assumption about the network it runs on:
       * HostPort reports a name it cannot resolve each time it builds an address from one,
       * so those records are what says this machine answers that the name below does not
       * exist. A resolver which synthesises an address for a name which does not -- consumer
       * ISPs, captive portals, some corporate DNS -- would have isEquivalentTo() answer this
       * case on its own, which leaves the arm the case is here for unread, the count below
       * vacuous and nothing red to say so.
       */
      assertThat(recordsContaining(TestCaseUtils.ERROR_TEXT_WRITER.getMessages(),
          ERR_COULD_NOT_SOLVE_HOSTNAME.get(UNRESOLVABLE_HOST).toString()))
          .as("the resolver of this machine must answer that " + UNRESOLVABLE_HOST
              + " does not exist, which is the premise of this case")
          .isNotEmpty();
 
      /*
       * What the addresses are built from is read once, when the start message names them:
       * a name which cannot be resolved is reported by HostPort each time one is built from
       * it, and the connect thread compares them on every pass. The comparison below is the
       * one that thread makes.
       */
      TestCaseUtils.ERROR_TEXT_WRITER.clear();
      for (int pass = 0; pass < 3; pass++)
      {
        domain.isConnectedToServerAt(peerAddress);
      }
      assertThat(recordsContaining(TestCaseUtils.ERROR_TEXT_WRITER.getMessages(),
          ERR_COULD_NOT_SOLVE_HOSTNAME.get(UNRESOLVABLE_HOST).toString()))
          .as("comparing the addresses of a handler must not build them again")
          .isEmpty();
 
      final List<String> records =
          errorLogRecordsOfHandshakeWith(rs, baseDN, peerAddress, namedAddress);
 
      assertThat(duplicateServerIdRecords(records, rs, loopbackAt(ports[1]), peerAddress))
          .as("a peer whose name this server cannot resolve is one server all the same, and"
              + " every attempt to reach it would log this again")
          .isEmpty();
      assertThat(domain.getConnectedRSs().get(PEER_RS_ID))
          .as("the session the peer dialled must outlive the duplicate connection")
          .isSameAs(registered);
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that two genuinely different replication servers sharing a server id are still
   * reported, which is the misconfiguration the address comparison is there to catch.
   * <p>
   * Nothing is shared here: the connected peer names one address and dialled from the
   * loopback interface, and the server answering the handshake names, and is reached at,
   * another address on the same port. The port is the same one on purpose: it is the host
   * which tells the two servers apart, and a second port would answer the comparison before
   * any host of it is read.
   */
  @Test
  public void twoServersSharingAServerIdAreStillReported() throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = documentationAddress(PEER_ADDRESS, ports[1]);
    final HostPort otherAddress = documentationAddress(OTHER_ADDRESS, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerConflictDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      inbound = registerPeerFrom(ports[0], peerAddress, baseDN);
      waitForRegistration(domain);
 
      final List<String> records =
          errorLogRecordsOfHandshakeWith(rs, baseDN, otherAddress, otherAddress);
 
      assertThat(duplicateServerIdRecords(records, rs, loopbackAt(ports[1]), otherAddress))
          .as("two servers which share nothing but a server id must still be reported")
          .isNotEmpty();
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that a second session which comes from the address a peer is already connected on
   * is read as that peer, whatever that session names.
   * <p>
   * This is the other arm of the comparison the handshake makes, and what it gives up:
   * a session from the address a peer is connected on is that peer, so two replication
   * servers which share a server id and reach this one from behind a single gateway are read
   * as one and the second of them is dropped in silence. The base did the same with them --
   * it compared the addresses the two sockets carried, which for such a pair is one string
   * twice -- and this is the arm which recognises the peer of the case above, whose named
   * address matches nothing in this configuration.
   */
  @Test
  public void aSecondSessionFromTheAddressAPeerIsConnectedOnIsReadAsThatPeer() throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = documentationAddress(PEER_ADDRESS, ports[1]);
    final HostPort otherAddress = documentationAddress(OTHER_ADDRESS, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerGatewayDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      inbound = registerPeerFrom(ports[0], peerAddress, baseDN);
      final ReplicationServerHandler registered = waitForRegistration(domain);
 
      /*
       * The handshake is answered from the loopback address the session of the registered
       * peer came from, and what answers it names an address neither handler carries
       * otherwise: the named addresses of the two differ, so the address the two sessions
       * have in common is the only thing which can match them.
       */
      final List<String> records =
          errorLogRecordsOfHandshakeWith(rs, baseDN, loopbackAt(ports[1]), otherAddress);
 
      assertThat(
          duplicateServerIdRecords(records, rs, loopbackAt(ports[1]), loopbackAt(ports[1])))
          .as("a session from the address a peer is connected on is that peer, whatever that"
              + " session names")
          .isEmpty();
      assertThat(domain.getConnectedRSs().get(PEER_RS_ID))
          .as("the session the peer dialled must outlive the duplicate connection")
          .isSameAs(registered);
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that a peer taken out of the configuration is disconnected even though the
   * session it is connected on came from another address.
   * <p>
   * {@code disconnectRemovedReplicationServers()} hands the addresses which were removed
   * from {@code ds-cfg-replication-server} to this domain, and a handler which does not
   * answer to any of them stays connected: the peer an administrator took out of the
   * topology keeps replicating with this server until one of the two is restarted.
   */
  @Test
  public void aPeerRemovedFromTheConfigurationIsDisconnectedByItsConfiguredAddress()
      throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort peerAddress = documentationAddress(PEER_ADDRESS, ports[1]);
 
    ReplicationServer rs = null;
    Session inbound = null;
    try
    {
      rs = startServerWithPeerConfiguredAt(ports[0], "multiHomedPeerRemovedDb", peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      inbound = registerPeerFrom(ports[0], peerAddress, baseDN);
      waitForRegistration(domain);
 
      domain.stopReplicationServers(Collections.singletonList(peerAddress));
 
      assertThat(domain.getConnectedRSs().keySet())
          .as("the peer removed from the configuration must be disconnected")
          .doesNotContain(PEER_RS_ID);
    }
    finally
    {
      close(inbound);
      removeQuietly(rs);
    }
  }
 
  /**
   * Tests that taking the entry of this server out of {@code ds-cfg-replication-server}
   * stops no peer, while taking the entry of a peer out still stops that peer.
   * <p>
   * {@code HostPort} normalises every address local to this machine to {@code localhost}, so
   * the entry of this server and the loopback address a peer names for itself are one
   * address on this port: a peer whose own configuration lists {@code localhost:P} for
   * itself names exactly that in its start messages -- {@code setServerURL()} takes the
   * first configured entry which is local to it -- and on this server that name normalises
   * to what its own entry does. The connect thread skips the entry of this server before it
   * dials; the road which disconnects removed replication servers has to skip it as well, or
   * removing it -- a configuration which does not list this server is supported, see
   * {@code runConnect()} -- costs such a peer its session, which it then has to dial again.
   * <p>
   * Both entries go in one change, so what tells the two peers apart is that guard alone:
   * the peer which names the entry of this server stays, the peer whose own entry was
   * removed goes. What the in JVM fixture cannot give is the fold itself, which needs one
   * port on two machines; what it names is the address of this server, which normalises the
   * same way.
   */
  @Test
  public void removingTheEntryOfThisServerStopsNoPeer() throws Exception
  {
    TestCaseUtils.startServer();
 
    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
    final int[] ports = TestCaseUtils.findFreePorts(2);
    final HostPort ownAddress = loopbackAt(ports[0]);
    final HostPort peerAddress = loopbackAt(ports[1]);
    final String dbDirName = "multiHomedPeerOwnEntryDb";
 
    ReplicationServer rs = null;
    Session namesThisServer = null;
    Session namesItsOwnEntry = null;
    try
    {
      rs = startServerWithPeersConfiguredAt(ports[0], dbDirName, ownAddress, peerAddress);
      final ReplicationServerDomain domain = rs.getReplicationServerDomain(baseDN, true);
      namesThisServer = registerPeerFrom(ports[0], PEER_RS_ID, ownAddress, baseDN);
      final ReplicationServerHandler namedThisServer = waitForRegistration(domain, PEER_RS_ID);
      namesItsOwnEntry = registerPeerFrom(ports[0], SECOND_PEER_RS_ID, peerAddress, baseDN);
      waitForRegistration(domain, SECOND_PEER_RS_ID);
 
      // The precondition of the case rather than an assumption: a peer which does not answer
      // to the entry of this server is a peer this case says nothing about.
      assertThat(namedThisServer.isServerAt(HostPort.localAddress(ports[0])))
          .as("the peer which names a loopback address on the port of this server should"
              + " answer to the entry of this server")
          .isTrue();
 
      // Both entries removed at once, which is what an administrator emptying the list does.
      rs.applyConfigurationChange(configurationWithPeersAt(ports[0], dbDirName));
 
      assertThat(domain.getConnectedRSs().keySet())
          .as("a peer which names the address this server listens on must outlive the removal"
              + " of the entry of this server")
          .contains(PEER_RS_ID);
      assertThat(domain.getConnectedRSs().keySet())
          .as("the peer whose own entry was removed must still be disconnected")
          .doesNotContain(SECOND_PEER_RS_ID);
    }
    finally
    {
      close(namesThisServer);
      close(namesItsOwnEntry);
      removeQuietly(rs);
    }
  }
 
  /**
   * Starts a replication server whose only configured peer is at the provided address.
   * <p>
   * Nothing ever answers there -- the address is a documentation address -- which is what
   * the connect thread of the server does with it: it dials it, fails, and comes back to it
   * six passes later, the failed dial being blacklisted for the five in between. Whether it
   * dials it at all is what the first case is about.
   */
  private ReplicationServer startServerWithPeerConfiguredAt(int port, String dbDirName,
      HostPort peerAddress) throws Exception
  {
    return startServerWithPeersConfiguredAt(port, dbDirName, peerAddress);
  }
 
  /** Starts a replication server whose configured peers are the provided addresses. */
  private ReplicationServer startServerWithPeersConfiguredAt(int port, String dbDirName,
      HostPort... peerAddresses) throws Exception
  {
    return new ReplicationServer(configurationWithPeersAt(port, dbDirName, peerAddresses));
  }
 
  /**
   * Returns the configuration of the server under test, listing the provided addresses as
   * the replication servers of its topology.
   */
  private ReplServerFakeConfiguration configurationWithPeersAt(int port, String dbDirName,
      HostPort... peerAddresses)
  {
    final SortedSet<String> configured = newTreeSet();
    for (HostPort peerAddress : peerAddresses)
    {
      configured.add(peerAddress.toString());
    }
    return new ReplServerFakeConfiguration(port, dbDirName, 0, RS_ID, 0, 100, configured);
  }
 
  /**
   * Has the fake peer dial the server under test over the loopback interface and complete
   * the handshake, which registers it under the address that connection came from while its
   * start message names the address it is configured under.
   *
   * @return the session the registration hangs on, closed by the caller
   */
  private Session registerPeerFrom(int port, HostPort registeredAs, DN baseDN) throws Exception
  {
    return registerPeerFrom(port, PEER_RS_ID, registeredAs, baseDN);
  }
 
  /**
   * Has a fake peer of the provided server id dial the server under test, as
   * {@link #registerPeerFrom(int, HostPort, DN)} does for the peer of the cases which need
   * one peer only.
   *
   * @return the session the registration hangs on, closed by the caller
   */
  private Session registerPeerFrom(int port, int peerServerId, HostPort registeredAs, DN baseDN)
      throws Exception
  {
    final ReplSessionSecurity security = getReplSessionSecurity();
    final Socket socket = new Socket();
    Session session = null;
    try
    {
      socket.setTcpNoDelay(true);
      socket.connect(new InetSocketAddress("127.0.0.1", port), SOCKET_TIMEOUT_MS);
      session = security.createClientSession(socket, SOCKET_TIMEOUT_MS);
      session.publish(peerStartMsg(peerServerId, registeredAs, baseDN));
      session.receive();
      // The initiator of a session decides whether it is encrypted, and the start message
      // above asked for it not to be: both ends leave the SSL session together, right after
      // the start messages have been exchanged.
      session.stopEncryption();
      /*
       * The second phase: the server reads this one before it sends its own, and registers
       * the handler once it has sent it. The list holds this peer and nothing else --
       * waitAndProcessTopoFromRemoteRS() reads rsInfos.get(0) above protocol version 4, so
       * an empty one ends the handshake on an IndexOutOfBoundsException instead, which is
       * an abort like any other and would leave the peer unregistered.
       */
      final RSInfo peerInfo = new RSInfo(peerServerId, registeredAs.toString(), -1, (byte) 1, 1);
      session.publish(new TopologyMsg(Collections.<DSInfo> emptyList(), newArrayList(peerInfo)));
      session.receive();
      return session;
    }
    catch (Exception e)
    {
      close(session);
      close(socket);
      throw e;
    }
  }
 
  /**
   * Runs the handshake the server under test offers a peer it dials at the provided address
   * and returns the error log records that handshake wrote.
   * <p>
   * The session is dialled over the loopback interface and reports {@code dialledAt} as the
   * address it reaches the peer at, which is what dialling a peer at its configured address
   * gives whatever interface the peer's own connection to this server used. The answer is
   * a well formed start message naming {@code answersAs}: what ends the handshake is the
   * handler this server already holds for that server id, not what the peer answers here.
   *
   * @param listenPort
   *          the port the fake peer answers the handshake on
   * @param dialledAt
   *          the address the session reports the peer at
   * @param answersAs
   *          the address the peer names in the start message it answers with
   */
  private List<String> errorLogRecordsOfHandshakeWith(final ReplicationServer rs,
      final DN baseDN, final HostPort dialledAt, final HostPort answersAs)
      throws Exception
  {
    final ExecutorService peerThread = Executors.newSingleThreadExecutor();
    try (ServerSocket peerListen = TestCaseUtils.bindFreePort())
    {
      peerListen.setSoTimeout(SOCKET_TIMEOUT_MS);
      final ReplSessionSecurity security = getReplSessionSecurity();
      final int listenPort = peerListen.getLocalPort();
 
      final Future<Session> dialled = peerThread.submit(new Callable<Session>()
      {
        @Override
        public Session call() throws Exception
        {
          return dialPeerAt(security, dialledAt, listenPort);
        }
      });
 
      try (Session peerEnd =
          security.createServerSession(peerListen.accept(), SOCKET_TIMEOUT_MS);
          Session session = dialled.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS))
      {
        final Future<?> answer = peerThread.submit(new Callable<Void>()
        {
          @Override
          public Void call() throws Exception
          {
            peerEnd.receive();
            peerEnd.publish(peerStartMsg(answersAs, baseDN));
            return null;
          }
        });
 
        TestCaseUtils.ERROR_TEXT_WRITER.clear();
        new ReplicationServerHandler(session, 100, rs, 100).connect(baseDN, false);
        answer.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        return TestCaseUtils.ERROR_TEXT_WRITER.getMessages();
      }
    }
    finally
    {
      peerThread.shutdownNow();
    }
  }
 
  /**
   * Connects to the provided local port with a socket which reports the provided address as
   * the one it is connected to, as dialling a peer at that address does.
   */
  private Session dialPeerAt(ReplSessionSecurity security, HostPort dialledAt, int port)
      throws Exception
  {
    // Named, so that the SSL socket factory asking the socket for its host name does not
    // send a reverse lookup of an address no name server knows anything about.
    final InetAddress reported = InetAddress.getByAddress("peer.example.com",
        InetAddress.getByName(dialledAt.getHost()).getAddress());
    final Socket socket = new Socket()
    {
      @Override
      public InetAddress getInetAddress()
      {
        return reported;
      }
    };
    try
    {
      socket.setTcpNoDelay(true);
      socket.connect(new InetSocketAddress("127.0.0.1", port), SOCKET_TIMEOUT_MS);
      return security.createClientSession(socket, SOCKET_TIMEOUT_MS);
    }
    catch (Exception e)
    {
      close(socket);
      throw e;
    }
  }
 
  /** Returns the start message of the fake peer, naming the provided address. */
  private ReplServerStartMsg peerStartMsg(HostPort address, DN baseDN)
  {
    return peerStartMsg(PEER_RS_ID, address, baseDN);
  }
 
  /** Returns the start message of a fake peer of the provided server id. */
  private ReplServerStartMsg peerStartMsg(int peerServerId, HostPort address, DN baseDN)
  {
    return new ReplServerStartMsg(peerServerId, address.toString(), baseDN, 100,
        new ServerState(), -1, false, (byte) 1, 5000);
  }
 
  /** Returns an address of the documentation ranges, which nothing routes. */
  private HostPort documentationAddress(byte[] address, int port) throws Exception
  {
    return new HostPort(InetAddress.getByAddress(address).getHostAddress(), port);
  }
 
  /** Returns the loopback address a connection of these tests comes from. */
  private HostPort loopbackAt(int port)
  {
    return new HostPort("127.0.0.1", port);
  }
 
  /**
   * Waits for the domain to hold the handler of the fake peer, which the server registers
   * after it has sent the topology message the handshake above reads: the registration
   * lands just behind the thread which drove it.
   */
  private ReplicationServerHandler waitForRegistration(ReplicationServerDomain domain)
      throws Exception
  {
    return waitForRegistration(domain, PEER_RS_ID);
  }
 
  /**
   * Waits for the domain to hold the handler of the fake peer of the provided server id, as
   * {@link #waitForRegistration(ReplicationServerDomain)} does for the peer of the cases
   * which need one peer only.
   */
  private ReplicationServerHandler waitForRegistration(ReplicationServerDomain domain,
      int peerServerId) throws Exception
  {
    final long deadline = System.currentTimeMillis() + REGISTRATION_TIMEOUT_MS;
    ReplicationServerHandler registered;
    while (true)
    {
      registered = domain.getConnectedRSs().get(peerServerId);
      if (registered != null || System.currentTimeMillis() > deadline)
      {
        break;
      }
      Thread.sleep(50);
    }
    assertThat(registered)
        .as("the fake peer of server id " + peerServerId + " should have registered with the"
            + " domain")
        .isNotNull();
    return registered;
  }
 
  /**
   * Waits for the connect thread to report the peer at the provided address connected,
   * driving a pass rather than waiting one out.
   * <p>
   * That report is what the already connected branch of {@code runConnect()} does with a
   * peer it finds registered, and the only thing this server does with one: the branch
   * which dials a peer reports a connection only for a handshake it completed, which the
   * documentation address of these tests never gives.
   */
  private boolean waitForConnectRestored(ReplicationServer rs, HostPort peer, DN baseDN)
      throws Exception
  {
    final String message =
        NOTE_REPLICATION_SERVER_CONNECT_RESTORED.get(RS_ID, peer, baseDN).toString();
    final long deadline = System.currentTimeMillis() + REGISTRATION_TIMEOUT_MS;
    while (true)
    {
      if (!recordsContaining(TestCaseUtils.ERROR_TEXT_WRITER.getMessages(), message).isEmpty())
      {
        return true;
      }
      if (System.currentTimeMillis() > deadline)
      {
        return false;
      }
      rs.waitConnections();
    }
  }
 
  /**
   * Returns the records of the provided log which report the two provided address URLs as
   * two replication servers sharing a server id.
   * <p>
   * The addresses are the ones the message names, so a record of the handshake next door
   * cannot be read as one of the handshake under test.
   */
  private List<String> duplicateServerIdRecords(List<String> records,
      ReplicationServer rs, HostPort connectedAs, HostPort reachedAt)
  {
    return recordsContaining(records, ERR_DUPLICATE_REPLICATION_SERVER_ID.get(
        rs.getMonitorInstanceName(), connectedAs, reachedAt, PEER_RS_ID).toString());
  }
 
  /** Returns the records of the provided log which carry the provided message. */
  private List<String> recordsContaining(List<String> records, String message)
  {
    final List<String> results = newArrayList();
    for (String record : records)
    {
      if (record.contains(message))
      {
        results.add(record);
      }
    }
    return results;
  }
 
  /** Teardown must never mask the primary assertion failure. */
  private void removeQuietly(ReplicationServer replicationServer)
  {
    try
    {
      if (replicationServer != null)
      {
        remove(replicationServer);
      }
    }
    catch (Exception ignored)
    {
    }
  }
}