Compare commits

29 Commits

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

View File

@@ -14,4 +14,12 @@ This project uses the standalone version of the ASIO C++ library for asynchronou
- **Website:** https://github.com/jarro2783/cxxopts/ - **Website:** https://github.com/jarro2783/cxxopts/
- **Copyright:** (c) 2014-2025 Christopher M. Kohlhoff - **Copyright:** (c) 2014-2025 Christopher M. Kohlhoff
- **License:** MIT License - **License:** MIT License
- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt` - **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,16 +6,20 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA # If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx project(ColumnLynx
VERSION 0.0.1 VERSION 0.0.5
LANGUAGES CXX LANGUAGES CXX
) )
# --------------------------------------------------------- # ---------------------------------------------------------
# General C++ setup # General C++ setup
# --------------------------------------------------------- # ---------------------------------------------------------
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
#set(CMAKE_CXX_FLAGS_DEBUG "-g")
#add_compile_options(${CMAKE_CXX_FLAGS_DEBUG})
add_compile_definitions(DEBUG=1) # TODO: Forcing for now, add dymanic based on compile flags later
include(FetchContent) include(FetchContent)
@@ -25,7 +29,7 @@ include(FetchContent)
if(APPLE) if(APPLE)
# Build universal (arm64 + x86_64), or limit to one arch if you prefer # Build universal (arm64 + x86_64), or limit to one arch if you prefer
# e.g., set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "" FORCE) # 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() endif()
# --------------------------------------------------------- # ---------------------------------------------------------
@@ -58,6 +62,15 @@ set(SODIUM_CMAKE_ARGS
FetchContent_MakeAvailable(Sodium) 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 # Output directories
# --------------------------------------------------------- # ---------------------------------------------------------
@@ -77,7 +90,7 @@ endforeach()
# --------------------------------------------------------- # ---------------------------------------------------------
file(GLOB_RECURSE COMMON_SRC CONFIGURE_DEPENDS src/common/*.cpp) file(GLOB_RECURSE COMMON_SRC CONFIGURE_DEPENDS src/common/*.cpp)
add_library(common STATIC ${COMMON_SRC}) 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 target_include_directories(common PUBLIC
${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include ${sodium_SOURCE_DIR}/src/libsodium/include
@@ -90,7 +103,7 @@ target_compile_definitions(common PUBLIC ASIO_STANDALONE)
# --------------------------------------------------------- # ---------------------------------------------------------
file(GLOB_RECURSE CLIENT_SRC CONFIGURE_DEPENDS src/client/*.cpp) file(GLOB_RECURSE CLIENT_SRC CONFIGURE_DEPENDS src/client/*.cpp)
add_executable(client ${CLIENT_SRC}) 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 target_include_directories(client PRIVATE
${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include ${sodium_SOURCE_DIR}/src/libsodium/include
@@ -104,7 +117,7 @@ set_target_properties(client PROPERTIES OUTPUT_NAME "columnlynx_client")
# --------------------------------------------------------- # ---------------------------------------------------------
file(GLOB_RECURSE SERVER_SRC CONFIGURE_DEPENDS src/server/*.cpp) file(GLOB_RECURSE SERVER_SRC CONFIGURE_DEPENDS src/server/*.cpp)
add_executable(server ${SERVER_SRC}) 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 target_include_directories(server PRIVATE
${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include ${sodium_SOURCE_DIR}/src/libsodium/include

View File

@@ -1,6 +1,20 @@
# ColumnLynx # 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 ## 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 | | uint64_t | 8 bytes | **Header** - Session ID | The unique and random session identifier for the client |
| uint8_t | variable | Data | General data / payload | | uint8_t | variable | Data | General data / payload |
## Misc.
Building the binary for Windows requires the Wintun DLL. The include header is pre-packaged.
## Legal ## 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): 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 You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. 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. 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).
### Licensing 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.* *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).

View File

@@ -13,6 +13,9 @@
#include <array> #include <array>
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <unordered_map>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp; using asio::ip::tcp;
@@ -24,7 +27,9 @@ namespace ColumnLynx::Net::TCP {
const std::string& port, const std::string& port,
Utils::LibSodiumWrapper* sodiumWrapper, Utils::LibSodiumWrapper* sodiumWrapper,
std::array<uint8_t, 32>* aesKey, std::array<uint8_t, 32>* aesKey,
uint64_t* sessionIDRef) uint64_t* sessionIDRef,
bool* insecureMode,
std::shared_ptr<VirtualInterface> tun = nullptr)
: :
mResolver(ioContext), mResolver(ioContext),
mSocket(ioContext), mSocket(ioContext),
@@ -33,242 +38,50 @@ namespace ColumnLynx::Net::TCP {
mLibSodiumWrapper(sodiumWrapper), mLibSodiumWrapper(sodiumWrapper),
mGlobalKeyRef(aesKey), mGlobalKeyRef(aesKey),
mSessionIDRef(sessionIDRef), mSessionIDRef(sessionIDRef),
mInsecureMode(insecureMode),
mHeartbeatTimer(mSocket.get_executor()), mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()), mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now()) mLastHeartbeatSent(std::chrono::steady_clock::now()),
{} mTun(tun)
{
// Preload the config map
mRawClientConfig = Utils::getConfigMap("client_config");
void start() { if (!mRawClientConfig.empty()) {
auto self = shared_from_this(); Utils::debug("Loading the keys");
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.");
std::vector<uint8_t> payload; PrivateKey sk;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES); PublicKey pk;
payload.push_back(Utils::protocolVersion()); std::copy_n(Utils::hexStringToBytes(mRawClientConfig.find("CLIENT_PRIVATE_KEY")->second).begin(), sk.size(), sk.begin()); // This is extremely stupid, but the C++ compiler has forced my hand (I would've just used to_array, but fucking asio decls)
payload.insert(payload.end(), std::copy_n(Utils::hexStringToBytes(mRawClientConfig.find("CLIENT_PUBLIC_KEY")->second).begin(), pk.size(), pk.begin());
mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
);
mHandler->sendMessage(ClientMessageType::HANDSHAKE_INIT, Utils::uint8ArrayToString(payload.data(), payload.size())); mLibSodiumWrapper->setKeys(pk, sk);
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 = "") { Utils::debug("Newly-Loaded Public Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPublicKey(), 32));
if (!mConnected) { Utils::debug("Newly-Loaded Private Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPrivateKey(), 64));
Utils::error("Cannot send message, client not connected."); Utils::debug("Public Encryption Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getXPublicKey(), 32));
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) { // Starts the TCP Client and initiaties the handshake
if (mConnected && mHandler) { void start();
if (echo) { // Sends a TCP message to the server
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye"); void sendMessage(ClientMessageType type, const std::string& data = "");
} // Attempt to gracefully disconnect from the server
void disconnect(bool echo = true);
asio::error_code ec; // Get the handshake status
mHeartbeatTimer.cancel(); bool isHandshakeComplete() const;
// Get the connection status
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec); bool isConnected() const;
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;
}
private: private:
void mStartHeartbeat() { // Start the heartbeat routine
auto self = shared_from_this(); void mStartHeartbeat();
mHeartbeatTimer.expires_after(std::chrono::seconds(5)); // Handle an incoming TCP message
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) { void mHandleMessage(ServerMessageType type, const std::string& data);
if (ec == asio::error::operation_aborted) {
return; // Timer was cancelled
}
auto now = std::chrono::steady_clock::now(); // TODO: Move ptrs to smart ptrs
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;
}
}
bool mConnected = false; bool mConnected = false;
bool mHandshakeComplete = false; bool mHandshakeComplete = false;
@@ -283,9 +96,14 @@ namespace ColumnLynx::Net::TCP {
SymmetricKey mConnectionAESKey; SymmetricKey mConnectionAESKey;
std::array<uint8_t, 32>* mGlobalKeyRef; // Reference to global AES key std::array<uint8_t, 32>* mGlobalKeyRef; // Reference to global AES key
uint64_t* mSessionIDRef; // Reference to global Session ID uint64_t* mSessionIDRef; // Reference to global Session ID
bool* mInsecureMode; // Reference to insecure mode flag
asio::steady_timer mHeartbeatTimer; asio::steady_timer mHeartbeatTimer;
std::chrono::steady_clock::time_point mLastHeartbeatReceived; std::chrono::steady_clock::time_point mLastHeartbeatReceived;
std::chrono::steady_clock::time_point mLastHeartbeatSent; std::chrono::steady_clock::time_point mLastHeartbeatSent;
int mMissedHeartbeats = 0; int mMissedHeartbeats = 0;
bool mIsHostDomain;
Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr;
std::unordered_map<std::string, std::string> mRawClientConfig;
}; };
} }

View File

@@ -9,6 +9,7 @@
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <array> #include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net::UDP { namespace ColumnLynx::Net::UDP {
class UDPClient { class UDPClient {
@@ -17,113 +18,25 @@ namespace ColumnLynx::Net::UDP {
const std::string& host, const std::string& host,
const std::string& port, const std::string& port,
std::array<uint8_t, 32>* aesKeyRef, std::array<uint8_t, 32>* aesKeyRef,
uint64_t* sessionIDRef) uint64_t* sessionIDRef,
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef) { mStartReceive(); } std::shared_ptr<VirtualInterface> tunRef = nullptr)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef)
void start() { {
auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort); mStartReceive();
mRemoteEndpoint = *endpoints.begin();
mSocket.open(asio::ip::udp::v4());
Utils::log("UDP Client ready to send to " + mRemoteEndpoint.address().to_string() + ":" + std::to_string(mRemoteEndpoint.port()));
} }
void sendMessage(const std::string& data = "") { // Start the UDP client
UDPPacketHeader hdr{}; void start();
randombytes_buf(hdr.nonce.data(), hdr.nonce.size()); // Send a UDP message
void sendMessage(const std::string& data = "");
if (mAesKeyRef == nullptr || mSessionIDRef == nullptr) { // Stop the UDP client
Utils::error("UDP Client AES key or Session ID reference is null!"); void stop();
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.");
}
}
private: private:
void mStartReceive() { // Start the UDP listener routine
mSocket.async_receive_from( void mStartReceive();
asio::buffer(mRecvBuffer), mRemoteEndpoint, // Handle an incoming UDP message
[this](asio::error_code ec, std::size_t bytes) { void mHandlePacket(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));
}
asio::ip::udp::socket mSocket; asio::ip::udp::socket mSocket;
asio::ip::udp::resolver mResolver; asio::ip::udp::resolver mResolver;
@@ -132,6 +45,7 @@ namespace ColumnLynx::Net::UDP {
std::string mPort; std::string mPort;
std::array<uint8_t, 32>* mAesKeyRef; std::array<uint8_t, 32>* mAesKeyRef;
uint64_t* mSessionIDRef; uint64_t* mSessionIDRef;
std::shared_ptr<VirtualInterface> mTunRef = nullptr;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
}; };
} }

View File

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

@@ -14,13 +14,16 @@
namespace ColumnLynx::Net { namespace ColumnLynx::Net {
struct SessionState { struct SessionState {
SymmetricKey aesKey; // Immutable after creation SymmetricKey aesKey; // Agreed-upon AES-256 kes for that session; Immutable after creation
std::atomic<uint64_t> send_ctr{0}; // Per-direction counters std::atomic<uint64_t> send_ctr{0}; // Per-direction counters
std::atomic<uint64_t> recv_ctr{0}; std::atomic<uint64_t> recv_ctr{0}; // Per-direction counters
asio::ip::udp::endpoint udpEndpoint; asio::ip::udp::endpoint udpEndpoint; // Deducted IP + Port of that session client
std::atomic<uint64_t> sendCounter{0}; std::atomic<uint64_t> sendCounter{0}; // Counter of sent messages
std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point created = std::chrono::steady_clock::now(); // Time created
std::chrono::steady_clock::time_point expires{}; std::chrono::steady_clock::time_point expires{}; // Time of expiry
uint32_t clientTunIP; // Assigned IP
uint32_t serverTunIP; // Server IP
uint64_t sessionID; // Session ID
Nonce base_nonce{}; Nonce base_nonce{};
~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); } ~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); }
@@ -29,10 +32,11 @@ namespace ColumnLynx::Net {
SessionState(SessionState&&) = default; SessionState(SessionState&&) = default;
SessionState& operator=(SessionState&&) = default; SessionState& operator=(SessionState&&) = default;
explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24)) : aesKey(k) { explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), uint32_t clientIP = 0, uint32_t serverIP = 0, uint64_t id = 0) : aesKey(k), clientTunIP(clientIP), serverTunIP(serverIP), sessionID(id) {
expires = created + ttl; expires = created + ttl;
} }
// Set the UDP endpoint
void setUDPEndpoint(const asio::ip::udp::endpoint& ep) { void setUDPEndpoint(const asio::ip::udp::endpoint& ep) {
udpEndpoint = ep; udpEndpoint = ep;
} }
@@ -40,21 +44,31 @@ namespace ColumnLynx::Net {
class SessionRegistry { class SessionRegistry {
public: public:
// Return a reference to the Session Registry instance
static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; } static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; }
// Insert or replace // Insert or replace a session entry
void put(uint64_t sessionID, std::shared_ptr<SessionState> state) { void put(uint64_t sessionID, std::shared_ptr<SessionState> state) {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
mSessions[sessionID] = std::move(state); mSessions[sessionID] = std::move(state);
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
} }
// Lookup // Lookup a session entry by session ID
std::shared_ptr<const SessionState> get(uint64_t sessionID) const { std::shared_ptr<const SessionState> get(uint64_t sessionID) const {
std::shared_lock lock(mMutex); std::shared_lock lock(mMutex);
auto it = mSessions.find(sessionID); auto it = mSessions.find(sessionID);
return (it == mSessions.end()) ? nullptr : it->second; return (it == mSessions.end()) ? nullptr : it->second;
} }
// Lookup a session entry by IPv4
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const {
std::shared_lock lock(mMutex);
auto it = mIPSessions.find(ip);
return (it == mIPSessions.end()) ? nullptr : it->second;
}
// Get a snapshot of the Session Registry
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const { std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const {
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap; std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex); std::shared_lock lock(mMutex);
@@ -62,7 +76,7 @@ namespace ColumnLynx::Net {
return snap; return snap;
} }
// Remove // Remove a session by ID
void erase(uint64_t sessionID) { void erase(uint64_t sessionID) {
std::unique_lock lock(mMutex); std::unique_lock lock(mMutex);
mSessions.erase(sessionID); mSessions.erase(sessionID);
@@ -79,10 +93,56 @@ namespace ColumnLynx::Net {
++it; ++it;
} }
} }
for (auto it = mIPSessions.begin(); it != mIPSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mIPSessions.erase(it);
} else {
++it;
}
}
}
// Get the number of registered sessions
int size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
}
// IP management (simple for /24 subnet)
// Get the lowest available IPv4 address; Returns 0 if none available
uint32_t getFirstAvailableIP() const {
std::shared_lock lock(mMutex);
uint32_t baseIP = 0x0A0A0002; // 10.10.0.2
// TODO: Expand to support larger subnets
for (uint32_t offset = 0; offset < 254; offset++) {
uint32_t candidateIP = baseIP + offset;
if (mSessionIPs.find(candidateIP) == mSessionIPs.end()) {
return candidateIP;
}
return 0; // Unavailable
}
}
// Lock an IP as assigned to a specific session
void lockIP(uint64_t sessionID, uint32_t ip) {
std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip;
}
// Unlock the IP associated with a given session
void deallocIP(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessionIPs.erase(sessionID);
} }
private: private:
mutable std::shared_mutex mMutex; mutable std::shared_mutex mMutex;
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> mSessions; std::unordered_map<uint64_t, std::shared_ptr<SessionState>> mSessions;
std::unordered_map<uint64_t, uint32_t> mSessionIPs;
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mIPSessions;
}; };
} }

View File

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

View File

@@ -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

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

View File

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

View File

@@ -16,6 +16,7 @@
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/session_registry.hpp> #include <columnlynx/common/net/session_registry.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
namespace ColumnLynx::Net::TCP { namespace ColumnLynx::Net::TCP {
class TCPConnection : public std::enable_shared_from_this<TCPConnection> { class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
@@ -32,56 +33,19 @@ namespace ColumnLynx::Net::TCP {
return conn; return conn;
} }
void start() { // Start a TCP Connection (Handler for an incoming connection)
mHandler->onMessage([this](AnyMessageType type, const std::string& data) { void start();
mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data); // Send a message to the TCP client
}); void sendMessage(ServerMessageType type, const std::string& data = "");
// Set callback for disconnects
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb);
// Disconnect the client
void disconnect();
mHandler->onDisconnect([this](const asio::error_code& ec) { // Get the assigned session ID
Utils::log("Client disconnected: " + mHandler->socket().remote_endpoint().address().to_string() + " - " + ec.message()); uint64_t getSessionID() const;
disconnect(); // Get the assigned AES key; You should probably access this via the Session Registry instead
}); std::array<uint8_t, 32> getAESKey() const;
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;
}
private: private:
TCPConnection(asio::ip::tcp::socket socket, Utils::LibSodiumWrapper* sodiumWrapper) TCPConnection(asio::ip::tcp::socket socket, Utils::LibSodiumWrapper* sodiumWrapper)
@@ -93,164 +57,10 @@ namespace ColumnLynx::Net::TCP {
mLastHeartbeatSent(std::chrono::steady_clock::now()) mLastHeartbeatSent(std::chrono::steady_clock::now())
{} {}
void mStartHeartbeat() { // Start the heartbeat routine
auto self = shared_from_this(); void mStartHeartbeat();
mHeartbeatTimer.expires_after(std::chrono::seconds(5)); // Handle an incoming TCP message
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) { void mHandleMessage(ClientMessageType type, const std::string& data);
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;
}
}
std::shared_ptr<MessageHandler> mHandler; std::shared_ptr<MessageHandler> mHandler;
std::function<void(std::shared_ptr<TCPConnection>)> mOnDisconnect; std::function<void(std::shared_ptr<TCPConnection>)> mOnDisconnect;

View File

@@ -16,27 +16,65 @@
#include <columnlynx/common/utils.hpp> #include <columnlynx/common/utils.hpp>
#include <columnlynx/server/net/tcp/tcp_connection.hpp> #include <columnlynx/server/net/tcp/tcp_connection.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
namespace ColumnLynx::Net::TCP { namespace ColumnLynx::Net::TCP {
class TCPServer { class TCPServer {
public: public:
TCPServer(asio::io_context& ioContext, uint16_t port, Utils::LibSodiumWrapper* sodiumWrapper, bool* hostRunning) TCPServer(asio::io_context& ioContext,
: mIoContext(ioContext), mAcceptor(ioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), mSodiumWrapper(sodiumWrapper), mHostRunning(hostRunning) uint16_t port,
Utils::LibSodiumWrapper* sodiumWrapper,
bool* hostRunning, bool ipv4Only = false)
: mIoContext(ioContext),
mAcceptor(ioContext),
mSodiumWrapper(sodiumWrapper),
mHostRunning(hostRunning)
{ {
// Preload the config map
mRawServerConfig = Utils::getConfigMap("server_config");
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)); Utils::log("Started TCP server on port " + std::to_string(port));
mStartAccept(); mStartAccept();
} }
// Stop the TCP Server
void stop(); void stop();
private: private:
// Start accepting clients via TCP
void mStartAccept(); void mStartAccept();
asio::io_context &mIoContext; asio::io_context &mIoContext;
asio::ip::tcp::acceptor mAcceptor; asio::ip::tcp::acceptor mAcceptor;
std::unordered_set<TCPConnection::pointer> mClients; std::unordered_set<TCPConnection::pointer> mClients;
Utils::LibSodiumWrapper *mSodiumWrapper; Utils::LibSodiumWrapper *mSodiumWrapper;
bool* mHostRunning; bool* mHostRunning;
std::unordered_map<std::string, std::string> mRawServerConfig;
}; };
} }

View File

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

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/tcp/tcp_client.hpp>
#include <columnlynx/client/net/udp/udp_client.hpp> #include <columnlynx/client/net/udp/udp_client.hpp>
#include <cxxopts/cxxopts.hpp> #include <cxxopts/cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp; using asio::ip::tcp;
using namespace ColumnLynx::Utils; using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net;
using namespace ColumnLynx;
volatile sig_atomic_t done = 0; volatile sig_atomic_t done = 0;
void signalHandler(int signum) { void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) { if (signum == SIGINT || signum == SIGTERM) {
log("Received termination signal. Shutting down client."); //log("Received termination signal. Shutting down client.");
done = 1; done = 1;
} }
} }
@@ -38,11 +41,17 @@ int main(int argc, char** argv) {
options.add_options() options.add_options()
("h,help", "Print help") ("h,help", "Print help")
("s,server", "Server address", cxxopts::value<std::string>()->default_value("127.0.0.1")) ("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); auto result = options.parse(argc, argv);
if (result.count("help")) { if (result.count("help")) {
std::cout << options.help() << std::endl; std::cout << options.help() << std::endl;
std::cout << "This software is licensed under the GPLv2-only license OR the GPLv3 license.\n";
std::cout << "Copyright (C) 2025, The ColumnLynx Contributors.\n";
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
return 0; return 0;
} }
@@ -51,16 +60,25 @@ int main(int argc, char** argv) {
try { try {
log("ColumnLynx Client, Version " + getVersion()); 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(); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper.getPublicKey(), 32));
debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper.getPrivateKey(), 64));
std::array<uint8_t, 32> aesKey = {0}; // Defualt zeroed state until modified by handshake std::array<uint8_t, 32> aesKey = {0}; // Defualt zeroed state until modified by handshake
uint64_t sessionID = 0; uint64_t sessionID = 0;
asio::io_context io; asio::io_context io;
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, &sodiumWrapper, &aesKey, &sessionID); auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode, tun);
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, &aesKey, &sessionID); auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, &aesKey, &sessionID, tun);
client->start(); client->start();
udpClient->start(); udpClient->start();
@@ -69,31 +87,33 @@ int main(int argc, char** argv) {
std::thread ioThread([&io]() { std::thread ioThread([&io]() {
io.run(); io.run();
}); });
ioThread.detach(); //ioThread.join();
log("Client connected to " + host + ":" + port); log("Client connected to " + host + ":" + port);
// Client is running // Client is running
while ((!done && client->isConnected()) || !client->isHandshakeComplete()) { while ((client->isConnected() || !client->isHandshakeComplete()) && !done) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Temp wait auto packet = tun->readPacket();
if (!client->isConnected() || done) {
if (client->isHandshakeComplete()) { break; // Bail out if connection died or signal set while blocked
// Send a test UDP message every 5 seconds after handshake is complete
static auto lastSendTime = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastSendTime).count() >= 5) {
udpClient->sendMessage("Hello from UDP client!");
lastSendTime = now;
}
} }
if (packet.empty()) {
continue;
}
udpClient->sendMessage(std::string(packet.begin(), packet.end()));
} }
log("Client shutting down."); log("Client shutting down.");
udpClient->stop(); udpClient->stop();
client->disconnect(); client->disconnect();
io.stop(); io.stop();
ioThread.join();
if (ioThread.joinable())
ioThread.join();
} catch (const std::exception& e) { } catch (const std::exception& e) {
error("Client error: " + std::string(e.what())); error("Client error: " + std::string(e.what()));
} }
} }

View File

@@ -0,0 +1,296 @@
// 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; // Voodoo black magic
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
);*/
payload.insert(payload.end(),
mLibSodiumWrapper->getPublicKey(),
mLibSodiumWrapper->getPublicKey() + crypto_sign_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::cbe64toh(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,121 @@
// 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;
}
//Utils::debug("Using AES key: " + Utils::bytesToHexString(mAesKeyRef->data(), 32));
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
*mAesKeyRef, hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
);
std::vector<uint8_t> packet;
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::debug("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"
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
);
if (plaintext.empty()) {
Utils::warn("UDP Client failed to decrypt received packet.");
return;
}
Utils::debug("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
// Write to TUN
if (mTunRef) {
mTunRef->writePacket(plaintext);
}
}
}

View File

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

View File

@@ -0,0 +1,227 @@
// 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>
// This is all fucking voodoo dark magic.
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,76 @@
#include <columnlynx/server/net/udp/udp_server.hpp> #include <columnlynx/server/net/udp/udp_server.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp> #include <columnlynx/common/libsodium_wrapper.hpp>
#include <unordered_set> #include <unordered_set>
#include <cxxopts/cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
using asio::ip::tcp; using asio::ip::tcp;
using namespace ColumnLynx::Utils; using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net::TCP; using namespace ColumnLynx::Net::TCP;
using namespace ColumnLynx::Net::UDP; using namespace ColumnLynx::Net::UDP;
using namespace ColumnLynx::Net;
using namespace ColumnLynx;
volatile sig_atomic_t done = 0; volatile sig_atomic_t done = 0;
/*void signalHandler(int signum) { void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) { if (signum == SIGINT || signum == SIGTERM) {
log("Received termination signal. Shutting down server gracefully."); log("Received termination signal. Shutting down server gracefully.");
done = 1; done = 1;
} }
}*/ }
int main(int argc, char** argv) { 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"))
("c,config", "Specify config file location", cxxopts::value<std::string>()->default_value("config.json"));
PanicHandler::init(); PanicHandler::init();
try { try {
// Catch SIGINT and SIGTERM for graceful shutdown auto result = options.parse(argc, argv);
/*struct sigaction action; if (result.count("help")) {
memset(&action, 0, sizeof(struct sigaction)); std::cout << options.help() << std::endl;
action.sa_handler = signalHandler; std::cout << "This software is licensed under the GPLv2-only license OR the GPLv3 license.\n";
sigaction(SIGINT, &action, nullptr); std::cout << "Copyright (C) 2025, The ColumnLynx Contributors.\n";
sigaction(SIGTERM, &action, nullptr);*/ std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
return 0;
}
bool ipv4Only = result["ipv4-only"].as<bool>();
std::string configPath = result["config"].as<std::string>();
log("ColumnLynx Server, Version " + getVersion()); log("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) // Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory)
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper(); LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES)); log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES));
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; bool hostRunning = true;
asio::io_context io; asio::io_context io;
auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning); auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning); auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning, ipv4Only, tun);
asio::signal_set signals(io, SIGINT, SIGTERM); asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](const std::error_code&, int) { signals.async_wait([&](const std::error_code&, int) {
@@ -72,7 +102,21 @@ int main(int argc, char** argv) {
log("Server started on port " + std::to_string(serverPort())); log("Server started on port " + std::to_string(serverPort()));
while (!done) { while (!done) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); auto packet = tun->readPacket();
if (packet.empty()) {
continue;
}
const uint8_t* ip = packet.data();
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset in IPv6-mapped header
auto session = SessionRegistry::getInstance().getByIP(dstIP);
if (!session) {
Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipv4ToString(dstIP));
continue;
}
udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end()));
} }
log("Shutting down server..."); log("Shutting down server...");

View File

@@ -0,0 +1,263 @@
// 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 + ".");
PublicKey signPk;
std::memcpy(signPk.data(), data.data() + 1, std::min(data.size() - 1, sizeof(signPk)));
// We can safely store this without further checking, the client will need to send the encrypted AES key in a way where they must possess the corresponding private key anyways.
crypto_sign_ed25519_pk_to_curve25519(mConnectionPublicKey.data(), signPk.data()); // Store the client's public encryption key key (for identification)
Utils::debug("Client " + reqAddr + " converted public encryption key: " + Utils::bytesToHexString(mConnectionPublicKey.data(), 32));
Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size()));
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys();
if (std::find(whitelistedKeys.begin(), whitelistedKeys.end(), Utils::bytesToHexString(signPk.data(), signPk.size())) == whitelistedKeys.end()) {
Utils::warn("Non-whitelisted client attempted to connect, terminating. Client IP: " + reqAddr);
disconnect();
return;
}
Utils::debug("Client " + reqAddr + " passed authorized_keys");
mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist
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();
if (clientIP == 0) {
Utils::warn("Out of available IPs! Disconnecting client " + reqAddr);
disconnect();
return;
}
Protocol::TunConfig tunConfig{};
tunConfig.version = Utils::protocolVersion();
tunConfig.prefixLength = 24;
tunConfig.mtu = 1420;
tunConfig.serverIP = htonl(0x0A0A0001); // 10.10.0.1
tunConfig.clientIP = htonl(clientIP); // 10.10.0.X
tunConfig.dns1 = htonl(0x08080808); // 8.8.8.8
tunConfig.dns2 = 0;
SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP);
uint64_t sessionIDNet = Utils::chtobe64(mConnectionSessionID);
std::vector<uint8_t> payload(sizeof(uint64_t) + sizeof(tunConfig));
std::memcpy(payload.data(), &sessionIDNet, sizeof(uint64_t));
std::memcpy(payload.data() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig));
std::vector<uint8_t> encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
payload.data(), payload.size(),
mConnectionAESKey, symNonce
);
mHandler->sendMessage(ServerMessageType::HANDSHAKE_EXCHANGE_KEY_CONFIRM, Utils::uint8ArrayToString(encryptedPayload.data(), encryptedPayload.size()));
// Add to session registry
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned (" + std::to_string(mConnectionSessionID) + ").");
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 { namespace ColumnLynx::Net::TCP {
void TCPServer::mStartAccept() { 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( mAcceptor.async_accept(
[this](asio::error_code ec, asio::ip::tcp::socket socket) { [this](asio::error_code ec, asio::ip::tcp::socket socket) {
if (ec) { if (ec) {

View File

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

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.