Context fill-in and CI tests
This commit adds common units tests and CI sanitasion.
Additional context for commit b64d9c4498:
- Fixed macOS/Linux non-portable and unsafe shell usage by adding a posix_spawn helper and replacing system() calls in virtual_interface.cpp.
- Fixed SessionRegistry::erase() to remove mIPSessions and mSessionIPs entries in session_registry.cpp.
- Prevented message-length truncation in tcp_message_handler.cpp by rejecting payloads > 65535 bytes.
- Validated handshake message sizes and removed silent truncation in:
- tcp_connection.cpp
- tcp_client.cpp
- Canonicalized and validated config and whitelist paths in utils.cpp using std::filesystem.
- Hardened environment-provided config path handling in main.cpp.
- Validated UDP ciphertext lengths and fixed session ID endianness in udp_client.cpp.
- Scheduled periodic SessionRegistry::cleanupExpired() in main.cpp (every 5 minutes).
This commit is contained in:
39
.github/workflows/sanitizers.yml
vendored
Normal file
39
.github/workflows/sanitizers.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Sanitizers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SANITIZERS: "-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
|
||||
ASAN_OPTIONS: "detect_leaks=1:abort_on_error=1"
|
||||
UBSAN_OPTIONS: "print_stacktrace=1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake build-essential clang
|
||||
|
||||
- name: Configure (CMake)
|
||||
run: |
|
||||
mkdir -p build-sanitizers
|
||||
cd build-sanitizers
|
||||
CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="$SANITIZERS" -DCMAKE_EXE_LINKER_FLAGS="$SANITIZERS" ..
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd build-sanitizers
|
||||
cmake --build . -- -j
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd build-sanitizers
|
||||
ctest --output-on-failure || (echo "ctest failed"; exit 1)
|
||||
@@ -173,3 +173,28 @@ install(FILES
|
||||
LICENSES/GPL-2.0-only.txt
|
||||
LICENSES/GPL-3.0.txt
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME})
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Unit tests
|
||||
# ---------------------------------------------------------
|
||||
option(BUILD_TESTS "Build unit tests" ON)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
enable_testing()
|
||||
file(GLOB_RECURSE TEST_SRC CONFIGURE_DEPENDS tests/*.cpp)
|
||||
if(TEST_SRC)
|
||||
foreach(TEST_FILE IN LISTS TEST_SRC)
|
||||
get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
|
||||
add_executable(${TEST_NAME} ${TEST_FILE})
|
||||
target_link_libraries(${TEST_NAME} PRIVATE common sodium)
|
||||
target_include_directories(${TEST_NAME} PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
${asio_SOURCE_DIR}/asio/include
|
||||
${sodium_SOURCE_DIR}/src/libsodium/include
|
||||
${sodium_BINARY_DIR}/src/libsodium/include
|
||||
)
|
||||
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
|
||||
set_target_properties(${TEST_NAME} PROPERTIES OUTPUT_NAME "columnlynx_${TEST_NAME}")
|
||||
endforeach()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
42
tests/test_libsodium_wrapper.cpp
Normal file
42
tests/test_libsodium_wrapper.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
// Tests for LibSodiumWrapper: random, symmetric encrypt/decrypt, sign/verify
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
|
||||
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||
|
||||
int main() {
|
||||
using namespace ColumnLynx::Utils;
|
||||
|
||||
// Random bytes uniqueness
|
||||
auto a = LibSodiumWrapper::generateRandom256Bit();
|
||||
auto b = LibSodiumWrapper::generateRandom256Bit();
|
||||
assert(a != b && "generateRandom256Bit() should produce different outputs (very likely)");
|
||||
|
||||
// Symmetric encrypt/decrypt roundtrip
|
||||
ColumnLynx::SymmetricKey key = {};
|
||||
for (size_t i = 0; i < key.size(); ++i) key[i] = static_cast<uint8_t>(i);
|
||||
auto nonce = LibSodiumWrapper::generateNonce();
|
||||
|
||||
std::string plaintext = "The quick brown fox jumps over the lazy dog";
|
||||
auto ct = LibSodiumWrapper::encryptMessage(reinterpret_cast<const uint8_t*>(plaintext.data()), plaintext.size(), key, nonce, "aad");
|
||||
auto pt = LibSodiumWrapper::decryptMessage(ct.data(), ct.size(), key, nonce, "aad");
|
||||
std::string recovered(pt.begin(), pt.end());
|
||||
assert(recovered == plaintext && "decrypt should recover original plaintext");
|
||||
|
||||
// Sign and verify
|
||||
ColumnLynx::PrivateKey sk{}; ColumnLynx::PublicKey pk{};
|
||||
randombytes_buf(sk.data(), sk.size());
|
||||
// naive keypair generation for test purposes: use libsodium functions via wrapper
|
||||
// generate a real keypair using crypto_sign
|
||||
if (crypto_sign_keypair(pk.data(), sk.data()) != 0) {
|
||||
std::cerr << "Failed to generate keypair\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto sig = LibSodiumWrapper::signMessage(plaintext, sk);
|
||||
bool ok = LibSodiumWrapper::verifyMessage(plaintext, sig, pk);
|
||||
assert(ok && "Signature should verify");
|
||||
|
||||
std::cout << "LibSodiumWrapper tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
49
tests/test_session_registry.cpp
Normal file
49
tests/test_session_registry.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Simple unit tests for SessionRegistry
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
|
||||
#include <columnlynx/common/net/session_registry.hpp>
|
||||
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||
|
||||
int main() {
|
||||
using namespace ColumnLynx::Net;
|
||||
using namespace ColumnLynx::Utils;
|
||||
|
||||
auto ® = SessionRegistry::getInstance();
|
||||
|
||||
// Use a unique session id to avoid colliding with any running instance
|
||||
const uint32_t sid = 0xDEADBEEF;
|
||||
// ensure clean state
|
||||
reg.erase(sid);
|
||||
|
||||
auto key = LibSodiumWrapper::generateRandom256Bit();
|
||||
auto state = std::make_shared<SessionState>(key, std::chrono::hours(24), 0xC0A80101 /*192.168.1.1*/, 0, sid);
|
||||
|
||||
reg.put(sid, state);
|
||||
|
||||
assert(reg.exists(sid) && "Session should exist after put()");
|
||||
|
||||
auto got = reg.get(sid);
|
||||
assert(got && got->sessionID == sid && "get() should return stored session");
|
||||
|
||||
auto byip = reg.getByIP(0xC0A80101);
|
||||
assert(byip && byip->sessionID == sid && "getByIP() should find session by client IP");
|
||||
|
||||
// Erase and verify removed
|
||||
reg.erase(sid);
|
||||
assert(!reg.exists(sid) && "Session should not exist after erase()");
|
||||
assert(reg.getByIP(0xC0A80101) == nullptr && "getByIP() should return nullptr after erase");
|
||||
|
||||
// Test cleanupExpired: insert an already-expired session
|
||||
const uint32_t sid2 = 0xFEEDBEEF;
|
||||
reg.erase(sid2);
|
||||
auto expiredState = std::make_shared<SessionState>(key, std::chrono::seconds(0), 0xC0A80102, 0, sid2);
|
||||
reg.put(sid2, expiredState);
|
||||
// Force cleanup
|
||||
reg.cleanupExpired();
|
||||
assert(!reg.exists(sid2) && "Expired session should be removed by cleanupExpired()");
|
||||
|
||||
std::cout << "SessionRegistry tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
43
tests/test_session_registry_ip.cpp
Normal file
43
tests/test_session_registry_ip.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
// Tests for SessionRegistry IP allocation and lock/dealloc
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
|
||||
#include <columnlynx/common/net/session_registry.hpp>
|
||||
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||
|
||||
int main() {
|
||||
using namespace ColumnLynx::Net;
|
||||
using namespace ColumnLynx::Utils;
|
||||
|
||||
auto ® = SessionRegistry::getInstance();
|
||||
|
||||
const uint32_t sid = 0xABCDEF01;
|
||||
reg.erase(sid);
|
||||
|
||||
auto key = LibSodiumWrapper::generateRandom256Bit();
|
||||
auto state = std::make_shared<SessionState>(key, std::chrono::hours(24), 0, 0, sid);
|
||||
reg.put(sid, state);
|
||||
|
||||
// Lock IP
|
||||
uint32_t ip = 0xC0A80201; // 192.168.2.1
|
||||
reg.lockIP(sid, ip);
|
||||
|
||||
auto byip = reg.getByIP(ip);
|
||||
assert(byip && byip->sessionID == sid && "lockIP should populate mIPSessions");
|
||||
|
||||
// deallocIP
|
||||
reg.deallocIP(sid);
|
||||
assert(reg.getByIP(ip) == nullptr && "deallocIP should remove mapping");
|
||||
|
||||
// getFirstAvailableIP: choose a small /30 range to limit hosts
|
||||
uint32_t base = 0x0A000000; // 10.0.0.0
|
||||
uint8_t mask = 30; // 2 usable hosts
|
||||
uint32_t first = reg.getFirstAvailableIP(base, mask);
|
||||
assert(first != 0 && "Should find available IP in empty registry");
|
||||
|
||||
// cleanup
|
||||
reg.erase(sid);
|
||||
|
||||
std::cout << "SessionRegistry IP tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
30
tests/test_tcp_message_handler_static.cpp
Normal file
30
tests/test_tcp_message_handler_static.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// Tests for TCP MessageHandler static helpers
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
#include <columnlynx/common/net/tcp/tcp_message_handler.hpp>
|
||||
#include <columnlynx/common/net/tcp/tcp_message_type.hpp>
|
||||
|
||||
int main() {
|
||||
using namespace ColumnLynx::Net::TCP;
|
||||
|
||||
// server message special codes
|
||||
auto t1 = MessageHandler::decodeMessageType(0xFE);
|
||||
// Expect GRACEFUL_DISCONNECT mapped
|
||||
// Compare by converting back to uint8
|
||||
assert(MessageHandler::toUint8(t1) == 0xFE);
|
||||
|
||||
auto t2 = MessageHandler::decodeMessageType(0xFF);
|
||||
assert(MessageHandler::toUint8(t2) == 0xFF);
|
||||
|
||||
// Client message range (>= 0xA0)
|
||||
auto t3 = MessageHandler::decodeMessageType(0xA5);
|
||||
assert(MessageHandler::toUint8(t3) == 0xA5);
|
||||
|
||||
// Server message range (< 0xA0) and not special
|
||||
auto t4 = MessageHandler::decodeMessageType(0x10);
|
||||
assert(MessageHandler::toUint8(t4) == 0x10);
|
||||
|
||||
std::cout << "TCP MessageHandler static helpers tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user