librats 2.1.4 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/lib/index.d.ts +51 -6
- package/lib/index.js +42 -3
- package/native-src/CMakeLists.txt +13 -0
- package/native-src/src/librats/bindings/rats.cpp +90 -6
- package/native-src/src/librats/bindings/rats.h +92 -3
- package/native-src/src/librats/core/types.cpp +4 -3
- package/native-src/src/librats/core/types.h +25 -12
- package/native-src/src/librats/node/circuit_service.h +84 -0
- package/native-src/src/librats/node/config.h +9 -2
- package/native-src/src/librats/node/node.cpp +59 -5
- package/native-src/src/librats/node/node.h +38 -7
- package/native-src/src/librats/node/peer_network.h +19 -2
- package/native-src/src/librats/peer/peer.h +17 -6
- package/native-src/src/librats/peer/peer_table.cpp +41 -8
- package/native-src/src/librats/security/noise_security.cpp +2 -0
- package/native-src/src/librats/security/plaintext_security.h +1 -0
- package/native-src/src/librats/security/session.h +7 -0
- package/native-src/src/librats/storage/storage.cpp +434 -213
- package/native-src/src/librats/storage/storage.h +163 -19
- package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
- package/native-src/src/librats/subsystems/hole_punch.cpp +81 -8
- package/native-src/src/librats/subsystems/hole_punch.h +24 -0
- package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
- package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
- package/native-src/src/librats/subsystems/relay.cpp +1143 -0
- package/native-src/src/librats/subsystems/relay.h +211 -0
- package/native-src/src/librats/subsystems/relay_service.h +46 -0
- package/native-src/src/librats/transport/connection.cpp +40 -8
- package/native-src/src/librats/transport/connection.h +12 -2
- package/native-src/src/librats/transport/reactor.cpp +56 -8
- package/native-src/src/librats/transport/reactor.h +38 -0
- package/native-src/src/librats/transport/relay_link.cpp +208 -0
- package/native-src/src/librats/transport/relay_link.h +303 -0
- package/native-src/src/librats/util/logger.h +37 -5
- package/native-src/src/librats/util/network_monitor.cpp +14 -0
- package/native-src/src/librats/util/network_utils.cpp +10 -1
- package/native-src/src/librats/wire/frame.h +1 -0
- package/package.json +1 -1
- package/src/librats_node.cpp +76 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file relay.h
|
|
5
|
+
* @brief Reaching a peer through a node both ends can already reach.
|
|
6
|
+
*
|
|
7
|
+
* The last rung of the connectivity ladder. A direct dial handles the easy cases;
|
|
8
|
+
* PortMappingService and HolePunch handle most of the rest. What is left is the
|
|
9
|
+
* pair of nodes for which no endpoint either one can advertise will ever work —
|
|
10
|
+
* a symmetric NAT on one side, a network that drops UDP and blocks inbound TCP,
|
|
11
|
+
* a punch that simply never lands. For them the only way through is to borrow a
|
|
12
|
+
* path: a node connected to both carries the bytes.
|
|
13
|
+
*
|
|
14
|
+
* ── What is relayed, and what is not ────────────────────────────────────────
|
|
15
|
+
* The relayed thing is a *byte stream*, not a message. It becomes a `Circuit` and
|
|
16
|
+
* a `RelayLink` (transport/relay_link.h), the reactor adopts it as an ordinary
|
|
17
|
+
* Connection, and everything above the Link runs unchanged — which is why:
|
|
18
|
+
*
|
|
19
|
+
* - the Noise_XX handshake is END TO END. The relay moves ciphertext it cannot
|
|
20
|
+
* read, cannot forge and cannot replay. It is a pipe, not a party, and it
|
|
21
|
+
* learns nothing beyond who is talking to whom and how much;
|
|
22
|
+
* - the peers authenticate each other's real keys, so a relay cannot substitute
|
|
23
|
+
* itself for either end;
|
|
24
|
+
* - every subsystem — pub/sub, file transfer, PEX — works over a relayed peer
|
|
25
|
+
* with no code of its own. To the application it is simply a peer, and
|
|
26
|
+
* PeerInfo::transport is the only thing that says otherwise.
|
|
27
|
+
*
|
|
28
|
+
* ── Finding a relay ─────────────────────────────────────────────────────────
|
|
29
|
+
* This stage covers the case where the two ends share a peer — the same topology
|
|
30
|
+
* HolePunch already relies on for its rendezvous, and the one a bootstrap or DHT
|
|
31
|
+
* mesh produces on its own. The initiator asks a few of its peers "do you hold
|
|
32
|
+
* this id?" (Probe) and opens a circuit with the first that says yes. Probing
|
|
33
|
+
* first, rather than opening with everybody at once, is what keeps a successful
|
|
34
|
+
* search from producing three redundant connections to the same peer — each with
|
|
35
|
+
* its own handshake — for the peer table to then tear two of down.
|
|
36
|
+
*
|
|
37
|
+
* Not covered here, deliberately: reaching a peer with whom we share NO peer. That
|
|
38
|
+
* needs reservations (a node asking a relay to hold a slot for it) and a way to
|
|
39
|
+
* advertise "reachable via R" as an address, which touches identify and PEX. The
|
|
40
|
+
* version byte and the free op-codes in the wire format leave room for it.
|
|
41
|
+
*
|
|
42
|
+
* ── Getting off the relay again ─────────────────────────────────────────────
|
|
43
|
+
* A circuit is a fallback, not a destination: it costs a third node bandwidth and
|
|
44
|
+
* a round trip. So when one comes up, this module asks HolePunch to try the target
|
|
45
|
+
* again — now that the two ends are peers, they can exchange what a punch needs
|
|
46
|
+
* over the very circuit that is carrying them. If the punch lands, PeerTable::add
|
|
47
|
+
* prefers the direct link at BOTH ends (any direct transport outranks Relay) and
|
|
48
|
+
* swaps the route with no disconnect event: the application never sees the seam.
|
|
49
|
+
*
|
|
50
|
+
* ── Carrying other peers' traffic ───────────────────────────────────────────
|
|
51
|
+
* Serving as a relay is off by default. Forwarding a rendezvous, as HolePunch
|
|
52
|
+
* does, is a few dozen bytes; forwarding a connection is somebody else's file
|
|
53
|
+
* transfer on your uplink, and that is a decision to be made rather than assumed.
|
|
54
|
+
* A node that turns it on is protected by, in order of how much they matter:
|
|
55
|
+
*
|
|
56
|
+
* - the end-to-end credit window (transport/relay_link.h), which bounds what one
|
|
57
|
+
* circuit can make the relay hold no matter what either end does. Without it a
|
|
58
|
+
* slow receiver would grow the relay's send queue until the relay dropped that
|
|
59
|
+
* peer entirely — losing all of its traffic, not just the circuit's;
|
|
60
|
+
* - forwarding only between peers it already holds. A relay never dials, never
|
|
61
|
+
* resolves an address, and can only ever deliver to somebody that chose to
|
|
62
|
+
* connect to it, so it cannot be turned into an open reflector;
|
|
63
|
+
* - no chaining. A circuit is refused if either end is itself only reachable
|
|
64
|
+
* through a relay, which is what stops loops and multi-hop amplification;
|
|
65
|
+
* - per-circuit byte and duration caps, per-peer and total circuit counts, and a
|
|
66
|
+
* rate limit on the requests themselves.
|
|
67
|
+
*
|
|
68
|
+
* ── Threading ───────────────────────────────────────────────────────────────
|
|
69
|
+
* Message handlers run on reactor threads; one worker thread drives attempt
|
|
70
|
+
* timeouts and the relay's own expiry. The tables are behind one mutex, which is
|
|
71
|
+
* never held across a call into a circuit or the reactor — a circuit is touched
|
|
72
|
+
* only by the reactor thread that owns its connection, which is by construction
|
|
73
|
+
* the thread that owns its carrier's (see node/circuit_service.h).
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
#include "librats/util/rats_export.h"
|
|
77
|
+
#include "librats/core/service_registry.h"
|
|
78
|
+
#include "librats/node/peer_network.h"
|
|
79
|
+
#include "librats/peer/peer_id.h"
|
|
80
|
+
#include "librats/subsystems/relay_service.h"
|
|
81
|
+
#include "librats/transport/relay_link.h" // Circuit::kDefaultWindow
|
|
82
|
+
|
|
83
|
+
#include <atomic>
|
|
84
|
+
#include <chrono>
|
|
85
|
+
#include <condition_variable>
|
|
86
|
+
#include <cstdint>
|
|
87
|
+
#include <memory>
|
|
88
|
+
#include <mutex>
|
|
89
|
+
#include <thread>
|
|
90
|
+
|
|
91
|
+
namespace librats {
|
|
92
|
+
|
|
93
|
+
class RATS_API Relay final : public Subsystem, public RelayService {
|
|
94
|
+
public:
|
|
95
|
+
struct Config {
|
|
96
|
+
// ── Using relays to reach peers we cannot dial ──────────────────────
|
|
97
|
+
|
|
98
|
+
/// Open circuits through other peers. Off makes this a relay-only node.
|
|
99
|
+
bool enable_client = true;
|
|
100
|
+
|
|
101
|
+
/// Peers asked whether they hold a target, per attempt. Small: each probe
|
|
102
|
+
/// is a question to somebody who probably cannot help, and one useful
|
|
103
|
+
/// answer is all an attempt needs.
|
|
104
|
+
size_t max_probes = 3;
|
|
105
|
+
|
|
106
|
+
/// Circuits this node may hold open as a client at once. Bounds both memory
|
|
107
|
+
/// and how many relayed peers a discovery module can talk us into.
|
|
108
|
+
size_t max_outbound_circuits = 8;
|
|
109
|
+
|
|
110
|
+
/// How long an attempt may take from the first probe to a live connection.
|
|
111
|
+
/// Covers the probe round trip, the circuit setup, and the handshake across
|
|
112
|
+
/// two hops.
|
|
113
|
+
std::chrono::milliseconds open_timeout{8000};
|
|
114
|
+
|
|
115
|
+
/// How long after a failed attempt before the same target may be tried
|
|
116
|
+
/// again. Only an attempt WE started ever ends in one.
|
|
117
|
+
std::chrono::milliseconds cooldown{60000};
|
|
118
|
+
|
|
119
|
+
// ── Accepting circuits opened to us ─────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/// Accept circuits other peers open toward us. Off makes this node
|
|
122
|
+
/// unreachable by relay, which is a choice a well-connected node can make.
|
|
123
|
+
bool accept_inbound = true;
|
|
124
|
+
|
|
125
|
+
/// Circuits opened TO us that may be live at once.
|
|
126
|
+
size_t max_inbound_circuits = 16;
|
|
127
|
+
|
|
128
|
+
// ── Carrying other peers' traffic ───────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/// Serve as a relay. OFF by default: unlike a hole-punch rendezvous, this
|
|
131
|
+
/// spends real bandwidth on somebody else's connection, so it is opted into
|
|
132
|
+
/// rather than assumed. A mesh in which nobody serves cannot relay at all.
|
|
133
|
+
bool serve = false;
|
|
134
|
+
|
|
135
|
+
/// Circuits this node will carry for others at once.
|
|
136
|
+
size_t max_circuits = 32;
|
|
137
|
+
|
|
138
|
+
/// Circuits one peer may have us carrying, so a single peer cannot take the
|
|
139
|
+
/// whole budget.
|
|
140
|
+
size_t max_circuits_per_peer = 2;
|
|
141
|
+
|
|
142
|
+
/// Bytes one circuit may move before it is closed. 0 removes the cap. The
|
|
143
|
+
/// default is generous for a fallback path and still bounds what one pair
|
|
144
|
+
/// of peers can spend of ours.
|
|
145
|
+
uint64_t max_bytes_per_circuit = 64ull * 1024 * 1024;
|
|
146
|
+
|
|
147
|
+
/// How long one circuit may live. 0 removes the cap.
|
|
148
|
+
std::chrono::seconds max_circuit_duration{600};
|
|
149
|
+
|
|
150
|
+
/// Requests (probes and circuit openings) one peer may make per window.
|
|
151
|
+
size_t request_budget = 8;
|
|
152
|
+
std::chrono::milliseconds request_window{1000};
|
|
153
|
+
|
|
154
|
+
// ── Both ends ───────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/// Bytes this node is willing to have in flight per circuit — the credit
|
|
157
|
+
/// window it advertises (see transport/relay_link.h). Raising it makes a
|
|
158
|
+
/// relayed peer faster on a long path and asks the relay to hold more.
|
|
159
|
+
uint32_t window = Circuit::kDefaultWindow;
|
|
160
|
+
|
|
161
|
+
/// When a relayed peer connects, ask HolePunch to try it directly, so the
|
|
162
|
+
/// circuit is replaced by a direct link as soon as one is possible. Costs
|
|
163
|
+
/// nothing when no HolePunch is attached.
|
|
164
|
+
bool upgrade_with_hole_punch = true;
|
|
165
|
+
|
|
166
|
+
/// Bookkeeping cadence — attempt timeouts, cooldowns, circuit expiry.
|
|
167
|
+
std::chrono::milliseconds tick{250};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
Relay();
|
|
171
|
+
explicit Relay(Config config);
|
|
172
|
+
~Relay() override;
|
|
173
|
+
|
|
174
|
+
void attach(NodeContext& ctx) override;
|
|
175
|
+
void start() override;
|
|
176
|
+
void stop() override;
|
|
177
|
+
|
|
178
|
+
/// @copydoc RelayService::connect_via_relay
|
|
179
|
+
bool connect_via_relay(const PeerId& target) override;
|
|
180
|
+
|
|
181
|
+
// — diagnostics and tests —
|
|
182
|
+
/// Circuits this node terminates (relayed peers, live or still being set up).
|
|
183
|
+
size_t circuits() const;
|
|
184
|
+
/// Circuits this node is carrying on behalf of other peers.
|
|
185
|
+
size_t carried_circuits() const;
|
|
186
|
+
/// Bytes forwarded on behalf of other peers.
|
|
187
|
+
uint64_t carried_bytes() const;
|
|
188
|
+
/// Attempts to reach a peer through a relay that are currently running.
|
|
189
|
+
size_t attempts() const;
|
|
190
|
+
|
|
191
|
+
private:
|
|
192
|
+
/// Everything mutable, held by shared_ptr because a Connection — and therefore
|
|
193
|
+
/// the Link that reports its circuit released — can outlive this subsystem: the
|
|
194
|
+
/// node stops subsystems before it stops the reactors that own the connections.
|
|
195
|
+
struct State;
|
|
196
|
+
|
|
197
|
+
Config config_;
|
|
198
|
+
std::shared_ptr<State> state_;
|
|
199
|
+
/// Kept from attach() so start() can resolve HolePunchService — which may be
|
|
200
|
+
/// attached after us, and is only guaranteed to have registered by then.
|
|
201
|
+
ServiceRegistry* services_ = nullptr;
|
|
202
|
+
|
|
203
|
+
std::thread worker_;
|
|
204
|
+
std::atomic<bool> running_{false};
|
|
205
|
+
std::mutex worker_mutex_;
|
|
206
|
+
std::condition_variable worker_cv_;
|
|
207
|
+
|
|
208
|
+
void loop();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
} // namespace librats
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file relay_service.h
|
|
5
|
+
* @brief Capability that lets a sibling module ask for a peer to be reached
|
|
6
|
+
* through a third node, by PeerId.
|
|
7
|
+
*
|
|
8
|
+
* Published by Relay via ServiceRegistry (see service_registry.h). It is the last
|
|
9
|
+
* rung of the connectivity ladder — a direct dial, then a hole punch, then this —
|
|
10
|
+
* and the module that gives up on the rung above should not have to know anything
|
|
11
|
+
* about how the rung below works. HolePunch, for instance, learns that a target is
|
|
12
|
+
* unreachable (a symmetric NAT it declined to punch, or a rendezvous that ran out
|
|
13
|
+
* of attempts) and this is the whole contract it needs to hand the id on: no relay
|
|
14
|
+
* selection, no circuits, no policy, all of which are Relay's own business.
|
|
15
|
+
*
|
|
16
|
+
* Resolving it returns nullptr when relaying is not enabled, which is exactly what
|
|
17
|
+
* makes the fallback optional at the consumer's side:
|
|
18
|
+
*
|
|
19
|
+
* if (auto* relay = ctx.services.get<RelayService>()) relay->connect_via_relay(id);
|
|
20
|
+
*
|
|
21
|
+
* The provider registers during attach(), so resolve it in start() (every attach()
|
|
22
|
+
* runs before any start()) rather than in your own attach(), where the provider may
|
|
23
|
+
* not have been attached yet. The pointer is NON-owning and valid while the node is.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
#include "librats/util/rats_export.h"
|
|
27
|
+
#include "librats/peer/peer_id.h"
|
|
28
|
+
|
|
29
|
+
namespace librats {
|
|
30
|
+
|
|
31
|
+
struct RATS_API RelayService {
|
|
32
|
+
virtual ~RelayService() = default;
|
|
33
|
+
|
|
34
|
+
/// Try to reach `target` through a node both ends are connected to.
|
|
35
|
+
/// Non-blocking; success surfaces as an ordinary peer-connected event, and the
|
|
36
|
+
/// resulting peer is ordinary in every way except that its bytes take a detour
|
|
37
|
+
/// (PeerInfo::transport is TransportKind::Relay).
|
|
38
|
+
///
|
|
39
|
+
/// @return whether an attempt actually started — false is routine (already
|
|
40
|
+
/// connected, an attempt is already running, the target is in cooldown,
|
|
41
|
+
/// or this node has no peer that could carry the circuit) and needs no
|
|
42
|
+
/// handling by the caller.
|
|
43
|
+
virtual bool connect_via_relay(const PeerId& target) = 0;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
} // namespace librats
|
|
@@ -51,6 +51,46 @@ uint8_t Connection::reactor_index() const noexcept { return reactor_.index(); }
|
|
|
51
51
|
bool Connection::send(FrameHeader header, ByteView payload) {
|
|
52
52
|
if (state_ != ConnState::Established) return false; // frames only flow post-handshake
|
|
53
53
|
|
|
54
|
+
// The one thing that is refused outright rather than queued: a message too
|
|
55
|
+
// large to be framed at all. The block prefix tops out at kMaxBlockSize and the
|
|
56
|
+
// peer's decoder rejects anything past it, so queueing this would spend the
|
|
57
|
+
// link only to have the far end hang up on a protocol error it was handed on
|
|
58
|
+
// purpose. Waiting cannot help either — no amount of draining makes it fit — so
|
|
59
|
+
// the honest answer is a refusal, with the frame unqueued and the connection
|
|
60
|
+
// untouched. Checked before encrypt() because the nonce counters run in
|
|
61
|
+
// lockstep: a message encrypted and then dropped would break every one after.
|
|
62
|
+
if (framer::kHeaderSize + payload.size() + session_->overhead() > framer::kMaxBlockSize) {
|
|
63
|
+
LOG_WARN("connection", "Peer " << remote_id_.short_hex() << " refused a "
|
|
64
|
+
<< payload.size() << " B message: past the " << framer::kMaxBlockSize
|
|
65
|
+
<< " B block ceiling; split it");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Weighed on the backlog as it stands BEFORE this message, never after it has
|
|
70
|
+
// been added. A message is indivisible — there is no queueing half of one — so
|
|
71
|
+
// judging a caller by the mark its own message has just crossed answers "that
|
|
72
|
+
// message was bigger than a limit nobody published" with a disconnection, on a
|
|
73
|
+
// connection that is idle, healthy and draining at full speed. What the mark is
|
|
74
|
+
// for is a peer that cannot keep up, and the evidence for that is a caller
|
|
75
|
+
// piling MORE on top of a backlog that is already over it.
|
|
76
|
+
//
|
|
77
|
+
// So one message may always be queued, whatever its size — the queue may exceed
|
|
78
|
+
// the mark by exactly that message and no more — and offering another before it
|
|
79
|
+
// has drained is what makes a caller a slow consumer. Nothing is ever dropped
|
|
80
|
+
// or refused here, which is what the layers above rely on: a relayed circuit
|
|
81
|
+
// (transport/relay_link.cpp) reports bytes as written the moment it hands them
|
|
82
|
+
// over, and a byte stream cannot survive one of them going missing.
|
|
83
|
+
const size_t backlog_before = backlog();
|
|
84
|
+
if (backlog_before > send_high_water_) {
|
|
85
|
+
LOG_WARN("connection", "Peer " << remote_id_.short_hex() << " offered more with "
|
|
86
|
+
<< backlog_before << " B still queued past the high-water mark ("
|
|
87
|
+
<< send_high_water_ << " B); closing as slow consumer");
|
|
88
|
+
close_reason_ = CloseReason::SlowConsumer;
|
|
89
|
+
state_ = ConnState::Closing;
|
|
90
|
+
reactor_.close(id_, CloseReason::SlowConsumer);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
54
94
|
Bytes inner;
|
|
55
95
|
framer::encode_message(inner, header, payload);
|
|
56
96
|
|
|
@@ -64,14 +104,6 @@ bool Connection::send(FrameHeader header, ByteView payload) {
|
|
|
64
104
|
queue_block(std::move(cipher));
|
|
65
105
|
|
|
66
106
|
const size_t backlog_now = backlog();
|
|
67
|
-
if (backlog_now > send_high_water_) {
|
|
68
|
-
LOG_WARN("connection", "Peer " << remote_id_.short_hex() << " over send high-water ("
|
|
69
|
-
<< backlog_now << " B); closing as slow consumer");
|
|
70
|
-
close_reason_ = CloseReason::SlowConsumer;
|
|
71
|
-
state_ = ConnState::Closing;
|
|
72
|
-
reactor_.close(id_, CloseReason::SlowConsumer);
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
107
|
|
|
76
108
|
// The queue has grown past what a caller should keep adding to. Everything
|
|
77
109
|
// still goes out — nothing is dropped here — but the answer to "may I send
|
|
@@ -85,8 +85,11 @@ public:
|
|
|
85
85
|
|
|
86
86
|
class Connection {
|
|
87
87
|
public:
|
|
88
|
-
/// High-water mark for the memory held by the send queue
|
|
89
|
-
/// connection
|
|
88
|
+
/// High-water mark for the memory held by the send queue. A caller that offers
|
|
89
|
+
/// another message while the queue is *still* over it has the connection closed
|
|
90
|
+
/// with CloseReason::SlowConsumer. The mark is never applied to the message that
|
|
91
|
+
/// crosses it: one message may always be queued, whatever its size (see send()),
|
|
92
|
+
/// so the queue's ceiling is this plus one message rather than this exactly.
|
|
90
93
|
static constexpr size_t kDefaultSendHighWater = 8 * 1024 * 1024;
|
|
91
94
|
|
|
92
95
|
/// Where send() starts answering "no room". A quarter of the hard limit, so a
|
|
@@ -125,6 +128,13 @@ public:
|
|
|
125
128
|
|
|
126
129
|
/// Queue an application frame for the peer. No-op unless Established.
|
|
127
130
|
///
|
|
131
|
+
/// Never refuses and never drops: whatever it is handed is queued, however
|
|
132
|
+
/// large. The size of a single message is not what the high-water mark is
|
|
133
|
+
/// about — a message cannot be queued by halves, so a connection that is
|
|
134
|
+
/// draining perfectly well must not be torn down over one big frame. What
|
|
135
|
+
/// closes a connection is offering *another* message while the queue is still
|
|
136
|
+
/// over the mark, which is the actual evidence that the peer is not keeping up.
|
|
137
|
+
///
|
|
128
138
|
/// @return whether there is still room for more. False means the queue is
|
|
129
139
|
/// past its low-water mark — this frame is queued like any other,
|
|
130
140
|
/// nothing is dropped, but the caller should stop and wait for
|
|
@@ -150,6 +150,27 @@ void Reactor::start_dial(ConnId id, const std::string& host, int port, Transport
|
|
|
150
150
|
conn->set_dial_address(host, static_cast<uint16_t>(port));
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
void Reactor::adopt_link(ConnId id, std::unique_ptr<Link> link, ConnRole role, bool connected) {
|
|
154
|
+
// A unique_ptr cannot be captured by a std::function, which is what the task
|
|
155
|
+
// queue holds — so the link travels in a shared holder it is moved out of on
|
|
156
|
+
// arrival. If the task is never run (a reactor stopped under us), the holder
|
|
157
|
+
// dies with it and takes the link with it, which is the right outcome.
|
|
158
|
+
auto holder = std::make_shared<std::unique_ptr<Link>>(std::move(link));
|
|
159
|
+
execute([this, id, holder, role, connected] {
|
|
160
|
+
Connection* conn = adopt(std::move(*holder), role, id);
|
|
161
|
+
if (!conn) return;
|
|
162
|
+
// Already-connected links skip the Connecting state entirely, exactly as an
|
|
163
|
+
// accepted socket does; the rest wait for the PollOut that wake() will bring
|
|
164
|
+
// once the far end answers.
|
|
165
|
+
if (connected) conn->start_handshake();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
void Reactor::wake(ConnId id, uint32_t events) {
|
|
170
|
+
if (events == 0) return;
|
|
171
|
+
execute([this, id, events] { dispatch_events(id, events); });
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
void Reactor::abort_dial(ConnId id, const std::string& host, int port) {
|
|
154
175
|
resolve_dial(id); // the id is spent either way; release any cancellation slot
|
|
155
176
|
delegate_.on_dial_aborted(index_, id, host, static_cast<uint16_t>(port));
|
|
@@ -243,6 +264,21 @@ void Reactor::run() {
|
|
|
243
264
|
if (mux_) timeout = mux_->next_timeout_ms(timeout);
|
|
244
265
|
const int n = poller_->wait(events, kMaxEvents, timeout);
|
|
245
266
|
|
|
267
|
+
// Empty the wakeup pipe BEFORE taking the task snapshot, never after.
|
|
268
|
+
//
|
|
269
|
+
// post() pushes a task and then writes a byte, and those two arrive here as
|
|
270
|
+
// two separate facts. Draining last would let a byte written *while this
|
|
271
|
+
// turn was running its tasks* be thrown away along with the ones that
|
|
272
|
+
// brought us here — and the task it belonged to was pushed after the
|
|
273
|
+
// snapshot, so it does not run either. It would then sit in the queue with
|
|
274
|
+
// nothing left to announce it, until the next poll timeout woke the loop
|
|
275
|
+
// for some other reason: up to kMaxPollMs of latency, at random.
|
|
276
|
+
//
|
|
277
|
+
// Draining first inverts that. A byte written any time after this line is
|
|
278
|
+
// still in the pipe when the loop comes back round, so wait() returns at
|
|
279
|
+
// once — whether or not its task made this turn's snapshot. The cost is one
|
|
280
|
+
// recv per turn, which is nothing against the work a turn does.
|
|
281
|
+
drain_wakeup();
|
|
246
282
|
drain_tasks(task_batch); // connect/close/send-arm
|
|
247
283
|
for (int i = 0; i < n; ++i) handle_event(events[i]);
|
|
248
284
|
timers_.run_due();
|
|
@@ -325,7 +361,9 @@ void Reactor::drain_wakeup() {
|
|
|
325
361
|
void Reactor::handle_event(const PollResult& ev) {
|
|
326
362
|
const socket_t fd = ev.fd;
|
|
327
363
|
|
|
328
|
-
|
|
364
|
+
// Already emptied at the top of the turn — see run(). Nothing to do but
|
|
365
|
+
// recognise it, so it is not mistaken for a connection's socket.
|
|
366
|
+
if (fd == wakeup_.fd()) return;
|
|
329
367
|
if (fd == server_socket_) { if (ev.events & PollIn) do_accept(); return; }
|
|
330
368
|
if (mux_ && fd == mux_->socket()) {
|
|
331
369
|
// Any event, not just PollIn. An error on a datagram socket belongs to one
|
|
@@ -479,16 +517,26 @@ void Reactor::mark_for_close(ConnId id, CloseReason reason) {
|
|
|
479
517
|
}
|
|
480
518
|
|
|
481
519
|
void Reactor::flush_dirty() {
|
|
482
|
-
if (dirty_.empty()) return;
|
|
483
520
|
// Swapped out first: a flush can close a connection, and a close can queue
|
|
484
521
|
// further work — none of which should extend the batch being walked.
|
|
522
|
+
//
|
|
523
|
+
// Looped, because a flush can also *book another one*: writing a relayed
|
|
524
|
+
// circuit hands its bytes to the connection carrying it (see
|
|
525
|
+
// transport/relay_link.h), which books its own flush from inside this one. A
|
|
526
|
+
// single pass would leave those bytes queued until the next turn of the loop —
|
|
527
|
+
// i.e. behind a poll wait — adding tens of milliseconds to every hop of a
|
|
528
|
+
// relayed path. The bound is a backstop against a pathological chain, not an
|
|
529
|
+
// expected limit: honest work needs two passes at most.
|
|
485
530
|
std::vector<ConnId> batch;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
531
|
+
for (int pass = 0; pass < kMaxFlushPasses && !dirty_.empty(); ++pass) {
|
|
532
|
+
batch.clear();
|
|
533
|
+
batch.swap(dirty_);
|
|
534
|
+
for (const ConnId id : batch) {
|
|
535
|
+
Connection* conn = find(id);
|
|
536
|
+
if (!conn) continue; // gone since it booked
|
|
537
|
+
if (conn->state() != ConnState::Established) continue; // closing; nothing to owe
|
|
538
|
+
if (!conn->flush_pending()) mark_for_close(id, conn->close_reason());
|
|
539
|
+
}
|
|
492
540
|
}
|
|
493
541
|
}
|
|
494
542
|
|
|
@@ -104,6 +104,41 @@ public:
|
|
|
104
104
|
/// Valid for an id whose dial has not started yet — it is cancelled instead.
|
|
105
105
|
void close(ConnId id, CloseReason reason);
|
|
106
106
|
|
|
107
|
+
// — links this reactor did not open itself ──────────────────────────────
|
|
108
|
+
//
|
|
109
|
+
// A relayed circuit is a byte stream that comes out of another peer's
|
|
110
|
+
// connection rather than out of a socket (see transport/relay_link.h), so the
|
|
111
|
+
// module that speaks the relay protocol builds the Link and hands it over.
|
|
112
|
+
// These three are that hand-over, and they are deliberately the whole of it:
|
|
113
|
+
// the reactor learns nothing about relaying, and the module learns nothing
|
|
114
|
+
// about reactors beyond "connections need to be woken".
|
|
115
|
+
//
|
|
116
|
+
// The circuit MUST be given to the reactor that owns its carrier's connection.
|
|
117
|
+
// That is what keeps every byte of it on one thread, exactly like the streams
|
|
118
|
+
// the mux hands over above, and it is the caller's job to arrange (see
|
|
119
|
+
// node/circuit_service.h).
|
|
120
|
+
|
|
121
|
+
/// Reserve a ConnId without starting anything. Thread-safe, and synchronous for
|
|
122
|
+
/// the same reason connect() is: the caller has to be able to name the
|
|
123
|
+
/// connection — to wake it, to close it — from the moment it asks for one,
|
|
124
|
+
/// rather than after a task it posted has run.
|
|
125
|
+
ConnId reserve_conn_id() noexcept {
|
|
126
|
+
return next_conn_id_.fetch_add(1, std::memory_order_relaxed);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Adopt an already-built Link under the id `reserve_conn_id()` handed out.
|
|
130
|
+
/// Thread-safe; the adoption itself runs on the reactor thread (inline when the
|
|
131
|
+
/// caller is already on it).
|
|
132
|
+
/// @param connected whether the far end is already there. Such a link starts its
|
|
133
|
+
/// handshake at once, like an accepted socket; one that is not waits for
|
|
134
|
+
/// a PollOut from wake(), exactly as an outbound connect does.
|
|
135
|
+
void adopt_link(ConnId id, std::unique_ptr<Link> link, ConnRole role, bool connected);
|
|
136
|
+
|
|
137
|
+
/// Deliver poll-equivalent events (PollIn/PollOut/PollErr) to a connection whose
|
|
138
|
+
/// Link has no socket for the poller to watch. Thread-safe. A no-op for an id
|
|
139
|
+
/// that is gone, so a late wake for a torn-down circuit costs nothing.
|
|
140
|
+
void wake(ConnId id, uint32_t events);
|
|
141
|
+
|
|
107
142
|
/// Drop a dial that lost its race — but only while it is still a dial. Once an
|
|
108
143
|
/// attempt has established it is not an attempt any more, it is one of possibly
|
|
109
144
|
/// several links to a peer, and which of those survives is the peer table's
|
|
@@ -175,6 +210,9 @@ private:
|
|
|
175
210
|
// Negligible idle cost; on epoll/kqueue the loop still wakes on real events,
|
|
176
211
|
// so this only caps idle latency.
|
|
177
212
|
static constexpr int kMaxPollMs = 50;
|
|
213
|
+
/// Passes flush_dirty() makes over the connections booked for a write. More than
|
|
214
|
+
/// one because a write can book another connection's write — see flush_dirty().
|
|
215
|
+
static constexpr int kMaxFlushPasses = 4;
|
|
178
216
|
/// Deadline from adopt() to reaching Established (covers connect + handshake).
|
|
179
217
|
static constexpr std::chrono::milliseconds kEstablishTimeout{15000};
|
|
180
218
|
/// Cadence of the housekeeping sweep over this reactor's connections (currently
|