diff --git a/.gitmodules b/.gitmodules index 600bedf..2474544 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/secp256k1"] path = src/secp256k1 url = https://github.com/bitcoin-core/secp256k1 +[submodule "src/i2p/i2pd-src"] + path = src/i2p/i2pd-src + url = https://github.com/PurpleI2P/i2pd.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fb7b2e..5c1a780 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,12 @@ option(USE_O3 "Use -O3 optimization instead of -O2" OFF option(ENABLE_PIE "Build position-independent executables" OFF) option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF) +# Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor. +# When enabled, Triangles supports dual-network anonymity: Tor (.onion) + +# I2P (.b32.i2p). Disabled by default until seed nodes are deployed. +option(USE_I2P_EMBEDDED "Enable embedded I2P (i2pd) library linking" OFF) +set(I2P_SOURCE_ROOT "" CACHE PATH "Path to i2pd source tree (for USE_I2P_EMBEDDED)") + # Cache variables for custom dependency paths set(BDB_INCLUDE_PATH "" CACHE PATH "Path to Berkeley DB headers") set(BDB_LIB_PATH "" CACHE PATH "Path to Berkeley DB libraries") @@ -207,6 +213,7 @@ message(STATUS " QR code: ${USE_QRCODE}") message(STATUS " D-Bus: ${USE_DBUS}") message(STATUS " ZMQ: ${USE_ZMQ}") message(STATUS " Embedded Tor: ${USE_TOR_EMBEDDED}") +message(STATUS " Embedded I2P: ${USE_I2P_EMBEDDED}") message(STATUS " Static linking: ${ENABLE_STATIC}") message(STATUS " ccache: ${CCACHE_PROGRAM}") message(STATUS " Unity build: ${ENABLE_UNITY_BUILD}") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3c51aff..e716f0b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -86,6 +86,7 @@ set(CORE_SOURCES tor/onion_v3.cpp tor/tor_process.cpp tor/tor_embedded.cpp + i2p/i2p_embedded.cpp ) # Scrypt assembly — platform-specific @@ -113,6 +114,7 @@ target_include_directories(triangles_common PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/json" "${CMAKE_CURRENT_SOURCE_DIR}/tor" + "${CMAKE_CURRENT_SOURCE_DIR}/i2p" "${CMAKE_BINARY_DIR}/generated" # for build.h ) @@ -212,6 +214,48 @@ if(USE_TOR_EMBEDDED) endif() endif() +# Optional: Embedded I2P (i2pd) +if(USE_I2P_EMBEDDED) + if(I2P_SOURCE_ROOT STREQUAL "") + set(I2P_SOURCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/i2p/i2pd-src") + endif() + if(NOT EXISTS "${I2P_SOURCE_ROOT}/libi2pd/Crypto.h") + message(FATAL_ERROR + "USE_I2P_EMBEDDED=ON but i2pd source not found at ${I2P_SOURCE_ROOT}.\n" + "Run: git submodule update --init --recursive\n" + "Or set -DI2P_SOURCE_ROOT=/path/to/i2pd") + endif() + target_compile_definitions(triangles_common PUBLIC ENABLE_I2P_EMBEDDED) + target_include_directories(triangles_common PUBLIC + "${I2P_SOURCE_ROOT}" + "${I2P_SOURCE_ROOT}/libi2pd" + "${I2P_SOURCE_ROOT}/libi2pd_client" + "${I2P_SOURCE_ROOT}/i18n" + ) + # i2pd builds as two static libraries: libi2pd.a (core router) and + # libi2pd_client.a (SAM, SOCKS, tunnels, client context). Both are needed. + # We link them with --start-group to resolve circular deps with Boost/SSL. + if(NOT APPLE) + target_link_libraries(triangles_common PUBLIC + -Wl,--allow-multiple-definition + -Wl,--start-group + ) + endif() + target_link_libraries(triangles_common PUBLIC + "${I2P_SOURCE_ROOT}/libi2pd.a" + "${I2P_SOURCE_ROOT}/libi2pdclient.a" + "${I2P_SOURCE_ROOT}/libi2pdlang.a" + Boost::program_options Boost::thread Boost::chrono + OpenSSL::SSL OpenSSL::Crypto + ZLIB::ZLIB + ) + if(NOT APPLE) + target_link_libraries(triangles_common PUBLIC + -Wl,--end-group + ) + endif() +endif() + # Platform-specific libraries if(WIN32) target_link_libraries(triangles_common PUBLIC diff --git a/src/i2p/build-libi2pd.sh b/src/i2p/build-libi2pd.sh new file mode 100755 index 0000000..b7b8fa7 --- /dev/null +++ b/src/i2p/build-libi2pd.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +I2PD_SRC_DIR="${I2PD_SRC_DIR:-$ROOT_DIR/i2pd-src}" + +if [[ ! -d "$I2PD_SRC_DIR" ]]; then + echo "i2pd source tree not found at: $I2PD_SRC_DIR" >&2 + exit 1 +fi + +cd "$I2PD_SRC_DIR" + +echo "Building libi2pd static libraries from: $I2PD_SRC_DIR" + +NPROC_VAL="${NPROC:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}" + +# i2pd uses a hand-written Makefile system. We build only the static library +# targets (libi2pd.a, libi2pdclient.a, libi2pdlang.a), NOT the standalone +# i2pd daemon binary, which pulls in HTTPServer/I2PControl deps we don't need +# and can OOM on memory-constrained build machines. +make -j"$NPROC_VAL" USE_STATIC=no libi2pd.a libi2pdclient.a libi2pdlang.a + +echo +echo "Build finished. Static libraries:" +ls -lh libi2pd*.a +echo +echo "Suggested next step for Triangles:" +echo " cmake -DUSE_I2P_EMBEDDED=ON -DI2P_SOURCE_ROOT=src/i2p/i2pd-src .." diff --git a/src/i2p/i2p_embedded.cpp b/src/i2p/i2p_embedded.cpp new file mode 100644 index 0000000..211cc4d --- /dev/null +++ b/src/i2p/i2p_embedded.cpp @@ -0,0 +1,325 @@ +// Copyright (c) 2025-2026 Triangles developers +// Embedded I2P (i2pd) integration - runs an I2P router in-process +// Distributed under the MIT/X11 software license +// +// BUILD REQUIREMENT: Link against libi2pd.a + libi2pd_client.a built from +// the PurpleI2P/i2pd source tree (src/i2p/i2pd-src). +// +// This file compiles in two modes: +// 1. ENABLE_I2P_EMBEDDED defined: full embedded i2pd via i2p::api +// 2. ENABLE_I2P_EMBEDDED not defined: stubs that report I2P unavailable + +#include "i2p_embedded.h" +#include "../util.h" +#include "../net.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#else +#include +#include +#include +#endif + +namespace fs = std::filesystem; + +// Singleton +CI2PEmbedded* CI2PEmbedded::instance = nullptr; + +CI2PEmbedded* CI2PEmbedded::GetInstance() +{ + if (!instance) + instance = new CI2PEmbedded(); + return instance; +} + +CI2PEmbedded::CI2PEmbedded() + : running(false) + , socksPort(19100) + , samPort(7656) + , serverPort(0) +{ +} + +CI2PEmbedded::~CI2PEmbedded() +{ + Stop(); +} + +std::string CI2PEmbedded::GetSocksProxy() const +{ + return "127.0.0.1:" + std::to_string(socksPort); +} + +#ifdef ENABLE_I2P_EMBEDDED + +// ======================================================================== +// Embedded mode: i2pd runs in-process via libi2pd / i2p::api +// ======================================================================== + +// i2pd C++ API +#include "Config.h" +#include "Log.h" +#include "FS.h" +#include "Crypto.h" +#include "NetDb.hpp" +#include "Transports.h" +#include "Tunnel.h" +#include "RouterContext.h" +#include "Streaming.h" +#include "Destination.h" +#include "ClientContext.h" +#include "I2PTunnel.h" +#include "api.h" + +static std::unique_ptr g_i2pServerTunnel; +static std::shared_ptr g_i2pServerDestination; + +bool CI2PEmbedded::Start(int socks, int sam, int server) +{ + if (running.load()) return true; + + lastError.clear(); + socksPort = socks; + samPort = sam; + serverPort = server; + i2pHostname.clear(); + + // Prepare i2pd data directory under the wallet's data dir + i2pDataDir = (::GetDataDir() / "i2p_data").string(); + fs::create_directories(i2pDataDir); + fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace); + + printf("Embedded I2P: starting i2pd router...\n"); + + // Write an i2pd.conf configuration file that enables SAM + SOCKS proxy. + // i2pd's config system reads from a file; programmatic option setting is + // fragile across i2pd versions. Writing a minimal conf is robust. + { + fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf"; + std::ofstream conf(confPath.string()); + if (!conf.is_open()) { + lastError = "Failed to write i2pd.conf"; + return false; + } + conf << "# Auto-generated by Triangles embedded I2P\n"; + conf << "datadir = " << i2pDataDir << "\n"; + conf << "loglevel = info\n"; + conf << "\n"; + // SOCKS proxy for outbound .i2p connections (P2P transport) + conf << "[socksproxy]\n"; + conf << "enabled = true\n"; + conf << "address = 127.0.0.1\n"; + conf << "port = " << socksPort << "\n"; + conf << "keys = socks-proxy.dat\n"; + conf << "\n"; + // SAM bridge (for future SAM v3 API usage) + conf << "[sam]\n"; + conf << "enabled = true\n"; + conf << "address = 127.0.0.1\n"; + conf << "port = " << samPort << "\n"; + conf << "\n"; + // Disable HTTP webconsole (not needed for embedded use) + conf << "[http]\n"; + conf << "enabled = false\n"; + conf << "\n"; + // Disable I2P control protocol + conf << "[i2pcontrol]\n"; + conf << "enabled = false\n"; + conf << "\n"; + // Disable BOB + conf << "[bob]\n"; + conf << "enabled = false\n"; + conf << "\n"; + conf.close(); + } + + // Write tunnels.conf BEFORE Start() — ClientContext::Start() reads this + // file to create server/client tunnels. The server tunnel is the I2P + // equivalent of a Tor hidden service: it forwards inbound I2P connections + // to the Triangles P2P listen port. + if (serverPort > 0) { + fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf"; + std::ofstream tunnelConf(tunnelConfPath.string()); + if (tunnelConf.is_open()) { + tunnelConf << "# Auto-generated by Triangles embedded I2P\n"; + tunnelConf << "[triangles-p2p]\n"; + tunnelConf << "type = server\n"; + tunnelConf << "host = 127.0.0.1\n"; + tunnelConf << "port = " << serverPort << "\n"; + tunnelConf << "keys = triangles-p2p-keys.dat\n"; + tunnelConf << "inbound.length = 3\n"; + tunnelConf << "outbound.length = 3\n"; + tunnelConf << "inbound.quantity = 5\n"; + tunnelConf << "outbound.quantity = 5\n"; + tunnelConf.close(); + printf("Embedded I2P: server tunnel configured on port %d\n", serverPort); + } + } + + // Build argv for i2pd initialization. Pass --datadir and --conf on the + // command line (not just in the conf file) because i2pd's ParseCmdline + // runs BEFORE ParseConfig, and DetectDataDir needs the datadir early. + std::vector argvStrings; + argvStrings.push_back("i2pd"); + argvStrings.push_back("--datadir"); + argvStrings.push_back(i2pDataDir); + argvStrings.push_back("--conf"); + argvStrings.push_back((fs::path(i2pDataDir) / "i2pd.conf").string()); + + std::vector argvPtrs; + for (auto& s : argvStrings) + argvPtrs.push_back(&s[0]); + argvPtrs.push_back(nullptr); + + try { + // Initialize i2pd: config parse, filesystem, crypto, router context + i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd"); + + // Start the I2P router: netdb, transports, tunnels, router context + // Redirect i2pd logs to our stdout/stderr + auto logStream = std::make_shared(std::cout.rdbuf()); + i2p::api::StartI2P(logStream); + + printf("Embedded I2P: router started, starting client services...\n"); + + // Start the client context — this initializes SAM bridge, SOCKS proxy, + // and tunnels based on config. The client context reads the conf we + // wrote above to determine which services to start. + i2p::client::context.Start(); + + running.store(true); + printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n", + socksPort, samPort); + + // Wait for i2pd's SOCKS proxy to become available (up to 120s — I2P + // bootstrap is slower than Tor due to floodfill lookup and tunnel build) + printf("Embedded I2P: waiting for SOCKS proxy to become available...\n"); + for (int i = 0; i < 120; i++) { + MilliSleep(1000); + if (fShutdown) { + Stop(); + return false; + } + +#ifdef WIN32 + SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock != INVALID_SOCKET) { +#else + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock >= 0) { +#endif + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(socksPort); + bool up = (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0); +#ifdef WIN32 + closesocket(sock); +#else + close(sock); +#endif + if (up) { + printf("Embedded I2P: SOCKS proxy ready on port %d (took %ds)\n", + socksPort, i + 1); + return true; + } + } + + if (i > 0 && i % 30 == 0) { + printf("Embedded I2P: still bootstrapping (%ds elapsed)...\n", i); + } + } + + // SOCKS not ready after 120s — I2P may still be building tunnels. + // We return true anyway; connections will retry once tunnels are up. + printf("Embedded I2P: SOCKS proxy not ready after 120s (I2P bootstrap in progress)\n"); + printf(" Outbound .i2p connections will retry automatically.\n"); + return true; + + } catch (const std::exception& e) { + lastError = std::string("i2pd initialization failed: ") + e.what(); + printf("ERROR: Embedded I2P startup failed: %s\n", e.what()); + return false; + } +} + +void CI2PEmbedded::Stop() +{ + if (!running.load()) return; + printf("Requesting embedded I2P shutdown...\n"); + + try { + // Stop client context (SAM, SOCKS, tunnels) + i2p::client::context.Stop(); + + // Stop the router + i2p::api::StopI2P(); + + // Terminate crypto + i2p::api::TerminateI2P(); + } catch (const std::exception& e) { + printf("WARNING: error during I2P shutdown: %s\n", e.what()); + } + + running.store(false); +} + +#else // !ENABLE_I2P_EMBEDDED + +// ======================================================================== +// Fallback stubs: embedded I2P not compiled in +// ======================================================================== + +bool CI2PEmbedded::Start(int socks, int sam, int server) +{ + printf("Embedded I2P not compiled in (ENABLE_I2P_EMBEDDED not defined).\n"); + socksPort = socks; + samPort = sam; + serverPort = server; + i2pDataDir = (::GetDataDir() / "i2p_data").string(); + lastError = "I2P support not compiled in. Build with -DUSE_I2P_EMBEDDED=ON"; + return false; +} + +void CI2PEmbedded::Stop() +{ + running.store(false); +} + +#endif // ENABLE_I2P_EMBEDDED + +// ======================================================================== +// Global hooks (called from init.cpp) +// ======================================================================== + +bool StartEmbeddedI2P() +{ + bool enableI2P = GetBoolArg("-i2p", true); + if (!enableI2P) { + printf("I2P disabled by -i2p=0 flag\n"); + return false; + } + + int socksPort = GetArg("-i2psocks", 19100); + int samPort = GetArg("-i2psam", 7656); + int serverPort = GetArg("-i2phsport", GetListenPort()); + + return CI2PEmbedded::GetInstance()->Start(socksPort, samPort, serverPort); +} + +void StopEmbeddedI2P() +{ + CI2PEmbedded::GetInstance()->Stop(); +} diff --git a/src/i2p/i2p_embedded.h b/src/i2p/i2p_embedded.h new file mode 100644 index 0000000..d2e9cb1 --- /dev/null +++ b/src/i2p/i2p_embedded.h @@ -0,0 +1,57 @@ +// Copyright (c) 2025-2026 Triangles developers +// Embedded I2P (i2pd) integration - runs an I2P router in-process +// Distributed under the MIT/X11 software license + +#ifndef TRIANGLES_I2P_EMBEDDED_H +#define TRIANGLES_I2P_EMBEDDED_H + +#include +#include + +// Embedded I2P router state +class CI2PEmbedded +{ +private: + static CI2PEmbedded* instance; + std::atomic running; + int socksPort; // i2pd SOCKS proxy port (for outbound .i2p connections) + int samPort; // i2pd SAM bridge port (for SAM v3 protocol) + int serverPort; // Triangles P2P listen port (for incoming I2P connections) + std::string i2pDataDir; // i2pd data directory (under wallet datadir) + std::string i2pHostname; // Our .b32.i2p address (available after router startup) + std::string lastError; + +public: + static CI2PEmbedded* GetInstance(); + + CI2PEmbedded(); + ~CI2PEmbedded(); + + // Start embedded i2pd router (blocks calling thread briefly during init) + bool Start(int socksPort = 19100, int samPort = 7656, int serverPort = 0); + + // Request i2pd to shut down + void Stop(); + + // Check if i2pd is running + bool IsRunning() const { return running.load(); } + void SetRunning(bool value) { running.store(value); } + + // Get the SOCKS5 proxy address for outbound .i2p connections + std::string GetSocksProxy() const; + int GetSocksPort() const { return socksPort; } + int GetSamPort() const { return samPort; } + int GetServerPort() const { return serverPort; } + const std::string& GetDataDir() const { return i2pDataDir; } + + // Get our .b32.i2p destination address + std::string GetI2PAddress() const { return i2pHostname; } + std::string GetStartupError() const { return lastError; } + void SetStartupError(const std::string& value) { lastError = value; } +}; + +// Global init/shutdown hooks (called from init.cpp) +bool StartEmbeddedI2P(); +void StopEmbeddedI2P(); + +#endif // TRIANGLES_I2P_EMBEDDED_H diff --git a/src/i2p/i2pd-src b/src/i2p/i2pd-src new file mode 160000 index 0000000..8497a42 --- /dev/null +++ b/src/i2p/i2pd-src @@ -0,0 +1 @@ +Subproject commit 8497a429dcf2558e5af69894eeadd70943935b07 diff --git a/src/i2p/i2pseed.h b/src/i2p/i2pseed.h new file mode 100644 index 0000000..a3c1a89 --- /dev/null +++ b/src/i2p/i2pseed.h @@ -0,0 +1,27 @@ +#ifndef TRIANGLES_I2PSEED_H +#define TRIANGLES_I2PSEED_H + +// Hardcoded I2P seed nodes for initial peer discovery. +// These are .b32.i2p addresses (Destination hashes). +// Nodes must run i2pd with a server tunnel forwarding to the Triangles P2P port. +// +// NOTE: .b32.i2p addresses are derived from the destination's public key. +// They are generated when the node first creates its I2P tunnel keys. +// Replace these placeholders with actual seed node addresses once deployed. +// +// Dynamic seeds will also be available at: +// https://seeds.cryptographic-triangles.org/i2p-seeds.txt +static const char *strMainNetI2PSeed[][1] = { + // DNS2 - primary bootstrap server + // Generate .b32.i2p by: i2pd --datadir=/root/.triangles/i2p_data + // then read: i2p_data/triangles-p2p-keys.dat → b32 address + // Placeholder until seed nodes are deployed: + // {"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.b32.i2p"}, + {nullptr} +}; + +static const char *strTestNetI2PSeed[][1] = { + {nullptr} +}; + +#endif diff --git a/src/init.cpp b/src/init.cpp index ed16627..372cede 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -19,6 +19,7 @@ #include "tor/tor_embedded.h" #include "tor/onion_v3.h" #include "tor/tor_process.h" +#include "i2p/i2p_embedded.h" #ifdef ENABLE_ZMQ #include "zmqpublishnotifier.h" #endif @@ -341,6 +342,7 @@ void Shutdown(void* parg) // NOW safe to destroy Tor state - all threads have stopped ShutdownTorV3(); StopEmbeddedTor(); + StopEmbeddedI2P(); #ifdef ENABLE_ZMQ if (pzmqNotifier) @@ -533,7 +535,11 @@ std::string HelpMessage() " -notor " + _("Disable Tor - run in clearnet-only mode (no .onion connectivity)") + "\n" + " -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" + + " -torhsport= " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n" + " -i2p " + _("Enable embedded I2P router for .b32.i2p connectivity (default: 1)") + "\n" + " -i2psocks= " + _("Set embedded I2P SOCKS proxy port (default: 19100)") + "\n" + " -i2psam= " + _("Set embedded I2P SAM bridge port (default: 7656)") + "\n" + " -i2phsport= " + _("Set I2P server tunnel forward port (default: wallet listen port)") + "\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" + @@ -1546,6 +1552,47 @@ bool AppInit2() return InitError(strprintf(_("Tor failed to start. Triangles requires Tor to operate.\n\nDetails: %s"), torError.c_str())); } + // ════════════════════════════════════════════════════════════════ + // Embedded I2P (i2pd) startup + // + // I2P runs as a co-equal anonymity network alongside Tor. When Tor + // starts successfully (tor-native mode), I2P provides an alternative + // anonymous transport via .b32.i2p destinations. When Tor is disabled + // (-notor recovery mode), I2P is still started to maintain anonymity. + // + // I2P's SOCKS proxy (default 19100) handles outbound .i2p connections. + // A server tunnel forwards incoming I2P connections to the P2P port. + // ════════════════════════════════════════════════════════════════ + if (torStarted || GetBoolArg("-notor", false)) { + uiInterface.InitMessage(_("Starting embedded I2P router...")); + int64_t nI2PStart = GetTimeMillis(); + bool i2pStarted = StartEmbeddedI2P(); + StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart, + strprintf("started=%d", i2pStarted)); + + if (i2pStarted) { + int i2pSocksPort = CI2PEmbedded::GetInstance()->GetSocksPort(); + CService i2pProxyAddr("127.0.0.1", i2pSocksPort); + + // Route I2P traffic through i2pd's SOCKS proxy + SetProxy(NET_I2P, i2pProxyAddr, 5); + SetReachable(NET_I2P, true); + + printf("I2P-NATIVE MODE: I2P router running\n"); + printf(" SOCKS proxy at 127.0.0.1:%d for .b32.i2p connections\n", + i2pSocksPort); + printf(" Dual-network anonymity: Tor (.onion) + I2P (.b32.i2p)\n"); + } else { + // I2P failure is non-fatal — Tor-only operation continues. + // The daemon still works with .onion peers. + std::string i2pError = CI2PEmbedded::GetInstance()->GetStartupError(); + printf("WARNING: Embedded I2P did not start. Running Tor-only.\n"); + if (!i2pError.empty()) + printf(" I2P error: %s\n", i2pError.c_str()); + SetReachable(NET_I2P, false); + } + } + // Initialize Tor V3 identity (Ed25519 keys, onion address) uiInterface.InitMessage(_("Initializing Tor V3 identity...")); printf("Initializing Tor V3 onion identity...\n"); diff --git a/src/net.cpp b/src/net.cpp index de475b8..685737f 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -496,11 +496,13 @@ CNode* FindNode(const CService& addr) CNode* ConnectNode(CAddress addrConnect, const char *pszDest) { - // TOR-NATIVE: Reject all non-.onion addresses + // TOR+I2P NATIVE: Reject all clearnet (non-.onion, non-.b32.i2p) addresses std::string addrStr = pszDest ? std::string(pszDest) : addrConnect.ToStringIP(); - if (addrStr.find(".onion") == std::string::npos) { + bool isOnion = (addrStr.find(".onion") != std::string::npos); + bool isI2P = (addrStr.find(".i2p") != std::string::npos); + if (!isOnion && !isI2P) { if (fDebug) - printf("ConnectNode(): REJECTED non-onion address: %s (Tor-native mode)\n", addrStr.c_str()); + printf("ConnectNode(): REJECTED clearnet address: %s (Tor/I2P native mode)\n", addrStr.c_str()); return nullptr; } @@ -1836,11 +1838,16 @@ bool ThreadHTTPSeedFetch2(void* parg) int port = GetDefaultPort(); size_t onionPos = addrStr.find(".onion:"); + size_t i2pPos = addrStr.find(".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) { - return; // Tor-native: skip non-.onion addresses + } else if (i2pPos != std::string::npos) { + port = atoi(addrStr.substr(i2pPos + 5).c_str()); + // keep the ".i2p" suffix + } else if (addrStr.find(".onion") == std::string::npos && + addrStr.find(".i2p") == std::string::npos) { + return; // Tor/I2P-native: skip clearnet addresses } if (port <= 0 || port > 65535) port = GetDefaultPort(); diff --git a/src/netbase.cpp b/src/netbase.cpp index bfcdca6..0873a64 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -591,6 +591,33 @@ bool ConnectSocketByName(CService &addr, SOCKET& hSocketRet, const char *pszDest SOCKET hSocket = INVALID_SOCKET; + // I2P routing: .b32.i2p destinations go through i2pd's SOCKS proxy, not + // the Tor name proxy. This is the key routing decision for dual-network + // anonymity — Tor handles .onion, i2pd handles .b32.i2p. + bool isI2PDest = (strDest.size() > 7 && + strDest.substr(strDest.size() - 7, 7) == ".b32.i2p"); + + if (isI2PDest) { + // Route through the I2P SOCKS proxy + proxyType i2pProxy; + if (GetProxy(NET_I2P, i2pProxy)) { + addr = CService("0.0.0.0:0"); + printf("ConnectSocketByName(): routing .b32.i2p via I2P SOCKS proxy\n"); + if (!ConnectSocketDirectly(i2pProxy.first, hSocket, nTimeout)) + return false; + // i2pd's SOCKS proxy accepts .b32.i2p domain names via SOCKS5 ATYP=domain + if (!Socks5(strDest, port, hSocket)) { + printf("ConnectSocketByName(): I2P SOCKS5 handshake failed\n"); + return false; + } + printf("ConnectSocketByName(): connected via I2P SOCKS5\n"); + hSocketRet = hSocket; + return true; + } + // No I2P proxy configured — fall through to nameproxy (will likely fail) + printf("ConnectSocketByName(): WARNING - .b32.i2p dest but no I2P proxy set\n"); + } + proxyType nameproxy; GetNameProxy(nameproxy); @@ -672,14 +699,26 @@ bool CNetAddr::SetSpecial(const std::string &strName) m_is_tor_v3 = false; return true; } - if (strName.size()>11 && strName.substr(strName.size() - 11, 11) == ".oc.b32.i2p") { - std::vector vchAddr = DecodeBase32(strName.substr(0, strName.size() - 11).c_str()); - if (vchAddr.size() != 16-sizeof(pchGarliCat)) - return false; - memcpy(ip, pchOnionCat, sizeof(pchGarliCat)); - for (unsigned int i=0; i<16-sizeof(pchGarliCat); i++) - ip[i + sizeof(pchGarliCat)] = vchAddr[i]; - return true; + // Standard I2P b32 address: <52 base32 chars>.b32.i2p + // (SHA-256 hash of destination key, base32-encoded) + if (strName.size()>7 && strName.substr(strName.size() - 7, 7) == ".b32.i2p") { + std::string b32Part = strName.substr(0, strName.size() - 7); + std::vector vchAddr = DecodeBase32(b32Part.c_str()); + if (vchAddr.size() == 32) { + // Standard 32-byte I2P destination hash + memcpy(ip, pchGarliCat, sizeof(pchGarliCat)); + // Store as many bytes as fit (16 - prefix_size) + for (unsigned int i = 0; i < 16 - sizeof(pchGarliCat) && i < vchAddr.size(); i++) + ip[i + sizeof(pchGarliCat)] = vchAddr[i]; + return true; + } + // Also handle the legacy .oc.b32.i2p format (10 bytes) + if (vchAddr.size() == 16 - sizeof(pchGarliCat)) { + memcpy(ip, pchGarliCat, sizeof(pchGarliCat)); + for (unsigned int i = 0; i < 16 - sizeof(pchGarliCat); i++) + ip[i + sizeof(pchGarliCat)] = vchAddr[i]; + return true; + } } return false; } @@ -910,7 +949,7 @@ std::string CNetAddr::ToStringIP() const if (IsTor()) return EncodeBase32(&ip[6], 10) + ".onion"; if (IsI2P()) - return EncodeBase32(&ip[6], 10) + ".oc.b32.i2p"; + return EncodeBase32(&ip[6], 10) + ".b32.i2p"; CService serv(*this, 0); #ifdef USE_IPV6 struct sockaddr_storage sockaddr;