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,211 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file relay.h
5
+ * @brief Reaching a peer through a node both ends can already reach.
6
+ *
7
+ * The last rung of the connectivity ladder. A direct dial handles the easy cases;
8
+ * PortMappingService and HolePunch handle most of the rest. What is left is the
9
+ * pair of nodes for which no endpoint either one can advertise will ever work —
10
+ * a symmetric NAT on one side, a network that drops UDP and blocks inbound TCP,
11
+ * a punch that simply never lands. For them the only way through is to borrow a
12
+ * path: a node connected to both carries the bytes.
13
+ *
14
+ * ── What is relayed, and what is not ────────────────────────────────────────
15
+ * The relayed thing is a *byte stream*, not a message. It becomes a `Circuit` and
16
+ * a `RelayLink` (transport/relay_link.h), the reactor adopts it as an ordinary
17
+ * Connection, and everything above the Link runs unchanged — which is why:
18
+ *
19
+ * - the Noise_XX handshake is END TO END. The relay moves ciphertext it cannot
20
+ * read, cannot forge and cannot replay. It is a pipe, not a party, and it
21
+ * learns nothing beyond who is talking to whom and how much;
22
+ * - the peers authenticate each other's real keys, so a relay cannot substitute
23
+ * itself for either end;
24
+ * - every subsystem — pub/sub, file transfer, PEX — works over a relayed peer
25
+ * with no code of its own. To the application it is simply a peer, and
26
+ * PeerInfo::transport is the only thing that says otherwise.
27
+ *
28
+ * ── Finding a relay ─────────────────────────────────────────────────────────
29
+ * This stage covers the case where the two ends share a peer — the same topology
30
+ * HolePunch already relies on for its rendezvous, and the one a bootstrap or DHT
31
+ * mesh produces on its own. The initiator asks a few of its peers "do you hold
32
+ * this id?" (Probe) and opens a circuit with the first that says yes. Probing
33
+ * first, rather than opening with everybody at once, is what keeps a successful
34
+ * search from producing three redundant connections to the same peer — each with
35
+ * its own handshake — for the peer table to then tear two of down.
36
+ *
37
+ * Not covered here, deliberately: reaching a peer with whom we share NO peer. That
38
+ * needs reservations (a node asking a relay to hold a slot for it) and a way to
39
+ * advertise "reachable via R" as an address, which touches identify and PEX. The
40
+ * version byte and the free op-codes in the wire format leave room for it.
41
+ *
42
+ * ── Getting off the relay again ─────────────────────────────────────────────
43
+ * A circuit is a fallback, not a destination: it costs a third node bandwidth and
44
+ * a round trip. So when one comes up, this module asks HolePunch to try the target
45
+ * again — now that the two ends are peers, they can exchange what a punch needs
46
+ * over the very circuit that is carrying them. If the punch lands, PeerTable::add
47
+ * prefers the direct link at BOTH ends (any direct transport outranks Relay) and
48
+ * swaps the route with no disconnect event: the application never sees the seam.
49
+ *
50
+ * ── Carrying other peers' traffic ───────────────────────────────────────────
51
+ * Serving as a relay is off by default. Forwarding a rendezvous, as HolePunch
52
+ * does, is a few dozen bytes; forwarding a connection is somebody else's file
53
+ * transfer on your uplink, and that is a decision to be made rather than assumed.
54
+ * A node that turns it on is protected by, in order of how much they matter:
55
+ *
56
+ * - the end-to-end credit window (transport/relay_link.h), which bounds what one
57
+ * circuit can make the relay hold no matter what either end does. Without it a
58
+ * slow receiver would grow the relay's send queue until the relay dropped that
59
+ * peer entirely — losing all of its traffic, not just the circuit's;
60
+ * - forwarding only between peers it already holds. A relay never dials, never
61
+ * resolves an address, and can only ever deliver to somebody that chose to
62
+ * connect to it, so it cannot be turned into an open reflector;
63
+ * - no chaining. A circuit is refused if either end is itself only reachable
64
+ * through a relay, which is what stops loops and multi-hop amplification;
65
+ * - per-circuit byte and duration caps, per-peer and total circuit counts, and a
66
+ * rate limit on the requests themselves.
67
+ *
68
+ * ── Threading ───────────────────────────────────────────────────────────────
69
+ * Message handlers run on reactor threads; one worker thread drives attempt
70
+ * timeouts and the relay's own expiry. The tables are behind one mutex, which is
71
+ * never held across a call into a circuit or the reactor — a circuit is touched
72
+ * only by the reactor thread that owns its connection, which is by construction
73
+ * the thread that owns its carrier's (see node/circuit_service.h).
74
+ */
75
+
76
+ #include "librats/util/rats_export.h"
77
+ #include "librats/core/service_registry.h"
78
+ #include "librats/node/peer_network.h"
79
+ #include "librats/peer/peer_id.h"
80
+ #include "librats/subsystems/relay_service.h"
81
+ #include "librats/transport/relay_link.h" // Circuit::kDefaultWindow
82
+
83
+ #include <atomic>
84
+ #include <chrono>
85
+ #include <condition_variable>
86
+ #include <cstdint>
87
+ #include <memory>
88
+ #include <mutex>
89
+ #include <thread>
90
+
91
+ namespace librats {
92
+
93
+ class RATS_API Relay final : public Subsystem, public RelayService {
94
+ public:
95
+ struct Config {
96
+ // ── Using relays to reach peers we cannot dial ──────────────────────
97
+
98
+ /// Open circuits through other peers. Off makes this a relay-only node.
99
+ bool enable_client = true;
100
+
101
+ /// Peers asked whether they hold a target, per attempt. Small: each probe
102
+ /// is a question to somebody who probably cannot help, and one useful
103
+ /// answer is all an attempt needs.
104
+ size_t max_probes = 3;
105
+
106
+ /// Circuits this node may hold open as a client at once. Bounds both memory
107
+ /// and how many relayed peers a discovery module can talk us into.
108
+ size_t max_outbound_circuits = 8;
109
+
110
+ /// How long an attempt may take from the first probe to a live connection.
111
+ /// Covers the probe round trip, the circuit setup, and the handshake across
112
+ /// two hops.
113
+ std::chrono::milliseconds open_timeout{8000};
114
+
115
+ /// How long after a failed attempt before the same target may be tried
116
+ /// again. Only an attempt WE started ever ends in one.
117
+ std::chrono::milliseconds cooldown{60000};
118
+
119
+ // ── Accepting circuits opened to us ─────────────────────────────────
120
+
121
+ /// Accept circuits other peers open toward us. Off makes this node
122
+ /// unreachable by relay, which is a choice a well-connected node can make.
123
+ bool accept_inbound = true;
124
+
125
+ /// Circuits opened TO us that may be live at once.
126
+ size_t max_inbound_circuits = 16;
127
+
128
+ // ── Carrying other peers' traffic ───────────────────────────────────
129
+
130
+ /// Serve as a relay. OFF by default: unlike a hole-punch rendezvous, this
131
+ /// spends real bandwidth on somebody else's connection, so it is opted into
132
+ /// rather than assumed. A mesh in which nobody serves cannot relay at all.
133
+ bool serve = false;
134
+
135
+ /// Circuits this node will carry for others at once.
136
+ size_t max_circuits = 32;
137
+
138
+ /// Circuits one peer may have us carrying, so a single peer cannot take the
139
+ /// whole budget.
140
+ size_t max_circuits_per_peer = 2;
141
+
142
+ /// Bytes one circuit may move before it is closed. 0 removes the cap. The
143
+ /// default is generous for a fallback path and still bounds what one pair
144
+ /// of peers can spend of ours.
145
+ uint64_t max_bytes_per_circuit = 64ull * 1024 * 1024;
146
+
147
+ /// How long one circuit may live. 0 removes the cap.
148
+ std::chrono::seconds max_circuit_duration{600};
149
+
150
+ /// Requests (probes and circuit openings) one peer may make per window.
151
+ size_t request_budget = 8;
152
+ std::chrono::milliseconds request_window{1000};
153
+
154
+ // ── Both ends ───────────────────────────────────────────────────────
155
+
156
+ /// Bytes this node is willing to have in flight per circuit — the credit
157
+ /// window it advertises (see transport/relay_link.h). Raising it makes a
158
+ /// relayed peer faster on a long path and asks the relay to hold more.
159
+ uint32_t window = Circuit::kDefaultWindow;
160
+
161
+ /// When a relayed peer connects, ask HolePunch to try it directly, so the
162
+ /// circuit is replaced by a direct link as soon as one is possible. Costs
163
+ /// nothing when no HolePunch is attached.
164
+ bool upgrade_with_hole_punch = true;
165
+
166
+ /// Bookkeeping cadence — attempt timeouts, cooldowns, circuit expiry.
167
+ std::chrono::milliseconds tick{250};
168
+ };
169
+
170
+ Relay();
171
+ explicit Relay(Config config);
172
+ ~Relay() override;
173
+
174
+ void attach(NodeContext& ctx) override;
175
+ void start() override;
176
+ void stop() override;
177
+
178
+ /// @copydoc RelayService::connect_via_relay
179
+ bool connect_via_relay(const PeerId& target) override;
180
+
181
+ // — diagnostics and tests —
182
+ /// Circuits this node terminates (relayed peers, live or still being set up).
183
+ size_t circuits() const;
184
+ /// Circuits this node is carrying on behalf of other peers.
185
+ size_t carried_circuits() const;
186
+ /// Bytes forwarded on behalf of other peers.
187
+ uint64_t carried_bytes() const;
188
+ /// Attempts to reach a peer through a relay that are currently running.
189
+ size_t attempts() const;
190
+
191
+ private:
192
+ /// Everything mutable, held by shared_ptr because a Connection — and therefore
193
+ /// the Link that reports its circuit released — can outlive this subsystem: the
194
+ /// node stops subsystems before it stops the reactors that own the connections.
195
+ struct State;
196
+
197
+ Config config_;
198
+ std::shared_ptr<State> state_;
199
+ /// Kept from attach() so start() can resolve HolePunchService — which may be
200
+ /// attached after us, and is only guaranteed to have registered by then.
201
+ ServiceRegistry* services_ = nullptr;
202
+
203
+ std::thread worker_;
204
+ std::atomic<bool> running_{false};
205
+ std::mutex worker_mutex_;
206
+ std::condition_variable worker_cv_;
207
+
208
+ void loop();
209
+ };
210
+
211
+ } // namespace librats
@@ -0,0 +1,46 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file relay_service.h
5
+ * @brief Capability that lets a sibling module ask for a peer to be reached
6
+ * through a third node, by PeerId.
7
+ *
8
+ * Published by Relay via ServiceRegistry (see service_registry.h). It is the last
9
+ * rung of the connectivity ladder — a direct dial, then a hole punch, then this —
10
+ * and the module that gives up on the rung above should not have to know anything
11
+ * about how the rung below works. HolePunch, for instance, learns that a target is
12
+ * unreachable (a symmetric NAT it declined to punch, or a rendezvous that ran out
13
+ * of attempts) and this is the whole contract it needs to hand the id on: no relay
14
+ * selection, no circuits, no policy, all of which are Relay's own business.
15
+ *
16
+ * Resolving it returns nullptr when relaying is not enabled, which is exactly what
17
+ * makes the fallback optional at the consumer's side:
18
+ *
19
+ * if (auto* relay = ctx.services.get<RelayService>()) relay->connect_via_relay(id);
20
+ *
21
+ * The provider registers during attach(), so resolve it in start() (every attach()
22
+ * runs before any start()) rather than in your own attach(), where the provider may
23
+ * not have been attached yet. The pointer is NON-owning and valid while the node is.
24
+ */
25
+
26
+ #include "librats/util/rats_export.h"
27
+ #include "librats/peer/peer_id.h"
28
+
29
+ namespace librats {
30
+
31
+ struct RATS_API RelayService {
32
+ virtual ~RelayService() = default;
33
+
34
+ /// Try to reach `target` through a node both ends are connected to.
35
+ /// Non-blocking; success surfaces as an ordinary peer-connected event, and the
36
+ /// resulting peer is ordinary in every way except that its bytes take a detour
37
+ /// (PeerInfo::transport is TransportKind::Relay).
38
+ ///
39
+ /// @return whether an attempt actually started — false is routine (already
40
+ /// connected, an attempt is already running, the target is in cooldown,
41
+ /// or this node has no peer that could carry the circuit) and needs no
42
+ /// handling by the caller.
43
+ virtual bool connect_via_relay(const PeerId& target) = 0;
44
+ };
45
+
46
+ } // namespace librats
@@ -150,6 +150,27 @@ void Reactor::start_dial(ConnId id, const std::string& host, int port, Transport
150
150
  conn->set_dial_address(host, static_cast<uint16_t>(port));
151
151
  }
152
152
 
153
+ void Reactor::adopt_link(ConnId id, std::unique_ptr<Link> link, ConnRole role, bool connected) {
154
+ // A unique_ptr cannot be captured by a std::function, which is what the task
155
+ // queue holds — so the link travels in a shared holder it is moved out of on
156
+ // arrival. If the task is never run (a reactor stopped under us), the holder
157
+ // dies with it and takes the link with it, which is the right outcome.
158
+ auto holder = std::make_shared<std::unique_ptr<Link>>(std::move(link));
159
+ execute([this, id, holder, role, connected] {
160
+ Connection* conn = adopt(std::move(*holder), role, id);
161
+ if (!conn) return;
162
+ // Already-connected links skip the Connecting state entirely, exactly as an
163
+ // accepted socket does; the rest wait for the PollOut that wake() will bring
164
+ // once the far end answers.
165
+ if (connected) conn->start_handshake();
166
+ });
167
+ }
168
+
169
+ void Reactor::wake(ConnId id, uint32_t events) {
170
+ if (events == 0) return;
171
+ execute([this, id, events] { dispatch_events(id, events); });
172
+ }
173
+
153
174
  void Reactor::abort_dial(ConnId id, const std::string& host, int port) {
154
175
  resolve_dial(id); // the id is spent either way; release any cancellation slot
155
176
  delegate_.on_dial_aborted(index_, id, host, static_cast<uint16_t>(port));
@@ -243,6 +264,21 @@ void Reactor::run() {
243
264
  if (mux_) timeout = mux_->next_timeout_ms(timeout);
244
265
  const int n = poller_->wait(events, kMaxEvents, timeout);
245
266
 
267
+ // Empty the wakeup pipe BEFORE taking the task snapshot, never after.
268
+ //
269
+ // post() pushes a task and then writes a byte, and those two arrive here as
270
+ // two separate facts. Draining last would let a byte written *while this
271
+ // turn was running its tasks* be thrown away along with the ones that
272
+ // brought us here — and the task it belonged to was pushed after the
273
+ // snapshot, so it does not run either. It would then sit in the queue with
274
+ // nothing left to announce it, until the next poll timeout woke the loop
275
+ // for some other reason: up to kMaxPollMs of latency, at random.
276
+ //
277
+ // Draining first inverts that. A byte written any time after this line is
278
+ // still in the pipe when the loop comes back round, so wait() returns at
279
+ // once — whether or not its task made this turn's snapshot. The cost is one
280
+ // recv per turn, which is nothing against the work a turn does.
281
+ drain_wakeup();
246
282
  drain_tasks(task_batch); // connect/close/send-arm
247
283
  for (int i = 0; i < n; ++i) handle_event(events[i]);
248
284
  timers_.run_due();
@@ -325,7 +361,9 @@ void Reactor::drain_wakeup() {
325
361
  void Reactor::handle_event(const PollResult& ev) {
326
362
  const socket_t fd = ev.fd;
327
363
 
328
- if (fd == wakeup_.fd()) { drain_wakeup(); return; }
364
+ // Already emptied at the top of the turn — see run(). Nothing to do but
365
+ // recognise it, so it is not mistaken for a connection's socket.
366
+ if (fd == wakeup_.fd()) return;
329
367
  if (fd == server_socket_) { if (ev.events & PollIn) do_accept(); return; }
330
368
  if (mux_ && fd == mux_->socket()) {
331
369
  // Any event, not just PollIn. An error on a datagram socket belongs to one
@@ -479,16 +517,26 @@ void Reactor::mark_for_close(ConnId id, CloseReason reason) {
479
517
  }
480
518
 
481
519
  void Reactor::flush_dirty() {
482
- if (dirty_.empty()) return;
483
520
  // Swapped out first: a flush can close a connection, and a close can queue
484
521
  // further work — none of which should extend the batch being walked.
522
+ //
523
+ // Looped, because a flush can also *book another one*: writing a relayed
524
+ // circuit hands its bytes to the connection carrying it (see
525
+ // transport/relay_link.h), which books its own flush from inside this one. A
526
+ // single pass would leave those bytes queued until the next turn of the loop —
527
+ // i.e. behind a poll wait — adding tens of milliseconds to every hop of a
528
+ // relayed path. The bound is a backstop against a pathological chain, not an
529
+ // expected limit: honest work needs two passes at most.
485
530
  std::vector<ConnId> batch;
486
- batch.swap(dirty_);
487
- for (const ConnId id : batch) {
488
- Connection* conn = find(id);
489
- if (!conn) continue; // gone since it booked
490
- if (conn->state() != ConnState::Established) continue; // closing; nothing to owe
491
- if (!conn->flush_pending()) mark_for_close(id, conn->close_reason());
531
+ for (int pass = 0; pass < kMaxFlushPasses && !dirty_.empty(); ++pass) {
532
+ batch.clear();
533
+ batch.swap(dirty_);
534
+ for (const ConnId id : batch) {
535
+ Connection* conn = find(id);
536
+ if (!conn) continue; // gone since it booked
537
+ if (conn->state() != ConnState::Established) continue; // closing; nothing to owe
538
+ if (!conn->flush_pending()) mark_for_close(id, conn->close_reason());
539
+ }
492
540
  }
493
541
  }
494
542
 
@@ -104,6 +104,41 @@ public:
104
104
  /// Valid for an id whose dial has not started yet — it is cancelled instead.
105
105
  void close(ConnId id, CloseReason reason);
106
106
 
107
+ // — links this reactor did not open itself ──────────────────────────────
108
+ //
109
+ // A relayed circuit is a byte stream that comes out of another peer's
110
+ // connection rather than out of a socket (see transport/relay_link.h), so the
111
+ // module that speaks the relay protocol builds the Link and hands it over.
112
+ // These three are that hand-over, and they are deliberately the whole of it:
113
+ // the reactor learns nothing about relaying, and the module learns nothing
114
+ // about reactors beyond "connections need to be woken".
115
+ //
116
+ // The circuit MUST be given to the reactor that owns its carrier's connection.
117
+ // That is what keeps every byte of it on one thread, exactly like the streams
118
+ // the mux hands over above, and it is the caller's job to arrange (see
119
+ // node/circuit_service.h).
120
+
121
+ /// Reserve a ConnId without starting anything. Thread-safe, and synchronous for
122
+ /// the same reason connect() is: the caller has to be able to name the
123
+ /// connection — to wake it, to close it — from the moment it asks for one,
124
+ /// rather than after a task it posted has run.
125
+ ConnId reserve_conn_id() noexcept {
126
+ return next_conn_id_.fetch_add(1, std::memory_order_relaxed);
127
+ }
128
+
129
+ /// Adopt an already-built Link under the id `reserve_conn_id()` handed out.
130
+ /// Thread-safe; the adoption itself runs on the reactor thread (inline when the
131
+ /// caller is already on it).
132
+ /// @param connected whether the far end is already there. Such a link starts its
133
+ /// handshake at once, like an accepted socket; one that is not waits for
134
+ /// a PollOut from wake(), exactly as an outbound connect does.
135
+ void adopt_link(ConnId id, std::unique_ptr<Link> link, ConnRole role, bool connected);
136
+
137
+ /// Deliver poll-equivalent events (PollIn/PollOut/PollErr) to a connection whose
138
+ /// Link has no socket for the poller to watch. Thread-safe. A no-op for an id
139
+ /// that is gone, so a late wake for a torn-down circuit costs nothing.
140
+ void wake(ConnId id, uint32_t events);
141
+
107
142
  /// Drop a dial that lost its race — but only while it is still a dial. Once an
108
143
  /// attempt has established it is not an attempt any more, it is one of possibly
109
144
  /// several links to a peer, and which of those survives is the peer table's
@@ -175,6 +210,9 @@ private:
175
210
  // Negligible idle cost; on epoll/kqueue the loop still wakes on real events,
176
211
  // so this only caps idle latency.
177
212
  static constexpr int kMaxPollMs = 50;
213
+ /// Passes flush_dirty() makes over the connections booked for a write. More than
214
+ /// one because a write can book another connection's write — see flush_dirty().
215
+ static constexpr int kMaxFlushPasses = 4;
178
216
  /// Deadline from adopt() to reaching Established (covers connect + handshake).
179
217
  static constexpr std::chrono::milliseconds kEstablishTimeout{15000};
180
218
  /// Cadence of the housekeeping sweep over this reactor's connections (currently
@@ -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