Compare commits

23 Commits

Author SHA1 Message Date
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
DcruBro
aebca5cd7e update readme and attributions 2025-11-12 19:20:44 +01:00
6fceb84930 Added basic virtual interface implementation, needs testing. Added Wintun licenses. 2025-11-12 19:14:44 +01:00
DcruBro
5a72895f8d added includes to build on linux 2025-11-12 18:49:53 +01:00
6cd4e01066 cleanup 2025-11-12 16:09:44 +01:00
e695008e10 Added IPv6 support, added option to disable IPv6 (IPv4-Only mode) 2025-11-12 09:07:22 +01:00
fbafeafc9f Added a hostname check if connecting via domain hostname (Not IPv4/IPv6 - IPv6 still not supported / tested). 2025-11-11 17:35:53 +01:00
eda3cf87d1 misc. 2025-11-11 15:26:05 +01:00
705962e5ce Added partial verification of server public key on client side - needs hostname verification. Added startup flag to ignore verification fail. 2025-11-11 13:19:59 +01:00
DcruBro
fd95816721 Update README.md 2025-11-11 00:06:09 +01:00
DcruBro
4b4451d1a9 Refactoring: Moved some code from headers to dedicated source files 2025-11-10 23:19:39 +01:00
DcruBro
9252425bdf Fix licensing printout on start 2025-11-10 21:00:37 +01:00
a92b9de15d Add legal clarification 2025-11-10 16:57:57 +01:00
25 changed files with 2023 additions and 612 deletions

View File

@@ -15,3 +15,11 @@ This project uses the standalone version of the ASIO C++ library for asynchronou
- **Copyright:** (c) 2014-2025 Christopher M. Kohlhoff
- **License:** MIT License
- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt`
## Wintun C++ Library
- **Name:** wintun
- **Website:** https://www.wintun.net/
- **Copyright:** (c) 2018-2025 WireGuard LLC
- **License:** MIT License OR GPL-2.0 License
- **License Text:** See `third_party/wintun/`
- **Utilized Under:** MIT License

View File

@@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx
VERSION 0.0.1
VERSION 0.0.4
LANGUAGES CXX
)
@@ -25,7 +25,7 @@ include(FetchContent)
if(APPLE)
# Build universal (arm64 + x86_64), or limit to one arch if you prefer
# e.g., set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "" FORCE)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures" FORCE)
set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "Build architectures" FORCE)
endif()
# ---------------------------------------------------------
@@ -58,6 +58,15 @@ set(SODIUM_CMAKE_ARGS
FetchContent_MakeAvailable(Sodium)
# OpenSSL
find_package(OpenSSL REQUIRED)
if(OPENSSL_FOUND)
message(STATUS "Found OpenSSL version ${OPENSSL_VERSION}")
include_directories(${OPENSSL_INCLUDE_DIR})
else()
message(FATAL_ERROR "OpenSSL not found")
endif()
# ---------------------------------------------------------
# Output directories
# ---------------------------------------------------------
@@ -77,7 +86,7 @@ endforeach()
# ---------------------------------------------------------
file(GLOB_RECURSE COMMON_SRC CONFIGURE_DEPENDS src/common/*.cpp)
add_library(common STATIC ${COMMON_SRC})
target_link_libraries(common PUBLIC sodium)
target_link_libraries(common PUBLIC sodium OpenSSL::SSL OpenSSL::Crypto)
target_include_directories(common PUBLIC
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include
@@ -90,7 +99,7 @@ target_compile_definitions(common PUBLIC ASIO_STANDALONE)
# ---------------------------------------------------------
file(GLOB_RECURSE CLIENT_SRC CONFIGURE_DEPENDS src/client/*.cpp)
add_executable(client ${CLIENT_SRC})
target_link_libraries(client PRIVATE common sodium)
target_link_libraries(client PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto)
target_include_directories(client PRIVATE
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include
@@ -104,7 +113,7 @@ set_target_properties(client PROPERTIES OUTPUT_NAME "columnlynx_client")
# ---------------------------------------------------------
file(GLOB_RECURSE SERVER_SRC CONFIGURE_DEPENDS src/server/*.cpp)
add_executable(server ${SERVER_SRC})
target_link_libraries(server PRIVATE common sodium)
target_link_libraries(server PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto)
target_include_directories(server PRIVATE
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include

View File

@@ -1,6 +1,20 @@
# ColumnLynx
ColumnLynx is a VPN protocol designed to be as lightweight as possible.
## What is it?
ColumnLynx is a VPN protocol designed to be as lightweight and simple to understand as possible.
### Origin
The original goal of this project was for me to learn about the inner-workings of VPN protocols, but overtime, it has transformed into the goal seen above.
### Design Philosophy
A VPN (Virtual Private Network), in the most basic terms, is a protocol that tunnels network traffic from a client to a server over an encrypted tunnel and having the server send that traffic on its behalf. It can be catagorized into sitting somewhere in-between the 3rd and 4th layers of the ISO/OSI model.
This project aims to be just that, an encrypted tunneling protocol that works on the 3rd and 4th layers of the ISO/OSI model, nothing more, nothing less. We leave complex functions like compression, to the higher layers (though it could be argued that making an encrypted tunnel already pushes us up to Layer 6).
This simplicity-focused design approach allows us to make an efficient, low-overhead VPN protocol and minimize any potential attack surface.
## How does it work
@@ -94,9 +108,27 @@ The **Data** is generally just the **raw underlying packet** forwarded to the se
| uint64_t | 8 bytes | **Header** - Session ID | The unique and random session identifier for the client |
| uint8_t | variable | Data | General data / payload |
## Misc.
Building the binary for Windows requires the Wintun DLL. The include header is pre-packaged.
## Legal
Copyright (C) 2025 Jonas Korene Novak
### Copyright ownership:
Unless explicitly stated otherwise, all source code and material contained in this project
is the copyright of their respective authors, as identified in (but not limited to)
the project's version control history (e.g., Git commit authorship).
Each contribution is provided under the terms of the GNU General Public License,
version 2 or (at your option) any later version, as published by the Free Software Foundation,
unless an individual file or component specifies a different license.
No contributor or maintainer claims exclusive ownership of the entire project.
All rights are retained by their respective authors.
By submitting a contribution, you agree that it will be licensed under the
same dual GPL terms as the project as a whole.
### Licensing:
This project is **dual-licensed** under the GNU General Public License (GPL):
@@ -120,11 +152,12 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
DcruBro is the online pseudonym of Jonas Korene Novak. Both refer to the same individual and may be used interchangeably for copyright attribution purposes.
### Licensing
*See **ATTRIBUTIONS.md** for details.*
This project includes the [ASIO C++ Library](https://think-async.com/Asio/),
distributed under the [Boost Software License, Version 1.0](https://www.boost.org/LICENSE_1_0.txt).
This project includes the CXXOPTS Library
distributed under the MIT License
This project includes the [Wintun Library](https://www.wintun.net/), distributed under the MIT License or the GPL-2.0 License. This project utilizes it under the MIT license.
*See **ATTRIBUTIONS.md** for details.*

View File

@@ -13,6 +13,8 @@
#include <array>
#include <algorithm>
#include <vector>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp;
@@ -24,7 +26,9 @@ namespace ColumnLynx::Net::TCP {
const std::string& port,
Utils::LibSodiumWrapper* sodiumWrapper,
std::array<uint8_t, 32>* aesKey,
uint64_t* sessionIDRef)
uint64_t* sessionIDRef,
bool* insecureMode,
std::shared_ptr<VirtualInterface> tun = nullptr)
:
mResolver(ioContext),
mSocket(ioContext),
@@ -33,242 +37,23 @@ namespace ColumnLynx::Net::TCP {
mLibSodiumWrapper(sodiumWrapper),
mGlobalKeyRef(aesKey),
mSessionIDRef(sessionIDRef),
mInsecureMode(insecureMode),
mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now())
mLastHeartbeatSent(std::chrono::steady_clock::now()),
mTun(tun)
{}
void start() {
auto self = shared_from_this();
mResolver.async_resolve(mHost, mPort,
[this, self](asio::error_code ec, tcp::resolver::results_type endpoints) {
if (!ec) {
asio::async_connect(mSocket, endpoints,
[this, self](asio::error_code ec, const tcp::endpoint&) {
if (!NetHelper::isExpectedDisconnect(ec)) {
mConnected = true;
Utils::log("Client connected.");
mHandler = std::make_shared<MessageHandler>(std::move(mSocket));
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
});
mHandler->start();
void start();
void sendMessage(ClientMessageType type, const std::string& data = "");
void disconnect(bool echo = true);
// Init connection handshake
Utils::log("Sending handshake init to server.");
std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
payload.push_back(Utils::protocolVersion());
payload.insert(payload.end(),
mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
);
mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size()));
mStartHeartbeat();
} else {
Utils::error("Client connect failed: " + ec.message());
}
});
} else {
Utils::error("Client resolve failed: " + ec.message());
}
});
}
void sendMessage(ClientMessageType type, const std::string& data = "") {
if (!mConnected) {
Utils::error("Cannot send message, client not connected.");
return;
}
if (mHandler) {
asio::post(mHandler->socket().get_executor(), [self = shared_from_this(), type, data]() {
self->mHandler->sendMessage(type, data);
});
}
}
void disconnect(bool echo = true) {
if (mConnected && mHandler) {
if (echo) {
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
}
asio::error_code ec;
mHeartbeatTimer.cancel();
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec) {
Utils::error("Error during socket shutdown: " + ec.message());
}
mHandler->socket().close(ec);
if (ec) {
Utils::error("Error during socket close: " + ec.message());
}
mConnected = false;
Utils::log("Client disconnected.");
}
}
bool isHandshakeComplete() const {
return mHandshakeComplete;
}
bool isConnected() const {
return mConnected;
}
bool isHandshakeComplete() const;
bool isConnected() const;
private:
void mStartHeartbeat() {
auto self = shared_from_this();
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - self->mLastHeartbeatReceived).count();
if (elapsed >= 15) { // 3 missed heartbeats
Utils::error("Missed 3 heartbeats. I think the other party might have died! Disconnecting.");
// Close sockets forcefully, server is dead
asio::error_code ec;
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec);
mConnected = false;
mGlobalKeyRef = nullptr;
if (mSessionIDRef) {
*mSessionIDRef = 0;
}
return;
}
self->sendMessage(ClientMessageType::HEARTBEAT);
Utils::log("Sent HEARTBEAT to server.");
self->mLastHeartbeatSent = std::chrono::steady_clock::now();
self->mStartHeartbeat(); // Recursive
});
}
void mHandleMessage(ServerMessageType type, const std::string& data) {
switch (type) {
case ServerMessageType::HANDSHAKE_IDENTIFY:
Utils::log("Received server identity: " + data);
std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey)));
// Generate and send challenge
{
Utils::log("Sending challenge to server.");
mSubmittedChallenge = Utils::LibSodiumWrapper::generateRandom256Bit(); // Temporarily store the challenge to verify later
mHandler->sendMessage(ClientMessageType::HANDSHAKE_CHALLENGE, Utils::uint8ArrayToString(mSubmittedChallenge));
}
break;
case ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE:
Utils::log("Received challenge response from server.");
{
// Verify the signature
Signature sig{};
std::memcpy(sig.data(), data.data(), std::min(data.size(), sig.size()));
if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) {
Utils::log("Challenge response verified successfully.");
// Convert the server's public key to Curve25519 for encryption
AsymPublicKey serverXPubKey{};
crypto_sign_ed25519_pk_to_curve25519(serverXPubKey.data(), mServerPublicKey);
// Generate AES key and send confirmation
mConnectionAESKey = Utils::LibSodiumWrapper::generateRandom256Bit();
if (mGlobalKeyRef) { // Copy to the global reference
std::copy(mConnectionAESKey.begin(), mConnectionAESKey.end(), mGlobalKeyRef->begin());
}
AsymNonce nonce{};
randombytes_buf(nonce.data(), nonce.size());
// TODO: This is pretty redundant, it should return the required type directly
std::array<uint8_t, 32> arrayPrivateKey;
std::copy(mLibSodiumWrapper->getXPrivateKey(),
mLibSodiumWrapper->getXPrivateKey() + 32,
arrayPrivateKey.begin());
std::vector<uint8_t> encr = Utils::LibSodiumWrapper::encryptAsymmetric(
mConnectionAESKey.data(), mConnectionAESKey.size(),
nonce,
serverXPubKey,
arrayPrivateKey
);
std::vector<uint8_t> payload;
payload.reserve(nonce.size() + encr.size());
payload.insert(payload.end(), nonce.begin(), nonce.end());
payload.insert(payload.end(), encr.begin(), encr.end());
mHandler->sendMessage(ClientMessageType::HANDSHAKE_EXCHANGE_KEY, Utils::uint8ArrayToString(payload.data(), payload.size()));
} else {
Utils::error("Challenge response verification failed. Terminating connection.");
disconnect();
}
}
break;
case ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM:
Utils::log("Received handshake exchange key confirmation from server.");
// Decrypt the session ID using the established AES key
{
Nonce symNonce{}; // All zeros
std::vector<uint8_t> ciphertext(data.begin(), data.end());
std::vector<uint8_t> decrypted = Utils::LibSodiumWrapper::decryptMessage(
ciphertext.data(), ciphertext.size(),
mConnectionAESKey, symNonce
);
if (decrypted.size() != sizeof(mConnectionSessionID)) {
Utils::error("Decrypted session ID has invalid size. Terminating connection.");
disconnect();
return;
}
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID));
Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID));
if (mSessionIDRef) { // Copy to the global reference
*mSessionIDRef = mConnectionSessionID;
}
mHandshakeComplete = true;
}
break;
case ServerMessageType::HEARTBEAT:
Utils::log("Received HEARTBEAT from server.");
mHandler->sendMessage(ClientMessageType::HEARTBEAT_ACK, ""); // Send ACK
break;
case ServerMessageType::HEARTBEAT_ACK:
Utils::log("Received HEARTBEAT_ACK from server.");
mLastHeartbeatReceived = std::chrono::steady_clock::now();
mMissedHeartbeats = 0; // Reset missed heartbeat count
break;
case ServerMessageType::GRACEFUL_DISCONNECT:
Utils::log("Server is disconnecting: " + data);
if (mConnected) { // Prevent Recursion
disconnect(false);
}
break;
default:
Utils::log("Received unknown message type from server.");
break;
}
}
void mStartHeartbeat();
void mHandleMessage(ServerMessageType type, const std::string& data);
bool mConnected = false;
bool mHandshakeComplete = false;
@@ -283,9 +68,13 @@ namespace ColumnLynx::Net::TCP {
SymmetricKey mConnectionAESKey;
std::array<uint8_t, 32>* mGlobalKeyRef; // Reference to global AES key
uint64_t* mSessionIDRef; // Reference to global Session ID
bool* mInsecureMode; // Reference to insecure mode flag
asio::steady_timer mHeartbeatTimer;
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
std::chrono::steady_clock::time_point mLastHeartbeatSent;
int mMissedHeartbeats = 0;
bool mIsHostDomain;
Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr;
};
}

View File

@@ -9,6 +9,7 @@
#include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net::UDP {
class UDPClient {
@@ -17,113 +18,20 @@ namespace ColumnLynx::Net::UDP {
const std::string& host,
const std::string& port,
std::array<uint8_t, 32>* aesKeyRef,
uint64_t* sessionIDRef)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef) { mStartReceive(); }
void start() {
auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort);
mRemoteEndpoint = *endpoints.begin();
mSocket.open(asio::ip::udp::v4());
Utils::log("UDP Client ready to send to " + mRemoteEndpoint.address().to_string() + ":" + std::to_string(mRemoteEndpoint.port()));
uint64_t* sessionIDRef,
std::shared_ptr<VirtualInterface> tunRef = nullptr)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef)
{
mStartReceive();
}
void sendMessage(const std::string& data = "") {
UDPPacketHeader hdr{};
randombytes_buf(hdr.nonce.data(), hdr.nonce.size());
if (mAesKeyRef == nullptr || mSessionIDRef == nullptr) {
Utils::error("UDP Client AES key or Session ID reference is null!");
return;
}
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
*mAesKeyRef, hdr.nonce, "udp-data"
);
std::vector<uint8_t> packet;
packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size());
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&hdr),
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
);
uint64_t sid = *mSessionIDRef;
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&sid),
reinterpret_cast<uint8_t*>(&sid) + sizeof(sid)
);
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
mSocket.send_to(asio::buffer(packet), mRemoteEndpoint);
Utils::log("Sent UDP packet of size " + std::to_string(packet.size()));
}
void stop() {
if (mSocket.is_open()) {
asio::error_code ec;
mSocket.cancel(ec);
mSocket.close(ec);
Utils::log("UDP Client socket closed.");
}
}
void start();
void sendMessage(const std::string& data = "");
void stop();
private:
void mStartReceive() {
mSocket.async_receive_from(
asio::buffer(mRecvBuffer), mRemoteEndpoint,
[this](asio::error_code ec, std::size_t bytes) {
if (ec) {
if (ec == asio::error::operation_aborted) return; // Socket closed
// Other recv error
mStartReceive();
return;
}
if (bytes > 0) {
mHandlePacket(bytes);
}
mStartReceive();
}
);
}
void mHandlePacket(std::size_t bytes) {
if (bytes < sizeof(UDPPacketHeader) + sizeof(uint64_t)) {
Utils::warn("UDP Client received packet too small to process.");
return;
}
// Parse header
UDPPacketHeader hdr;
std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader));
// Parse session ID
uint64_t sessionID;
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
// Decrypt payload
std::vector<uint8_t> ciphertext(
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t),
mRecvBuffer.begin() + bytes
);
if (mAesKeyRef == nullptr) {
Utils::error("UDP Client AES key reference is null!");
return;
}
std::vector<uint8_t> plaintext = Utils::LibSodiumWrapper::decryptMessage(
ciphertext.data(), ciphertext.size(), *mAesKeyRef, hdr.nonce, "udp-data"
);
if (plaintext.empty()) {
Utils::warn("UDP Client failed to decrypt received packet.");
return;
}
Utils::log("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
}
void mStartReceive();
void mHandlePacket(std::size_t bytes);
asio::ip::udp::socket mSocket;
asio::ip::udp::resolver mResolver;
@@ -132,6 +40,7 @@ namespace ColumnLynx::Net::UDP {
std::string mPort;
std::array<uint8_t, 32>* mAesKeyRef;
uint64_t* mSessionIDRef;
std::shared_ptr<VirtualInterface> mTunRef = nullptr;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
};
}

View File

@@ -11,6 +11,12 @@
#include <columnlynx/common/utils.hpp>
#include <array>
#include <vector>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <memory>
#include <cstring>
namespace ColumnLynx {
using PublicKey = std::array<uint8_t, crypto_sign_PUBLICKEYBYTES>; // Ed25519
@@ -178,6 +184,98 @@ namespace ColumnLynx::Utils {
return plaintext;
}
static inline bool verifyCertificateWithSystemCAs(const std::vector<uint8_t>& cert_der) {
// Parse DER-encoded certificate
const unsigned char* p = cert_der.data();
std::unique_ptr<X509, decltype(&X509_free)> cert(
d2i_X509(nullptr, &p, cert_der.size()), X509_free
);
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;
}
static inline std::vector<std::string> getCertificateHostname(const std::vector<uint8_t>& cert_der) {
std::vector<std::string> names;
if (cert_der.empty())
return names;
// Parse DER certificate
const unsigned char* p = cert_der.data();
X509* cert = d2i_X509(nullptr, &p, cert_der.size());
if (!cert)
return names;
// --- Subject Alternative Names (SAN) ---
GENERAL_NAMES* san_names =
(GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr);
if (san_names) {
int san_count = sk_GENERAL_NAME_num(san_names);
for (int i = 0; i < san_count; i++) {
const GENERAL_NAME* current = sk_GENERAL_NAME_value(san_names, i);
if (current->type == GEN_DNS) {
const char* dns_name = (const char*)ASN1_STRING_get0_data(current->d.dNSName);
// Safety: ensure no embedded nulls
if (ASN1_STRING_length(current->d.dNSName) == (int)std::strlen(dns_name)) {
names.emplace_back(dns_name);
}
}
}
GENERAL_NAMES_free(san_names);
}
// --- Fallback: Common Name (CN) ---
if (names.empty()) {
X509_NAME* subject = X509_get_subject_name(cert);
if (subject) {
int idx = X509_NAME_get_index_by_NID(subject, NID_commonName, -1);
if (idx >= 0) {
X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, idx);
ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(entry);
const char* cn_str = (const char*)ASN1_STRING_get0_data(cn_asn1);
if (ASN1_STRING_length(cn_asn1) == (int)std::strlen(cn_str)) {
names.emplace_back(cn_str);
}
}
}
}
X509_free(cert);
return names;
}
private:
std::array<uint8_t, crypto_sign_PUBLICKEYBYTES> mPublicKey;
std::array<uint8_t, crypto_sign_SECRETKEYBYTES> mPrivateKey;

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

@@ -21,6 +21,9 @@ namespace ColumnLynx::Net {
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()); }
@@ -29,7 +32,7 @@ namespace ColumnLynx::Net {
SessionState(SessionState&&) = default;
SessionState& operator=(SessionState&&) = default;
explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24)) : aesKey(k) {
explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), uint32_t clientIP = 0, uint32_t serverIP = 0, uint64_t id = 0) : aesKey(k), clientTunIP(clientIP), serverTunIP(serverIP), sessionID(id) {
expires = created + ttl;
}
@@ -46,6 +49,7 @@ namespace ColumnLynx::Net {
void put(uint64_t sessionID, std::shared_ptr<SessionState> state) {
std::unique_lock lock(mMutex);
mSessions[sessionID] = std::move(state);
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
}
// Lookup
@@ -55,6 +59,12 @@ namespace ColumnLynx::Net {
return (it == mSessions.end()) ? nullptr : it->second;
}
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const {
std::shared_lock lock(mMutex);
auto it = mIPSessions.find(ip);
return (it == mIPSessions.end()) ? nullptr : it->second;
}
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const {
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex);
@@ -79,10 +89,50 @@ namespace ColumnLynx::Net {
++it;
}
}
for (auto it = mIPSessions.begin(); it != mIPSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mIPSessions.erase(it);
} else {
++it;
}
}
}
int size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
}
// IP management (simple for /24 subnet)
uint32_t getFirstAvailableIP() const {
std::shared_lock lock(mMutex);
uint32_t baseIP = 0x0A0A0002; // 10.10.0.2
// TODO: Expand to support larger subnets
for (uint32_t offset = 0; offset < 254; offset++) {
uint32_t candidateIP = baseIP + offset;
if (mSessionIPs.find(candidateIP) == mSessionIPs.end()) {
return candidateIP;
}
}
}
void lockIP(uint64_t sessionID, uint32_t ip) {
std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip;
}
void deallocIP(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessionIPs.erase(sessionID);
}
private:
mutable std::shared_mutex mMutex;
std::unordered_map<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

@@ -0,0 +1,80 @@
// virtual_interface.hpp - Virtual Interface for Network Communication
// Copyright (C) 2025 DcruBro
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#pragma once
#include <stdexcept>
#include <cstring>
#include <cerrno>
#include <vector>
#include <iostream>
#include <columnlynx/common/utils.hpp>
#if defined(__linux__)
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <arpa/inet.h>
#elif defined(__APPLE__)
#include <sys/socket.h>
#include <sys/kern_control.h>
#include <sys/sys_domain.h>
#include <net/if_utun.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <arpa/inet.h>
#elif defined(_WIN32)
#include <windows.h>
#include <ws2tcpip.h>
#include <winsock2.h>
#include <wintun/wintun.h>
#pragma comment(lib, "advapi32.lib")
#endif
namespace ColumnLynx::Net {
class VirtualInterface {
public:
explicit VirtualInterface(const std::string& ifName);
~VirtualInterface();
bool configureIP(uint32_t clientIP, uint32_t serverIP,
uint8_t prefixLen, uint16_t mtu);
std::vector<uint8_t> readPacket();
void writePacket(const std::vector<uint8_t>& packet);
const std::string& getName() const;
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:
bool mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu);
bool mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu);
bool mApplyWindowsIP(uint32_t clientIP, uint32_t serverIP, uint8_t prefixLen, uint16_t mtu);
std::string mIfName;
int mFd; // POSIX
#if defined(_WIN32)
HANDLE mHandle; // Windows
#endif
};
}

View File

@@ -38,4 +38,23 @@ 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 bswap64(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);
}
inline constexpr uint64_t htobe64(uint64_t x) {
return bswap64(x); // host -> big-endian (for little-endian hosts)
}
inline constexpr uint64_t be64toh(uint64_t x) {
return bswap64(x); // big-endian -> host (for little-endian hosts)
}
};

View File

@@ -16,6 +16,7 @@
#include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/session_registry.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
namespace ColumnLynx::Net::TCP {
class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
@@ -32,56 +33,13 @@ namespace ColumnLynx::Net::TCP {
return conn;
}
void start() {
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
});
void start();
void sendMessage(ServerMessageType type, const std::string& data = "");
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb);
void disconnect();
mHandler->onDisconnect([this](const asio::error_code& ec) {
Utils::log("Client disconnected: " + mHandler->socket().remote_endpoint().address().to_string() + " - " + ec.message());
disconnect();
});
mHandler->start();
mStartHeartbeat();
// Placeholder for message handling setup
Utils::log("Client connected: " + mHandler->socket().remote_endpoint().address().to_string());
}
void sendMessage(ServerMessageType type, const std::string& data = "") {
if (mHandler) {
mHandler->sendMessage(type, data);
}
}
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb) {
mOnDisconnect = std::move(cb);
}
void disconnect() {
std::string ip = mHandler->socket().remote_endpoint().address().to_string();
mHandler->sendMessage(ServerMessageType::GRACEFUL_DISCONNECT, "Server initiated disconnect.");
mHeartbeatTimer.cancel();
asio::error_code ec;
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec);
Utils::log("Closed connection to " + ip);
if (mOnDisconnect) {
mOnDisconnect(shared_from_this());
}
}
uint64_t getSessionID() const {
return mConnectionSessionID;
}
std::array<uint8_t, 32> getAESKey() const {
return mConnectionAESKey;
}
uint64_t getSessionID() const;
std::array<uint8_t, 32> getAESKey() const;
private:
TCPConnection(asio::ip::tcp::socket socket, Utils::LibSodiumWrapper* sodiumWrapper)
@@ -93,164 +51,8 @@ namespace ColumnLynx::Net::TCP {
mLastHeartbeatSent(std::chrono::steady_clock::now())
{}
void mStartHeartbeat() {
auto self = shared_from_this();
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - self->mLastHeartbeatReceived).count();
if (elapsed >= 15) { // 3 missed heartbeats
Utils::error("Missed 3 heartbeats. I think the other party (client " + std::to_string(self->mConnectionSessionID) + ") might have died! Disconnecting.");
// Remove socket forcefully, client is dead
asio::error_code ec;
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec);
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
return;
}
self->sendMessage(ServerMessageType::HEARTBEAT);
Utils::log("Sent HEARTBEAT to client " + std::to_string(self->mConnectionSessionID));
self->mLastHeartbeatSent = now;
self->mStartHeartbeat(); // Recursive
});
}
void mHandleMessage(ClientMessageType type, const std::string& data) {
std::string reqAddr = mHandler->socket().remote_endpoint().address().to_string();
switch (type) {
case ClientMessageType::HANDSHAKE_INIT: {
Utils::log("Received HANDSHAKE_INIT from " + reqAddr);
if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) {
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short.");
disconnect();
return;
}
uint8_t clientProtoVer = static_cast<uint8_t>(data[0]);
if (clientProtoVer != Utils::protocolVersion()) {
Utils::warn("Client protocol version mismatch from " + reqAddr + ". Expected " +
std::to_string(Utils::protocolVersion()) + ", got " + std::to_string(clientProtoVer) + ".");
disconnect();
return;
}
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + ".");
std::memcpy(mConnectionPublicKey.data(), data.data() + 1, std::min(data.size() - 1, sizeof(mConnectionPublicKey))); // Store the client's public key (for identification)
mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist
break;
}
case ClientMessageType::HANDSHAKE_CHALLENGE: {
Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr);
// Convert to byte array
uint8_t challengeData[32];
std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData)));
// Sign the challenge
Signature sig = Utils::LibSodiumWrapper::signMessage(
challengeData, sizeof(challengeData),
mLibSodiumWrapper->getPrivateKey()
);
mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response
break;
}
case ClientMessageType::HANDSHAKE_EXCHANGE_KEY: {
Utils::log("Received HANDSHAKE_EXCHANGE_KEY from " + reqAddr);
// Extract encrypted AES key and nonce (nonce is the first 24 bytes, rest is the ciphertext)
if (data.size() < 24) { // Minimum size check (nonce)
Utils::warn("HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " is too short.");
disconnect();
return;
}
AsymNonce nonce{};
std::memcpy(nonce.data(), data.data(), nonce.size());
std::vector<uint8_t> ciphertext(data.size() - nonce.size());
std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size());
try {
std::array<uint8_t, 32> arrayPrivateKey;
std::copy(mLibSodiumWrapper->getXPrivateKey(),
mLibSodiumWrapper->getXPrivateKey() + 32,
arrayPrivateKey.begin());
// Decrypt the AES key using the client's public key and server's private key
std::vector<uint8_t> decrypted = Utils::LibSodiumWrapper::decryptAsymmetric(
ciphertext.data(), ciphertext.size(),
nonce,
mConnectionPublicKey,
arrayPrivateKey
);
if (decrypted.size() != 32) {
Utils::warn("Decrypted HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " has invalid size.");
disconnect();
return;
}
std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size());
// Make a Session ID
randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID));
// TODO: Make the session ID little-endian for network transmission
// Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose)
Nonce symNonce{}; // All zeros
std::vector<uint8_t> encryptedSessionID = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<uint8_t*>(&mConnectionSessionID), sizeof(mConnectionSessionID),
mConnectionAESKey, symNonce
);
mHandler->sendMessage(ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM, Utils::uint8ArrayToString(encryptedSessionID.data(), encryptedSessionID.size()));
// Add to session registry
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned.");
auto session = std::make_shared<SessionState>(mConnectionAESKey, std::chrono::hours(12));
SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session));
} catch (const std::exception& e) {
Utils::error("Failed to decrypt HANDSHAKE_EXCHANGE_KEY from " + reqAddr + ": " + e.what());
disconnect();
}
break;
}
case ClientMessageType::HEARTBEAT: {
Utils::log("Received HEARTBEAT from " + reqAddr);
mHandler->sendMessage(ServerMessageType::HEARTBEAT_ACK, ""); // Send ACK
break;
}
case ClientMessageType::HEARTBEAT_ACK: {
Utils::log("Received HEARTBEAT_ACK from " + reqAddr);
mLastHeartbeatReceived = std::chrono::steady_clock::now();
mMissedHeartbeats = 0; // Reset missed heartbeat count
break;
}
case ClientMessageType::GRACEFUL_DISCONNECT: {
Utils::log("Received GRACEFUL_DISCONNECT from " + reqAddr + ": " + data);
disconnect();
break;
}
default:
Utils::warn("Unhandled message type from " + reqAddr);
break;
}
}
void mStartHeartbeat();
void mHandleMessage(ClientMessageType type, const std::string& data);
std::shared_ptr<MessageHandler> mHandler;
std::function<void(std::shared_ptr<TCPConnection>)> mOnDisconnect;

View File

@@ -16,14 +16,45 @@
#include <columnlynx/common/utils.hpp>
#include <columnlynx/server/net/tcp/tcp_connection.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
namespace ColumnLynx::Net::TCP {
class TCPServer {
public:
TCPServer(asio::io_context& ioContext, uint16_t port, Utils::LibSodiumWrapper* sodiumWrapper, bool* hostRunning)
: mIoContext(ioContext), mAcceptor(ioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), mSodiumWrapper(sodiumWrapper), mHostRunning(hostRunning)
TCPServer(asio::io_context& ioContext,
uint16_t port,
Utils::LibSodiumWrapper* sodiumWrapper,
bool* hostRunning, bool ipv4Only = false)
: mIoContext(ioContext),
mAcceptor(ioContext),
mSodiumWrapper(sodiumWrapper),
mHostRunning(hostRunning)
{
asio::error_code ec;
if (!ipv4Only) {
// Try IPv6 first (dual-stack check)
asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port);
mAcceptor.open(endpoint_v6.protocol(), ec);
if (!ec) {
mAcceptor.set_option(asio::ip::v6_only(false), ec); // Allow dual-stack if possible
mAcceptor.bind(endpoint_v6, ec);
}
}
// Fallback to IPv4 if anything failed
if (ec || ipv4Only) {
Utils::warn("TCP: IPv6 unavailable (" + ec.message() + "), falling back to IPv4 only");
asio::ip::tcp::endpoint endpoint_v4(asio::ip::tcp::v4(), port);
mAcceptor.close(); // ensure clean state
mAcceptor.open(endpoint_v4.protocol());
mAcceptor.bind(endpoint_v4);
}
// Start listening
mAcceptor.listen();
Utils::log("Started TCP server on port " + std::to_string(port));
mStartAccept();
}

View File

@@ -8,26 +8,51 @@
#include <columnlynx/common/net/udp/udp_message_type.hpp>
#include <columnlynx/common/utils.hpp>
#include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net::UDP {
class UDPServer {
public:
UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning)
: mSocket(ioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)), mHostRunning(hostRunning)
UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false, std::shared_ptr<VirtualInterface> tun = nullptr)
: mSocket(ioContext), mHostRunning(hostRunning), mTun(tun)
{
asio::error_code ec;
if (!ipv4Only) {
// Try IPv6 first (dual-stack check)
asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port);
mSocket.open(endpoint_v6.protocol(), ec);
if (!ec) {
mSocket.set_option(asio::ip::v6_only(false), ec); // Allow dual-stack if possible
mSocket.bind(endpoint_v6, ec);
}
}
// Fallback to IPv4 if anything failed
if (ec || ipv4Only) {
Utils::warn("UDP: IPv6 unavailable (" + ec.message() + "), falling back to IPv4 only");
asio::ip::udp::endpoint endpoint_v4(asio::ip::udp::v4(), port);
mSocket.close(); // ensure clean state
mSocket.open(endpoint_v4.protocol());
mSocket.bind(endpoint_v4);
}
Utils::log("Started UDP server on port " + std::to_string(port));
mStartReceive();
}
void stop();
void sendData(const uint64_t sessionID, const std::string& data);
private:
void mStartReceive();
void mHandlePacket(std::size_t bytes);
void mSendData(const uint64_t sessionID, const std::string& data);
asio::ip::udp::socket mSocket;
asio::ip::udp::endpoint mRemoteEndpoint;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
bool* mHostRunning;
std::shared_ptr<VirtualInterface> mTun;
};
}

270
include/wintun/wintun.h Normal file
View File

@@ -0,0 +1,270 @@
/* SPDX-License-Identifier: GPL-2.0 OR MIT
*
* Copyright (C) 2018-2021 WireGuard LLC. All Rights Reserved.
*/
#pragma once
#include <winsock2.h>
#include <windows.h>
#include <ipexport.h>
#include <ifdef.h>
#include <ws2ipdef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef ALIGNED
# if defined(_MSC_VER)
# define ALIGNED(n) __declspec(align(n))
# elif defined(__GNUC__)
# define ALIGNED(n) __attribute__((aligned(n)))
# else
# error "Unable to define ALIGNED"
# endif
#endif
/* MinGW is missing this one, unfortunately. */
#ifndef _Post_maybenull_
# define _Post_maybenull_
#endif
#pragma warning(push)
#pragma warning(disable : 4324) /* structure was padded due to alignment specifier */
/**
* A handle representing Wintun adapter
*/
typedef struct _WINTUN_ADAPTER *WINTUN_ADAPTER_HANDLE;
/**
* Creates a new Wintun adapter.
*
* @param Name The requested name of the adapter. Zero-terminated string of up to MAX_ADAPTER_NAME-1
* characters.
*
* @param TunnelType Name of the adapter tunnel type. Zero-terminated string of up to MAX_ADAPTER_NAME-1
* characters.
*
* @param RequestedGUID The GUID of the created network adapter, which then influences NLA generation deterministically.
* If it is set to NULL, the GUID is chosen by the system at random, and hence a new NLA entry is
* created for each new adapter. It is called "requested" GUID because the API it uses is
* completely undocumented, and so there could be minor interesting complications with its usage.
*
* @return If the function succeeds, the return value is the adapter handle. Must be released with
* WintunCloseAdapter. If the function fails, the return value is NULL. To get extended error information, call
* GetLastError.
*/
typedef _Must_inspect_result_
_Return_type_success_(return != NULL)
_Post_maybenull_
WINTUN_ADAPTER_HANDLE(WINAPI WINTUN_CREATE_ADAPTER_FUNC)
(_In_z_ LPCWSTR Name, _In_z_ LPCWSTR TunnelType, _In_opt_ const GUID *RequestedGUID);
/**
* Opens an existing Wintun adapter.
*
* @param Name The requested name of the adapter. Zero-terminated string of up to MAX_ADAPTER_NAME-1
* characters.
*
* @return If the function succeeds, the return value is the adapter handle. Must be released with
* WintunCloseAdapter. If the function fails, the return value is NULL. To get extended error information, call
* GetLastError.
*/
typedef _Must_inspect_result_
_Return_type_success_(return != NULL)
_Post_maybenull_
WINTUN_ADAPTER_HANDLE(WINAPI WINTUN_OPEN_ADAPTER_FUNC)(_In_z_ LPCWSTR Name);
/**
* Releases Wintun adapter resources and, if adapter was created with WintunCreateAdapter, removes adapter.
*
* @param Adapter Adapter handle obtained with WintunCreateAdapter or WintunOpenAdapter.
*/
typedef VOID(WINAPI WINTUN_CLOSE_ADAPTER_FUNC)(_In_opt_ WINTUN_ADAPTER_HANDLE Adapter);
/**
* Deletes the Wintun driver if there are no more adapters in use.
*
* @return If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To
* get extended error information, call GetLastError.
*/
typedef _Return_type_success_(return != FALSE)
BOOL(WINAPI WINTUN_DELETE_DRIVER_FUNC)(VOID);
/**
* Returns the LUID of the adapter.
*
* @param Adapter Adapter handle obtained with WintunCreateAdapter or WintunOpenAdapter
*
* @param Luid Pointer to LUID to receive adapter LUID.
*/
typedef VOID(WINAPI WINTUN_GET_ADAPTER_LUID_FUNC)(_In_ WINTUN_ADAPTER_HANDLE Adapter, _Out_ NET_LUID *Luid);
/**
* Determines the version of the Wintun driver currently loaded.
*
* @return If the function succeeds, the return value is the version number. If the function fails, the return value is
* zero. To get extended error information, call GetLastError. Possible errors include the following:
* ERROR_FILE_NOT_FOUND Wintun not loaded
*/
typedef _Return_type_success_(return != 0)
DWORD(WINAPI WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC)(VOID);
/**
* Determines the level of logging, passed to WINTUN_LOGGER_CALLBACK.
*/
typedef enum
{
WINTUN_LOG_INFO, /**< Informational */
WINTUN_LOG_WARN, /**< Warning */
WINTUN_LOG_ERR /**< Error */
} WINTUN_LOGGER_LEVEL;
/**
* Called by internal logger to report diagnostic messages
*
* @param Level Message level.
*
* @param Timestamp Message timestamp in in 100ns intervals since 1601-01-01 UTC.
*
* @param Message Message text.
*/
typedef VOID(CALLBACK *WINTUN_LOGGER_CALLBACK)(
_In_ WINTUN_LOGGER_LEVEL Level,
_In_ DWORD64 Timestamp,
_In_z_ LPCWSTR Message);
/**
* Sets logger callback function.
*
* @param NewLogger Pointer to callback function to use as a new global logger. NewLogger may be called from various
* threads concurrently. Should the logging require serialization, you must handle serialization in
* NewLogger. Set to NULL to disable.
*/
typedef VOID(WINAPI WINTUN_SET_LOGGER_FUNC)(_In_ WINTUN_LOGGER_CALLBACK NewLogger);
/**
* Minimum ring capacity.
*/
#define WINTUN_MIN_RING_CAPACITY 0x20000 /* 128kiB */
/**
* Maximum ring capacity.
*/
#define WINTUN_MAX_RING_CAPACITY 0x4000000 /* 64MiB */
/**
* A handle representing Wintun session
*/
typedef struct _TUN_SESSION *WINTUN_SESSION_HANDLE;
/**
* Starts Wintun session.
*
* @param Adapter Adapter handle obtained with WintunOpenAdapter or WintunCreateAdapter
*
* @param Capacity Rings capacity. Must be between WINTUN_MIN_RING_CAPACITY and WINTUN_MAX_RING_CAPACITY (incl.)
* Must be a power of two.
*
* @return Wintun session handle. Must be released with WintunEndSession. If the function fails, the return value is
* NULL. To get extended error information, call GetLastError.
*/
typedef _Must_inspect_result_
_Return_type_success_(return != NULL)
_Post_maybenull_
WINTUN_SESSION_HANDLE(WINAPI WINTUN_START_SESSION_FUNC)(_In_ WINTUN_ADAPTER_HANDLE Adapter, _In_ DWORD Capacity);
/**
* Ends Wintun session.
*
* @param Session Wintun session handle obtained with WintunStartSession
*/
typedef VOID(WINAPI WINTUN_END_SESSION_FUNC)(_In_ WINTUN_SESSION_HANDLE Session);
/**
* Gets Wintun session's read-wait event handle.
*
* @param Session Wintun session handle obtained with WintunStartSession
*
* @return Pointer to receive event handle to wait for available data when reading. Should
* WintunReceivePackets return ERROR_NO_MORE_ITEMS (after spinning on it for a while under heavy
* load), wait for this event to become signaled before retrying WintunReceivePackets. Do not call
* CloseHandle on this event - it is managed by the session.
*/
typedef HANDLE(WINAPI WINTUN_GET_READ_WAIT_EVENT_FUNC)(_In_ WINTUN_SESSION_HANDLE Session);
/**
* Maximum IP packet size
*/
#define WINTUN_MAX_IP_PACKET_SIZE 0xFFFF
/**
* Retrieves one or packet. After the packet content is consumed, call WintunReleaseReceivePacket with Packet returned
* from this function to release internal buffer. This function is thread-safe.
*
* @param Session Wintun session handle obtained with WintunStartSession
*
* @param PacketSize Pointer to receive packet size.
*
* @return Pointer to layer 3 IPv4 or IPv6 packet. Client may modify its content at will. If the function fails, the
* return value is NULL. To get extended error information, call GetLastError. Possible errors include the
* following:
* ERROR_HANDLE_EOF Wintun adapter is terminating;
* ERROR_NO_MORE_ITEMS Wintun buffer is exhausted;
* ERROR_INVALID_DATA Wintun buffer is corrupt
*/
typedef _Must_inspect_result_
_Return_type_success_(return != NULL)
_Post_maybenull_
_Post_writable_byte_size_(*PacketSize)
BYTE *(WINAPI WINTUN_RECEIVE_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _Out_ DWORD *PacketSize);
/**
* Releases internal buffer after the received packet has been processed by the client. This function is thread-safe.
*
* @param Session Wintun session handle obtained with WintunStartSession
*
* @param Packet Packet obtained with WintunReceivePacket
*/
typedef VOID(
WINAPI WINTUN_RELEASE_RECEIVE_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _In_ const BYTE *Packet);
/**
* Allocates memory for a packet to send. After the memory is filled with packet data, call WintunSendPacket to send
* and release internal buffer. WintunAllocateSendPacket is thread-safe and the WintunAllocateSendPacket order of
* calls define the packet sending order.
*
* @param Session Wintun session handle obtained with WintunStartSession
*
* @param PacketSize Exact packet size. Must be less or equal to WINTUN_MAX_IP_PACKET_SIZE.
*
* @return Returns pointer to memory where to prepare layer 3 IPv4 or IPv6 packet for sending. If the function fails,
* the return value is NULL. To get extended error information, call GetLastError. Possible errors include the
* following:
* ERROR_HANDLE_EOF Wintun adapter is terminating;
* ERROR_BUFFER_OVERFLOW Wintun buffer is full;
*/
typedef _Must_inspect_result_
_Return_type_success_(return != NULL)
_Post_maybenull_
_Post_writable_byte_size_(PacketSize)
BYTE *(WINAPI WINTUN_ALLOCATE_SEND_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _In_ DWORD PacketSize);
/**
* Sends the packet and releases internal buffer. WintunSendPacket is thread-safe, but the WintunAllocateSendPacket
* order of calls define the packet sending order. This means the packet is not guaranteed to be sent in the
* WintunSendPacket yet.
*
* @param Session Wintun session handle obtained with WintunStartSession
*
* @param Packet Packet obtained with WintunAllocateSendPacket
*/
typedef VOID(WINAPI WINTUN_SEND_PACKET_FUNC)(_In_ WINTUN_SESSION_HANDLE Session, _In_ const BYTE *Packet);
#pragma warning(pop)
#ifdef __cplusplus
}
#endif

View File

@@ -10,15 +10,18 @@
#include <columnlynx/client/net/tcp/tcp_client.hpp>
#include <columnlynx/client/net/udp/udp_client.hpp>
#include <cxxopts/cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp;
using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net;
using namespace ColumnLynx;
volatile sig_atomic_t done = 0;
void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
log("Received termination signal. Shutting down client.");
//log("Received termination signal. Shutting down client.");
done = 1;
}
}
@@ -38,7 +41,10 @@ int main(int argc, char** argv) {
options.add_options()
("h,help", "Print help")
("s,server", "Server address", cxxopts::value<std::string>()->default_value("127.0.0.1"))
("p,port", "Server port", cxxopts::value<uint16_t>()->default_value(std::to_string(serverPort())));
("p,port", "Server port", cxxopts::value<uint16_t>()->default_value(std::to_string(serverPort())))
("allow-selfsigned", "Allow self-signed certificates", cxxopts::value<bool>()->default_value("false"));
bool insecureMode = options.parse(argc, argv).count("allow-selfsigned") > 0;
auto result = options.parse(argc, argv);
if (result.count("help")) {
@@ -51,7 +57,14 @@ int main(int argc, char** argv) {
try {
log("ColumnLynx Client, Version " + getVersion());
log("This software is licensed under the GPLv3. See LICENSE for details.");
log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details.");
#if defined(__WIN32__)
WintunInitialize();
#endif
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>("utun1");
log("Using virtual interface: " + tun->getName());
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
@@ -59,8 +72,8 @@ int main(int argc, char** argv) {
uint64_t sessionID = 0;
asio::io_context io;
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, &sodiumWrapper, &aesKey, &sessionID);
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, &aesKey, &sessionID);
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, tun);
client->start();
udpClient->start();
@@ -69,29 +82,31 @@ int main(int argc, char** argv) {
std::thread ioThread([&io]() {
io.run();
});
ioThread.detach();
//ioThread.join();
log("Client connected to " + host + ":" + port);
// Client is running
while ((!done && client->isConnected()) || !client->isHandshakeComplete()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Temp wait
if (client->isHandshakeComplete()) {
// Send a test UDP message every 5 seconds after handshake is complete
static auto lastSendTime = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastSendTime).count() >= 5) {
udpClient->sendMessage("Hello from UDP client!");
lastSendTime = now;
}
while ((client->isConnected() || !client->isHandshakeComplete()) && !done) {
auto packet = tun->readPacket();
if (!client->isConnected() || done) {
break; // Bail out if connection died or signal set while blocked
}
if (packet.empty()) {
continue;
}
udpClient->sendMessage(std::string(packet.begin(), packet.end()));
}
log("Client shutting down.");
udpClient->stop();
client->disconnect();
io.stop();
ioThread.join();
if (ioThread.joinable())
ioThread.join();
} catch (const std::exception& e) {
error("Client error: " + std::string(e.what()));

View File

@@ -0,0 +1,292 @@
// tcp_client.cpp - TCP Client for ColumnLynx
// Copyright (C) 2025 DcruBro
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#include <columnlynx/client/net/tcp/tcp_client.hpp>
#include <arpa/inet.h>
namespace ColumnLynx::Net::TCP {
void TCPClient::start() {
auto self = shared_from_this();
mResolver.async_resolve(mHost, mPort,
[this, self](asio::error_code ec, tcp::resolver::results_type endpoints) {
if (!ec) {
asio::async_connect(mSocket, endpoints,
[this, self](asio::error_code ec, const tcp::endpoint&) {
if (!NetHelper::isExpectedDisconnect(ec)) {
mConnected = true;
Utils::log("Client connected.");
mHandler = std::make_shared<MessageHandler>(std::move(mSocket));
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
});
mHandler->start();
// Init connection handshake
Utils::log("Sending handshake init to server.");
// Check if hostname or IPv4/IPv6
sockaddr_in addr4{};
sockaddr_in6 addr6{};
self->mIsHostDomain = inet_pton(AF_INET, mHost.c_str(), (void*)(&addr4)) != 1 && inet_pton(AF_INET6, mHost.c_str(), (void*)(&addr6)) != 1;
std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
payload.push_back(Utils::protocolVersion());
payload.insert(payload.end(),
mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
);
mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size()));
mStartHeartbeat();
} else {
Utils::error("Client connect failed: " + ec.message());
}
});
} else {
Utils::error("Client resolve failed: " + ec.message());
}
});
}
void TCPClient::sendMessage(ClientMessageType type, const std::string& data) {
if (!mConnected) {
Utils::error("Cannot send message, client not connected.");
return;
}
if (mHandler) {
asio::post(mHandler->socket().get_executor(), [self = shared_from_this(), type, data]() {
self->mHandler->sendMessage(type, data);
});
}
}
void TCPClient::disconnect(bool echo) {
if (mConnected && mHandler) {
if (echo) {
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
}
asio::error_code ec;
mHeartbeatTimer.cancel();
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec) {
Utils::error("Error during socket shutdown: " + ec.message());
}
mHandler->socket().close(ec);
if (ec) {
Utils::error("Error during socket close: " + ec.message());
}
mConnected = false;
Utils::log("Client disconnected.");
}
}
bool TCPClient::isHandshakeComplete() const {
return mHandshakeComplete;
}
bool TCPClient::isConnected() const {
return mConnected;
}
void TCPClient::mStartHeartbeat() {
auto self = shared_from_this();
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - self->mLastHeartbeatReceived).count();
if (elapsed >= 15) { // 3 missed heartbeats
Utils::error("Missed 3 heartbeats. I think the other party might have died! Disconnecting.");
// Close sockets forcefully, server is dead
asio::error_code ec;
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec);
mConnected = false;
mGlobalKeyRef = nullptr;
if (mSessionIDRef) {
*mSessionIDRef = 0;
}
return;
}
self->sendMessage(ClientMessageType::HEARTBEAT);
Utils::log("Sent HEARTBEAT to server.");
self->mLastHeartbeatSent = std::chrono::steady_clock::now();
self->mStartHeartbeat(); // Recursive
});
}
void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) {
switch (type) {
case ServerMessageType::HANDSHAKE_IDENTIFY: {
Utils::log("Received server identity: " + data);
std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey)));
// Convert key (uint8_t raw array) to vector
std::vector<uint8_t> serverPublicKeyVec(std::begin(mServerPublicKey), std::end(mServerPublicKey));
// Verify server public key
if (!Utils::LibSodiumWrapper::verifyCertificateWithSystemCAs(serverPublicKeyVec)) {
if (!(*mInsecureMode)) {
Utils::error("Server public key verification failed. Terminating connection.");
disconnect();
return;
}
Utils::warn("Warning: Server public key verification failed, but continuing due to insecure mode.");
}
// Extract and verify hostname from certificate if not IP
if (mIsHostDomain) {
std::vector<std::string> certHostnames = Utils::LibSodiumWrapper::getCertificateHostname(serverPublicKeyVec);
// Temp: print extracted hostnames if any
for (const auto& hostname : certHostnames) {
Utils::log("Extracted hostname from certificate: " + hostname);
}
if (certHostnames.empty() || std::find(certHostnames.begin(), certHostnames.end(), mHost) == certHostnames.end()) {
if (!(*mInsecureMode)) {
Utils::error("Server hostname verification failed. Terminating connection.");
disconnect();
return;
}
Utils::warn("Warning: Server hostname verification failed, but continuing due to insecure mode.");
}
} else {
Utils::warn("Connecting via IP address, I can't verify the server's identity! You might be getting MITM'd!");
}
// Generate and send challenge
Utils::log("Sending challenge to server.");
mSubmittedChallenge = Utils::LibSodiumWrapper::generateRandom256Bit(); // Temporarily store the challenge to verify later
mHandler->sendMessage(ClientMessageType::HANDSHAKE_CHALLENGE, Utils::uint8ArrayToString(mSubmittedChallenge));
}
break;
case ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE:
Utils::log("Received challenge response from server.");
{
// Verify the signature
Signature sig{};
std::memcpy(sig.data(), data.data(), std::min(data.size(), sig.size()));
if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) {
Utils::log("Challenge response verified successfully.");
// Convert the server's public key to Curve25519 for encryption
AsymPublicKey serverXPubKey{};
crypto_sign_ed25519_pk_to_curve25519(serverXPubKey.data(), mServerPublicKey);
// Generate AES key and send confirmation
mConnectionAESKey = Utils::LibSodiumWrapper::generateRandom256Bit();
if (mGlobalKeyRef) { // Copy to the global reference
std::copy(mConnectionAESKey.begin(), mConnectionAESKey.end(), mGlobalKeyRef->begin());
}
AsymNonce nonce{};
randombytes_buf(nonce.data(), nonce.size());
// TODO: This is pretty redundant, it should return the required type directly
std::array<uint8_t, 32> arrayPrivateKey;
std::copy(mLibSodiumWrapper->getXPrivateKey(),
mLibSodiumWrapper->getXPrivateKey() + 32,
arrayPrivateKey.begin());
std::vector<uint8_t> encr = Utils::LibSodiumWrapper::encryptAsymmetric(
mConnectionAESKey.data(), mConnectionAESKey.size(),
nonce,
serverXPubKey,
arrayPrivateKey
);
std::vector<uint8_t> payload;
payload.reserve(nonce.size() + encr.size());
payload.insert(payload.end(), nonce.begin(), nonce.end());
payload.insert(payload.end(), encr.begin(), encr.end());
mHandler->sendMessage(ClientMessageType::HANDSHAKE_EXCHANGE_KEY, Utils::uint8ArrayToString(payload.data(), payload.size()));
} else {
Utils::error("Challenge response verification failed. Terminating connection.");
disconnect();
}
}
break;
case ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM:
Utils::log("Received handshake exchange key confirmation from server.");
// Decrypt the session ID using the established AES key
{
Nonce symNonce{}; // All zeros
std::vector<uint8_t> ciphertext(data.begin(), data.end());
std::vector<uint8_t> decrypted = Utils::LibSodiumWrapper::decryptMessage(
ciphertext.data(), ciphertext.size(),
mConnectionAESKey, symNonce
);
if (decrypted.size() != sizeof(mConnectionSessionID) + sizeof(Protocol::TunConfig)) {
Utils::error("Decrypted config has invalid size. Terminating connection.");
disconnect();
return;
}
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID));
std::memcpy(&mTunConfig, decrypted.data() + sizeof(mConnectionSessionID), sizeof(Protocol::TunConfig));
mConnectionSessionID = Utils::be64toh(mConnectionSessionID);
Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID));
if (mSessionIDRef) { // Copy to the global reference
*mSessionIDRef = mConnectionSessionID;
}
uint32_t clientIP = ntohl(mTunConfig.clientIP);
uint32_t serverIP = ntohl(mTunConfig.serverIP);
uint8_t prefixLen = mTunConfig.prefixLength;
uint16_t mtu = mTunConfig.mtu;
if (mTun) {
mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
}
mHandshakeComplete = true;
}
break;
case ServerMessageType::HEARTBEAT:
Utils::log("Received HEARTBEAT from server.");
mHandler->sendMessage(ClientMessageType::HEARTBEAT_ACK, ""); // Send ACK
break;
case ServerMessageType::HEARTBEAT_ACK:
Utils::log("Received HEARTBEAT_ACK from server.");
mLastHeartbeatReceived = std::chrono::steady_clock::now();
mMissedHeartbeats = 0; // Reset missed heartbeat count
break;
case ServerMessageType::GRACEFUL_DISCONNECT:
Utils::log("Server is disconnecting: " + data);
if (mConnected) { // Prevent Recursion
disconnect(false);
}
break;
default:
Utils::log("Received unknown message type from server.");
break;
}
}
}

View File

@@ -0,0 +1,117 @@
// udp_client.cpp - UDP Client for ColumnLynx
// Copyright (C) 2025 DcruBro
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#include <columnlynx/client/net/udp/udp_client.hpp>
namespace ColumnLynx::Net::UDP {
void UDPClient::start() {
// TODO: Add IPv6
auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort);
mRemoteEndpoint = *endpoints.begin();
mSocket.open(asio::ip::udp::v4());
Utils::log("UDP Client ready to send to " + mRemoteEndpoint.address().to_string() + ":" + std::to_string(mRemoteEndpoint.port()));
}
void UDPClient::sendMessage(const std::string& data) {
UDPPacketHeader hdr{};
randombytes_buf(hdr.nonce.data(), hdr.nonce.size());
if (mAesKeyRef == nullptr || mSessionIDRef == nullptr) {
Utils::error("UDP Client AES key or Session ID reference is null!");
return;
}
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
*mAesKeyRef, hdr.nonce, "udp-data"
);
std::vector<uint8_t> packet;
packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size());
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&hdr),
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
);
uint64_t sid = *mSessionIDRef;
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&sid),
reinterpret_cast<uint8_t*>(&sid) + sizeof(sid)
);
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
mSocket.send_to(asio::buffer(packet), mRemoteEndpoint);
Utils::log("Sent UDP packet of size " + std::to_string(packet.size()));
}
void UDPClient::stop() {
if (mSocket.is_open()) {
asio::error_code ec;
mSocket.cancel(ec);
mSocket.close(ec);
Utils::log("UDP Client socket closed.");
}
}
void UDPClient::mStartReceive() {
mSocket.async_receive_from(
asio::buffer(mRecvBuffer), mRemoteEndpoint,
[this](asio::error_code ec, std::size_t bytes) {
if (ec) {
if (ec == asio::error::operation_aborted) return; // Socket closed
// Other recv error
mStartReceive();
return;
}
if (bytes > 0) {
mHandlePacket(bytes);
}
mStartReceive();
}
);
}
void UDPClient::mHandlePacket(std::size_t bytes) {
if (bytes < sizeof(UDPPacketHeader) + sizeof(uint64_t)) {
Utils::warn("UDP Client received packet too small to process.");
return;
}
// Parse header
UDPPacketHeader hdr;
std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader));
// Parse session ID
uint64_t sessionID;
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
// Decrypt payload
std::vector<uint8_t> ciphertext(
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t),
mRecvBuffer.begin() + bytes
);
if (mAesKeyRef == nullptr) {
Utils::error("UDP Client AES key reference is null!");
return;
}
std::vector<uint8_t> plaintext = Utils::LibSodiumWrapper::decryptMessage(
ciphertext.data(), ciphertext.size(), *mAesKeyRef, hdr.nonce, "udp-data"
);
if (plaintext.empty()) {
Utils::warn("UDP Client failed to decrypt received packet.");
return;
}
Utils::log("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
// Write to TUN
if (mTunRef) {
mTunRef->writePacket(plaintext);
}
}
}

View File

@@ -37,7 +37,7 @@ namespace ColumnLynx::Utils {
}
std::string getVersion() {
return "a0.2";
return "a0.4";
}
unsigned short serverPort() {

View File

@@ -0,0 +1,225 @@
// virtual_interface.cpp - Virtual Interface for Network Communication
// Copyright (C) 2025 DcruBro
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net {
// ------------------------------ Constructor ------------------------------
VirtualInterface::VirtualInterface(const std::string& ifName)
: mIfName(ifName), mFd(-1)
{
#if defined(__linux__)
// ---- Linux: /dev/net/tun ----
mFd = open("/dev/net/tun", O_RDWR);
if (mFd < 0)
throw std::runtime_error("Failed to open /dev/net/tun: " + std::string(strerror(errno)));
struct ifreq ifr {};
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
std::strncpy(ifr.ifr_name, ifName.c_str(), IFNAMSIZ);
if (ioctl(mFd, TUNSETIFF, &ifr) < 0) {
close(mFd);
throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno)));
}
#elif defined(__APPLE__)
// ---- macOS: UTUN (system control socket) ----
mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
if (mFd < 0)
throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno)));
struct ctl_info ctlInfo {};
std::strncpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(ctlInfo.ctl_name));
if (ioctl(mFd, CTLIOCGINFO, &ctlInfo) == -1)
throw std::runtime_error("ioctl(CTLIOCGINFO) failed: " + std::string(strerror(errno)));
struct sockaddr_ctl sc {};
sc.sc_len = sizeof(sc);
sc.sc_family = AF_SYSTEM;
sc.ss_sysaddr = AF_SYS_CONTROL;
sc.sc_id = ctlInfo.ctl_id;
sc.sc_unit = 0; // utun0 (0 = auto-assign)
if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) {
if (errno == EPERM)
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: Insufficient permissions (try running as root)");
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno)));
}
// Retrieve actual utun device name
struct sockaddr_storage addr;
socklen_t addrlen = sizeof(addr);
if (getsockname(mFd, (struct sockaddr*)&addr, &addrlen) == 0) {
const struct sockaddr_ctl* addr_ctl = (const struct sockaddr_ctl*)&addr;
mIfName = "utun" + std::to_string(addr_ctl->sc_unit - 1);
} else {
mIfName = "utunX";
}
#elif defined(_WIN32)
// ---- Windows: Wintun (WireGuard virtual adapter) ----
WINTUN_ADAPTER_HANDLE adapter =
WintunOpenAdapter(L"ColumnLynx", std::wstring(ifName.begin(), ifName.end()).c_str());
if (!adapter)
throw std::runtime_error("Wintun adapter not found or not installed");
WINTUN_SESSION_HANDLE session =
WintunStartSession(adapter, 0x200000); // ring buffer size
if (!session)
throw std::runtime_error("Failed to start Wintun session");
mHandle = WintunGetReadWaitEvent(session);
mFd = -1; // not used on Windows
mIfName = ifName;
#else
throw std::runtime_error("Unsupported platform");
#endif
}
// ------------------------------ Destructor ------------------------------
VirtualInterface::~VirtualInterface() {
#if defined(__linux__) || defined(__APPLE__)
if (mFd >= 0)
close(mFd);
#elif defined(_WIN32)
// Wintun sessions need explicit stop
// (assuming you stored the session handle as member)
// WintunEndSession(mSession);
#endif
}
// ------------------------------ Read ------------------------------
std::vector<uint8_t> VirtualInterface::readPacket() {
#if defined(__linux__) || defined(__APPLE__)
std::vector<uint8_t> buf(4096);
ssize_t n = read(mFd, buf.data(), buf.size());
if (n < 0) {
if (errno == EINTR) {
return {}; // Interrupted, return empty
}
throw std::runtime_error("read() failed: " + std::string(strerror(errno)));
}
buf.resize(n);
return buf;
#elif defined(_WIN32)
WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr);
if (!packet) return {};
std::vector<uint8_t> buf(packet->Data, packet->Data + packet->Length);
WintunReleaseReceivePacket(mSession, packet);
return buf;
#else
return {};
#endif
}
// ------------------------------ Write ------------------------------
void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) {
#if defined(__linux__) || defined(__APPLE__)
ssize_t n = write(mFd, packet.data(), packet.size());
if (n < 0)
throw std::runtime_error("write() failed: " + std::string(strerror(errno)));
#elif defined(_WIN32)
WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size());
if (!tx) throw std::runtime_error("WintunAllocateSendPacket failed");
memcpy(tx->Data, packet.data(), packet.size());
WintunSendPacket(mSession, tx);
#endif
}
// ------------------------------ Accessors ------------------------------
const std::string& VirtualInterface::getName() const { return mIfName; }
int VirtualInterface::getFd() const { return mFd; }
// ------------------------------------------------------------
// 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

@@ -10,46 +10,71 @@
#include <columnlynx/server/net/udp/udp_server.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <unordered_set>
#include <cxxopts/cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp;
using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net::TCP;
using namespace ColumnLynx::Net::UDP;
using namespace ColumnLynx::Net;
using namespace ColumnLynx;
volatile sig_atomic_t done = 0;
/*void signalHandler(int signum) {
void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
log("Received termination signal. Shutting down server gracefully.");
done = 1;
}
}*/
}
int main(int argc, char** argv) {
// Capture SIGINT and SIGTERM for graceful shutdown
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = signalHandler;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);
cxxopts::Options options("columnlynx_server", "ColumnLynx Server Application");
options.add_options()
("h,help", "Print help")
("4,ipv4-only", "Force IPv4 only operation", cxxopts::value<bool>()->default_value("false"));
PanicHandler::init();
try {
// Catch SIGINT and SIGTERM for graceful shutdown
/*struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = signalHandler;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);*/
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
return 0;
}
bool ipv4Only = result["ipv4-only"].as<bool>();
log("ColumnLynx Server, Version " + getVersion());
log("This software is licensed under the GPLv3. See LICENSE for details.");
log("This software is licensed under the GPLv2 only OR the GPLv3. See 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)
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES));
log("Server private key: " + bytesToHexString(sodiumWrapper.getPrivateKey(), crypto_sign_SECRETKEYBYTES)); // TEMP, remove later
//log("Server private key: " + bytesToHexString(sodiumWrapper.getPrivateKey(), crypto_sign_SECRETKEYBYTES)); // TEMP, remove later
bool hostRunning = true;
asio::io_context io;
auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning);
auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning, ipv4Only, tun);
asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](const std::error_code&, int) {
@@ -72,7 +97,21 @@ int main(int argc, char** argv) {
log("Server started on port " + std::to_string(serverPort()));
while (!done) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto packet = tun->readPacket();
if (packet.empty()) {
continue;
}
const uint8_t* ip = packet.data();
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset in IPv6-mapped header
auto session = SessionRegistry::getInstance().getByIP(dstIP);
if (!session) {
Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipv4ToString(dstIP));
continue;
}
udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end()));
}
log("Shutting down server...");

View File

@@ -0,0 +1,237 @@
// tcp_connection.cpp - TCP Connection for ColumnLynx
// Copyright (C) 2025 DcruBro
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
#include <columnlynx/server/net/tcp/tcp_connection.hpp>
namespace ColumnLynx::Net::TCP {
void TCPConnection::start() {
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
});
mHandler->onDisconnect([this](const asio::error_code& ec) {
Utils::log("Client disconnected: " + mHandler->socket().remote_endpoint().address().to_string() + " - " + ec.message());
disconnect();
});
mHandler->start();
mStartHeartbeat();
// Placeholder for message handling setup
Utils::log("Client connected: " + mHandler->socket().remote_endpoint().address().to_string());
}
void TCPConnection::sendMessage(ServerMessageType type, const std::string& data) {
if (mHandler) {
mHandler->sendMessage(type, data);
}
}
void TCPConnection::setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb) {
mOnDisconnect = std::move(cb);
}
void TCPConnection::disconnect() {
std::string ip = mHandler->socket().remote_endpoint().address().to_string();
mHandler->sendMessage(ServerMessageType::GRACEFUL_DISCONNECT, "Server initiated disconnect.");
mHeartbeatTimer.cancel();
asio::error_code ec;
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec);
SessionRegistry::getInstance().erase(mConnectionSessionID);
SessionRegistry::getInstance().deallocIP(mConnectionSessionID);
Utils::log("Closed connection to " + ip);
if (mOnDisconnect) {
mOnDisconnect(shared_from_this());
}
}
uint64_t TCPConnection::getSessionID() const {
return mConnectionSessionID;
}
std::array<uint8_t, 32> TCPConnection::getAESKey() const {
return mConnectionAESKey;
}
void TCPConnection::mStartHeartbeat() {
auto self = shared_from_this();
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - self->mLastHeartbeatReceived).count();
if (elapsed >= 15) { // 3 missed heartbeats
Utils::error("Missed 3 heartbeats. I think the other party (client " + std::to_string(self->mConnectionSessionID) + ") might have died! Disconnecting.");
// Remove socket forcefully, client is dead
asio::error_code ec;
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
mHandler->socket().close(ec);
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
return;
}
self->sendMessage(ServerMessageType::HEARTBEAT);
Utils::log("Sent HEARTBEAT to client " + std::to_string(self->mConnectionSessionID));
self->mLastHeartbeatSent = now;
self->mStartHeartbeat(); // Recursive
});
}
void TCPConnection::mHandleMessage(ClientMessageType type, const std::string& data) {
std::string reqAddr = mHandler->socket().remote_endpoint().address().to_string();
switch (type) {
case ClientMessageType::HANDSHAKE_INIT: {
Utils::log("Received HANDSHAKE_INIT from " + reqAddr);
if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) {
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short.");
disconnect();
return;
}
uint8_t clientProtoVer = static_cast<uint8_t>(data[0]);
if (clientProtoVer != Utils::protocolVersion()) {
Utils::warn("Client protocol version mismatch from " + reqAddr + ". Expected " +
std::to_string(Utils::protocolVersion()) + ", got " + std::to_string(clientProtoVer) + ".");
disconnect();
return;
}
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + ".");
std::memcpy(mConnectionPublicKey.data(), data.data() + 1, std::min(data.size() - 1, sizeof(mConnectionPublicKey))); // Store the client's public key (for identification)
mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist
break;
}
case ClientMessageType::HANDSHAKE_CHALLENGE: {
Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr);
// Convert to byte array
uint8_t challengeData[32];
std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData)));
// Sign the challenge
Signature sig = Utils::LibSodiumWrapper::signMessage(
challengeData, sizeof(challengeData),
mLibSodiumWrapper->getPrivateKey()
);
mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response
break;
}
case ClientMessageType::HANDSHAKE_EXCHANGE_KEY: {
Utils::log("Received HANDSHAKE_EXCHANGE_KEY from " + reqAddr);
// Extract encrypted AES key and nonce (nonce is the first 24 bytes, rest is the ciphertext)
if (data.size() < 24) { // Minimum size check (nonce)
Utils::warn("HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " is too short.");
disconnect();
return;
}
AsymNonce nonce{};
std::memcpy(nonce.data(), data.data(), nonce.size());
std::vector<uint8_t> ciphertext(data.size() - nonce.size());
std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size());
try {
std::array<uint8_t, 32> arrayPrivateKey;
std::copy(mLibSodiumWrapper->getXPrivateKey(),
mLibSodiumWrapper->getXPrivateKey() + 32,
arrayPrivateKey.begin());
// Decrypt the AES key using the client's public key and server's private key
std::vector<uint8_t> decrypted = Utils::LibSodiumWrapper::decryptAsymmetric(
ciphertext.data(), ciphertext.size(),
nonce,
mConnectionPublicKey,
arrayPrivateKey
);
if (decrypted.size() != 32) {
Utils::warn("Decrypted HANDSHAKE_EXCHANGE_KEY from " + reqAddr + " has invalid size.");
disconnect();
return;
}
std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size());
// Make a Session ID
randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID));
// 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();
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::htobe64(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
);
mHandler->sendMessage(ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM, Utils::uint8ArrayToString(encryptedPayload.data(), encryptedPayload.size()));
// Add to session registry
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned.");
auto session = std::make_shared<SessionState>(mConnectionAESKey, std::chrono::hours(12), clientIP, htonl(0x0A0A0001), mConnectionSessionID);
SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session));
} catch (const std::exception& e) {
Utils::error("Failed to decrypt HANDSHAKE_EXCHANGE_KEY from " + reqAddr + ": " + e.what());
disconnect();
}
break;
}
case ClientMessageType::HEARTBEAT: {
Utils::log("Received HEARTBEAT from " + reqAddr);
mHandler->sendMessage(ServerMessageType::HEARTBEAT_ACK, ""); // Send ACK
break;
}
case ClientMessageType::HEARTBEAT_ACK: {
Utils::log("Received HEARTBEAT_ACK from " + reqAddr);
mLastHeartbeatReceived = std::chrono::steady_clock::now();
mMissedHeartbeats = 0; // Reset missed heartbeat count
break;
}
case ClientMessageType::GRACEFUL_DISCONNECT: {
Utils::log("Received GRACEFUL_DISCONNECT from " + reqAddr + ": " + data);
disconnect();
break;
}
default:
Utils::warn("Unhandled message type from " + reqAddr);
break;
}
}
}

View File

@@ -20,14 +20,6 @@
namespace ColumnLynx::Net::TCP {
void TCPServer::mStartAccept() {
// A bit of a shotty implementation, might improve later
/*std::cout << "Host running pointer: " << *mHostRunning << std::endl;
if (mHostRunning != nullptr && !(*mHostRunning)) {
Utils::log("Server is stopping, not accepting new connections.");
return;
}*/
mAcceptor.async_accept(
[this](asio::error_code ec, asio::ip::tcp::socket socket) {
if (ec) {

View File

@@ -10,12 +10,6 @@
namespace ColumnLynx::Net::UDP {
void UDPServer::mStartReceive() {
// A bit of a shotty implementation, might improve later
/*if (mHostRunning != nullptr && !(*mHostRunning)) {
Utils::log("Server is stopping, not receiving new packets.");
return;
}*/
mSocket.async_receive_from(
asio::buffer(mRecvBuffer), mRemoteEndpoint,
[this](asio::error_code ec, std::size_t bytes) {
@@ -69,17 +63,16 @@ namespace ColumnLynx::Net::UDP {
std::string payloadStr(plaintext.begin(), plaintext.end());
Utils::log("UDP: Received packet from " + mRemoteEndpoint.address().to_string() + " - Payload: " + payloadStr);
// TODO: Process the packet payload, for now just echo back
mSendData(sessionID, std::string(plaintext.begin(), plaintext.end()));
if (mTun) {
mTun->writePacket(plaintext); // Send to virtual interface
}
} catch (...) {
Utils::warn("UDP: Failed to decrypt payload from " + mRemoteEndpoint.address().to_string());
return;
}
}
void UDPServer::mSendData(const uint64_t sessionID, const std::string& data) {
// TODO: Implement
void UDPServer::sendData(const uint64_t sessionID, const std::string& data) {
// Find the IPv4/IPv6 endpoint for the session
std::shared_ptr<const SessionState> session = SessionRegistry::getInstance().get(sessionID);
if (!session) {

View File

@@ -0,0 +1,338 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Moe Ghoul>, 1 April 1989
Moe Ghoul, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

19
third_party/wintun/LICENSE_1_0_MIT.txt vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2018 WireGuard LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.