30 Commits

Author SHA1 Message Date
918b80931e Merge branch 'beta' - Version 1.1.0
This version introduces protocol version 2:
 - UDP Headers stripped to 16 Bytes, allowing more throughput
 - Nonce is derived, but unique every packet - allows faster sending
2026-02-10 19:35:19 +01:00
0299b03d9c Merge branch 'dev' into beta - Version 1.1.0
This version introduces protocol version 2:
 - UDP Headers stripped to 16 Bytes, allowing more throughput
 - Nonce is derived, but unique every packet - allows faster sending
2026-02-10 19:33:56 +01:00
07458c348b Merge branch 'beta' 2026-01-18 19:55:13 +01:00
204f89006f Merge branch 'dev' into beta 2026-01-18 19:54:59 +01:00
833629486e Merge branch 'beta'; Version 1.0.1 2026-01-18 19:48:02 +01:00
57d260976c Merge branch 'dev' into beta 2026-01-18 19:47:48 +01:00
cbfbcaa153 Merge branch 'beta' 2026-01-11 20:32:46 +01:00
4fa26d51d0 Merge branch 'dev' into beta 2026-01-11 20:32:28 +01:00
e101f5ddd6 Merge branch 'beta' - Version 1.0.0 2026-01-01 17:21:28 +01:00
e1118ccafe Merge branch 'dev' into beta 2026-01-01 17:21:15 +01:00
c715a43a10 Merge branch 'beta' - Version 1.0.0 2026-01-01 16:33:21 +01:00
00f72e1a64 Merge branch 'dev' into beta - Version 1.0.0 2026-01-01 16:32:59 +01:00
3cd99243ad Version 1.0.0 2026-01-01 16:32:14 +01:00
8f536abe77 Merge branch 'dev' into beta - Version 1.0.0 2026-01-01 16:23:37 +01:00
f99036c523 Merge branch 'beta' 2025-12-29 20:28:34 +01:00
3eadd41a00 Merge branch 'dev' into beta 2025-12-29 20:28:15 +01:00
471224b043 Merge branch 'beta' - b0.3 2025-12-29 19:07:16 +01:00
714aa52f98 Merge branch 'dev' into beta 2025-12-29 19:06:59 +01:00
cb0f674c52 Merge branch 'beta' - Version b0.1
macOS Support
2025-12-08 17:38:05 +01:00
a2ecc589f8 Merge branch 'dev' into beta - Version b0.1 2025-12-08 17:37:44 +01:00
33bbd7cce6 Merge branch 'beta' - Alpha 0.6
This version adds Dynamic IP assignment based on config.
2025-12-02 18:47:58 +01:00
640a751f9b Merge branch 'dev' into beta - Alpha 0.6
This version adds dynamic IP assignment based on config.
2025-12-02 18:46:28 +01:00
f9c5c56a1b Merge branch 'beta'
This is the merge of version a0.5 into master.
This version adds general authentication of the client and server, and control of connection via key whitelisting.
Also added loading of keypairs via a config file system.
2025-11-28 19:31:01 +01:00
a08dba5b59 Merge branch 'dev' into beta
This is the merge of version a0.5 into beta.
This version adds general authentication of the client and server, and control of connection via key whitelisting.
Also added loading of keypairs via a config file system.
2025-11-28 19:27:15 +01:00
17dd504a7a Merge pull request 'First working alpha, version a0.4' (#7) from beta into master
Reviewed-on: #7
2025-11-18 20:09:11 +00:00
4ba59fb23f Merge pull request 'First working alpha, version a0.4.' (#6) from dev into beta
Reviewed-on: #6
2025-11-18 20:07:30 +00:00
9f52bdd54c Merge pull request 'beta' (#4) from beta into master
Reviewed-on: #4
2025-11-10 15:58:29 +00:00
9e5e728438 Merge pull request 'Add legal clarification' (#3) from dev into beta
Reviewed-on: #3
2025-11-10 15:58:18 +00:00
29e90938c5 Merge pull request 'beta - Update License' (#2) from beta into master
Reviewed-on: #2
2025-11-10 15:15:31 +00:00
d20bee9e60 Merge pull request 'Update license' (#1) from dev into beta
Reviewed-on: #1
2025-11-10 15:15:10 +00:00
23 changed files with 212 additions and 792 deletions

View File

@@ -1,39 +0,0 @@
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)

1
.gitignore vendored
View File

@@ -12,6 +12,5 @@ _deps
CMakeUserPresets.json CMakeUserPresets.json
build/ build/
build*/
.vscode/ .vscode/
.DS_Store .DS_Store

View File

@@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA # If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx project(ColumnLynx
VERSION 1.2.0 VERSION 1.1.0
LANGUAGES CXX LANGUAGES CXX
) )
@@ -173,28 +173,3 @@ install(FILES
LICENSES/GPL-2.0-only.txt LICENSES/GPL-2.0-only.txt
LICENSES/GPL-3.0.txt LICENSES/GPL-3.0.txt
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME}) 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()

View File

@@ -55,22 +55,7 @@ openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32
# Output example: 1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e # Output example: 1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
``` ```
Write the hex output into two separate files in the matching config directory: You can then set these keys accordingly in the **server_config** and **client_config** files.
- `public.key` for the public key
- `private.key` for the private key seed
The files should contain only the hex ASCII characters, optionally followed by a trailing newline. Hex parsing is case-insensitive.
For example:
```bash
printf '%s' '1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e' > /etc/columnlynx/public.key
printf '%s' '9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e' > /etc/columnlynx/private.key
chmod 600 /etc/columnlynx/private.key
```
On Unix-like systems, the software will warn if the private key file is too permissive and recommend tightening it with `chmod 600`.
### Server Setup (Linux Server ONLY) ### Server Setup (Linux Server ONLY)
@@ -172,19 +157,20 @@ sudo nft add rule nat postroute ip saddr 10.10.0.0/24 oifname "eth0" masquerade
"**server_config**" is a file that contains the server configuration, **one variable per line**. These are the current configuration available variables: "**server_config**" is a file that contains the server configuration, **one variable per line**. These are the current configuration available variables:
- **SERVER_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
- **SERVER_PRIVATE_KEY** (Hex String): The private key seed to be used
- **NETWORK** (IPv4 Format): The network IPv4 to be used (Server Interface still needs to be configured manually) - **NETWORK** (IPv4 Format): The network IPv4 to be used (Server Interface still needs to be configured manually)
- **SUBNET_MASK** (Integer): The subnet mask to be used (ensure proper length, it will not be checked) - **SUBNET_MASK** (Integer): The subnet mask to be used (ensure proper length, it will not be checked)
**Example:** **Example:**
``` ```
SERVER_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
SERVER_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
NETWORK=10.10.0.0 NETWORK=10.10.0.0
SUBNET_MASK=24 SUBNET_MASK=24
``` ```
The server keypair must now live in the same directory as `server_config`, stored in `public.key` and `private.key`.
`server_config` no longer stores key material.
<hr></hr> <hr></hr>
"**whitelisted_keys**" is a file that **public keys of clients that are allowed to connect to the server, one key per line**. "**whitelisted_keys**" is a file that **public keys of clients that are allowed to connect to the server, one key per line**.
@@ -198,9 +184,17 @@ The server keypair must now live in the same directory as `server_config`, store
### Client ### Client
"**client_config**" is a file that contains the client configuration, **one variable per line**. Key material is no longer stored here; if you do not have any client-only settings yet, this file can stay empty. "**client_config**" is a file that contains the client configuration, **one variable per line**. These are the current configuration available variables:
The client keypair must now live in the same directory as `client_config`, stored in `public.key` and `private.key`. - **CLIENT_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
- **CLIENT_PRIVATE_KEY** (Hex String): The private key seed to be used
**Example:**
```
CLIENT_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
CLIENT_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
```
<hr></hr> <hr></hr>

View File

@@ -87,10 +87,6 @@ namespace ColumnLynx {
void incrementSendCount() { void incrementSendCount() {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
if (mClientState->send_cnt == std::numeric_limits<uint64_t>::max()) {
Utils::error("ClientSession: send counter overflow detected");
return;
}
mClientState->send_cnt++; mClientState->send_cnt++;
} }

View File

@@ -11,9 +11,9 @@
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <array> #include <array>
#include <atomic>
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <unordered_map>
#include <string> #include <string>
#include <columnlynx/common/net/protocol_structs.hpp> #include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp> #include <columnlynx/common/net/virtual_interface.hpp>
@@ -26,7 +26,48 @@ namespace ColumnLynx::Net::TCP {
public: public:
TCPClient(asio::io_context& ioContext, TCPClient(asio::io_context& ioContext,
const std::string& host, const std::string& host,
const std::string& port); const std::string& port)
:
mResolver(ioContext),
mSocket(ioContext),
mHost(host),
mPort(port),
mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now())
{
// Get initial client config
std::string configPath = ClientSession::getInstance().getConfigPath();
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper();
// Preload the config map
mRawClientConfig = Utils::getConfigMap(configPath + "client_config");
auto itPubkey = mRawClientConfig.find("CLIENT_PUBLIC_KEY");
auto itPrivkey = mRawClientConfig.find("CLIENT_PRIVATE_KEY");
if (itPubkey != mRawClientConfig.end() && itPrivkey != mRawClientConfig.end()) {
Utils::log("Loading keypair from config file.");
PublicKey pk;
PrivateSeed seed;
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), seed.size(), seed.begin()); // This is extremely stupid, but the C++ compiler has forced my hand (I would've just used to_array, but fucking asio decls)
std::copy_n(Utils::hexStringToBytes(itPubkey->second).begin(), pk.size(), pk.begin());
if (!mLibSodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute keypair from config file values!");
}
Utils::debug("Newly-Loaded Public Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPublicKey(), 32));
} else {
#if defined(DEBUG)
Utils::warn("No keypair found in config file! Using random key.");
#else
throw std::runtime_error("No keypair found in config file! Cannot start client without keys.");
#endif
}
}
// Starts the TCP Client and initiaties the handshake // Starts the TCP Client and initiaties the handshake
void start(); void start();
@@ -48,8 +89,8 @@ namespace ColumnLynx::Net::TCP {
// TODO: Move ptrs to smart ptrs // TODO: Move ptrs to smart ptrs
std::atomic<bool> mConnected{false}; bool mConnected = false;
std::atomic<bool> mHandshakeComplete{false}; bool mHandshakeComplete = false;
tcp::resolver mResolver; tcp::resolver mResolver;
tcp::socket mSocket; tcp::socket mSocket;
std::shared_ptr<MessageHandler> mHandler; std::shared_ptr<MessageHandler> mHandler;
@@ -64,5 +105,6 @@ namespace ColumnLynx::Net::TCP {
int mMissedHeartbeats = 0; int mMissedHeartbeats = 0;
bool mIsHostDomain; bool mIsHostDomain;
Protocol::TunConfig mTunConfig; Protocol::TunConfig mTunConfig;
std::unordered_map<std::string, std::string> mRawClientConfig;
}; };
} }

View File

@@ -39,6 +39,6 @@ namespace ColumnLynx::Net::TCP {
std::array<uint8_t, 3> mHeader{}; // [type][lenHigh][lenLow] std::array<uint8_t, 3> mHeader{}; // [type][lenHigh][lenLow]
std::vector<uint8_t> mBody; std::vector<uint8_t> mBody;
std::function<void(AnyMessageType, std::string)> mOnMessage; std::function<void(AnyMessageType, std::string)> mOnMessage;
std::function<void(const asio::error_code&)> mOnDisconnect; std::function<void(asio::error_code&)> mOnDisconnect;
}; };
} }

View File

@@ -15,7 +15,7 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <algorithm> #include <algorithm>
#include <bits/stdc++.h>
#ifdef _WIN32 #ifdef _WIN32
#include <winsock2.h> #include <winsock2.h>
@@ -79,15 +79,4 @@ namespace ColumnLynx::Utils {
// Returns the config file in an unordered_map format. This purely reads the config file, you still need to parse it manually. // Returns the config file in an unordered_map format. This purely reads the config file, you still need to parse it manually.
std::unordered_map<std::string, std::string> getConfigMap(std::string path, std::vector<std::string> requiredKeys = {}); std::unordered_map<std::string, std::string> getConfigMap(std::string path, std::vector<std::string> requiredKeys = {});
// Load a hex-encoded file, validate its byte length, and return the decoded bytes.
std::vector<uint8_t> loadHexBytesFromFile(const std::string& path, size_t expectedBytes, const std::string& description = "key", bool warnOnInsecurePermissions = false);
template <size_t N>
inline std::array<uint8_t, N> loadHexArrayFromFile(const std::string& path, const std::string& description = "key", bool warnOnInsecurePermissions = false) {
auto bytes = loadHexBytesFromFile(path, N, description, warnOnInsecurePermissions);
std::array<uint8_t, N> out{};
std::copy_n(bytes.begin(), N, out.begin());
return out;
}
}; };

View File

@@ -5,7 +5,6 @@
#include <asio.hpp> #include <asio.hpp>
#include <csignal> #include <csignal>
#include <iostream> #include <iostream>
#include <filesystem>
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/panic_handler.hpp> #include <columnlynx/common/panic_handler.hpp>
#include <columnlynx/client/net/tcp/tcp_client.hpp> #include <columnlynx/client/net/tcp/tcp_client.hpp>
@@ -91,20 +90,7 @@ int main(int argc, char** argv) {
std::string configPath = optionsObj["config-dir"].as<std::string>(); std::string configPath = optionsObj["config-dir"].as<std::string>();
const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR"); const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR");
if (envConfigPath != nullptr) { if (envConfigPath != nullptr) {
// Validate and canonicalize environment-provided path configPath = std::string(envConfigPath);
try {
namespace fs = std::filesystem;
std::error_code ec;
fs::path candidate(envConfigPath);
fs::path abs = fs::absolute(candidate, ec);
if (!ec) {
configPath = abs.string();
} else {
warn(std::string("Invalid COLUMNLYNX_CONFIG_DIR value: ") + envConfigPath + " - using default");
}
} catch (const std::exception& e) {
warn(std::string("Failed to canonicalize COLUMNLYNX_CONFIG_DIR: ") + e.what());
}
} }
if (configPath.back() != '/' && configPath.back() != '\\') { if (configPath.back() != '/' && configPath.back() != '\\') {
@@ -127,28 +113,6 @@ int main(int argc, char** argv) {
initialState.virtualInterface = tun; initialState.virtualInterface = tun;
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>(); std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
const std::string clientPublicKeyPath = configPath + "public.key";
const std::string clientPrivateKeyPath = configPath + "private.key";
namespace fs = std::filesystem;
bool clientKeyFilesPresent = fs::exists(clientPublicKeyPath) && fs::exists(clientPrivateKeyPath);
if (clientKeyFilesPresent) {
Utils::log("Loading client keypair from key files.");
PublicKey pk = Utils::loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(clientPublicKeyPath, "client public key");
PrivateSeed seed = Utils::loadHexArrayFromFile<crypto_sign_SEEDBYTES>(clientPrivateKeyPath, "client private key", true);
if (!sodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute client keypair from key files!");
}
} else {
#if defined(DEBUG)
Utils::warn("No client keypair files found! Using random key.");
#else
throw std::runtime_error("No client keypair files found! Cannot start client without keys.");
#endif
}
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper->getPublicKey(), 32)); debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper->getPublicKey(), 32));
debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper->getPrivateKey(), 64)); debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper->getPrivateKey(), 64));
initialState.sodiumWrapper = sodiumWrapper; initialState.sodiumWrapper = sodiumWrapper;

View File

@@ -6,20 +6,6 @@
//#include <arpa/inet.h> //#include <arpa/inet.h>
namespace ColumnLynx::Net::TCP { namespace ColumnLynx::Net::TCP {
TCPClient::TCPClient(asio::io_context& ioContext, const std::string& host, const std::string& port)
: mResolver(ioContext),
mSocket(ioContext),
mHost(host),
mPort(port),
mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now())
{
if (!ClientSession::getInstance().getSodiumWrapper()) {
throw std::runtime_error("ClientSession sodium wrapper is not initialized");
}
}
void TCPClient::start() { void TCPClient::start() {
auto self = shared_from_this(); auto self = shared_from_this();
mResolver.async_resolve(mHost, mPort, mResolver.async_resolve(mHost, mPort,
@@ -28,25 +14,19 @@ namespace ColumnLynx::Net::TCP {
asio::async_connect(mSocket, endpoints, asio::async_connect(mSocket, endpoints,
[this, self](asio::error_code ec, const tcp::endpoint&) { [this, self](asio::error_code ec, const tcp::endpoint&) {
if (!ec) { if (!ec) {
mConnected.store(true, std::memory_order_relaxed); mConnected = true;
Utils::log("Client connected."); Utils::log("Client connected.");
mHandler = std::make_shared<MessageHandler>(std::move(mSocket)); mHandler = std::make_shared<MessageHandler>(std::move(mSocket));
mHandler->onMessage([weakSelf = weak_from_this()](AnyMessageType type, const std::string& data) { mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
if (auto self = weakSelf.lock()) { mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
self->mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
}
}); });
// Close only after peer FIN to avoid RSTs // Close only after peer FIN to avoid RSTs
mHandler->onDisconnect([weakSelf = weak_from_this()](const asio::error_code& ec) { mHandler->onDisconnect([this](const asio::error_code& ec) {
auto self = weakSelf.lock();
if (!self) {
return;
}
asio::error_code ec2; asio::error_code ec2;
if (self->mHandler) { if (mHandler) {
self->mHandler->socket().close(ec2); mHandler->socket().close(ec2);
} }
self->mConnected.store(false, std::memory_order_relaxed); mConnected = false;
Utils::log(std::string("Server disconnected: ") + ec.message()); Utils::log(std::string("Server disconnected: ") + ec.message());
}); });
mHandler->start(); mHandler->start();
@@ -66,7 +46,6 @@ namespace ColumnLynx::Net::TCP {
std::vector<uint8_t> payload; std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES); payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
payload.push_back(Utils::protocolVersion()); payload.push_back(Utils::protocolVersion());
Utils::log("Using protocol version: " + std::to_string(Utils::protocolVersion()));
/*payload.insert(payload.end(), /*payload.insert(payload.end(),
mLibSodiumWrapper->getXPublicKey(), mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
@@ -94,7 +73,7 @@ namespace ColumnLynx::Net::TCP {
} }
void TCPClient::sendMessage(ClientMessageType type, const std::string& data) { void TCPClient::sendMessage(ClientMessageType type, const std::string& data) {
if (!mConnected.load(std::memory_order_relaxed)) { if (!mConnected) {
Utils::error("Cannot send message, client not connected."); Utils::error("Cannot send message, client not connected.");
return; return;
} }
@@ -107,7 +86,7 @@ namespace ColumnLynx::Net::TCP {
} }
void TCPClient::disconnect(bool echo) { void TCPClient::disconnect(bool echo) {
if (mConnected.load(std::memory_order_relaxed) && mHandler) { if (mConnected && mHandler) {
if (echo) { if (echo) {
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye"); mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
} }
@@ -127,17 +106,17 @@ namespace ColumnLynx::Net::TCP {
} }
bool TCPClient::isHandshakeComplete() const { bool TCPClient::isHandshakeComplete() const {
return mHandshakeComplete.load(std::memory_order_relaxed); return mHandshakeComplete;
} }
bool TCPClient::isConnected() const { bool TCPClient::isConnected() const {
return mConnected.load(std::memory_order_relaxed); return mConnected;
} }
void TCPClient::mStartHeartbeat() { void TCPClient::mStartHeartbeat() {
auto self = shared_from_this(); auto self = shared_from_this();
mHeartbeatTimer.expires_after(std::chrono::seconds(5)); mHeartbeatTimer.expires_after(std::chrono::seconds(5));
mHeartbeatTimer.async_wait([self](const asio::error_code& ec) { mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) { if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled return; // Timer was cancelled
} }
@@ -150,11 +129,9 @@ namespace ColumnLynx::Net::TCP {
// Close sockets forcefully, server is dead // Close sockets forcefully, server is dead
asio::error_code ec; asio::error_code ec;
if (self->mHandler) { mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
self->mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); mHandler->socket().close(ec);
self->mHandler->socket().close(ec); mConnected = false;
}
self->mConnected.store(false, std::memory_order_relaxed);
ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros
ClientSession::getInstance().setSessionID(0); ClientSession::getInstance().setSessionID(0);
@@ -173,13 +150,7 @@ namespace ColumnLynx::Net::TCP {
void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) { void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) {
switch (type) { switch (type) {
case ServerMessageType::HANDSHAKE_IDENTIFY: { case ServerMessageType::HANDSHAKE_IDENTIFY: {
if (data.size() != sizeof(mServerPublicKey)) { std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey)));
Utils::warn("HANDSHAKE_IDENTIFY has invalid size: " + std::to_string(data.size()));
disconnect();
return;
}
std::memcpy(mServerPublicKey, data.data(), sizeof(mServerPublicKey));
std::string hexServerPub = Utils::bytesToHexString(mServerPublicKey, 32); std::string hexServerPub = Utils::bytesToHexString(mServerPublicKey, 32);
Utils::log("Received server identity. Public Key: " + hexServerPub); Utils::log("Received server identity. Public Key: " + hexServerPub);
@@ -208,13 +179,7 @@ namespace ColumnLynx::Net::TCP {
{ {
// Verify the signature // Verify the signature
Signature sig{}; Signature sig{};
if (data.size() != sig.size()) { std::memcpy(sig.data(), data.data(), std::min(data.size(), sig.size()));
Utils::warn("HANDSHAKE_CHALLENGE_RESPONSE has invalid size: " + std::to_string(data.size()));
disconnect();
return;
}
std::memcpy(sig.data(), data.data(), sig.size());
if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) { if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) {
Utils::log("Challenge response verified successfully."); Utils::log("Challenge response verified successfully.");
@@ -295,7 +260,7 @@ namespace ColumnLynx::Net::TCP {
mTun->configureIP(clientIP, serverIP, prefixLen, mtu); mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
} }
mHandshakeComplete.store(true, std::memory_order_relaxed); mHandshakeComplete = true;
} }
break; break;
@@ -310,13 +275,13 @@ namespace ColumnLynx::Net::TCP {
break; break;
case ServerMessageType::GRACEFUL_DISCONNECT: case ServerMessageType::GRACEFUL_DISCONNECT:
Utils::log("Server is disconnecting: " + data); Utils::log("Server is disconnecting: " + data);
if (mConnected.load(std::memory_order_relaxed)) { // Prevent Recursion if (mConnected) { // Prevent Recursion
disconnect(false); disconnect(false);
} }
break; break;
case ServerMessageType::KILL_CONNECTION: case ServerMessageType::KILL_CONNECTION:
Utils::warn("Server is killing the connection: " + data); Utils::warn("Server is killing the connection: " + data);
if (mConnected.load(std::memory_order_relaxed)) { if (mConnected) {
disconnect(false); disconnect(false);
} }
break; break;

View File

@@ -3,8 +3,6 @@
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. // Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#include <columnlynx/client/net/udp/udp_client.hpp> #include <columnlynx/client/net/udp/udp_client.hpp>
#include <thread>
#include <chrono>
namespace ColumnLynx::Net::UDP { namespace ColumnLynx::Net::UDP {
void UDPClient::start() { void UDPClient::start() {
@@ -75,7 +73,7 @@ namespace ColumnLynx::Net::UDP {
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader) reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
); );
uint32_t sessionID = static_cast<uint32_t>(ClientSession::getInstance().getSessionID()); uint32_t sessionID = static_cast<uint32_t>(ClientSession::getInstance().getSessionID());
uint32_t sessionIDNet = htonl(sessionID); uint32_t sessionIDNet = sessionID;
packet.insert(packet.end(), packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&sessionIDNet), reinterpret_cast<uint8_t*>(&sessionIDNet),
reinterpret_cast<uint8_t*>(&sessionIDNet) + sizeof(uint32_t) reinterpret_cast<uint8_t*>(&sessionIDNet) + sizeof(uint32_t)
@@ -104,12 +102,7 @@ namespace ColumnLynx::Net::UDP {
if (ec) { if (ec) {
if (ec == asio::error::operation_aborted) return; // Socket closed if (ec == asio::error::operation_aborted) return; // Socket closed
// Other recv error // Other recv error
Utils::warn("UDPClient receive error: " + ec.message());
// Back off briefly before restarting receive to avoid busy error loops
asio::post(mSocket.get_executor(), [this]() {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
mStartReceive(); mStartReceive();
});
return; return;
} }
@@ -143,24 +136,9 @@ namespace ColumnLynx::Net::UDP {
} }
// Decrypt payload // Decrypt payload
// Extract ciphertext safely
size_t headerLen = sizeof(UDPPacketHeader) + sizeof(uint32_t);
if (bytes < headerLen) {
Utils::warn("UDP Client received packet too small after header check.");
return;
}
size_t ciphertextLen = bytes - headerLen;
// Enforce reasonable maximum (UDP payload practical limit)
const size_t MAX_UDP_PAYLOAD = 65507; // 65535 - UDP/IP headers
if (ciphertextLen > MAX_UDP_PAYLOAD) {
Utils::warn("UDP Client received packet with excessive payload size: " + std::to_string(ciphertextLen));
return;
}
std::vector<uint8_t> ciphertext( std::vector<uint8_t> ciphertext(
mRecvBuffer.begin() + headerLen, mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t),
mRecvBuffer.begin() + headerLen + ciphertextLen mRecvBuffer.begin() + bytes
); );
if (ClientSession::getInstance().getAESKey().empty()) { if (ClientSession::getInstance().getAESKey().empty()) {

View File

@@ -32,26 +32,7 @@ namespace ColumnLynx::Net {
void SessionRegistry::erase(uint32_t sessionID) { void SessionRegistry::erase(uint32_t sessionID) {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
auto it = mSessions.find(sessionID); mSessions.erase(sessionID);
if (it != mSessions.end()) {
// If the session has a client IP mapping, remove it to avoid stale entries
if (it->second) {
uint32_t ip = it->second->clientTunIP;
auto ipIt = mIPSessions.find(ip);
if (ipIt != mIPSessions.end()) {
// Only erase if it points to the same session
if (ipIt->second == it->second) {
mIPSessions.erase(ipIt);
}
}
}
// Remove any session->ip bookkeeping
mSessionIPs.erase(sessionID);
// Finally erase the session
mSessions.erase(it);
}
} }
void SessionRegistry::cleanupExpired() { void SessionRegistry::cleanupExpired() {
@@ -105,14 +86,10 @@ namespace ColumnLynx::Net {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip; mSessionIPs[sessionID] = ip;
auto it = mSessions.find(sessionID); /*if (mIPSessions.find(sessionID) == mIPSessions.end()) {
if (it == mSessions.end() || !it->second) { Utils::debug("yikes");
Utils::warn("SessionRegistry::lockIP called for unknown session " + std::to_string(sessionID)); }*/
mSessionIPs.erase(sessionID); mIPSessions[ip] = mSessions.find(sessionID)->second;
return;
}
mIPSessions[ip] = it->second;
} }
void SessionRegistry::deallocIP(uint32_t sessionID) { void SessionRegistry::deallocIP(uint32_t sessionID) {

View File

@@ -5,7 +5,6 @@
#include <columnlynx/common/net/tcp/tcp_message_handler.hpp> #include <columnlynx/common/net/tcp/tcp_message_handler.hpp>
#include <columnlynx/common/net/tcp/net_helper.hpp> #include <columnlynx/common/net/tcp/net_helper.hpp>
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <memory>
namespace ColumnLynx::Net::TCP { namespace ColumnLynx::Net::TCP {
void MessageHandler::start() { void MessageHandler::start() {
@@ -18,23 +17,17 @@ namespace ColumnLynx::Net::TCP {
return static_cast<uint8_t>(type); return static_cast<uint8_t>(type);
}, type); }, type);
auto data = std::make_shared<std::vector<uint8_t>>(); std::vector<uint8_t> data;
data->push_back(typeByte); data.push_back(typeByte);
// Ensure payload fits into protocol's 16-bit length field uint16_t length = payload.size();
if (payload.size() > static_cast<size_t>(std::numeric_limits<uint16_t>::max())) {
Utils::error("sendMessage(): payload too large (>65535 bytes)");
return;
}
uint16_t length = static_cast<uint16_t>(payload.size()); data.push_back(length >> 8);
data.push_back(length & 0xFF);
data->push_back(static_cast<uint8_t>(length >> 8)); data.insert(data.end(), payload.begin(), payload.end());
data->push_back(static_cast<uint8_t>(length & 0xFF));
data->insert(data->end(), payload.begin(), payload.end());
auto self = shared_from_this(); auto self = shared_from_this();
asio::async_write(mSocket, asio::buffer(*data), asio::async_write(mSocket, asio::buffer(data),
[self, data](asio::error_code ec, std::size_t) { [self](asio::error_code ec, std::size_t) {
if (ec) { if (ec) {
Utils::error("Send failed: " + ec.message()); Utils::error("Send failed: " + ec.message());
} }

View File

@@ -3,8 +3,6 @@
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. // Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <filesystem>
#include <cctype>
namespace ColumnLynx::Utils { namespace ColumnLynx::Utils {
std::string unixMillisToISO8601(uint64_t unixMillis, bool local) { std::string unixMillisToISO8601(uint64_t unixMillis, bool local) {
@@ -87,7 +85,7 @@ namespace ColumnLynx::Utils {
} }
std::string getVersion() { std::string getVersion() {
return "1.2.0"; return "1.1.0";
} }
unsigned short serverPort() { unsigned short serverPort() {
@@ -146,49 +144,19 @@ namespace ColumnLynx::Utils {
std::vector<std::string> out; std::vector<std::string> out;
namespace fs = std::filesystem; std::ifstream file(basePath + "whitelisted_keys");
std::error_code ec;
fs::path base(basePath);
fs::path absBase = fs::absolute(base, ec);
if (ec) {
warn("getWhitelistedKeys(): failed to resolve base path: " + basePath + " - " + ec.message());
return out;
}
fs::path whitelist = absBase / "whitelisted_keys";
if (!fs::exists(whitelist, ec) || ec) {
warn("getWhitelistedKeys(): whitelist file not found: " + whitelist.string());
return out;
}
// Canonicalize to avoid symlink tricks
fs::path canon = fs::canonical(whitelist, ec);
if (ec) {
warn("getWhitelistedKeys(): failed to canonicalize path: " + whitelist.string());
return out;
}
std::ifstream file(canon);
if (!file.is_open()) { if (!file.is_open()) {
warn("getWhitelistedKeys(): failed to open whitelist file: " + canon.string()); warn("Failed to open whitelisted_keys file at path: " + basePath + "whitelisted_keys");
return out; return out;
} }
std::string line; std::string line;
while (std::getline(file, line)) { while (std::getline(file, line)) {
// Trim whitespace
while (!line.empty() && isspace(static_cast<unsigned char>(line.back()))) line.pop_back();
size_t start = 0;
while (start < line.size() && isspace(static_cast<unsigned char>(line[start]))) ++start;
if (start >= line.size()) continue;
std::string key = line.substr(start);
// Convert to upper case to align with the bytesToHexString() output // Convert to upper case to align with the bytesToHexString() output
for (size_t i = 0; i < key.length(); ++i) { for (int i = 0; i < line.length(); i++) {
key[i] = static_cast<char>(toupper(static_cast<unsigned char>(key[i]))); line[i] = toupper(line[i]);
} }
out.push_back(key); out.push_back(line);
} }
return out; return out;
@@ -198,26 +166,9 @@ namespace ColumnLynx::Utils {
// TODO: Currently re-reads every time. // TODO: Currently re-reads every time.
std::vector<std::string> readLines; std::vector<std::string> readLines;
namespace fs = std::filesystem; std::ifstream file(path);
std::error_code ec;
fs::path p(path);
fs::path abs = fs::absolute(p, ec);
if (ec) {
throw std::runtime_error("getConfigMap(): failed to resolve path: " + path + " - " + ec.message());
}
if (!fs::exists(abs, ec) || ec) {
throw std::runtime_error("getConfigMap(): config file does not exist: " + abs.string());
}
fs::path canon = fs::canonical(abs, ec);
if (ec) {
throw std::runtime_error("getConfigMap(): failed to canonicalize config path: " + abs.string());
}
std::ifstream file(canon);
if (!file.is_open()) { if (!file.is_open()) {
throw std::runtime_error("Failed to open config file at path: " + canon.string()); throw std::runtime_error("Failed to open config file at path: " + path);
} }
std::string line; std::string line;
@@ -252,78 +203,4 @@ namespace ColumnLynx::Utils {
return config; return config;
} }
std::vector<uint8_t> loadHexBytesFromFile(const std::string& path, size_t expectedBytes, const std::string& description, bool warnOnInsecurePermissions) {
namespace fs = std::filesystem;
std::error_code ec;
fs::path p(path);
fs::path abs = fs::absolute(p, ec);
if (ec) {
throw std::runtime_error("loadHexBytesFromFile(): failed to resolve path: " + path + " - " + ec.message());
}
if (!fs::exists(abs, ec) || ec) {
throw std::runtime_error("loadHexBytesFromFile(): file does not exist: " + abs.string());
}
fs::path canon = fs::canonical(abs, ec);
if (ec) {
throw std::runtime_error("loadHexBytesFromFile(): failed to canonicalize path: " + abs.string());
}
#ifndef _WIN32
if (warnOnInsecurePermissions) {
ec.clear();
fs::file_status status = fs::status(canon, ec);
if (ec) {
warn("loadHexBytesFromFile(): failed to inspect permissions for " + canon.string() + " - " + ec.message());
} else {
auto perms = status.permissions();
if ((perms & (fs::perms::group_all | fs::perms::others_all)) != fs::perms::none) {
warn(description + " file permissions are too permissive: " + canon.string() + " (recommend chmod 600)");
}
if (!fs::is_regular_file(status)) {
warn(description + " path is not a regular file: " + canon.string());
}
}
}
#endif
std::ifstream file(canon);
if (!file.is_open()) {
throw std::runtime_error("Failed to open " + description + " file at path: " + canon.string());
}
std::string hex((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
auto trim = [](std::string& s) {
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
s.pop_back();
}
size_t start = 0;
while (start < s.size() && std::isspace(static_cast<unsigned char>(s[start]))) {
++start;
}
if (start > 0) {
s.erase(0, start);
}
};
trim(hex);
if (hex.empty()) {
throw std::runtime_error(description + " file is empty: " + canon.string());
}
std::vector<uint8_t> bytes = hexStringToBytes(hex);
if (bytes.size() != expectedBytes) {
throw std::runtime_error(description + " file must contain exactly " + std::to_string(expectedBytes * 2) + " hex characters (" + std::to_string(expectedBytes) + " bytes), got " + std::to_string(bytes.size()) + " bytes");
}
return bytes;
}
} }

View File

@@ -4,11 +4,6 @@
#include <columnlynx/common/net/virtual_interface.hpp> #include <columnlynx/common/net/virtual_interface.hpp>
#include <spawn.h>
#include <sys/wait.h>
extern char **environ;
// This is all fucking voodoo dark magic. // This is all fucking voodoo dark magic.
#if defined(_WIN32) #if defined(_WIN32)
@@ -61,33 +56,6 @@ static void InitializeWintun()
#endif // _WIN32 #endif // _WIN32
namespace ColumnLynx::Net { namespace ColumnLynx::Net {
// Run a command without invoking a shell. Arguments are passed directly
// to the underlying process to avoid shell injection vulnerabilities.
static bool runCommand(const std::vector<std::string>& args) {
if (args.empty()) return false;
std::vector<char*> argv;
argv.reserve(args.size() + 1);
for (const auto &s : args) {
argv.push_back(const_cast<char*>(s.c_str()));
}
argv.push_back(nullptr);
pid_t pid;
int rc = posix_spawnp(&pid, argv[0], nullptr, nullptr, argv.data(), environ);
if (rc != 0) {
return false;
}
int status = 0;
if (waitpid(pid, &status, 0) == -1) {
return false;
}
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
// ------------------------------ Constructor ------------------------------ // ------------------------------ Constructor ------------------------------
VirtualInterface::VirtualInterface(const std::string& ifName) VirtualInterface::VirtualInterface(const std::string& ifName)
: mIfName(ifName), mFd(-1) : mIfName(ifName), mFd(-1)
@@ -213,8 +181,8 @@ static bool runCommand(const std::vector<std::string>& args) {
pfd.fd = mFd; pfd.fd = mFd;
pfd.events = POLLIN; pfd.events = POLLIN;
// timeout in ms; keep it small so shutdown is responsive. Reduced for lower latency. // timeout in ms; keep it small so shutdown is responsive
int ret = poll(&pfd, 1, 50); int ret = poll(&pfd, 1, 200);
if (ret == 0) { if (ret == 0) {
// No data yet // No data yet
@@ -339,10 +307,25 @@ static bool runCommand(const std::vector<std::string>& args) {
void VirtualInterface::resetIP() { void VirtualInterface::resetIP() {
#if defined(__linux__) #if defined(__linux__)
runCommand({"ip", "addr", "flush", "dev", mIfName}); char cmd[512];
snprintf(cmd, sizeof(cmd),
"ip addr flush dev %s",
mIfName.c_str()
);
system(cmd);
#elif defined(__APPLE__) #elif defined(__APPLE__)
runCommand({"ifconfig", mIfName, "inet", "0.0.0.0", "delete"}); char cmd[512];
runCommand({"ifconfig", mIfName, "inet6", "::", "delete"}); snprintf(cmd, sizeof(cmd),
"ifconfig %s inet 0.0.0.0 delete",
mIfName.c_str()
);
system(cmd);
snprintf(cmd, sizeof(cmd),
"ifconfig %s inet6 :: delete",
mIfName.c_str()
);
system(cmd);
// Wipe old routes // Wipe old routes
//snprintf(cmd, sizeof(cmd), //snprintf(cmd, sizeof(cmd),
@@ -374,19 +357,26 @@ static bool runCommand(const std::vector<std::string>& args) {
bool VirtualInterface::mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP, bool VirtualInterface::mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP,
uint8_t prefixLen, uint16_t mtu) uint8_t prefixLen, uint16_t mtu)
{ {
char cmd[512];
std::string ipStr = ipv4ToString(clientIP); std::string ipStr = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP); std::string peerStr = ipv4ToString(serverIP);
// Wipe the current config // Wipe the current config
runCommand({"ip", "addr", "flush", "dev", mIfName}); snprintf(cmd, sizeof(cmd),
"ip addr flush dev %s",
mIfName.c_str()
);
system(cmd);
// Add address with peer snprintf(cmd, sizeof(cmd),
std::string addrArg = ipStr + "/" + std::to_string(prefixLen); "ip addr add %s/%d peer %s dev %s",
runCommand({"ip", "addr", "add", addrArg, "peer", peerStr, "dev", mIfName}); ipStr.c_str(), prefixLen, peerStr.c_str(), mIfName.c_str());
system(cmd);
// Bring link up and set MTU snprintf(cmd, sizeof(cmd),
runCommand({"ip", "link", "set", "dev", mIfName, "up", "mtu", std::to_string(mtu)}); "ip link set dev %s up mtu %d", mIfName.c_str(), mtu);
system(cmd);
return true; return true;
} }
@@ -397,23 +387,39 @@ static bool runCommand(const std::vector<std::string>& args) {
bool VirtualInterface::mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP, bool VirtualInterface::mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP,
uint8_t prefixLen, uint16_t mtu) uint8_t prefixLen, uint16_t mtu)
{ {
char cmd[512];
std::string ipStr = ipv4ToString(clientIP); std::string ipStr = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP); std::string peerStr = ipv4ToString(serverIP);
std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false); std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false);
Utils::debug("Prefix string: " + prefixStr); Utils::debug("Prefix string: " + prefixStr);
// Reset IPv4 and IPv6 addresses // Reset
runCommand({"ifconfig", mIfName, "inet", "0.0.0.0", "delete"}); snprintf(cmd, sizeof(cmd),
runCommand({"ifconfig", mIfName, "inet6", "::", "delete"}); "ifconfig %s inet 0.0.0.0 delete",
mIfName.c_str()
);
system(cmd);
// Set address and netmask snprintf(cmd, sizeof(cmd),
std::string netArg = ipStr + " " + peerStr; // ifconfig expects ip peer "ifconfig %s inet6 :: delete",
runCommand({"ifconfig", mIfName, "inet", ipStr, peerStr, "mtu", std::to_string(mtu), "netmask", prefixStr, "up"}); mIfName.c_str()
);
system(cmd);
// Add route for the network // Set
std::string networkArg = ipStr + "/" + std::to_string(prefixLen); snprintf(cmd, sizeof(cmd),
runCommand({"route", "-n", "add", "-net", networkArg, "-interface", mIfName}); "ifconfig %s inet %s %s mtu %d netmask %s up",
mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
system(cmd);
// Host bits are auto-normalized by the kernel on macOS, so we don't need to worry about them not being zeroed out.
snprintf(cmd, sizeof(cmd),
"route -n add -net %s/%d -interface %s",
ipStr.c_str(), prefixLen, mIfName.c_str());
system(cmd);
Utils::log("Executed command: " + std::string(cmd));
return true; return true;
} }

View File

@@ -6,8 +6,6 @@
#include <iostream> #include <iostream>
#include <thread> #include <thread>
#include <chrono> #include <chrono>
#include <filesystem>
#include <cstring>
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/panic_handler.hpp> #include <columnlynx/common/panic_handler.hpp>
#include <columnlynx/server/net/tcp/tcp_server.hpp> #include <columnlynx/server/net/tcp/tcp_server.hpp>
@@ -94,7 +92,8 @@ int main(int argc, char** argv) {
#if defined(DEBUG) #if defined(DEBUG)
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" }); std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" });
#else #else
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" }); // A production server should never use random keys. If the config file cannot be read or does not contain keys, the server will fail to start.
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK", "SERVER_PUBLIC_KEY", "SERVER_PRIVATE_KEY" });
#endif #endif
serverState.serverConfig = config; serverState.serverConfig = config;
@@ -105,28 +104,30 @@ int main(int argc, char** argv) {
// Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs) // Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs)
serverState.virtualInterface = tun; serverState.virtualInterface = tun;
// Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory)
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>(); std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
const std::string serverPublicKeyPath = configPath + "public.key"; auto itPubkey = config.find("SERVER_PUBLIC_KEY");
const std::string serverPrivateKeyPath = configPath + "private.key"; auto itPrivkey = config.find("SERVER_PRIVATE_KEY");
namespace fs = std::filesystem; if (itPubkey != config.end() && itPrivkey != config.end()) {
bool serverKeyFilesPresent = fs::exists(serverPublicKeyPath) && fs::exists(serverPrivateKeyPath); log("Loading keypair from config file.");
if (serverKeyFilesPresent) {
log("Loading server keypair from key files.");
PublicKey pk = Utils::loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(serverPublicKeyPath, "server public key"); PublicKey pk;
PrivateSeed seed = Utils::loadHexArrayFromFile<crypto_sign_SEEDBYTES>(serverPrivateKeyPath, "server private key", true); PrivateSeed seed;
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), seed.size(), seed.begin());
std::copy_n(Utils::hexStringToBytes(itPubkey->second).begin(), pk.size(), pk.begin());
if (!sodiumWrapper->recomputeKeys(seed, pk)) { if (!sodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute keypair from key files!"); throw std::runtime_error("Failed to recompute keypair from config file values!");
} }
} else { } else {
#if defined(DEBUG) #if defined(DEBUG)
warn("No server keypair files found! Using random key."); warn("No keypair found in config file! Using random key.");
#else #else
throw std::runtime_error("No server keypair files found! Cannot start server without keys."); throw std::runtime_error("No keypair found in config file! Cannot start server without keys.");
#endif #endif
} }
log("Server public key: " + bytesToHexString(sodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); log("Server public key: " + bytesToHexString(sodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES));
@@ -143,22 +144,6 @@ int main(int argc, char** argv) {
auto server = std::make_shared<TCPServer>(io, serverPort()); auto server = std::make_shared<TCPServer>(io, serverPort());
auto udpServer = std::make_shared<UDPServer>(io, serverPort()); auto udpServer = std::make_shared<UDPServer>(io, serverPort());
// Schedule periodic cleanup of expired sessions every 5 minutes
auto cleanupTimer = std::make_shared<asio::steady_timer>(io);
auto cleanupHandler = std::make_shared<std::function<void(const asio::error_code&)>>();
*cleanupHandler = [cleanupTimer, cleanupHandler](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) return; // Timer cancelled
try {
SessionRegistry::getInstance().cleanupExpired();
} catch (const std::exception& e) {
Utils::warn(std::string("SessionRegistry::cleanupExpired() threw: ") + e.what());
}
cleanupTimer->expires_after(std::chrono::minutes(5));
cleanupTimer->async_wait(*cleanupHandler);
};
cleanupTimer->expires_after(std::chrono::minutes(5));
cleanupTimer->async_wait(*cleanupHandler);
asio::signal_set signals(io, SIGINT, SIGTERM); asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](const std::error_code&, int) { signals.async_wait([&](const std::error_code&, int) {
log("Received termination signal. Shutting down server gracefully."); log("Received termination signal. Shutting down server gracefully.");
@@ -167,8 +152,6 @@ int main(int argc, char** argv) {
ServerSession::getInstance().setHostRunning(false); ServerSession::getInstance().setHostRunning(false);
server->stop(); server->stop();
udpServer->stop(); udpServer->stop();
// Cancel cleanup timer
cleanupTimer->cancel();
}); });
}); });
@@ -189,35 +172,15 @@ int main(int argc, char** argv) {
continue; continue;
} }
if (packet.size() < 20) {
Utils::warn("TUN: Dropping packet smaller than IPv4 header (" + std::to_string(packet.size()) + " bytes)");
continue;
}
const uint8_t* ip = packet.data(); const uint8_t* ip = packet.data();
uint8_t ipVersion = (ip[0] >> 4); uint32_t srcIP = ntohl(*(uint32_t*)(ip + 12)); // IPv4 source address offset
if (ipVersion != 4) { uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset
Utils::debug("TUN: Non-IPv4 packet received (version=" + std::to_string(ipVersion) + "), skipping server IPv4 routing path.");
continue;
}
uint32_t srcIPNet = 0;
uint32_t dstIPNet = 0;
std::memcpy(&srcIPNet, ip + 12, sizeof(srcIPNet)); // IPv4 source address offset
std::memcpy(&dstIPNet, ip + 16, sizeof(dstIPNet)); // IPv4 destination address offset
uint32_t srcIP = ntohl(srcIPNet);
uint32_t dstIP = ntohl(dstIPNet);
// First, check if destination IP is a registered client (e.g., server responding to client or client-to-client) // First, check if destination IP is a registered client (e.g., server responding to client or client-to-client)
auto dstSession = SessionRegistry::getInstance().getByIP(dstIP); auto dstSession = SessionRegistry::getInstance().getByIP(dstIP);
if (dstSession) { if (dstSession) {
// Destination is a registered client, enforce MTU and forward to that client's session // Destination is a registered client, forward to that client's session
const size_t MTU = 1420; // Enforce configured MTU; TODO: read from server config
if (packet.size() > MTU) {
Utils::warn("TUN: Dropping oversized packet (" + std::to_string(packet.size()) + " > MTU " + std::to_string(MTU) + ")");
} else {
udpServer->sendData(dstSession->sessionID, std::string(packet.begin(), packet.end())); udpServer->sendData(dstSession->sessionID, std::string(packet.begin(), packet.end()));
}
continue; continue;
} }

View File

@@ -14,31 +14,23 @@ namespace ColumnLynx::Net::TCP {
Utils::warn("Failed to get remote endpoint: " + std::string(e.what())); Utils::warn("Failed to get remote endpoint: " + std::string(e.what()));
} }
mHandler->onMessage([weakSelf = weak_from_this()](AnyMessageType type, const std::string& data) { mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
if (auto self = weakSelf.lock()) { mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
self->mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
}
}); });
mHandler->onDisconnect([weakSelf = weak_from_this()](const asio::error_code& ec) { mHandler->onDisconnect([this](const asio::error_code& ec) {
auto self = weakSelf.lock();
if (!self) {
return;
}
// Peer has closed; finalize locally without sending RST // Peer has closed; finalize locally without sending RST
Utils::log("Client disconnected: " + self->mRemoteIP + " - " + ec.message()); Utils::log("Client disconnected: " + mRemoteIP + " - " + ec.message());
asio::error_code ec2; asio::error_code ec2;
if (self->mHandler) { mHandler->socket().close(ec2);
self->mHandler->socket().close(ec2);
}
SessionRegistry::getInstance().erase(self->mConnectionSessionID); SessionRegistry::getInstance().erase(mConnectionSessionID);
SessionRegistry::getInstance().deallocIP(self->mConnectionSessionID); SessionRegistry::getInstance().deallocIP(mConnectionSessionID);
Utils::log("Closed connection to " + self->mRemoteIP); Utils::log("Closed connection to " + mRemoteIP);
if (self->mOnDisconnect) { if (mOnDisconnect) {
self->mOnDisconnect(self); mOnDisconnect(shared_from_this());
} }
}); });
@@ -85,7 +77,7 @@ namespace ColumnLynx::Net::TCP {
void TCPConnection::mStartHeartbeat() { void TCPConnection::mStartHeartbeat() {
auto self = shared_from_this(); auto self = shared_from_this();
mHeartbeatTimer.expires_after(std::chrono::seconds(5)); mHeartbeatTimer.expires_after(std::chrono::seconds(5));
mHeartbeatTimer.async_wait([self](const asio::error_code& ec) { mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) { if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled return; // Timer was cancelled
} }
@@ -98,13 +90,10 @@ namespace ColumnLynx::Net::TCP {
// Remove socket forcefully, client is dead // Remove socket forcefully, client is dead
asio::error_code ec; asio::error_code ec;
if (self->mHandler) { mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
self->mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); mHandler->socket().close(ec);
self->mHandler->socket().close(ec);
}
SessionRegistry::getInstance().erase(self->mConnectionSessionID); SessionRegistry::getInstance().erase(self->mConnectionSessionID);
SessionRegistry::getInstance().deallocIP(self->mConnectionSessionID);
return; return;
} }
@@ -124,8 +113,8 @@ namespace ColumnLynx::Net::TCP {
case ClientMessageType::HANDSHAKE_INIT: { case ClientMessageType::HANDSHAKE_INIT: {
Utils::log("Received HANDSHAKE_INIT from " + reqAddr); Utils::log("Received HANDSHAKE_INIT from " + reqAddr);
if (data.size() != 1 + crypto_sign_PUBLICKEYBYTES) { if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) {
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " has invalid size: " + std::to_string(data.size())); Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short.");
disconnect(); disconnect();
return; return;
} }
@@ -141,7 +130,7 @@ namespace ColumnLynx::Net::TCP {
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + "."); Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + ".");
PublicKey signPk; PublicKey signPk;
std::memcpy(signPk.data(), data.data() + 1, sizeof(signPk)); std::memcpy(signPk.data(), data.data() + 1, std::min(data.size() - 1, sizeof(signPk)));
// We can safely store this without further checking, the client will need to send the encrypted AES key in a way where they must possess the corresponding private key anyways. // We can safely store this without further checking, the client will need to send the encrypted AES key in a way where they must possess the corresponding private key anyways.
int r = crypto_sign_ed25519_pk_to_curve25519(mConnectionPublicKey.data(), signPk.data()); // Store the client's public encryption key key (for identification) int r = crypto_sign_ed25519_pk_to_curve25519(mConnectionPublicKey.data(), signPk.data()); // Store the client's public encryption key key (for identification)
@@ -173,15 +162,9 @@ namespace ColumnLynx::Net::TCP {
case ClientMessageType::HANDSHAKE_CHALLENGE: { case ClientMessageType::HANDSHAKE_CHALLENGE: {
Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr); Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr);
// Convert to byte array - require exact size // Convert to byte array
if (data.size() != 32) {
Utils::warn("HANDSHAKE_CHALLENGE has invalid size: " + std::to_string(data.size()));
disconnect();
return;
}
uint8_t challengeData[32]; uint8_t challengeData[32];
std::memcpy(challengeData, data.data(), sizeof(challengeData)); std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData)));
// Sign the challenge // Sign the challenge
Signature sig = Utils::LibSodiumWrapper::signMessage( Signature sig = Utils::LibSodiumWrapper::signMessage(

View File

@@ -95,23 +95,7 @@ namespace ColumnLynx::Net::UDP {
UDPPacketHeader hdr{}; UDPPacketHeader hdr{};
uint8_t nonce[12]; uint8_t nonce[12];
uint32_t prefix = session->noncePrefix; uint32_t prefix = session->noncePrefix;
// Increment send counter with overflow protection uint64_t sendCount = const_cast<SessionState*>(session.get())->send_ctr.fetch_add(1, std::memory_order_relaxed);
uint64_t sendCount = 0;
{
auto ptr = const_cast<SessionState*>(session.get());
uint64_t old = ptr->send_ctr.load(std::memory_order_relaxed);
for (;;) {
if (old == std::numeric_limits<uint64_t>::max()) {
Utils::error("UDP: send counter overflow for session " + std::to_string(sessionID));
return;
}
if (ptr->send_ctr.compare_exchange_weak(old, old + 1, std::memory_order_relaxed)) {
sendCount = old;
break;
}
// old updated by compare_exchange_weak, loop
}
}
memcpy(nonce, &prefix, sizeof(uint32_t)); // Prefix nonce memcpy(nonce, &prefix, sizeof(uint32_t)); // Prefix nonce
memcpy(nonce + sizeof(uint32_t), &sendCount, sizeof(uint64_t)); // Use send count as nonce suffix to ensure uniqueness memcpy(nonce + sizeof(uint32_t), &sendCount, sizeof(uint64_t)); // Use send count as nonce suffix to ensure uniqueness
std::copy_n(nonce, 12, hdr.nonce.data()); std::copy_n(nonce, 12, hdr.nonce.data());

View File

@@ -1,62 +0,0 @@
// Tests for hex key file loading helpers
#include <cassert>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <columnlynx/common/utils.hpp>
#include <sodium.h>
int main() {
namespace fs = std::filesystem;
using namespace ColumnLynx::Utils;
fs::path tempDir = fs::temp_directory_path() / "columnlynx_key_loader_test";
fs::remove_all(tempDir);
fs::create_directories(tempDir);
auto publicKeyPath = tempDir / "public.key";
auto privateKeyPath = tempDir / "private.key";
{
std::ofstream pub(publicKeyPath);
pub << "00112233445566778899aabbccddeeff00112233445566778899AABBCCDDEEFF";
}
{
std::ofstream priv(privateKeyPath);
priv << "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100\n";
}
auto pk = loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(publicKeyPath.string(), "public key");
auto sk = loadHexArrayFromFile<crypto_sign_SEEDBYTES>(privateKeyPath.string(), "private key", true);
assert(pk[0] == 0x00 && pk[1] == 0x11 && pk.back() == 0xFF);
assert(sk[0] == 0xFF && sk[1] == 0xEE && sk.back() == 0x00);
bool threwMissing = false;
try {
(void)loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>((tempDir / "missing.key").string(), "missing key");
} catch (const std::exception&) {
threwMissing = true;
}
assert(threwMissing && "Missing key file should throw");
auto badLengthPath = tempDir / "bad-length.key";
{
std::ofstream bad(badLengthPath);
bad << "abcd";
}
bool threwBadLength = false;
try {
(void)loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(badLengthPath.string(), "bad length key");
} catch (const std::exception&) {
threwBadLength = true;
}
assert(threwBadLength && "Wrong-length key file should throw");
std::cout << "Key file loader tests passed\n";
fs::remove_all(tempDir);
return 0;
}

View File

@@ -1,42 +0,0 @@
// 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;
}

View File

@@ -1,49 +0,0 @@
// 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 &reg = 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;
}

View File

@@ -1,43 +0,0 @@
// 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 &reg = 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;
}

View File

@@ -1,30 +0,0 @@
// 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;
}