diff --git a/src/test/beast/IPEndpoint_test.cpp b/src/test/beast/IPEndpoint_test.cpp index b1cd4709b4..bc04087891 100644 --- a/src/test/beast/IPEndpoint_test.cpp +++ b/src/test/beast/IPEndpoint_test.cpp @@ -305,6 +305,15 @@ public: BEAST_EXPECT(!isLoopback(ep)); BEAST_EXPECTS(to_string(ep) == "fd00::1", to_string(ep)); + // unspecified IPv6 (::) + ep = Endpoint(AddressV6{}); + BEAST_EXPECT(isUnspecified(ep)); + BEAST_EXPECT(!isPublic(ep)); + BEAST_EXPECT(!isPrivate(ep)); + BEAST_EXPECT(!isMulticast(ep)); + BEAST_EXPECT(!isLoopback(ep)); + BEAST_EXPECTS(to_string(ep) == "::", to_string(ep)); + { ep = Endpoint::fromString("192.0.2.112"); BEAST_EXPECT(!isUnspecified(ep)); diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp index c4f129c1a3..cf91800951 100644 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ b/src/test/peerfinder/PeerFinder_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace xrpl::PeerFinder { @@ -64,8 +65,9 @@ public: void asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) { + // NOLINTNEXTLINE(misc-const-correctness) boost::system::error_code ec; - handler(ep, ep, ec); + handler(ec); } }; @@ -368,6 +370,259 @@ public: } } + 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() { @@ -524,6 +779,8 @@ public: testActivateInboundDisabled(); testAddFixedPeerNoPort(); testOnConnectedSelfConnection(); + testIsValidAddress(); + testVerifyEndpoints(); } };