librats 0.7.2 → 0.9.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 (35) hide show
  1. package/README.md +9 -4
  2. package/lib/index.d.ts +9 -22
  3. package/native-src/CMakeLists.txt +132 -50
  4. package/native-src/cmake/ratsConfig.cmake.in +8 -0
  5. package/native-src/src/bt_network.cpp +296 -183
  6. package/native-src/src/bt_network.h +25 -5
  7. package/native-src/src/crypto/sha256.c +1 -1
  8. package/native-src/src/dht.cpp +297 -47
  9. package/native-src/src/dht.h +70 -6
  10. package/native-src/src/file_transfer.cpp +1185 -1578
  11. package/native-src/src/file_transfer.h +240 -521
  12. package/native-src/src/io_poller.cpp +917 -0
  13. package/native-src/src/io_poller.h +138 -0
  14. package/native-src/src/krpc.cpp +161 -96
  15. package/native-src/src/krpc.h +18 -4
  16. package/native-src/src/librats.cpp +812 -1236
  17. package/native-src/src/librats.h +207 -208
  18. package/native-src/src/librats_bittorrent.cpp +1 -5
  19. package/native-src/src/librats_c.cpp +22 -39
  20. package/native-src/src/librats_discovery.cpp +377 -0
  21. package/native-src/src/librats_encryption.cpp +130 -283
  22. package/native-src/src/librats_file_transfer.cpp +27 -109
  23. package/native-src/src/librats_gossipsub.cpp +1 -5
  24. package/native-src/src/librats_ice.cpp +5 -1
  25. package/native-src/src/librats_log_macros.h +36 -0
  26. package/native-src/src/librats_logging.cpp +1 -7
  27. package/native-src/src/librats_mdns.cpp +1 -11
  28. package/native-src/src/librats_persistence.cpp +1 -11
  29. package/native-src/src/librats_reconnection.cpp +2 -13
  30. package/native-src/src/librats_statistic.cpp +105 -0
  31. package/native-src/src/socket.cpp +15 -3
  32. package/package.json +1 -1
  33. package/scripts/build-librats.js +3 -0
  34. package/scripts/prepare-package.js +10 -0
  35. package/src/librats_node.cpp +53 -64
@@ -0,0 +1,138 @@
1
+ #pragma once
2
+
3
+ /**
4
+ * @file io_poller.h
5
+ * @brief Platform-optimal I/O multiplexing abstraction
6
+ *
7
+ * Provides a unified interface over platform-specific I/O multiplexers:
8
+ * - Linux: epoll (O(1) per event, scales to millions of fds)
9
+ * - macOS/BSD: kqueue (O(1) per event, scales to millions of fds)
10
+ * - Windows: IOCP (true async completion ports, O(1) per event)
11
+ *
12
+ * Usage:
13
+ * auto poller = IOPoller::create();
14
+ * poller->add(fd, PollIn);
15
+ *
16
+ * PollResult results[64];
17
+ * int n = poller->wait(results, 64, 100); // 100ms timeout
18
+ * for (int i = 0; i < n; i++) {
19
+ * if (results[i].events & PollIn) handle_read(results[i].fd);
20
+ * if (results[i].events & PollOut) handle_write(results[i].fd);
21
+ * }
22
+ */
23
+
24
+ #include "socket.h"
25
+
26
+ #include <memory>
27
+ #include <cstdint>
28
+
29
+ namespace librats {
30
+
31
+ //=============================================================================
32
+ // Poll Event Flags
33
+ //=============================================================================
34
+
35
+ /**
36
+ * @brief I/O event flags for polling
37
+ */
38
+ enum PollFlags : uint32_t {
39
+ PollNone = 0,
40
+ PollIn = 1 << 0, ///< Socket is readable (data available or connection accepted)
41
+ PollOut = 1 << 1, ///< Socket is writable (can send or connect completed)
42
+ PollErr = 1 << 2, ///< Error condition on socket
43
+ PollHup = 1 << 3, ///< Hang up / peer disconnected
44
+ };
45
+
46
+ inline uint32_t operator|(PollFlags a, PollFlags b) {
47
+ return static_cast<uint32_t>(a) | static_cast<uint32_t>(b);
48
+ }
49
+
50
+ //=============================================================================
51
+ // Poll Result
52
+ //=============================================================================
53
+
54
+ /**
55
+ * @brief Result entry from a poll wait
56
+ */
57
+ struct PollResult {
58
+ socket_t fd; ///< The socket that has events
59
+ uint32_t events; ///< Bitmask of PollFlags that occurred
60
+ };
61
+
62
+ //=============================================================================
63
+ // IOPoller Abstract Interface
64
+ //=============================================================================
65
+
66
+ /**
67
+ * @brief Abstract I/O multiplexer
68
+ *
69
+ * Thread-safety:
70
+ * - add/modify/remove: Safe to call from any thread (epoll_ctl is thread-safe,
71
+ * kqueue changes are atomic, WSAPoll rebuilds on wait).
72
+ * - wait: Should be called from a single I/O thread.
73
+ * - add/modify/remove can be called concurrently with wait().
74
+ */
75
+ class IOPoller {
76
+ public:
77
+ virtual ~IOPoller() = default;
78
+
79
+ /**
80
+ * @brief Create the platform-optimal poller instance
81
+ *
82
+ * Returns:
83
+ * - EpollPoller on Linux
84
+ * - KqueuePoller on macOS/FreeBSD
85
+ * - IocpPoller on Windows (I/O Completion Ports)
86
+ */
87
+ static std::unique_ptr<IOPoller> create();
88
+
89
+ /**
90
+ * @brief Add a socket to the poll set
91
+ *
92
+ * @param fd Socket to monitor
93
+ * @param events Bitmask of PollFlags to watch for
94
+ * @return true on success
95
+ */
96
+ virtual bool add(socket_t fd, uint32_t events) = 0;
97
+
98
+ /**
99
+ * @brief Modify the event mask for a monitored socket
100
+ *
101
+ * @param fd Socket already in the poll set
102
+ * @param events New bitmask of PollFlags
103
+ * @return true on success
104
+ */
105
+ virtual bool modify(socket_t fd, uint32_t events) = 0;
106
+
107
+ /**
108
+ * @brief Remove a socket from the poll set
109
+ *
110
+ * @param fd Socket to remove
111
+ * @return true on success (false if fd was not registered)
112
+ */
113
+ virtual bool remove(socket_t fd) = 0;
114
+
115
+ /**
116
+ * @brief Wait for I/O events
117
+ *
118
+ * Blocks until events occur or timeout expires.
119
+ *
120
+ * @param results Array to fill with ready socket events
121
+ * @param max_results Maximum entries in results array
122
+ * @param timeout_ms Timeout in milliseconds (-1 = block forever, 0 = non-blocking)
123
+ * @return Number of ready descriptors (0 on timeout, -1 on error)
124
+ */
125
+ virtual int wait(PollResult* results, int max_results, int timeout_ms) = 0;
126
+
127
+ /**
128
+ * @brief Get the backend name (for logging/diagnostics)
129
+ */
130
+ virtual const char* name() const = 0;
131
+
132
+ // Non-copyable
133
+ IOPoller() = default;
134
+ IOPoller(const IOPoller&) = delete;
135
+ IOPoller& operator=(const IOPoller&) = delete;
136
+ };
137
+
138
+ } // namespace librats
@@ -183,6 +183,15 @@ BencodeValue KrpcProtocol::encode_query(const KrpcMessage& message) {
183
183
  break;
184
184
  }
185
185
 
186
+ // BEP 32: advertise which node families we want back
187
+ if (!message.want.empty()) {
188
+ BencodeValue want_list = BencodeValue::create_list();
189
+ for (const auto& w : message.want) {
190
+ want_list.push_back(BencodeValue(w));
191
+ }
192
+ args["want"] = want_list;
193
+ }
194
+
186
195
  root["a"] = args;
187
196
  return root;
188
197
  }
@@ -198,13 +207,24 @@ BencodeValue KrpcProtocol::encode_response(const KrpcMessage& message) {
198
207
  BencodeValue response = BencodeValue::create_dict();
199
208
  response["id"] = BencodeValue(node_id_to_string(message.response_id));
200
209
 
201
- // Add nodes if present
210
+ // Add nodes if present. BEP 32: IPv4 nodes go in "nodes" (26 bytes each),
211
+ // IPv6 nodes go in "nodes6" (38 bytes each).
202
212
  if (!message.nodes.empty()) {
203
- std::string compact_nodes;
213
+ std::string compact_nodes_v4;
214
+ std::string compact_nodes_v6;
204
215
  for (const auto& node : message.nodes) {
205
- compact_nodes += compact_node_info(node);
216
+ if (network_utils::is_valid_ipv6(node.ip)) {
217
+ compact_nodes_v6 += compact_node_info(node);
218
+ } else {
219
+ compact_nodes_v4 += compact_node_info(node);
220
+ }
221
+ }
222
+ if (!compact_nodes_v4.empty()) {
223
+ response["nodes"] = BencodeValue(compact_nodes_v4);
224
+ }
225
+ if (!compact_nodes_v6.empty()) {
226
+ response["nodes6"] = BencodeValue(compact_nodes_v6);
206
227
  }
207
- response["nodes"] = BencodeValue(compact_nodes);
208
228
  }
209
229
 
210
230
  // Add peers if present
@@ -220,8 +240,18 @@ BencodeValue KrpcProtocol::encode_response(const KrpcMessage& message) {
220
240
  if (!message.token.empty()) {
221
241
  response["token"] = BencodeValue(message.token);
222
242
  }
223
-
243
+
224
244
  root["r"] = response;
245
+
246
+ // BEP 42: echo the requester's external address as a top-level "ip" field
247
+ // (compact 6-byte IPv4 / 18-byte IPv6 ip+port).
248
+ if (!message.external_ip.empty()) {
249
+ std::string compact = compact_peer_info(Peer(message.external_ip, message.external_port));
250
+ if (!compact.empty()) {
251
+ root["ip"] = BencodeValue(compact);
252
+ }
253
+ }
254
+
225
255
  return root;
226
256
  }
227
257
 
@@ -295,7 +325,17 @@ std::unique_ptr<KrpcMessage> KrpcProtocol::decode_query(const BencodeValue& data
295
325
  }
296
326
 
297
327
  message->sender_id = string_to_node_id(args["id"].as_string());
298
-
328
+
329
+ // BEP 32: optional "want" list specifying desired node families
330
+ if (args.has_key("want")) {
331
+ const BencodeValue& want_list = args["want"];
332
+ if (want_list.is_list()) {
333
+ for (size_t i = 0; i < want_list.size(); ++i) {
334
+ message->want.push_back(want_list[i].as_string());
335
+ }
336
+ }
337
+ }
338
+
299
339
  switch (message->query_type) {
300
340
  case KrpcQueryType::Ping:
301
341
  // No additional arguments
@@ -347,10 +387,15 @@ std::unique_ptr<KrpcMessage> KrpcProtocol::decode_response(const BencodeValue& d
347
387
 
348
388
  message->response_id = string_to_node_id(response["id"].as_string());
349
389
 
350
- // Parse nodes if present
390
+ // Parse nodes if present (BEP 32: "nodes" = IPv4, "nodes6" = IPv6)
351
391
  if (response.has_key("nodes")) {
352
392
  std::string compact_nodes = response["nodes"].as_string();
353
- message->nodes = parse_compact_node_info(compact_nodes);
393
+ message->nodes = parse_compact_node_info(compact_nodes, /*ipv6=*/false);
394
+ }
395
+ if (response.has_key("nodes6")) {
396
+ std::string compact_nodes6 = response["nodes6"].as_string();
397
+ auto nodes6 = parse_compact_node_info(compact_nodes6, /*ipv6=*/true);
398
+ message->nodes.insert(message->nodes.end(), nodes6.begin(), nodes6.end());
354
399
  }
355
400
 
356
401
  // Parse peers if present
@@ -369,7 +414,16 @@ std::unique_ptr<KrpcMessage> KrpcProtocol::decode_response(const BencodeValue& d
369
414
  if (response.has_key("token")) {
370
415
  message->token = response["token"].as_string();
371
416
  }
372
-
417
+
418
+ // BEP 42: top-level "ip" field tells us our external address as the responder sees it.
419
+ if (data.has_key("ip")) {
420
+ auto endpoints = parse_compact_peer_info(data["ip"].as_string());
421
+ if (!endpoints.empty()) {
422
+ message->external_ip = endpoints[0].ip;
423
+ message->external_port = endpoints[0].port;
424
+ }
425
+ }
426
+
373
427
  return message;
374
428
  }
375
429
 
@@ -415,132 +469,143 @@ NodeId KrpcProtocol::string_to_node_id(const std::string& str) {
415
469
  return id;
416
470
  }
417
471
 
418
- std::string KrpcProtocol::compact_peer_info(const Peer& peer) {
419
- std::string result;
420
- result.reserve(6);
421
-
422
- // Convert IP address to 4 bytes
423
- if (network_utils::is_valid_ipv4(peer.ip)) {
424
- struct in_addr addr;
425
- if (inet_pton(AF_INET, peer.ip.c_str(), &addr) == 1) {
426
- uint32_t ip = ntohl(addr.s_addr);
427
- result += static_cast<char>((ip >> 24) & 0xFF);
428
- result += static_cast<char>((ip >> 16) & 0xFF);
429
- result += static_cast<char>((ip >> 8) & 0xFF);
430
- result += static_cast<char>(ip & 0xFF);
431
- } else {
432
- // Invalid IP, use 0.0.0.0
433
- result += "\x00\x00\x00\x00";
472
+ // Append a compact IP address (4 bytes for IPv4, 16 bytes for IPv6) to `out`.
473
+ // Returns the number of address bytes written, or 0 on failure.
474
+ static size_t append_compact_address(const std::string& ip, std::string& out) {
475
+ if (network_utils::is_valid_ipv6(ip)) {
476
+ struct in6_addr addr6;
477
+ if (inet_pton(AF_INET6, ip.c_str(), &addr6) == 1) {
478
+ out.append(reinterpret_cast<const char*>(addr6.s6_addr), 16);
479
+ return 16;
434
480
  }
435
- } else {
436
- // Not IPv4, use 0.0.0.0
437
- result += "\x00\x00\x00\x00";
481
+ return 0;
438
482
  }
439
-
483
+
484
+ struct in_addr addr;
485
+ if (inet_pton(AF_INET, ip.c_str(), &addr) == 1) {
486
+ uint32_t v = ntohl(addr.s_addr);
487
+ out += static_cast<char>((v >> 24) & 0xFF);
488
+ out += static_cast<char>((v >> 16) & 0xFF);
489
+ out += static_cast<char>((v >> 8) & 0xFF);
490
+ out += static_cast<char>(v & 0xFF);
491
+ return 4;
492
+ }
493
+ // Invalid IPv4, use 0.0.0.0
494
+ out.append(4, '\x00');
495
+ return 4;
496
+ }
497
+
498
+ // Read a compact IP address of `addr_len` bytes (4=IPv4, 16=IPv6) at compact_info[offset]
499
+ // into a printable string.
500
+ static std::string read_compact_address(const std::string& compact_info, size_t offset, size_t addr_len) {
501
+ if (addr_len == 16) {
502
+ struct in6_addr addr6;
503
+ memcpy(addr6.s6_addr, compact_info.data() + offset, 16);
504
+ char ip_str[INET6_ADDRSTRLEN];
505
+ inet_ntop(AF_INET6, &addr6, ip_str, INET6_ADDRSTRLEN);
506
+ return std::string(ip_str);
507
+ }
508
+
509
+ struct in_addr addr;
510
+ uint32_t ip = 0;
511
+ ip |= (static_cast<uint8_t>(compact_info[offset]) << 24);
512
+ ip |= (static_cast<uint8_t>(compact_info[offset + 1]) << 16);
513
+ ip |= (static_cast<uint8_t>(compact_info[offset + 2]) << 8);
514
+ ip |= static_cast<uint8_t>(compact_info[offset + 3]);
515
+ addr.s_addr = htonl(ip);
516
+ char ip_str[INET_ADDRSTRLEN];
517
+ inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
518
+ return std::string(ip_str);
519
+ }
520
+
521
+ std::string KrpcProtocol::compact_peer_info(const Peer& peer) {
522
+ std::string result;
523
+ result.reserve(18);
524
+
525
+ // IP address (4 bytes IPv4 / 16 bytes IPv6)
526
+ append_compact_address(peer.ip, result);
527
+
440
528
  // Convert port to 2 bytes (network byte order)
441
529
  result += static_cast<char>((peer.port >> 8) & 0xFF);
442
530
  result += static_cast<char>(peer.port & 0xFF);
443
-
531
+
444
532
  return result;
445
533
  }
446
534
 
447
535
  std::string KrpcProtocol::compact_node_info(const KrpcNode& node) {
448
536
  std::string result;
449
- result.reserve(26);
450
-
537
+ result.reserve(38);
538
+
451
539
  // Node ID (20 bytes)
452
540
  result += node_id_to_string(node.id);
453
-
454
- // IP address (4 bytes)
455
- if (network_utils::is_valid_ipv4(node.ip)) {
456
- struct in_addr addr;
457
- if (inet_pton(AF_INET, node.ip.c_str(), &addr) == 1) {
458
- uint32_t ip = ntohl(addr.s_addr);
459
- result += static_cast<char>((ip >> 24) & 0xFF);
460
- result += static_cast<char>((ip >> 16) & 0xFF);
461
- result += static_cast<char>((ip >> 8) & 0xFF);
462
- result += static_cast<char>(ip & 0xFF);
463
- } else {
464
- // Invalid IP, use 0.0.0.0
465
- result += "\x00\x00\x00\x00";
466
- }
467
- } else {
468
- // Not IPv4, use 0.0.0.0
469
- result += "\x00\x00\x00\x00";
470
- }
471
-
541
+
542
+ // IP address (4 bytes IPv4 / 16 bytes IPv6)
543
+ append_compact_address(node.ip, result);
544
+
472
545
  // Port (2 bytes, network byte order)
473
546
  result += static_cast<char>((node.port >> 8) & 0xFF);
474
547
  result += static_cast<char>(node.port & 0xFF);
475
-
548
+
476
549
  return result;
477
550
  }
478
551
 
479
552
  std::vector<Peer> KrpcProtocol::parse_compact_peer_info(const std::string& compact_info) {
480
553
  std::vector<Peer> peers;
481
-
482
- if (compact_info.size() % 6 != 0) {
554
+
555
+ // BEP 5/BEP 7: each "values" entry is a single compact peer: 6 bytes (IPv4) or 18 bytes (IPv6).
556
+ // Detect the family from the entry length. An exact length of 18 is treated as one IPv6 peer;
557
+ // otherwise we chunk by 6 (some implementations concatenate multiple IPv4 peers).
558
+ size_t record_size = 6;
559
+ size_t addr_len = 4;
560
+ if (compact_info.size() == 18) {
561
+ record_size = 18;
562
+ addr_len = 16;
563
+ } else if (compact_info.size() % 6 != 0) {
483
564
  LOG_KRPC_WARN("Invalid compact peer info size: " << compact_info.size());
484
565
  return peers;
485
566
  }
486
-
487
- for (size_t i = 0; i < compact_info.size(); i += 6) {
488
- // Extract IP address (4 bytes)
489
- uint32_t ip = 0;
490
- ip |= (static_cast<uint8_t>(compact_info[i]) << 24);
491
- ip |= (static_cast<uint8_t>(compact_info[i + 1]) << 16);
492
- ip |= (static_cast<uint8_t>(compact_info[i + 2]) << 8);
493
- ip |= static_cast<uint8_t>(compact_info[i + 3]);
494
-
495
- struct in_addr addr;
496
- addr.s_addr = htonl(ip);
497
- char ip_str[INET_ADDRSTRLEN];
498
- inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
499
-
500
- // Extract port (2 bytes)
567
+
568
+ for (size_t i = 0; i + record_size <= compact_info.size(); i += record_size) {
569
+ std::string ip_str = read_compact_address(compact_info, i, addr_len);
570
+
501
571
  uint16_t port = 0;
502
- port |= (static_cast<uint8_t>(compact_info[i + 4]) << 8);
503
- port |= static_cast<uint8_t>(compact_info[i + 5]);
504
-
572
+ port |= (static_cast<uint8_t>(compact_info[i + addr_len]) << 8);
573
+ port |= static_cast<uint8_t>(compact_info[i + addr_len + 1]);
574
+
505
575
  peers.emplace_back(ip_str, port);
506
576
  }
507
-
577
+
508
578
  return peers;
509
579
  }
510
580
 
511
- std::vector<KrpcNode> KrpcProtocol::parse_compact_node_info(const std::string& compact_info) {
581
+ std::vector<KrpcNode> KrpcProtocol::parse_compact_node_info(const std::string& compact_info, bool ipv6) {
512
582
  std::vector<KrpcNode> nodes;
513
-
514
- if (compact_info.size() % 26 != 0) {
515
- LOG_KRPC_WARN("Invalid compact node info size: " << compact_info.size());
583
+
584
+ const size_t addr_len = ipv6 ? 16 : 4;
585
+ const size_t record_size = 20 + addr_len + 2; // 26 (IPv4) or 38 (IPv6)
586
+
587
+ if (compact_info.size() % record_size != 0) {
588
+ LOG_KRPC_WARN("Invalid compact node info size: " << compact_info.size()
589
+ << " (expected multiple of " << record_size << ")");
516
590
  return nodes;
517
591
  }
518
-
519
- for (size_t i = 0; i < compact_info.size(); i += 26) {
592
+
593
+ for (size_t i = 0; i < compact_info.size(); i += record_size) {
520
594
  // Extract node ID (20 bytes)
521
595
  NodeId node_id;
522
596
  std::copy_n(compact_info.begin() + i, 20, node_id.begin());
523
-
524
- // Extract IP address (4 bytes)
525
- uint32_t ip = 0;
526
- ip |= (static_cast<uint8_t>(compact_info[i + 20]) << 24);
527
- ip |= (static_cast<uint8_t>(compact_info[i + 21]) << 16);
528
- ip |= (static_cast<uint8_t>(compact_info[i + 22]) << 8);
529
- ip |= static_cast<uint8_t>(compact_info[i + 23]);
530
-
531
- struct in_addr addr;
532
- addr.s_addr = htonl(ip);
533
- char ip_str[INET_ADDRSTRLEN];
534
- inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
535
-
597
+
598
+ // Extract IP address (4 or 16 bytes)
599
+ std::string ip_str = read_compact_address(compact_info, i + 20, addr_len);
600
+
536
601
  // Extract port (2 bytes)
537
602
  uint16_t port = 0;
538
- port |= (static_cast<uint8_t>(compact_info[i + 24]) << 8);
539
- port |= static_cast<uint8_t>(compact_info[i + 25]);
540
-
603
+ port |= (static_cast<uint8_t>(compact_info[i + 20 + addr_len]) << 8);
604
+ port |= static_cast<uint8_t>(compact_info[i + 20 + addr_len + 1]);
605
+
541
606
  nodes.emplace_back(node_id, ip_str, port);
542
607
  }
543
-
608
+
544
609
  return nodes;
545
610
  }
546
611
 
@@ -71,15 +71,26 @@ struct KrpcMessage {
71
71
  bool implied_port; // BEP 5: if true, use UDP source port instead of 'port' field
72
72
  std::string token;
73
73
 
74
+ // BEP 32: requested node families for queries ("n4" and/or "n6").
75
+ // Empty means "no want specified" (responder defaults to the family the request arrived on).
76
+ std::vector<std::string> want;
77
+
74
78
  // For responses
75
79
  NodeId response_id;
76
- std::vector<KrpcNode> nodes;
80
+ std::vector<KrpcNode> nodes; // may mix IPv4/IPv6 nodes; encoder splits into nodes/nodes6
77
81
  std::vector<Peer> peers;
78
-
82
+
83
+ // BEP 42: top-level "ip" field (compact ip+port).
84
+ // On responses we SEND: the requester's external address, so they can learn how the
85
+ // network sees them and derive a compliant node ID.
86
+ // On responses we RECEIVE: our own external address as observed by the responder.
87
+ std::string external_ip;
88
+ uint16_t external_port = 0;
89
+
79
90
  // For errors
80
91
  KrpcErrorCode error_code;
81
92
  std::string error_message;
82
-
93
+
83
94
  KrpcMessage() : type(KrpcMessageType::Query), query_type(KrpcQueryType::Ping), sender_id(), target_id(), info_hash(), port(0), implied_port(false), response_id(), error_code(KrpcErrorCode::GenericError) {}
84
95
  };
85
96
 
@@ -123,10 +134,13 @@ public:
123
134
  */
124
135
  static std::string node_id_to_string(const NodeId& id);
125
136
  static NodeId string_to_node_id(const std::string& str);
137
+ // Compact encodings auto-select IPv4 (6/26 bytes) or IPv6 (18/38 bytes) based on the address.
126
138
  static std::string compact_peer_info(const Peer& peer);
127
139
  static std::string compact_node_info(const KrpcNode& node);
140
+ // parse_compact_peer_info detects the family from the string length (one peer per string).
128
141
  static std::vector<Peer> parse_compact_peer_info(const std::string& compact_info);
129
- static std::vector<KrpcNode> parse_compact_node_info(const std::string& compact_info);
142
+ // parse_compact_node_info reads fixed-size records; pass ipv6=true for 38-byte ("nodes6") records.
143
+ static std::vector<KrpcNode> parse_compact_node_info(const std::string& compact_info, bool ipv6 = false);
130
144
 
131
145
  private:
132
146
  static BencodeValue encode_query(const KrpcMessage& message);