/* * Copyright (c) 2013-2024, 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 */ #include #include #include #include #include #include #include #include #include #include "Crypto.h" #include "I2PEndian.h" #include "Reseed.h" #include "FS.h" #include "Log.h" #include "Identity.h" #include "NetDb.hpp" #include "HTTP.h" #include "util.h" #include "Config.h" #include "Socks5.h" namespace i2p { namespace data { Reseeder::Reseeder() { } Reseeder::~Reseeder() { } /** @brief tries to bootstrap into I2P network (from local files and servers, with respect of options) */ void Reseeder::Bootstrap () { std::string su3FileName; i2p::config::GetOption("reseed.file", su3FileName); std::string zipFileName; i2p::config::GetOption("reseed.zipfile", zipFileName); if (su3FileName.length() > 0) // bootstrap from SU3 file or URL { int num; if (su3FileName.length() > 8 && su3FileName.substr(0, 8) == "https://") { num = ReseedFromSU3Url (su3FileName); // from https URL } else { num = ProcessSU3File (su3FileName.c_str ()); } if (num == 0) LogPrint (eLogWarning, "Reseed: Failed to reseed from ", su3FileName); } else if (zipFileName.length() > 0) // bootstrap from ZIP file { int num = ProcessZIPFile (zipFileName.c_str ()); if (num == 0) LogPrint (eLogWarning, "Reseed: Failed to reseed from ", zipFileName); } else // bootstrap from reseed servers { int num = ReseedFromServers (); if (num == 0) LogPrint (eLogWarning, "Reseed: Failed to reseed from servers"); } } /** * @brief bootstrap from random server, retry 10 times * @return number of entries added to netDb */ int Reseeder::ReseedFromServers () { bool ipv6; i2p::config::GetOption("ipv6", ipv6); bool ipv4; i2p::config::GetOption("ipv4", ipv4); bool yggdrasil; i2p::config::GetOption("meshnets.yggdrasil", yggdrasil); std::vector httpsReseedHostList; if (ipv4 || ipv6) { std::string reseedURLs; i2p::config::GetOption("reseed.urls", reseedURLs); if (!reseedURLs.empty ()) boost::split(httpsReseedHostList, reseedURLs, boost::is_any_of(","), boost::token_compress_on); } std::vector yggReseedHostList; if (yggdrasil && !i2p::util::net::GetYggdrasilAddress ().is_unspecified ()) { LogPrint (eLogInfo, "Reseed: Yggdrasil is supported"); std::string yggReseedURLs; i2p::config::GetOption("reseed.yggurls", yggReseedURLs); if (!yggReseedURLs.empty ()) boost::split(yggReseedHostList, yggReseedURLs, boost::is_any_of(","), boost::token_compress_on); } if (httpsReseedHostList.empty () && yggReseedHostList.empty()) { LogPrint (eLogWarning, "Reseed: No reseed servers specified"); return 0; } int reseedRetries = 0; while (reseedRetries < 10) { auto ind = rand () % (httpsReseedHostList.size () + yggReseedHostList.size ()); bool isHttps = ind < httpsReseedHostList.size (); std::string reseedUrl = isHttps ? httpsReseedHostList[ind] : yggReseedHostList[ind - httpsReseedHostList.size ()]; reseedUrl += "i2pseeds.su3"; auto num = ReseedFromSU3Url (reseedUrl, isHttps); if (num > 0) return num; // success reseedRetries++; } LogPrint (eLogWarning, "Reseed: Failed to reseed from servers after 10 attempts"); return 0; } /** * @brief bootstrap from HTTPS URL with SU3 file * @param url * @return number of entries added to netDb */ int Reseeder::ReseedFromSU3Url (const std::string& url, bool isHttps) { LogPrint (eLogInfo, "Reseed: Downloading SU3 from ", url); std::string su3 = isHttps ? HttpsRequest (url) : YggdrasilRequest (url); if (su3.length () > 0) { std::stringstream s(su3); return ProcessSU3Stream (s); } else { LogPrint (eLogWarning, "Reseed: SU3 download failed"); return 0; } } int Reseeder::ProcessSU3File (const char * filename) { std::ifstream s(filename, std::ifstream::binary); if (s.is_open ()) return ProcessSU3Stream (s); else { LogPrint (eLogCritical, "Reseed: Can't open file ", filename); return 0; } } int Reseeder::ProcessZIPFile (const char * filename) { std::ifstream s(filename, std::ifstream::binary); if (s.is_open ()) { s.seekg (0, std::ios::end); auto len = s.tellg (); s.seekg (0, std::ios::beg); return ProcessZIPStream (s, len); } else { LogPrint (eLogCritical, "Reseed: Can't open file ", filename); return 0; } } const char SU3_MAGIC_NUMBER[]="I2Psu3"; int Reseeder::ProcessSU3Stream (std::istream& s) { char magicNumber[7]; s.read (magicNumber, 7); // magic number and zero byte 6 if (strcmp (magicNumber, SU3_MAGIC_NUMBER)) { LogPrint (eLogError, "Reseed: Unexpected SU3 magic number"); return 0; } s.seekg (1, std::ios::cur); // su3 file format version SigningKeyType signatureType; s.read ((char *)&signatureType, 2); // signature type signatureType = be16toh (signatureType); uint16_t signatureLength; s.read ((char *)&signatureLength, 2); // signature length signatureLength = be16toh (signatureLength); s.seekg (1, std::ios::cur); // unused uint8_t versionLength; s.read ((char *)&versionLength, 1); // version length s.seekg (1, std::ios::cur); // unused uint8_t signerIDLength; s.read ((char *)&signerIDLength, 1); // signer ID length uint64_t contentLength; s.read ((char *)&contentLength, 8); // content length contentLength = be64toh (contentLength); s.seekg (1, std::ios::cur); // unused uint8_t fileType; s.read ((char *)&fileType, 1); // file type if (fileType != 0x00) // zip file { LogPrint (eLogError, "Reseed: Can't handle file type ", (int)fileType); return 0; } s.seekg (1, std::ios::cur); // unused uint8_t contentType; s.read ((char *)&contentType, 1); // content type if (contentType != 0x03) // reseed data { LogPrint (eLogError, "Reseed: Unexpected content type ", (int)contentType); return 0; } s.seekg (12, std::ios::cur); // unused s.seekg (versionLength, std::ios::cur); // skip version char signerID[256]; s.read (signerID, signerIDLength); // signerID signerID[signerIDLength] = 0; bool verify; i2p::config::GetOption("reseed.verify", verify); if (verify) { //try to verify signature auto it = m_SigningKeys.find (signerID); if (it != m_SigningKeys.end ()) { // TODO: implement all signature types if (signatureType == SIGNING_KEY_TYPE_RSA_SHA512_4096) { size_t pos = s.tellg (); size_t tbsLen = pos + contentLength; uint8_t * tbs = new uint8_t[tbsLen]; s.seekg (0, std::ios::beg); s.read ((char *)tbs, tbsLen); uint8_t * signature = new uint8_t[signatureLength]; s.read ((char *)signature, signatureLength); // RSA-raw { // calculate digest uint8_t digest[64]; SHA512 (tbs, tbsLen, digest); // encrypt signature BN_CTX * bnctx = BN_CTX_new (); BIGNUM * s = BN_new (), * n = BN_new (); BN_bin2bn (signature, signatureLength, s); BN_bin2bn (it->second, 512, n); // RSA 4096 assumed BN_mod_exp (s, s, i2p::crypto::GetRSAE (), n, bnctx); // s = s^e mod n uint8_t * enSigBuf = new uint8_t[signatureLength]; i2p::crypto::bn2buf (s, enSigBuf, signatureLength); // digest is right aligned // we can't use RSA_verify due wrong padding in SU3 if (memcmp (enSigBuf + (signatureLength - 64), digest, 64)) LogPrint (eLogWarning, "Reseed: SU3 signature verification failed"); else verify = false; // verified delete[] enSigBuf; BN_free (s); BN_free (n); BN_CTX_free (bnctx); } delete[] signature; delete[] tbs; s.seekg (pos, std::ios::beg); } else LogPrint (eLogWarning, "Reseed: Signature type ", signatureType, " is not supported"); } else LogPrint (eLogWarning, "Reseed: Certificate for ", signerID, " not loaded"); } if (verify) // not verified { LogPrint (eLogCritical, "Reseed: SU3 verification failed"); return 0; } // handle content return ProcessZIPStream (s, contentLength); } const uint32_t ZIP_HEADER_SIGNATURE = 0x04034B50; const uint32_t ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014B50; const uint16_t ZIP_BIT_FLAG_DATA_DESCRIPTOR = 0x0008; int Reseeder::ProcessZIPStream (std::istream& s, uint64_t contentLength) { int numFiles = 0; size_t contentPos = s.tellg (); while (!s.eof ()) { uint32_t signature; s.read ((char *)&signature, 4); signature = le32toh (signature); if (signature == ZIP_HEADER_SIGNATURE) { // next local file s.seekg (2, std::ios::cur); // version uint16_t bitFlag; s.read ((char *)&bitFlag, 2); bitFlag = le16toh (bitFlag); uint16_t compressionMethod; s.read ((char *)&compressionMethod, 2); compressionMethod = le16toh (compressionMethod); s.seekg (4, std::ios::cur); // skip fields we don't care about uint32_t compressedSize, uncompressedSize; uint32_t crc_32; s.read ((char *)&crc_32, 4); crc_32 = le32toh (crc_32); s.read ((char *)&compressedSize, 4); compressedSize = le32toh (compressedSize); s.read ((char *)&uncompressedSize, 4); uncompressedSize = le32toh (uncompressedSize); uint16_t fileNameLength, extraFieldLength; s.read ((char *)&fileNameLength, 2); fileNameLength = le16toh (fileNameLength); if ( fileNameLength >= 255 ) { // too big LogPrint(eLogError, "Reseed: SU3 fileNameLength too large: ", fileNameLength); return numFiles; } s.read ((char *)&extraFieldLength, 2); extraFieldLength = le16toh (extraFieldLength); char localFileName[255]; s.read (localFileName, fileNameLength); localFileName[fileNameLength] = 0; s.seekg (extraFieldLength, std::ios::cur); // take care about data descriptor if presented if (bitFlag & ZIP_BIT_FLAG_DATA_DESCRIPTOR) { size_t pos = s.tellg (); if (!FindZipDataDescriptor (s)) { LogPrint (eLogError, "Reseed: SU3 archive data descriptor not found"); return numFiles; } s.read ((char *)&crc_32, 4); crc_32 = le32toh (crc_32); s.read ((char *)&compressedSize, 4); compressedSize = le32toh (compressedSize) + 4; // ??? we must consider signature as part of compressed data s.read ((char *)&uncompressedSize, 4); uncompressedSize = le32toh (uncompressedSize); // now we know compressed and uncompressed size s.seekg (pos, std::ios::beg); // back to compressed data } LogPrint (eLogDebug, "Reseed: Processing file ", localFileName, " ", compressedSize, " bytes"); if (!compressedSize) { LogPrint (eLogWarning, "Reseed: Unexpected size 0. Skipped"); continue; } uint8_t * compressed = new uint8_t[compressedSize]; s.read ((char *)compressed, compressedSize); if (compressionMethod) // we assume Deflate { z_stream inflator; memset (&inflator, 0, sizeof (inflator)); inflateInit2 (&inflator, -MAX_WBITS); // no zlib header uint8_t * uncompressed = new uint8_t[uncompressedSize]; inflator.next_in = compressed; inflator.avail_in = compressedSize; inflator.next_out = uncompressed; inflator.avail_out = uncompressedSize; int err; if ((err = inflate (&inflator, Z_SYNC_FLUSH)) >= 0) { uncompressedSize -= inflator.avail_out; if (crc32 (0, uncompressed, uncompressedSize) == crc_32) { i2p::data::netdb.AddRouterInfo (uncompressed, uncompressedSize); numFiles++; } else LogPrint (eLogError, "Reseed: CRC32 verification failed"); } else LogPrint (eLogError, "Reseed: SU3 decompression error ", err); delete[] uncompressed; inflateEnd (&inflator); } else // no compression { i2p::data::netdb.AddRouterInfo (compressed, compressedSize); numFiles++; } delete[] compressed; if (bitFlag & ZIP_BIT_FLAG_DATA_DESCRIPTOR) s.seekg (12, std::ios::cur); // skip data descriptor section if presented (12 = 16 - 4) } else { if (signature != ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE) LogPrint (eLogWarning, "Reseed: Missing zip central directory header"); break; // no more files } size_t end = s.tellg (); if (end - contentPos >= contentLength) break; // we are beyond contentLength } if (numFiles) // check if routers are not outdated { auto ts = i2p::util::GetMillisecondsSinceEpoch (); int numOutdated = 0; i2p::data::netdb.VisitRouterInfos ( [&numOutdated, ts](std::shared_ptr r) { if (r && ts > r->GetTimestamp () + 10*i2p::data::NETDB_MAX_EXPIRATION_TIMEOUT*1000LL) // 270 hours { LogPrint (eLogError, "Reseed: Router ", r->GetIdentHash().ToBase64 (), " is outdated by ", (ts - r->GetTimestamp ())/1000LL/3600LL, " hours"); numOutdated++; } }); if (numOutdated > numFiles/2) // more than half { LogPrint (eLogError, "Reseed: Mammoth's shit\n" " *_____*\n" " *_*****_*\n" " *_(O)_(O)_*\n" " **____V____**\n" " **_________**\n" " **_________**\n" " *_________*\n" " ***___***"); i2p::data::netdb.ClearRouterInfos (); numFiles = 0; } } return numFiles; } const uint8_t ZIP_DATA_DESCRIPTOR_SIGNATURE[] = { 0x50, 0x4B, 0x07, 0x08 }; bool Reseeder::FindZipDataDescriptor (std::istream& s) { size_t nextInd = 0; while (!s.eof ()) { uint8_t nextByte; s.read ((char *)&nextByte, 1); if (nextByte == ZIP_DATA_DESCRIPTOR_SIGNATURE[nextInd]) { nextInd++; if (nextInd >= sizeof (ZIP_DATA_DESCRIPTOR_SIGNATURE)) return true; } else nextInd = 0; } return false; } void Reseeder::LoadCertificate (const std::string& filename) { SSL_CTX * ctx = SSL_CTX_new (TLS_method ()); int ret = SSL_CTX_use_certificate_file (ctx, filename.c_str (), SSL_FILETYPE_PEM); if (ret) { SSL * ssl = SSL_new (ctx); X509 * cert = SSL_get_certificate (ssl); // verify if (cert) { // extract issuer name char name[100]; X509_NAME_oneline (X509_get_issuer_name(cert), name, 100); char * cn = strstr (name, "CN="); if (cn) { cn += 3; char * terminator = strchr (cn, '/'); if (terminator) terminator[0] = 0; } // extract RSA key (we need n only, e = 65537) const RSA * key = EVP_PKEY_get0_RSA (X509_get_pubkey (cert)); const BIGNUM * n, * e, * d; RSA_get0_key(key, &n, &e, &d); PublicKey value; i2p::crypto::bn2buf (n, value, 512); if (cn) m_SigningKeys[cn] = value; else LogPrint (eLogError, "Reseed: Can't find CN field in ", filename); } SSL_free (ssl); } else LogPrint (eLogCritical, "Reseed: Can't open certificate file ", filename); SSL_CTX_free (ctx); } void Reseeder::LoadCertificates () { std::string certDir = i2p::fs::GetCertsDir() + i2p::fs::dirSep + "reseed"; std::vector files; int numCertificates = 0; if (!i2p::fs::ReadDir(certDir, files)) { LogPrint(eLogWarning, "Reseed: Can't load reseed certificates from ", certDir); return; } for (const std::string & file : files) { if (file.compare(file.size() - 4, 4, ".crt") != 0) { LogPrint(eLogWarning, "Reseed: Ignoring file ", file); continue; } LoadCertificate (file); numCertificates++; } LogPrint (eLogInfo, "Reseed: ", numCertificates, " certificates loaded"); } std::string Reseeder::HttpsRequest (const std::string& address) { i2p::http::URL proxyUrl; std::string proxy; i2p::config::GetOption("reseed.proxy", proxy); // check for proxy url if(proxy.size()) { // parse if(proxyUrl.parse(proxy)) { if (proxyUrl.schema == "http" && !proxyUrl.port) { proxyUrl.port = 80; } else if (proxyUrl.schema == "socks" && !proxyUrl.port) { proxyUrl.port = 1080; } // check for valid proxy url schema if (proxyUrl.schema != "http" && proxyUrl.schema != "socks") { LogPrint(eLogCritical, "Reseed: Bad proxy url: ", proxy); return ""; } } else { LogPrint(eLogCritical, "Reseed: Bad proxy url: ", proxy); return ""; } } i2p::http::URL url; if (!url.parse(address)) { LogPrint(eLogCritical, "Reseed: Failed to parse url: ", address); return ""; } url.schema = "https"; if (!url.port) url.port = 443; boost::asio::io_context service; boost::system::error_code ecode; boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23); ctx.set_verify_mode(boost::asio::ssl::context::verify_none); boost::asio::ssl::stream s(service, ctx); if(proxyUrl.schema.size()) { // proxy connection auto it = boost::asio::ip::tcp::resolver(service).resolve (proxyUrl.host, std::to_string(proxyUrl.port), ecode); if(!ecode) { s.lowest_layer().connect(*it.begin (), ecode); if(!ecode) { auto & sock = s.next_layer(); if(proxyUrl.schema == "http") { i2p::http::HTTPReq proxyReq; i2p::http::HTTPRes proxyRes; proxyReq.method = "CONNECT"; proxyReq.version = "HTTP/1.1"; proxyReq.uri = url.host + ":" + std::to_string(url.port); auto auth = i2p::http::CreateBasicAuthorizationString (proxyUrl.user, proxyUrl.pass); if (!auth.empty ()) proxyReq.AddHeader("Proxy-Authorization", auth); boost::asio::streambuf writebuf, readbuf; std::ostream out(&writebuf); out << proxyReq.to_string(); boost::asio::write(sock, writebuf.data(), boost::asio::transfer_all(), ecode); if (ecode) { sock.close(); LogPrint(eLogError, "Reseed: HTTP CONNECT write error: ", ecode.message()); return ""; } boost::asio::read_until(sock, readbuf, "\r\n\r\n", ecode); if (ecode) { sock.close(); LogPrint(eLogError, "Reseed: HTTP CONNECT read error: ", ecode.message()); return ""; } if(proxyRes.parse(std::string {boost::asio::buffers_begin(readbuf.data ()), boost::asio::buffers_begin(readbuf.data ()) + readbuf.size ()}) <= 0) { sock.close(); LogPrint(eLogError, "Reseed: HTTP CONNECT malformed reply"); return ""; } if(proxyRes.code != 200) { sock.close(); LogPrint(eLogError, "Reseed: HTTP CONNECT got bad status: ", proxyRes.code); return ""; } } else { // assume socks if not http, is checked before this for other types // TODO: support username/password auth etc bool success = false; i2p::transport::Socks5Handshake (sock, std::make_pair(url.host, url.port), [&success](const boost::system::error_code& ec) { if (!ec) success = true; else LogPrint (eLogError, "Reseed: SOCKS handshake failed: ", ec.message()); }); service.run (); // execute all async operations if (!success) { sock.close(); return ""; } } } } } else { // direct connection auto endpoints = boost::asio::ip::tcp::resolver(service).resolve (url.host, std::to_string(url.port), ecode); if (!ecode) { bool connected = false; for (auto it = endpoints.begin (); it != endpoints.end ();) { boost::asio::ip::tcp::endpoint ep = *it; bool supported = false; if (!ep.address ().is_unspecified ()) { if (ep.address ().is_v4 ()) supported = i2p::context.SupportsV4 (); else if (ep.address ().is_v6 ()) supported = i2p::util::net::IsYggdrasilAddress (ep.address ()) ? i2p::context.SupportsMesh () : i2p::context.SupportsV6 (); } if (supported) { s.lowest_layer().connect (ep, ecode); if (!ecode) { LogPrint (eLogDebug, "Reseed: Resolved to ", ep.address ()); connected = true; break; } } it++; } if (!connected) { LogPrint(eLogError, "Reseed: Failed to connect to ", url.host); return ""; } } } if (!ecode) { SSL_set_tlsext_host_name(s.native_handle(), url.host.c_str ()); s.handshake (boost::asio::ssl::stream_base::client, ecode); if (!ecode) { LogPrint (eLogDebug, "Reseed: Connected to ", url.host, ":", url.port); return ReseedRequest (s, url.to_string()); } else LogPrint (eLogError, "Reseed: SSL handshake failed: ", ecode.message ()); } else LogPrint (eLogError, "Reseed: Couldn't connect to ", url.host, ": ", ecode.message ()); return ""; } template std::string Reseeder::ReseedRequest (Stream& s, const std::string& uri) { boost::system::error_code ecode; i2p::http::HTTPReq req; req.uri = uri; req.AddHeader("User-Agent", "Wget/1.11.4"); req.AddHeader("Connection", "close"); s.write_some (boost::asio::buffer (req.to_string())); // read response std::stringstream rs; char recv_buf[1024]; size_t l = 0; do { l = s.read_some (boost::asio::buffer (recv_buf, sizeof(recv_buf)), ecode); if (l) rs.write (recv_buf, l); } while (!ecode && l); // process response std::string data = rs.str(); i2p::http::HTTPRes res; int len = res.parse(data); if (len <= 0) { LogPrint(eLogWarning, "Reseed: Incomplete/broken response from ", uri); return ""; } if (res.code != 200) { LogPrint(eLogError, "Reseed: Failed to reseed from ", uri, ", http code ", res.code); return ""; } data.erase(0, len); /* drop http headers from response */ LogPrint(eLogDebug, "Reseed: Got ", data.length(), " bytes of data from ", uri); if (res.is_chunked()) { std::stringstream in(data), out; if (!i2p::http::MergeChunkedResponse(in, out)) { LogPrint(eLogWarning, "Reseed: Failed to merge chunked response from ", uri); return ""; } LogPrint(eLogDebug, "Reseed: Got ", data.length(), "(", out.tellg(), ") bytes of data from ", uri); data = out.str(); } return data; } std::string Reseeder::YggdrasilRequest (const std::string& address) { i2p::http::URL url; if (!url.parse(address)) { LogPrint(eLogError, "Reseed: Failed to parse url: ", address); return ""; } url.schema = "http"; if (!url.port) url.port = 80; boost::system::error_code ecode; boost::asio::io_context service; boost::asio::ip::tcp::socket s(service, boost::asio::ip::tcp::v6()); auto endpoints = boost::asio::ip::tcp::resolver(service).resolve (url.host, std::to_string(url.port), ecode); if (!ecode) { bool connected = false; for (auto it = endpoints.begin (); it != endpoints.end ();) { boost::asio::ip::tcp::endpoint ep = *it; if ( i2p::util::net::IsYggdrasilAddress (ep.address ()) && i2p::context.SupportsMesh () ) { LogPrint (eLogDebug, "Reseed: Yggdrasil: Resolved to ", ep.address ()); s.connect (ep, ecode); if (!ecode) { connected = true; break; } } it++; } if (!connected) { LogPrint(eLogError, "Reseed: Yggdrasil: Failed to connect to ", url.host); return ""; } } if (!ecode) { LogPrint (eLogDebug, "Reseed: Yggdrasil: Connected to ", url.host, ":", url.port); return ReseedRequest (s, url.to_string()); } else LogPrint (eLogError, "Reseed: Yggdrasil: Couldn't connect to ", url.host, ": ", ecode.message ()); return ""; } } }