librats 2.1.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +9 -0
  2. package/lib/index.d.ts +51 -6
  3. package/lib/index.js +42 -3
  4. package/native-src/CMakeLists.txt +13 -0
  5. package/native-src/src/librats/bindings/rats.cpp +90 -6
  6. package/native-src/src/librats/bindings/rats.h +92 -3
  7. package/native-src/src/librats/core/types.cpp +4 -3
  8. package/native-src/src/librats/core/types.h +25 -12
  9. package/native-src/src/librats/node/circuit_service.h +84 -0
  10. package/native-src/src/librats/node/config.h +9 -2
  11. package/native-src/src/librats/node/node.cpp +59 -5
  12. package/native-src/src/librats/node/node.h +38 -7
  13. package/native-src/src/librats/node/peer_network.h +19 -2
  14. package/native-src/src/librats/peer/peer.h +17 -6
  15. package/native-src/src/librats/peer/peer_table.cpp +41 -8
  16. package/native-src/src/librats/security/noise_security.cpp +2 -0
  17. package/native-src/src/librats/security/plaintext_security.h +1 -0
  18. package/native-src/src/librats/security/session.h +7 -0
  19. package/native-src/src/librats/storage/storage.cpp +434 -213
  20. package/native-src/src/librats/storage/storage.h +163 -19
  21. package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
  22. package/native-src/src/librats/subsystems/hole_punch.cpp +81 -8
  23. package/native-src/src/librats/subsystems/hole_punch.h +24 -0
  24. package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
  25. package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
  26. package/native-src/src/librats/subsystems/relay.cpp +1143 -0
  27. package/native-src/src/librats/subsystems/relay.h +211 -0
  28. package/native-src/src/librats/subsystems/relay_service.h +46 -0
  29. package/native-src/src/librats/transport/connection.cpp +40 -8
  30. package/native-src/src/librats/transport/connection.h +12 -2
  31. package/native-src/src/librats/transport/reactor.cpp +56 -8
  32. package/native-src/src/librats/transport/reactor.h +38 -0
  33. package/native-src/src/librats/transport/relay_link.cpp +208 -0
  34. package/native-src/src/librats/transport/relay_link.h +303 -0
  35. package/native-src/src/librats/util/logger.h +37 -5
  36. package/native-src/src/librats/util/network_monitor.cpp +14 -0
  37. package/native-src/src/librats/util/network_utils.cpp +10 -1
  38. package/native-src/src/librats/wire/frame.h +1 -0
  39. package/package.json +1 -1
  40. package/src/librats_node.cpp +76 -1
@@ -0,0 +1,208 @@
1
+ #include "librats/transport/relay_link.h"
2
+
3
+ #include "librats/core/io_poller.h" // PollIn / PollOut / PollErr
4
+ #include "librats/util/logger.h"
5
+
6
+ #include <algorithm>
7
+ #include <cstring>
8
+ #include <utility>
9
+
10
+ namespace librats {
11
+
12
+ namespace {
13
+
14
+ /// Slices one data message gathers from the caller's list. The send buffer hands
15
+ /// out a length prefix and a body per queued frame, so this covers a run of frames
16
+ /// rather than a single one; a longer backlog than this simply comes back on the
17
+ /// next turn of Connection::flush.
18
+ constexpr size_t kMaxParts = 16;
19
+
20
+ } // namespace
21
+
22
+ Circuit::Circuit(uint32_t id, std::shared_ptr<CircuitCarrier> carrier, bool open,
23
+ uint32_t recv_window, uint32_t peer_window)
24
+ : id_(id),
25
+ carrier_(std::move(carrier)),
26
+ state_(open ? State::Open : State::Pending),
27
+ recv_window_(recv_window == 0 ? kDefaultWindow : recv_window),
28
+ send_credit_(open ? peer_window : 0) {}
29
+
30
+ std::shared_ptr<Circuit> Circuit::opening(uint32_t id, std::shared_ptr<CircuitCarrier> carrier,
31
+ uint32_t recv_window) {
32
+ return std::shared_ptr<Circuit>(
33
+ new Circuit(id, std::move(carrier), /*open=*/false, recv_window, /*peer_window=*/0));
34
+ }
35
+
36
+ std::shared_ptr<Circuit> Circuit::accepted(uint32_t id, std::shared_ptr<CircuitCarrier> carrier,
37
+ uint32_t peer_window, uint32_t recv_window) {
38
+ return std::shared_ptr<Circuit>(
39
+ new Circuit(id, std::move(carrier), /*open=*/true, recv_window, peer_window));
40
+ }
41
+
42
+ // ── Fed by the relay module ─────────────────────────────────────────────────
43
+
44
+ uint32_t Circuit::on_data(ByteView bytes) {
45
+ if (state_ == State::Closed || bytes.empty()) return 0;
46
+
47
+ // The window is the ONE thing bounding what this circuit can cost us and the
48
+ // relay carrying it, so a peer that sends past what it was granted is not
49
+ // merely impolite — it is doing the thing the window exists to prevent. Fail
50
+ // the circuit rather than accept the bytes.
51
+ if (bytes.size() > recv_window_ - recv_in_flight_) {
52
+ LOG_WARN("relay", "Circuit " << id_ << " overran its receive window ("
53
+ << bytes.size() << " B with " << (recv_window_ - recv_in_flight_)
54
+ << " B granted); failing it");
55
+ return on_closed(CloseReason::ProtocolError, /*orderly=*/false);
56
+ }
57
+ recv_in_flight_ += static_cast<uint32_t>(bytes.size());
58
+
59
+ // Hand back the consumed prefix before growing: the connection normally drains
60
+ // the inbox to empty on every readable event, so this is nearly always a reset
61
+ // of an already-empty buffer rather than a move.
62
+ if (inbox_read_ == inbox_.size()) {
63
+ inbox_.clear();
64
+ inbox_read_ = 0;
65
+ } else if (inbox_read_ >= kCompactThreshold) {
66
+ inbox_.erase(inbox_.begin(), inbox_.begin() + static_cast<std::ptrdiff_t>(inbox_read_));
67
+ inbox_read_ = 0;
68
+ }
69
+
70
+ inbox_.insert(inbox_.end(), bytes.begin(), bytes.end());
71
+ return PollIn;
72
+ }
73
+
74
+ uint32_t Circuit::on_accept(uint32_t peer_window) {
75
+ if (state_ != State::Pending) return 0;
76
+ state_ = State::Open;
77
+ send_credit_ = peer_window;
78
+ // Unconditional, unlike the other openings below: the Connection is still in
79
+ // Connecting and has nothing queued, so want_write_ is false — yet this is
80
+ // precisely the event it is waiting for to finish connecting and start its
81
+ // handshake (see Connection::on_writable).
82
+ return PollOut;
83
+ }
84
+
85
+ uint32_t Circuit::on_credit(uint32_t bytes) {
86
+ if (state_ == State::Closed || bytes == 0) return 0;
87
+ // Saturate rather than wrap: a far end that grants nonsense should cost us
88
+ // nothing worse than an over-generous window, and the receiver's own window
89
+ // check is what actually bounds the traffic.
90
+ if (bytes > UINT32_MAX - send_credit_) send_credit_ = UINT32_MAX;
91
+ else send_credit_ += bytes;
92
+
93
+ if (!credit_blocked_) return 0;
94
+ credit_blocked_ = false;
95
+ return writable_event();
96
+ }
97
+
98
+ uint32_t Circuit::on_closed(CloseReason reason, bool orderly) {
99
+ if (state_ == State::Closed) return 0;
100
+ state_ = State::Closed;
101
+ reason_ = reason;
102
+ orderly_ = orderly;
103
+ // An orderly close still owes the application whatever the far end sent before
104
+ // it: read() drains the inbox first and only then reports the end of stream, so
105
+ // what this needs is a readable event, not an error. That is the same contract
106
+ // the other links keep (see Link::read).
107
+ return orderly ? PollIn : PollErr;
108
+ }
109
+
110
+ uint32_t Circuit::on_carrier_writable() {
111
+ if (!carrier_blocked_) return 0;
112
+ carrier_blocked_ = false;
113
+ return writable_event();
114
+ }
115
+
116
+ uint32_t Circuit::writable_event() const noexcept {
117
+ if (blocked() || !want_write_ || state_ != State::Open) return 0;
118
+ return PollOut;
119
+ }
120
+
121
+ // ── Driven by the Connection ────────────────────────────────────────────────
122
+
123
+ Link::IoResult Circuit::read(ByteSpan into) {
124
+ const size_t available = inbox_.size() - inbox_read_;
125
+ const size_t n = (std::min)(available, into.size());
126
+ if (n > 0) {
127
+ std::memcpy(into.data(), inbox_.data() + inbox_read_, n);
128
+ inbox_read_ += n;
129
+ if (inbox_read_ == inbox_.size()) {
130
+ inbox_.clear();
131
+ inbox_read_ = 0;
132
+ }
133
+
134
+ // These bytes are out of the pipe, so the far end may put that much back
135
+ // in. Granted in batches: one small message per half-window rather than one
136
+ // per chunk, which is what keeps the credit scheme's overhead invisible.
137
+ recv_in_flight_ -= static_cast<uint32_t>(n);
138
+ recv_ungranted_ += static_cast<uint32_t>(n);
139
+ if (state_ != State::Closed && recv_ungranted_ >= recv_window_ / kCreditGrantFraction) {
140
+ carrier_->circuit_send_credit(id_, recv_ungranted_);
141
+ recv_ungranted_ = 0;
142
+ }
143
+ return {n, Link::Status::Ok};
144
+ }
145
+
146
+ // Drained. Only now may the end of the stream be reported — data and
147
+ // end-of-stream are never delivered together.
148
+ if (state_ == State::Closed)
149
+ return {0, orderly_ ? Link::Status::Closed : Link::Status::Error};
150
+ return {0, Link::Status::WouldBlock};
151
+ }
152
+
153
+ Link::IoResult Circuit::write(const ByteView* slices, size_t count) {
154
+ if (state_ == State::Closed) return {0, Link::Status::Error};
155
+ // Pending: opened but not yet accepted. Nothing may go out — and nothing tries
156
+ // to, since the Connection is still Connecting.
157
+ if (state_ != State::Open) return {0, Link::Status::WouldBlock};
158
+ if (blocked()) return {0, Link::Status::WouldBlock};
159
+
160
+ const size_t budget = (std::min)(static_cast<size_t>(send_credit_), kMaxDataChunk);
161
+ if (budget == 0) {
162
+ credit_blocked_ = true;
163
+ return {0, Link::Status::WouldBlock};
164
+ }
165
+
166
+ // One data message per call, gathered across as many of the caller's slices as
167
+ // fit. Connection::flush loops while a link keeps reporting Ok, so a backlog
168
+ // larger than one chunk simply comes back around — no loop is needed here, and
169
+ // each turn of that loop re-checks the credit and the carrier.
170
+ ByteView parts[kMaxParts];
171
+ size_t nparts = 0;
172
+ size_t taken = 0;
173
+ for (size_t i = 0; i < count && taken < budget && nparts < kMaxParts; ++i) {
174
+ if (slices[i].empty()) continue;
175
+ const size_t take = (std::min)(slices[i].size(), budget - taken);
176
+ parts[nparts++] = ByteView(slices[i].data(), take);
177
+ taken += take;
178
+ }
179
+ if (taken == 0) return {0, Link::Status::WouldBlock};
180
+
181
+ send_credit_ -= static_cast<uint32_t>(taken);
182
+ // The carrier queues what it is given either way; false is "stop and wait", so
183
+ // the bytes are ours to report as written and the block applies to the NEXT
184
+ // call (see CircuitCarrier::circuit_send_data).
185
+ if (!carrier_->circuit_send_data(id_, parts, nparts)) carrier_blocked_ = true;
186
+ return {taken, Link::Status::Ok};
187
+ }
188
+
189
+ void Circuit::shutdown(CloseReason reason) {
190
+ if (!close_sent_) {
191
+ close_sent_ = true;
192
+ carrier_->circuit_send_close(id_, reason);
193
+ }
194
+ if (state_ != State::Closed) {
195
+ state_ = State::Closed;
196
+ reason_ = reason;
197
+ orderly_ = false;
198
+ }
199
+ }
200
+
201
+ void Circuit::release() {
202
+ if (released_) return;
203
+ released_ = true;
204
+ state_ = State::Closed;
205
+ carrier_->circuit_released(id_);
206
+ }
207
+
208
+ } // namespace librats
@@ -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
@@ -29,6 +29,9 @@
29
29
  #endif
30
30
  #else
31
31
  #include <unistd.h>
32
+ #if defined(__ANDROID__)
33
+ #include <android/log.h>
34
+ #endif
32
35
  #endif
33
36
 
34
37
  namespace librats {
@@ -149,7 +152,24 @@ public:
149
152
  if (level < min_level_.load(std::memory_order_relaxed)) {
150
153
  return;
151
154
  }
152
-
155
+
156
+ #if defined(__ANDROID__)
157
+ // An Android app's stdout/stderr are discarded by default, so the cout/cerr
158
+ // sink below is invisible in logcat. Route console output through the platform
159
+ // logger instead: logcat supplies its own timestamp, level and coloring, so the
160
+ // bare message goes out under the module name as the tag. The file sink is
161
+ // unaffected and stays identical to every other platform.
162
+ if (console_logging_enabled_) {
163
+ __android_log_print(get_android_priority(level),
164
+ module.empty() ? "librats" : module.c_str(),
165
+ "%s", message.c_str());
166
+ }
167
+ if (file_logging_enabled_ && log_file_.is_open()) {
168
+ write_to_file(level, module, message);
169
+ }
170
+ return;
171
+ #endif
172
+
153
173
  // Prepare console output with colors
154
174
  std::ostringstream console_oss;
155
175
 
@@ -239,6 +259,18 @@ private:
239
259
  default: return "UNKNOWN";
240
260
  }
241
261
  }
262
+
263
+ #if defined(__ANDROID__)
264
+ int get_android_priority(LogLevel level) {
265
+ switch (level) {
266
+ case LogLevel::DEBUG: return ANDROID_LOG_DEBUG;
267
+ case LogLevel::INFO: return ANDROID_LOG_INFO;
268
+ case LogLevel::WARN: return ANDROID_LOG_WARN;
269
+ case LogLevel::ERROR: return ANDROID_LOG_ERROR;
270
+ default: return ANDROID_LOG_INFO;
271
+ }
272
+ }
273
+ #endif
242
274
 
243
275
  std::string get_color_code(LogLevel level) {
244
276
  if (!colors_enabled_ || !is_terminal_) return "";
@@ -411,10 +443,9 @@ private:
411
443
  std::string old_name = log_file_path_ + "." + std::to_string(i);
412
444
  std::string new_name = log_file_path_ + "." + std::to_string(i + 1);
413
445
 
414
- // Delete the oldest file if it exists
415
- if (i == max_log_files_ - 1) {
416
- std::remove(new_name.c_str());
417
- }
446
+ // std::rename() does not replace an existing destination on Windows,
447
+ // so clear the target first - not just the oldest file.
448
+ std::remove(new_name.c_str());
418
449
 
419
450
  // Rename old file to new name
420
451
  std::rename(old_name.c_str(), new_name.c_str());
@@ -422,6 +453,7 @@ private:
422
453
 
423
454
  // Move current log file to .1
424
455
  std::string backup_name = log_file_path_ + ".1";
456
+ std::remove(backup_name.c_str());
425
457
  std::rename(log_file_path_.c_str(), backup_name.c_str());
426
458
  }
427
459
 
@@ -25,7 +25,21 @@
25
25
  #include <cerrno>
26
26
  #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
27
27
  defined(__OpenBSD__) || defined(__DragonFly__)
28
+ #ifdef __APPLE__
29
+ #include <TargetConditionals.h>
30
+ #endif
31
+ // Apple ships <net/route.h> in the macOS SDK only. On iOS the PF_ROUTE
32
+ // socket itself is usable but the message declarations are not public, so
33
+ // there is nothing to parse against — those targets take the polling
34
+ // fallback at the bottom of this file (backend_start() returns false).
35
+ // A native backend for iOS belongs on Network.framework's nw_path_monitor
36
+ // rather than on route messages.
37
+ #if !defined(__APPLE__) || (defined(TARGET_OS_OSX) && TARGET_OS_OSX)
28
38
  #define RATS_MONITOR_BSD_ROUTES 1
39
+ #endif
40
+ #endif
41
+
42
+ #if defined(RATS_MONITOR_BSD_ROUTES)
29
43
  #include <sys/types.h>
30
44
  #include <sys/socket.h>
31
45
  #include <net/route.h>
@@ -17,8 +17,17 @@
17
17
  #include <ifaddrs.h>
18
18
  #endif
19
19
 
20
+ #ifdef __APPLE__
21
+ #include <TargetConditionals.h>
22
+ #endif
23
+
20
24
  // macOS / BSD default-gateway lookup via the PF_ROUTE sysctl routing table.
21
- #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
25
+ // Apple ships <net/route.h> in the macOS SDK only: on iOS (and tvOS/watchOS)
26
+ // the sysctl still exists but its declarations are not public, so those
27
+ // targets skip this branch and rely on append_gateway_heuristics() below,
28
+ // which every platform falls back to anyway.
29
+ #if (defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX) || \
30
+ defined(__FreeBSD__) || defined(__NetBSD__) || \
22
31
  defined(__OpenBSD__) || defined(__DragonFly__)
23
32
  #define RATS_HAVE_BSD_ROUTES 1
24
33
  #include <sys/types.h>
@@ -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.3.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",