librats 2.1.4 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/lib/index.d.ts +51 -6
- package/lib/index.js +42 -3
- package/native-src/CMakeLists.txt +13 -0
- package/native-src/src/librats/bindings/rats.cpp +90 -6
- package/native-src/src/librats/bindings/rats.h +92 -3
- package/native-src/src/librats/core/types.cpp +4 -3
- package/native-src/src/librats/core/types.h +25 -12
- package/native-src/src/librats/node/circuit_service.h +84 -0
- package/native-src/src/librats/node/config.h +9 -2
- package/native-src/src/librats/node/node.cpp +59 -5
- package/native-src/src/librats/node/node.h +38 -7
- package/native-src/src/librats/node/peer_network.h +19 -2
- package/native-src/src/librats/peer/peer.h +17 -6
- package/native-src/src/librats/peer/peer_table.cpp +41 -8
- package/native-src/src/librats/security/noise_security.cpp +2 -0
- package/native-src/src/librats/security/plaintext_security.h +1 -0
- package/native-src/src/librats/security/session.h +7 -0
- package/native-src/src/librats/storage/storage.cpp +434 -213
- package/native-src/src/librats/storage/storage.h +163 -19
- package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
- package/native-src/src/librats/subsystems/hole_punch.cpp +81 -8
- package/native-src/src/librats/subsystems/hole_punch.h +24 -0
- package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
- package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
- package/native-src/src/librats/subsystems/relay.cpp +1143 -0
- package/native-src/src/librats/subsystems/relay.h +211 -0
- package/native-src/src/librats/subsystems/relay_service.h +46 -0
- package/native-src/src/librats/transport/connection.cpp +40 -8
- package/native-src/src/librats/transport/connection.h +12 -2
- package/native-src/src/librats/transport/reactor.cpp +56 -8
- package/native-src/src/librats/transport/reactor.h +38 -0
- package/native-src/src/librats/transport/relay_link.cpp +208 -0
- package/native-src/src/librats/transport/relay_link.h +303 -0
- package/native-src/src/librats/util/logger.h +37 -5
- package/native-src/src/librats/util/network_monitor.cpp +14 -0
- package/native-src/src/librats/util/network_utils.cpp +10 -1
- package/native-src/src/librats/wire/frame.h +1 -0
- package/package.json +1 -1
- package/src/librats_node.cpp +76 -1
|
@@ -0,0 +1,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
|