librats 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,303 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file relay_link.h
5
+ * @brief A peer connection carried inside another peer's connection.
6
+ *
7
+ * When two nodes cannot reach each other directly — a symmetric NAT on one side,
8
+ * a network that swallows UDP, a hole punch that simply did not land — the one
9
+ * thing left is to route their bytes through a node they can both reach. The
10
+ * question is *where* in the stack to do that, and this file is the answer: at the
11
+ * very bottom, as one more way of obtaining the ordered, reliable byte stream a
12
+ * Connection runs on (see link.h).
13
+ *
14
+ * That choice is what makes a relayed peer indistinguishable from a direct one
15
+ * everywhere above:
16
+ *
17
+ * - the Noise_XX handshake runs END TO END between the two peers, so the relay
18
+ * carries ciphertext it cannot read, cannot forge and cannot replay into
19
+ * anything. It is a pipe, not a party;
20
+ * - the self-certifying PeerId and the protocol_id bound into the handshake are
21
+ * checked exactly as usual, so a relay cannot impersonate either end;
22
+ * - block framing, the send queue and its high-water mark, the establish
23
+ * deadline, identify, and the peer table's duplicate resolution all apply
24
+ * unchanged, because there is nothing here for them to know about;
25
+ * - every subsystem — pub/sub, file transfer, PEX — works over a relayed peer
26
+ * without a line of its own.
27
+ *
28
+ * Relaying at the message level instead (a third node forwarding application
29
+ * frames) would have meant a second handshake, a second authentication scheme and
30
+ * a second backpressure story — and would have handed the relay the plaintext.
31
+ *
32
+ * ── The two halves ──────────────────────────────────────────────────────────
33
+ * `Circuit` is the state of one relayed connection: the bytes in flight in each
34
+ * direction and the credit windows that bound them. `RelayLink` is the paper-thin
35
+ * adapter that presents that state to a Connection as a Link. They are separate
36
+ * because the Connection owns its Link outright (unique_ptr, destroyed on the
37
+ * reactor thread) while the module driving the relay protocol has to keep hold of
38
+ * the same circuit to feed it — so the state is shared and only the adapter is
39
+ * owned.
40
+ *
41
+ * `CircuitCarrier` is the other direction: what the circuit needs from whoever
42
+ * speaks the relay protocol. It is deliberately tiny and knows nothing about the
43
+ * wire format, which stays entirely in the module that implements it.
44
+ *
45
+ * ── Flow control, and why it is not optional ────────────────────────────────
46
+ * A relay reads from one peer and writes to another, and it cannot stop reading:
47
+ * the reactor drains a readable connection to the end. So without a bound, a slow
48
+ * receiver makes the relay's send queue to it grow until that peer is dropped as a
49
+ * slow consumer — losing all of its traffic, not just the circuit's, and letting
50
+ * any pair of peers spend a stranger's memory at will.
51
+ *
52
+ * The bound is an end-to-end credit window, granted by the receiver and spent by
53
+ * the sender, exactly as in a stream multiplexer. Each end advertises how much it
54
+ * is willing to have in flight; the sender may not exceed it, and the receiver
55
+ * grants more as it consumes. Two things follow:
56
+ *
57
+ * - the relay can never hold more than about one window per circuit, whatever
58
+ * either end does, so its memory is bounded by configuration rather than by
59
+ * the goodwill of its users;
60
+ * - a slow path makes the circuit slow instead of making it fail. The window
61
+ * only ever binds when the far end is not keeping up, which is precisely when
62
+ * something ought to give.
63
+ *
64
+ * Credit is returned as the bytes are read OUT of the circuit, not as they arrive,
65
+ * and in batches (kCreditGrantFraction of the window) so the grant costs one small
66
+ * message per window-worth of data rather than one per chunk.
67
+ *
68
+ * ── Threading ───────────────────────────────────────────────────────────────
69
+ * A Circuit is touched only by the reactor thread that owns its Connection, which
70
+ * is by construction the same thread that owns the carrier's connection (see
71
+ * node/circuit_service.h). It therefore holds no locks and no atomics, like
72
+ * everything else on this path. The module that owns circuits may keep its table
73
+ * of them under a mutex; the state below must not be reached from off that one
74
+ * thread.
75
+ */
76
+
77
+ #include "librats/util/rats_export.h"
78
+ #include "librats/core/bytes.h"
79
+ #include "librats/core/types.h"
80
+ #include "librats/transport/link.h"
81
+
82
+ #include <cstdint>
83
+ #include <memory>
84
+
85
+ namespace librats {
86
+
87
+ /// What a circuit needs from whoever speaks the relay protocol on the wire.
88
+ ///
89
+ /// Implemented by the relay module; called by Circuit on the reactor thread. None
90
+ /// of these say anything about the wire format — the implementer frames the
91
+ /// message and hands it to the carrier peer — so this header stays a transport
92
+ /// header with no protocol in it.
93
+ class CircuitCarrier {
94
+ public:
95
+ virtual ~CircuitCarrier() = default;
96
+
97
+ /// Send `count` slices (together no larger than kMaxDataChunk) toward the far
98
+ /// end as one data message.
99
+ /// @return whether the carrier's own send queue still has room. False does NOT
100
+ /// mean the data was dropped — it was queued like any other message —
101
+ /// it means stop, and say so again through on_carrier_writable() once
102
+ /// the queue drains. Exactly what PeerNetwork::send answers, which is
103
+ /// what an implementation normally forwards.
104
+ virtual bool circuit_send_data(uint32_t circuit, const ByteView* slices, size_t count) = 0;
105
+
106
+ /// Grant the far end `bytes` more of our receive window.
107
+ virtual void circuit_send_credit(uint32_t circuit, uint32_t bytes) = 0;
108
+
109
+ /// Tell the far end (and the relay in between) that this circuit is over.
110
+ /// Called at most once per circuit.
111
+ virtual void circuit_send_close(uint32_t circuit, CloseReason reason) = 0;
112
+
113
+ /// The Link is gone: its Connection has been destroyed. The circuit will not
114
+ /// be read or written again and its bookkeeping can be dropped.
115
+ ///
116
+ /// The only one of these that is NOT guaranteed to arrive on the reactor
117
+ /// thread. It normally does — a Connection is destroyed by its reactor — but a
118
+ /// link handed to a reactor that never ran the adoption (the node was stopping)
119
+ /// is released when the task queue is destroyed instead, on whichever thread
120
+ /// takes the reactor down. An implementation must therefore be safe to call
121
+ /// here from any thread, and must not assume the circuit ever opened.
122
+ virtual void circuit_released(uint32_t circuit) = 0;
123
+ };
124
+
125
+ /// One relayed connection's byte stream and credit windows. See the file comment.
126
+ class RATS_API Circuit {
127
+ public:
128
+ /// Largest data message a circuit puts on the wire. Big enough that the
129
+ /// per-message overhead (a relay header, a frame header, an AEAD tag, and the
130
+ /// relay's own copy) is noise against the payload; small enough that one
131
+ /// circuit cannot monopolise the carrier's send queue for long, and that the
132
+ /// relay's per-message work stays granular.
133
+ static constexpr size_t kMaxDataChunk = 16 * 1024;
134
+
135
+ /// Receive window a circuit advertises unless told otherwise. This is the
136
+ /// memory ONE circuit can make the relay (and this node) hold, so it is the
137
+ /// knob that turns "how fast can a relayed peer go" into "how much is a relay
138
+ /// asked to carry": a 256 KiB window keeps a 100 ms round trip busy at roughly
139
+ /// 20 Mbit/s, which is far more than a fallback path is meant to be used for.
140
+ static constexpr uint32_t kDefaultWindow = 256 * 1024;
141
+
142
+ /// Credit is granted back once this much of the window has been consumed.
143
+ /// Halves mean one small grant per half-window of data, and leave the sender
144
+ /// the other half to work with while the grant is in flight.
145
+ static constexpr uint32_t kCreditGrantFraction = 2;
146
+
147
+ /// A circuit we are opening: the request is out, the far end has not answered.
148
+ /// Nothing may be written until on_accept() brings the window it will receive.
149
+ ///
150
+ /// @param id the circuit's id on the carrier link (see the relay module).
151
+ /// @param carrier how outbound bytes leave; a shared_ptr because a Connection
152
+ /// can outlive the module that opened the circuit (subsystems
153
+ /// stop before the reactors do).
154
+ /// @param recv_window what we are willing to have in flight toward us.
155
+ static std::shared_ptr<Circuit> opening(uint32_t id, std::shared_ptr<CircuitCarrier> carrier,
156
+ uint32_t recv_window = kDefaultWindow);
157
+
158
+ /// A circuit opened TO us: the far end is already there and its request carried
159
+ /// the window it will accept, so the stream is open from the first instant.
160
+ ///
161
+ /// Two factories rather than one flag plus a window that is sometimes ignored,
162
+ /// because "the circuit is open" and "we know what the far end will take" are
163
+ /// the same fact — an open circuit whose send window nobody set would be a
164
+ /// stream that silently refuses to carry anything.
165
+ static std::shared_ptr<Circuit> accepted(uint32_t id, std::shared_ptr<CircuitCarrier> carrier,
166
+ uint32_t peer_window,
167
+ uint32_t recv_window = kDefaultWindow);
168
+
169
+ Circuit(const Circuit&) = delete;
170
+ Circuit& operator=(const Circuit&) = delete;
171
+
172
+ uint32_t id() const noexcept { return id_; }
173
+ /// What we advertise to the far end as our receive window.
174
+ uint32_t recv_window() const noexcept { return recv_window_; }
175
+ bool is_open() const noexcept { return state_ == State::Open; }
176
+ bool is_closed() const noexcept { return state_ == State::Closed; }
177
+
178
+ // ── Fed by the relay module (reactor thread) ────────────────────────────
179
+ //
180
+ // Each of these returns the poll-equivalent events the circuit's Connection
181
+ // should now be given (PollIn / PollOut / PollErr), or 0 for none. The caller
182
+ // dispatches them — the circuit deliberately knows nothing about reactors.
183
+ // Returning the mask rather than raising it keeps that dependency out of the
184
+ // transport layer and lets the caller batch several changes into one wake.
185
+
186
+ /// Bytes arrived from the far end. A peer that overruns the window it was
187
+ /// granted is a protocol error and the circuit is failed rather than allowed
188
+ /// to grow: that window is the whole bound on what a circuit can cost.
189
+ uint32_t on_data(ByteView bytes);
190
+
191
+ /// The far end accepted the circuit and advertised `peer_window` bytes of
192
+ /// receive window. No-op unless the circuit is still pending.
193
+ uint32_t on_accept(uint32_t peer_window);
194
+
195
+ /// The far end granted `bytes` more of send window.
196
+ uint32_t on_credit(uint32_t bytes);
197
+
198
+ /// The circuit is over. `orderly` means the far end closed cleanly, so
199
+ /// whatever it already sent is still delivered before the end of stream is
200
+ /// reported; anything else fails the connection at once.
201
+ uint32_t on_closed(CloseReason reason, bool orderly);
202
+
203
+ /// The carrier's send queue, which had filled up, has room again.
204
+ uint32_t on_carrier_writable();
205
+
206
+ // ── Driven by the Connection through RelayLink (reactor thread) ─────────
207
+
208
+ Link::IoResult read(ByteSpan into);
209
+ Link::IoResult write(const ByteView* slices, size_t count);
210
+ void want_write(bool on) noexcept { want_write_ = on; }
211
+ CloseReason close_reason() const noexcept { return reason_; }
212
+ /// Begin teardown from our side: tell the far end, then refuse further I/O.
213
+ void shutdown(CloseReason reason);
214
+ /// The Link is being destroyed; release the carrier's bookkeeping. Idempotent.
215
+ void release();
216
+
217
+ /// Bytes waiting to be read out (diagnostics and tests).
218
+ size_t pending_input() const noexcept { return inbox_.size() - inbox_read_; }
219
+ /// Send window still available (diagnostics and tests).
220
+ uint32_t send_credit() const noexcept { return send_credit_; }
221
+
222
+ private:
223
+ Circuit(uint32_t id, std::shared_ptr<CircuitCarrier> carrier, bool open,
224
+ uint32_t recv_window, uint32_t peer_window);
225
+
226
+ enum class State : uint8_t {
227
+ Pending, ///< outbound: opened, waiting for the far end to accept
228
+ Open, ///< bytes may flow
229
+ Closed, ///< no further I/O; read() drains what is left, then reports the end
230
+ };
231
+
232
+ /// Consumed prefix tolerated before the inbox is compacted. The connection
233
+ /// normally drains the inbox to empty on every readable event, which resets it
234
+ /// for free; this only bounds the pathological case where it does not.
235
+ static constexpr size_t kCompactThreshold = 64 * 1024;
236
+
237
+ bool blocked() const noexcept { return credit_blocked_ || carrier_blocked_; }
238
+ /// PollOut, but only if anyone is waiting for it — see want_write().
239
+ uint32_t writable_event() const noexcept;
240
+
241
+ uint32_t id_;
242
+ std::shared_ptr<CircuitCarrier> carrier_;
243
+ State state_;
244
+ CloseReason reason_ = CloseReason::PeerReset;
245
+ bool orderly_ = false; ///< the far end closed cleanly
246
+ bool released_ = false;
247
+ bool close_sent_ = false;
248
+
249
+ // — inbound —
250
+ Bytes inbox_;
251
+ size_t inbox_read_ = 0; ///< consumed prefix of inbox_
252
+ uint32_t recv_window_; ///< what we advertised to the far end
253
+ uint32_t recv_in_flight_ = 0; ///< delivered to us and not yet granted back
254
+ uint32_t recv_ungranted_ = 0; ///< consumed, owed back to the far end
255
+
256
+ // — outbound —
257
+ uint32_t send_credit_ = 0; ///< bytes the far end still allows us to send
258
+ bool want_write_ = false; ///< the Connection has more to write
259
+ bool credit_blocked_ = false; ///< a write stopped for want of credit
260
+ bool carrier_blocked_ = false;///< a write stopped on the carrier's queue
261
+ };
262
+
263
+ /// The Link a Connection holds for a relayed circuit.
264
+ ///
265
+ /// Deliberately empty of logic: the state has to be reachable by the module that
266
+ /// drives the relay protocol, which the Connection's unique_ptr would not allow,
267
+ /// so everything lives in the shared Circuit and this is the handle.
268
+ class RATS_API RelayLink final : public Link {
269
+ public:
270
+ explicit RelayLink(std::shared_ptr<Circuit> circuit) : circuit_(std::move(circuit)) {}
271
+ ~RelayLink() override { circuit_->release(); }
272
+
273
+ TransportKind kind() const noexcept override { return TransportKind::Relay; }
274
+
275
+ IoResult read(ByteSpan into) override { return circuit_->read(into); }
276
+ IoResult write(const ByteView* slices, size_t count) override {
277
+ return circuit_->write(slices, count);
278
+ }
279
+
280
+ /// A circuit is "connected" once the far end has accepted it. The Connection
281
+ /// only asks after a PollOut, which the relay module raises on acceptance; a
282
+ /// refusal arrives as PollErr instead and never reaches here.
283
+ bool connect_completed() override { return circuit_->is_open(); }
284
+
285
+ void want_write(bool on) override { circuit_->want_write(on); }
286
+
287
+ /// No endpoint of its own, and deliberately not the relay's: identify pairs the
288
+ /// address a link reports with the peer's advertised listen port to build a
289
+ /// dialable address, and NatStatus reads it as our own NAT mapping. Both would
290
+ /// be wrong here — the address on this path belongs to the node in the middle.
291
+ /// Reporting nothing is what keeps a relayed peer out of that machinery.
292
+ std::optional<Address> remote_endpoint() const override { return std::nullopt; }
293
+
294
+ CloseReason error_reason() const override { return circuit_->close_reason(); }
295
+ void close(CloseReason reason) override { circuit_->shutdown(reason); }
296
+
297
+ Circuit& circuit() noexcept { return *circuit_; }
298
+
299
+ private:
300
+ std::shared_ptr<Circuit> circuit_;
301
+ };
302
+
303
+ } // namespace librats
@@ -46,6 +46,7 @@ enum class MessageType : uint8_t {
46
46
  Typed = 7, ///< typed JSON message exchange (MessageJson)
47
47
  Pex = 8, ///< peer exchange — gossip of known peer addresses (PeerExchange)
48
48
  Punch = 9, ///< NAT hole-punch rendezvous, relayed peer→peer (HolePunch)
49
+ Relay = 10, ///< relayed circuits: a peer's connection carried through a third node (Relay)
49
50
  };
50
51
 
51
52
  /// Fixed header of an inner message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "2.1.4",
3
+ "version": "2.2.0",
4
4
  "description": "Node.js bindings for librats - A high-performance peer-to-peer networking library",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -132,6 +132,8 @@ private:
132
132
  void EnablePortMapping(const Napi::CallbackInfo& info);
133
133
  void EnableHolePunch(const Napi::CallbackInfo& info);
134
134
  Napi::Value PunchPeer(const Napi::CallbackInfo& info);
135
+ void EnableRelay(const Napi::CallbackInfo& info);
136
+ Napi::Value ConnectViaRelay(const Napi::CallbackInfo& info);
135
137
  Napi::Value GetNatMapping(const Napi::CallbackInfo& info);
136
138
 
137
139
  // ---- pub/sub ----
@@ -570,6 +572,28 @@ Napi::Value RatsNode::PunchPeer(const Napi::CallbackInfo& info) {
570
572
  return env.Undefined();
571
573
  }
572
574
 
575
+ // Relaying. serveAsRelay (default false) also carries OTHER peers' connections,
576
+ // which spends real bandwidth — so it is opted into rather than assumed.
577
+ void RatsNode::EnableRelay(const Napi::CallbackInfo& info) {
578
+ RATS_REQUIRE_NODE();
579
+ int serve = 0;
580
+ if (info.Length() >= 1 && info[0].IsBoolean()) serve = info[0].As<Napi::Boolean>().Value() ? 1 : 0;
581
+ throw_on_error(info.Env(), rats_enable_relay(node_, serve));
582
+ }
583
+
584
+ // Reach a peer through a relay. Non-blocking: success arrives as onPeerConnected.
585
+ Napi::Value RatsNode::ConnectViaRelay(const Napi::CallbackInfo& info) {
586
+ RATS_REQUIRE_NODE(info.Env().Undefined());
587
+ Napi::Env env = info.Env();
588
+ if (info.Length() < 1 || !info[0].IsString()) {
589
+ Napi::TypeError::New(env, "Expected peerId (string)").ThrowAsJavaScriptException();
590
+ return env.Undefined();
591
+ }
592
+ std::string peer = info[0].As<Napi::String>().Utf8Value();
593
+ throw_on_error(env, rats_connect_via_relay(node_, peer.c_str()));
594
+ return env.Undefined();
595
+ }
596
+
573
597
  // What the mesh has shown about this node's own NAT (a RATS_NAT_* value).
574
598
  Napi::Value RatsNode::GetNatMapping(const Napi::CallbackInfo& info) {
575
599
  RATS_REQUIRE_NODE(info.Env().Undefined());
@@ -988,6 +1012,8 @@ Napi::Object RatsNode::Init(Napi::Env env, Napi::Object exports) {
988
1012
  InstanceMethod("enablePortMapping", &RatsNode::EnablePortMapping),
989
1013
  InstanceMethod("enableHolePunch", &RatsNode::EnableHolePunch),
990
1014
  InstanceMethod("punchPeer", &RatsNode::PunchPeer),
1015
+ InstanceMethod("enableRelay", &RatsNode::EnableRelay),
1016
+ InstanceMethod("connectViaRelay", &RatsNode::ConnectViaRelay),
991
1017
  InstanceMethod("natMapping", &RatsNode::GetNatMapping),
992
1018
  // pub/sub
993
1019
  InstanceMethod("enablePubsub", &RatsNode::EnablePubsub),