Inside Bitcoin
Opening the selected file…
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
252const Reply& connect_reply = SendRequestAndGetReply(253*sock, strprintf("STREAM CONNECT ID=%s DESTINATION=%s SILENT=false", session_id, dest),254false);256const std::string& result = connect_reply.Get("RESULT");258if (result == "OK") {259conn.sock = std::move(sock);260return true;261}263if (result == "INVALID_ID") {264LOCK(m_mutex);265Disconnect();266throw std::runtime_error("Invalid session id");267}269if (result == "CANT_REACH_PEER" || result == "TIMEOUT") {270proxy_error = false;271}273throw std::runtime_error(strprintf("\"%s\"", connect_reply.full));274} catch (const std::runtime_error& e) {275LogDebug(BCLog::I2P, "Error connecting to %s: %s\n", to.ToStringAddrPort(), e.what());276CheckControlSock();277return false;278}279}281// Private methods283std::string Session::Reply::Get(const std::string& key) const284{285const auto& pos = keys.find(key);286if (pos == keys.end() || !pos->second.has_value()) {287throw std::runtime_error(288strprintf("Missing %s= in the reply to \"%s\"", key, request));289}290return pos->second.value();291}293Session::Reply Session::SendRequestAndGetReply(const Sock& sock,294const std::string& request,295bool check_result_ok) const296{297sock.SendComplete(request + "\n", MAX_WAIT_FOR_IO, *m_interrupt);299Reply reply;301// Don't log the full "SESSION CREATE ..." because it contains our private key.302reply.request = request.starts_with("SESSION CREATE") ? "SESSION CREATE ..." : request;304// It could take a few minutes for the I2P router to reply as it is querying the I2P network305// (when doing name lookup, for example). Notice: `RecvUntilTerminator()` is checking306// `m_interrupt` more often, so we would not be stuck here for long if `m_interrupt` is307// signaled.308static constexpr auto recv_timeout = 3min;310reply.full = sock.RecvUntilTerminator('\n', recv_timeout, *m_interrupt, MAX_MSG_SIZE);312for (const auto& kv : Split(reply.full, ' ')) {313const auto pos{std::ranges::find(kv, '=')};314if (pos != kv.end()) {315reply.keys.emplace(std::string{kv.begin(), pos}, std::string{pos + 1, kv.end()});316} else {317reply.keys.emplace(std::string{kv.begin(), kv.end()}, std::nullopt);318}319}321if (check_result_ok && reply.Get("RESULT") != "OK") {322throw std::runtime_error(323strprintf("Reply to \"%s\": had a RESULT not equal to OK.", reply.request));324}326return reply;327}329std::unique_ptr<Sock> Session::Hello() const330{331auto sock = m_control_host.Connect();333if (!sock) {334throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToString()));335}337SendRequestAndGetReply(*sock, "HELLO VERSION MIN=3.1 MAX=3.1");339return sock;340}342void Session::CheckControlSock()343{344LOCK(m_mutex);346std::string errmsg;347if (m_control_sock && !m_control_sock->IsConnected(errmsg)) {348LogDebug(BCLog::I2P, "Control socket error: %s\n", errmsg);349Disconnect();350}351}353void Session::DestGenerate(const Sock& sock)354{355// https://i2p.net/en/docs/specs/common-structures/#key-certificates356// "7" or "EdDSA_SHA512_Ed25519" - "Recent Router Identities and Destinations".357// Use "7" because i2pd <2.24.0 does not recognize the textual form.358// If SIGNATURE_TYPE is not specified, then the default one is DSA_SHA1.359const Reply& reply = SendRequestAndGetReply(sock, "DEST GENERATE SIGNATURE_TYPE=7", false);361m_private_key = DecodeI2PBase64(reply.Get("PRIV"));362}364void Session::GenerateAndSavePrivateKey(const Sock& sock)365{366DestGenerate(sock);368// umask is set to 0077 in common/system.cpp, which is ok.369if (!WriteBinaryFile(m_private_key_file,370std::string(m_private_key.begin(), m_private_key.end()))) {371throw std::runtime_error(372strprintf("Cannot save I2P private key to %s", fs::quoted(fs::PathToString(m_private_key_file))));373}374}376Binary Session::MyDestination() const377{378// From https://i2p.net/en/docs/specs/common-structures/#destination:379// "They are 387 bytes plus the certificate length specified at bytes 385-386, which may be380// non-zero"381static constexpr size_t DEST_LEN_BASE = 387;382static constexpr size_t CERT_LEN_POS = 385;384uint16_t cert_len;386if (m_private_key.size() < CERT_LEN_POS + sizeof(cert_len)) {387throw std::runtime_error(strprintf("The private key is too short (%d < %d)",388m_private_key.size(),389CERT_LEN_POS + sizeof(cert_len)));390}392memcpy(&cert_len, &m_private_key.at(CERT_LEN_POS), sizeof(cert_len));393cert_len = be16toh_internal(cert_len);395const size_t dest_len = DEST_LEN_BASE + cert_len;397if (dest_len > m_private_key.size()) {398throw std::runtime_error(strprintf("Certificate length (%d) designates that the private key should "399"be %d bytes, but it is only %d bytes",400cert_len,401dest_len,402m_private_key.size()));403}405return Binary{m_private_key.begin(), m_private_key.begin() + dest_len};406}408void Session::CreateIfNotCreatedAlready()409{410std::string errmsg;411if (m_control_sock && m_control_sock->IsConnected(errmsg)) {412return;413}415const auto session_type = m_transient ? "transient" : "persistent";416const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs418LogDebug(BCLog::I2P, "Creating %s I2P SAM session %s with %s\n", session_type, session_id, m_control_host.ToString());420auto sock = Hello();422if (m_transient) {423// The destination (private key) is generated upon session creation and returned424// in the reply in DESTINATION=.425const Reply& reply = SendRequestAndGetReply(426*sock,427strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 "428"i2cp.leaseSetEncType=4,0 inbound.quantity=1 outbound.quantity=1",429session_id));431m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));432} else {433// Read our persistent destination (private key) from disk or generate434// one and save it to disk. Then use it when creating the session.435const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);436if (read_ok) {437m_private_key.assign(data.begin(), data.end());438} else {439GenerateAndSavePrivateKey(*sock);440}442const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));444SendRequestAndGetReply(*sock,445strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s "446"i2cp.leaseSetEncType=4,0 inbound.quantity=3 outbound.quantity=3",447session_id,448private_key_b64));449}451m_my_addr = CService(DestBinToAddr(MyDestination()), I2P_SAM31_PORT);452m_session_id = session_id;453m_control_sock = std::move(sock);455LogInfo("%s I2P SAM session %s created, my address=%s",456Capitalize(session_type),457m_session_id,458m_my_addr.ToStringAddrPort());459}461std::unique_ptr<Sock> Session::StreamAccept()462{463auto sock = Hello();465const Reply& reply = SendRequestAndGetReply(466*sock, strprintf("STREAM ACCEPT ID=%s SILENT=false", m_session_id), false);468const std::string& result = reply.Get("RESULT");470if (result == "OK") {471return sock;472}474if (result == "INVALID_ID") {475// If our session id is invalid, then force session re-creation on next usage.476Disconnect();477}479throw std::runtime_error(strprintf("\"%s\"", reply.full));480}482void Session::Disconnect()483{484if (m_control_sock) {485if (m_session_id.empty()) {486LogInfo("Destroying incomplete I2P SAM session");487} else {488LogInfo("Destroying I2P SAM session %s", m_session_id);489}490m_control_sock.reset();491}492m_session_id.clear();493}494} // namespace sam495} // namespace i2p