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,1143 @@
1
+ #include "librats/subsystems/relay.h"
2
+
3
+ #include "librats/core/io_poller.h" // PollIn / PollOut / PollErr
4
+ #include "librats/node/circuit_service.h"
5
+ #include "librats/node/node_context.h"
6
+ #include "librats/peer/peer.h"
7
+ #include "librats/peer/peer_info.h"
8
+ #include "librats/subsystems/hole_punch_service.h"
9
+ #include "librats/util/logger.h"
10
+
11
+ #include <algorithm>
12
+ #include <optional>
13
+ #include <unordered_map>
14
+ #include <utility>
15
+ #include <vector>
16
+
17
+ namespace librats {
18
+
19
+ namespace {
20
+
21
+ // ── Wire format (MessageType::Relay), big-endian ────────────────────────────
22
+ //
23
+ // [u8 ver=1][u8 op][u32 circuit][body]
24
+ //
25
+ // op=1 Open [32B dst][u32 window] client → relay
26
+ // op=2 Incoming [32B src][u32 window] relay → target
27
+ // op=3 Accept [u32 window] target → relay → client
28
+ // op=4 Deny [u8 reason] relay → client, or target → relay → client
29
+ // op=5 Data [bytes] end to end, through the relay
30
+ // op=6 Credit [u32 bytes] end to end, through the relay
31
+ // op=7 Close [u8 reason] either end, or the relay
32
+ // op=8 Probe [32B dst] client → relay (circuit = 0)
33
+ // op=9 ProbeOk [32B dst] relay → client (circuit = 0)
34
+ //
35
+ // `circuit` is scoped to the LINK the message travels on, never globally: the
36
+ // relay holds two ids for one circuit, one per side, and translates between them.
37
+ // Both ends of a link allocate ids from that one space without coordinating, by
38
+ // parity — the smaller PeerId takes the even ids, the larger the odd ones, which
39
+ // both compute identically from what they already know. Ids 0 and 1 are reserved.
40
+ //
41
+ // Every length is bounds-checked and the payload is capped, so a malformed or
42
+ // hostile message is dropped rather than acted on.
43
+
44
+ constexpr uint8_t kVersion = 1;
45
+ constexpr size_t kHeaderSize = 6; // ver + op + u32 circuit
46
+
47
+ constexpr uint8_t kOpOpen = 1;
48
+ constexpr uint8_t kOpIncoming = 2;
49
+ constexpr uint8_t kOpAccept = 3;
50
+ constexpr uint8_t kOpDeny = 4;
51
+ constexpr uint8_t kOpData = 5;
52
+ constexpr uint8_t kOpCredit = 6;
53
+ constexpr uint8_t kOpClose = 7;
54
+ constexpr uint8_t kOpProbe = 8;
55
+ constexpr uint8_t kOpProbeOk = 9;
56
+
57
+ constexpr uint8_t kDenyNoTarget = 0; ///< the relay does not hold that peer
58
+ constexpr uint8_t kDenyNotPermitted = 1; ///< relaying is off here
59
+ constexpr uint8_t kDenyResourceLimit = 2; ///< over a circuit or rate budget
60
+ constexpr uint8_t kDenyRefused = 3; ///< the target itself said no
61
+ constexpr uint8_t kDenyLoop = 4; ///< one end is itself only reachable by relay
62
+
63
+ constexpr uint8_t kCloseNormal = 0; ///< an orderly end of stream
64
+ constexpr uint8_t kCloseError = 1; ///< anything else
65
+
66
+ /// Reserved: a message that is about no particular circuit (Probe / ProbeOk).
67
+ constexpr uint32_t kNoCircuit = 0;
68
+ /// First id either side may allocate; 0 and 1 are reserved so parity still works.
69
+ constexpr uint32_t kFirstEvenId = 2;
70
+ constexpr uint32_t kFirstOddId = 3;
71
+ /// Where the allocator wraps. Well below UINT32_MAX so the wrap needs no special
72
+ /// case for the parity of the last step.
73
+ constexpr uint32_t kIdWrap = 0xFFFFFF00u;
74
+
75
+ const char* deny_text(uint8_t reason) {
76
+ switch (reason) {
77
+ case kDenyNoTarget: return "the relay does not hold the target";
78
+ case kDenyNotPermitted: return "relaying is not offered";
79
+ case kDenyResourceLimit: return "the relay is at its limit";
80
+ case kDenyRefused: return "the target refused";
81
+ case kDenyLoop: return "one end is itself relayed";
82
+ default: return "unspecified";
83
+ }
84
+ }
85
+
86
+ void put_u32(Bytes& out, uint32_t v) {
87
+ out.push_back(static_cast<uint8_t>(v >> 24));
88
+ out.push_back(static_cast<uint8_t>(v >> 16));
89
+ out.push_back(static_cast<uint8_t>(v >> 8));
90
+ out.push_back(static_cast<uint8_t>(v));
91
+ }
92
+
93
+ uint32_t get_u32(const uint8_t* p) {
94
+ return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
95
+ (static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);
96
+ }
97
+
98
+ /// A message with its header written and room reserved for `body` bytes more.
99
+ Bytes message(uint8_t op, uint32_t circuit, size_t body = 0) {
100
+ Bytes out;
101
+ out.reserve(kHeaderSize + body);
102
+ out.push_back(kVersion);
103
+ out.push_back(op);
104
+ put_u32(out, circuit);
105
+ return out;
106
+ }
107
+
108
+ void append_id(Bytes& out, const PeerId& id) {
109
+ const auto& raw = id.bytes();
110
+ out.insert(out.end(), raw.begin(), raw.end());
111
+ }
112
+
113
+ /// How a peer is reachable right now. The distinction that matters everywhere in
114
+ /// this file is Direct vs Relayed: a circuit whose either end is itself relayed
115
+ /// would be a chain, and chains are what turn a relay mesh into an amplifier.
116
+ enum class Reach { None, Direct, Relayed };
117
+
118
+ Reach reach_of(PeerNetwork& network, const PeerId& id) {
119
+ // A snapshot, which is not free — so this is only ever called on the rare
120
+ // paths (opening a circuit, answering a request), never per data message.
121
+ for (const PeerInfo& info : network.peers())
122
+ if (info.id == id)
123
+ return info.transport == TransportKind::Relay ? Reach::Relayed : Reach::Direct;
124
+ return Reach::None;
125
+ }
126
+
127
+ } // namespace
128
+
129
+ // ── Shared state ────────────────────────────────────────────────────────────
130
+
131
+ struct Relay::State : public std::enable_shared_from_this<Relay::State> {
132
+ using Clock = std::chrono::steady_clock;
133
+
134
+ /// One circuit this node TERMINATES: a relayed peer of ours.
135
+ struct Leg {
136
+ std::shared_ptr<Circuit> circuit;
137
+ PeerRoute route{};
138
+ PeerId target; ///< the far end; a hint until the handshake
139
+ bool outbound = false; ///< we opened it
140
+ };
141
+
142
+ /// What one circuit this node CARRIES has cost so far. Shared by the circuit's
143
+ /// two legs, so either side's accounting is the same accounting.
144
+ struct Meter {
145
+ uint64_t bytes = 0;
146
+ Clock::time_point started{};
147
+ };
148
+
149
+ /// One circuit this node carries: where its other side is.
150
+ struct Forward {
151
+ PeerId peer;
152
+ uint32_t id;
153
+ std::shared_ptr<Meter> meter;
154
+ /// Whether the peer whose link holds THIS entry is the one that opened the
155
+ /// circuit. Only the opener is charged for it: being a popular destination
156
+ /// is not something a peer chooses, and charging for it would let anyone
157
+ /// use up a third party's budget by opening circuits toward them.
158
+ bool opener = false;
159
+ };
160
+
161
+ /// Everything about one peer link: the ids we have allocated on it, the
162
+ /// circuits riding it in either role, and what that peer has spent lately.
163
+ struct PeerLink {
164
+ uint32_t next_id = 0;
165
+ std::unordered_map<uint32_t, Leg> legs;
166
+ std::unordered_map<uint32_t, Forward> forwards;
167
+ size_t opened = 0; ///< circuits this peer opened
168
+ Clock::time_point window_started{};
169
+ size_t requests = 0;
170
+ };
171
+
172
+ /// One search for a way to reach a peer.
173
+ struct Attempt {
174
+ Clock::time_point deadline{};
175
+ std::vector<PeerId> ready; ///< relays that said they hold the target
176
+ bool opening = false; ///< a circuit is in flight right now
177
+ PeerId via; ///< the relay it is in flight through
178
+ uint32_t circuit = kNoCircuit;
179
+ };
180
+
181
+ /// The circuit's way out. One per circuit rather than one per module, because
182
+ /// a circuit id means nothing without the link it was allocated on — and this
183
+ /// is where that link is remembered. Defined below.
184
+ class Carrier;
185
+
186
+ Config config;
187
+ PeerNetwork* network = nullptr;
188
+ CircuitService* circuits = nullptr;
189
+ std::atomic<HolePunchService*> punch{nullptr};
190
+ std::atomic<bool> running{false};
191
+ std::atomic<uint64_t> carried_bytes{0};
192
+
193
+ mutable std::mutex mutex;
194
+ std::unordered_map<PeerId, PeerLink, PeerId::Hash> links;
195
+ std::unordered_map<PeerId, Attempt, PeerId::Hash> attempts;
196
+ std::unordered_map<PeerId, Clock::time_point, PeerId::Hash> cooldown;
197
+ size_t outbound_circuits = 0;
198
+ size_t inbound_circuits = 0;
199
+ size_t carried_circuits = 0;
200
+
201
+ // — inbound dispatch (reactor threads) —
202
+ void handle(const Peer& from, ByteView payload);
203
+ void handle_probe(const PeerId& from, ByteView body);
204
+ void handle_probe_ok(const PeerId& from, ByteView body);
205
+ void handle_open(const PeerId& from, uint32_t circuit, ByteView body);
206
+ void handle_incoming(const PeerId& from, uint32_t circuit, ByteView body);
207
+ void handle_accept(const PeerId& from, uint32_t circuit, ByteView body);
208
+ void handle_deny(const PeerId& from, uint32_t circuit, ByteView body);
209
+ void handle_data(const PeerId& from, uint32_t circuit, ByteView body);
210
+ void handle_credit(const PeerId& from, uint32_t circuit, ByteView body);
211
+ void handle_close(const PeerId& from, uint32_t circuit, ByteView body);
212
+
213
+ void on_peer_connected(const Peer& peer);
214
+ void on_peer_disconnected(const PeerId& id);
215
+ void on_peer_writable(const PeerId& id);
216
+
217
+ // — the client half —
218
+ bool begin_attempt(const PeerId& target);
219
+ /// Move an attempt on to the next relay that answered, or leave it to time out.
220
+ /// Picks the candidate and commits to it under one lock: a relay taken out of
221
+ /// `ready` before the last refusal can check is a relay that is never tried.
222
+ void advance(const PeerId& target);
223
+ /// Detach an attempt from the outbound circuit it was waiting on, so that the
224
+ /// next `advance` may try another relay. Caller holds the mutex. The attempt
225
+ /// itself stays: it keeps its deadline and whatever is left in `ready`.
226
+ void release_attempt(const PeerId& target, const PeerId& carrier, uint32_t circuit);
227
+ void finish_attempt(const PeerId& target, bool succeeded);
228
+
229
+ // — bookkeeping —
230
+ /// Caller holds the mutex.
231
+ uint32_t allocate_id(PeerLink& link, const PeerId& peer);
232
+ /// Caller holds the mutex. Whether `peer` may make one more request now.
233
+ bool spend_request(const PeerId& peer);
234
+ /// Caller holds the mutex.
235
+ bool in_cooldown(const PeerId& target) const;
236
+ /// Take a circuit we terminate out of the tables, and say what to do with it.
237
+ /// The circuit is NOT closed here — the caller does that outside the mutex.
238
+ struct Dropped {
239
+ std::shared_ptr<Circuit> circuit;
240
+ PeerRoute route{};
241
+ PeerId target;
242
+ bool found = false;
243
+ bool outbound = false;
244
+ };
245
+ Dropped take_leg(const PeerId& carrier, uint32_t circuit);
246
+ /// Hand `events` to a circuit's connection. No-op for an unset route.
247
+ void wake(PeerRoute route, uint32_t events);
248
+
249
+ // — the relay half —
250
+ /// Erase one entry of a carried circuit and un-charge its opener. Caller holds
251
+ /// the mutex. Returns what it pointed at, if anything.
252
+ std::optional<Forward> erase_forward(const PeerId& peer, uint32_t circuit);
253
+ /// End a circuit we carry and tell whoever still needs telling: `tell_far` for
254
+ /// the side opposite `peer`, `tell_near` for `peer` itself — which is wanted
255
+ /// when WE are ending the circuit, and not when we are passing on an ending
256
+ /// that came from `peer` in the first place. Caller must not hold the mutex.
257
+ void close_carried(const PeerId& peer, uint32_t circuit, uint8_t reason,
258
+ bool tell_far, bool tell_near);
259
+ void deny(const PeerId& to, uint32_t circuit, uint8_t reason);
260
+
261
+ void send(const PeerId& to, const Bytes& msg) {
262
+ if (network) network->send(to, MessageType::Relay, ByteView(msg));
263
+ }
264
+
265
+ void tick();
266
+ void shutdown();
267
+ };
268
+
269
+ class Relay::State::Carrier final : public CircuitCarrier {
270
+ public:
271
+ Carrier(std::shared_ptr<Relay::State> state, PeerId carrier)
272
+ : state_(std::move(state)), carrier_(std::move(carrier)) {}
273
+
274
+ bool circuit_send_data(uint32_t circuit, const ByteView* slices, size_t count) override {
275
+ size_t total = 0;
276
+ for (size_t i = 0; i < count; ++i) total += slices[i].size();
277
+
278
+ Bytes msg = message(kOpData, circuit, total);
279
+ for (size_t i = 0; i < count; ++i)
280
+ msg.insert(msg.end(), slices[i].begin(), slices[i].end());
281
+
282
+ if (!state_->network) return false;
283
+ return state_->network->send(carrier_, MessageType::Relay, ByteView(msg));
284
+ }
285
+
286
+ void circuit_send_credit(uint32_t circuit, uint32_t bytes) override {
287
+ Bytes msg = message(kOpCredit, circuit, 4);
288
+ put_u32(msg, bytes);
289
+ state_->send(carrier_, msg);
290
+ }
291
+
292
+ void circuit_send_close(uint32_t circuit, CloseReason reason) override {
293
+ Bytes msg = message(kOpClose, circuit, 1);
294
+ msg.push_back(reason == CloseReason::LocalClose || reason == CloseReason::PeerClosed
295
+ ? kCloseNormal
296
+ : kCloseError);
297
+ state_->send(carrier_, msg);
298
+ }
299
+
300
+ void circuit_released(uint32_t circuit) override {
301
+ // May arrive on any thread — see CircuitCarrier::circuit_released — so it
302
+ // does the least it possibly can: forget the circuit. Anything that has to
303
+ // follow (retrying an attempt through another relay) is left to the worker,
304
+ // which finds the attempt with nothing in flight on its next tick.
305
+ std::lock_guard<std::mutex> lock(state_->mutex);
306
+ auto link = state_->links.find(carrier_);
307
+ if (link == state_->links.end()) return;
308
+ auto leg = link->second.legs.find(circuit);
309
+ if (leg == link->second.legs.end()) return;
310
+
311
+ if (leg->second.outbound) {
312
+ if (state_->outbound_circuits > 0) --state_->outbound_circuits;
313
+ auto attempt = state_->attempts.find(leg->second.target);
314
+ if (attempt != state_->attempts.end() && attempt->second.circuit == circuit &&
315
+ attempt->second.via == carrier_) {
316
+ attempt->second.opening = false;
317
+ attempt->second.circuit = kNoCircuit;
318
+ }
319
+ } else if (state_->inbound_circuits > 0) {
320
+ --state_->inbound_circuits;
321
+ }
322
+ link->second.legs.erase(leg);
323
+ }
324
+
325
+ private:
326
+ std::shared_ptr<Relay::State> state_;
327
+ PeerId carrier_;
328
+ };
329
+
330
+ // ── Dispatch ────────────────────────────────────────────────────────────────
331
+
332
+ void Relay::State::handle(const Peer& from, ByteView payload) {
333
+ if (!running.load()) return;
334
+ if (payload.size() < kHeaderSize) return;
335
+ // The largest honest message is one data chunk plus its header. Anything past
336
+ // that is malformed by construction and is not worth looking at.
337
+ if (payload.size() > kHeaderSize + Circuit::kMaxDataChunk) return;
338
+ if (payload.data()[0] != kVersion) return;
339
+
340
+ const uint8_t op = payload.data()[1];
341
+ const uint32_t circuit = get_u32(payload.data() + 2);
342
+ const ByteView body(payload.data() + kHeaderSize, payload.size() - kHeaderSize);
343
+ const PeerId& peer = from.id();
344
+
345
+ switch (op) {
346
+ case kOpProbe: handle_probe(peer, body); break;
347
+ case kOpProbeOk: handle_probe_ok(peer, body); break;
348
+ case kOpOpen: handle_open(peer, circuit, body); break;
349
+ case kOpIncoming: handle_incoming(peer, circuit, body); break;
350
+ case kOpAccept: handle_accept(peer, circuit, body); break;
351
+ case kOpDeny: handle_deny(peer, circuit, body); break;
352
+ case kOpData: handle_data(peer, circuit, body); break;
353
+ case kOpCredit: handle_credit(peer, circuit, body); break;
354
+ case kOpClose: handle_close(peer, circuit, body); break;
355
+ default: break; // an op from a newer version: ignore, never fatal
356
+ }
357
+ }
358
+
359
+ // ── The relay half: answering for peers we hold ─────────────────────────────
360
+
361
+ void Relay::State::handle_probe(const PeerId& from, ByteView body) {
362
+ if (!config.serve) return;
363
+ if (body.size() < PeerId::kSize) return;
364
+ const auto dst = PeerId::from_bytes(ByteView(body.data(), PeerId::kSize));
365
+ if (!dst || *dst == from || *dst == network->local_id()) return;
366
+
367
+ {
368
+ std::lock_guard<std::mutex> lock(mutex);
369
+ if (!spend_request(from)) return;
370
+ if (carried_circuits >= config.max_circuits) return;
371
+ }
372
+
373
+ // Only a peer we hold DIRECTLY counts. Answering for a peer we ourselves reach
374
+ // through a relay would offer a chain, and the open would be refused anyway.
375
+ if (reach_of(*network, *dst) != Reach::Direct) return;
376
+
377
+ Bytes msg = message(kOpProbeOk, kNoCircuit, PeerId::kSize);
378
+ append_id(msg, *dst);
379
+ send(from, msg);
380
+ }
381
+
382
+ void Relay::State::handle_open(const PeerId& from, uint32_t circuit, ByteView body) {
383
+ if (circuit == kNoCircuit) return;
384
+ if (!config.serve) return deny(from, circuit, kDenyNotPermitted);
385
+ if (body.size() < PeerId::kSize + 4) return deny(from, circuit, kDenyNotPermitted);
386
+
387
+ const auto dst = PeerId::from_bytes(ByteView(body.data(), PeerId::kSize));
388
+ if (!dst) return deny(from, circuit, kDenyNoTarget);
389
+ if (*dst == from || *dst == network->local_id())
390
+ return deny(from, circuit, kDenyNoTarget);
391
+ const uint32_t window = get_u32(body.data() + PeerId::kSize);
392
+
393
+ {
394
+ std::lock_guard<std::mutex> lock(mutex);
395
+ if (!spend_request(from)) return deny(from, circuit, kDenyResourceLimit);
396
+ if (carried_circuits >= config.max_circuits)
397
+ return deny(from, circuit, kDenyResourceLimit);
398
+ PeerLink& link = links[from]; // spend_request just created it if needed
399
+ if (link.opened >= config.max_circuits_per_peer)
400
+ return deny(from, circuit, kDenyResourceLimit);
401
+ // Already known: a retransmitted Open, or a peer reusing an id it still has
402
+ // a circuit on. Either way there is nothing new to set up.
403
+ if (link.forwards.count(circuit) || link.legs.count(circuit)) return;
404
+ }
405
+
406
+ // Neither end may itself be relayed. This is the rule that keeps circuits from
407
+ // being chained: a chain multiplies one peer's bytes across several relays and
408
+ // gives a loop somewhere to form.
409
+ if (reach_of(*network, from) != Reach::Direct) return deny(from, circuit, kDenyLoop);
410
+ switch (reach_of(*network, *dst)) {
411
+ case Reach::None: return deny(from, circuit, kDenyNoTarget);
412
+ case Reach::Relayed: return deny(from, circuit, kDenyLoop);
413
+ case Reach::Direct: break;
414
+ }
415
+
416
+ uint32_t far_id = kNoCircuit;
417
+ {
418
+ // Re-checked: the reachability lookups above ran without the lock, and a
419
+ // burst of Opens could otherwise all pass the test and then all allocate.
420
+ std::lock_guard<std::mutex> lock(mutex);
421
+ if (carried_circuits >= config.max_circuits)
422
+ return deny(from, circuit, kDenyResourceLimit);
423
+ PeerLink& near_link = links[from];
424
+ if (near_link.opened >= config.max_circuits_per_peer)
425
+ return deny(from, circuit, kDenyResourceLimit);
426
+ if (near_link.forwards.count(circuit)) return;
427
+
428
+ PeerLink& far_link = links[*dst];
429
+ far_id = allocate_id(far_link, *dst);
430
+
431
+ auto meter = std::make_shared<Meter>();
432
+ meter->started = Clock::now();
433
+
434
+ links[from].forwards.emplace(circuit, Forward{*dst, far_id, meter, /*opener=*/true});
435
+ links[*dst].forwards.emplace(far_id, Forward{from, circuit, meter, /*opener=*/false});
436
+ ++links[from].opened;
437
+ ++carried_circuits;
438
+ }
439
+
440
+ LOG_DEBUG("relay", "Carrying a circuit from " << from.short_hex() << " to "
441
+ << dst->short_hex());
442
+
443
+ Bytes msg = message(kOpIncoming, far_id, PeerId::kSize + 4);
444
+ append_id(msg, from);
445
+ put_u32(msg, window);
446
+ send(*dst, msg);
447
+ }
448
+
449
+ // ── The target half: a circuit opened toward us ─────────────────────────────
450
+
451
+ void Relay::State::handle_incoming(const PeerId& from, uint32_t circuit, ByteView body) {
452
+ if (circuit == kNoCircuit) return;
453
+ if (!config.accept_inbound) return deny(from, circuit, kDenyRefused);
454
+ if (body.size() < PeerId::kSize + 4) return deny(from, circuit, kDenyRefused);
455
+
456
+ const auto src = PeerId::from_bytes(ByteView(body.data(), PeerId::kSize));
457
+ if (!src || *src == network->local_id()) return deny(from, circuit, kDenyRefused);
458
+ const uint32_t peer_window = get_u32(body.data() + PeerId::kSize);
459
+
460
+ // `src` is the relay's word, not the far end's — the handshake is what actually
461
+ // establishes who is there. It is used only to turn away a circuit that would
462
+ // duplicate a peer we already hold, which costs the far end a Deny instead of a
463
+ // whole handshake it was going to lose anyway.
464
+ if (reach_of(*network, *src) == Reach::Direct) return deny(from, circuit, kDenyRefused);
465
+ // And the carrier itself must be a direct peer: a circuit inside a circuit is
466
+ // the chain this refuses to be part of.
467
+ if (reach_of(*network, from) != Reach::Direct) return deny(from, circuit, kDenyLoop);
468
+
469
+ std::shared_ptr<Circuit> incoming;
470
+ {
471
+ std::lock_guard<std::mutex> lock(mutex);
472
+ if (!running.load()) return deny(from, circuit, kDenyRefused);
473
+ if (inbound_circuits >= config.max_inbound_circuits) return deny(from, circuit, kDenyResourceLimit);
474
+ PeerLink& link = links[from];
475
+ if (link.legs.count(circuit) || link.forwards.count(circuit)) return; // a repeat
476
+
477
+ // Open from the first instant: the far end is already there, and its
478
+ // request carried the window it will receive.
479
+ incoming = Circuit::accepted(circuit, std::make_shared<Carrier>(shared_from_this(), from),
480
+ peer_window, config.window);
481
+ link.legs.emplace(circuit, Leg{incoming, PeerRoute{}, *src, /*outbound=*/false});
482
+ ++inbound_circuits;
483
+ }
484
+
485
+ const auto route = circuits->adopt_circuit(from, std::make_unique<RelayLink>(incoming),
486
+ ConnRole::Inbound, /*connected=*/true);
487
+ if (!route) {
488
+ // The node will not take another peer, or the carrier went away between the
489
+ // two lines above. Either way nothing was started.
490
+ take_leg(from, circuit);
491
+ return deny(from, circuit, kDenyResourceLimit);
492
+ }
493
+ {
494
+ std::lock_guard<std::mutex> lock(mutex);
495
+ auto link = links.find(from);
496
+ if (link != links.end()) {
497
+ auto leg = link->second.legs.find(circuit);
498
+ if (leg != link->second.legs.end()) leg->second.route = *route;
499
+ }
500
+ }
501
+
502
+ Bytes msg = message(kOpAccept, circuit, 4);
503
+ put_u32(msg, config.window);
504
+ send(from, msg);
505
+ LOG_DEBUG("relay", "Accepted a circuit from " << src->short_hex() << " via "
506
+ << from.short_hex());
507
+ }
508
+
509
+ // ── Messages that are either ours to act on or ours to forward ──────────────
510
+
511
+ void Relay::State::handle_accept(const PeerId& from, uint32_t circuit, ByteView body) {
512
+ if (body.size() < 4) return;
513
+ const uint32_t window = get_u32(body.data());
514
+
515
+ std::shared_ptr<Circuit> ours;
516
+ PeerRoute route{};
517
+ std::optional<Forward> forward;
518
+ {
519
+ std::lock_guard<std::mutex> lock(mutex);
520
+ auto link = links.find(from);
521
+ if (link == links.end()) return;
522
+ if (auto leg = link->second.legs.find(circuit); leg != link->second.legs.end()) {
523
+ ours = leg->second.circuit;
524
+ route = leg->second.route;
525
+ } else if (auto fwd = link->second.forwards.find(circuit);
526
+ fwd != link->second.forwards.end()) {
527
+ forward = fwd->second;
528
+ }
529
+ }
530
+
531
+ if (ours) {
532
+ // The far end is there: the connection may finish connecting and start its
533
+ // handshake, exactly as a completed TCP connect would let it.
534
+ wake(route, ours->on_accept(window));
535
+ return;
536
+ }
537
+ if (forward) {
538
+ Bytes msg = message(kOpAccept, forward->id, 4);
539
+ put_u32(msg, window);
540
+ send(forward->peer, msg);
541
+ }
542
+ }
543
+
544
+ void Relay::State::handle_deny(const PeerId& from, uint32_t circuit, ByteView body) {
545
+ const uint8_t reason = body.empty() ? 0xFF : body.data()[0];
546
+
547
+ std::optional<Forward> forward;
548
+ {
549
+ std::lock_guard<std::mutex> lock(mutex);
550
+ auto link = links.find(from);
551
+ if (link == links.end()) return;
552
+ if (auto fwd = link->second.forwards.find(circuit); fwd != link->second.forwards.end())
553
+ forward = fwd->second;
554
+ }
555
+
556
+ if (forward) {
557
+ Bytes msg = message(kOpDeny, forward->id, 1);
558
+ msg.push_back(reason);
559
+ send(forward->peer, msg);
560
+ // The refusal IS the ending, and both sides have now had it — a Close on
561
+ // top would name a circuit neither of them still has.
562
+ close_carried(from, circuit, kCloseError, /*tell_far=*/false, /*tell_near=*/false);
563
+ return;
564
+ }
565
+
566
+ const Dropped dropped = take_leg(from, circuit);
567
+ if (!dropped.found) return;
568
+ LOG_DEBUG("relay", "Circuit through " << from.short_hex() << " refused: "
569
+ << deny_text(reason));
570
+ wake(dropped.route, dropped.circuit->on_closed(CloseReason::ConnectFailed, /*orderly=*/false));
571
+ circuits->close_circuit(dropped.route, CloseReason::ConnectFailed);
572
+ // The attempt is now free to try the next relay that said it could help.
573
+ if (dropped.outbound) advance(dropped.target);
574
+ }
575
+
576
+ void Relay::State::handle_data(const PeerId& from, uint32_t circuit, ByteView body) {
577
+ if (body.empty()) return;
578
+
579
+ std::shared_ptr<Circuit> ours;
580
+ PeerRoute route{};
581
+ std::optional<Forward> forward;
582
+ bool over_budget = false;
583
+ {
584
+ std::lock_guard<std::mutex> lock(mutex);
585
+ auto link = links.find(from);
586
+ if (link == links.end()) return;
587
+ if (auto leg = link->second.legs.find(circuit); leg != link->second.legs.end()) {
588
+ ours = leg->second.circuit;
589
+ route = leg->second.route;
590
+ } else if (auto fwd = link->second.forwards.find(circuit);
591
+ fwd != link->second.forwards.end()) {
592
+ forward = fwd->second;
593
+ forward->meter->bytes += body.size();
594
+ over_budget = config.max_bytes_per_circuit != 0 &&
595
+ forward->meter->bytes > config.max_bytes_per_circuit;
596
+ }
597
+ }
598
+
599
+ if (ours) {
600
+ // on_data enforces the window we advertised: a far end that sends past it
601
+ // fails the circuit rather than being allowed to grow it.
602
+ wake(route, ours->on_data(body));
603
+ return;
604
+ }
605
+ if (!forward) return; // a circuit we have already forgotten; nothing to do
606
+
607
+ if (over_budget) {
608
+ LOG_DEBUG("relay", "Circuit from " << from.short_hex() << " hit its byte cap");
609
+ close_carried(from, circuit, kCloseError, /*tell_far=*/true, /*tell_near=*/true);
610
+ return;
611
+ }
612
+
613
+ Bytes msg = message(kOpData, forward->id, body.size());
614
+ msg.insert(msg.end(), body.begin(), body.end());
615
+ send(forward->peer, msg);
616
+ carried_bytes.fetch_add(body.size(), std::memory_order_relaxed);
617
+ }
618
+
619
+ void Relay::State::handle_credit(const PeerId& from, uint32_t circuit, ByteView body) {
620
+ if (body.size() < 4) return;
621
+ const uint32_t granted = get_u32(body.data());
622
+
623
+ std::shared_ptr<Circuit> ours;
624
+ PeerRoute route{};
625
+ std::optional<Forward> forward;
626
+ {
627
+ std::lock_guard<std::mutex> lock(mutex);
628
+ auto link = links.find(from);
629
+ if (link == links.end()) return;
630
+ if (auto leg = link->second.legs.find(circuit); leg != link->second.legs.end()) {
631
+ ours = leg->second.circuit;
632
+ route = leg->second.route;
633
+ } else if (auto fwd = link->second.forwards.find(circuit);
634
+ fwd != link->second.forwards.end()) {
635
+ forward = fwd->second;
636
+ }
637
+ }
638
+
639
+ if (ours) { wake(route, ours->on_credit(granted)); return; }
640
+ if (forward) {
641
+ Bytes msg = message(kOpCredit, forward->id, 4);
642
+ put_u32(msg, granted);
643
+ send(forward->peer, msg);
644
+ }
645
+ }
646
+
647
+ void Relay::State::handle_close(const PeerId& from, uint32_t circuit, ByteView body) {
648
+ const uint8_t reason = body.empty() ? kCloseError : body.data()[0];
649
+
650
+ bool carries = false;
651
+ {
652
+ std::lock_guard<std::mutex> lock(mutex);
653
+ auto link = links.find(from);
654
+ if (link == links.end()) return;
655
+ carries = link->second.forwards.count(circuit) > 0;
656
+ }
657
+ if (carries) {
658
+ close_carried(from, circuit, reason, /*tell_far=*/true, /*tell_near=*/false);
659
+ return;
660
+ }
661
+
662
+ const Dropped dropped = take_leg(from, circuit);
663
+ if (!dropped.found) return;
664
+ const bool orderly = reason == kCloseNormal;
665
+ wake(dropped.route, dropped.circuit->on_closed(
666
+ orderly ? CloseReason::PeerClosed : CloseReason::PeerReset, orderly));
667
+ if (dropped.outbound) advance(dropped.target);
668
+ }
669
+
670
+ // ── The client half: finding a way to a peer ────────────────────────────────
671
+
672
+ void Relay::State::handle_probe_ok(const PeerId& from, ByteView body) {
673
+ if (body.size() < PeerId::kSize) return;
674
+ const auto target = PeerId::from_bytes(ByteView(body.data(), PeerId::kSize));
675
+ if (!target) return;
676
+
677
+ bool open_now = false;
678
+ {
679
+ std::lock_guard<std::mutex> lock(mutex);
680
+ auto it = attempts.find(*target);
681
+ if (it == attempts.end()) return; // no attempt, or it already ended
682
+ if (std::find(it->second.ready.begin(), it->second.ready.end(), from) !=
683
+ it->second.ready.end())
684
+ return;
685
+ it->second.ready.push_back(from);
686
+ // The first useful answer is acted on at once; the rest are kept in case
687
+ // this one refuses or dies, which is the whole reason for keeping them.
688
+ open_now = !it->second.opening;
689
+ }
690
+ if (open_now) advance(*target);
691
+ }
692
+
693
+ bool Relay::State::begin_attempt(const PeerId& target) {
694
+ std::vector<PeerId> candidates;
695
+ {
696
+ std::lock_guard<std::mutex> lock(mutex);
697
+ if (!running.load()) return false;
698
+ if (attempts.count(target)) return false;
699
+ if (in_cooldown(target)) return false;
700
+ if (outbound_circuits >= config.max_outbound_circuits) return false;
701
+ }
702
+
703
+ // Peers that could carry the circuit: connected, not the target, and not
704
+ // themselves relayed — asking a relayed peer to relay is the chain we refuse.
705
+ for (const PeerInfo& info : network->peers()) {
706
+ if (info.id == target) return false; // already reachable directly
707
+ if (info.transport == TransportKind::Relay) continue;
708
+ candidates.push_back(info.id);
709
+ }
710
+ if (candidates.size() > config.max_probes) candidates.resize(config.max_probes);
711
+ if (candidates.empty()) return false;
712
+
713
+ {
714
+ std::lock_guard<std::mutex> lock(mutex);
715
+ if (attempts.count(target)) return false; // another caller got here first
716
+ Attempt attempt;
717
+ attempt.deadline = Clock::now() + config.open_timeout;
718
+ attempts.emplace(target, std::move(attempt));
719
+ }
720
+
721
+ Bytes msg = message(kOpProbe, kNoCircuit, PeerId::kSize);
722
+ append_id(msg, target);
723
+ for (const PeerId& candidate : candidates) send(candidate, msg);
724
+
725
+ LOG_DEBUG("relay", "Asking " << candidates.size() << " peer(s) for a way to "
726
+ << target.short_hex());
727
+ return true;
728
+ }
729
+
730
+ void Relay::State::advance(const PeerId& target) {
731
+ std::shared_ptr<Circuit> circuit;
732
+ uint32_t id = kNoCircuit;
733
+ PeerId via;
734
+ {
735
+ std::lock_guard<std::mutex> lock(mutex);
736
+ if (!running.load()) return;
737
+ auto it = attempts.find(target);
738
+ if (it == attempts.end() || it->second.opening) return;
739
+ if (it->second.ready.empty()) return; // nothing left to try; let it time out
740
+ // Every reason to refuse is behind this line, so the candidate taken here is
741
+ // a candidate actually tried. Popping earlier would spend one relay per
742
+ // refusal — at the outbound cap a burst of ProbeOks would empty `ready`
743
+ // against the cap and leave the attempt to time out with relays to spare.
744
+ if (outbound_circuits >= config.max_outbound_circuits) return;
745
+
746
+ via = it->second.ready.back();
747
+ it->second.ready.pop_back();
748
+
749
+ PeerLink& link = links[via];
750
+ id = allocate_id(link, via);
751
+ circuit = Circuit::opening(id, std::make_shared<Carrier>(shared_from_this(), via),
752
+ config.window);
753
+ link.legs.emplace(id, Leg{circuit, PeerRoute{}, target, /*outbound=*/true});
754
+ ++outbound_circuits;
755
+
756
+ it->second.opening = true;
757
+ it->second.via = via;
758
+ it->second.circuit = id;
759
+ }
760
+
761
+ // Adopted BEFORE the request goes out, so the route is on record by the time an
762
+ // answer to it can possibly arrive.
763
+ const auto route = circuits->adopt_circuit(via, std::make_unique<RelayLink>(circuit),
764
+ ConnRole::Outbound, /*connected=*/false);
765
+ if (!route) {
766
+ take_leg(via, id);
767
+ advance(target);
768
+ return;
769
+ }
770
+ {
771
+ std::lock_guard<std::mutex> lock(mutex);
772
+ auto link = links.find(via);
773
+ if (link != links.end()) {
774
+ auto leg = link->second.legs.find(id);
775
+ if (leg != link->second.legs.end()) leg->second.route = *route;
776
+ }
777
+ }
778
+
779
+ Bytes msg = message(kOpOpen, id, PeerId::kSize + 4);
780
+ append_id(msg, target);
781
+ put_u32(msg, config.window);
782
+ send(via, msg);
783
+ }
784
+
785
+ void Relay::State::release_attempt(const PeerId& target, const PeerId& carrier,
786
+ uint32_t circuit) {
787
+ auto attempt = attempts.find(target);
788
+ if (attempt == attempts.end()) return;
789
+ // Only the circuit the attempt is actually waiting on: a late drop belonging to
790
+ // a superseded leg must not clear the state of the one now in flight.
791
+ if (attempt->second.circuit != circuit || attempt->second.via != carrier) return;
792
+ attempt->second.opening = false;
793
+ attempt->second.circuit = kNoCircuit;
794
+ }
795
+
796
+ void Relay::State::finish_attempt(const PeerId& target, bool succeeded) {
797
+ std::lock_guard<std::mutex> lock(mutex);
798
+ if (attempts.erase(target) == 0) return;
799
+ if (!succeeded) cooldown[target] = Clock::now() + config.cooldown;
800
+ }
801
+
802
+ // ── Peer lifecycle ──────────────────────────────────────────────────────────
803
+
804
+ void Relay::State::on_peer_connected(const Peer& peer) {
805
+ if (!running.load()) return;
806
+ finish_attempt(peer.id(), /*succeeded=*/true);
807
+
808
+ if (!config.upgrade_with_hole_punch) return;
809
+ const auto info = peer.info();
810
+ if (!info || info->transport != TransportKind::Relay) return;
811
+
812
+ // A circuit is a fallback, not a destination. Now that the two ends are peers
813
+ // they can arrange a punch over the very circuit carrying them, and if it lands
814
+ // the peer table prefers the direct link at both ends and swaps the route with
815
+ // no disconnect event — the application never sees the seam.
816
+ if (HolePunchService* hole_punch = punch.load()) {
817
+ LOG_DEBUG("relay", "Relayed peer " << peer.id().short_hex()
818
+ << " is up; trying to replace the circuit with a direct link");
819
+ hole_punch->punch(peer.id());
820
+ }
821
+ }
822
+
823
+ void Relay::State::on_peer_disconnected(const PeerId& id) {
824
+ // Everything riding this link is over: the circuits we terminate through it,
825
+ // and the ones we were carrying across it.
826
+ std::unordered_map<uint32_t, Leg> legs;
827
+ std::unordered_map<uint32_t, Forward> forwards;
828
+ {
829
+ std::lock_guard<std::mutex> lock(mutex);
830
+ auto link = links.find(id);
831
+ if (link == links.end()) return;
832
+ legs = std::move(link->second.legs);
833
+ forwards = std::move(link->second.forwards);
834
+ links.erase(link);
835
+
836
+ // Same bookkeeping take_leg does, which this path deliberately bypasses to
837
+ // empty the whole link at once. Releasing the attempt is the part that must
838
+ // not be skipped: the link entry is gone by the time `advance` runs below,
839
+ // so nothing else would ever clear `opening` and the attempt would sit on a
840
+ // dead carrier until its deadline, with answered relays left untried.
841
+ for (const auto& [circuit, leg] : legs) {
842
+ if (leg.outbound) {
843
+ if (outbound_circuits) --outbound_circuits;
844
+ release_attempt(leg.target, id, circuit);
845
+ } else if (inbound_circuits) {
846
+ --inbound_circuits;
847
+ }
848
+ }
849
+ }
850
+
851
+ for (const auto& [circuit, leg] : legs) {
852
+ wake(leg.route, leg.circuit->on_closed(CloseReason::PeerReset, /*orderly=*/false));
853
+ circuits->close_circuit(leg.route, CloseReason::PeerReset);
854
+ if (leg.outbound) advance(leg.target);
855
+ }
856
+
857
+ for (const auto& [circuit, fwd] : forwards) {
858
+ // Tell the other side, and take its half out of the tables. This peer's own
859
+ // half went with the link above, charges and all.
860
+ Bytes msg = message(kOpClose, fwd.id, 1);
861
+ msg.push_back(kCloseError);
862
+ send(fwd.peer, msg);
863
+ std::lock_guard<std::mutex> lock(mutex);
864
+ if (erase_forward(fwd.peer, fwd.id) && carried_circuits) --carried_circuits;
865
+ }
866
+ }
867
+
868
+ void Relay::State::on_peer_writable(const PeerId& id) {
869
+ // The carrier's send queue has drained, so circuits that stopped on it may go
870
+ // again. Gathered first, woken after: waking runs the connection's send path.
871
+ std::vector<std::pair<std::shared_ptr<Circuit>, PeerRoute>> waiting;
872
+ {
873
+ std::lock_guard<std::mutex> lock(mutex);
874
+ auto link = links.find(id);
875
+ if (link == links.end()) return;
876
+ waiting.reserve(link->second.legs.size());
877
+ for (const auto& [circuit, leg] : link->second.legs)
878
+ waiting.emplace_back(leg.circuit, leg.route);
879
+ }
880
+ for (const auto& [circuit, route] : waiting) wake(route, circuit->on_carrier_writable());
881
+ }
882
+
883
+ // ── Bookkeeping ─────────────────────────────────────────────────────────────
884
+
885
+ uint32_t Relay::State::allocate_id(PeerLink& link, const PeerId& peer) {
886
+ const bool even = network->local_id() < peer;
887
+ const uint32_t first = even ? kFirstEvenId : kFirstOddId;
888
+ if (link.next_id < first) link.next_id = first;
889
+
890
+ for (int guard = 0; guard < 64; ++guard) {
891
+ const uint32_t id = link.next_id;
892
+ link.next_id = id + 2 >= kIdWrap ? first : id + 2;
893
+ if (!link.legs.count(id) && !link.forwards.count(id)) return id;
894
+ }
895
+ return link.next_id; // a link with tens of thousands of circuits; not reachable
896
+ }
897
+
898
+ bool Relay::State::spend_request(const PeerId& peer) {
899
+ const auto now = Clock::now();
900
+ // No ceiling and no sweep, deliberately. An entry is only ever created for a
901
+ // peer we hold — nothing else can reach this code — and on_peer_disconnected
902
+ // erases it, so the table is bounded by the peer count. The "drop the whole
903
+ // table when it grows too large" trick that guards the equivalent budget in
904
+ // HolePunch would be actively wrong here: this table holds live circuits, not
905
+ // just counters, and clearing it would orphan every one of them.
906
+ PeerLink& link = links[peer];
907
+ if (now - link.window_started >= config.request_window) {
908
+ link.window_started = now;
909
+ link.requests = 0;
910
+ }
911
+ if (link.requests >= config.request_budget) return false;
912
+ ++link.requests;
913
+ return true;
914
+ }
915
+
916
+ bool Relay::State::in_cooldown(const PeerId& target) const {
917
+ auto it = cooldown.find(target);
918
+ return it != cooldown.end() && Clock::now() < it->second;
919
+ }
920
+
921
+ Relay::State::Dropped Relay::State::take_leg(const PeerId& carrier, uint32_t circuit) {
922
+ std::lock_guard<std::mutex> lock(mutex);
923
+ Dropped out;
924
+ auto link = links.find(carrier);
925
+ if (link == links.end()) return out;
926
+ auto leg = link->second.legs.find(circuit);
927
+ if (leg == link->second.legs.end()) return out;
928
+
929
+ out.found = true;
930
+ out.circuit = leg->second.circuit;
931
+ out.route = leg->second.route;
932
+ out.target = leg->second.target;
933
+ out.outbound = leg->second.outbound;
934
+
935
+ if (out.outbound) {
936
+ if (outbound_circuits) --outbound_circuits;
937
+ release_attempt(out.target, carrier, circuit);
938
+ } else if (inbound_circuits) {
939
+ --inbound_circuits;
940
+ }
941
+ link->second.legs.erase(leg);
942
+ return out;
943
+ }
944
+
945
+ void Relay::State::wake(PeerRoute route, uint32_t events) {
946
+ if (events == 0 || route.conn == kInvalidConnId) return;
947
+ circuits->wake_circuit(route, events);
948
+ }
949
+
950
+ std::optional<Relay::State::Forward> Relay::State::erase_forward(const PeerId& peer,
951
+ uint32_t circuit) {
952
+ auto link = links.find(peer);
953
+ if (link == links.end()) return std::nullopt;
954
+ auto fwd = link->second.forwards.find(circuit);
955
+ if (fwd == link->second.forwards.end()) return std::nullopt;
956
+
957
+ const Forward taken = fwd->second;
958
+ if (taken.opener && link->second.opened) --link->second.opened;
959
+ link->second.forwards.erase(fwd);
960
+ return taken;
961
+ }
962
+
963
+ void Relay::State::close_carried(const PeerId& peer, uint32_t circuit, uint8_t reason,
964
+ bool tell_far, bool tell_near) {
965
+ std::optional<Forward> forward;
966
+ {
967
+ std::lock_guard<std::mutex> lock(mutex);
968
+ forward = erase_forward(peer, circuit);
969
+ if (!forward) return;
970
+ erase_forward(forward->peer, forward->id);
971
+ if (carried_circuits) --carried_circuits;
972
+ }
973
+
974
+ if (tell_far) {
975
+ Bytes msg = message(kOpClose, forward->id, 1);
976
+ msg.push_back(reason);
977
+ send(forward->peer, msg);
978
+ }
979
+ if (tell_near) {
980
+ Bytes msg = message(kOpClose, circuit, 1);
981
+ msg.push_back(reason);
982
+ send(peer, msg);
983
+ }
984
+ }
985
+
986
+ void Relay::State::deny(const PeerId& to, uint32_t circuit, uint8_t reason) {
987
+ Bytes msg = message(kOpDeny, circuit, 1);
988
+ msg.push_back(reason);
989
+ send(to, msg);
990
+ }
991
+
992
+ // ── Periodic work ───────────────────────────────────────────────────────────
993
+
994
+ void Relay::State::tick() {
995
+ if (!running.load()) return;
996
+ const auto now = Clock::now();
997
+
998
+ std::vector<PeerId> timed_out;
999
+ std::vector<PeerId> to_advance;
1000
+ std::vector<std::pair<PeerId, uint32_t>> expired;
1001
+ {
1002
+ std::lock_guard<std::mutex> lock(mutex);
1003
+ for (auto it = cooldown.begin(); it != cooldown.end();) {
1004
+ if (now >= it->second) it = cooldown.erase(it);
1005
+ else ++it;
1006
+ }
1007
+ for (const auto& [target, attempt] : attempts) {
1008
+ if (now >= attempt.deadline) timed_out.push_back(target);
1009
+ else if (!attempt.opening && !attempt.ready.empty()) to_advance.push_back(target);
1010
+ }
1011
+
1012
+ // Circuits we carry that have outstayed their welcome. The byte cap is
1013
+ // enforced as the bytes go past (handle_data); this is the clock half.
1014
+ if (config.max_circuit_duration.count() != 0) {
1015
+ for (const auto& [peer, link] : links)
1016
+ for (const auto& [circuit, fwd] : link.forwards)
1017
+ if (now - fwd.meter->started >= config.max_circuit_duration)
1018
+ expired.emplace_back(peer, circuit);
1019
+ }
1020
+ }
1021
+
1022
+ for (const PeerId& target : timed_out) {
1023
+ LOG_DEBUG("relay", "No relay found for " << target.short_hex());
1024
+ finish_attempt(target, /*succeeded=*/false);
1025
+ }
1026
+ for (const PeerId& target : to_advance) advance(target);
1027
+ for (const auto& [peer, circuit] : expired)
1028
+ close_carried(peer, circuit, kCloseError, /*tell_far=*/true, /*tell_near=*/true);
1029
+ }
1030
+
1031
+ void Relay::State::shutdown() {
1032
+ running.store(false);
1033
+ punch.store(nullptr);
1034
+
1035
+ // Only the tables. The circuits themselves are deliberately NOT touched here:
1036
+ // stop() runs on whatever thread is taking the node down, while a circuit
1037
+ // belongs to the reactor thread that owns its connection, and reaching into one
1038
+ // from here would be exactly the cross-thread access this design does not have
1039
+ // anywhere else. Nothing is left dangling by that — subsystems stop before the
1040
+ // reactors do, and each reactor then closes its connections, which shuts each
1041
+ // circuit down and releases its link on the one thread that owns it.
1042
+ std::lock_guard<std::mutex> lock(mutex);
1043
+ links.clear();
1044
+ attempts.clear();
1045
+ cooldown.clear();
1046
+ outbound_circuits = 0;
1047
+ inbound_circuits = 0;
1048
+ carried_circuits = 0;
1049
+ }
1050
+
1051
+ // ── Subsystem ───────────────────────────────────────────────────────────────
1052
+
1053
+ Relay::Relay() : Relay(Config()) {}
1054
+
1055
+ Relay::Relay(Config config) : config_(std::move(config)), state_(std::make_shared<State>()) {
1056
+ state_->config = config_;
1057
+ }
1058
+
1059
+ Relay::~Relay() { stop(); }
1060
+
1061
+ void Relay::attach(NodeContext& ctx) {
1062
+ state_->network = &ctx.network;
1063
+ // Without it this node can still serve as a relay, but it cannot terminate a
1064
+ // circuit — there would be nothing to turn the byte stream into a connection.
1065
+ state_->circuits = ctx.services.get<CircuitService>();
1066
+
1067
+ ctx.services.provide<RelayService>(this);
1068
+
1069
+ // The handlers hold the shared state rather than `this`: the router keeps them
1070
+ // for the node's life, and the state is what they need anyway.
1071
+ auto state = state_;
1072
+ ctx.network.on(MessageType::Relay,
1073
+ [state](const Peer& peer, ByteView payload) { state->handle(peer, payload); });
1074
+ ctx.network.on_peer_connected([state](const Peer& peer) { state->on_peer_connected(peer); });
1075
+ ctx.network.on_peer_disconnected(
1076
+ [state](const PeerId& id, CloseReason) { state->on_peer_disconnected(id); });
1077
+ ctx.network.on_peer_writable([state](const Peer& peer) { state->on_peer_writable(peer.id()); });
1078
+
1079
+ services_ = &ctx.services;
1080
+ }
1081
+
1082
+ void Relay::start() {
1083
+ if (!state_->circuits) {
1084
+ LOG_WARN("relay", "No CircuitService is published; this node can relay for others "
1085
+ "but cannot itself be relayed");
1086
+ }
1087
+ // Resolved here rather than in attach(): HolePunch may be attached after us, and
1088
+ // every attach() runs before any start().
1089
+ if (services_ && config_.upgrade_with_hole_punch)
1090
+ state_->punch.store(services_->get<HolePunchService>());
1091
+
1092
+ state_->running.store(true);
1093
+ if (running_.exchange(true)) return;
1094
+ worker_ = std::thread([this] { loop(); });
1095
+ }
1096
+
1097
+ void Relay::stop() {
1098
+ state_->running.store(false);
1099
+ if (running_.exchange(false)) {
1100
+ worker_cv_.notify_all();
1101
+ if (worker_.joinable()) worker_.join();
1102
+ }
1103
+ state_->shutdown();
1104
+ }
1105
+
1106
+ bool Relay::connect_via_relay(const PeerId& target) {
1107
+ if (!state_->running.load() || !config_.enable_client) return false;
1108
+ if (!state_->circuits || !state_->network) return false;
1109
+ if (target == state_->network->local_id()) return false;
1110
+ return state_->begin_attempt(target);
1111
+ }
1112
+
1113
+ void Relay::loop() {
1114
+ std::unique_lock<std::mutex> lock(worker_mutex_);
1115
+ while (running_.load()) {
1116
+ worker_cv_.wait_for(lock, config_.tick, [this] { return !running_.load(); });
1117
+ if (!running_.load()) return;
1118
+ lock.unlock();
1119
+ state_->tick();
1120
+ lock.lock();
1121
+ }
1122
+ }
1123
+
1124
+ size_t Relay::circuits() const {
1125
+ std::lock_guard<std::mutex> lock(state_->mutex);
1126
+ return state_->outbound_circuits + state_->inbound_circuits;
1127
+ }
1128
+
1129
+ size_t Relay::carried_circuits() const {
1130
+ std::lock_guard<std::mutex> lock(state_->mutex);
1131
+ return state_->carried_circuits;
1132
+ }
1133
+
1134
+ uint64_t Relay::carried_bytes() const {
1135
+ return state_->carried_bytes.load(std::memory_order_relaxed);
1136
+ }
1137
+
1138
+ size_t Relay::attempts() const {
1139
+ std::lock_guard<std::mutex> lock(state_->mutex);
1140
+ return state_->attempts.size();
1141
+ }
1142
+
1143
+ } // namespace librats