From 4cba2fdafa483cbdb70f581174138ec253c80d48 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 18 Nov 2020 17:13:27 +0100 Subject: [PATCH 01/20] util: extract {Read,Write}BinaryFile() to its own files Extract `ReadBinaryFile()` and `WriteBinaryFile()` from `torcontrol.cpp` to its own `readwritefile.{h,cpp}` files, so that it can be reused from other modules. --- src/Makefile.am | 2 ++ src/torcontrol.cpp | 47 +------------------------------------- src/util/readwritefile.cpp | 47 ++++++++++++++++++++++++++++++++++++++ src/util/readwritefile.h | 28 +++++++++++++++++++++++ 4 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 src/util/readwritefile.cpp create mode 100644 src/util/readwritefile.h diff --git a/src/Makefile.am b/src/Makefile.am index eae226b1d4d..d6af3049e80 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -242,6 +242,7 @@ BITCOIN_CORE_H = \ util/message.h \ util/moneystr.h \ util/rbf.h \ + util/readwritefile.h \ util/ref.h \ util/settings.h \ util/sock.h \ @@ -572,6 +573,7 @@ libbitcoin_util_a_SOURCES = \ util/message.cpp \ util/moneystr.cpp \ util/rbf.cpp \ + util/readwritefile.cpp \ util/settings.cpp \ util/threadnames.cpp \ util/spanparsing.cpp \ diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 605c77fc3a8..0f2a4895f1b 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -362,52 +363,6 @@ std::map ParseTorReplyMapping(const std::string &s) return mapping; } -/** Read full contents of a file and return them in a std::string. - * Returns a pair . - * If an error occurred, status will be false, otherwise status will be true and the data will be returned in string. - * - * @param maxsize Puts a maximum size limit on the file that is read. If the file is larger than this, truncated data - * (with len > maxsize) will be returned. - */ -static std::pair ReadBinaryFile(const fs::path &filename, size_t maxsize=std::numeric_limits::max()) -{ - FILE *f = fsbridge::fopen(filename, "rb"); - if (f == nullptr) - return std::make_pair(false,""); - std::string retval; - char buffer[128]; - size_t n; - while ((n=fread(buffer, 1, sizeof(buffer), f)) > 0) { - // Check for reading errors so we don't return any data if we couldn't - // read the entire file (or up to maxsize) - if (ferror(f)) { - fclose(f); - return std::make_pair(false,""); - } - retval.append(buffer, buffer+n); - if (retval.size() > maxsize) - break; - } - fclose(f); - return std::make_pair(true,retval); -} - -/** Write contents of std::string to a file. - * @return true on success. - */ -static bool WriteBinaryFile(const fs::path &filename, const std::string &data) -{ - FILE *f = fsbridge::fopen(filename, "wb"); - if (f == nullptr) - return false; - if (fwrite(data.data(), 1, data.size(), f) != data.size()) { - fclose(f); - return false; - } - fclose(f); - return true; -} - /****** Bitcoin specific TorController implementation ********/ /** Controller that connects to Tor control socket, authenticate, then create diff --git a/src/util/readwritefile.cpp b/src/util/readwritefile.cpp new file mode 100644 index 00000000000..1d9beb111fd --- /dev/null +++ b/src/util/readwritefile.cpp @@ -0,0 +1,47 @@ +// Copyright (c) 2015-2020 The Bitcoin Core developers +// Copyright (c) 2017 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include + +std::pair ReadBinaryFile(const fs::path &filename, size_t maxsize=std::numeric_limits::max()) +{ + FILE *f = fsbridge::fopen(filename, "rb"); + if (f == nullptr) + return std::make_pair(false,""); + std::string retval; + char buffer[128]; + size_t n; + while ((n=fread(buffer, 1, sizeof(buffer), f)) > 0) { + // Check for reading errors so we don't return any data if we couldn't + // read the entire file (or up to maxsize) + if (ferror(f)) { + fclose(f); + return std::make_pair(false,""); + } + retval.append(buffer, buffer+n); + if (retval.size() > maxsize) + break; + } + fclose(f); + return std::make_pair(true,retval); +} + +bool WriteBinaryFile(const fs::path &filename, const std::string &data) +{ + FILE *f = fsbridge::fopen(filename, "wb"); + if (f == nullptr) + return false; + if (fwrite(data.data(), 1, data.size(), f) != data.size()) { + fclose(f); + return false; + } + fclose(f); + return true; +} diff --git a/src/util/readwritefile.h b/src/util/readwritefile.h new file mode 100644 index 00000000000..1dab874b38c --- /dev/null +++ b/src/util/readwritefile.h @@ -0,0 +1,28 @@ +// Copyright (c) 2015-2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_UTIL_READWRITEFILE_H +#define BITCOIN_UTIL_READWRITEFILE_H + +#include + +#include +#include +#include + +/** Read full contents of a file and return them in a std::string. + * Returns a pair . + * If an error occurred, status will be false, otherwise status will be true and the data will be returned in string. + * + * @param maxsize Puts a maximum size limit on the file that is read. If the file is larger than this, truncated data + * (with len > maxsize) will be returned. + */ +std::pair ReadBinaryFile(const fs::path &filename, size_t maxsize=std::numeric_limits::max()); + +/** Write contents of std::string to a file. + * @return true on success. + */ +bool WriteBinaryFile(const fs::path &filename, const std::string &data); + +#endif /* BITCOIN_UTIL_READWRITEFILE_H */ From 8b6e4b3b23027da263d257b342f5d9a53e4032d5 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 18 Nov 2020 17:34:41 +0100 Subject: [PATCH 02/20] util: fix ReadBinaryFile() returning partial contents If an error occurs and `fread()` returns `0` (nothing was read) then the code before this patch would have returned "success" with a partially read contents of the file. --- src/util/readwritefile.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/util/readwritefile.cpp b/src/util/readwritefile.cpp index 1d9beb111fd..15e9d7a9b91 100644 --- a/src/util/readwritefile.cpp +++ b/src/util/readwritefile.cpp @@ -17,8 +17,8 @@ std::pair ReadBinaryFile(const fs::path &filename, size_t maxs return std::make_pair(false,""); std::string retval; char buffer[128]; - size_t n; - while ((n=fread(buffer, 1, sizeof(buffer), f)) > 0) { + do { + const size_t n = fread(buffer, 1, sizeof(buffer), f); // Check for reading errors so we don't return any data if we couldn't // read the entire file (or up to maxsize) if (ferror(f)) { @@ -26,9 +26,7 @@ std::pair ReadBinaryFile(const fs::path &filename, size_t maxs return std::make_pair(false,""); } retval.append(buffer, buffer+n); - if (retval.size() > maxsize) - break; - } + } while (!feof(f) && retval.size() <= maxsize); fclose(f); return std::make_pair(true,retval); } From 545bc5f81d60fa6ff7c5cc43a2e9eef82f911466 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 18 Nov 2020 17:38:43 +0100 Subject: [PATCH 03/20] util: fix WriteBinaryFile() claiming success even if error occurred `fclose()` is flushing any buffered data to disk, so if it fails then that could mean that the data was not completely written to disk. Thus, check if `fclose()` succeeds and only then claim success from `WriteBinaryFile()`. --- src/util/readwritefile.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/readwritefile.cpp b/src/util/readwritefile.cpp index 15e9d7a9b91..a45c41d367d 100644 --- a/src/util/readwritefile.cpp +++ b/src/util/readwritefile.cpp @@ -40,6 +40,8 @@ bool WriteBinaryFile(const fs::path &filename, const std::string &data) fclose(f); return false; } - fclose(f); + if (fclose(f) != 0) { + return false; + } return true; } From 25605895afe84b1765dd9da9240af22f99489df7 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 24 Nov 2020 12:15:07 +0100 Subject: [PATCH 04/20] net: check for invalid socket earlier in CConnman::AcceptConnection() This check is related to an `accept()` failure. So do the check earlier, closer to the `accept()` call. This will allow to isolate the `accept()`-specific code at the beginning of `CConnman::AcceptConnection()` and reuse the code that follows it. --- src/net.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index d9571e7036d..f90131f1c93 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1008,10 +1008,16 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { int nInbound = 0; int nMaxInbound = nMaxConnections - m_max_outbound; - if (hSocket != INVALID_SOCKET) { - if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr)) { - LogPrintf("Warning: Unknown socket family\n"); + if (hSocket == INVALID_SOCKET) { + const int nErr = WSAGetLastError(); + if (nErr != WSAEWOULDBLOCK) { + LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr)); } + return; + } + + if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr)) { + LogPrintf("Warning: Unknown socket family\n"); } NetPermissionFlags permissionFlags = NetPermissionFlags::PF_NONE; @@ -1032,14 +1038,6 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { } } - if (hSocket == INVALID_SOCKET) - { - int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK) - LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr)); - return; - } - if (!fNetworkActive) { LogPrint(BCLog::NET, "connection from %s dropped: not accepting new connections\n", addr.ToString()); CloseSocket(hSocket); From 1f75a653dd3b24ba2e4383bf951a6e5a3d5ccbcf Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 24 Nov 2020 12:24:18 +0100 Subject: [PATCH 05/20] net: get the bind address earlier in CConnman::AcceptConnection() Call `GetBindAddress()` earlier in `CConnman::AcceptConnection()`. That is specific to the TCP protocol and makes the code below it reusable for other protocols, if the caller provides `addr_bind`, retrieved by other means. --- src/net.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/net.cpp b/src/net.cpp index f90131f1c93..a0072800e22 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1020,6 +1020,8 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { LogPrintf("Warning: Unknown socket family\n"); } + const CAddress addr_bind = GetBindAddress(hSocket); + NetPermissionFlags permissionFlags = NetPermissionFlags::PF_NONE; hListenSocket.AddSocketPermissionFlags(permissionFlags); AddWhitelistPermissionFlags(permissionFlags, addr); @@ -1085,7 +1087,6 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { NodeId id = GetNewNodeId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); - CAddress addr_bind = GetBindAddress(hSocket); ServiceFlags nodeServices = nLocalServices; if (NetPermissions::HasFlag(permissionFlags, PF_BLOOMFILTER)) { From 7c224fdac4699a2c4953b33ab423f9cddbf95cf7 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 24 Nov 2020 12:40:03 +0100 Subject: [PATCH 06/20] net: isolate the protocol-agnostic part of CConnman::AcceptConnection() Isolate the second half of `CConnman::AcceptConnection()` into a new separate method, which could be reused if we accept incoming connections by other means than `accept()` (first half of `CConnman::AcceptConnection()`). --- src/net.cpp | 14 ++++++++++++-- src/net.h | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index a0072800e22..6109fc4e5a5 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1005,8 +1005,6 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { socklen_t len = sizeof(sockaddr); SOCKET hSocket = accept(hListenSocket.socket, (struct sockaddr*)&sockaddr, &len); CAddress addr; - int nInbound = 0; - int nMaxInbound = nMaxConnections - m_max_outbound; if (hSocket == INVALID_SOCKET) { const int nErr = WSAGetLastError(); @@ -1024,6 +1022,18 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { NetPermissionFlags permissionFlags = NetPermissionFlags::PF_NONE; hListenSocket.AddSocketPermissionFlags(permissionFlags); + + CreateNodeFromAcceptedSocket(hSocket, permissionFlags, addr_bind, addr); +} + +void CConnman::CreateNodeFromAcceptedSocket(SOCKET hSocket, + NetPermissionFlags permissionFlags, + const CAddress& addr_bind, + const CAddress& addr) +{ + int nInbound = 0; + int nMaxInbound = nMaxConnections - m_max_outbound; + AddWhitelistPermissionFlags(permissionFlags, addr); if (NetPermissions::HasFlag(permissionFlags, NetPermissionFlags::PF_ISIMPLICIT)) { NetPermissions::ClearFlag(permissionFlags, PF_ISIMPLICIT); diff --git a/src/net.h b/src/net.h index 67d1cf0e55c..be38441008b 100644 --- a/src/net.h +++ b/src/net.h @@ -1049,6 +1049,20 @@ private: void ThreadOpenConnections(std::vector connect); void ThreadMessageHandler(); void AcceptConnection(const ListenSocket& hListenSocket); + + /** + * Create a `CNode` object from a socket that has just been accepted and add the node to + * the `vNodes` member. + * @param[in] hSocket Connected socket to communicate with the peer. + * @param[in] permissionFlags The peer's permissions. + * @param[in] addr_bind The address and port at our side of the connection. + * @param[in] addr The address and port at the peer's side of the connection. + */ + void CreateNodeFromAcceptedSocket(SOCKET hSocket, + NetPermissionFlags permissionFlags, + const CAddress& addr_bind, + const CAddress& addr); + void DisconnectNodes(); void NotifyNumConnectionsChanged(); /** Return true if the peer is inactive and should be disconnected. */ From f6c267db3be2d7077fd2bdbd34860eba838dea99 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 24 Nov 2020 15:36:27 +0100 Subject: [PATCH 07/20] net: avoid unnecessary GetBindAddress() call Our local (bind) address is already saved in `CNode::addrBind` and there is no need to re-retrieve it again with `GetBindAddress()`. Also, for I2P connections `CNode::addrBind` would contain our I2P address, but `GetBindAddress()` would return something like `127.0.0.1:RANDOM_PORT`. --- src/net.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 6109fc4e5a5..d03e3cac37f 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2606,9 +2606,7 @@ std::vector CConnman::GetAddresses(size_t max_addresses, size_t max_pc std::vector CConnman::GetAddresses(CNode& requestor, size_t max_addresses, size_t max_pct) { - SOCKET socket; - WITH_LOCK(requestor.cs_hSocket, socket = requestor.hSocket); - auto local_socket_bytes = GetBindAddress(socket).GetAddrBytes(); + auto local_socket_bytes = requestor.addrBind.GetAddrBytes(); uint64_t cache_id = GetDeterministicRandomizer(RANDOMIZER_ID_ADDRCACHE) .Write(requestor.addr.GetNetwork()) .Write(local_socket_bytes.data(), local_socket_bytes.size()) From cff65c4a270887ec171293409ab84f5d0d0be7fc Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 21 Oct 2020 11:35:19 +0200 Subject: [PATCH 08/20] net: extend CNetAddr::SetSpecial() to support I2P Recognize also I2P addresses in the form `base32hashofpublickey.b32.i2p` from `CNetAddr::SetSpecial()`. This makes `Lookup()` support them, which in turn makes it possible to manually connect to an I2P node by using `-proxy=i2p_socks5_proxy:port -addnode=i2p_address.b32.i2p:port` Co-authored-by: Lucas Ontivero --- src/netaddress.cpp | 59 ++++++++++++++++++++++++++++++++++-------- src/netaddress.h | 30 ++++++++++++++++++++- src/test/net_tests.cpp | 31 ++++++++++++++++++++++ 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/netaddress.cpp b/src/netaddress.cpp index 85e46fd373e..80313b88cea 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -221,25 +221,34 @@ static void Checksum(Span addr_pubkey, uint8_t (&checksum)[CHECKS }; // namespace torv3 -/** - * Parse a TOR address and set this object to it. - * - * @returns Whether or not the operation was successful. - * - * @see CNetAddr::IsTor() - */ -bool CNetAddr::SetSpecial(const std::string& str) +bool CNetAddr::SetSpecial(const std::string& addr) +{ + if (!ValidAsCString(addr)) { + return false; + } + + if (SetTor(addr)) { + return true; + } + + if (SetI2P(addr)) { + return true; + } + + return false; +} + +bool CNetAddr::SetTor(const std::string& addr) { static const char* suffix{".onion"}; static constexpr size_t suffix_len{6}; - if (!ValidAsCString(str) || str.size() <= suffix_len || - str.substr(str.size() - suffix_len) != suffix) { + if (addr.size() <= suffix_len || addr.substr(addr.size() - suffix_len) != suffix) { return false; } bool invalid; - const auto& input = DecodeBase32(str.substr(0, str.size() - suffix_len).c_str(), &invalid); + const auto& input = DecodeBase32(addr.substr(0, addr.size() - suffix_len).c_str(), &invalid); if (invalid) { return false; @@ -275,6 +284,34 @@ bool CNetAddr::SetSpecial(const std::string& str) return false; } +bool CNetAddr::SetI2P(const std::string& addr) +{ + // I2P addresses that we support consist of 52 base32 characters + ".b32.i2p". + static constexpr size_t b32_len{52}; + static const char* suffix{".b32.i2p"}; + static constexpr size_t suffix_len{8}; + + if (addr.size() != b32_len + suffix_len || ToLower(addr.substr(b32_len)) != suffix) { + return false; + } + + // Remove the ".b32.i2p" suffix and pad to a multiple of 8 chars, so DecodeBase32() + // can decode it. + const std::string b32_padded = addr.substr(0, b32_len) + "===="; + + bool invalid; + const auto& address_bytes = DecodeBase32(b32_padded.c_str(), &invalid); + + if (invalid || address_bytes.size() != ADDR_I2P_SIZE) { + return false; + } + + m_net = NET_I2P; + m_addr.assign(address_bytes.begin(), address_bytes.end()); + + return true; +} + CNetAddr::CNetAddr(const struct in_addr& ipv4Addr) { m_net = NET_IPV4; diff --git a/src/netaddress.h b/src/netaddress.h index d0986557f79..897ce46cdaf 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -151,7 +151,16 @@ class CNetAddr bool SetInternal(const std::string& name); - bool SetSpecial(const std::string &strName); // for Tor addresses + /** + * Parse a Tor or I2P address and set this object to it. + * @param[in] addr Address to parse, for example + * pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion or + * ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p. + * @returns Whether the operation was successful. + * @see CNetAddr::IsTor(), CNetAddr::IsI2P() + */ + bool SetSpecial(const std::string& addr); + bool IsBindAny() const; // INADDR_ANY equivalent bool IsIPv4() const; // IPv4 mapped address (::FFFF:0:0/96, 0.0.0.0/0) bool IsIPv6() const; // IPv6 address (not mapped IPv4, not Tor) @@ -248,6 +257,25 @@ class CNetAddr friend class CSubNet; private: + /** + * Parse a Tor address and set this object to it. + * @param[in] addr Address to parse, must be a valid C string, for example + * pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion or + * 6hzph5hv6337r6p2.onion. + * @returns Whether the operation was successful. + * @see CNetAddr::IsTor() + */ + bool SetTor(const std::string& addr); + + /** + * Parse an I2P address and set this object to it. + * @param[in] addr Address to parse, must be a valid C string, for example + * ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p. + * @returns Whether the operation was successful. + * @see CNetAddr::IsI2P() + */ + bool SetI2P(const std::string& addr); + /** * BIP155 network ids recognized by this software. */ diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 1c7c35528e7..f039f76f82a 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -322,6 +322,7 @@ BOOST_AUTO_TEST_CASE(cnetaddr_basic) BOOST_REQUIRE(addr.IsValid()); BOOST_REQUIRE(addr.IsTor()); + BOOST_CHECK(!addr.IsI2P()); BOOST_CHECK(!addr.IsBindAny()); BOOST_CHECK(addr.IsAddrV1Compatible()); BOOST_CHECK_EQUAL(addr.ToString(), "6hzph5hv6337r6p2.onion"); @@ -332,6 +333,7 @@ BOOST_AUTO_TEST_CASE(cnetaddr_basic) BOOST_REQUIRE(addr.IsValid()); BOOST_REQUIRE(addr.IsTor()); + BOOST_CHECK(!addr.IsI2P()); BOOST_CHECK(!addr.IsBindAny()); BOOST_CHECK(!addr.IsAddrV1Compatible()); BOOST_CHECK_EQUAL(addr.ToString(), torv3_addr); @@ -352,6 +354,35 @@ BOOST_AUTO_TEST_CASE(cnetaddr_basic) // TOR, invalid base32 BOOST_CHECK(!addr.SetSpecial(std::string{"mf*g zak.onion"})); + // I2P + const char* i2p_addr = "UDHDrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.I2P"; + BOOST_REQUIRE(addr.SetSpecial(i2p_addr)); + BOOST_REQUIRE(addr.IsValid()); + BOOST_REQUIRE(addr.IsI2P()); + + BOOST_CHECK(!addr.IsTor()); + BOOST_CHECK(!addr.IsBindAny()); + BOOST_CHECK(!addr.IsAddrV1Compatible()); + BOOST_CHECK_EQUAL(addr.ToString(), ToLower(i2p_addr)); + + // I2P, correct length, but decodes to less than the expected number of bytes. + BOOST_CHECK(!addr.SetSpecial("udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jn=.b32.i2p")); + + // I2P, extra unnecessary padding + BOOST_CHECK(!addr.SetSpecial("udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna=.b32.i2p")); + + // I2P, malicious + BOOST_CHECK(!addr.SetSpecial("udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v\0wtf.b32.i2p"s)); + + // I2P, valid but unsupported (56 Base32 characters) + // See "Encrypted LS with Base 32 Addresses" in + // https://geti2p.net/spec/encryptedleaseset.txt + BOOST_CHECK( + !addr.SetSpecial("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscsad.b32.i2p")); + + // I2P, invalid base32 + BOOST_CHECK(!addr.SetSpecial(std::string{"tp*szydbh4dp.b32.i2p"})); + // Internal addr.SetInternal("esffpp"); BOOST_REQUIRE(!addr.IsValid()); // "internal" is considered invalid From 34bcfab562bac9887ca9c3831cf4fd0ee7f98149 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 4 Dec 2020 15:10:09 +0100 Subject: [PATCH 09/20] net: move the constant maxWait out of InterruptibleRecv() Move `maxWait` out of `InterruptibleRecv()` and rename it to `MAX_WAIT_FOR_IO` so that it can be reused by other code. --- src/netbase.cpp | 11 +++++------ src/util/sock.h | 7 +++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/netbase.cpp b/src/netbase.cpp index 0c5b3a220ee..5a714f00260 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -360,9 +361,6 @@ static IntrRecvError InterruptibleRecv(uint8_t* data, size_t len, int timeout, c { int64_t curTime = GetTimeMillis(); int64_t endTime = curTime + timeout; - // Maximum time to wait for I/O readiness. It will take up until this time - // (in millis) to break off in case of an interruption. - const int64_t maxWait = 1000; while (len > 0 && curTime < endTime) { ssize_t ret = sock.Recv(data, len, 0); // Optimistically try the recv first if (ret > 0) { @@ -373,10 +371,11 @@ static IntrRecvError InterruptibleRecv(uint8_t* data, size_t len, int timeout, c } else { // Other error or blocking int nErr = WSAGetLastError(); if (nErr == WSAEINPROGRESS || nErr == WSAEWOULDBLOCK || nErr == WSAEINVAL) { - // Only wait at most maxWait milliseconds at a time, unless + // Only wait at most MAX_WAIT_FOR_IO at a time, unless // we're approaching the end of the specified total timeout - int timeout_ms = std::min(endTime - curTime, maxWait); - if (!sock.Wait(std::chrono::milliseconds{timeout_ms}, Sock::RECV)) { + const auto remaining = std::chrono::milliseconds{endTime - curTime}; + const auto timeout = std::min(remaining, std::chrono::milliseconds{MAX_WAIT_FOR_IO}); + if (!sock.Wait(timeout, Sock::RECV)) { return IntrRecvError::NetworkError; } } else { diff --git a/src/util/sock.h b/src/util/sock.h index 26fe60f18f7..23c3c35dad5 100644 --- a/src/util/sock.h +++ b/src/util/sock.h @@ -6,10 +6,17 @@ #define BITCOIN_UTIL_SOCK_H #include +#include #include #include +/** + * Maximum time to wait for I/O readiness. + * It will take up until this time to break off in case of an interruption. + */ +static constexpr auto MAX_WAIT_FOR_IO = 1s; + /** * RAII helper class that manages a socket. Mimics `std::unique_ptr`, but instead of a pointer it * contains a socket and closes it automatically when it goes out of scope. From 78fdfbea666201b25919dd67454eb04d6a34326f Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 12 Feb 2021 15:56:15 +0100 Subject: [PATCH 10/20] net: dedup MSG_NOSIGNAL and MSG_DONTWAIT definitions Deduplicate `MSG_NOSIGNAL` and `MSG_DONTWAIT` definitions from `net.cpp` and `netbase.cpp` to `compat.h` where they can also be reused by other code. --- src/compat.h | 10 ++++++++++ src/net.cpp | 11 +---------- src/netbase.cpp | 5 +---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/compat.h b/src/compat.h index dad14748a2e..640be235462 100644 --- a/src/compat.h +++ b/src/compat.h @@ -96,4 +96,14 @@ bool static inline IsSelectableSocket(const SOCKET& s) { #endif } +// MSG_NOSIGNAL is not available on some platforms, if it doesn't exist define it as 0 +#if !defined(MSG_NOSIGNAL) +#define MSG_NOSIGNAL 0 +#endif + +// MSG_DONTWAIT is not available on some platforms, if it doesn't exist define it as 0 +#if !defined(MSG_DONTWAIT) +#define MSG_DONTWAIT 0 +#endif + #endif // BITCOIN_COMPAT_H diff --git a/src/net.cpp b/src/net.cpp index d03e3cac37f..89f7ef0f064 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -72,16 +73,6 @@ static constexpr std::chrono::seconds MAX_UPLOAD_TIMEFRAME{60 * 60 * 24}; // We add a random period time (0 to 1 seconds) to feeler connections to prevent synchronization. #define FEELER_SLEEP_WINDOW 1 -// MSG_NOSIGNAL is not available on some platforms, if it doesn't exist define it as 0 -#if !defined(MSG_NOSIGNAL) -#define MSG_NOSIGNAL 0 -#endif - -// MSG_DONTWAIT is not available on some platforms, if it doesn't exist define it as 0 -#if !defined(MSG_DONTWAIT) -#define MSG_DONTWAIT 0 -#endif - /** Used to pass flags to the Bind() function */ enum BindFlags { BF_NONE = 0, diff --git a/src/netbase.cpp b/src/netbase.cpp index 5a714f00260..7ccd24a7789 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -5,6 +5,7 @@ #include +#include #include #include #include @@ -30,10 +31,6 @@ #include #endif -#if !defined(MSG_NOSIGNAL) -#define MSG_NOSIGNAL 0 -#endif - // Settings static Mutex g_proxyinfo_mutex; static proxyType proxyInfo[NET_MAX] GUARDED_BY(g_proxyinfo_mutex); From ea1845315a109eb105113cb5fbb6f869e1cf010c Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 12 Feb 2021 14:05:53 +0100 Subject: [PATCH 11/20] net: extend Sock::Wait() to report a timeout Previously `Sock::Wait()` would not have signaled to the caller whether a timeout or one of the requested events occurred since that was not needed by any of the callers. Such functionality will be needed in the I2P implementation, thus extend the `Sock::Wait()` method. --- src/util/sock.cpp | 34 +++++++++++++++++++++++++++++++--- src/util/sock.h | 7 ++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/util/sock.cpp b/src/util/sock.cpp index 4c65b5b6809..d8fd9d41383 100644 --- a/src/util/sock.cpp +++ b/src/util/sock.cpp @@ -59,7 +59,7 @@ ssize_t Sock::Recv(void* buf, size_t len, int flags) const return recv(m_socket, static_cast(buf), len, flags); } -bool Sock::Wait(std::chrono::milliseconds timeout, Event requested) const +bool Sock::Wait(std::chrono::milliseconds timeout, Event requested, Event* occurred) const { #ifdef USE_POLL pollfd fd; @@ -72,7 +72,21 @@ bool Sock::Wait(std::chrono::milliseconds timeout, Event requested) const fd.events |= POLLOUT; } - return poll(&fd, 1, count_milliseconds(timeout)) != SOCKET_ERROR; + if (poll(&fd, 1, count_milliseconds(timeout)) == SOCKET_ERROR) { + return false; + } + + if (occurred != nullptr) { + *occurred = 0; + if (fd.revents & POLLIN) { + *occurred |= RECV; + } + if (fd.revents & POLLOUT) { + *occurred |= SEND; + } + } + + return true; #else if (!IsSelectableSocket(m_socket)) { return false; @@ -93,7 +107,21 @@ bool Sock::Wait(std::chrono::milliseconds timeout, Event requested) const timeval timeout_struct = MillisToTimeval(timeout); - return select(m_socket + 1, &fdset_recv, &fdset_send, nullptr, &timeout_struct) != SOCKET_ERROR; + if (select(m_socket + 1, &fdset_recv, &fdset_send, nullptr, &timeout_struct) == SOCKET_ERROR) { + return false; + } + + if (occurred != nullptr) { + *occurred = 0; + if (FD_ISSET(m_socket, &fdset_recv)) { + *occurred |= RECV; + } + if (FD_ISSET(m_socket, &fdset_send)) { + *occurred |= SEND; + } + } + + return true; #endif /* USE_POLL */ } diff --git a/src/util/sock.h b/src/util/sock.h index 23c3c35dad5..2d9cac14af6 100644 --- a/src/util/sock.h +++ b/src/util/sock.h @@ -105,9 +105,14 @@ public: * Wait for readiness for input (recv) or output (send). * @param[in] timeout Wait this much for at least one of the requested events to occur. * @param[in] requested Wait for those events, bitwise-or of `RECV` and `SEND`. + * @param[out] occurred If not nullptr and `true` is returned, then upon return this + * indicates which of the requested events occurred. A timeout is indicated by return + * value of `true` and `occurred` being set to 0. * @return true on success and false otherwise */ - virtual bool Wait(std::chrono::milliseconds timeout, Event requested) const; + virtual bool Wait(std::chrono::milliseconds timeout, + Event requested, + Event* occurred = nullptr) const; private: /** From 42c779f503eb8437b6232773a4a2472306cc9f3d Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 4 Dec 2020 15:11:25 +0100 Subject: [PATCH 12/20] net: extend Sock with methods for robust send & read until terminator Introduce two high level, convenience methods in the `Sock` class: * `SendComplete()`: keep trying to send the specified data until either successfully sent all of it, timeout or interrupted. * `RecvUntilTerminator()`: read until a terminator is encountered (never after it), timeout or interrupted. These will be convenient in the I2P SAM implementation. `SendComplete()` can also be used in the SOCKS5 implementation instead of calling `send()` directly. --- src/compat.h | 9 ++++ src/util/sock.cpp | 125 ++++++++++++++++++++++++++++++++++++++++++++++ src/util/sock.h | 29 +++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/compat.h b/src/compat.h index 640be235462..3449bc2661c 100644 --- a/src/compat.h +++ b/src/compat.h @@ -44,6 +44,7 @@ typedef unsigned int SOCKET; #define WSAEINVAL EINVAL #define WSAEALREADY EALREADY #define WSAEWOULDBLOCK EWOULDBLOCK +#define WSAEAGAIN EAGAIN #define WSAEMSGSIZE EMSGSIZE #define WSAEINTR EINTR #define WSAEINPROGRESS EINPROGRESS @@ -51,6 +52,14 @@ typedef unsigned int SOCKET; #define WSAENOTSOCK EBADF #define INVALID_SOCKET (SOCKET)(~0) #define SOCKET_ERROR -1 +#else +#ifndef WSAEAGAIN +#ifdef EAGAIN +#define WSAEAGAIN EAGAIN +#else +#define WSAEAGAIN WSAEWOULDBLOCK +#endif +#endif #endif #ifdef WIN32 diff --git a/src/util/sock.cpp b/src/util/sock.cpp index d8fd9d41383..36493ec2418 100644 --- a/src/util/sock.cpp +++ b/src/util/sock.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -12,12 +13,18 @@ #include #include #include +#include #include #ifdef USE_POLL #include #endif +static inline bool IOErrorIsPermanent(int err) +{ + return err != WSAEAGAIN && err != WSAEINTR && err != WSAEWOULDBLOCK && err != WSAEINPROGRESS; +} + Sock::Sock() : m_socket(INVALID_SOCKET) {} Sock::Sock(SOCKET s) : m_socket(s) {} @@ -125,6 +132,124 @@ bool Sock::Wait(std::chrono::milliseconds timeout, Event requested, Event* occur #endif /* USE_POLL */ } +void Sock::SendComplete(const std::string& data, + std::chrono::milliseconds timeout, + CThreadInterrupt& interrupt) const +{ + const auto deadline = GetTime() + timeout; + size_t sent{0}; + + for (;;) { + const ssize_t ret{Send(data.data() + sent, data.size() - sent, MSG_NOSIGNAL)}; + + if (ret > 0) { + sent += static_cast(ret); + if (sent == data.size()) { + break; + } + } else { + const int err{WSAGetLastError()}; + if (IOErrorIsPermanent(err)) { + throw std::runtime_error(strprintf("send(): %s", NetworkErrorString(err))); + } + } + + const auto now = GetTime(); + + if (now >= deadline) { + throw std::runtime_error(strprintf( + "Send timeout (sent only %u of %u bytes before that)", sent, data.size())); + } + + if (interrupt) { + throw std::runtime_error(strprintf( + "Send interrupted (sent only %u of %u bytes before that)", sent, data.size())); + } + + // Wait for a short while (or the socket to become ready for sending) before retrying + // if nothing was sent. + const auto wait_time = std::min(deadline - now, std::chrono::milliseconds{MAX_WAIT_FOR_IO}); + Wait(wait_time, SEND); + } +} + +std::string Sock::RecvUntilTerminator(uint8_t terminator, + std::chrono::milliseconds timeout, + CThreadInterrupt& interrupt) const +{ + const auto deadline = GetTime() + timeout; + std::string data; + bool terminator_found{false}; + + // We must not consume any bytes past the terminator from the socket. + // One option is to read one byte at a time and check if we have read a terminator. + // However that is very slow. Instead, we peek at what is in the socket and only read + // as many bytes as possible without crossing the terminator. + // Reading 64 MiB of random data with 262526 terminator chars takes 37 seconds to read + // one byte at a time VS 0.71 seconds with the "peek" solution below. Reading one byte + // at a time is about 50 times slower. + + for (;;) { + char buf[512]; + + const ssize_t peek_ret{Recv(buf, sizeof(buf), MSG_PEEK)}; + + switch (peek_ret) { + case -1: { + const int err{WSAGetLastError()}; + if (IOErrorIsPermanent(err)) { + throw std::runtime_error(strprintf("recv(): %s", NetworkErrorString(err))); + } + break; + } + case 0: + throw std::runtime_error("Connection unexpectedly closed by peer"); + default: + auto end = buf + peek_ret; + auto terminator_pos = std::find(buf, end, terminator); + terminator_found = terminator_pos != end; + + const size_t try_len{terminator_found ? terminator_pos - buf + 1 : + static_cast(peek_ret)}; + + const ssize_t read_ret{Recv(buf, try_len, 0)}; + + if (read_ret < 0 || static_cast(read_ret) != try_len) { + throw std::runtime_error( + strprintf("recv() returned %u bytes on attempt to read %u bytes but previous " + "peek claimed %u bytes are available", + read_ret, try_len, peek_ret)); + } + + // Don't include the terminator in the output. + const size_t append_len{terminator_found ? try_len - 1 : try_len}; + + data.append(buf, buf + append_len); + + if (terminator_found) { + return data; + } + } + + const auto now = GetTime(); + + if (now >= deadline) { + throw std::runtime_error(strprintf( + "Receive timeout (received %u bytes without terminator before that)", data.size())); + } + + if (interrupt) { + throw std::runtime_error(strprintf( + "Receive interrupted (received %u bytes without terminator before that)", + data.size())); + } + + // Wait for a short while (or the socket to become ready for reading) before retrying. + const auto wait_time = std::min(deadline - now, std::chrono::milliseconds{MAX_WAIT_FOR_IO}); + Wait(wait_time, RECV); + } +} + #ifdef WIN32 std::string NetworkErrorString(int err) { diff --git a/src/util/sock.h b/src/util/sock.h index 2d9cac14af6..209d30def4e 100644 --- a/src/util/sock.h +++ b/src/util/sock.h @@ -6,6 +6,7 @@ #define BITCOIN_UTIL_SOCK_H #include +#include #include #include @@ -114,6 +115,34 @@ public: Event requested, Event* occurred = nullptr) const; + /* Higher level, convenience, methods. These may throw. */ + + /** + * Send the given data, retrying on transient errors. + * @param[in] data Data to send. + * @param[in] timeout Timeout for the entire operation. + * @param[in] interrupt If this is signaled then the operation is canceled. + * @throws std::runtime_error if the operation cannot be completed. In this case only some of + * the data will be written to the socket. + */ + virtual void SendComplete(const std::string& data, + std::chrono::milliseconds timeout, + CThreadInterrupt& interrupt) const; + + /** + * Read from socket until a terminator character is encountered. Will never consume bytes past + * the terminator from the socket. + * @param[in] terminator Character up to which to read from the socket. + * @param[in] timeout Timeout for the entire operation. + * @param[in] interrupt If this is signaled then the operation is canceled. + * @return The data that has been read, without the terminating character. + * @throws std::runtime_error if the operation cannot be completed. In this case some bytes may + * have been consumed from the socket. + */ + virtual std::string RecvUntilTerminator(uint8_t terminator, + std::chrono::milliseconds timeout, + CThreadInterrupt& interrupt) const; + private: /** * Contained socket. `INVALID_SOCKET` designates the object is empty. From 5bac7e45e1d3a07115b5ff002d988438fcc92a53 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Thu, 21 Jan 2021 16:59:55 +0100 Subject: [PATCH 13/20] net: extend Sock with a method to check whether connected This will be convenient in the I2P SAM implementation. --- src/util/sock.cpp | 25 +++++++++++++++++++++++++ src/util/sock.h | 7 +++++++ 2 files changed, 32 insertions(+) diff --git a/src/util/sock.cpp b/src/util/sock.cpp index 36493ec2418..e13c52a16a1 100644 --- a/src/util/sock.cpp +++ b/src/util/sock.cpp @@ -250,6 +250,31 @@ std::string Sock::RecvUntilTerminator(uint8_t terminator, } } +bool Sock::IsConnected(std::string& errmsg) const +{ + if (m_socket == INVALID_SOCKET) { + errmsg = "not connected"; + return false; + } + + char c; + switch (Recv(&c, sizeof(c), MSG_PEEK)) { + case -1: { + const int err = WSAGetLastError(); + if (IOErrorIsPermanent(err)) { + errmsg = NetworkErrorString(err); + return false; + } + return true; + } + case 0: + errmsg = "closed"; + return false; + default: + return true; + } +} + #ifdef WIN32 std::string NetworkErrorString(int err) { diff --git a/src/util/sock.h b/src/util/sock.h index 209d30def4e..ecebb84205e 100644 --- a/src/util/sock.h +++ b/src/util/sock.h @@ -143,6 +143,13 @@ public: std::chrono::milliseconds timeout, CThreadInterrupt& interrupt) const; + /** + * Check if still connected. + * @param[out] err The error string, if the socket has been disconnected. + * @return true if connected + */ + virtual bool IsConnected(std::string& errmsg) const; + private: /** * Contained socket. `INVALID_SOCKET` designates the object is empty. From c22daa2ecff1acd25426cd46f98f2587d1d324c3 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 27 Nov 2020 13:59:26 +0100 Subject: [PATCH 14/20] net: implement the necessary parts of the I2P SAM protocol Implement the following commands from the I2P SAM protocol: * HELLO: needed for all of the remaining ones * DEST GENERATE: to generate our private key and destination * NAMING LOOKUP: to convert .i2p addresses to destinations * SESSION CREATE: needed for STREAM CONNECT and STREAM ACCEPT * STREAM CONNECT: to make outgoing connections * STREAM ACCEPT: to accept incoming connections --- src/Makefile.am | 2 + src/i2p.cpp | 407 ++++++++++++++++++++++++++++++++++++++++++++++++ src/i2p.h | 260 +++++++++++++++++++++++++++++++ src/logging.cpp | 1 + src/logging.h | 1 + 5 files changed, 671 insertions(+) create mode 100644 src/i2p.cpp create mode 100644 src/i2p.h diff --git a/src/Makefile.am b/src/Makefile.am index d6af3049e80..1c6d97b7144 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -148,6 +148,7 @@ BITCOIN_CORE_H = \ fs.h \ httprpc.h \ httpserver.h \ + i2p.h \ index/base.h \ index/blockfilterindex.h \ index/disktxpos.h \ @@ -315,6 +316,7 @@ libbitcoin_server_a_SOURCES = \ flatfile.cpp \ httprpc.cpp \ httpserver.cpp \ + i2p.cpp \ index/base.cpp \ index/blockfilterindex.cpp \ index/txindex.cpp \ diff --git a/src/i2p.cpp b/src/i2p.cpp new file mode 100644 index 00000000000..42270deaebf --- /dev/null +++ b/src/i2p.cpp @@ -0,0 +1,407 @@ +// Copyright (c) 2020-2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace i2p { + +/** + * Swap Standard Base64 <-> I2P Base64. + * Standard Base64 uses `+` and `/` as last two characters of its alphabet. + * I2P Base64 uses `-` and `~` respectively. + * So it is easy to detect in which one is the input and convert to the other. + * @param[in] from Input to convert. + * @return converted `from` + */ +static std::string SwapBase64(const std::string& from) +{ + std::string to; + to.resize(from.size()); + for (size_t i = 0; i < from.size(); ++i) { + switch (from[i]) { + case '-': + to[i] = '+'; + break; + case '~': + to[i] = '/'; + break; + case '+': + to[i] = '-'; + break; + case '/': + to[i] = '~'; + break; + default: + to[i] = from[i]; + break; + } + } + return to; +} + +/** + * Decode an I2P-style Base64 string. + * @param[in] i2p_b64 I2P-style Base64 string. + * @return decoded `i2p_b64` + * @throw std::runtime_error if decoding fails + */ +static Binary DecodeI2PBase64(const std::string& i2p_b64) +{ + const std::string& std_b64 = SwapBase64(i2p_b64); + bool invalid; + Binary decoded = DecodeBase64(std_b64.c_str(), &invalid); + if (invalid) { + throw std::runtime_error(strprintf("Cannot decode Base64: \"%s\"", i2p_b64)); + } + return decoded; +} + +/** + * Derive the .b32.i2p address of an I2P destination (binary). + * @param[in] dest I2P destination. + * @return the address that corresponds to `dest` + * @throw std::runtime_error if conversion fails + */ +static CNetAddr DestBinToAddr(const Binary& dest) +{ + CSHA256 hasher; + hasher.Write(dest.data(), dest.size()); + unsigned char hash[CSHA256::OUTPUT_SIZE]; + hasher.Finalize(hash); + + CNetAddr addr; + const std::string addr_str = EncodeBase32(hash, false) + ".b32.i2p"; + if (!addr.SetSpecial(addr_str)) { + throw std::runtime_error(strprintf("Cannot parse I2P address: \"%s\"", addr_str)); + } + + return addr; +} + +/** + * Derive the .b32.i2p address of an I2P destination (I2P-style Base64). + * @param[in] dest I2P destination. + * @return the address that corresponds to `dest` + * @throw std::runtime_error if conversion fails + */ +static CNetAddr DestB64ToAddr(const std::string& dest) +{ + const Binary& decoded = DecodeI2PBase64(dest); + return DestBinToAddr(decoded); +} + +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) +{ +} + +Session::~Session() +{ + LOCK(m_mutex); + Disconnect(); +} + +bool Session::Listen(Connection& conn) +{ + try { + LOCK(m_mutex); + CreateIfNotCreatedAlready(); + conn.me = m_my_addr; + conn.sock = StreamAccept(); + return true; + } catch (const std::runtime_error& e) { + Log("Error listening: %s", e.what()); + CheckControlSock(); + } + return false; +} + +bool Session::Accept(Connection& conn) +{ + try { + while (!*m_interrupt) { + Sock::Event occurred; + conn.sock.Wait(MAX_WAIT_FOR_IO, Sock::RECV, &occurred); + + if ((occurred & Sock::RECV) == 0) { + // Timeout, no incoming connections within MAX_WAIT_FOR_IO. + continue; + } + + const std::string& peer_dest = + conn.sock.RecvUntilTerminator('\n', MAX_WAIT_FOR_IO, *m_interrupt); + + conn.peer = CService(DestB64ToAddr(peer_dest), Params().GetDefaultPort()); + + return true; + } + } catch (const std::runtime_error& e) { + Log("Error accepting: %s", e.what()); + CheckControlSock(); + } + return false; +} + +bool Session::Connect(const CService& to, Connection& conn, bool& proxy_error) +{ + proxy_error = true; + + std::string session_id; + Sock sock; + conn.peer = to; + + try { + { + LOCK(m_mutex); + CreateIfNotCreatedAlready(); + session_id = m_session_id; + conn.me = m_my_addr; + sock = Hello(); + } + + const Reply& lookup_reply = + SendRequestAndGetReply(sock, strprintf("NAMING LOOKUP NAME=%s", to.ToStringIP())); + + const std::string& dest = lookup_reply.Get("VALUE"); + + const Reply& connect_reply = SendRequestAndGetReply( + sock, strprintf("STREAM CONNECT ID=%s DESTINATION=%s SILENT=false", session_id, dest), + false); + + const std::string& result = connect_reply.Get("RESULT"); + + if (result == "OK") { + conn.sock = std::move(sock); + return true; + } + + if (result == "INVALID_ID") { + LOCK(m_mutex); + Disconnect(); + throw std::runtime_error("Invalid session id"); + } + + if (result == "CANT_REACH_PEER" || result == "TIMEOUT") { + proxy_error = false; + } + + throw std::runtime_error(strprintf("\"%s\"", connect_reply.full)); + } catch (const std::runtime_error& e) { + Log("Error connecting to %s: %s", to.ToString(), e.what()); + CheckControlSock(); + return false; + } +} + +// Private methods + +std::string Session::Reply::Get(const std::string& key) const +{ + const auto& pos = keys.find(key); + if (pos == keys.end() || !pos->second.has_value()) { + throw std::runtime_error( + strprintf("Missing %s= in the reply to \"%s\": \"%s\"", key, request, full)); + } + return pos->second.value(); +} + +template +void Session::Log(const std::string& fmt, const Args&... args) const +{ + LogPrint(BCLog::I2P, "I2P: %s\n", tfm::format(fmt, args...)); +} + +Session::Reply Session::SendRequestAndGetReply(const Sock& sock, + const std::string& request, + bool check_result_ok) const +{ + sock.SendComplete(request + "\n", MAX_WAIT_FOR_IO, *m_interrupt); + + Reply reply; + + // Don't log the full "SESSION CREATE ..." because it contains our private key. + reply.request = request.substr(0, 14) == "SESSION CREATE" ? "SESSION CREATE ..." : request; + + // It could take a few minutes for the I2P router to reply as it is querying the I2P network + // (when doing name lookup, for example). Notice: `RecvUntilTerminator()` is checking + // `m_interrupt` more often, so we would not be stuck here for long if `m_interrupt` is + // signaled. + static constexpr auto recv_timeout = 3min; + + reply.full = sock.RecvUntilTerminator('\n', recv_timeout, *m_interrupt); + + for (const auto& kv : spanparsing::Split(reply.full, ' ')) { + const auto& pos = std::find(kv.begin(), kv.end(), '='); + if (pos != kv.end()) { + reply.keys.emplace(std::string{kv.begin(), pos}, std::string{pos + 1, kv.end()}); + } else { + reply.keys.emplace(std::string{kv.begin(), kv.end()}, std::nullopt); + } + } + + if (check_result_ok && reply.Get("RESULT") != "OK") { + throw std::runtime_error( + strprintf("Unexpected reply to \"%s\": \"%s\"", request, reply.full)); + } + + return reply; +} + +Sock Session::Hello() const +{ + auto sock = CreateSock(m_control_host); + + if (!sock) { + throw std::runtime_error("Cannot create socket"); + } + + if (!ConnectSocketDirectly(m_control_host, sock->Get(), nConnectTimeout, true)) { + throw std::runtime_error(strprintf("Cannot connect to %s", m_control_host.ToString())); + } + + SendRequestAndGetReply(*sock, "HELLO VERSION MIN=3.1 MAX=3.1"); + + return std::move(*sock); +} + +void Session::CheckControlSock() +{ + LOCK(m_mutex); + + std::string errmsg; + if (!m_control_sock.IsConnected(errmsg)) { + Log("Control socket error: %s", errmsg); + Disconnect(); + } +} + +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. + const Reply& reply = SendRequestAndGetReply(sock, "DEST GENERATE SIGNATURE_TYPE=7", false); + + m_private_key = DecodeI2PBase64(reply.Get("PRIV")); +} + +void Session::GenerateAndSavePrivateKey(const Sock& sock) +{ + DestGenerate(sock); + + // umask is set to 077 in init.cpp, which is ok (unless -sysperms is given) + if (!WriteBinaryFile(m_private_key_file, + std::string(m_private_key.begin(), m_private_key.end()))) { + throw std::runtime_error( + strprintf("Cannot save I2P private key to %s", m_private_key_file)); + } +} + +Binary Session::MyDestination() const +{ + // From https://geti2p.net/spec/common-structures#destination: + // "They are 387 bytes plus the certificate length specified at bytes 385-386, which may be + // non-zero" + static constexpr size_t DEST_LEN_BASE = 387; + static constexpr size_t CERT_LEN_POS = 385; + + uint16_t cert_len; + memcpy(&cert_len, &m_private_key.at(CERT_LEN_POS), sizeof(cert_len)); + cert_len = be16toh(cert_len); + + const size_t dest_len = DEST_LEN_BASE + cert_len; + + return Binary{m_private_key.begin(), m_private_key.begin() + dest_len}; +} + +void Session::CreateIfNotCreatedAlready() +{ + std::string errmsg; + if (m_control_sock.IsConnected(errmsg)) { + return; + } + + Log("Creating SAM session with %s", m_control_host.ToString()); + + Sock sock = Hello(); + + 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)); + + SendRequestAndGetReply(sock, strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s", + session_id, private_key_b64)); + + m_my_addr = CService(DestBinToAddr(MyDestination()), Params().GetDefaultPort()); + 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()); +} + +Sock Session::StreamAccept() +{ + Sock sock = Hello(); + + const Reply& reply = SendRequestAndGetReply( + sock, strprintf("STREAM ACCEPT ID=%s SILENT=false", m_session_id), false); + + const std::string& result = reply.Get("RESULT"); + + if (result == "OK") { + return sock; + } + + if (result == "INVALID_ID") { + // If our session id is invalid, then force session re-creation on next usage. + Disconnect(); + } + + throw std::runtime_error(strprintf("\"%s\"", reply.full)); +} + +void Session::Disconnect() +{ + if (m_control_sock.Get() != INVALID_SOCKET) { + if (m_session_id.empty()) { + Log("Destroying incomplete session"); + } else { + Log("Destroying session %s", m_session_id); + } + } + m_control_sock.Reset(); + m_session_id.clear(); +} +} // namespace sam +} // namespace i2p diff --git a/src/i2p.h b/src/i2p.h new file mode 100644 index 00000000000..8fafe0a4d01 --- /dev/null +++ b/src/i2p.h @@ -0,0 +1,260 @@ +// Copyright (c) 2020-2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_I2P_H +#define BITCOIN_I2P_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace i2p { + +/** + * Binary data. + */ +using Binary = std::vector; + +/** + * An established connection with another peer. + */ +struct Connection { + /** Connected socket. */ + Sock sock; + + /** Our I2P address. */ + CService me; + + /** The peer's I2P address. */ + CService peer; +}; + +namespace sam { + +/** + * I2P SAM session. + */ +class Session +{ +public: + /** + * Construct a session. This will not initiate any IO, the session will be lazily created + * later when first used. + * @param[in] private_key_file Path to a private key file. If the file does not exist then the + * private key will be generated and saved into the file. + * @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 fs::path& private_key_file, + 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 + * the SAM proxy because the session is destroyed. So they will return an error next time + * we try to read or write to them. + */ + ~Session(); + + /** + * Start listening for an incoming connection. + * @param[out] conn Upon successful completion the `sock` and `me` members will be set + * to the listening socket and address. + * @return true on success + */ + bool Listen(Connection& conn); + + /** + * Wait for and accept a new incoming connection. + * @param[in,out] conn The `sock` member is used for waiting and accepting. Upon successful + * completion the `peer` member will be set to the address of the incoming peer. + * @return true on success + */ + bool Accept(Connection& conn); + + /** + * Connect to an I2P peer. + * @param[in] to Peer to connect to. + * @param[out] conn Established connection. Only set if `true` is returned. + * @param[out] proxy_error If an error occurs due to proxy or general network failure, then + * this is set to `true`. If an error occurs due to unreachable peer (likely peer is down), then + * it is set to `false`. Only set if `false` is returned. + * @return true on success + */ + bool Connect(const CService& to, Connection& conn, bool& proxy_error); + +private: + /** + * A reply from the SAM proxy. + */ + struct Reply { + /** + * Full, unparsed reply. + */ + std::string full; + + /** + * Request, used for detailed error reporting. + */ + std::string request; + + /** + * A map of keywords from the parsed reply. + * For example, if the reply is "A=X B C=YZ", then the map will be + * keys["A"] == "X" + * keys["B"] == (empty std::optional) + * keys["C"] == "YZ" + */ + std::unordered_map> keys; + + /** + * Get the value of a given key. + * For example if the reply is "A=X B" then: + * Value("A") -> "X" + * Value("B") -> throws + * Value("C") -> throws + * @param[in] key Key whose value to retrieve + * @returns the key's value + * @throws std::runtime_error if the key is not present or if it has no value + */ + std::string Get(const std::string& key) const; + }; + + /** + * Log a message in the `BCLog::I2P` category. + * @param[in] fmt printf(3)-like format string. + * @param[in] args printf(3)-like arguments that correspond to `fmt`. + */ + template + void Log(const std::string& fmt, const Args&... args) const; + + /** + * Send request and get a reply from the SAM proxy. + * @param[in] sock A socket that is connected to the SAM proxy. + * @param[in] request Raw request to send, a newline terminator is appended to it. + * @param[in] check_result_ok If true then after receiving the reply a check is made + * whether it contains "RESULT=OK" and an exception is thrown if it does not. + * @throws std::runtime_error if an error occurs + */ + Reply SendRequestAndGetReply(const Sock& sock, + const std::string& request, + bool check_result_ok = true) const; + + /** + * Open a new connection to the SAM proxy. + * @return a connected socket + * @throws std::runtime_error if an error occurs + */ + Sock Hello() const EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * Check the control socket for errors and possibly disconnect. + */ + void CheckControlSock(); + + /** + * Generate a new destination with the SAM proxy and set `m_private_key` to it. + * @param[in] sock Socket to use for talking to the SAM proxy. + * @throws std::runtime_error if an error occurs + */ + void DestGenerate(const Sock& sock) EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * Generate a new destination with the SAM proxy, set `m_private_key` to it and save + * it on disk to `m_private_key_file`. + * @param[in] sock Socket to use for talking to the SAM proxy. + * @throws std::runtime_error if an error occurs + */ + void GenerateAndSavePrivateKey(const Sock& sock) EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * Derive own destination from `m_private_key`. + * @see https://geti2p.net/spec/common-structures#destination + * @return an I2P destination + */ + Binary MyDestination() const EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * Create the session if not already created. Reads the private key file and connects to the + * SAM proxy. + * @throws std::runtime_error if an error occurs + */ + void CreateIfNotCreatedAlready() EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * Open a new connection to the SAM proxy and issue "STREAM ACCEPT" request using the existing + * session id. Return the idle socket that is waiting for a peer to connect to us. + * @throws std::runtime_error if an error occurs + */ + Sock StreamAccept() EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * Destroy the session, closing the internally used sockets. + */ + void Disconnect() EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + + /** + * The name of the file where this peer's private key is stored (in binary). + */ + const fs::path m_private_key_file; + + /** + * The host and port of the SAM control service. + */ + const CService m_control_host; + + /** + * Cease network activity when this is signaled. + */ + CThreadInterrupt* const m_interrupt; + + /** + * Mutex protecting the members that can be concurrently accessed. + */ + mutable Mutex m_mutex; + + /** + * The private key of this peer. + * @see The reply to the "DEST GENERATE" command in https://geti2p.net/en/docs/api/samv3 + */ + Binary m_private_key GUARDED_BY(m_mutex); + + /** + * SAM control socket. + * Used to connect to the I2P SAM service and create a session + * ("SESSION CREATE"). With the established session id we later open + * other connections to the SAM service to accept incoming I2P + * connections and make outgoing ones. + * See https://geti2p.net/en/docs/api/samv3 + */ + Sock m_control_sock GUARDED_BY(m_mutex); + + /** + * Our .b32.i2p address. + * Derived from `m_private_key`. + */ + CService m_my_addr GUARDED_BY(m_mutex); + + /** + * SAM session id. + */ + std::string m_session_id GUARDED_BY(m_mutex); +}; + +} // namespace sam +} // namespace i2p + +#endif /* BITCOIN_I2P_H */ diff --git a/src/logging.cpp b/src/logging.cpp index e82f2c28104..866213786e1 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -156,6 +156,7 @@ const CLogCategoryDesc LogCategories[] = {BCLog::QT, "qt"}, {BCLog::LEVELDB, "leveldb"}, {BCLog::VALIDATION, "validation"}, + {BCLog::I2P, "i2p"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, }; diff --git a/src/logging.h b/src/logging.h index 4ece8f5e3aa..436f0cd12e5 100644 --- a/src/logging.h +++ b/src/logging.h @@ -57,6 +57,7 @@ namespace BCLog { QT = (1 << 19), LEVELDB = (1 << 20), VALIDATION = (1 << 21), + I2P = (1 << 22), ALL = ~(uint32_t)0, }; From 76c35c60f338937071bcfad4211ef7254d3830ec Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 4 Dec 2020 18:03:05 +0100 Subject: [PATCH 15/20] init: introduce I2P connectivity options Introduce two new options to reach the I2P network: * `-i2psam=` point to the I2P SAM proxy. If this is set then the I2P network is considered reachable and we can make outgoing connections to I2P peers via that proxy. We listen for and accept incoming connections from I2P peers if the below is set in addition to `-i2psam=` * `-i2pacceptincoming` if this is set together with `-i2psam=` then we accept incoming I2P connections via the I2P SAM proxy. --- doc/files.md | 1 + src/init.cpp | 20 ++++++++++++++++ src/net.cpp | 6 +++++ src/net.h | 14 +++++++++++ src/rpc/net.cpp | 2 +- test/functional/feature_proxy.py | 41 ++++++++++++++++++++++++++------ 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/doc/files.md b/doc/files.md index 545e8fc92c8..a80adf30ea9 100644 --- a/doc/files.md +++ b/doc/files.md @@ -64,6 +64,7 @@ Subdirectory | File(s) | Description `./` | `ip_asn.map` | IP addresses to Autonomous System Numbers (ASNs) mapping used for bucketing of the peers; path can be specified with the `-asmap` option `./` | `mempool.dat` | Dump of the mempool's transactions `./` | `onion_v3_private_key` | Cached Tor onion service private key for `-listenonion` option +`./` | `i2p_private_key` | Private key that corresponds to our I2P address. When `-i2psam=` is specified the contents of this file is used to identify ourselves for making outgoing connections to I2P peers and possibly accepting incoming ones. Automatically generated if it does not exist. `./` | `peers.dat` | Peer IP address database (custom format) `./` | `settings.json` | Read-write settings set through GUI or RPC interfaces, augmenting manual settings from [bitcoin.conf](bitcoin-conf.md). File is created automatically if read-write settings storage is not disabled with `-nosettings` option. Path can be specified with `-settings` option `./` | `.cookie` | Session RPC authentication cookie; if used, created at start and deleted on shutdown; can be specified by `-rpccookiefile` option diff --git a/src/init.cpp b/src/init.cpp index befba2eb2d9..5adebe3b1ba 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -447,6 +447,8 @@ void SetupServerArgs(NodeContext& node) argsman.AddArg("-maxtimeadjustment", strprintf("Maximum allowed median peer time offset adjustment. Local perspective of time may be influenced by peers forward or backward by this amount. (default: %u seconds)", DEFAULT_MAX_TIME_ADJUSTMENT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); 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("-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 ipv4 or ipv6 but not onion 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("-peerbloomfilters", strprintf("Support filtering of blocks and transaction with bloom filters (default: %u)", DEFAULT_PEERBLOOMFILTERS), 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); @@ -847,6 +849,9 @@ void InitParameterInteraction(ArgsManager& args) LogPrintf("%s: parameter interaction: -listen=0 -> setting -discover=0\n", __func__); if (args.SoftSetBoolArg("-listenonion", false)) LogPrintf("%s: parameter interaction: -listen=0 -> setting -listenonion=0\n", __func__); + if (args.SoftSetBoolArg("-i2pacceptincoming", false)) { + LogPrintf("%s: parameter interaction: -listen=0 -> setting -i2pacceptincoming=0\n", __func__); + } } if (args.IsArgSet("-externalip")) { @@ -1990,6 +1995,21 @@ bool AppInitMain(const util::Ref& context, NodeContext& node, interfaces::BlockA connOptions.m_specified_outgoing = connect; } } + + const std::string& i2psam_arg = args.GetArg("-i2psam", ""); + if (!i2psam_arg.empty()) { + CService addr; + if (!Lookup(i2psam_arg, addr, 7656, fNameLookup) || !addr.IsValid()) { + return InitError(strprintf(_("Invalid -i2psam address or hostname: '%s'"), i2psam_arg)); + } + SetReachable(NET_I2P, true); + SetProxy(NET_I2P, proxyType{addr}); + } else { + SetReachable(NET_I2P, false); + } + + connOptions.m_i2p_accept_incoming = args.GetBoolArg("-i2pacceptincoming", true); + if (!node.connman->Start(*node.scheduler, connOptions)) { return false; } diff --git a/src/net.cpp b/src/net.cpp index 89f7ef0f064..42505a1139c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2374,6 +2374,12 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) return false; } + proxyType i2p_sam; + if (GetProxy(NET_I2P, i2p_sam)) { + m_i2p_sam_session = std::make_unique(GetDataDir() / "i2p_private_key", + i2p_sam.proxy, &interruptNet); + } + for (const auto& strDest : connOptions.vSeedNodes) { AddAddrFetch(strDest); } diff --git a/src/net.h b/src/net.h index be38441008b..ab3111b0a2b 100644 --- a/src/net.h +++ b/src/net.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -831,6 +832,7 @@ public: std::vector m_specified_outgoing; std::vector m_added_nodes; std::vector m_asmap; + bool m_i2p_accept_incoming; }; void Init(const Options& connOptions) { @@ -1221,8 +1223,20 @@ private: Mutex mutexMsgProc; std::atomic flagInterruptMsgProc{false}; + /** + * This is signaled when network activity should cease. + * A pointer to it is saved in `m_i2p_sam_session`, so make sure that + * the lifetime of `interruptNet` is not shorter than + * the lifetime of `m_i2p_sam_session`. + */ CThreadInterrupt interruptNet; + /** + * I2P SAM session. + * Used to accept incoming and make outgoing I2P connections. + */ + std::unique_ptr m_i2p_sam_session; + std::thread threadDNSAddressSeed; std::thread threadSocketHandler; std::thread threadOpenAddedConnections; diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 0224ee697aa..e80270d0388 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -546,7 +546,7 @@ static UniValue GetNetworksInfo() UniValue networks(UniValue::VARR); for (int n = 0; n < NET_MAX; ++n) { enum Network network = static_cast(n); - if (network == NET_UNROUTABLE || network == NET_I2P || network == NET_CJDNS || network == NET_INTERNAL) continue; + if (network == NET_UNROUTABLE || network == NET_CJDNS || network == NET_INTERNAL) continue; proxyType proxy; UniValue obj(UniValue::VOBJ); GetProxy(network, proxy); diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 2983feaa0d6..8bee43b8ada 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -49,9 +49,10 @@ NET_UNROUTABLE = "not_publicly_routable" NET_IPV4 = "ipv4" NET_IPV6 = "ipv6" NET_ONION = "onion" +NET_I2P = "i2p" # Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() -NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION}) +NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P}) class ProxyTest(BitcoinTestFramework): @@ -90,11 +91,15 @@ class ProxyTest(BitcoinTestFramework): self.serv3 = Socks5Server(self.conf3) self.serv3.start() + # We will not try to connect to this. + self.i2p_sam = ('127.0.0.1', 7656) + # 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),'-proxyrandomize=0'], + ['-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'], [] ] @@ -199,9 +204,16 @@ class ProxyTest(BitcoinTestFramework): n0 = networks_dict(self.nodes[0].getnetworkinfo()) assert_equal(NETWORKS, n0.keys()) for net in NETWORKS: - assert_equal(n0[net]['proxy'], '%s:%i' % (self.conf1.addr)) - assert_equal(n0[net]['proxy_randomize_credentials'], True) + if net == NET_I2P: + expected_proxy = '' + expected_randomize = False + else: + expected_proxy = '%s:%i' % (self.conf1.addr) + expected_randomize = True + assert_equal(n0[net]['proxy'], expected_proxy) + assert_equal(n0[net]['proxy_randomize_credentials'], expected_randomize) assert_equal(n0['onion']['reachable'], True) + assert_equal(n0['i2p']['reachable'], False) n1 = networks_dict(self.nodes[1].getnetworkinfo()) assert_equal(NETWORKS, n1.keys()) @@ -211,21 +223,36 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n1['onion']['proxy'], '%s:%i' % (self.conf2.addr)) 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_randomize_credentials'], False) + assert_equal(n1['i2p']['reachable'], True) n2 = networks_dict(self.nodes[2].getnetworkinfo()) assert_equal(NETWORKS, n2.keys()) for net in NETWORKS: - assert_equal(n2[net]['proxy'], '%s:%i' % (self.conf2.addr)) - assert_equal(n2[net]['proxy_randomize_credentials'], True) + if net == NET_I2P: + expected_proxy = '' + expected_randomize = False + else: + expected_proxy = '%s:%i' % (self.conf2.addr) + expected_randomize = True + assert_equal(n2[net]['proxy'], expected_proxy) + assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) assert_equal(n2['onion']['reachable'], True) + assert_equal(n2['i2p']['reachable'], False) if self.have_ipv6: n3 = networks_dict(self.nodes[3].getnetworkinfo()) assert_equal(NETWORKS, n3.keys()) for net in NETWORKS: - assert_equal(n3[net]['proxy'], '[%s]:%i' % (self.conf3.addr)) + if net == NET_I2P: + expected_proxy = '' + else: + expected_proxy = '[%s]:%i' % (self.conf3.addr) + assert_equal(n3[net]['proxy'], expected_proxy) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) + assert_equal(n3['i2p']['reachable'], False) if __name__ == '__main__': From 9559bd1404fbf74b0d09fe9019a9305cb4e151ce Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 9 Dec 2020 05:24:09 +0100 Subject: [PATCH 16/20] net: add I2P to the reachability map Update `CNetAddr::GetReachabilityFrom()` to recognize the I2P network so that we would prefer to advertise our I2P address to I2P peers. --- src/netaddress.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/netaddress.cpp b/src/netaddress.cpp index 80313b88cea..69edc15c66c 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -878,6 +878,11 @@ int CNetAddr::GetReachabilityFrom(const CNetAddr *paddrPartner) const case NET_IPV4: return REACH_IPV4; // Tor users can connect to IPv4 as well case NET_ONION: return REACH_PRIVATE; } + case NET_I2P: + switch (ourNet) { + case NET_I2P: return REACH_PRIVATE; + default: return REACH_DEFAULT; + } case NET_TEREDO: switch(ourNet) { default: return REACH_DEFAULT; From 0635233a1e7e8c303073430092afd3e0fb0d927b Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Wed, 25 Nov 2020 14:42:33 +0100 Subject: [PATCH 17/20] net: make outgoing I2P connections from CConnman --- src/net.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 42505a1139c..b4f0a41ffbf 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -421,10 +421,20 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo bool connected = false; std::unique_ptr sock; proxyType proxy; + CAddress addr_bind; + assert(!addr_bind.IsValid()); + if (addrConnect.IsValid()) { bool proxyConnectionFailed = false; - if (GetProxy(addrConnect.GetNetwork(), proxy)) { + if (addrConnect.GetNetwork() == NET_I2P && m_i2p_sam_session.get() != nullptr) { + i2p::Connection conn; + if (m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed)) { + connected = true; + sock = std::make_unique(std::move(conn.sock)); + addr_bind = CAddress{conn.me, NODE_NONE}; + } + } else if (GetProxy(addrConnect.GetNetwork(), proxy)) { sock = CreateSock(proxy.proxy); if (!sock) { return nullptr; @@ -464,7 +474,9 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo // Add node NodeId id = GetNewNodeId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); - CAddress addr_bind = GetBindAddress(sock->Get()); + 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); pnode->AddRef(); From b905363fa8b0bb03fe34b53b5410880f42e0af39 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 24 Nov 2020 11:28:52 +0100 Subject: [PATCH 18/20] net: accept incoming I2P connections from CConnman --- src/net.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/net.h | 2 ++ 2 files changed, 51 insertions(+) diff --git a/src/net.cpp b/src/net.cpp index b4f0a41ffbf..6dcff7e32e0 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -2187,6 +2188,45 @@ void CConnman::ThreadMessageHandler() } } +void CConnman::ThreadI2PAcceptIncoming() +{ + static constexpr auto err_wait_begin = 1s; + static constexpr auto err_wait_cap = 5min; + auto err_wait = err_wait_begin; + + bool advertising_listen_addr = false; + i2p::Connection conn; + + while (!interruptNet) { + + if (!m_i2p_sam_session->Listen(conn)) { + if (advertising_listen_addr && conn.me.IsValid()) { + RemoveLocal(conn.me); + advertising_listen_addr = false; + } + + interruptNet.sleep_for(err_wait); + if (err_wait < err_wait_cap) { + err_wait *= 2; + } + + continue; + } + + if (!advertising_listen_addr) { + AddLocal(conn.me, LOCAL_BIND); + advertising_listen_addr = true; + } + + if (!m_i2p_sam_session->Accept(conn)) { + continue; + } + + CreateNodeFromAcceptedSocket(conn.sock.Release(), NetPermissionFlags::PF_NONE, + CAddress{conn.me, NODE_NONE}, CAddress{conn.peer, NODE_NONE}); + } +} + bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, NetPermissionFlags permissions) { int nOne = 1; @@ -2472,6 +2512,12 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) // Process messages threadMessageHandler = std::thread(&TraceThread >, "msghand", std::function(std::bind(&CConnman::ThreadMessageHandler, this))); + if (connOptions.m_i2p_accept_incoming && m_i2p_sam_session.get() != nullptr) { + threadI2PAcceptIncoming = + std::thread(&TraceThread>, "i2paccept", + std::function(std::bind(&CConnman::ThreadI2PAcceptIncoming, this))); + } + // Dump network addresses scheduler.scheduleEvery([this] { DumpAddresses(); }, DUMP_PEERS_INTERVAL); @@ -2519,6 +2565,9 @@ void CConnman::Interrupt() void CConnman::StopThreads() { + if (threadI2PAcceptIncoming.joinable()) { + threadI2PAcceptIncoming.join(); + } if (threadMessageHandler.joinable()) threadMessageHandler.join(); if (threadOpenConnections.joinable()) diff --git a/src/net.h b/src/net.h index ab3111b0a2b..fbdaf75150f 100644 --- a/src/net.h +++ b/src/net.h @@ -1050,6 +1050,7 @@ private: void ProcessAddrFetch(); void ThreadOpenConnections(std::vector connect); void ThreadMessageHandler(); + void ThreadI2PAcceptIncoming(); void AcceptConnection(const ListenSocket& hListenSocket); /** @@ -1242,6 +1243,7 @@ private: std::thread threadOpenAddedConnections; std::thread threadOpenConnections; std::thread threadMessageHandler; + std::thread threadI2PAcceptIncoming; /** flag for deciding to connect to an extra outbound peer, * in excess of m_max_outbound_full_relay From 0181e244394bd9e68e9f0d44704e7b0fd12a6b1f Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 7 Dec 2020 17:24:09 +0100 Subject: [PATCH 19/20] net: recognize I2P from ParseNetwork() so that -onlynet=i2p works --- src/init.cpp | 2 +- src/netbase.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/init.cpp b/src/init.cpp index 5adebe3b1ba..f40cf7975ba 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -449,7 +449,7 @@ void SetupServerArgs(NodeContext& node) 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("-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 ipv4 or ipv6 but not onion 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("-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("-peerbloomfilters", strprintf("Support filtering of blocks and transaction with bloom filters (default: %u)", DEFAULT_PEERBLOOMFILTERS), 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("-permitbaremultisig", strprintf("Relay non-P2SH multisig (default: %u)", DEFAULT_PERMIT_BAREMULTISIG), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); diff --git a/src/netbase.cpp b/src/netbase.cpp index 7ccd24a7789..b09b21c2a16 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -51,6 +51,9 @@ enum Network ParseNetwork(const std::string& net_in) { LogPrintf("Warning: net name 'tor' is deprecated and will be removed in the future. You should use 'onion' instead.\n"); return NET_ONION; } + if (net == "i2p") { + return NET_I2P; + } return NET_UNROUTABLE; } From a701fcf01f3ea9a12e869bfa52321302cf68351c Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 15 Feb 2021 16:55:24 +0100 Subject: [PATCH 20/20] net: Do not skip the I2P network from GetNetworkNames() So that help texts include "i2p" in: * `./bitcoind -help` (in `-onlynet` description) * `getpeerinfo` RPC * `getnetworkinfo` RPC Co-authored-by: Jon Atack --- src/netbase.cpp | 2 +- test/functional/rpc_net.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/netbase.cpp b/src/netbase.cpp index b09b21c2a16..7dc616080d1 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -78,7 +78,7 @@ std::vector GetNetworkNames(bool append_unroutable) std::vector names; for (int n = 0; n < NET_MAX; ++n) { const enum Network network{static_cast(n)}; - if (network == NET_UNROUTABLE || network == NET_I2P || network == NET_CJDNS || network == NET_INTERNAL) continue; + if (network == NET_UNROUTABLE || network == NET_CJDNS || network == NET_INTERNAL) continue; names.emplace_back(GetNetworkName(network)); } if (append_unroutable) { diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index 2d41963beb8..9adb32c3c55 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -105,7 +105,7 @@ class NetTest(BitcoinTestFramework): assert_equal(peer_info[1][1]['connection_type'], 'inbound') # Check dynamically generated networks list in getpeerinfo help output. - assert "(ipv4, ipv6, onion, not_publicly_routable)" in self.nodes[0].help("getpeerinfo") + assert "(ipv4, ipv6, onion, i2p, not_publicly_routable)" in self.nodes[0].help("getpeerinfo") def test_getnettotals(self): self.log.info("Test getnettotals") @@ -156,7 +156,7 @@ class NetTest(BitcoinTestFramework): assert_net_servicesnames(int(info["localservices"], 0x10), info["localservicesnames"]) # Check dynamically generated networks list in getnetworkinfo help output. - assert "(ipv4, ipv6, onion)" in self.nodes[0].help("getnetworkinfo") + assert "(ipv4, ipv6, onion, i2p)" in self.nodes[0].help("getnetworkinfo") def test_getaddednodeinfo(self): self.log.info("Test getaddednodeinfo")