From 757d0d251d317e6c72e989bb21c4c9ae2ed2c859 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Sun, 8 Feb 2026 19:20:27 +0100 Subject: [PATCH 1/6] Moved client data passing to a dedicated ClientSession class instead of passing through a bunch of pointers at init --- include/columnlynx/client/client_session.hpp | 71 +++++++++++++++++++ .../columnlynx/client/net/tcp/tcp_client.hpp | 27 ++----- .../columnlynx/client/net/udp/udp_client.hpp | 11 +-- src/client/client_session.cpp | 71 +++++++++++++++++++ src/client/main.cpp | 22 ++++-- src/client/net/tcp/tcp_client.cpp | 28 ++++---- src/client/net/udp/udp_client.cpp | 18 ++--- 7 files changed, 192 insertions(+), 56 deletions(-) create mode 100644 include/columnlynx/client/client_session.hpp create mode 100644 src/client/client_session.cpp diff --git a/include/columnlynx/client/client_session.hpp b/include/columnlynx/client/client_session.hpp new file mode 100644 index 0000000..a456ed1 --- /dev/null +++ b/include/columnlynx/client/client_session.hpp @@ -0,0 +1,71 @@ +// client_session.hpp - Client Session data for ColumnLynx +// Copyright (C) 2026 DcruBro +// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. + +#pragma once + +#include +#include +#include +#include +#include + +namespace ColumnLynx { + struct ClientState { + std::shared_ptr sodiumWrapper; + SymmetricKey aesKey; + bool insecureMode; + std::string configPath; + std::shared_ptr virtualInterface; + uint64_t sessionID; + + ~ClientState() { sodium_memzero(aesKey.data(), aesKey.size()); } + ClientState(const ClientState&) = delete; + ClientState& operator=(const ClientState&) = delete; + ClientState(ClientState&&) = default; + ClientState& operator=(ClientState&&) = default; + + explicit ClientState() = default; + + explicit ClientState(std::shared_ptr sodium, SymmetricKey& k, bool insecure, + std::string& config, std::shared_ptr tun, uint64_t session) + : sodiumWrapper(sodium), aesKey(k), insecureMode(insecure), configPath(config), virtualInterface(tun), sessionID(session) {} + }; + + class ClientSession { + public: + // Return a reference to the Client Session instance + static ClientSession& getInstance() { static ClientSession instance; return instance; } + + // Return the current client state + std::shared_ptr getClientState() const; + + // Set the client state + void setClientState(std::shared_ptr state); + + // Get the wrapper for libsodium + const std::shared_ptr& getSodiumWrapper() const; + // Get the AES key + const SymmetricKey& getAESKey() const; + // Get whether insecure mode is enabled + bool isInsecureMode() const; + // Get the config path + const std::string& getConfigPath() const; + // Get the virtual interface + const std::shared_ptr& getVirtualInterface() const; + // Get the session ID + uint64_t getSessionID() const; + + // Setters + void setSodiumWrapper(std::shared_ptr sodiumWrapper); + void setAESKey(const SymmetricKey& aesKey); + void setInsecureMode(bool insecureMode); + void setConfigPath(const std::string& configPath); + void setVirtualInterface(std::shared_ptr virtualInterface); + void setSessionID(uint64_t sessionID); + + private: + mutable std::shared_mutex mMutex; + std::shared_ptr mClientState{nullptr}; + }; +} \ No newline at end of file diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index 219e708..b9f011d 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -17,6 +17,7 @@ #include #include #include +#include using asio::ip::tcp; @@ -25,28 +26,20 @@ namespace ColumnLynx::Net::TCP { public: TCPClient(asio::io_context& ioContext, const std::string& host, - const std::string& port, - std::shared_ptr sodiumWrapper, - std::shared_ptr> aesKey, - std::shared_ptr sessionIDRef, - bool insecureMode, - std::string& configPath, - std::shared_ptr tun = nullptr) + const std::string& port) : mResolver(ioContext), mSocket(ioContext), mHost(host), mPort(port), - mLibSodiumWrapper(sodiumWrapper), - mGlobalKeyRef(aesKey), - mSessionIDRef(sessionIDRef), - mInsecureMode(insecureMode), mHeartbeatTimer(mSocket.get_executor()), mLastHeartbeatReceived(std::chrono::steady_clock::now()), - mLastHeartbeatSent(std::chrono::steady_clock::now()), - mTun(tun), - mConfigDirPath(configPath) + mLastHeartbeatSent(std::chrono::steady_clock::now()) { + // Get initial client config + std::string configPath = ClientSession::getInstance().getConfigPath(); + std::shared_ptr mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper(); + // Preload the config map mRawClientConfig = Utils::getConfigMap(configPath + "client_config"); @@ -104,20 +97,14 @@ namespace ColumnLynx::Net::TCP { std::string mHost, mPort; uint8_t mServerPublicKey[32]; // Assuming 256-bit public key std::array mSubmittedChallenge{}; - std::shared_ptr mLibSodiumWrapper; uint64_t mConnectionSessionID; SymmetricKey mConnectionAESKey; - std::shared_ptr> mGlobalKeyRef; // Reference to global AES key - std::shared_ptr mSessionIDRef; // Reference to global Session ID - bool mInsecureMode; // Insecure mode flag asio::steady_timer mHeartbeatTimer; std::chrono::steady_clock::time_point mLastHeartbeatReceived; std::chrono::steady_clock::time_point mLastHeartbeatSent; int mMissedHeartbeats = 0; bool mIsHostDomain; Protocol::TunConfig mTunConfig; - std::shared_ptr mTun = nullptr; std::unordered_map mRawClientConfig; - std::string mConfigDirPath; }; } \ No newline at end of file diff --git a/include/columnlynx/client/net/udp/udp_client.hpp b/include/columnlynx/client/net/udp/udp_client.hpp index 1c42add..af613d9 100644 --- a/include/columnlynx/client/net/udp/udp_client.hpp +++ b/include/columnlynx/client/net/udp/udp_client.hpp @@ -10,17 +10,15 @@ #include #include #include +#include namespace ColumnLynx::Net::UDP { class UDPClient { public: UDPClient(asio::io_context& ioContext, const std::string& host, - const std::string& port, - std::shared_ptr> aesKeyRef, - std::shared_ptr sessionIDRef, - std::shared_ptr tunRef = nullptr) - : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef) + const std::string& port) + : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port) { mStartReceive(); } @@ -43,9 +41,6 @@ namespace ColumnLynx::Net::UDP { asio::ip::udp::endpoint mRemoteEndpoint; std::string mHost; std::string mPort; - std::shared_ptr> mAesKeyRef; - std::shared_ptr mSessionIDRef; - std::shared_ptr mTunRef = nullptr; std::array mRecvBuffer; // Adjust size as needed }; } \ No newline at end of file diff --git a/src/client/client_session.cpp b/src/client/client_session.cpp new file mode 100644 index 0000000..ce193bd --- /dev/null +++ b/src/client/client_session.cpp @@ -0,0 +1,71 @@ +// client_session.cpp - Client Session data for ColumnLynx +// Copyright (C) 2026 DcruBro +// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. + +#include + +namespace ColumnLynx { + std::shared_ptr ClientSession::getClientState() const { + std::shared_lock lock(mMutex); + return mClientState; + } + + void ClientSession::setClientState(std::shared_ptr state) { + std::unique_lock lock(mMutex); + mClientState = state; + } + + const std::shared_ptr& ClientSession::getSodiumWrapper() const { + return getClientState()->sodiumWrapper; + } + + const SymmetricKey& ClientSession::getAESKey() const { + return getClientState()->aesKey; + } + + bool ClientSession::isInsecureMode() const { + return getClientState()->insecureMode; + } + + const std::string& ClientSession::getConfigPath() const { + return getClientState()->configPath; + } + + const std::shared_ptr& ClientSession::getVirtualInterface() const { + return getClientState()->virtualInterface; + } + + uint64_t ClientSession::getSessionID() const { + return getClientState()->sessionID; + } + + void ClientSession::setSodiumWrapper(std::shared_ptr sodiumWrapper) { + std::unique_lock lock(mMutex); + mClientState->sodiumWrapper = sodiumWrapper; + } + + void ClientSession::setAESKey(const SymmetricKey& aesKey) { + std::unique_lock lock(mMutex); + mClientState->aesKey = aesKey; + } + + void ClientSession::setInsecureMode(bool insecureMode) { + std::unique_lock lock(mMutex); + mClientState->insecureMode = insecureMode; + } + + void ClientSession::setConfigPath(const std::string& configPath) { + std::unique_lock lock(mMutex); + mClientState->configPath = configPath; + } + + void ClientSession::setVirtualInterface(std::shared_ptr virtualInterface) { + std::unique_lock lock(mMutex); + mClientState->virtualInterface = virtualInterface; + } + + void ClientSession::setSessionID(uint64_t sessionID) { + std::unique_lock lock(mMutex); + mClientState->sessionID = sessionID; + } +} \ No newline at end of file diff --git a/src/client/main.cpp b/src/client/main.cpp index e0b7a08..7419b35 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #if defined(__WIN32__) #include @@ -99,24 +101,34 @@ int main(int argc, char** argv) { #endif } + struct ClientState initialState{}; + initialState.configPath = configPath; + initialState.insecureMode = insecureMode; + std::shared_ptr tun = std::make_shared(optionsObj["interface"].as()); log("Using virtual interface: " + tun->getName()); + initialState.virtualInterface = tun; std::shared_ptr sodiumWrapper = std::make_shared(); debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper->getPublicKey(), 32)); debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper->getPrivateKey(), 64)); + initialState.sodiumWrapper = sodiumWrapper; - std::shared_ptr> aesKey = std::make_shared>(); - aesKey->fill(0); // Defualt zeroed state until modified by handshake - std::shared_ptr sessionID = std::make_shared(0); + std::array aesKey = std::array(); + aesKey.fill(0); // Defualt zeroed state until modified by handshake + uint64_t sessionID = 0; + initialState.aesKey = aesKey; + initialState.sessionID = sessionID; + + ColumnLynx::ClientSession::getInstance().setClientState(std::make_shared(std::move(initialState))); // Set initial state if (insecureMode) { warn("You have started the client with the --ignore-whitelist. This means that the client will NOT attempt to verify the server's public key. This is INSECURE and SHOULDN'T be used!"); } asio::io_context io; - auto client = std::make_shared(io, host, port, sodiumWrapper, aesKey, sessionID, insecureMode, configPath, tun); - auto udpClient = std::make_shared(io, host, port, aesKey, sessionID, tun); + auto client = std::make_shared(io, host, port); // TODO: Move to ClientSession state + auto udpClient = std::make_shared(io, host, port); client->start(); udpClient->start(); diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp index b070a94..7e04960 100644 --- a/src/client/net/tcp/tcp_client.cpp +++ b/src/client/net/tcp/tcp_client.cpp @@ -50,6 +50,8 @@ namespace ColumnLynx::Net::TCP { mLibSodiumWrapper->getXPublicKey(), mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES );*/ + + const auto& mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper(); payload.insert(payload.end(), mLibSodiumWrapper->getPublicKey(), mLibSodiumWrapper->getPublicKey() + crypto_sign_PUBLICKEYBYTES @@ -130,11 +132,9 @@ namespace ColumnLynx::Net::TCP { mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); mHandler->socket().close(ec); mConnected = false; - - mGlobalKeyRef = nullptr; - if (mSessionIDRef) { - *mSessionIDRef = 0; - } + + ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros + ClientSession::getInstance().setSessionID(0); return; } @@ -155,9 +155,10 @@ namespace ColumnLynx::Net::TCP { Utils::log("Received server identity. Public Key: " + hexServerPub); // Verify pubkey against whitelisted_keys - std::vector whitelistedKeys = Utils::getWhitelistedKeys(mConfigDirPath); + const std::string& configPath = ClientSession::getInstance().getConfigPath(); + std::vector whitelistedKeys = Utils::getWhitelistedKeys(configPath); if (std::find(whitelistedKeys.begin(), whitelistedKeys.end(), Utils::bytesToHexString(mServerPublicKey, 32)) == whitelistedKeys.end()) { // Key verification is handled in later steps of the handshake - if (!mInsecureMode) { + if (!ClientSession::getInstance().isInsecureMode()) { Utils::error("Server public key not in whitelisted_keys. Terminating connection."); disconnect(); return; @@ -193,16 +194,14 @@ namespace ColumnLynx::Net::TCP { // Generate AES key and send confirmation mConnectionAESKey = Utils::LibSodiumWrapper::generateRandom256Bit(); - if (mGlobalKeyRef) { // Copy to the global reference - std::copy(mConnectionAESKey.begin(), mConnectionAESKey.end(), mGlobalKeyRef->begin()); - } + ClientSession::getInstance().setAESKey(mConnectionAESKey); AsymNonce nonce{}; randombytes_buf(nonce.data(), nonce.size()); // TODO: This is pretty redundant, it should return the required type directly std::array arrayPrivateKey; - std::copy(mLibSodiumWrapper->getXPrivateKey(), - mLibSodiumWrapper->getXPrivateKey() + 32, + std::copy(ClientSession::getInstance().getSodiumWrapper()->getXPrivateKey(), + ClientSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32, arrayPrivateKey.begin()); std::vector encr = Utils::LibSodiumWrapper::encryptAsymmetric( @@ -249,15 +248,14 @@ namespace ColumnLynx::Net::TCP { Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID)); - if (mSessionIDRef) { // Copy to the global reference - *mSessionIDRef = mConnectionSessionID; - } + ClientSession::getInstance().setSessionID(mConnectionSessionID); uint32_t clientIP = ntohl(mTunConfig.clientIP); uint32_t serverIP = ntohl(mTunConfig.serverIP); uint8_t prefixLen = mTunConfig.prefixLength; uint16_t mtu = mTunConfig.mtu; + const auto& mTun = ClientSession::getInstance().getVirtualInterface(); if (mTun) { mTun->configureIP(clientIP, serverIP, prefixLen, mtu); } diff --git a/src/client/net/udp/udp_client.cpp b/src/client/net/udp/udp_client.cpp index 10fba23..e5e6702 100644 --- a/src/client/net/udp/udp_client.cpp +++ b/src/client/net/udp/udp_client.cpp @@ -48,7 +48,7 @@ namespace ColumnLynx::Net::UDP { UDPPacketHeader hdr{}; randombytes_buf(hdr.nonce.data(), hdr.nonce.size()); - if (mAesKeyRef == nullptr || mSessionIDRef == nullptr) { + if (ClientSession::getInstance().getAESKey().empty() || ClientSession::getInstance().getSessionID() == 0) { Utils::error("UDP Client AES key or Session ID reference is null!"); return; } @@ -57,7 +57,7 @@ namespace ColumnLynx::Net::UDP { auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage( reinterpret_cast(data.data()), data.size(), - *mAesKeyRef, hdr.nonce, "udp-data" + ClientSession::getInstance().getAESKey(), hdr.nonce, "udp-data" //std::string(reinterpret_cast(&mSessionIDRef), sizeof(uint64_t)) ); @@ -67,9 +67,10 @@ namespace ColumnLynx::Net::UDP { reinterpret_cast(&hdr), reinterpret_cast(&hdr) + sizeof(UDPPacketHeader) ); + uint64_t sessionID = ClientSession::getInstance().getSessionID(); packet.insert(packet.end(), - reinterpret_cast(mSessionIDRef.get()), - reinterpret_cast(mSessionIDRef.get()) + sizeof(uint64_t) + reinterpret_cast(&sessionID), + reinterpret_cast(&sessionID) + sizeof(uint64_t) ); packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end()); @@ -120,8 +121,8 @@ namespace ColumnLynx::Net::UDP { uint64_t sessionID; std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t)); - if (sessionID != *mSessionIDRef) { - Utils::warn("Got packet that isn't for me! Dropping!"); + if (sessionID != ClientSession::getInstance().getSessionID()) { + Utils::warn("This packet that isn't for me! Dropping!"); return; } @@ -131,13 +132,13 @@ namespace ColumnLynx::Net::UDP { mRecvBuffer.begin() + bytes ); - if (mAesKeyRef == nullptr) { + if (ClientSession::getInstance().getAESKey().empty()) { Utils::error("UDP Client AES key reference is null!"); return; } std::vector plaintext = Utils::LibSodiumWrapper::decryptMessage( - ciphertext.data(), ciphertext.size(), *mAesKeyRef, hdr.nonce, "udp-data" + ciphertext.data(), ciphertext.size(), ClientSession::getInstance().getAESKey(), hdr.nonce, "udp-data" //std::string(reinterpret_cast(&mSessionIDRef), sizeof(uint64_t)) ); @@ -149,6 +150,7 @@ namespace ColumnLynx::Net::UDP { Utils::debug("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes)); // Write to TUN + const auto& mTunRef = ClientSession::getInstance().getVirtualInterface(); if (mTunRef) { mTunRef->writePacket(plaintext); } From 6d40dbe00d8365d07d8434eacec0a54b8150c29f Mon Sep 17 00:00:00 2001 From: DcruBro Date: Mon, 9 Feb 2026 07:32:39 +0100 Subject: [PATCH 2/6] version update --- CMakeLists.txt | 2 +- src/common/utils.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 267a960..0e48f08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16) # If MAJOR is 0, and MINOR > 0, Version is BETA project(ColumnLynx - VERSION 1.0.1 + VERSION 1.1.0 LANGUAGES CXX ) diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 92282b7..4cdbc97 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -85,7 +85,7 @@ namespace ColumnLynx::Utils { } std::string getVersion() { - return "1.0.1"; + return "1.1.0"; } unsigned short serverPort() { From 316498c7456dc0e8d03755c97130fd5833c70abb Mon Sep 17 00:00:00 2001 From: DcruBro Date: Mon, 9 Feb 2026 14:20:26 +0100 Subject: [PATCH 3/6] TESTING: Move server stuff to a server session config for central/global resources --- .../server/net/tcp/tcp_connection.hpp | 16 +--- .../columnlynx/server/net/tcp/tcp_server.hpp | 26 ++---- .../columnlynx/server/net/udp/udp_server.hpp | 16 ++-- include/columnlynx/server/server_session.hpp | 62 +++++++++++++ src/server/main.cpp | 30 +++++- src/server/net/tcp/tcp_connection.cpp | 16 ++-- src/server/net/tcp/tcp_server.cpp | 7 +- src/server/net/udp/udp_server.cpp | 4 +- src/server/server_session.cpp | 92 +++++++++++++++++++ 9 files changed, 213 insertions(+), 56 deletions(-) create mode 100644 include/columnlynx/server/server_session.hpp create mode 100644 src/server/server_session.cpp diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index bbc59f1..040a2a2 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { class TCPConnection : public std::enable_shared_from_this { @@ -26,12 +27,9 @@ namespace ColumnLynx::Net::TCP { static pointer create( asio::ip::tcp::socket socket, - std::shared_ptr sodiumWrapper, - std::unordered_map* serverConfig, - std::string configDirPath, std::function onDisconnect) { - auto conn = pointer(new TCPConnection(std::move(socket), sodiumWrapper, serverConfig, configDirPath)); + auto conn = pointer(new TCPConnection(std::move(socket))); conn->mOnDisconnect = std::move(onDisconnect); return conn; } @@ -51,15 +49,12 @@ namespace ColumnLynx::Net::TCP { std::array getAESKey() const; private: - TCPConnection(asio::ip::tcp::socket socket, std::shared_ptr sodiumWrapper, std::unordered_map* serverConfig, std::string configDirPath) + TCPConnection(asio::ip::tcp::socket socket) : mHandler(std::make_shared(std::move(socket))), - mLibSodiumWrapper(sodiumWrapper), - mRawServerConfig(serverConfig), mHeartbeatTimer(mHandler->socket().get_executor()), mLastHeartbeatReceived(std::chrono::steady_clock::now()), - mLastHeartbeatSent(std::chrono::steady_clock::now()), - mConfigDirPath(configDirPath) + mLastHeartbeatSent(std::chrono::steady_clock::now()) {} // Start the heartbeat routine @@ -69,8 +64,6 @@ namespace ColumnLynx::Net::TCP { std::shared_ptr mHandler; std::function)> mOnDisconnect; - std::shared_ptr mLibSodiumWrapper; - std::unordered_map* mRawServerConfig; std::array mConnectionAESKey; uint64_t mConnectionSessionID; AsymPublicKey mConnectionPublicKey; @@ -79,6 +72,5 @@ namespace ColumnLynx::Net::TCP { std::chrono::steady_clock::time_point mLastHeartbeatSent; int mMissedHeartbeats = 0; std::string mRemoteIP; // Cached remote IP to avoid calling remote_endpoint() on closed sockets - std::string mConfigDirPath; }; } \ No newline at end of file diff --git a/include/columnlynx/server/net/tcp/tcp_server.hpp b/include/columnlynx/server/net/tcp/tcp_server.hpp index 7e532e2..224993e 100644 --- a/include/columnlynx/server/net/tcp/tcp_server.hpp +++ b/include/columnlynx/server/net/tcp/tcp_server.hpp @@ -17,29 +17,23 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { class TCPServer { public: TCPServer(asio::io_context& ioContext, - uint16_t port, - std::shared_ptr sodiumWrapper, - std::shared_ptr hostRunning, - std::string& configPath, - bool ipv4Only = false) + uint16_t port) : mIoContext(ioContext), - mAcceptor(ioContext), - mSodiumWrapper(sodiumWrapper), - mHostRunning(hostRunning), - mConfigDirPath(configPath) + mAcceptor(ioContext) { // Preload the config map - mRawServerConfig = Utils::getConfigMap(configPath + "server_config", {"NETWORK", "SUBNET_MASK"}); - asio::error_code ec_open, ec_v6only, ec_bind; - if (!ipv4Only) { + bool isIPv4Only = ServerSession::getInstance().isIPv4Only(); + + if (!isIPv4Only) { // Try IPv6 (dual-stack if supported) asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port); @@ -55,8 +49,8 @@ namespace ColumnLynx::Net::TCP { } // If IPv6 bind failed OR IPv6 open failed OR forced IPv4-only - if (ipv4Only || ec_open || ec_bind) { - if (!ipv4Only) + if (isIPv4Only || ec_open || ec_bind) { + if (!isIPv4Only) Utils::warn("TCP: IPv6 unavailable (open=" + ec_open.message() + ", bind=" + ec_bind.message() + "), falling back to IPv4 only"); @@ -84,10 +78,6 @@ namespace ColumnLynx::Net::TCP { asio::io_context &mIoContext; asio::ip::tcp::acceptor mAcceptor; std::unordered_set mClients; - std::shared_ptr mSodiumWrapper; - std::shared_ptr mHostRunning; - std::unordered_map mRawServerConfig; - std::string mConfigDirPath; }; } \ No newline at end of file diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp index 61feb05..a46b628 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -9,16 +9,18 @@ #include #include #include +#include +#include namespace ColumnLynx::Net::UDP { class UDPServer { public: - UDPServer(asio::io_context& ioContext, uint16_t port, std::shared_ptr hostRunning, bool ipv4Only = false, std::shared_ptr tun = nullptr) - : mSocket(ioContext), mHostRunning(hostRunning), mTun(tun) + UDPServer(asio::io_context& ioContext, uint16_t port) + : mSocket(ioContext) { asio::error_code ec_open, ec_v6only, ec_bind; - if (!ipv4Only) { + if (!mIpv4Only) { asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port); // Try opening IPv6 socket @@ -34,8 +36,8 @@ namespace ColumnLynx::Net::UDP { } // Fallback to IPv4 if IPv6 is unusable - if (ipv4Only || ec_open || ec_bind) { - if (!ipv4Only) { + if (mIpv4Only || ec_open || ec_bind) { + if (!mIpv4Only) { Utils::warn( "UDP: IPv6 unavailable (open=" + ec_open.message() + ", bind=" + ec_bind.message() + @@ -70,7 +72,7 @@ namespace ColumnLynx::Net::UDP { asio::ip::udp::socket mSocket; asio::ip::udp::endpoint mRemoteEndpoint; std::array mRecvBuffer; // 2048 seems stable - std::shared_ptr mHostRunning; - std::shared_ptr mTun; + bool mIpv4Only = ServerSession::getInstance().isIPv4Only(); + const std::shared_ptr mTun = ServerSession::getInstance().getVirtualInterface(); }; } \ No newline at end of file diff --git a/include/columnlynx/server/server_session.hpp b/include/columnlynx/server/server_session.hpp new file mode 100644 index 0000000..bbb05a6 --- /dev/null +++ b/include/columnlynx/server/server_session.hpp @@ -0,0 +1,62 @@ +// server_session.hpp - Client Session data for ColumnLynx +// Copyright (C) 2026 DcruBro +// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. + +#pragma once + +#include +#include +#include +#include +#include + +namespace ColumnLynx { + struct ServerState { + std::shared_ptr sodiumWrapper; + std::shared_ptr virtualInterface; + std::string configPath; + std::unordered_map serverConfig; + bool ipv4Only; + bool hostRunning; + + ~ServerState() = default; + ServerState(const ServerState&) = delete; + ServerState& operator=(const ServerState&) = delete; + ServerState(ServerState&&) = default; + ServerState& operator=(ServerState&&) = default; + + explicit ServerState() = default; + }; + + class ServerSession { + public: + // Return a reference to the Server Session instance + static ServerSession& getInstance() { static ServerSession instance; return instance; } + + // Return the current server state + std::shared_ptr getServerState() const; + + // Set the server state + void setServerState(std::shared_ptr state); + + // Getters + std::shared_ptr getSodiumWrapper() const; + const std::string& getConfigPath() const; + const std::unordered_map& getRawServerConfig() const; + const std::shared_ptr& getVirtualInterface() const; + bool isIPv4Only() const; + bool isHostRunning() const; + + // Setters + void setSodiumWrapper(std::shared_ptr sodiumWrapper); + void setConfigPath(const std::string& configPath); + void setRawServerConfig(const std::unordered_map& config); + void setVirtualInterface(std::shared_ptr tun); + void setIPv4Only(bool ipv4Only); + void setHostRunning(bool hostRunning); + + private: + mutable std::shared_mutex mMutex; + std::shared_ptr mServerState{nullptr}; + }; +} \ No newline at end of file diff --git a/src/server/main.cpp b/src/server/main.cpp index 3d8da0d..35b8188 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #if defined(__WIN32__) #include @@ -69,6 +70,8 @@ int main(int argc, char** argv) { //WintunInitialize(); #endif + struct ServerState serverState{}; + // Get the config path, ENV > CLI > /etc/columnlynx std::string configPath = optionsObj["config-dir"].as(); const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR"); @@ -84,11 +87,23 @@ int main(int argc, char** argv) { #endif } - std::unordered_map config = Utils::getConfigMap(configPath + "server_config"); + serverState.configPath = configPath; + +#if defined(DEBUG) + std::unordered_map config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" }); +#else + // 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 config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK", "SERVER_PUBLIC_KEY", "SERVER_PRIVATE_KEY" }); +#endif + + serverState.serverConfig = config; std::shared_ptr tun = std::make_shared(optionsObj["interface"].as()); log("Using virtual interface: " + tun->getName()); + // Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs) + serverState.virtualInterface = tun; + // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) std::shared_ptr sodiumWrapper = std::make_shared(); @@ -117,19 +132,24 @@ int main(int argc, char** argv) { log("Server public key: " + bytesToHexString(sodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); - std::shared_ptr hostRunning = std::make_shared(true); + serverState.sodiumWrapper = sodiumWrapper; + serverState.ipv4Only = ipv4Only; + serverState.hostRunning = true; + + // Store the global state; from now on, it should only be accessed through the ServerSession singleton, which will ensure thread safety with its internal mutex + ServerSession::getInstance().setServerState(std::make_shared(std::move(serverState))); asio::io_context io; - auto server = std::make_shared(io, serverPort(), sodiumWrapper, hostRunning, configPath, ipv4Only); - auto udpServer = std::make_shared(io, serverPort(), hostRunning, ipv4Only, tun); + auto server = std::make_shared(io, serverPort()); + auto udpServer = std::make_shared(io, serverPort()); asio::signal_set signals(io, SIGINT, SIGTERM); signals.async_wait([&](const std::error_code&, int) { log("Received termination signal. Shutting down server gracefully."); done = 1; asio::post(io, [&]() { - *hostRunning = false; + ServerSession::getInstance().setHostRunning(false); server->stop(); udpServer->stop(); }); diff --git a/src/server/net/tcp/tcp_connection.cpp b/src/server/net/tcp/tcp_connection.cpp index 3626c5b..4226e04 100644 --- a/src/server/net/tcp/tcp_connection.cpp +++ b/src/server/net/tcp/tcp_connection.cpp @@ -145,7 +145,7 @@ namespace ColumnLynx::Net::TCP { Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size())); - std::vector whitelistedKeys = Utils::getWhitelistedKeys(mConfigDirPath); + std::vector whitelistedKeys = Utils::getWhitelistedKeys(ServerSession::getInstance().getConfigPath()); if (std::find(whitelistedKeys.begin(), whitelistedKeys.end(), Utils::bytesToHexString(signPk.data(), signPk.size())) == whitelistedKeys.end()) { Utils::warn("Non-whitelisted client attempted to connect, terminating. Client IP: " + reqAddr); @@ -156,7 +156,7 @@ namespace ColumnLynx::Net::TCP { Utils::debug("Client " + reqAddr + " passed authorized_keys"); - mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist + mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(ServerSession::getInstance().getSodiumWrapper()->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist break; } case ClientMessageType::HANDSHAKE_CHALLENGE: { @@ -169,7 +169,7 @@ namespace ColumnLynx::Net::TCP { // Sign the challenge Signature sig = Utils::LibSodiumWrapper::signMessage( challengeData, sizeof(challengeData), - mLibSodiumWrapper->getPrivateKey() + ServerSession::getInstance().getSodiumWrapper()->getPrivateKey() ); mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response @@ -191,8 +191,8 @@ namespace ColumnLynx::Net::TCP { std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size()); try { std::array arrayPrivateKey; - std::copy(mLibSodiumWrapper->getXPrivateKey(), - mLibSodiumWrapper->getXPrivateKey() + 32, + std::copy(ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey(), + ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32, arrayPrivateKey.begin()); // Decrypt the AES key using the client's public key and server's private key @@ -217,8 +217,10 @@ namespace ColumnLynx::Net::TCP { // Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose) Nonce symNonce{}; // All zeros - std::string networkString = mRawServerConfig->find("NETWORK")->second; // The load check guarantees that this value exists - uint8_t configMask = std::stoi(mRawServerConfig->find("SUBNET_MASK")->second); // Same deal here + const auto& serverConfig = ServerSession::getInstance().getRawServerConfig(); + + std::string networkString = serverConfig.find("NETWORK")->second; // The load check guarantees that this value exists + uint8_t configMask = std::stoi(serverConfig.find("SUBNET_MASK")->second); // Same deal here uint32_t baseIP = Net::VirtualInterface::stringToIpv4(networkString); diff --git a/src/server/net/tcp/tcp_server.cpp b/src/server/net/tcp/tcp_server.cpp index a9dc472..77c30b7 100644 --- a/src/server/net/tcp/tcp_server.cpp +++ b/src/server/net/tcp/tcp_server.cpp @@ -27,16 +27,13 @@ namespace ColumnLynx::Net::TCP { } Utils::error("Accept failed: " + ec.message()); // Try again only if still running - if (mHostRunning && *mHostRunning && mAcceptor.is_open()) + if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open()) mStartAccept(); return; } auto client = TCPConnection::create( std::move(socket), - mSodiumWrapper, - &mRawServerConfig, - mConfigDirPath, [this](std::shared_ptr c) { mClients.erase(c); Utils::log("Client removed."); @@ -46,7 +43,7 @@ namespace ColumnLynx::Net::TCP { client->start(); Utils::log("Accepted new client connection."); - if (mHostRunning && *mHostRunning && mAcceptor.is_open()) + if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open()) mStartAccept(); } ); diff --git a/src/server/net/udp/udp_server.cpp b/src/server/net/udp/udp_server.cpp index c6730d2..b1b3284 100644 --- a/src/server/net/udp/udp_server.cpp +++ b/src/server/net/udp/udp_server.cpp @@ -16,11 +16,11 @@ namespace ColumnLynx::Net::UDP { if (ec) { if (ec == asio::error::operation_aborted) return; // Socket closed // Other recv error - if (mHostRunning && *mHostRunning) mStartReceive(); + if (ServerSession::getInstance().isHostRunning()) mStartReceive(); return; } if (bytes > 0) mHandlePacket(bytes); - if (mHostRunning && *mHostRunning) mStartReceive(); + if (ServerSession::getInstance().isHostRunning()) mStartReceive(); } ); } diff --git a/src/server/server_session.cpp b/src/server/server_session.cpp new file mode 100644 index 0000000..14addc6 --- /dev/null +++ b/src/server/server_session.cpp @@ -0,0 +1,92 @@ +// server_session.cpp - Client Session data for ColumnLynx +// Copyright (C) 2026 DcruBro +// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. + +#include + +namespace ColumnLynx { + std::shared_ptr ServerSession::getServerState() const { + std::shared_lock lock(mMutex); + return mServerState; + } + + void ServerSession::setServerState(std::shared_ptr state) { + std::unique_lock lock(mMutex); + mServerState = std::move(state); + } + + std::shared_ptr ServerSession::getSodiumWrapper() const { + std::shared_lock lock(mMutex); + return mServerState ? mServerState->sodiumWrapper : nullptr; + } + + const std::string& ServerSession::getConfigPath() const { + static const std::string emptyString; + std::shared_ptr state = getServerState(); + return state ? state->configPath : emptyString; + } + + const std::unordered_map& ServerSession::getRawServerConfig() const { + static const std::unordered_map emptyMap; + std::shared_ptr state = getServerState(); + return state ? state->serverConfig : emptyMap; + } + + const std::shared_ptr& ServerSession::getVirtualInterface() const { + static const std::shared_ptr nullTun = nullptr; + std::shared_ptr state = getServerState(); + return state ? state->virtualInterface : nullTun; + } + + bool ServerSession::isIPv4Only() const { + std::shared_ptr state = getServerState(); + return state ? state->ipv4Only : false; + } + + bool ServerSession::isHostRunning() const { + std::shared_ptr state = getServerState(); + return state ? state->hostRunning : false; + } + + void ServerSession::setSodiumWrapper(std::shared_ptr sodiumWrapper) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->sodiumWrapper = std::move(sodiumWrapper); + } + + void ServerSession::setConfigPath(const std::string& configPath) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->configPath = configPath; + } + + void ServerSession::setRawServerConfig(const std::unordered_map& config) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->serverConfig = config; + } + + void ServerSession::setVirtualInterface(std::shared_ptr tun) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->virtualInterface = std::move(tun); + } + + void ServerSession::setIPv4Only(bool ipv4Only) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->ipv4Only = ipv4Only; + } + + void ServerSession::setHostRunning(bool hostRunning) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->hostRunning = hostRunning; + } +} \ No newline at end of file From 14298453b3ba0e5e5e354d648984a90c9c824dbc Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 10 Feb 2026 13:27:15 +0100 Subject: [PATCH 4/6] TESTING: protocol version 2 --- README.md | 8 ++-- include/columnlynx/client/client_session.hpp | 41 ++++++++++++++++--- .../columnlynx/client/net/tcp/tcp_client.hpp | 2 +- .../common/net/session_registry.hpp | 26 +++++++----- include/columnlynx/common/utils.hpp | 21 ---------- .../server/net/tcp/tcp_connection.hpp | 4 +- .../columnlynx/server/net/udp/udp_server.hpp | 2 +- src/client/client_session.cpp | 4 +- src/client/main.cpp | 5 ++- src/client/net/tcp/tcp_client.cpp | 2 +- src/client/net/udp/udp_client.cpp | 11 ++++- src/common/session_registry.cpp | 14 +++---- src/common/utils.cpp | 2 +- src/server/net/tcp/tcp_connection.cpp | 11 ++--- src/server/net/udp/udp_server.cpp | 31 ++++++++------ 15 files changed, 110 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 834d5ab..d3ad9d7 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,8 @@ ColumnLynx makes use of both **TCP** and **UDP**. **TCP** is used for the initia It operates on port **48042** for both TCP and UDP. +Current protocol version is **2**. + Generally, all transmission is done in **little-endian byte order**, since pretty much every single modern architecture uses it by default. The only exemption to this is the **transmission of IP addresses** (for the **Virtual Interface**), which is **big-endian**. ### Handshake Procedure @@ -231,7 +233,7 @@ The Client now generates a random aesKey (32 bytes long) C: HANDSHAKE_EXCHANGE_KEY -The Server now assigns a local 8 byte session ID in the Session Registry. +The Server now assigns a local 4 byte session ID in the Session Registry. S: HANDSHAKE_EXCHANGE_KEY_CONFIRM ``` @@ -242,7 +244,7 @@ The **Client** and **Server** have now securely exchanged a symmetric **AES Key* Packet exchange and the general data tunneling is done via **Standard UDP** (*see the **UDP Packet** in **Data***). -The **header** of the sent packet always includes a **random 12 byte nonce** used to obscure the **encrypted payload / data** and the **Session ID** assigned by the server to the client (8 bytes). This makes the header **20 bytes long**. +The **header** of the sent packet always includes a **12 byte nonce** derived from a random **4 byte base nonce** and the **send count** to ensure a unique nonce, used to obscure the **encrypted payload / data** and the **Session ID** assigned by the server to the client (4 bytes). This makes the header **16 bytes long**. The **payload / data** of the sent packet is **always encrypted** using the exchanged **AES Key** and obscured using the **random nonce**. @@ -298,7 +300,7 @@ The **Data** is generally just the **raw underlying packet** forwarded to the se | Type | Length | Name | Description | |:-----|:-------|:-----|:------------| | uint8_t | 12 bytes | **Header** - Nonce | Random nonce to obfuscate encrypted contents | -| uint64_t | 8 bytes | **Header** - Session ID | The unique and random session identifier for the client | +| uint32_t | 4 bytes | **Header** - Session ID | The unique and random session identifier for the client | | uint8_t | variable | Data | General data / payload | ## Misc. diff --git a/include/columnlynx/client/client_session.hpp b/include/columnlynx/client/client_session.hpp index a456ed1..1cb3188 100644 --- a/include/columnlynx/client/client_session.hpp +++ b/include/columnlynx/client/client_session.hpp @@ -17,7 +17,10 @@ namespace ColumnLynx { bool insecureMode; std::string configPath; std::shared_ptr virtualInterface; - uint64_t sessionID; + uint32_t sessionID; + uint64_t recv_cnt; + uint64_t send_cnt; + uint32_t noncePrefix; ~ClientState() { sodium_memzero(aesKey.data(), aesKey.size()); } ClientState(const ClientState&) = delete; @@ -28,8 +31,8 @@ namespace ColumnLynx { explicit ClientState() = default; explicit ClientState(std::shared_ptr sodium, SymmetricKey& k, bool insecure, - std::string& config, std::shared_ptr tun, uint64_t session) - : sodiumWrapper(sodium), aesKey(k), insecureMode(insecure), configPath(config), virtualInterface(tun), sessionID(session) {} + std::string& config, std::shared_ptr tun, uint32_t session, uint64_t recv, uint64_t send) + : sodiumWrapper(sodium), aesKey(k), insecureMode(insecure), configPath(config), virtualInterface(tun), sessionID(session), recv_cnt(recv), send_cnt(send) {} }; class ClientSession { @@ -54,7 +57,21 @@ namespace ColumnLynx { // Get the virtual interface const std::shared_ptr& getVirtualInterface() const; // Get the session ID - uint64_t getSessionID() const; + uint32_t getSessionID() const; + uint64_t getRecvCount() const { + std::shared_lock lock(mMutex); + return mClientState->recv_cnt; + } + + uint64_t getSendCount() const { + std::shared_lock lock(mMutex); + return mClientState->send_cnt; + } + + uint32_t getNoncePrefix() const { + std::shared_lock lock(mMutex); + return mClientState->noncePrefix; + } // Setters void setSodiumWrapper(std::shared_ptr sodiumWrapper); @@ -62,7 +79,21 @@ namespace ColumnLynx { void setInsecureMode(bool insecureMode); void setConfigPath(const std::string& configPath); void setVirtualInterface(std::shared_ptr virtualInterface); - void setSessionID(uint64_t sessionID); + void setSessionID(uint32_t sessionID); + void incrementRecvCount() { + std::unique_lock lock(mMutex); + mClientState->recv_cnt++; + } + + void incrementSendCount() { + std::unique_lock lock(mMutex); + mClientState->send_cnt++; + } + + void setNoncePrefix(uint32_t prefix) { + std::unique_lock lock(mMutex); + mClientState->noncePrefix = prefix; + } private: mutable std::shared_mutex mMutex; diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index b9f011d..2faf3db 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -97,7 +97,7 @@ namespace ColumnLynx::Net::TCP { std::string mHost, mPort; uint8_t mServerPublicKey[32]; // Assuming 256-bit public key std::array mSubmittedChallenge{}; - uint64_t mConnectionSessionID; + uint32_t mConnectionSessionID; SymmetricKey mConnectionAESKey; asio::steady_timer mHeartbeatTimer; std::chrono::steady_clock::time_point mLastHeartbeatReceived; diff --git a/include/columnlynx/common/net/session_registry.hpp b/include/columnlynx/common/net/session_registry.hpp index d686aae..cc764cc 100644 --- a/include/columnlynx/common/net/session_registry.hpp +++ b/include/columnlynx/common/net/session_registry.hpp @@ -27,8 +27,9 @@ namespace ColumnLynx::Net { std::chrono::steady_clock::time_point expires{}; // Time of expiry uint32_t clientTunIP; // Assigned IP uint32_t serverTunIP; // Server IP - uint64_t sessionID; // Session ID + uint32_t sessionID; // Session ID Nonce base_nonce{}; + uint32_t noncePrefix; ~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); } SessionState(const SessionState&) = delete; @@ -36,7 +37,7 @@ namespace ColumnLynx::Net { SessionState(SessionState&&) = default; SessionState& operator=(SessionState&&) = default; - explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), uint32_t clientIP = 0, uint32_t serverIP = 0, uint64_t id = 0) : aesKey(k), clientTunIP(clientIP), serverTunIP(serverIP), sessionID(id) { + explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), uint32_t clientIP = 0, uint32_t serverIP = 0, uint32_t id = 0) : aesKey(k), clientTunIP(clientIP), serverTunIP(serverIP), sessionID(id) { expires = created + ttl; } @@ -44,6 +45,11 @@ namespace ColumnLynx::Net { void setUDPEndpoint(const asio::ip::udp::endpoint& ep) { udpEndpoint = ep; } + + void setBaseNonce() { + Utils::debug("Generating random base nonce for session " + std::to_string(sessionID)); + randombytes_buf(base_nonce.data(), base_nonce.size()); + } }; class SessionRegistry { @@ -52,19 +58,19 @@ namespace ColumnLynx::Net { static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; } // Insert or replace a session entry - void put(uint64_t sessionID, std::shared_ptr state); + void put(uint32_t sessionID, std::shared_ptr state); // Lookup a session entry by session ID - std::shared_ptr get(uint64_t sessionID) const; + std::shared_ptr get(uint32_t sessionID) const; // Lookup a session entry by IPv4 std::shared_ptr getByIP(uint32_t ip) const; // Get a snapshot of the Session Registry - std::unordered_map> snapshot() const; + std::unordered_map> snapshot() const; // Remove a session by ID - void erase(uint64_t sessionID); + void erase(uint32_t sessionID); // Cleanup expired sessions void cleanupExpired(); @@ -78,15 +84,15 @@ namespace ColumnLynx::Net { uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const; // Lock IP to session ID; Do NOT call before put() - You will segfault! - void lockIP(uint64_t sessionID, uint32_t ip); + void lockIP(uint32_t sessionID, uint32_t ip); // Unlock IP from session ID - void deallocIP(uint64_t sessionID); + void deallocIP(uint32_t sessionID); private: mutable std::shared_mutex mMutex; - std::unordered_map> mSessions; - std::unordered_map mSessionIPs; + std::unordered_map> mSessions; + std::unordered_map mSessionIPs; std::unordered_map> mIPSessions; }; } diff --git a/include/columnlynx/common/utils.hpp b/include/columnlynx/common/utils.hpp index 9702f09..51d6f69 100644 --- a/include/columnlynx/common/utils.hpp +++ b/include/columnlynx/common/utils.hpp @@ -65,27 +65,6 @@ namespace ColumnLynx::Utils { return std::string(reinterpret_cast(data), length); } - inline constexpr uint64_t cbswap64(uint64_t x) { - return ((x & 0x00000000000000FFULL) << 56) | - ((x & 0x000000000000FF00ULL) << 40) | - ((x & 0x0000000000FF0000ULL) << 24) | - ((x & 0x00000000FF000000ULL) << 8) | - ((x & 0x000000FF00000000ULL) >> 8) | - ((x & 0x0000FF0000000000ULL) >> 24) | - ((x & 0x00FF000000000000ULL) >> 40) | - ((x & 0xFF00000000000000ULL) >> 56); - } - - // host -> big-endian (for little-endian hosts) - 64 bit - inline constexpr uint64_t chtobe64(uint64_t x) { - return cbswap64(x); - } - - // big-endian -> host (for little-endian hosts) - 64 bit - inline constexpr uint64_t cbe64toh(uint64_t x) { - return cbswap64(x); - } - template T cbswap128(const T& x) { static_assert(sizeof(T) == 16, "cbswap128 requires a 128-bit type"); diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index 040a2a2..0afdcdf 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -44,7 +44,7 @@ namespace ColumnLynx::Net::TCP { void disconnect(bool echo = true); // Get the assigned session ID - uint64_t getSessionID() const; + uint32_t getSessionID() const; // Get the assigned AES key; You should probably access this via the Session Registry instead std::array getAESKey() const; @@ -65,7 +65,7 @@ namespace ColumnLynx::Net::TCP { std::shared_ptr mHandler; std::function)> mOnDisconnect; std::array mConnectionAESKey; - uint64_t mConnectionSessionID; + uint32_t mConnectionSessionID; AsymPublicKey mConnectionPublicKey; asio::steady_timer mHeartbeatTimer; std::chrono::steady_clock::time_point mLastHeartbeatReceived; diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp index a46b628..73866f5 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -61,7 +61,7 @@ namespace ColumnLynx::Net::UDP { void stop(); // Send UDP data to an endpoint; Fetched via the Session Registry - void sendData(const uint64_t sessionID, const std::string& data); + void sendData(uint32_t sessionID, const std::string& data); private: // Start receiving UDP data diff --git a/src/client/client_session.cpp b/src/client/client_session.cpp index ce193bd..86f174d 100644 --- a/src/client/client_session.cpp +++ b/src/client/client_session.cpp @@ -35,7 +35,7 @@ namespace ColumnLynx { return getClientState()->virtualInterface; } - uint64_t ClientSession::getSessionID() const { + uint32_t ClientSession::getSessionID() const { return getClientState()->sessionID; } @@ -64,7 +64,7 @@ namespace ColumnLynx { mClientState->virtualInterface = virtualInterface; } - void ClientSession::setSessionID(uint64_t sessionID) { + void ClientSession::setSessionID(uint32_t sessionID) { std::unique_lock lock(mMutex); mClientState->sessionID = sessionID; } diff --git a/src/client/main.cpp b/src/client/main.cpp index 7419b35..f29724d 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -104,6 +104,9 @@ int main(int argc, char** argv) { struct ClientState initialState{}; initialState.configPath = configPath; initialState.insecureMode = insecureMode; + initialState.send_cnt = 0; + initialState.recv_cnt = 0; + randombytes_buf(&initialState.noncePrefix, sizeof(uint32_t)); // Randomize nonce prefix std::shared_ptr tun = std::make_shared(optionsObj["interface"].as()); log("Using virtual interface: " + tun->getName()); @@ -116,7 +119,7 @@ int main(int argc, char** argv) { std::array aesKey = std::array(); aesKey.fill(0); // Defualt zeroed state until modified by handshake - uint64_t sessionID = 0; + uint32_t sessionID = 0; initialState.aesKey = aesKey; initialState.sessionID = sessionID; diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp index 7e04960..c5ed047 100644 --- a/src/client/net/tcp/tcp_client.cpp +++ b/src/client/net/tcp/tcp_client.cpp @@ -244,7 +244,7 @@ namespace ColumnLynx::Net::TCP { std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID)); std::memcpy(&mTunConfig, decrypted.data() + sizeof(mConnectionSessionID), sizeof(Protocol::TunConfig)); - mConnectionSessionID = Utils::cbe64toh(mConnectionSessionID); + mConnectionSessionID = ntohl(mConnectionSessionID); Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID)); diff --git a/src/client/net/udp/udp_client.cpp b/src/client/net/udp/udp_client.cpp index e5e6702..b284a23 100644 --- a/src/client/net/udp/udp_client.cpp +++ b/src/client/net/udp/udp_client.cpp @@ -46,7 +46,12 @@ namespace ColumnLynx::Net::UDP { void UDPClient::sendMessage(const std::string& data) { UDPPacketHeader hdr{}; - randombytes_buf(hdr.nonce.data(), hdr.nonce.size()); + uint8_t nonce[12]; + uint32_t prefix = ClientSession::getInstance().getNoncePrefix(); + uint64_t sendCount = ClientSession::getInstance().getSendCount(); + memcpy(nonce, &prefix, sizeof(uint32_t)); // Prefix nonce with client-specific random value + 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()); if (ClientSession::getInstance().getAESKey().empty() || ClientSession::getInstance().getSessionID() == 0) { Utils::error("UDP Client AES key or Session ID reference is null!"); @@ -62,7 +67,7 @@ namespace ColumnLynx::Net::UDP { ); std::vector packet; - packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size()); + packet.reserve(sizeof(UDPPacketHeader) + encryptedPayload.size()); packet.insert(packet.end(), reinterpret_cast(&hdr), reinterpret_cast(&hdr) + sizeof(UDPPacketHeader) @@ -76,6 +81,8 @@ namespace ColumnLynx::Net::UDP { mSocket.send_to(asio::buffer(packet), mRemoteEndpoint); Utils::debug("Sent UDP packet of size " + std::to_string(packet.size())); + + ClientSession::getInstance().incrementSendCount(); } void UDPClient::stop() { diff --git a/src/common/session_registry.cpp b/src/common/session_registry.cpp index 1a4bf12..491264d 100644 --- a/src/common/session_registry.cpp +++ b/src/common/session_registry.cpp @@ -5,13 +5,13 @@ #include namespace ColumnLynx::Net { - void SessionRegistry::put(uint64_t sessionID, std::shared_ptr state) { + void SessionRegistry::put(uint32_t sessionID, std::shared_ptr state) { std::unique_lock lock(mMutex); mSessions[sessionID] = std::move(state); mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID]; } - std::shared_ptr SessionRegistry::get(uint64_t sessionID) const { + std::shared_ptr SessionRegistry::get(uint32_t sessionID) const { std::shared_lock lock(mMutex); auto it = mSessions.find(sessionID); return (it == mSessions.end()) ? nullptr : it->second; @@ -23,14 +23,14 @@ namespace ColumnLynx::Net { return (it == mIPSessions.end()) ? nullptr : it->second; } - std::unordered_map> SessionRegistry::snapshot() const { - std::unordered_map> snap; + std::unordered_map> SessionRegistry::snapshot() const { + std::unordered_map> snap; std::shared_lock lock(mMutex); snap = mSessions; return snap; } - void SessionRegistry::erase(uint64_t sessionID) { + void SessionRegistry::erase(uint32_t sessionID) { std::unique_lock lock(mMutex); mSessions.erase(sessionID); } @@ -77,7 +77,7 @@ namespace ColumnLynx::Net { return 0; } - void SessionRegistry::lockIP(uint64_t sessionID, uint32_t ip) { + void SessionRegistry::lockIP(uint32_t sessionID, uint32_t ip) { std::unique_lock lock(mMutex); mSessionIPs[sessionID] = ip; @@ -87,7 +87,7 @@ namespace ColumnLynx::Net { mIPSessions[ip] = mSessions.find(sessionID)->second; } - void SessionRegistry::deallocIP(uint64_t sessionID) { + void SessionRegistry::deallocIP(uint32_t sessionID) { std::unique_lock lock(mMutex); auto it = mSessionIPs.find(sessionID); diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 4cdbc97..a2429fd 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -93,7 +93,7 @@ namespace ColumnLynx::Utils { } unsigned char protocolVersion() { - return 1; + return 2; } std::string bytesToHexString(const uint8_t* bytes, size_t length) { diff --git a/src/server/net/tcp/tcp_connection.cpp b/src/server/net/tcp/tcp_connection.cpp index 4226e04..334e056 100644 --- a/src/server/net/tcp/tcp_connection.cpp +++ b/src/server/net/tcp/tcp_connection.cpp @@ -66,7 +66,7 @@ namespace ColumnLynx::Net::TCP { Utils::log("Initiated graceful disconnect (half-close) to " + mRemoteIP); } - uint64_t TCPConnection::getSessionID() const { + uint32_t TCPConnection::getSessionID() const { return mConnectionSessionID; } @@ -247,11 +247,11 @@ namespace ColumnLynx::Net::TCP { tunConfig.dns1 = htonl(0x08080808); // 8.8.8.8 tunConfig.dns2 = 0; - uint64_t sessionIDNet = Utils::chtobe64(mConnectionSessionID); + uint32_t sessionIDNet = htonl(mConnectionSessionID); - std::vector payload(sizeof(uint64_t) + sizeof(tunConfig)); - std::memcpy(payload.data(), &sessionIDNet, sizeof(uint64_t)); - std::memcpy(payload.data() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig)); + std::vector payload(sizeof(uint32_t) + sizeof(tunConfig)); + std::memcpy(payload.data(), &sessionIDNet, sizeof(uint32_t)); + std::memcpy(payload.data() + sizeof(uint32_t), &tunConfig, sizeof(tunConfig)); std::vector encryptedPayload = Utils::LibSodiumWrapper::encryptMessage( payload.data(), payload.size(), @@ -263,6 +263,7 @@ namespace ColumnLynx::Net::TCP { // Add to session registry Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned (" + std::to_string(mConnectionSessionID) + ")."); auto session = std::make_shared(mConnectionAESKey, std::chrono::hours(12), clientIP, htonl(0x0A0A0001), mConnectionSessionID); + session->setBaseNonce(); // Set it SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session)); SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP); diff --git a/src/server/net/udp/udp_server.cpp b/src/server/net/udp/udp_server.cpp index b1b3284..85015a3 100644 --- a/src/server/net/udp/udp_server.cpp +++ b/src/server/net/udp/udp_server.cpp @@ -26,16 +26,17 @@ namespace ColumnLynx::Net::UDP { } void UDPServer::mHandlePacket(std::size_t bytes) { - if (bytes < sizeof(UDPPacketHeader)) + if (bytes < sizeof(UDPPacketHeader) + sizeof(uint32_t)) return; const auto* hdr = reinterpret_cast(mRecvBuffer.data()); - // Get plaintext session ID (assuming first 8 bytes after nonce (header)) - uint64_t sessionID = 0; - std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t)); + // Get plaintext session ID (assuming first 4 bytes after nonce (header)) + uint32_t sessionIDNet = 0; + std::memcpy(&sessionIDNet, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint32_t)); + uint32_t sessionID = ntohl(sessionIDNet); - auto it = mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t); + auto it = mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t); std::vector encryptedPayload(it, mRecvBuffer.begin() + bytes); // Get associated session state @@ -54,7 +55,7 @@ namespace ColumnLynx::Net::UDP { encryptedPayload.data(), encryptedPayload.size(), session->aesKey, hdr->nonce, "udp-data" - //std::string(reinterpret_cast(&sessionID), sizeof(uint64_t)) + //std::string(reinterpret_cast(&sessionID), sizeof(uint32_t)) ); Utils::debug("Passed decryption"); @@ -76,7 +77,7 @@ namespace ColumnLynx::Net::UDP { } } - void UDPServer::sendData(const uint64_t sessionID, const std::string& data) { + void UDPServer::sendData(uint32_t sessionID, const std::string& data) { // Find the IPv4/IPv6 endpoint for the session std::shared_ptr session = SessionRegistry::getInstance().get(sessionID); if (!session) { @@ -92,23 +93,29 @@ namespace ColumnLynx::Net::UDP { // Prepare packet UDPPacketHeader hdr{}; - randombytes_buf(hdr.nonce.data(), hdr.nonce.size()); + uint8_t nonce[12]; + uint32_t prefix = session->noncePrefix; + uint64_t sendCount = const_cast(session.get())->send_ctr.fetch_add(1, std::memory_order_relaxed); + 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 + std::copy_n(nonce, 12, hdr.nonce.data()); auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage( reinterpret_cast(data.data()), data.size(), session->aesKey, hdr.nonce, "udp-data" - //std::string(reinterpret_cast(&sessionID), sizeof(uint64_t)) + //std::string(reinterpret_cast(&sessionID), sizeof(uint32_t)) ); std::vector packet; - packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size()); + packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint32_t) + encryptedPayload.size()); packet.insert(packet.end(), reinterpret_cast(&hdr), reinterpret_cast(&hdr) + sizeof(UDPPacketHeader) ); + uint32_t sessionIDNet = htonl(sessionID); packet.insert(packet.end(), - reinterpret_cast(&sessionID), - reinterpret_cast(&sessionID) + sizeof(sessionID) + reinterpret_cast(&sessionIDNet), + reinterpret_cast(&sessionIDNet) + sizeof(sessionIDNet) ); packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end()); From 27bd2cd2ecdafaeb93ab4e4278d3f37d6be0c02e Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 10 Feb 2026 19:24:07 +0100 Subject: [PATCH 5/6] endianness --- src/client/net/udp/udp_client.cpp | 16 +++++++++------- src/server/net/udp/udp_server.cpp | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/client/net/udp/udp_client.cpp b/src/client/net/udp/udp_client.cpp index b284a23..9c12c8a 100644 --- a/src/client/net/udp/udp_client.cpp +++ b/src/client/net/udp/udp_client.cpp @@ -72,10 +72,11 @@ namespace ColumnLynx::Net::UDP { reinterpret_cast(&hdr), reinterpret_cast(&hdr) + sizeof(UDPPacketHeader) ); - uint64_t sessionID = ClientSession::getInstance().getSessionID(); + uint32_t sessionID = static_cast(ClientSession::getInstance().getSessionID()); + uint32_t sessionIDNet = sessionID; packet.insert(packet.end(), - reinterpret_cast(&sessionID), - reinterpret_cast(&sessionID) + sizeof(uint64_t) + reinterpret_cast(&sessionIDNet), + reinterpret_cast(&sessionIDNet) + sizeof(uint32_t) ); packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end()); @@ -115,7 +116,7 @@ namespace ColumnLynx::Net::UDP { } void UDPClient::mHandlePacket(std::size_t bytes) { - if (bytes < sizeof(UDPPacketHeader) + sizeof(uint64_t)) { + if (bytes < sizeof(UDPPacketHeader) + sizeof(uint32_t)) { Utils::warn("UDP Client received packet too small to process."); return; } @@ -125,8 +126,9 @@ namespace ColumnLynx::Net::UDP { std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader)); // Parse session ID - uint64_t sessionID; - std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t)); + uint32_t sessionIDNet; + std::memcpy(&sessionIDNet, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint32_t)); + uint32_t sessionID = ntohl(sessionIDNet); if (sessionID != ClientSession::getInstance().getSessionID()) { Utils::warn("This packet that isn't for me! Dropping!"); @@ -135,7 +137,7 @@ namespace ColumnLynx::Net::UDP { // Decrypt payload std::vector ciphertext( - mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t), + mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t), mRecvBuffer.begin() + bytes ); diff --git a/src/server/net/udp/udp_server.cpp b/src/server/net/udp/udp_server.cpp index 85015a3..d99ee1e 100644 --- a/src/server/net/udp/udp_server.cpp +++ b/src/server/net/udp/udp_server.cpp @@ -31,10 +31,10 @@ namespace ColumnLynx::Net::UDP { const auto* hdr = reinterpret_cast(mRecvBuffer.data()); - // Get plaintext session ID (assuming first 4 bytes after nonce (header)) + // Get plaintext session ID (first 4 bytes after header, in network byte order) uint32_t sessionIDNet = 0; std::memcpy(&sessionIDNet, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint32_t)); - uint32_t sessionID = ntohl(sessionIDNet); + uint32_t sessionID = sessionIDNet; // ntohl(sessionIDNet); --- IGNORE --- auto it = mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t); std::vector encryptedPayload(it, mRecvBuffer.begin() + bytes); From 8c54250449104148473e51b22ba78442d6314909 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 10 Feb 2026 19:32:26 +0100 Subject: [PATCH 6/6] enforce unique ids --- include/columnlynx/common/net/session_registry.hpp | 2 ++ src/common/session_registry.cpp | 5 +++++ src/server/net/tcp/tcp_connection.cpp | 6 ++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/include/columnlynx/common/net/session_registry.hpp b/include/columnlynx/common/net/session_registry.hpp index cc764cc..e18f11e 100644 --- a/include/columnlynx/common/net/session_registry.hpp +++ b/include/columnlynx/common/net/session_registry.hpp @@ -78,6 +78,8 @@ namespace ColumnLynx::Net { // Get the number of registered sessions int size() const; + bool exists(uint32_t sessionID) const; + // IP management // Get the lowest available IPv4 address; Returns 0 if none available diff --git a/src/common/session_registry.cpp b/src/common/session_registry.cpp index 491264d..ff3c065 100644 --- a/src/common/session_registry.cpp +++ b/src/common/session_registry.cpp @@ -60,6 +60,11 @@ namespace ColumnLynx::Net { return static_cast(mSessions.size()); } + bool SessionRegistry::exists(uint32_t sessionID) const { + std::shared_lock lock(mMutex); + return mSessions.find(sessionID) != mSessions.end(); + } + uint32_t SessionRegistry::getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const { std::shared_lock lock(mMutex); diff --git a/src/server/net/tcp/tcp_connection.cpp b/src/server/net/tcp/tcp_connection.cpp index 334e056..d5792f1 100644 --- a/src/server/net/tcp/tcp_connection.cpp +++ b/src/server/net/tcp/tcp_connection.cpp @@ -211,8 +211,10 @@ namespace ColumnLynx::Net::TCP { std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size()); - // Make a Session ID - randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID)); + // Make a Session ID - unique and not zero (zero is reserved for invalid sessions) + do { + randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID)); + } while (SessionRegistry::getInstance().exists(mConnectionSessionID) || mConnectionSessionID == 0); // Regenerate if it already exists or is zero (zero is reserved for invalid sessions) // Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose) Nonce symNonce{}; // All zeros