/* * Copyright (c) 2026, The PurpleI2P Project * * This file is part of Purple i2pd project and licensed under BSD3 * * See full license text in LICENSE file at top of project tree */ #ifndef TORRENTS_DHT_H__ #define TORRENTS_DHT_H__ #ifndef NO_TORRENTS #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Base.h" #include "Identity.h" #include "I2PService.h" #include "util.h" #include "Timestamp.h" #include "Torrents.h" namespace i2p { namespace torrents { constexpr int DHT_UPDATE_CHECK_INTERVAL = 24; // in seconds constexpr int DHT_EXPIRATION_CHECK_INTERVAL = 73; // in seconds constexpr int DHT_SEND_PING_CHECK_INTERVAL = 38; // in seconds constexpr int DHT_QUERY_EXPIRATION_CHECK_INTERVAL = 8; // in seconds constexpr int DHT_EXPLORATORY_INTERVAL = 230; // in seconds constexpr int DHT_EXPLORATORY_INTERVAL_VARIANCE = 40; // in seconds constexpr int DHT_INITIAL_EXPLORATORY_INTERVAL = 90; // in seconds constexpr int DHT_NODE_SEND_PING_TIME = 740; // in seconds constexpr int DHT_NODE_EXPIRATION_TIME = 855; // in seconds constexpr int DHT_BUCKET_EXPIRATION_THRESHOLD = 290; // in seconds constexpr int DHT_TORRENT_PEER_EXPIRATION_TIME = 3*3600; // in seconds constexpr int DHT_INCOMING_GET_PEERS_TOKEN_EXPIRATION_TIME = 600; // in seconds constexpr int DHT_HEARD_FROM_EXPIRATION_TIME = 330; // in seconds constexpr int DHT_QUERY_EXPIRATION_TIME = 25; // in seconds constexpr int DHT_PEER_FAILURE_INTERVAL = 250; // in seconds constexpr int DHT_MAX_NUM_GET_PEERS_ATTEMPTS = 22; constexpr int DHT_MAX_NUM_FIND_NODE_ATTEMPTS = 8; constexpr int DHT_MAX_NUM_CLOSEST_NODES_TO_ANNOUNCE = 4; using Distance = Torrent::InfoHash; struct NodeID: public Torrent::InfoHash { static constexpr size_t len = std::tuple_size::value; constexpr Distance operator^(const Torrent::InfoHash& hash) const { Distance d; for (size_t i = 0; i < size (); i++) d[i] = (*this)[i] ^ hash[i]; return d; } constexpr int FindLowestBit () const // -1 in not found { for (int i = size () - 1; i >= 0; i--) { uint8_t byte = (*this)[i]; if (byte) { for (int j = 7; j >= 0; j--) if (byte & (0x80 >> j)) return i*8 + j; } } return -1; } std::string ToBase64 () const { return i2p::data::ByteStreamToBase64 (data (), size ()); } }; using NodeInfo = std::array; struct Node { NodeID id; i2p::data::IdentHash peer; uint16_t port; uint64_t lastUpdateTime; // monotonic seconds Node (const NodeID& id1, const i2p::data::IdentHash& peer1, uint16_t port1): id (id1), peer (peer1), port (port1), lastUpdateTime (i2p::util::GetMonotonicSeconds ()) {} Node (const NodeInfo& nodeInfo); NodeInfo GetNodeInfo () const; bool VerifyID () const; }; constexpr size_t MAX_BUCKET_CAPACITY = 8; struct Bucket { Bucket * next; std::map > nodes; NodeID start; uint64_t lastUpdateTime; // monotonic seconds Bucket (): next (nullptr), start{}, lastUpdateTime (i2p::util::GetMonotonicSeconds ()) {} Bucket (const NodeID& start1): next (nullptr), start (start1), lastUpdateTime (i2p::util::GetMonotonicSeconds ()) {} bool IsFull () const { return nodes.size () >= MAX_BUCKET_CAPACITY; } bool IsEmpty () const { return nodes.empty (); } bool IsInBucket (const NodeID& id) const { return id >= start && (!next || id < next->start); } std::optional GetMiddleID () const; NodeID GetRandomID (std::mt19937& rng) const; bool Split (); }; class RoutingTable { public: RoutingTable (const NodeID& ourNode); ~RoutingTable (); void CleanUp (); size_t GetNumBuckets () const; size_t GetNumNodes () const; std::list > GetNodes (); Bucket * FindBucket (const Torrent::InfoHash& id) const; std::shared_ptr AddNode (std::shared_ptr node); void RemoveNode (const NodeID& id); std::list > GetExploratoryTargets (std::mt19937& rng, uint64_t ts); // (target, node to send find_node to) size_t DeleteExpiredNodes (uint64_t ts); std::list > GetNodesToPing (uint64_t ts); private: Bucket * m_Buckets; NodeID m_OurNode; }; using GetPeersToken = uint64_t; class DHTTorrent { public: DHTTorrent () = default; std::string GetBEncodedPeers () const; bool AddPeer (const i2p::data::IdentHash& peer); bool CleanUp (uint64_t ts); // return true if empty bool HasPeers () const { return !m_Peers.empty (); }; std::unordered_set GetPeers () const; private: std::unordered_map m_Peers; // ident -> update time in monotonic seconds }; enum KRPCQuery { eKRPCQueryPing = 0, eKRPCQueryFindNode, eKRPCQueryGetPeers, eKRPCQueryAnnouncePeer, eNumKRPCQueries }; constexpr std::array KRPCQueryStr { "ping", "find_node", "get_peers", "announce_peer" }; struct RequestInfo { using NodeToRequest = std::pair >; struct NodeToRequestCmp { bool operator() (const NodeToRequest& n1, const NodeToRequest& n2) const { auto cmp = n1.first <=> n2.first; if (cmp != 0) return cmp < 0; return n1.second->id < n2.second->id; } }; using NodeToken = std::tuple, uint64_t >; struct NodeTokenCmp { bool operator() (const NodeToken& n1, const NodeToken& n2) const { auto cmp = std::get<0>(n1) <=> std::get<0>(n2); if (cmp != 0) return cmp < 0; return std::get<1>(n1)->id < std::get<1>(n2)->id; } }; std::shared_ptr torrent; // for get_peers NodeID target; // for find_node std::set nodesToRequest; std::set tokens; // token received from nodes for get_peers std::set tried; int numAttempts, maxNumAttempts; std::shared_ptr lastNode; RequestInfo (std::shared_ptr t, int maxNumAttempts1): torrent (t), numAttempts (0), maxNumAttempts (maxNumAttempts1) { } bool IsDone () const { return numAttempts >= maxNumAttempts; } bool AddNode (std::shared_ptr node); bool AddNodeToken (std::shared_ptr node, uint64_t token); std::shared_ptr GetNextNode (); }; class TorrentsTunnel; class TorrentsDHT { public: TorrentsDHT (TorrentsTunnel& tunnel, uint16_t port, std::string_view dhtFileName); void Start (); void Stop (); uint16_t GetPort () const { return m_Port; }; uint16_t GetRPort () const { return m_Port + 1; }; void HandleRawDatagram (const uint8_t * buf, size_t len); void SendPingQuery (const i2p::data::IdentHash& toIdent, uint16_t toPort); void GetPeersAndAnnounce (std::shared_ptr torrent); size_t GetNumBuckets () const { return m_RoutingTable ? m_RoutingTable->GetNumBuckets () : 0; } size_t GetNumNodes () const { return m_RoutingTable ? m_RoutingTable->GetNumNodes () : 0; } size_t GetNumTorrents () const { return m_Torrents.size (); } size_t GetNumHeardFrom () const { return m_HeardFrom.size (); } size_t GetNumFailedPeers () const { return m_FailedPeers.size (); } private: void HandleDatagram (const i2p::data::IdentityEx& from, uint16_t fromPort, uint16_t toPort, const uint8_t * buf, size_t len, const i2p::util::Mapping * options); void HandlePingQuery (const i2p::data::IdentHash& fromIdent, uint16_t fromPort, std::string_view transactionID, const NodeID& nodeID); void HandleGetPeersQuery (std::string_view transactionID, std::shared_ptr from, const Torrent::InfoHash& infoHash); void HandleFindNodeQuery (const i2p::data::IdentHash& fromIdent, uint16_t fromPort, std::string_view transactionID, const NodeID& target); void HandleResponse (std::string_view transactionID, const NodeID& nodeID, uint64_t token, const std::vector& values, std::string_view nodes); void HandleGetPeersResponseNodes (std::shared_ptr info, std::string_view nodes); void HandleGetPeersResponsePeersAndAnnounce (std::shared_ptr info, const std::vector& peers, uint64_t token, const i2p::data::IdentHash& toIdent, uint16_t toPort); void HandleFindNodeResponse (std::shared_ptr info, std::string_view nodes); void HandleAnnouncePeerResponse (std::shared_ptr info); void HandleAnnouncePeer (std::string_view transactionID, const NodeID& nodeID, const Torrent::InfoHash& infoHash, uint64_t token); void SendDatagram (std::string_view msg, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendRawDatagram (std::string_view msg, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendQueryMsg (KRPCQuery query, std::string_view arguments, const i2p::data::IdentHash& toIdent, uint16_t toPort, bool isRaw = false, std::shared_ptr info = nullptr); void SendFindNodeQuery (std::shared_ptr info, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendNextFindNodeQuery (std::shared_ptr info); void SendGetPeersQuery (std::shared_ptr info, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendNextGetPeersQuery (std::shared_ptr info); void SendResponseMsg (std::string_view response, std::string_view transactionID, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendPingResponse (std::string_view transactionID, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendGetPeersResponse (std::string_view transactionID, std::shared_ptr torrent, uint64_t token, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendGetPeersResponse (std::string_view transactionID, std::string_view nodes, uint64_t token, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendFindNodeResponse (std::string_view transactionID, std::string_view nodes, const i2p::data::IdentHash& toIdent, uint16_t toPort); void SendAnnouncePeerQuery (std::shared_ptr info, uint64_t token, const i2p::data::IdentHash& toIdent, uint16_t toPort); std::filesystem::path GetDHTFilePath (std::string_view filename) const; void Save (const std::filesystem::path& file); void Load (const std::filesystem::path& file); void Explore (); std::shared_ptr UpdateNode (std::shared_ptr node); // return true if added void UpdateHeardFrom (const NodeID& nodeID); void ScheduleDHTUpdateCheck (); void HandleDHTUpdateCheckTimer (const boost::system::error_code& ecode); void ScheduleDHTExpirationCheck (); void HandleDHTExpirationCheckTimer (const boost::system::error_code& ecode); void ScheduleDHTSendPingCheck (); void HandleDHTSendPingCheckTimer (const boost::system::error_code& ecode); void ScheduleDHTQueryExpirationCheck (); void DHTQueryExpirationCheckTimer (const boost::system::error_code& ecode); private: TorrentsTunnel& m_Tunnel; uint16_t m_Port; std::string m_DHTFileName; NodeID m_NodeID; boost::asio::steady_timer m_DHTUpdateCheckTimer, m_DHTExpirationCheckTimer, m_DHTSendPingCheckTimer, m_DHTQueryExpirationCheckTimer; std::unique_ptr m_RoutingTable; std::map m_HeardFrom; // nodeid -> time in monotonic seconds std::unordered_map m_FailedPeers; // peer -> failure time in monotonic seconds std::unordered_map, uint64_t> > m_IncomingTokens; // token ->(node, time in monotonic second) // transactionID -> (ident, port, query, request info, time in monotonic seconds) std::unordered_map, uint64_t > > m_Queries; std::map > m_Torrents; uint64_t m_NextDHTExploratoryTime; // monotonic seconds }; } } #endif #endif