librats 2.1.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +9 -0
  2. package/lib/index.d.ts +51 -6
  3. package/lib/index.js +42 -3
  4. package/native-src/CMakeLists.txt +13 -0
  5. package/native-src/src/librats/bindings/rats.cpp +90 -6
  6. package/native-src/src/librats/bindings/rats.h +92 -3
  7. package/native-src/src/librats/core/types.cpp +4 -3
  8. package/native-src/src/librats/core/types.h +25 -12
  9. package/native-src/src/librats/node/circuit_service.h +84 -0
  10. package/native-src/src/librats/node/config.h +9 -2
  11. package/native-src/src/librats/node/node.cpp +59 -5
  12. package/native-src/src/librats/node/node.h +38 -7
  13. package/native-src/src/librats/node/peer_network.h +19 -2
  14. package/native-src/src/librats/peer/peer.h +17 -6
  15. package/native-src/src/librats/peer/peer_table.cpp +41 -8
  16. package/native-src/src/librats/security/noise_security.cpp +2 -0
  17. package/native-src/src/librats/security/plaintext_security.h +1 -0
  18. package/native-src/src/librats/security/session.h +7 -0
  19. package/native-src/src/librats/storage/storage.cpp +434 -213
  20. package/native-src/src/librats/storage/storage.h +163 -19
  21. package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
  22. package/native-src/src/librats/subsystems/hole_punch.cpp +81 -8
  23. package/native-src/src/librats/subsystems/hole_punch.h +24 -0
  24. package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
  25. package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
  26. package/native-src/src/librats/subsystems/relay.cpp +1143 -0
  27. package/native-src/src/librats/subsystems/relay.h +211 -0
  28. package/native-src/src/librats/subsystems/relay_service.h +46 -0
  29. package/native-src/src/librats/transport/connection.cpp +40 -8
  30. package/native-src/src/librats/transport/connection.h +12 -2
  31. package/native-src/src/librats/transport/reactor.cpp +56 -8
  32. package/native-src/src/librats/transport/reactor.h +38 -0
  33. package/native-src/src/librats/transport/relay_link.cpp +208 -0
  34. package/native-src/src/librats/transport/relay_link.h +303 -0
  35. package/native-src/src/librats/util/logger.h +37 -5
  36. package/native-src/src/librats/util/network_monitor.cpp +14 -0
  37. package/native-src/src/librats/util/network_utils.cpp +10 -1
  38. package/native-src/src/librats/wire/frame.h +1 -0
  39. package/package.json +1 -1
  40. package/src/librats_node.cpp +76 -1
@@ -0,0 +1,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
@@ -52,8 +52,15 @@ struct RATS_API NodeConfig {
52
52
  /// connection. 0 disables the fallback: only the preferred transport is tried.
53
53
  uint32_t transport_fallback_ms = 1200;
54
54
 
55
- /// Bytes a peer's send queue may hold before the peer is dropped as a slow
56
- /// consumer. 0 uses the library default (8 MiB).
55
+ /// Bytes a peer's send queue may hold before an application that keeps
56
+ /// sending anyway has the peer dropped as a slow consumer. 0 uses the library
57
+ /// default (8 MiB).
58
+ ///
59
+ /// It is not a maximum message size. A single message is always queued
60
+ /// whatever its size — a message cannot be sent by halves, and a healthy
61
+ /// connection must not die over one large frame — so the queue's real ceiling
62
+ /// is this plus one message. What gets a peer dropped is offering *another*
63
+ /// message while the queue is still over the mark.
57
64
  ///
58
65
  /// A quarter of this is the mark at which send() starts answering "no room"
59
66
  /// and on_peer_writable is what says the room is back — so lowering it makes
@@ -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
@@ -449,7 +488,15 @@ bool Node::send(const PeerId& to, std::string_view channel, ByteView payload) {
449
488
 
450
489
  bool Node::peer_writable(const PeerId& id) const {
451
490
  const auto dest = peers_.destination(id);
452
- return dest && dest->writable;
491
+ if (!dest) return false;
492
+ // Both halves of the backlog, exactly as send() weighs them: what the reactor
493
+ // has already queued (`writable`) *and* what a caller has handed over that the
494
+ // reactor has not taken up yet (`owed`). Reporting only the first would answer
495
+ // "there is room" to a caller that has just filled the queue itself and is
496
+ // waiting to hear otherwise — the queue it filled has not been looked at yet,
497
+ // so nothing about it has changed, and no event is coming either.
498
+ return dest->writable &&
499
+ dest->owed->load(std::memory_order_relaxed) <= send_low_water();
453
500
  }
454
501
 
455
502
  bool Node::broadcast(std::string_view channel, ByteView payload) {
@@ -643,7 +690,7 @@ void Node::on_closed(Connection& conn, CloseReason reason) {
643
690
  // let a long-gone peer keep voting on how our NAT behaves.
644
691
  nat_status_.forget(id);
645
692
  LOG_INFO("node", "Peer " << id.short_hex() << " disconnected (" << to_string(reason) << ")");
646
- for (auto& cb : peer_disconnected_) cb(id);
693
+ for (auto& cb : peer_disconnected_) cb(id, reason);
647
694
  }
648
695
 
649
696
  void Node::on_writable_changed(Connection& conn, bool writable) {
@@ -810,9 +857,16 @@ std::vector<Address> Node::observed_addresses() const {
810
857
 
811
858
  // ── Peer handle methods (defined here for the full Node type) ────────────────
812
859
 
813
- void Peer::send(std::string_view channel, ByteView payload) const {
814
- node_->route_send(route_, FrameHeader{MessageType::App, 0, MessageRouter::channel_id(channel)},
815
- payload.to_bytes());
860
+ bool Peer::send(std::string_view channel, ByteView payload) const {
861
+ // Deliberately by id, not by this handle's route. The route existed to skip the
862
+ // directory lookup on the reply path — but answering "is there room?" needs the
863
+ // peer's writability and its in-transit counter, both of which live in the
864
+ // directory, so the lookup is unavoidable the moment send() has an answer to
865
+ // give. Once it is being paid anyway, the captured route buys nothing and can
866
+ // only be wrong: a handle outlives its connection (a relayed link superseded by
867
+ // a direct one, a dial race, a reconnect), and sending on the superseded one
868
+ // drops the message with nothing said. The peer is what the caller means.
869
+ return node_->send(id_, channel, payload);
816
870
  }
817
871
 
818
872
  void Peer::disconnect() const {
@@ -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); }
@@ -182,6 +193,14 @@ public:
182
193
  /// low-water mark, and an application that keeps going regardless
183
194
  /// will eventually have the peer dropped as a slow consumer. Wait for
184
195
  /// on_peer_writable instead. Also false if the peer is not connected.
196
+ ///
197
+ /// "Stop" is meant literally, and yielding is part of it: the mark is
198
+ /// re-tested inside the reactor task this call hands off to, and it is
199
+ /// that test which flips the peer to unwritable and later raises
200
+ /// on_peer_writable. A caller that answers a false by looping straight
201
+ /// back into send() therefore starves the very thread that would tell
202
+ /// it to stop — the queue keeps growing while the signal it is waiting
203
+ /// for never gets a turn to be produced.
185
204
  bool send(const PeerId& to, std::string_view channel, ByteView payload);
186
205
  /// Send raw bytes on a named channel to every connected peer.
187
206
  /// @return whether *every* one of them still has room — a fan-out can only
@@ -194,12 +213,24 @@ public:
194
213
  /// transfer, a large stream — address peers individually with send().
195
214
  bool broadcast(std::string_view channel, ByteView payload);
196
215
 
197
- /// Whether a peer's send queue currently has room the same answer send()
198
- /// returns, without sending anything. False for a peer that is not connected.
199
- /// Note that the answer describes the queue as the reactor last left it: a
200
- /// message handed over a moment ago may not be counted yet, which is why the
201
- /// signal to stop is the *return of send()* rather than a poll before it.
202
- bool peer_writable(const PeerId& id) const;
216
+ /// Whether a peer's send queue has room for more. False for a peer that is
217
+ /// not connected.
218
+ ///
219
+ /// The same question send() answers, asked without sending anything: it
220
+ /// weighs both halves of what the peer is carrying the bytes the reactor
221
+ /// has queued, and the bytes a caller has handed to send() that the reactor
222
+ /// has not taken up yet. So it is safe to poll: a caller that has just filled
223
+ /// the queue in a tight loop keeps being told "no room" until the reactor has
224
+ /// actually looked at what it was given, rather than being told "go on"
225
+ /// because nothing observable has changed yet.
226
+ ///
227
+ /// It stays a hint about a queue that drains as it is read, so the ordinary
228
+ /// flow is unchanged: the signal to stop is the return of send(), and the
229
+ /// signal to resume is on_peer_writable. This is for a caller that must wait
230
+ /// for room on a thread of its own — the event alone cannot serve it, because
231
+ /// a queue that filled *only* with bytes still in transit never crossed
232
+ /// anything on the connection and so raises no event when they drain.
233
+ bool peer_writable(const PeerId& id) const override;
203
234
 
204
235
  // — events (register before start(); invoked on a reactor thread). Multiple
205
236
  // listeners are supported, so subsystems and the app can both subscribe. —
@@ -17,6 +17,7 @@
17
17
  #include "librats/peer/peer_id.h"
18
18
  #include "librats/peer/peer_info.h"
19
19
  #include "librats/core/address.h"
20
+ #include "librats/core/types.h" // CloseReason
20
21
 
21
22
  #include <cstdint>
22
23
  #include <functional>
@@ -33,7 +34,12 @@ public:
33
34
  using MessageHandler = std::function<void(const Peer&, ByteView)>;
34
35
 
35
36
  using PeerEventHandler = std::function<void(const Peer&)>;
36
- using PeerDisconnectHandler = std::function<void(const PeerId&)>;
37
+ /// A peer went away, and why. The reason matters as much as the event: an
38
+ /// application dropped as a slow consumer (CloseReason::SlowConsumer) has to
39
+ /// slow down, while one whose peer simply left should reconnect — and without
40
+ /// the reason those look identical, so the usual answer to both is to redial
41
+ /// and repeat whatever caused it.
42
+ using PeerDisconnectHandler = std::function<void(const PeerId&, CloseReason)>;
37
43
  using DialFailedHandler = std::function<void(const Address&)>;
38
44
 
39
45
  virtual const PeerId& local_id() const = 0;
@@ -49,7 +55,9 @@ public:
49
55
  /// Send to one peer. @return whether that peer's send queue still has room;
50
56
  /// false means "stop and wait for on_peer_writable" — the message is queued
51
57
  /// either way, but continuing past this is what gets a peer dropped as a slow
52
- /// consumer. Also false if the peer is not connected.
58
+ /// consumer. Also false if the peer is not connected. Wait on the event and
59
+ /// not on a poll of the queue's state: the answer is re-derived on the reactor
60
+ /// thread, so a caller that spins instead of yielding never lets it change.
53
61
  virtual bool send(const PeerId& to, MessageType type, ByteView payload) = 0;
54
62
  /// Send to every connected peer. @return whether *every* one of them still
55
63
  /// has room, so a subsystem that fans out can pause on the slowest.
@@ -72,6 +80,15 @@ public:
72
80
  /// subsystem that never checks that return never needs this either. Runs on a
73
81
  /// reactor thread. Default: not offered (nothing subscribes).
74
82
  virtual void on_peer_writable(PeerEventHandler /*handler*/) {}
83
+ /// The same question send() answers — "has this peer room for more?" — asked
84
+ /// without sending anything. For a subsystem that paces a long fan-out from a
85
+ /// thread of its own (StorageManager streaming a snapshot): the event alone
86
+ /// cannot serve it, because a queue filled *only* with bytes handed to send()
87
+ /// that the reactor has not taken up yet never crossed anything on the
88
+ /// connection, so nothing raises on_peer_writable when they drain. Safe to
89
+ /// poll — it weighs those in-flight bytes too. False for an unknown peer.
90
+ /// Default: always writable, all a mock that merely moves messages can claim.
91
+ virtual bool peer_writable(const PeerId& /*id*/) const { return true; }
75
92
  };
76
93
 
77
94
  struct NodeContext; // node/node_context.h — bundles network + events + services
@@ -4,10 +4,12 @@
4
4
  * @file peer.h
5
5
  * @brief A lightweight handle to a connected peer.
6
6
  *
7
- * Peer is a value passed to callbacks. It carries the peer's id and its
8
- * route (which reactor + connection), so send()/disconnect() reach the right
9
- * reactor directly no PeerTable lookup on the reply path. info() does
10
- * consult the directory, on demand, for metadata.
7
+ * Peer is a value passed to callbacks. It carries the peer's id and its route
8
+ * (which reactor + connection). disconnect() uses the route, reaching that exact
9
+ * connection with no directory lookup. send() goes by id instead: it has to
10
+ * consult the directory anyway for the backpressure answer it returns, and by
11
+ * then the peer's *current* route is the better destination — a handle can
12
+ * outlive the link it names. info() consults the directory on demand too.
11
13
  */
12
14
 
13
15
  #include "librats/util/rats_export.h"
@@ -27,8 +29,17 @@ class RATS_API Peer {
27
29
  public:
28
30
  const PeerId& id() const noexcept { return id_; }
29
31
 
30
- /// Send bytes on a named application channel.
31
- void send(std::string_view channel, ByteView payload) const;
32
+ /// Send bytes on a named application channel, to this peer over whichever
33
+ /// connection currently serves it.
34
+ ///
35
+ /// @return whether that peer's queue still has room — the same answer, and the
36
+ /// same contract, as Node::send(): false means stop and wait for
37
+ /// on_peer_writable rather than keep going. A handler that replies
38
+ /// through this handle is the most ordinary way to write to a peer, so
39
+ /// it has to be able to feel backpressure like any other sender; while
40
+ /// this returned void it could not, and the bytes it queued were
41
+ /// invisible to peer_writable() into the bargain.
42
+ bool send(std::string_view channel, ByteView payload) const;
32
43
 
33
44
  /// Request this peer be disconnected.
34
45
  void disconnect() const;
@@ -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) {
@@ -44,6 +44,8 @@ public:
44
44
  return true;
45
45
  }
46
46
 
47
+ size_t overhead() const noexcept override { return librats::NOISE_TAG_SIZE; }
48
+
47
49
  const PeerId& remote_id() const override { return remote_id_; }
48
50
  bool is_secure() const override { return true; }
49
51
 
@@ -32,6 +32,7 @@ class PlaintextSession final : public Session {
32
32
  public:
33
33
  explicit PlaintextSession(PeerId remote) : remote_id_(remote) {}
34
34
  bool encrypt(ByteView plain, Bytes& out) override { out.assign(plain.begin(), plain.end()); return true; }
35
+ size_t overhead() const noexcept override { return 0; }
35
36
  bool decrypt(ByteView cipher, Bytes& out) override { out.assign(cipher.begin(), cipher.end()); return true; }
36
37
  const PeerId& remote_id() const override { return remote_id_; }
37
38
  bool is_secure() const override { return false; }
@@ -30,6 +30,13 @@ public:
30
30
  /// The remote peer's identity, proven during the handshake.
31
31
  virtual const PeerId& remote_id() const = 0;
32
32
 
33
+ /// Bytes encrypt() adds to a plaintext of any size (an AEAD tag, typically).
34
+ /// The send path needs the ciphertext's size *before* encrypting it, because
35
+ /// a message it turns out it cannot frame must be refused without a nonce
36
+ /// having been spent on it — the counters run in lockstep at both ends, so a
37
+ /// message encrypted and then not sent would break every one after it.
38
+ virtual size_t overhead() const noexcept = 0;
39
+
33
40
  /// True if traffic is actually encrypted (false for the plaintext passthrough).
34
41
  virtual bool is_secure() const = 0;
35
42
  };