diff --git a/cfg/xrpld-example.cfg b/cfg/xrpld-example.cfg index 6d65824fb9..058c750475 100644 --- a/cfg/xrpld-example.cfg +++ b/cfg/xrpld-example.cfg @@ -527,6 +527,13 @@ # # The current default (which is subject to change) is 300 seconds. # +# verify_endpoints = <0 | 1> +# +# If set to 1, the server will verify the validity of endpoint +# addresses received in TMEndpoints peer protocol messages by +# checking that they are well-formed and publicly routable. Invalid +# endpoints will be dropped. The default is 1 (verification enabled). +# # # [transaction_queue] EXPERIMENTAL # diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp index 325c4e1246..0687e47053 100644 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ b/src/test/peerfinder/PeerFinder_test.cpp @@ -52,7 +52,7 @@ public: async_connect(beast::IP::Endpoint const& ep, Handler&& handler) { boost::system::error_code ec; - handler(ep, ep, ec); + handler(ec); } }; @@ -541,6 +541,84 @@ public: pass("[2606:4700:4700::1111]:8080"); } + void + test_verify_endpoints() + { + // 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::from_string("65.0.0.1:5"); + auto const local = + beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + auto const [slot, r] = logic.new_outbound_slot(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.on_endpoints(slot, std::move(eps)); + + auto const size = logic.livecache_.size(); + logic.on_closed(slot); + return size; + }; + + { + testcase("verify_endpoints enabled"); + + // Valid public addresses + Endpoints eps; + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("44.0.0.1:5"), 1}); + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("44.0.0.2:6"), 1}); + // Invalid: private address + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("10.0.0.1:5"), 1}); + // Invalid: port 0 + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("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.push_back( + Endpoint{beast::IP::Endpoint::from_string("44.0.0.1:5"), 1}); + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("44.0.0.2:6"), 1}); + // Private address — kept when verification is off + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("10.0.0.1:5"), 1}); + // Port 0 — kept when verification is off + eps.push_back( + Endpoint{beast::IP::Endpoint::from_string("44.0.0.3:0"), 1}); + + // Without verification, all 4 endpoints survive + BEAST_EXPECT(run(false, eps) == 4); + } + } + void test_onConnected_self_connection() { @@ -597,7 +675,7 @@ public: (c.PEERS_MAX == max && c.PEERS_IN_MAX == 0 && c.PEERS_OUT_MAX == 0) || (c.PEERS_IN_MAX == *maxIn && c.PEERS_OUT_MAX == *maxOut)); - Config const config = Config::makeConfig(c, port, false, 0); + Config config = Config::makeConfig(c, port, false, 0, true); Counts counts; counts.onConfig(config); @@ -698,6 +776,7 @@ public: test_addFixedPeer_no_port(); test_onConnected_self_connection(); test_is_valid_address(); + test_verify_endpoints(); } }; diff --git a/src/xrpld/overlay/Overlay.h b/src/xrpld/overlay/Overlay.h index 7d2508a584..03b3af9f3e 100644 --- a/src/xrpld/overlay/Overlay.h +++ b/src/xrpld/overlay/Overlay.h @@ -51,6 +51,7 @@ public: std::uint32_t crawlOptions = 0; std::optional networkID; bool vlEnabled = true; + bool verifyEndpoints = false; }; using PeerSequence = std::vector>; diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 10ebae6b0f..eec43a01a4 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -447,7 +447,8 @@ OverlayImpl::start() app_.config(), serverHandler_.setup().overlay.port(), app_.getValidationPublicKey().has_value(), - setup_.ipLimit); + setup_.ipLimit, + setup_.verifyEndpoints); m_peerFinder->setConfig(config); m_peerFinder->start(); @@ -1469,6 +1470,8 @@ setup_Overlay(BasicConfig const& config) if (ec || beast::IP::is_private(setup.public_ip)) Throw("Configured public IP is invalid"); } + + set(setup.verifyEndpoints, true, "verify_endpoints", section); } { diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index 57fde8a569..e412118726 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -61,6 +61,9 @@ struct Config /** Limit how many incoming connections we allow per IP */ int ipLimit{0}; + /** `true` if we want to verify endpoints in TMEndpoints messages */ + bool verifyEndpoints = false; + //-------------------------------------------------------------------------- /** Create a configuration with default values. */ @@ -83,6 +86,8 @@ struct Config * @param port server's listening port * @param validationPublicKey true if validation public key is not empty * @param ipLimit limit of incoming connections per IP + * @param verifyEndpoints `true` if we want to verify endpoints in + * TMEndpoints messages * @return PeerFinder::Config */ static Config @@ -90,10 +95,11 @@ struct Config xrpl::Config const& config, std::uint16_t port, bool validationPublicKey, - int ipLimit); + int ipLimit, + bool verifyEndpoints); friend bool - operator==(Config const& lhs, Config const& rhs); + operator==(Config const& lhs, Config const& rhs) = default; }; //------------------------------------------------------------------------------ diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index a645ec762c..5e98a9795e 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -693,7 +693,7 @@ public: } // Discard invalid addresses - if (!is_valid_address(ep.address)) + if (!is_valid_address(ep.address) && config_.verifyEndpoints) { JLOG(m_journal.debug()) << beast::leftw(18) << "Endpoints drop " << ep.address << " as invalid"; diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index 6a158fbbab..a9274a40e5 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -11,16 +11,6 @@ Config::Config() : outPeers(calcOutPeers()) { } -bool -operator==(Config const& lhs, Config const& rhs) -{ - return lhs.autoConnect == rhs.autoConnect && lhs.peerPrivate == rhs.peerPrivate && - lhs.wantIncoming == rhs.wantIncoming && lhs.inPeers == rhs.inPeers && - lhs.maxPeers == rhs.maxPeers && lhs.outPeers == rhs.outPeers && - lhs.features == rhs.features && lhs.ipLimit == rhs.ipLimit && - lhs.listeningPort == rhs.listeningPort; -} - std::size_t Config::calcOutPeers() const { @@ -56,6 +46,7 @@ Config::onWrite(beast::PropertyStream::Map& map) const map["port"] = listeningPort; map["features"] = features; map["ip_limit"] = ipLimit; + map["verify_endpoints"] = verifyEndpoints; } Config @@ -63,7 +54,8 @@ Config::makeConfig( xrpl::Config const& cfg, std::uint16_t port, bool validationPublicKey, - int ipLimit) + int ipLimit, + bool verifyEndpoints) { PeerFinder::Config config; @@ -116,6 +108,7 @@ Config::makeConfig( config.listeningPort = port; config.features = ""; config.ipLimit = ipLimit; + config.verifyEndpoints = verifyEndpoints; // Enforce business rules config.applyTuning();