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.
package/README.md CHANGED
@@ -112,12 +112,20 @@ fails. Both are opt-in, and punching needs peers that relay the rendezvous.
112
112
  ```javascript
113
113
  node.enablePortMapping(); // UPnP IGD + NAT-PMP
114
114
  node.enableHolePunch(true); // true = also relay other peers' rendezvous
115
+ node.enableRelay(false); // last resort; true = also carry others' connections
115
116
  node.start();
116
117
 
117
118
  node.punchPeer(peerId); // success arrives as onPeerConnected
118
119
  console.log(node.natMapping); // NatMapping.ENDPOINT_INDEPENDENT ⇒ punchable
119
120
  ```
120
121
 
122
+ A symmetric NAT cannot be punched at all, and that is what `enableRelay` is for: the
123
+ connection itself is routed through a node both ends already reach. It stays
124
+ encrypted end to end — the relay moves ciphertext — and the peer behaves like any
125
+ other, except that `peerTransport(peerId)` reports `Transport.RELAY`. With both
126
+ enabled the ladder runs itself: a punch that cannot work falls back to a relay, and
127
+ a relayed peer keeps trying to become a direct one.
128
+
121
129
  ## API
122
130
 
123
131
  ### Construction
@@ -170,6 +178,7 @@ new RatsNode(config) // full config
170
178
  | mDNS discovery | `enableMdns()` | — |
171
179
  | NAT port mapping | `enablePortMapping(upnp?, natpmp?)` | — |
172
180
  | Hole punching | `enableHolePunch(serveAsRelay?)` | `punchPeer(peerId)`, `natMapping` |
181
+ | Relaying | `enableRelay(serveAsRelay?)` | `connectViaRelay(peerId)` |
173
182
  | Pub/sub | `enablePubsub()` | `subscribe(topic, cb)`, `unsubscribe(topic)`, `publish(topic, data)` |
174
183
  | Typed JSON | `enableJson()` | `onJson(type, cb)`, `onceJson(type, cb)`, `offJson(type)`, `sendJson(peerId, type, value)`, `broadcastJson(type, value)` |
175
184
  | File transfer | `enableFileTransfer(tempDir?)` | `onFileOffer/onFileProgress/onFileComplete`, `sendFile`, `sendDirectory`, `acceptFile`, `rejectFile`, `cancelFile`, `pauseFile`, `resumeFile` |
package/lib/index.d.ts CHANGED
@@ -19,15 +19,18 @@ declare module 'librats' {
19
19
  };
20
20
 
21
21
  /**
22
- * Which wire a peer connection runs on. Both carry the identical protocol and
23
- * the identical encrypted handshake; they differ only in how the ordered,
24
- * reliable byte stream underneath is obtained.
22
+ * Which wire a peer connection runs on. TCP and UDP carry the identical protocol
23
+ * and the identical encrypted handshake, and differ only in how the ordered,
24
+ * reliable byte stream underneath is obtained; RELAY is that same stream one hop
25
+ * further away, out of another peer's connection rather than out of a socket.
25
26
  */
26
27
  export const Transport: {
27
28
  /** One kernel socket per peer. */
28
29
  readonly TCP: 0;
29
30
  /** Reliable stream over the shared UDP socket. */
30
31
  readonly UDP: 1;
32
+ /** Carried through a third node (see `enableRelay`). */
33
+ readonly RELAY: 2;
31
34
  };
32
35
 
33
36
  /** Bitmask flags used by `node.transports` and `node.peerTransports()`. */
@@ -57,7 +60,10 @@ declare module 'librats' {
57
60
  };
58
61
 
59
62
  export type SecurityValue = 0 | 1;
63
+ /** A wire a dial can choose: TCP or UDP. A relay is never dialed. */
60
64
  export type TransportValue = 0 | 1;
65
+ /** What a connected peer's link actually runs on, relays included. */
66
+ export type PeerTransportValue = TransportValue | 2;
61
67
  export type NatMappingValue = 0 | 1 | 2 | 3;
62
68
  export type LogLevelValue = 0 | 1 | 2 | 3;
63
69
 
@@ -165,7 +171,7 @@ declare module 'librats' {
165
171
  /** Cap on established peers (0 = unlimited). Settable at any time. */
166
172
  maxPeers: number;
167
173
  /** Which wire a connected peer's link runs on, or `null` if not connected. */
168
- peerTransport(peerId: string): TransportValue | null;
174
+ peerTransport(peerId: string): PeerTransportValue | null;
169
175
  /**
170
176
  * Transports a connected peer advertised, as a `TransportMask` bitmask, or
171
177
  * `null` if not connected. 0 means the peer did not say (an older build).
@@ -207,6 +213,17 @@ declare module 'librats' {
207
213
  * ordinary `onPeerConnected`.
208
214
  */
209
215
  punchPeer(peerId: string): void;
216
+ /**
217
+ * Enable relaying: reach a peer nothing else could, through a node both ends
218
+ * are already connected to. `serveAsRelay` (default `false`) also carries
219
+ * OTHER peers' connections, which spends real bandwidth.
220
+ */
221
+ enableRelay(serveAsRelay?: boolean): void;
222
+ /**
223
+ * Try to reach a peer through a relay. Non-blocking: success arrives as an
224
+ * ordinary `onPeerConnected`.
225
+ */
226
+ connectViaRelay(peerId: string): void;
210
227
  /** A `NatMapping` value describing this node's own NAT. */
211
228
  readonly natMapping: NatMappingValue;
212
229
 
package/lib/index.js CHANGED
@@ -59,8 +59,9 @@ const Security = Object.freeze({
59
59
  * how the ordered, reliable byte stream underneath is obtained.
60
60
  */
61
61
  const Transport = Object.freeze({
62
- TCP: 0, // one kernel socket per peer
63
- UDP: 1, // reliable stream over the shared UDP socket
62
+ TCP: 0, // one kernel socket per peer
63
+ UDP: 1, // reliable stream over the shared UDP socket
64
+ RELAY: 2, // carried through a third node (see enableRelay)
64
65
  });
65
66
 
66
67
  /** Bitmask flags used by `node.transports` and `node.peerTransports()`. */
@@ -250,6 +251,29 @@ class RatsNode {
250
251
  */
251
252
  punchPeer(peerId) { this._native.punchPeer(peerId); }
252
253
 
254
+ /**
255
+ * Enable relaying: reach a peer that neither port forwarding nor hole punching
256
+ * could make reachable, by routing the connection through a node both ends are
257
+ * already connected to. The peer that comes out is ordinary in every way — the
258
+ * same end-to-end encryption, the same channels — except that `peerTransport()`
259
+ * reports it as `Transport.RELAY`.
260
+ * @param {boolean} [serveAsRelay=false] also carry OTHER peers' connections.
261
+ * Unlike a hole-punch rendezvous this spends real bandwidth on somebody else's
262
+ * traffic, so it is off by default; a mesh in which nobody serves cannot relay.
263
+ */
264
+ enableRelay(serveAsRelay = false) { this._native.enableRelay(serveAsRelay); }
265
+
266
+ /**
267
+ * Try to reach a peer through a relay. Non-blocking: success arrives as an
268
+ * ordinary `onPeerConnected`. Throws `NO_SUCH_PEER` when there is nothing to do
269
+ * or nothing to try with — already connected, an attempt already running, in
270
+ * cooldown, or no peer that could carry the connection. Usually unnecessary:
271
+ * with hole punching enabled too, a punch that cannot work hands the target over
272
+ * by itself.
273
+ * @param {string} peerId
274
+ */
275
+ connectViaRelay(peerId) { this._native.connectViaRelay(peerId); }
276
+
253
277
  /** @type {number} a {@link NatMapping} value describing this node's own NAT. */
254
278
  get natMapping() { return this._native.natMapping(); }
255
279
 
@@ -206,6 +206,8 @@ set(LIBRARY_SOURCES
206
206
  src/librats/transport/udp_stream.cpp
207
207
  src/librats/transport/udp_mux.h
208
208
  src/librats/transport/udp_mux.cpp
209
+ src/librats/transport/relay_link.h
210
+ src/librats/transport/relay_link.cpp
209
211
  src/librats/transport/connection.h
210
212
  src/librats/transport/connection.cpp
211
213
  src/librats/transport/reactor.h
@@ -242,6 +244,7 @@ set(LIBRARY_SOURCES
242
244
  src/librats/node/dialer.h
243
245
  src/librats/node/dialer.cpp
244
246
  src/librats/node/dial_service.h
247
+ src/librats/node/circuit_service.h
245
248
  src/librats/node/nat_status.h
246
249
  src/librats/node/nat_status.cpp
247
250
 
@@ -268,6 +271,9 @@ set(LIBRARY_SOURCES
268
271
  src/librats/subsystems/hole_punch.h
269
272
  src/librats/subsystems/hole_punch.cpp
270
273
  src/librats/subsystems/hole_punch_service.h
274
+ src/librats/subsystems/relay.h
275
+ src/librats/subsystems/relay.cpp
276
+ src/librats/subsystems/relay_service.h
271
277
 
272
278
  # dht/ — Kademlia DHT + KRPC (bencode is shared with bittorrent, always built)
273
279
  src/librats/dht/dht.h
@@ -649,6 +655,7 @@ if(RATS_BUILD_TESTS)
649
655
  tests/test_frame.cpp
650
656
  tests/test_reactor.cpp
651
657
  tests/test_transport_udp.cpp
658
+ tests/test_relay_link.cpp
652
659
  tests/test_dialer.cpp
653
660
  tests/test_peer_table.cpp
654
661
  tests/test_handshake.cpp
@@ -665,6 +672,7 @@ if(RATS_BUILD_TESTS)
665
672
  tests/test_reconnection.cpp
666
673
  tests/test_nat_status.cpp
667
674
  tests/test_hole_punch.cpp
675
+ tests/test_relay.cpp
668
676
  tests/test_logging.cpp
669
677
  )
670
678
 
@@ -6,6 +6,7 @@
6
6
  #include "librats/subsystems/mdns_discovery.h"
7
7
  #include "librats/subsystems/port_mapping_service.h"
8
8
  #include "librats/subsystems/hole_punch.h"
9
+ #include "librats/subsystems/relay.h"
9
10
  #include "librats/subsystems/pubsub.h"
10
11
  #include "librats/subsystems/message_json.h"
11
12
  #include "librats/subsystems/file_transfer.h"
@@ -42,6 +43,7 @@ struct RatsHandle {
42
43
  PingService* ping = nullptr;
43
44
  ReconnectionService* reconnect = nullptr;
44
45
  HolePunch* punch = nullptr;
46
+ Relay* relay = nullptr;
45
47
  #ifdef RATS_SEARCH_FEATURES
46
48
  Bittorrent* bittorrent = nullptr;
47
49
  #endif
@@ -299,6 +301,29 @@ rats_error_t rats_enable_hole_punch(rats_t node, int serve_as_relay) {
299
301
  return RATS_OK;
300
302
  }
301
303
 
304
+ rats_error_t rats_enable_relay(rats_t node, int serve_as_relay) {
305
+ auto* h = as_handle(node);
306
+ if (h->started) return RATS_ERR_ALREADY_STARTED;
307
+ if (!h->relay) {
308
+ Relay::Config config;
309
+ config.serve = serve_as_relay != 0;
310
+ h->relay = h->node->add_subsystem(std::make_unique<Relay>(config));
311
+ }
312
+ return RATS_OK;
313
+ }
314
+
315
+ rats_error_t rats_connect_via_relay(rats_t node, const char* peer_id_hex) {
316
+ if (!peer_id_hex) return RATS_ERR_INVALID_ARG;
317
+ auto* h = as_handle(node);
318
+ if (!h->relay) return RATS_ERR_NOT_ENABLED;
319
+ auto id = PeerId::from_hex(peer_id_hex);
320
+ if (!id) return RATS_ERR_INVALID_ARG;
321
+ // As with punching, every reason an attempt is declined is "nothing to do or
322
+ // nothing to try with" — already connected, already trying, in cooldown, or no
323
+ // peer that could carry it — and none of them is separately actionable.
324
+ return h->relay->connect_via_relay(*id) ? RATS_OK : RATS_ERR_NO_SUCH_PEER;
325
+ }
326
+
302
327
  rats_error_t rats_punch_peer(rats_t node, const char* peer_id_hex) {
303
328
  if (!peer_id_hex) return RATS_ERR_INVALID_ARG;
304
329
  auto* h = as_handle(node);
@@ -338,9 +363,15 @@ int rats_peer_transport(rats_t node, const char* peer_id_hex) {
338
363
  if (!peer_id_hex) return -1;
339
364
  auto id = PeerId::from_hex(peer_id_hex);
340
365
  if (!id) return -1;
341
- for (const PeerInfo& info : node_of(node)->peers())
342
- if (info.id == *id)
343
- return info.transport == TransportKind::Udp ? RATS_TRANSPORT_UDP : RATS_TRANSPORT_TCP;
366
+ for (const PeerInfo& info : node_of(node)->peers()) {
367
+ if (info.id != *id) continue;
368
+ switch (info.transport) {
369
+ case TransportKind::Udp: return RATS_TRANSPORT_UDP;
370
+ case TransportKind::Relay: return RATS_TRANSPORT_RELAY;
371
+ case TransportKind::Tcp: break;
372
+ }
373
+ return RATS_TRANSPORT_TCP;
374
+ }
344
375
  return -1;
345
376
  }
346
377
 
@@ -45,8 +45,12 @@ typedef enum {
45
45
  * the identical encrypted handshake; they differ only in how the ordered,
46
46
  * reliable byte stream underneath is obtained. */
47
47
  typedef enum {
48
- RATS_TRANSPORT_TCP = 0, /* one kernel socket per peer */
49
- RATS_TRANSPORT_UDP = 1 /* reliable stream over the shared UDP socket */
48
+ RATS_TRANSPORT_TCP = 0, /* one kernel socket per peer */
49
+ RATS_TRANSPORT_UDP = 1, /* reliable stream over the shared UDP socket */
50
+ /* Carried inside another peer's connection (see rats_enable_relay). Reported
51
+ by rats_peer_transport(); never a value to put in preferred_transport,
52
+ which chooses what a DIAL tries and a relay is never dialed. */
53
+ RATS_TRANSPORT_RELAY = 2
50
54
  } rats_transport_t;
51
55
 
52
56
  /* Bitmask of transports (see rats_transports / rats_peer_transports). */
@@ -192,6 +196,30 @@ RATS_API rats_error_t rats_enable_hole_punch(rats_t node, int serve_as_relay);
192
196
  * its own to advertise (it needs at least one datagram peer first). */
193
197
  RATS_API rats_error_t rats_punch_peer(rats_t node, const char* peer_id_hex);
194
198
 
199
+ /** Relaying: reach a peer that neither port forwarding nor hole punching could
200
+ * make reachable, by routing the connection through a node both ends are already
201
+ * connected to. Call before start(). The peer that comes out is ordinary in every
202
+ * way — the same end-to-end encryption, the same channels — except that its bytes
203
+ * take a detour, which rats_peer_transport() reports as RATS_TRANSPORT_RELAY.
204
+ *
205
+ * `serve_as_relay` (non-zero) also carries OTHER peers' connections. Unlike a
206
+ * hole-punch rendezvous, that spends real bandwidth on somebody else's traffic, so
207
+ * it is off by default and opted into here; a mesh in which nobody serves cannot
208
+ * relay at all. A serving node forwards only between peers it already holds, never
209
+ * chains circuits, and caps each one by bytes, duration and count. */
210
+ RATS_API rats_error_t rats_enable_relay(rats_t node, int serve_as_relay);
211
+
212
+ /** Try to reach `peer_id_hex` through a relay. Non-blocking: success arrives as an
213
+ * ordinary peer-connected callback. RATS_OK if an attempt was started;
214
+ * RATS_ERR_NOT_ENABLED if relaying is off; RATS_ERR_NO_SUCH_PEER if there is
215
+ * nothing to do or nothing to try with — the peer is already connected, an attempt
216
+ * is already running, it is in cooldown, or this node has no peer that could carry
217
+ * the connection.
218
+ *
219
+ * Usually there is no need to call this: with hole punching enabled too, a punch
220
+ * that cannot work hands the target over by itself. */
221
+ RATS_API rats_error_t rats_connect_via_relay(rats_t node, const char* peer_id_hex);
222
+
195
223
  /** What the mesh has shown about this node's own NAT, from the endpoints datagram
196
224
  * peers report seeing its shared UDP socket at. One of the RATS_NAT_* values;
197
225
  * RATS_NAT_ENDPOINT_DEPENDENT means punching cannot work from here. */
@@ -33,8 +33,9 @@ const char* to_string(CloseReason r) noexcept {
33
33
 
34
34
  const char* to_string(TransportKind t) noexcept {
35
35
  switch (t) {
36
- case TransportKind::Tcp: return "tcp";
37
- case TransportKind::Udp: return "udp";
36
+ case TransportKind::Tcp: return "tcp";
37
+ case TransportKind::Udp: return "udp";
38
+ case TransportKind::Relay: return "relay";
38
39
  }
39
40
  return "?";
40
41
  }
@@ -26,13 +26,23 @@ enum class ConnRole {
26
26
  Outbound, ///< We dialed out to a remote address.
27
27
  };
28
28
 
29
- /// Which wire a connection runs over. Both are first-class: they carry the exact
30
- /// same block/frame protocol and the same secure handshake, and differ only in how
31
- /// an ordered, reliable byte stream is obtained — the kernel's TCP stack, or the
32
- /// library's own reliability layer on top of datagrams (see transport/udp_stream.h).
29
+ /// Which wire a connection runs over. Tcp and Udp are first-class equals: they
30
+ /// carry the exact same block/frame protocol and the same secure handshake, and
31
+ /// differ only in how an ordered, reliable byte stream is obtained — the kernel's
32
+ /// TCP stack, or the library's own reliability layer on top of datagrams (see
33
+ /// transport/udp_stream.h). Relay is a third way of obtaining that same stream —
34
+ /// out of another peer's connection rather than out of a socket — and is a last
35
+ /// resort rather than an equal (see below).
33
36
  enum class TransportKind {
34
- Tcp, ///< One kernel socket per peer.
35
- Udp, ///< Reliable ordered stream over the shared UDP socket (NAT-friendly).
37
+ Tcp, ///< One kernel socket per peer.
38
+ Udp, ///< Reliable ordered stream over the shared UDP socket (NAT-friendly).
39
+ /// Carried inside another peer's connection: the byte stream is chopped into
40
+ /// messages a third node forwards between the two ends (see subsystems/relay.h).
41
+ /// Not a wire of its own — it is one of the two above, one hop further away —
42
+ /// but it behaves differently enough to be worth naming: it costs the relay
43
+ /// bandwidth, it cannot be dialed, and it must always lose to a direct link
44
+ /// (see PeerTable::add). Never a value connect() or the Dialer chooses.
45
+ Relay,
36
46
  };
37
47
 
38
48
  /// How hard an outbound datagram dial tries before it is called failed.
@@ -0,0 +1,84 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file circuit_service.h
5
+ * @brief Turning a relayed byte stream into a peer connection — the capability a
6
+ * relay module needs and PeerNetwork deliberately does not offer.
7
+ *
8
+ * A relay module speaks a protocol (who forwards what to whom) and owns the byte
9
+ * stream that comes out of it (transport/relay_link.h). What it cannot do on its
10
+ * own is the one remaining step: making that stream a Connection, so the ordinary
11
+ * Noise handshake, framing, identify and peer table all run over it and the peer
12
+ * at the far end becomes an ordinary peer. That step needs the reactor pool, which
13
+ * a Subsystem has no business holding.
14
+ *
15
+ * So this is the narrow escape hatch, published by the Node in its ServiceRegistry
16
+ * next to DialService, and resolved by whoever needs it:
17
+ *
18
+ * if (auto* circuits = ctx.services.get<CircuitService>())
19
+ * auto route = circuits->adopt_circuit(relay_id, std::move(link),
20
+ * ConnRole::Outbound, false);
21
+ *
22
+ * ── One thread, chosen for you ──────────────────────────────────────────────
23
+ * The node places the circuit on the reactor that owns the CARRIER's connection,
24
+ * and that is the whole reason this call exists rather than a bare "adopt this
25
+ * link somewhere". Every byte of a circuit arrives on the carrier's connection and
26
+ * leaves through it, so putting the two on one thread means the entire relayed
27
+ * path is handled without a lock, a queue or a hand-off — the same shared-nothing
28
+ * property every other connection has. Any other placement would move bytes
29
+ * between reactor threads twice per message.
30
+ *
31
+ * The route comes back synchronously (the ConnId is reserved before the adoption
32
+ * is posted, exactly as Reactor::connect does), so the caller can wake and close
33
+ * the circuit from the moment it asks for one.
34
+ *
35
+ * ── What is deliberately NOT here ───────────────────────────────────────────
36
+ * Nothing about relaying: no relay selection, no protocol, no policy about when a
37
+ * circuit is worth opening. Those belong to the module. This interface would serve
38
+ * any transport that arrives from outside the reactor pool.
39
+ */
40
+
41
+ #include "librats/core/types.h"
42
+ #include "librats/peer/peer_id.h"
43
+ #include "librats/peer/peer_table.h" // PeerRoute
44
+ #include "librats/transport/link.h"
45
+
46
+ #include <cstdint>
47
+ #include <memory>
48
+ #include <optional>
49
+
50
+ namespace librats {
51
+
52
+ class CircuitService {
53
+ public:
54
+ virtual ~CircuitService() = default;
55
+
56
+ /// Adopt `link` as a connection carried by the peer `carrier`, on the reactor
57
+ /// that owns the carrier's own connection.
58
+ ///
59
+ /// @param role Outbound for a circuit we opened, Inbound for one opened to
60
+ /// us. It is what the secure handshake and the peer table's
61
+ /// duplicate resolution read, so it must say who asked.
62
+ /// @param connected whether the far end is already there. An inbound circuit is
63
+ /// (the opener would not have sent it otherwise); an outbound
64
+ /// one is not until its acceptance arrives, and until then the
65
+ /// connection waits in Connecting for a PollOut.
66
+ /// @return the new connection's route, or nullopt when the carrier is no longer
67
+ /// a peer, or when an inbound circuit would push this node past its peer
68
+ /// limit. Nothing is started in that case and `link` is released.
69
+ virtual std::optional<PeerRoute> adopt_circuit(const PeerId& carrier,
70
+ std::unique_ptr<Link> link,
71
+ ConnRole role, bool connected) = 0;
72
+
73
+ /// Deliver poll-equivalent events (PollIn / PollOut / PollErr, see
74
+ /// core/io_poller.h) to a circuit connection — the events a socket-backed link
75
+ /// would have got from the poller. Thread-safe, and a no-op for a route that is
76
+ /// already gone.
77
+ virtual void wake_circuit(PeerRoute route, uint32_t events) = 0;
78
+
79
+ /// Tear a circuit connection down. Thread-safe; a no-op for a route that is
80
+ /// already gone.
81
+ virtual void close_circuit(PeerRoute route, CloseReason reason) = 0;
82
+ };
83
+
84
+ } // namespace librats
@@ -94,6 +94,7 @@ Node::Node(NodeConfig config)
94
94
  // are narrow capabilities a module asks for by interface (see
95
95
  // node/dial_service.h, node/nat_status.h).
96
96
  services_.provide<DialService>(this);
97
+ services_.provide<CircuitService>(this);
97
98
  services_.provide<ExternalAddressService>(&nat_status_);
98
99
  }
99
100
 
@@ -402,6 +403,9 @@ bool Node::dial_direct(const Address& addr, TransportKind kind, const DialProfil
402
403
  if (!addr.is_valid()) return false;
403
404
  if (kind == TransportKind::Udp && !reactors_->has_udp()) return false;
404
405
  if (kind == TransportKind::Tcp && !config_.enable_tcp) return false;
406
+ // A relayed circuit is not dialed: it is negotiated with a third node and then
407
+ // adopted (see node/circuit_service.h). There is no address to aim at here.
408
+ if (kind == TransportKind::Relay) return false;
405
409
 
406
410
  // Straight to the reactor, deliberately around the Dialer: this dial is not a
407
411
  // race and must not become one (see node/dial_service.h). The Dialer is left
@@ -412,6 +416,41 @@ bool Node::dial_direct(const Address& addr, TransportKind kind, const DialProfil
412
416
  return id != kInvalidConnId;
413
417
  }
414
418
 
419
+ // ── CircuitService: a relayed byte stream becomes a peer connection ─────────
420
+
421
+ std::optional<PeerRoute> Node::adopt_circuit(const PeerId& carrier, std::unique_ptr<Link> link,
422
+ ConnRole role, bool connected) {
423
+ if (!link) return std::nullopt;
424
+
425
+ // The carrier has to still be a peer: it is the only thing this circuit's bytes
426
+ // can travel through, and a connection whose carrier is already gone would
427
+ // simply sit there until the establish deadline reaped it.
428
+ const auto carrier_route = peers_.route(carrier);
429
+ if (!carrier_route) return std::nullopt;
430
+
431
+ // Inbound circuits go through the same admission gate as an accepted socket.
432
+ // A relayed connection is cheaper for whoever opens it than a direct one — no
433
+ // NAT to get through, someone else's bandwidth — so if anything it deserves the
434
+ // gate more, not less.
435
+ if (role == ConnRole::Inbound && !admit_inbound()) return std::nullopt;
436
+
437
+ Reactor& reactor = reactors_->by_index(carrier_route->reactor);
438
+ // Same reactor as the carrier, which is the whole point: every byte of this
439
+ // circuit arrives on the carrier's connection and leaves through it, so the two
440
+ // share a thread and the relayed path needs no locks and no hand-offs.
441
+ const ConnId id = reactor.reserve_conn_id();
442
+ reactor.adopt_link(id, std::move(link), role, connected);
443
+ return PeerRoute{carrier_route->reactor, id};
444
+ }
445
+
446
+ void Node::wake_circuit(PeerRoute route, uint32_t events) {
447
+ reactors_->by_index(route.reactor).wake(route.conn, events);
448
+ }
449
+
450
+ void Node::close_circuit(PeerRoute route, CloseReason reason) {
451
+ reactors_->by_index(route.reactor).close(route.conn, reason);
452
+ }
453
+
415
454
  void Node::report_dial_failed(const std::string& host, uint16_t port) {
416
455
  // Only a numeric target can be handed on as an Address; a dial by hostname has
417
456
  // no dialable Address to report (peers cannot use our resolver), and the
@@ -48,6 +48,7 @@
48
48
  #include "librats/peer/peer_info.h"
49
49
  #include "librats/security/identity.h"
50
50
  #include "librats/security/handshaker.h" // SecurityProvider
51
+ #include "librats/node/circuit_service.h"
51
52
  #include "librats/node/config.h"
52
53
  #include "librats/node/dial_service.h"
53
54
  #include "librats/node/dialer.h"
@@ -74,7 +75,10 @@ namespace librats {
74
75
  class NetworkMonitor; // util/network_monitor.h — owned via unique_ptr, included in node.cpp
75
76
  class MessageJson; // subsystems/message_json.h — reached via json() (json.h stays out of node.h)
76
77
 
77
- class RATS_API Node final : public ConnectionDelegate, public PeerNetwork, public DialService {
78
+ class RATS_API Node final : public ConnectionDelegate,
79
+ public PeerNetwork,
80
+ public DialService,
81
+ public CircuitService {
78
82
  public:
79
83
  /// Construct a node from its configuration (see NodeConfig). This only loads
80
84
  /// the identity and prepares the layers; no socket is opened until start().
@@ -163,6 +167,13 @@ public:
163
167
  bool dial_direct(const Address& addr, TransportKind kind,
164
168
  const DialProfile& profile) override;
165
169
 
170
+ // — CircuitService: make a relayed byte stream an ordinary peer connection
171
+ // (see node/circuit_service.h; used by the relay module) —
172
+ std::optional<PeerRoute> adopt_circuit(const PeerId& carrier, std::unique_ptr<Link> link,
173
+ ConnRole role, bool connected) override;
174
+ void wake_circuit(PeerRoute route, uint32_t events) override;
175
+ void close_circuit(PeerRoute route, CloseReason reason) override;
176
+
166
177
  // — peer admission limit (0 = unlimited; guards inbound, not our own dials) —
167
178
  size_t max_peers() const noexcept { return max_peers_.load(std::memory_order_relaxed); }
168
179
  void set_max_peers(size_t n) noexcept { max_peers_.store(n, std::memory_order_relaxed); }
@@ -5,6 +5,30 @@
5
5
 
6
6
  namespace librats {
7
7
 
8
+ namespace {
9
+
10
+ /// How much a transport is worth when two connections to the same peer have to be
11
+ /// ranked. Both ends see the same pair of transports and apply the same order, so
12
+ /// they converge on the same survivor without exchanging a word.
13
+ ///
14
+ /// Udp — one socket, one NAT mapping, a source port that can be dialed back
15
+ /// (see dialer.h). The best link there is.
16
+ /// Tcp — a direct link all the same, just a less useful one to a NAT.
17
+ /// Relay — not a direct link at all: every byte costs a third node bandwidth and
18
+ /// a round trip. Ranked last here for completeness; in practice a relayed
19
+ /// link is settled before this by the rule in add(), which lets ANY direct
20
+ /// link beat it regardless of role.
21
+ int transport_rank(TransportKind t) noexcept {
22
+ switch (t) {
23
+ case TransportKind::Udp: return 2;
24
+ case TransportKind::Tcp: return 1;
25
+ case TransportKind::Relay: return 0;
26
+ }
27
+ return 0;
28
+ }
29
+
30
+ } // namespace
31
+
8
32
  PeerTable::AddOutcome PeerTable::add(const PeerInfo& info, PeerRoute route,
9
33
  bool prefer_outbound) {
10
34
  std::unique_lock<std::shared_mutex> lock(mutex_);
@@ -26,23 +50,32 @@ PeerTable::AddOutcome PeerTable::add(const PeerInfo& info, PeerRoute route,
26
50
  // first" gets a different answer at each end, and two ends that each keep the
27
51
  // link the other just dropped are left with no link at all.
28
52
  //
53
+ // one relayed, one not ⇒ not a race at all but an upgrade (or a late relay
54
+ // arriving under a direct link): the direct one wins,
55
+ // whatever the roles. Ranked FIRST, ahead of the role
56
+ // rule, because a relayed link that won on role would
57
+ // keep costing a third node bandwidth for the life of a
58
+ // peer we can reach ourselves. Both ends see the same
59
+ // pair of transports, so both reach this same verdict.
29
60
  // opposite roles ⇒ cross-connect: keep the link started by the smaller id
30
61
  // (prefer_outbound, opposite at each end by construction).
31
62
  // different wires ⇒ a dial race whose attempts both got through. Both ends
32
- // see the transports, and there is a right answer: the
33
- // datagram link (one socket, one NAT mapping, a source
34
- // port that can be dialed back — see dialer.h). "Is it
35
- // UDP" ranks the pair only because there are exactly two
36
- // wires; a third would need an explicit order, or the two
37
- // ends could rank the same pair differently.
63
+ // see the transports and rank them by the same explicit
64
+ // order (transport_rank above), so neither can rank the
65
+ // same pair differently.
38
66
  // same role and wire ⇒ a reconnect, not a simultaneous pair: the old link is
39
67
  // stale or already dead, so the newcomer is the live one.
68
+ const bool cur_relayed = cur.info.transport == TransportKind::Relay;
69
+ const bool new_relayed = info.transport == TransportKind::Relay;
70
+
40
71
  bool keep_new = true;
41
- if (cur.info.direction != info.direction) {
72
+ if (cur_relayed != new_relayed) {
73
+ keep_new = !new_relayed;
74
+ } else if (cur.info.direction != info.direction) {
42
75
  const ConnRole survivor = prefer_outbound ? ConnRole::Outbound : ConnRole::Inbound;
43
76
  keep_new = (info.direction == survivor);
44
77
  } else if (cur.info.transport != info.transport) {
45
- keep_new = (info.transport == TransportKind::Udp);
78
+ keep_new = transport_rank(info.transport) > transport_rank(cur.info.transport);
46
79
  }
47
80
 
48
81
  if (keep_new) {