feat: periodically refresh dht node id (#6695)

This commit is contained in:
Yat Ho 2024-03-18 07:24:36 +08:00 committed by GitHub
parent 1edd9193a3
commit 2548dd478f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 13 deletions

View File

@ -148,6 +148,7 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"honorsSessionLimits"sv,
"host"sv,
"id"sv,
"id_timestamp"sv,
"idle-limit"sv,
"idle-mode"sv,
"idle-seeding-limit"sv,

View File

@ -149,6 +149,7 @@ enum
TR_KEY_honorsSessionLimits,
TR_KEY_host,
TR_KEY_id,
TR_KEY_id_timestamp,
TR_KEY_idle_limit,
TR_KEY_idle_mode,
TR_KEY_idle_seeding_limit,

View File

@ -140,7 +140,7 @@ public:
tr_logAddDebug(fmt::format("Starting DHT on port {port}", fmt::arg("port", peer_port.host())));
// init state from scratch, or load from state file if it exists
std::tie(id_, bootstrap_queue_) = init_state(state_filename_);
init_state(state_filename_);
get_nodes_from_bootstrap_file(tr_pathbuf{ mediator_.config_dir(), "/dht.bootstrap"sv }, bootstrap_queue_);
get_nodes_from_name("dht.transmissionbt.com", tr_port::from_host(6881), bootstrap_queue_);
@ -426,8 +426,9 @@ private:
tr_logAddTrace(fmt::format("Saving {} ({} + {}) nodes", n, num4, num6));
tr_variant benc;
tr_variantInitDict(&benc, 3);
tr_variantInitDict(&benc, 4);
tr_variantDictAddRaw(&benc, TR_KEY_id, std::data(id_), std::size(id_));
tr_variantDictAddInt(&benc, TR_KEY_id_timestamp, id_timestamp_);
if (num4 > 0)
{
@ -462,32 +463,38 @@ private:
tr_variant_serde::benc().to_file(benc, state_filename_);
}
[[nodiscard]] static std::pair<Id, Nodes> init_state(std::string_view filename)
void init_state(std::string_view filename)
{
// Note that DHT ids need to be distributed uniformly,
// so it should be something truly random
auto id = tr_rand_obj<Id>();
id_ = tr_rand_obj<Id>();
id_timestamp_ = tr_time();
if (!tr_sys_path_exists(std::data(filename)))
{
return { id, {} };
return;
}
auto otop = tr_variant_serde::benc().parse_file(filename);
if (!otop)
{
return { id, {} };
return;
}
static auto constexpr CompactLen = tr_socket_address::CompactSockAddrBytes[TR_AF_INET];
static auto constexpr Compact6Len = tr_socket_address::CompactSockAddrBytes[TR_AF_INET6];
static auto constexpr IdTtl = time_t{ 30 * 24 * 60 * 60 }; // 30 days
auto& top = *otop;
auto nodes = Nodes{};
if (auto sv = std::string_view{}; tr_variantDictFindStrView(&top, TR_KEY_id, &sv) && std::size(sv) == std::size(id))
if (auto t = int64_t{}; tr_variantDictFindInt(&top, TR_KEY_id_timestamp, &t) && t + IdTtl > id_timestamp_)
{
std::copy(std::begin(sv), std::end(sv), std::begin(id));
if (auto sv = std::string_view{};
tr_variantDictFindStrView(&top, TR_KEY_id, &sv) && std::size(sv) == std::size(id_))
{
id_timestamp_ = t;
std::copy(std::begin(sv), std::end(sv), std::begin(id_));
}
}
size_t raw_len = 0U;
@ -502,7 +509,7 @@ private:
auto port = tr_port{};
std::tie(addr, walk) = tr_address::from_compact_ipv4(walk);
std::tie(port, walk) = tr_port::from_compact(walk);
nodes.emplace_back(addr, port);
bootstrap_queue_.emplace_back(addr, port);
}
}
@ -516,11 +523,9 @@ private:
auto port = tr_port{};
std::tie(addr, walk) = tr_address::from_compact_ipv6(walk);
std::tie(port, walk) = tr_port::from_compact(walk);
nodes.emplace_back(addr, port);
bootstrap_queue_.emplace_back(addr, port);
}
}
return { id, std::move(nodes) };
}
///
@ -601,6 +606,7 @@ private:
std::unique_ptr<libtransmission::Timer> const periodic_timer_;
Id id_ = {};
int64_t id_timestamp_ = {};
Nodes bootstrap_queue_;
size_t n_bootstrapped_ = 0;

View File

@ -85,6 +85,7 @@ protected:
// Fake data to be written to the test state file
std::array<char, IdLength> const id_ = tr_rand_obj<std::array<char, IdLength>>();
int64_t id_timestamp_ = std::time(nullptr);
std::vector<tr_socket_address> ipv4_nodes_ = { { *tr_address::from_string("10.10.10.1"), tr_port::from_host(128) },
{ *tr_address::from_string("10.10.10.2"), tr_port::from_host(129) },
@ -128,6 +129,7 @@ protected:
auto dict = tr_variant{};
tr_variantInitDict(&dict, 3U);
tr_variantDictAddRaw(&dict, TR_KEY_id, std::data(id_), std::size(id_));
tr_variantDictAddInt(&dict, TR_KEY_id_timestamp, id_timestamp_);
auto compact = std::vector<std::byte>{};
for (auto const& socket_address : ipv4_nodes_)
{
@ -262,6 +264,7 @@ protected:
std::vector<Pinged> pinged_;
std::vector<Searched> searched_;
std::array<char, IdLength> id_ = {};
int64_t id_timestamp_ = {};
tr_socket_t dht_socket_ = TR_BAD_SOCKET;
tr_socket_t dht_socket6_ = TR_BAD_SOCKET;
};
@ -476,6 +479,8 @@ TEST_F(DhtTest, loadsStateFromStateFile)
auto const state_file = MockStateFile{};
state_file.save(sandboxDir());
tr_timeUpdate(time(nullptr));
// Make the DHT
auto mediator = MockMediator{ event_base_ };
mediator.config_dir_ = sandboxDir();
@ -501,6 +506,41 @@ TEST_F(DhtTest, loadsStateFromStateFile)
EXPECT_EQ(state_file.nodesString(), actual_nodes_str);
}
TEST_F(DhtTest, loadsStateFromStateFileExpiredId)
{
auto state_file = MockStateFile{};
state_file.id_timestamp_ = 0;
state_file.save(sandboxDir());
tr_timeUpdate(time(nullptr));
// Make the DHT
auto mediator = MockMediator{ event_base_ };
mediator.config_dir_ = sandboxDir();
auto dht = tr_dht::create(mediator, ArbitraryPeerPort, ArbitrarySock4, ArbitrarySock6);
// Wait for all the state nodes to be pinged
auto& pinged = mediator.mock_dht_.pinged_;
auto const n_expected_nodes = std::size(state_file.ipv4_nodes_) + std::size(state_file.ipv6_nodes_);
waitFor(event_base_, [&pinged, n_expected_nodes]() { return std::size(pinged) >= n_expected_nodes; });
auto actual_nodes_str = std::string{};
for (auto const& [addrport, timestamp] : pinged)
{
actual_nodes_str += addrport.display_name();
actual_nodes_str += ',';
}
/// Confirm that the state was loaded
// dht_init() should have been called with the state file's id
// N.B. There is a minuscule chance for this to fail, this is
// normal because id generation is random
EXPECT_NE(state_file.id_, mediator.mock_dht_.id_);
// dht_ping_nodedht_init() should have been called with state file's nodes
EXPECT_EQ(state_file.nodesString(), actual_nodes_str);
}
TEST_F(DhtTest, stopsBootstrappingWhenSwarmHealthIsGoodEnough)
{
auto const state_file = MockStateFile{};