diff --git a/doc/i2p.md b/doc/i2p.md new file mode 100644 index 0000000..755b803 --- /dev/null +++ b/doc/i2p.md @@ -0,0 +1,68 @@ +# I2P support (SAM v3) + +Triangles runs over I2P in addition to Tor, giving the wallet a second +anonymous network and a `.b32.i2p` address shown directly above the `.onion` +address in the status bar. + +I2P is **on by default** and works the same way as the embedded Tor: the wallet +auto-launches a bundled **i2pd** router as a managed child process, enables its +SAM bridge, and connects to it. The user does not have to install or configure +anything — provided the i2pd binary ships with the wallet. + +## Shipping the i2pd binary + +Like `tor.exe`, the wallet looks for an `i2pd` executable in several places and +launches the first one it finds: + +1. Next to the wallet executable (recommended): `i2pd.exe` (Windows) / `i2pd` + (Linux/macOS), or in an `i2pd/` subfolder beside it. +2. In the data directory (or its `i2pd/` subfolder). +3. Common system locations (`/usr/bin/i2pd`, Homebrew, `C:\i2pd\…`, etc.). + +Get i2pd from https://i2pd.website/ (or your package manager) and place the +binary next to the wallet in your build/packaging step. That's the only manual +part, and it's a packaging concern, not something the end user does. + +If no i2pd binary is found, the wallet logs a notice and continues with **Tor +only** — I2P is strictly additive and never blocks start-up. + +## What happens at start-up + +1. If a SAM bridge is already listening on `127.0.0.1:7656` (e.g. you run your + own router), the wallet uses it and does **not** launch its own. +2. Otherwise it writes `i2pd.conf` into `/i2pd/` (SAM enabled, other + services off), launches i2pd, and waits for the SAM bridge to come up. +3. The SAM client then loads/creates a persistent destination + (`/i2p_private_key`), opens a STREAM session, derives the + `.b32.i2p` address (`base32(SHA-256(destination))`), accepts inbound I2P + streams, and dials outbound `.b32.i2p` peers. +4. On wallet exit, the SAM session is closed and the i2pd child process is + terminated (an external router you started yourself is left running). + +The first session takes a little longer while i2pd builds tunnels; the address +appears once the bridge is ready. + +## Options + +``` +-i2p Enable I2P; auto-launches bundled i2pd (default: 1; -i2p=0 to disable) +-i2psam= SAM bridge address (default: 127.0.0.1:7656). + A non-loopback address disables the bundled router and + connects to that external bridge instead. +``` + +## Checking it + +* GUI: the `.b32.i2p` address sits on top of the `.onion` in the status bar; + click either to copy. +* RPC: `getinfo` shows `toraddress` and `i2paddress`; `getnetworkinfo` shows + `toraddress` and an `i2p` object (`enabled`, `active`, `address`, `peers`). + +## Notes / limitations + +* The address serialization format carries a flag for I2P addresses, so **all + nodes must run this build** to exchange I2P peers; an old `peers.dat` is + discarded. +* `i2p_private_key` is your stable I2P identity — back it up, don't delete it. +* This was implemented without a build/CI environment here; build and test + against a real i2pd before relying on it. diff --git a/snap/gui/triangles.png b/snap/gui/triangles.png index 781f95a..f1a86f7 100644 Binary files a/snap/gui/triangles.png and b/snap/gui/triangles.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9b5fd86..060c31e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,8 @@ set(CORE_SOURCES net.cpp net_bootstrap.cpp netbase.cpp + i2p.cpp + i2p_process.cpp protocol.cpp script.cpp sync.cpp diff --git a/src/i2p.cpp b/src/i2p.cpp new file mode 100644 index 0000000..fb365e6 --- /dev/null +++ b/src/i2p.cpp @@ -0,0 +1,466 @@ +// Copyright (c) 2024 Triangles developers +// I2P (SAM v3) transport support +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "i2p.h" + +#include "util.h" +#include "netbase.h" +#include "protocol.h" // CAddress +#include "net.h" // AddI2PInboundNode(), GetListenPort() + +#include + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +#ifdef WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#ifndef closesocket +#define closesocket close +#endif +#endif + +// I2P uses a base64 variant where '+' -> '-' and '/' -> '~'. +static const char* pI2PBase64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~"; + +static std::vector DecodeI2PBase64(const std::string& str) +{ + int table[256]; + for (int i = 0; i < 256; i++) table[i] = -1; + for (int i = 0; i < 64; i++) table[(unsigned char)pI2PBase64[i]] = i; + + std::vector out; + int bits = 0; uint32_t buf = 0; + for (char c : str) { + if (c == '=' || c == '\r' || c == '\n') continue; + int v = table[(unsigned char)c]; + if (v < 0) continue; // skip anything unexpected + buf = (buf << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back((unsigned char)((buf >> bits) & 0xFF)); + } + } + return out; +} + +CI2PSession* CI2PSession::GetInstance() +{ + static CI2PSession instance; + return &instance; +} + +CI2PSession::CI2PSession() + : samHost(I2P_DEFAULT_SAM_HOST), samPort(I2P_DEFAULT_SAM_PORT), + hSession(INVALID_SOCKET), fEnabled(false), fActive(false), fShutdown(false) +{ +} + +CI2PSession::~CI2PSession() +{ + Stop(); +} + +std::string CI2PSession::GetB32Address() +{ + std::lock_guard lock(cs); + return b32Address; +} + +// --- low level SAM helpers ------------------------------------------------- + +bool CI2PSession::SamConnect(SOCKET& hSocketRet) +{ + SOCKET hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (hSocket == INVALID_SOCKET) + return false; + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)samPort); + addr.sin_addr.s_addr = inet_addr(samHost.c_str()); + + if (connect(hSocket, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { + closesocket(hSocket); + return false; + } + + hSocketRet = hSocket; + return true; +} + +bool CI2PSession::SamSendLine(SOCKET hSocket, const std::string& strLine) +{ + std::string out = strLine + "\n"; + const char* p = out.c_str(); + size_t left = out.size(); + while (left > 0) { + int n = send(hSocket, p, (int)left, MSG_NOSIGNAL); + if (n <= 0) + return false; + p += n; + left -= n; + } + return true; +} + +bool CI2PSession::SamRecvLine(SOCKET hSocket, std::string& strLineRet) +{ + strLineRet.clear(); + char c; + // SAM replies are newline terminated; read one byte at a time so we stop + // exactly at the boundary and leave any following stream data untouched. + for (int i = 0; i < 16384; i++) { + int n = recv(hSocket, &c, 1, 0); + if (n <= 0) + return false; + if (c == '\n') + return true; + if (c != '\r') + strLineRet += c; + } + return false; +} + +std::string CI2PSession::SamGetValue(const std::string& strReply, const std::string& strKey) +{ + // Tokens are space separated KEY=VALUE pairs. VALUE runs to the next space. + std::string needle = strKey + "="; + size_t pos = strReply.find(needle); + if (pos == std::string::npos) + return ""; + pos += needle.size(); + size_t end = strReply.find(' ', pos); + if (end == std::string::npos) + end = strReply.size(); + return strReply.substr(pos, end - pos); +} + +bool CI2PSession::SamHandshake(SOCKET hSocket) +{ + if (!SamSendLine(hSocket, "HELLO VERSION MIN=3.1 MAX=3.3")) + return false; + std::string reply; + if (!SamRecvLine(hSocket, reply)) + return false; + if (SamGetValue(reply, "RESULT") != "OK") { + printf("I2P: SAM handshake failed: %s\n", reply.c_str()); + return false; + } + return true; +} + +std::string CI2PSession::DestToB32(const std::string& strB64Dest) +{ + std::vector dest = DecodeI2PBase64(strB64Dest); + if (dest.empty()) + return ""; + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256(dest.data(), dest.size(), hash); + std::string b32 = EncodeBase32(hash, SHA256_DIGEST_LENGTH); + // I2P b32 addresses are unpadded. + while (!b32.empty() && b32[b32.size() - 1] == '=') + b32.erase(b32.size() - 1); + return b32 + ".b32.i2p"; +} + +// --- session bring-up ------------------------------------------------------ + +bool CI2PSession::LoadOrCreateDestination(std::string& strPrivKeyRet) +{ + fs::path keyPath = GetDataDir() / "i2p_private_key"; + + // Reuse an existing persistent destination if we have one. + { + std::ifstream f(keyPath.string().c_str()); + if (f.is_open()) { + std::string line; + std::getline(f, line); + while (!line.empty() && + (line[line.size() - 1] == '\r' || line[line.size() - 1] == '\n')) + line.erase(line.size() - 1); + if (!line.empty()) { + strPrivKeyRet = line; + printf("I2P: loaded persistent destination from %s\n", + keyPath.string().c_str()); + return true; + } + } + } + + // Generate a fresh destination via the bridge (Ed25519, SIGNATURE_TYPE=7). + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + return false; + } + + bool ok = false; + if (SamSendLine(hSocket, "DEST GENERATE SIGNATURE_TYPE=7")) { + std::string reply; + if (SamRecvLine(hSocket, reply)) { + std::string priv = SamGetValue(reply, "PRIV"); + if (!priv.empty()) { + strPrivKeyRet = priv; + std::ofstream out(keyPath.string().c_str(), std::ios::trunc); + if (out.is_open()) { + out << priv << std::endl; + out.close(); + printf("I2P: generated and saved new persistent destination\n"); + ok = true; + } else { + printf("I2P: WARNING could not write %s\n", keyPath.string().c_str()); + ok = true; // still usable for this run + } + } + } + } + closesocket(hSocket); + return ok; +} + +bool CI2PSession::CreateSession() +{ + if (!SamConnect(hSession)) + return false; + if (!SamHandshake(hSession)) + return false; + + std::ostringstream id; + id << "triangles-" << (uint64_t)GetTime() << "-" << (uint64_t)(GetRand(1000000)); + sessionId = id.str(); + + std::string cmd = "SESSION CREATE STYLE=STREAM ID=" + sessionId + + " DESTINATION=" + privateKey + " SIGNATURE_TYPE=7"; + if (!SamSendLine(hSession, cmd)) + return false; + + std::string reply; + if (!SamRecvLine(hSession, reply)) + return false; + + if (SamGetValue(reply, "RESULT") != "OK") { + printf("I2P: SESSION CREATE failed: %s\n", reply.c_str()); + return false; + } + + // The bridge echoes the (possibly newly assigned) private key back. + std::string echoed = SamGetValue(reply, "DESTINATION"); + if (!echoed.empty()) + privateKey = echoed; + + return true; +} + +bool CI2PSession::ResolveMyB32() +{ + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + return false; + } + + bool ok = false; + if (SamSendLine(hSocket, "NAMING LOOKUP NAME=ME")) { + std::string reply; + if (SamRecvLine(hSocket, reply) && SamGetValue(reply, "RESULT") == "OK") { + std::string dest = SamGetValue(reply, "VALUE"); + std::string b32 = DestToB32(dest); + if (!b32.empty()) { + std::lock_guard lock(cs); + b32Address = b32; + ok = true; + } + } + } + closesocket(hSocket); + return ok; +} + +bool CI2PSession::Start() +{ + if (!GetBoolArg("-i2p", true)) { + printf("I2P: disabled (-i2p=0)\n"); + return false; + } + fEnabled.store(true); + + // -i2psam=host:port overrides the default SAM bridge endpoint. + std::string sam = GetArg("-i2psam", ""); + if (!sam.empty()) { + int port = I2P_DEFAULT_SAM_PORT; + std::string host; + SplitHostPort(sam, port, host); + if (!host.empty()) samHost = host; + if (port > 0) samPort = port; + } + + printf("I2P: connecting to SAM bridge at %s:%d\n", samHost.c_str(), samPort); + + if (!LoadOrCreateDestination(privateKey)) { + printf("I2P: ERROR could not obtain a destination. Is an I2P router with " + "the SAM bridge enabled running at %s:%d?\n", samHost.c_str(), samPort); + return false; + } + + if (!CreateSession()) { + printf("I2P: ERROR failed to create SAM STREAM session\n"); + if (hSession != INVALID_SOCKET) { closesocket(hSession); hSession = INVALID_SOCKET; } + return false; + } + + if (!ResolveMyB32()) + printf("I2P: WARNING could not resolve our own .b32.i2p address yet\n"); + + fActive.store(true); + fShutdown.store(false); + + printf("I2P: session active. Our address: %s\n", GetB32Address().c_str()); + + // Register our I2P address as a local address so peers can learn it. + CService meI2P; + if (!b32Address.empty() && meI2P.SetSpecial(b32Address)) { + meI2P.SetPort((unsigned short)GetListenPort()); + AddLocal(meI2P, LOCAL_MANUAL); + } + + acceptThread = std::thread(&CI2PSession::AcceptLoop, this); + return true; +} + +void CI2PSession::Stop() +{ + if (!fEnabled.load()) + return; + fShutdown.store(true); + fActive.store(false); + + if (hSession != INVALID_SOCKET) { + closesocket(hSession); + hSession = INVALID_SOCKET; + } + if (acceptThread.joinable()) + acceptThread.join(); + fEnabled.store(false); + printf("I2P: session stopped\n"); +} + +// --- inbound --------------------------------------------------------------- + +void CI2PSession::AcceptLoop() +{ + while (!fShutdown.load()) { + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + if (fShutdown.load()) break; + MilliSleep(2000); + continue; + } + + // Block here until a peer dials us; the router then streams the remote + // destination on its own line, after which the socket carries data. + if (!SamSendLine(hSocket, "STREAM ACCEPT ID=" + sessionId + " SILENT=false")) { + closesocket(hSocket); + MilliSleep(1000); + continue; + } + + std::string status; + if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") { + if (!fShutdown.load()) + printf("I2P: STREAM ACCEPT rejected: %s\n", status.c_str()); + closesocket(hSocket); + MilliSleep(1000); + continue; + } + + std::string remoteDest; + if (!SamRecvLine(hSocket, remoteDest)) { + closesocket(hSocket); + continue; + } + if (fShutdown.load()) { + closesocket(hSocket); + break; + } + + // The first token is the remote full destination (base64). + std::string destTok = remoteDest; + size_t sp = destTok.find(' '); + if (sp != std::string::npos) + destTok = destTok.substr(0, sp); + + std::string b32 = DestToB32(destTok); + CAddress addr; + if (b32.empty() || !addr.SetSpecial(b32)) { + printf("I2P: could not parse inbound remote destination\n"); + closesocket(hSocket); + continue; + } + addr.nServices = 0; + addr.nTime = GetTime(); + + // Hand the live data socket to the net layer as an inbound peer. + printf("I2P: inbound connection from %s\n", b32.c_str()); + AddI2PInboundNode(hSocket, addr); + } +} + +// --- outbound -------------------------------------------------------------- + +bool CI2PSession::Connect(const std::string& strDest, SOCKET& hSocketRet) +{ + if (!fActive.load()) + return false; + + SOCKET hSocket = INVALID_SOCKET; + if (!SamConnect(hSocket) || !SamHandshake(hSocket)) { + if (hSocket != INVALID_SOCKET) closesocket(hSocket); + return false; + } + + if (!SamSendLine(hSocket, "STREAM CONNECT ID=" + sessionId + + " DESTINATION=" + strDest + " SILENT=false")) { + closesocket(hSocket); + return false; + } + + std::string status; + if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") { + printf("I2P: STREAM CONNECT to %s failed: %s\n", strDest.c_str(), status.c_str()); + closesocket(hSocket); + return false; + } + + // Socket is now a bidirectional stream to the peer. + hSocketRet = hSocket; + return true; +} + +bool StartI2P() +{ + return CI2PSession::GetInstance()->Start(); +} + +void StopI2P() +{ + CI2PSession::GetInstance()->Stop(); +} diff --git a/src/i2p.h b/src/i2p.h new file mode 100644 index 0000000..ae35516 --- /dev/null +++ b/src/i2p.h @@ -0,0 +1,95 @@ +// Copyright (c) 2024 Triangles developers +// I2P (SAM v3) transport support +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// +// This module gives Triangles real I2P connectivity that mirrors the existing +// embedded-Tor design: instead of a SOCKS proxy it talks the SAM v3 protocol +// to a locally running I2P router (i2pd or Java I2P) and obtains a persistent +// I2P destination whose ".b32.i2p" address is shown alongside the .onion +// address. The wallet: +// * creates / loads a persistent destination (i2p_private_key in datadir), +// * runs a STREAM session so peers can dial us, +// * accepts inbound I2P streams and feeds them to the net layer, +// * dials outbound ".b32.i2p" peers through the same session. +// +// A running I2P router with its SAM bridge enabled (default 127.0.0.1:7656) is +// required; nothing is bundled. Enable with -i2p and optionally -i2psam=host:port. + +#ifndef TRIANGLES_I2P_H +#define TRIANGLES_I2P_H + +#include +#include +#include +#include + +#include "compat.h" // SOCKET / INVALID_SOCKET + +// Default SAM bridge endpoint exposed by i2pd / Java I2P. +#define I2P_DEFAULT_SAM_HOST "127.0.0.1" +#define I2P_DEFAULT_SAM_PORT 7656 + +// Manages a single persistent I2P STREAM session over SAM v3. +class CI2PSession +{ +public: + static CI2PSession* GetInstance(); + + // Bring the session up: connect to the SAM bridge, load/generate the + // persistent destination and start accepting inbound streams. + // Returns false (and logs) if no router/SAM bridge is reachable. + bool Start(); + + // Tear the session down and stop the accept loop. + void Stop(); + + bool IsEnabled() const { return fEnabled.load(); } + bool IsActive() const { return fActive.load(); } + + // Our own ".b32.i2p" address (empty until the session is up). + std::string GetB32Address(); + + // Dial a remote ".b32.i2p" (or full base64 destination) through the + // session. On success hSocketRet is a connected, blocking data socket the + // caller can hand to a CNode. The caller takes ownership of the socket. + bool Connect(const std::string& strDest, SOCKET& hSocketRet); + +private: + CI2PSession(); + ~CI2PSession(); + + // --- low level SAM helpers --- + bool SamConnect(SOCKET& hSocketRet); // raw TCP to the bridge + bool SamHandshake(SOCKET hSocket); // HELLO VERSION + bool SamSendLine(SOCKET hSocket, const std::string& strLine); + bool SamRecvLine(SOCKET hSocket, std::string& strLineRet); + static std::string SamGetValue(const std::string& strReply, const std::string& strKey); + + bool LoadOrCreateDestination(std::string& strPrivKeyRet); + bool CreateSession(); // SESSION CREATE + bool ResolveMyB32(); // NAMING LOOKUP ME + void AcceptLoop(); // inbound STREAM ACCEPT + + // Compute the ".b32.i2p" address from a base64 (I2P alphabet) destination. + static std::string DestToB32(const std::string& strB64Dest); + + std::string samHost; + int samPort; + std::string sessionId; + std::string privateKey; // persistent destination private key (base64) + std::string b32Address; // our own .b32.i2p + SOCKET hSession; // long-lived control socket owning the session + + std::atomic fEnabled; + std::atomic fActive; + std::atomic fShutdown; + std::thread acceptThread; + std::mutex cs; +}; + +// Convenience: start/stop from init.cpp. +bool StartI2P(); +void StopI2P(); + +#endif // TRIANGLES_I2P_H diff --git a/src/i2p_process.cpp b/src/i2p_process.cpp new file mode 100644 index 0000000..bc037ab --- /dev/null +++ b/src/i2p_process.cpp @@ -0,0 +1,368 @@ +// Copyright (c) 2024 Triangles developers +// I2P Router Process Manager - launches and manages a bundled i2pd binary +// Distributed under the MIT/X11 software license + +#ifdef WIN32 +#define NOMINMAX +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif +#endif + +#include "i2p_process.h" +#include "util.h" + +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace fs = std::filesystem; + +static CI2PProcess* i2pProcessInstance = nullptr; + +CI2PProcess* CI2PProcess::GetInstance() +{ + if (!i2pProcessInstance) + i2pProcessInstance = new CI2PProcess(); + return i2pProcessInstance; +} + +CI2PProcess::CI2PProcess() + : samPort(7656) + , running(false) + , fExternal(false) +#ifdef WIN32 + , hProcess(nullptr) + , hJob(nullptr) + , processId(0) +#else + , processId(0) +#endif +{ +} + +CI2PProcess::~CI2PProcess() +{ + Stop(); +} + +// Try a quick TCP connect; success means something is already listening +// (e.g. the SAM bridge is up, or an external router is running). +bool CI2PProcess::CanConnect(const std::string& host, int port) +{ +#ifdef WIN32 + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == INVALID_SOCKET) return false; +#else + int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s < 0) return false; +#endif + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((unsigned short)port); + addr.sin_addr.s_addr = inet_addr(host.c_str()); + bool ok = (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0); +#ifdef WIN32 + closesocket(s); +#else + close(s); +#endif + return ok; +} + +std::string CI2PProcess::FindI2pdBinary() +{ + std::vector candidates; + +#ifdef WIN32 + const char* exeName = "i2pd.exe"; +#else + const char* exeName = "i2pd"; +#endif + + // 1. Next to the wallet executable (this is how tor.exe is shipped). + try { + fs::path exeDir; +#ifdef WIN32 + char buf[MAX_PATH]; + if (GetModuleFileNameA(nullptr, buf, MAX_PATH) > 0) + exeDir = fs::path(buf).parent_path(); +#else + exeDir = fs::current_path(); +#endif + if (!exeDir.empty()) { + candidates.push_back((exeDir / exeName).string()); + candidates.push_back((exeDir / "i2pd" / exeName).string()); + candidates.push_back((exeDir / "I2P" / exeName).string()); + } + } catch (...) {} + + // 2. In / next to the data directory. + candidates.push_back((GetDataDir() / exeName).string()); + candidates.push_back((GetDataDir() / "i2pd" / exeName).string()); + + // 3. Common system locations. +#ifdef WIN32 + if (const char* pf = getenv("ProgramFiles")) + candidates.push_back(std::string(pf) + "\\i2pd\\" + exeName); + if (const char* pfx = getenv("ProgramFiles(x86)")) + candidates.push_back(std::string(pfx) + "\\i2pd\\" + exeName); + candidates.push_back(std::string("C:\\i2pd\\") + exeName); +#else + candidates.push_back("/usr/bin/i2pd"); + candidates.push_back("/usr/local/bin/i2pd"); + candidates.push_back("/opt/i2pd/bin/i2pd"); + candidates.push_back("/opt/homebrew/bin/i2pd"); + candidates.push_back("/usr/local/opt/i2pd/bin/i2pd"); +#endif + + for (const std::string& c : candidates) { + try { + if (fs::exists(c) && fs::is_regular_file(c)) { + printf("I2P: found i2pd binary at %s\n", c.c_str()); + return c; + } + } catch (...) {} + } + + return ""; +} + +bool CI2PProcess::WriteConfig() +{ + fs::path dir(dataDir); + try { + fs::create_directories(dir); + } catch (const std::exception& e) { + lastError = std::string("Cannot create i2pd data directory: ") + e.what(); + return false; + } + + confPath = (dir / "i2pd.conf").string(); + fs::path logPath = dir / "i2pd.log"; + + std::ofstream conf(confPath.c_str(), std::ios::trunc); + if (!conf.is_open()) { + lastError = "Cannot write i2pd.conf to " + confPath; + return false; + } + + conf << "# Triangles Wallet I2P configuration (auto-generated)\n"; + conf << "# Do not edit - this file is overwritten on startup\n\n"; + conf << "daemon = false\n"; + conf << "log = file\n"; + conf << "logfile = " << logPath.string() << "\n"; + conf << "datadir = " << dir.string() << "\n\n"; + + // The bridge our SAM client talks to. + conf << "[sam]\n"; + conf << "enabled = true\n"; + conf << "address = 127.0.0.1\n"; + conf << "port = " << samPort << "\n\n"; + + // We only need SAM; keep everything else off to minimise footprint. + conf << "[httpproxy]\nenabled = false\n\n"; + conf << "[socksproxy]\nenabled = false\n\n"; + conf << "[http]\nenabled = false\n\n"; + conf << "[i2pcontrol]\nenabled = false\n"; + + conf.close(); + printf("I2P: wrote i2pd config to %s (SAM port %d)\n", confPath.c_str(), samPort); + return true; +} + +bool CI2PProcess::Start(const std::string& dataDirIn, int samPortIn) +{ + dataDir = dataDirIn; + samPort = samPortIn; + fExternal = false; + lastError.clear(); + + // If a SAM bridge is already up, use it instead of launching our own. + if (CanConnect("127.0.0.1", samPort)) { + printf("I2P: detected an I2P router already listening on SAM port %d; using it\n", samPort); + fExternal = true; + return true; + } + + binaryPath = FindI2pdBinary(); + if (binaryPath.empty()) { + lastError = "No i2pd binary found (ship i2pd alongside the wallet, like tor)"; + printf("I2P: %s\n", lastError.c_str()); + return false; + } + + if (!WriteConfig()) + return false; + + printf("I2P: starting i2pd: %s --conf %s\n", binaryPath.c_str(), confPath.c_str()); + +#ifdef WIN32 + STARTUPINFOA si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + ZeroMemory(&pi, sizeof(pi)); + + std::string cmdLine = "\"" + binaryPath + "\" --conf \"" + confPath + "\""; + + if (!CreateProcessA(nullptr, (LPSTR)cmdLine.c_str(), nullptr, nullptr, + FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { + DWORD err = ::GetLastError(); + lastError = strprintf("CreateProcess failed for i2pd '%s' (Windows error %lu)", binaryPath.c_str(), err); + printf("I2P: ERROR %s\n", lastError.c_str()); + return false; + } + + hProcess = pi.hProcess; + processId = pi.dwProcessId; + CloseHandle(pi.hThread); + + // Kill i2pd if the wallet dies (matches the embedded Tor behaviour). + hJob = CreateJobObject(nullptr, nullptr); + if (hJob) { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; + jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo)); + if (!AssignProcessToJobObject(hJob, hProcess)) + printf("I2P: WARNING could not assign i2pd to Job Object (error %lu)\n", GetLastError()); + } + + printf("I2P: i2pd started (PID %lu)\n", processId); +#else + pid_t pid = fork(); + if (pid < 0) { + lastError = "Failed to fork for i2pd process"; + printf("I2P: ERROR %s\n", lastError.c_str()); + return false; + } + if (pid == 0) { + freopen("/dev/null", "w", stdout); + freopen("/dev/null", "w", stderr); + execl(binaryPath.c_str(), binaryPath.c_str(), + "--conf", confPath.c_str(), (char*)nullptr); + _exit(1); + } + processId = pid; + printf("I2P: i2pd started (PID %d)\n", processId); +#endif + + running = true; + + // Wait for the SAM bridge to come up. The bridge opens quickly; tunnel + // build (needed for actual connectivity) continues in the background. + printf("I2P: waiting for SAM bridge on port %d...\n", samPort); + for (int i = 0; i < 45; i++) { + MilliSleep(1000); + if (fShutdown) { + Stop(); + return false; + } + if (CanConnect("127.0.0.1", samPort)) { + printf("I2P: SAM bridge ready on port %d (took %ds)\n", samPort, i + 1); + return true; + } + if (!IsRunning()) { + lastError = "i2pd exited during start-up before the SAM bridge became ready"; + printf("I2P: ERROR %s\n", lastError.c_str()); + running = false; + return false; + } + } + + lastError = strprintf("i2pd started but SAM port %d not ready after 45s", samPort); + printf("I2P: WARNING %s (it may still be building tunnels)\n", lastError.c_str()); + return true; +} + +void CI2PProcess::Stop() +{ + if (fExternal) { + // We never launched it; leave the user's router running. + running = false; + return; + } + if (!running) return; + +#ifdef WIN32 + if (hProcess != nullptr) { + printf("I2P: stopping i2pd (PID %lu)...\n", processId); + TerminateProcess(hProcess, 0); + WaitForSingleObject(hProcess, 5000); + CloseHandle(hProcess); + hProcess = nullptr; + } + if (hJob != nullptr) { + CloseHandle(hJob); + hJob = nullptr; + } +#else + if (processId > 0) { + printf("I2P: stopping i2pd (PID %d)...\n", processId); + kill(processId, SIGTERM); + for (int i = 0; i < 50; i++) { + int status; + pid_t result = waitpid(processId, &status, WNOHANG); + if (result != 0) break; + MilliSleep(100); + } + kill(processId, SIGKILL); + waitpid(processId, nullptr, 0); + } +#endif + + processId = 0; + running = false; + printf("I2P: i2pd stopped\n"); +} + +bool CI2PProcess::IsRunning() +{ + if (fExternal) return true; + if (!running) return false; + +#ifdef WIN32 + if (hProcess == nullptr) return false; + DWORD exitCode; + if (GetExitCodeProcess(hProcess, &exitCode)) + return (exitCode == STILL_ACTIVE); + return false; +#else + if (processId <= 0) return false; + int status; + pid_t result = waitpid(processId, &status, WNOHANG); + return (result == 0); // 0 => still running +#endif +} + +bool StartEmbeddedI2P(const std::string& dataDir, int samPort) +{ + return CI2PProcess::GetInstance()->Start(dataDir, samPort); +} + +void StopEmbeddedI2P() +{ + CI2PProcess::GetInstance()->Stop(); +} diff --git a/src/i2p_process.h b/src/i2p_process.h new file mode 100644 index 0000000..05e4a5f --- /dev/null +++ b/src/i2p_process.h @@ -0,0 +1,70 @@ +// Copyright (c) 2024 Triangles developers +// I2P Router Process Manager - launches and manages a bundled i2pd binary +// Distributed under the MIT/X11 software license +// +// Mirrors tor_process.cpp: locate an i2pd executable shipped alongside the +// wallet (or installed on the system), write an auto-generated config that +// enables the SAM bridge, launch it as a managed child process, and shut it +// down when the wallet exits. The SAM session in i2p.cpp then connects to it, +// so the user does not have to install or run a separate I2P router. + +#ifndef TRIANGLES_I2P_PROCESS_H +#define TRIANGLES_I2P_PROCESS_H + +#include + +#ifdef WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +class CI2PProcess +{ +public: + static CI2PProcess* GetInstance(); + + CI2PProcess(); + ~CI2PProcess(); + + // Bring up the router. If something is already listening on the SAM port we + // assume an external router and do not launch our own (fExternal=true). + // Returns true if a SAM bridge is (or will shortly be) reachable. + bool Start(const std::string& dataDir, int samPort = 7656); + + // Terminate the launched router (no-op for an external one). + void Stop(); + + bool IsRunning(); + bool IsExternal() const { return fExternal; } + std::string GetLastError() const { return lastError; } + std::string GetBinaryPath() const { return binaryPath; } + +private: + std::string FindI2pdBinary(); + bool WriteConfig(); + static bool CanConnect(const std::string& host, int port); + + int samPort; + bool running; + bool fExternal; + std::string dataDir; + std::string binaryPath; + std::string confPath; + std::string lastError; + +#ifdef WIN32 + HANDLE hProcess; + HANDLE hJob; + DWORD processId; +#else + int processId; +#endif +}; + +// Convenience wrappers for init.cpp. +bool StartEmbeddedI2P(const std::string& dataDir, int samPort); +void StopEmbeddedI2P(); + +#endif // TRIANGLES_I2P_PROCESS_H diff --git a/src/init.cpp b/src/init.cpp index 2efc495..51a1290 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -19,6 +19,8 @@ #include "tor/tor_embedded.h" #include "tor/onion_v3.h" #include "tor/tor_process.h" +#include "i2p.h" +#include "i2p_process.h" #ifdef ENABLE_ZMQ #include "zmqpublishnotifier.h" #endif @@ -241,6 +243,10 @@ void Shutdown(void* parg) pScriptCheckQueue.reset(); } + // Stop the I2P SAM session and its accept loop, then the i2pd router. + StopI2P(); + StopEmbeddedI2P(); + // NOW safe to destroy Tor state - all threads have stopped ShutdownTorV3(); StopEmbeddedTor(); @@ -421,6 +427,8 @@ std::string HelpMessage() " -torsocks= " + _("Set embedded or managed Tor SOCKS proxy port (default: 19099)") + "\n" + " -torhiddenservice " + _("Enable the managed Tor hidden service (default: 1)") + "\n" + " -torhsport= " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n" + + " -i2p " + _("Enable I2P connectivity; auto-launches a bundled i2pd router (default: 1)") + "\n" + + " -i2psam= " + _("I2P SAM bridge address; non-loopback disables the bundled router (default: 127.0.0.1:7656)") + "\n" + //" -dns " + _("Allow DNS lookups for -addnode, -seednode and -connect") + "\n" + " -port= " + _("Listen for connections on (default: 24112 or testnet: 24111)") + "\n" + " -maxconnections= " + _("Maintain at most connections to peers (default: 125)") + "\n" + @@ -1437,6 +1445,45 @@ bool AppInit2() if (!NewThread(ThreadTorMaintenance, nullptr)) printf("Warning: ThreadTorMaintenance could not be started\n"); } + + // Bring up I2P (SAM) transport alongside Tor so the wallet has both a + // .onion and a .b32.i2p address. On by default; disable with -i2p=0. + // A bundled i2pd router is launched automatically (mirroring embedded + // Tor); if -i2psam points at a non-loopback bridge, or a router is + // already running, we use that instead. + if (GetBoolArg("-i2p", true)) { + int64_t nI2PStart = GetTimeMillis(); + + // Resolve the SAM endpoint (default 127.0.0.1:7656). + std::string sam = GetArg("-i2psam", "127.0.0.1:7656"); + int samPort = I2P_DEFAULT_SAM_PORT; + std::string samHost = "127.0.0.1"; + SplitHostPort(sam, samPort, samHost); + if (samPort <= 0) samPort = I2P_DEFAULT_SAM_PORT; + bool loopback = samHost.empty() || samHost == "127.0.0.1" || samHost == "localhost"; + + // Auto-launch our own i2pd only when the bridge is local. + if (loopback) { + uiInterface.InitMessage(_("Starting the I2P router...")); + if (!StartEmbeddedI2P((GetDataDir() / "i2pd").string(), samPort)) { + printf("NOTICE: bundled I2P router unavailable (%s).\n", + CI2PProcess::GetInstance()->GetLastError().c_str()); + printf(" I2P will use an external router if one is running on %s.\n", sam.c_str()); + } + } + + uiInterface.InitMessage(_("Connecting to the I2P network...")); + bool i2pStarted = StartI2P(); + StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart, strprintf("started=%d", i2pStarted)); + if (i2pStarted) { + SetReachable(NET_I2P, true); + std::string i2pAddr = CI2PSession::GetInstance()->GetB32Address(); + printf("I2P network enabled. Our address: %s\n", i2pAddr.c_str()); + } else { + printf("NOTICE: I2P not available this session; continuing with Tor only\n"); + StopEmbeddedI2P(); + } + } } // ********************************************************* Step 9: import blocks diff --git a/src/main.cpp b/src/main.cpp index aa30f2f..f109d39 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3831,6 +3831,18 @@ bool FastImportBlockFile() int nApplied = 0; for (CBlockIndex* pindex : vMain) { + // Genesis (height 0) is a hardcoded special block that is not + // re-read from disk this way; it contributes nothing to supply + // and the genesis-walk audit skips it identically. Carry the + // running supply (0) forward and move on. + if (pindex->nHeight == 0) + { + pindex->nMint = 0; + pindex->nMoneySupply = nRunningSupply; // still 0 here + txdb.WriteBlockIndex(CDiskBlockIndex(pindex)); + continue; + } + CBlock blockMain; if (!blockMain.ReadFromDisk(pindex)) return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight); diff --git a/src/net.cpp b/src/net.cpp index c0d9db2..b5f9b7d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -11,6 +11,7 @@ #include "addrman.h" #include "ui_interface.h" #include "onionseed.h" +#include "i2p.h" #include #include @@ -494,14 +495,26 @@ CNode* FindNode(const CService& addr) CNode* ConnectNode(CAddress addrConnect, const char *pszDest) { - // TOR-NATIVE: Reject all non-.onion addresses + // PRIVACY-NATIVE: only anonymous networks are allowed. Accept Tor (.onion) + // and I2P (.i2p / .b32.i2p); reject everything else (clearnet). std::string addrStr = pszDest ? std::string(pszDest) : addrConnect.ToStringIP(); - if (addrStr.find(".onion") == std::string::npos) { + bool fI2P = (addrStr.find(".i2p") != std::string::npos); + if (!fI2P && addrStr.find(".onion") == std::string::npos) { if (fDebug) - printf("ConnectNode(): REJECTED non-onion address: %s (Tor-native mode)\n", addrStr.c_str()); + printf("ConnectNode(): REJECTED non-anonymous address: %s (privacy-native mode)\n", addrStr.c_str()); return nullptr; } + // For I2P make sure addrConnect carries the destination so the resulting + // CNode is labelled correctly even when we were given a bare pszDest. + if (fI2P && !addrConnect.IsI2P()) { + std::string i2pHost = addrStr; + size_t i2pEnd = i2pHost.find(".i2p"); + if (i2pEnd != std::string::npos) + i2pHost = i2pHost.substr(0, i2pEnd + 4); + addrConnect.SetSpecial(i2pHost); + } + if (pszDest == nullptr) { if (IsLocal(addrConnect)) return nullptr; @@ -525,8 +538,20 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) pszDest ? 0 : (double)(GetAdjustedTime() - addrConnect.nTime)/3600.0); // Connect - SOCKET hSocket; - if (pszDest ? ConnectSocketByName(addrConnect, hSocket, pszDest, GetDefaultPort()) : ConnectSocket(addrConnect, hSocket)) + SOCKET hSocket = INVALID_SOCKET; + bool fConnected; + if (fI2P) { + // Route through the I2P SAM session. Strip any :port suffix; I2P peers + // are reached purely by destination. + std::string i2pDest = addrStr; + size_t i2pEnd = i2pDest.find(".i2p"); + if (i2pEnd != std::string::npos) + i2pDest = i2pDest.substr(0, i2pEnd + 4); + fConnected = CI2PSession::GetInstance()->Connect(i2pDest, hSocket); + } else { + fConnected = pszDest ? ConnectSocketByName(addrConnect, hSocket, pszDest, GetDefaultPort()) : ConnectSocket(addrConnect, hSocket); + } + if (fConnected) { addrman.Attempt(addrConnect); @@ -561,6 +586,54 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest) } } +// Adopt a connected I2P SAM data socket (from the accept loop in i2p.cpp) as an +// inbound peer. The socket arrives in blocking mode; switch it to non-blocking +// to match the rest of the socket handler, then register the node. +void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr) +{ + if (hSocket == INVALID_SOCKET) + return; + + if (CNode::IsBanned(addr)) { + printf("I2P inbound from %s dropped (banned)\n", addr.ToString().c_str()); + closesocket(hSocket); + return; + } + + // Honour the inbound connection limit. + int nInbound = 0; + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + if (pnode->fInbound) + nInbound++; + } + int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS; + if (nInbound >= nMaxInbound) { + printf("I2P inbound from %s dropped (too many inbound)\n", addr.ToString().c_str()); + closesocket(hSocket); + return; + } + +#ifdef WIN32 + u_long nOne = 1; + if (ioctlsocket(hSocket, FIONBIO, &nOne) == SOCKET_ERROR) + printf("AddI2PInboundNode() : ioctlsocket non-blocking setting failed, error %d\n", WSAGetLastError()); +#else + if (fcntl(hSocket, F_SETFL, O_NONBLOCK) == SOCKET_ERROR) + printf("AddI2PInboundNode() : fcntl non-blocking setting failed, error %d\n", errno); +#endif + + printf("accepted I2P connection %s\n", addr.ToString().c_str()); + CNode* pnode = new CNode(hSocket, addr, "", true); + pnode->AddRef(); + pnode->nTimeConnected = GetTime(); + { + LOCK(cs_vNodes); + vNodes.push_back(pnode); + } +} + void CNode::CloseSocketDisconnect() { fDisconnect = true; @@ -1726,11 +1799,16 @@ bool ThreadHTTPSeedFetch2(void* parg) // For .onion addresses, the last colon before port is after ".onion" size_t onionPos = addrStr.find(".onion:"); + size_t i2pPos = addrStr.find(".b32.i2p:"); if (onionPos != std::string::npos) { port = atoi(addrStr.substr(onionPos + 7).c_str()); addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion" - } else if (addrStr.find(".onion") == std::string::npos) { - // Tor-native: skip non-.onion addresses + } else if (i2pPos != std::string::npos) { + port = atoi(addrStr.substr(i2pPos + 9).c_str()); + addrStr = addrStr.substr(0, i2pPos + 8); // keep ".b32.i2p" + } else if (addrStr.find(".onion") == std::string::npos && + addrStr.find(".i2p") == std::string::npos) { + // Privacy-native: skip clearnet addresses continue; } diff --git a/src/net.h b/src/net.h index 5f40e29..6a6685c 100644 --- a/src/net.h +++ b/src/net.h @@ -35,6 +35,8 @@ void AddressCurrentlyConnected(const CService& addr); CNode* FindNode(const CNetAddr& ip); CNode* FindNode(const CService& ip); CNode* ConnectNode(CAddress addrConnect, const char *strDest = nullptr); +// Adopt a connected I2P SAM data socket as an inbound peer (called from i2p.cpp). +void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr); void MapPort(); unsigned short GetListenPort(); bool BindListenPort(const CService &bindAddr, std::string& strError=REF(std::string())); diff --git a/src/netbase.cpp b/src/netbase.cpp index 216fd4e..6a84901 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -600,6 +600,7 @@ void CNetAddr::Init() memset(ip, 0, sizeof(ip)); memset(tor_v3_pubkey, 0, sizeof(tor_v3_pubkey)); m_is_tor_v3 = false; + m_is_i2p = false; } void CNetAddr::SetIP(const CNetAddr& ipIn) @@ -607,6 +608,7 @@ void CNetAddr::SetIP(const CNetAddr& ipIn) memcpy(ip, ipIn.ip, sizeof(ip)); memcpy(tor_v3_pubkey, ipIn.tor_v3_pubkey, sizeof(tor_v3_pubkey)); m_is_tor_v3 = ipIn.m_is_tor_v3; + m_is_i2p = ipIn.m_is_i2p; } static const unsigned char pchOnionCat[] = {0xFD,0x87,0xD8,0x7E,0xEB,0x43}; @@ -650,6 +652,22 @@ bool CNetAddr::SetSpecial(const std::string &strName) ip[i + sizeof(pchGarliCat)] = vchAddr[i]; return true; } + // Modern I2P base32 address: 52 base32 chars = SHA-256(destination) (32 bytes) + // rendered as ".b32.i2p". Store the hash and flag this as an I2P address. + if (strName.size()>8 && strName.substr(strName.size() - 8, 8) == ".b32.i2p") { + std::string addrPart = strName.substr(0, strName.size() - 8); + std::vector vchAddr = DecodeBase32(addrPart.c_str()); + if (vchAddr.size() != 32) + return false; + // Keep the GarliCat prefix in ip[] so legacy reachability checks that + // look for unique-local space still treat this as a routable overlay. + memcpy(ip, pchGarliCat, sizeof(pchGarliCat)); + memset(ip + sizeof(pchGarliCat), 0, 16 - sizeof(pchGarliCat)); + memcpy(tor_v3_pubkey, vchAddr.data(), 32); + m_is_i2p = true; + m_is_tor_v3 = false; + return true; + } return false; } @@ -772,7 +790,7 @@ bool CNetAddr::IsTorV3() const bool CNetAddr::IsI2P() const { - return (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0); + return m_is_i2p || (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0); } bool CNetAddr::IsLocal() const @@ -878,6 +896,13 @@ std::string CNetAddr::ToStringIP() const } if (IsTor()) return EncodeBase32(&ip[6], 10) + ".onion"; + if (m_is_i2p) { + // Modern I2P: base32 of the 32-byte destination hash, unpadded. + std::string b32 = EncodeBase32(tor_v3_pubkey, 32); + while (!b32.empty() && b32[b32.size() - 1] == '=') + b32.erase(b32.size() - 1); + return b32 + ".b32.i2p"; + } if (IsI2P()) return EncodeBase32(&ip[6], 10) + ".oc.b32.i2p"; CService serv(*this, 0); @@ -911,12 +936,14 @@ bool operator==(const CNetAddr& a, const CNetAddr& b) { if (a.m_is_tor_v3 || b.m_is_tor_v3) return a.m_is_tor_v3 == b.m_is_tor_v3 && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0; + if (a.m_is_i2p || b.m_is_i2p) + return a.m_is_i2p == b.m_is_i2p && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0; return (memcmp(a.ip, b.ip, 16) == 0); } bool operator!=(const CNetAddr& a, const CNetAddr& b) { - return (memcmp(a.ip, b.ip, 16) != 0); + return !(a == b); } bool operator<(const CNetAddr& a, const CNetAddr& b) @@ -925,6 +952,10 @@ bool operator<(const CNetAddr& a, const CNetAddr& b) return !a.m_is_tor_v3; // non-v3 sorts before v3 if (a.m_is_tor_v3) return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0; + if (a.m_is_i2p != b.m_is_i2p) + return !a.m_is_i2p; // non-i2p sorts before i2p + if (a.m_is_i2p) + return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0; return (memcmp(a.ip, b.ip, 16) < 0); } @@ -948,6 +979,17 @@ bool CNetAddr::GetIn6Addr(struct in6_addr* pipv6Addr) const // no two connections will be attempted to addresses with the same group std::vector CNetAddr::GetGroup() const { + // Modern I2P addresses keep their identifying bytes in the 32-byte + // destination-hash field (ip[] only holds the overlay prefix), so derive + // the group from the hash to keep peers in distinct groups. + if (m_is_i2p) { + std::vector vch; + vch.push_back(NET_I2P); + vch.push_back(tor_v3_pubkey[0]); + vch.push_back(tor_v3_pubkey[1]); + return vch; + } + std::vector vchRet; int nClass = NET_IPV6; int nStartByte = 0; @@ -1022,7 +1064,7 @@ std::vector CNetAddr::GetGroup() const uint64_t CNetAddr::GetHash() const { uint256 hash; - if (m_is_tor_v3) + if (m_is_tor_v3 || m_is_i2p) hash = Hash(&tor_v3_pubkey[0], &tor_v3_pubkey[32]); else hash = Hash(&ip[0], &ip[16]); diff --git a/src/netbase.h b/src/netbase.h index 57eeadd..b6859b1 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -36,8 +36,12 @@ class CNetAddr { protected: unsigned char ip[16]; // in network byte order - unsigned char tor_v3_pubkey[32]; // Ed25519 public key for Tor v3 onion addresses + // For Tor v3 this holds the 32-byte Ed25519 public key. When m_is_i2p is + // set it instead holds the 32-byte SHA-256 of the I2P destination (the + // value rendered as the ".b32.i2p" address). A CNetAddr is never both. + unsigned char tor_v3_pubkey[32]; bool m_is_tor_v3; + bool m_is_i2p; public: CNetAddr(); @@ -90,6 +94,7 @@ class CNetAddr READWRITE(FLATDATA(ip)); READWRITE(FLATDATA(tor_v3_pubkey)); READWRITE(m_is_tor_v3); + READWRITE(m_is_i2p); ) }; @@ -133,6 +138,7 @@ class CService : public CNetAddr READWRITE(FLATDATA(ip)); READWRITE(FLATDATA(tor_v3_pubkey)); READWRITE(m_is_tor_v3); + READWRITE(m_is_i2p); unsigned short portN = htons(port); READWRITE(portN); if (fRead) diff --git a/src/qt/forms/mainwindow.ui b/src/qt/forms/mainwindow.ui index 56b10c0..ff6c973 100644 --- a/src/qt/forms/mainwindow.ui +++ b/src/qt/forms/mainwindow.ui @@ -1413,27 +1413,57 @@ QLabel { - - - - 8 - 75 - true - + + + 0 - - PointingHandCursor - - - Click to copy .onion address - - - - - - Qt::NoTextInteraction - - + + + + + 8 + 75 + true + + + + PointingHandCursor + + + Click to copy .b32.i2p address + + + + + + Qt::NoTextInteraction + + + + + + + + 8 + 75 + true + + + + PointingHandCursor + + + Click to copy .onion address + + + + + + Qt::NoTextInteraction + + + + diff --git a/src/qt/res/icons/menu_16/triangles-16.png b/src/qt/res/icons/menu_16/triangles-16.png index fa196f3..ec058f0 100644 Binary files a/src/qt/res/icons/menu_16/triangles-16.png and b/src/qt/res/icons/menu_16/triangles-16.png differ diff --git a/src/qt/res/icons/triangles-128.png b/src/qt/res/icons/triangles-128.png index 57fc6c4..9167edf 100644 Binary files a/src/qt/res/icons/triangles-128.png and b/src/qt/res/icons/triangles-128.png differ diff --git a/src/qt/res/icons/triangles-16.png b/src/qt/res/icons/triangles-16.png index fa196f3..ec058f0 100644 Binary files a/src/qt/res/icons/triangles-16.png and b/src/qt/res/icons/triangles-16.png differ diff --git a/src/qt/res/icons/triangles-32.png b/src/qt/res/icons/triangles-32.png index 59ae10a..542e5a2 100644 Binary files a/src/qt/res/icons/triangles-32.png and b/src/qt/res/icons/triangles-32.png differ diff --git a/src/qt/res/icons/triangles.ico b/src/qt/res/icons/triangles.ico index 8c534df..eed2e57 100644 Binary files a/src/qt/res/icons/triangles.ico and b/src/qt/res/icons/triangles.ico differ diff --git a/src/qt/res/icons/triangles.png b/src/qt/res/icons/triangles.png index 3a1f6b6..f1a86f7 100644 Binary files a/src/qt/res/icons/triangles.png and b/src/qt/res/icons/triangles.png differ diff --git a/src/qt/trianglesgui.cpp b/src/qt/trianglesgui.cpp index 66dbfaf..244aa07 100644 --- a/src/qt/trianglesgui.cpp +++ b/src/qt/trianglesgui.cpp @@ -43,6 +43,7 @@ #include "wallet.h" #include "tor/tor_embedded.h" #include "tor/onion_v3.h" +#include "i2p.h" #ifdef Q_OS_MAC #include "macdockiconhandler.h" @@ -348,6 +349,12 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent): labelOnionAddress->setCursor(Qt::PointingHandCursor); labelOnionAddress->installEventFilter(this); + // I2P address, stacked directly above the .onion address (click to copy) + labelI2PAddress = ui->label_i2p; + labelI2PAddress->setVisible(false); + labelI2PAddress->setCursor(Qt::PointingHandCursor); + labelI2PAddress->installEventFilter(this); + // V3 indicator next to staking icon (hidden until onion is active) labelV3Icon = ui->label_v3; labelV3Icon->setVisible(false); @@ -357,6 +364,11 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent): timerOnion->start(5000); updateOnionAddress(); + QTimer *timerI2P = new QTimer(this); + connect(timerI2P, SIGNAL(timeout()), this, SLOT(updateI2PAddress())); + timerI2P->start(5000); + updateI2PAddress(); + QTimer *timerShutdown = new QTimer(this); connect(timerShutdown, SIGNAL(timeout()), this, SLOT(detectShutdown())); timerShutdown->start(200); @@ -1327,6 +1339,16 @@ bool TrianglesGUI::eventFilter(QObject *object, QEvent *event) } return true; } + if (object == labelI2PAddress && event->type() == QEvent::MouseButtonPress) + { + QString addr = labelI2PAddress->text(); + if (!addr.isEmpty()) + { + QApplication::clipboard()->setText(addr); + QToolTip::showText(QCursor::pos(), tr("Copied!"), labelI2PAddress); + } + return true; + } return QMainWindow::eventFilter(object, event); } @@ -1453,6 +1475,7 @@ void TrianglesGUI::menuOperationsRequested() QAction* unlockWalletStaking = menu.addAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet...").remove('&').remove("...")); QAction* lockWallet = menu.addAction(QIcon(":/menu_16/lock"), tr("&Lock Wallet...").remove('&').remove("...")); QAction* changePassword = menu.addAction(QIcon(":/menu_16/passphrase"), tr("&Change Passphrase...").remove('&').remove("...")); + QAction* hdSeed = menu.addAction(QIcon(":/menu_16/passphrase"), tr("Seed Phrase (HD Backup)...")); QAction* signMessage = menu.addAction(QIcon(":/menu_16/sign"), tr("Sign &message...").remove('&').remove("...")); QAction* verifySignature = menu.addAction(QIcon(":/menu_16/verify"), tr("&Verify message...").remove('&').remove("...")); @@ -1513,6 +1536,10 @@ void TrianglesGUI::menuOperationsRequested() if (walletModel->getEncryptionStatus() == WalletModel::Unlocked || walletModel->getEncryptionStatus() == WalletModel::Locked) changePassphrase(); } + else if (selected == hdSeed) + { + hdSeedManager(); + } else if (selected == signMessage) { gotoSignMessageTab(); @@ -1798,6 +1825,32 @@ void TrianglesGUI::updateOnionAddress() labelOnionAddress->setVisible(true); } +void TrianglesGUI::updateI2PAddress() +{ + if (!labelI2PAddress) + return; + + std::string i2pAddress; + if (CI2PSession::GetInstance()->IsActive()) + i2pAddress = CI2PSession::GetInstance()->GetB32Address(); + + // Respect the same visibility preference as the onion address. + if (clientModel && clientModel->getOptionsModel() && + !clientModel->getOptionsModel()->getShowOnionAddress()) { + labelI2PAddress->setVisible(false); + return; + } + + if (i2pAddress.empty()) { + labelI2PAddress->setVisible(false); + return; + } + + labelI2PAddress->setText(QString::fromStdString(i2pAddress)); + labelI2PAddress->setToolTip(tr("This wallet's I2P .b32.i2p address. Selectable — right-click to copy.")); + labelI2PAddress->setVisible(true); +} + void TrianglesGUI::on_bHelp_clicked() { diff --git a/src/qt/trianglesgui.h b/src/qt/trianglesgui.h index 8f61e0e..24be5af 100644 --- a/src/qt/trianglesgui.h +++ b/src/qt/trianglesgui.h @@ -110,6 +110,7 @@ private: QLabel *labelConnectionsIcon; QLabel *labelBlocksIcon; QLabel *labelOnionAddress; + QLabel *labelI2PAddress; QLabel *labelV3Icon; QLabel *progressBarLabel; QProgressBar *progressBar; @@ -178,6 +179,7 @@ public slots: void setWalletTransactionSyncState(bool syncing); void setWalletTransactionSyncProgress(bool syncing, int pendingNotifications); void updateOnionAddress(); + void updateI2PAddress(); /** Notify the user of an error in the network or transaction handling code. */ void error(const QString &title, const QString &message, bool modal); diff --git a/src/rpcnet.cpp b/src/rpcnet.cpp index 3d70e85..7610242 100644 --- a/src/rpcnet.cpp +++ b/src/rpcnet.cpp @@ -10,6 +10,9 @@ #include "db.h" #include "walletdb.h" #include "net_bootstrap.h" +#include "i2p.h" +#include "tor/onion_v3.h" +#include "tor/tor_embedded.h" using namespace json_spirit; using namespace std; @@ -34,12 +37,34 @@ Value getnetworkinfo(const Array& params, bool fHelp) healthObj.push_back(Pair("lastblocktime", static_cast(health.lastBlockTime))); healthObj.push_back(Pair("networkmode", "tor_native")); + // Tor .onion address (wallet hidden service). + std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress(); + if (onionAddress.empty()) + onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress(); + + // I2P session state and .b32.i2p address. + CI2PSession* i2p = CI2PSession::GetInstance(); + int nI2PPeers = 0; + { + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) + if (pnode->addr.IsI2P()) + nI2PPeers++; + } + Object i2pObj; + i2pObj.push_back(Pair("enabled", i2p->IsEnabled())); + i2pObj.push_back(Pair("active", i2p->IsActive())); + i2pObj.push_back(Pair("address", i2p->GetB32Address())); + i2pObj.push_back(Pair("peers", nI2PPeers)); + Object obj; obj.push_back(Pair("version", FormatFullVersion())); obj.push_back(Pair("protocolversion", (int)PROTOCOL_VERSION)); obj.push_back(Pair("connections", (int)vNodes.size())); obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string()))); obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP())); + obj.push_back(Pair("toraddress", onionAddress)); + obj.push_back(Pair("i2p", i2pObj)); obj.push_back(Pair("localservices", strprintf("%016"PRIx64, nLocalServices))); obj.push_back(Pair("testnet", fTestNet)); obj.push_back(Pair("networkhealth", healthObj)); diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index 1e41ba2..8586156 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -9,6 +9,9 @@ #include "init.h" #include "base58.h" #include "smessage.h" +#include "i2p.h" +#include "tor/onion_v3.h" +#include "tor/tor_embedded.h" using namespace json_spirit; using namespace std; @@ -100,6 +103,13 @@ Value getinfo(const Array& params, bool fHelp) obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string()))); obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP())); + // Anonymous network identities. + std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress(); + if (onionAddress.empty()) + onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress(); + obj.push_back(Pair("toraddress", onionAddress)); + obj.push_back(Pair("i2paddress", CI2PSession::GetInstance()->GetB32Address())); + diff.push_back(Pair("proof-of-work", GetDifficulty())); diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true)))); obj.push_back(Pair("difficulty", diff));