From 9252425bdf26b5a5e2fa977690963864fd262dbb Mon Sep 17 00:00:00 2001 From: DcruBro Date: Mon, 10 Nov 2025 21:00:37 +0100 Subject: [PATCH 01/20] Fix licensing printout on start --- src/client/main.cpp | 2 +- src/server/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/main.cpp b/src/client/main.cpp index 190ebf9..1aff47b 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -51,7 +51,7 @@ int main(int argc, char** argv) { try { log("ColumnLynx Client, Version " + getVersion()); - log("This software is licensed under the GPLv3. See LICENSE for details."); + log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSE for details."); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); diff --git a/src/server/main.cpp b/src/server/main.cpp index b921492..b4c85fd 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -37,7 +37,7 @@ int main(int argc, char** argv) { sigaction(SIGTERM, &action, nullptr);*/ log("ColumnLynx Server, Version " + getVersion()); - log("This software is licensed under the GPLv3. See LICENSE for details."); + log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSE for details."); // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); From 4b4451d1a90eec167adab4fbf2037cd451eccb0b Mon Sep 17 00:00:00 2001 From: DcruBro Date: Mon, 10 Nov 2025 23:19:39 +0100 Subject: [PATCH 02/20] Refactoring: Moved some code from headers to dedicated source files --- .../columnlynx/client/net/tcp/tcp_client.hpp | 235 +---------------- .../columnlynx/client/net/udp/udp_client.hpp | 107 +------- .../server/net/tcp/tcp_connection.hpp | 215 +--------------- src/client/main.cpp | 3 +- src/client/net/tcp/tcp_client.cpp | 238 ++++++++++++++++++ src/client/net/udp/udp_client.cpp | 111 ++++++++ src/server/server/net/tcp/tcp_connection.cpp | 217 ++++++++++++++++ 7 files changed, 588 insertions(+), 538 deletions(-) create mode 100644 src/client/net/tcp/tcp_client.cpp create mode 100644 src/client/net/udp/udp_client.cpp create mode 100644 src/server/server/net/tcp/tcp_connection.cpp diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index 9b1a17b..28001ee 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -38,237 +38,16 @@ namespace ColumnLynx::Net::TCP { mLastHeartbeatSent(std::chrono::steady_clock::now()) {} - void start() { - auto self = shared_from_this(); - mResolver.async_resolve(mHost, mPort, - [this, self](asio::error_code ec, tcp::resolver::results_type endpoints) { - if (!ec) { - asio::async_connect(mSocket, endpoints, - [this, self](asio::error_code ec, const tcp::endpoint&) { - if (!NetHelper::isExpectedDisconnect(ec)) { - mConnected = true; - Utils::log("Client connected."); - mHandler = std::make_shared(std::move(mSocket)); - mHandler->onMessage([this](AnyMessageType type, const std::string& data) { - mHandleMessage(static_cast(MessageHandler::toUint8(type)), data); - }); - mHandler->start(); - - // Init connection handshake - Utils::log("Sending handshake init to server."); + void start(); + void sendMessage(ClientMessageType type, const std::string& data = ""); + void disconnect(bool echo = true); - std::vector payload; - payload.reserve(1 + crypto_box_PUBLICKEYBYTES); - payload.push_back(Utils::protocolVersion()); - payload.insert(payload.end(), - mLibSodiumWrapper->getXPublicKey(), - mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES - ); - - mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size())); - - mStartHeartbeat(); - } else { - Utils::error("Client connect failed: " + ec.message()); - } - }); - } else { - Utils::error("Client resolve failed: " + ec.message()); - } - }); - } - - void sendMessage(ClientMessageType type, const std::string& data = "") { - if (!mConnected) { - Utils::error("Cannot send message, client not connected."); - return; - } - - if (mHandler) { - asio::post(mHandler->socket().get_executor(), [self = shared_from_this(), type, data]() { - self->mHandler->sendMessage(type, data); - }); - } - } - - void disconnect(bool echo = true) { - if (mConnected && mHandler) { - if (echo) { - mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye"); - } - - asio::error_code ec; - mHeartbeatTimer.cancel(); - - mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); - if (ec) { - Utils::error("Error during socket shutdown: " + ec.message()); - } - - mHandler->socket().close(ec); - if (ec) { - Utils::error("Error during socket close: " + ec.message()); - } - - mConnected = false; - Utils::log("Client disconnected."); - } - } - - bool isHandshakeComplete() const { - return mHandshakeComplete; - } - - bool isConnected() const { - return mConnected; - } + bool isHandshakeComplete() const; + bool isConnected() const; private: - void mStartHeartbeat() { - auto self = shared_from_this(); - mHeartbeatTimer.expires_after(std::chrono::seconds(5)); - mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) { - if (ec == asio::error::operation_aborted) { - return; // Timer was cancelled - } - - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast(now - self->mLastHeartbeatReceived).count(); - - if (elapsed >= 15) { // 3 missed heartbeats - Utils::error("Missed 3 heartbeats. I think the other party might have died! Disconnecting."); - - // Close sockets forcefully, server is dead - asio::error_code ec; - mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); - mHandler->socket().close(ec); - mConnected = false; - - mGlobalKeyRef = nullptr; - if (mSessionIDRef) { - *mSessionIDRef = 0; - } - - return; - } - - self->sendMessage(ClientMessageType::HEARTBEAT); - Utils::log("Sent HEARTBEAT to server."); - self->mLastHeartbeatSent = std::chrono::steady_clock::now(); - - self->mStartHeartbeat(); // Recursive - }); - } - - void mHandleMessage(ServerMessageType type, const std::string& data) { - switch (type) { - case ServerMessageType::HANDSHAKE_IDENTIFY: - Utils::log("Received server identity: " + data); - std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey))); - - // Generate and send challenge - { - Utils::log("Sending challenge to server."); - mSubmittedChallenge = Utils::LibSodiumWrapper::generateRandom256Bit(); // Temporarily store the challenge to verify later - mHandler->sendMessage(ClientMessageType::HANDSHAKE_CHALLENGE, Utils::uint8ArrayToString(mSubmittedChallenge)); - } - - break; - case ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE: - Utils::log("Received challenge response from server."); - { - // Verify the signature - Signature sig{}; - std::memcpy(sig.data(), data.data(), std::min(data.size(), sig.size())); - if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) { - Utils::log("Challenge response verified successfully."); - - // Convert the server's public key to Curve25519 for encryption - AsymPublicKey serverXPubKey{}; - crypto_sign_ed25519_pk_to_curve25519(serverXPubKey.data(), mServerPublicKey); - - // 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()); - - // TODO: This is pretty redundant, it should return the required type directly - std::array arrayPrivateKey; - std::copy(mLibSodiumWrapper->getXPrivateKey(), - mLibSodiumWrapper->getXPrivateKey() + 32, - arrayPrivateKey.begin()); - - std::vector encr = Utils::LibSodiumWrapper::encryptAsymmetric( - mConnectionAESKey.data(), mConnectionAESKey.size(), - nonce, - serverXPubKey, - arrayPrivateKey - ); - - std::vector payload; - payload.reserve(nonce.size() + encr.size()); - payload.insert(payload.end(), nonce.begin(), nonce.end()); - payload.insert(payload.end(), encr.begin(), encr.end()); - - mHandler->sendMessage(ClientMessageType::HANDSHAKE_EXCHANGE_KEY, Utils::uint8ArrayToString(payload.data(), payload.size())); - } else { - Utils::error("Challenge response verification failed. Terminating connection."); - disconnect(); - } - } - - break; - case ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM: - Utils::log("Received handshake exchange key confirmation from server."); - // Decrypt the session ID using the established AES key - { - Nonce symNonce{}; // All zeros - std::vector ciphertext(data.begin(), data.end()); - std::vector decrypted = Utils::LibSodiumWrapper::decryptMessage( - ciphertext.data(), ciphertext.size(), - mConnectionAESKey, symNonce - ); - - if (decrypted.size() != sizeof(mConnectionSessionID)) { - Utils::error("Decrypted session ID has invalid size. Terminating connection."); - disconnect(); - return; - } - - 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; - case ServerMessageType::HEARTBEAT: - Utils::log("Received HEARTBEAT from server."); - mHandler->sendMessage(ClientMessageType::HEARTBEAT_ACK, ""); // Send ACK - break; - case ServerMessageType::HEARTBEAT_ACK: - Utils::log("Received HEARTBEAT_ACK from server."); - mLastHeartbeatReceived = std::chrono::steady_clock::now(); - mMissedHeartbeats = 0; // Reset missed heartbeat count - break; - case ServerMessageType::GRACEFUL_DISCONNECT: - Utils::log("Server is disconnecting: " + data); - if (mConnected) { // Prevent Recursion - disconnect(false); - } - break; - default: - Utils::log("Received unknown message type from server."); - break; - } - } + void mStartHeartbeat(); + void mHandleMessage(ServerMessageType type, const std::string& data); bool mConnected = false; bool mHandshakeComplete = false; diff --git a/include/columnlynx/client/net/udp/udp_client.hpp b/include/columnlynx/client/net/udp/udp_client.hpp index f49d043..0d804ec 100644 --- a/include/columnlynx/client/net/udp/udp_client.hpp +++ b/include/columnlynx/client/net/udp/udp_client.hpp @@ -20,110 +20,13 @@ namespace ColumnLynx::Net::UDP { uint64_t* sessionIDRef) : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef) { mStartReceive(); } - 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())); - } - - void stop() { - if (mSocket.is_open()) { - asio::error_code ec; - mSocket.cancel(ec); - mSocket.close(ec); - Utils::log("UDP Client socket closed."); - } - } + void start(); + void sendMessage(const std::string& data = ""); + void stop(); private: - void mStartReceive() { - mSocket.async_receive_from( - asio::buffer(mRecvBuffer), mRemoteEndpoint, - [this](asio::error_code ec, std::size_t bytes) { - if (ec) { - if (ec == asio::error::operation_aborted) return; // Socket closed - // Other recv error - mStartReceive(); - return; - } - - if (bytes > 0) { - mHandlePacket(bytes); - } - - mStartReceive(); - } - ); - } - - void mHandlePacket(std::size_t bytes) { - if (bytes < sizeof(UDPPacketHeader) + sizeof(uint64_t)) { - Utils::warn("UDP Client received packet too small to process."); - return; - } - - // Parse header - UDPPacketHeader hdr; - std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader)); - - // Parse session ID - uint64_t sessionID; - std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t)); - - // Decrypt payload - std::vector ciphertext( - mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t), - mRecvBuffer.begin() + bytes - ); - - if (mAesKeyRef == nullptr) { - 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" - ); - - if (plaintext.empty()) { - Utils::warn("UDP Client failed to decrypt received packet."); - return; - } - - Utils::log("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes)); - } + void mStartReceive(); + void mHandlePacket(std::size_t bytes); asio::ip::udp::socket mSocket; asio::ip::udp::resolver mResolver; diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index acc7969..7681b97 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -32,56 +32,13 @@ namespace ColumnLynx::Net::TCP { return conn; } - void start() { - mHandler->onMessage([this](AnyMessageType type, const std::string& data) { - mHandleMessage(static_cast(MessageHandler::toUint8(type)), data); - }); + void start(); + void sendMessage(ServerMessageType type, const std::string& data = ""); + void setDisconnectCallback(std::function)> cb); + void disconnect(); - mHandler->onDisconnect([this](const asio::error_code& ec) { - Utils::log("Client disconnected: " + mHandler->socket().remote_endpoint().address().to_string() + " - " + ec.message()); - disconnect(); - }); - - mHandler->start(); - mStartHeartbeat(); - - // Placeholder for message handling setup - Utils::log("Client connected: " + mHandler->socket().remote_endpoint().address().to_string()); - } - - void sendMessage(ServerMessageType type, const std::string& data = "") { - if (mHandler) { - mHandler->sendMessage(type, data); - } - } - - void setDisconnectCallback(std::function)> cb) { - mOnDisconnect = std::move(cb); - } - - void disconnect() { - std::string ip = mHandler->socket().remote_endpoint().address().to_string(); - - mHandler->sendMessage(ServerMessageType::GRACEFUL_DISCONNECT, "Server initiated disconnect."); - mHeartbeatTimer.cancel(); - asio::error_code ec; - mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); - mHandler->socket().close(ec); - - Utils::log("Closed connection to " + ip); - - if (mOnDisconnect) { - mOnDisconnect(shared_from_this()); - } - } - - uint64_t getSessionID() const { - return mConnectionSessionID; - } - - std::array getAESKey() const { - return mConnectionAESKey; - } + uint64_t getSessionID() const; + std::array getAESKey() const; private: TCPConnection(asio::ip::tcp::socket socket, Utils::LibSodiumWrapper* sodiumWrapper) @@ -93,164 +50,8 @@ namespace ColumnLynx::Net::TCP { mLastHeartbeatSent(std::chrono::steady_clock::now()) {} - void mStartHeartbeat() { - auto self = shared_from_this(); - mHeartbeatTimer.expires_after(std::chrono::seconds(5)); - mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) { - if (ec == asio::error::operation_aborted) { - return; // Timer was cancelled - } - - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast(now - self->mLastHeartbeatReceived).count(); - - if (elapsed >= 15) { // 3 missed heartbeats - Utils::error("Missed 3 heartbeats. I think the other party (client " + std::to_string(self->mConnectionSessionID) + ") might have died! Disconnecting."); - - // Remove socket forcefully, client is dead - asio::error_code ec; - mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); - mHandler->socket().close(ec); - - SessionRegistry::getInstance().erase(self->mConnectionSessionID); - - return; - } - - self->sendMessage(ServerMessageType::HEARTBEAT); - Utils::log("Sent HEARTBEAT to client " + std::to_string(self->mConnectionSessionID)); - self->mLastHeartbeatSent = now; - - self->mStartHeartbeat(); // Recursive - }); - } - - void mHandleMessage(ClientMessageType type, const std::string& data) { - std::string reqAddr = mHandler->socket().remote_endpoint().address().to_string(); - - switch (type) { - case ClientMessageType::HANDSHAKE_INIT: { - Utils::log("Received HANDSHAKE_INIT from " + reqAddr); - - if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) { - Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short."); - disconnect(); - return; - } - - uint8_t clientProtoVer = static_cast(data[0]); - if (clientProtoVer != Utils::protocolVersion()) { - Utils::warn("Client protocol version mismatch from " + reqAddr + ". Expected " + - std::to_string(Utils::protocolVersion()) + ", got " + std::to_string(clientProtoVer) + "."); - disconnect(); - return; - } - - Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + "."); - - std::memcpy(mConnectionPublicKey.data(), data.data() + 1, std::min(data.size() - 1, sizeof(mConnectionPublicKey))); // Store the client's public key (for identification) - mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist - break; - } - case ClientMessageType::HANDSHAKE_CHALLENGE: { - Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr); - - // Convert to byte array - uint8_t challengeData[32]; - std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData))); - - // Sign the challenge - Signature sig = Utils::LibSodiumWrapper::signMessage( - challengeData, sizeof(challengeData), - mLibSodiumWrapper->getPrivateKey() - ); - - mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response - break; - } - case ClientMessageType::HANDSHAKE_EXCHANGE_KEY: { - Utils::log("Received HANDSHAKE_EXCHANGE_KEY from " + reqAddr); - - // Extract encrypted AES key and nonce (nonce is the first 24 bytes, rest is the ciphertext) - if (data.size() < 24) { // Minimum size check (nonce) - Utils::warn("HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " is too short."); - disconnect(); - return; - } - - AsymNonce nonce{}; - std::memcpy(nonce.data(), data.data(), nonce.size()); - std::vector ciphertext(data.size() - nonce.size()); - std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size()); - try { - std::array arrayPrivateKey; - std::copy(mLibSodiumWrapper->getXPrivateKey(), - mLibSodiumWrapper->getXPrivateKey() + 32, - arrayPrivateKey.begin()); - - // Decrypt the AES key using the client's public key and server's private key - std::vector decrypted = Utils::LibSodiumWrapper::decryptAsymmetric( - ciphertext.data(), ciphertext.size(), - nonce, - mConnectionPublicKey, - arrayPrivateKey - ); - - if (decrypted.size() != 32) { - Utils::warn("Decrypted HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " has invalid size."); - disconnect(); - return; - } - - std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size()); - - // 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( - reinterpret_cast(&mConnectionSessionID), sizeof(mConnectionSessionID), - mConnectionAESKey, symNonce - ); - - 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(); - } - - break; - } - case ClientMessageType::HEARTBEAT: { - Utils::log("Received HEARTBEAT from " + reqAddr); - mHandler->sendMessage(ServerMessageType::HEARTBEAT_ACK, ""); // Send ACK - break; - } - case ClientMessageType::HEARTBEAT_ACK: { - Utils::log("Received HEARTBEAT_ACK from " + reqAddr); - mLastHeartbeatReceived = std::chrono::steady_clock::now(); - mMissedHeartbeats = 0; // Reset missed heartbeat count - break; - } - case ClientMessageType::GRACEFUL_DISCONNECT: { - Utils::log("Received GRACEFUL_DISCONNECT from " + reqAddr + ": " + data); - disconnect(); - break; - } - default: - Utils::warn("Unhandled message type from " + reqAddr); - break; - } - } + void mStartHeartbeat(); + void mHandleMessage(ClientMessageType type, const std::string& data); std::shared_ptr mHandler; std::function)> mOnDisconnect; diff --git a/src/client/main.cpp b/src/client/main.cpp index 1aff47b..7c6edd1 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -74,7 +74,8 @@ int main(int argc, char** argv) { log("Client connected to " + host + ":" + port); // Client is running - while ((!done && client->isConnected()) || !client->isHandshakeComplete()) { + // TODO: SIGINT or SIGTERM seems to not kill this instantly! + while (client->isConnected() || !client->isHandshakeComplete() || !done) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Temp wait if (client->isHandshakeComplete()) { diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp new file mode 100644 index 0000000..7532bc8 --- /dev/null +++ b/src/client/net/tcp/tcp_client.cpp @@ -0,0 +1,238 @@ +// tcp_client.cpp - TCP Client for ColumnLynx +// Copyright (C) 2025 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::Net::TCP { + void TCPClient::start() { + auto self = shared_from_this(); + mResolver.async_resolve(mHost, mPort, + [this, self](asio::error_code ec, tcp::resolver::results_type endpoints) { + if (!ec) { + asio::async_connect(mSocket, endpoints, + [this, self](asio::error_code ec, const tcp::endpoint&) { + if (!NetHelper::isExpectedDisconnect(ec)) { + mConnected = true; + Utils::log("Client connected."); + mHandler = std::make_shared(std::move(mSocket)); + mHandler->onMessage([this](AnyMessageType type, const std::string& data) { + mHandleMessage(static_cast(MessageHandler::toUint8(type)), data); + }); + mHandler->start(); + + // Init connection handshake + Utils::log("Sending handshake init to server."); + + std::vector payload; + payload.reserve(1 + crypto_box_PUBLICKEYBYTES); + payload.push_back(Utils::protocolVersion()); + payload.insert(payload.end(), + mLibSodiumWrapper->getXPublicKey(), + mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES + ); + + mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size())); + + mStartHeartbeat(); + } else { + Utils::error("Client connect failed: " + ec.message()); + } + }); + } else { + Utils::error("Client resolve failed: " + ec.message()); + } + }); + } + + void TCPClient::sendMessage(ClientMessageType type, const std::string& data) { + if (!mConnected) { + Utils::error("Cannot send message, client not connected."); + return; + } + + if (mHandler) { + asio::post(mHandler->socket().get_executor(), [self = shared_from_this(), type, data]() { + self->mHandler->sendMessage(type, data); + }); + } + } + + void TCPClient::disconnect(bool echo) { + if (mConnected && mHandler) { + if (echo) { + mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye"); + } + + asio::error_code ec; + mHeartbeatTimer.cancel(); + + mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); + if (ec) { + Utils::error("Error during socket shutdown: " + ec.message()); + } + + mHandler->socket().close(ec); + if (ec) { + Utils::error("Error during socket close: " + ec.message()); + } + + mConnected = false; + Utils::log("Client disconnected."); + } + } + + bool TCPClient::isHandshakeComplete() const { + return mHandshakeComplete; + } + + bool TCPClient::isConnected() const { + return mConnected; + } + + void TCPClient::mStartHeartbeat() { + auto self = shared_from_this(); + mHeartbeatTimer.expires_after(std::chrono::seconds(5)); + mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) { + if (ec == asio::error::operation_aborted) { + return; // Timer was cancelled + } + + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - self->mLastHeartbeatReceived).count(); + + if (elapsed >= 15) { // 3 missed heartbeats + Utils::error("Missed 3 heartbeats. I think the other party might have died! Disconnecting."); + + // Close sockets forcefully, server is dead + asio::error_code ec; + mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); + mHandler->socket().close(ec); + mConnected = false; + + mGlobalKeyRef = nullptr; + if (mSessionIDRef) { + *mSessionIDRef = 0; + } + + return; + } + + self->sendMessage(ClientMessageType::HEARTBEAT); + Utils::log("Sent HEARTBEAT to server."); + self->mLastHeartbeatSent = std::chrono::steady_clock::now(); + + self->mStartHeartbeat(); // Recursive + }); + } + + void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) { + switch (type) { + case ServerMessageType::HANDSHAKE_IDENTIFY: + Utils::log("Received server identity: " + data); + std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey))); + + // Generate and send challenge + { + Utils::log("Sending challenge to server."); + mSubmittedChallenge = Utils::LibSodiumWrapper::generateRandom256Bit(); // Temporarily store the challenge to verify later + mHandler->sendMessage(ClientMessageType::HANDSHAKE_CHALLENGE, Utils::uint8ArrayToString(mSubmittedChallenge)); + } + + break; + case ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE: + Utils::log("Received challenge response from server."); + { + // Verify the signature + Signature sig{}; + std::memcpy(sig.data(), data.data(), std::min(data.size(), sig.size())); + if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) { + Utils::log("Challenge response verified successfully."); + + // Convert the server's public key to Curve25519 for encryption + AsymPublicKey serverXPubKey{}; + crypto_sign_ed25519_pk_to_curve25519(serverXPubKey.data(), mServerPublicKey); + + // 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()); + + // TODO: This is pretty redundant, it should return the required type directly + std::array arrayPrivateKey; + std::copy(mLibSodiumWrapper->getXPrivateKey(), + mLibSodiumWrapper->getXPrivateKey() + 32, + arrayPrivateKey.begin()); + + std::vector encr = Utils::LibSodiumWrapper::encryptAsymmetric( + mConnectionAESKey.data(), mConnectionAESKey.size(), + nonce, + serverXPubKey, + arrayPrivateKey + ); + + std::vector payload; + payload.reserve(nonce.size() + encr.size()); + payload.insert(payload.end(), nonce.begin(), nonce.end()); + payload.insert(payload.end(), encr.begin(), encr.end()); + + mHandler->sendMessage(ClientMessageType::HANDSHAKE_EXCHANGE_KEY, Utils::uint8ArrayToString(payload.data(), payload.size())); + } else { + Utils::error("Challenge response verification failed. Terminating connection."); + disconnect(); + } + } + + break; + case ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM: + Utils::log("Received handshake exchange key confirmation from server."); + // Decrypt the session ID using the established AES key + { + Nonce symNonce{}; // All zeros + std::vector ciphertext(data.begin(), data.end()); + std::vector decrypted = Utils::LibSodiumWrapper::decryptMessage( + ciphertext.data(), ciphertext.size(), + mConnectionAESKey, symNonce + ); + + if (decrypted.size() != sizeof(mConnectionSessionID)) { + Utils::error("Decrypted session ID has invalid size. Terminating connection."); + disconnect(); + return; + } + + 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; + case ServerMessageType::HEARTBEAT: + Utils::log("Received HEARTBEAT from server."); + mHandler->sendMessage(ClientMessageType::HEARTBEAT_ACK, ""); // Send ACK + break; + case ServerMessageType::HEARTBEAT_ACK: + Utils::log("Received HEARTBEAT_ACK from server."); + mLastHeartbeatReceived = std::chrono::steady_clock::now(); + mMissedHeartbeats = 0; // Reset missed heartbeat count + break; + case ServerMessageType::GRACEFUL_DISCONNECT: + Utils::log("Server is disconnecting: " + data); + if (mConnected) { // Prevent Recursion + disconnect(false); + } + break; + default: + Utils::log("Received unknown message type from server."); + break; + } + } +} \ No newline at end of file diff --git a/src/client/net/udp/udp_client.cpp b/src/client/net/udp/udp_client.cpp new file mode 100644 index 0000000..c5cdc5c --- /dev/null +++ b/src/client/net/udp/udp_client.cpp @@ -0,0 +1,111 @@ +// udp_client.cpp - UDP Client for ColumnLynx +// Copyright (C) 2025 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::Net::UDP { + void UDPClient::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 UDPClient::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())); + } + + void UDPClient::stop() { + if (mSocket.is_open()) { + asio::error_code ec; + mSocket.cancel(ec); + mSocket.close(ec); + Utils::log("UDP Client socket closed."); + } + } + + void UDPClient::mStartReceive() { + mSocket.async_receive_from( + asio::buffer(mRecvBuffer), mRemoteEndpoint, + [this](asio::error_code ec, std::size_t bytes) { + if (ec) { + if (ec == asio::error::operation_aborted) return; // Socket closed + // Other recv error + mStartReceive(); + return; + } + + if (bytes > 0) { + mHandlePacket(bytes); + } + + mStartReceive(); + } + ); + } + + void UDPClient::mHandlePacket(std::size_t bytes) { + if (bytes < sizeof(UDPPacketHeader) + sizeof(uint64_t)) { + Utils::warn("UDP Client received packet too small to process."); + return; + } + + // Parse header + UDPPacketHeader hdr; + std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader)); + + // Parse session ID + uint64_t sessionID; + std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t)); + + // Decrypt payload + std::vector ciphertext( + mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t), + mRecvBuffer.begin() + bytes + ); + + if (mAesKeyRef == nullptr) { + 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" + ); + + if (plaintext.empty()) { + Utils::warn("UDP Client failed to decrypt received packet."); + return; + } + + Utils::log("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes)); + } +} \ No newline at end of file diff --git a/src/server/server/net/tcp/tcp_connection.cpp b/src/server/server/net/tcp/tcp_connection.cpp new file mode 100644 index 0000000..196a7cd --- /dev/null +++ b/src/server/server/net/tcp/tcp_connection.cpp @@ -0,0 +1,217 @@ +// tcp_connection.cpp - TCP Connection for ColumnLynx +// Copyright (C) 2025 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::Net::TCP { + void TCPConnection::start() { + mHandler->onMessage([this](AnyMessageType type, const std::string& data) { + mHandleMessage(static_cast(MessageHandler::toUint8(type)), data); + }); + + mHandler->onDisconnect([this](const asio::error_code& ec) { + Utils::log("Client disconnected: " + mHandler->socket().remote_endpoint().address().to_string() + " - " + ec.message()); + disconnect(); + }); + + mHandler->start(); + mStartHeartbeat(); + + // Placeholder for message handling setup + Utils::log("Client connected: " + mHandler->socket().remote_endpoint().address().to_string()); + } + + void TCPConnection::sendMessage(ServerMessageType type, const std::string& data) { + if (mHandler) { + mHandler->sendMessage(type, data); + } + } + + void TCPConnection::setDisconnectCallback(std::function)> cb) { + mOnDisconnect = std::move(cb); + } + + void TCPConnection::disconnect() { + std::string ip = mHandler->socket().remote_endpoint().address().to_string(); + + mHandler->sendMessage(ServerMessageType::GRACEFUL_DISCONNECT, "Server initiated disconnect."); + mHeartbeatTimer.cancel(); + asio::error_code ec; + mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); + mHandler->socket().close(ec); + + Utils::log("Closed connection to " + ip); + + if (mOnDisconnect) { + mOnDisconnect(shared_from_this()); + } + } + + uint64_t TCPConnection::getSessionID() const { + return mConnectionSessionID; + } + + std::array TCPConnection::getAESKey() const { + return mConnectionAESKey; + } + + void TCPConnection::mStartHeartbeat() { + auto self = shared_from_this(); + mHeartbeatTimer.expires_after(std::chrono::seconds(5)); + mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) { + if (ec == asio::error::operation_aborted) { + return; // Timer was cancelled + } + + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - self->mLastHeartbeatReceived).count(); + + if (elapsed >= 15) { // 3 missed heartbeats + Utils::error("Missed 3 heartbeats. I think the other party (client " + std::to_string(self->mConnectionSessionID) + ") might have died! Disconnecting."); + + // Remove socket forcefully, client is dead + asio::error_code ec; + mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); + mHandler->socket().close(ec); + + SessionRegistry::getInstance().erase(self->mConnectionSessionID); + + return; + } + + self->sendMessage(ServerMessageType::HEARTBEAT); + Utils::log("Sent HEARTBEAT to client " + std::to_string(self->mConnectionSessionID)); + self->mLastHeartbeatSent = now; + + self->mStartHeartbeat(); // Recursive + }); + } + + void TCPConnection::mHandleMessage(ClientMessageType type, const std::string& data) { + std::string reqAddr = mHandler->socket().remote_endpoint().address().to_string(); + + switch (type) { + case ClientMessageType::HANDSHAKE_INIT: { + Utils::log("Received HANDSHAKE_INIT from " + reqAddr); + + if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) { + Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short."); + disconnect(); + return; + } + + uint8_t clientProtoVer = static_cast(data[0]); + if (clientProtoVer != Utils::protocolVersion()) { + Utils::warn("Client protocol version mismatch from " + reqAddr + ". Expected " + + std::to_string(Utils::protocolVersion()) + ", got " + std::to_string(clientProtoVer) + "."); + disconnect(); + return; + } + + Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + "."); + + std::memcpy(mConnectionPublicKey.data(), data.data() + 1, std::min(data.size() - 1, sizeof(mConnectionPublicKey))); // Store the client's public key (for identification) + mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist + break; + } + case ClientMessageType::HANDSHAKE_CHALLENGE: { + Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr); + + // Convert to byte array + uint8_t challengeData[32]; + std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData))); + + // Sign the challenge + Signature sig = Utils::LibSodiumWrapper::signMessage( + challengeData, sizeof(challengeData), + mLibSodiumWrapper->getPrivateKey() + ); + + mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response + break; + } + case ClientMessageType::HANDSHAKE_EXCHANGE_KEY: { + Utils::log("Received HANDSHAKE_EXCHANGE_KEY from " + reqAddr); + + // Extract encrypted AES key and nonce (nonce is the first 24 bytes, rest is the ciphertext) + if (data.size() < 24) { // Minimum size check (nonce) + Utils::warn("HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " is too short."); + disconnect(); + return; + } + + AsymNonce nonce{}; + std::memcpy(nonce.data(), data.data(), nonce.size()); + std::vector ciphertext(data.size() - nonce.size()); + std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size()); + try { + std::array arrayPrivateKey; + std::copy(mLibSodiumWrapper->getXPrivateKey(), + mLibSodiumWrapper->getXPrivateKey() + 32, + arrayPrivateKey.begin()); + + // Decrypt the AES key using the client's public key and server's private key + std::vector decrypted = Utils::LibSodiumWrapper::decryptAsymmetric( + ciphertext.data(), ciphertext.size(), + nonce, + mConnectionPublicKey, + arrayPrivateKey + ); + + if (decrypted.size() != 32) { + Utils::warn("Decrypted HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " has invalid size."); + disconnect(); + return; + } + + std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size()); + + // 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( + reinterpret_cast(&mConnectionSessionID), sizeof(mConnectionSessionID), + mConnectionAESKey, symNonce + ); + + 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(); + } + + break; + } + case ClientMessageType::HEARTBEAT: { + Utils::log("Received HEARTBEAT from " + reqAddr); + mHandler->sendMessage(ServerMessageType::HEARTBEAT_ACK, ""); // Send ACK + break; + } + case ClientMessageType::HEARTBEAT_ACK: { + Utils::log("Received HEARTBEAT_ACK from " + reqAddr); + mLastHeartbeatReceived = std::chrono::steady_clock::now(); + mMissedHeartbeats = 0; // Reset missed heartbeat count + break; + } + case ClientMessageType::GRACEFUL_DISCONNECT: { + Utils::log("Received GRACEFUL_DISCONNECT from " + reqAddr + ": " + data); + disconnect(); + break; + } + default: + Utils::warn("Unhandled message type from " + reqAddr); + break; + } + } +} \ No newline at end of file From fd95816721c83fc47c0502dbba1e9e606bc4cab1 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 11 Nov 2025 00:06:09 +0100 Subject: [PATCH 03/20] Update README.md --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7379e90..b4c93d1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ # ColumnLynx -ColumnLynx is a VPN protocol designed to be as lightweight as possible. +## What is it? + +ColumnLynx is a VPN protocol designed to be as lightweight and simple to understand as possible. + +### Origin + +The original goal of this project was for me to learn about the inner-workings of VPN protocols, but overtime, it has transformed into the goal seen above. + +### Design Philosophy + +A VPN (Virtual Private Network), in the most basic terms, is a protocol that tunnels network traffic from a client to a server over an encrypted tunnel and having the server send that traffic on its behalf. It can be catagorized into sitting somewhere in-between the 3rd and 4th layers of the ISO/OSI model. + +This project aims to be just that, an encrypted tunneling protocol that works on the 3rd and 4th layers of the ISO/OSI model, nothing more, nothing less. We leave complex functions like compression, to the higher layers (though it could be argued that making an encrypted tunnel already pushes us up to Layer 6). + +This simplicity-focused design approach allows us to make an efficient, low-overhead VPN protocol and minimize any potential attack surface. ## How does it work From 705962e5ce4cb505a90828683843e70ab9ed1092 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 11 Nov 2025 13:19:59 +0100 Subject: [PATCH 04/20] Added partial verification of server public key on client side - needs hostname verification. Added startup flag to ignore verification fail. --- CMakeLists.txt | 19 +++++--- .../columnlynx/client/net/tcp/tcp_client.hpp | 5 ++- .../columnlynx/common/libsodium_wrapper.hpp | 44 +++++++++++++++++++ src/client/main.cpp | 7 ++- src/client/net/tcp/tcp_client.cpp | 24 +++++++--- src/common/utils.cpp | 2 +- src/server/main.cpp | 2 +- 7 files changed, 88 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f4fb42..53fc39d 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 0.0.1 + VERSION 0.0.3 LANGUAGES CXX ) @@ -25,7 +25,7 @@ include(FetchContent) if(APPLE) # Build universal (arm64 + x86_64), or limit to one arch if you prefer # e.g., set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "" FORCE) - set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures" FORCE) + set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "Build architectures" FORCE) endif() # --------------------------------------------------------- @@ -58,6 +58,15 @@ set(SODIUM_CMAKE_ARGS FetchContent_MakeAvailable(Sodium) +# OpenSSL +find_package(OpenSSL REQUIRED) +if(OPENSSL_FOUND) + message(STATUS "Found OpenSSL version ${OPENSSL_VERSION}") + include_directories(${OPENSSL_INCLUDE_DIR}) +else() + message(FATAL_ERROR "OpenSSL not found") +endif() + # --------------------------------------------------------- # Output directories # --------------------------------------------------------- @@ -77,7 +86,7 @@ endforeach() # --------------------------------------------------------- file(GLOB_RECURSE COMMON_SRC CONFIGURE_DEPENDS src/common/*.cpp) add_library(common STATIC ${COMMON_SRC}) -target_link_libraries(common PUBLIC sodium) +target_link_libraries(common PUBLIC sodium OpenSSL::SSL OpenSSL::Crypto) target_include_directories(common PUBLIC ${PROJECT_SOURCE_DIR}/include ${sodium_SOURCE_DIR}/src/libsodium/include @@ -90,7 +99,7 @@ target_compile_definitions(common PUBLIC ASIO_STANDALONE) # --------------------------------------------------------- file(GLOB_RECURSE CLIENT_SRC CONFIGURE_DEPENDS src/client/*.cpp) add_executable(client ${CLIENT_SRC}) -target_link_libraries(client PRIVATE common sodium) +target_link_libraries(client PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto) target_include_directories(client PRIVATE ${PROJECT_SOURCE_DIR}/include ${sodium_SOURCE_DIR}/src/libsodium/include @@ -104,7 +113,7 @@ set_target_properties(client PROPERTIES OUTPUT_NAME "columnlynx_client") # --------------------------------------------------------- file(GLOB_RECURSE SERVER_SRC CONFIGURE_DEPENDS src/server/*.cpp) add_executable(server ${SERVER_SRC}) -target_link_libraries(server PRIVATE common sodium) +target_link_libraries(server PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto) target_include_directories(server PRIVATE ${PROJECT_SOURCE_DIR}/include ${sodium_SOURCE_DIR}/src/libsodium/include diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index 28001ee..4a2193e 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -24,7 +24,8 @@ namespace ColumnLynx::Net::TCP { const std::string& port, Utils::LibSodiumWrapper* sodiumWrapper, std::array* aesKey, - uint64_t* sessionIDRef) + uint64_t* sessionIDRef, + bool* insecureMode) : mResolver(ioContext), mSocket(ioContext), @@ -33,6 +34,7 @@ namespace ColumnLynx::Net::TCP { mLibSodiumWrapper(sodiumWrapper), mGlobalKeyRef(aesKey), mSessionIDRef(sessionIDRef), + mInsecureMode(insecureMode), mHeartbeatTimer(mSocket.get_executor()), mLastHeartbeatReceived(std::chrono::steady_clock::now()), mLastHeartbeatSent(std::chrono::steady_clock::now()) @@ -62,6 +64,7 @@ namespace ColumnLynx::Net::TCP { SymmetricKey mConnectionAESKey; std::array* mGlobalKeyRef; // Reference to global AES key uint64_t* mSessionIDRef; // Reference to global Session ID + bool* mInsecureMode; // Reference to insecure mode flag asio::steady_timer mHeartbeatTimer; std::chrono::steady_clock::time_point mLastHeartbeatReceived; std::chrono::steady_clock::time_point mLastHeartbeatSent; diff --git a/include/columnlynx/common/libsodium_wrapper.hpp b/include/columnlynx/common/libsodium_wrapper.hpp index 1d99989..331855e 100644 --- a/include/columnlynx/common/libsodium_wrapper.hpp +++ b/include/columnlynx/common/libsodium_wrapper.hpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include namespace ColumnLynx { using PublicKey = std::array; // Ed25519 @@ -178,6 +181,47 @@ namespace ColumnLynx::Utils { return plaintext; } + static inline bool verifyCertificateWithSystemCAs(const std::vector& cert_der) { + // Parse DER-encoded certificate + const unsigned char* p = cert_der.data(); + std::unique_ptr cert( + d2i_X509(nullptr, &p, cert_der.size()), X509_free + ); + if (!cert) { + return false; + } + + // Create a certificate store + std::unique_ptr store( + X509_STORE_new(), X509_STORE_free + ); + if (!store) { + return false; + } + + // Load system default CA paths (/etc/ssl/certs, etc.) + if (X509_STORE_set_default_paths(store.get()) != 1) { + return false; + } + + // Create a verification context + std::unique_ptr ctx( + X509_STORE_CTX_new(), X509_STORE_CTX_free + ); + if (!ctx) { + return false; + } + + // Initialize verification context + if (X509_STORE_CTX_init(ctx.get(), store.get(), cert.get(), nullptr) != 1) { + return false; + } + + // Perform the actual certificate verification + int result = X509_verify_cert(ctx.get()); + return result == 1; + } + private: std::array mPublicKey; std::array mPrivateKey; diff --git a/src/client/main.cpp b/src/client/main.cpp index 7c6edd1..c182fe0 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -38,7 +38,10 @@ int main(int argc, char** argv) { options.add_options() ("h,help", "Print help") ("s,server", "Server address", cxxopts::value()->default_value("127.0.0.1")) - ("p,port", "Server port", cxxopts::value()->default_value(std::to_string(serverPort()))); + ("p,port", "Server port", cxxopts::value()->default_value(std::to_string(serverPort()))) + ("as,allow-selfsigned", "Allow self-signed certificates", cxxopts::value()->default_value("false")); + + bool insecureMode = options.parse(argc, argv).count("allow-selfsigned") > 0; auto result = options.parse(argc, argv); if (result.count("help")) { @@ -59,7 +62,7 @@ int main(int argc, char** argv) { uint64_t sessionID = 0; asio::io_context io; - auto client = std::make_shared(io, host, port, &sodiumWrapper, &aesKey, &sessionID); + auto client = std::make_shared(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode); auto udpClient = std::make_shared(io, host, port, &aesKey, &sessionID); client->start(); diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp index 7532bc8..4ca2016 100644 --- a/src/client/net/tcp/tcp_client.cpp +++ b/src/client/net/tcp/tcp_client.cpp @@ -128,12 +128,26 @@ namespace ColumnLynx::Net::TCP { void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) { switch (type) { - case ServerMessageType::HANDSHAKE_IDENTIFY: - Utils::log("Received server identity: " + data); - std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey))); + case ServerMessageType::HANDSHAKE_IDENTIFY: { + Utils::log("Received server identity: " + data); + std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey))); - // Generate and send challenge - { + // Convert key (uint8_t raw array) to vector + std::vector serverPublicKeyVec(std::begin(mServerPublicKey), std::end(mServerPublicKey)); + + // Verify server public key + // TODO: Verify / Match hostname of public key to hostname of server + if (!Utils::LibSodiumWrapper::verifyCertificateWithSystemCAs(serverPublicKeyVec)) { + if (!(*mInsecureMode)) { + Utils::error("Server public key verification failed. Terminating connection."); + disconnect(); + return; + } + + Utils::log("Warning: Server public key verification failed, but continuing due to insecure mode."); + } + + // Generate and send challenge Utils::log("Sending challenge to server."); mSubmittedChallenge = Utils::LibSodiumWrapper::generateRandom256Bit(); // Temporarily store the challenge to verify later mHandler->sendMessage(ClientMessageType::HANDSHAKE_CHALLENGE, Utils::uint8ArrayToString(mSubmittedChallenge)); diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 9feca23..7003ebf 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -37,7 +37,7 @@ namespace ColumnLynx::Utils { } std::string getVersion() { - return "a0.2"; + return "a0.3"; } unsigned short serverPort() { diff --git a/src/server/main.cpp b/src/server/main.cpp index b4c85fd..fdddac3 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -42,7 +42,7 @@ int main(int argc, char** argv) { // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES)); - log("Server private key: " + bytesToHexString(sodiumWrapper.getPrivateKey(), crypto_sign_SECRETKEYBYTES)); // TEMP, remove later + //log("Server private key: " + bytesToHexString(sodiumWrapper.getPrivateKey(), crypto_sign_SECRETKEYBYTES)); // TEMP, remove later bool hostRunning = true; From eda3cf87d1049a7fb8fd7e0512d0e0a70b444245 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 11 Nov 2025 15:26:05 +0100 Subject: [PATCH 05/20] misc. --- src/client/main.cpp | 4 ++-- src/server/main.cpp | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/client/main.cpp b/src/client/main.cpp index c182fe0..b832efb 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -54,7 +54,7 @@ int main(int argc, char** argv) { try { log("ColumnLynx Client, Version " + getVersion()); - log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSE for details."); + log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details."); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); @@ -78,7 +78,7 @@ int main(int argc, char** argv) { // Client is running // TODO: SIGINT or SIGTERM seems to not kill this instantly! - while (client->isConnected() || !client->isHandshakeComplete() || !done) { + while ((client->isConnected() || !client->isHandshakeComplete()) && !done) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Temp wait if (client->isHandshakeComplete()) { diff --git a/src/server/main.cpp b/src/server/main.cpp index fdddac3..385dde9 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -18,26 +18,27 @@ using namespace ColumnLynx::Net::UDP; volatile sig_atomic_t done = 0; -/*void signalHandler(int signum) { +void signalHandler(int signum) { if (signum == SIGINT || signum == SIGTERM) { log("Received termination signal. Shutting down server gracefully."); done = 1; } -}*/ +} int main(int argc, char** argv) { + // Capture SIGINT and SIGTERM for graceful shutdown + struct sigaction action; + memset(&action, 0, sizeof(struct sigaction)); + action.sa_handler = signalHandler; + sigaction(SIGINT, &action, nullptr); + sigaction(SIGTERM, &action, nullptr); + PanicHandler::init(); try { - // Catch SIGINT and SIGTERM for graceful shutdown - /*struct sigaction action; - memset(&action, 0, sizeof(struct sigaction)); - action.sa_handler = signalHandler; - sigaction(SIGINT, &action, nullptr); - sigaction(SIGTERM, &action, nullptr);*/ log("ColumnLynx Server, Version " + getVersion()); - log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSE for details."); + log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details."); // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); From fbafeafc9f82ed0bc8d2dc7b643e6d80fa56414c Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 11 Nov 2025 17:35:53 +0100 Subject: [PATCH 06/20] Added a hostname check if connecting via domain hostname (Not IPv4/IPv6 - IPv6 still not supported / tested). --- .../columnlynx/client/net/tcp/tcp_client.hpp | 1 + .../columnlynx/common/libsodium_wrapper.hpp | 52 +++++++++++++++++++ src/client/net/tcp/tcp_client.cpp | 31 ++++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index 4a2193e..ae6fbd6 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -69,5 +69,6 @@ namespace ColumnLynx::Net::TCP { std::chrono::steady_clock::time_point mLastHeartbeatReceived; std::chrono::steady_clock::time_point mLastHeartbeatSent; int mMissedHeartbeats = 0; + bool mIsHostDomain; }; } \ No newline at end of file diff --git a/include/columnlynx/common/libsodium_wrapper.hpp b/include/columnlynx/common/libsodium_wrapper.hpp index 331855e..2867bd8 100644 --- a/include/columnlynx/common/libsodium_wrapper.hpp +++ b/include/columnlynx/common/libsodium_wrapper.hpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace ColumnLynx { using PublicKey = std::array; // Ed25519 @@ -222,6 +223,57 @@ namespace ColumnLynx::Utils { return result == 1; } + static inline std::vector getCertificateHostname(const std::vector& cert_der) { + std::vector names; + + if (cert_der.empty()) + return names; + + // Parse DER certificate + const unsigned char* p = cert_der.data(); + X509* cert = d2i_X509(nullptr, &p, cert_der.size()); + if (!cert) + return names; + + // --- Subject Alternative Names (SAN) --- + GENERAL_NAMES* san_names = + (GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr); + + if (san_names) { + int san_count = sk_GENERAL_NAME_num(san_names); + for (int i = 0; i < san_count; i++) { + const GENERAL_NAME* current = sk_GENERAL_NAME_value(san_names, i); + if (current->type == GEN_DNS) { + const char* dns_name = (const char*)ASN1_STRING_get0_data(current->d.dNSName); + // Safety: ensure no embedded nulls + if (ASN1_STRING_length(current->d.dNSName) == (int)std::strlen(dns_name)) { + names.emplace_back(dns_name); + } + } + } + GENERAL_NAMES_free(san_names); + } + + // --- Fallback: Common Name (CN) --- + if (names.empty()) { + X509_NAME* subject = X509_get_subject_name(cert); + if (subject) { + int idx = X509_NAME_get_index_by_NID(subject, NID_commonName, -1); + if (idx >= 0) { + X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, idx); + ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(entry); + const char* cn_str = (const char*)ASN1_STRING_get0_data(cn_asn1); + if (ASN1_STRING_length(cn_asn1) == (int)std::strlen(cn_str)) { + names.emplace_back(cn_str); + } + } + } + } + + X509_free(cert); + return names; + } + private: std::array mPublicKey; std::array mPrivateKey; diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp index 4ca2016..d5388a3 100644 --- a/src/client/net/tcp/tcp_client.cpp +++ b/src/client/net/tcp/tcp_client.cpp @@ -3,6 +3,7 @@ // Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. #include +#include namespace ColumnLynx::Net::TCP { void TCPClient::start() { @@ -24,6 +25,11 @@ namespace ColumnLynx::Net::TCP { // Init connection handshake Utils::log("Sending handshake init to server."); + // Check if hostname or IPv4/IPv6 + sockaddr_in addr4{}; + sockaddr_in6 addr6{}; + self->mIsHostDomain = inet_pton(AF_INET, mHost.c_str(), (void*)(&addr4)) != 1 && inet_pton(AF_INET6, mHost.c_str(), (void*)(&addr6)) != 1; + std::vector payload; payload.reserve(1 + crypto_box_PUBLICKEYBYTES); payload.push_back(Utils::protocolVersion()); @@ -136,7 +142,6 @@ namespace ColumnLynx::Net::TCP { std::vector serverPublicKeyVec(std::begin(mServerPublicKey), std::end(mServerPublicKey)); // Verify server public key - // TODO: Verify / Match hostname of public key to hostname of server if (!Utils::LibSodiumWrapper::verifyCertificateWithSystemCAs(serverPublicKeyVec)) { if (!(*mInsecureMode)) { Utils::error("Server public key verification failed. Terminating connection."); @@ -144,7 +149,29 @@ namespace ColumnLynx::Net::TCP { return; } - Utils::log("Warning: Server public key verification failed, but continuing due to insecure mode."); + Utils::warn("Warning: Server public key verification failed, but continuing due to insecure mode."); + } + + // Extract and verify hostname from certificate if not IP + if (mIsHostDomain) { + std::vector certHostnames = Utils::LibSodiumWrapper::getCertificateHostname(serverPublicKeyVec); + + // Temp: print extracted hostnames if any + for (const auto& hostname : certHostnames) { + Utils::log("Extracted hostname from certificate: " + hostname); + } + + if (certHostnames.empty() || std::find(certHostnames.begin(), certHostnames.end(), mHost) == certHostnames.end()) { + if (!(*mInsecureMode)) { + Utils::error("Server hostname verification failed. Terminating connection."); + disconnect(); + return; + } + + Utils::warn("Warning: Server hostname verification failed, but continuing due to insecure mode."); + } + } else { + Utils::warn("Connecting via IP address, I can't verify the server's identity! You might be getting MITM'd!"); } // Generate and send challenge From e695008e109773a1c8265b525987356d8ee20bcc Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 12 Nov 2025 09:07:22 +0100 Subject: [PATCH 07/20] Added IPv6 support, added option to disable IPv6 (IPv4-Only mode) --- .../columnlynx/server/net/tcp/tcp_server.hpp | 34 +++++++++++++++++-- .../columnlynx/server/net/udp/udp_server.hpp | 26 ++++++++++++-- src/client/main.cpp | 2 +- src/server/main.cpp | 18 ++++++++-- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/include/columnlynx/server/net/tcp/tcp_server.hpp b/include/columnlynx/server/net/tcp/tcp_server.hpp index 57037f2..599a5ad 100644 --- a/include/columnlynx/server/net/tcp/tcp_server.hpp +++ b/include/columnlynx/server/net/tcp/tcp_server.hpp @@ -21,9 +21,39 @@ namespace ColumnLynx::Net::TCP { class TCPServer { public: - TCPServer(asio::io_context& ioContext, uint16_t port, Utils::LibSodiumWrapper* sodiumWrapper, bool* hostRunning) - : mIoContext(ioContext), mAcceptor(ioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), mSodiumWrapper(sodiumWrapper), mHostRunning(hostRunning) + TCPServer(asio::io_context& ioContext, + uint16_t port, + Utils::LibSodiumWrapper* sodiumWrapper, + bool* hostRunning, bool ipv4Only = false) + : mIoContext(ioContext), + mAcceptor(ioContext), + mSodiumWrapper(sodiumWrapper), + mHostRunning(hostRunning) { + asio::error_code ec; + + if (!ipv4Only) { + // Try IPv6 first (dual-stack check) + asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port); + mAcceptor.open(endpoint_v6.protocol(), ec); + if (!ec) { + mAcceptor.set_option(asio::ip::v6_only(false), ec); // Allow dual-stack if possible + mAcceptor.bind(endpoint_v6, ec); + } + } + + // Fallback to IPv4 if anything failed + if (ec || ipv4Only) { + Utils::warn("TCP: IPv6 unavailable (" + ec.message() + "), falling back to IPv4 only"); + + asio::ip::tcp::endpoint endpoint_v4(asio::ip::tcp::v4(), port); + mAcceptor.close(); // ensure clean state + mAcceptor.open(endpoint_v4.protocol()); + mAcceptor.bind(endpoint_v4); + } + + // Start listening + mAcceptor.listen(); Utils::log("Started TCP server on port " + std::to_string(port)); mStartAccept(); } diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp index 99dc3cd..be7a47a 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -12,9 +12,31 @@ namespace ColumnLynx::Net::UDP { class UDPServer { public: - UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning) - : mSocket(ioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)), mHostRunning(hostRunning) + UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false) + : mSocket(ioContext), mHostRunning(hostRunning) { + asio::error_code ec; + + if (!ipv4Only) { + // Try IPv6 first (dual-stack check) + asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port); + mSocket.open(endpoint_v6.protocol(), ec); + if (!ec) { + mSocket.set_option(asio::ip::v6_only(false), ec); // Allow dual-stack if possible + mSocket.bind(endpoint_v6, ec); + } + } + + // Fallback to IPv4 if anything failed + if (ec || ipv4Only) { + Utils::warn("UDP: IPv6 unavailable (" + ec.message() + "), falling back to IPv4 only"); + + asio::ip::udp::endpoint endpoint_v4(asio::ip::udp::v4(), port); + mSocket.close(); // ensure clean state + mSocket.open(endpoint_v4.protocol()); + mSocket.bind(endpoint_v4); + } + Utils::log("Started UDP server on port " + std::to_string(port)); mStartReceive(); } diff --git a/src/client/main.cpp b/src/client/main.cpp index b832efb..d863422 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -39,7 +39,7 @@ int main(int argc, char** argv) { ("h,help", "Print help") ("s,server", "Server address", cxxopts::value()->default_value("127.0.0.1")) ("p,port", "Server port", cxxopts::value()->default_value(std::to_string(serverPort()))) - ("as,allow-selfsigned", "Allow self-signed certificates", cxxopts::value()->default_value("false")); + ("allow-selfsigned", "Allow self-signed certificates", cxxopts::value()->default_value("false")); bool insecureMode = options.parse(argc, argv).count("allow-selfsigned") > 0; diff --git a/src/server/main.cpp b/src/server/main.cpp index 385dde9..0477c67 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include using asio::ip::tcp; using namespace ColumnLynx::Utils; @@ -33,9 +34,22 @@ int main(int argc, char** argv) { sigaction(SIGINT, &action, nullptr); sigaction(SIGTERM, &action, nullptr); + cxxopts::Options options("columnlynx_server", "ColumnLynx Server Application"); + + options.add_options() + ("h,help", "Print help") + ("4,ipv4-only", "Force IPv4 only operation", cxxopts::value()->default_value("false")); + PanicHandler::init(); try { + auto result = options.parse(argc, argv); + if (result.count("help")) { + std::cout << options.help() << std::endl; + return 0; + } + + bool ipv4Only = result["ipv4-only"].as(); log("ColumnLynx Server, Version " + getVersion()); log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details."); @@ -49,8 +63,8 @@ int main(int argc, char** argv) { asio::io_context io; - auto server = std::make_shared(io, serverPort(), &sodiumWrapper, &hostRunning); - auto udpServer = std::make_shared(io, serverPort(), &hostRunning); + auto server = std::make_shared(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only); + auto udpServer = std::make_shared(io, serverPort(), &hostRunning, ipv4Only); asio::signal_set signals(io, SIGINT, SIGTERM); signals.async_wait([&](const std::error_code&, int) { From 6cd4e01066aa477cbb091ae77ac2f647c7972345 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 12 Nov 2025 16:09:44 +0100 Subject: [PATCH 08/20] cleanup --- src/server/server/net/tcp/tcp_server.cpp | 8 -------- src/server/server/net/udp/udp_server.cpp | 8 -------- 2 files changed, 16 deletions(-) diff --git a/src/server/server/net/tcp/tcp_server.cpp b/src/server/server/net/tcp/tcp_server.cpp index 0392027..3069e3d 100644 --- a/src/server/server/net/tcp/tcp_server.cpp +++ b/src/server/server/net/tcp/tcp_server.cpp @@ -20,14 +20,6 @@ namespace ColumnLynx::Net::TCP { void TCPServer::mStartAccept() { - // A bit of a shotty implementation, might improve later - /*std::cout << "Host running pointer: " << *mHostRunning << std::endl; - - if (mHostRunning != nullptr && !(*mHostRunning)) { - Utils::log("Server is stopping, not accepting new connections."); - return; - }*/ - mAcceptor.async_accept( [this](asio::error_code ec, asio::ip::tcp::socket socket) { if (ec) { diff --git a/src/server/server/net/udp/udp_server.cpp b/src/server/server/net/udp/udp_server.cpp index 82ec463..1afa8a7 100644 --- a/src/server/server/net/udp/udp_server.cpp +++ b/src/server/server/net/udp/udp_server.cpp @@ -10,12 +10,6 @@ namespace ColumnLynx::Net::UDP { void UDPServer::mStartReceive() { - // A bit of a shotty implementation, might improve later - /*if (mHostRunning != nullptr && !(*mHostRunning)) { - Utils::log("Server is stopping, not receiving new packets."); - return; - }*/ - mSocket.async_receive_from( asio::buffer(mRecvBuffer), mRemoteEndpoint, [this](asio::error_code ec, std::size_t bytes) { @@ -78,8 +72,6 @@ namespace ColumnLynx::Net::UDP { } void UDPServer::mSendData(const uint64_t sessionID, const std::string& data) { - // TODO: Implement - // Find the IPv4/IPv6 endpoint for the session std::shared_ptr session = SessionRegistry::getInstance().get(sessionID); if (!session) { From 5a72895f8d812dd14f27b2176c4538eabff7fc25 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 12 Nov 2025 18:49:53 +0100 Subject: [PATCH 09/20] added includes to build on linux --- include/columnlynx/common/libsodium_wrapper.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/columnlynx/common/libsodium_wrapper.hpp b/include/columnlynx/common/libsodium_wrapper.hpp index 2867bd8..435f617 100644 --- a/include/columnlynx/common/libsodium_wrapper.hpp +++ b/include/columnlynx/common/libsodium_wrapper.hpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include namespace ColumnLynx { using PublicKey = std::array; // Ed25519 From 6fceb8493066173c83ace14cadd6daa02666bcfb Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 12 Nov 2025 19:14:44 +0100 Subject: [PATCH 10/20] Added basic virtual interface implementation, needs testing. Added Wintun licenses. --- ATTRIBUTIONS.md | 9 +- README.md | 8 + .../common/net/virtual_interface.hpp | 51 +++ include/wintun/wintun.h | 270 ++++++++++++++ src/client/main.cpp | 9 + src/common/virtual_interface.cpp | 131 +++++++ .../wintun/LICENSE_1_0_GPL-2.0-only.txt | 338 ++++++++++++++++++ third_party/wintun/LICENSE_1_0_MIT.txt | 19 + 8 files changed, 834 insertions(+), 1 deletion(-) create mode 100644 include/columnlynx/common/net/virtual_interface.hpp create mode 100644 include/wintun/wintun.h create mode 100644 src/common/virtual_interface.cpp create mode 100644 third_party/wintun/LICENSE_1_0_GPL-2.0-only.txt create mode 100644 third_party/wintun/LICENSE_1_0_MIT.txt diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md index 06ddd20..9a1efa3 100644 --- a/ATTRIBUTIONS.md +++ b/ATTRIBUTIONS.md @@ -14,4 +14,11 @@ This project uses the standalone version of the ASIO C++ library for asynchronou - **Website:** https://github.com/jarro2783/cxxopts/ - **Copyright:** (c) 2014-2025 Christopher M. Kohlhoff - **License:** MIT License -- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt` \ No newline at end of file +- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt` + +## Wintun C++ Library +- **Name:** wintun +- **Website:** https://www.wintun.net/ +- **Copyright:** (c) 2018-2025 WireGuard LLC +- **License:** MIT License OR GPL-2.0 License +- **License Text:** See `third_party/wintun/` \ No newline at end of file diff --git a/README.md b/README.md index b4c93d1..47252e4 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ The **Data** is generally just the **raw underlying packet** forwarded to the se | uint64_t | 8 bytes | **Header** - Session ID | The unique and random session identifier for the client | | uint8_t | variable | Data | General data / payload | +## Misc. +Building the binary for Windows requires the Wintun DLL. The include header is pre-packaged. + ## Legal ### Copyright ownership: @@ -152,4 +155,9 @@ along with this program. If not, see . 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). +This project includes the CXXOPTS Library +distributed under the MIT License + +This project includes the [Wintun Library](https://www.wintun.net/), distributed under the MIT License or the GPL-2.0 License. + *See **ATTRIBUTIONS.md** for details.* \ No newline at end of file diff --git a/include/columnlynx/common/net/virtual_interface.hpp b/include/columnlynx/common/net/virtual_interface.hpp new file mode 100644 index 0000000..6cdf874 --- /dev/null +++ b/include/columnlynx/common/net/virtual_interface.hpp @@ -0,0 +1,51 @@ +// virtual_interface.hpp - Virtual Interface for Network Communication +// Copyright (C) 2025 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 +#include + +#if defined(__linux__) + #include + #include + #include + #include + #include +#elif defined(__APPLE__) + #include + #include + #include + #include + #include + #include +#elif defined(_WIN32) + #include + #include + #pragma comment(lib, "advapi32.lib") +#endif + +namespace ColumnLynx::Net { + class VirtualInterface { + public: + explicit VirtualInterface(const std::string& ifName); + ~VirtualInterface(); + + std::vector readPacket(); + void writePacket(const std::vector& packet); + + const std::string& getName() const; + int getFd() const; // for ASIO integration (on POSIX) + private: + std::string mIfName; + int mFd; // POSIX + #if defined(_WIN32) + HANDLE mHandle; // Windows + #endif + }; +} \ No newline at end of file diff --git a/include/wintun/wintun.h b/include/wintun/wintun.h new file mode 100644 index 0000000..55d441f --- /dev/null +++ b/include/wintun/wintun.h @@ -0,0 +1,270 @@ +/* SPDX-License-Identifier: GPL-2.0 OR MIT + * + * Copyright (C) 2018-2021 WireGuard LLC. All Rights Reserved. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ALIGNED +# if defined(_MSC_VER) +# define ALIGNED(n) __declspec(align(n)) +# elif defined(__GNUC__) +# define ALIGNED(n) __attribute__((aligned(n))) +# else +# error "Unable to define ALIGNED" +# endif +#endif + +/* MinGW is missing this one, unfortunately. */ +#ifndef _Post_maybenull_ +# define _Post_maybenull_ +#endif + +#pragma warning(push) +#pragma warning(disable : 4324) /* structure was padded due to alignment specifier */ + +/** + * A handle representing Wintun adapter + */ +typedef struct _WINTUN_ADAPTER *WINTUN_ADAPTER_HANDLE; + +/** + * Creates a new Wintun adapter. + * + * @param Name The requested name of the adapter. Zero-terminated string of up to MAX_ADAPTER_NAME-1 + * characters. + * + * @param TunnelType Name of the adapter tunnel type. Zero-terminated string of up to MAX_ADAPTER_NAME-1 + * characters. + * + * @param RequestedGUID The GUID of the created network adapter, which then influences NLA generation deterministically. + * If it is set to NULL, the GUID is chosen by the system at random, and hence a new NLA entry is + * created for each new adapter. It is called "requested" GUID because the API it uses is + * completely undocumented, and so there could be minor interesting complications with its usage. + * + * @return If the function succeeds, the return value is the adapter handle. Must be released with + * WintunCloseAdapter. If the function fails, the return value is NULL. To get extended error information, call + * GetLastError. + */ +typedef _Must_inspect_result_ +_Return_type_success_(return != NULL) +_Post_maybenull_ +WINTUN_ADAPTER_HANDLE(WINAPI WINTUN_CREATE_ADAPTER_FUNC) +(_In_z_ LPCWSTR Name, _In_z_ LPCWSTR TunnelType, _In_opt_ const GUID *RequestedGUID); + +/** + * Opens an existing Wintun adapter. + * + * @param Name The requested name of the adapter. Zero-terminated string of up to MAX_ADAPTER_NAME-1 + * characters. + * + * @return If the function succeeds, the return value is the adapter handle. Must be released with + * WintunCloseAdapter. If the function fails, the return value is NULL. To get extended error information, call + * GetLastError. + */ +typedef _Must_inspect_result_ +_Return_type_success_(return != NULL) +_Post_maybenull_ +WINTUN_ADAPTER_HANDLE(WINAPI WINTUN_OPEN_ADAPTER_FUNC)(_In_z_ LPCWSTR Name); + +/** + * Releases Wintun adapter resources and, if adapter was created with WintunCreateAdapter, removes adapter. + * + * @param Adapter Adapter handle obtained with WintunCreateAdapter or WintunOpenAdapter. + */ +typedef VOID(WINAPI WINTUN_CLOSE_ADAPTER_FUNC)(_In_opt_ WINTUN_ADAPTER_HANDLE Adapter); + +/** + * Deletes the Wintun driver if there are no more adapters in use. + * + * @return If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To + * get extended error information, call GetLastError. + */ +typedef _Return_type_success_(return != FALSE) +BOOL(WINAPI WINTUN_DELETE_DRIVER_FUNC)(VOID); + +/** + * Returns the LUID of the adapter. + * + * @param Adapter Adapter handle obtained with WintunCreateAdapter or WintunOpenAdapter + * + * @param Luid Pointer to LUID to receive adapter LUID. + */ +typedef VOID(WINAPI WINTUN_GET_ADAPTER_LUID_FUNC)(_In_ WINTUN_ADAPTER_HANDLE Adapter, _Out_ NET_LUID *Luid); + +/** + * Determines the version of the Wintun driver currently loaded. + * + * @return If the function succeeds, the return value is the version number. If the function fails, the return value is + * zero. To get extended error information, call GetLastError. Possible errors include the following: + * ERROR_FILE_NOT_FOUND Wintun not loaded + */ +typedef _Return_type_success_(return != 0) +DWORD(WINAPI WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC)(VOID); + +/** + * Determines the level of logging, passed to WINTUN_LOGGER_CALLBACK. + */ +typedef enum +{ + WINTUN_LOG_INFO, /**< Informational */ + WINTUN_LOG_WARN, /**< Warning */ + WINTUN_LOG_ERR /**< Error */ +} WINTUN_LOGGER_LEVEL; + +/** + * Called by internal logger to report diagnostic messages + * + * @param Level Message level. + * + * @param Timestamp Message timestamp in in 100ns intervals since 1601-01-01 UTC. + * + * @param Message Message text. + */ +typedef VOID(CALLBACK *WINTUN_LOGGER_CALLBACK)( + _In_ WINTUN_LOGGER_LEVEL Level, + _In_ DWORD64 Timestamp, + _In_z_ LPCWSTR Message); + +/** + * Sets logger callback function. + * + * @param NewLogger Pointer to callback function to use as a new global logger. NewLogger may be called from various + * threads concurrently. Should the logging require serialization, you must handle serialization in + * NewLogger. Set to NULL to disable. + */ +typedef VOID(WINAPI WINTUN_SET_LOGGER_FUNC)(_In_ WINTUN_LOGGER_CALLBACK NewLogger); + +/** + * Minimum ring capacity. + */ +#define WINTUN_MIN_RING_CAPACITY 0x20000 /* 128kiB */ + +/** + * Maximum ring capacity. + */ +#define WINTUN_MAX_RING_CAPACITY 0x4000000 /* 64MiB */ + +/** + * A handle representing Wintun session + */ +typedef struct _TUN_SESSION *WINTUN_SESSION_HANDLE; + +/** + * Starts Wintun session. + * + * @param Adapter Adapter handle obtained with WintunOpenAdapter or WintunCreateAdapter + * + * @param Capacity Rings capacity. Must be between WINTUN_MIN_RING_CAPACITY and WINTUN_MAX_RING_CAPACITY (incl.) + * Must be a power of two. + * + * @return Wintun session handle. Must be released with WintunEndSession. If the function fails, the return value is + * NULL. To get extended error information, call GetLastError. + */ +typedef _Must_inspect_result_ +_Return_type_success_(return != NULL) +_Post_maybenull_ +WINTUN_SESSION_HANDLE(WINAPI WINTUN_START_SESSION_FUNC)(_In_ WINTUN_ADAPTER_HANDLE Adapter, _In_ DWORD Capacity); + +/** + * Ends Wintun session. + * + * @param Session Wintun session handle obtained with WintunStartSession + */ +typedef VOID(WINAPI WINTUN_END_SESSION_FUNC)(_In_ WINTUN_SESSION_HANDLE Session); + +/** + * Gets Wintun session's read-wait event handle. + * + * @param Session Wintun session handle obtained with WintunStartSession + * + * @return Pointer to receive event handle to wait for available data when reading. Should + * WintunReceivePackets return ERROR_NO_MORE_ITEMS (after spinning on it for a while under heavy + * load), wait for this event to become signaled before retrying WintunReceivePackets. Do not call + * CloseHandle on this event - it is managed by the session. + */ +typedef HANDLE(WINAPI WINTUN_GET_READ_WAIT_EVENT_FUNC)(_In_ WINTUN_SESSION_HANDLE Session); + +/** + * Maximum IP packet size + */ +#define WINTUN_MAX_IP_PACKET_SIZE 0xFFFF + +/** + * Retrieves one or packet. After the packet content is consumed, call WintunReleaseReceivePacket with Packet returned + * from this function to release internal buffer. This function is thread-safe. + * + * @param Session Wintun session handle obtained with WintunStartSession + * + * @param PacketSize Pointer to receive packet size. + * + * @return Pointer to layer 3 IPv4 or IPv6 packet. Client may modify its content at will. If the function fails, the + * return value is NULL. To get extended error information, call GetLastError. Possible errors include the + * following: + * ERROR_HANDLE_EOF Wintun adapter is terminating; + * ERROR_NO_MORE_ITEMS Wintun buffer is exhausted; + * ERROR_INVALID_DATA Wintun buffer is corrupt + */ +typedef _Must_inspect_result_ +_Return_type_success_(return != NULL) +_Post_maybenull_ +_Post_writable_byte_size_(*PacketSize) +BYTE *(WINAPI WINTUN_RECEIVE_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _Out_ DWORD *PacketSize); + +/** + * Releases internal buffer after the received packet has been processed by the client. This function is thread-safe. + * + * @param Session Wintun session handle obtained with WintunStartSession + * + * @param Packet Packet obtained with WintunReceivePacket + */ +typedef VOID( + WINAPI WINTUN_RELEASE_RECEIVE_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _In_ const BYTE *Packet); + +/** + * Allocates memory for a packet to send. After the memory is filled with packet data, call WintunSendPacket to send + * and release internal buffer. WintunAllocateSendPacket is thread-safe and the WintunAllocateSendPacket order of + * calls define the packet sending order. + * + * @param Session Wintun session handle obtained with WintunStartSession + * + * @param PacketSize Exact packet size. Must be less or equal to WINTUN_MAX_IP_PACKET_SIZE. + * + * @return Returns pointer to memory where to prepare layer 3 IPv4 or IPv6 packet for sending. If the function fails, + * the return value is NULL. To get extended error information, call GetLastError. Possible errors include the + * following: + * ERROR_HANDLE_EOF Wintun adapter is terminating; + * ERROR_BUFFER_OVERFLOW Wintun buffer is full; + */ +typedef _Must_inspect_result_ +_Return_type_success_(return != NULL) +_Post_maybenull_ +_Post_writable_byte_size_(PacketSize) +BYTE *(WINAPI WINTUN_ALLOCATE_SEND_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _In_ DWORD PacketSize); + +/** + * Sends the packet and releases internal buffer. WintunSendPacket is thread-safe, but the WintunAllocateSendPacket + * order of calls define the packet sending order. This means the packet is not guaranteed to be sent in the + * WintunSendPacket yet. + * + * @param Session Wintun session handle obtained with WintunStartSession + * + * @param Packet Packet obtained with WintunAllocateSendPacket + */ +typedef VOID(WINAPI WINTUN_SEND_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _In_ const BYTE *Packet); + +#pragma warning(pop) + +#ifdef __cplusplus +} +#endif diff --git a/src/client/main.cpp b/src/client/main.cpp index d863422..33cf9a2 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -10,9 +10,11 @@ #include #include #include +#include using asio::ip::tcp; using namespace ColumnLynx::Utils; +using namespace ColumnLynx::Net; volatile sig_atomic_t done = 0; @@ -56,6 +58,13 @@ int main(int argc, char** argv) { log("ColumnLynx Client, Version " + getVersion()); log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details."); +#if defined(__WIN32__) + WintunInitialize(); +#endif + + VirtualInterface tun("columnlynxtun0"); + log("Using virtual interface: " + tun.getName()); + LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); std::array aesKey = {0}; // Defualt zeroed state until modified by handshake diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp new file mode 100644 index 0000000..2d43a7b --- /dev/null +++ b/src/common/virtual_interface.cpp @@ -0,0 +1,131 @@ +// virtual_interface.cpp - Virtual Interface for Network Communication +// Copyright (C) 2025 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::Net { + // ------------------------------ Constructor ------------------------------ + VirtualInterface::VirtualInterface(const std::string& ifName) + : mIfName(ifName), mFd(-1) + { + #if defined(__linux__) + // ---- Linux: /dev/net/tun ---- + mFd = open("/dev/net/tun", O_RDWR); + if (mFd < 0) + throw std::runtime_error("Failed to open /dev/net/tun: " + std::string(strerror(errno))); + + struct ifreq ifr {}; + ifr.ifr_flags = IFF_TUN | IFF_NO_PI; + std::strncpy(ifr.ifr_name, ifName.c_str(), IFNAMSIZ); + + if (ioctl(mFd, TUNSETIFF, &ifr) < 0) { + close(mFd); + throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno))); + } + + #elif defined(__APPLE__) + // ---- macOS: UTUN (system control socket) ---- + mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); + if (mFd < 0) + throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno))); + + struct ctl_info ctlInfo {}; + std::strncpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(ctlInfo.ctl_name)); + if (ioctl(mFd, CTLIOCGINFO, &ctlInfo) == -1) + throw std::runtime_error("ioctl(CTLIOCGINFO) failed: " + std::string(strerror(errno))); + + struct sockaddr_ctl sc {}; + sc.sc_len = sizeof(sc); + sc.sc_family = AF_SYSTEM; + sc.ss_sysaddr = AF_SYS_CONTROL; + sc.sc_id = ctlInfo.ctl_id; + sc.sc_unit = 0; // utun0 (0 = auto-assign) + + if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) + throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno))); + + // Retrieve actual utun device name + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + if (getsockname(mFd, (struct sockaddr*)&addr, &addrlen) == 0) { + const struct sockaddr_ctl* addr_ctl = (const struct sockaddr_ctl*)&addr; + mIfName = "utun" + std::to_string(addr_ctl->sc_unit - 1); + } else { + mIfName = "utunX"; + } + + #elif defined(_WIN32) + // ---- Windows: Wintun (WireGuard virtual adapter) ---- + WINTUN_ADAPTER_HANDLE adapter = + WintunOpenAdapter(L"ColumnLynx", std::wstring(ifName.begin(), ifName.end()).c_str()); + if (!adapter) + throw std::runtime_error("Wintun adapter not found or not installed"); + + WINTUN_SESSION_HANDLE session = + WintunStartSession(adapter, 0x200000); // ring buffer size + if (!session) + throw std::runtime_error("Failed to start Wintun session"); + + mHandle = WintunGetReadWaitEvent(session); + mFd = -1; // not used on Windows + mIfName = ifName; + + #else + throw std::runtime_error("Unsupported platform"); + #endif + } + + // ------------------------------ Destructor ------------------------------ + VirtualInterface::~VirtualInterface() { + #if defined(__linux__) || defined(__APPLE__) + if (mFd >= 0) + close(mFd); + #elif defined(_WIN32) + // Wintun sessions need explicit stop + // (assuming you stored the session handle as member) + // WintunEndSession(mSession); + #endif + } + + // ------------------------------ Read ------------------------------ + std::vector VirtualInterface::readPacket() { + #if defined(__linux__) || defined(__APPLE__) + std::vector buf(4096); + ssize_t n = read(mFd, buf.data(), buf.size()); + if (n < 0) + throw std::runtime_error("read() failed: " + std::string(strerror(errno))); + buf.resize(n); + return buf; + + #elif defined(_WIN32) + WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr); + if (!packet) return {}; + std::vector buf(packet->Data, packet->Data + packet->Length); + WintunReleaseReceivePacket(mSession, packet); + return buf; + #else + return {}; + #endif + } + + // ------------------------------ Write ------------------------------ + void VirtualInterface::writePacket(const std::vector& packet) { + #if defined(__linux__) || defined(__APPLE__) + ssize_t n = write(mFd, packet.data(), packet.size()); + if (n < 0) + throw std::runtime_error("write() failed: " + std::string(strerror(errno))); + + #elif defined(_WIN32) + WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size()); + if (!tx) throw std::runtime_error("WintunAllocateSendPacket failed"); + memcpy(tx->Data, packet.data(), packet.size()); + WintunSendPacket(mSession, tx); + #endif + } + + // ------------------------------ Accessors ------------------------------ + const std::string& VirtualInterface::getName() const { return mIfName; } + + int VirtualInterface::getFd() const { return mFd; } +} \ No newline at end of file diff --git a/third_party/wintun/LICENSE_1_0_GPL-2.0-only.txt b/third_party/wintun/LICENSE_1_0_GPL-2.0-only.txt new file mode 100644 index 0000000..a66a55f --- /dev/null +++ b/third_party/wintun/LICENSE_1_0_GPL-2.0-only.txt @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/third_party/wintun/LICENSE_1_0_MIT.txt b/third_party/wintun/LICENSE_1_0_MIT.txt new file mode 100644 index 0000000..9a18de4 --- /dev/null +++ b/third_party/wintun/LICENSE_1_0_MIT.txt @@ -0,0 +1,19 @@ +Copyright (c) 2018 WireGuard LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file From aebca5cd7edde2f3682b607d2846c24d6cf4795c Mon Sep 17 00:00:00 2001 From: DcruBro Date: Wed, 12 Nov 2025 19:20:44 +0100 Subject: [PATCH 11/20] update readme and attributions --- ATTRIBUTIONS.md | 3 ++- README.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md index 9a1efa3..102969e 100644 --- a/ATTRIBUTIONS.md +++ b/ATTRIBUTIONS.md @@ -21,4 +21,5 @@ This project uses the standalone version of the ASIO C++ library for asynchronou - **Website:** https://www.wintun.net/ - **Copyright:** (c) 2018-2025 WireGuard LLC - **License:** MIT License OR GPL-2.0 License -- **License Text:** See `third_party/wintun/` \ No newline at end of file +- **License Text:** See `third_party/wintun/` +- **Utilized Under:** MIT License diff --git a/README.md b/README.md index 47252e4..db9358a 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,6 @@ distributed under the [Boost Software License, Version 1.0](https://www.boost.or This project includes the CXXOPTS Library distributed under the MIT License -This project includes the [Wintun Library](https://www.wintun.net/), distributed under the MIT License or the GPL-2.0 License. +This project includes the [Wintun Library](https://www.wintun.net/), distributed under the MIT License or the GPL-2.0 License. This project utilizes it under the MIT license. -*See **ATTRIBUTIONS.md** for details.* \ No newline at end of file +*See **ATTRIBUTIONS.md** for details.* From 5c8409b3127df4772236bd70f299183304f63e3e Mon Sep 17 00:00:00 2001 From: DcruBro Date: Thu, 13 Nov 2025 08:31:46 +0100 Subject: [PATCH 12/20] Added a basic TUN, testing the implementation. --- .../columnlynx/client/net/tcp/tcp_client.hpp | 7 +- .../columnlynx/client/net/udp/udp_client.hpp | 7 +- .../common/net/protocol_structs.hpp | 21 +++ .../common/net/session_registry.hpp | 52 +++++++- .../common/net/virtual_interface.hpp | 25 +++- .../server/net/tcp/tcp_connection.hpp | 1 + .../columnlynx/server/net/tcp/tcp_server.hpp | 1 + .../columnlynx/server/net/udp/udp_server.hpp | 9 +- src/client/main.cpp | 34 +++-- src/client/net/tcp/tcp_client.cpp | 14 +- src/client/net/udp/udp_client.cpp | 5 + src/common/virtual_interface.cpp | 122 +++++++++++++++--- src/server/main.cpp | 28 +++- src/server/server/net/tcp/tcp_connection.cpp | 28 +++- src/server/server/net/udp/udp_server.cpp | 8 +- 15 files changed, 310 insertions(+), 52 deletions(-) create mode 100644 include/columnlynx/common/net/protocol_structs.hpp diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index ae6fbd6..a0eabcb 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include using asio::ip::tcp; @@ -25,7 +27,8 @@ namespace ColumnLynx::Net::TCP { Utils::LibSodiumWrapper* sodiumWrapper, std::array* aesKey, uint64_t* sessionIDRef, - bool* insecureMode) + bool* insecureMode, + VirtualInterface* tun = nullptr) : mResolver(ioContext), mSocket(ioContext), @@ -70,5 +73,7 @@ namespace ColumnLynx::Net::TCP { std::chrono::steady_clock::time_point mLastHeartbeatSent; int mMissedHeartbeats = 0; bool mIsHostDomain; + Protocol::TunConfig mTunConfig; + VirtualInterface* mTun = nullptr; }; } \ 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 0d804ec..ad21fda 100644 --- a/include/columnlynx/client/net/udp/udp_client.hpp +++ b/include/columnlynx/client/net/udp/udp_client.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace ColumnLynx::Net::UDP { class UDPClient { @@ -17,8 +18,9 @@ namespace ColumnLynx::Net::UDP { 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) { mStartReceive(); } + uint64_t* sessionIDRef, + VirtualInterface* tunRef = nullptr) + : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef) { mStartReceive(); } void start(); void sendMessage(const std::string& data = ""); @@ -35,6 +37,7 @@ namespace ColumnLynx::Net::UDP { std::string mPort; std::array* mAesKeyRef; uint64_t* mSessionIDRef; + VirtualInterface* mTunRef; std::array mRecvBuffer; // Adjust size as needed }; } \ No newline at end of file diff --git a/include/columnlynx/common/net/protocol_structs.hpp b/include/columnlynx/common/net/protocol_structs.hpp new file mode 100644 index 0000000..dbbc54f --- /dev/null +++ b/include/columnlynx/common/net/protocol_structs.hpp @@ -0,0 +1,21 @@ +// protocol_structs.hpp - Network Protocol Structures +// Copyright (C) 2025 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 + +namespace ColumnLynx::Protocol { + #pragma pack(push, 1) + struct TunConfig { + uint8_t version; + uint8_t prefixLength; + uint16_t mtu; + uint32_t serverIP; + uint32_t clientIP; + uint32_t dns1; + uint32_t dns2; + }; + #pragma pack(pop) +} \ No newline at end of file diff --git a/include/columnlynx/common/net/session_registry.hpp b/include/columnlynx/common/net/session_registry.hpp index ad21a2f..533d0ba 100644 --- a/include/columnlynx/common/net/session_registry.hpp +++ b/include/columnlynx/common/net/session_registry.hpp @@ -21,6 +21,9 @@ namespace ColumnLynx::Net { std::atomic sendCounter{0}; std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point expires{}; + uint32_t clientTunIP; + uint32_t serverTunIP; + uint64_t sessionID; Nonce base_nonce{}; ~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); } @@ -29,7 +32,7 @@ namespace ColumnLynx::Net { SessionState(SessionState&&) = default; SessionState& operator=(SessionState&&) = default; - explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24)) : aesKey(k) { + 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) { expires = created + ttl; } @@ -46,6 +49,7 @@ namespace ColumnLynx::Net { void put(uint64_t sessionID, std::shared_ptr state) { std::unique_lock lock(mMutex); mSessions[sessionID] = std::move(state); + mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID]; } // Lookup @@ -55,6 +59,12 @@ namespace ColumnLynx::Net { return (it == mSessions.end()) ? nullptr : it->second; } + std::shared_ptr getByIP(uint32_t ip) const { + std::shared_lock lock(mMutex); + auto it = mIPSessions.find(ip); + return (it == mIPSessions.end()) ? nullptr : it->second; + } + std::unordered_map> snapshot() const { std::unordered_map> snap; std::shared_lock lock(mMutex); @@ -79,10 +89,50 @@ namespace ColumnLynx::Net { ++it; } } + + for (auto it = mIPSessions.begin(); it != mIPSessions.end(); ) { + if (it->second && it->second->expires <= now) { + it = mIPSessions.erase(it); + } else { + ++it; + } + } + } + + int size() const { + std::shared_lock lock(mMutex); + return static_cast(mSessions.size()); + } + + // IP management (simple for /24 subnet) + + uint32_t getFirstAvailableIP() const { + std::shared_lock lock(mMutex); + uint32_t baseIP = 0x0A0A0002; // 10.10.0.2 + + // TODO: Expand to support larger subnets + for (uint32_t offset = 0; offset < 254; offset++) { + uint32_t candidateIP = baseIP + offset; + if (mSessionIPs.find(candidateIP) == mSessionIPs.end()) { + return candidateIP; + } + } + } + + void lockIP(uint64_t sessionID, uint32_t ip) { + std::unique_lock lock(mMutex); + mSessionIPs[sessionID] = ip; + } + + void deallocIP(uint64_t sessionID) { + std::unique_lock lock(mMutex); + mSessionIPs.erase(sessionID); } private: mutable std::shared_mutex mMutex; std::unordered_map> mSessions; + std::unordered_map mSessionIPs; + std::unordered_map> mIPSessions; }; } \ No newline at end of file diff --git a/include/columnlynx/common/net/virtual_interface.hpp b/include/columnlynx/common/net/virtual_interface.hpp index 6cdf874..a3dcb76 100644 --- a/include/columnlynx/common/net/virtual_interface.hpp +++ b/include/columnlynx/common/net/virtual_interface.hpp @@ -17,6 +17,7 @@ #include #include #include + #include #elif defined(__APPLE__) #include #include @@ -24,8 +25,11 @@ #include #include #include + #include #elif defined(_WIN32) #include + #include + #include #include #pragma comment(lib, "advapi32.lib") #endif @@ -36,12 +40,31 @@ namespace ColumnLynx::Net { explicit VirtualInterface(const std::string& ifName); ~VirtualInterface(); + bool configureIP(uint32_t clientIP, uint32_t serverIP, + uint8_t prefixLen, uint16_t mtu); + std::vector readPacket(); void writePacket(const std::vector& packet); const std::string& getName() const; - int getFd() const; // for ASIO integration (on POSIX) + int getFd() const; // For ASIO integration (on POSIX) + + static inline std::string ipToString(uint32_t ip) { + struct in_addr addr; + addr.s_addr = ip; // expected in network byte order + + char buf[INET_ADDRSTRLEN]; + if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf))) + return "0.0.0.0"; + + return std::string(buf); + } + private: + bool mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu); + bool mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu); + bool mApplyWindowsIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu); + std::string mIfName; int mFd; // POSIX #if defined(_WIN32) diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index 7681b97..0ac06bf 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { class TCPConnection : public std::enable_shared_from_this { diff --git a/include/columnlynx/server/net/tcp/tcp_server.hpp b/include/columnlynx/server/net/tcp/tcp_server.hpp index 599a5ad..50a890f 100644 --- a/include/columnlynx/server/net/tcp/tcp_server.hpp +++ b/include/columnlynx/server/net/tcp/tcp_server.hpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp index be7a47a..640bb9c 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -8,12 +8,13 @@ #include #include #include +#include namespace ColumnLynx::Net::UDP { class UDPServer { public: - UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false) - : mSocket(ioContext), mHostRunning(hostRunning) + UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false, VirtualInterface* tun = nullptr) + : mSocket(ioContext), mHostRunning(hostRunning), mTun(tun) { asio::error_code ec; @@ -43,13 +44,15 @@ namespace ColumnLynx::Net::UDP { void stop(); + void sendData(const uint64_t sessionID, const std::string& data); + 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 bool* mHostRunning; + VirtualInterface* mTun; }; } \ No newline at end of file diff --git a/src/client/main.cpp b/src/client/main.cpp index 33cf9a2..9d93866 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -15,6 +15,7 @@ using asio::ip::tcp; using namespace ColumnLynx::Utils; using namespace ColumnLynx::Net; +using namespace ColumnLynx; volatile sig_atomic_t done = 0; @@ -62,7 +63,7 @@ int main(int argc, char** argv) { WintunInitialize(); #endif - VirtualInterface tun("columnlynxtun0"); + VirtualInterface tun("utun1"); log("Using virtual interface: " + tun.getName()); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); @@ -71,8 +72,8 @@ int main(int argc, char** argv) { uint64_t sessionID = 0; asio::io_context io; - auto client = std::make_shared(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode); - auto udpClient = std::make_shared(io, host, port, &aesKey, &sessionID); + auto client = std::make_shared(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode, &tun); + auto udpClient = std::make_shared(io, host, port, &aesKey, &sessionID, &tun); client->start(); udpClient->start(); @@ -88,18 +89,25 @@ int main(int argc, char** argv) { // Client is running // TODO: SIGINT or SIGTERM seems to not kill this instantly! while ((client->isConnected() || !client->isHandshakeComplete()) && !done) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Temp wait + auto packet = tun.readPacket(); - 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; - } - } + Nonce nonce{}; + randombytes_buf(nonce.data(), nonce.size()); + + auto ciphertext = LibSodiumWrapper::encryptMessage( + packet.data(), packet.size(), + aesKey, + nonce, + "udp-data" + ); + + std::vector udpPayload; + udpPayload.insert(udpPayload.end(), nonce.begin(), nonce.end()); + udpPayload.insert(udpPayload.end(), reinterpret_cast(&sessionID), reinterpret_cast(&sessionID) + sizeof(sessionID)); + udpPayload.insert(udpPayload.end(), ciphertext.begin(), ciphertext.end()); + udpClient->sendMessage(std::string(udpPayload.begin(), udpPayload.end())); } + log("Client shutting down."); udpClient->stop(); client->disconnect(); diff --git a/src/client/net/tcp/tcp_client.cpp b/src/client/net/tcp/tcp_client.cpp index d5388a3..c6185e7 100644 --- a/src/client/net/tcp/tcp_client.cpp +++ b/src/client/net/tcp/tcp_client.cpp @@ -239,19 +239,29 @@ namespace ColumnLynx::Net::TCP { mConnectionAESKey, symNonce ); - if (decrypted.size() != sizeof(mConnectionSessionID)) { - Utils::error("Decrypted session ID has invalid size. Terminating connection."); + if (decrypted.size() != sizeof(mConnectionSessionID) + sizeof(Protocol::TunConfig)) { + Utils::error("Decrypted config has invalid size. Terminating connection."); disconnect(); return; } std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID)); + std::memcpy(&mTunConfig, decrypted.data() + sizeof(mConnectionSessionID), sizeof(Protocol::TunConfig)); Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID)); if (mSessionIDRef) { // Copy to the global reference *mSessionIDRef = mConnectionSessionID; } + uint32_t clientIP = ntohl(mTunConfig.clientIP); + uint32_t serverIP = ntohl(mTunConfig.serverIP); + uint8_t prefixLen = mTunConfig.prefixLength; + uint16_t mtu = mTunConfig.mtu; + + if (mTun) { + mTun->configureIP(clientIP, serverIP, prefixLen, mtu); + } + mHandshakeComplete = true; } diff --git a/src/client/net/udp/udp_client.cpp b/src/client/net/udp/udp_client.cpp index c5cdc5c..e012e8a 100644 --- a/src/client/net/udp/udp_client.cpp +++ b/src/client/net/udp/udp_client.cpp @@ -107,5 +107,10 @@ namespace ColumnLynx::Net::UDP { } Utils::log("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes)); + + // Write to TUN + if (mTunRef) { + mTunRef->writePacket(plaintext); + } } } \ No newline at end of file diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp index 2d43a7b..f544cac 100644 --- a/src/common/virtual_interface.cpp +++ b/src/common/virtual_interface.cpp @@ -14,37 +14,37 @@ namespace ColumnLynx::Net { mFd = open("/dev/net/tun", O_RDWR); if (mFd < 0) throw std::runtime_error("Failed to open /dev/net/tun: " + std::string(strerror(errno))); - + struct ifreq ifr {}; ifr.ifr_flags = IFF_TUN | IFF_NO_PI; std::strncpy(ifr.ifr_name, ifName.c_str(), IFNAMSIZ); - + if (ioctl(mFd, TUNSETIFF, &ifr) < 0) { close(mFd); throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno))); } - + #elif defined(__APPLE__) // ---- macOS: UTUN (system control socket) ---- mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (mFd < 0) throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno))); - + struct ctl_info ctlInfo {}; std::strncpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(ctlInfo.ctl_name)); if (ioctl(mFd, CTLIOCGINFO, &ctlInfo) == -1) throw std::runtime_error("ioctl(CTLIOCGINFO) failed: " + std::string(strerror(errno))); - + struct sockaddr_ctl sc {}; sc.sc_len = sizeof(sc); sc.sc_family = AF_SYSTEM; sc.ss_sysaddr = AF_SYS_CONTROL; sc.sc_id = ctlInfo.ctl_id; sc.sc_unit = 0; // utun0 (0 = auto-assign) - + if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno))); - + // Retrieve actual utun device name struct sockaddr_storage addr; socklen_t addrlen = sizeof(addr); @@ -54,28 +54,28 @@ namespace ColumnLynx::Net { } else { mIfName = "utunX"; } - + #elif defined(_WIN32) // ---- Windows: Wintun (WireGuard virtual adapter) ---- WINTUN_ADAPTER_HANDLE adapter = WintunOpenAdapter(L"ColumnLynx", std::wstring(ifName.begin(), ifName.end()).c_str()); if (!adapter) throw std::runtime_error("Wintun adapter not found or not installed"); - + WINTUN_SESSION_HANDLE session = WintunStartSession(adapter, 0x200000); // ring buffer size if (!session) throw std::runtime_error("Failed to start Wintun session"); - + mHandle = WintunGetReadWaitEvent(session); mFd = -1; // not used on Windows mIfName = ifName; - + #else throw std::runtime_error("Unsupported platform"); #endif } - + // ------------------------------ Destructor ------------------------------ VirtualInterface::~VirtualInterface() { #if defined(__linux__) || defined(__APPLE__) @@ -87,7 +87,7 @@ namespace ColumnLynx::Net { // WintunEndSession(mSession); #endif } - + // ------------------------------ Read ------------------------------ std::vector VirtualInterface::readPacket() { #if defined(__linux__) || defined(__APPLE__) @@ -97,7 +97,7 @@ namespace ColumnLynx::Net { throw std::runtime_error("read() failed: " + std::string(strerror(errno))); buf.resize(n); return buf; - + #elif defined(_WIN32) WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr); if (!packet) return {}; @@ -108,14 +108,14 @@ namespace ColumnLynx::Net { return {}; #endif } - + // ------------------------------ Write ------------------------------ void VirtualInterface::writePacket(const std::vector& packet) { #if defined(__linux__) || defined(__APPLE__) ssize_t n = write(mFd, packet.data(), packet.size()); if (n < 0) throw std::runtime_error("write() failed: " + std::string(strerror(errno))); - + #elif defined(_WIN32) WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size()); if (!tx) throw std::runtime_error("WintunAllocateSendPacket failed"); @@ -123,9 +123,93 @@ namespace ColumnLynx::Net { WintunSendPacket(mSession, tx); #endif } - + // ------------------------------ Accessors ------------------------------ const std::string& VirtualInterface::getName() const { return mIfName; } - + int VirtualInterface::getFd() const { return mFd; } -} \ No newline at end of file + + // ------------------------------------------------------------ + // IP CONFIGURATION + // ------------------------------------------------------------ + bool VirtualInterface::configureIP(uint32_t clientIP, uint32_t serverIP, + uint8_t prefixLen, uint16_t mtu) + { + #if defined(__linux__) + return mApplyLinuxIP(clientIP, serverIP, prefixLen, mtu); + #elif defined(__APPLE__) + return mApplyMacOSIP(clientIP, serverIP, prefixLen, mtu); + #elif defined(_WIN32) + return mApplyWindowsIP(clientIP, serverIP, prefixLen, mtu); + #else + return false; + #endif + } + + // ------------------------------------------------------------ + // Linux + // ------------------------------------------------------------ + bool VirtualInterface::mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP, + uint8_t prefixLen, uint16_t mtu) + { + char cmd[512]; + + std::string ipStr = ipToString(clientIP); + std::string peerStr = ipToString(serverIP); + + snprintf(cmd, sizeof(cmd), + "ip addr add %s/%d peer %s dev %s", + ipStr.c_str(), prefixLen, peerStr.c_str(), mIfName.c_str()); + system(cmd); + + snprintf(cmd, sizeof(cmd), + "ip link set dev %s up mtu %d", mIfName.c_str(), mtu); + system(cmd); + + return true; + } + + // ------------------------------------------------------------ + // macOS (utun) + // ------------------------------------------------------------ + bool VirtualInterface::mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP, + uint8_t prefixLen, uint16_t mtu) + { + char cmd[512]; + + std::string ipStr = ipToString(clientIP); + std::string peerStr = ipToString(serverIP); + + snprintf(cmd, sizeof(cmd), + "ifconfig utun0 %s %s mtu %d up", + ipStr.c_str(), peerStr.c_str(), mtu); + system(cmd); + + return true; + } + + // ------------------------------------------------------------ + // Windows (Wintun) + // ------------------------------------------------------------ + bool VirtualInterface::mApplyWindowsIP(uint32_t clientIP, uint32_t serverIP, + uint8_t prefixLen, uint16_t mtu) + { + #ifdef _WIN32 + char ip[32], gw[32]; + strcpy(ip, ipToString(clientIP).c_str()); + strcpy(gw, ipToString(serverIP).c_str()); + + char cmd[256]; + snprintf(cmd, sizeof(cmd), + "netsh interface ip set address name=\"%s\" static %s %d.%d.%d.%d", + mIfName.c_str(), ip, + (prefixLen <= 8) ? ((prefixLen << 3) & 255) : 255, + 255, 255, 255); + system(cmd); + + return true; + #else + return false; + #endif + } +} // namespace ColumnLynx::Net \ No newline at end of file diff --git a/src/server/main.cpp b/src/server/main.cpp index 0477c67..ff20232 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -11,11 +11,14 @@ #include #include #include +#include using asio::ip::tcp; using namespace ColumnLynx::Utils; using namespace ColumnLynx::Net::TCP; using namespace ColumnLynx::Net::UDP; +using namespace ColumnLynx::Net; +using namespace ColumnLynx; volatile sig_atomic_t done = 0; @@ -54,6 +57,13 @@ int main(int argc, char** argv) { log("ColumnLynx Server, Version " + getVersion()); log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details."); +#if defined(__WIN32__) + WintunInitialize(); +#endif + + VirtualInterface tun("utun0"); + log("Using virtual interface: " + tun.getName()); + // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES)); @@ -64,7 +74,7 @@ int main(int argc, char** argv) { asio::io_context io; auto server = std::make_shared(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only); - auto udpServer = std::make_shared(io, serverPort(), &hostRunning, ipv4Only); + auto udpServer = std::make_shared(io, serverPort(), &hostRunning, ipv4Only, &tun); asio::signal_set signals(io, SIGINT, SIGTERM); signals.async_wait([&](const std::error_code&, int) { @@ -87,7 +97,21 @@ int main(int argc, char** argv) { log("Server started on port " + std::to_string(serverPort())); while (!done) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto packet = tun.readPacket(); + if (packet.empty()) { + continue; + } + + const uint8_t* ip = packet.data(); + uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset in IPv6-mapped header + + auto session = SessionRegistry::getInstance().getByIP(dstIP); + if (!session) { + Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipToString(dstIP)); + continue; + } + + udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end())); } log("Shutting down server..."); diff --git a/src/server/server/net/tcp/tcp_connection.cpp b/src/server/server/net/tcp/tcp_connection.cpp index 196a7cd..4c0282f 100644 --- a/src/server/server/net/tcp/tcp_connection.cpp +++ b/src/server/server/net/tcp/tcp_connection.cpp @@ -41,6 +41,9 @@ namespace ColumnLynx::Net::TCP { mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); mHandler->socket().close(ec); + SessionRegistry::getInstance().erase(mConnectionSessionID); + SessionRegistry::getInstance().deallocIP(mConnectionSessionID); + Utils::log("Closed connection to " + ip); if (mOnDisconnect) { @@ -174,16 +177,33 @@ 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::vector encryptedSessionID = Utils::LibSodiumWrapper::encryptMessage( - reinterpret_cast(&mConnectionSessionID), sizeof(mConnectionSessionID), + + uint32_t clientIP = SessionRegistry::getInstance().getFirstAvailableIP(); + Protocol::TunConfig tunConfig{}; + tunConfig.version = Utils::protocolVersion(); + tunConfig.prefixLength = 24; + tunConfig.mtu = 1420; + tunConfig.serverIP = htonl(0x0A0A0001); // 10.10.0.1 + tunConfig.clientIP = htonl(clientIP); // 10.10.0.X + tunConfig.dns1 = htonl(0x08080808); // 8.8.8.8 + tunConfig.dns2 = 0; + + SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP); + + std::vector payload(sizeof(uint64_t) + sizeof(tunConfig)); + std::memcpy(payload.data(), &mConnectionSessionID, sizeof(uint64_t)); + std::memcpy(payload.data() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig)); + + std::vector encryptedPayload = Utils::LibSodiumWrapper::encryptMessage( + payload.data(), payload.size(), mConnectionAESKey, symNonce ); - mHandler->sendMessage(ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM, Utils::uint8ArrayToString(encryptedSessionID.data(), encryptedSessionID.size())); + mHandler->sendMessage(ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM, Utils::uint8ArrayToString(encryptedPayload.data(), encryptedPayload.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)); + auto session = std::make_shared(mConnectionAESKey, std::chrono::hours(12), clientIP, htonl(0x0A0A0001), mConnectionSessionID); SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session)); } catch (const std::exception& e) { diff --git a/src/server/server/net/udp/udp_server.cpp b/src/server/server/net/udp/udp_server.cpp index 1afa8a7..161737f 100644 --- a/src/server/server/net/udp/udp_server.cpp +++ b/src/server/server/net/udp/udp_server.cpp @@ -62,16 +62,16 @@ namespace ColumnLynx::Net::UDP { // 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); - - // TODO: Process the packet payload, for now just echo back - mSendData(sessionID, std::string(plaintext.begin(), plaintext.end())); + if (mTun) { + mTun->writePacket(plaintext); // Send to virtual interface + } } 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) { + void UDPServer::sendData(const uint64_t sessionID, const std::string& data) { // Find the IPv4/IPv6 endpoint for the session std::shared_ptr session = SessionRegistry::getInstance().get(sessionID); if (!session) { From c85f622a60614742bb89230209b8276e6dc2eba1 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Thu, 13 Nov 2025 15:12:00 +0100 Subject: [PATCH 13/20] test2 --- include/columnlynx/client/net/tcp/tcp_client.hpp | 4 ++-- include/columnlynx/client/net/udp/udp_client.hpp | 4 ++-- include/columnlynx/server/net/udp/udp_server.hpp | 4 ++-- panic_dump.txt | 1 + src/client/main.cpp | 10 +++++----- src/server/main.cpp | 10 ++++++---- src/server/server/net/udp/udp_server.cpp | 1 + 7 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 panic_dump.txt diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index a0eabcb..965b7fc 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -28,7 +28,7 @@ namespace ColumnLynx::Net::TCP { std::array* aesKey, uint64_t* sessionIDRef, bool* insecureMode, - VirtualInterface* tun = nullptr) + std::shared_ptr tun = nullptr) : mResolver(ioContext), mSocket(ioContext), @@ -74,6 +74,6 @@ namespace ColumnLynx::Net::TCP { int mMissedHeartbeats = 0; bool mIsHostDomain; Protocol::TunConfig mTunConfig; - VirtualInterface* mTun = nullptr; + std::shared_ptr mTun = nullptr; }; } \ 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 ad21fda..eddaec9 100644 --- a/include/columnlynx/client/net/udp/udp_client.hpp +++ b/include/columnlynx/client/net/udp/udp_client.hpp @@ -19,7 +19,7 @@ namespace ColumnLynx::Net::UDP { const std::string& port, std::array* aesKeyRef, uint64_t* sessionIDRef, - VirtualInterface* tunRef = nullptr) + std::shared_ptr tunRef = nullptr) : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef) { mStartReceive(); } void start(); @@ -37,7 +37,7 @@ namespace ColumnLynx::Net::UDP { std::string mPort; std::array* mAesKeyRef; uint64_t* mSessionIDRef; - VirtualInterface* mTunRef; + std::shared_ptr mTunRef = nullptr; std::array mRecvBuffer; // Adjust size as needed }; } \ 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 640bb9c..ab99610 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -13,7 +13,7 @@ namespace ColumnLynx::Net::UDP { class UDPServer { public: - UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false, VirtualInterface* tun = nullptr) + UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false, std::shared_ptr tun = nullptr) : mSocket(ioContext), mHostRunning(hostRunning), mTun(tun) { asio::error_code ec; @@ -53,6 +53,6 @@ namespace ColumnLynx::Net::UDP { asio::ip::udp::endpoint mRemoteEndpoint; std::array mRecvBuffer; // Adjust size as needed bool* mHostRunning; - VirtualInterface* mTun; + std::shared_ptr mTun; }; } \ No newline at end of file diff --git a/panic_dump.txt b/panic_dump.txt new file mode 100644 index 0000000..851c75c --- /dev/null +++ b/panic_dump.txt @@ -0,0 +1 @@ += \ No newline at end of file diff --git a/src/client/main.cpp b/src/client/main.cpp index 9d93866..bcd4b6e 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -63,8 +63,8 @@ int main(int argc, char** argv) { WintunInitialize(); #endif - VirtualInterface tun("utun1"); - log("Using virtual interface: " + tun.getName()); + std::shared_ptr tun = std::make_shared("utun0"); + log("Using virtual interface: " + tun->getName()); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); @@ -72,8 +72,8 @@ int main(int argc, char** argv) { uint64_t sessionID = 0; asio::io_context io; - auto client = std::make_shared(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode, &tun); - auto udpClient = std::make_shared(io, host, port, &aesKey, &sessionID, &tun); + auto client = std::make_shared(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode, tun); + auto udpClient = std::make_shared(io, host, port, &aesKey, &sessionID, tun); client->start(); udpClient->start(); @@ -89,7 +89,7 @@ int main(int argc, char** argv) { // Client is running // TODO: SIGINT or SIGTERM seems to not kill this instantly! while ((client->isConnected() || !client->isHandshakeComplete()) && !done) { - auto packet = tun.readPacket(); + auto packet = tun->readPacket(); Nonce nonce{}; randombytes_buf(nonce.data(), nonce.size()); diff --git a/src/server/main.cpp b/src/server/main.cpp index ff20232..b2d5b2f 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -61,8 +61,8 @@ int main(int argc, char** argv) { WintunInitialize(); #endif - VirtualInterface tun("utun0"); - log("Using virtual interface: " + tun.getName()); + std::shared_ptr tun = std::make_shared("utun0"); + log("Using virtual interface: " + tun->getName()); // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); @@ -74,7 +74,7 @@ int main(int argc, char** argv) { asio::io_context io; auto server = std::make_shared(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only); - auto udpServer = std::make_shared(io, serverPort(), &hostRunning, ipv4Only, &tun); + auto udpServer = std::make_shared(io, serverPort(), &hostRunning, ipv4Only, tun); asio::signal_set signals(io, SIGINT, SIGTERM); signals.async_wait([&](const std::error_code&, int) { @@ -84,6 +84,8 @@ int main(int argc, char** argv) { hostRunning = false; server->stop(); udpServer->stop(); + tun.reset(); + tun = nullptr; }); }); @@ -97,7 +99,7 @@ int main(int argc, char** argv) { log("Server started on port " + std::to_string(serverPort())); while (!done) { - auto packet = tun.readPacket(); + auto packet = tun->readPacket(); if (packet.empty()) { continue; } diff --git a/src/server/server/net/udp/udp_server.cpp b/src/server/server/net/udp/udp_server.cpp index 161737f..de5ca67 100644 --- a/src/server/server/net/udp/udp_server.cpp +++ b/src/server/server/net/udp/udp_server.cpp @@ -62,6 +62,7 @@ namespace ColumnLynx::Net::UDP { // 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); + if (mTun) { mTun->writePacket(plaintext); // Send to virtual interface } From b37a999274e8d2bb4110c545247397c6bcdd9989 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Thu, 13 Nov 2025 15:43:46 +0100 Subject: [PATCH 14/20] Fixed crash trigger on Ctrl+C (errno == EINTR check) --- .../columnlynx/client/net/tcp/tcp_client.hpp | 3 +- .../columnlynx/client/net/udp/udp_client.hpp | 5 ++- panic_dump.txt | 1 - src/client/main.cpp | 38 ++++++++----------- src/client/net/udp/udp_client.cpp | 1 + src/common/virtual_interface.cpp | 6 ++- src/server/main.cpp | 2 - 7 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 panic_dump.txt diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index 965b7fc..820f534 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -40,7 +40,8 @@ namespace ColumnLynx::Net::TCP { mInsecureMode(insecureMode), mHeartbeatTimer(mSocket.get_executor()), mLastHeartbeatReceived(std::chrono::steady_clock::now()), - mLastHeartbeatSent(std::chrono::steady_clock::now()) + mLastHeartbeatSent(std::chrono::steady_clock::now()), + mTun(tun) {} void start(); diff --git a/include/columnlynx/client/net/udp/udp_client.hpp b/include/columnlynx/client/net/udp/udp_client.hpp index eddaec9..3eed06e 100644 --- a/include/columnlynx/client/net/udp/udp_client.hpp +++ b/include/columnlynx/client/net/udp/udp_client.hpp @@ -20,7 +20,10 @@ namespace ColumnLynx::Net::UDP { std::array* aesKeyRef, uint64_t* sessionIDRef, std::shared_ptr tunRef = nullptr) - : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef) { mStartReceive(); } + : mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef) + { + mStartReceive(); + } void start(); void sendMessage(const std::string& data = ""); diff --git a/panic_dump.txt b/panic_dump.txt deleted file mode 100644 index 851c75c..0000000 --- a/panic_dump.txt +++ /dev/null @@ -1 +0,0 @@ -= \ No newline at end of file diff --git a/src/client/main.cpp b/src/client/main.cpp index bcd4b6e..c81c043 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -21,7 +21,7 @@ volatile sig_atomic_t done = 0; void signalHandler(int signum) { if (signum == SIGINT || signum == SIGTERM) { - log("Received termination signal. Shutting down client."); + //log("Received termination signal. Shutting down client."); done = 1; } } @@ -63,7 +63,7 @@ int main(int argc, char** argv) { WintunInitialize(); #endif - std::shared_ptr tun = std::make_shared("utun0"); + std::shared_ptr tun = std::make_shared("utun1"); log("Using virtual interface: " + tun->getName()); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); @@ -82,37 +82,31 @@ int main(int argc, char** argv) { std::thread ioThread([&io]() { io.run(); }); - ioThread.detach(); + //ioThread.join(); log("Client connected to " + host + ":" + port); - + // Client is running - // TODO: SIGINT or SIGTERM seems to not kill this instantly! while ((client->isConnected() || !client->isHandshakeComplete()) && !done) { auto packet = tun->readPacket(); - - Nonce nonce{}; - randombytes_buf(nonce.data(), nonce.size()); - - auto ciphertext = LibSodiumWrapper::encryptMessage( - packet.data(), packet.size(), - aesKey, - nonce, - "udp-data" - ); - - std::vector udpPayload; - udpPayload.insert(udpPayload.end(), nonce.begin(), nonce.end()); - udpPayload.insert(udpPayload.end(), reinterpret_cast(&sessionID), reinterpret_cast(&sessionID) + sizeof(sessionID)); - udpPayload.insert(udpPayload.end(), ciphertext.begin(), ciphertext.end()); - udpClient->sendMessage(std::string(udpPayload.begin(), udpPayload.end())); + if (!client->isConnected() || done) { + break; // Bail out if connection died or signal set while blocked + } + + if (packet.empty()) { + continue; + } + + udpClient->sendMessage(std::string(packet.begin(), packet.end())); } log("Client shutting down."); udpClient->stop(); client->disconnect(); io.stop(); - ioThread.join(); + + if (ioThread.joinable()) + ioThread.join(); } catch (const std::exception& e) { error("Client error: " + std::string(e.what())); diff --git a/src/client/net/udp/udp_client.cpp b/src/client/net/udp/udp_client.cpp index e012e8a..61cf0f8 100644 --- a/src/client/net/udp/udp_client.cpp +++ b/src/client/net/udp/udp_client.cpp @@ -6,6 +6,7 @@ namespace ColumnLynx::Net::UDP { void UDPClient::start() { + // TODO: Add IPv6 auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort); mRemoteEndpoint = *endpoints.begin(); mSocket.open(asio::ip::udp::v4()); diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp index f544cac..76a0c67 100644 --- a/src/common/virtual_interface.cpp +++ b/src/common/virtual_interface.cpp @@ -93,8 +93,12 @@ namespace ColumnLynx::Net { #if defined(__linux__) || defined(__APPLE__) std::vector buf(4096); ssize_t n = read(mFd, buf.data(), buf.size()); - if (n < 0) + if (n < 0) { + if (errno == EINTR) { + return {}; // Interrupted, return empty + } throw std::runtime_error("read() failed: " + std::string(strerror(errno))); + } buf.resize(n); return buf; diff --git a/src/server/main.cpp b/src/server/main.cpp index b2d5b2f..c32074e 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -84,8 +84,6 @@ int main(int argc, char** argv) { hostRunning = false; server->stop(); udpServer->stop(); - tun.reset(); - tun = nullptr; }); }); From 5a5f830cd98f79707b173733acbd94ab2e95b22e Mon Sep 17 00:00:00 2001 From: DcruBro Date: Thu, 13 Nov 2025 15:45:24 +0100 Subject: [PATCH 15/20] Change tun interface, change to different name at some point --- src/client/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/main.cpp b/src/client/main.cpp index c81c043..a331bf1 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -63,7 +63,7 @@ int main(int argc, char** argv) { WintunInitialize(); #endif - std::shared_ptr tun = std::make_shared("utun1"); + std::shared_ptr tun = std::make_shared("utun0"); log("Using virtual interface: " + tun->getName()); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); From 3ad98b8403d9941c3f13e8841d546648b375d056 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Thu, 13 Nov 2025 16:04:39 +0100 Subject: [PATCH 16/20] root permissions check and version counter update --- CMakeLists.txt | 2 +- src/common/utils.cpp | 2 +- src/common/virtual_interface.cpp | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 53fc39d..1344026 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 0.0.3 + VERSION 0.0.4 LANGUAGES CXX ) diff --git a/src/common/utils.cpp b/src/common/utils.cpp index 7003ebf..c62b6ec 100644 --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -37,7 +37,7 @@ namespace ColumnLynx::Utils { } std::string getVersion() { - return "a0.3"; + return "a0.4"; } unsigned short serverPort() { diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp index 76a0c67..953bd27 100644 --- a/src/common/virtual_interface.cpp +++ b/src/common/virtual_interface.cpp @@ -42,8 +42,11 @@ namespace ColumnLynx::Net { sc.sc_id = ctlInfo.ctl_id; sc.sc_unit = 0; // utun0 (0 = auto-assign) - if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) + if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) { + if (errno == EPERM) + throw std::runtime_error("connect(AF_SYS_CONTROL) failed: Insufficient permissions (try running as root)"); throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno))); + } // Retrieve actual utun device name struct sockaddr_storage addr; From 2343fdd1e28fde0f00be921fc08b3f0f97abe38e Mon Sep 17 00:00:00 2001 From: DcruBro Date: Fri, 14 Nov 2025 19:25:52 +0100 Subject: [PATCH 17/20] htonl in ipToString --- include/columnlynx/common/net/virtual_interface.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/columnlynx/common/net/virtual_interface.hpp b/include/columnlynx/common/net/virtual_interface.hpp index a3dcb76..8bd74ed 100644 --- a/include/columnlynx/common/net/virtual_interface.hpp +++ b/include/columnlynx/common/net/virtual_interface.hpp @@ -51,7 +51,7 @@ namespace ColumnLynx::Net { static inline std::string ipToString(uint32_t ip) { struct in_addr addr; - addr.s_addr = ip; // expected in network byte order + addr.s_addr = htonl(ip); char buf[INET_ADDRSTRLEN]; if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf))) From ff81bfed31f1b64be096b46736aed238f323ea08 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Fri, 14 Nov 2025 20:29:25 +0100 Subject: [PATCH 18/20] Temporary /24 netmask for macOS --- include/columnlynx/common/net/virtual_interface.hpp | 6 ++++++ src/common/virtual_interface.cpp | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/include/columnlynx/common/net/virtual_interface.hpp b/include/columnlynx/common/net/virtual_interface.hpp index 8bd74ed..321b742 100644 --- a/include/columnlynx/common/net/virtual_interface.hpp +++ b/include/columnlynx/common/net/virtual_interface.hpp @@ -60,6 +60,12 @@ namespace ColumnLynx::Net { return std::string(buf); } + static inline uint32_t prefixLengthToNetmask(uint8_t prefixLen) { + if (prefixLen == 0) return 0; + uint32_t mask = (0xFFFFFFFF << (32 - prefixLen)) & 0xFFFFFFFF; + return htonl(mask); // convert to network byte order + } + private: bool mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu); bool mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu); diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp index 953bd27..78bec31 100644 --- a/src/common/virtual_interface.cpp +++ b/src/common/virtual_interface.cpp @@ -191,6 +191,14 @@ namespace ColumnLynx::Net { "ifconfig utun0 %s %s mtu %d up", ipStr.c_str(), peerStr.c_str(), mtu); system(cmd); + + // Reset cmd buffer + cmd[0] = '\0'; + + // Set netmask (/24 CIDR temporarily with raw command, improve later) + snprintf(cmd, sizeof(cmd), + "ifconfig utun0 netmask 255.255.255.0"); + system(cmd); return true; } From 09806c3c0f4a933c83ad5667e7dc2bf738c3ae1b Mon Sep 17 00:00:00 2001 From: DcruBro Date: Fri, 14 Nov 2025 20:36:19 +0100 Subject: [PATCH 19/20] test --- src/common/virtual_interface.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp index 78bec31..369ffa6 100644 --- a/src/common/virtual_interface.cpp +++ b/src/common/virtual_interface.cpp @@ -187,18 +187,13 @@ namespace ColumnLynx::Net { std::string ipStr = ipToString(clientIP); std::string peerStr = ipToString(serverIP); + // Set netmask (/24 CIDR temporarily with raw command, improve later) snprintf(cmd, sizeof(cmd), - "ifconfig utun0 %s %s mtu %d up", + "ifconfig utun0 %s %s mtu %d netmask 255.255.255.0 up", ipStr.c_str(), peerStr.c_str(), mtu); system(cmd); - // Reset cmd buffer - cmd[0] = '\0'; - - // Set netmask (/24 CIDR temporarily with raw command, improve later) - snprintf(cmd, sizeof(cmd), - "ifconfig utun0 netmask 255.255.255.0"); - system(cmd); + Utils::log("Executed command: " + std::string(cmd)); return true; } From a78b98ac56a2aac2408e562bc9fe81225bd815a4 Mon Sep 17 00:00:00 2001 From: DcruBro Date: Tue, 18 Nov 2025 20:12:45 +0100 Subject: [PATCH 20/20] rename iptostring --- include/columnlynx/common/net/virtual_interface.hpp | 2 +- src/common/virtual_interface.cpp | 12 ++++++------ src/server/main.cpp | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/columnlynx/common/net/virtual_interface.hpp b/include/columnlynx/common/net/virtual_interface.hpp index 321b742..cf3d2d4 100644 --- a/include/columnlynx/common/net/virtual_interface.hpp +++ b/include/columnlynx/common/net/virtual_interface.hpp @@ -49,7 +49,7 @@ namespace ColumnLynx::Net { const std::string& getName() const; int getFd() const; // For ASIO integration (on POSIX) - static inline std::string ipToString(uint32_t ip) { + static inline std::string ipv4ToString(uint32_t ip) { struct in_addr addr; addr.s_addr = htonl(ip); diff --git a/src/common/virtual_interface.cpp b/src/common/virtual_interface.cpp index 369ffa6..42ad8bd 100644 --- a/src/common/virtual_interface.cpp +++ b/src/common/virtual_interface.cpp @@ -161,8 +161,8 @@ namespace ColumnLynx::Net { { char cmd[512]; - std::string ipStr = ipToString(clientIP); - std::string peerStr = ipToString(serverIP); + std::string ipStr = ipv4ToString(clientIP); + std::string peerStr = ipv4ToString(serverIP); snprintf(cmd, sizeof(cmd), "ip addr add %s/%d peer %s dev %s", @@ -184,8 +184,8 @@ namespace ColumnLynx::Net { { char cmd[512]; - std::string ipStr = ipToString(clientIP); - std::string peerStr = ipToString(serverIP); + std::string ipStr = ipv4ToString(clientIP); + std::string peerStr = ipv4ToString(serverIP); // Set netmask (/24 CIDR temporarily with raw command, improve later) snprintf(cmd, sizeof(cmd), @@ -206,8 +206,8 @@ namespace ColumnLynx::Net { { #ifdef _WIN32 char ip[32], gw[32]; - strcpy(ip, ipToString(clientIP).c_str()); - strcpy(gw, ipToString(serverIP).c_str()); + strcpy(ip, ipv4ToString(clientIP).c_str()); + strcpy(gw, ipv4ToString(serverIP).c_str()); char cmd[256]; snprintf(cmd, sizeof(cmd), diff --git a/src/server/main.cpp b/src/server/main.cpp index c32074e..75dff3b 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -107,7 +107,7 @@ int main(int argc, char** argv) { auto session = SessionRegistry::getInstance().getByIP(dstIP); if (!session) { - Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipToString(dstIP)); + Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipv4ToString(dstIP)); continue; }