diff --git a/docs/stellar-core_example.cfg b/docs/stellar-core_example.cfg index 05a77efc3a..a31699650e 100644 --- a/docs/stellar-core_example.cfg +++ b/docs/stellar-core_example.cfg @@ -120,6 +120,10 @@ NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" # The port other instances of stellar-core can connect to you on. PEER_PORT=11625 +# PEER_LISTEN_IP (string) default "" (bind to 0.0.0.0) +# The IP address other instances of stellar-core can connect to you on. +PEER_LISTEN_IP="" + # TARGET_PEER_CONNECTIONS (Integer) default 8 # This controls how aggressively the server will connect to other peers. # It will send outbound connection attempts until it is at this diff --git a/src/main/Config.cpp b/src/main/Config.cpp index 2c7a44d2a2..a75367c86d 100644 --- a/src/main/Config.cpp +++ b/src/main/Config.cpp @@ -1094,6 +1094,8 @@ Config::processConfig(std::shared_ptr t) #endif {"PEER_PORT", [&]() { PEER_PORT = readInt(item, 1); }}, + {"PEER_LISTEN_IP", + [&]() { PEER_LISTEN_IP = readString(item); }}, {"HTTP_PORT", [&]() { HTTP_PORT = readInt(item); }}, {"HTTP_QUERY_PORT", diff --git a/src/main/Config.h b/src/main/Config.h index c583d63477..5029d37105 100644 --- a/src/main/Config.h +++ b/src/main/Config.h @@ -695,6 +695,7 @@ class Config : public std::enable_shared_from_this // overlay config unsigned short PEER_PORT; + std::string PEER_LISTEN_IP; // IP address to listen on (empty = 0.0.0.0) unsigned short TARGET_PEER_CONNECTIONS; unsigned short MAX_PENDING_CONNECTIONS; int MAX_ADDITIONAL_PEER_CONNECTIONS; diff --git a/src/overlay/OverlayManager.h b/src/overlay/OverlayManager.h index bd2f453799..b8aac80a55 100644 --- a/src/overlay/OverlayManager.h +++ b/src/overlay/OverlayManager.h @@ -140,8 +140,8 @@ class OverlayManager virtual bool acceptAuthenticatedPeer(Peer::pointer peer) = 0; virtual bool isPreferred(Peer* peer) const = 0; - virtual bool isPossiblyPreferred(std::string const& ip) const = 0; - virtual bool haveSpaceForConnection(std::string const& ip) const = 0; + virtual bool isPossiblyPreferred(asio::ip::address const& ip) const = 0; + virtual bool haveSpaceForConnection(asio::ip::address const& ip) const = 0; // Return the current in-memory set of inbound pending peers. virtual std::vector const& diff --git a/src/overlay/OverlayManagerImpl.cpp b/src/overlay/OverlayManagerImpl.cpp index 3cf558029e..461b27ad7b 100644 --- a/src/overlay/OverlayManagerImpl.cpp +++ b/src/overlay/OverlayManagerImpl.cpp @@ -906,7 +906,7 @@ OverlayManagerImpl::maybeAddInboundConnection(Peer::pointer peer) } bool -OverlayManagerImpl::isPossiblyPreferred(std::string const& ip) const +OverlayManagerImpl::isPossiblyPreferred(asio::ip::address const& ip) const { return std::any_of( std::begin(mConfigurationPreferredPeers), @@ -915,7 +915,7 @@ OverlayManagerImpl::isPossiblyPreferred(std::string const& ip) const } bool -OverlayManagerImpl::haveSpaceForConnection(std::string const& ip) const +OverlayManagerImpl::haveSpaceForConnection(asio::ip::address const& ip) const { auto totalAuthenticated = getInboundAuthenticatedPeers().size(); auto totalTracked = *getLiveInboundPeersCounter(); @@ -947,7 +947,7 @@ OverlayManagerImpl::haveSpaceForConnection(std::string const& ip) const CLOG_DEBUG( Overlay, "Peer rejected - all pending inbound connections are taken: {}", - ip); + ip.to_string()); CLOG_DEBUG(Overlay, "If you wish to allow for more pending " "inbound connections, please update your " "MAX_PENDING_CONNECTIONS setting in " diff --git a/src/overlay/OverlayManagerImpl.h b/src/overlay/OverlayManagerImpl.h index 3a62ddd45e..e45866b2a5 100644 --- a/src/overlay/OverlayManagerImpl.h +++ b/src/overlay/OverlayManagerImpl.h @@ -206,8 +206,10 @@ class OverlayManagerImpl : public OverlayManager int availableOutboundAuthenticatedSlots() const; int nonPreferredAuthenticatedCount() const; - virtual bool isPossiblyPreferred(std::string const& ip) const override; - virtual bool haveSpaceForConnection(std::string const& ip) const override; + virtual bool + isPossiblyPreferred(asio::ip::address const& ip) const override; + virtual bool + haveSpaceForConnection(asio::ip::address const& ip) const override; void updateSizeCounters(); diff --git a/src/overlay/Peer.cpp b/src/overlay/Peer.cpp index e6d16ee3c3..6e225c917e 100644 --- a/src/overlay/Peer.cpp +++ b/src/overlay/Peer.cpp @@ -131,7 +131,7 @@ static constexpr VirtualClock::time_point PING_NOT_SENT = VirtualClock::time_point::min(); static constexpr uint32_t QUERY_RESPONSE_MULTIPLIER = 5; -Peer::Peer(Application& app, PeerRole role) +Peer::Peer(Application& app, PeerRole role, PeerBareAddress const& address) : mAppConnector(app.getAppConnector()) , mNetworkID(app.getNetworkID()) , mFlowControl( @@ -145,6 +145,7 @@ Peer::Peer(Application& app, PeerRole role) , mState(role == WE_CALLED_REMOTE ? CONNECTING : CONNECTED) , mRemoteOverlayMinVersion(0) , mRemoteOverlayVersion(0) + , mAddress(address) , mCreationTime(app.getClock().now()) , mRecurringTimer(app) , mDelayedExecutionTimer(app) @@ -1719,7 +1720,6 @@ void Peer::updatePeerRecordAfterEcho() { releaseAssert(threadIsMain()); - releaseAssert(!getAddress().isEmpty()); PeerType type; if (mAppConnector.getOverlayManager().isPreferred(this)) @@ -1745,7 +1745,6 @@ void Peer::updatePeerRecordAfterAuthentication() { releaseAssert(threadIsMain()); - releaseAssert(!getAddress().isEmpty()); if (mRole == WE_CALLED_REMOTE) { @@ -1801,12 +1800,6 @@ Peer::recvHello(Hello const& elo) // mAddress is set in TCPPeer::initiate and TCPPeer::accept. It should // contain valid IP (but not necessarily port yet) auto ip = mAddress.getIP(); - if (ip.empty()) - { - drop("failed to determine remote address", - Peer::DropDirection::WE_DROPPED_REMOTE); - return; - } mAddress = PeerBareAddress{ip, static_cast(elo.listeningPort)}; @@ -1853,7 +1846,7 @@ Peer::recvHello(Hello const& elo) return; } - if (elo.listeningPort <= 0 || elo.listeningPort > UINT16_MAX || ip.empty()) + if (elo.listeningPort <= 0 || elo.listeningPort > UINT16_MAX) { sendErrorAndDrop(ERR_CONF, "bad address"); return; @@ -1989,14 +1982,7 @@ Peer::recvPeers(StellarMessage const& msg) peer.port); continue; } - if (peer.ip.type() == IPv6) - { - CLOG_DEBUG(Overlay, - "ignoring received IPv6 address (not yet supported)"); - continue; - } - releaseAssert(peer.ip.type() == IPv4); auto address = PeerBareAddress{peer}; if (address.isPrivate()) diff --git a/src/overlay/Peer.h b/src/overlay/Peer.h index fcba076869..51e8208e31 100644 --- a/src/overlay/Peer.h +++ b/src/overlay/Peer.h @@ -349,7 +349,7 @@ class Peer : public std::enable_shared_from_this, public: /* The following functions must all be called from the main thread (they all * contain releaseAssert(threadIsMain())) */ - Peer(Application& app, PeerRole role); + Peer(Application& app, PeerRole role, PeerBareAddress const& address); void cancelTimers(); diff --git a/src/overlay/PeerBareAddress.cpp b/src/overlay/PeerBareAddress.cpp index 674e8856c6..68f3b58dc5 100644 --- a/src/overlay/PeerBareAddress.cpp +++ b/src/overlay/PeerBareAddress.cpp @@ -10,35 +10,33 @@ #include #include -namespace stellar +namespace { - -PeerBareAddress::PeerBareAddress() - : mType{Type::EMPTY}, mPort{0}, mStringValue("(empty)") +std::string +formatAddress(asio::ip::address const& ip, unsigned short port) { + if (ip.is_v6()) + { + return fmt::format(FMT_STRING("[{}]:{:d}"), ip.to_string(), port); + } + return fmt::format(FMT_STRING("{}:{:d}"), ip.to_string(), port); } +} // namespace -PeerBareAddress::PeerBareAddress(std::string ip, unsigned short port) - : mType{Type::IPv4} - , mIP{std::move(ip)} - , mPort{port} - , mStringValue{fmt::format(FMT_STRING("{}:{:d}"), mIP, mPort)} +namespace stellar +{ + +PeerBareAddress::PeerBareAddress(asio::ip::address ip, unsigned short port) + : mIP{std::move(ip)}, mPort{port}, mStringValue{formatAddress(mIP, mPort)} { - if (mIP.empty()) - { - throw std::runtime_error("Cannot create PeerBareAddress with empty ip"); - } } PeerBareAddress::PeerBareAddress(PeerAddress const& pa) - : mType{Type::IPv4} - , mIP{pa.ip.type() == IPv4 - ? fmt::format(FMT_STRING("{:d}.{:d}.{:d}.{:d}"), - (int)pa.ip.ipv4()[0], (int)pa.ip.ipv4()[1], - (int)pa.ip.ipv4()[2], (int)pa.ip.ipv4()[3]) - : throw std::runtime_error("IPv6 addresses not supported")} + : mIP{pa.ip.type() == IPv4 + ? asio::ip::address{asio::ip::address_v4{pa.ip.ipv4()}} + : asio::ip::address_v6(pa.ip.ipv6())} , mPort{static_cast(pa.port)} - , mStringValue{fmt::format(FMT_STRING("{}:{:d}"), mIP, mPort)} + , mStringValue{formatAddress(mIP, mPort)} { } @@ -46,8 +44,9 @@ PeerBareAddress PeerBareAddress::resolve(std::string const& ipPort, Application& app, unsigned short defaultPort) { + // (?:(v4addr)|(v6addr)|(hostname))(?:(:port))? static std::regex re( - "^(?:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|([[:alnum:].-]+))" + R"(^(?:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:\[([[:xdigit:]:]+)\])|([[:alnum:].-]+)))" "(?:\\:(\\d{1,5}))?$"); std::smatch m; @@ -64,11 +63,17 @@ PeerBareAddress::resolve(std::string const& ipPort, Application& app, resolveflags = asio::ip::tcp::resolver::query::flags::numeric_host; toResolve = m[1].str(); } - else + else if (m[2].matched) { - resolveflags = asio::ip::tcp::resolver::query::flags::v4_mapped; + resolveflags = asio::ip::tcp::resolver::query::flags::numeric_host; toResolve = m[2].str(); } + else + { + resolveflags = + asio::ip::tcp::resolver::query::flags::address_configured; + toResolve = m[3].str(); + } asio::ip::tcp::resolver resolver(app.getWorkerIOContext()); asio::ip::tcp::resolver::query query(toResolve, "", resolveflags); @@ -83,28 +88,23 @@ PeerBareAddress::resolve(std::string const& ipPort, Application& app, FMT_STRING("Could not resolve '{}': {}"), ipPort, ec.message())); } - std::string ip; - while (i != asio::ip::tcp::resolver::iterator()) + std::optional ip; + if (i != asio::ip::tcp::resolver::iterator()) { asio::ip::tcp::endpoint end = *i; - if (end.address().is_v4()) - { - ip = end.address().to_v4().to_string(); - break; - } - i++; + ip = end.address(); } - if (ip.empty()) + if (!ip.has_value()) { throw std::runtime_error(fmt::format( - FMT_STRING("Could not resolve '{}': no IPv4 addresses found"), + FMT_STRING("Could not resolve '{}': no IP addresses found"), ipPort)); } unsigned short port = defaultPort; - if (m[3].matched) + if (m[4].matched) { - int parsedPort = atoi(m[3].str().c_str()); + int parsedPort = atoi(m[4].str().c_str()); if (parsedPort <= 0 || parsedPort > UINT16_MAX) { throw std::runtime_error(fmt::format( @@ -114,10 +114,9 @@ PeerBareAddress::resolve(std::string const& ipPort, Application& app, port = static_cast(parsedPort); } - releaseAssert(!ip.empty()); releaseAssert(port != 0); - return PeerBareAddress{ip, port}; + return PeerBareAddress{*ip, port}; } std::string const& @@ -126,29 +125,144 @@ PeerBareAddress::toString() const return mStringValue; } +namespace +{ +template +inline bool +ipv4InSubnet(unsigned long val) +{ + unsigned long constexpr subnet = (((static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(c) << 8) | + (static_cast(d)))); + return (val & ~0UL << (32 - bits)) == subnet; +} + +bool +isIPv4Private(asio::ip::address_v4 const& addr) +{ + // See RFC6890. Note: Table 10 (6to4 Relay Anycast) is global and Table 4 + // (Loopback) is handled in isLocalhost(). + unsigned long val = addr.to_ulong(); + return + // 0.0.0.0/8 Table 1: This host on this network + ipv4InSubnet<0, 0, 0, 0, 8>(val) || + // 10.0.0.0/8 Table 2: Private-Use Networks + ipv4InSubnet<10, 0, 0, 0, 8>(val) || + // 100.64.0.0/10 Table 3: Shared Address Space + ipv4InSubnet<100, 64, 0, 0, 10>(val) || + // 169.254.0.0/16 Table 5: Link Local + ipv4InSubnet<169, 254, 0, 0, 16>(val) || + // 172.16.0.0/12 Table 6: Private-Use Networks + ipv4InSubnet<172, 16, 0, 0, 12>(val) || + // 192.0.0.0/24 Table 7: IETF Protocol Assignments (and 192.0.0.0/29 + // Table 8: DS-Lite) + ipv4InSubnet<192, 0, 0, 0, 24>(val) || + // 192.0.2.0/24 Table 9: TEST-NET-1 + ipv4InSubnet<192, 0, 2, 0, 24>(val) || + // 192.168.0.0/16 Table 11: Private-Use Networks + ipv4InSubnet<192, 168, 0, 0, 16>(val) || + // 198.18.0.0/15 Table 12: Network Interconnect Device Benchmark + // Testing + ipv4InSubnet<198, 18, 0, 0, 15>(val) || + // 198.51.100.0/24 Table 13: TEST-NET-2 + ipv4InSubnet<198, 51, 100, 0, 24>(val) || + // 203.0.113.0/24 Table 14: TEST-NET-3 + ipv4InSubnet<203, 0, 113, 0, 24>(val) || + // 240.0.0.0/4 Table 15: Reserved for Future Use (and 255.255.255.255/32 + // Table 16: Limited Broadcast) + ipv4InSubnet<240, 0, 0, 0, 4>(val); +} + +template +constexpr inline bool +ipv6InSubnetHelper(std::array const& bytes) +{ + if constexpr (bits == 0) + { + return true; + } + else if constexpr (bits >= 8) + { + if (bytes[index] != currentByte) + { + return false; + } + if constexpr (index < 15) + { + if constexpr (sizeof...(octets) > 0) + { + return ipv6InSubnetHelper( + bytes); + } + else + { + return ipv6InSubnetHelper(bytes); + } + } + else + { + return true; + } + } + else + { + uint8_t constexpr mask = static_cast(0xFF << (8 - bits)); + return (bytes[index] & mask) == (currentByte & mask); + } +} + +template +bool +ipv6InSubnet(std::array const& bytes) +{ + return ipv6InSubnetHelper> 8), a & 0xFF, (b >> 8), b & 0xFF>( + bytes); +} +} // namespace + bool PeerBareAddress::isPrivate() const { - asio::error_code ec; - asio::ip::address_v4 addr = asio::ip::address_v4::from_string(mIP, ec); - if (ec) + if (mIP.is_v4()) { - return false; + return isIPv4Private(mIP.to_v4()); } - unsigned long val = addr.to_ulong(); - if (((val >> 24) == 10) // 10.x.y.z - || ((val >> 20) == 2753) // 172.[16-31].x.y - || ((val >> 16) == 49320)) // 192.168.x.y + + auto v6 = mIP.to_v6(); + if (v6.is_v4_mapped() || v6.is_v4_compatible()) { - return true; + return isIPv4Private(v6.to_v4()); } - return false; + + // See RFC6890. Note that it's okay if we return false on addresses that may + // be private. So, since table 19 contains global addresses, table 22 has an + // exception for more specific allocations, and table 27 is listed as N/A, + // ignore them for now. Table 20 is handled by is_v4_mapped above. + auto bytes = v6.to_bytes(); + return + // ::/128 Table 18: Unspecified Address + ipv6InSubnet<0, 0, 128>(bytes) + // 100::/64 Table 21: Discard-Only Prefix + || ipv6InSubnet<0x100, 0, 64>(bytes) + // 2001::/32 Table 23: TEREDO + || ipv6InSubnet<0x2001, 0, 32>(bytes) + // 2001:2::/48 Table 24: Benchmarking + || ipv6InSubnet<0x2001, 0x2, 48>(bytes) + // 2001:db8::/32 Table 25: Documentation + || ipv6InSubnet<0x2001, 0xdb8, 32>(bytes) + // 2001:10::/28 Table 26: ORCHID + || ipv6InSubnet<0x2001, 0x10, 28>(bytes) + // fc00::/7 Table 28: Unique-Local + || ipv6InSubnet<0xfc00, 0, 7>(bytes) + // fe80::/10 Table 29: Linked-Scoped Unicast + || ipv6InSubnet<0xfe80, 0, 10>(bytes); } bool PeerBareAddress::isLocalhost() const { - return mIP == "127.0.0.1"; + return mIP.is_loopback(); } bool diff --git a/src/overlay/PeerBareAddress.h b/src/overlay/PeerBareAddress.h index 3aa7c19ab4..d8c2ba33a4 100644 --- a/src/overlay/PeerBareAddress.h +++ b/src/overlay/PeerBareAddress.h @@ -15,33 +15,14 @@ class Application; class PeerBareAddress { public: - enum class Type - { - EMPTY, - IPv4 - }; - - PeerBareAddress(); - explicit PeerBareAddress(std::string ip, unsigned short port); + explicit PeerBareAddress(asio::ip::address ip, unsigned short port); explicit PeerBareAddress(PeerAddress const& pa); static PeerBareAddress resolve(std::string const& ipPort, Application& app, unsigned short defaultPort = DEFAULT_PEER_PORT); - bool - isEmpty() const - { - return mType == Type::EMPTY; - } - - Type - getType() const - { - return mType; - } - - std::string const& + asio::ip::address const& getIP() const { return mIP; @@ -63,8 +44,7 @@ class PeerBareAddress friend bool operator<(PeerBareAddress const& x, PeerBareAddress const& y); private: - Type mType; - std::string mIP; + asio::ip::address mIP; unsigned short mPort; std::string mStringValue; }; diff --git a/src/overlay/PeerDoor.cpp b/src/overlay/PeerDoor.cpp index a930446fd7..7eccf47115 100644 --- a/src/overlay/PeerDoor.cpp +++ b/src/overlay/PeerDoor.cpp @@ -30,7 +30,17 @@ PeerDoor::start() if (!mApp.getConfig().RUN_STANDALONE) { - tcp::endpoint endpoint(tcp::v4(), mApp.getConfig().PEER_PORT); + asio::ip::address listenAddr; + if (!mApp.getConfig().PEER_LISTEN_IP.empty()) + { + listenAddr = + asio::ip::make_address(mApp.getConfig().PEER_LISTEN_IP); + } + else + { + listenAddr = asio::ip::address_v4::any(); + } + tcp::endpoint endpoint(listenAddr, mApp.getConfig().PEER_PORT); CLOG_INFO(Overlay, "Binding to endpoint {}:{}", endpoint.address().to_string(), endpoint.port()); mAcceptor.open(endpoint.protocol()); diff --git a/src/overlay/PeerManager.cpp b/src/overlay/PeerManager.cpp index f204a5beb8..a517fbbb44 100644 --- a/src/overlay/PeerManager.cpp +++ b/src/overlay/PeerManager.cpp @@ -44,33 +44,27 @@ operator==(PeerRecord const& x, PeerRecord const& y) return x.mType == y.mType; } -namespace -{ - -void -ipToXdr(std::string const& ip, xdr::opaque_array<4U>& ret) -{ - std::stringstream ss(ip); - std::string item; - int n = 0; - while (getline(ss, item, '.') && n < 4) - { - ret[n] = static_cast(atoi(item.c_str())); - n++; - } - if (n != 4) - throw std::runtime_error("ipToXdr: failed on `" + ip + "`"); -} -} - PeerAddress toXdr(PeerBareAddress const& address) { + auto& ip = address.getIP(); + PeerAddress result; result.port = address.getPort(); - result.ip.type(IPv4); - ipToXdr(address.getIP(), result.ip.ipv4()); + + if (ip.is_v4()) + { + result.ip.type(IPv4); + auto bytes = ip.to_v4().to_bytes(); + std::copy(bytes.begin(), bytes.end(), result.ip.ipv4().begin()); + } + else + { + result.ip.type(IPv6); + auto bytes = ip.to_v6().to_bytes(); + std::copy(bytes.begin(), bytes.end(), result.ip.ipv6().begin()); + } result.numFailures = 0; return result; @@ -189,7 +183,7 @@ PeerManager::removePeersWithManyFailures(size_t minNumFailures, std::string ip; if (address) { - ip = address->getIP(); + ip = address->getIP().to_string(); st.exchange(use(ip)); } st.define_and_bind(); @@ -244,7 +238,7 @@ PeerManager::load(PeerBareAddress const& address) st.exchange(into(result.mNumFailures)); st.exchange(into(result.mNextAttempt)); st.exchange(into(result.mType)); - std::string ip = address.getIP(); + std::string ip = address.getIP().to_string(); st.exchange(use(ip)); int port = address.getPort(); st.exchange(use(port)); @@ -302,7 +296,7 @@ PeerManager::store(PeerBareAddress const& address, PeerRecord const& peerRecord, st.exchange(use(peerRecord.mNextAttempt)); st.exchange(use(peerRecord.mNumFailures)); st.exchange(use(peerRecord.mType)); - std::string ip = address.getIP(); + std::string ip = address.getIP().to_string(); st.exchange(use(ip)); int port = address.getPort(); st.exchange(use(port)); @@ -565,7 +559,18 @@ PeerManager::loadPeers(size_t limit, size_t offset, std::string const& where, { if (!ip.empty() && lport > 0) { - result.emplace_back(ip, static_cast(lport)); + asio::error_code ec; + auto parsed = asio::ip::address::from_string(ip, ec); + if (ec) + { + CLOG_WARNING(Overlay, "loadPeers: could not parse ip {}", + ip); + } + else + { + result.emplace_back(parsed, + static_cast(lport)); + } } st.fetch(); } @@ -616,8 +621,18 @@ PeerManager::loadAllPeers() } while (st.got_data()) { - PeerBareAddress pba{ip, static_cast(port)}; - result.emplace_back(std::make_pair(pba, record)); + asio::error_code ec; + auto parsed = asio::ip::address::from_string(ip, ec); + if (ec) + { + CLOG_WARNING(Overlay, "loadAllPeers: could not parse ip {}", + ip); + } + else + { + PeerBareAddress pba{parsed, static_cast(port)}; + result.emplace_back(std::make_pair(pba, record)); + } st.fetch(); } } diff --git a/src/overlay/TCPPeer.cpp b/src/overlay/TCPPeer.cpp index fd33d85ef1..6438f9d504 100644 --- a/src/overlay/TCPPeer.cpp +++ b/src/overlay/TCPPeer.cpp @@ -33,8 +33,8 @@ using namespace std; TCPPeer::TCPPeer(Application& app, Peer::PeerRole role, std::shared_ptr socket, - std::string address) - : Peer(app, role) + asio::ip::address address) + : Peer(app, role, PeerBareAddress{address, 0}) , mThreadVars(useBackgroundThread()) , mSocket(socket) , mIPAddress(std::move(address)) @@ -52,7 +52,6 @@ TCPPeer::pointer TCPPeer::initiate(Application& app, PeerBareAddress const& address) { releaseAssert(threadIsMain()); - releaseAssert(address.getType() == PeerBareAddress::Type::IPv4); CLOG_DEBUG(Overlay, "TCPPeer:initiate to {}", address.toString()); auto& ioContext = app.getConfig().BACKGROUND_OVERLAY_PROCESSING @@ -60,10 +59,9 @@ TCPPeer::initiate(Application& app, PeerBareAddress const& address) : app.getClock().getIOContext(); auto socket = make_shared(ioContext, BUFSZ); auto result = - make_shared(app, WE_CALLED_REMOTE, socket, address.toString()); + make_shared(app, WE_CALLED_REMOTE, socket, address.getIP()); result->initialize(address); - asio::ip::tcp::endpoint endpoint( - asio::ip::address::from_string(address.getIP()), address.getPort()); + asio::ip::tcp::endpoint endpoint(address.getIP(), address.getPort()); // Use weak_ptr here in case the peer is dropped before main thread saves // strong reference in pending/authenticated peer lists socket->next_layer().async_connect( @@ -119,7 +117,7 @@ TCPPeer::accept(Application& app, shared_ptr socket) // threads) Therefore, it is safe to call functions like `remote_endpoint` // and `set_option` (the socket object isn't passed to background yet) auto extractIP = [](shared_ptr socket) { - std::string result; + std::optional result{std::nullopt}; asio::error_code ec; auto ep = socket->next_layer().remote_endpoint(ec); if (ec) @@ -131,13 +129,13 @@ TCPPeer::accept(Application& app, shared_ptr socket) } else { - result = ep.address().to_string(); + result = ep.address(); } return result; }; auto ip = extractIP(socket); - if (ip.empty()) + if (!ip) { return nullptr; } @@ -145,7 +143,7 @@ TCPPeer::accept(Application& app, shared_ptr socket) // First check if there's enough space to accept peer // If not, do not even create a peer instance as to not trigger any // additional reads and memory allocations - if (!app.getOverlayManager().haveSpaceForConnection(ip)) + if (!app.getOverlayManager().haveSpaceForConnection(*ip)) { return nullptr; } @@ -162,8 +160,8 @@ TCPPeer::accept(Application& app, shared_ptr socket) if (!ec && !lingerEc) { CLOG_DEBUG(Overlay, "TCPPeer:accept"); - result = make_shared(app, REMOTE_CALLED_US, socket, ip); - result->initialize(PeerBareAddress{ip, 0}); + result = make_shared(app, REMOTE_CALLED_US, socket, *ip); + result->initialize(PeerBareAddress{*ip, 0}); // Use weak_ptr here in case the peer is dropped before main thread // saves strong reference in pending/authenticated peer lists @@ -334,8 +332,9 @@ TCPPeer::messageSender() break; } - CLOG_DEBUG(Overlay, "messageSender {} - b:{} n:{}/{}", mIPAddress, - expected_length, mThreadVars.getWriteBuffers().size(), + CLOG_DEBUG(Overlay, "messageSender {} - b:{} n:{}/{}", + mIPAddress.to_string(), expected_length, + mThreadVars.getWriteBuffers().size(), mThreadVars.getWriteQueue().size()); mOverlayMetrics.mAsyncWrite.Mark(); mPeerMetrics.mAsyncWrite++; @@ -429,7 +428,7 @@ TCPPeer::writeHandler(asio::error_code const& error, // errors during shutdown or connection are common/expected. mOverlayMetrics.mErrorWrite.Mark(); CLOG_ERROR(Overlay, "Error during sending message to {}", - mIPAddress); + mIPAddress.to_string()); } drop("error during write", Peer::DropDirection::WE_DROPPED_REMOTE); } @@ -528,8 +527,8 @@ TCPPeer::scheduleRead() self->startRead(); }; - std::string taskName = - fmt::format(FMT_STRING("TCPPeer::startRead for {}"), mIPAddress); + std::string taskName = fmt::format(FMT_STRING("TCPPeer::startRead for {}"), + mIPAddress.to_string()); if (useBackgroundThread()) { @@ -560,7 +559,7 @@ TCPPeer::startRead() mThreadVars.getIncomingHeader().clear(); CLOG_DEBUG(Overlay, "TCPPeer::startRead {} from {}", mSocket->in_avail(), - mIPAddress); + mIPAddress.to_string()); mThreadVars.getIncomingHeader().resize(HDRSZ); @@ -694,7 +693,7 @@ TCPPeer::getIncomingMsgLength() { mOverlayMetrics.mErrorRead.Mark(); CLOG_ERROR(Overlay, "{} TCP: message size unacceptable: {}{}", - mIPAddress, length, + mIPAddress.to_string(), length, (isAuthenticated(guard) ? "" : " while not authenticated")); drop("error during read", Peer::DropDirection::WE_DROPPED_REMOTE); length = 0; @@ -799,12 +798,13 @@ TCPPeer::recvMessage() catch (xdr::xdr_runtime_error& e) { CLOG_ERROR(Overlay, "{} - recvMessage got a corrupt xdr: {}", - mIPAddress, e.what()); + mIPAddress.to_string(), e.what()); errorMsg = "received corrupt XDR"; } catch (CryptoError const& e) { - CLOG_ERROR(Overlay, "{} - Crypto error: {}", mIPAddress, e.what()); + CLOG_ERROR(Overlay, "{} - Crypto error: {}", mIPAddress.to_string(), + e.what()); errorMsg = "crypto error"; } diff --git a/src/overlay/TCPPeer.h b/src/overlay/TCPPeer.h index 14755665f9..824fdc8d4f 100644 --- a/src/overlay/TCPPeer.h +++ b/src/overlay/TCPPeer.h @@ -86,7 +86,7 @@ class TCPPeer : public Peer // an atomic std::atomic mDropStarted{false}; std::shared_ptr mSocket; - std::string const mIPAddress; + asio::ip::address const mIPAddress; bool recvMessage(); void sendMessage(xdr::msg_ptr&& xdrBytes, @@ -143,12 +143,9 @@ class TCPPeer : public Peer public: typedef std::shared_ptr pointer; + // hollow constructor; use `initiate` or `accept` instead TCPPeer(Application& app, Peer::PeerRole role, - std::shared_ptr socket, - std::string address); // hollow - // constructor; use - // `initiate` or - // `accept` instead + std::shared_ptr socket, asio::ip::address address); static pointer initiate(Application& app, PeerBareAddress const& address); static pointer accept(Application& app, std::shared_ptr socket); diff --git a/src/overlay/test/LoopbackPeer.cpp b/src/overlay/test/LoopbackPeer.cpp index a02f63c33c..d5bcd7d060 100644 --- a/src/overlay/test/LoopbackPeer.cpp +++ b/src/overlay/test/LoopbackPeer.cpp @@ -24,16 +24,17 @@ using namespace std; // LoopbackPeer /////////////////////////////////////////////////////////////////////// -LoopbackPeer::LoopbackPeer(Application& app, PeerRole role) : Peer(app, role) +LoopbackPeer::LoopbackPeer(Application& app, PeerRole role) + : Peer(app, role, PeerBareAddress{asio::ip::address_v4::loopback(), 0}) { mFlowControl = std::make_shared(mAppConnector, useBackgroundThread()); } -std::string +asio::ip::address LoopbackPeer::getIP() const { - return "127.0.0.1"; + return asio::ip::address_v4::loopback(); } std::pair, std::shared_ptr> diff --git a/src/overlay/test/LoopbackPeer.h b/src/overlay/test/LoopbackPeer.h index 8a761273e5..7ef1e2a912 100644 --- a/src/overlay/test/LoopbackPeer.h +++ b/src/overlay/test/LoopbackPeer.h @@ -148,7 +148,7 @@ class LoopbackPeer : public Peer bool checkCapacity(std::shared_ptr otherPeer) const; - std::string getIP() const; + asio::ip::address getIP() const; using Peer::recvMessage; using Peer::sendAuth; diff --git a/src/overlay/test/OverlayManagerTests.cpp b/src/overlay/test/OverlayManagerTests.cpp index d339d7bd27..e248c3cadb 100644 --- a/src/overlay/test/OverlayManagerTests.cpp +++ b/src/overlay/test/OverlayManagerTests.cpp @@ -35,11 +35,10 @@ class PeerStub : public Peer public: int mSent = 0; PeerStub(Application& app, PeerBareAddress const& address) - : Peer(app, WE_CALLED_REMOTE) + : Peer(app, WE_CALLED_REMOTE, address) { mPeerID = SecretKey::pseudoRandomForTesting().getPublicKey(); mState = GOT_AUTH; - mAddress = address; mRemoteOverlayVersion = app.getConfig().OVERLAY_PROTOCOL_VERSION; } virtual void @@ -173,8 +172,9 @@ class OverlayManagerTests for (auto it = rs.begin(); it != rs.end(); ++it, ++i) { - PeerBareAddress pba{it->get(0), - static_cast(it->get(1))}; + PeerBareAddress pba{ + asio::ip::address::from_string(it->get(0)), + static_cast(it->get(1))}; auto type = it->get(2); if (i < fourPeers.size()) { @@ -199,8 +199,8 @@ class OverlayManagerTests // (from INBOUND to OUTBOUND) OverlayManagerStub& pm = app->getOverlayManager(); - PeerBareAddress prefPba{"127.0.0.1", 2011}; - PeerBareAddress pba{"127.0.0.1", 64000}; + PeerBareAddress prefPba{asio::ip::address_v4::loopback(), 2011}; + PeerBareAddress pba{asio::ip::address_v4::loopback(), 64000}; auto prefPr = pm.getPeerManager().load(prefPba); auto pr = pm.getPeerManager().load(pba); @@ -220,7 +220,7 @@ class OverlayManagerTests for (auto it = rs.begin(); it != rs.end(); ++it) { PeerBareAddress storedPba{ - it->get(0), + asio::ip::address::from_string(it->get(0)), static_cast(it->get(1))}; auto type = it->get(2); if (storedPba == pba) diff --git a/src/overlay/test/OverlayTestUtils.cpp b/src/overlay/test/OverlayTestUtils.cpp index 016a76d216..834a050d14 100644 --- a/src/overlay/test/OverlayTestUtils.cpp +++ b/src/overlay/test/OverlayTestUtils.cpp @@ -65,8 +65,9 @@ getSentDemandCount(std::shared_ptr app) bool knowsAs(Application& knowingApp, Application& knownApp, PeerType peerType) { - auto data = knowingApp.getOverlayManager().getPeerManager().load( - PeerBareAddress{"127.0.0.1", knownApp.getConfig().PEER_PORT}); + auto data = + knowingApp.getOverlayManager().getPeerManager().load(PeerBareAddress{ + asio::ip::address_v4::loopback(), knownApp.getConfig().PEER_PORT}); if (!data.second) { return false; @@ -80,7 +81,7 @@ doesNotKnow(Application& knowingApp, Application& knownApp) { return !knowingApp.getOverlayManager() .getPeerManager() - .load(PeerBareAddress{"127.0.0.1", + .load(PeerBareAddress{asio::ip::address_v4::loopback(), knownApp.getConfig().PEER_PORT}) .second; } diff --git a/src/overlay/test/OverlayTests.cpp b/src/overlay/test/OverlayTests.cpp index 797ab0b871..7c17b7e65d 100644 --- a/src/overlay/test/OverlayTests.cpp +++ b/src/overlay/test/OverlayTests.cpp @@ -795,7 +795,7 @@ TEST_CASE("peers during auth", "[overlay][connections]") auto app2 = createTestApplication(clock, cfg2); // Put a peer into Acceptor's DB to trigger sending of peers during auth app2->getOverlayManager().getPeerManager().ensureExists( - PeerBareAddress{"1.1.1.1", 11625}); + PeerBareAddress{asio::ip::address::from_string("1.1.1.1"), 11625}); LoopbackPeerConnection conn(*app1, *app2); testutil::crankSome(clock); @@ -1968,7 +1968,8 @@ TEST_CASE("inbounds nodes can be promoted to ouboundvalid", for (auto i = 0; i < 3; i++) { configs.push_back(getTestConfig(i + 1)); - addresses.emplace_back("127.0.0.1", configs[i].PEER_PORT); + addresses.emplace_back(asio::ip::address::from_string("127.0.0.1"), + configs[i].PEER_PORT); } configs[0].KNOWN_PEERS.emplace_back( @@ -2242,7 +2243,7 @@ TEST_CASE("overlay flow control", "[overlay][flowcontrol][acceptance]") PeerBareAddress localhost(unsigned short port) { - return PeerBareAddress{"127.0.0.1", port}; + return PeerBareAddress{asio::ip::address_v4::loopback(), port}; } TEST_CASE("database is purged at overlay start", "[overlay]") @@ -3020,10 +3021,10 @@ TEST_CASE("Queue purging after write completion", "[overlay][flowcontrol]") s->crankForAtLeast(std::chrono::seconds(1), false); auto p0 = n0->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n1->getConfig().PEER_PORT}); + localhost(n1->getConfig().PEER_PORT)); auto p1 = n1->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n0->getConfig().PEER_PORT}); + localhost(n0->getConfig().PEER_PORT)); REQUIRE(p0); REQUIRE(p1); @@ -3120,7 +3121,7 @@ TEST_CASE("background signature verification with missing account", // Get the connected TCPPeer auto receiverPeer = senderNode->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", receiverNode->getConfig().PEER_PORT}); + localhost(receiverNode->getConfig().PEER_PORT)); REQUIRE(receiverPeer); REQUIRE(receiverPeer->isAuthenticatedForTesting()); diff --git a/src/overlay/test/PeerManagerTests.cpp b/src/overlay/test/PeerManagerTests.cpp index 86586e3512..6d07b64aa8 100644 --- a/src/overlay/test/PeerManagerTests.cpp +++ b/src/overlay/test/PeerManagerTests.cpp @@ -21,97 +21,262 @@ using namespace std; static PeerBareAddress localhost(unsigned short port) { - return PeerBareAddress{"127.0.0.1", port}; + return PeerBareAddress{asio::ip::address_v4::loopback(), port}; } TEST_CASE("toXdr", "[overlay][PeerManager]") { - VirtualClock clock; - Application::pointer app = createTestApplication(clock, getTestConfig()); - auto& pm = app->getOverlayManager().getPeerManager(); - auto address = PeerBareAddress::resolve("1.25.50.200:256", *app); + auto run = [](asio::ip::address ip) { + VirtualClock clock; + Application::pointer app = + createTestApplication(clock, getTestConfig()); + auto& pm = app->getOverlayManager().getPeerManager(); + auto address = PeerBareAddress(ip, 256); + + SECTION("toXdr") + { + REQUIRE(address.getIP().to_string() == ip.to_string()); + REQUIRE(address.getPort() == 256); - SECTION("toXdr") - { - REQUIRE(address.getIP() == "1.25.50.200"); - REQUIRE(address.getPort() == 256); + auto xdr = toXdr(address); + REQUIRE(xdr.port == 256); + if (ip.is_v4()) + { + REQUIRE(xdr.ip.type() == IPv4); + REQUIRE(xdr.ip.ipv4() == ip.to_v4().to_bytes()); + } + else + { + REQUIRE(xdr.ip.type() == IPv6); + REQUIRE(xdr.ip.ipv6() == ip.to_v6().to_bytes()); + } + REQUIRE(xdr.numFailures == 0); + } - auto xdr = toXdr(address); - REQUIRE(xdr.port == 256); - REQUIRE(xdr.ip.ipv4()[0] == 1); - REQUIRE(xdr.ip.ipv4()[1] == 25); - REQUIRE(xdr.ip.ipv4()[2] == 50); - REQUIRE(xdr.ip.ipv4()[3] == 200); - REQUIRE(xdr.numFailures == 0); - } + SECTION("database roundtrip") + { + auto test = [&](PeerType peerType) { + auto loadedPR = pm.load(address); + REQUIRE(!loadedPR.second); + + auto storedPr = loadedPR.first; + storedPr.mType = static_cast(peerType); + pm.store(address, storedPr, false); + + auto actualPR = pm.load(address); + REQUIRE(actualPR.second); + REQUIRE(actualPR.first == storedPr); + }; + + SECTION("inbound") + { + test(PeerType::INBOUND); + } - SECTION("database roundtrip") + SECTION("outbound") + { + test(PeerType::OUTBOUND); + } + + SECTION("preferred") + { + test(PeerType::PREFERRED); + } + } + }; + + SECTION("IPv4") + { + run(asio::ip::address::from_string("1.25.50.200")); + } + SECTION("IPv6") { - auto test = [&](PeerType peerType) { - auto loadedPR = pm.load(address); - REQUIRE(!loadedPR.second); + run(asio::ip::address::from_string("2001:0db8::")); + } +} + +TEST_CASE("private addresses", "[overlay][PeerManager]") +{ + auto checkRange4 = [](char const* cidr, bool checkPrev = true, + bool checkNext = true) { + auto network = asio::ip::make_network_v4(cidr); + if (checkPrev) + { + auto addr4 = + asio::ip::make_address_v4(network.network().to_uint() - 1); + auto addr6 = asio::ip::make_address_v6(asio::ip::v4_mapped, addr4); + CHECK(!PeerBareAddress(addr4, 15).isPrivate()); + CHECK(!PeerBareAddress(addr6, 15).isPrivate()); + } - auto storedPr = loadedPR.first; - storedPr.mType = static_cast(peerType); - pm.store(address, storedPr, false); + auto addr4 = network.network(); + auto addr6 = asio::ip::make_address_v6(asio::ip::v4_mapped, addr4); + CHECK(PeerBareAddress(addr4, 15).isPrivate()); + CHECK(PeerBareAddress(addr6, 15).isPrivate()); - auto actualPR = pm.load(address); - REQUIRE(actualPR.second); - REQUIRE(actualPR.first == storedPr); - }; + addr4 = network.broadcast(); + addr6 = asio::ip::make_address_v6(asio::ip::v4_mapped, addr4); + CHECK(PeerBareAddress(addr4, 15).isPrivate()); + CHECK(PeerBareAddress(addr6, 15).isPrivate()); - SECTION("inbound") + if (checkNext) { - test(PeerType::INBOUND); + addr4 = + asio::ip::make_address_v4(network.broadcast().to_uint() + 1); + addr6 = asio::ip::make_address_v6(asio::ip::v4_mapped, addr4); + CHECK(!PeerBareAddress(addr4, 15).isPrivate()); + CHECK(!PeerBareAddress(addr6, 15).isPrivate()); } + }; - SECTION("outbound") + auto checkRange6 = [](char const* ip, int bits, bool checkPrev = true, + bool checkNext = true) { + asio::ip::address_v6 start = asio::ip::make_address_v6(ip); + CHECK(PeerBareAddress(start, 15).isPrivate()); + if (checkPrev) { - test(PeerType::OUTBOUND); + auto prevBytes = start.to_bytes(); + for (int i = 15; i >= 0; --i) + { + if (prevBytes[i]-- != 0) + { + break; + } + } + auto prev = asio::ip::address_v6(prevBytes); + CHECK(!PeerBareAddress(prev, 15).isPrivate()); } - SECTION("preferred") + auto endBytes = start.to_bytes(); + for (int i = 0; i < 16; i++) { - test(PeerType::PREFERRED); + if (bits >= 8) + { + bits -= 8; + } + else + { + endBytes[i] |= 0xFF >> bits; + bits = 0; + } } - } -} + CHECK(PeerBareAddress(asio::ip::address_v6(endBytes), 15).isPrivate()); -TEST_CASE("private addresses", "[overlay][PeerManager]") -{ - PeerBareAddress pa("1.2.3.4", 15); - CHECK(!pa.isPrivate()); - pa = PeerBareAddress("10.1.2.3", 15); - CHECK(pa.isPrivate()); - pa = PeerBareAddress("172.17.1.2", 15); - CHECK(pa.isPrivate()); - pa = PeerBareAddress("192.168.1.2", 15); - CHECK(pa.isPrivate()); -} + if (checkNext) + { + for (int i = 15; i >= 0; --i) + { + if (++endBytes[i] != 0) + { + break; + } + } + CHECK(!PeerBareAddress(asio::ip::address_v6(endBytes), 15) + .isPrivate()); + } + }; -TEST_CASE("create peer record", "[overlay][PeerManager]") -{ - SECTION("empty") + SECTION("non-private addresses") { - REQUIRE_THROWS_AS(PeerBareAddress("", 0), std::runtime_error); + PeerBareAddress pa(asio::ip::address::from_string("1.2.3.4"), 15); + CHECK(!pa.isPrivate()); + pa = PeerBareAddress(asio::ip::address::from_string("127.0.0.1"), 15); + CHECK(!pa.isPrivate()); + CHECK(pa.isLocalhost()); + pa = PeerBareAddress(asio::ip::address::from_string("::1"), 15); + CHECK(!pa.isPrivate()); + CHECK(pa.isLocalhost()); } - SECTION("empty ip") + SECTION("0.0.0.0/8") + { + checkRange4("0.0.0.0/8", false); + } + SECTION("10.0.0.0/8") + { + checkRange4("10.0.0.0/8"); + } + SECTION("100.64.0.0/10") + { + checkRange4("100.64.0.0/10"); + } + SECTION("169.254.0.0/16") + { + checkRange4("169.254.0.0/16"); + } + SECTION("172.16.0.0/12") + { + checkRange4("172.16.0.0/12"); + } + SECTION("192.0.0.0/24") { - REQUIRE_THROWS_AS(PeerBareAddress("", 80), std::runtime_error); + checkRange4("192.0.0.0/24"); + } + SECTION("192.0.2.0/24") + { + checkRange4("192.0.2.0/24"); + } + SECTION("192.168.0.0/16") + { + checkRange4("192.168.0.0/16"); + } + SECTION("198.18.0.0/15") + { + checkRange4("198.18.0.0/15"); + } + SECTION("198.51.100.0/24") + { + checkRange4("198.51.100.0/24"); + } + SECTION("203.0.113.0/24") + { + checkRange4("203.0.113.0/24"); + } + SECTION("240.0.0.0/4") + { + checkRange4("240.0.0.0/4", true, false); } - SECTION("random string") // PeerBareAddress does not validate IP format + SECTION("::/128") { - auto pa = PeerBareAddress("random string", 80); - REQUIRE(pa.getIP() == "random string"); - REQUIRE(pa.getPort() == 80); + checkRange6("::", 128); + } + SECTION("100::/64") + { + checkRange6("100::", 64); + } + SECTION("2001::/32") + { + checkRange6("2001::", 32); } + SECTION("2001:2::/48") + { + checkRange6("2001:2::", 48); + } + SECTION("2001:db8::/32") + { + checkRange6("2001:db8::", 32); + } + SECTION("2001:10::/28") + { + checkRange6("2001:10::", 28); + } + SECTION("fc00::/7") + { + checkRange6("fc00::", 7); + } + SECTION("fe80::/10") + { + checkRange6("fe80::", 10); + } +} +TEST_CASE("create peer record", "[overlay][PeerManager]") +{ SECTION("valid data") { auto pa = localhost(80); - REQUIRE(pa.getIP() == "127.0.0.1"); + REQUIRE(pa.getIP().to_string() == "127.0.0.1"); REQUIRE(pa.getPort() == 80); } } @@ -121,126 +286,141 @@ TEST_CASE("parse peer record", "[overlay][PeerManager]") VirtualClock clock; auto app = createTestApplication(clock, getTestConfig()); + auto requireSuccess = [&app](std::string const& str, + std::string const& expectedIP, + unsigned short expectedPort) { + auto pr = PeerBareAddress::resolve(str, *app); + REQUIRE(pr.getIP().to_string() == expectedIP); + REQUIRE(pr.getPort() == expectedPort); + }; + + auto requireThrow = [&app](std::string const& str) { + REQUIRE_THROWS_AS(PeerBareAddress::resolve(str, *app), + std::runtime_error); + }; + SECTION("empty") { - REQUIRE_THROWS_AS(PeerBareAddress::resolve("", *app), - std::runtime_error); + requireThrow(""); } SECTION("random string") { - REQUIRE_THROWS_AS(PeerBareAddress::resolve("random string", *app), - std::runtime_error); + requireThrow("random string"); } SECTION("invalid ipv4") { - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.256", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve("256.256.256.256", *app), - std::runtime_error); + requireThrow("127.0.0.256"); + requireThrow("256.256.256.256"); } SECTION("ipv4 mask instead of address") { - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.1/8", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.1/16", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.1/24", *app), - std::runtime_error); + requireThrow("127.0.0.1/8"); + requireThrow("127.0.0.1/16"); + requireThrow("127.0.0.1/24"); } - SECTION("valid ipv6") + SECTION("valid non-bracketed ipv6") { - REQUIRE_THROWS_AS( - PeerBareAddress::resolve("2001:db8:a0b:12f0::1", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve( - "2001:0db8:0a0b:12f0:0000:0000:0000:0001", *app), - std::runtime_error); + requireThrow("2001:db8:a0b:12f0::1"); + requireThrow("2001:0db8:0a0b:12f0:0000:0000:0000:0001"); } - SECTION("invalid ipv6") + SECTION("invalid non-bracketed ipv6") { - REQUIRE_THROWS_AS( - PeerBareAddress::resolve("10000:db8:a0b:12f0::1", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve( - "2001:0db8:0a0b:12f0:0000:10000:0000:0001", *app), - std::runtime_error); + requireThrow("10000:db8:a0b:12f0::1"); + requireThrow("2001:0db8:0a0b:12f0:0000:10000:0000:0001"); } - SECTION("ipv6 mask instead of address") + SECTION("non-bracketed ipv6 mask instead of address") { - REQUIRE_THROWS_AS( - PeerBareAddress::resolve("2001:db8:a0b:12f0::1/16", *app), - std::runtime_error); - REQUIRE_THROWS_AS( - PeerBareAddress::resolve("2001:db8:a0b:12f0::1/32", *app), - std::runtime_error); - REQUIRE_THROWS_AS( - PeerBareAddress::resolve("2001:db8:a0b:12f0::1/64", *app), - std::runtime_error); + requireThrow("2001:db8:a0b:12f0::1/16"); + requireThrow("2001:db8:a0b:12f0::1/32"); + requireThrow("2001:db8:a0b:12f0::1/64"); } SECTION("valid ipv4 with empty port") { - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.2:", *app), - std::runtime_error); + requireThrow("127.0.0.2:"); + } + + SECTION("valid ipv6 with empty port") + { + requireThrow("[2001:db8:a0b:12f0::1]:"); } SECTION("valid ipv4 with invalid port") { - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.2:-1", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.2:0", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.2:65536", *app), - std::runtime_error); - REQUIRE_THROWS_AS(PeerBareAddress::resolve("127.0.0.2:65537", *app), - std::runtime_error); + requireThrow("127.0.0.2:-1"); + requireThrow("127.0.0.2:0"); + requireThrow("127.0.0.2:65536"); + requireThrow("127.0.0.2:65537"); + } + + SECTION("valid ipv6 with invalid port") + { + requireThrow("[2001:db8:a0b:12f0::1]:-1"); + requireThrow("[2001:db8:a0b:12f0::1]:0"); + requireThrow("[2001:db8:a0b:12f0::1]:65536"); + requireThrow("[2001:db8:a0b:12f0::1]:65537"); } SECTION("valid ipv4 with default port") { - auto pr = PeerBareAddress::resolve("127.0.0.2", *app); - REQUIRE(pr.getIP() == "127.0.0.2"); - REQUIRE(pr.getPort() == DEFAULT_PEER_PORT); + requireSuccess("127.0.0.2", "127.0.0.2", DEFAULT_PEER_PORT); + requireSuccess("8.8.8.8", "8.8.8.8", DEFAULT_PEER_PORT); + } - pr = PeerBareAddress::resolve("8.8.8.8", *app); - REQUIRE(pr.getIP() == "8.8.8.8"); - REQUIRE(pr.getPort() == DEFAULT_PEER_PORT); + SECTION("valid ipv6 with default port") + { + requireSuccess("[2001:db8:a0b:12f0::1]", "2001:db8:a0b:12f0::1", + DEFAULT_PEER_PORT); + requireSuccess("[2001:0db8:0a0b:12f0:0000:0000:0000:0001]", + "2001:db8:a0b:12f0::1", DEFAULT_PEER_PORT); } SECTION("valid ipv4 with different default port") { auto pr = PeerBareAddress::resolve("127.0.0.2", *app, 10); - REQUIRE(pr.getIP() == "127.0.0.2"); + REQUIRE(pr.getIP().to_string() == "127.0.0.2"); REQUIRE(pr.getPort() == 10); pr = PeerBareAddress::resolve("8.8.8.8", *app, 10); - REQUIRE(pr.getIP() == "8.8.8.8"); + REQUIRE(pr.getIP().to_string() == "8.8.8.8"); REQUIRE(pr.getPort() == 10); } - SECTION("valid ipv4 with valid port") + SECTION("valid ipv6 with different default port") { - auto pr = PeerBareAddress::resolve("127.0.0.2:1", *app); - REQUIRE(pr.getIP() == "127.0.0.2"); - REQUIRE(pr.getPort() == 1); + auto pr = PeerBareAddress::resolve("[2001:db8:a0b:12f0::1]", *app, 10); + REQUIRE(pr.getIP().to_string() == "2001:db8:a0b:12f0::1"); + REQUIRE(pr.getPort() == 10); - pr = PeerBareAddress::resolve("127.0.0.2:1234", *app); - REQUIRE(pr.getIP() == "127.0.0.2"); - REQUIRE(pr.getPort() == 1234); + pr = PeerBareAddress::resolve( + "[2001:0db8:0a0b:12f0:0000:0000:0000:0001]", *app, 10); + REQUIRE(pr.getIP().to_string() == "2001:db8:a0b:12f0::1"); + REQUIRE(pr.getPort() == 10); + } - pr = PeerBareAddress::resolve("127.0.0.2:65534", *app); - REQUIRE(pr.getIP() == "127.0.0.2"); - REQUIRE(pr.getPort() == 65534); + SECTION("valid ipv4 with valid port") + { + requireSuccess("127.0.0.2:1", "127.0.0.2", 1); + requireSuccess("127.0.0.2:1234", "127.0.0.2", 1234); + requireSuccess("127.0.0.2:65534", "127.0.0.2", 65534); + requireSuccess("127.0.0.2:65535", "127.0.0.2", 65535); + } - pr = PeerBareAddress::resolve("127.0.0.2:65535", *app); - REQUIRE(pr.getIP() == "127.0.0.2"); - REQUIRE(pr.getPort() == 65535); + SECTION("valid ipv6 with valid port") + { + requireSuccess("[2001:db8:a0b:12f0::1]:1", "2001:db8:a0b:12f0::1", 1); + requireSuccess("[2001:db8:a0b:12f0::1]:1234", "2001:db8:a0b:12f0::1", + 1234); + requireSuccess("[2001:db8:a0b:12f0::1]:65534", "2001:db8:a0b:12f0::1", + 65534); + requireSuccess("[2001:db8:a0b:12f0::1]:65535", "2001:db8:a0b:12f0::1", + 65535); } } @@ -357,7 +537,8 @@ TEST_CASE("getPeersToSend", "[overlay][PeerManager]") VirtualClock clock; auto app = createTestApplication(clock, getTestConfig()); auto& peerManager = app->getOverlayManager().getPeerManager(); - auto myAddress = PeerBareAddress("127.0.0.255", 1); + auto myAddress = + PeerBareAddress(asio::ip::address::from_string("127.0.0.255"), 1); auto getSize = [&](int requestedSize) { return peerManager.getPeersToSend(requestedSize, myAddress).size(); }; diff --git a/src/overlay/test/TCPPeerTests.cpp b/src/overlay/test/TCPPeerTests.cpp index 226ef153fc..39cdf33c5a 100644 --- a/src/overlay/test/TCPPeerTests.cpp +++ b/src/overlay/test/TCPPeerTests.cpp @@ -76,11 +76,11 @@ TEST_CASE("TCPPeer lifetime", "[overlay]") .count() == 0); } - auto p0 = n0->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n1->getConfig().PEER_PORT}); + auto p0 = n0->getOverlayManager().getConnectedPeer(PeerBareAddress{ + asio::ip::address_v4::loopback(), n1->getConfig().PEER_PORT}); - auto p1 = n1->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n0->getConfig().PEER_PORT}); + auto p1 = n1->getOverlayManager().getConnectedPeer(PeerBareAddress{ + asio::ip::address_v4::loopback(), n0->getConfig().PEER_PORT}); REQUIRE(!p0); REQUIRE(!p1); @@ -112,11 +112,11 @@ TEST_CASE("TCPPeer can communicate", "[overlay]") s->startAllNodes(); s->crankForAtLeast(std::chrono::seconds(1), false); - auto p0 = n0->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n1->getConfig().PEER_PORT}); + auto p0 = n0->getOverlayManager().getConnectedPeer(PeerBareAddress{ + asio::ip::address_v4::loopback(), n1->getConfig().PEER_PORT}); - auto p1 = n1->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n0->getConfig().PEER_PORT}); + auto p1 = n1->getOverlayManager().getConnectedPeer(PeerBareAddress{ + asio::ip::address_v4::loopback(), n0->getConfig().PEER_PORT}); REQUIRE(p0); REQUIRE(p1); @@ -164,11 +164,11 @@ TEST_CASE("TCPPeer read malformed messages", "[overlay]") s->startAllNodes(); s->stopOverlayTick(); s->crankForAtLeast(std::chrono::seconds(5), false); - auto p0 = n0->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n1->getConfig().PEER_PORT}); + auto p0 = n0->getOverlayManager().getConnectedPeer(PeerBareAddress{ + asio::ip::address_v4::loopback(), n1->getConfig().PEER_PORT}); - auto p1 = n1->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", n0->getConfig().PEER_PORT}); + auto p1 = n1->getOverlayManager().getConnectedPeer(PeerBareAddress{ + asio::ip::address_v4::loopback(), n0->getConfig().PEER_PORT}); REQUIRE(p0); REQUIRE(p1); diff --git a/src/simulation/CoreTests.cpp b/src/simulation/CoreTests.cpp index 0d74403cea..98e73e3ed7 100644 --- a/src/simulation/CoreTests.cpp +++ b/src/simulation/CoreTests.cpp @@ -679,3 +679,64 @@ TEST_CASE("Bucket list entries vs write throughput", "[scalability][!hide]") } } } + +TEST_CASE("mixed IPv4 and IPv6 nodes", "[simulation][overlay][ipv6]") +{ + // This test verifies that stellar-core can handle a network where some + // nodes listen on IPv4 and others on IPv6. It creates a 4-node network + // with 2 IPv4 nodes and 2 IPv6 nodes, all forming a quorum. + Hash networkID = sha256(getTestConfig().NETWORK_PASSPHRASE); + + auto confGen = [](int i) { + auto cfg = getTestConfig(i); + cfg.ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING = true; + // Even nodes listen on IPv4, odd nodes on IPv6 + if (i % 2 == 0) + { + cfg.PEER_LISTEN_IP = "127.0.0.1"; + } + else + { + cfg.PEER_LISTEN_IP = "::1"; + } + return cfg; + }; + + Simulation::pointer simulation = + std::make_shared(Simulation::OVER_TCP, networkID, confGen); + + std::vector keys; + for (int i = 0; i < 4; i++) + { + keys.push_back(SecretKey::pseudoRandomForTesting()); + } + + SCPQuorumSet qSet; + qSet.threshold = 3; // 3 of 4 for consensus + for (auto& k : keys) + { + qSet.validators.push_back(k.getPublicKey()); + } + + for (auto& k : keys) + { + simulation->addNode(k, qSet); + } + + // Fully connect all nodes (IPv4 <-> IPv6 connections included) + simulation->fullyConnectAllPending(); + + simulation->startAllNodes(); + + // Verify that all nodes reach consensus + int nLedgers = 10; + simulation->crankUntil( + [&simulation, nLedgers]() { + return simulation->haveAllExternalized(nLedgers + 1, 5); + }, + 20 * nLedgers * simulation->getExpectedLedgerCloseTime(), true); + + REQUIRE(simulation->haveAllExternalized(nLedgers + 1, 5)); + + LOG_INFO(DEFAULT_LOG, "Mixed IPv4/IPv6 test completed successfully"); +} diff --git a/src/simulation/Simulation.cpp b/src/simulation/Simulation.cpp index 98736d72fc..ed794e0677 100644 --- a/src/simulation/Simulation.cpp +++ b/src/simulation/Simulation.cpp @@ -31,6 +31,22 @@ namespace stellar using namespace std; +namespace +{ +// Returns the IP address a node is listening on based on its config. +// If PEER_LISTEN_IP is set, returns that address; otherwise returns IPv4 +// loopback. +asio::ip::address +getNodeListenAddress(Config const& cfg) +{ + if (!cfg.PEER_LISTEN_IP.empty()) + { + return asio::ip::make_address(cfg.PEER_LISTEN_IP); + } + return asio::ip::address_v4::loopback(); +} +} // namespace + Simulation::Simulation(Mode mode, Hash const& networkID, ConfigGen confGen, QuorumSetAdjuster qSetAdjust) : mVirtualClockMode(mode != OVER_TCP) @@ -285,8 +301,9 @@ Simulation::dropConnection(NodeID initiator, NodeID acceptor) { auto& cAcceptor = mNodes[acceptor].mApp->getConfig(); - auto peer = iApp->getOverlayManager().getConnectedPeer( - PeerBareAddress{"127.0.0.1", cAcceptor.PEER_PORT}); + auto peer = + iApp->getOverlayManager().getConnectedPeer(PeerBareAddress{ + getNodeListenAddress(cAcceptor), cAcceptor.PEER_PORT}); if (peer) { peer->drop("drop", Peer::DropDirection::WE_DROPPED_REMOTE); @@ -352,7 +369,8 @@ Simulation::addTCPConnection(NodeID initiator, NodeID acceptor) { throw runtime_error("PEER_PORT cannot be set to 0"); } - auto address = PeerBareAddress{"127.0.0.1", to->getConfig().PEER_PORT}; + auto address = PeerBareAddress{getNodeListenAddress(to->getConfig()), + to->getConfig().PEER_PORT}; from->getOverlayManager().connectTo(address); }