rippled
Loading...
Searching...
No Matches
ConnectAttempt.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2012, 2013 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/overlay/Cluster.h>
21#include <xrpld/overlay/detail/ConnectAttempt.h>
22#include <xrpld/overlay/detail/PeerImp.h>
23#include <xrpld/overlay/detail/ProtocolVersion.h>
24
25#include <xrpl/json/json_reader.h>
26
27#include <sstream>
28
29namespace ripple {
30
32 Application& app,
33 boost::asio::io_context& io_context,
34 endpoint_type const& remote_endpoint,
36 shared_context const& context,
39 beast::Journal journal,
40 OverlayImpl& overlay)
41 : Child(overlay)
42 , app_(app)
43 , id_(id)
44 , sink_(journal, OverlayImpl::makePrefix(id))
45 , journal_(sink_)
46 , remote_endpoint_(remote_endpoint)
47 , usage_(usage)
48 , strand_(boost::asio::make_strand(io_context))
49 , timer_(io_context)
50 , stepTimer_(io_context)
51 , stream_ptr_(std::make_unique<stream_type>(
52 socket_type(std::forward<boost::asio::io_context&>(io_context)),
53 *context))
54 , socket_(stream_ptr_->next_layer().socket())
55 , stream_(*stream_ptr_)
56 , slot_(slot)
57{
58}
59
61{
62 // slot_ will be null if we successfully connected
63 // and transferred ownership to a PeerImp
64 if (slot_ != nullptr)
66}
67
68void
70{
71 if (!strand_.running_in_this_thread())
72 return boost::asio::post(
74
75 if (!socket_.is_open())
76 return;
77
78 JLOG(journal_.debug()) << "stop: Stop";
79
80 shutdown();
81}
82
83void
85{
86 if (!strand_.running_in_this_thread())
87 return boost::asio::post(
89
90 JLOG(journal_.debug()) << "run: connecting to " << remote_endpoint_;
91
92 ioPending_ = true;
93
94 // Allow up to connectTimeout_ seconds to establish remote peer connection
96
97 stream_.next_layer().async_connect(
99 boost::asio::bind_executor(
100 strand_,
101 std::bind(
104 std::placeholders::_1)));
105}
106
107//------------------------------------------------------------------------------
108
109void
111{
112 XRPL_ASSERT(
113 strand_.running_in_this_thread(),
114 "ripple::ConnectAttempt::shutdown: strand in this thread");
115
116 if (!socket_.is_open())
117 return;
118
119 shutdown_ = true;
120 boost::beast::get_lowest_layer(stream_).cancel();
121
123}
124
125void
127{
128 XRPL_ASSERT(
129 strand_.running_in_this_thread(),
130 "ripple::ConnectAttempt::tryAsyncShutdown : strand in this thread");
131
133 return;
134
135 if (ioPending_)
136 return;
137
138 // gracefully shutdown the SSL socket, performing a shutdown handshake
141 {
143 return stream_.async_shutdown(bind_executor(
144 strand_,
145 std::bind(
148 std::placeholders::_1)));
149 }
150
151 close();
152}
153
154void
156{
157 cancelTimer();
158
159 if (ec)
160 {
161 // - eof: the stream was cleanly closed
162 // - operation_aborted: an expired timer (slow shutdown)
163 // - stream_truncated: the tcp connection closed (no handshake) it could
164 // occur if a peer does not perform a graceful disconnect
165 // - broken_pipe: the peer is gone
166 // - application data after close notify: benign SSL shutdown condition
167 bool shouldLog =
168 (ec != boost::asio::error::eof &&
169 ec != boost::asio::error::operation_aborted &&
170 ec.message().find("application data after close notify") ==
171 std::string::npos);
172
173 if (shouldLog)
174 {
175 JLOG(journal_.debug()) << "onShutdown: " << ec.message();
176 }
177 }
178
179 close();
180}
181
182void
184{
185 XRPL_ASSERT(
186 strand_.running_in_this_thread(),
187 "ripple::ConnectAttempt::close : strand in this thread");
188 if (!socket_.is_open())
189 return;
190
191 cancelTimer();
192
193 error_code ec;
194 socket_.close(ec);
195}
196
197void
199{
200 JLOG(journal_.debug()) << reason;
201 shutdown();
202}
203
204void
206{
207 JLOG(journal_.debug()) << name << ": " << ec.message();
208 shutdown();
209}
210
211void
213{
214 currentStep_ = step;
215
216 // Set global timer (only if not already set)
217 if (timer_.expiry() == std::chrono::steady_clock::time_point{})
218 {
219 try
220 {
221 timer_.expires_after(connectTimeout);
222 timer_.async_wait(boost::asio::bind_executor(
223 strand_,
224 std::bind(
227 std::placeholders::_1)));
228 }
229 catch (std::exception const& ex)
230 {
231 JLOG(journal_.error()) << "setTimer (global): " << ex.what();
232 return close();
233 }
234 }
235
236 // Set step-specific timer
237 try
238 {
239 std::chrono::seconds stepTimeout;
240 switch (step)
241 {
243 stepTimeout = StepTimeouts::tcpConnect;
244 break;
246 stepTimeout = StepTimeouts::tlsHandshake;
247 break;
249 stepTimeout = StepTimeouts::httpWrite;
250 break;
252 stepTimeout = StepTimeouts::httpRead;
253 break;
255 stepTimeout = StepTimeouts::tlsShutdown;
256 break;
259 return; // No timer needed for init or complete step
260 }
261
262 // call to expires_after cancels previous timer
263 stepTimer_.expires_after(stepTimeout);
264 stepTimer_.async_wait(boost::asio::bind_executor(
265 strand_,
266 std::bind(
269 std::placeholders::_1)));
270
271 JLOG(journal_.trace()) << "setTimer: " << stepToString(step)
272 << " timeout=" << stepTimeout.count() << "s";
273 }
274 catch (std::exception const& ex)
275 {
276 JLOG(journal_.error())
277 << "setTimer (step " << stepToString(step) << "): " << ex.what();
278 return close();
279 }
280}
281
282void
284{
285 try
286 {
287 timer_.cancel();
288 stepTimer_.cancel();
289 }
290 catch (boost::system::system_error const&)
291 {
292 // ignored
293 }
294}
295
296void
298{
299 if (!socket_.is_open())
300 return;
301
302 if (ec)
303 {
304 // do not initiate shutdown, timers are frequently cancelled
305 if (ec == boost::asio::error::operation_aborted)
306 return;
307
308 // This should never happen
309 JLOG(journal_.error()) << "onTimer: " << ec.message();
310 return close();
311 }
312
313 // Determine which timer expired by checking their expiry times
314 auto const now = std::chrono::steady_clock::now();
315 bool globalExpired = (timer_.expiry() <= now);
316 bool stepExpired = (stepTimer_.expiry() <= now);
317
318 if (globalExpired)
319 {
320 JLOG(journal_.debug())
321 << "onTimer: Global timeout; step: " << stepToString(currentStep_);
322 }
323 else if (stepExpired)
324 {
325 JLOG(journal_.debug())
326 << "onTimer: Step timeout; step: " << stepToString(currentStep_);
327 }
328 else
329 {
330 JLOG(journal_.warn()) << "onTimer: Unexpected timer callback";
331 }
332
333 close();
334}
335
336void
338{
339 ioPending_ = false;
340
341 if (ec)
342 {
343 if (ec == boost::asio::error::operation_aborted)
344 return tryAsyncShutdown();
345
346 return fail("onConnect", ec);
347 }
348
349 if (!socket_.is_open())
350 return;
351
352 // check if connection has really been established
353 socket_.local_endpoint(ec);
354 if (ec)
355 return fail("onConnect", ec);
356
357 if (shutdown_)
358 return tryAsyncShutdown();
359
360 ioPending_ = true;
361
363
364 stream_.set_verify_mode(boost::asio::ssl::verify_none);
365 stream_.async_handshake(
366 boost::asio::ssl::stream_base::client,
367 boost::asio::bind_executor(
368 strand_,
369 std::bind(
372 std::placeholders::_1)));
373}
374
375void
377{
378 ioPending_ = false;
379
380 if (ec)
381 {
382 if (ec == boost::asio::error::operation_aborted)
383 return tryAsyncShutdown();
384
385 return fail("onHandshake", ec);
386 }
387
388 auto const local_endpoint = socket_.local_endpoint(ec);
389 if (ec)
390 return fail("onHandshake", ec);
391
393
394 // check if we connected to ourselves
397 return fail("Self connection");
398
399 auto const sharedValue = makeSharedValue(*stream_ptr_, journal_);
400 if (!sharedValue)
401 return shutdown(); // makeSharedValue logs
402
409
411 req_,
412 *sharedValue,
415 remote_endpoint_.address(),
416 app_);
417
418 if (shutdown_)
419 return tryAsyncShutdown();
420
421 ioPending_ = true;
422
423 boost::beast::http::async_write(
424 stream_,
425 req_,
426 boost::asio::bind_executor(
427 strand_,
428 std::bind(
431 std::placeholders::_1)));
432}
433
434void
436{
437 ioPending_ = false;
438
439 if (ec)
440 {
441 if (ec == boost::asio::error::operation_aborted)
442 return tryAsyncShutdown();
443
444 return fail("onWrite", ec);
445 }
446
447 if (shutdown_)
448 return tryAsyncShutdown();
449
450 ioPending_ = true;
451
453
454 boost::beast::http::async_read(
455 stream_,
456 read_buf_,
457 response_,
458 boost::asio::bind_executor(
459 strand_,
460 std::bind(
463 std::placeholders::_1)));
464}
465
466void
468{
469 cancelTimer();
470 ioPending_ = false;
472
473 if (ec)
474 {
475 if (ec == boost::asio::error::eof)
476 {
477 JLOG(journal_.debug()) << "EOF";
478 return shutdown();
479 }
480
481 if (ec == boost::asio::error::operation_aborted)
482 return tryAsyncShutdown();
483
484 return fail("onRead", ec);
485 }
486
487 if (shutdown_)
488 return tryAsyncShutdown();
489
491}
492
493//--------------------------------------------------------------------------
494
495void
497{
499 {
500 // A peer may respond with service_unavailable and a list of alternative
501 // peers to connect to, a differing status code is unexpected
502 if (response_.result() !=
503 boost::beast::http::status::service_unavailable)
504 {
505 JLOG(journal_.warn())
506 << "Unable to upgrade to peer protocol: " << response_.result()
507 << " (" << response_.reason() << ")";
508 return shutdown();
509 }
510
511 // Parse response body to determine if this is a redirect or other
512 // service unavailable
513 std::string responseBody;
514 responseBody.reserve(boost::asio::buffer_size(response_.body().data()));
515 for (auto const buffer : response_.body().data())
516 responseBody.append(
517 static_cast<char const*>(buffer.data()),
518 boost::asio::buffer_size(buffer));
519
520 Json::Value json;
521 Json::Reader reader;
522 auto const isValidJson = reader.parse(responseBody, json);
523
524 // Check if this is a redirect response (contains peer-ips field)
525 auto const isRedirect =
526 isValidJson && json.isObject() && json.isMember("peer-ips");
527
528 if (!isRedirect)
529 {
530 JLOG(journal_.warn())
531 << "processResponse: " << remote_endpoint_
532 << " failed to upgrade to peer protocol: " << response_.result()
533 << " (" << response_.reason() << ")";
534
535 return shutdown();
536 }
537
538 Json::Value const& peerIps = json["peer-ips"];
539 if (!peerIps.isArray())
540 return fail("processResponse: invalid peer-ips format");
541
542 // Extract and validate peer endpoints
544 redirectEndpoints.reserve(peerIps.size());
545
546 for (auto const& ipValue : peerIps)
547 {
548 if (!ipValue.isString())
549 continue;
550
551 error_code ec;
552 auto const endpoint = parse_endpoint(ipValue.asString(), ec);
553 if (!ec)
554 redirectEndpoints.push_back(endpoint);
555 }
556
557 // Notify PeerFinder about the redirect redirectEndpoints may be empty
558 overlay_.peerFinder().onRedirects(remote_endpoint_, redirectEndpoints);
559
560 return fail("processResponse: failed to connect to peer: redirected");
561 }
562
563 // Just because our peer selected a particular protocol version doesn't
564 // mean that it's acceptable to us. Check that it is:
565 std::optional<ProtocolVersion> negotiatedProtocol;
566
567 {
568 auto const pvs = parseProtocolVersions(response_["Upgrade"]);
569
570 if (pvs.size() == 1 && isProtocolSupported(pvs[0]))
571 negotiatedProtocol = pvs[0];
572
573 if (!negotiatedProtocol)
574 return fail(
575 "processResponse: Unable to negotiate protocol version");
576 }
577
578 auto const sharedValue = makeSharedValue(*stream_ptr_, journal_);
579 if (!sharedValue)
580 return shutdown(); // makeSharedValue logs
581
582 try
583 {
584 auto const publicKey = verifyHandshake(
585 response_,
586 *sharedValue,
589 remote_endpoint_.address(),
590 app_);
591
592 JLOG(journal_.debug())
593 << "Protocol: " << to_string(*negotiatedProtocol);
594 JLOG(journal_.info())
595 << "Public Key: " << toBase58(TokenType::NodePublic, publicKey);
596
597 auto const member = app_.cluster().member(publicKey);
598 if (member)
599 {
600 JLOG(journal_.info()) << "Cluster name: " << *member;
601 }
602
603 auto const result =
604 overlay_.peerFinder().activate(slot_, publicKey, !member->empty());
605 if (result != PeerFinder::Result::success)
606 {
608 ss << "Outbound Connect Attempt " << remote_endpoint_ << " "
609 << to_string(result);
610 return fail(ss.str());
611 }
612
613 if (!socket_.is_open())
614 return;
615
616 if (shutdown_)
617 return tryAsyncShutdown();
618
619 auto const peer = std::make_shared<PeerImp>(
620 app_,
621 std::move(stream_ptr_),
622 read_buf_.data(),
623 std::move(slot_),
624 std::move(response_),
625 usage_,
626 publicKey,
627 *negotiatedProtocol,
628 id_,
629 overlay_);
630
631 overlay_.add_active(peer);
632 }
633 catch (std::exception const& e)
634 {
635 return fail(std::string("Handshake failure (") + e.what() + ")");
636 }
637}
638
639} // namespace ripple
T append(T... args)
T bind(T... args)
Unserialize a JSON document into a Value.
Definition json_reader.h:39
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:149
bool isArray() const
UInt size() const
Number of values in array or object.
bool isObject() const
bool isMember(char const *key) const
Return true if the object has a member named key.
A generic endpoint for log messages.
Definition Journal.h:60
Stream error() const
Definition Journal.h:346
Stream debug() const
Definition Journal.h:328
Stream info() const
Definition Journal.h:334
Stream trace() const
Severity stream access functions.
Definition Journal.h:322
Stream warn() const
Definition Journal.h:340
virtual Config & config()=0
virtual Cluster & cluster()=0
std::optional< std::string > member(PublicKey const &node) const
Determines whether a node belongs in the cluster.
Definition Cluster.cpp:38
bool VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE
Definition Config.h:248
bool LEDGER_REPLAY
Definition Config.h:223
bool TX_REDUCE_RELAY_ENABLE
Definition Config.h:258
bool COMPRESSION
Definition Config.h:220
static constexpr std::chrono::seconds connectTimeout
void stop() override
Stop the connection attempt.
boost::asio::ip::tcp::socket socket_type
void cancelTimer()
Cancel both global and step timers.
boost::system::error_code error_code
std::unique_ptr< stream_type > stream_ptr_
std::shared_ptr< PeerFinder::Slot > slot_
ConnectAttempt(Application &app, boost::asio::io_context &io_context, endpoint_type const &remote_endpoint, Resource::Consumer usage, shared_context const &context, Peer::id_t id, std::shared_ptr< PeerFinder::Slot > const &slot, beast::Journal journal, OverlayImpl &overlay)
Construct a new ConnectAttempt object.
ConnectionStep currentStep_
Resource::Consumer usage_
boost::asio::strand< boost::asio::io_context::executor_type > strand_
static std::string stepToString(ConnectionStep step)
void run()
Begin the connection attempt.
void onHandshake(error_code ec)
boost::asio::ip::tcp::endpoint endpoint_type
void onWrite(error_code ec)
void processResponse()
Process the HTTP upgrade response from peer.
void onTimer(error_code ec)
Handle timer expiration events.
void setTimer(ConnectionStep step)
Set timers for the specified connection step.
void onShutdown(error_code ec)
boost::beast::ssl_stream< middle_type > stream_type
boost::beast::multi_buffer read_buf_
void onConnect(error_code ec)
void fail(std::string const &reason)
void onRead(error_code ec)
static boost::asio::ip::tcp::endpoint parse_endpoint(std::string const &s, boost::system::error_code &ec)
beast::Journal const journal_
ConnectionStep
Represents the current phase of the connection establishment process.
boost::asio::basic_waitable_timer< std::chrono::steady_clock > timer_
endpoint_type remote_endpoint_
boost::asio::basic_waitable_timer< std::chrono::steady_clock > stepTimer_
PeerFinder::Manager & peerFinder()
void add_active(std::shared_ptr< PeerImp > const &peer)
static bool isPeerUpgrade(http_request_type const &request)
Setup const & setup() const
virtual bool onConnected(std::shared_ptr< Slot > const &slot, beast::IP::Endpoint const &local_endpoint)=0
Called when an outbound connection attempt succeeds.
virtual Config config()=0
Returns the configuration for the manager.
virtual void on_closed(std::shared_ptr< Slot > const &slot)=0
Called when the slot is closed.
virtual void onRedirects(boost::asio::ip::tcp::endpoint const &remote_address, std::vector< boost::asio::ip::tcp::endpoint > const &eps)=0
Called when we received redirect IPs from a busy peer.
virtual Result activate(std::shared_ptr< Slot > const &slot, PublicKey const &key, bool reserved)=0
Request an active slot type.
An endpoint that consumes resources.
Definition Consumer.h:35
T count(T... args)
T is_same_v
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
std::vector< ProtocolVersion > parseProtocolVersions(boost::beast::string_view const &value)
Parse a set of protocol versions.
void buildHandshake(boost::beast::http::fields &h, ripple::uint256 const &sharedValue, std::optional< std::uint32_t > networkID, beast::IP::Address public_ip, beast::IP::Address remote_ip, Application &app)
Insert fields headers necessary for upgrading the link to the peer protocol.
std::optional< uint256 > makeSharedValue(stream_type &ssl, beast::Journal journal)
Computes a shared value based on the SSL connection state.
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:630
auto makeRequest(bool crawlPublic, bool comprEnabled, bool ledgerReplayEnabled, bool txReduceRelayEnabled, bool vpReduceRelayEnabled) -> request_type
Make outbound http request.
bool isProtocolSupported(ProtocolVersion const &v)
Determine whether we support a specific protocol version.
PublicKey verifyHandshake(boost::beast::http::fields const &headers, ripple::uint256 const &sharedValue, std::optional< std::uint32_t > networkID, beast::IP::Address public_ip, beast::IP::Address remote, Application &app)
Validate header fields necessary for upgrading the link to the peer protocol.
STL namespace.
T push_back(T... args)
T reserve(T... args)
T str(T... args)
static IP::Endpoint from_asio(boost::asio::ip::address const &address)
static constexpr std::chrono::seconds tcpConnect
static constexpr std::chrono::seconds tlsShutdown
static constexpr std::chrono::seconds tlsHandshake
static constexpr std::chrono::seconds httpWrite
static constexpr std::chrono::seconds httpRead
beast::IP::Address public_ip
Definition Overlay.h:69
std::optional< std::uint32_t > networkID
Definition Overlay.h:72
bool peerPrivate
true if we want our IP address kept private.
T what(T... args)