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
|
@@ -11,18 +11,40 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Replication model — an epidemic LWW broadcast:
|
|
13
13
|
* - A local put/remove builds a StorageEntry (a delete is a tombstone entry
|
|
14
|
-
* with `deleted=true`) and
|
|
14
|
+
* with `deleted=true`) and sends it to every connected peer.
|
|
15
15
|
* - On receiving an entry, a node applies it under LWW. It re-forwards the
|
|
16
16
|
* entry to its *other* peers ONLY if the entry actually won (carried new
|
|
17
17
|
* information). A duplicate loses LWW and is not forwarded, so flooding
|
|
18
18
|
* terminates naturally — no separate dedup table needed.
|
|
19
|
-
* - On peer connect, both sides
|
|
20
|
-
* late joiner catches up. LWW makes the merge
|
|
19
|
+
* - On peer connect, both sides ask each other for a full snapshot
|
|
20
|
+
* (anti-entropy) so a late joiner catches up. LWW makes the merge
|
|
21
|
+
* order-independent.
|
|
22
|
+
*
|
|
23
|
+
* Backpressure — why a snapshot is a *stream*, not a message:
|
|
24
|
+
* A database is unbounded, a peer's send queue is not. One message carrying
|
|
25
|
+
* the whole store stops being sendable at the connection's low-water mark,
|
|
26
|
+
* gets the peer dropped as a slow consumer past its high-water mark, and is
|
|
27
|
+
* not even representable past the block-size ceiling — and since both ends
|
|
28
|
+
* request a snapshot on connect, two of them cross on every single link. So
|
|
29
|
+
* a snapshot is served as a sequence of bounded SYNC_CHUNK messages walking
|
|
30
|
+
* the (ordered) key space, one chunk at a time, and the next chunk is only
|
|
31
|
+
* produced while `PeerNetwork::peer_writable()` says there is room. Every
|
|
32
|
+
* send() return value is honoured: a peer that fills up stops receiving
|
|
33
|
+
* individual entries and is instead owed a fresh snapshot, which the sync
|
|
34
|
+
* thread starts once on_peer_writable says the link has drained. Losing live
|
|
35
|
+
* updates to a congested peer is safe precisely because the store is LWW —
|
|
36
|
+
* the snapshot that follows carries the winning state either way.
|
|
37
|
+
*
|
|
38
|
+
* All of that runs on this module's own sync thread. Serialising even one
|
|
39
|
+
* chunk happens off the reactor thread, so a reactor never spends time (or
|
|
40
|
+
* holds storage_mutex_) proportional to the size of the database.
|
|
21
41
|
*
|
|
22
42
|
* Wire format (MessageType::Storage payload, opcode in byte 0):
|
|
23
43
|
* ENTRY: [1][StorageEntry.serialize()]
|
|
24
44
|
* SYNC_REQUEST: [2]
|
|
25
|
-
*
|
|
45
|
+
* SYNC_CHUNK: [3][flags:u8][count:u32][StorageEntry.serialize()] * count
|
|
46
|
+
* flags bit0 (LAST) marks the final chunk of a snapshot; a
|
|
47
|
+
* snapshot of an empty store is one chunk with count 0.
|
|
26
48
|
*
|
|
27
49
|
* The class is also usable standalone (no network attached) as a local,
|
|
28
50
|
* persistent key-value store; all network operations no-op until attach().
|
|
@@ -38,6 +60,7 @@
|
|
|
38
60
|
#include <string>
|
|
39
61
|
#include <vector>
|
|
40
62
|
#include <functional>
|
|
63
|
+
#include <map>
|
|
41
64
|
#include <memory>
|
|
42
65
|
#include <mutex>
|
|
43
66
|
#include <unordered_map>
|
|
@@ -113,7 +136,23 @@ struct RATS_API StorageEntry {
|
|
|
113
136
|
// Serialize entry to binary format
|
|
114
137
|
std::vector<uint8_t> serialize() const;
|
|
115
138
|
|
|
116
|
-
|
|
139
|
+
/// Append the serialized entry to `out`. What serialize() is built on: a
|
|
140
|
+
/// batch appends entry after entry into one buffer instead of allocating a
|
|
141
|
+
/// vector per entry only to copy it away again.
|
|
142
|
+
void serialize_into(std::vector<uint8_t>& out) const;
|
|
143
|
+
|
|
144
|
+
/// Serialized size in bytes, without serializing.
|
|
145
|
+
size_t serialized_size() const;
|
|
146
|
+
|
|
147
|
+
/// Deserialize one entry from `data[offset..]`, setting `bytes_read` to the
|
|
148
|
+
/// bytes it consumed. Every field is bounds-checked against the entry's own
|
|
149
|
+
/// declared length as well as the buffer, so a hostile length can neither
|
|
150
|
+
/// read past the buffer nor reach into the entry that follows.
|
|
151
|
+
static bool deserialize(const uint8_t* data, size_t size, size_t offset,
|
|
152
|
+
StorageEntry& entry, size_t& bytes_read);
|
|
153
|
+
|
|
154
|
+
/// Vector overload of the above; the wire path uses the pointer form to
|
|
155
|
+
/// parse straight out of the receive buffer without copying it first.
|
|
117
156
|
static bool deserialize(const std::vector<uint8_t>& data, size_t offset,
|
|
118
157
|
StorageEntry& entry, size_t& bytes_read);
|
|
119
158
|
|
|
@@ -136,23 +175,51 @@ struct StorageChangeEvent {
|
|
|
136
175
|
};
|
|
137
176
|
|
|
138
177
|
/**
|
|
139
|
-
* Storage configuration
|
|
178
|
+
* Storage configuration.
|
|
179
|
+
*
|
|
180
|
+
* The two size limits are not free parameters: both are bounded by what one
|
|
181
|
+
* peer's send queue can hold. A connection drops its peer as a slow consumer
|
|
182
|
+
* past its high-water mark (8 MiB by default, NodeConfig::send_queue_limit) and
|
|
183
|
+
* reports "no room" at a quarter of it, so a single value — and a single
|
|
184
|
+
* snapshot chunk — must stay well inside that quarter, or the very first one
|
|
185
|
+
* would kill the link it travels on. Both are clamped to kMaxValueSize /
|
|
186
|
+
* kMaxSyncBatchBytes on construction and in set_config().
|
|
140
187
|
*/
|
|
141
188
|
struct StorageConfig {
|
|
189
|
+
/// Ceiling on max_value_size: the default connection low-water mark. One
|
|
190
|
+
/// value at the ceiling makes a peer unwritable and is then waited out,
|
|
191
|
+
/// rather than counting toward the high-water mark that drops it.
|
|
192
|
+
static constexpr uint32_t kMaxValueSize = 2 * 1024 * 1024;
|
|
193
|
+
/// Ceiling on sync_batch_bytes, for the same reason.
|
|
194
|
+
static constexpr uint32_t kMaxSyncBatchBytes = 2 * 1024 * 1024;
|
|
195
|
+
/// Floor on sync_batch_bytes: below this a snapshot costs more in per-message
|
|
196
|
+
/// framing and round trips than it saves in queue occupancy.
|
|
197
|
+
static constexpr uint32_t kMinSyncBatchBytes = 16 * 1024;
|
|
198
|
+
|
|
142
199
|
std::string data_directory; // Directory for storage files
|
|
143
200
|
std::string database_name; // Database filename prefix
|
|
144
201
|
bool enable_sync; // Enable network synchronization
|
|
145
202
|
uint32_t compaction_threshold; // Number of tombstones before compaction
|
|
146
|
-
uint32_t max_value_size; // Maximum value size in bytes
|
|
203
|
+
uint32_t max_value_size; // Maximum value size in bytes (<= kMaxValueSize)
|
|
147
204
|
bool persist_to_disk; // Whether to persist data to disk
|
|
205
|
+
/// Target payload size of one snapshot chunk. The walk stops at the first
|
|
206
|
+
/// entry that takes the buffer past this, so a chunk is this size plus at
|
|
207
|
+
/// most one entry — which is why max_value_size shares the same ceiling.
|
|
208
|
+
uint32_t sync_batch_bytes;
|
|
209
|
+
/// Minimum gap between two snapshots served to the same peer. Bounds what a
|
|
210
|
+
/// peer can make us spend by asking, and paces the re-sync a peer is owed
|
|
211
|
+
/// after it has been congested.
|
|
212
|
+
uint32_t sync_min_interval_ms;
|
|
148
213
|
|
|
149
214
|
StorageConfig()
|
|
150
215
|
: data_directory("./storage"),
|
|
151
216
|
database_name("rats_storage"),
|
|
152
217
|
enable_sync(true),
|
|
153
218
|
compaction_threshold(1000),
|
|
154
|
-
max_value_size(
|
|
155
|
-
persist_to_disk(true)
|
|
219
|
+
max_value_size(1024 * 1024), // 1 MiB max value size
|
|
220
|
+
persist_to_disk(true),
|
|
221
|
+
sync_batch_bytes(256 * 1024), // 256 KiB per snapshot chunk
|
|
222
|
+
sync_min_interval_ms(5000) {}
|
|
156
223
|
};
|
|
157
224
|
|
|
158
225
|
/**
|
|
@@ -167,6 +234,9 @@ struct StorageStatistics {
|
|
|
167
234
|
uint64_t entries_sent; // Entries sent to peers
|
|
168
235
|
uint64_t sync_requests_received; // Number of sync requests received
|
|
169
236
|
uint64_t sync_requests_sent; // Number of sync requests sent
|
|
237
|
+
uint64_t sync_chunks_sent; // Snapshot chunks put on the wire
|
|
238
|
+
uint64_t sync_chunks_received; // Snapshot chunks applied from peers
|
|
239
|
+
uint64_t resyncs_scheduled; // Snapshots owed to peers that filled up
|
|
170
240
|
std::chrono::steady_clock::time_point last_sync_time; // Last sync timestamp
|
|
171
241
|
StorageSyncStatus sync_status; // Current sync status
|
|
172
242
|
};
|
|
@@ -216,6 +286,9 @@ public:
|
|
|
216
286
|
// Configuration
|
|
217
287
|
// =========================================================================
|
|
218
288
|
|
|
289
|
+
/// Replace the configuration. Size limits are clamped (see StorageConfig).
|
|
290
|
+
/// Call it before start(): the sync thread reads its tuning once when it
|
|
291
|
+
/// starts, and the store's other threads read the config without a lock.
|
|
219
292
|
void set_config(const StorageConfig& config);
|
|
220
293
|
const StorageConfig& get_config() const;
|
|
221
294
|
|
|
@@ -287,20 +360,60 @@ private:
|
|
|
287
360
|
// Network message handlers (run on a reactor thread).
|
|
288
361
|
void on_storage_message(const PeerId& from, ByteView payload);
|
|
289
362
|
void on_peer_connected(const PeerId& peer_id);
|
|
363
|
+
void on_peer_disconnected(const PeerId& peer_id);
|
|
364
|
+
void on_peer_writable(const PeerId& peer_id);
|
|
290
365
|
|
|
291
366
|
PeerNetwork* network_ = nullptr;
|
|
292
367
|
StorageConfig config_;
|
|
293
368
|
|
|
294
|
-
// In-memory storage
|
|
369
|
+
// In-memory storage.
|
|
370
|
+
//
|
|
371
|
+
// Ordered, not hashed, and that is load-bearing: a snapshot is streamed one
|
|
372
|
+
// bounded chunk at a time, and between chunks the only thing carried over is
|
|
373
|
+
// the last key sent. An ordered map turns that key back into a position with
|
|
374
|
+
// upper_bound(), so a stream costs nothing to keep alive and cannot be
|
|
375
|
+
// derailed by concurrent writes — an unordered_map would need either a
|
|
376
|
+
// materialised key list per peer or iterators that a rehash invalidates.
|
|
377
|
+
// It also makes keys_with_prefix() a range scan instead of a full walk.
|
|
295
378
|
mutable std::mutex storage_mutex_;
|
|
296
|
-
std::
|
|
379
|
+
std::map<std::string, StorageEntry> entries_;
|
|
297
380
|
|
|
298
|
-
// Sync state
|
|
381
|
+
// Sync state.
|
|
382
|
+
//
|
|
383
|
+
// Lock order, exactly one rule: storage_mutex_ and sync_mutex_ are never held
|
|
384
|
+
// at the same time, and neither is ever held across a call into PeerNetwork.
|
|
299
385
|
mutable std::mutex sync_mutex_;
|
|
300
386
|
StorageSyncStatus sync_status_;
|
|
301
387
|
bool initial_sync_complete_;
|
|
302
388
|
std::chrono::steady_clock::time_point last_sync_time_;
|
|
303
389
|
|
|
390
|
+
/// What we owe one peer. A snapshot is served as a walk over the key space
|
|
391
|
+
/// whose whole resumable state is `cursor` — the last key already sent.
|
|
392
|
+
struct PeerSync {
|
|
393
|
+
bool streaming = false; ///< a snapshot is in flight
|
|
394
|
+
bool started = false; ///< cursor is meaningful
|
|
395
|
+
std::string cursor; ///< last key sent
|
|
396
|
+
bool owed = false; ///< a (re)snapshot is due
|
|
397
|
+
std::chrono::steady_clock::time_point last_start{}; ///< for sync_min_interval_ms
|
|
398
|
+
};
|
|
399
|
+
std::unordered_map<PeerId, PeerSync, PeerId::Hash> peers_; ///< guarded by sync_mutex_
|
|
400
|
+
/// Bumped, under sync_mutex_, whenever something the sync thread would act on
|
|
401
|
+
/// changes — a snapshot scheduled, a link drained, a peer gone. The thread
|
|
402
|
+
/// reads it before it scans and compares it before it waits, because between
|
|
403
|
+
/// those two points it is outside the lock (serializing and sending), and a
|
|
404
|
+
/// notify that arrives there would otherwise be one nobody is waiting for.
|
|
405
|
+
uint64_t sync_epoch_ = 0;
|
|
406
|
+
|
|
407
|
+
// The sync thread: serializes and paces every snapshot chunk, so no reactor
|
|
408
|
+
// thread ever does work proportional to the size of the database.
|
|
409
|
+
std::atomic<bool> sync_running_{false};
|
|
410
|
+
std::thread sync_thread_;
|
|
411
|
+
std::condition_variable sync_cv_; ///< paired with sync_mutex_
|
|
412
|
+
// Sync tuning, copied out of the config when the thread starts and read-only
|
|
413
|
+
// afterwards, so the thread never races a set_config() on another thread.
|
|
414
|
+
size_t batch_bytes_{0};
|
|
415
|
+
std::chrono::milliseconds sync_interval_{0};
|
|
416
|
+
|
|
304
417
|
// Statistics
|
|
305
418
|
mutable std::mutex stats_mutex_;
|
|
306
419
|
StorageStatistics stats_;
|
|
@@ -317,14 +430,37 @@ private:
|
|
|
317
430
|
bool dirty_; // Flag indicating unsaved changes
|
|
318
431
|
|
|
319
432
|
// Wire opcodes (MessageType::Storage payload, byte 0)
|
|
320
|
-
static constexpr uint8_t OP_ENTRY
|
|
321
|
-
static constexpr uint8_t OP_SYNC_REQUEST
|
|
322
|
-
static constexpr uint8_t
|
|
433
|
+
static constexpr uint8_t OP_ENTRY = 1;
|
|
434
|
+
static constexpr uint8_t OP_SYNC_REQUEST = 2;
|
|
435
|
+
static constexpr uint8_t OP_SYNC_CHUNK = 3;
|
|
436
|
+
|
|
437
|
+
// SYNC_CHUNK flags (byte 1)
|
|
438
|
+
static constexpr uint8_t FLAG_LAST = 0x01; ///< final chunk of this snapshot
|
|
439
|
+
|
|
440
|
+
/// Refuse an inbound Storage message larger than this before parsing it. Our
|
|
441
|
+
/// own sender never comes near it (one chunk is sync_batch_bytes plus at most
|
|
442
|
+
/// one value, both capped at 2 MiB), while the block layer would happily hand
|
|
443
|
+
/// us 64 MiB from a peer that means us harm.
|
|
444
|
+
static constexpr size_t kMaxInboundMessage = 8 * 1024 * 1024;
|
|
323
445
|
|
|
324
446
|
// Private methods
|
|
325
447
|
void initialize();
|
|
326
448
|
void shutdown();
|
|
327
449
|
void persistence_thread_loop();
|
|
450
|
+
void start_sync_thread();
|
|
451
|
+
void stop_sync_thread();
|
|
452
|
+
void sync_thread_loop();
|
|
453
|
+
/// What one turn of a peer's snapshot stream left behind.
|
|
454
|
+
enum class ChunkResult {
|
|
455
|
+
Continue, ///< a chunk went out and the link has room for the next
|
|
456
|
+
Blocked, ///< a chunk went out and filled the link; retry when it drains
|
|
457
|
+
Finished ///< the snapshot is complete (or the peer is gone)
|
|
458
|
+
};
|
|
459
|
+
/// Serialize and send the next chunk of `peer`'s snapshot.
|
|
460
|
+
ChunkResult stream_snapshot_chunk(const PeerId& peer);
|
|
461
|
+
/// Note that `peer` is due a snapshot; the sync thread starts it once the
|
|
462
|
+
/// link has room and the per-peer interval has elapsed.
|
|
463
|
+
void schedule_snapshot(const PeerId& peer, bool requested_by_peer);
|
|
328
464
|
|
|
329
465
|
// Internal put with full control
|
|
330
466
|
bool put_internal(const std::string& key, StorageValueType type,
|
|
@@ -341,14 +477,20 @@ private:
|
|
|
341
477
|
double deserialize_double(const std::vector<uint8_t>& data) const;
|
|
342
478
|
std::string deserialize_string(const std::vector<uint8_t>& data) const;
|
|
343
479
|
|
|
344
|
-
// Network operations
|
|
345
|
-
|
|
346
|
-
|
|
480
|
+
// Network operations. Every one of them honours send()'s return value: a peer
|
|
481
|
+
// that answers "no room" is owed a snapshot instead of further entries.
|
|
482
|
+
/// Send one entry to every connected peer except `except` (null for none).
|
|
483
|
+
void replicate_entry(const StorageEntry& entry, const PeerId* except);
|
|
484
|
+
void broadcast_entry(const StorageEntry& entry) { replicate_entry(entry, nullptr); }
|
|
485
|
+
void forward_entry(const StorageEntry& entry, const PeerId& except) {
|
|
486
|
+
replicate_entry(entry, &except);
|
|
487
|
+
}
|
|
347
488
|
void send_sync_request(const PeerId& peer_id);
|
|
348
|
-
void send_sync_response(const PeerId& peer_id);
|
|
349
489
|
|
|
350
490
|
// Apply a remote entry with LWW; fills `out_event` and returns true if applied.
|
|
351
491
|
bool apply_remote_entry(const StorageEntry& entry, StorageChangeEvent* out_event);
|
|
492
|
+
/// Parse and apply the entries in one SYNC_CHUNK body. @return how many won.
|
|
493
|
+
uint32_t apply_chunk(const PeerId& from, const uint8_t* data, size_t size, uint32_t count);
|
|
352
494
|
|
|
353
495
|
// File path helpers
|
|
354
496
|
std::string get_data_file_path() const;
|
|
@@ -359,6 +501,8 @@ private:
|
|
|
359
501
|
bool read_data_file();
|
|
360
502
|
|
|
361
503
|
// Utility
|
|
504
|
+
/// Clamp the size limits to what one connection's send queue can carry.
|
|
505
|
+
static void sanitize_config(StorageConfig& config);
|
|
362
506
|
uint64_t get_current_timestamp_ms() const;
|
|
363
507
|
std::string get_our_peer_id() const;
|
|
364
508
|
void notify_change(const StorageChangeEvent& event);
|
|
@@ -80,7 +80,7 @@ void FileTransfer::attach(NodeContext& ctx) {
|
|
|
80
80
|
network_ = &ctx.network;
|
|
81
81
|
network_->on(MessageType::FileChunk,
|
|
82
82
|
[this](const Peer& peer, ByteView payload) { on_message(peer, payload); });
|
|
83
|
-
network_->on_peer_disconnected([this](const PeerId& id) {
|
|
83
|
+
network_->on_peer_disconnected([this](const PeerId& id, CloseReason) {
|
|
84
84
|
// Fail every in-flight transfer with the departed peer and reclaim temps.
|
|
85
85
|
std::vector<std::shared_ptr<Outgoing>> out;
|
|
86
86
|
std::vector<std::shared_ptr<Incoming>> in;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "librats/subsystems/hole_punch.h"
|
|
2
2
|
#include "librats/node/dial_service.h"
|
|
3
3
|
#include "librats/node/node_context.h"
|
|
4
|
+
#include "librats/peer/peer_info.h"
|
|
4
5
|
#include "librats/util/logger.h"
|
|
5
6
|
|
|
6
7
|
#include <algorithm>
|
|
@@ -135,6 +136,7 @@ void HolePunch::attach(NodeContext& ctx) {
|
|
|
135
136
|
// does not know its own external one simply cannot punch — it can still relay.
|
|
136
137
|
dialer_ = ctx.services.get<DialService>();
|
|
137
138
|
external_ = ctx.services.get<ExternalAddressService>();
|
|
139
|
+
services_ = &ctx.services;
|
|
138
140
|
|
|
139
141
|
// Offer punching to sibling modules. Registered even without a dialer: punch()
|
|
140
142
|
// answers false on its own then, which is exactly what a caller expects from a
|
|
@@ -151,17 +153,30 @@ void HolePunch::attach(NodeContext& ctx) {
|
|
|
151
153
|
// reconciles sessions against the peer set, so this is the fast path, not the
|
|
152
154
|
// only one.
|
|
153
155
|
network_->on_peer_connected([this](const Peer& peer) {
|
|
156
|
+
// A relayed link is not what a punch was for — it is the fallback the punch
|
|
157
|
+
// is trying to make unnecessary, and a punch may well have been started
|
|
158
|
+
// BECAUSE it came up (see subsystems/relay.h). Retiring the session here
|
|
159
|
+
// would call the upgrade off the moment it began.
|
|
160
|
+
const auto info = peer.info();
|
|
161
|
+
if (info && info->transport == TransportKind::Relay) return;
|
|
154
162
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
155
163
|
sessions_.erase(peer.id());
|
|
156
164
|
});
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
void HolePunch::start() {
|
|
168
|
+
// Resolved here rather than in attach(): Relay may be attached after us, and
|
|
169
|
+
// every attach() runs before any start(). Stored before running_ goes true, so
|
|
170
|
+
// an escalation on a reactor thread never sees a half-initialised state.
|
|
171
|
+
if (config_.relay_on_failure && services_)
|
|
172
|
+
relay_.store(services_->get<RelayService>());
|
|
173
|
+
|
|
160
174
|
if (running_.exchange(true)) return;
|
|
161
175
|
worker_ = std::thread([this] { loop(); });
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
void HolePunch::stop() {
|
|
179
|
+
relay_.store(nullptr);
|
|
165
180
|
if (!running_.exchange(false)) return;
|
|
166
181
|
cv_.notify_all();
|
|
167
182
|
if (worker_.joinable()) worker_.join();
|
|
@@ -178,14 +193,20 @@ bool HolePunch::punch(const PeerId& target) {
|
|
|
178
193
|
if (target == network_->local_id()) return false;
|
|
179
194
|
|
|
180
195
|
// Already reachable the ordinary way. Checked before any state is created so a
|
|
181
|
-
// caller can punch() freely on discovery without having to check first.
|
|
182
|
-
|
|
196
|
+
// caller can punch() freely on discovery without having to check first. A
|
|
197
|
+
// RELAYED peer is deliberately not "already reachable": punching it is how the
|
|
198
|
+
// circuit gets replaced by a direct link (see subsystems/relay.h).
|
|
199
|
+
for (const PeerId& id : directly_connected())
|
|
183
200
|
if (id == target) return false;
|
|
184
201
|
|
|
185
202
|
if (config_.skip_when_endpoint_dependent && external_ &&
|
|
186
203
|
external_->udp_mapping() == NatMapping::EndpointDependent) {
|
|
187
204
|
LOG_DEBUG("punch", "Not punching to " << target.short_hex()
|
|
188
205
|
<< ": our own mapping is per-destination (symmetric NAT)");
|
|
206
|
+
// Nothing about this will improve with time or retries: no endpoint we can
|
|
207
|
+
// advertise is the one the target's packets would arrive on. This is
|
|
208
|
+
// exactly the case a relayed path exists for.
|
|
209
|
+
escalate_to_relay(target);
|
|
189
210
|
return false;
|
|
190
211
|
}
|
|
191
212
|
|
|
@@ -193,6 +214,7 @@ bool HolePunch::punch(const PeerId& target) {
|
|
|
193
214
|
if (addresses.empty()) {
|
|
194
215
|
LOG_DEBUG("punch", "Not punching to " << target.short_hex()
|
|
195
216
|
<< ": no external datagram endpoint to advertise yet");
|
|
217
|
+
escalate_to_relay(target);
|
|
196
218
|
return false;
|
|
197
219
|
}
|
|
198
220
|
|
|
@@ -210,8 +232,11 @@ bool HolePunch::punch(const PeerId& target) {
|
|
|
210
232
|
}
|
|
211
233
|
|
|
212
234
|
if (!send_connect(target, /*opening=*/true, nullptr)) {
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
{
|
|
236
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
237
|
+
sessions_.erase(target); // nobody could carry it; nothing is in flight
|
|
238
|
+
}
|
|
239
|
+
escalate_to_relay(target);
|
|
215
240
|
return false;
|
|
216
241
|
}
|
|
217
242
|
LOG_DEBUG("punch", "Punch rendezvous started with " << target.short_hex());
|
|
@@ -461,10 +486,12 @@ void HolePunch::service_sessions() {
|
|
|
461
486
|
|
|
462
487
|
// Connected peers first, without the lock: a session whose target is now a peer
|
|
463
488
|
// succeeded, however it got there (our burst, or theirs arriving as inbound).
|
|
464
|
-
|
|
489
|
+
// Direct links only — a relayed one is what a punch is trying to replace.
|
|
490
|
+
std::vector<PeerId> connected = directly_connected();
|
|
465
491
|
|
|
466
492
|
struct Retry { PeerId target; PeerId via; bool have_via; };
|
|
467
493
|
std::vector<Retry> retry;
|
|
494
|
+
std::vector<PeerId> exhausted; ///< targets to hand on to the relay, once unlocked
|
|
468
495
|
{
|
|
469
496
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
470
497
|
|
|
@@ -496,7 +523,10 @@ void HolePunch::service_sessions() {
|
|
|
496
523
|
// first). Calling the target off here would drop the retries it is
|
|
497
524
|
// about to send, and block this node's own later punch to it, for
|
|
498
525
|
// the whole cooldown. So a responder just forgets the round.
|
|
499
|
-
if (initiator)
|
|
526
|
+
if (initiator) {
|
|
527
|
+
begin_cooldown(target);
|
|
528
|
+
exhausted.push_back(target);
|
|
529
|
+
}
|
|
500
530
|
it = sessions_.erase(it);
|
|
501
531
|
continue;
|
|
502
532
|
}
|
|
@@ -510,9 +540,52 @@ void HolePunch::service_sessions() {
|
|
|
510
540
|
|
|
511
541
|
for (const Retry& r : retry) {
|
|
512
542
|
if (send_connect(r.target, /*opening=*/true, r.have_via ? &r.via : nullptr)) continue;
|
|
513
|
-
|
|
514
|
-
|
|
543
|
+
{
|
|
544
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
545
|
+
sessions_.erase(r.target);
|
|
546
|
+
}
|
|
547
|
+
exhausted.push_back(r.target);
|
|
515
548
|
}
|
|
549
|
+
|
|
550
|
+
// The rungs above this one are spent: no address we can advertise got through.
|
|
551
|
+
// Handing the target on is the difference between a peer that is unreachable
|
|
552
|
+
// and one that is merely expensive to reach.
|
|
553
|
+
for (const PeerId& target : exhausted) escalate_to_relay(target);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
void HolePunch::escalate_to_relay(const PeerId& target) {
|
|
557
|
+
RelayService* relay = relay_.load();
|
|
558
|
+
if (!relay) return; // no Relay attached, or the fallback is off
|
|
559
|
+
if (!relay->connect_via_relay(target)) return;
|
|
560
|
+
|
|
561
|
+
LOG_DEBUG("punch", "Punching to " << target.short_hex()
|
|
562
|
+
<< " is not going to work; looking for a relay instead");
|
|
563
|
+
|
|
564
|
+
// The target has just changed hands, and the cooldown a give-up started must not
|
|
565
|
+
// outlive that. It exists to stop us hammering a peer we cannot reach — a job
|
|
566
|
+
// the relay now owns, with an attempt timeout and a cooldown of its own.
|
|
567
|
+
//
|
|
568
|
+
// And if that attempt lands, the circuit is precisely the new information that
|
|
569
|
+
// makes another punch worth trying: the two ends are peers at last, so the
|
|
570
|
+
// rendezvous can travel over the very circuit carrying them, which is what
|
|
571
|
+
// Relay asks for the moment the circuit comes up (see subsystems/relay.h).
|
|
572
|
+
// Leaving the cooldown standing would refuse that upgrade before it started —
|
|
573
|
+
// and nothing would ever ask again, so the circuit would outlive its purpose and
|
|
574
|
+
// keep costing a third node bandwidth for the life of the peer.
|
|
575
|
+
//
|
|
576
|
+
// Bounded, not a loop: an upgrade punch that fails lands here again, and
|
|
577
|
+
// connect_via_relay then answers false — the target is already a peer — so the
|
|
578
|
+
// fresh cooldown stands. Taken after the call, never around it: Relay resolves
|
|
579
|
+
// the reverse direction through us, and this mutex must not be held into it.
|
|
580
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
581
|
+
cooldown_.erase(target);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
std::vector<PeerId> HolePunch::directly_connected() const {
|
|
585
|
+
std::vector<PeerId> ids;
|
|
586
|
+
for (const PeerInfo& info : network_->peers())
|
|
587
|
+
if (info.transport != TransportKind::Relay) ids.push_back(info.id);
|
|
588
|
+
return ids;
|
|
516
589
|
}
|
|
517
590
|
|
|
518
591
|
bool HolePunch::in_cooldown(const PeerId& target) const {
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
#include "librats/peer/peer.h"
|
|
96
96
|
#include "librats/peer/peer_id.h"
|
|
97
97
|
#include "librats/subsystems/hole_punch_service.h"
|
|
98
|
+
#include "librats/subsystems/relay_service.h"
|
|
98
99
|
|
|
99
100
|
#include <atomic>
|
|
100
101
|
#include <chrono>
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
namespace librats {
|
|
109
110
|
|
|
110
111
|
class DialService;
|
|
112
|
+
class ServiceRegistry;
|
|
111
113
|
|
|
112
114
|
/// Published as HolePunchService, so a module that discovers a peer it cannot dial
|
|
113
115
|
/// (PeerExchange) can hand the id over without depending on this class.
|
|
@@ -163,6 +165,12 @@ public:
|
|
|
163
165
|
|
|
164
166
|
/// Session bookkeeping cadence — retries, timeouts, cooldown expiry.
|
|
165
167
|
std::chrono::milliseconds tick{250};
|
|
168
|
+
|
|
169
|
+
/// When a punch cannot be attempted at all, or has been given up on, hand
|
|
170
|
+
/// the target to RelayService — the next rung down the ladder (see
|
|
171
|
+
/// subsystems/relay.h). Costs nothing when no Relay is attached: the
|
|
172
|
+
/// service simply does not resolve.
|
|
173
|
+
bool relay_on_failure = true;
|
|
166
174
|
};
|
|
167
175
|
|
|
168
176
|
HolePunch();
|
|
@@ -235,6 +243,17 @@ private:
|
|
|
235
243
|
void service_sessions();
|
|
236
244
|
|
|
237
245
|
// — helpers —
|
|
246
|
+
/// Hand `target` to the relay module, if one is attached and the fallback is
|
|
247
|
+
/// on. Called when a punch is impossible or has run out of attempts — the
|
|
248
|
+
/// point at which a relayed path stops being the worse option and becomes the
|
|
249
|
+
/// only one. Retires the target's cooldown when the relay takes it on, so that
|
|
250
|
+
/// the upgrade punch a circuit asks for is not refused by the very give-up that
|
|
251
|
+
/// produced the circuit. Caller must NOT hold the mutex.
|
|
252
|
+
void escalate_to_relay(const PeerId& target);
|
|
253
|
+
/// Peers reachable over a DIRECT link. A relayed peer is deliberately not one:
|
|
254
|
+
/// a punch to it is an upgrade in progress, and counting the circuit as success
|
|
255
|
+
/// would retire the session before it had done anything.
|
|
256
|
+
std::vector<PeerId> directly_connected() const;
|
|
238
257
|
std::vector<Address> own_punch_addresses() const;
|
|
239
258
|
bool relay_budget_ok(const PeerId& from);
|
|
240
259
|
bool in_cooldown(const PeerId& target) const; ///< caller holds mutex_
|
|
@@ -244,6 +263,11 @@ private:
|
|
|
244
263
|
PeerNetwork* network_ = nullptr;
|
|
245
264
|
DialService* dialer_ = nullptr;
|
|
246
265
|
ExternalAddressService* external_ = nullptr;
|
|
266
|
+
ServiceRegistry* services_ = nullptr;
|
|
267
|
+
/// Resolved in start(), not attach(): Relay may be attached after us, and every
|
|
268
|
+
/// attach() runs before any start(). Atomic because it is read from reactor
|
|
269
|
+
/// threads and the worker alike.
|
|
270
|
+
std::atomic<RelayService*> relay_{nullptr};
|
|
247
271
|
|
|
248
272
|
std::atomic<bool> running_{false};
|
|
249
273
|
std::atomic<uint64_t> punches_started_{0};
|
|
@@ -106,7 +106,7 @@ void PubSub::attach(NodeContext& ctx) {
|
|
|
106
106
|
network_->on(MessageType::Gossip,
|
|
107
107
|
[this](const Peer& peer, ByteView payload) { on_gossip(peer, payload); });
|
|
108
108
|
network_->on_peer_connected([this](const Peer& peer) { on_new_peer(peer); });
|
|
109
|
-
network_->on_peer_disconnected([this](const PeerId& id) { on_peer_gone(id); });
|
|
109
|
+
network_->on_peer_disconnected([this](const PeerId& id, CloseReason) { on_peer_gone(id); });
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
void PubSub::start() {
|
|
@@ -74,7 +74,7 @@ std::vector<Address> ReconnectionService::known_peers(size_t n) const {
|
|
|
74
74
|
void ReconnectionService::attach(NodeContext& ctx) {
|
|
75
75
|
network_ = &ctx.network;
|
|
76
76
|
network_->on_peer_connected([this](const Peer& peer) { on_connected(peer); });
|
|
77
|
-
network_->on_peer_disconnected([this](const PeerId& id) { on_disconnected(id); });
|
|
77
|
+
network_->on_peer_disconnected([this](const PeerId& id, CloseReason) { on_disconnected(id); });
|
|
78
78
|
network_->on_dial_failed([this](const Address& addr) { on_dial_failed(addr); });
|
|
79
79
|
}
|
|
80
80
|
|