27 Commits

Author SHA1 Message Date
f99036c523 Merge branch 'beta' 2025-12-29 20:28:34 +01:00
3eadd41a00 Merge branch 'dev' into beta 2025-12-29 20:28:15 +01:00
8923f45356 Add routeset to win32 2025-12-29 19:33:57 +01:00
471224b043 Merge branch 'beta' - b0.3 2025-12-29 19:07:16 +01:00
714aa52f98 Merge branch 'dev' into beta 2025-12-29 19:06:59 +01:00
d5bf741650 Test Fix double SIGTERM 2025-12-29 19:05:25 +01:00
ae507c3fb9 Test Fix panic on disconnect 2025-12-29 19:02:22 +01:00
072fb69a4a test3 2025-12-29 18:30:54 +01:00
6031d9655a test3 2025-12-29 18:29:42 +01:00
16cd980c0a test2 2025-12-29 18:11:05 +01:00
68a825b7df test 2025-12-29 18:02:42 +01:00
17cc314c26 Fix wintun - connects but errors 2025-12-18 11:12:11 +01:00
225aa2a55d Create wintun interface if not found 2025-12-18 07:55:35 +01:00
cab1362053 Kinda working Windows version
Needs these DLLs:
- libgcc_s_seh-1.dll
- libstdc++-6.dll
- libwinpthread-1.dll
- wintun.dll
2025-12-17 19:11:28 +01:00
c047cb90f0 Test 1: Make WinTun work 2025-12-17 18:34:07 +01:00
5e3aef78a5 Test: proper ipv6 support, still sending v4 to client tun 2025-12-10 16:02:55 +01:00
4ff33ffdab Add IPv6 helpers 2025-12-10 15:08:29 +01:00
7d9018043d Add resetIP() method, need impl 2025-12-09 20:14:06 +01:00
cb0f674c52 Merge branch 'beta' - Version b0.1
macOS Support
2025-12-08 17:38:05 +01:00
a2ecc589f8 Merge branch 'dev' into beta - Version b0.1 2025-12-08 17:37:44 +01:00
b50b594d68 Added support for macOS-specific kernel stuff. 2025-12-08 17:01:41 +01:00
842752cd88 Version b0.1 (Beta 0.1) - Refactor some stuff and move session_registry to .cpp file 2025-12-04 15:48:18 +01:00
33bbd7cce6 Merge branch 'beta' - Alpha 0.6
This version adds Dynamic IP assignment based on config.
2025-12-02 18:47:58 +01:00
f9c5c56a1b Merge branch 'beta'
This is the merge of version a0.5 into master.
This version adds general authentication of the client and server, and control of connection via key whitelisting.
Also added loading of keypairs via a config file system.
2025-11-28 19:31:01 +01:00
17dd504a7a Merge pull request 'First working alpha, version a0.4' (#7) from beta into master
Reviewed-on: #7
2025-11-18 20:09:11 +00:00
9f52bdd54c Merge pull request 'beta' (#4) from beta into master
Reviewed-on: #4
2025-11-10 15:58:29 +00:00
29e90938c5 Merge pull request 'beta - Update License' (#2) from beta into master
Reviewed-on: #2
2025-11-10 15:15:31 +00:00
20 changed files with 618 additions and 233 deletions

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ CMakeUserPresets.json
build/
.vscode/
.DS_Store

View File

@@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx
VERSION 0.0.6
VERSION 0.3.0
LANGUAGES CXX
)
@@ -80,15 +80,6 @@ FetchContent_MakeAvailable(Sodium)
FetchContent_MakeAvailable(asio)
FetchContent_MakeAvailable(cxxopts)
# 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
# ---------------------------------------------------------
@@ -108,7 +99,17 @@ endforeach()
# ---------------------------------------------------------
file(GLOB_RECURSE COMMON_SRC CONFIGURE_DEPENDS src/common/*.cpp)
add_library(common STATIC ${COMMON_SRC})
target_link_libraries(common PUBLIC sodium OpenSSL::SSL OpenSSL::Crypto cxxopts::cxxopts)
target_link_libraries(common PUBLIC sodium cxxopts::cxxopts)
if (WIN32)
target_link_libraries(common PUBLIC
ws2_32
iphlpapi
advapi32
mswsock
)
endif()
target_include_directories(common PUBLIC
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include
@@ -122,7 +123,12 @@ target_compile_definitions(common PUBLIC ASIO_STANDALONE)
# ---------------------------------------------------------
file(GLOB_RECURSE CLIENT_SRC CONFIGURE_DEPENDS src/client/*.cpp)
add_executable(client ${CLIENT_SRC})
target_link_libraries(client PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto cxxopts::cxxopts)
target_link_libraries(client PRIVATE common sodium cxxopts::cxxopts)
if (WIN32)
target_link_libraries(client PRIVATE
dbghelp
)
endif()
target_include_directories(client PRIVATE
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include
@@ -137,7 +143,12 @@ set_target_properties(client PROPERTIES OUTPUT_NAME "columnlynx_client")
# ---------------------------------------------------------
file(GLOB_RECURSE SERVER_SRC CONFIGURE_DEPENDS src/server/*.cpp)
add_executable(server ${SERVER_SRC})
target_link_libraries(server PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto cxxopts::cxxopts)
target_link_libraries(server PRIVATE common sodium cxxopts::cxxopts)
if (WIN32)
target_link_libraries(server PRIVATE
dbghelp
)
endif()
target_include_directories(server PRIVATE
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include

View File

@@ -11,10 +11,6 @@
#include <columnlynx/common/utils.hpp>
#include <array>
#include <vector>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <memory>
#include <cstring>

View File

@@ -10,6 +10,9 @@
#include <array>
#include <cmath>
#include <sodium.h>
#include <mutex>
#include <atomic>
#include <asio.hpp>
#include <columnlynx/common/utils.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
@@ -49,107 +52,36 @@ namespace ColumnLynx::Net {
static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; }
// Insert or replace a session entry
void put(uint64_t sessionID, std::shared_ptr<SessionState> state) {
std::unique_lock lock(mMutex);
mSessions[sessionID] = std::move(state);
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
}
void put(uint64_t sessionID, std::shared_ptr<SessionState> state);
// Lookup a session entry by session ID
std::shared_ptr<const SessionState> get(uint64_t sessionID) const {
std::shared_lock lock(mMutex);
auto it = mSessions.find(sessionID);
return (it == mSessions.end()) ? nullptr : it->second;
}
std::shared_ptr<const SessionState> get(uint64_t sessionID) const;
// 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;
}
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const;
// Get a snapshot of the Session Registry
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const {
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex);
snap = mSessions;
return snap;
}
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const;
// Remove a session by ID
void erase(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessions.erase(sessionID);
}
void erase(uint64_t sessionID);
// Cleanup expired sessions
void cleanupExpired() {
std::unique_lock lock(mMutex);
auto now = std::chrono::steady_clock::now();
for (auto it = mSessions.begin(); it != mSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mSessions.erase(it);
} else {
++it;
}
}
for (auto it = mIPSessions.begin(); it != mIPSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mIPSessions.erase(it);
} else {
++it;
}
}
}
void cleanupExpired();
// Get the number of registered sessions
int size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
}
int size() const;
// IP management
// Get the lowest available IPv4 address; Returns 0 if none available
uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const {
std::shared_lock lock(mMutex);
uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const;
uint32_t hostCount = (1u << (32 - mask));
uint32_t firstHost = 2;
uint32_t lastHost = hostCount - 2;
// Lock IP to session ID; Do NOT call before put() - You will segfault!
void lockIP(uint64_t sessionID, uint32_t ip);
for (uint32_t offset = firstHost; offset <= lastHost; offset++) {
uint32_t candidateIP = baseIP + offset;
if (mIPSessions.find(candidateIP) == mIPSessions.end()) {
return candidateIP;
}
}
return 0;
}
void lockIP(uint64_t sessionID, uint32_t ip) {
std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip;
/*if (mIPSessions.find(sessionID) == mIPSessions.end()) {
Utils::debug("yikes");
}*/
mIPSessions[ip] = mSessions.find(sessionID)->second;
}
void deallocIP(uint64_t sessionID) {
std::unique_lock lock(mMutex);
auto it = mSessionIPs.find(sessionID);
if (it != mSessionIPs.end()) {
uint32_t ip = it->second;
mIPSessions.erase(ip);
mSessionIPs.erase(it);
}
}
// Unlock IP from session ID
void deallocIP(uint64_t sessionID);
private:
mutable std::shared_mutex mMutex;

View File

@@ -26,12 +26,16 @@
#include <sys/ioctl.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/poll.h>
#elif defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define WINTUN_STATIC
#include <windows.h>
#include <ws2tcpip.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <locale>
#include <codecvt>
#include <wintun/wintun.h>
#pragma comment(lib, "advapi32.lib")
#endif
namespace ColumnLynx::Net {
@@ -43,15 +47,21 @@ namespace ColumnLynx::Net {
bool configureIP(uint32_t clientIP, uint32_t serverIP,
uint8_t prefixLen, uint16_t mtu);
void resetIP();
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) {
static inline std::string ipv4ToString(uint32_t ip, bool flip = true) {
struct in_addr addr;
if (flip)
addr.s_addr = htonl(ip);
else
addr.s_addr = ip;
char buf[INET_ADDRSTRLEN];
if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf)))
@@ -70,6 +80,42 @@ namespace ColumnLynx::Net {
return ntohl(addr.s_addr);
}
static inline std::string ipv6ToString(IPv6Addr &ip,
bool flip = false)
{
struct in6_addr addr;
if (flip) {
IPv6Addr flipped;
for (size_t i = 0; i < 16; ++i)
flipped[i] = ip[15 - i];
memcpy(addr.s6_addr, flipped.data(), 16);
} else {
memcpy(addr.s6_addr, ip.data(), 16);
}
char buf[INET6_ADDRSTRLEN];
if (!inet_ntop(AF_INET6, &addr, buf, sizeof(buf)))
return "::"; // Fallback
return std::string(buf);
}
static inline IPv6Addr stringToIpv6(const std::string &ipStr)
{
IPv6Addr result{};
struct in6_addr addr;
if (inet_pton(AF_INET6, ipStr.c_str(), &addr) != 1) {
// "::"
result.fill(0);
return result;
}
memcpy(result.data(), addr.s6_addr, 16);
return result;
}
static inline uint32_t prefixLengthToNetmask(uint8_t prefixLen) {
if (prefixLen == 0) return 0;
uint32_t mask = (0xFFFFFFFF << (32 - prefixLen)) & 0xFFFFFFFF;
@@ -84,7 +130,9 @@ namespace ColumnLynx::Net {
std::string mIfName;
int mFd; // POSIX
#if defined(_WIN32)
HANDLE mHandle; // Windows
WINTUN_ADAPTER_HANDLE mAdapter = nullptr;
WINTUN_SESSION_HANDLE mSession = nullptr;
HANDLE mHandle = nullptr;
#endif
};
}

View File

@@ -186,7 +186,7 @@ namespace ColumnLynx::Utils {
// 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) {
std::cerr << "\n***\033[31m MAIN THREAD PANIC! \033[0m***\n";
std::cerr << "\n***\033[31m MASTER THREAD PANIC! \033[0m***\n";
std::cerr << "Reason: " << reason << "\n";
std::cerr << "Dumping panic trace...\n";

View File

@@ -42,7 +42,7 @@ namespace ColumnLynx::Net::TCP {
// Set callback for disconnects
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb);
// Disconnect the client
void disconnect();
void disconnect(bool echo = true);
// Get the assigned session ID
uint64_t getSessionID() const;
@@ -76,5 +76,6 @@ namespace ColumnLynx::Net::TCP {
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
std::chrono::steady_clock::time_point mLastHeartbeatSent;
int mMissedHeartbeats = 0;
std::string mRemoteIP; // Cached remote IP to avoid calling remote_endpoint() on closed sockets
};
}

View File

@@ -34,24 +34,33 @@ namespace ColumnLynx::Net::TCP {
// Preload the config map
mRawServerConfig = Utils::getConfigMap("server_config", {"NETWORK", "SUBNET_MASK"});
asio::error_code ec;
asio::error_code ec_open, ec_v6only, ec_bind;
if (!ipv4Only) {
// Try IPv6 first (dual-stack check)
// Try IPv6 (dual-stack if supported)
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);
mAcceptor.open(endpoint_v6.protocol(), ec_open);
if (!ec_open) {
// Try enabling dual-stack, but DO NOT treat failure as fatal
mAcceptor.set_option(asio::ip::v6_only(false), ec_v6only);
// Try binding IPv6
mAcceptor.bind(endpoint_v6, ec_bind);
}
}
// Fallback to IPv4 if anything failed
if (ec || ipv4Only) {
Utils::warn("TCP: IPv6 unavailable (" + ec.message() + "), falling back to IPv4 only");
// If IPv6 bind failed OR IPv6 open failed OR forced IPv4-only
if (ipv4Only || ec_open || ec_bind) {
if (!ipv4Only)
Utils::warn("TCP: IPv6 unavailable (open=" + ec_open.message() +
", bind=" + ec_bind.message() +
"), falling back to IPv4 only");
asio::ip::tcp::endpoint endpoint_v4(asio::ip::tcp::v4(), port);
mAcceptor.close(); // ensure clean state
mAcceptor.close(); // guarantee clean state
mAcceptor.open(endpoint_v4.protocol());
mAcceptor.bind(endpoint_v4);
}

View File

@@ -16,24 +16,37 @@ namespace ColumnLynx::Net::UDP {
UDPServer(asio::io_context& ioContext, uint16_t port, std::shared_ptr<bool> hostRunning, bool ipv4Only = false, std::shared_ptr<VirtualInterface> tun = nullptr)
: mSocket(ioContext), mHostRunning(hostRunning), mTun(tun)
{
asio::error_code ec;
asio::error_code ec_open, ec_v6only, ec_bind;
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);
// Try opening IPv6 socket
mSocket.open(endpoint_v6.protocol(), ec_open);
if (!ec_open) {
// Try enabling dual-stack (non fatal if it fails)
mSocket.set_option(asio::ip::v6_only(false), ec_v6only);
// Attempt bind
mSocket.bind(endpoint_v6, ec_bind);
}
}
// Fallback to IPv4 if anything failed
if (ec || ipv4Only) {
Utils::warn("UDP: IPv6 unavailable (" + ec.message() + "), falling back to IPv4 only");
// Fallback to IPv4 if IPv6 is unusable
if (ipv4Only || ec_open || ec_bind) {
if (!ipv4Only) {
Utils::warn(
"UDP: IPv6 unavailable (open=" + ec_open.message() +
", bind=" + ec_bind.message() +
"), falling back to IPv4 only"
);
}
asio::ip::udp::endpoint endpoint_v4(asio::ip::udp::v4(), port);
mSocket.close(); // ensure clean state
mSocket.close();
mSocket = asio::ip::udp::socket(ioContext); // fully reset internal state
mSocket.open(endpoint_v4.protocol());
mSocket.bind(endpoint_v4);
}

View File

@@ -21,18 +21,19 @@ volatile sig_atomic_t done = 0;
void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
//log("Received termination signal. Shutting down client.");
done = 1;
}
}
int main(int argc, char** argv) {
// Capture SIGINT and SIGTERM for graceful shutdown
#if !defined(_WIN32)
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = signalHandler;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);
#endif
PanicHandler::init();
@@ -68,7 +69,7 @@ int main(int argc, char** argv) {
log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details.");
#if defined(__WIN32__)
WintunInitialize();
//WintunInitialize();
#endif
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
@@ -95,14 +96,18 @@ int main(int argc, char** argv) {
});
//ioThread.join();
log("Client connected to " + host + ":" + port);
log("Attempting connection to " + host + ":" + port);
debug("Client connection flag: " + std::to_string(client->isConnected()));
debug("Client handshake flag: " + std::to_string(client->isHandshakeComplete()));
debug("isDone flag: " + std::to_string(done));
// Client is running
while ((client->isConnected() || !client->isHandshakeComplete()) && !done) {
//debug("Client connection flag: " + std::to_string(client->isConnected()));
auto packet = tun->readPacket();
if (!client->isConnected() || done) {
/*if (!client->isConnected() || done) {
break; // Bail out if connection died or signal set while blocked
}
}*/
if (packet.empty()) {
continue;

View File

@@ -3,7 +3,7 @@
// 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>
//#include <arpa/inet.h>
namespace ColumnLynx::Net::TCP {
void TCPClient::start() {
@@ -13,22 +13,35 @@ namespace ColumnLynx::Net::TCP {
if (!ec) {
asio::async_connect(mSocket, endpoints,
[this, self](asio::error_code ec, const tcp::endpoint&) {
if (!NetHelper::isExpectedDisconnect(ec)) {
if (!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);
});
// Close only after peer FIN to avoid RSTs
mHandler->onDisconnect([this](const asio::error_code& ec) {
asio::error_code ec2;
if (mHandler) {
mHandler->socket().close(ec2);
}
mConnected = false;
Utils::log(std::string("Server disconnected: ") + ec.message());
});
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
try {
asio::ip::make_address(mHost);
self->mIsHostDomain = false; // IPv4 or IPv6 literal
} catch (const asio::system_error&) {
self->mIsHostDomain = true; // hostname / domain
}
std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
@@ -46,8 +59,10 @@ namespace ColumnLynx::Net::TCP {
mStartHeartbeat();
} else {
if (!NetHelper::isExpectedDisconnect(ec)) {
Utils::error("Client connect failed: " + ec.message());
}
}
});
} else {
Utils::error("Client resolve failed: " + ec.message());
@@ -77,18 +92,14 @@ namespace ColumnLynx::Net::TCP {
asio::error_code ec;
mHeartbeatTimer.cancel();
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
// Half-close: stop sending, keep reading until peer FIN
mHandler->socket().shutdown(tcp::socket::shutdown_send, 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.");
// Do not close immediately; rely on onDisconnect to finalize
Utils::log("Client initiated graceful disconnect (half-close).");
}
}
@@ -269,6 +280,12 @@ namespace ColumnLynx::Net::TCP {
disconnect(false);
}
break;
case ServerMessageType::KILL_CONNECTION:
Utils::warn("Server is killing the connection: " + data);
if (mConnected) {
disconnect(false);
}
break;
default:
Utils::log("Received unknown message type from server.");
break;

View File

@@ -6,10 +6,41 @@
namespace ColumnLynx::Net::UDP {
void UDPClient::start() {
// TODO: Add IPv6
auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort);
asio::error_code ec;
// Resolve using an unspecified protocol (allows both IPv4 and IPv6)
auto endpoints = mResolver.resolve(
asio::ip::udp::v6(), // Try IPv6 first (dual-stack with v4)
mHost,
mPort,
ec
);
if (ec) {
// If IPv6 fails (host has no AAAA), try IPv4
endpoints = mResolver.resolve(
asio::ip::udp::v4(),
mHost,
mPort,
ec
);
}
if (ec) {
Utils::error("UDP resolve failed: " + ec.message());
return;
}
// Use whichever endpoint resolved
mRemoteEndpoint = *endpoints.begin();
mSocket.open(asio::ip::udp::v4());
// Open socket using the resolved endpoint's protocol
mSocket.open(mRemoteEndpoint.protocol(), ec);
if (ec) {
Utils::error("UDP socket open failed: " + ec.message());
return;
}
Utils::log("UDP Client ready to send to " + mRemoteEndpoint.address().to_string() + ":" + std::to_string(mRemoteEndpoint.port()));
}

View File

@@ -0,0 +1,100 @@
// session_registry.cpp - Session Registry 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/common/net/session_registry.hpp>
namespace ColumnLynx::Net {
void SessionRegistry::put(uint64_t sessionID, std::shared_ptr<SessionState> state) {
std::unique_lock lock(mMutex);
mSessions[sessionID] = std::move(state);
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
}
std::shared_ptr<const SessionState> SessionRegistry::get(uint64_t sessionID) const {
std::shared_lock lock(mMutex);
auto it = mSessions.find(sessionID);
return (it == mSessions.end()) ? nullptr : it->second;
}
std::shared_ptr<const SessionState> SessionRegistry::getByIP(uint32_t ip) const {
std::shared_lock lock(mMutex);
auto it = mIPSessions.find(ip);
return (it == mIPSessions.end()) ? nullptr : it->second;
}
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> SessionRegistry::snapshot() const {
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex);
snap = mSessions;
return snap;
}
void SessionRegistry::erase(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessions.erase(sessionID);
}
void SessionRegistry::cleanupExpired() {
std::unique_lock lock(mMutex);
auto now = std::chrono::steady_clock::now();
for (auto it = mSessions.begin(); it != mSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mSessions.erase(it);
} else {
++it;
}
}
for (auto it = mIPSessions.begin(); it != mIPSessions.end(); ) {
if (it->second && it->second->expires <= now) {
it = mIPSessions.erase(it);
} else {
++it;
}
}
}
int SessionRegistry::size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
}
uint32_t SessionRegistry::getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const {
std::shared_lock lock(mMutex);
uint32_t hostCount = (1u << (32 - mask));
uint32_t firstHost = 2;
uint32_t lastHost = hostCount - 2;
for (uint32_t offset = firstHost; offset <= lastHost; offset++) {
uint32_t candidateIP = baseIP + offset;
if (mIPSessions.find(candidateIP) == mIPSessions.end()) {
return candidateIP;
}
}
return 0;
}
void SessionRegistry::lockIP(uint64_t sessionID, uint32_t ip) {
std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip;
/*if (mIPSessions.find(sessionID) == mIPSessions.end()) {
Utils::debug("yikes");
}*/
mIPSessions[ip] = mSessions.find(sessionID)->second;
}
void SessionRegistry::deallocIP(uint64_t sessionID) {
std::unique_lock lock(mMutex);
auto it = mSessionIPs.find(sessionID);
if (it != mSessionIPs.end()) {
uint32_t ip = it->second;
mIPSessions.erase(ip);
mSessionIPs.erase(it);
}
}
}

View File

@@ -43,14 +43,20 @@ namespace ColumnLynx::Net::TCP {
auto self = shared_from_this();
asio::async_read(mSocket, asio::buffer(mHeader),
[this, self](asio::error_code ec, std::size_t) {
if (!NetHelper::isExpectedDisconnect(ec)) {
if (!ec) {
mCurrentType = decodeMessageType(mHeader[0]);
uint16_t len = (mHeader[1] << 8) | mHeader[2];
mReadBody(len);
} else {
if (!NetHelper::isExpectedDisconnect(ec)) {
Utils::error("Header read failed: " + ec.message());
}
// Connection closed, trigger disconnect handler
if (mOnDisconnect) {
mOnDisconnect(ec);
}
}
}
);
}
@@ -61,7 +67,7 @@ namespace ColumnLynx::Net::TCP {
asio::async_read(mSocket, asio::buffer(mBody),
[this, self](asio::error_code ec, std::size_t) {
if (!NetHelper::isExpectedDisconnect(ec)) {
if (!ec) {
std::string payload(mBody.begin(), mBody.end());
// Dispatch based on message type
@@ -71,8 +77,10 @@ namespace ColumnLynx::Net::TCP {
mReadHeader(); // Keep listening
} else {
if (!NetHelper::isExpectedDisconnect(ec)) {
Utils::error("Body read failed: " + ec.message());
}
// Connection closed, trigger disconnect handler
if (mOnDisconnect) {
mOnDisconnect(ec);
}

View File

@@ -49,7 +49,7 @@ namespace ColumnLynx::Utils {
}
std::string getVersion() {
return "a0.6";
return "b0.3";
}
unsigned short serverPort() {

View File

@@ -6,6 +6,55 @@
// This is all fucking voodoo dark magic.
#if defined(_WIN32)
static HMODULE gWintun = nullptr;
static WINTUN_OPEN_ADAPTER_FUNC* pWintunOpenAdapter;
static WINTUN_START_SESSION_FUNC* pWintunStartSession;
static WINTUN_END_SESSION_FUNC* pWintunEndSession;
static WINTUN_GET_READ_WAIT_EVENT_FUNC* pWintunGetReadWaitEvent;
static WINTUN_RECEIVE_PACKET_FUNC* pWintunReceivePacket;
static WINTUN_RELEASE_RECEIVE_PACKET_FUNC* pWintunReleaseReceivePacket;
static WINTUN_ALLOCATE_SEND_PACKET_FUNC* pWintunAllocateSendPacket;
static WINTUN_SEND_PACKET_FUNC* pWintunSendPacket;
static WINTUN_CREATE_ADAPTER_FUNC* pWintunCreateAdapter;
static void InitializeWintun()
{
if (gWintun)
return;
gWintun = LoadLibraryExW(
L"wintun.dll",
nullptr,
LOAD_LIBRARY_SEARCH_APPLICATION_DIR
);
if (!gWintun)
throw std::runtime_error("Failed to load wintun.dll");
#define RESOLVE(name, type) \
p##name = reinterpret_cast<type*>( \
GetProcAddress(gWintun, #name)); \
if (!p##name) \
throw std::runtime_error("Missing Wintun symbol: " #name);
RESOLVE(WintunOpenAdapter, WINTUN_OPEN_ADAPTER_FUNC)
RESOLVE(WintunStartSession, WINTUN_START_SESSION_FUNC)
RESOLVE(WintunEndSession, WINTUN_END_SESSION_FUNC)
RESOLVE(WintunGetReadWaitEvent, WINTUN_GET_READ_WAIT_EVENT_FUNC)
RESOLVE(WintunReceivePacket, WINTUN_RECEIVE_PACKET_FUNC)
RESOLVE(WintunReleaseReceivePacket, WINTUN_RELEASE_RECEIVE_PACKET_FUNC)
RESOLVE(WintunAllocateSendPacket, WINTUN_ALLOCATE_SEND_PACKET_FUNC)
RESOLVE(WintunSendPacket, WINTUN_SEND_PACKET_FUNC)
RESOLVE(WintunCreateAdapter, WINTUN_CREATE_ADAPTER_FUNC)
#undef RESOLVE
}
#endif // _WIN32
namespace ColumnLynx::Net {
// ------------------------------ Constructor ------------------------------
VirtualInterface::VirtualInterface(const std::string& ifName)
@@ -28,6 +77,7 @@ namespace ColumnLynx::Net {
#elif defined(__APPLE__)
// ---- macOS: UTUN (system control socket) ----
// TL;DR: macOS doesn't really have a "device file" for TUN/TAP like Linux. Instead we have to request a "system control socket" from the kernel.
mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
if (mFd < 0)
throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno)));
@@ -42,7 +92,7 @@ namespace ColumnLynx::Net {
sc.sc_family = AF_SYSTEM;
sc.ss_sysaddr = AF_SYS_CONTROL;
sc.sc_id = ctlInfo.ctl_id;
sc.sc_unit = 0; // lynx0 (0 = auto-assign)
sc.sc_unit = 0; // 0 = auto-assign next utunX
if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) {
if (errno == EPERM)
@@ -50,31 +100,45 @@ namespace ColumnLynx::Net {
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);
// Retrieve actual utun device name via UTUN_OPT_IFNAME
char ifname[IFNAMSIZ];
socklen_t ifname_len = sizeof(ifname);
if (getsockopt(mFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &ifname_len) == 0) {
mIfName = ifname; // Update to actual assigned name
} else {
mIfName = "utunX";
mIfName = "utun0"; // Fallback (should not happen)
}
#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");
Utils::log("VirtualInterface: opened macOS UTUN: " + mIfName);
WINTUN_SESSION_HANDLE session =
WintunStartSession(adapter, 0x200000); // ring buffer size
if (!session)
#elif defined(_WIN32)
// Convert to Windows' wchar_t* thingy
std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
std::wstring wide_string = converter.from_bytes(mIfName);
const wchar_t* wide_c_str = wide_string.c_str();
InitializeWintun();
mAdapter = pWintunOpenAdapter(wide_c_str);
if (!mAdapter) {
mAdapter = pWintunCreateAdapter(
wide_c_str,
L"ColumnLynx",
nullptr
);
}
if (!mAdapter)
throw std::runtime_error("Failed to open or create Wintun adapter (run running as admin)");
mSession = pWintunStartSession(mAdapter, 0x200000);
if (!mSession)
throw std::runtime_error("Failed to start Wintun session");
mHandle = WintunGetReadWaitEvent(session);
mFd = -1; // not used on Windows
mIfName = ifName;
mHandle = pWintunGetReadWaitEvent(mSession);
mFd = -1;
#else
throw std::runtime_error("Unsupported platform");
@@ -87,32 +151,82 @@ namespace ColumnLynx::Net {
if (mFd >= 0)
close(mFd);
#elif defined(_WIN32)
// Wintun sessions need explicit stop
// (assuming you stored the session handle as member)
// WintunEndSession(mSession);
if (mSession)
pWintunEndSession(mSession);
#endif
}
// ------------------------------ Read ------------------------------
std::vector<uint8_t> VirtualInterface::readPacket() {
#if defined(__linux__) || defined(__APPLE__)
#if defined(__linux__)
// Linux TUN: blocking read is fine, unblocks on fd close / EINTR
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
return {}; // Interrupted, just 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);
#elif defined(__APPLE__)
// macOS utun: must poll, or read() can block forever
std::vector<uint8_t> buf(4096);
struct pollfd pfd;
pfd.fd = mFd;
pfd.events = POLLIN;
// timeout in ms; keep it small so shutdown is responsive
int ret = poll(&pfd, 1, 200);
if (ret == 0) {
// No data yet
return {};
}
if (ret < 0) {
if (errno == EINTR) {
return {}; // Interrupted by signal
}
throw std::runtime_error("poll() failed: " + std::string(strerror(errno)));
}
if (!(pfd.revents & POLLIN)) {
return {};
}
ssize_t n = read(mFd, buf.data(), buf.size());
if (n <= 0) {
// 0 or -1: treat as EOF or transient; you can decide how aggressive to be
return {};
}
if (n > 4) {
// Drop macOS UTUN header (4 bytes)
std::memmove(buf.data(), buf.data() + 4, n - 4);
buf.resize(n - 4);
} else {
return {};
}
return buf;
#elif defined(_WIN32)
DWORD size = 0;
BYTE* packet = pWintunReceivePacket(mSession, &size);
if (!packet)
return {};
std::vector<uint8_t> buf(packet, packet + size);
pWintunReleaseReceivePacket(mSession, packet);
return buf;
#else
return {};
#endif
@@ -120,16 +234,52 @@ namespace ColumnLynx::Net {
// ------------------------------ Write ------------------------------
void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) {
#if defined(__linux__) || defined(__APPLE__)
#if defined(__linux__)
// Linux TUN expects raw IP packet
ssize_t n = write(mFd, packet.data(), packet.size());
if (n < 0)
throw std::runtime_error("write() failed: " + std::string(strerror(errno)));
#elif defined(__APPLE__)
if (packet.empty())
return;
// Detect IPv4 or IPv6
uint8_t version = packet[0] >> 4;
uint32_t af;
if (version == 4) {
af = htonl(AF_INET);
} else if (version == 6) {
af = htonl(AF_INET6);
} else {
throw std::runtime_error("writePacket(): unknown IP version");
}
// Prepend 4-byte AF header
std::vector<uint8_t> out(packet.size() + 4);
memcpy(out.data(), &af, 4);
memcpy(out.data() + 4, packet.data(), packet.size());
ssize_t n = write(mFd, out.data(), out.size());
if (n < 0)
throw std::runtime_error("utun 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);
BYTE* tx = pWintunAllocateSendPacket(
mSession,
static_cast<DWORD>(packet.size())
);
if (!tx)
throw std::runtime_error("WintunAllocateSendPacket failed");
memcpy(tx, packet.data(), packet.size());
pWintunSendPacket(mSession, tx);
#endif
}
@@ -155,6 +305,45 @@ namespace ColumnLynx::Net {
#endif
}
void VirtualInterface::resetIP() {
#if defined(__linux__)
char cmd[512];
snprintf(cmd, sizeof(cmd),
"ip addr flush dev %s",
mIfName.c_str()
);
system(cmd);
#elif defined(__APPLE__)
char cmd[512];
snprintf(cmd, sizeof(cmd),
"ifconfig %s inet 0.0.0.0 delete",
mIfName.c_str()
);
system(cmd);
snprintf(cmd, sizeof(cmd),
"ifconfig %s inet6 :: delete",
mIfName.c_str()
);
system(cmd);
#elif defined(_WIN32)
char cmd[512];
// Remove any persistent routes associated with this interface
snprintf(cmd, sizeof(cmd),
"netsh routing ip delete persistentroute all name=\"%s\"",
mIfName.c_str()
);
system(cmd);
// Reset to DHCP
snprintf(cmd, sizeof(cmd),
"netsh interface ip set address name=\"%s\" dhcp",
mIfName.c_str()
);
system(cmd);
#endif
}
// ------------------------------------------------------------
// Linux
// ------------------------------------------------------------
@@ -195,7 +384,8 @@ namespace ColumnLynx::Net {
std::string ipStr = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP);
std::string prefixStr = ipv4ToString(prefixLen);
std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false);
Utils::debug("Prefix string: " + prefixStr);
// Reset
snprintf(cmd, sizeof(cmd),
@@ -212,7 +402,7 @@ namespace ColumnLynx::Net {
// Set
snprintf(cmd, sizeof(cmd),
"ifconfig %s %s %s mtu %d netmask %s up",
"ifconfig %s inet %s %s mtu %d netmask %s up",
mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
system(cmd);
@@ -238,7 +428,11 @@ namespace ColumnLynx::Net {
uint32_t maskInt = (prefixLen == 0) ? 0 : (0xFFFFFFFF << (32 - prefixLen));
mask = ipv4ToString(maskInt);
char cmd[256];
// Calculate network address from IP and mask
uint32_t networkInt = (clientIP & maskInt);
std::string network = ipv4ToString(networkInt);
char cmd[512];
// 1. Set the static IP + mask + gateway
snprintf(cmd, sizeof(cmd),
@@ -254,6 +448,14 @@ namespace ColumnLynx::Net {
);
system(cmd);
// 3. Add route for the VPN network to go through the TUN interface
// This is critical: tells Windows to send packets destined for the server/network through the TUN interface
snprintf(cmd, sizeof(cmd),
"netsh routing ip add persistentroute dest=%s/%d name=\"%s\" nexthopcfg=%s",
network.c_str(), prefixLen, mIfName.c_str(), gw.c_str()
);
system(cmd);
return true;
#else
return false;

View File

@@ -4,6 +4,8 @@
#include <asio.hpp>
#include <iostream>
#include <thread>
#include <chrono>
#include <columnlynx/common/utils.hpp>
#include <columnlynx/common/panic_handler.hpp>
#include <columnlynx/server/net/tcp/tcp_server.hpp>
@@ -23,20 +25,7 @@ using namespace ColumnLynx;
volatile sig_atomic_t done = 0;
void signalHandler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
log("Received termination signal. Shutting down server gracefully.");
done = 1;
}
}
int main(int argc, char** argv) {
// Capture SIGINT and SIGTERM for graceful shutdown
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = signalHandler;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);
cxxopts::Options options("columnlynx_server", "ColumnLynx Server Application");
@@ -68,7 +57,7 @@ int main(int argc, char** argv) {
log("This software is licensed under the GPLv2 only OR the GPLv3. See LICENSES/ for details.");
#if defined(__WIN32__)
WintunInitialize();
//WintunInitialize();
#endif
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(optionsObj["config"].as<std::string>());
@@ -128,6 +117,8 @@ int main(int argc, char** argv) {
while (!done) {
auto packet = tun->readPacket();
if (packet.empty()) {
// Small sleep to avoid busy-waiting and to allow signal processing
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}

View File

@@ -6,20 +6,39 @@
namespace ColumnLynx::Net::TCP {
void TCPConnection::start() {
try {
// Cache the remote IP early to avoid calling remote_endpoint() on closed sockets later
mRemoteIP = mHandler->socket().remote_endpoint().address().to_string();
} catch (const std::exception& e) {
mRemoteIP = "unknown";
Utils::warn("Failed to get remote endpoint: " + std::string(e.what()));
}
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();
// Peer has closed; finalize locally without sending RST
Utils::log("Client disconnected: " + mRemoteIP + " - " + ec.message());
asio::error_code ec2;
mHandler->socket().close(ec2);
SessionRegistry::getInstance().erase(mConnectionSessionID);
SessionRegistry::getInstance().deallocIP(mConnectionSessionID);
Utils::log("Closed connection to " + mRemoteIP);
if (mOnDisconnect) {
mOnDisconnect(shared_from_this());
}
});
mHandler->start();
mStartHeartbeat();
// Placeholder for message handling setup
Utils::log("Client connected: " + mHandler->socket().remote_endpoint().address().to_string());
Utils::log("Client connected: " + mRemoteIP);
}
void TCPConnection::sendMessage(ServerMessageType type, const std::string& data) {
@@ -32,23 +51,19 @@ namespace ColumnLynx::Net::TCP {
mOnDisconnect = std::move(cb);
}
void TCPConnection::disconnect() {
std::string ip = mHandler->socket().remote_endpoint().address().to_string();
void TCPConnection::disconnect(bool echo) {
if (echo) {
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());
// Half-close: stop sending, keep reading until peer FIN
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_send, ec);
if (ec) {
Utils::error("Error during socket shutdown: " + ec.message());
}
// Do not close immediately; final cleanup happens in onDisconnect
Utils::log("Initiated graceful disconnect (half-close) to " + mRemoteIP);
}
uint64_t TCPConnection::getSessionID() const {
@@ -92,7 +107,7 @@ namespace ColumnLynx::Net::TCP {
}
void TCPConnection::mHandleMessage(ClientMessageType type, const std::string& data) {
std::string reqAddr = mHandler->socket().remote_endpoint().address().to_string();
std::string& reqAddr = mRemoteIP;
switch (type) {
case ClientMessageType::HANDSHAKE_INIT: {
@@ -272,6 +287,11 @@ namespace ColumnLynx::Net::TCP {
disconnect();
break;
}
case ClientMessageType::KILL_CONNECTION: {
Utils::warn("Received KILL_CONNECTION from " + reqAddr + ": " + data);
disconnect();
break;
}
default:
Utils::warn("Unhandled message type from " + reqAddr);
break;