correct 3-bit error handling for Golay(24,12,8); add tests against network engine code; correct sendmmsg implementation against documentation for sendmmsg to better handle partially sent queues instead of discarding;
parent
03068fe382
commit
8fcd066c02
@ -0,0 +1,300 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Test Suite
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/network/FrameQueue.h"
|
||||
#include "common/network/RawFrameQueue.h"
|
||||
#include "common/network/RTPFNEHeader.h"
|
||||
#include "common/network/RTPHeader.h"
|
||||
#include "common/network/udp/Socket.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
using namespace network;
|
||||
using namespace network::frame;
|
||||
using namespace network::udp;
|
||||
|
||||
/**
|
||||
* @brief Finds an available loopback UDP port.
|
||||
* @return uint16_t A free UDP port, or 0 on failure.
|
||||
*/
|
||||
static uint16_t reserveLoopbackPort()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd == INVALID_SOCKET)
|
||||
return 0U;
|
||||
#else
|
||||
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0)
|
||||
return 0U;
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(0U);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
socklen_t addrLen = sizeof(address);
|
||||
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
return ntohs(address.sin_port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Polls a RawFrameQueue until a message arrives or retries are exhausted.
|
||||
*/
|
||||
static bool waitForRawMessage(RawFrameQueue& queue, std::string& out)
|
||||
{
|
||||
for (uint32_t attempt = 0U; attempt < 50U; attempt++) {
|
||||
int messageLength = -1;
|
||||
sockaddr_storage source = {};
|
||||
uint32_t sourceLen = 0U;
|
||||
UInt8Array message = queue.read(messageLength, source, sourceLen);
|
||||
if (message != nullptr && messageLength > 0) {
|
||||
out.assign(reinterpret_cast<char*>(message.get()), (size_t)messageLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Polls a FrameQueue until a message arrives or retries are exhausted.
|
||||
*/
|
||||
static bool waitForFrameMessage(FrameQueue& queue, std::string& out, RTPHeader& rtpHeader, RTPFNEHeader& fneHeader)
|
||||
{
|
||||
for (uint32_t attempt = 0U; attempt < 50U; attempt++) {
|
||||
int messageLength = -1;
|
||||
sockaddr_storage source = {};
|
||||
uint32_t sourceLen = 0U;
|
||||
UInt8Array message = queue.read(messageLength, source, sourceLen, &rtpHeader, &fneHeader);
|
||||
if (message != nullptr && messageLength > 0) {
|
||||
out.assign(reinterpret_cast<char*>(message.get()), (size_t)messageLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST_CASE("RawFrameQueue writes and reads loopback payloads", "[network][framequeue][raw]")
|
||||
{
|
||||
const uint16_t receiverPort = reserveLoopbackPort();
|
||||
REQUIRE(receiverPort != 0U);
|
||||
|
||||
Socket receiverSocket(receiverPort);
|
||||
REQUIRE(receiverSocket.open(AF_INET));
|
||||
|
||||
Socket senderSocket;
|
||||
REQUIRE(senderSocket.open(AF_INET));
|
||||
|
||||
RawFrameQueue receiverQueue(&receiverSocket, false);
|
||||
RawFrameQueue senderQueue(&senderSocket, false);
|
||||
|
||||
sockaddr_storage destination = {};
|
||||
uint32_t destinationLen = 0U;
|
||||
REQUIRE(Socket::lookup("127.0.0.1", receiverPort, destination, destinationLen) == 0);
|
||||
|
||||
const std::string payload = "raw-frame-loopback";
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(senderQueue.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size(), destination, destinationLen, &bytesWritten));
|
||||
REQUIRE(bytesWritten == (ssize_t)payload.size());
|
||||
|
||||
std::string received;
|
||||
REQUIRE(waitForRawMessage(receiverQueue, received));
|
||||
REQUIRE(received == payload);
|
||||
|
||||
senderSocket.close();
|
||||
receiverSocket.close();
|
||||
}
|
||||
|
||||
TEST_CASE("RawFrameQueue enqueues and flushes buffered payloads", "[network][framequeue][raw]")
|
||||
{
|
||||
const uint16_t receiverPort = reserveLoopbackPort();
|
||||
REQUIRE(receiverPort != 0U);
|
||||
|
||||
Socket receiverSocket(receiverPort);
|
||||
REQUIRE(receiverSocket.open(AF_INET));
|
||||
|
||||
Socket senderSocket;
|
||||
REQUIRE(senderSocket.open(AF_INET));
|
||||
|
||||
RawFrameQueue receiverQueue(&receiverSocket, false);
|
||||
RawFrameQueue senderQueue(&senderSocket, false);
|
||||
|
||||
sockaddr_storage destination = {};
|
||||
uint32_t destinationLen = 0U;
|
||||
REQUIRE(Socket::lookup("127.0.0.1", receiverPort, destination, destinationLen) == 0);
|
||||
|
||||
BufferQueue queue;
|
||||
const std::string first = "raw-queued-one";
|
||||
const std::string second = "raw-queued-two";
|
||||
senderQueue.enqueueMessage(&queue, reinterpret_cast<const uint8_t*>(first.data()), first.size(), destination, destinationLen);
|
||||
senderQueue.enqueueMessage(&queue, reinterpret_cast<const uint8_t*>(second.data()), second.size(), destination, destinationLen);
|
||||
|
||||
REQUIRE(queue.size() == 2U);
|
||||
REQUIRE(senderQueue.flushQueue(&queue));
|
||||
REQUIRE(queue.empty());
|
||||
|
||||
std::string firstReceived;
|
||||
std::string secondReceived;
|
||||
REQUIRE(waitForRawMessage(receiverQueue, firstReceived));
|
||||
REQUIRE(waitForRawMessage(receiverQueue, secondReceived));
|
||||
REQUIRE(firstReceived == first);
|
||||
REQUIRE(secondReceived == second);
|
||||
|
||||
senderSocket.close();
|
||||
receiverSocket.close();
|
||||
}
|
||||
|
||||
TEST_CASE("FrameQueue writes decodable RTP FNE payloads", "[network][framequeue]")
|
||||
{
|
||||
const uint16_t receiverPort = reserveLoopbackPort();
|
||||
REQUIRE(receiverPort != 0U);
|
||||
|
||||
Socket receiverSocket(receiverPort);
|
||||
REQUIRE(receiverSocket.open(AF_INET));
|
||||
|
||||
Socket senderSocket;
|
||||
REQUIRE(senderSocket.open(AF_INET));
|
||||
|
||||
FrameQueue senderQueue(&senderSocket, 1001U, false);
|
||||
FrameQueue receiverQueue(&receiverSocket, 2002U, false);
|
||||
senderQueue.clearTimestamps();
|
||||
|
||||
sockaddr_storage destination = {};
|
||||
uint32_t destinationLen = 0U;
|
||||
REQUIRE(Socket::lookup("127.0.0.1", receiverPort, destination, destinationLen) == 0);
|
||||
|
||||
const std::string payload = "framequeue-rtp-message";
|
||||
const uint32_t streamId = 0x10203040U;
|
||||
const uint32_t peerId = 0x000F4240U;
|
||||
const uint32_t ssrc = 0x01020304U;
|
||||
const uint16_t sequence = 321U;
|
||||
FrameQueue::OpcodePair opcode = { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 };
|
||||
|
||||
REQUIRE(senderQueue.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size(), streamId, peerId,
|
||||
ssrc, opcode, sequence, destination, destinationLen));
|
||||
|
||||
std::string received;
|
||||
RTPHeader rtpHeader = RTPHeader();
|
||||
RTPFNEHeader fneHeader = RTPFNEHeader();
|
||||
REQUIRE(waitForFrameMessage(receiverQueue, received, rtpHeader, fneHeader));
|
||||
|
||||
REQUIRE(received == payload);
|
||||
REQUIRE(rtpHeader.getExtension());
|
||||
REQUIRE(rtpHeader.getPayloadType() == DVM_RTP_PAYLOAD_TYPE);
|
||||
REQUIRE(rtpHeader.getSequence() == sequence);
|
||||
REQUIRE(rtpHeader.getSSRC() == ssrc);
|
||||
REQUIRE(rtpHeader.getTimestamp() != INVALID_TS);
|
||||
|
||||
REQUIRE(fneHeader.getStreamId() == streamId);
|
||||
REQUIRE(fneHeader.getPeerId() == peerId);
|
||||
REQUIRE(fneHeader.getFunction() == opcode.first);
|
||||
REQUIRE(fneHeader.getSubFunction() == opcode.second);
|
||||
REQUIRE(fneHeader.getMessageLength() == payload.size());
|
||||
|
||||
senderSocket.close();
|
||||
receiverSocket.close();
|
||||
}
|
||||
|
||||
TEST_CASE("FrameQueue enqueues and flushes ordered RTP FNE payloads", "[network][framequeue]")
|
||||
{
|
||||
const uint16_t receiverPort = reserveLoopbackPort();
|
||||
REQUIRE(receiverPort != 0U);
|
||||
|
||||
Socket receiverSocket(receiverPort);
|
||||
REQUIRE(receiverSocket.open(AF_INET));
|
||||
|
||||
Socket senderSocket;
|
||||
REQUIRE(senderSocket.open(AF_INET));
|
||||
|
||||
FrameQueue senderQueue(&senderSocket, 3003U, false);
|
||||
FrameQueue receiverQueue(&receiverSocket, 4004U, false);
|
||||
senderQueue.clearTimestamps();
|
||||
|
||||
sockaddr_storage destination = {};
|
||||
uint32_t destinationLen = 0U;
|
||||
REQUIRE(Socket::lookup("127.0.0.1", receiverPort, destination, destinationLen) == 0);
|
||||
|
||||
BufferQueue queue;
|
||||
const std::string first = "frame-queued-one";
|
||||
const std::string second = "frame-queued-two";
|
||||
const uint32_t streamId = 0x22334455U;
|
||||
const uint32_t peerId = 0x00001000U;
|
||||
const uint32_t ssrc = 0x0A0B0C0DU;
|
||||
FrameQueue::OpcodePair opcode = { NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS };
|
||||
|
||||
senderQueue.enqueueMessage(&queue, reinterpret_cast<const uint8_t*>(first.data()), first.size(), streamId,
|
||||
peerId, ssrc, opcode, 500U, destination, destinationLen);
|
||||
senderQueue.enqueueMessage(&queue, reinterpret_cast<const uint8_t*>(second.data()), second.size(), streamId,
|
||||
peerId, ssrc, opcode, 501U, destination, destinationLen);
|
||||
|
||||
REQUIRE(queue.size() == 2U);
|
||||
REQUIRE(senderQueue.flushQueue(&queue));
|
||||
REQUIRE(queue.empty());
|
||||
|
||||
std::string firstReceived;
|
||||
std::string secondReceived;
|
||||
RTPHeader firstRtp = RTPHeader();
|
||||
RTPHeader secondRtp = RTPHeader();
|
||||
RTPFNEHeader firstFne = RTPFNEHeader();
|
||||
RTPFNEHeader secondFne = RTPFNEHeader();
|
||||
|
||||
REQUIRE(waitForFrameMessage(receiverQueue, firstReceived, firstRtp, firstFne));
|
||||
REQUIRE(waitForFrameMessage(receiverQueue, secondReceived, secondRtp, secondFne));
|
||||
|
||||
REQUIRE(firstReceived == first);
|
||||
REQUIRE(secondReceived == second);
|
||||
|
||||
REQUIRE(firstRtp.getSequence() == 500U);
|
||||
REQUIRE(secondRtp.getSequence() == 501U);
|
||||
REQUIRE(secondRtp.getTimestamp() > firstRtp.getTimestamp());
|
||||
|
||||
REQUIRE(firstFne.getStreamId() == streamId);
|
||||
REQUIRE(secondFne.getStreamId() == streamId);
|
||||
REQUIRE(firstFne.getPeerId() == peerId);
|
||||
REQUIRE(secondFne.getPeerId() == peerId);
|
||||
|
||||
senderSocket.close();
|
||||
receiverSocket.close();
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Test Suite
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/json/json.h"
|
||||
#include "common/network/NetRPC.h"
|
||||
#include "common/network/RPCHeader.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
using namespace network;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global Functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Finds an available loopback UDP port.
|
||||
* @return uint16_t A free UDP port, or 0 on failure.
|
||||
*/
|
||||
static uint16_t reserveLoopbackPort()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd == INVALID_SOCKET)
|
||||
return 0U;
|
||||
#else
|
||||
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0)
|
||||
return 0U;
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(0U);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
socklen_t addrLen = sizeof(address);
|
||||
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
return ntohs(address.sin_port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pumps two NetRPC clocks until a condition is met.
|
||||
* @param first The first NetRPC instance to clock.
|
||||
* @param second The second NetRPC instance to clock.
|
||||
* @param done A callable that returns true when the pumping should stop.
|
||||
* @returns True if the pumping stopped because the condition was met, false if the maximum number of iterations was reached.
|
||||
*/
|
||||
static bool pumpRPCUntil(NetRPC& first, NetRPC& second, const std::function<bool()>& done)
|
||||
{
|
||||
for (uint32_t i = 0U; i < 120U; i++) {
|
||||
first.clock(1U);
|
||||
second.clock(1U);
|
||||
if (done())
|
||||
return true;
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST_CASE("NetRPC defaultResponse populates status and message", "[network][rpc][netrpc]")
|
||||
{
|
||||
const uint16_t localPort = reserveLoopbackPort();
|
||||
REQUIRE(localPort != 0U);
|
||||
|
||||
NetRPC rpc("127.0.0.1", localPort, localPort, "test-password", false);
|
||||
|
||||
json::object reply;
|
||||
rpc.defaultResponse(reply, "invalid arguments", NetRPC::INVALID_ARGS);
|
||||
|
||||
REQUIRE(reply["status"].is<int>());
|
||||
REQUIRE(reply["status"].get<int>() == (int)NetRPC::INVALID_ARGS);
|
||||
REQUIRE(reply["message"].is<std::string>());
|
||||
REQUIRE(reply["message"].get<std::string>() == "invalid arguments");
|
||||
}
|
||||
|
||||
TEST_CASE("NetRPC register and unregister handler guards duplicate and out-of-range IDs", "[network][rpc][netrpc]")
|
||||
{
|
||||
const uint16_t localPort = reserveLoopbackPort();
|
||||
REQUIRE(localPort != 0U);
|
||||
|
||||
NetRPC rpc("127.0.0.1", localPort, localPort, "test-password", false);
|
||||
|
||||
NetRPC::RPCType handler = [](json::object& request, json::object& reply) {
|
||||
(void)request;
|
||||
int ok = (int)NetRPC::OK;
|
||||
reply["status"].set<int>(ok);
|
||||
};
|
||||
|
||||
REQUIRE(rpc.registerHandler(0x0123U, handler));
|
||||
REQUIRE_FALSE(rpc.registerHandler(0x0123U, handler));
|
||||
REQUIRE(rpc.unregisterHandler(0x0123U));
|
||||
REQUIRE_FALSE(rpc.unregisterHandler(0x0123U));
|
||||
|
||||
REQUIRE_FALSE(rpc.registerHandler(RPC_MAX_FUNC + 1U, handler));
|
||||
REQUIRE_FALSE(rpc.unregisterHandler(RPC_MAX_FUNC + 1U));
|
||||
}
|
||||
|
||||
TEST_CASE("NetRPC refuses to send requests to itself", "[network][rpc][netrpc]")
|
||||
{
|
||||
const uint16_t localPort = reserveLoopbackPort();
|
||||
REQUIRE(localPort != 0U);
|
||||
|
||||
NetRPC rpc("127.0.0.1", localPort, localPort, "test-password", false);
|
||||
|
||||
json::object request;
|
||||
request["op"].set<std::string>("self-test");
|
||||
|
||||
REQUIRE_FALSE(rpc.req(0x0100U, request, NetRPC::RPCType(), "127.0.0.1", localPort, false));
|
||||
}
|
||||
|
||||
TEST_CASE("NetRPC request receives JSON reply from registered remote handler", "[network][rpc][netrpc]")
|
||||
{
|
||||
const uint16_t serverPort = reserveLoopbackPort();
|
||||
const uint16_t clientPort = reserveLoopbackPort();
|
||||
REQUIRE(serverPort != 0U);
|
||||
REQUIRE(clientPort != 0U);
|
||||
REQUIRE(serverPort != clientPort);
|
||||
|
||||
NetRPC server("127.0.0.1", serverPort, serverPort, "shared-password", false);
|
||||
NetRPC client("127.0.0.1", clientPort, clientPort, "shared-password", false);
|
||||
|
||||
REQUIRE(server.open());
|
||||
REQUIRE(client.open());
|
||||
|
||||
bool serverCalled = false;
|
||||
bool clientReplyCalled = false;
|
||||
std::string echoed;
|
||||
|
||||
REQUIRE(server.registerHandler(0x2222U, [&](json::object& request, json::object& reply) {
|
||||
serverCalled = true;
|
||||
|
||||
std::string op = "";
|
||||
if (request["op"].is<std::string>())
|
||||
op = request["op"].get<std::string>();
|
||||
|
||||
int status = (int)NetRPC::OK;
|
||||
reply["status"].set<int>(status);
|
||||
reply["message"].set<std::string>("ok");
|
||||
reply["echo"].set<std::string>(op);
|
||||
}));
|
||||
|
||||
json::object request;
|
||||
request["op"].set<std::string>("ping");
|
||||
|
||||
REQUIRE(client.req(0x2222U, request,
|
||||
[&](json::object& response, json::object& unused) {
|
||||
(void)unused;
|
||||
if (!response["status"].is<int>())
|
||||
return;
|
||||
|
||||
if (response["status"].get<int>() != (int)NetRPC::OK)
|
||||
return;
|
||||
|
||||
if (!response["echo"].is<std::string>())
|
||||
return;
|
||||
|
||||
echoed = response["echo"].get<std::string>();
|
||||
clientReplyCalled = true;
|
||||
},
|
||||
"127.0.0.1", serverPort, false));
|
||||
|
||||
REQUIRE(pumpRPCUntil(server, client, [&]() { return clientReplyCalled; }));
|
||||
REQUIRE(serverCalled);
|
||||
REQUIRE(clientReplyCalled);
|
||||
REQUIRE(echoed == "ping");
|
||||
|
||||
server.close();
|
||||
client.close();
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Test Suite
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/network/RPCHeader.h"
|
||||
#include "common/network/RTPExtensionHeader.h"
|
||||
#include "common/network/RTPFNEHeader.h"
|
||||
#include "common/network/RTPHeader.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
using namespace network;
|
||||
using namespace network::frame;
|
||||
|
||||
class TestRTPExtensionHeader : public RTPExtensionHeader {
|
||||
public:
|
||||
void setType(uint16_t type) { setPayloadType(type); }
|
||||
void setLength(uint16_t length) { setPayloadLength(length); }
|
||||
uint16_t type() const { return m_payloadType; }
|
||||
uint16_t length() const { return m_payloadLength; }
|
||||
};
|
||||
|
||||
TEST_CASE("RTPHeader encodes and decodes all fields", "[network][rtp][header]")
|
||||
{
|
||||
const uint8_t payloadType = 0x56U;
|
||||
|
||||
RTPHeader encoded = RTPHeader();
|
||||
encoded.setExtension(true);
|
||||
encoded.setMarker(true);
|
||||
encoded.setPayloadType(payloadType);
|
||||
encoded.setSequence(0x1234U);
|
||||
encoded.setTimestamp(0x11223344U);
|
||||
encoded.setSSRC(0x55667788U);
|
||||
|
||||
std::array<uint8_t, RTP_HEADER_LENGTH_BYTES> buffer = {};
|
||||
encoded.encode(buffer.data());
|
||||
|
||||
RTPHeader decoded = RTPHeader();
|
||||
REQUIRE(decoded.decode(buffer.data()));
|
||||
|
||||
REQUIRE(decoded.getVersion() == 2U);
|
||||
REQUIRE(decoded.getPadding() == false);
|
||||
REQUIRE(decoded.getExtension() == true);
|
||||
REQUIRE(decoded.getCSRCCount() == 0U);
|
||||
REQUIRE(decoded.getMarker() == true);
|
||||
REQUIRE(decoded.getPayloadType() == payloadType);
|
||||
REQUIRE(decoded.getSequence() == 0x1234U);
|
||||
REQUIRE(decoded.getTimestamp() == 0x11223344U);
|
||||
REQUIRE(decoded.getSSRC() == 0x55667788U);
|
||||
}
|
||||
|
||||
TEST_CASE("RTPHeader rejects invalid RTP version", "[network][rtp][header]")
|
||||
{
|
||||
std::array<uint8_t, RTP_HEADER_LENGTH_BYTES> buffer = {};
|
||||
buffer[0U] = 0x40U;
|
||||
|
||||
RTPHeader decoded = RTPHeader();
|
||||
REQUIRE_FALSE(decoded.decode(buffer.data()));
|
||||
}
|
||||
|
||||
TEST_CASE("RTPExtensionHeader encodes and decodes payload metadata", "[network][rtp][extension]")
|
||||
{
|
||||
TestRTPExtensionHeader encoded = TestRTPExtensionHeader();
|
||||
encoded.setType(0xF00DU);
|
||||
encoded.setLength(0x0004U);
|
||||
|
||||
std::array<uint8_t, RTP_EXTENSION_HEADER_LENGTH_BYTES> buffer = {};
|
||||
encoded.encode(buffer.data());
|
||||
|
||||
TestRTPExtensionHeader decoded = TestRTPExtensionHeader();
|
||||
REQUIRE(decoded.decode(buffer.data()));
|
||||
REQUIRE(decoded.type() == 0xF00DU);
|
||||
REQUIRE(decoded.length() == 0x0004U);
|
||||
}
|
||||
|
||||
TEST_CASE("RTPFNEHeader encodes and decodes frame metadata", "[network][rtp][fne]")
|
||||
{
|
||||
RTPFNEHeader encoded = RTPFNEHeader();
|
||||
encoded.setCRC(0xA1B2U);
|
||||
encoded.setFunction(NET_FUNC::TRANSFER);
|
||||
encoded.setSubFunction(NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS);
|
||||
encoded.setStreamId(0x10203040U);
|
||||
encoded.setPeerId(0x55667788U);
|
||||
encoded.setMessageLength(123U);
|
||||
|
||||
std::array<uint8_t, RTP_EXTENSION_HEADER_LENGTH_BYTES + RTP_FNE_HEADER_LENGTH_BYTES> buffer = {};
|
||||
encoded.encode(buffer.data());
|
||||
|
||||
RTPFNEHeader decoded = RTPFNEHeader();
|
||||
REQUIRE(decoded.decode(buffer.data()));
|
||||
REQUIRE(decoded.getCRC() == 0xA1B2U);
|
||||
REQUIRE(decoded.getFunction() == NET_FUNC::TRANSFER);
|
||||
REQUIRE(decoded.getSubFunction() == NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS);
|
||||
REQUIRE(decoded.getStreamId() == 0x10203040U);
|
||||
REQUIRE(decoded.getPeerId() == 0x55667788U);
|
||||
REQUIRE(decoded.getMessageLength() == 123U);
|
||||
}
|
||||
|
||||
TEST_CASE("RTPFNEHeader rejects invalid extension payload shape", "[network][rtp][fne]")
|
||||
{
|
||||
std::array<uint8_t, RTP_EXTENSION_HEADER_LENGTH_BYTES + RTP_FNE_HEADER_LENGTH_BYTES> buffer = {};
|
||||
|
||||
RTPFNEHeader baseline = RTPFNEHeader();
|
||||
baseline.setCRC(0x0102U);
|
||||
baseline.setFunction(NET_FUNC::PROTOCOL);
|
||||
baseline.setSubFunction(NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR);
|
||||
baseline.setStreamId(1U);
|
||||
baseline.setPeerId(2U);
|
||||
baseline.setMessageLength(3U);
|
||||
baseline.encode(buffer.data());
|
||||
|
||||
buffer[0U] = 0x00U;
|
||||
buffer[1U] = 0x00U;
|
||||
|
||||
RTPFNEHeader decoded = RTPFNEHeader();
|
||||
REQUIRE_FALSE(decoded.decode(buffer.data()));
|
||||
|
||||
baseline.encode(buffer.data());
|
||||
buffer[2U] = 0x00U;
|
||||
buffer[3U] = 0x03U;
|
||||
REQUIRE_FALSE(decoded.decode(buffer.data()));
|
||||
}
|
||||
|
||||
TEST_CASE("RPCHeader encodes and decodes frame metadata", "[network][rpc][header]")
|
||||
{
|
||||
RPCHeader encoded = RPCHeader();
|
||||
encoded.setCRC(0xBEEFU);
|
||||
encoded.setFunction(0x1234U);
|
||||
encoded.setMessageLength(0x10203040U);
|
||||
|
||||
std::array<uint8_t, RPC_HEADER_LENGTH_BYTES> buffer = {};
|
||||
encoded.encode(buffer.data());
|
||||
|
||||
RPCHeader decoded = RPCHeader();
|
||||
REQUIRE(decoded.decode(buffer.data()));
|
||||
REQUIRE(decoded.getCRC() == 0xBEEFU);
|
||||
REQUIRE(decoded.getFunction() == 0x1234U);
|
||||
REQUIRE(decoded.getMessageLength() == 0x10203040U);
|
||||
}
|
||||
@ -0,0 +1,208 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Test Suite
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/network/tcp/Socket.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
using namespace network::tcp;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global Functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Finds an available loopback TCP port.
|
||||
* @return uint16_t A free TCP port, or 0 on failure.
|
||||
*/
|
||||
static uint16_t reserveLoopbackPort()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SOCKET fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd == INVALID_SOCKET)
|
||||
return 0U;
|
||||
#else
|
||||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return 0U;
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(0U);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
socklen_t addrLen = sizeof(address);
|
||||
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
return ntohs(address.sin_port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Waits for a pending TCP accept and returns the accepted file descriptor.
|
||||
*/
|
||||
static int acceptWithRetry(Socket& server, sockaddr_storage& remote, socklen_t& remoteLen)
|
||||
{
|
||||
for (uint32_t attempt = 0U; attempt < 100U; attempt++) {
|
||||
int fd = server.accept(reinterpret_cast<sockaddr*>(&remote), &remoteLen);
|
||||
if (fd >= 0)
|
||||
return fd;
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a single TCP message with bounded retries.
|
||||
*/
|
||||
static bool readWithRetry(Socket& socket, const std::string& expected)
|
||||
{
|
||||
for (uint32_t attempt = 0U; attempt < 100U; attempt++) {
|
||||
uint8_t buffer[256U] = { 0U };
|
||||
ssize_t len = socket.read(buffer, sizeof(buffer));
|
||||
if (len < 0)
|
||||
return false;
|
||||
|
||||
if (len > 0) {
|
||||
if ((size_t)len != expected.size())
|
||||
return false;
|
||||
|
||||
return ::memcmp(buffer, expected.data(), expected.size()) == 0;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST_CASE("TCP socket exchanges data over loopback", "[network][tcp]")
|
||||
{
|
||||
const uint16_t port = reserveLoopbackPort();
|
||||
REQUIRE(port != 0U);
|
||||
|
||||
Socket server(AF_INET, SOCK_STREAM, 0);
|
||||
REQUIRE(server.listen("127.0.0.1", port, 1) == 0);
|
||||
|
||||
Socket client(AF_INET, SOCK_STREAM, 0);
|
||||
REQUIRE(client.connect("127.0.0.1", port));
|
||||
|
||||
sockaddr_storage remote = {};
|
||||
socklen_t remoteLen = sizeof(remote);
|
||||
int acceptedFd = acceptWithRetry(server, remote, remoteLen);
|
||||
REQUIRE(acceptedFd >= 0);
|
||||
|
||||
Socket accepted(acceptedFd);
|
||||
|
||||
const std::string clientToServer = "tcp-client-to-server";
|
||||
REQUIRE(client.write(reinterpret_cast<const uint8_t*>(clientToServer.data()), clientToServer.size()) == (ssize_t)clientToServer.size());
|
||||
REQUIRE(readWithRetry(accepted, clientToServer));
|
||||
|
||||
const std::string serverToClient = "tcp-server-to-client";
|
||||
REQUIRE(accepted.write(reinterpret_cast<const uint8_t*>(serverToClient.data()), serverToClient.size()) == (ssize_t)serverToClient.size());
|
||||
REQUIRE(readWithRetry(client, serverToClient));
|
||||
|
||||
REQUIRE(Socket::address(remote) == "127.0.0.1");
|
||||
REQUIRE(Socket::port(remote) > 0U);
|
||||
REQUIRE(Socket::addr(remote) == htonl(INADDR_LOOPBACK));
|
||||
}
|
||||
|
||||
TEST_CASE("TCP socket read returns zero when no data is ready", "[network][tcp]")
|
||||
{
|
||||
const uint16_t port = reserveLoopbackPort();
|
||||
REQUIRE(port != 0U);
|
||||
|
||||
Socket server(AF_INET, SOCK_STREAM, 0);
|
||||
REQUIRE(server.listen("127.0.0.1", port, 1) == 0);
|
||||
|
||||
Socket client(AF_INET, SOCK_STREAM, 0);
|
||||
REQUIRE(client.connect("127.0.0.1", port));
|
||||
|
||||
sockaddr_storage remote = {};
|
||||
socklen_t remoteLen = sizeof(remote);
|
||||
int acceptedFd = acceptWithRetry(server, remote, remoteLen);
|
||||
REQUIRE(acceptedFd >= 0);
|
||||
|
||||
Socket accepted(acceptedFd);
|
||||
|
||||
uint8_t buffer[32U] = { 0U };
|
||||
ssize_t len = accepted.read(buffer, sizeof(buffer));
|
||||
REQUIRE(len == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("TCP helper detects INADDR_NONE", "[network][tcp]")
|
||||
{
|
||||
sockaddr_storage noneAddr = {};
|
||||
sockaddr_in* in = reinterpret_cast<sockaddr_in*>(&noneAddr);
|
||||
in->sin_family = AF_INET;
|
||||
in->sin_addr.s_addr = htonl(INADDR_NONE);
|
||||
in->sin_port = htons(12345U);
|
||||
|
||||
REQUIRE(Socket::isNone(noneAddr));
|
||||
|
||||
sockaddr_storage loopbackAddr = {};
|
||||
sockaddr_in* loop = reinterpret_cast<sockaddr_in*>(&loopbackAddr);
|
||||
loop->sin_family = AF_INET;
|
||||
loop->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
loop->sin_port = htons(54321U);
|
||||
|
||||
REQUIRE_FALSE(Socket::isNone(loopbackAddr));
|
||||
REQUIRE(Socket::address(loopbackAddr) == "127.0.0.1");
|
||||
REQUIRE(Socket::port(loopbackAddr) == 54321U);
|
||||
}
|
||||
|
||||
TEST_CASE("TCP operations fail on uninitialized socket descriptor", "[network][tcp]")
|
||||
{
|
||||
Socket socket;
|
||||
|
||||
sockaddr_storage target = {};
|
||||
sockaddr_in* in = reinterpret_cast<sockaddr_in*>(&target);
|
||||
in->sin_family = AF_INET;
|
||||
in->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
in->sin_port = htons(6553U);
|
||||
|
||||
const std::string payload = "payload";
|
||||
uint8_t buffer[16U] = { 0U };
|
||||
|
||||
REQUIRE_FALSE(socket.connect("127.0.0.1", 6553U));
|
||||
REQUIRE(socket.listen("127.0.0.1", 6553U, 1) == -1);
|
||||
REQUIRE(socket.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size()) == -1);
|
||||
REQUIRE(socket.read(buffer, sizeof(buffer)) == -1);
|
||||
}
|
||||
@ -0,0 +1,589 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Digital Voice Modem - Test Suite
|
||||
* GPLv2 Open Source. Use is subject to license terms.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER.
|
||||
*
|
||||
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
|
||||
*
|
||||
*/
|
||||
#include "Defines.h"
|
||||
#include "common/network/udp/Socket.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace network::udp;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Network Loopback implementation for testing.
|
||||
*/
|
||||
class LoopbackNetwork {
|
||||
public:
|
||||
/**
|
||||
* @brief Initializes a new instance of the LoopbackNetwork class.
|
||||
*/
|
||||
LoopbackNetwork() :
|
||||
m_fd(INVALID_NATIVE_SOCKET),
|
||||
m_address(),
|
||||
m_addrLen(0U),
|
||||
m_wsaStarted(false)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
WSAData data;
|
||||
if (::WSAStartup(MAKEWORD(2, 2), &data) != 0)
|
||||
return;
|
||||
m_wsaStarted = true;
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
m_fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (m_fd == INVALID_NATIVE_SOCKET)
|
||||
return;
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(0U);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
if (::bind(m_fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
|
||||
#if defined(_WIN32)
|
||||
if (m_fd != INVALID_NATIVE_SOCKET)
|
||||
::closesocket(m_fd);
|
||||
#else
|
||||
if (m_fd != INVALID_NATIVE_SOCKET)
|
||||
::close(m_fd);
|
||||
#endif // defined(_WIN32)
|
||||
m_fd = INVALID_NATIVE_SOCKET;
|
||||
return;
|
||||
}
|
||||
|
||||
socklen_t addrLen = sizeof(address);
|
||||
if (::getsockname(m_fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
|
||||
#if defined(_WIN32)
|
||||
if (m_fd != INVALID_NATIVE_SOCKET)
|
||||
::closesocket(m_fd);
|
||||
#else
|
||||
if (m_fd != INVALID_NATIVE_SOCKET)
|
||||
::close(m_fd);
|
||||
#endif // defined(_WIN32)
|
||||
m_fd = INVALID_NATIVE_SOCKET;
|
||||
return;
|
||||
}
|
||||
|
||||
::memcpy(&m_address, &address, sizeof(address));
|
||||
m_addrLen = (uint32_t)addrLen;
|
||||
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout = 250U;
|
||||
::setsockopt(m_fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeout), sizeof(timeout));
|
||||
#else
|
||||
timeval timeout = {};
|
||||
timeout.tv_usec = 250000;
|
||||
::setsockopt(m_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
#endif // defined(_WIN32)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finalizes an instance of the LoopbackNetwork class.
|
||||
*/
|
||||
~LoopbackNetwork()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
if (m_fd != INVALID_NATIVE_SOCKET)
|
||||
::closesocket(m_fd);
|
||||
if (m_wsaStarted)
|
||||
::WSACleanup();
|
||||
#else
|
||||
if (m_fd != INVALID_NATIVE_SOCKET)
|
||||
::close(m_fd);
|
||||
#endif // defined(_WIN32)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if the LoopbackNetwork instance is valid.
|
||||
* A valid instance has a properly initialized socket.
|
||||
* @return bool True if the instance is valid, false otherwise.
|
||||
*/
|
||||
bool valid() const { return m_fd != INVALID_NATIVE_SOCKET; }
|
||||
|
||||
/**
|
||||
* @brief Gets the address information of the LoopbackNetwork instance.
|
||||
* @return const sockaddr_storage& The address information of the LoopbackNetwork instance.
|
||||
*/
|
||||
const sockaddr_storage& address() const { return m_address; }
|
||||
|
||||
/**
|
||||
* @brief Gets the length of the address information of the LoopbackNetwork instance.
|
||||
* @return uint32_t The length of the address information of the LoopbackNetwork instance.
|
||||
*/
|
||||
uint32_t addrLen() const { return m_addrLen; }
|
||||
|
||||
/**
|
||||
* @brief Receives a UDP datagram and compares its payload with the expected string.
|
||||
* @param expected The expected payload string.
|
||||
* @return bool True if the received payload matches the expected string, false otherwise.
|
||||
*/
|
||||
bool receive(const std::string& expected)
|
||||
{
|
||||
uint8_t buffer[256U] = { 0U };
|
||||
sockaddr_storage source = {};
|
||||
socklen_t sourceLen = sizeof(source);
|
||||
ssize_t length = ::recvfrom(m_fd, reinterpret_cast<char*>(buffer), sizeof(buffer), 0,
|
||||
reinterpret_cast<sockaddr*>(&source), &sourceLen);
|
||||
|
||||
if (length < 0)
|
||||
return false;
|
||||
|
||||
if ((size_t)length != expected.size())
|
||||
return false;
|
||||
|
||||
return ::memcmp(buffer, expected.data(), expected.size()) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Receives a UDP datagram into a raw byte buffer.
|
||||
* @param[out] payload The datagram payload bytes.
|
||||
* @return bool True if a datagram was received, false otherwise.
|
||||
*/
|
||||
bool receiveBytes(std::vector<uint8_t>& payload)
|
||||
{
|
||||
uint8_t buffer[256U] = { 0U };
|
||||
sockaddr_storage source = {};
|
||||
socklen_t sourceLen = sizeof(source);
|
||||
ssize_t length = ::recvfrom(m_fd, reinterpret_cast<char*>(buffer), sizeof(buffer), 0,
|
||||
reinterpret_cast<sockaddr*>(&source), &sourceLen);
|
||||
|
||||
if (length <= 0)
|
||||
return false;
|
||||
|
||||
payload.assign(buffer, buffer + length);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
#if defined(_WIN32)
|
||||
static constexpr SOCKET INVALID_NATIVE_SOCKET = INVALID_SOCKET;
|
||||
SOCKET m_fd;
|
||||
#else
|
||||
static constexpr int INVALID_NATIVE_SOCKET = -1;
|
||||
int m_fd;
|
||||
#endif // defined(_WIN32)
|
||||
sockaddr_storage m_address;
|
||||
uint32_t m_addrLen;
|
||||
bool m_wsaStarted;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global Functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Creates a new UDPDatagram with the given payload and address information.
|
||||
* @param payload The data to be sent in the UDP datagram.
|
||||
* @param address The destination address for the UDP datagram.
|
||||
* @param addrLen The length of the destination address.
|
||||
* @return UDPDatagram* A pointer to the newly created UDPDatagram.
|
||||
*/
|
||||
UDPDatagram* makeDatagram(const std::string& payload, const sockaddr_storage& address, uint32_t addrLen)
|
||||
{
|
||||
UDPDatagram* datagram = new UDPDatagram;
|
||||
datagram->buffer = new uint8_t[payload.size()];
|
||||
|
||||
::memcpy(datagram->buffer, payload.data(), payload.size());
|
||||
|
||||
datagram->length = payload.size();
|
||||
datagram->address = address;
|
||||
datagram->addrLen = addrLen;
|
||||
return datagram;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the encrypted datagram payload size, including packet magic.
|
||||
* @param payloadLength Plaintext payload length.
|
||||
* @return size_t Encrypted payload length.
|
||||
*/
|
||||
size_t encryptedLength(size_t payloadLength)
|
||||
{
|
||||
size_t padded = ((payloadLength + crypto::AES::BLOCK_BYTES_LEN - 1U) / crypto::AES::BLOCK_BYTES_LEN) * crypto::AES::BLOCK_BYTES_LEN;
|
||||
return padded + 2U;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds an available loopback UDP port.
|
||||
* @return uint16_t A free UDP port, or 0 on failure.
|
||||
*/
|
||||
uint16_t reserveLoopbackPort()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd == INVALID_SOCKET)
|
||||
return 0U;
|
||||
#else
|
||||
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0)
|
||||
return 0U;
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(0U);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
socklen_t addrLen = sizeof(address);
|
||||
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
return 0U;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
::closesocket(fd);
|
||||
#else
|
||||
::close(fd);
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
return ntohs(address.sin_port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a UDP datagram from a socket with bounded retries.
|
||||
* @param socket Socket instance to read from.
|
||||
* @param expected Expected plaintext payload.
|
||||
* @return bool True if the expected payload was received.
|
||||
*/
|
||||
bool receiveFromSocketWithRetry(Socket& socket, const std::string& expected)
|
||||
{
|
||||
for (uint32_t attempt = 0U; attempt < 50U; attempt++) {
|
||||
uint8_t buffer[256U] = { 0U };
|
||||
sockaddr_storage source = {};
|
||||
uint32_t sourceLen = 0U;
|
||||
|
||||
ssize_t length = socket.read(buffer, sizeof(buffer), source, sourceLen);
|
||||
if (length < 0)
|
||||
return false;
|
||||
|
||||
if (length > 0) {
|
||||
if ((size_t)length < expected.size())
|
||||
return false;
|
||||
|
||||
if (::memcmp(buffer, expected.data(), expected.size()) != 0)
|
||||
return false;
|
||||
|
||||
for (size_t i = expected.size(); i < (size_t)length; i++) {
|
||||
if (buffer[i] != 0x00U)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket sends a single datagram", "[network][udp]")
|
||||
{
|
||||
LoopbackNetwork receiver;
|
||||
REQUIRE(receiver.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
const std::string payload = "single-datagram";
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(sender.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size(), receiver.address(), receiver.addrLen(), &bytesWritten));
|
||||
REQUIRE(bytesWritten == (ssize_t)payload.size());
|
||||
REQUIRE(receiver.receive(payload));
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket reports single datagram send failure", "[network][udp]")
|
||||
{
|
||||
LoopbackNetwork receiver;
|
||||
REQUIRE(receiver.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
const std::string payload = "invalid-single-send";
|
||||
ssize_t bytesWritten = 0;
|
||||
REQUIRE_FALSE(sender.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size(), receiver.address(), 0U, &bytesWritten));
|
||||
REQUIRE(bytesWritten == -1);
|
||||
REQUIRE_FALSE(receiver.receive(payload));
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket encrypts a single datagram when a key is configured", "[network][udp][encrypted]")
|
||||
{
|
||||
LoopbackNetwork receiver;
|
||||
REQUIRE(receiver.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
std::array<uint8_t, AES_WRAPPED_PCKT_KEY_LEN> key = {};
|
||||
for (size_t i = 0U; i < key.size(); i++)
|
||||
key[i] = (uint8_t)(i + 1U);
|
||||
sender.setPresharedKey(key.data());
|
||||
|
||||
const std::string payload = "encrypted-single";
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(sender.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size(), receiver.address(), receiver.addrLen(), &bytesWritten));
|
||||
REQUIRE(bytesWritten == (ssize_t)encryptedLength(payload.size()));
|
||||
|
||||
std::vector<uint8_t> rawPayload;
|
||||
REQUIRE(receiver.receiveBytes(rawPayload));
|
||||
REQUIRE(rawPayload.size() == encryptedLength(payload.size()));
|
||||
const uint16_t packetMagic = (uint16_t(rawPayload[0U]) << 8) | uint16_t(rawPayload[1U]);
|
||||
REQUIRE(packetMagic == AES_WRAPPED_PCKT_MAGIC);
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket decrypts single datagrams with matching keys", "[network][udp][encrypted]")
|
||||
{
|
||||
const uint16_t receiverPort = reserveLoopbackPort();
|
||||
REQUIRE(receiverPort != 0U);
|
||||
|
||||
Socket receiver(receiverPort);
|
||||
REQUIRE(receiver.open(AF_INET));
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
std::array<uint8_t, AES_WRAPPED_PCKT_KEY_LEN> key = {};
|
||||
for (size_t i = 0U; i < key.size(); i++)
|
||||
key[i] = (uint8_t)(0xA0U + i);
|
||||
receiver.setPresharedKey(key.data());
|
||||
sender.setPresharedKey(key.data());
|
||||
|
||||
sockaddr_storage destination = {};
|
||||
uint32_t destinationLen = 0U;
|
||||
REQUIRE(Socket::lookup("127.0.0.1", receiverPort, destination, destinationLen) == 0);
|
||||
|
||||
const std::string payload = "encrypted-roundtrip-single";
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(sender.write(reinterpret_cast<const uint8_t*>(payload.data()), payload.size(), destination, destinationLen, &bytesWritten));
|
||||
REQUIRE(bytesWritten == (ssize_t)encryptedLength(payload.size()));
|
||||
REQUIRE(receiveFromSocketWithRetry(receiver, payload));
|
||||
|
||||
sender.close();
|
||||
receiver.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket decrypts sendmmsg datagrams with matching keys", "[network][udp][encrypted]")
|
||||
{
|
||||
const uint16_t receiverPort = reserveLoopbackPort();
|
||||
REQUIRE(receiverPort != 0U);
|
||||
|
||||
Socket receiver(receiverPort);
|
||||
REQUIRE(receiver.open(AF_INET));
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
std::array<uint8_t, AES_WRAPPED_PCKT_KEY_LEN> key = {};
|
||||
for (size_t i = 0U; i < key.size(); i++)
|
||||
key[i] = (uint8_t)(0x30U + i);
|
||||
receiver.setPresharedKey(key.data());
|
||||
sender.setPresharedKey(key.data());
|
||||
|
||||
sockaddr_storage destination = {};
|
||||
uint32_t destinationLen = 0U;
|
||||
REQUIRE(Socket::lookup("127.0.0.1", receiverPort, destination, destinationLen) == 0);
|
||||
|
||||
const std::string payloads[3U] = { "enc-batch-one", "enc-batch-two", "enc-batch-three" };
|
||||
BufferQueue queue;
|
||||
ssize_t expectedWritten = 0;
|
||||
for (size_t i = 0U; i < 3U; i++) {
|
||||
queue.push(makeDatagram(payloads[i], destination, destinationLen));
|
||||
expectedWritten += (ssize_t)encryptedLength(payloads[i].size());
|
||||
}
|
||||
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(sender.write(&queue, &bytesWritten));
|
||||
REQUIRE(queue.empty());
|
||||
REQUIRE(bytesWritten == expectedWritten);
|
||||
|
||||
for (size_t i = 0U; i < 3U; i++)
|
||||
REQUIRE(receiveFromSocketWithRetry(receiver, payloads[i]));
|
||||
|
||||
sender.close();
|
||||
receiver.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket sends every datagram in a sendmmsg", "[network][udp]")
|
||||
{
|
||||
LoopbackNetwork receivers[5U];
|
||||
for (const LoopbackNetwork& receiver : receivers)
|
||||
REQUIRE(receiver.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
const std::string payloads[5U] = { "peer-1", "peer-2", "peer-3", "peer-4", "peer-5" };
|
||||
BufferQueue queue;
|
||||
ssize_t expectedBytes = 0;
|
||||
for (size_t i = 0U; i < 5U; i++) {
|
||||
queue.push(makeDatagram(payloads[i], receivers[i].address(), receivers[i].addrLen()));
|
||||
expectedBytes += (ssize_t)payloads[i].size();
|
||||
}
|
||||
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(sender.write(&queue, &bytesWritten));
|
||||
REQUIRE(bytesWritten == expectedBytes);
|
||||
REQUIRE(queue.empty());
|
||||
|
||||
for (size_t i = 0U; i < 5U; i++)
|
||||
REQUIRE(receivers[i].receive(payloads[i]));
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
// The old implementation may terminate under an address sanitizer because it
|
||||
// reads uninitialized cleanup pointers. Keep this destructive regression case
|
||||
// opt-in when the suite is run against an old binary.
|
||||
TEST_CASE("UDP socket compacts a sendmmsg containing a skipped entry", "[network][udp]")
|
||||
{
|
||||
LoopbackNetwork first;
|
||||
LoopbackNetwork second;
|
||||
REQUIRE(first.valid());
|
||||
REQUIRE(second.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
const std::string firstPayload = "before-skip";
|
||||
const std::string secondPayload = "after-skip";
|
||||
BufferQueue queue;
|
||||
queue.push(makeDatagram(firstPayload, first.address(), first.addrLen()));
|
||||
queue.push(nullptr);
|
||||
queue.push(makeDatagram(secondPayload, second.address(), second.addrLen()));
|
||||
|
||||
ssize_t bytesWritten = -1;
|
||||
REQUIRE(sender.write(&queue, &bytesWritten));
|
||||
REQUIRE(bytesWritten == (ssize_t)(firstPayload.size() + secondPayload.size()));
|
||||
REQUIRE(queue.empty());
|
||||
REQUIRE(first.receive(firstPayload));
|
||||
REQUIRE(second.receive(secondPayload));
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket reports a complete sendmmsg send failure", "[network][udp]")
|
||||
{
|
||||
LoopbackNetwork receiver;
|
||||
REQUIRE(receiver.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
BufferQueue queue;
|
||||
queue.push(makeDatagram("invalid-address-length", receiver.address(), 0U));
|
||||
|
||||
ssize_t bytesWritten = 0;
|
||||
REQUIRE_FALSE(sender.write(&queue, &bytesWritten));
|
||||
REQUIRE(bytesWritten == -1);
|
||||
REQUIRE(queue.empty());
|
||||
REQUIRE_FALSE(receiver.receive("invalid-address-length"));
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
TEST_CASE("UDP socket does not report a partial sendmmsg send as success", "[network][udp]")
|
||||
{
|
||||
LoopbackNetwork first;
|
||||
LoopbackNetwork third;
|
||||
REQUIRE(first.valid());
|
||||
REQUIRE(third.valid());
|
||||
|
||||
Socket sender;
|
||||
REQUIRE(sender.open(AF_INET));
|
||||
|
||||
const std::string firstPayload = "sent-before-error";
|
||||
const std::string thirdPayload = "not-sent-after-error";
|
||||
BufferQueue queue;
|
||||
queue.push(makeDatagram(firstPayload, first.address(), first.addrLen()));
|
||||
queue.push(makeDatagram("invalid-address-length", third.address(), 0U));
|
||||
queue.push(makeDatagram(thirdPayload, third.address(), third.addrLen()));
|
||||
|
||||
ssize_t bytesWritten = 0;
|
||||
REQUIRE_FALSE(sender.write(&queue, &bytesWritten));
|
||||
REQUIRE(bytesWritten == -1);
|
||||
REQUIRE(queue.empty());
|
||||
REQUIRE(first.receive(firstPayload));
|
||||
REQUIRE_FALSE(third.receive(thirdPayload));
|
||||
|
||||
sender.close();
|
||||
}
|
||||
|
||||
#if (defined(HAVE_SENDMSG) && !defined(HAVE_SENDMMSG)) || defined(_WIN32)
|
||||
TEST_CASE("UDP sendmmsg compatibility wrapper returns a datagram count", "[network][udp][compatibility]")
|
||||
{
|
||||
LoopbackNetwork first;
|
||||
LoopbackNetwork second;
|
||||
REQUIRE(first.valid());
|
||||
REQUIRE(second.valid());
|
||||
|
||||
NativeSocket sender = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
REQUIRE(sender != INVALID_NATIVE_SOCKET);
|
||||
|
||||
const std::string firstPayload = "compatibility-one";
|
||||
const std::string secondPayload = "compatibility-two";
|
||||
struct iovec chunks[2U] = {};
|
||||
struct mmsghdr headers[2U] = {};
|
||||
|
||||
chunks[0U].iov_base = (void*)firstPayload.data();
|
||||
chunks[0U].iov_len = firstPayload.size();
|
||||
chunks[1U].iov_base = (void*)secondPayload.data();
|
||||
chunks[1U].iov_len = secondPayload.size();
|
||||
|
||||
headers[0U].msg_hdr.msg_name = (void*)&first.address();
|
||||
headers[0U].msg_hdr.msg_namelen = first.addrLen();
|
||||
headers[0U].msg_hdr.msg_iov = &chunks[0U];
|
||||
headers[0U].msg_hdr.msg_iovlen = 1U;
|
||||
headers[1U].msg_hdr.msg_name = (void*)&second.address();
|
||||
headers[1U].msg_hdr.msg_namelen = second.addrLen();
|
||||
headers[1U].msg_hdr.msg_iov = &chunks[1U];
|
||||
headers[1U].msg_hdr.msg_iovlen = 1U;
|
||||
|
||||
int sent = sendmmsg(sender, headers, 2U, 0);
|
||||
REQUIRE(sent == 2);
|
||||
REQUIRE(first.receive(firstPayload));
|
||||
REQUIRE(second.receive(secondPayload));
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (sender != INVALID_NATIVE_SOCKET)
|
||||
::closesocket(sender);
|
||||
#else
|
||||
if (sender != INVALID_NATIVE_SOCKET)
|
||||
::close(sender);
|
||||
#endif // defined(_WIN32)
|
||||
}
|
||||
#endif // (defined(HAVE_SENDMSG) && !defined(HAVE_SENDMMSG)) || defined(_WIN32)
|
||||
Loading…
Reference in new issue