From ab1d3ece026844e682676673b8a461964a5b3ce4 Mon Sep 17 00:00:00 2001 From: laanwj <126646+laanwj@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:28:28 +0200 Subject: [PATCH 1/5] net: Add optional length checking to CService::SetSockAddr In almost all cases (the only exception is `getifaddrs`), we know the size of the data passed into SetSockAddr, so we can check this to be what is expected. --- src/common/netif.cpp | 9 +-------- src/common/pcp.cpp | 2 +- src/net.cpp | 4 ++-- src/netaddress.cpp | 4 +++- src/netaddress.h | 9 ++++++++- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/common/netif.cpp b/src/common/netif.cpp index 08f034a412a..7424f977c7e 100644 --- a/src/common/netif.cpp +++ b/src/common/netif.cpp @@ -169,16 +169,9 @@ std::optional QueryDefaultGatewayImpl(sa_family_t family) std::optional FromSockAddr(const struct sockaddr* addr) { - // Check valid length. Note that sa_len is not part of POSIX, and exists on MacOS and some BSDs only, so we can't - // do this check in SetSockAddr. - if (!(addr->sa_family == AF_INET && addr->sa_len == sizeof(struct sockaddr_in)) && - !(addr->sa_family == AF_INET6 && addr->sa_len == sizeof(struct sockaddr_in6))) { - return std::nullopt; - } - // Fill in a CService from the sockaddr, then drop the port part. CService service; - if (service.SetSockAddr(addr)) { + if (service.SetSockAddr(addr, addr->sa_len)) { return (CNetAddr)service; } return std::nullopt; diff --git a/src/common/pcp.cpp b/src/common/pcp.cpp index 3cc1cba9242..7fbc1472b4e 100644 --- a/src/common/pcp.cpp +++ b/src/common/pcp.cpp @@ -426,7 +426,7 @@ std::variant PCPRequestPortMap(const PCPMappingNonc return MappingError::NETWORK_ERROR; } CService internal; - if (!internal.SetSockAddr((struct sockaddr*)&internal_addr)) return MappingError::NETWORK_ERROR; + if (!internal.SetSockAddr((struct sockaddr*)&internal_addr, internal_addrlen)) return MappingError::NETWORK_ERROR; LogPrintLevel(BCLog::NET, BCLog::Level::Debug, "pcp: Internal address after connect: %s\n", internal.ToStringAddr()); // Build request packet. Make sure the packet is zeroed so that reserved fields are zero diff --git a/src/net.cpp b/src/net.cpp index 8ea7f6ce445..3ff466c1531 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -383,7 +383,7 @@ static CAddress GetBindAddress(const Sock& sock) struct sockaddr_storage sockaddr_bind; socklen_t sockaddr_bind_len = sizeof(sockaddr_bind); if (!sock.GetSockName((struct sockaddr*)&sockaddr_bind, &sockaddr_bind_len)) { - addr_bind.SetSockAddr((const struct sockaddr*)&sockaddr_bind); + addr_bind.SetSockAddr((const struct sockaddr*)&sockaddr_bind, sockaddr_bind_len); } else { LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "getsockname failed\n"); } @@ -1728,7 +1728,7 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { return; } - if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr)) { + if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr, len)) { LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Unknown socket family\n"); } else { addr = CAddress{MaybeFlipIPv6toCJDNS(addr), NODE_NONE}; diff --git a/src/netaddress.cpp b/src/netaddress.cpp index bd2353a7121..5ad0da7792e 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -807,13 +807,15 @@ CService::CService(const struct sockaddr_in6 &addr) : CNetAddr(addr.sin6_addr, a assert(addr.sin6_family == AF_INET6); } -bool CService::SetSockAddr(const struct sockaddr *paddr) +bool CService::SetSockAddr(const struct sockaddr *paddr, socklen_t addrlen) { switch (paddr->sa_family) { case AF_INET: + if (addrlen != sizeof(struct sockaddr_in)) return false; *this = CService(*(const struct sockaddr_in*)paddr); return true; case AF_INET6: + if (addrlen != sizeof(struct sockaddr_in6)) return false; *this = CService(*(const struct sockaddr_in6*)paddr); return true; default: diff --git a/src/netaddress.h b/src/netaddress.h index 24f5c3fb962..ad83c5381c3 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -539,7 +539,14 @@ public: explicit CService(const struct sockaddr_in& addr); uint16_t GetPort() const; bool GetSockAddr(struct sockaddr* paddr, socklen_t* addrlen) const; - bool SetSockAddr(const struct sockaddr* paddr); + /** + * Set CService from a network sockaddr. + * @param[in] paddr Pointer to sockaddr structure + * @param[in] addrlen Length of sockaddr structure in bytes. This will be checked to exactly match the length of + * a socket address of the provided family, unless std::nullopt is passed + * @returns true on success + */ + bool SetSockAddr(const struct sockaddr* paddr, socklen_t addrlen); /** * Get the address family * @returns AF_UNSPEC if unspecified From 03648321ecb704b69e47eed7e3df6a779aee8f11 Mon Sep 17 00:00:00 2001 From: laanwj <126646+laanwj@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:24:58 +0200 Subject: [PATCH 2/5] util: Add mockable steady_clock This adds a NodeSteadyClock, which is a steady_clock that can be mocked with millisecond precision. --- src/util/time.cpp | 25 +++++++++++++++++++++++++ src/util/time.h | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/util/time.cpp b/src/util/time.cpp index 00f0f473926..cafc27e0d05 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -21,6 +21,7 @@ void UninterruptibleSleep(const std::chrono::microseconds& n) { std::this_thread static std::atomic g_mock_time{}; //!< For testing std::atomic g_used_system_time{false}; +static std::atomic g_mock_steady_time{}; //!< For testing NodeClock::time_point NodeClock::now() noexcept { @@ -48,6 +49,30 @@ std::chrono::seconds GetMockTime() return g_mock_time.load(std::memory_order_relaxed); } +MockableSteadyClock::time_point MockableSteadyClock::now() noexcept +{ + const auto mocktime{g_mock_steady_time.load(std::memory_order_relaxed)}; + if (!mocktime.count()) { + g_used_system_time = true; + } + const auto ret{ + mocktime.count() ? + mocktime : + std::chrono::steady_clock::now().time_since_epoch()}; + return time_point{ret}; +}; + +void MockableSteadyClock::SetMockTime(std::chrono::milliseconds mock_time_in) +{ + Assert(mock_time_in >= 0s); + g_mock_steady_time.store(mock_time_in, std::memory_order_relaxed); +} + +void MockableSteadyClock::ClearMockTime() +{ + g_mock_steady_time.store(0ms, std::memory_order_relaxed); +} + int64_t GetTime() { return GetTime().count(); } std::string FormatISO8601DateTime(int64_t nTime) diff --git a/src/util/time.h b/src/util/time.h index 27cbe50581f..c43b306ff24 100644 --- a/src/util/time.h +++ b/src/util/time.h @@ -31,6 +31,31 @@ using SteadyMicroseconds = std::chrono::time_point; + + static constexpr std::chrono::milliseconds INITIAL_MOCK_TIME{1}; + + /** Return current system time or mocked time, if set */ + static time_point now() noexcept; + static std::time_t to_time_t(const time_point&) = delete; // unused + static time_point from_time_t(std::time_t) = delete; // unused + + /** Set mock time for testing. + * When mocking the steady clock, start at INITIAL_MOCK_TIME and add durations to elapse time as necessary + * for testing. + * To stop mocking, call ClearMockTime(). + */ + static void SetMockTime(std::chrono::milliseconds mock_time_in); + + /** Clear mock time, go back to system steady clock. */ + static void ClearMockTime(); +}; + void UninterruptibleSleep(const std::chrono::microseconds& n); /** From caf952103317a7fa8bd2bceb35d4e8ace5968906 Mon Sep 17 00:00:00 2001 From: laanwj <126646+laanwj@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:26:21 +0200 Subject: [PATCH 3/5] net: Use mockable steady clock in PCP implementation This will be needed for the test harness. --- src/common/pcp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/pcp.cpp b/src/common/pcp.cpp index 7fbc1472b4e..111a8e69d40 100644 --- a/src/common/pcp.cpp +++ b/src/common/pcp.cpp @@ -235,9 +235,9 @@ std::optional> PCPSendRecv(Sock &sock, const std::string &p } // Wait for response(s) until we get a valid response, a network error, or time out. - auto cur_time = time_point_cast(steady_clock::now()); + auto cur_time = time_point_cast(MockableSteadyClock::now()); auto deadline = cur_time + timeout_per_try; - while ((cur_time = time_point_cast(steady_clock::now())) < deadline) { + while ((cur_time = time_point_cast(MockableSteadyClock::now())) < deadline) { Sock::Event occurred = 0; if (!sock.Wait(deadline - cur_time, Sock::RECV, &occurred)) { LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "%s: Could not wait on socket: %s\n", protocol, NetworkErrorString(WSAGetLastError())); From fc700bb47fd8b6ac58f612b932aef0e361686cc3 Mon Sep 17 00:00:00 2001 From: laanwj <126646+laanwj@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:26:54 +0200 Subject: [PATCH 4/5] test: Add tests for PCP and NATPMP implementations Add a mock for a simple scriptable UDP server, and use this to test various code paths (including successful mappings, timeouts and errors) in the PCP and NATPMP implementations. --- src/test/CMakeLists.txt | 1 + src/test/pcp_tests.cpp | 581 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 src/test/pcp_tests.cpp diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 859b9132067..b9b1664b8bd 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -87,6 +87,7 @@ add_executable(test_bitcoin netbase_tests.cpp node_warnings_tests.cpp orphanage_tests.cpp + pcp_tests.cpp peerman_tests.cpp pmt_tests.cpp policy_fee_tests.cpp diff --git a/src/test/pcp_tests.cpp b/src/test/pcp_tests.cpp new file mode 100644 index 00000000000..91bf741142e --- /dev/null +++ b/src/test/pcp_tests.cpp @@ -0,0 +1,581 @@ +// Copyright (c) 2024 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 + +using namespace std::literals; + +/// UDP test server operation. +struct TestOp { + std::chrono::milliseconds delay; + enum Op { + SEND, // Expect send (with optional data) + RECV, // Expect receive (with data) + NOP, // Just delay + } op; + std::vector data; + + //! Injected error. + //! Set this field to a non-zero value to return the errno code from send or receive operation. + int error; + + TestOp(std::chrono::milliseconds delay_in, Op op_in, const std::vector &data_in, int error_in): + delay(delay_in), op(op_in), data(data_in), error(error_in) {} +}; + +/// Save the value of CreateSock and restore when the test ends. +class PCPTestingSetup : public BasicTestingSetup +{ +public: + explicit PCPTestingSetup(const ChainType chainType = ChainType::MAIN, + TestOpts opts = {}) + : BasicTestingSetup{chainType, opts}, + m_create_sock_orig{CreateSock} + { + const std::optional local_ipv4{Lookup("192.168.0.6", 1, false)}; + const std::optional local_ipv6{Lookup("2a10:1234:5678:9abc:def0:1234:5678:9abc", 1, false)}; + const std::optional gateway_ipv4{Lookup("192.168.0.1", 1, false)}; + const std::optional gateway_ipv6{Lookup("2a10:1234:5678:9abc:def0:0000:0000:0000", 1, false)}; + BOOST_REQUIRE(local_ipv4 && local_ipv6 && gateway_ipv4 && gateway_ipv6); + default_local_ipv4 = *local_ipv4; + default_local_ipv6 = *local_ipv6; + default_gateway_ipv4 = *gateway_ipv4; + default_gateway_ipv6 = *gateway_ipv6; + + struct in_addr inaddr_any; + inaddr_any.s_addr = htonl(INADDR_ANY); + bind_any_ipv4 = CNetAddr(inaddr_any); + } + + ~PCPTestingSetup() + { + CreateSock = m_create_sock_orig; + MockableSteadyClock::ClearMockTime(); + } + + // Default testing nonce. + static constexpr PCPMappingNonce TEST_NONCE{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc}; + // Default network addresses. + CNetAddr default_local_ipv4; + CNetAddr default_local_ipv6; + CNetAddr default_gateway_ipv4; + CNetAddr default_gateway_ipv6; + // IPv4 bind + CNetAddr bind_any_ipv4; +private: + const decltype(CreateSock) m_create_sock_orig; +}; + +/** Simple scripted UDP server emulation for testing. + */ +class PCPTestSock final : public Sock +{ +public: + // Note: we awkwardly mark all methods as const, and properties as mutable, + // because Sock expects all networking calls to be const. + explicit PCPTestSock(const CNetAddr &local_ip, const CNetAddr &gateway_ip, const std::vector &script) + : Sock{INVALID_SOCKET}, + m_script(script), + m_local_ip(local_ip), + m_gateway_ip(gateway_ip) + { + ElapseTime(std::chrono::seconds(0)); // start mocking steady time + PrepareOp(); + } + + PCPTestSock& operator=(Sock&& other) override + { + assert(false && "Move of Sock into PCPTestSock not allowed."); + return *this; + } + + ssize_t Send(const void* data, size_t len, int) const override { + if (!m_connected) return -1; + Span in_pkt = Span(static_cast(data), len); + if (AtEndOfScript() || CurOp().op != TestOp::SEND) { + // Ignore sends after end of script, or sends when we expect a receive. + FailScript(); + return len; + } + if (CurOp().error) return -1; // Inject failure + if (CurOp().data.empty() || std::ranges::equal(CurOp().data, in_pkt)) { + AdvanceOp(); + } else { + // Wrong send, fail script + FailScript(); + } + return len; + } + + ssize_t Recv(void* buf, size_t len, int flags) const override + { + if (!m_connected || AtEndOfScript() || CurOp().op != TestOp::RECV || m_time_left != 0s) { + return -1; + } + if (CurOp().error) return -1; // Inject failure + const auto &recv_pkt = CurOp().data; + const size_t consume_bytes{std::min(len, recv_pkt.size())}; + std::memcpy(buf, recv_pkt.data(), consume_bytes); + if ((flags & MSG_PEEK) == 0) { + AdvanceOp(); + } + return consume_bytes; + } + + int Connect(const sockaddr* sa, socklen_t sa_len) const override { + CService service; + if (service.SetSockAddr(sa, sa_len) && service == CService(m_gateway_ip, 5351)) { + if (m_bound.IsBindAny()) { // If bind-any, bind to local ip. + m_bound = CService(m_local_ip, 0); + } + if (m_bound.GetPort() == 0) { // If no port assigned, assign port 1. + m_bound = CService(m_bound, 1); + } + m_connected = true; + return 0; + } + return -1; + } + + int Bind(const sockaddr* sa, socklen_t sa_len) const override { + CService service; + if (service.SetSockAddr(sa, sa_len)) { + // Can only bind to one of our local ips + if (!service.IsBindAny() && service != m_local_ip) { + return -1; + } + m_bound = service; + return 0; + } + return -1; + } + + int Listen(int) const override { return -1; } + + std::unique_ptr Accept(sockaddr* addr, socklen_t* addr_len) const override + { + return nullptr; + }; + + int GetSockOpt(int level, int opt_name, void* opt_val, socklen_t* opt_len) const override + { + return -1; + } + + int SetSockOpt(int, int, const void*, socklen_t) const override { return 0; } + + int GetSockName(sockaddr* name, socklen_t* name_len) const override + { + // Return the address we've been bound to. + return m_bound.GetSockAddr(name, name_len) ? 0 : -1; + } + + bool SetNonBlocking() const override { return true; } + + bool IsSelectable() const override { return true; } + + bool Wait(std::chrono::milliseconds timeout, + Event requested, + Event* occurred = nullptr) const override + { + // Only handles receive events. + if (AtEndOfScript() || requested != Sock::RECV) { + ElapseTime(timeout); + } else { + std::chrono::milliseconds delay = std::min(m_time_left, timeout); + ElapseTime(delay); + m_time_left -= delay; + if (CurOp().op == TestOp::RECV && m_time_left == 0s && occurred != nullptr) { + *occurred = Sock::RECV; + } + if (CurOp().op == TestOp::NOP) { + // This was a pure delay operation, move to the next op. + AdvanceOp(); + } + } + return true; + } + + bool WaitMany(std::chrono::milliseconds timeout, EventsPerSock& events_per_sock) const override + { + return false; + } + + bool IsConnected(std::string&) const override + { + return true; + } + +private: + const std::vector m_script; + mutable size_t m_script_ptr = 0; + mutable std::chrono::milliseconds m_time_left; + mutable std::chrono::milliseconds m_time{MockableSteadyClock::INITIAL_MOCK_TIME}; + mutable bool m_connected{false}; + mutable CService m_bound; + mutable CNetAddr m_local_ip; + mutable CNetAddr m_gateway_ip; + + void ElapseTime(std::chrono::milliseconds duration) const + { + m_time += duration; + MockableSteadyClock::SetMockTime(m_time); + } + + bool AtEndOfScript() const { return m_script_ptr == m_script.size(); } + const TestOp &CurOp() const { + BOOST_REQUIRE(m_script_ptr < m_script.size()); + return m_script[m_script_ptr]; + } + + void PrepareOp() const { + if (AtEndOfScript()) return; + m_time_left = CurOp().delay; + } + + void AdvanceOp() const + { + m_script_ptr += 1; + PrepareOp(); + } + + void FailScript() const { m_script_ptr = m_script.size(); } +}; + +BOOST_FIXTURE_TEST_SUITE(pcp_tests, PCPTestingSetup) + +// NAT-PMP IPv4 good-weather scenario. +BOOST_AUTO_TEST_CASE(natpmp_ipv4) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x00, 0x00, // version, opcode (request external IP) + }, 0 + }, + { + 2ms, TestOp::RECV, + { + 0x00, 0x80, 0x00, 0x00, // version, opcode (external IP), result code (success) + 0x66, 0xfd, 0xa1, 0xee, // seconds sinds start of epoch + 0x01, 0x02, 0x03, 0x04, // external IP address + }, 0 + }, + { + 0ms, TestOp::SEND, + { + 0x00, 0x02, 0x00, 0x00, // version, opcode (request map TCP) + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x00, 0x00, 0x03, 0xe8, // requested mapping lifetime in seconds + }, 0 + }, + { + 2ms, TestOp::RECV, + { + 0x00, 0x82, 0x00, 0x00, // version, opcode (mapped TCP) + 0x66, 0xfd, 0xa1, 0xee, // seconds sinds start of epoch + 0x04, 0xd2, 0x04, 0xd2, // internal port, mapped external port + 0x00, 0x00, 0x01, 0xf4, // mapping lifetime in seconds + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + auto res = NATPMPRequestPortMap(default_gateway_ipv4, 1234, 1000, 1, 200ms); + + MappingResult* mapping = std::get_if(&res); + BOOST_REQUIRE(mapping); + BOOST_CHECK_EQUAL(mapping->version, 0); + BOOST_CHECK_EQUAL(mapping->internal.ToStringAddrPort(), "192.168.0.6:1234"); + BOOST_CHECK_EQUAL(mapping->external.ToStringAddrPort(), "1.2.3.4:1234"); + BOOST_CHECK_EQUAL(mapping->lifetime, 500); +} + +// PCP IPv4 good-weather scenario. +BOOST_AUTO_TEST_CASE(pcp_ipv4) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xa8, 0x00, 0x06, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, // suggested external IP + }, 0 + }, + { + 250ms, TestOp::RECV, // 250ms delay before answer + { + 0x02, 0x81, 0x00, 0x00, // version, opcode, result success + 0x00, 0x00, 0x01, 0xf4, // granted lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, assigned external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04, // assigned external IP + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv4, bind_any_ipv4, 1234, 1000, 1, 1000ms); + + MappingResult* mapping = std::get_if(&res); + BOOST_REQUIRE(mapping); + BOOST_CHECK_EQUAL(mapping->version, 2); + BOOST_CHECK_EQUAL(mapping->internal.ToStringAddrPort(), "192.168.0.6:1234"); + BOOST_CHECK_EQUAL(mapping->external.ToStringAddrPort(), "1.2.3.4:1234"); + BOOST_CHECK_EQUAL(mapping->lifetime, 500); +} + +// PCP IPv6 good-weather scenario. +BOOST_AUTO_TEST_CASE(pcp_ipv6) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // suggested external IP + }, 0 + }, + { + 500ms, TestOp::RECV, // 500ms delay before answer + { + 0x02, 0x81, 0x00, 0x00, // version, opcode, result success + 0x00, 0x00, 0x01, 0xf4, // granted lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, assigned external port + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // suggested external IP + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET6 && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv6, default_gateway_ipv6, script); + return std::unique_ptr(); + }; + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv6, default_local_ipv6, 1234, 1000, 1, 1000ms); + + MappingResult* mapping = std::get_if(&res); + BOOST_REQUIRE(mapping); + BOOST_CHECK_EQUAL(mapping->version, 2); + BOOST_CHECK_EQUAL(mapping->internal.ToStringAddrPort(), "[2a10:1234:5678:9abc:def0:1234:5678:9abc]:1234"); + BOOST_CHECK_EQUAL(mapping->external.ToStringAddrPort(), "[2a10:1234:5678:9abc:def0:1234:5678:9abc]:1234"); + BOOST_CHECK_EQUAL(mapping->lifetime, 500); +} + +// PCP timeout. +BOOST_AUTO_TEST_CASE(pcp_timeout) +{ + const std::vector script{}; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + ASSERT_DEBUG_LOG("pcp: Retrying (1)"); + ASSERT_DEBUG_LOG("pcp: Retrying (2)"); + ASSERT_DEBUG_LOG("pcp: Timeout"); + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv4, bind_any_ipv4, 1234, 1000, 3, 2000ms); + + MappingError* err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::NETWORK_ERROR); +} + +// PCP failure receiving (router sends ICMP port closed). +BOOST_AUTO_TEST_CASE(pcp_connrefused) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { // May send anything. + }, 0 + }, + { + 0ms, TestOp::RECV, + { + }, ECONNREFUSED + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + ASSERT_DEBUG_LOG("pcp: Could not receive response"); + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv4, bind_any_ipv4, 1234, 1000, 3, 2000ms); + + MappingError* err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::NETWORK_ERROR); +} + +// PCP IPv6 success after one timeout. +BOOST_AUTO_TEST_CASE(pcp_ipv6_timeout_success) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // suggested external IP + }, 0 + }, + { + 2001ms, TestOp::NOP, // Takes longer to respond than timeout of 2000ms + {}, 0 + }, + { + 0ms, TestOp::SEND, // Repeated send (try 2) + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // suggested external IP + }, 0 + }, + { + 200ms, TestOp::RECV, // This time we're in time + { + 0x02, 0x81, 0x00, 0x00, // version, opcode, result success + 0x00, 0x00, 0x01, 0xf4, // granted lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, assigned external port + 0x2a, 0x10, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, // suggested external IP + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET6 && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv6, default_gateway_ipv6, script); + return std::unique_ptr(); + }; + + ASSERT_DEBUG_LOG("pcp: Retrying (1)"); + ASSERT_DEBUG_LOG("pcp: Timeout"); + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv6, default_local_ipv6, 1234, 1000, 2, 2000ms); + + BOOST_CHECK(std::get_if(&res)); +} + +// PCP IPv4 failure (no resources). +BOOST_AUTO_TEST_CASE(pcp_ipv4_fail_no_resources) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xa8, 0x00, 0x06, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, // suggested external IP + }, 0 + }, + { + 500ms, TestOp::RECV, + { + 0x02, 0x81, 0x00, 0x08, // version, opcode, result 0x08: no resources + 0x00, 0x00, 0x00, 0x00, // granted lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x00, 0x00, // internal port, assigned external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // assigned external IP + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv4, bind_any_ipv4, 1234, 1000, 3, 1000ms); + + MappingError* err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::NO_RESOURCES); +} + +// PCP IPv4 failure (test NATPMP downgrade scenario). +BOOST_AUTO_TEST_CASE(pcp_ipv4_fail_unsupported_version) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xa8, 0x00, 0x06, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, // suggested external IP + }, 0 + }, + { + 500ms, TestOp::RECV, + { + 0x00, 0x81, 0x00, 0x01, // version, opcode, result 0x01: unsupported version + 0x00, 0x00, 0x00, 0x00, + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv4, bind_any_ipv4, 1234, 1000, 3, 1000ms); + + MappingError* err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::UNSUPP_VERSION); +} + +BOOST_AUTO_TEST_SUITE_END() + From 0f716f28896c6edfcd4e2a2b25c88f478a029c7b Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 13 Jan 2025 14:30:35 -0500 Subject: [PATCH 5/5] qa: cover PROTOCOL_ERROR variant in PCP unit tests --- src/test/pcp_tests.cpp | 118 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/test/pcp_tests.cpp b/src/test/pcp_tests.cpp index 91bf741142e..967bef1946c 100644 --- a/src/test/pcp_tests.cpp +++ b/src/test/pcp_tests.cpp @@ -577,5 +577,123 @@ BOOST_AUTO_TEST_CASE(pcp_ipv4_fail_unsupported_version) BOOST_CHECK_EQUAL(*err, MappingError::UNSUPP_VERSION); } +// NAT-PMP IPv4 protocol error scenarii. +BOOST_AUTO_TEST_CASE(natpmp_protocol_error) +{ + // First scenario: non-0 result code when requesting external IP. + std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x00, 0x00, // version, opcode (request external IP) + }, 0 + }, + { + 2ms, TestOp::RECV, + { + 0x00, 0x80, 0x00, 0x42, // version, opcode (external IP), result code (*NOT* success) + 0x66, 0xfd, 0xa1, 0xee, // seconds sinds start of epoch + 0x01, 0x02, 0x03, 0x04, // external IP address + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + auto res = NATPMPRequestPortMap(default_gateway_ipv4, 1234, 1000, 1, 200ms); + + MappingError* err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::PROTOCOL_ERROR); + + // First scenario: non-0 result code when requesting port mapping. + script = { + { + 0ms, TestOp::SEND, + { + 0x00, 0x00, // version, opcode (request external IP) + }, 0 + }, + { + 2ms, TestOp::RECV, + { + 0x00, 0x80, 0x00, 0x00, // version, opcode (external IP), result code (success) + 0x66, 0xfd, 0xa1, 0xee, // seconds sinds start of epoch + 0x01, 0x02, 0x03, 0x04, // external IP address + }, 0 + }, + { + 0ms, TestOp::SEND, + { + 0x00, 0x02, 0x00, 0x00, // version, opcode (request map TCP) + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x00, 0x00, 0x03, 0xe8, // requested mapping lifetime in seconds + }, 0 + }, + { + 2ms, TestOp::RECV, + { + 0x00, 0x82, 0x00, 0x43, // version, opcode (mapped TCP) + 0x66, 0xfd, 0xa1, 0xee, // seconds sinds start of epoch + 0x04, 0xd2, 0x04, 0xd2, // internal port, mapped external port + 0x00, 0x00, 0x01, 0xf4, // mapping lifetime in seconds + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + res = NATPMPRequestPortMap(default_gateway_ipv4, 1234, 1000, 1, 200ms); + + err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::PROTOCOL_ERROR); +} + +// PCP IPv4 protocol error scenario. +BOOST_AUTO_TEST_CASE(pcp_protocol_error) +{ + const std::vector script{ + { + 0ms, TestOp::SEND, + { + 0x02, 0x01, 0x00, 0x00, // version, opcode + 0x00, 0x00, 0x03, 0xe8, // lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc0, 0xa8, 0x00, 0x06, // internal IP + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, suggested external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, // suggested external IP + }, 0 + }, + { + 250ms, TestOp::RECV, // 250ms delay before answer + { + 0x02, 0x81, 0x00, 0x42, // version, opcode, result error + 0x00, 0x00, 0x01, 0xf4, // granted lifetime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, // nonce + 0x06, 0x00, 0x00, 0x00, // protocol (TCP), reserved + 0x04, 0xd2, 0x04, 0xd2, // internal port, assigned external port + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04, // assigned external IP + }, 0 + }, + }; + CreateSock = [this, &script](int domain, int type, int protocol) { + if (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP) return std::make_unique(default_local_ipv4, default_gateway_ipv4, script); + return std::unique_ptr(); + }; + + auto res = PCPRequestPortMap(TEST_NONCE, default_gateway_ipv4, bind_any_ipv4, 1234, 1000, 1, 1000ms); + + MappingError* err = std::get_if(&res); + BOOST_REQUIRE(err); + BOOST_CHECK_EQUAL(*err, MappingError::PROTOCOL_ERROR); +} + BOOST_AUTO_TEST_SUITE_END()