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.
Files changed (40) hide show
  1. package/README.md +9 -0
  2. package/lib/index.d.ts +51 -6
  3. package/lib/index.js +42 -3
  4. package/native-src/CMakeLists.txt +13 -0
  5. package/native-src/src/librats/bindings/rats.cpp +90 -6
  6. package/native-src/src/librats/bindings/rats.h +92 -3
  7. package/native-src/src/librats/core/types.cpp +4 -3
  8. package/native-src/src/librats/core/types.h +25 -12
  9. package/native-src/src/librats/node/circuit_service.h +84 -0
  10. package/native-src/src/librats/node/config.h +9 -2
  11. package/native-src/src/librats/node/node.cpp +59 -5
  12. package/native-src/src/librats/node/node.h +38 -7
  13. package/native-src/src/librats/node/peer_network.h +19 -2
  14. package/native-src/src/librats/peer/peer.h +17 -6
  15. package/native-src/src/librats/peer/peer_table.cpp +41 -8
  16. package/native-src/src/librats/security/noise_security.cpp +2 -0
  17. package/native-src/src/librats/security/plaintext_security.h +1 -0
  18. package/native-src/src/librats/security/session.h +7 -0
  19. package/native-src/src/librats/storage/storage.cpp +434 -213
  20. package/native-src/src/librats/storage/storage.h +163 -19
  21. package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
  22. package/native-src/src/librats/subsystems/hole_punch.cpp +81 -8
  23. package/native-src/src/librats/subsystems/hole_punch.h +24 -0
  24. package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
  25. package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
  26. package/native-src/src/librats/subsystems/relay.cpp +1143 -0
  27. package/native-src/src/librats/subsystems/relay.h +211 -0
  28. package/native-src/src/librats/subsystems/relay_service.h +46 -0
  29. package/native-src/src/librats/transport/connection.cpp +40 -8
  30. package/native-src/src/librats/transport/connection.h +12 -2
  31. package/native-src/src/librats/transport/reactor.cpp +56 -8
  32. package/native-src/src/librats/transport/reactor.h +38 -0
  33. package/native-src/src/librats/transport/relay_link.cpp +208 -0
  34. package/native-src/src/librats/transport/relay_link.h +303 -0
  35. package/native-src/src/librats/util/logger.h +37 -5
  36. package/native-src/src/librats/util/network_monitor.cpp +14 -0
  37. package/native-src/src/librats/util/network_utils.cpp +10 -1
  38. package/native-src/src/librats/wire/frame.h +1 -0
  39. package/package.json +1 -1
  40. 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; exceeding it closes the
89
- /// connection with CloseReason::SlowConsumer.
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
- if (fd == wakeup_.fd()) { drain_wakeup(); return; }
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
- batch.swap(dirty_);
487
- for (const ConnId id : batch) {
488
- Connection* conn = find(id);
489
- if (!conn) continue; // gone since it booked
490
- if (conn->state() != ConnState::Established) continue; // closing; nothing to owe
491
- if (!conn->flush_pending()) mark_for_close(id, conn->close_reason());
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