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
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
 
@@ -85,9 +91,23 @@ declare module 'librats' {
85
91
  preferredTransport?: TransportValue;
86
92
  /** Delay before the other transport is raced alongside; 0 disables. Default 1200. */
87
93
  transportFallbackMs?: number;
94
+ /**
95
+ * Bytes a peer's send queue may hold before an app that keeps sending anyway
96
+ * has that peer dropped with reason `RATS_CLOSE_SLOW_CONSUMER`. 0 (default)
97
+ * uses the library's 8 MiB. A quarter of it is where `peerWritable()` starts
98
+ * returning false. Not a maximum message size.
99
+ */
100
+ sendQueueLimit?: number;
88
101
  }
89
102
 
90
103
  export type PeerHandler = (peerId: string) => void;
104
+ /**
105
+ * `reason` is why the peer went — "RATS_CLOSE_PEER_CLOSED",
106
+ * "RATS_CLOSE_SLOW_CONSUMER" and so on. Worth branching on: a peer that left is
107
+ * one to redial, while SLOW_CONSUMER means *you* were sending faster than the
108
+ * link drained, and redialing that one just repeats the overload.
109
+ */
110
+ export type PeerDisconnectHandler = (peerId: string, reason: string) => void;
91
111
  export type MessageHandler = (peerId: string, data: Buffer) => void;
92
112
  export type TopicHandler = (peerId: string, topic: string, data: Buffer) => void;
93
113
  export type JsonHandler = (peerId: string, value: any) => void;
@@ -165,7 +185,7 @@ declare module 'librats' {
165
185
  /** Cap on established peers (0 = unlimited). Settable at any time. */
166
186
  maxPeers: number;
167
187
  /** Which wire a connected peer's link runs on, or `null` if not connected. */
168
- peerTransport(peerId: string): TransportValue | null;
188
+ peerTransport(peerId: string): PeerTransportValue | null;
169
189
  /**
170
190
  * Transports a connected peer advertised, as a `TransportMask` bitmask, or
171
191
  * `null` if not connected. 0 means the peer did not say (an older build).
@@ -174,17 +194,31 @@ declare module 'librats' {
174
194
 
175
195
  // ---- raw channel messaging ----
176
196
 
177
- /** Send raw bytes on a named channel to one peer. */
197
+ /**
198
+ * Send raw bytes on a named channel to one peer. Returning means the message
199
+ * was queued, never that it arrived — if you send in bulk, follow it with
200
+ * `peerWritable()`.
201
+ */
178
202
  send(peerId: string, channel: string, data: string | Buffer): void;
179
203
  /** Broadcast raw bytes on a named channel to every connected peer. */
180
204
  broadcast(channel: string, data: string | Buffer): void;
205
+ /**
206
+ * Whether this peer's send queue still has room (false also when it is not
207
+ * connected). False means stop: what you just sent was queued like anything
208
+ * else and nothing was dropped, but keep piling on and the peer is dropped
209
+ * with reason `RATS_CLOSE_SLOW_CONSUMER`. Wait for `onPeerWritable`, or poll
210
+ * this. Not a size limit — one message of any size is always queued.
211
+ */
212
+ peerWritable(peerId: string): boolean;
181
213
  /** Register a handler for a named channel. Additive; register before `start()`. */
182
214
  on(channel: string, handler: MessageHandler): void;
183
215
 
184
216
  // ---- peer events (register before start) ----
185
217
 
186
218
  onPeerConnected(handler: PeerHandler): void;
187
- onPeerDisconnected(handler: PeerHandler): void;
219
+ onPeerDisconnected(handler: PeerDisconnectHandler): void;
220
+ /** A peer whose queue had filled has drained back under its mark. */
221
+ onPeerWritable(handler: PeerHandler): void;
188
222
 
189
223
  // ---- discovery (enable before start) ----
190
224
 
@@ -207,6 +241,17 @@ declare module 'librats' {
207
241
  * ordinary `onPeerConnected`.
208
242
  */
209
243
  punchPeer(peerId: string): void;
244
+ /**
245
+ * Enable relaying: reach a peer nothing else could, through a node both ends
246
+ * are already connected to. `serveAsRelay` (default `false`) also carries
247
+ * OTHER peers' connections, which spends real bandwidth.
248
+ */
249
+ enableRelay(serveAsRelay?: boolean): void;
250
+ /**
251
+ * Try to reach a peer through a relay. Non-blocking: success arrives as an
252
+ * ordinary `onPeerConnected`.
253
+ */
254
+ connectViaRelay(peerId: string): void;
210
255
  /** A `NatMapping` value describing this node's own NAT. */
211
256
  readonly natMapping: NatMappingValue;
212
257
 
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()`. */
@@ -192,6 +193,14 @@ class RatsNode {
192
193
  */
193
194
  broadcast(channel, data) { this._native.broadcast(channel, data); }
194
195
 
196
+ /**
197
+ * Whether this peer's send queue still has room. False means pause and wait for
198
+ * `onPeerWritable` — keep sending regardless and the peer is dropped with
199
+ * reason `RATS_CLOSE_SLOW_CONSUMER`.
200
+ * @param {string} peerId @returns {boolean}
201
+ */
202
+ peerWritable(peerId) { return this._native.peerWritable(peerId); }
203
+
195
204
  /**
196
205
  * Register a handler for a named channel. Additive; register before `start()`.
197
206
  * @param {string} channel
@@ -204,9 +213,16 @@ class RatsNode {
204
213
  /** @param {(peerId: string) => void} handler fired when a peer connects. */
205
214
  onPeerConnected(handler) { this._native.onPeerConnected(handler); }
206
215
 
207
- /** @param {(peerId: string) => void} handler fired when a peer disconnects. */
216
+ /**
217
+ * @param {(peerId: string, reason: string) => void} handler fired when a peer
218
+ * disconnects. `reason` is a name like "RATS_CLOSE_SLOW_CONSUMER", which means
219
+ * this node was sending faster than the link drained.
220
+ */
208
221
  onPeerDisconnected(handler) { this._native.onPeerDisconnected(handler); }
209
222
 
223
+ /** @param {(peerId: string) => void} handler fired when a full queue drained. */
224
+ onPeerWritable(handler) { this._native.onPeerWritable(handler); }
225
+
210
226
  // ---- discovery (enable before start) ----
211
227
 
212
228
  /**
@@ -250,6 +266,29 @@ class RatsNode {
250
266
  */
251
267
  punchPeer(peerId) { this._native.punchPeer(peerId); }
252
268
 
269
+ /**
270
+ * Enable relaying: reach a peer that neither port forwarding nor hole punching
271
+ * could make reachable, by routing the connection through a node both ends are
272
+ * already connected to. The peer that comes out is ordinary in every way — the
273
+ * same end-to-end encryption, the same channels — except that `peerTransport()`
274
+ * reports it as `Transport.RELAY`.
275
+ * @param {boolean} [serveAsRelay=false] also carry OTHER peers' connections.
276
+ * Unlike a hole-punch rendezvous this spends real bandwidth on somebody else's
277
+ * traffic, so it is off by default; a mesh in which nobody serves cannot relay.
278
+ */
279
+ enableRelay(serveAsRelay = false) { this._native.enableRelay(serveAsRelay); }
280
+
281
+ /**
282
+ * Try to reach a peer through a relay. Non-blocking: success arrives as an
283
+ * ordinary `onPeerConnected`. Throws `NO_SUCH_PEER` when there is nothing to do
284
+ * or nothing to try with — already connected, an attempt already running, in
285
+ * cooldown, or no peer that could carry the connection. Usually unnecessary:
286
+ * with hole punching enabled too, a punch that cannot work hands the target over
287
+ * by itself.
288
+ * @param {string} peerId
289
+ */
290
+ connectViaRelay(peerId) { this._native.connectViaRelay(peerId); }
291
+
253
292
  /** @type {number} a {@link NatMapping} value describing this node's own NAT. */
254
293
  get natMapping() { return this._native.natMapping(); }
255
294
 
@@ -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
@@ -487,6 +493,11 @@ if(WIN32)
487
493
  endif()
488
494
 
489
495
  if(ANDROID)
496
+ # The logger routes console output through __android_log_print, because an app's
497
+ # stdout/stderr go nowhere on Android.
498
+ find_library(log-lib log)
499
+ target_link_libraries(rats ${log-lib})
500
+
490
501
  if(DEFINED ANDROID_PLATFORM)
491
502
  string(REGEX REPLACE "android-" "" ANDROID_API_LEVEL ${ANDROID_PLATFORM})
492
503
  math(EXPR ANDROID_API_LEVEL "${ANDROID_API_LEVEL}")
@@ -649,6 +660,7 @@ if(RATS_BUILD_TESTS)
649
660
  tests/test_frame.cpp
650
661
  tests/test_reactor.cpp
651
662
  tests/test_transport_udp.cpp
663
+ tests/test_relay_link.cpp
652
664
  tests/test_dialer.cpp
653
665
  tests/test_peer_table.cpp
654
666
  tests/test_handshake.cpp
@@ -665,6 +677,7 @@ if(RATS_BUILD_TESTS)
665
677
  tests/test_reconnection.cpp
666
678
  tests/test_nat_status.cpp
667
679
  tests/test_hole_punch.cpp
680
+ tests/test_relay.cpp
668
681
  tests/test_logging.cpp
669
682
  )
670
683
 
@@ -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
@@ -83,6 +85,24 @@ char* dup_string(const std::string& s) {
83
85
  return out;
84
86
  }
85
87
 
88
+ rats_close_reason_t to_c_reason(CloseReason r) {
89
+ switch (r) {
90
+ case CloseReason::LocalClose: return RATS_CLOSE_LOCAL;
91
+ case CloseReason::PeerClosed: return RATS_CLOSE_PEER_CLOSED;
92
+ case CloseReason::PeerReset: return RATS_CLOSE_PEER_RESET;
93
+ case CloseReason::ConnectFailed: return RATS_CLOSE_CONNECT_FAILED;
94
+ case CloseReason::HandshakeFailed: return RATS_CLOSE_HANDSHAKE_FAILED;
95
+ case CloseReason::ProtocolError: return RATS_CLOSE_PROTOCOL_ERROR;
96
+ case CloseReason::SlowConsumer: return RATS_CLOSE_SLOW_CONSUMER;
97
+ case CloseReason::ReactorShutdown: return RATS_CLOSE_SHUTDOWN;
98
+ case CloseReason::DuplicateConn: return RATS_CLOSE_DUPLICATE;
99
+ case CloseReason::PeerLimit: return RATS_CLOSE_PEER_LIMIT;
100
+ case CloseReason::IdleTimeout: return RATS_CLOSE_IDLE_TIMEOUT;
101
+ case CloseReason::DialSuperseded: return RATS_CLOSE_DIAL_SUPERSEDED;
102
+ }
103
+ return RATS_CLOSE_PEER_CLOSED;
104
+ }
105
+
86
106
  } // namespace
87
107
 
88
108
  extern "C" {
@@ -101,6 +121,24 @@ const char* rats_error_str(rats_error_t err) {
101
121
  return "RATS_ERR_UNKNOWN";
102
122
  }
103
123
 
124
+ const char* rats_close_reason_str(rats_close_reason_t reason) {
125
+ switch (reason) {
126
+ case RATS_CLOSE_LOCAL: return "RATS_CLOSE_LOCAL";
127
+ case RATS_CLOSE_PEER_CLOSED: return "RATS_CLOSE_PEER_CLOSED";
128
+ case RATS_CLOSE_PEER_RESET: return "RATS_CLOSE_PEER_RESET";
129
+ case RATS_CLOSE_CONNECT_FAILED: return "RATS_CLOSE_CONNECT_FAILED";
130
+ case RATS_CLOSE_HANDSHAKE_FAILED: return "RATS_CLOSE_HANDSHAKE_FAILED";
131
+ case RATS_CLOSE_PROTOCOL_ERROR: return "RATS_CLOSE_PROTOCOL_ERROR";
132
+ case RATS_CLOSE_SLOW_CONSUMER: return "RATS_CLOSE_SLOW_CONSUMER";
133
+ case RATS_CLOSE_SHUTDOWN: return "RATS_CLOSE_SHUTDOWN";
134
+ case RATS_CLOSE_DUPLICATE: return "RATS_CLOSE_DUPLICATE";
135
+ case RATS_CLOSE_PEER_LIMIT: return "RATS_CLOSE_PEER_LIMIT";
136
+ case RATS_CLOSE_IDLE_TIMEOUT: return "RATS_CLOSE_IDLE_TIMEOUT";
137
+ case RATS_CLOSE_DIAL_SUPERSEDED: return "RATS_CLOSE_DIAL_SUPERSEDED";
138
+ }
139
+ return "RATS_CLOSE_UNKNOWN";
140
+ }
141
+
104
142
  /* — construction / lifecycle — */
105
143
 
106
144
  rats_config_t rats_config_default(void) {
@@ -116,6 +154,7 @@ rats_config_t rats_config_default(void) {
116
154
  c.enable_udp = 1;
117
155
  c.preferred_transport = RATS_TRANSPORT_UDP;
118
156
  c.transport_fallback_ms = 1200;
157
+ c.send_queue_limit = 0;
119
158
  return c;
120
159
  }
121
160
 
@@ -151,6 +190,7 @@ rats_t rats_create_config(const rats_config_t* cfg) {
151
190
  ? TransportKind::Tcp
152
191
  : TransportKind::Udp;
153
192
  config.transport_fallback_ms = cfg->transport_fallback_ms;
193
+ config.send_queue_limit = cfg->send_queue_limit;
154
194
  }
155
195
  return make_handle(std::move(config));
156
196
  }
@@ -222,6 +262,13 @@ rats_error_t rats_send(rats_t node, const char* peer_id_hex,
222
262
  return RATS_OK;
223
263
  }
224
264
 
265
+ int rats_peer_writable(rats_t node, const char* peer_id_hex) {
266
+ if (!peer_id_hex) return 0;
267
+ auto id = PeerId::from_hex(peer_id_hex);
268
+ if (!id) return 0;
269
+ return node_of(node)->peer_writable(*id) ? 1 : 0;
270
+ }
271
+
225
272
  rats_error_t rats_broadcast(rats_t node, const char* channel, const void* data, size_t len) {
226
273
  if (!channel) return RATS_ERR_INVALID_ARG;
227
274
  node_of(node)->broadcast(channel, ByteView(static_cast<const uint8_t*>(data), len));
@@ -236,10 +283,18 @@ rats_error_t rats_on_peer_connected(rats_t node, rats_peer_cb cb, void* user) {
236
283
  return RATS_OK;
237
284
  }
238
285
 
239
- rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_cb cb, void* user) {
286
+ rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_disconnect_cb cb, void* user) {
287
+ if (!cb) return RATS_ERR_INVALID_ARG;
288
+ node_of(node)->on_peer_disconnected([cb, user](const PeerId& id, CloseReason reason) {
289
+ cb(user, id.to_hex().c_str(), to_c_reason(reason));
290
+ });
291
+ return RATS_OK;
292
+ }
293
+
294
+ rats_error_t rats_on_peer_writable(rats_t node, rats_peer_cb cb, void* user) {
240
295
  if (!cb) return RATS_ERR_INVALID_ARG;
241
- node_of(node)->on_peer_disconnected([cb, user](const PeerId& id) {
242
- cb(user, id.to_hex().c_str());
296
+ node_of(node)->on_peer_writable([cb, user](const Peer& peer) {
297
+ cb(user, peer.id().to_hex().c_str());
243
298
  });
244
299
  return RATS_OK;
245
300
  }
@@ -299,6 +354,29 @@ rats_error_t rats_enable_hole_punch(rats_t node, int serve_as_relay) {
299
354
  return RATS_OK;
300
355
  }
301
356
 
357
+ rats_error_t rats_enable_relay(rats_t node, int serve_as_relay) {
358
+ auto* h = as_handle(node);
359
+ if (h->started) return RATS_ERR_ALREADY_STARTED;
360
+ if (!h->relay) {
361
+ Relay::Config config;
362
+ config.serve = serve_as_relay != 0;
363
+ h->relay = h->node->add_subsystem(std::make_unique<Relay>(config));
364
+ }
365
+ return RATS_OK;
366
+ }
367
+
368
+ rats_error_t rats_connect_via_relay(rats_t node, const char* peer_id_hex) {
369
+ if (!peer_id_hex) return RATS_ERR_INVALID_ARG;
370
+ auto* h = as_handle(node);
371
+ if (!h->relay) return RATS_ERR_NOT_ENABLED;
372
+ auto id = PeerId::from_hex(peer_id_hex);
373
+ if (!id) return RATS_ERR_INVALID_ARG;
374
+ // As with punching, every reason an attempt is declined is "nothing to do or
375
+ // nothing to try with" — already connected, already trying, in cooldown, or no
376
+ // peer that could carry it — and none of them is separately actionable.
377
+ return h->relay->connect_via_relay(*id) ? RATS_OK : RATS_ERR_NO_SUCH_PEER;
378
+ }
379
+
302
380
  rats_error_t rats_punch_peer(rats_t node, const char* peer_id_hex) {
303
381
  if (!peer_id_hex) return RATS_ERR_INVALID_ARG;
304
382
  auto* h = as_handle(node);
@@ -338,9 +416,15 @@ int rats_peer_transport(rats_t node, const char* peer_id_hex) {
338
416
  if (!peer_id_hex) return -1;
339
417
  auto id = PeerId::from_hex(peer_id_hex);
340
418
  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;
419
+ for (const PeerInfo& info : node_of(node)->peers()) {
420
+ if (info.id != *id) continue;
421
+ switch (info.transport) {
422
+ case TransportKind::Udp: return RATS_TRANSPORT_UDP;
423
+ case TransportKind::Relay: return RATS_TRANSPORT_RELAY;
424
+ case TransportKind::Tcp: break;
425
+ }
426
+ return RATS_TRANSPORT_TCP;
427
+ }
344
428
  return -1;
345
429
  }
346
430
 
@@ -45,10 +45,41 @@ 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
 
56
+ /* Why a peer connection ended, handed to rats_on_peer_disconnected(). The
57
+ * reason is what tells "I was sending too fast" apart from "the peer left" —
58
+ * without it both look the same and the usual answer to either is to reconnect
59
+ * and repeat whatever caused it. Use rats_close_reason_str() for a name. */
60
+ typedef enum {
61
+ RATS_CLOSE_LOCAL = 0, /* we asked to disconnect */
62
+ RATS_CLOSE_PEER_CLOSED = 1, /* the peer closed cleanly */
63
+ RATS_CLOSE_PEER_RESET = 2, /* connection reset / socket error */
64
+ RATS_CLOSE_CONNECT_FAILED = 3, /* the dial never completed */
65
+ RATS_CLOSE_HANDSHAKE_FAILED = 4, /* secure handshake failed or timed out */
66
+ RATS_CLOSE_PROTOCOL_ERROR = 5, /* malformed frame / decryption failure */
67
+ /* We kept sending while this peer's queue was already past its limit. THE
68
+ * ONE REASON THE APPLICATION CAN PREVENT: heed rats_send()'s answer and
69
+ * rats_peer_writable(), and wait for the writable callback. It does NOT mean
70
+ * a single message was too big — one message is always accepted whatever its
71
+ * size; only piling more on a full queue gets a peer dropped. */
72
+ RATS_CLOSE_SLOW_CONSUMER = 6,
73
+ RATS_CLOSE_SHUTDOWN = 7, /* the node is stopping */
74
+ RATS_CLOSE_DUPLICATE = 8, /* superseded by another link to the same peer */
75
+ RATS_CLOSE_PEER_LIMIT = 9, /* inbound refused: peer limit reached */
76
+ RATS_CLOSE_IDLE_TIMEOUT = 10, /* datagram link went silent */
77
+ RATS_CLOSE_DIAL_SUPERSEDED = 11 /* a racing dial over the other transport won */
78
+ } rats_close_reason_t;
79
+
80
+ /** Static human-readable name for a close reason (never NULL, never freed). */
81
+ RATS_API const char* rats_close_reason_str(rats_close_reason_t reason);
82
+
52
83
  /* Bitmask of transports (see rats_transports / rats_peer_transports). */
53
84
  #define RATS_TRANSPORT_MASK_TCP 0x1u
54
85
  #define RATS_TRANSPORT_MASK_UDP 0x2u
@@ -98,6 +129,14 @@ typedef struct {
98
129
  rats_transport_t preferred_transport; /* tried first when dialing (default UDP) */
99
130
  uint32_t transport_fallback_ms; /* delay before racing the other transport;
100
131
  * 0 = never fall back (default 1200) */
132
+
133
+ /* Bytes a peer's send queue may hold before an application that keeps sending
134
+ * anyway has that peer dropped with RATS_CLOSE_SLOW_CONSUMER. 0 = the library
135
+ * default (8 MiB). A quarter of it is where rats_peer_writable() starts
136
+ * answering 0, so lowering this makes an application feel backpressure sooner.
137
+ * It is NOT a maximum message size: one message is always queued whatever its
138
+ * size. */
139
+ size_t send_queue_limit;
101
140
  } rats_config_t;
102
141
 
103
142
  /** A config pre-filled with the library defaults (listening, Noise, ephemeral
@@ -149,18 +188,44 @@ RATS_API size_t rats_max_peers(rats_t node);
149
188
 
150
189
  /* — messaging (named application channel, raw bytes) — */
151
190
 
191
+ /** Queue bytes for one peer on a named channel. RATS_OK means accepted and
192
+ * queued, never that it arrived. Pair it with rats_peer_writable() below if you
193
+ * send in bulk — that is the only way to learn you are outrunning the link
194
+ * before the peer is dropped for it. */
152
195
  RATS_API rats_error_t rats_send(rats_t node, const char* peer_id_hex,
153
196
  const char* channel, const void* data, size_t len);
154
197
  RATS_API rats_error_t rats_broadcast(rats_t node, const char* channel,
155
198
  const void* data, size_t len);
156
199
 
200
+ /** Whether this peer's send queue still has room — the same question rats_send()
201
+ * answers, asked without sending. Returns 1 for room, 0 for none (and 0 for a
202
+ * peer that is not connected).
203
+ *
204
+ * A 0 says "stop". The message you just sent was queued like any other and
205
+ * nothing was dropped, but keep piling on and this peer is dropped with
206
+ * RATS_CLOSE_SLOW_CONSUMER. Wait for the callback registered with
207
+ * rats_on_peer_writable(), or poll this from a thread of your own.
208
+ *
209
+ * Ask it right after rats_send(): the bytes you just handed over are already
210
+ * counted, so the answer covers your own send and not just what the reactor has
211
+ * got round to. It is NOT a size limit — a single message of any size is always
212
+ * accepted; only sending more on top of a full queue drops a peer. */
213
+ RATS_API int rats_peer_writable(rats_t node, const char* peer_id_hex);
214
+
157
215
  /* — callbacks (register before start; invoked on a reactor thread) — */
158
216
 
159
217
  typedef void (*rats_peer_cb)(void* user, const char* peer_id_hex);
218
+ typedef void (*rats_peer_disconnect_cb)(void* user, const char* peer_id_hex,
219
+ rats_close_reason_t reason);
160
220
  typedef void (*rats_message_cb)(void* user, const char* peer_id_hex, const void* data, size_t len);
161
221
 
162
222
  RATS_API rats_error_t rats_on_peer_connected(rats_t node, rats_peer_cb cb, void* user);
163
- RATS_API rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_cb cb, void* user);
223
+ RATS_API rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_disconnect_cb cb,
224
+ void* user);
225
+ /** "This peer can be written to again" — fired when a peer whose queue had filled
226
+ * past its mark has drained back under it. The other half of rats_send()
227
+ * reporting 0; an application that never asks never needs this. */
228
+ RATS_API rats_error_t rats_on_peer_writable(rats_t node, rats_peer_cb cb, void* user);
164
229
  RATS_API rats_error_t rats_on(rats_t node, const char* channel, rats_message_cb cb, void* user);
165
230
 
166
231
  /* — optional subsystems (enable before start) — */
@@ -192,6 +257,30 @@ RATS_API rats_error_t rats_enable_hole_punch(rats_t node, int serve_as_relay);
192
257
  * its own to advertise (it needs at least one datagram peer first). */
193
258
  RATS_API rats_error_t rats_punch_peer(rats_t node, const char* peer_id_hex);
194
259
 
260
+ /** Relaying: reach a peer that neither port forwarding nor hole punching could
261
+ * make reachable, by routing the connection through a node both ends are already
262
+ * connected to. Call before start(). The peer that comes out is ordinary in every
263
+ * way — the same end-to-end encryption, the same channels — except that its bytes
264
+ * take a detour, which rats_peer_transport() reports as RATS_TRANSPORT_RELAY.
265
+ *
266
+ * `serve_as_relay` (non-zero) also carries OTHER peers' connections. Unlike a
267
+ * hole-punch rendezvous, that spends real bandwidth on somebody else's traffic, so
268
+ * it is off by default and opted into here; a mesh in which nobody serves cannot
269
+ * relay at all. A serving node forwards only between peers it already holds, never
270
+ * chains circuits, and caps each one by bytes, duration and count. */
271
+ RATS_API rats_error_t rats_enable_relay(rats_t node, int serve_as_relay);
272
+
273
+ /** Try to reach `peer_id_hex` through a relay. Non-blocking: success arrives as an
274
+ * ordinary peer-connected callback. RATS_OK if an attempt was started;
275
+ * RATS_ERR_NOT_ENABLED if relaying is off; RATS_ERR_NO_SUCH_PEER if there is
276
+ * nothing to do or nothing to try with — the peer is already connected, an attempt
277
+ * is already running, it is in cooldown, or this node has no peer that could carry
278
+ * the connection.
279
+ *
280
+ * Usually there is no need to call this: with hole punching enabled too, a punch
281
+ * that cannot work hands the target over by itself. */
282
+ RATS_API rats_error_t rats_connect_via_relay(rats_t node, const char* peer_id_hex);
283
+
195
284
  /** What the mesh has shown about this node's own NAT, from the endpoints datagram
196
285
  * peers report seeing its shared UDP socket at. One of the RATS_NAT_* values;
197
286
  * RATS_NAT_ENDPOINT_DEPENDENT means punching cannot work from here. */
@@ -13,7 +13,7 @@ const char* to_string(ConnState s) noexcept {
13
13
  return "?";
14
14
  }
15
15
 
16
- const char* to_string(CloseReason r) noexcept {
16
+ RATS_API const char* to_string(CloseReason r) noexcept {
17
17
  switch (r) {
18
18
  case CloseReason::LocalClose: return "LocalClose";
19
19
  case CloseReason::PeerClosed: return "PeerClosed";
@@ -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.
@@ -74,7 +84,9 @@ enum class CloseReason {
74
84
  ConnectFailed, ///< Outbound transport connect never completed.
75
85
  HandshakeFailed, ///< Secure-channel handshake failed or timed out.
76
86
  ProtocolError, ///< Malformed frame / decryption failure on the wire.
77
- SlowConsumer, ///< Send buffer exceeded its high-water mark.
87
+ SlowConsumer, ///< Kept sending with the queue already past its high-water
88
+ ///< mark. Not "a message was too large": one message is
89
+ ///< always queued whatever its size (Connection::send).
78
90
  ReactorShutdown, ///< Reactor is stopping.
79
91
  DuplicateConn, ///< Redundant connection to a peer we already hold; superseded.
80
92
  PeerLimit, ///< Inbound rejected: the configured peer limit is reached.
@@ -83,11 +95,12 @@ enum class CloseReason {
83
95
  };
84
96
 
85
97
  const char* to_string(ConnState) noexcept;
86
- const char* to_string(CloseReason) noexcept;
87
- /// Exported, unlike its siblings above: TransportKind is part of the public
88
- /// surface (NodeConfig::preferred_transport, PeerInfo::transport), so a consumer
89
- /// of the shared build needs to be able to render one. ConnState/CloseReason only
90
- /// ever appear on Connection, which does not cross the library boundary.
98
+ /// Exported: a CloseReason is handed to every on_peer_disconnected handler, so a
99
+ /// consumer of the shared build has to be able to render one. (ConnState above is
100
+ /// not it never leaves Connection.)
101
+ RATS_API const char* to_string(CloseReason) noexcept;
102
+ /// Exported for the same reason: TransportKind is part of the public surface
103
+ /// (NodeConfig::preferred_transport, PeerInfo::transport).
91
104
  RATS_API const char* to_string(TransportKind) noexcept;
92
105
 
93
106
  } // namespace librats