librats 0.8.0 → 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.
- package/README.md +9 -4
- package/lib/index.d.ts +9 -22
- package/native-src/CMakeLists.txt +122 -48
- package/native-src/cmake/ratsConfig.cmake.in +8 -0
- package/native-src/src/crypto/sha256.c +1 -1
- package/native-src/src/dht.cpp +297 -47
- package/native-src/src/dht.h +70 -6
- package/native-src/src/file_transfer.cpp +1185 -1578
- package/native-src/src/file_transfer.h +240 -521
- package/native-src/src/io_poller.cpp +7 -0
- package/native-src/src/krpc.cpp +161 -96
- package/native-src/src/krpc.h +18 -4
- package/native-src/src/librats.cpp +3 -0
- package/native-src/src/librats.h +61 -154
- package/native-src/src/librats_c.cpp +21 -38
- package/native-src/src/librats_discovery.cpp +107 -39
- package/native-src/src/librats_file_transfer.cpp +25 -103
- package/native-src/src/librats_ice.cpp +4 -0
- package/native-src/src/librats_statistic.cpp +10 -2
- package/native-src/src/socket.cpp +15 -3
- package/package.json +1 -1
- package/scripts/build-librats.js +3 -0
- package/scripts/prepare-package.js +10 -0
- package/src/librats_node.cpp +53 -64
package/native-src/src/dht.cpp
CHANGED
|
@@ -28,17 +28,19 @@
|
|
|
28
28
|
namespace librats {
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
DhtClient::DhtClient(int port, const std::string& bind_address, const std::string& data_directory
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
DhtClient::DhtClient(int port, const std::string& bind_address, const std::string& data_directory,
|
|
32
|
+
AddressFamily address_family)
|
|
33
|
+
: port_(port), bind_address_(bind_address), data_directory_(data_directory),
|
|
34
|
+
address_family_(address_family), socket_(INVALID_SOCKET_VALUE), running_(false) {
|
|
34
35
|
node_id_ = generate_node_id();
|
|
35
36
|
routing_table_.resize(NODE_ID_SIZE * 8); // 160 buckets for 160-bit node IDs
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
if (data_directory_.empty()) {
|
|
38
39
|
data_directory_ = ".";
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
LOG_DHT_INFO("DHT client created with node ID: " << node_id_to_hex(node_id_) <<
|
|
43
|
+
" family: " << (is_ipv6() ? "IPv6" : "IPv4") <<
|
|
42
44
|
(bind_address_.empty() ? "" : " bind address: " + bind_address_) <<
|
|
43
45
|
" data directory: " << data_directory_);
|
|
44
46
|
}
|
|
@@ -61,20 +63,22 @@ bool DhtClient::start() {
|
|
|
61
63
|
return false;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
// Each family gets its own single-family socket. IPv6 uses V6ONLY so the two
|
|
67
|
+
// sockets can share the same port and never cross-deliver mapped addresses.
|
|
68
|
+
socket_ = create_udp_socket(port_, bind_address_, address_family_);
|
|
65
69
|
// Fallback to free port
|
|
66
70
|
if (!is_valid_socket(socket_) && port_ > 0) {
|
|
67
71
|
// Requested port is not available, try ephemeral port as fallback
|
|
68
72
|
int original_port = port_;
|
|
69
73
|
LOG_DHT_WARN("UDP port " << original_port << " is not available, falling back to ephemeral port");
|
|
70
|
-
socket_ = create_udp_socket(0, bind_address_);
|
|
74
|
+
socket_ = create_udp_socket(0, bind_address_, address_family_);
|
|
71
75
|
if (is_valid_socket(socket_)) {
|
|
72
76
|
port_ = get_bound_port(socket_);
|
|
73
77
|
LOG_DHT_INFO("Fell back from port " << original_port << " to ephemeral port " << port_);
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
if (!is_valid_socket(socket_)) {
|
|
77
|
-
LOG_DHT_ERROR("Failed to create
|
|
81
|
+
LOG_DHT_ERROR("Failed to create " << (is_ipv6() ? "IPv6" : "IPv4") << " UDP socket on port " << port_);
|
|
78
82
|
return false;
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -378,10 +382,14 @@ size_t DhtClient::get_active_announces_count() const {
|
|
|
378
382
|
}
|
|
379
383
|
|
|
380
384
|
std::vector<Peer> DhtClient::get_default_bootstrap_nodes() {
|
|
385
|
+
// Hostnames are resolved to the instance's family at send time (A for IPv4, AAAA for
|
|
386
|
+
// IPv6). dht.libtorrent.org publishes an AAAA record, giving the IPv6 network a
|
|
387
|
+
// reliable bootstrap entry point.
|
|
381
388
|
return {
|
|
382
389
|
{"router.bittorrent.com", 6881},
|
|
383
390
|
{"dht.transmissionbt.com", 6881},
|
|
384
391
|
{"router.utorrent.com", 6881},
|
|
392
|
+
{"dht.libtorrent.org", 25401},
|
|
385
393
|
{"dht.aelitis.com", 6881}
|
|
386
394
|
};
|
|
387
395
|
}
|
|
@@ -509,6 +517,14 @@ void DhtClient::handle_message(const std::vector<uint8_t>& data, const Peer& sen
|
|
|
509
517
|
}
|
|
510
518
|
|
|
511
519
|
void DhtClient::add_node(const DhtNode& node, bool confirmed, bool no_verify) {
|
|
520
|
+
// IPv4 and IPv6 are separate Kademlia networks (BEP 32). Reject any node whose
|
|
521
|
+
// address family does not match this instance to keep the routing table clean.
|
|
522
|
+
if (network_utils::is_valid_ipv6(node.peer.ip) != is_ipv6()) {
|
|
523
|
+
LOG_DHT_DEBUG("Ignoring " << (is_ipv6() ? "non-IPv6" : "non-IPv4") << " node "
|
|
524
|
+
<< node.peer.ip << ":" << node.peer.port << " (wrong family)");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
512
528
|
std::lock_guard<std::mutex> ping_lock(pending_pings_mutex_);
|
|
513
529
|
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
514
530
|
|
|
@@ -980,7 +996,14 @@ void DhtClient::handle_krpc_announce_peer(const KrpcMessage& message, const Peer
|
|
|
980
996
|
|
|
981
997
|
void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sender) {
|
|
982
998
|
LOG_DHT_DEBUG("Handling KRPC response from " << sender.ip << ":" << sender.port);
|
|
983
|
-
|
|
999
|
+
|
|
1000
|
+
// BEP 42: nodes echo our external address back in the top-level "ip" field. Responses
|
|
1001
|
+
// here are always replies to queries we sent, so the source is a node we chose to contact;
|
|
1002
|
+
// we still require a consensus of distinct responders before regenerating our node ID.
|
|
1003
|
+
if (!message.external_ip.empty()) {
|
|
1004
|
+
maybe_update_external_ip(message.external_ip, sender);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
984
1007
|
// Check if this is a ping verification response before normal processing
|
|
985
1008
|
handle_ping_verification_response(message.transaction_id, message.response_id, sender);
|
|
986
1009
|
|
|
@@ -1077,14 +1100,25 @@ void DhtClient::handle_krpc_error(const KrpcMessage& message, const Peer& sender
|
|
|
1077
1100
|
|
|
1078
1101
|
// KRPC sending functions
|
|
1079
1102
|
bool DhtClient::send_krpc_message(const KrpcMessage& message, const Peer& peer) {
|
|
1080
|
-
|
|
1103
|
+
// BEP 42: stamp the requester's external address into every response we send so the
|
|
1104
|
+
// remote node can derive an IP-based node ID. Only copy the message when needed.
|
|
1105
|
+
const KrpcMessage* out = &message;
|
|
1106
|
+
KrpcMessage stamped;
|
|
1107
|
+
if (message.type == KrpcMessageType::Response && message.external_ip.empty()) {
|
|
1108
|
+
stamped = message;
|
|
1109
|
+
stamped.external_ip = peer.ip;
|
|
1110
|
+
stamped.external_port = peer.port;
|
|
1111
|
+
out = &stamped;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
auto data = KrpcProtocol::encode_message(*out);
|
|
1081
1115
|
if (data.empty()) {
|
|
1082
1116
|
LOG_DHT_ERROR("Failed to encode KRPC message");
|
|
1083
1117
|
return false;
|
|
1084
1118
|
}
|
|
1085
1119
|
|
|
1086
1120
|
LOG_DHT_DEBUG("Sending KRPC message (" << data.size() << " bytes) to " << peer.ip << ":" << peer.port);
|
|
1087
|
-
int result = send_udp_data(socket_, data, peer.ip, peer.port);
|
|
1121
|
+
int result = send_udp_data(socket_, data, peer.ip, peer.port, address_family_);
|
|
1088
1122
|
|
|
1089
1123
|
if (result > 0) {
|
|
1090
1124
|
LOG_DHT_DEBUG("Successfully sent KRPC message to " << peer.ip << ":" << peer.port);
|
|
@@ -1104,12 +1138,16 @@ void DhtClient::send_krpc_ping(const Peer& peer) {
|
|
|
1104
1138
|
void DhtClient::send_krpc_find_node(const Peer& peer, const NodeId& target) {
|
|
1105
1139
|
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
1106
1140
|
auto message = KrpcProtocol::create_find_node_query(transaction_id, node_id_, target);
|
|
1141
|
+
// BEP 32: ask only for nodes of our own family (separate Kademlia networks)
|
|
1142
|
+
message.want.push_back(is_ipv6() ? "n6" : "n4");
|
|
1107
1143
|
send_krpc_message(message, peer);
|
|
1108
1144
|
}
|
|
1109
1145
|
|
|
1110
1146
|
void DhtClient::send_krpc_get_peers(const Peer& peer, const InfoHash& info_hash) {
|
|
1111
1147
|
std::string transaction_id = KrpcProtocol::generate_transaction_id();
|
|
1112
1148
|
auto message = KrpcProtocol::create_get_peers_query(transaction_id, node_id_, info_hash);
|
|
1149
|
+
// BEP 32: ask only for nodes of our own family (separate Kademlia networks)
|
|
1150
|
+
message.want.push_back(is_ipv6() ? "n6" : "n4");
|
|
1113
1151
|
send_krpc_message(message, peer);
|
|
1114
1152
|
}
|
|
1115
1153
|
|
|
@@ -1152,14 +1190,218 @@ NodeId DhtClient::generate_node_id() {
|
|
|
1152
1190
|
std::random_device rd;
|
|
1153
1191
|
std::mt19937 gen(rd());
|
|
1154
1192
|
std::uniform_int_distribution<> dis(0, 255);
|
|
1155
|
-
|
|
1193
|
+
|
|
1156
1194
|
for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
|
|
1157
1195
|
id[i] = dis(gen);
|
|
1158
1196
|
}
|
|
1159
|
-
|
|
1197
|
+
|
|
1160
1198
|
return id;
|
|
1161
1199
|
}
|
|
1162
1200
|
|
|
1201
|
+
// ============================================================================
|
|
1202
|
+
// BEP 42: deriving the node ID from the external IP address
|
|
1203
|
+
// ============================================================================
|
|
1204
|
+
namespace {
|
|
1205
|
+
|
|
1206
|
+
// CRC-32C (Castagnoli) — the polynomial mandated by BEP 42. Standard parameters:
|
|
1207
|
+
// reflected, init 0xFFFFFFFF, final XOR 0xFFFFFFFF. Bytes are processed in array order,
|
|
1208
|
+
// matching the BEP 42 reference implementation and its published test vectors.
|
|
1209
|
+
uint32_t crc32c(const uint8_t* data, size_t len) {
|
|
1210
|
+
uint32_t crc = 0xFFFFFFFFu;
|
|
1211
|
+
for (size_t i = 0; i < len; ++i) {
|
|
1212
|
+
crc ^= data[i];
|
|
1213
|
+
for (int k = 0; k < 8; ++k) {
|
|
1214
|
+
crc = (crc & 1u) ? (crc >> 1) ^ 0x82F63B78u : (crc >> 1);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return crc ^ 0xFFFFFFFFu;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Mask the leading octets of an IP per BEP 42 (4 octets for IPv4, 8 for IPv6).
|
|
1221
|
+
// Returns the number of octets written to `out`, or 0 on a parse failure.
|
|
1222
|
+
int masked_octets(const std::string& ip, uint8_t out[8]) {
|
|
1223
|
+
static const uint8_t v4_mask[4] = { 0x03, 0x0f, 0x3f, 0xff };
|
|
1224
|
+
static const uint8_t v6_mask[8] = { 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff };
|
|
1225
|
+
|
|
1226
|
+
if (network_utils::is_valid_ipv6(ip)) {
|
|
1227
|
+
struct in6_addr addr6;
|
|
1228
|
+
if (inet_pton(AF_INET6, ip.c_str(), &addr6) != 1) return 0;
|
|
1229
|
+
for (int i = 0; i < 8; ++i) out[i] = addr6.s6_addr[i] & v6_mask[i];
|
|
1230
|
+
return 8;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
struct in_addr addr;
|
|
1234
|
+
if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) return 0;
|
|
1235
|
+
uint32_t h = ntohl(addr.s_addr);
|
|
1236
|
+
uint8_t ipb[4] = {
|
|
1237
|
+
static_cast<uint8_t>((h >> 24) & 0xff), static_cast<uint8_t>((h >> 16) & 0xff),
|
|
1238
|
+
static_cast<uint8_t>((h >> 8) & 0xff), static_cast<uint8_t>(h & 0xff)
|
|
1239
|
+
};
|
|
1240
|
+
for (int i = 0; i < 4; ++i) out[i] = ipb[i] & v4_mask[i];
|
|
1241
|
+
return 4;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Compute the 3 deterministic prefix bytes of a BEP 42 node ID for (ip, seed).
|
|
1245
|
+
// Only the high 5 bits of prefix[2] are deterministic (the low 3 bits are random in a real ID).
|
|
1246
|
+
bool bep42_prefix(const std::string& ip, uint8_t seed, uint8_t prefix[3]) {
|
|
1247
|
+
uint8_t octets[8];
|
|
1248
|
+
int num = masked_octets(ip, octets);
|
|
1249
|
+
if (num == 0) return false;
|
|
1250
|
+
octets[0] |= static_cast<uint8_t>((seed & 0x7) << 5);
|
|
1251
|
+
uint32_t c = crc32c(octets, static_cast<size_t>(num));
|
|
1252
|
+
prefix[0] = static_cast<uint8_t>((c >> 24) & 0xff);
|
|
1253
|
+
prefix[1] = static_cast<uint8_t>((c >> 16) & 0xff);
|
|
1254
|
+
prefix[2] = static_cast<uint8_t>((c >> 8) & 0xf8);
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
} // namespace
|
|
1259
|
+
|
|
1260
|
+
bool DhtClient::is_public_address(const std::string& ip) {
|
|
1261
|
+
if (ip.empty()) return false;
|
|
1262
|
+
|
|
1263
|
+
if (network_utils::is_valid_ipv6(ip)) {
|
|
1264
|
+
struct in6_addr a;
|
|
1265
|
+
if (inet_pton(AF_INET6, ip.c_str(), &a) != 1) return false;
|
|
1266
|
+
const uint8_t* b = a.s6_addr;
|
|
1267
|
+
bool all_zero = true;
|
|
1268
|
+
for (int i = 0; i < 16; ++i) { if (b[i]) { all_zero = false; break; } }
|
|
1269
|
+
if (all_zero) return false; // :: (unspecified)
|
|
1270
|
+
bool loopback = (b[15] == 1);
|
|
1271
|
+
for (int i = 0; i < 15; ++i) { if (b[i]) { loopback = false; break; } }
|
|
1272
|
+
if (loopback) return false; // ::1
|
|
1273
|
+
if ((b[0] & 0xfe) == 0xfc) return false; // fc00::/7 unique local
|
|
1274
|
+
if (b[0] == 0xfe && (b[1] & 0xc0) == 0x80) return false; // fe80::/10 link-local
|
|
1275
|
+
if (b[0] == 0xff) return false; // ff00::/8 multicast
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
struct in_addr a;
|
|
1280
|
+
if (inet_pton(AF_INET, ip.c_str(), &a) != 1) return false;
|
|
1281
|
+
uint32_t h = ntohl(a.s_addr);
|
|
1282
|
+
uint8_t o1 = static_cast<uint8_t>((h >> 24) & 0xff);
|
|
1283
|
+
uint8_t o2 = static_cast<uint8_t>((h >> 16) & 0xff);
|
|
1284
|
+
if (o1 == 0) return false; // 0.0.0.0/8
|
|
1285
|
+
if (o1 == 127) return false; // loopback
|
|
1286
|
+
if (o1 == 10) return false; // 10.0.0.0/8
|
|
1287
|
+
if (o1 == 172 && o2 >= 16 && o2 <= 31) return false; // 172.16.0.0/12
|
|
1288
|
+
if (o1 == 192 && o2 == 168) return false; // 192.168.0.0/16
|
|
1289
|
+
if (o1 == 169 && o2 == 254) return false; // 169.254.0.0/16 link-local
|
|
1290
|
+
if (o1 == 100 && o2 >= 64 && o2 <= 127) return false; // 100.64.0.0/10 CGNAT
|
|
1291
|
+
if (o1 >= 224) return false; // multicast / reserved
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
bool DhtClient::generate_node_id_from_ip(const std::string& ip, NodeId& out, std::mt19937& gen) {
|
|
1296
|
+
uint8_t prefix[3];
|
|
1297
|
+
std::uniform_int_distribution<int> dis(0, 255);
|
|
1298
|
+
uint8_t seed = static_cast<uint8_t>(dis(gen));
|
|
1299
|
+
if (!bep42_prefix(ip, seed, prefix)) return false;
|
|
1300
|
+
|
|
1301
|
+
out[0] = prefix[0];
|
|
1302
|
+
out[1] = prefix[1];
|
|
1303
|
+
out[2] = static_cast<uint8_t>(prefix[2] | (dis(gen) & 0x7)); // low 3 bits are random
|
|
1304
|
+
for (int i = 3; i < 19; ++i) out[i] = static_cast<uint8_t>(dis(gen));
|
|
1305
|
+
out[19] = seed;
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
bool DhtClient::verify_node_id_for_ip(const NodeId& id, const std::string& ip) {
|
|
1310
|
+
// Local/private addresses cannot be verified (their IDs would be "wrong" anyway).
|
|
1311
|
+
if (!is_public_address(ip)) return true;
|
|
1312
|
+
uint8_t prefix[3];
|
|
1313
|
+
if (!bep42_prefix(ip, id[19], prefix)) return true; // unparseable -> don't reject
|
|
1314
|
+
return id[0] == prefix[0]
|
|
1315
|
+
&& id[1] == prefix[1]
|
|
1316
|
+
&& (id[2] & 0xf8) == prefix[2];
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
void DhtClient::rebuild_routing_table_unlocked() {
|
|
1320
|
+
// node_id_ changed; every node's bucket index is now stale. Re-bucket all of them.
|
|
1321
|
+
std::vector<DhtNode> all;
|
|
1322
|
+
for (auto& bucket : routing_table_) {
|
|
1323
|
+
for (auto& node : bucket) all.push_back(std::move(node));
|
|
1324
|
+
bucket.clear();
|
|
1325
|
+
}
|
|
1326
|
+
for (auto& node : all) {
|
|
1327
|
+
int bucket_index = get_bucket_index(node.id);
|
|
1328
|
+
if (bucket_index < 0 || bucket_index >= static_cast<int>(routing_table_.size())) continue;
|
|
1329
|
+
auto& bucket = routing_table_[bucket_index];
|
|
1330
|
+
if (bucket.size() < K_BUCKET_SIZE) bucket.push_back(std::move(node));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
void DhtClient::set_external_ip(const std::string& ip) {
|
|
1335
|
+
// Only public addresses of this instance's family can derive a valid node ID.
|
|
1336
|
+
if (!is_public_address(ip)) return;
|
|
1337
|
+
if (network_utils::is_valid_ipv6(ip) != is_ipv6()) return;
|
|
1338
|
+
|
|
1339
|
+
{
|
|
1340
|
+
std::lock_guard<std::mutex> ext_lock(external_ip_mutex_);
|
|
1341
|
+
if (ip == external_address_) return; // already using this address
|
|
1342
|
+
external_address_ = ip;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
1346
|
+
// If our current ID already matches this IP, keep it (avoids needless churn).
|
|
1347
|
+
if (verify_node_id_for_ip(node_id_, ip)) {
|
|
1348
|
+
LOG_DHT_DEBUG("External IP " << ip << " already matches current node ID (BEP 42)");
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
std::random_device rd;
|
|
1353
|
+
std::mt19937 gen(rd());
|
|
1354
|
+
NodeId new_id;
|
|
1355
|
+
if (!generate_node_id_from_ip(ip, new_id, gen)) return;
|
|
1356
|
+
|
|
1357
|
+
NodeId old_id = node_id_;
|
|
1358
|
+
node_id_ = new_id;
|
|
1359
|
+
rebuild_routing_table_unlocked();
|
|
1360
|
+
LOG_DHT_INFO("Regenerated DHT node ID from external IP " << ip << " (BEP 42): "
|
|
1361
|
+
<< node_id_to_hex(old_id) << " -> " << node_id_to_hex(new_id));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
std::string DhtClient::get_external_address() const {
|
|
1365
|
+
std::lock_guard<std::mutex> lock(external_ip_mutex_);
|
|
1366
|
+
return external_address_;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
void DhtClient::maybe_update_external_ip(const std::string& reported_ip, const Peer& responder) {
|
|
1370
|
+
// Only consider public addresses of our own family.
|
|
1371
|
+
if (!is_public_address(reported_ip)) return;
|
|
1372
|
+
if (network_utils::is_valid_ipv6(reported_ip) != is_ipv6()) return;
|
|
1373
|
+
|
|
1374
|
+
std::string winner;
|
|
1375
|
+
{
|
|
1376
|
+
std::lock_guard<std::mutex> lock(external_ip_mutex_);
|
|
1377
|
+
if (reported_ip == external_address_) return; // already adopted
|
|
1378
|
+
|
|
1379
|
+
// One vote per distinct responder so a single peer cannot force a change.
|
|
1380
|
+
if (external_ip_voters_.size() >= MAX_EXTERNAL_IP_VOTERS) {
|
|
1381
|
+
external_ip_voters_.clear();
|
|
1382
|
+
external_ip_votes_.clear();
|
|
1383
|
+
}
|
|
1384
|
+
if (!external_ip_voters_.insert(responder.ip).second) return; // this responder already voted
|
|
1385
|
+
|
|
1386
|
+
int votes = ++external_ip_votes_[reported_ip];
|
|
1387
|
+
// A vote for an address other than our current one is genuinely interesting
|
|
1388
|
+
// (startup ramp-up, or a possible address change), so surface it at INFO.
|
|
1389
|
+
LOG_DHT_INFO("External IP vote from " << responder.ip << " -> " << reported_ip
|
|
1390
|
+
<< " (" << votes << "/" << EXTERNAL_IP_VOTE_THRESHOLD << ")");
|
|
1391
|
+
if (votes >= EXTERNAL_IP_VOTE_THRESHOLD) {
|
|
1392
|
+
winner = reported_ip;
|
|
1393
|
+
external_ip_votes_.clear();
|
|
1394
|
+
external_ip_voters_.clear();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (!winner.empty()) {
|
|
1399
|
+
LOG_DHT_INFO("External IP consensus reached: " << winner
|
|
1400
|
+
<< " (>= " << EXTERNAL_IP_VOTE_THRESHOLD << " responders agree)");
|
|
1401
|
+
set_external_ip(winner);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1163
1405
|
NodeId DhtClient::xor_distance(const NodeId& a, const NodeId& b) {
|
|
1164
1406
|
NodeId result;
|
|
1165
1407
|
for (size_t i = 0; i < NODE_ID_SIZE; ++i) {
|
|
@@ -2243,8 +2485,10 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
|
|
|
2243
2485
|
LOG_DHT_DEBUG("Querying node " << node_id_to_hex(node.id) << " at " << node.peer.ip << ":" << node.peer.port);
|
|
2244
2486
|
|
|
2245
2487
|
auto message = KrpcProtocol::create_get_peers_query(transaction_id, node_id_, search.info_hash);
|
|
2488
|
+
// BEP 32: request peers/nodes of our own family only
|
|
2489
|
+
message.want.push_back(is_ipv6() ? "n6" : "n4");
|
|
2246
2490
|
send_krpc_message(message, node.peer);
|
|
2247
|
-
|
|
2491
|
+
|
|
2248
2492
|
queries_sent++;
|
|
2249
2493
|
}
|
|
2250
2494
|
|
|
@@ -2574,12 +2818,29 @@ std::string node_id_to_hex(const NodeId& id) {
|
|
|
2574
2818
|
}
|
|
2575
2819
|
|
|
2576
2820
|
// Routing table persistence implementation
|
|
2821
|
+
std::string DhtClient::routing_table_file_path() const {
|
|
2822
|
+
// IPv6 instance uses a distinct filename suffix so it doesn't collide with the
|
|
2823
|
+
// IPv4 instance bound to the same port.
|
|
2824
|
+
const char* suffix = is_ipv6() ? "_v6" : "";
|
|
2825
|
+
#ifdef TESTING
|
|
2826
|
+
if (port_ == 0) {
|
|
2827
|
+
std::ostringstream oss;
|
|
2828
|
+
oss << "dht_routing_" << this << suffix << ".json";
|
|
2829
|
+
return oss.str();
|
|
2830
|
+
}
|
|
2831
|
+
return "dht_routing_" + std::to_string(port_) + suffix + ".json";
|
|
2832
|
+
#else
|
|
2833
|
+
return data_directory_ + "/dht_routing_" + std::to_string(port_) + suffix + ".json";
|
|
2834
|
+
#endif
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2577
2837
|
bool DhtClient::save_routing_table() {
|
|
2578
2838
|
std::lock_guard<std::mutex> lock(routing_table_mutex_);
|
|
2579
|
-
|
|
2839
|
+
|
|
2580
2840
|
try {
|
|
2581
2841
|
nlohmann::json routing_data;
|
|
2582
2842
|
routing_data["version"] = 1;
|
|
2843
|
+
routing_data["family"] = is_ipv6() ? "ipv6" : "ipv4";
|
|
2583
2844
|
routing_data["node_id"] = node_id_to_hex(node_id_);
|
|
2584
2845
|
routing_data["saved_at"] = std::chrono::system_clock::now().time_since_epoch().count();
|
|
2585
2846
|
|
|
@@ -2609,21 +2870,10 @@ bool DhtClient::save_routing_table() {
|
|
|
2609
2870
|
|
|
2610
2871
|
routing_data["nodes"] = nodes_array;
|
|
2611
2872
|
routing_data["count"] = saved_count;
|
|
2612
|
-
|
|
2873
|
+
|
|
2613
2874
|
// Determine file path
|
|
2614
|
-
std::string file_path;
|
|
2615
|
-
|
|
2616
|
-
if (port_ == 0) {
|
|
2617
|
-
std::ostringstream oss;
|
|
2618
|
-
oss << "dht_routing_" << this << ".json";
|
|
2619
|
-
file_path = oss.str();
|
|
2620
|
-
} else {
|
|
2621
|
-
file_path = "dht_routing_" + std::to_string(port_) + ".json";
|
|
2622
|
-
}
|
|
2623
|
-
#else
|
|
2624
|
-
file_path = data_directory_ + "/dht_routing_" + std::to_string(port_) + ".json";
|
|
2625
|
-
#endif
|
|
2626
|
-
|
|
2875
|
+
std::string file_path = routing_table_file_path();
|
|
2876
|
+
|
|
2627
2877
|
// Write to file
|
|
2628
2878
|
std::ofstream file(file_path);
|
|
2629
2879
|
if (!file.is_open()) {
|
|
@@ -2648,19 +2898,8 @@ bool DhtClient::load_routing_table() {
|
|
|
2648
2898
|
|
|
2649
2899
|
try {
|
|
2650
2900
|
// Determine file path
|
|
2651
|
-
std::string file_path;
|
|
2652
|
-
|
|
2653
|
-
if (port_ == 0) {
|
|
2654
|
-
std::ostringstream oss;
|
|
2655
|
-
oss << "dht_routing_" << this << ".json";
|
|
2656
|
-
file_path = oss.str();
|
|
2657
|
-
} else {
|
|
2658
|
-
file_path = "dht_routing_" + std::to_string(port_) + ".json";
|
|
2659
|
-
}
|
|
2660
|
-
#else
|
|
2661
|
-
file_path = data_directory_ + "/dht_routing_" + std::to_string(port_) + ".json";
|
|
2662
|
-
#endif
|
|
2663
|
-
|
|
2901
|
+
std::string file_path = routing_table_file_path();
|
|
2902
|
+
|
|
2664
2903
|
// Check if file exists
|
|
2665
2904
|
std::ifstream file(file_path);
|
|
2666
2905
|
if (!file.is_open()) {
|
|
@@ -2694,7 +2933,12 @@ bool DhtClient::load_routing_table() {
|
|
|
2694
2933
|
std::string node_id_hex = node_data["id"];
|
|
2695
2934
|
std::string ip = node_data["ip"];
|
|
2696
2935
|
int port = node_data["port"];
|
|
2697
|
-
|
|
2936
|
+
|
|
2937
|
+
// Skip nodes that don't belong to this instance's family
|
|
2938
|
+
if (network_utils::is_valid_ipv6(ip) != is_ipv6()) {
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2698
2942
|
NodeId node_id = hex_to_node_id(node_id_hex);
|
|
2699
2943
|
Peer peer(ip, port);
|
|
2700
2944
|
DhtNode node(node_id, peer);
|
|
@@ -3049,11 +3293,17 @@ void DhtClient::spider_walk() {
|
|
|
3049
3293
|
pool_size = spider_nodes_.size();
|
|
3050
3294
|
}
|
|
3051
3295
|
|
|
3052
|
-
// If still empty, bootstrap
|
|
3296
|
+
// If still empty, bootstrap (rate-limited to once per 30 seconds)
|
|
3053
3297
|
if (pool_size == 0) {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3298
|
+
auto now = std::chrono::steady_clock::now();
|
|
3299
|
+
if (now - last_spider_bootstrap_ >= std::chrono::seconds(30)) {
|
|
3300
|
+
last_spider_bootstrap_ = now;
|
|
3301
|
+
LOG_DHT_DEBUG("Spider walk: routing table empty too, re-bootstrapping");
|
|
3302
|
+
for (const auto& bootstrap : get_default_bootstrap_nodes()) {
|
|
3303
|
+
send_krpc_find_node(bootstrap, node_id_);
|
|
3304
|
+
}
|
|
3305
|
+
} else {
|
|
3306
|
+
LOG_DHT_DEBUG("Spider walk: skipping re-bootstrap (rate limited, last was < 30s ago)");
|
|
3057
3307
|
}
|
|
3058
3308
|
}
|
|
3059
3309
|
}
|
package/native-src/src/dht.h
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
#include <atomic>
|
|
14
14
|
#include <chrono>
|
|
15
15
|
#include <memory>
|
|
16
|
+
#include <random>
|
|
16
17
|
#include <condition_variable>
|
|
17
18
|
|
|
18
19
|
// Hash specialization for Peer and NodeId (must be defined before use in unordered_map/set)
|
|
@@ -165,8 +166,12 @@ public:
|
|
|
165
166
|
* Constructor
|
|
166
167
|
* @param port The UDP port to bind to (default: 6881)
|
|
167
168
|
* @param bind_address The interface IP address to bind to (empty for all interfaces)
|
|
169
|
+
* @param data_directory Directory for routing-table persistence
|
|
170
|
+
* @param address_family Address family this node operates on. IPv4 and IPv6 form
|
|
171
|
+
* separate Kademlia networks (BEP 32), so each family needs its own DhtClient.
|
|
168
172
|
*/
|
|
169
|
-
DhtClient(int port = DHT_PORT, const std::string& bind_address = "", const std::string& data_directory = ""
|
|
173
|
+
DhtClient(int port = DHT_PORT, const std::string& bind_address = "", const std::string& data_directory = "",
|
|
174
|
+
AddressFamily address_family = AddressFamily::IPv4);
|
|
170
175
|
|
|
171
176
|
/**
|
|
172
177
|
* Destructor
|
|
@@ -222,9 +227,33 @@ public:
|
|
|
222
227
|
|
|
223
228
|
/**
|
|
224
229
|
* Get our node ID
|
|
225
|
-
* @return The node ID
|
|
230
|
+
* @return The node ID (returned by value; the ID may be regenerated at runtime
|
|
231
|
+
* when our external IP is learned, see set_external_ip)
|
|
226
232
|
*/
|
|
227
|
-
|
|
233
|
+
NodeId get_node_id() const { return node_id_; }
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Set our external (public) IP address and regenerate the node ID from it per BEP 42.
|
|
237
|
+
* IPv4/IPv6 node IDs must be derived from the matching-family external address, so this
|
|
238
|
+
* is a no-op if the address family does not match this instance. Safe to call at runtime;
|
|
239
|
+
* the routing table is re-bucketed against the new ID. Non-public/invalid addresses are ignored.
|
|
240
|
+
* @param ip External IP address as seen by the network (e.g. from STUN or DHT "ip" voting)
|
|
241
|
+
*/
|
|
242
|
+
void set_external_ip(const std::string& ip);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the external address currently used to derive our node ID ("" if none/random).
|
|
246
|
+
*/
|
|
247
|
+
std::string get_external_address() const;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* BEP 42 helpers (public for testing / explicit use).
|
|
251
|
+
* generate_node_id_from_ip: build an IP-derived node ID (random where BEP 42 allows).
|
|
252
|
+
* verify_node_id_for_ip: check whether a node ID is valid for the given source IP
|
|
253
|
+
* (always true for non-public IPs, which cannot be verified).
|
|
254
|
+
*/
|
|
255
|
+
static bool generate_node_id_from_ip(const std::string& ip, NodeId& out, std::mt19937& gen);
|
|
256
|
+
static bool verify_node_id_for_ip(const NodeId& id, const std::string& ip);
|
|
228
257
|
|
|
229
258
|
/**
|
|
230
259
|
* Get number of nodes in routing table
|
|
@@ -337,11 +366,26 @@ public:
|
|
|
337
366
|
#endif // RATS_SEARCH_FEATURES
|
|
338
367
|
|
|
339
368
|
/**
|
|
340
|
-
* Get default BitTorrent DHT bootstrap nodes
|
|
369
|
+
* Get default BitTorrent DHT bootstrap nodes.
|
|
370
|
+
* Hostnames are resolved per-family at send time, so the same list serves both
|
|
371
|
+
* IPv4 and IPv6 (nodes without an AAAA record are simply skipped on IPv6).
|
|
341
372
|
* @return Vector of bootstrap nodes
|
|
342
373
|
*/
|
|
343
374
|
static std::vector<Peer> get_default_bootstrap_nodes();
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Address family this DHT node operates on.
|
|
378
|
+
*/
|
|
379
|
+
AddressFamily address_family() const { return address_family_; }
|
|
380
|
+
bool is_ipv6() const { return address_family_ == AddressFamily::IPv6; }
|
|
344
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Compute the routing-table persistence file path for this instance.
|
|
384
|
+
* Includes the address family so the IPv4 and IPv6 nodes (which may share a port)
|
|
385
|
+
* never overwrite each other's saved state.
|
|
386
|
+
*/
|
|
387
|
+
std::string routing_table_file_path() const;
|
|
388
|
+
|
|
345
389
|
/**
|
|
346
390
|
* Save routing table to disk
|
|
347
391
|
* @return true if successful, false otherwise
|
|
@@ -364,10 +408,23 @@ private:
|
|
|
364
408
|
int port_;
|
|
365
409
|
std::string bind_address_;
|
|
366
410
|
std::string data_directory_;
|
|
367
|
-
|
|
411
|
+
AddressFamily address_family_; // IPv4 or IPv6 (this node's Kademlia network)
|
|
412
|
+
NodeId node_id_; // protected by routing_table_mutex_ for writes; lock-free reads are benign
|
|
368
413
|
socket_t socket_;
|
|
369
414
|
std::atomic<bool> running_;
|
|
370
|
-
|
|
415
|
+
|
|
416
|
+
// BEP 42: external-IP-derived node ID.
|
|
417
|
+
// We learn our external address from the top-level "ip" field other nodes echo back in
|
|
418
|
+
// their responses, and vote across distinct responders before trusting it (so a single
|
|
419
|
+
// malicious node cannot poison our node ID). external_address_/voting are guarded by
|
|
420
|
+
// external_ip_mutex_, which is NEVER held while acquiring any other DHT mutex.
|
|
421
|
+
std::string external_address_; // IP currently used to derive node_id_ ("" = random)
|
|
422
|
+
std::unordered_map<std::string, int> external_ip_votes_;// reported external IP -> vote count
|
|
423
|
+
std::unordered_set<std::string> external_ip_voters_; // responder IPs already counted (dedup)
|
|
424
|
+
mutable std::mutex external_ip_mutex_; // Lock order: independent (never nested with others)
|
|
425
|
+
static constexpr int EXTERNAL_IP_VOTE_THRESHOLD = 5; // distinct responders that must agree
|
|
426
|
+
static constexpr size_t MAX_EXTERNAL_IP_VOTERS = 1000; // cap voter set to bound memory
|
|
427
|
+
|
|
371
428
|
// ============================================================================
|
|
372
429
|
// MUTEX LOCK ORDER - CRITICAL: Always acquire mutexes in this order to avoid deadlocks
|
|
373
430
|
// ============================================================================
|
|
@@ -507,6 +564,9 @@ private:
|
|
|
507
564
|
// Used to decide whether to respond with neighbor_id (spider contact) or real node_id (organic DHT)
|
|
508
565
|
std::unordered_set<std::string> spider_contacted_ips_;
|
|
509
566
|
|
|
567
|
+
// Rate-limit re-bootstrapping in spider_walk to avoid DDOSing bootstrap nodes
|
|
568
|
+
std::chrono::steady_clock::time_point last_spider_bootstrap_{}; // epoch = never bootstrapped
|
|
569
|
+
|
|
510
570
|
// Spider helper methods
|
|
511
571
|
void add_spider_node(const DhtNode& node);
|
|
512
572
|
void add_spider_nodes(const std::vector<DhtNode>& nodes);
|
|
@@ -549,6 +609,10 @@ private:
|
|
|
549
609
|
int get_bucket_index(const NodeId& id);
|
|
550
610
|
|
|
551
611
|
NodeId generate_node_id();
|
|
612
|
+
// BEP 42 internals
|
|
613
|
+
static bool is_public_address(const std::string& ip);
|
|
614
|
+
void rebuild_routing_table_unlocked(); // re-bucket all nodes after node_id_ changed (routing_table_mutex_ held)
|
|
615
|
+
void maybe_update_external_ip(const std::string& reported_ip, const Peer& responder);
|
|
552
616
|
NodeId xor_distance(const NodeId& a, const NodeId& b);
|
|
553
617
|
bool is_closer(const NodeId& a, const NodeId& b, const NodeId& target);
|
|
554
618
|
|