/* * Copyright (c) 2013-2022, 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" 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(eLogError, "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(eLogError, "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(eLogError, "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(eLogError, "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(eLogError, "Reseed: Bad proxy url: ", proxy); return ""; } } else { LogPrint(eLogError, "Reseed: Bad proxy url: ", proxy); return ""; } } i2p::http::URL url; if (!url.parse(address)) { LogPrint(eLogError, "Reseed: Failed to parse url: ", address); return ""; } url.schema = "https"; if (!url.port) url.port = 443; boost::asio::io_service 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( boost::asio::ip::tcp::resolver::query(proxyUrl.host, std::to_string(proxyUrl.port)), ecode); if (!ecode) { s.lowest_layer().connect(*it, 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(boost::asio::buffer_cast(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 uint8_t hs_writebuf[3] = {0x05, 0x01, 0x00}; uint8_t hs_readbuf[2]; boost::asio::write(sock, boost::asio::buffer(hs_writebuf, 3), boost::asio::transfer_all(), ecode); if (ecode) { sock.close(); LogPrint(eLogError, "Reseed: SOCKS handshake write failed: ", ecode.message()); return ""; } boost::asio::read(sock, boost::asio::buffer(hs_readbuf, 2), ecode); if (ecode) { sock.close(); LogPrint(eLogError, "Reseed: SOCKS handshake read failed: ", ecode.message()); return ""; } size_t sz = 0; uint8_t buf[256]; buf[0] = 0x05; buf[1] = 0x01; buf[2] = 0x00; buf[3] = 0x03; sz += 4; size_t hostsz = url.host.size(); if (1 + 2 + hostsz + sz > sizeof(buf)) { sock.close(); LogPrint(eLogError, "Reseed: SOCKS handshake failed, hostname too big: ", url.host); return ""; } buf[4] = (uint8_t) hostsz; memcpy(buf + 5, url.host.c_str(), hostsz); sz += hostsz + 1; htobe16buf(buf + sz, url.port); sz += 2; boost::asio::write(sock, boost::asio::buffer(buf, sz), boost::asio::transfer_all(), ecode); if (ecode) { sock.close(); LogPrint(eLogError, "Reseed: SOCKS handshake failed writing: ", ecode.message()); return ""; } boost::asio::read(sock, boost::asio::buffer(buf, 10), ecode); if (ecode) { sock.close(); LogPrint(eLogError, "Reseed: SOCKS handshake failed reading: ", ecode.message()); return ""; } if (buf[1] != 0x00) { sock.close(); LogPrint(eLogError, "Reseed: SOCKS handshake bad reply code: ", std::to_string(buf[1])); return ""; } } } } } else { // direct connection auto it = boost::asio::ip::tcp::resolver(service).resolve( boost::asio::ip::tcp::resolver::query(url.host, std::to_string(url.port)), ecode); if (!ecode) { bool connected = false; boost::asio::ip::tcp::resolver::iterator end; while (it != end) { boost::asio::ip::tcp::endpoint ep = *it; if ((ep.address().is_v4() && i2p::context.SupportsV4()) || (ep.address().is_v6() && i2p::context.SupportsV6())) { s.lowest_layer().connect(ep, ecode); if (!ecode) { 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_service service; boost::asio::ip::tcp::socket s(service, boost::asio::ip::tcp::v6()); if (url.host.length() < 2) return ""; // assume [] auto host = url.host.substr(1, url.host.length() - 2); LogPrint(eLogDebug, "Reseed: Connecting to Yggdrasil ", url.host, ":", url.port); s.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::address_v6::from_string(host), url.port), ecode); if (!ecode) { LogPrint(eLogDebug, "Reseed: Connected to Yggdrasil ", url.host, ":", url.port); return ReseedRequest(s, url.to_string()); } else LogPrint(eLogError, "Reseed: Couldn't connect to Yggdrasil ", url.host, ": ", ecode.message()); return ""; } } }