#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::PeerFinder { class PeerFinder_test : public beast::unit_test::Suite { test::SuiteJournal journal_; public: PeerFinder_test() : journal_("PeerFinder_test", *this) { } struct TestStore : Store { std::size_t load(load_callback const& cb) override { return 0; } void save(std::vector const&) override { } }; struct TestChecker { void stop() { } void wait() { } template void asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) { // NOLINTNEXTLINE(misc-const-correctness) boost::system::error_code ec; handler(ec); } }; void testBackoff1() { auto const seconds = 10000; testcase("backoff 1"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5")); { Config c; c.autoConnect = false; c.listeningPort = 1024; logic.config(c); } std::size_t n = 0; for (std::size_t i = 0; i < seconds; ++i) { auto const list = logic.autoconnect(); if (!list.empty()) { BEAST_EXPECT(list.size() == 1); auto const [slot, _] = logic.newOutboundSlot(list.front()); BEAST_EXPECT( logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5"))); logic.onClosed(slot); ++n; } clock.advance(std::chrono::seconds(1)); logic.oncePerSecond(); } // Less than 20 attempts BEAST_EXPECT(n < 20); } // with activate void testBackoff2() { auto const seconds = 10000; testcase("backoff 2"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5")); { Config c; c.autoConnect = false; c.listeningPort = 1024; logic.config(c); } PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); std::size_t n = 0; for (std::size_t i = 0; i < seconds; ++i) { auto const list = logic.autoconnect(); if (!list.empty()) { BEAST_EXPECT(list.size() == 1); auto const [slot, _] = logic.newOutboundSlot(list.front()); if (!BEAST_EXPECT( logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5")))) return; if (!BEAST_EXPECT(logic.activate(slot, pk, false) == PeerFinder::Result::Success)) return; logic.onClosed(slot); ++n; } clock.advance(std::chrono::seconds(1)); logic.oncePerSecond(); } // No more often than once per minute BEAST_EXPECT(n <= (seconds + 59) / 60); } // test accepting an incoming slot for an already existing outgoing slot void testDuplicateOutIn() { testcase("duplicate out/in"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; logic.config(c); } auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); auto const [slot1, r] = logic.newOutboundSlot(remote); BEAST_EXPECT(slot1 != nullptr); BEAST_EXPECT(r == Result::Success); BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); auto const [slot2, r2] = logic.newInboundSlot(local, remote); BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); BEAST_EXPECT(r2 == Result::DuplicatePeer); if (!BEAST_EXPECT(slot2 == nullptr)) logic.onClosed(slot2); logic.onClosed(slot1); } // test establishing outgoing slot for an already existing incoming slot void testDuplicateInOut() { testcase("duplicate in/out"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; logic.config(c); } auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); auto const [slot1, r] = logic.newInboundSlot(local, remote); BEAST_EXPECT(slot1 != nullptr); BEAST_EXPECT(r == Result::Success); BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); auto const [slot2, r2] = logic.newOutboundSlot(remote); BEAST_EXPECT(r2 == Result::DuplicatePeer); BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); if (!BEAST_EXPECT(slot2 == nullptr)) logic.onClosed(slot2); logic.onClosed(slot1); } void testPeerLimitExceeded() { testcase("peer limit exceeded"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; logic.config(c); } auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); auto const [slot, r] = logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025")); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(r == Result::Success); auto const [slot1, r1] = logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026")); BEAST_EXPECT(slot1 != nullptr); BEAST_EXPECT(r1 == Result::Success); auto const [slot2, r2] = logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1027")); BEAST_EXPECT(r2 == Result::IpLimitExceeded); if (!BEAST_EXPECT(slot2 == nullptr)) logic.onClosed(slot2); logic.onClosed(slot1); logic.onClosed(slot); } void testActivateDuplicatePeer() { testcase("test activate duplicate peer"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; logic.config(c); } auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first); auto const [slot, rSlot] = logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1025")); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(rSlot == Result::Success); auto const [slot2, r2Slot] = logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1026")); BEAST_EXPECT(slot2 != nullptr); BEAST_EXPECT(r2Slot == Result::Success); BEAST_EXPECT(logic.onConnected(slot, local)); BEAST_EXPECT(logic.onConnected(slot2, local)); BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success); // activating a different slot with the same node ID (pk) must fail BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::DuplicatePeer); logic.onClosed(slot); // accept the same key for a new slot after removing the old slot BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::Success); logic.onClosed(slot2); } void testActivateInboundDisabled() { testcase("test activate inbound disabled"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; logic.config(c); } PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first); auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); auto const [slot, rSlot] = logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025")); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(rSlot == Result::Success); BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::InboundDisabled); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; c.inPeers = 1; logic.config(c); } // new inbound slot must succeed when inbound connections are enabled BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success); // creating a new inbound slot must succeed as IP Limit is not exceeded auto const [slot2, r2Slot] = logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026")); BEAST_EXPECT(slot2 != nullptr); BEAST_EXPECT(r2Slot == Result::Success); PublicKey const pk2(randomKeyPair(KeyType::Secp256k1).first); // an inbound slot exceeding inPeers limit must fail BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::Full); logic.onClosed(slot2); logic.onClosed(slot); } void testAddFixedPeerNoPort() { testcase("test addFixedPeer no port"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); try { logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.2")); fail("invalid endpoint successfully added"); } catch (std::runtime_error const& e) { pass(); } } void testIsValidAddress() { testcase("is_valid_address"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); auto const pass = [&](std::string const& s) { BEAST_EXPECT(logic.isValidAddress(beast::IP::Endpoint::fromString(s))); }; auto const fail = [&](std::string const& s) { BEAST_EXPECT(!logic.isValidAddress(beast::IP::Endpoint::fromString(s))); }; // Invalid: port 0 fail("65.0.0.1:0"); // --- IPv4 ranges --- // For each range: 1 before (pass), first (fail), last (fail), // 1 after (pass) // 0.0.0.0/8 - "This network" // No "before" - nothing before 0.0.0.0 fail("0.0.0.0:8080"); fail("0.255.255.255:8080"); pass("1.0.0.0:8080"); // 10.0.0.0/8 - Private (RFC 1918) pass("9.255.255.255:8080"); fail("10.0.0.0:8080"); fail("10.255.255.255:8080"); pass("11.0.0.0:8080"); // 100.64.0.0/10 - Shared Address Space / CGNAT (RFC 6598) pass("100.63.255.255:8080"); fail("100.64.0.0:8080"); fail("100.127.255.255:8080"); pass("100.128.0.0:8080"); // 127.0.0.0/8 - Loopback pass("126.255.255.255:8080"); fail("127.0.0.0:8080"); fail("127.255.255.255:8080"); pass("128.0.0.0:8080"); // 169.254.0.0/16 - Link-local pass("169.253.255.255:8080"); fail("169.254.0.0:8080"); fail("169.254.255.255:8080"); pass("169.255.0.0:8080"); // 172.16.0.0/12 - Private (RFC 1918) pass("172.15.255.255:8080"); fail("172.16.0.0:8080"); fail("172.31.255.255:8080"); pass("172.32.0.0:8080"); // 192.0.0.0/24 - IETF Protocol Assignments (RFC 6890) pass("191.255.255.255:8080"); fail("192.0.0.0:8080"); fail("192.0.0.255:8080"); pass("192.0.1.0:8080"); // 192.0.2.0/24 - TEST-NET-1 (RFC 5737) pass("192.0.1.255:8080"); fail("192.0.2.0:8080"); fail("192.0.2.255:8080"); pass("192.0.3.0:8080"); // 192.88.99.0/24 - 6to4 Relay Anycast (RFC 7526) pass("192.88.98.255:8080"); fail("192.88.99.0:8080"); fail("192.88.99.255:8080"); pass("192.88.100.0:8080"); // 192.168.0.0/16 - Private (RFC 1918) pass("192.167.255.255:8080"); fail("192.168.0.0:8080"); fail("192.168.255.255:8080"); pass("192.169.0.0:8080"); // 198.18.0.0/15 - Benchmarking (RFC 2544) pass("198.17.255.255:8080"); fail("198.18.0.0:8080"); fail("198.19.255.255:8080"); pass("198.20.0.0:8080"); // 198.51.100.0/24 - TEST-NET-2 (RFC 5737) pass("198.51.99.255:8080"); fail("198.51.100.0:8080"); fail("198.51.100.255:8080"); pass("198.51.101.0:8080"); // 203.0.113.0/24 - TEST-NET-3 (RFC 5737) pass("203.0.112.255:8080"); fail("203.0.113.0:8080"); fail("203.0.113.255:8080"); pass("203.0.114.0:8080"); // 224.0.0.0/4 - Multicast pass("223.255.255.255:8080"); fail("224.0.0.0:8080"); fail("239.255.255.255:8080"); // 240.0.0.0 (after multicast) is also blocked (reserved) // 240.0.0.0/4 - Reserved (RFC 1112) // 239.255.255.255 (before reserved) is also blocked (multicast) fail("240.0.0.0:8080"); fail("255.255.255.255:8080"); // --- IPv6 ranges --- // ::1 - Loopback (single address) fail("[::1]:8080"); // :: - Unspecified (single address) fail("[::]:8080"); // fc00::/7 - Unique Local Address (ULA) pass("[fb00::1]:8080"); fail("[fc00::1]:8080"); fail("[fdff::1]:8080"); pass("[fe00::1]:8080"); // fe80::/10 - Link-local pass("[fe7f::1]:8080"); fail("[fe80::1]:8080"); fail("[febf::1]:8080"); pass("[fec0::1]:8080"); // ff00::/8 - Multicast pass("[feff::1]:8080"); fail("[ff00::1]:8080"); fail("[ffff::1]:8080"); // No "after" - ffff:... is the highest IPv6 range // 100::/64 - Discard prefix (RFC 6666) pass("[ff::1]:8080"); fail("[100::]:8080"); fail("[100::ffff:ffff:ffff:ffff]:8080"); pass("[100:0:0:1::1]:8080"); // 2001::/32 - IETF Protocol Assignments / Teredo (RFC 4380) pass("[2000:ffff::1]:8080"); fail("[2001::]:8080"); fail("[2001:0:ffff::1]:8080"); pass("[2001:1::1]:8080"); // 2001:20::/28 - ORCHIDv2 (RFC 7343) pass("[2001:1f::1]:8080"); fail("[2001:20::1]:8080"); fail("[2001:2f::1]:8080"); pass("[2001:30::1]:8080"); // 2001:db8::/32 - Documentation (RFC 3849) pass("[2001:db7::1]:8080"); fail("[2001:db8::1]:8080"); fail("[2001:db8:ffff::1]:8080"); pass("[2001:db9::1]:8080"); // 2002::/16 - 6to4 (RFC 3056, deprecated) pass("[2001:ffff::1]:8080"); fail("[2002::1]:8080"); fail("[2002:ffff::1]:8080"); pass("[2003::1]:8080"); // --- IPv6 v4-mapped (delegates to IPv4 checks) --- fail("[::ffff:10.0.0.1]:8080"); fail("[::ffff:100.64.0.1]:8080"); fail("[::ffff:169.254.1.1]:8080"); fail("[::ffff:192.0.2.1]:8080"); fail("[::ffff:198.18.0.1]:8080"); fail("[::ffff:224.0.0.1]:8080"); fail("[::ffff:240.0.0.1]:8080"); // --- Valid public addresses --- pass("8.8.8.8:443"); pass("65.0.0.1:8080"); pass("[2001:4860:4860::8888]:8080"); pass("[2606:4700:4700::1111]:8080"); } void testVerifyEndpoints() { // Helper that sets up a Logic instance, creates and activates a slot, // then calls on_endpoints with the given list and returns the // livecache size afterwards. auto run = [&](bool verifyEndpoints, Endpoints eps) -> std::size_t { TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); { Config c; c.autoConnect = false; c.listeningPort = 1024; c.ipLimit = 2; c.verifyEndpoints = verifyEndpoints; logic.config(c); } auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); auto const [slot, r] = logic.newOutboundSlot(remote); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(r == Result::Success); BEAST_EXPECT(logic.onConnected(slot, local)); PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); BEAST_EXPECT(logic.activate(slot, pk, false) == Result::Success); logic.onEndpoints(slot, std::move(eps)); auto const size = logic.livecache.size(); logic.onClosed(slot); return size; }; { testcase("verify_endpoints enabled"); // Valid public addresses Endpoints eps; eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); // Invalid: private address eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); // Invalid: port 0 eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); // With verification enabled, only the 2 valid endpoints survive BEAST_EXPECT(run(true, eps) == 2); } { testcase("verify_endpoints disabled"); Endpoints eps; eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); // Private address — kept when verification is off eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); // Port 0 — kept when verification is off eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); // Without verification, all 4 endpoints survive BEAST_EXPECT(run(false, eps) == 4); } } void testOnConnectedSelfConnection() { testcase("test onConnected self connection"); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1234"); auto const [slot, r] = logic.newOutboundSlot(local); BEAST_EXPECT(slot != nullptr); BEAST_EXPECT(r == Result::Success); // Must fail when a slot is to our own IP address BEAST_EXPECT(!logic.onConnected(slot, local)); logic.onClosed(slot); } void testConfig() { // if peers_max is configured then peers_in_max and peers_out_max // are ignored auto run = [&](std::string const& test, std::optional maxPeers, std::optional maxIn, std::optional maxOut, std::uint16_t port, std::uint16_t expectOut, std::uint16_t expectIn, std::uint16_t expectIpLimit) { xrpl::Config c; testcase(test); std::string toLoad; int max = 0; if (maxPeers) { max = maxPeers.value(); toLoad += "[peers_max]\n" + std::to_string(max) + "\n" + "[peers_in_max]\n" + std::to_string(maxIn.value_or(0)) + "\n" + "[peers_out_max]\n" + std::to_string(maxOut.value_or(0)) + "\n"; } else if (maxIn && maxOut) { toLoad += "[peers_in_max]\n" + std::to_string(*maxIn) + "\n" + "[peers_out_max]\n" + std::to_string(*maxOut) + "\n"; } c.loadFromString(toLoad); BEAST_EXPECT( (c.peersMax == max && c.peersInMax == 0 && c.peersOutMax == 0) || (c.peersInMax == *maxIn && c.peersOutMax == *maxOut)); Config const config = Config::makeConfig(c, port, false, 0, true); Counts counts; counts.onConfig(config); BEAST_EXPECT( counts.outMax() == expectOut && counts.inMax() == expectIn && config.ipLimit == expectIpLimit); TestStore store; TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); logic.config(config); BEAST_EXPECT(logic.config() == config); }; // if max_peers == 0 => maxPeers = 21, // else if max_peers < 10 => maxPeers = 10 else maxPeers = // max_peers // expectOut => if legacy => max(0.15 * maxPeers, 10), // if legacy && !wantIncoming => maxPeers else max_out_peers // expectIn => if legacy && wantIncoming => maxPeers - outPeers // else if !wantIncoming => 0 else max_in_peers // ipLimit => if expectIn <= 21 => 2 else 2 + min(5, expectIn/21) // ipLimit = max(1, min(ipLimit, expectIn/2)) // legacy test with max_peers run("legacy no config", {}, {}, {}, 4000, 10, 11, 2); run("legacy max_peers 0", 0, 100, 10, 4000, 10, 11, 2); run("legacy max_peers 5", 5, 100, 10, 4000, 10, 0, 1); run("legacy max_peers 20", 20, 100, 10, 4000, 10, 10, 2); run("legacy max_peers 100", 100, 100, 10, 4000, 15, 85, 6); run("legacy max_peers 20, private", 20, 100, 10, 0, 20, 0, 1); // test with max_in_peers and max_out_peers run("new in 100/out 10", {}, 100, 10, 4000, 10, 100, 6); run("new in 0/out 10", {}, 0, 10, 4000, 10, 0, 1); run("new in 100/out 10, private", {}, 100, 10, 0, 10, 0, 6); } void testInvalidConfig() { testcase("invalid config"); auto run = [&](std::string const& toLoad) { xrpl::Config c; try { c.loadFromString(toLoad); fail(); } catch (...) { pass(); } }; run(R"xrpldConfig( [peers_in_max] 100 )xrpldConfig"); run(R"xrpldConfig( [peers_out_max] 100 )xrpldConfig"); run(R"xrpldConfig( [peers_in_max] 100 [peers_out_max] 5 )xrpldConfig"); run(R"xrpldConfig( [peers_in_max] 1001 [peers_out_max] 10 )xrpldConfig"); run(R"xrpldConfig( [peers_in_max] 10 [peers_out_max] 1001 )xrpldConfig"); } void run() override { testBackoff1(); testBackoff2(); testDuplicateOutIn(); testDuplicateInOut(); testConfig(); testInvalidConfig(); testPeerLimitExceeded(); testActivateDuplicatePeer(); testActivateInboundDisabled(); testAddFixedPeerNoPort(); testOnConnectedSelfConnection(); testIsValidAddress(); testVerifyEndpoints(); } }; BEAST_DEFINE_TESTSUITE(PeerFinder, peerfinder, xrpl); } // namespace xrpl::PeerFinder