Compare commits

6 Commits

19 changed files with 134 additions and 402 deletions

View File

@@ -6,20 +6,16 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx
VERSION 0.0.5
VERSION 0.0.4
LANGUAGES CXX
)
# ---------------------------------------------------------
# General C++ setup
# ---------------------------------------------------------
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
#set(CMAKE_CXX_FLAGS_DEBUG "-g")
#add_compile_options(${CMAKE_CXX_FLAGS_DEBUG})
add_compile_definitions(DEBUG=1) # TODO: Forcing for now, add dymanic based on compile flags later
include(FetchContent)

View File

@@ -13,7 +13,6 @@
#include <array>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
@@ -43,46 +42,19 @@ namespace ColumnLynx::Net::TCP {
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now()),
mTun(tun)
{
// Preload the config map
mRawClientConfig = Utils::getConfigMap("client_config");
{}
if (!mRawClientConfig.empty()) {
Utils::debug("Loading the keys");
PrivateKey sk;
PublicKey pk;
std::copy_n(Utils::hexStringToBytes(mRawClientConfig.find("CLIENT_PRIVATE_KEY")->second).begin(), sk.size(), sk.begin()); // This is extremely stupid, but the C++ compiler has forced my hand (I would've just used to_array, but fucking asio decls)
std::copy_n(Utils::hexStringToBytes(mRawClientConfig.find("CLIENT_PUBLIC_KEY")->second).begin(), pk.size(), pk.begin());
mLibSodiumWrapper->setKeys(pk, sk);
Utils::debug("Newly-Loaded Public Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPublicKey(), 32));
Utils::debug("Newly-Loaded Private Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPrivateKey(), 64));
Utils::debug("Public Encryption Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getXPublicKey(), 32));
}
}
// Starts the TCP Client and initiaties the handshake
void start();
// Sends a TCP message to the server
void sendMessage(ClientMessageType type, const std::string& data = "");
// Attempt to gracefully disconnect from the server
void disconnect(bool echo = true);
// Get the handshake status
bool isHandshakeComplete() const;
// Get the connection status
bool isConnected() const;
private:
// Start the heartbeat routine
void mStartHeartbeat();
// Handle an incoming TCP message
void mHandleMessage(ServerMessageType type, const std::string& data);
// TODO: Move ptrs to smart ptrs
bool mConnected = false;
bool mHandshakeComplete = false;
tcp::resolver mResolver;
@@ -104,6 +76,5 @@ namespace ColumnLynx::Net::TCP {
bool mIsHostDomain;
Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr;
std::unordered_map<std::string, std::string> mRawClientConfig;
};
}

View File

@@ -25,17 +25,12 @@ namespace ColumnLynx::Net::UDP {
mStartReceive();
}
// Start the UDP client
void start();
// Send a UDP message
void sendMessage(const std::string& data = "");
// Stop the UDP client
void stop();
private:
// Start the UDP listener routine
void mStartReceive();
// Handle an incoming UDP message
void mHandlePacket(std::size_t bytes);
asio::ip::udp::socket mSocket;

View File

@@ -35,35 +35,27 @@ namespace ColumnLynx::Utils {
public:
LibSodiumWrapper();
// These are pretty self-explanatory
uint8_t* getPublicKey();
uint8_t* getPrivateKey();
uint8_t* getXPublicKey() { return mXPublicKey.data(); }
uint8_t* getXPrivateKey() { return mXPrivateKey.data(); }
// Set the Asymmetric signing keypair. This also regenerates the corresponding encryption keypair; Dangerous!
void setKeys(PublicKey pk, PrivateKey sk) {
mPublicKey = pk;
mPrivateKey = sk;
// Convert to Curve25519 keys for encryption
crypto_sign_ed25519_pk_to_curve25519(mXPublicKey.data(), mPublicKey.data());
crypto_sign_ed25519_sk_to_curve25519(mXPrivateKey.data(), mPrivateKey.data());
}
// Helper section
// Generates a random 256-bit (32-byte) array
static std::array<uint8_t, 32> generateRandom256Bit();
// Sign a message with the stored private key
static inline Signature signMessage(const uint8_t* msg, size_t len, const PrivateKey& sk) {
Signature sig{};
crypto_sign_detached(sig.data(), nullptr, msg, len, sk.data());
return sig;
}
static inline bool verifyMessage(const uint8_t* msg, size_t len,
const Signature& sig, const PublicKey& pk) {
return crypto_sign_verify_detached(sig.data(), msg, len, pk.data()) == 0;
}
// Overloads for std::string / std::array
static inline Signature signMessage(const std::string& msg, const PrivateKey& sk) {
return signMessage(reinterpret_cast<const uint8_t*>(msg.data()), msg.size(), sk);
@@ -80,11 +72,6 @@ namespace ColumnLynx::Utils {
return sig;
}
// Verify a message with a given public key
static inline bool verifyMessage(const uint8_t* msg, size_t len, const Signature& sig, const PublicKey& pk) {
return crypto_sign_verify_detached(sig.data(), msg, len, pk.data()) == 0;
}
static inline bool verifyMessage(const std::string& msg, const Signature& sig, const PublicKey& pk) {
return verifyMessage(reinterpret_cast<const uint8_t*>(msg.data()), msg.size(), sig, pk);
}
@@ -99,7 +86,7 @@ namespace ColumnLynx::Utils {
return crypto_sign_verify_detached(sig.data(), msg, len, pk_raw) == 0;
}
// Encrypt symmetrically with ChaCha20-Poly1305; returns ciphertext as bytes
// Encrypt with ChaCha20-Poly1305 (returns ciphertext as bytes)
static inline std::vector<uint8_t> encryptMessage(
const uint8_t* plaintext, size_t len,
const SymmetricKey& key, const Nonce& nonce,
@@ -122,7 +109,7 @@ namespace ColumnLynx::Utils {
return ciphertext;
}
// Decrypt symmetrically with ChaCha20-Poly1305; Returns plaintext as bytes
// Decrypt with ChaCha20-Poly1305 (returns plaintext as bytes)
static inline std::vector<uint8_t> decryptMessage(
const uint8_t* ciphertext, size_t len,
const SymmetricKey& key, const Nonce& nonce,
@@ -148,14 +135,12 @@ namespace ColumnLynx::Utils {
return plaintext;
}
// Returns a random nonce
static inline Nonce generateNonce() {
Nonce n{};
randombytes_buf(n.data(), n.size());
return n;
}
// Encrypt message asymmetrically; Returns ciphertext as bytes
static inline std::vector<uint8_t> encryptAsymmetric(
const uint8_t* plaintext, size_t len,
const AsymNonce& nonce,
@@ -176,7 +161,6 @@ namespace ColumnLynx::Utils {
return ciphertext;
}
// Decrypt message asymmetrically; Returns plaintext as bytes
static inline std::vector<uint8_t> decryptAsymmetric(
const uint8_t* ciphertext, size_t len,
const AsymNonce& nonce,
@@ -200,7 +184,6 @@ namespace ColumnLynx::Utils {
return plaintext;
}
// Verify a public key (certificate) against system-installed CAs
static inline bool verifyCertificateWithSystemCAs(const std::vector<uint8_t>& cert_der) {
// Parse DER-encoded certificate
const unsigned char* p = cert_der.data();
@@ -242,7 +225,6 @@ namespace ColumnLynx::Utils {
return result == 1;
}
// Extract the hostnames (Subject Alternative Names and Common Names) out of a public key (certificate)
static inline std::vector<std::string> getCertificateHostname(const std::vector<uint8_t>& cert_der) {
std::vector<std::string> names;

View File

@@ -14,16 +14,16 @@
namespace ColumnLynx::Net {
struct SessionState {
SymmetricKey aesKey; // Agreed-upon AES-256 kes for that session; Immutable after creation
SymmetricKey aesKey; // Immutable after creation
std::atomic<uint64_t> send_ctr{0}; // Per-direction counters
std::atomic<uint64_t> recv_ctr{0}; // Per-direction counters
asio::ip::udp::endpoint udpEndpoint; // Deducted IP + Port of that session client
std::atomic<uint64_t> sendCounter{0}; // Counter of sent messages
std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); // Time created
std::chrono::steady_clock::time_point expires{}; // Time of expiry
uint32_t clientTunIP; // Assigned IP
uint32_t serverTunIP; // Server IP
uint64_t sessionID; // Session ID
std::atomic<uint64_t> recv_ctr{0};
asio::ip::udp::endpoint udpEndpoint;
std::atomic<uint64_t> 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()); }
@@ -36,7 +36,6 @@ namespace ColumnLynx::Net {
expires = created + ttl;
}
// Set the UDP endpoint
void setUDPEndpoint(const asio::ip::udp::endpoint& ep) {
udpEndpoint = ep;
}
@@ -44,31 +43,28 @@ namespace ColumnLynx::Net {
class SessionRegistry {
public:
// Return a reference to the Session Registry instance
static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; }
// Insert or replace a session entry
// Insert or replace
void put(uint64_t sessionID, std::shared_ptr<SessionState> state) {
std::unique_lock lock(mMutex);
mSessions[sessionID] = std::move(state);
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
}
// Lookup a session entry by session ID
// Lookup
std::shared_ptr<const SessionState> get(uint64_t sessionID) const {
std::shared_lock lock(mMutex);
auto it = mSessions.find(sessionID);
return (it == mSessions.end()) ? nullptr : it->second;
}
// Lookup a session entry by IPv4
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const {
std::shared_lock lock(mMutex);
auto it = mIPSessions.find(ip);
return (it == mIPSessions.end()) ? nullptr : it->second;
}
// Get a snapshot of the Session Registry
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const {
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex);
@@ -76,7 +72,7 @@ namespace ColumnLynx::Net {
return snap;
}
// Remove a session by ID
// Remove
void erase(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessions.erase(sessionID);
@@ -103,7 +99,6 @@ namespace ColumnLynx::Net {
}
}
// Get the number of registered sessions
int size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
@@ -111,7 +106,6 @@ namespace ColumnLynx::Net {
// IP management (simple for /24 subnet)
// Get the lowest available IPv4 address; Returns 0 if none available
uint32_t getFirstAvailableIP() const {
std::shared_lock lock(mMutex);
uint32_t baseIP = 0x0A0A0002; // 10.10.0.2
@@ -122,18 +116,14 @@ namespace ColumnLynx::Net {
if (mSessionIPs.find(candidateIP) == mSessionIPs.end()) {
return candidateIP;
}
return 0; // Unavailable
}
}
// Lock an IP as assigned to a specific session
void lockIP(uint64_t sessionID, uint32_t ip) {
std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip;
}
// Unlock the IP associated with a given session
void deallocIP(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessionIPs.erase(sessionID);

View File

@@ -9,7 +9,6 @@
#include <array>
namespace ColumnLynx::Net::UDP {
// @deprecated
// Shared between server and client
enum class MessageType : uint8_t {
PING = 0x01,

View File

@@ -204,7 +204,6 @@ namespace ColumnLynx::Utils {
std::cerr << "Panic trace written to panic_dump.txt\n";
}
// Gets the current time
static std::string currentTime() {
std::time_t t = std::time(nullptr);
char buf[64];

View File

@@ -7,12 +7,6 @@
#include <string>
#include <cstdint>
#include <array>
#include <iomanip>
#include <sstream>
#include <vector>
#include <fstream>
#include <chrono>
#include <unordered_map>
#ifdef _WIN32
#include <winsock2.h>
@@ -23,27 +17,17 @@
#endif
namespace ColumnLynx::Utils {
// General log function. Use for logging important information.
void log(const std::string &msg);
// General warning function. Use for logging important warnings.
void warn(const std::string &msg);
// General error function. Use for logging failures and general errors.
void error(const std::string &msg);
// Debug log function. Use for logging non-important information. These will not print unless the binary is compiled with DEBUG=1
void debug(const std::string &msg);
// Returns the hostname of the running platform.
std::string getHostname();
// Returns the version of the running release.
std::string getVersion();
unsigned short serverPort();
unsigned char protocolVersion();
std::vector<std::string> getWhitelistedKeys();
// Raw byte to hex string conversion helper
std::string bytesToHexString(const uint8_t* bytes, size_t length);
// Hex string to raw byte conversion helper
std::vector<uint8_t> hexStringToBytes(const std::string& hex);
// uint8_t to raw string conversion helper
template <size_t N>
@@ -54,28 +38,4 @@ namespace ColumnLynx::Utils {
inline std::string uint8ArrayToString(const uint8_t* data, size_t length) {
return std::string(reinterpret_cast<const char*>(data), length);
}
inline constexpr uint64_t cbswap64(uint64_t x) {
return ((x & 0x00000000000000FFULL) << 56) |
((x & 0x000000000000FF00ULL) << 40) |
((x & 0x0000000000FF0000ULL) << 24) |
((x & 0x00000000FF000000ULL) << 8) |
((x & 0x000000FF00000000ULL) >> 8) |
((x & 0x0000FF0000000000ULL) >> 24) |
((x & 0x00FF000000000000ULL) >> 40) |
((x & 0xFF00000000000000ULL) >> 56);
}
// host -> big-endian (for little-endian hosts) - 64 bit
inline constexpr uint64_t chtobe64(uint64_t x) {
return cbswap64(x);
}
// big-endian -> host (for little-endian hosts) - 64 bit
inline constexpr uint64_t cbe64toh(uint64_t x) {
return cbswap64(x);
}
// Returns the config file in an unordered_map format. This purely reads the config file, you still need to parse it manually.
std::unordered_map<std::string, std::string> getConfigMap(std::string path);
};

View File

@@ -33,18 +33,12 @@ namespace ColumnLynx::Net::TCP {
return conn;
}
// Start a TCP Connection (Handler for an incoming connection)
void start();
// Send a message to the TCP client
void sendMessage(ServerMessageType type, const std::string& data = "");
// Set callback for disconnects
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb);
// Disconnect the client
void disconnect();
// Get the assigned session ID
uint64_t getSessionID() const;
// Get the assigned AES key; You should probably access this via the Session Registry instead
std::array<uint8_t, 32> getAESKey() const;
private:
@@ -57,9 +51,7 @@ namespace ColumnLynx::Net::TCP {
mLastHeartbeatSent(std::chrono::steady_clock::now())
{}
// Start the heartbeat routine
void mStartHeartbeat();
// Handle an incoming TCP message
void mHandleMessage(ClientMessageType type, const std::string& data);
std::shared_ptr<MessageHandler> mHandler;

View File

@@ -31,9 +31,6 @@ namespace ColumnLynx::Net::TCP {
mSodiumWrapper(sodiumWrapper),
mHostRunning(hostRunning)
{
// Preload the config map
mRawServerConfig = Utils::getConfigMap("server_config");
asio::error_code ec;
if (!ipv4Only) {
@@ -62,19 +59,15 @@ namespace ColumnLynx::Net::TCP {
mStartAccept();
}
// Stop the TCP Server
void stop();
private:
// Start accepting clients via TCP
void mStartAccept();
asio::io_context &mIoContext;
asio::ip::tcp::acceptor mAcceptor;
std::unordered_set<TCPConnection::pointer> mClients;
Utils::LibSodiumWrapper *mSodiumWrapper;
bool* mHostRunning;
std::unordered_map<std::string, std::string> mRawServerConfig;
};
}

View File

@@ -42,18 +42,13 @@ namespace ColumnLynx::Net::UDP {
mStartReceive();
}
// Stop the UDP server
void stop();
// Send UDP data to an endpoint; Fetched via the Session Registry
void sendData(const uint64_t sessionID, const std::string& data);
private:
// Start receiving UDP data
void mStartReceive();
// Handle an incoming UDP packet
void mHandlePacket(std::size_t bytes);
asio::ip::udp::socket mSocket;
asio::ip::udp::endpoint mRemoteEndpoint;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed

View File

@@ -49,9 +49,6 @@ int main(int argc, char** argv) {
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
std::cout << "This software is licensed under the GPLv2-only license OR the GPLv3 license.\n";
std::cout << "Copyright (C) 2025, The ColumnLynx Contributors.\n";
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
return 0;
}
@@ -66,12 +63,10 @@ int main(int argc, char** argv) {
WintunInitialize();
#endif
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>("utun1");
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>("utun0");
log("Using virtual interface: " + tun->getName());
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper.getPublicKey(), 32));
debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper.getPrivateKey(), 64));
std::array<uint8_t, 32> aesKey = {0}; // Defualt zeroed state until modified by handshake
uint64_t sessionID = 0;

View File

@@ -28,18 +28,14 @@ namespace ColumnLynx::Net::TCP {
// 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; // Voodoo black magic
self->mIsHostDomain = inet_pton(AF_INET, mHost.c_str(), (void*)(&addr4)) != 1 && inet_pton(AF_INET6, mHost.c_str(), (void*)(&addr6)) != 1;
std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
payload.push_back(Utils::protocolVersion());
/*payload.insert(payload.end(),
payload.insert(payload.end(),
mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
);*/
payload.insert(payload.end(),
mLibSodiumWrapper->getPublicKey(),
mLibSodiumWrapper->getPublicKey() + crypto_sign_PUBLICKEYBYTES
);
mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size()));
@@ -251,9 +247,6 @@ namespace ColumnLynx::Net::TCP {
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID));
std::memcpy(&mTunConfig, decrypted.data() + sizeof(mConnectionSessionID), sizeof(Protocol::TunConfig));
mConnectionSessionID = Utils::cbe64toh(mConnectionSessionID);
Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID));
if (mSessionIDRef) { // Copy to the global reference

View File

@@ -22,12 +22,9 @@ namespace ColumnLynx::Net::UDP {
return;
}
//Utils::debug("Using AES key: " + Utils::bytesToHexString(mAesKeyRef->data(), 32));
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
*mAesKeyRef, hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
);
std::vector<uint8_t> packet;
@@ -44,7 +41,7 @@ namespace ColumnLynx::Net::UDP {
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
mSocket.send_to(asio::buffer(packet), mRemoteEndpoint);
Utils::debug("Sent UDP packet of size " + std::to_string(packet.size()));
Utils::log("Sent UDP packet of size " + std::to_string(packet.size()));
}
void UDPClient::stop() {
@@ -103,7 +100,6 @@ namespace ColumnLynx::Net::UDP {
std::vector<uint8_t> plaintext = Utils::LibSodiumWrapper::decryptMessage(
ciphertext.data(), ciphertext.size(), *mAesKeyRef, hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
);
if (plaintext.empty()) {
@@ -111,7 +107,7 @@ namespace ColumnLynx::Net::UDP {
return;
}
Utils::debug("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
Utils::log("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
// Write to TUN
if (mTunRef) {

View File

@@ -6,27 +6,15 @@
namespace ColumnLynx::Utils {
void log(const std::string &msg) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "\033[0m[" << std::to_string(now) << " LOG] " << msg << std::endl;
std::cout << "[LOG] " << msg << std::endl;
}
void warn(const std::string &msg) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cerr << "\033[33m[" << std::to_string(now) << " WARN] " << msg << "\033[0m" << std::endl;
std::cerr << "[WARN] " << msg << std::endl;
}
void error(const std::string &msg) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cerr << "\033[31m[" << std::to_string(now) << " ERROR] " << msg << "\033[0m" << std::endl;
}
void debug(const std::string &msg) {
#if DEBUG || _DEBUG
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cerr << "\033[95m[" << std::to_string(now) << " DEBUG] " << msg << "\033[0m" << std::endl;
#else
return;
#endif
std::cerr << "[ERROR] " << msg << std::endl;
}
std::string getHostname() {
@@ -49,7 +37,7 @@ namespace ColumnLynx::Utils {
}
std::string getVersion() {
return "a0.5";
return "a0.4";
}
unsigned short serverPort() {
@@ -73,78 +61,4 @@ namespace ColumnLynx::Utils {
return hexString;
}
std::vector<uint8_t> hexStringToBytes(const std::string& hex) {
// TODO: recover from errors
if (hex.length() % 2 != 0) {
throw std::invalid_argument("Hex string must have even length");
}
auto hexValue = [](char c) -> uint8_t {
if ('0' <= c && c <= '9') return c - '0';
if ('A' <= c && c <= 'F') return c - 'A' + 10;
if ('a' <= c && c <= 'f') return c - 'a' + 10;
throw std::invalid_argument("Invalid hex character");
};
size_t len = hex.length();
std::vector<uint8_t> bytes;
bytes.reserve(len / 2);
for (size_t i = 0; i < len; i += 2) {
uint8_t high = hexValue(hex[i]);
uint8_t low = hexValue(hex[i + 1]);
bytes.push_back((high << 4) | low);
}
return bytes;
}
std::vector<std::string> getWhitelistedKeys() {
// Currently re-reads the file every time, should be fine.
// Advantage of it is that you don't need to reload the server binary after adding/removing keys. Disadvantage is re-reading the file every time.
// I might redo this part.
std::vector<std::string> out;
std::ifstream file("whitelisted_keys"); // TODO: This is hardcoded for now, make dynamic
std::string line;
while (std::getline(file, line)) {
out.push_back(line);
}
return out;
}
std::unordered_map<std::string, std::string> getConfigMap(std::string path) {
// TODO: Currently re-reads every time.
std::vector<std::string> readLines;
std::ifstream file(path);
std::string line;
while (std::getline(file, line)) {
readLines.push_back(line);
}
// Parse them into the struct
std::unordered_map<std::string, std::string> config;
char delimiter = '=';
for (std::string str : readLines) {
std::stringstream ss(str);
std::string key;
std::string val;
std::getline(ss, key, delimiter);
std::getline(ss, val, delimiter);
config.insert({ key, val });
}
return config;
}
}

View File

@@ -4,8 +4,6 @@
#include <columnlynx/common/net/virtual_interface.hpp>
// This is all fucking voodoo dark magic.
namespace ColumnLynx::Net {
// ------------------------------ Constructor ------------------------------
VirtualInterface::VirtualInterface(const std::string& ifName)

View File

@@ -41,8 +41,7 @@ int main(int argc, char** argv) {
options.add_options()
("h,help", "Print help")
("4,ipv4-only", "Force IPv4 only operation", cxxopts::value<bool>()->default_value("false"))
("c,config", "Specify config file location", cxxopts::value<std::string>()->default_value("config.json"));
("4,ipv4-only", "Force IPv4 only operation", cxxopts::value<bool>()->default_value("false"));
PanicHandler::init();
@@ -50,14 +49,10 @@ int main(int argc, char** argv) {
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
std::cout << "This software is licensed under the GPLv2-only license OR the GPLv3 license.\n";
std::cout << "Copyright (C) 2025, The ColumnLynx Contributors.\n";
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
return 0;
}
bool ipv4Only = result["ipv4-only"].as<bool>();
std::string configPath = result["config"].as<std::string>();
log("ColumnLynx Server, Version " + getVersion());
log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details.");

View File

@@ -114,26 +114,7 @@ namespace ColumnLynx::Net::TCP {
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + ".");
PublicKey signPk;
std::memcpy(signPk.data(), data.data() + 1, std::min(data.size() - 1, sizeof(signPk)));
// We can safely store this without further checking, the client will need to send the encrypted AES key in a way where they must possess the corresponding private key anyways.
crypto_sign_ed25519_pk_to_curve25519(mConnectionPublicKey.data(), signPk.data()); // Store the client's public encryption key key (for identification)
Utils::debug("Client " + reqAddr + " converted public encryption key: " + Utils::bytesToHexString(mConnectionPublicKey.data(), 32));
Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size()));
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys();
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);
disconnect();
return;
}
Utils::debug("Client " + reqAddr + " passed authorized_keys");
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;
}
@@ -192,17 +173,12 @@ namespace ColumnLynx::Net::TCP {
// Make a Session ID
randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID));
// TODO: Make the session ID little-endian for network transmission
// Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose)
Nonce symNonce{}; // All zeros
uint32_t clientIP = SessionRegistry::getInstance().getFirstAvailableIP();
if (clientIP == 0) {
Utils::warn("Out of available IPs! Disconnecting client " + reqAddr);
disconnect();
return;
}
Protocol::TunConfig tunConfig{};
tunConfig.version = Utils::protocolVersion();
tunConfig.prefixLength = 24;
@@ -214,10 +190,8 @@ namespace ColumnLynx::Net::TCP {
SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP);
uint64_t sessionIDNet = Utils::chtobe64(mConnectionSessionID);
std::vector<uint8_t> payload(sizeof(uint64_t) + sizeof(tunConfig));
std::memcpy(payload.data(), &sessionIDNet, sizeof(uint64_t));
std::memcpy(payload.data(), &mConnectionSessionID, sizeof(uint64_t));
std::memcpy(payload.data() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig));
std::vector<uint8_t> encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
@@ -228,7 +202,7 @@ namespace ColumnLynx::Net::TCP {
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 (" + std::to_string(mConnectionSessionID) + ").");
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned.");
auto session = std::make_shared<SessionState>(mConnectionAESKey, std::chrono::hours(12), clientIP, htonl(0x0A0A0001), mConnectionSessionID);
SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session));

View File

@@ -48,30 +48,26 @@ namespace ColumnLynx::Net::UDP {
// Decrypt the actual payload
try {
//Utils::debug("Using AES key " + Utils::bytesToHexString(session->aesKey.data(), 32));
auto plaintext = Utils::LibSodiumWrapper::decryptMessage(
encryptedPayload.data(), encryptedPayload.size(),
session->aesKey,
hdr->nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint64_t))
hdr->nonce,
"udp-data"
);
Utils::debug("Passed decryption");
const_cast<SessionState*>(session.get())->setUDPEndpoint(mRemoteEndpoint); // Update endpoint after confirming decryption
// Update recv counter
const_cast<SessionState*>(session.get())->recv_ctr.fetch_add(1, std::memory_order_relaxed);
// For now, just log the decrypted payload
std::string payloadStr(plaintext.begin(), plaintext.end());
Utils::debug("UDP: Received packet from " + mRemoteEndpoint.address().to_string() + " - Payload: " + payloadStr);
Utils::log("UDP: Received packet from " + mRemoteEndpoint.address().to_string() + " - Payload: " + payloadStr);
if (mTun) {
mTun->writePacket(plaintext); // Send to virtual interface
}
} catch (const std::exception &ex) {
Utils::warn("UDP: Failed to process payload from " + mRemoteEndpoint.address().to_string() + " Raw Error: '" + ex.what() + "'");
} catch (...) {
Utils::warn("UDP: Failed to decrypt payload from " + mRemoteEndpoint.address().to_string());
return;
}
}
@@ -97,7 +93,6 @@ namespace ColumnLynx::Net::UDP {
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
session->aesKey, hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint64_t))
);
std::vector<uint8_t> packet;
@@ -114,7 +109,7 @@ namespace ColumnLynx::Net::UDP {
// Send packet
mSocket.send_to(asio::buffer(packet), endpoint);
Utils::debug("UDP: Sent packet of size " + std::to_string(packet.size()) + " to " + std::to_string(sessionID) + " (" + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()) + ")");
Utils::log("UDP: Sent packet of size " + std::to_string(packet.size()) + " to " + std::to_string(sessionID) + " (" + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()) + ")");
}
void UDPServer::stop() {