Compare commits

17 Commits

Author SHA1 Message Date
d2242ebbc7 Comment some code 2025-11-26 21:52:01 +01:00
84c00b7bcb Update version number, add timestamp to logs (maybe also other stuff that I forgot) 2025-11-26 18:13:41 +01:00
7b757c814c Moved UDP msg logs to debug 2025-11-26 03:34:39 +01:00
DcruBro
e3df3cd0a7 Switched to C++23 as the project standard.
Added a basic parser for client_config and server_config, and added some basic authorization.
Need to work on verification of the server.
2025-11-25 21:45:10 +01:00
DcruBro
f776f1fdd1 Colourful logs 2025-11-25 00:36:29 +01:00
DcruBro
de3ec98363 Added checking of whitelisted keys on server 2025-11-25 00:22:52 +01:00
022debfa5b Fix byte order for sessionid, add 64-bit conversion helpers - damn you C standard for no htonl/ntohl for 64-bit :( 2025-11-22 21:53:45 +01:00
a78b98ac56 rename iptostring 2025-11-18 20:12:45 +01:00
09806c3c0f test 2025-11-14 20:36:19 +01:00
ff81bfed31 Temporary /24 netmask for macOS 2025-11-14 20:29:25 +01:00
2343fdd1e2 htonl in ipToString 2025-11-14 19:25:52 +01:00
3ad98b8403 root permissions check and version counter update 2025-11-13 16:04:39 +01:00
766f878a8d Merge pull request 'Merge tun-test into dev' (#5) from tun-test into dev
Reviewed-on: #5
2025-11-13 14:51:36 +00:00
5a5f830cd9 Change tun interface, change to different name at some point 2025-11-13 15:45:24 +01:00
b37a999274 Fixed crash trigger on Ctrl+C (errno == EINTR check) 2025-11-13 15:43:46 +01:00
c85f622a60 test2 2025-11-13 15:12:00 +01:00
5c8409b312 Added a basic TUN, testing the implementation. 2025-11-13 08:31:46 +01:00
21 changed files with 725 additions and 183 deletions

View File

@@ -6,16 +6,20 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA # If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx project(ColumnLynx
VERSION 0.0.3 VERSION 0.0.5
LANGUAGES CXX LANGUAGES CXX
) )
# --------------------------------------------------------- # ---------------------------------------------------------
# General C++ setup # General C++ setup
# --------------------------------------------------------- # ---------------------------------------------------------
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) 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) include(FetchContent)

View File

@@ -13,6 +13,9 @@
#include <array> #include <array>
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <unordered_map>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp; using asio::ip::tcp;
@@ -25,7 +28,8 @@ namespace ColumnLynx::Net::TCP {
Utils::LibSodiumWrapper* sodiumWrapper, Utils::LibSodiumWrapper* sodiumWrapper,
std::array<uint8_t, 32>* aesKey, std::array<uint8_t, 32>* aesKey,
uint64_t* sessionIDRef, uint64_t* sessionIDRef,
bool* insecureMode) bool* insecureMode,
std::shared_ptr<VirtualInterface> tun = nullptr)
: :
mResolver(ioContext), mResolver(ioContext),
mSocket(ioContext), mSocket(ioContext),
@@ -37,20 +41,48 @@ namespace ColumnLynx::Net::TCP {
mInsecureMode(insecureMode), mInsecureMode(insecureMode),
mHeartbeatTimer(mSocket.get_executor()), mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()), mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(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(); void start();
// Sends a TCP message to the server
void sendMessage(ClientMessageType type, const std::string& data = ""); void sendMessage(ClientMessageType type, const std::string& data = "");
// Attempt to gracefully disconnect from the server
void disconnect(bool echo = true); void disconnect(bool echo = true);
// Get the handshake status
bool isHandshakeComplete() const; bool isHandshakeComplete() const;
// Get the connection status
bool isConnected() const; bool isConnected() const;
private: private:
// Start the heartbeat routine
void mStartHeartbeat(); void mStartHeartbeat();
// Handle an incoming TCP message
void mHandleMessage(ServerMessageType type, const std::string& data); void mHandleMessage(ServerMessageType type, const std::string& data);
// TODO: Move ptrs to smart ptrs
bool mConnected = false; bool mConnected = false;
bool mHandshakeComplete = false; bool mHandshakeComplete = false;
tcp::resolver mResolver; tcp::resolver mResolver;
@@ -70,5 +102,8 @@ namespace ColumnLynx::Net::TCP {
std::chrono::steady_clock::time_point mLastHeartbeatSent; std::chrono::steady_clock::time_point mLastHeartbeatSent;
int mMissedHeartbeats = 0; int mMissedHeartbeats = 0;
bool mIsHostDomain; bool mIsHostDomain;
Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr;
std::unordered_map<std::string, std::string> mRawClientConfig;
}; };
} }

View File

@@ -9,6 +9,7 @@
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <array> #include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net::UDP { namespace ColumnLynx::Net::UDP {
class UDPClient { class UDPClient {
@@ -17,15 +18,24 @@ namespace ColumnLynx::Net::UDP {
const std::string& host, const std::string& host,
const std::string& port, const std::string& port,
std::array<uint8_t, 32>* aesKeyRef, std::array<uint8_t, 32>* aesKeyRef,
uint64_t* sessionIDRef) uint64_t* sessionIDRef,
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef) { mStartReceive(); } std::shared_ptr<VirtualInterface> tunRef = nullptr)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef)
{
mStartReceive();
}
// Start the UDP client
void start(); void start();
// Send a UDP message
void sendMessage(const std::string& data = ""); void sendMessage(const std::string& data = "");
// Stop the UDP client
void stop(); void stop();
private: private:
// Start the UDP listener routine
void mStartReceive(); void mStartReceive();
// Handle an incoming UDP message
void mHandlePacket(std::size_t bytes); void mHandlePacket(std::size_t bytes);
asio::ip::udp::socket mSocket; asio::ip::udp::socket mSocket;
@@ -35,6 +45,7 @@ namespace ColumnLynx::Net::UDP {
std::string mPort; std::string mPort;
std::array<uint8_t, 32>* mAesKeyRef; std::array<uint8_t, 32>* mAesKeyRef;
uint64_t* mSessionIDRef; uint64_t* mSessionIDRef;
std::shared_ptr<VirtualInterface> mTunRef = nullptr;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
}; };
} }

View File

@@ -35,27 +35,35 @@ namespace ColumnLynx::Utils {
public: public:
LibSodiumWrapper(); LibSodiumWrapper();
// These are pretty self-explanatory
uint8_t* getPublicKey(); uint8_t* getPublicKey();
uint8_t* getPrivateKey(); uint8_t* getPrivateKey();
uint8_t* getXPublicKey() { return mXPublicKey.data(); } uint8_t* getXPublicKey() { return mXPublicKey.data(); }
uint8_t* getXPrivateKey() { return mXPrivateKey.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 // Helper section
// Generates a random 256-bit (32-byte) array // Generates a random 256-bit (32-byte) array
static std::array<uint8_t, 32> generateRandom256Bit(); 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) { static inline Signature signMessage(const uint8_t* msg, size_t len, const PrivateKey& sk) {
Signature sig{}; Signature sig{};
crypto_sign_detached(sig.data(), nullptr, msg, len, sk.data()); crypto_sign_detached(sig.data(), nullptr, msg, len, sk.data());
return sig; 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 // Overloads for std::string / std::array
static inline Signature signMessage(const std::string& msg, const PrivateKey& sk) { static inline Signature signMessage(const std::string& msg, const PrivateKey& sk) {
return signMessage(reinterpret_cast<const uint8_t*>(msg.data()), msg.size(), sk); return signMessage(reinterpret_cast<const uint8_t*>(msg.data()), msg.size(), sk);
@@ -72,6 +80,11 @@ namespace ColumnLynx::Utils {
return sig; 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) { 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); return verifyMessage(reinterpret_cast<const uint8_t*>(msg.data()), msg.size(), sig, pk);
} }
@@ -86,7 +99,7 @@ namespace ColumnLynx::Utils {
return crypto_sign_verify_detached(sig.data(), msg, len, pk_raw) == 0; return crypto_sign_verify_detached(sig.data(), msg, len, pk_raw) == 0;
} }
// Encrypt with ChaCha20-Poly1305 (returns ciphertext as bytes) // Encrypt symmetrically with ChaCha20-Poly1305; returns ciphertext as bytes
static inline std::vector<uint8_t> encryptMessage( static inline std::vector<uint8_t> encryptMessage(
const uint8_t* plaintext, size_t len, const uint8_t* plaintext, size_t len,
const SymmetricKey& key, const Nonce& nonce, const SymmetricKey& key, const Nonce& nonce,
@@ -109,7 +122,7 @@ namespace ColumnLynx::Utils {
return ciphertext; return ciphertext;
} }
// Decrypt with ChaCha20-Poly1305 (returns plaintext as bytes) // Decrypt symmetrically with ChaCha20-Poly1305; Returns plaintext as bytes
static inline std::vector<uint8_t> decryptMessage( static inline std::vector<uint8_t> decryptMessage(
const uint8_t* ciphertext, size_t len, const uint8_t* ciphertext, size_t len,
const SymmetricKey& key, const Nonce& nonce, const SymmetricKey& key, const Nonce& nonce,
@@ -120,7 +133,7 @@ namespace ColumnLynx::Utils {
std::vector<uint8_t> plaintext(len - crypto_aead_chacha20poly1305_ietf_ABYTES); std::vector<uint8_t> plaintext(len - crypto_aead_chacha20poly1305_ietf_ABYTES);
unsigned long long plen = 0; unsigned long long plen = 0;
if (crypto_aead_chacha20poly1305_ietf_decrypt( if (crypto_aead_chacha20poly1305_ietf_decrypt(
plaintext.data(), &plen, plaintext.data(), &plen,
nullptr, nullptr,
@@ -135,12 +148,14 @@ namespace ColumnLynx::Utils {
return plaintext; return plaintext;
} }
// Returns a random nonce
static inline Nonce generateNonce() { static inline Nonce generateNonce() {
Nonce n{}; Nonce n{};
randombytes_buf(n.data(), n.size()); randombytes_buf(n.data(), n.size());
return n; return n;
} }
// Encrypt message asymmetrically; Returns ciphertext as bytes
static inline std::vector<uint8_t> encryptAsymmetric( static inline std::vector<uint8_t> encryptAsymmetric(
const uint8_t* plaintext, size_t len, const uint8_t* plaintext, size_t len,
const AsymNonce& nonce, const AsymNonce& nonce,
@@ -161,6 +176,7 @@ namespace ColumnLynx::Utils {
return ciphertext; return ciphertext;
} }
// Decrypt message asymmetrically; Returns plaintext as bytes
static inline std::vector<uint8_t> decryptAsymmetric( static inline std::vector<uint8_t> decryptAsymmetric(
const uint8_t* ciphertext, size_t len, const uint8_t* ciphertext, size_t len,
const AsymNonce& nonce, const AsymNonce& nonce,
@@ -184,97 +200,99 @@ namespace ColumnLynx::Utils {
return plaintext; return plaintext;
} }
static inline bool verifyCertificateWithSystemCAs(const std::vector<uint8_t>& cert_der) { // Verify a public key (certificate) against system-installed CAs
// Parse DER-encoded certificate static inline bool verifyCertificateWithSystemCAs(const std::vector<uint8_t>& cert_der) {
const unsigned char* p = cert_der.data(); // Parse DER-encoded certificate
std::unique_ptr<X509, decltype(&X509_free)> cert( const unsigned char* p = cert_der.data();
d2i_X509(nullptr, &p, cert_der.size()), X509_free std::unique_ptr<X509, decltype(&X509_free)> cert(
); d2i_X509(nullptr, &p, cert_der.size()), X509_free
if (!cert) { );
return false; if (!cert) {
return false;
}
// Create a certificate store
std::unique_ptr<X509_STORE, decltype(&X509_STORE_free)> 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<X509_STORE_CTX, decltype(&X509_STORE_CTX_free)> 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;
} }
// Create a certificate store
std::unique_ptr<X509_STORE, decltype(&X509_STORE_free)> 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<X509_STORE_CTX, decltype(&X509_STORE_CTX_free)> 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;
}
static inline std::vector<std::string> getCertificateHostname(const std::vector<uint8_t>& cert_der) { // Extract the hostnames (Subject Alternative Names and Common Names) out of a public key (certificate)
std::vector<std::string> names; static inline std::vector<std::string> getCertificateHostname(const std::vector<uint8_t>& cert_der) {
std::vector<std::string> names;
if (cert_der.empty()) if (cert_der.empty())
return names; return names;
// Parse DER certificate // Parse DER certificate
const unsigned char* p = cert_der.data(); const unsigned char* p = cert_der.data();
X509* cert = d2i_X509(nullptr, &p, cert_der.size()); X509* cert = d2i_X509(nullptr, &p, cert_der.size());
if (!cert) if (!cert)
return names; return names;
// --- Subject Alternative Names (SAN) --- // --- Subject Alternative Names (SAN) ---
GENERAL_NAMES* san_names = GENERAL_NAMES* san_names =
(GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr); (GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr);
if (san_names) { if (san_names) {
int san_count = sk_GENERAL_NAME_num(san_names); int san_count = sk_GENERAL_NAME_num(san_names);
for (int i = 0; i < san_count; i++) { for (int i = 0; i < san_count; i++) {
const GENERAL_NAME* current = sk_GENERAL_NAME_value(san_names, i); const GENERAL_NAME* current = sk_GENERAL_NAME_value(san_names, i);
if (current->type == GEN_DNS) { if (current->type == GEN_DNS) {
const char* dns_name = (const char*)ASN1_STRING_get0_data(current->d.dNSName); const char* dns_name = (const char*)ASN1_STRING_get0_data(current->d.dNSName);
// Safety: ensure no embedded nulls // Safety: ensure no embedded nulls
if (ASN1_STRING_length(current->d.dNSName) == (int)std::strlen(dns_name)) { if (ASN1_STRING_length(current->d.dNSName) == (int)std::strlen(dns_name)) {
names.emplace_back(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);
}
} }
} }
} }
GENERAL_NAMES_free(san_names);
X509_free(cert);
return 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: private:
std::array<uint8_t, crypto_sign_PUBLICKEYBYTES> mPublicKey; std::array<uint8_t, crypto_sign_PUBLICKEYBYTES> mPublicKey;

View File

@@ -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 <cstdint>
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)
}

View File

@@ -14,13 +14,16 @@
namespace ColumnLynx::Net { namespace ColumnLynx::Net {
struct SessionState { struct SessionState {
SymmetricKey aesKey; // Immutable after creation SymmetricKey aesKey; // Agreed-upon AES-256 kes for that session; Immutable after creation
std::atomic<uint64_t> send_ctr{0}; // Per-direction counters std::atomic<uint64_t> send_ctr{0}; // Per-direction counters
std::atomic<uint64_t> recv_ctr{0}; std::atomic<uint64_t> recv_ctr{0}; // Per-direction counters
asio::ip::udp::endpoint udpEndpoint; asio::ip::udp::endpoint udpEndpoint; // Deducted IP + Port of that session client
std::atomic<uint64_t> sendCounter{0}; std::atomic<uint64_t> sendCounter{0}; // Counter of sent messages
std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); // Time created
std::chrono::steady_clock::time_point expires{}; 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
Nonce base_nonce{}; Nonce base_nonce{};
~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); } ~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); }
@@ -29,10 +32,11 @@ namespace ColumnLynx::Net {
SessionState(SessionState&&) = default; SessionState(SessionState&&) = default;
SessionState& operator=(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; expires = created + ttl;
} }
// Set the UDP endpoint
void setUDPEndpoint(const asio::ip::udp::endpoint& ep) { void setUDPEndpoint(const asio::ip::udp::endpoint& ep) {
udpEndpoint = ep; udpEndpoint = ep;
} }
@@ -40,21 +44,31 @@ namespace ColumnLynx::Net {
class SessionRegistry { class SessionRegistry {
public: public:
// Return a reference to the Session Registry instance
static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; } static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; }
// Insert or replace // Insert or replace a session entry
void put(uint64_t sessionID, std::shared_ptr<SessionState> state) { void put(uint64_t sessionID, std::shared_ptr<SessionState> state) {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
mSessions[sessionID] = std::move(state); mSessions[sessionID] = std::move(state);
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
} }
// Lookup // Lookup a session entry by session ID
std::shared_ptr<const SessionState> get(uint64_t sessionID) const { std::shared_ptr<const SessionState> get(uint64_t sessionID) const {
std::shared_lock lock(mMutex); std::shared_lock lock(mMutex);
auto it = mSessions.find(sessionID); auto it = mSessions.find(sessionID);
return (it == mSessions.end()) ? nullptr : it->second; 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>> snapshot() const {
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap; std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex); std::shared_lock lock(mMutex);
@@ -62,7 +76,7 @@ namespace ColumnLynx::Net {
return snap; return snap;
} }
// Remove // Remove a session by ID
void erase(uint64_t sessionID) { void erase(uint64_t sessionID) {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
mSessions.erase(sessionID); mSessions.erase(sessionID);
@@ -79,10 +93,56 @@ namespace ColumnLynx::Net {
++it; ++it;
} }
} }
for (auto it = mIPSessions.begin(); it != mIPSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mIPSessions.erase(it);
} else {
++it;
}
}
}
// Get the number of registered sessions
int size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
}
// 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
// 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;
}
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);
} }
private: private:
mutable std::shared_mutex mMutex; mutable std::shared_mutex mMutex;
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> mSessions; std::unordered_map<uint64_t, std::shared_ptr<SessionState>> mSessions;
std::unordered_map<uint64_t, uint32_t> mSessionIPs;
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mIPSessions;
}; };
} }

View File

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

View File

@@ -17,6 +17,7 @@
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <linux/if.h> #include <linux/if.h>
#include <linux/if_tun.h> #include <linux/if_tun.h>
#include <arpa/inet.h>
#elif defined(__APPLE__) #elif defined(__APPLE__)
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/kern_control.h> #include <sys/kern_control.h>
@@ -24,8 +25,11 @@
#include <net/if_utun.h> #include <net/if_utun.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#include <arpa/inet.h>
#elif defined(_WIN32) #elif defined(_WIN32)
#include <windows.h> #include <windows.h>
#include <ws2tcpip.h>
#include <winsock2.h>
#include <wintun/wintun.h> #include <wintun/wintun.h>
#pragma comment(lib, "advapi32.lib") #pragma comment(lib, "advapi32.lib")
#endif #endif
@@ -36,12 +40,37 @@ namespace ColumnLynx::Net {
explicit VirtualInterface(const std::string& ifName); explicit VirtualInterface(const std::string& ifName);
~VirtualInterface(); ~VirtualInterface();
bool configureIP(uint32_t clientIP, uint32_t serverIP,
uint8_t prefixLen, uint16_t mtu);
std::vector<uint8_t> readPacket(); std::vector<uint8_t> readPacket();
void writePacket(const std::vector<uint8_t>& packet); void writePacket(const std::vector<uint8_t>& packet);
const std::string& getName() const; 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 ipv4ToString(uint32_t ip) {
struct in_addr addr;
addr.s_addr = htonl(ip);
char buf[INET_ADDRSTRLEN];
if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf)))
return "0.0.0.0";
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: 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; std::string mIfName;
int mFd; // POSIX int mFd; // POSIX
#if defined(_WIN32) #if defined(_WIN32)

View File

@@ -184,7 +184,7 @@ namespace ColumnLynx::Utils {
out << "----------------------\n"; out << "----------------------\n";
} }
//Panic the main thread and instantly halt execution. This produces a stack trace dump. Do not use by itself, throw an error instead. // Panic the main thread and instantly halt execution. This produces a stack trace dump. Do not use by itself, throw an error instead.
static void panic(const std::string& reason) { static void panic(const std::string& reason) {
std::cerr << "\n***\033[31m MAIN THREAD PANIC! \033[0m***\n"; std::cerr << "\n***\033[31m MAIN THREAD PANIC! \033[0m***\n";
std::cerr << "Reason: " << reason << "\n"; std::cerr << "Reason: " << reason << "\n";
@@ -204,6 +204,7 @@ namespace ColumnLynx::Utils {
std::cerr << "Panic trace written to panic_dump.txt\n"; std::cerr << "Panic trace written to panic_dump.txt\n";
} }
// Gets the current time
static std::string currentTime() { static std::string currentTime() {
std::time_t t = std::time(nullptr); std::time_t t = std::time(nullptr);
char buf[64]; char buf[64];

View File

@@ -7,6 +7,12 @@
#include <string> #include <string>
#include <cstdint> #include <cstdint>
#include <array> #include <array>
#include <iomanip>
#include <sstream>
#include <vector>
#include <fstream>
#include <chrono>
#include <unordered_map>
#ifdef _WIN32 #ifdef _WIN32
#include <winsock2.h> #include <winsock2.h>
@@ -17,17 +23,27 @@
#endif #endif
namespace ColumnLynx::Utils { namespace ColumnLynx::Utils {
// General log function. Use for logging important information.
void log(const std::string &msg); void log(const std::string &msg);
// General warning function. Use for logging important warnings.
void warn(const std::string &msg); void warn(const std::string &msg);
// General error function. Use for logging failures and general errors.
void error(const std::string &msg); 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(); std::string getHostname();
// Returns the version of the running release.
std::string getVersion(); std::string getVersion();
unsigned short serverPort(); unsigned short serverPort();
unsigned char protocolVersion(); unsigned char protocolVersion();
std::vector<std::string> getWhitelistedKeys();
// Raw byte to hex string conversion helper // Raw byte to hex string conversion helper
std::string bytesToHexString(const uint8_t* bytes, size_t length); 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 // uint8_t to raw string conversion helper
template <size_t N> template <size_t N>
@@ -38,4 +54,28 @@ namespace ColumnLynx::Utils {
inline std::string uint8ArrayToString(const uint8_t* data, size_t length) { inline std::string uint8ArrayToString(const uint8_t* data, size_t length) {
return std::string(reinterpret_cast<const char*>(data), 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

@@ -16,6 +16,7 @@
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/session_registry.hpp> #include <columnlynx/common/net/session_registry.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
namespace ColumnLynx::Net::TCP { namespace ColumnLynx::Net::TCP {
class TCPConnection : public std::enable_shared_from_this<TCPConnection> { class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
@@ -32,12 +33,18 @@ namespace ColumnLynx::Net::TCP {
return conn; return conn;
} }
// Start a TCP Connection (Handler for an incoming connection)
void start(); void start();
// Send a message to the TCP client
void sendMessage(ServerMessageType type, const std::string& data = ""); void sendMessage(ServerMessageType type, const std::string& data = "");
// Set callback for disconnects
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb); void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb);
// Disconnect the client
void disconnect(); void disconnect();
// Get the assigned session ID
uint64_t getSessionID() const; 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; std::array<uint8_t, 32> getAESKey() const;
private: private:
@@ -50,7 +57,9 @@ namespace ColumnLynx::Net::TCP {
mLastHeartbeatSent(std::chrono::steady_clock::now()) mLastHeartbeatSent(std::chrono::steady_clock::now())
{} {}
// Start the heartbeat routine
void mStartHeartbeat(); void mStartHeartbeat();
// Handle an incoming TCP message
void mHandleMessage(ClientMessageType type, const std::string& data); void mHandleMessage(ClientMessageType type, const std::string& data);
std::shared_ptr<MessageHandler> mHandler; std::shared_ptr<MessageHandler> mHandler;

View File

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

View File

@@ -8,12 +8,13 @@
#include <columnlynx/common/net/udp/udp_message_type.hpp> #include <columnlynx/common/net/udp/udp_message_type.hpp>
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <array> #include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net::UDP { namespace ColumnLynx::Net::UDP {
class UDPServer { class UDPServer {
public: public:
UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false) UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false, std::shared_ptr<VirtualInterface> tun = nullptr)
: mSocket(ioContext), mHostRunning(hostRunning) : mSocket(ioContext), mHostRunning(hostRunning), mTun(tun)
{ {
asio::error_code ec; asio::error_code ec;
@@ -41,15 +42,22 @@ namespace ColumnLynx::Net::UDP {
mStartReceive(); mStartReceive();
} }
// Stop the UDP server
void stop(); void stop();
// Send UDP data to an endpoint; Fetched via the Session Registry
void sendData(const uint64_t sessionID, const std::string& data);
private: private:
// Start receiving UDP data
void mStartReceive(); void mStartReceive();
// Handle an incoming UDP packet
void mHandlePacket(std::size_t bytes); void mHandlePacket(std::size_t bytes);
void mSendData(const uint64_t sessionID, const std::string& data);
asio::ip::udp::socket mSocket; asio::ip::udp::socket mSocket;
asio::ip::udp::endpoint mRemoteEndpoint; asio::ip::udp::endpoint mRemoteEndpoint;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
bool* mHostRunning; bool* mHostRunning;
std::shared_ptr<VirtualInterface> mTun;
}; };
} }

View File

@@ -15,12 +15,13 @@
using asio::ip::tcp; using asio::ip::tcp;
using namespace ColumnLynx::Utils; using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net; using namespace ColumnLynx::Net;
using namespace ColumnLynx;
volatile sig_atomic_t done = 0; volatile sig_atomic_t done = 0;
void signalHandler(int signum) { void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) { if (signum == SIGINT || signum == SIGTERM) {
log("Received termination signal. Shutting down client."); //log("Received termination signal. Shutting down client.");
done = 1; done = 1;
} }
} }
@@ -48,6 +49,9 @@ int main(int argc, char** argv) {
auto result = options.parse(argc, argv); auto result = options.parse(argc, argv);
if (result.count("help")) { if (result.count("help")) {
std::cout << options.help() << std::endl; 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; return 0;
} }
@@ -62,17 +66,19 @@ int main(int argc, char** argv) {
WintunInitialize(); WintunInitialize();
#endif #endif
VirtualInterface tun("columnlynxtun0"); std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>("utun1");
log("Using virtual interface: " + tun.getName()); log("Using virtual interface: " + tun->getName());
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); 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 std::array<uint8_t, 32> aesKey = {0}; // Defualt zeroed state until modified by handshake
uint64_t sessionID = 0; uint64_t sessionID = 0;
asio::io_context io; asio::io_context io;
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode); auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode, tun);
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, &aesKey, &sessionID); auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, &aesKey, &sessionID, tun);
client->start(); client->start();
udpClient->start(); udpClient->start();
@@ -81,32 +87,33 @@ int main(int argc, char** argv) {
std::thread ioThread([&io]() { std::thread ioThread([&io]() {
io.run(); io.run();
}); });
ioThread.detach(); //ioThread.join();
log("Client connected to " + host + ":" + port); log("Client connected to " + host + ":" + port);
// Client is running // 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 auto packet = tun->readPacket();
if (!client->isConnected() || done) {
if (client->isHandshakeComplete()) { break; // Bail out if connection died or signal set while blocked
// 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<std::chrono::seconds>(now - lastSendTime).count() >= 5) {
udpClient->sendMessage("Hello from UDP client!");
lastSendTime = now;
}
} }
if (packet.empty()) {
continue;
}
udpClient->sendMessage(std::string(packet.begin(), packet.end()));
} }
log("Client shutting down."); log("Client shutting down.");
udpClient->stop(); udpClient->stop();
client->disconnect(); client->disconnect();
io.stop(); io.stop();
ioThread.join();
if (ioThread.joinable())
ioThread.join();
} catch (const std::exception& e) { } catch (const std::exception& e) {
error("Client error: " + std::string(e.what())); error("Client error: " + std::string(e.what()));
} }
} }

View File

@@ -28,14 +28,18 @@ namespace ColumnLynx::Net::TCP {
// Check if hostname or IPv4/IPv6 // Check if hostname or IPv4/IPv6
sockaddr_in addr4{}; sockaddr_in addr4{};
sockaddr_in6 addr6{}; 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; 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
std::vector<uint8_t> payload; std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES); payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
payload.push_back(Utils::protocolVersion()); payload.push_back(Utils::protocolVersion());
payload.insert(payload.end(), /*payload.insert(payload.end(),
mLibSodiumWrapper->getXPublicKey(), mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES 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())); mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size()));
@@ -239,19 +243,32 @@ namespace ColumnLynx::Net::TCP {
mConnectionAESKey, symNonce mConnectionAESKey, symNonce
); );
if (decrypted.size() != sizeof(mConnectionSessionID)) { if (decrypted.size() != sizeof(mConnectionSessionID) + sizeof(Protocol::TunConfig)) {
Utils::error("Decrypted session ID has invalid size. Terminating connection."); Utils::error("Decrypted config has invalid size. Terminating connection.");
disconnect(); disconnect();
return; return;
} }
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID)); 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)); Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID));
if (mSessionIDRef) { // Copy to the global reference if (mSessionIDRef) { // Copy to the global reference
*mSessionIDRef = mConnectionSessionID; *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; mHandshakeComplete = true;
} }

View File

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

View File

@@ -6,15 +6,27 @@
namespace ColumnLynx::Utils { namespace ColumnLynx::Utils {
void log(const std::string &msg) { void log(const std::string &msg) {
std::cout << "[LOG] " << msg << std::endl; 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;
} }
void warn(const std::string &msg) { void warn(const std::string &msg) {
std::cerr << "[WARN] " << msg << std::endl; 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;
} }
void error(const std::string &msg) { void error(const std::string &msg) {
std::cerr << "[ERROR] " << msg << std::endl; 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::string getHostname() { std::string getHostname() {
@@ -37,7 +49,7 @@ namespace ColumnLynx::Utils {
} }
std::string getVersion() { std::string getVersion() {
return "a0.3"; return "a0.5";
} }
unsigned short serverPort() { unsigned short serverPort() {
@@ -61,4 +73,78 @@ namespace ColumnLynx::Utils {
return hexString; 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,6 +4,8 @@
#include <columnlynx/common/net/virtual_interface.hpp> #include <columnlynx/common/net/virtual_interface.hpp>
// This is all fucking voodoo dark magic.
namespace ColumnLynx::Net { namespace ColumnLynx::Net {
// ------------------------------ Constructor ------------------------------ // ------------------------------ Constructor ------------------------------
VirtualInterface::VirtualInterface(const std::string& ifName) VirtualInterface::VirtualInterface(const std::string& ifName)
@@ -14,37 +16,40 @@ namespace ColumnLynx::Net {
mFd = open("/dev/net/tun", O_RDWR); mFd = open("/dev/net/tun", O_RDWR);
if (mFd < 0) if (mFd < 0)
throw std::runtime_error("Failed to open /dev/net/tun: " + std::string(strerror(errno))); throw std::runtime_error("Failed to open /dev/net/tun: " + std::string(strerror(errno)));
struct ifreq ifr {}; struct ifreq ifr {};
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
std::strncpy(ifr.ifr_name, ifName.c_str(), IFNAMSIZ); std::strncpy(ifr.ifr_name, ifName.c_str(), IFNAMSIZ);
if (ioctl(mFd, TUNSETIFF, &ifr) < 0) { if (ioctl(mFd, TUNSETIFF, &ifr) < 0) {
close(mFd); close(mFd);
throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno))); throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno)));
} }
#elif defined(__APPLE__) #elif defined(__APPLE__)
// ---- macOS: UTUN (system control socket) ---- // ---- macOS: UTUN (system control socket) ----
mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
if (mFd < 0) if (mFd < 0)
throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno))); throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno)));
struct ctl_info ctlInfo {}; struct ctl_info ctlInfo {};
std::strncpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(ctlInfo.ctl_name)); std::strncpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(ctlInfo.ctl_name));
if (ioctl(mFd, CTLIOCGINFO, &ctlInfo) == -1) if (ioctl(mFd, CTLIOCGINFO, &ctlInfo) == -1)
throw std::runtime_error("ioctl(CTLIOCGINFO) failed: " + std::string(strerror(errno))); throw std::runtime_error("ioctl(CTLIOCGINFO) failed: " + std::string(strerror(errno)));
struct sockaddr_ctl sc {}; struct sockaddr_ctl sc {};
sc.sc_len = sizeof(sc); sc.sc_len = sizeof(sc);
sc.sc_family = AF_SYSTEM; sc.sc_family = AF_SYSTEM;
sc.ss_sysaddr = AF_SYS_CONTROL; sc.ss_sysaddr = AF_SYS_CONTROL;
sc.sc_id = ctlInfo.ctl_id; sc.sc_id = ctlInfo.ctl_id;
sc.sc_unit = 0; // utun0 (0 = auto-assign) 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))); throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno)));
}
// Retrieve actual utun device name // Retrieve actual utun device name
struct sockaddr_storage addr; struct sockaddr_storage addr;
socklen_t addrlen = sizeof(addr); socklen_t addrlen = sizeof(addr);
@@ -54,28 +59,28 @@ namespace ColumnLynx::Net {
} else { } else {
mIfName = "utunX"; mIfName = "utunX";
} }
#elif defined(_WIN32) #elif defined(_WIN32)
// ---- Windows: Wintun (WireGuard virtual adapter) ---- // ---- Windows: Wintun (WireGuard virtual adapter) ----
WINTUN_ADAPTER_HANDLE adapter = WINTUN_ADAPTER_HANDLE adapter =
WintunOpenAdapter(L"ColumnLynx", std::wstring(ifName.begin(), ifName.end()).c_str()); WintunOpenAdapter(L"ColumnLynx", std::wstring(ifName.begin(), ifName.end()).c_str());
if (!adapter) if (!adapter)
throw std::runtime_error("Wintun adapter not found or not installed"); throw std::runtime_error("Wintun adapter not found or not installed");
WINTUN_SESSION_HANDLE session = WINTUN_SESSION_HANDLE session =
WintunStartSession(adapter, 0x200000); // ring buffer size WintunStartSession(adapter, 0x200000); // ring buffer size
if (!session) if (!session)
throw std::runtime_error("Failed to start Wintun session"); throw std::runtime_error("Failed to start Wintun session");
mHandle = WintunGetReadWaitEvent(session); mHandle = WintunGetReadWaitEvent(session);
mFd = -1; // not used on Windows mFd = -1; // not used on Windows
mIfName = ifName; mIfName = ifName;
#else #else
throw std::runtime_error("Unsupported platform"); throw std::runtime_error("Unsupported platform");
#endif #endif
} }
// ------------------------------ Destructor ------------------------------ // ------------------------------ Destructor ------------------------------
VirtualInterface::~VirtualInterface() { VirtualInterface::~VirtualInterface() {
#if defined(__linux__) || defined(__APPLE__) #if defined(__linux__) || defined(__APPLE__)
@@ -87,17 +92,21 @@ namespace ColumnLynx::Net {
// WintunEndSession(mSession); // WintunEndSession(mSession);
#endif #endif
} }
// ------------------------------ Read ------------------------------ // ------------------------------ Read ------------------------------
std::vector<uint8_t> VirtualInterface::readPacket() { std::vector<uint8_t> VirtualInterface::readPacket() {
#if defined(__linux__) || defined(__APPLE__) #if defined(__linux__) || defined(__APPLE__)
std::vector<uint8_t> buf(4096); std::vector<uint8_t> buf(4096);
ssize_t n = read(mFd, buf.data(), buf.size()); 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))); throw std::runtime_error("read() failed: " + std::string(strerror(errno)));
}
buf.resize(n); buf.resize(n);
return buf; return buf;
#elif defined(_WIN32) #elif defined(_WIN32)
WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr); WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr);
if (!packet) return {}; if (!packet) return {};
@@ -108,14 +117,14 @@ namespace ColumnLynx::Net {
return {}; return {};
#endif #endif
} }
// ------------------------------ Write ------------------------------ // ------------------------------ Write ------------------------------
void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) { void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) {
#if defined(__linux__) || defined(__APPLE__) #if defined(__linux__) || defined(__APPLE__)
ssize_t n = write(mFd, packet.data(), packet.size()); ssize_t n = write(mFd, packet.data(), packet.size());
if (n < 0) if (n < 0)
throw std::runtime_error("write() failed: " + std::string(strerror(errno))); throw std::runtime_error("write() failed: " + std::string(strerror(errno)));
#elif defined(_WIN32) #elif defined(_WIN32)
WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size()); WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size());
if (!tx) throw std::runtime_error("WintunAllocateSendPacket failed"); if (!tx) throw std::runtime_error("WintunAllocateSendPacket failed");
@@ -123,9 +132,96 @@ namespace ColumnLynx::Net {
WintunSendPacket(mSession, tx); WintunSendPacket(mSession, tx);
#endif #endif
} }
// ------------------------------ Accessors ------------------------------ // ------------------------------ Accessors ------------------------------
const std::string& VirtualInterface::getName() const { return mIfName; } const std::string& VirtualInterface::getName() const { return mIfName; }
int VirtualInterface::getFd() const { return mFd; } int VirtualInterface::getFd() const { return mFd; }
}
// ------------------------------------------------------------
// 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 = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(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 = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP);
// Set netmask (/24 CIDR temporarily with raw command, improve later)
snprintf(cmd, sizeof(cmd),
"ifconfig utun0 %s %s mtu %d netmask 255.255.255.0 up",
ipStr.c_str(), peerStr.c_str(), mtu);
system(cmd);
Utils::log("Executed command: " + std::string(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, ipv4ToString(clientIP).c_str());
strcpy(gw, ipv4ToString(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

View File

@@ -11,11 +11,14 @@
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <unordered_set> #include <unordered_set>
#include <cxxopts/cxxopts.hpp> #include <cxxopts/cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp; using asio::ip::tcp;
using namespace ColumnLynx::Utils; using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net::TCP; using namespace ColumnLynx::Net::TCP;
using namespace ColumnLynx::Net::UDP; using namespace ColumnLynx::Net::UDP;
using namespace ColumnLynx::Net;
using namespace ColumnLynx;
volatile sig_atomic_t done = 0; volatile sig_atomic_t done = 0;
@@ -38,7 +41,8 @@ int main(int argc, char** argv) {
options.add_options() options.add_options()
("h,help", "Print help") ("h,help", "Print help")
("4,ipv4-only", "Force IPv4 only operation", cxxopts::value<bool>()->default_value("false")); ("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"));
PanicHandler::init(); PanicHandler::init();
@@ -46,14 +50,25 @@ int main(int argc, char** argv) {
auto result = options.parse(argc, argv); auto result = options.parse(argc, argv);
if (result.count("help")) { if (result.count("help")) {
std::cout << options.help() << std::endl; 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; return 0;
} }
bool ipv4Only = result["ipv4-only"].as<bool>(); bool ipv4Only = result["ipv4-only"].as<bool>();
std::string configPath = result["config"].as<std::string>();
log("ColumnLynx Server, Version " + getVersion()); log("ColumnLynx Server, Version " + getVersion());
log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details."); log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details.");
#if defined(__WIN32__)
WintunInitialize();
#endif
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>("utun0");
log("Using virtual interface: " + tun->getName());
// Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory) // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory)
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES)); log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES));
@@ -64,7 +79,7 @@ int main(int argc, char** argv) {
asio::io_context io; asio::io_context io;
auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only); auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning, ipv4Only); auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning, ipv4Only, tun);
asio::signal_set signals(io, SIGINT, SIGTERM); asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](const std::error_code&, int) { signals.async_wait([&](const std::error_code&, int) {
@@ -87,7 +102,21 @@ int main(int argc, char** argv) {
log("Server started on port " + std::to_string(serverPort())); log("Server started on port " + std::to_string(serverPort()));
while (!done) { 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::ipv4ToString(dstIP));
continue;
}
udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end()));
} }
log("Shutting down server..."); log("Shutting down server...");

View File

@@ -41,6 +41,9 @@ namespace ColumnLynx::Net::TCP {
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec); mHandler->socket().close(ec);
SessionRegistry::getInstance().erase(mConnectionSessionID);
SessionRegistry::getInstance().deallocIP(mConnectionSessionID);
Utils::log("Closed connection to " + ip); Utils::log("Closed connection to " + ip);
if (mOnDisconnect) { if (mOnDisconnect) {
@@ -111,7 +114,26 @@ namespace ColumnLynx::Net::TCP {
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + "."); 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) 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");
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(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist
break; break;
} }
@@ -170,20 +192,44 @@ namespace ColumnLynx::Net::TCP {
// Make a Session ID // Make a Session ID
randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID)); 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) // Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose)
Nonce symNonce{}; // All zeros Nonce symNonce{}; // All zeros
std::vector<uint8_t> encryptedSessionID = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<uint8_t*>(&mConnectionSessionID), sizeof(mConnectionSessionID), 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;
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);
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() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig));
std::vector<uint8_t> encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
payload.data(), payload.size(),
mConnectionAESKey, symNonce 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 // Add to session registry
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned."); Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned (" + std::to_string(mConnectionSessionID) + ").");
auto session = std::make_shared<SessionState>(mConnectionAESKey, std::chrono::hours(12)); auto session = std::make_shared<SessionState>(mConnectionAESKey, std::chrono::hours(12), clientIP, htonl(0x0A0A0001), mConnectionSessionID);
SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session)); SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session));
} catch (const std::exception& e) { } catch (const std::exception& e) {

View File

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