diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index bbc59f1..040a2a2 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { class TCPConnection : public std::enable_shared_from_this { @@ -26,12 +27,9 @@ namespace ColumnLynx::Net::TCP { static pointer create( asio::ip::tcp::socket socket, - std::shared_ptr sodiumWrapper, - std::unordered_map* serverConfig, - std::string configDirPath, std::function onDisconnect) { - auto conn = pointer(new TCPConnection(std::move(socket), sodiumWrapper, serverConfig, configDirPath)); + auto conn = pointer(new TCPConnection(std::move(socket))); conn->mOnDisconnect = std::move(onDisconnect); return conn; } @@ -51,15 +49,12 @@ namespace ColumnLynx::Net::TCP { std::array getAESKey() const; private: - TCPConnection(asio::ip::tcp::socket socket, std::shared_ptr sodiumWrapper, std::unordered_map* serverConfig, std::string configDirPath) + TCPConnection(asio::ip::tcp::socket socket) : mHandler(std::make_shared(std::move(socket))), - mLibSodiumWrapper(sodiumWrapper), - mRawServerConfig(serverConfig), mHeartbeatTimer(mHandler->socket().get_executor()), mLastHeartbeatReceived(std::chrono::steady_clock::now()), - mLastHeartbeatSent(std::chrono::steady_clock::now()), - mConfigDirPath(configDirPath) + mLastHeartbeatSent(std::chrono::steady_clock::now()) {} // Start the heartbeat routine @@ -69,8 +64,6 @@ namespace ColumnLynx::Net::TCP { std::shared_ptr mHandler; std::function)> mOnDisconnect; - std::shared_ptr mLibSodiumWrapper; - std::unordered_map* mRawServerConfig; std::array mConnectionAESKey; uint64_t mConnectionSessionID; AsymPublicKey mConnectionPublicKey; @@ -79,6 +72,5 @@ namespace ColumnLynx::Net::TCP { std::chrono::steady_clock::time_point mLastHeartbeatSent; int mMissedHeartbeats = 0; std::string mRemoteIP; // Cached remote IP to avoid calling remote_endpoint() on closed sockets - std::string mConfigDirPath; }; } \ No newline at end of file diff --git a/include/columnlynx/server/net/tcp/tcp_server.hpp b/include/columnlynx/server/net/tcp/tcp_server.hpp index 7e532e2..224993e 100644 --- a/include/columnlynx/server/net/tcp/tcp_server.hpp +++ b/include/columnlynx/server/net/tcp/tcp_server.hpp @@ -17,29 +17,23 @@ #include #include #include +#include namespace ColumnLynx::Net::TCP { class TCPServer { public: TCPServer(asio::io_context& ioContext, - uint16_t port, - std::shared_ptr sodiumWrapper, - std::shared_ptr hostRunning, - std::string& configPath, - bool ipv4Only = false) + uint16_t port) : mIoContext(ioContext), - mAcceptor(ioContext), - mSodiumWrapper(sodiumWrapper), - mHostRunning(hostRunning), - mConfigDirPath(configPath) + mAcceptor(ioContext) { // Preload the config map - mRawServerConfig = Utils::getConfigMap(configPath + "server_config", {"NETWORK", "SUBNET_MASK"}); - asio::error_code ec_open, ec_v6only, ec_bind; - if (!ipv4Only) { + bool isIPv4Only = ServerSession::getInstance().isIPv4Only(); + + if (!isIPv4Only) { // Try IPv6 (dual-stack if supported) asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port); @@ -55,8 +49,8 @@ namespace ColumnLynx::Net::TCP { } // If IPv6 bind failed OR IPv6 open failed OR forced IPv4-only - if (ipv4Only || ec_open || ec_bind) { - if (!ipv4Only) + if (isIPv4Only || ec_open || ec_bind) { + if (!isIPv4Only) Utils::warn("TCP: IPv6 unavailable (open=" + ec_open.message() + ", bind=" + ec_bind.message() + "), falling back to IPv4 only"); @@ -84,10 +78,6 @@ namespace ColumnLynx::Net::TCP { asio::io_context &mIoContext; asio::ip::tcp::acceptor mAcceptor; std::unordered_set mClients; - std::shared_ptr mSodiumWrapper; - std::shared_ptr mHostRunning; - std::unordered_map mRawServerConfig; - std::string mConfigDirPath; }; } \ No newline at end of file diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp index 61feb05..a46b628 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -9,16 +9,18 @@ #include #include #include +#include +#include namespace ColumnLynx::Net::UDP { class UDPServer { public: - UDPServer(asio::io_context& ioContext, uint16_t port, std::shared_ptr hostRunning, bool ipv4Only = false, std::shared_ptr tun = nullptr) - : mSocket(ioContext), mHostRunning(hostRunning), mTun(tun) + UDPServer(asio::io_context& ioContext, uint16_t port) + : mSocket(ioContext) { asio::error_code ec_open, ec_v6only, ec_bind; - if (!ipv4Only) { + if (!mIpv4Only) { asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port); // Try opening IPv6 socket @@ -34,8 +36,8 @@ namespace ColumnLynx::Net::UDP { } // Fallback to IPv4 if IPv6 is unusable - if (ipv4Only || ec_open || ec_bind) { - if (!ipv4Only) { + if (mIpv4Only || ec_open || ec_bind) { + if (!mIpv4Only) { Utils::warn( "UDP: IPv6 unavailable (open=" + ec_open.message() + ", bind=" + ec_bind.message() + @@ -70,7 +72,7 @@ namespace ColumnLynx::Net::UDP { asio::ip::udp::socket mSocket; asio::ip::udp::endpoint mRemoteEndpoint; std::array mRecvBuffer; // 2048 seems stable - std::shared_ptr mHostRunning; - std::shared_ptr mTun; + bool mIpv4Only = ServerSession::getInstance().isIPv4Only(); + const std::shared_ptr mTun = ServerSession::getInstance().getVirtualInterface(); }; } \ No newline at end of file diff --git a/include/columnlynx/server/server_session.hpp b/include/columnlynx/server/server_session.hpp new file mode 100644 index 0000000..bbb05a6 --- /dev/null +++ b/include/columnlynx/server/server_session.hpp @@ -0,0 +1,62 @@ +// server_session.hpp - Client Session data for ColumnLynx +// Copyright (C) 2026 DcruBro +// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. + +#pragma once + +#include +#include +#include +#include +#include + +namespace ColumnLynx { + struct ServerState { + std::shared_ptr sodiumWrapper; + std::shared_ptr virtualInterface; + std::string configPath; + std::unordered_map serverConfig; + bool ipv4Only; + bool hostRunning; + + ~ServerState() = default; + ServerState(const ServerState&) = delete; + ServerState& operator=(const ServerState&) = delete; + ServerState(ServerState&&) = default; + ServerState& operator=(ServerState&&) = default; + + explicit ServerState() = default; + }; + + class ServerSession { + public: + // Return a reference to the Server Session instance + static ServerSession& getInstance() { static ServerSession instance; return instance; } + + // Return the current server state + std::shared_ptr getServerState() const; + + // Set the server state + void setServerState(std::shared_ptr state); + + // Getters + std::shared_ptr getSodiumWrapper() const; + const std::string& getConfigPath() const; + const std::unordered_map& getRawServerConfig() const; + const std::shared_ptr& getVirtualInterface() const; + bool isIPv4Only() const; + bool isHostRunning() const; + + // Setters + void setSodiumWrapper(std::shared_ptr sodiumWrapper); + void setConfigPath(const std::string& configPath); + void setRawServerConfig(const std::unordered_map& config); + void setVirtualInterface(std::shared_ptr tun); + void setIPv4Only(bool ipv4Only); + void setHostRunning(bool hostRunning); + + private: + mutable std::shared_mutex mMutex; + std::shared_ptr mServerState{nullptr}; + }; +} \ No newline at end of file diff --git a/src/server/main.cpp b/src/server/main.cpp index 3d8da0d..35b8188 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #if defined(__WIN32__) #include @@ -69,6 +70,8 @@ int main(int argc, char** argv) { //WintunInitialize(); #endif + struct ServerState serverState{}; + // Get the config path, ENV > CLI > /etc/columnlynx std::string configPath = optionsObj["config-dir"].as(); const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR"); @@ -84,11 +87,23 @@ int main(int argc, char** argv) { #endif } - std::unordered_map config = Utils::getConfigMap(configPath + "server_config"); + serverState.configPath = configPath; + +#if defined(DEBUG) + std::unordered_map config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" }); +#else + // A production server should never use random keys. If the config file cannot be read or does not contain keys, the server will fail to start. + std::unordered_map config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK", "SERVER_PUBLIC_KEY", "SERVER_PRIVATE_KEY" }); +#endif + + serverState.serverConfig = config; std::shared_ptr tun = std::make_shared(optionsObj["interface"].as()); log("Using virtual interface: " + tun->getName()); + // Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs) + serverState.virtualInterface = tun; + // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) std::shared_ptr sodiumWrapper = std::make_shared(); @@ -117,19 +132,24 @@ int main(int argc, char** argv) { log("Server public key: " + bytesToHexString(sodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); - std::shared_ptr hostRunning = std::make_shared(true); + serverState.sodiumWrapper = sodiumWrapper; + serverState.ipv4Only = ipv4Only; + serverState.hostRunning = true; + + // Store the global state; from now on, it should only be accessed through the ServerSession singleton, which will ensure thread safety with its internal mutex + ServerSession::getInstance().setServerState(std::make_shared(std::move(serverState))); asio::io_context io; - auto server = std::make_shared(io, serverPort(), sodiumWrapper, hostRunning, configPath, ipv4Only); - auto udpServer = std::make_shared(io, serverPort(), hostRunning, ipv4Only, tun); + auto server = std::make_shared(io, serverPort()); + auto udpServer = std::make_shared(io, serverPort()); asio::signal_set signals(io, SIGINT, SIGTERM); signals.async_wait([&](const std::error_code&, int) { log("Received termination signal. Shutting down server gracefully."); done = 1; asio::post(io, [&]() { - *hostRunning = false; + ServerSession::getInstance().setHostRunning(false); server->stop(); udpServer->stop(); }); diff --git a/src/server/net/tcp/tcp_connection.cpp b/src/server/net/tcp/tcp_connection.cpp index 3626c5b..4226e04 100644 --- a/src/server/net/tcp/tcp_connection.cpp +++ b/src/server/net/tcp/tcp_connection.cpp @@ -145,7 +145,7 @@ namespace ColumnLynx::Net::TCP { Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size())); - std::vector whitelistedKeys = Utils::getWhitelistedKeys(mConfigDirPath); + std::vector whitelistedKeys = Utils::getWhitelistedKeys(ServerSession::getInstance().getConfigPath()); if (std::find(whitelistedKeys.begin(), whitelistedKeys.end(), Utils::bytesToHexString(signPk.data(), signPk.size())) == whitelistedKeys.end()) { Utils::warn("Non-whitelisted client attempted to connect, terminating. Client IP: " + reqAddr); @@ -156,7 +156,7 @@ namespace ColumnLynx::Net::TCP { Utils::debug("Client " + reqAddr + " passed authorized_keys"); - mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist + mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(ServerSession::getInstance().getSodiumWrapper()->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist break; } case ClientMessageType::HANDSHAKE_CHALLENGE: { @@ -169,7 +169,7 @@ namespace ColumnLynx::Net::TCP { // Sign the challenge Signature sig = Utils::LibSodiumWrapper::signMessage( challengeData, sizeof(challengeData), - mLibSodiumWrapper->getPrivateKey() + ServerSession::getInstance().getSodiumWrapper()->getPrivateKey() ); mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response @@ -191,8 +191,8 @@ namespace ColumnLynx::Net::TCP { std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size()); try { std::array arrayPrivateKey; - std::copy(mLibSodiumWrapper->getXPrivateKey(), - mLibSodiumWrapper->getXPrivateKey() + 32, + std::copy(ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey(), + ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32, arrayPrivateKey.begin()); // Decrypt the AES key using the client's public key and server's private key @@ -217,8 +217,10 @@ namespace ColumnLynx::Net::TCP { // Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose) Nonce symNonce{}; // All zeros - std::string networkString = mRawServerConfig->find("NETWORK")->second; // The load check guarantees that this value exists - uint8_t configMask = std::stoi(mRawServerConfig->find("SUBNET_MASK")->second); // Same deal here + const auto& serverConfig = ServerSession::getInstance().getRawServerConfig(); + + std::string networkString = serverConfig.find("NETWORK")->second; // The load check guarantees that this value exists + uint8_t configMask = std::stoi(serverConfig.find("SUBNET_MASK")->second); // Same deal here uint32_t baseIP = Net::VirtualInterface::stringToIpv4(networkString); diff --git a/src/server/net/tcp/tcp_server.cpp b/src/server/net/tcp/tcp_server.cpp index a9dc472..77c30b7 100644 --- a/src/server/net/tcp/tcp_server.cpp +++ b/src/server/net/tcp/tcp_server.cpp @@ -27,16 +27,13 @@ namespace ColumnLynx::Net::TCP { } Utils::error("Accept failed: " + ec.message()); // Try again only if still running - if (mHostRunning && *mHostRunning && mAcceptor.is_open()) + if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open()) mStartAccept(); return; } auto client = TCPConnection::create( std::move(socket), - mSodiumWrapper, - &mRawServerConfig, - mConfigDirPath, [this](std::shared_ptr c) { mClients.erase(c); Utils::log("Client removed."); @@ -46,7 +43,7 @@ namespace ColumnLynx::Net::TCP { client->start(); Utils::log("Accepted new client connection."); - if (mHostRunning && *mHostRunning && mAcceptor.is_open()) + if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open()) mStartAccept(); } ); diff --git a/src/server/net/udp/udp_server.cpp b/src/server/net/udp/udp_server.cpp index c6730d2..b1b3284 100644 --- a/src/server/net/udp/udp_server.cpp +++ b/src/server/net/udp/udp_server.cpp @@ -16,11 +16,11 @@ namespace ColumnLynx::Net::UDP { if (ec) { if (ec == asio::error::operation_aborted) return; // Socket closed // Other recv error - if (mHostRunning && *mHostRunning) mStartReceive(); + if (ServerSession::getInstance().isHostRunning()) mStartReceive(); return; } if (bytes > 0) mHandlePacket(bytes); - if (mHostRunning && *mHostRunning) mStartReceive(); + if (ServerSession::getInstance().isHostRunning()) mStartReceive(); } ); } diff --git a/src/server/server_session.cpp b/src/server/server_session.cpp new file mode 100644 index 0000000..14addc6 --- /dev/null +++ b/src/server/server_session.cpp @@ -0,0 +1,92 @@ +// server_session.cpp - Client Session data for ColumnLynx +// Copyright (C) 2026 DcruBro +// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details. + +#include + +namespace ColumnLynx { + std::shared_ptr ServerSession::getServerState() const { + std::shared_lock lock(mMutex); + return mServerState; + } + + void ServerSession::setServerState(std::shared_ptr state) { + std::unique_lock lock(mMutex); + mServerState = std::move(state); + } + + std::shared_ptr ServerSession::getSodiumWrapper() const { + std::shared_lock lock(mMutex); + return mServerState ? mServerState->sodiumWrapper : nullptr; + } + + const std::string& ServerSession::getConfigPath() const { + static const std::string emptyString; + std::shared_ptr state = getServerState(); + return state ? state->configPath : emptyString; + } + + const std::unordered_map& ServerSession::getRawServerConfig() const { + static const std::unordered_map emptyMap; + std::shared_ptr state = getServerState(); + return state ? state->serverConfig : emptyMap; + } + + const std::shared_ptr& ServerSession::getVirtualInterface() const { + static const std::shared_ptr nullTun = nullptr; + std::shared_ptr state = getServerState(); + return state ? state->virtualInterface : nullTun; + } + + bool ServerSession::isIPv4Only() const { + std::shared_ptr state = getServerState(); + return state ? state->ipv4Only : false; + } + + bool ServerSession::isHostRunning() const { + std::shared_ptr state = getServerState(); + return state ? state->hostRunning : false; + } + + void ServerSession::setSodiumWrapper(std::shared_ptr sodiumWrapper) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->sodiumWrapper = std::move(sodiumWrapper); + } + + void ServerSession::setConfigPath(const std::string& configPath) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->configPath = configPath; + } + + void ServerSession::setRawServerConfig(const std::unordered_map& config) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->serverConfig = config; + } + + void ServerSession::setVirtualInterface(std::shared_ptr tun) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->virtualInterface = std::move(tun); + } + + void ServerSession::setIPv4Only(bool ipv4Only) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->ipv4Only = ipv4Only; + } + + void ServerSession::setHostRunning(bool hostRunning) { + std::unique_lock lock(mMutex); + if (!mServerState) + mServerState = std::make_shared(); + mServerState->hostRunning = hostRunning; + } +} \ No newline at end of file