From 5127fe1999b9849eb9a0e608720e32d7c8fdef05 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Fri, 7 Nov 2025 21:55:13 +0100 Subject: [PATCH] Added basic UDP handling - only sending from client supported. --- README.md | 31 ++++++- .../columnlynx/client/net/tcp/tcp_client.hpp | 22 ++++- .../columnlynx/client/net/udp/udp_client.hpp | 70 +++++++++++++++ .../common/net/session_registry.hpp | 88 +++++++++++++++++++ .../common/net/udp/udp_message_type.hpp | 33 +++++++ .../server/net/tcp/tcp_connection.hpp | 16 ++++ .../columnlynx/server/net/udp/udp_server.hpp | 30 +++++++ src/client/main.cpp | 18 +++- src/server/main.cpp | 4 + src/server/server/net/udp/udp_server.cpp | 72 +++++++++++++++ 10 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 include/columnlynx/client/net/udp/udp_client.hpp create mode 100644 include/columnlynx/common/net/session_registry.hpp create mode 100644 include/columnlynx/common/net/udp/udp_message_type.hpp create mode 100644 include/columnlynx/server/net/udp/udp_server.hpp create mode 100644 src/server/server/net/udp/udp_server.cpp diff --git a/README.md b/README.md index 3d408d0..8f45336 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,16 @@ It operates on port **48042** for both TCP and UDP. ## Packet Structure +These are the general packet structures for both the TCP and UDP sides of the protocol. Generally **headers** are **plain-text (unencrypted)** and do not contain any sensitive data. + + +The **data / payload** section is: +- For **TCP**: **encrypted** or **plain-text** depending on the **packet type** (packets with **sensitive data** are **encrypted**) +- For **UDP**: **encrypted**, as they're transfering the actual data + ### TCP Packets -TCP Packets generally follow the structure **Packet ID + Data** +TCP Packets generally follow the structure **Packet ID + Data**. They're only used for the **inital handshake** and **commands sent between the client and server**. #### Packet ID @@ -20,14 +27,30 @@ The **Packet ID** is an **8 bit unsigned integer** that is predefined from eithe **Server to Client** IDs are always below **0xA0** (exclusive) and **Client to Server** IDs are always above **0xA0** (exclusive). **0xFE** and **OxFF** are shared for **GRACEFUL_DISCONNECT** and **KILL_CONNECTION** respectively. - #### Data The data section is unspecified. It may change depending on the **Packet ID**. It is encoded as a **raw byte array** +#### Final General Structure + +| Type | Length | Name | Description | +|:-----|:-------|:-----|:------------| +| uint8_t | 1 byte | **Header** - Packet Type | General type of packet | +| uint8_t/byte array | variable | Data | General packet data - changes for packet to packet | + ### UDP Packets -*WIP, fill in later* +**UDP Packets** follow the same general structure of **Packet ID + Data**, however, they are **encrypted in full** with the exchanged AES key. This is done to prevent any metadata leakage by either the client or the server. + +The **Data** is generally just the **raw underlying packet** forwarded to the server/client. + +#### Final General Structure + +| 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 | +| uint8_t | variable | Data | General data / payload | ## Legal @@ -50,5 +73,7 @@ DcruBro is the online pseudonym of Jonas Korene Novak. Both refer to the same in ### Licensing +*See **ATTRIBUTIONS.md** for details.* + This project includes the [ASIO C++ Library](https://think-async.com/Asio/), distributed under the [Boost Software License, Version 1.0](https://www.boost.org/LICENSE_1_0.txt). \ 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 712d0e5..d94eac0 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -22,8 +22,10 @@ namespace ColumnLynx::Net::TCP { TCPClient(asio::io_context& ioContext, const std::string& host, const std::string& port, - Utils::LibSodiumWrapper* sodiumWrapper) - : mResolver(ioContext), mSocket(ioContext), mHost(host), mPort(port), mLibSodiumWrapper(sodiumWrapper) {} + Utils::LibSodiumWrapper* sodiumWrapper, + std::array* aesKey, + uint64_t* sessionIDRef) + : mResolver(ioContext), mSocket(ioContext), mHost(host), mPort(port), mLibSodiumWrapper(sodiumWrapper), mGlobalKeyRef(aesKey), mSessionIDRef(sessionIDRef) {} void start() { auto self = shared_from_this(); @@ -88,6 +90,10 @@ namespace ColumnLynx::Net::TCP { } } + bool isHandshakeComplete() const { + return mHandshakeComplete; + } + private: void mHandleMessage(ServerMessageType type, const std::string& data) { switch (type) { @@ -118,6 +124,9 @@ 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()); + } AsymNonce nonce{}; randombytes_buf(nonce.data(), nonce.size()); @@ -166,6 +175,12 @@ namespace ColumnLynx::Net::TCP { std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID)); Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID)); + + if (mSessionIDRef) { // Copy to the global reference + *mSessionIDRef = mConnectionSessionID; + } + + mHandshakeComplete = true; } break; @@ -182,6 +197,7 @@ namespace ColumnLynx::Net::TCP { } bool mConnected = false; + bool mHandshakeComplete = false; tcp::resolver mResolver; tcp::socket mSocket; std::shared_ptr mHandler; @@ -191,5 +207,7 @@ namespace ColumnLynx::Net::TCP { Utils::LibSodiumWrapper* mLibSodiumWrapper; uint64_t mConnectionSessionID; SymmetricKey mConnectionAESKey; + std::array* mGlobalKeyRef; // Reference to global AES key + uint64_t* mSessionIDRef; // Reference to global Session ID }; } \ 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 new file mode 100644 index 0000000..806413b --- /dev/null +++ b/include/columnlynx/client/net/udp/udp_client.hpp @@ -0,0 +1,70 @@ +// udp_client.hpp - UDP Client for ColumnLynx +// Copyright (C) 2025 DcruBro +// Distributed under the GPLv3 license. See LICENSE for details. + +#pragma once + +#include +#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::array* aesKeyRef, + uint64_t* sessionIDRef) + : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef) {} + + void start() { + auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort); + mRemoteEndpoint = *endpoints.begin(); + mSocket.open(asio::ip::udp::v4()); + Utils::log("UDP Client ready to send to " + mRemoteEndpoint.address().to_string() + ":" + std::to_string(mRemoteEndpoint.port())); + } + + void sendMessage(const std::string& data = "") { + UDPPacketHeader hdr{}; + randombytes_buf(hdr.nonce.data(), hdr.nonce.size()); + + if (mAesKeyRef == nullptr || mSessionIDRef == nullptr) { + Utils::error("UDP Client AES key or Session ID reference is null!"); + return; + } + + auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage( + reinterpret_cast(data.data()), data.size(), + *mAesKeyRef, hdr.nonce, "udp-data" + ); + + std::vector packet; + packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size()); + packet.insert(packet.end(), + reinterpret_cast(&hdr), + reinterpret_cast(&hdr) + sizeof(UDPPacketHeader) + ); + uint64_t sid = *mSessionIDRef; + packet.insert(packet.end(), + reinterpret_cast(&sid), + reinterpret_cast(&sid) + sizeof(sid) + ); + packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end()); + + mSocket.send_to(asio::buffer(packet), mRemoteEndpoint); + Utils::log("Sent UDP packet of size " + std::to_string(packet.size())); + } + + private: + asio::ip::udp::socket mSocket; + asio::ip::udp::resolver mResolver; + asio::ip::udp::endpoint mRemoteEndpoint; + std::string mHost; + std::string mPort; + std::array* mAesKeyRef; + uint64_t* mSessionIDRef; + }; +} \ No newline at end of file diff --git a/include/columnlynx/common/net/session_registry.hpp b/include/columnlynx/common/net/session_registry.hpp new file mode 100644 index 0000000..3ba4651 --- /dev/null +++ b/include/columnlynx/common/net/session_registry.hpp @@ -0,0 +1,88 @@ +// session_registry.hpp - Session Registry for ColumnLynx +// Copyright (C) 2025 DcruBro +// Distributed under the GPLv3 license. See LICENSE for details. + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ColumnLynx::Net { + struct SessionState { + SymmetricKey aesKey; // Immutable after creation + std::atomic send_ctr{0}; // Per-direction counters + std::atomic recv_ctr{0}; + asio::ip::udp::endpoint udpEndpoint; + std::atomic sendCounter{0}; + std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point expires{}; + Nonce base_nonce{}; + + ~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); } + SessionState(const SessionState&) = delete; + SessionState& operator=(const SessionState&) = delete; + SessionState(SessionState&&) = default; + SessionState& operator=(SessionState&&) = default; + + explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24)) : aesKey(k) { + expires = created + ttl; + } + + void setUDPEndpoint(const asio::ip::udp::endpoint& ep) { + udpEndpoint = ep; + } + }; + + class SessionRegistry { + public: + static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; } + + // Insert or replace + void put(uint64_t sessionID, std::shared_ptr state) { + std::unique_lock lock(mMutex); + mSessions[sessionID] = std::move(state); + } + + // Lookup + std::shared_ptr get(uint64_t sessionID) const { + std::shared_lock lock(mMutex); + auto it = mSessions.find(sessionID); + return (it == mSessions.end()) ? nullptr : it->second; + } + + std::unordered_map> snapshot() const { + std::unordered_map> snap; + std::shared_lock lock(mMutex); + snap = mSessions; + return snap; + } + + // Remove + void erase(uint64_t sessionID) { + std::unique_lock lock(mMutex); + mSessions.erase(sessionID); + } + + // Cleanup expired sessions + void cleanupExpired() { + std::unique_lock lock(mMutex); + auto now = std::chrono::steady_clock::now(); + for (auto it = mSessions.begin(); it != mSessions.end(); ) { + if (it->second && it->second->expires <= now) { + it = mSessions.erase(it); + } else { + ++it; + } + } + } + + private: + mutable std::shared_mutex mMutex; + std::unordered_map> mSessions; + }; +} \ No newline at end of file diff --git a/include/columnlynx/common/net/udp/udp_message_type.hpp b/include/columnlynx/common/net/udp/udp_message_type.hpp new file mode 100644 index 0000000..036e361 --- /dev/null +++ b/include/columnlynx/common/net/udp/udp_message_type.hpp @@ -0,0 +1,33 @@ +// udp_message_type.hpp - UDP Message Types for ColumnLynx +// Copyright (C) 2025 DcruBro +// Distributed under the GPLv3 license. See LICENSE for details. + +#pragma once + +#include +#include +#include + +namespace ColumnLynx::Net::UDP { + // Shared between server and client + enum class MessageType : uint8_t { + PING = 0x01, + PONG = 0x02, + DATA = 0x03 + }; + + struct UDPPacketHeader { + std::array nonce; + }; + + /*enum class ServerMessageType : uint8_t { // Server to Client + + }; + + enum class ClientMessageType : uint8_t { // Client to Server + + }; + + // Make a variant type for either message type + using AnyMessageType = std::variant;*/ +} \ No newline at end of file diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index 8f3cf97..e321008 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { class TCPConnection : public std::enable_shared_from_this { @@ -70,6 +71,14 @@ namespace ColumnLynx::Net::TCP { mOnDisconnect(shared_from_this()); } } + + uint64_t getSessionID() const { + return mConnectionSessionID; + } + + std::array getAESKey() const { + return mConnectionAESKey; + } private: TCPConnection(asio::ip::tcp::socket socket, Utils::LibSodiumWrapper* sodiumWrapper) @@ -140,6 +149,8 @@ namespace ColumnLynx::Net::TCP { // Make a Session ID randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID)); + // TODO: Make the session ID little-endian for network transmission + // 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::vector encryptedSessionID = Utils::LibSodiumWrapper::encryptMessage( @@ -149,6 +160,11 @@ namespace ColumnLynx::Net::TCP { mHandler->sendMessage(ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM, Utils::uint8ArrayToString(encryptedSessionID.data(), encryptedSessionID.size())); + // Add to session registry + Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned."); + auto session = std::make_shared(mConnectionAESKey, std::chrono::hours(12)); + SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session)); + } catch (const std::exception& e) { Utils::error("Failed to decrypt HANDSHAKE_EXCHANGE_KEY from " + reqAddr + ": " + e.what()); disconnect(); diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp new file mode 100644 index 0000000..95c25b7 --- /dev/null +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -0,0 +1,30 @@ +// udp_server.hpp - UDP Server for ColumnLynx +// Copyright (C) 2025 DcruBro +// Distributed under the GPLv3 license. See LICENSE for details. + +#pragma once + +#include +#include +#include +#include + +namespace ColumnLynx::Net::UDP { + class UDPServer { + public: + UDPServer(asio::io_context& ioContext, uint16_t port) + : mSocket(ioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)) + { + Utils::log("Started UDP server on port " + std::to_string(port)); + mStartReceive(); + } + + private: + void mStartReceive(); + void mHandlePacket(std::size_t bytes); + void mSendData(const uint64_t sessionID, const std::string& data); + asio::ip::udp::socket mSocket; + asio::ip::udp::endpoint mRemoteEndpoint; + std::array mRecvBuffer; // Adjust size as needed + }; +} \ No newline at end of file diff --git a/src/client/main.cpp b/src/client/main.cpp index 9dba8e7..edbfc1b 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include using asio::ip::tcp; using namespace ColumnLynx::Utils; @@ -34,10 +35,15 @@ int main(int argc, char** argv) { try { LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); + std::array aesKey = {0}; // Defualt zeroed state until modified by handshake + uint64_t sessionID = 0; + asio::io_context io; - auto client = std::make_shared(io, "127.0.0.1", std::to_string(serverPort()), &sodiumWrapper); + auto client = std::make_shared(io, "127.0.0.1", std::to_string(serverPort()), &sodiumWrapper, &aesKey, &sessionID); + auto udpClient = std::make_shared(io, "127.0.0.1", std::to_string(serverPort()), &aesKey, &sessionID); client->start(); + udpClient->start(); // Run the IO context in a separate thread std::thread ioThread([&io]() { @@ -50,6 +56,16 @@ int main(int argc, char** argv) { // Client is running while (!done) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Temp wait + + if (client->isHandshakeComplete()) { + // Send a test UDP message every 5 seconds after handshake is complete + static auto lastSendTime = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - lastSendTime).count() >= 5) { + udpClient->sendMessage("Hello from UDP client!"); + lastSendTime = now; + } + } } log("Client shutting down."); client->disconnect(); diff --git a/src/server/main.cpp b/src/server/main.cpp index 2c938f6..0102f8b 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -7,11 +7,14 @@ #include #include #include +#include #include +#include using asio::ip::tcp; using namespace ColumnLynx::Utils; using namespace ColumnLynx::Net::TCP; +using namespace ColumnLynx::Net::UDP; int main(int argc, char** argv) { PanicHandler::init(); @@ -26,6 +29,7 @@ int main(int argc, char** argv) { asio::io_context io; auto server = std::make_shared(io, serverPort(), &sodiumWrapper); + auto udpServer = std::make_shared(io, serverPort()); // Run the IO context in a separate thread std::thread ioThread([&io]() { diff --git a/src/server/server/net/udp/udp_server.cpp b/src/server/server/net/udp/udp_server.cpp new file mode 100644 index 0000000..d12913f --- /dev/null +++ b/src/server/server/net/udp/udp_server.cpp @@ -0,0 +1,72 @@ +// udp_server.cpp - UDP Server for ColumnLynx +// Copyright (C) 2025 DcruBro +// Distributed under the GPLv3 license. See LICENSE for details. + +#include +#include +#include +#include +#include + +namespace ColumnLynx::Net::UDP { + void UDPServer::mStartReceive() { + mSocket.async_receive_from( + asio::buffer(mRecvBuffer), mRemoteEndpoint, + [this](asio::error_code ec, std::size_t bytes) { + if (!ec && bytes > 0) { + mHandlePacket(bytes); + } + mStartReceive(); // Continue receiving + } + ); + } + + void UDPServer::mHandlePacket(std::size_t bytes) { + if (bytes < sizeof(UDPPacketHeader)) + 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)); + + auto it = mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t); + std::vector encryptedPayload(it, mRecvBuffer.begin() + bytes); + + // Get associated session state + std::shared_ptr session = SessionRegistry::getInstance().get(sessionID); + + if (!session) { + Utils::warn("UDP: Unknown or invalid session from " + mRemoteEndpoint.address().to_string()); + return; + } + + // Decrypt the actual payload + try { + auto plaintext = Utils::LibSodiumWrapper::decryptMessage( + encryptedPayload.data(), encryptedPayload.size(), + session->aesKey, + hdr->nonce, + "udp-data" + ); + + const_cast(session.get())->setUDPEndpoint(mRemoteEndpoint); // Update endpoint after confirming decryption + // Update recv counter + const_cast(session.get())->recv_ctr.fetch_add(1, std::memory_order_relaxed); + + // TODO: Process the packet payload + + // For now, just log the decrypted payload + std::string payloadStr(plaintext.begin(), plaintext.end()); + Utils::log("UDP: Received packet from " + mRemoteEndpoint.address().to_string() + " - Payload: " + payloadStr); + } catch (...) { + Utils::warn("UDP: Failed to decrypt payload from " + mRemoteEndpoint.address().to_string()); + return; + } + } + + void UDPServer::mSendData(const uint64_t sessionID, const std::string& data) { + // TODO: Implement + } +} \ No newline at end of file