librats 2.1.4 → 2.2.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 +21 -4
- package/lib/index.js +26 -2
- package/native-src/CMakeLists.txt +8 -0
- package/native-src/src/librats/bindings/rats.cpp +34 -3
- package/native-src/src/librats/bindings/rats.h +30 -2
- package/native-src/src/librats/core/types.cpp +3 -2
- package/native-src/src/librats/core/types.h +16 -6
- package/native-src/src/librats/node/circuit_service.h +84 -0
- package/native-src/src/librats/node/node.cpp +39 -0
- package/native-src/src/librats/node/node.h +12 -1
- package/native-src/src/librats/peer/peer_table.cpp +41 -8
- 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/relay.cpp +1142 -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/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/wire/frame.h +1 -0
- package/package.json +1 -1
- package/src/librats_node.cpp +26 -0
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#include "librats/transport/relay_link.h"
|
|
2
|
+
|
|
3
|
+
#include "librats/core/io_poller.h" // PollIn / PollOut / PollErr
|
|
4
|
+
#include "librats/util/logger.h"
|
|
5
|
+
|
|
6
|
+
#include <algorithm>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
#include <utility>
|
|
9
|
+
|
|
10
|
+
namespace librats {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
/// Slices one data message gathers from the caller's list. The send buffer hands
|
|
15
|
+
/// out a length prefix and a body per queued frame, so this covers a run of frames
|
|
16
|
+
/// rather than a single one; a longer backlog than this simply comes back on the
|
|
17
|
+
/// next turn of Connection::flush.
|
|
18
|
+
constexpr size_t kMaxParts = 16;
|
|
19
|
+
|
|
20
|
+
} // namespace
|
|
21
|
+
|
|
22
|
+
Circuit::Circuit(uint32_t id, std::shared_ptr<CircuitCarrier> carrier, bool open,
|
|
23
|
+
uint32_t recv_window, uint32_t peer_window)
|
|
24
|
+
: id_(id),
|
|
25
|
+
carrier_(std::move(carrier)),
|
|
26
|
+
state_(open ? State::Open : State::Pending),
|
|
27
|
+
recv_window_(recv_window == 0 ? kDefaultWindow : recv_window),
|
|
28
|
+
send_credit_(open ? peer_window : 0) {}
|
|
29
|
+
|
|
30
|
+
std::shared_ptr<Circuit> Circuit::opening(uint32_t id, std::shared_ptr<CircuitCarrier> carrier,
|
|
31
|
+
uint32_t recv_window) {
|
|
32
|
+
return std::shared_ptr<Circuit>(
|
|
33
|
+
new Circuit(id, std::move(carrier), /*open=*/false, recv_window, /*peer_window=*/0));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
std::shared_ptr<Circuit> Circuit::accepted(uint32_t id, std::shared_ptr<CircuitCarrier> carrier,
|
|
37
|
+
uint32_t peer_window, uint32_t recv_window) {
|
|
38
|
+
return std::shared_ptr<Circuit>(
|
|
39
|
+
new Circuit(id, std::move(carrier), /*open=*/true, recv_window, peer_window));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Fed by the relay module ─────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
uint32_t Circuit::on_data(ByteView bytes) {
|
|
45
|
+
if (state_ == State::Closed || bytes.empty()) return 0;
|
|
46
|
+
|
|
47
|
+
// The window is the ONE thing bounding what this circuit can cost us and the
|
|
48
|
+
// relay carrying it, so a peer that sends past what it was granted is not
|
|
49
|
+
// merely impolite — it is doing the thing the window exists to prevent. Fail
|
|
50
|
+
// the circuit rather than accept the bytes.
|
|
51
|
+
if (bytes.size() > recv_window_ - recv_in_flight_) {
|
|
52
|
+
LOG_WARN("relay", "Circuit " << id_ << " overran its receive window ("
|
|
53
|
+
<< bytes.size() << " B with " << (recv_window_ - recv_in_flight_)
|
|
54
|
+
<< " B granted); failing it");
|
|
55
|
+
return on_closed(CloseReason::ProtocolError, /*orderly=*/false);
|
|
56
|
+
}
|
|
57
|
+
recv_in_flight_ += static_cast<uint32_t>(bytes.size());
|
|
58
|
+
|
|
59
|
+
// Hand back the consumed prefix before growing: the connection normally drains
|
|
60
|
+
// the inbox to empty on every readable event, so this is nearly always a reset
|
|
61
|
+
// of an already-empty buffer rather than a move.
|
|
62
|
+
if (inbox_read_ == inbox_.size()) {
|
|
63
|
+
inbox_.clear();
|
|
64
|
+
inbox_read_ = 0;
|
|
65
|
+
} else if (inbox_read_ >= kCompactThreshold) {
|
|
66
|
+
inbox_.erase(inbox_.begin(), inbox_.begin() + static_cast<std::ptrdiff_t>(inbox_read_));
|
|
67
|
+
inbox_read_ = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
inbox_.insert(inbox_.end(), bytes.begin(), bytes.end());
|
|
71
|
+
return PollIn;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
uint32_t Circuit::on_accept(uint32_t peer_window) {
|
|
75
|
+
if (state_ != State::Pending) return 0;
|
|
76
|
+
state_ = State::Open;
|
|
77
|
+
send_credit_ = peer_window;
|
|
78
|
+
// Unconditional, unlike the other openings below: the Connection is still in
|
|
79
|
+
// Connecting and has nothing queued, so want_write_ is false — yet this is
|
|
80
|
+
// precisely the event it is waiting for to finish connecting and start its
|
|
81
|
+
// handshake (see Connection::on_writable).
|
|
82
|
+
return PollOut;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
uint32_t Circuit::on_credit(uint32_t bytes) {
|
|
86
|
+
if (state_ == State::Closed || bytes == 0) return 0;
|
|
87
|
+
// Saturate rather than wrap: a far end that grants nonsense should cost us
|
|
88
|
+
// nothing worse than an over-generous window, and the receiver's own window
|
|
89
|
+
// check is what actually bounds the traffic.
|
|
90
|
+
if (bytes > UINT32_MAX - send_credit_) send_credit_ = UINT32_MAX;
|
|
91
|
+
else send_credit_ += bytes;
|
|
92
|
+
|
|
93
|
+
if (!credit_blocked_) return 0;
|
|
94
|
+
credit_blocked_ = false;
|
|
95
|
+
return writable_event();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
uint32_t Circuit::on_closed(CloseReason reason, bool orderly) {
|
|
99
|
+
if (state_ == State::Closed) return 0;
|
|
100
|
+
state_ = State::Closed;
|
|
101
|
+
reason_ = reason;
|
|
102
|
+
orderly_ = orderly;
|
|
103
|
+
// An orderly close still owes the application whatever the far end sent before
|
|
104
|
+
// it: read() drains the inbox first and only then reports the end of stream, so
|
|
105
|
+
// what this needs is a readable event, not an error. That is the same contract
|
|
106
|
+
// the other links keep (see Link::read).
|
|
107
|
+
return orderly ? PollIn : PollErr;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
uint32_t Circuit::on_carrier_writable() {
|
|
111
|
+
if (!carrier_blocked_) return 0;
|
|
112
|
+
carrier_blocked_ = false;
|
|
113
|
+
return writable_event();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
uint32_t Circuit::writable_event() const noexcept {
|
|
117
|
+
if (blocked() || !want_write_ || state_ != State::Open) return 0;
|
|
118
|
+
return PollOut;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Driven by the Connection ────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
Link::IoResult Circuit::read(ByteSpan into) {
|
|
124
|
+
const size_t available = inbox_.size() - inbox_read_;
|
|
125
|
+
const size_t n = (std::min)(available, into.size());
|
|
126
|
+
if (n > 0) {
|
|
127
|
+
std::memcpy(into.data(), inbox_.data() + inbox_read_, n);
|
|
128
|
+
inbox_read_ += n;
|
|
129
|
+
if (inbox_read_ == inbox_.size()) {
|
|
130
|
+
inbox_.clear();
|
|
131
|
+
inbox_read_ = 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// These bytes are out of the pipe, so the far end may put that much back
|
|
135
|
+
// in. Granted in batches: one small message per half-window rather than one
|
|
136
|
+
// per chunk, which is what keeps the credit scheme's overhead invisible.
|
|
137
|
+
recv_in_flight_ -= static_cast<uint32_t>(n);
|
|
138
|
+
recv_ungranted_ += static_cast<uint32_t>(n);
|
|
139
|
+
if (state_ != State::Closed && recv_ungranted_ >= recv_window_ / kCreditGrantFraction) {
|
|
140
|
+
carrier_->circuit_send_credit(id_, recv_ungranted_);
|
|
141
|
+
recv_ungranted_ = 0;
|
|
142
|
+
}
|
|
143
|
+
return {n, Link::Status::Ok};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Drained. Only now may the end of the stream be reported — data and
|
|
147
|
+
// end-of-stream are never delivered together.
|
|
148
|
+
if (state_ == State::Closed)
|
|
149
|
+
return {0, orderly_ ? Link::Status::Closed : Link::Status::Error};
|
|
150
|
+
return {0, Link::Status::WouldBlock};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Link::IoResult Circuit::write(const ByteView* slices, size_t count) {
|
|
154
|
+
if (state_ == State::Closed) return {0, Link::Status::Error};
|
|
155
|
+
// Pending: opened but not yet accepted. Nothing may go out — and nothing tries
|
|
156
|
+
// to, since the Connection is still Connecting.
|
|
157
|
+
if (state_ != State::Open) return {0, Link::Status::WouldBlock};
|
|
158
|
+
if (blocked()) return {0, Link::Status::WouldBlock};
|
|
159
|
+
|
|
160
|
+
const size_t budget = (std::min)(static_cast<size_t>(send_credit_), kMaxDataChunk);
|
|
161
|
+
if (budget == 0) {
|
|
162
|
+
credit_blocked_ = true;
|
|
163
|
+
return {0, Link::Status::WouldBlock};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// One data message per call, gathered across as many of the caller's slices as
|
|
167
|
+
// fit. Connection::flush loops while a link keeps reporting Ok, so a backlog
|
|
168
|
+
// larger than one chunk simply comes back around — no loop is needed here, and
|
|
169
|
+
// each turn of that loop re-checks the credit and the carrier.
|
|
170
|
+
ByteView parts[kMaxParts];
|
|
171
|
+
size_t nparts = 0;
|
|
172
|
+
size_t taken = 0;
|
|
173
|
+
for (size_t i = 0; i < count && taken < budget && nparts < kMaxParts; ++i) {
|
|
174
|
+
if (slices[i].empty()) continue;
|
|
175
|
+
const size_t take = (std::min)(slices[i].size(), budget - taken);
|
|
176
|
+
parts[nparts++] = ByteView(slices[i].data(), take);
|
|
177
|
+
taken += take;
|
|
178
|
+
}
|
|
179
|
+
if (taken == 0) return {0, Link::Status::WouldBlock};
|
|
180
|
+
|
|
181
|
+
send_credit_ -= static_cast<uint32_t>(taken);
|
|
182
|
+
// The carrier queues what it is given either way; false is "stop and wait", so
|
|
183
|
+
// the bytes are ours to report as written and the block applies to the NEXT
|
|
184
|
+
// call (see CircuitCarrier::circuit_send_data).
|
|
185
|
+
if (!carrier_->circuit_send_data(id_, parts, nparts)) carrier_blocked_ = true;
|
|
186
|
+
return {taken, Link::Status::Ok};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
void Circuit::shutdown(CloseReason reason) {
|
|
190
|
+
if (!close_sent_) {
|
|
191
|
+
close_sent_ = true;
|
|
192
|
+
carrier_->circuit_send_close(id_, reason);
|
|
193
|
+
}
|
|
194
|
+
if (state_ != State::Closed) {
|
|
195
|
+
state_ = State::Closed;
|
|
196
|
+
reason_ = reason;
|
|
197
|
+
orderly_ = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
void Circuit::release() {
|
|
202
|
+
if (released_) return;
|
|
203
|
+
released_ = true;
|
|
204
|
+
state_ = State::Closed;
|
|
205
|
+
carrier_->circuit_released(id_);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
} // namespace librats
|