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;

pull/126/head
Bryan Biedenkapp 2 months ago
parent 03068fe382
commit 8fcd066c02

@ -6,7 +6,7 @@
*
* Copyright (C) 2002 by Robert H. Morelos-Zaragoza., All rights reserved.
* Copyright (C) 2010,2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -1082,7 +1082,7 @@ bool Golay24128::decode24128(uint32_t code, uint32_t& out)
out = code ^ error_pattern;
bool valid = (Utils::countBits32(syndrome) < 3U) || !(Utils::countBits32(out) & 1);
bool valid = !(Utils::countBits32(out) & 1U) || (Utils::countBits32(error_pattern) <= 2U);
out >>= 12;
return valid;

@ -22,6 +22,7 @@
#include "common/network/RawFrameQueue.h"
#include <mutex>
#include <unordered_map>
#include <vector>
namespace network

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2006-2016,2020 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -673,32 +673,43 @@ bool Socket::write(BufferQueue* buffers, ssize_t* lenWritten) noexcept
return false;
}
if (sendmmsg(m_fd, headers, msgs, 0) < 0) {
// iterate through the messages and begin transmission -- this loop will attempt to send all prepared messages
// using sendmmsg, handling partial sends and errors appropriately
int totalMsgs = 0;
while (totalMsgs < msgs) {
uint32_t remaining = (uint32_t)(msgs - totalMsgs);
int ret = sendmmsg(m_fd, headers + totalMsgs, remaining, 0);
if (ret < 0) {
#if defined(_WIN32)
LogError(LOG_NET, "Error returned from sendmmsg, err: %lu", ::GetLastError());
LogError(LOG_NET, "Error returned from sendmmsg after sending %d of %d packet, err: %lu", totalMsgs, msgs, ::GetLastError());
#else
LogError(LOG_NET, "Error returned from sendmmsg, err: %d (%s)", errno, strerror(errno));
if (errno == EINTR)
continue;
LogError(LOG_NET, "Error returned from sendmmsg after sending %d of %d packet, err: %d (%s)", totalMsgs, msgs, errno, strerror(errno));
#endif // _WIN32
if (lenWritten != nullptr) {
*lenWritten = -1;
break;
}
}
if (sent < 0) {
#if defined(_WIN32)
LogError(LOG_NET, "Error returned from sendmmsg, err: %lu", ::GetLastError());
#else
LogError(LOG_NET, "Error returned from sendmmsg, err: %d (%s)", errno, strerror(errno));
#endif // _WIN32
if (lenWritten != nullptr) {
*lenWritten = -1;
if (ret == 0 || ret > (int)remaining) {
LogError(LOG_NET, "Returned invalid packet count, sent = %d, remaining = %u, total = %d", ret, remaining, msgs);
break;
}
if (ret < (int)remaining)
LogWarning(LOG_NET, "Required multiple writes to send all %d packet (this shouldn't happen!)", msgs);
totalMsgs += ret;
}
else {
if (msgs > 0 && totalMsgs == msgs) {
result = true;
if (lenWritten != nullptr) {
if (lenWritten != nullptr)
*lenWritten = sent;
}
}
else {
if (lenWritten != nullptr)
*lenWritten = -1;
}
// cleanup buffers

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2006-2016,2020 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -86,7 +86,8 @@ namespace network
ssize_t ret = sendmsg(sockfd, &msgvec[i].msg_hdr, flags);
if (ret < 0)
break;
n += ret;
msgvec[i].msg_len = (unsigned int)ret;
n++;
}
if (n == 0)
@ -140,7 +141,8 @@ namespace network
ssize_t ret = ::sendto(sockfd, (char*)&msgvec[i].msg_hdr.msg_iov->iov_base, msgvec[i].msg_hdr.msg_iov->iov_len, 0, (sockaddr*)&msgvec[i].msg_hdr.msg_name, msgvec[i].msg_hdr.msg_namelen);
if (ret < 0)
break;
n += ret;
msgvec[i].msg_len = (unsigned int)ret;
n++;
}
if (n == 0)

@ -16,4 +16,5 @@ file(GLOB dvmtests_SRC
"tests/edac/*.cpp"
"tests/p25/*.cpp"
"tests/nxdn/*.cpp"
"tests/network/*.cpp"
)

@ -138,11 +138,42 @@ TEST_CASE("Golay24128 encode24128 detects uncorrectable errors", "[edac][golay24
}
}
/*
** NOTE: Three-bit error correction test disabled. While Golay(24,12,8) theoretically
** corrects up to 3 errors, the underlying getSyndrome23127 has edge case bugs that
** can cause incorrect decoding with certain error patterns.
*/
TEST_CASE("Golay24128 encode24128 accepts all correctable <=3-bit errors", "[edac][golay24128]") {
const uint32_t testValues[] = {0x000U, 0xA5AU};
// Test all correctable error patterns (1, 2, and 3 bit errors) for each test value
for (auto original : testValues) {
uint32_t encoded = Golay24128::encode24128(original);
// Test single-bit errors first
for (uint32_t i = 0U; i < 24U; i++) {
uint32_t corrupted = encoded ^ (1U << i);
uint32_t decoded;
bool result = Golay24128::decode24128(corrupted, decoded);
REQUIRE(result);
REQUIRE(decoded == original);
// Test two-bit errors next
for (uint32_t j = i + 1U; j < 24U; j++) {
corrupted = encoded ^ (1U << i) ^ (1U << j);
result = Golay24128::decode24128(corrupted, decoded);
REQUIRE(result);
REQUIRE(decoded == original);
// Test three-bit errors next
for (uint32_t k = j + 1U; k < 24U; k++) {
corrupted = encoded ^ (1U << i) ^ (1U << j) ^ (1U << k);
result = Golay24128::decode24128(corrupted, decoded);
REQUIRE(result);
REQUIRE(decoded == original);
}
}
}
}
}
TEST_CASE("Golay24128 encode24128 byte array interface works", "[edac][golay24128]") {
// Test the byte array encode/decode interface

@ -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…
Cancel
Save

Powered by TurnKey Linux.