9 Commits

Author SHA1 Message Date
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
10 changed files with 223 additions and 102 deletions

View File

@@ -6,7 +6,7 @@ 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.6 VERSION 0.1.0
LANGUAGES CXX LANGUAGES CXX
) )

View File

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

View File

@@ -26,6 +26,7 @@
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <sys/poll.h>
#elif defined(_WIN32) #elif defined(_WIN32)
#include <windows.h> #include <windows.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
@@ -49,9 +50,13 @@ namespace ColumnLynx::Net {
const std::string& getName() const; const std::string& getName() const;
int getFd() const; // For ASIO integration (on POSIX) int getFd() const; // For ASIO integration (on POSIX)
static inline std::string ipv4ToString(uint32_t ip) { static inline std::string ipv4ToString(uint32_t ip, bool flip = true) {
struct in_addr addr; struct in_addr addr;
addr.s_addr = htonl(ip);
if (flip)
addr.s_addr = htonl(ip);
else
addr.s_addr = ip;
char buf[INET_ADDRSTRLEN]; char buf[INET_ADDRSTRLEN];
if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf))) if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf)))

View File

@@ -107,7 +107,7 @@ int main(int argc, char** argv) {
if (packet.empty()) { if (packet.empty()) {
continue; continue;
} }
udpClient->sendMessage(std::string(packet.begin(), packet.end())); udpClient->sendMessage(std::string(packet.begin(), packet.end()));
} }

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

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

View File

@@ -28,6 +28,7 @@ namespace ColumnLynx::Net {
#elif defined(__APPLE__) #elif defined(__APPLE__)
// ---- macOS: UTUN (system control socket) ---- // ---- 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); mFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
if (mFd < 0) if (mFd < 0)
throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno))); throw std::runtime_error("socket(PF_SYSTEM) failed: " + std::string(strerror(errno)));
@@ -42,7 +43,7 @@ namespace ColumnLynx::Net {
sc.sc_family = AF_SYSTEM; sc.sc_family = AF_SYSTEM;
sc.ss_sysaddr = AF_SYS_CONTROL; sc.ss_sysaddr = AF_SYS_CONTROL;
sc.sc_id = ctlInfo.ctl_id; sc.sc_id = ctlInfo.ctl_id;
sc.sc_unit = 0; // lynx0 (0 = auto-assign) sc.sc_unit = 0; // 0 = auto-assign next utunX
if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) { if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) {
if (errno == EPERM) if (errno == EPERM)
@@ -50,16 +51,17 @@ namespace ColumnLynx::Net {
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno))); throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno)));
} }
// Retrieve actual utun device name // Retrieve actual utun device name via UTUN_OPT_IFNAME
struct sockaddr_storage addr; char ifname[IFNAMSIZ];
socklen_t addrlen = sizeof(addr); socklen_t ifname_len = sizeof(ifname);
if (getsockname(mFd, (struct sockaddr*)&addr, &addrlen) == 0) { if (getsockopt(mFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &ifname_len) == 0) {
const struct sockaddr_ctl* addr_ctl = (const struct sockaddr_ctl*)&addr; mIfName = ifname; // Update to actual assigned name
mIfName = "utun" + std::to_string(addr_ctl->sc_unit - 1);
} else { } else {
mIfName = "utunX"; mIfName = "utun0"; // Fallback (should not happen)
} }
Utils::log("VirtualInterface: opened macOS UTUN: " + mIfName);
#elif defined(_WIN32) #elif defined(_WIN32)
// ---- Windows: Wintun (WireGuard virtual adapter) ---- // ---- Windows: Wintun (WireGuard virtual adapter) ----
WINTUN_ADAPTER_HANDLE adapter = WINTUN_ADAPTER_HANDLE adapter =
@@ -95,24 +97,73 @@ namespace ColumnLynx::Net {
// ------------------------------ Read ------------------------------ // ------------------------------ Read ------------------------------
std::vector<uint8_t> VirtualInterface::readPacket() { 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); std::vector<uint8_t> buf(4096);
ssize_t n = read(mFd, buf.data(), buf.size()); ssize_t n = read(mFd, buf.data(), buf.size());
if (n < 0) { if (n < 0) {
if (errno == EINTR) { if (errno == EINTR) {
return {}; // Interrupted, return empty return {}; // Interrupted, just return empty
} }
throw std::runtime_error("read() failed: " + std::string(strerror(errno))); throw std::runtime_error("read() failed: " + std::string(strerror(errno)));
} }
buf.resize(n); buf.resize(n);
return buf; return buf;
#elif defined(__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) #elif defined(_WIN32)
WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr); WINTUN_PACKET* packet = WintunReceivePacket(mSession, nullptr);
if (!packet) return {}; if (!packet) return {};
std::vector<uint8_t> buf(packet->Data, packet->Data + packet->Length); std::vector<uint8_t> buf(packet->Data, packet->Data + packet->Length);
WintunReleaseReceivePacket(mSession, packet); WintunReleaseReceivePacket(mSession, packet);
return buf; return buf;
#else #else
return {}; return {};
#endif #endif
@@ -120,16 +171,48 @@ namespace ColumnLynx::Net {
// ------------------------------ Write ------------------------------ // ------------------------------ Write ------------------------------
void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) { void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) {
#if defined(__linux__) || defined(__APPLE__) #if defined(__linux__)
// Linux TUN expects raw IP packet
ssize_t n = write(mFd, packet.data(), packet.size()); ssize_t n = write(mFd, packet.data(), packet.size());
if (n < 0) if (n < 0)
throw std::runtime_error("write() failed: " + std::string(strerror(errno))); throw std::runtime_error("write() failed: " + std::string(strerror(errno)));
#elif defined(__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) #elif defined(_WIN32)
WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size()); WINTUN_PACKET* tx = WintunAllocateSendPacket(mSession, (DWORD)packet.size());
if (!tx) throw std::runtime_error("WintunAllocateSendPacket failed"); if (!tx)
throw std::runtime_error("WintunAllocateSendPacket failed");
memcpy(tx->Data, packet.data(), packet.size()); memcpy(tx->Data, packet.data(), packet.size());
WintunSendPacket(mSession, tx); WintunSendPacket(mSession, tx);
#endif #endif
} }
@@ -195,7 +278,8 @@ namespace ColumnLynx::Net {
std::string ipStr = ipv4ToString(clientIP); std::string ipStr = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP); std::string peerStr = ipv4ToString(serverIP);
std::string prefixStr = ipv4ToString(prefixLen); std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false);
Utils::debug("Prefix string: " + prefixStr);
// Reset // Reset
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
@@ -212,7 +296,7 @@ namespace ColumnLynx::Net {
// Set // Set
snprintf(cmd, sizeof(cmd), 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()); mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
system(cmd); system(cmd);