diff --git a/doc/i2p.md b/doc/i2p.md index 052dfbba2300..9ce825ff5f3b 100644 --- a/doc/i2p.md +++ b/doc/i2p.md @@ -9,12 +9,22 @@ started with I2P terminology. ## Run Dash Core with an I2P router (proxy) -A running I2P router (proxy) with [SAM](https://geti2p.net/en/docs/api/samv3) -enabled is required (there is an [official one](https://geti2p.net) and -[a few alternatives](https://en.wikipedia.org/wiki/I2P#Routers)). Notice the IP -address and port the SAM proxy is listening to; usually, it is -`127.0.0.1:7656`. Once it is up and running with SAM enabled, use the following -Dash Core options: +A running I2P router (proxy) is required with the [SAM](https://geti2p.net/en/docs/api/samv3) +application bridge enabled. The following routers are recommended for use with Dash Core: + +- [i2prouter (I2P Router)](https://geti2p.net), the official implementation in + Java. The SAM bridge is not enabled by default; it must be started manually, + or configured to start automatically, in the Clients page in the + router console (`http://127.0.0.1:7657/configclients`) or in the `clients.config` file. +- [i2pd (I2P Daemon)](https://github.com/PurpleI2P/i2pd) + ([documentation](https://i2pd.readthedocs.io/en/latest)), a lighter + alternative in C++. It enables the SAM bridge by default. + +Note the IP address and port the SAM proxy is listening to; usually, it is +`127.0.0.1:7656`. + +Once an I2P router with SAM enabled is up and running, use the following Dash +Core configuration options: ``` -i2psam= @@ -22,12 +32,10 @@ Dash Core options: none) -i2pacceptincoming - If set and -i2psam is also set then incoming I2P connections are - accepted via the SAM proxy. If this is not set but -i2psam is set - then only outgoing connections will be made to the I2P network. - Ignored if -i2psam is not set. Listening for incoming I2P - connections is done through the SAM proxy, not by binding to a - local address and port (default: 1) + Whether to accept inbound I2P connections (default: 1). Ignored if + -i2psam is not set. Listening for inbound I2P connections is + done through the SAM proxy, not by binding to a local address and + port. ``` In a typical situation, this suffices: @@ -36,26 +44,68 @@ In a typical situation, this suffices: dashd -i2psam=127.0.0.1:7656 ``` -The first time Dash Core connects to the I2P router, its I2P address (and -corresponding private key) will be automatically generated and saved in a file -named `i2p_private_key` in the Dash Core data directory. - ## Additional configuration options related to I2P -You may set the `debug=i2p` config logging option to have additional -information in the debug log about your I2P configuration and connections. Run -`dash-cli help logging` for more information. +``` +-debug=i2p +``` + +Set the `debug=i2p` config logging option to see additional information in the +debug log about your I2P configuration and connections. Run `dash-cli help +logging` for more information. + +``` +-onlynet=i2p +``` + +Make outgoing connections only to I2P addresses. Incoming connections are not +affected by this option. It can be specified multiple times to allow multiple +network types, e.g. onlynet=ipv4, onlynet=ipv6, onlynet=onion, onlynet=i2p. + +Warning: if you use -onlynet with values other than onion, and the -onion or +-proxy option is set, then outgoing onion connections will still be made; use +-noonion or -onion=0 to disable outbound onion connections in this case. -It is possible to restrict outgoing connections in the usual way with -`onlynet=i2p`. I2P support was added to Dash Core in version 20.0 (fall 2023) -and there may be fewer I2P peers than Tor or IP ones. Therefore, using -`onlynet=i2p` alone (without other `onlynet=`) may make a node more susceptible -to [Sybil attacks](https://en.dash.it/wiki/Weaknesses#Sybil_attack). Use +I2P support was added to Dash Core in version 20.0 and there may be fewer I2P +peers than Tor or IP ones. Therefore, using I2P alone without other networks may +make a node more susceptible to [Sybil +attacks](https://en.bitcoin.it/wiki/Weaknesses#Sybil_attack). You can use `dash-cli -addrinfo` to see the number of I2P addresses known to your node. -## I2P related information in Dash Core +Another consideration with `onlynet=i2p` is that the initial blocks download +phase when syncing up a new node can be very slow. This phase can be sped up by +using other networks, for instance `onlynet=onion`, at the same time. + +In general, a node can be run with both onion and I2P hidden services (or +any/all of IPv4/IPv6/onion/I2P), which can provide a potential fallback if one +of the networks has issues. + +## Persistent vs transient I2P addresses + +The first time Dash Core connects to the I2P router, it automatically +generates a persistent I2P address and its corresponding private key by default +or if `-i2pacceptincoming=1` is set. The private key is saved in a file named +`i2p_private_key` in the Dash Core data directory. The persistent I2P +address is used for making outbound connections and accepting inbound +connections. + +In the I2P network, the receiver of an inbound connection sees the address of +the initiator. This is unlike the Tor network, where the recipient does not +know who is connecting to it. -There are several ways to see your I2P address in Dash Core: +If your node is configured by setting `-i2pacceptincoming=0` to not accept +inbound I2P connections, then it will use a random transient I2P address for +itself on each outbound connection to make it harder to discriminate, +fingerprint or analyze it based on its I2P address. + +I2P addresses are designed to be long-lived. Waiting for tunnels to be built +for every peer connection adds delay to connection setup time. Therefore, I2P +listening should only be turned off if really needed. + +## Fetching I2P-related information from Dash Core + +There are several ways to see your I2P address in Dash Core if accepting +incoming I2P connections (`-i2pacceptincoming`): - in the debug log (grep for `AddLocal`, the I2P address ends in `.b32.i2p`) - in the output of the `getnetworkinfo` RPC in the "localaddresses" section - in the output of `dash-cli -netinfo` peer connections dashboard @@ -73,8 +123,7 @@ to connect to the I2P network. Any I2P router that supports it can be used. ## Ports in I2P and Dash Core -Dash Core uses the [SAM v3.1](https://geti2p.net/en/docs/api/samv3) -protocol. One particularity of SAM v3.1 is that it does not support ports, +One particularity of SAM v3.1 is that it does not support ports, unlike newer versions of SAM (v3.2 and up) that do support them and default the port numbers to 0. From the point of view of peers that use newer versions of SAM or other protocols that support ports, a SAM v3.1 peer is connecting to them @@ -85,3 +134,40 @@ listening port to 0 when listening for incoming I2P connections and advertises its own I2P address with port 0. Furthermore, it will not attempt to connect to I2P addresses with a non-zero port number because with SAM v3.1 the destination port (`TO_PORT`) is always set to 0 and is not in the control of Dash Core. + +## Bandwidth + +By default, your node shares bandwidth and transit tunnels with the I2P network +in order to increase your anonymity with cover traffic, help the I2P router used +by your node integrate optimally with the network, and give back to the network. +It's important that the nodes of a popular application like Dash contribute +as much to the I2P network as they consume. + +It is possible, though strongly discouraged, to change your I2P router +configuration to limit the amount of I2P traffic relayed by your node. + +With `i2pd`, this can be done by adjusting the `bandwidth`, `share` and +`transittunnels` options in your `i2pd.conf` file. For example, to limit total +I2P traffic to 256KB/s and share 50% of this limit for a maximum of 20 transit +tunnels: + +``` +bandwidth = 256 +share = 50 + +[limits] +transittunnels = 20 +``` + +Similar bandwidth configuration options for the Java I2P router can be found in +`http://127.0.0.1:7657/config` under the "Bandwidth" tab. + +Before doing this, please see the "Participating Traffic Considerations" section +in [Embedding I2P in your Application](https://geti2p.net/en/docs/applications/embedding). + +In most cases, the default router settings should work fine. + +## Bundling I2P in a Dash application + +Please see the "General Guidance for Developers" section in https://geti2p.net/en/docs/api/samv3 +if you are developing a downstream application that may be bundling I2P with Dash. diff --git a/doc/release-notes-25355.md b/doc/release-notes-25355.md new file mode 100644 index 000000000000..b539d6bdb1ad --- /dev/null +++ b/doc/release-notes-25355.md @@ -0,0 +1,5 @@ +P2P and network changes +----------------------- + +- With I2P connections, a new, transient address is used for each outbound + connection if `-i2pacceptincoming=0`. diff --git a/doc/tor.md b/doc/tor.md index 8ef222615c48..fdfb4c59eb8f 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -51,11 +51,11 @@ outgoing connections, but more is possible. -onlynet=onion Make outgoing connections only to .onion addresses. Incoming connections are not affected by this option. This option can be specified multiple times to allow multiple network types, e.g. - ipv4, ipv6 or onion. If you use this option with values other - than onion you *cannot* disable onion connections; outgoing onion - connections will be enabled when you use -proxy or -onion. Use - -noonion or -onion=0 if you want to be sure there are no outbound - onion connections over the default proxy or your defined -proxy. + onlynet=ipv4, onlynet=ipv6, onlynet=onion, onlynet=i2p. + Warning: if you use -onlynet with values other than onion, and + the -onion or -proxy option is set, then outgoing onion + connections will still be made; use -noonion or -onion=0 to + disable outbound onion connections in this case. An example how to start the client if the Tor proxy is running on local host on port 9050 and only allows .onion nodes to connect: diff --git a/src/addrman.cpp b/src/addrman.cpp index 1ef9b46dbb2d..e9cd711f463d 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -778,100 +777,3 @@ std::vector CAddrMan::DecodeAsmap(fs::path path) } return bits; } - -void CAddrMan::ResetI2PPorts() -{ - for (int bucket = 0; bucket < ADDRMAN_NEW_BUCKET_COUNT; ++bucket) { - for (int i = 0; i < ADDRMAN_BUCKET_SIZE; ++i) { - const auto id = vvNew[bucket][i]; - if (id == -1) { - continue; - } - auto it = mapInfo.find(id); - if (it == mapInfo.end()) { - return; - } - auto& addr_info = it->second; - if (!addr_info.IsI2P() || addr_info.GetPort() == I2P_SAM31_PORT) { - continue; - } - - auto addr_info_newport = addr_info; - // The below changes addr_info_newport.GetKey(), which is used in finding a - // bucket and a position within that bucket. So a re-bucketing may be necessary. - addr_info_newport.port = I2P_SAM31_PORT; - - // Reposition entries of vvNew within the same bucket because we don't know the source - // address which led to the decision to store the entry in vvNew[bucket] so we can't - // re-evaluate that decision, but even if we could, CAddrInfo::GetNewBucket() does not - // use CAddrInfo::GetKey() so it would end up in the same bucket as before the port - // change. - const auto i_target = addr_info_newport.GetBucketPosition(nKey, true, bucket); - - if (i_target == i) { // No need to re-position. - addr_info = addr_info_newport; - continue; - } - - // Reposition from i to i_target, removing the entry from i_target (if any). - ClearNew(bucket, i_target); - vvNew[bucket][i_target] = id; - vvNew[bucket][i] = -1; - addr_info = addr_info_newport; - } - } - - for (int bucket = 0; bucket < ADDRMAN_TRIED_BUCKET_COUNT; ++bucket) { - for (int i = 0; i < ADDRMAN_BUCKET_SIZE; ++i) { - const auto id = vvTried[bucket][i]; - if (id == -1) { - continue; - } - auto it = mapInfo.find(id); - if (it == mapInfo.end()) { - return; - } - auto& addr_info = it->second; - if (!addr_info.IsI2P() || addr_info.GetPort() == I2P_SAM31_PORT) { - continue; - } - - auto addr_info_newport = addr_info; - // The below changes addr_info_newport.GetKey(), which is used in finding a - // bucket and a position within that bucket. So a re-bucketing may be necessary. - addr_info_newport.port = I2P_SAM31_PORT; - - const auto bucket_target = addr_info_newport.GetTriedBucket(nKey, m_asmap); - const auto i_target = addr_info_newport.GetBucketPosition(nKey, false, bucket_target); - - if (bucket_target == bucket && i_target == i) { // No need to re-position. - addr_info = addr_info_newport; - continue; - } - - // Reposition from (bucket, i) to (bucket_target, i_target). If the latter is - // occupied, then move the entry from there to vvNew. - - const auto old_target_id = vvTried[bucket_target][i_target]; - if (old_target_id != -1) { - CAddrInfo& old_target_info = mapInfo[old_target_id]; - - old_target_info.fInTried = false; - vvTried[bucket_target][i_target] = -1; - --nTried; - - const auto new_bucket = old_target_info.GetNewBucket(nKey, m_asmap); - const auto new_bucket_i = old_target_info.GetBucketPosition(nKey, true, new_bucket); - ClearNew(new_bucket, new_bucket_i); - - old_target_info.nRefCount = 1; - vvNew[new_bucket][new_bucket_i] = old_target_id; - ++nNew; - } - - vvTried[bucket_target][i_target] = id; - vvTried[bucket][i] = -1; - addr_info = addr_info_newport; - } - } -} diff --git a/src/addrman.h b/src/addrman.h index 7debddb842f5..c41bc86abe50 100644 --- a/src/addrman.h +++ b/src/addrman.h @@ -464,8 +464,6 @@ class CAddrMan RemoveInvalid(); - ResetI2PPorts(); - Check(); } @@ -800,14 +798,6 @@ class CAddrMan //! Remove invalid addresses. void RemoveInvalid() EXCLUSIVE_LOCKS_REQUIRED(cs); - /** - * Reset the ports of I2P peers to 0. - * This is needed as a temporary measure because now we enforce port 0 and - * only connect to I2P hosts if the port is 0, but in the early days some - * I2P addresses with port 8333 were rumoured and persisted into addrmans. - */ - void ResetI2PPorts() EXCLUSIVE_LOCKS_REQUIRED(cs); - friend class CAddrManTest; friend class CAddrManDeterministic; }; diff --git a/src/i2p.cpp b/src/i2p.cpp index 5e7e42fb775a..8dfab826ef90 100644 --- a/src/i2p.cpp +++ b/src/i2p.cpp @@ -12,11 +12,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include @@ -116,8 +116,19 @@ namespace sam { Session::Session(const fs::path& private_key_file, const CService& control_host, CThreadInterrupt* interrupt) - : m_private_key_file(private_key_file), m_control_host(control_host), m_interrupt(interrupt), - m_control_sock(std::make_unique(INVALID_SOCKET)) + : m_private_key_file{private_key_file}, + m_control_host{control_host}, + m_interrupt{interrupt}, + m_control_sock{std::make_unique(INVALID_SOCKET)}, + m_transient{false} +{ +} + +Session::Session(const CService& control_host, CThreadInterrupt* interrupt) + : m_control_host{control_host}, + m_interrupt{interrupt}, + m_control_sock{std::make_unique(INVALID_SOCKET)}, + m_transient{true} { } @@ -315,6 +326,7 @@ void Session::DestGenerate(const Sock& sock) // https://geti2p.net/spec/common-structures#key-certificates // "7" or "EdDSA_SHA512_Ed25519" - "Recent Router Identities and Destinations". // Use "7" because i2pd <2.24.0 does not recognize the textual form. + // If SIGNATURE_TYPE is not specified, then the default one is DSA_SHA1. const Reply& reply = SendRequestAndGetReply(sock, "DEST GENERATE SIGNATURE_TYPE=7", false); m_private_key = DecodeI2PBase64(reply.Get("PRIV")); @@ -356,29 +368,50 @@ void Session::CreateIfNotCreatedAlready() return; } - Log("Creating SAM session with %s", m_control_host.ToString()); + const auto session_type = m_transient ? "transient" : "persistent"; + const auto session_id = GetRandHash().GetHex().substr(0, 10); // full is overkill, too verbose in the logs + + Log("Creating %s SAM session %s with %s", session_type, session_id, m_control_host.ToString()); auto sock = Hello(); - const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file); - if (read_ok) { - m_private_key.assign(data.begin(), data.end()); + if (m_transient) { + // The destination (private key) is generated upon session creation and returned + // in the reply in DESTINATION=. + const Reply& reply = SendRequestAndGetReply( + *sock, + strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 " + "inbound.quantity=1 outbound.quantity=1", + session_id)); + + m_private_key = DecodeI2PBase64(reply.Get("DESTINATION")); } else { - GenerateAndSavePrivateKey(*sock); - } + // Read our persistent destination (private key) from disk or generate + // one and save it to disk. Then use it when creating the session. + const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file); + if (read_ok) { + m_private_key.assign(data.begin(), data.end()); + } else { + GenerateAndSavePrivateKey(*sock); + } - const std::string& session_id = GetRandHash().GetHex().substr(0, 10); // full is an overkill, too verbose in the logs - const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key)); + const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key)); - SendRequestAndGetReply(*sock, strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s", - session_id, private_key_b64)); + SendRequestAndGetReply(*sock, + strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s " + "inbound.quantity=3 outbound.quantity=3", + session_id, + private_key_b64)); + } m_my_addr = CService(DestBinToAddr(MyDestination()), I2P_SAM31_PORT); m_session_id = session_id; m_control_sock = std::move(sock); - LogPrintf("I2P: SAM session created: session id=%s, my address=%s\n", m_session_id, - m_my_addr.ToString()); + Log("%s SAM session %s created, my address=%s", + Capitalize(session_type), + m_session_id, + m_my_addr.ToString()); } std::unique_ptr Session::StreamAccept() @@ -406,9 +439,9 @@ void Session::Disconnect() { if (m_control_sock->Get() != INVALID_SOCKET) { if (m_session_id.empty()) { - Log("Destroying incomplete session"); + Log("Destroying incomplete SAM session"); } else { - Log("Destroying session %s", m_session_id); + Log("Destroying SAM session %s", m_session_id); } } m_control_sock->Reset(); diff --git a/src/i2p.h b/src/i2p.h index 3899dbf81f3c..6183885435c2 100644 --- a/src/i2p.h +++ b/src/i2p.h @@ -70,6 +70,19 @@ class Session const CService& control_host, CThreadInterrupt* interrupt); + /** + * Construct a transient session which will generate its own I2P private key + * rather than read the one from disk (it will not be saved on disk either and + * will be lost once this object is destroyed). This will not initiate any IO, + * the session will be lazily created later when first used. + * @param[in] control_host Location of the SAM proxy. + * @param[in,out] interrupt If this is signaled then all operations are canceled as soon as + * possible and executing methods throw an exception. Notice: only a pointer to the + * `CThreadInterrupt` object is saved, so it must not be destroyed earlier than this + * `Session` object. + */ + Session(const CService& control_host, CThreadInterrupt* interrupt); + /** * Destroy the session, closing the internally used sockets. The sockets that have been * returned by `Accept()` or `Connect()` will not be closed, but they will be closed by @@ -262,6 +275,12 @@ class Session * SAM session id. */ std::string m_session_id GUARDED_BY(m_mutex); + + /** + * Whether this is a transient session (the I2P private key will not be + * read or written to disk). + */ + const bool m_transient; }; } // namespace sam diff --git a/src/init.cpp b/src/init.cpp index 3a83d70086a2..d600e54a856f 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -131,8 +131,9 @@ #include #endif -static const bool DEFAULT_PROXYRANDOMIZE = true; -static const bool DEFAULT_REST_ENABLE = false; +static constexpr bool DEFAULT_PROXYRANDOMIZE{true}; +static constexpr bool DEFAULT_REST_ENABLE{false}; +static constexpr bool DEFAULT_I2P_ACCEPT_INCOMING{true}; static CDSNotificationInterface* pdsNotificationInterface = nullptr; @@ -581,7 +582,7 @@ void SetupServerArgs(NodeContext& node) argsman.AddArg("-maxuploadtarget=", strprintf("Tries to keep outbound traffic under the given target (in MiB per 24h). Limit does not apply to peers with 'download' permission. 0 = no limit (default: %d)", DEFAULT_MAX_UPLOAD_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-onion=", "Use separate SOCKS5 proxy to reach peers via Tor onion services, set -noonion to disable (default: -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-i2psam=", "I2P SAM proxy to reach I2P peers and accept I2P connections (default: none)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); - argsman.AddArg("-i2pacceptincoming", "If set and -i2psam is also set then incoming I2P connections are accepted via the SAM proxy. If this is not set but -i2psam is set then only outgoing connections will be made to the I2P network. Ignored if -i2psam is not set. Listening for incoming I2P connections is done through the SAM proxy, not by binding to a local address and port (default: 1)", ArgsManager::ALLOW_BOOL, OptionsCategory::CONNECTION); + argsman.AddArg("-i2pacceptincoming", strprintf("Whether to accept inbound I2P connections (default: %i). Ignored if -i2psam is not set. Listening for inbound I2P connections is done through the SAM proxy, not by binding to a local address and port.", DEFAULT_I2P_ACCEPT_INCOMING), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-onlynet=", "Make outgoing connections only through network (" + Join(GetNetworkNames(), ", ") + "). Incoming connections are not affected by this option. This option can be specified multiple times to allow multiple networks. Warning: if it is used with non-onion networks and the -onion or -proxy option is set, then outbound onion connections will still be made; use -noonion or -onion=0 to disable outbound onion connections in this case.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-peerblockfilters", strprintf("Serve compact block filters to peers per BIP 157 (default: %u)", DEFAULT_PEERBLOCKFILTERS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-peerbloomfilters", strprintf("Support filtering of blocks and transaction with bloom filters (default: %u)", DEFAULT_PEERBLOOMFILTERS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -2534,7 +2535,7 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc SetReachable(NET_I2P, false); } - connOptions.m_i2p_accept_incoming = args.GetBoolArg("-i2pacceptincoming", true); + connOptions.m_i2p_accept_incoming = args.GetBoolArg("-i2pacceptincoming", DEFAULT_I2P_ACCEPT_INCOMING); if (!node.connman->Start(*node.dmnman, *node.mn_metaman, *node.mn_sync, *node.scheduler, connOptions)) { return false; diff --git a/src/net.cpp b/src/net.cpp index e8175240f5e2..2e5c297cacb6 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -420,6 +420,7 @@ static CAddress GetBindAddress(SOCKET sock) CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type) { + AssertLockNotHeld(m_unused_i2p_sessions_mutex); assert(conn_type != ConnectionType::INBOUND); if (pszDest == nullptr) { @@ -475,18 +476,42 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo proxyType proxy; CAddress addr_bind; assert(!addr_bind.IsValid()); + std::unique_ptr i2p_transient_session; if (addrConnect.IsValid()) { + const bool use_proxy{GetProxy(addrConnect.GetNetwork(), proxy)}; bool proxyConnectionFailed = false; - if (addrConnect.GetNetwork() == NET_I2P && m_i2p_sam_session.get() != nullptr) { + if (addrConnect.GetNetwork() == NET_I2P && use_proxy) { i2p::Connection conn; - if (m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed)) { - connected = true; + + if (m_i2p_sam_session) { + connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed); + } else { + { + LOCK(m_unused_i2p_sessions_mutex); + if (m_unused_i2p_sessions.empty()) { + i2p_transient_session = + std::make_unique(proxy.proxy, &interruptNet); + } else { + i2p_transient_session.swap(m_unused_i2p_sessions.front()); + m_unused_i2p_sessions.pop(); + } + } + connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed); + if (!connected) { + LOCK(m_unused_i2p_sessions_mutex); + if (m_unused_i2p_sessions.size() < MAX_UNUSED_I2P_SESSIONS_SIZE) { + m_unused_i2p_sessions.emplace(i2p_transient_session.release()); + } + } + } + + if (connected) { sock = std::move(conn.sock); addr_bind = CAddress{conn.me, NODE_NONE}; } - } else if (GetProxy(addrConnect.GetNetwork(), proxy)) { + } else if (use_proxy) { sock = CreateSock(proxy.proxy); if (!sock) { return nullptr; @@ -528,7 +553,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo if (!addr_bind.IsValid()) { addr_bind = GetBindAddress(sock->Get()); } - CNode* pnode = new CNode(id, nLocalServices, sock->Release(), addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", conn_type, /* inbound_onion */ false); + CNode* pnode = new CNode(id, nLocalServices, sock->Release(), addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", conn_type, /* inbound_onion */ false, std::move(i2p_transient_session)); pnode->AddRef(); statsClient.inc("peers.connect", 1.0f); @@ -571,6 +596,8 @@ void CNode::CloseSocketDisconnect(CConnman* connman) LogPrint(BCLog::NET, "disconnecting peer=%d\n", id); CloseSocket(hSocket); + m_i2p_sam_session.reset(); + statsClient.inc("peers.disconnect", 1.0f); } @@ -1336,6 +1363,7 @@ void CConnman::CreateNodeFromAcceptedSocket(SOCKET hSocket, bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type) { + AssertLockNotHeld(m_unused_i2p_sessions_mutex); std::optional max_connections; switch (conn_type) { case ConnectionType::INBOUND: @@ -2305,6 +2333,7 @@ void CConnman::DumpAddresses() void CConnman::ProcessAddrFetch() { + AssertLockNotHeld(m_unused_i2p_sessions_mutex); std::string strDest; { LOCK(m_addr_fetches_mutex); @@ -2371,6 +2400,7 @@ int CConnman::GetExtraBlockRelayCount() const void CConnman::ThreadOpenConnections(const std::vector connect, CDeterministicMNManager& dmnman) { + AssertLockNotHeld(m_unused_i2p_sessions_mutex); FastRandomContext rng; // Connect to specific addresses if (!connect.empty()) @@ -2739,6 +2769,7 @@ std::vector CConnman::GetAddedNodeInfo() const void CConnman::ThreadOpenAddedConnections() { + AssertLockNotHeld(m_unused_i2p_sessions_mutex); while (true) { CSemaphoreGrant grant(*semAddnode); @@ -2942,6 +2973,7 @@ void CConnman::ThreadOpenMasternodeConnections(CDeterministicMNManager& dmnman, // if successful, this moves the passed grant to the constructed node void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant *grantOutbound, const char *pszDest, ConnectionType conn_type, MasternodeConn masternode_connection, MasternodeProbeConn masternode_probe_connection) { + AssertLockNotHeld(m_unused_i2p_sessions_mutex); assert(conn_type != ConnectionType::INBOUND); // @@ -3342,7 +3374,7 @@ bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_met } proxyType i2p_sam; - if (GetProxy(NET_I2P, i2p_sam)) { + if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) { m_i2p_sam_session = std::make_unique(GetDataDir() / "i2p_private_key", i2p_sam.proxy, &interruptNet); } @@ -3444,7 +3476,7 @@ bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_met // Process messages threadMessageHandler = std::thread(&util::TraceThread, "msghand", [this] { ThreadMessageHandler(); }); - if (connOptions.m_i2p_accept_incoming && m_i2p_sam_session.get() != nullptr) { + if (m_i2p_sam_session) { threadI2PAcceptIncoming = std::thread(&util::TraceThread, "i2paccept", [this, &mn_sync] { ThreadI2PAcceptIncoming(mn_sync); }); } @@ -4012,17 +4044,18 @@ ServiceFlags CConnman::GetLocalServices() const unsigned int CConnman::GetReceiveFloodSize() const { return nReceiveFloodSize; } -CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress& addrBindIn, const std::string& addrNameIn, ConnectionType conn_type_in, bool inbound_onion) - : nTimeConnected(GetTimeSeconds()), - addr(addrIn), - addrBind(addrBindIn), +CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress& addrBindIn, const std::string& addrNameIn, ConnectionType conn_type_in, bool inbound_onion, std::unique_ptr&& i2p_sam_session) + : nTimeConnected{GetTimeSeconds()}, + addr{addrIn}, + addrBind{addrBindIn}, m_addr_name{addrNameIn.empty() ? addr.ToStringIPPort() : addrNameIn}, - m_inbound_onion(inbound_onion), - nKeyedNetGroup(nKeyedNetGroupIn), - id(idIn), - nLocalHostNonce(nLocalHostNonceIn), - m_conn_type(conn_type_in), - nLocalServices(nLocalServicesIn) + m_inbound_onion{inbound_onion}, + nKeyedNetGroup{nKeyedNetGroupIn}, + id{idIn}, + nLocalHostNonce{nLocalHostNonceIn}, + m_conn_type{conn_type_in}, + nLocalServices{nLocalServicesIn}, + m_i2p_sam_session{std::move(i2p_sam_session)} { if (inbound_onion) assert(conn_type_in == ConnectionType::INBOUND); hSocket = hSocketIn; diff --git a/src/net.h b/src/net.h index d7cbecd77923..b6f510f05c54 100644 --- a/src/net.h +++ b/src/net.h @@ -41,9 +41,9 @@ #include #include #include -#include #include #include +#include #include class CConnman; @@ -622,7 +622,7 @@ class CNode bool IsBlockRelayOnly() const; - CNode(NodeId id, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const CAddress &addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress &addrBindIn, const std::string &addrNameIn, ConnectionType conn_type_in, bool inbound_onion); + CNode(NodeId id, ServiceFlags nLocalServicesIn, SOCKET hSocketIn, const CAddress &addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress &addrBindIn, const std::string &addrNameIn, ConnectionType conn_type_in, bool inbound_onion, std::unique_ptr&& i2p_sam_session = nullptr); ~CNode(); CNode(const CNode&) = delete; CNode& operator=(const CNode&) = delete; @@ -776,6 +776,18 @@ class CNode mapMsgCmdSize mapSendBytesPerMsgCmd GUARDED_BY(cs_vSend); mapMsgCmdSize mapRecvBytesPerMsgCmd GUARDED_BY(cs_vRecv); + + /** + * If an I2P session is created per connection (for outbound transient I2P + * connections) then it is stored here so that it can be destroyed when the + * socket is closed. I2P sessions involve a data/transport socket (in `m_sock`) + * and a control socket (in `m_i2p_sam_session`). For transient sessions, once + * the data socket is closed, the control socket is not going to be used anymore + * and is just taking up resources. So better close it as soon as `m_sock` is + * closed. + * Otherwise this unique_ptr is empty. + */ + std::unique_ptr m_i2p_sam_session GUARDED_BY(cs_hSocket); }; /** @@ -913,9 +925,9 @@ friend class CNode; const char* strDest, ConnectionType conn_type, MasternodeConn masternode_connection = MasternodeConn::IsNotConnection, MasternodeProbeConn masternode_probe_connection = MasternodeProbeConn::IsNotConnection) - EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); + EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex, !mutexMsgProc); void OpenMasternodeConnection(const CAddress& addrConnect, MasternodeProbeConn probe = MasternodeProbeConn::IsConnection) - EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); + EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex, !mutexMsgProc); bool CheckIncomingNonce(uint64_t nonce); struct CFullyConnectedOnly { @@ -1113,7 +1125,8 @@ friend class CNode; * - Max total outbound connection capacity filled * - Max connection capacity for type is filled */ - bool AddConnection(const std::string& address, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); + bool AddConnection(const std::string& address, ConnectionType conn_type) + EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex, !mutexMsgProc); bool AddPendingMasternode(const uint256& proTxHash); void SetMasternodeQuorumNodes(Consensus::LLMQType llmqType, const uint256& quorumHash, const std::set& proTxHashes); @@ -1219,11 +1232,13 @@ friend class CNode; const std::vector& whiteBinds, const std::vector& onion_binds); - void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !mutexMsgProc); + void ThreadOpenAddedConnections() + EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_unused_i2p_sessions_mutex, !mutexMsgProc); void AddAddrFetch(const std::string& strDest) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex); - void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !mutexMsgProc); + void ProcessAddrFetch() + EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_unused_i2p_sessions_mutex, !mutexMsgProc); void ThreadOpenConnections(const std::vector connect, CDeterministicMNManager& dmnman) - EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !mutexMsgProc); + EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex, !mutexMsgProc); void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); void ThreadI2PAcceptIncoming(CMasternodeSync& mn_sync) EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); void AcceptConnection(const ListenSocket& hListenSocket, CMasternodeSync& mn_sync) @@ -1329,7 +1344,7 @@ friend class CNode; void ThreadDNSAddressSeed() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_nodes_mutex); void ThreadOpenMasternodeConnections(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_metaman, CMasternodeSync& mn_sync) - EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_nodes_mutex, !mutexMsgProc); + EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex, !mutexMsgProc); uint64_t CalculateKeyedNetGroup(const CAddress& ad) const; @@ -1345,7 +1360,8 @@ friend class CNode; bool AlreadyConnectedToAddress(const CAddress& addr); bool AttemptToEvictConnection(); - CNode* ConnectNode(CAddress addrConnect, const char *pszDest = nullptr, bool fCountFailure = false, ConnectionType conn_type = ConnectionType::OUTBOUND_FULL_RELAY); + CNode* ConnectNode(CAddress addrConnect, const char *pszDest = nullptr, bool fCountFailure = false, ConnectionType conn_type = ConnectionType::OUTBOUND_FULL_RELAY) + EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const; void DeleteNode(CNode* pnode); @@ -1498,7 +1514,8 @@ friend class CNode; /** * I2P SAM session. - * Used to accept incoming and make outgoing I2P connections. + * Used to accept incoming and make outgoing I2P connections from a persistent + * address. */ std::unique_ptr m_i2p_sam_session; @@ -1550,6 +1567,26 @@ friend class CNode; */ std::vector m_onion_binds; + /** + * Mutex protecting m_i2p_sam_sessions. + */ + Mutex m_unused_i2p_sessions_mutex; + + /** + * A pool of created I2P SAM transient sessions that should be used instead + * of creating new ones in order to reduce the load on the I2P network. + * Creating a session in I2P is not cheap, thus if this is not empty, then + * pick an entry from it instead of creating a new session. If connecting to + * a host fails, then the created session is put to this pool for reuse. + */ + std::queue> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex); + + /** + * Cap on the size of `m_unused_i2p_sessions`, to ensure it does not + * unexpectedly use too much memory. + */ + static constexpr size_t MAX_UNUSED_I2P_SESSIONS_SIZE{10}; + friend struct CConnmanTest; friend struct ConnmanTestMsg; }; diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp index 52300422e94a..470344a998a3 100644 --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -968,121 +967,5 @@ BOOST_AUTO_TEST_CASE(addrman_evictionworks) BOOST_CHECK(addrman.SelectTriedCollision().ToString() == "[::]:0"); } -BOOST_AUTO_TEST_CASE(reset_i2p_ports) -{ - CAddrManTest addrman1; - CAddrManTest addrman2; - const uint32_t good_time{static_cast(GetAdjustedTime())}; - constexpr uint16_t port = 8333; - - // Has its port changed, will be re-positioned within the same bucket in vvNew. - const CAddress i2p_new1{ - ResolveService("72l3ucjkuscrbiiepoehuwqgknyzgo7zuix5ty4puwrkyhtmnsga.b32.i2p", port), - NODE_NONE, - good_time}; - - // Has its port changed, will not be re-positioned in vvNew because ports 0 and 10075 result in - // the same bucket position. - const CAddress i2p_new2{ - ResolveService("gehtac45oaghz54ypyopim64mql7oad2bqclla74l6tfeolzmodq.b32.i2p", 10075), - NODE_NONE, - good_time}; - - // Remains unchanged, port is already as it should be. - const CAddress i2p_new3{ - ResolveService("c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p", - I2P_SAM31_PORT), - NODE_NONE, - good_time}; - - // Has its port changed, re-positioning in vvNew will cause i2p_new3 to be evicted. - const CAddress i2p_new4{ - ResolveService("c4cbbkn46qxftwja53pxiykntegfyfjqtnzbm6iv6r5mungmqgmq.b32.i2p", port), - NODE_NONE, - good_time}; - - // Remains unchanged. - const CAddress ipv4_new{ResolveService("1.2.3.4", port), NODE_NONE, good_time}; - - // Has its port changed, will be re-positioned in vvTried. - const CAddress i2p_tried1{ - ResolveService("h3r6bkn46qxftwja53pxiykntegfyfjqtnzbm6iv6r5mungmqgmq.b32.i2p", port), - NODE_NONE, - good_time}; - - // Has its port changed, will not be re-positioned in vvTried because ports 0 and 10537 - // result in the same position (bucket, i). - const CAddress i2p_tried2{ - ResolveService("pjs7or2ctvteeo5tu4bwyrtydeuhqhvdprtujn4daxr75jpebjxa.b32.i2p", 10537), - NODE_NONE, - good_time}; - - // Remains unchanged, port is already as it should be. - const CAddress i2p_tried3{ - ResolveService("hnbbyjpxx54623l555sta7pocy3se4sdgmuebi5k6reesz5rjp6q.b32.i2p", - I2P_SAM31_PORT), - NODE_NONE, - good_time}; - - // Has its port changed, re-positioning in vvTried will cause i2p_tried3 to be moved to vvNew. - const CAddress i2p_tried4{ - ResolveService("hna37nqr3ahkqv62cuqfwgtneekvvpnuc4i4f6yo7tpoqjswvcwa.b32.i2p", port), - NODE_NONE, - good_time}; - - // Remains unchanged. - const CAddress ipv4_tried{ResolveService("2.3.4.5", port), NODE_NONE, good_time}; - - const CNetAddr source; - - CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); - - addrman1.Add(i2p_new1, source); - addrman1.Add(i2p_new2, source); - addrman1.Add(i2p_new3, source); - addrman1.Add(i2p_new4, source); - addrman1.Add(ipv4_new, source); - - addrman1.Add(i2p_tried1, source); - addrman1.Good(i2p_tried1); - addrman1.Add(i2p_tried2, source); - addrman1.Good(i2p_tried2); - addrman1.Add(i2p_tried3, source); - addrman1.Good(i2p_tried3); - addrman1.Add(i2p_tried4, source); - addrman1.Good(i2p_tried4); - addrman1.Add(ipv4_tried, source); - addrman1.Good(ipv4_tried); - - stream << addrman1; - stream >> addrman2; - - const size_t max_addresses{0}; - const size_t max_pct{0}; - - auto addresses = addrman2.GetAddr(max_addresses, max_pct, NET_I2P); - BOOST_REQUIRE_EQUAL(addresses.size(), 7UL); - std::sort(addresses.begin(), addresses.end()); // Just some deterministic order. - BOOST_CHECK_EQUAL(addresses[0].ToStringIP(), i2p_new4.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[0].GetPort(), I2P_SAM31_PORT); - BOOST_CHECK_EQUAL(addresses[1].ToStringIP(), i2p_new2.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[1].GetPort(), I2P_SAM31_PORT); - BOOST_CHECK_EQUAL(addresses[2].ToStringIP(), i2p_tried4.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[2].GetPort(), I2P_SAM31_PORT); - BOOST_CHECK_EQUAL(addresses[3].ToStringIP(), i2p_tried3.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[3].GetPort(), I2P_SAM31_PORT); - BOOST_CHECK_EQUAL(addresses[4].ToStringIP(), i2p_tried1.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[4].GetPort(), I2P_SAM31_PORT); - BOOST_CHECK_EQUAL(addresses[5].ToStringIP(), i2p_tried2.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[5].GetPort(), I2P_SAM31_PORT); - BOOST_CHECK_EQUAL(addresses[6].ToStringIP(), i2p_new1.ToStringIP()); - BOOST_CHECK_EQUAL(addresses[6].GetPort(), I2P_SAM31_PORT); - - addresses = addrman2.GetAddr(max_addresses, max_pct, NET_IPV4); - BOOST_REQUIRE_EQUAL(addresses.size(), 2UL); - std::sort(addresses.begin(), addresses.end()); // Just some deterministic order. - BOOST_CHECK_EQUAL(addresses[0].ToStringIPPort(), ipv4_new.ToStringIPPort()); - BOOST_CHECK_EQUAL(addresses[1].ToStringIPPort(), ipv4_tried.ToStringIPPort()); -} BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/i2p_tests.cpp b/src/test/i2p_tests.cpp index 1c2a6a433d29..2f36afaab880 100644 --- a/src/test/i2p_tests.cpp +++ b/src/test/i2p_tests.cpp @@ -31,7 +31,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv) i2p::sam::Session session(GetDataDir() / "test_i2p_private_key", CService{}, &interrupt); { - ASSERT_DEBUG_LOG("Creating SAM session"); + ASSERT_DEBUG_LOG("Creating persistent SAM session"); ASSERT_DEBUG_LOG("too many bytes without a terminator"); i2p::Connection conn; diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 29b714c5841e..09f2000d0ff1 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -761,37 +761,42 @@ BOOST_AUTO_TEST_CASE(ipv4_peer_with_ipv6_addrMe_test) BOOST_AUTO_TEST_CASE(LimitedAndReachable_Network) { - BOOST_CHECK_EQUAL(IsReachable(NET_IPV4), true); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV6), true); - BOOST_CHECK_EQUAL(IsReachable(NET_ONION), true); + BOOST_CHECK(IsReachable(NET_IPV4)); + BOOST_CHECK(IsReachable(NET_IPV6)); + BOOST_CHECK(IsReachable(NET_ONION)); + BOOST_CHECK(IsReachable(NET_I2P)); SetReachable(NET_IPV4, false); SetReachable(NET_IPV6, false); SetReachable(NET_ONION, false); + SetReachable(NET_I2P, false); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV4), false); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV6), false); - BOOST_CHECK_EQUAL(IsReachable(NET_ONION), false); + BOOST_CHECK(!IsReachable(NET_IPV4)); + BOOST_CHECK(!IsReachable(NET_IPV6)); + BOOST_CHECK(!IsReachable(NET_ONION)); + BOOST_CHECK(!IsReachable(NET_I2P)); SetReachable(NET_IPV4, true); SetReachable(NET_IPV6, true); SetReachable(NET_ONION, true); + SetReachable(NET_I2P, true); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV4), true); - BOOST_CHECK_EQUAL(IsReachable(NET_IPV6), true); - BOOST_CHECK_EQUAL(IsReachable(NET_ONION), true); + BOOST_CHECK(IsReachable(NET_IPV4)); + BOOST_CHECK(IsReachable(NET_IPV6)); + BOOST_CHECK(IsReachable(NET_ONION)); + BOOST_CHECK(IsReachable(NET_I2P)); } BOOST_AUTO_TEST_CASE(LimitedAndReachable_NetworkCaseUnroutableAndInternal) { - BOOST_CHECK_EQUAL(IsReachable(NET_UNROUTABLE), true); - BOOST_CHECK_EQUAL(IsReachable(NET_INTERNAL), true); + BOOST_CHECK(IsReachable(NET_UNROUTABLE)); + BOOST_CHECK(IsReachable(NET_INTERNAL)); SetReachable(NET_UNROUTABLE, false); SetReachable(NET_INTERNAL, false); - BOOST_CHECK_EQUAL(IsReachable(NET_UNROUTABLE), true); // Ignored for both networks - BOOST_CHECK_EQUAL(IsReachable(NET_INTERNAL), true); + BOOST_CHECK(IsReachable(NET_UNROUTABLE)); // Ignored for both networks + BOOST_CHECK(IsReachable(NET_INTERNAL)); } CNetAddr UtilBuildAddress(unsigned char p1, unsigned char p2, unsigned char p3, unsigned char p4) @@ -810,10 +815,10 @@ BOOST_AUTO_TEST_CASE(LimitedAndReachable_CNetAddr) CNetAddr addr = UtilBuildAddress(0x001, 0x001, 0x001, 0x001); // 1.1.1.1 SetReachable(NET_IPV4, true); - BOOST_CHECK_EQUAL(IsReachable(addr), true); + BOOST_CHECK(IsReachable(addr)); SetReachable(NET_IPV4, false); - BOOST_CHECK_EQUAL(IsReachable(addr), false); + BOOST_CHECK(!IsReachable(addr)); SetReachable(NET_IPV4, true); // have to reset this, because this is stateful. } @@ -825,12 +830,12 @@ BOOST_AUTO_TEST_CASE(LocalAddress_BasicLifecycle) SetReachable(NET_IPV4, true); - BOOST_CHECK_EQUAL(IsLocal(addr), false); - BOOST_CHECK_EQUAL(AddLocal(addr, 1000), true); - BOOST_CHECK_EQUAL(IsLocal(addr), true); + BOOST_CHECK(!IsLocal(addr)); + BOOST_CHECK(AddLocal(addr, 1000)); + BOOST_CHECK(IsLocal(addr)); RemoveLocal(addr); - BOOST_CHECK_EQUAL(IsLocal(addr), false); + BOOST_CHECK(!IsLocal(addr)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 76bdc8534f36..a04a836c0ea4 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -97,14 +97,14 @@ def setup_nodes(self): # Note: proxies are not used to connect to local nodes. This is because the proxy to # use is based on CService.GetNetwork(), which returns NET_UNROUTABLE for localhost. args = [ - ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-proxyrandomize=1'], - ['-listen', '-proxy=%s:%i' % (self.conf1.addr),'-onion=%s:%i' % (self.conf2.addr), - '-i2psam=%s:%i' % (self.i2p_sam), '-i2pacceptincoming=0', '-proxyrandomize=0'], - ['-listen', '-proxy=%s:%i' % (self.conf2.addr),'-proxyrandomize=1'], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1'], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}',f'-onion={self.conf2.addr[0]}:{self.conf2.addr[1]}', + f'-i2psam={self.i2p_sam[0]}:{self.i2p_sam[1]}', '-i2pacceptincoming=0', '-proxyrandomize=0'], + ['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'], [] ] if self.have_ipv6: - args[3] = ['-listen', '-proxy=[%s]:%i' % (self.conf3.addr),'-proxyrandomize=0', '-noonion'] + args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion'] self.add_nodes(self.num_nodes, extra_args=args) self.start_nodes() @@ -116,7 +116,7 @@ def network_test(self, node, addr, network): def node_test(self, node, proxies, auth, test_onion=True): rv = [] addr = "15.61.23.23:1234" - self.log.debug("Test: outgoing IPv4 connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing IPv4 connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[0].queue.get() assert isinstance(cmd, Socks5Command) @@ -132,7 +132,7 @@ def node_test(self, node, proxies, auth, test_onion=True): if self.have_ipv6: addr = "[1233:3432:2434:2343:3234:2345:6546:4534]:5443" - self.log.debug("Test: outgoing IPv6 connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing IPv6 connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[1].queue.get() assert isinstance(cmd, Socks5Command) @@ -148,7 +148,7 @@ def node_test(self, node, proxies, auth, test_onion=True): if test_onion: addr = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333" - self.log.debug("Test: outgoing onion connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing onion connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[2].queue.get() assert isinstance(cmd, Socks5Command) @@ -162,7 +162,7 @@ def node_test(self, node, proxies, auth, test_onion=True): self.network_test(node, addr, network=NET_ONION) addr = "node.noumenon:8333" - self.log.debug("Test: outgoing DNS name connection through node for address {}".format(addr)) + self.log.debug(f"Test: outgoing DNS name connection through node for address {addr}") node.addnode(addr, "onetry") cmd = proxies[3].queue.get() assert isinstance(cmd, Socks5Command) @@ -218,12 +218,12 @@ def networks_dict(d): n1 = networks_dict(self.nodes[1].getnetworkinfo()) assert_equal(NETWORKS, n1.keys()) for net in ['ipv4', 'ipv6']: - assert_equal(n1[net]['proxy'], '%s:%i' % (self.conf1.addr)) + assert_equal(n1[net]['proxy'], f'{self.conf1.addr[0]}:{self.conf1.addr[1]}') assert_equal(n1[net]['proxy_randomize_credentials'], False) - assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr)) + assert_equal(n1['onion']['proxy'], f'{self.conf2.addr[0]}:{self.conf2.addr[1]}') assert_equal(n1['onion']['proxy_randomize_credentials'], False) assert_equal(n1['onion']['reachable'], True) - assert_equal(n1['i2p']['proxy'], '%s:%i' % (self.i2p_sam)) + assert_equal(n1['i2p']['proxy'], f'{self.i2p_sam[0]}:{self.i2p_sam[1]}') assert_equal(n1['i2p']['proxy_randomize_credentials'], False) assert_equal(n1['i2p']['reachable'], True) @@ -234,7 +234,7 @@ def networks_dict(d): expected_proxy = '' expected_randomize = False else: - expected_proxy = '%s:%i' % (self.conf2.addr) + expected_proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}' expected_randomize = True assert_equal(n2[net]['proxy'], expected_proxy) assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) @@ -248,7 +248,7 @@ def networks_dict(d): if net == NET_I2P: expected_proxy = '' else: - expected_proxy = '[%s]:%i' % (self.conf3.addr) + expected_proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}' assert_equal(n3[net]['proxy'], expected_proxy) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) diff --git a/test/functional/p2p_i2p_sessions.py b/test/functional/p2p_i2p_sessions.py new file mode 100755 index 000000000000..9e7fdc6e1446 --- /dev/null +++ b/test/functional/p2p_i2p_sessions.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022-2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test whether persistent or transient I2P sessions are being used, based on `-i2pacceptincoming`. +""" + +from test_framework.test_framework import BitcoinTestFramework + + +class I2PSessions(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + # The test assumes that an I2P SAM proxy is not listening here. + self.extra_args = [ + ["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=1"], + ["-i2psam=127.0.0.1:60000", "-i2pacceptincoming=0"], + ] + + def run_test(self): + addr = "zsxwyo6qcn3chqzwxnseusqgsnuw3maqnztkiypyfxtya4snkoka.b32.i2p" + + self.log.info("Ensure we create a persistent session when -i2pacceptincoming=1") + node0 = self.nodes[0] + with node0.assert_debug_log(expected_msgs=["Creating persistent SAM session"]): + node0.addnode(node=addr, command="onetry") + + self.log.info("Ensure we create a transient session when -i2pacceptincoming=0") + node1 = self.nodes[1] + with node1.assert_debug_log(expected_msgs=["Creating transient SAM session"]): + node1.addnode(node=addr, command="onetry") + + +if __name__ == '__main__': + I2PSessions().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index d789f9839f1e..07c6efd4ab1b 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -330,6 +330,7 @@ 'feature_blocksdir.py', 'wallet_startup.py', 'p2p_i2p_ports.py', + 'p2p_i2p_sessions.py', 'feature_config_args.py', 'feature_settings.py', 'rpc_getdescriptorinfo.py',