4 Commits

Author SHA1 Message Date
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
31 changed files with 368 additions and 1190 deletions

1
.gitignore vendored
View File

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

View File

@@ -3,7 +3,7 @@
## ASIO C++ Library
- **Name:** ASIO (standalone)
- **Website:** https://think-async.com/Asio/
- **Copyright:** (c) 2003-2026 Christopher M. Kohlhoff
- **Copyright:** (c) 2003-2025 Christopher M. Kohlhoff
- **License:** Boost Software License, Version 1.0
- **License Text:** See `third_party/asio/LICENSE_1_0.txt`
@@ -12,14 +12,14 @@ This project uses the standalone version of the ASIO C++ library for asynchronou
## CXXOPTS C++ Library
- **Name:** cxxopts
- **Website:** https://github.com/jarro2783/cxxopts/
- **Copyright:** (c) 2014-2026 Christopher M. Kohlhoff
- **Copyright:** (c) 2014-2025 Christopher M. Kohlhoff
- **License:** MIT License
- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt`
## Wintun C++ Library
- **Name:** wintun
- **Website:** https://www.wintun.net/
- **Copyright:** (c) 2018-2026 WireGuard LLC
- **Copyright:** (c) 2018-2025 WireGuard LLC
- **License:** MIT License OR GPL-2.0 License
- **License Text:** See `third_party/wintun/`
- **Utilized Under:** MIT License

View File

@@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx
VERSION 1.0.1
VERSION 0.0.5
LANGUAGES CXX
)
@@ -19,9 +19,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
#set(CMAKE_CXX_FLAGS_DEBUG "-g")
#add_compile_options(${CMAKE_CXX_FLAGS_DEBUG})
if(DEBUG)
add_compile_definitions(DEBUG=1) # TODO: Forcing for now, add dymanic based on compile flags later
endif()
add_compile_definitions(DEBUG=1) # TODO: Forcing for now, add dymanic based on compile flags later
include(FetchContent)
@@ -40,7 +38,7 @@ endif()
if(WIN32)
add_compile_definitions(_WIN32_WINNT=0x0A00 NOMINMAX WIN32_LEAN_AND_MEAN)
elseif(UNIX)
add_compile_options(-Wall -Wextra -Wpedantic -O3)
add_compile_options(-Wall -Wextra -Wpedantic)
add_link_options(-pthread)
endif()
@@ -50,7 +48,7 @@ endif()
FetchContent_Declare(
Sodium
GIT_REPOSITORY https://github.com/robinlinden/libsodium-cmake.git
GIT_TAG e5b985ad0dd235d8c4307ea3a385b45e76c74c6a
GIT_TAG e5b985ad0dd235d8c4307ea3a385b45e76c74c6a # Last updated at 2025-04-13
)
set(SODIUM_DISABLE_TESTS ON CACHE BOOL "" FORCE)
@@ -80,6 +78,15 @@ 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
# ---------------------------------------------------------
@@ -99,17 +106,7 @@ endforeach()
# ---------------------------------------------------------
file(GLOB_RECURSE COMMON_SRC CONFIGURE_DEPENDS src/common/*.cpp)
add_library(common STATIC ${COMMON_SRC})
target_link_libraries(common PUBLIC sodium cxxopts::cxxopts)
if (WIN32)
target_link_libraries(common PUBLIC
ws2_32
iphlpapi
advapi32
mswsock
)
endif()
target_link_libraries(common PUBLIC sodium OpenSSL::SSL OpenSSL::Crypto cxxopts::cxxopts)
target_include_directories(common PUBLIC
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include
@@ -123,12 +120,7 @@ target_compile_definitions(common PUBLIC ASIO_STANDALONE)
# ---------------------------------------------------------
file(GLOB_RECURSE CLIENT_SRC CONFIGURE_DEPENDS src/client/*.cpp)
add_executable(client ${CLIENT_SRC})
target_link_libraries(client PRIVATE common sodium cxxopts::cxxopts)
if (WIN32)
target_link_libraries(client PRIVATE
dbghelp
)
endif()
target_link_libraries(client PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto cxxopts::cxxopts)
target_include_directories(client PRIVATE
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include
@@ -143,12 +135,7 @@ set_target_properties(client PROPERTIES OUTPUT_NAME "columnlynx_client")
# ---------------------------------------------------------
file(GLOB_RECURSE SERVER_SRC CONFIGURE_DEPENDS src/server/*.cpp)
add_executable(server ${SERVER_SRC})
target_link_libraries(server PRIVATE common sodium cxxopts::cxxopts)
if (WIN32)
target_link_libraries(server PRIVATE
dbghelp
)
endif()
target_link_libraries(server PRIVATE common sodium OpenSSL::SSL OpenSSL::Crypto cxxopts::cxxopts)
target_include_directories(server PRIVATE
${PROJECT_SOURCE_DIR}/include
${sodium_SOURCE_DIR}/src/libsodium/include

157
README.md
View File

@@ -18,157 +18,20 @@ This simplicity-focused design approach allows us to make an efficient, low-over
## Configuration
Configurating the server and client are are relatively easy. Currently (since the project is in alpha), the configuration files **must be in your system-specific config location** (which can be overriden via a CLI argument or the **COLUMNLYNX_CONFIG_DIR** Environment Variable).
The defaults depends on your system.
For the server:
- Linux: **/etc/columnlynx**
- macOS: **/etc/columnlynx**
- Windows: **C:\ProgramData\ColumnLynx**
For the client:
- Linux: **~/.config/columnlynx**
- macOS: **~/Library/Application Support/columnlynx**
- Windows: **C:\Users\USERNAME\AppData\Local\ColumnLynx**
### Getting a keypair
Release builds of the software force you to specify your own keypairs. That's why you need to generate a keypair with some other software that you can use.
This guide will show a generation example with openssl:
#### Generate a keypair:
```bash
openssl genpkey -algorithm ED25519 -out key.pem
```
#### Extract the **Private Key Seed**:
```bash
openssl pkey -in key.pem -outform DER | tail -c 32 | xxd -p -c 32
# Output example: 9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
```
#### Extract the **Raw Public Key**:
```bash
openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32
# Output example: 1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
```
You can then set these keys accordingly in the **server_config** and **client_config** files.
### Server Setup (Linux Server ONLY)
#### Creating the Tun Interface
In order for the VPN server to work, you need to create the Tun interface that the VPN will use.
This is the set of commands to create one on Linux. Replace the example 10.10.0.1/24 IPv4 address with the FIRST IPv4 in the Network and Subnet Mask that you set in server_config.
```bash
sudo ip tuntap add dev lynx0 mode tun
sudo ip addr add 10.10.0.1/24 dev lynx0
sudo ip link set dev lynx0 mtu 1420
sudo ip link set dev lynx0 up
```
#### Creating the systemd service
It is highly recommended to **run the server as a systemd service**, as systemd is the primary service manager on Linux.
**1. Create a file for the service**
```bash
sudo touch /etc/systemd/system/columnlynx.service
```
**2. Open the file in your editor of choice**
```bash
sudo nano /etc/systemd/system/columnlynx.service
# OR
sudo vim /etc/systemd/system/columnlynx.service
# OR any other editor of your choice...
```
**3. Configure the service**
**Replace** the **ExecStart** and **WorkingDirectory** paths with the paths where your binaries are stored.
If you configured your tun interface to belong to a custom user, you may also replace the **User** and **Group** with that user, however you must ensure that that user owns the **tun interface**, **config directory in /etc/columnlynx** and the **working directory**.
This is a **simple example** for the **root user** and the executable in **/opt/columnlynx**:
```
[Unit]
Description=ColumnLynx Server Service
After=network.target
[Service]
Type=simple
ExecStart=/opt/columnlynx/columnlynx_server
WorkingDirectory=/opt/columnlynx
User=root
Group=root
Restart=on-failure
StandardOutput=append:/var/log/columnlynx.log
StandardError=append:/var/log/columnlynx.err
[Install]
WantedBy=multi-user.target
```
**4. Reload systemd and enable the service**
```bash
sudo systemctl daemon-reload
sudo systemctl enable columnlynx.service
sudo systemctl start columnlynx.service
```
#### Set firewall rules
This part greatly depends on your firewall of choice. Generally you just need to **allow port 48042 on both TCP and UDP** (Both IPv4 and IPv6).
This example is for **UFW**:
```bash
sudo ufw allow 48042
sudo ufw reload
```
#### IPTables rules for forwarding (Optional)
In addition to creating the interface, you'll also need to make some **iptables** rules if you want to be able to **send traffic to foreign networks** (more like a *commercial VPN*).
You can do these as such (example with NFT IPTABLES):
- Enable the **generic IPv4 forwarding**:
```bash
sudo sysctl net.ipv4.ip_forward=1
```
- Create the masquerade (**Replace the IP subnet** with your own that you set in the config and **replace the interface** with your server's main (NOT *lynx0*) interface):
```bash
sudo nft add table nat
sudo nft add chain nat postroute { type nat hook postrouting priority 100 \; }
sudo nft add rule nat postroute ip saddr 10.10.0.0/24 oifname "eth0" masquerade
```
Configurating the server and client are are relatively easy. Currently (since the project is in alpha), the configuration files **must be in the same directory as the working directory**.
### Server
"**server_config**" is a file that contains the server configuration, **one variable per line**. These are the current configuration available variables:
- **SERVER_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
- **SERVER_PRIVATE_KEY** (Hex String): The private key seed to be used
- **NETWORK** (IPv4 Format): The network IPv4 to be used (Server Interface still needs to be configured manually)
- **SUBNET_MASK** (Integer): The subnet mask to be used (ensure proper length, it will not be checked)
- **SERVER_PUBLIC_KEY** (Hex String): The public key to be used
- **SERVER_PRIVATE_KEY** (Hex String): The private key to be used
**Example:**
```
SERVER_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
SERVER_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
NETWORK=10.10.0.0
SUBNET_MASK=24
SERVER_PUBLIC_KEY=787B648046F10DDD0B77A6303BE42D859AA65C52F5708CC3C58EB5691F217C7B
SERVER_PRIVATE_KEY=778604245F57B847E63BD85DE8208FF1A127FB559895195928C3987E246B77B8787B648046F10DDD0B77A6303BE42D859AA65C52F5708CC3C58EB5691F217C7B
```
<hr></hr>
@@ -186,14 +49,14 @@ SUBNET_MASK=24
"**client_config**" is a file that contains the client configuration, **one variable per line**. These are the current configuration available variables:
- **CLIENT_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
- **CLIENT_PRIVATE_KEY** (Hex String): The private key seed to be used
- **CLIENT_PUBLIC_KEY** (Hex String): The public key to be used
- **CLIENT_PRIVATE_KEY** (Hex String): The private key to be used
**Example:**
```
CLIENT_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
CLIENT_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
CLIENT_PUBLIC_KEY=8CC8BE1A9D24639D0492EF143E84E2BD4C757C9B3B687E7035173EBFCA8FEDDA
CLIENT_PRIVATE_KEY=9B486A5B1509FA216F9EEFED85CACF2384E9D902A76CC979BFA143C18B869F5C8CC8BE1A9D24639D0492EF143E84E2BD4C757C9B3B687E7035173EBFCA8FEDDA
```
<hr></hr>
@@ -213,8 +76,6 @@ ColumnLynx makes use of both **TCP** and **UDP**. **TCP** is used for the initia
It operates on port **48042** for both TCP and UDP.
Generally, all transmission is done in **little-endian byte order**, since pretty much every single modern architecture uses it by default. The only exemption to this is the **transmission of IP addresses** (for the **Virtual Interface**), which is **big-endian**.
### Handshake Procedure
The handshake between the client and server is done over **TCP**. This is to ensure delivery without much hassle.

View File

@@ -1,5 +1,5 @@
// tcp_client.hpp - TCP Client for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -14,7 +14,6 @@
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <string>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
@@ -26,11 +25,10 @@ namespace ColumnLynx::Net::TCP {
TCPClient(asio::io_context& ioContext,
const std::string& host,
const std::string& port,
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
std::shared_ptr<std::array<uint8_t, 32>> aesKey,
std::shared_ptr<uint64_t> sessionIDRef,
bool insecureMode,
std::string& configPath,
Utils::LibSodiumWrapper* sodiumWrapper,
std::array<uint8_t, 32>* aesKey,
uint64_t* sessionIDRef,
bool* insecureMode,
std::shared_ptr<VirtualInterface> tun = nullptr)
:
mResolver(ioContext),
@@ -44,11 +42,10 @@ namespace ColumnLynx::Net::TCP {
mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now()),
mTun(tun),
mConfigDirPath(configPath)
mTun(tun)
{
// Preload the config map
mRawClientConfig = Utils::getConfigMap(configPath + "client_config");
mRawClientConfig = Utils::getConfigMap("client_config");
auto itPubkey = mRawClientConfig.find("CLIENT_PUBLIC_KEY");
auto itPrivkey = mRawClientConfig.find("CLIENT_PRIVATE_KEY");
@@ -57,22 +54,16 @@ namespace ColumnLynx::Net::TCP {
Utils::log("Loading keypair from config file.");
PublicKey pk;
PrivateSeed seed;
PrivateKey sk;
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), seed.size(), seed.begin()); // This is extremely stupid, but the C++ compiler has forced my hand (I would've just used to_array, but fucking asio decls)
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), sk.size(), sk.begin()); // This is extremely stupid, but the C++ compiler has forced my hand (I would've just used to_array, but fucking asio decls)
std::copy_n(Utils::hexStringToBytes(itPubkey->second).begin(), pk.size(), pk.begin());
if (!mLibSodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute keypair from config file values!");
}
mLibSodiumWrapper->setKeys(pk, sk);
Utils::debug("Newly-Loaded Public Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPublicKey(), 32));
} else {
#if defined(DEBUG)
Utils::warn("No keypair found in config file! Using random key.");
#else
throw std::runtime_error("No keypair found in config file! Cannot start client without keys.");
#endif
}
}
@@ -104,12 +95,12 @@ namespace ColumnLynx::Net::TCP {
std::string mHost, mPort;
uint8_t mServerPublicKey[32]; // Assuming 256-bit public key
std::array<uint8_t, 32> mSubmittedChallenge{};
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper;
Utils::LibSodiumWrapper* mLibSodiumWrapper;
uint64_t mConnectionSessionID;
SymmetricKey mConnectionAESKey;
std::shared_ptr<std::array<uint8_t, 32>> mGlobalKeyRef; // Reference to global AES key
std::shared_ptr<uint64_t> mSessionIDRef; // Reference to global Session ID
bool mInsecureMode; // Insecure mode flag
std::array<uint8_t, 32>* mGlobalKeyRef; // Reference to global AES key
uint64_t* mSessionIDRef; // Reference to global Session ID
bool* mInsecureMode; // Reference to insecure mode flag
asio::steady_timer mHeartbeatTimer;
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
std::chrono::steady_clock::time_point mLastHeartbeatSent;
@@ -118,6 +109,5 @@ namespace ColumnLynx::Net::TCP {
Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr;
std::unordered_map<std::string, std::string> mRawClientConfig;
std::string mConfigDirPath;
};
}

View File

@@ -1,5 +1,5 @@
// udp_client.hpp - UDP Client for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -17,8 +17,8 @@ namespace ColumnLynx::Net::UDP {
UDPClient(asio::io_context& ioContext,
const std::string& host,
const std::string& port,
std::shared_ptr<std::array<uint8_t, 32>> aesKeyRef,
std::shared_ptr<uint64_t> sessionIDRef,
std::array<uint8_t, 32>* aesKeyRef,
uint64_t* sessionIDRef,
std::shared_ptr<VirtualInterface> tunRef = nullptr)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef)
{
@@ -43,8 +43,8 @@ namespace ColumnLynx::Net::UDP {
asio::ip::udp::endpoint mRemoteEndpoint;
std::string mHost;
std::string mPort;
std::shared_ptr<std::array<uint8_t, 32>> mAesKeyRef;
std::shared_ptr<uint64_t> mSessionIDRef;
std::array<uint8_t, 32>* mAesKeyRef;
uint64_t* mSessionIDRef;
std::shared_ptr<VirtualInterface> mTunRef = nullptr;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
};

View File

@@ -1,5 +1,5 @@
// libsodium_wrapper.hpp - Libsodium Wrapper for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -11,13 +11,16 @@
#include <columnlynx/common/utils.hpp>
#include <array>
#include <vector>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
#include <openssl/pem.h>
#include <openssl/x509v3.h>
#include <memory>
#include <cstring>
namespace ColumnLynx {
using PublicKey = std::array<uint8_t, crypto_sign_PUBLICKEYBYTES>; // Ed25519
using PrivateKey = std::array<uint8_t, crypto_sign_SECRETKEYBYTES>; // Ed25519
using PrivateSeed = std::array<uint8_t, crypto_sign_SEEDBYTES>; // 32 bytes
using Signature = std::array<uint8_t, crypto_sign_BYTES>; // 64 bytes
using SymmetricKey = std::array<uint8_t, crypto_aead_chacha20poly1305_ietf_KEYBYTES>; // 32 bytes
using Nonce = std::array<uint8_t, crypto_aead_chacha20poly1305_ietf_NPUBBYTES>; // 12 bytes
@@ -54,9 +57,6 @@ namespace ColumnLynx::Utils {
}
}
// Recompute the keypair from a given private seed; Will return false on failure
bool recomputeKeys(PrivateSeed privateSeed, PublicKey storedPubKey);
// Helper section
// Generates a random 256-bit (32-byte) array

View File

@@ -1,5 +1,5 @@
// protocol_structs.hpp - Network Protocol Structures
// Copyright (C) 2026 DcruBro
// 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

View File

@@ -1,5 +1,5 @@
// session_registry.hpp - Session Registry for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -8,11 +8,7 @@
#include <memory>
#include <chrono>
#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>
@@ -52,36 +48,96 @@ 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);
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
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
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
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
void erase(uint64_t sessionID);
void erase(uint64_t sessionID) {
std::unique_lock lock(mMutex);
mSessions.erase(sessionID);
}
// 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
int size() const;
int size() const {
std::shared_lock lock(mMutex);
return static_cast<int>(mSessions.size());
}
// IP management
// IP management (simple for /24 subnet)
// Get the lowest available IPv4 address; Returns 0 if none available
uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const;
uint32_t getFirstAvailableIP() const {
std::shared_lock lock(mMutex);
uint32_t baseIP = 0x0A0A0002; // 10.10.0.2
// Lock IP to session ID; Do NOT call before put() - You will segfault!
void lockIP(uint64_t sessionID, uint32_t ip);
// 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;
}
}
// Unlock IP from session ID
void deallocIP(uint64_t sessionID);
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:
mutable std::shared_mutex mMutex;

View File

@@ -1,5 +1,5 @@
// net_helper.hpp - Network Helper Functions for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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

View File

@@ -1,5 +1,5 @@
// tcp_message_handler.hpp - TCP Message Handler for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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

View File

@@ -1,5 +1,5 @@
// tcp_message_type.hpp - TCP Message Types for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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

View File

@@ -1,5 +1,5 @@
// udp_message_type.hpp - UDP Message Types for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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

View File

@@ -1,5 +1,5 @@
// virtual_interface.hpp - Virtual Interface for Network Communication
// Copyright (C) 2026 DcruBro
// 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
@@ -26,22 +26,12 @@
#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 <winsock2.h>
#include <ws2tcpip.h>
#include <locale>
#include <codecvt>
#include <winsock2.h>
#include <wintun/wintun.h>
#include <iphlpapi.h>
#include <netioapi.h>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "advapi32.lib")
#endif
namespace ColumnLynx::Net {
@@ -53,21 +43,15 @@ 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, bool flip = true) {
static inline std::string ipv4ToString(uint32_t ip) {
struct in_addr addr;
if (flip)
addr.s_addr = htonl(ip);
else
addr.s_addr = ip;
addr.s_addr = htonl(ip);
char buf[INET_ADDRSTRLEN];
if (!inet_ntop(AF_INET, &addr, buf, sizeof(buf)))
@@ -76,52 +60,6 @@ namespace ColumnLynx::Net {
return std::string(buf);
}
static inline uint32_t stringToIpv4(const std::string &ipStr) {
struct in_addr addr;
if (inet_pton(AF_INET, ipStr.c_str(), &addr) != 1) {
return 0; // "0.0.0.0"
}
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;
@@ -136,9 +74,7 @@ namespace ColumnLynx::Net {
std::string mIfName;
int mFd; // POSIX
#if defined(_WIN32)
WINTUN_ADAPTER_HANDLE mAdapter = nullptr;
WINTUN_SESSION_HANDLE mSession = nullptr;
HANDLE mHandle = nullptr;
HANDLE mHandle; // Windows
#endif
};
}

View File

@@ -1,5 +1,5 @@
// panic_handler.hpp - Panic Handler for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -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 MASTER THREAD PANIC! \033[0m***\n";
std::cerr << "\n***\033[31m MAIN THREAD PANIC! \033[0m***\n";
std::cerr << "Reason: " << reason << "\n";
std::cerr << "Dumping panic trace...\n";

View File

@@ -1,5 +1,5 @@
// utils.hpp - Utility functions for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -13,8 +13,6 @@
#include <fstream>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
#ifdef _WIN32
#include <winsock2.h>
@@ -24,14 +22,7 @@
#include <unistd.h>
#endif
namespace ColumnLynx {
using IPv6Addr = std::array<uint8_t, 16>;
}
namespace ColumnLynx::Utils {
// Converts unix milliseconds to a local ISO 8601 formatted string; Defaults to local time; Will use UTC if local is false.
std::string unixMillisToISO8601(uint64_t unixMillis, bool local = true);
// General log function. Use for logging important information.
void log(const std::string &msg);
// General warning function. Use for logging important warnings.
@@ -47,7 +38,7 @@ namespace ColumnLynx::Utils {
std::string getVersion();
unsigned short serverPort();
unsigned char protocolVersion();
std::vector<std::string> getWhitelistedKeys(std::string basePath);
std::vector<std::string> getWhitelistedKeys();
// Raw byte to hex string conversion helper
std::string bytesToHexString(const uint8_t* bytes, size_t length);
@@ -85,18 +76,6 @@ namespace ColumnLynx::Utils {
return cbswap64(x);
}
template <typename T>
T cbswap128(const T& x) {
static_assert(sizeof(T) == 16, "cbswap128 requires a 128-bit type");
T out{};
const uint8_t* src = reinterpret_cast<const uint8_t*>(&x);
uint8_t* dst = reinterpret_cast<uint8_t*>(&out);
std::reverse_copy(src, src + 16, dst);
return out;
}
// 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, std::vector<std::string> requiredKeys = {});
std::unordered_map<std::string, std::string> getConfigMap(std::string path);
};

View File

@@ -1,5 +1,5 @@
// tcp_connection.hpp - TCP Connection for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -17,7 +17,6 @@
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/session_registry.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
namespace ColumnLynx::Net::TCP {
class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
@@ -26,12 +25,10 @@ namespace ColumnLynx::Net::TCP {
static pointer create(
asio::ip::tcp::socket socket,
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
std::unordered_map<std::string, std::string>* serverConfig,
std::string configDirPath,
Utils::LibSodiumWrapper* sodiumWrapper,
std::function<void(pointer)> onDisconnect)
{
auto conn = pointer(new TCPConnection(std::move(socket), sodiumWrapper, serverConfig, configDirPath));
auto conn = pointer(new TCPConnection(std::move(socket), sodiumWrapper));
conn->mOnDisconnect = std::move(onDisconnect);
return conn;
}
@@ -43,7 +40,7 @@ namespace ColumnLynx::Net::TCP {
// Set callback for disconnects
void setDisconnectCallback(std::function<void(std::shared_ptr<TCPConnection>)> cb);
// Disconnect the client
void disconnect(bool echo = true);
void disconnect();
// Get the assigned session ID
uint64_t getSessionID() const;
@@ -51,15 +48,13 @@ namespace ColumnLynx::Net::TCP {
std::array<uint8_t, 32> getAESKey() const;
private:
TCPConnection(asio::ip::tcp::socket socket, std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper, std::unordered_map<std::string, std::string>* serverConfig, std::string configDirPath)
TCPConnection(asio::ip::tcp::socket socket, Utils::LibSodiumWrapper* sodiumWrapper)
:
mHandler(std::make_shared<MessageHandler>(std::move(socket))),
mLibSodiumWrapper(sodiumWrapper),
mRawServerConfig(serverConfig),
mHeartbeatTimer(mHandler->socket().get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now()),
mConfigDirPath(configDirPath)
mLastHeartbeatSent(std::chrono::steady_clock::now())
{}
// Start the heartbeat routine
@@ -69,8 +64,7 @@ namespace ColumnLynx::Net::TCP {
std::shared_ptr<MessageHandler> mHandler;
std::function<void(std::shared_ptr<TCPConnection>)> mOnDisconnect;
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper;
std::unordered_map<std::string, std::string>* mRawServerConfig;
Utils::LibSodiumWrapper *mLibSodiumWrapper;
std::array<uint8_t, 32> mConnectionAESKey;
uint64_t mConnectionSessionID;
AsymPublicKey mConnectionPublicKey;
@@ -78,7 +72,5 @@ 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
std::string mConfigDirPath;
};
}

View File

@@ -1,5 +1,5 @@
// tcp_server.hpp - TCP Server for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -24,46 +24,34 @@ namespace ColumnLynx::Net::TCP {
public:
TCPServer(asio::io_context& ioContext,
uint16_t port,
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
std::shared_ptr<bool> hostRunning,
std::string& configPath,
bool ipv4Only = false)
Utils::LibSodiumWrapper* sodiumWrapper,
bool* hostRunning, bool ipv4Only = false)
: mIoContext(ioContext),
mAcceptor(ioContext),
mSodiumWrapper(sodiumWrapper),
mHostRunning(hostRunning),
mConfigDirPath(configPath)
mHostRunning(hostRunning)
{
// Preload the config map
mRawServerConfig = Utils::getConfigMap(configPath + "server_config", {"NETWORK", "SUBNET_MASK"});
mRawServerConfig = Utils::getConfigMap("server_config");
asio::error_code ec_open, ec_v6only, ec_bind;
asio::error_code ec;
if (!ipv4Only) {
// Try IPv6 (dual-stack if supported)
// Try IPv6 first (dual-stack check)
asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port);
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);
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);
}
}
// 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");
// 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(); // guarantee clean state
mAcceptor.close(); // ensure clean state
mAcceptor.open(endpoint_v4.protocol());
mAcceptor.bind(endpoint_v4);
}
@@ -84,10 +72,9 @@ namespace ColumnLynx::Net::TCP {
asio::io_context &mIoContext;
asio::ip::tcp::acceptor mAcceptor;
std::unordered_set<TCPConnection::pointer> mClients;
std::shared_ptr<Utils::LibSodiumWrapper> mSodiumWrapper;
std::shared_ptr<bool> mHostRunning;
Utils::LibSodiumWrapper *mSodiumWrapper;
bool* mHostRunning;
std::unordered_map<std::string, std::string> mRawServerConfig;
std::string mConfigDirPath;
};
}

View File

@@ -1,5 +1,5 @@
// udp_server.hpp - UDP Server for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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
@@ -13,40 +13,27 @@
namespace ColumnLynx::Net::UDP {
class UDPServer {
public:
UDPServer(asio::io_context& ioContext, uint16_t port, std::shared_ptr<bool> hostRunning, bool ipv4Only = false, std::shared_ptr<VirtualInterface> tun = nullptr)
UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning, bool ipv4Only = false, std::shared_ptr<VirtualInterface> tun = nullptr)
: mSocket(ioContext), mHostRunning(hostRunning), mTun(tun)
{
asio::error_code ec_open, ec_v6only, ec_bind;
asio::error_code ec;
if (!ipv4Only) {
// Try IPv6 first (dual-stack check)
asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port);
// 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);
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 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"
);
}
// 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();
mSocket = asio::ip::udp::socket(ioContext); // fully reset internal state
mSocket.close(); // ensure clean state
mSocket.open(endpoint_v4.protocol());
mSocket.bind(endpoint_v4);
}
@@ -69,8 +56,8 @@ namespace ColumnLynx::Net::UDP {
asio::ip::udp::socket mSocket;
asio::ip::udp::endpoint mRemoteEndpoint;
std::array<uint8_t, 2048> mRecvBuffer; // 2048 seems stable
std::shared_ptr<bool> mHostRunning;
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
bool* mHostRunning;
std::shared_ptr<VirtualInterface> mTun;
};
}

View File

@@ -1,5 +1,5 @@
// main.cpp - Client entry point for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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 <asio.hpp>
@@ -12,10 +12,6 @@
#include <cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
#if defined(__WIN32__)
#include <windows.h>
#endif
using asio::ip::tcp;
using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net;
@@ -25,19 +21,18 @@ 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();
@@ -52,23 +47,15 @@ int main(int argc, char** argv) {
#else
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
#endif
("ignore-whitelist", "Ignore if server is not in whitelisted_keys", cxxopts::value<bool>()->default_value("false"))
#if defined(__WIN32__)
/* Get config dir in LOCALAPPDATA\ColumnLynx\ */
("config-dir", "Override config dir path", cxxopts::value<std::string>()->default_value(std::string((std::getenv("LOCALAPPDATA") ? std::getenv("LOCALAPPDATA") : "C:\\ProgramData")) + "\\ColumnLynx\\"));
#elif defined(__APPLE__)
("config-dir", "Override config dir path", cxxopts::value<std::string>()->default_value(std::string((std::getenv("HOME") ? std::getenv("HOME") : "")) + "/Library/Application Support/ColumnLynx/"));
#else
("config-dir", "Override config dir path", cxxopts::value<std::string>()->default_value(std::string((std::getenv("SUDO_USER") ? "/home/" + std::string(std::getenv("SUDO_USER")) : (std::getenv("HOME") ? std::getenv("HOME") : ""))) + "/.config/columnlynx/"));
#endif
("allow-selfsigned", "Allow self-signed certificates", cxxopts::value<bool>()->default_value("false"));
bool insecureMode = options.parse(argc, argv).count("ignore-whitelist") > 0;
bool insecureMode = options.parse(argc, argv).count("allow-selfsigned") > 0;
auto optionsObj = options.parse(argc, argv);
if (optionsObj.count("help")) {
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) 2026, The ColumnLynx Contributors.\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;
}
@@ -81,42 +68,22 @@ 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
// Get the config path, ENV > CLI > /etc/columnlynx
std::string configPath = optionsObj["config-dir"].as<std::string>();
const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR");
if (envConfigPath != nullptr) {
configPath = std::string(envConfigPath);
}
if (configPath.back() != '/' && configPath.back() != '\\') {
#if defined(__WIN32__)
configPath += "\\";
#else
configPath += "/";
#endif
}
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
log("Using virtual interface: " + tun->getName());
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper->getPublicKey(), 32));
debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper->getPrivateKey(), 64));
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper.getPublicKey(), 32));
debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper.getPrivateKey(), 64));
std::shared_ptr<std::array<uint8_t, 32>> aesKey = std::make_shared<std::array<uint8_t, 32>>();
aesKey->fill(0); // Defualt zeroed state until modified by handshake
std::shared_ptr<uint64_t> sessionID = std::make_shared<uint64_t>(0);
if (insecureMode) {
warn("You have started the client with the --ignore-whitelist. This means that the client will NOT attempt to verify the server's public key. This is INSECURE and SHOULDN'T be used!");
}
std::array<uint8_t, 32> aesKey = {0}; // Defualt zeroed state until modified by handshake
uint64_t sessionID = 0;
asio::io_context io;
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, sodiumWrapper, aesKey, sessionID, insecureMode, configPath, tun);
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, aesKey, sessionID, tun);
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, &sodiumWrapper, &aesKey, &sessionID, &insecureMode, tun);
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port, &aesKey, &sessionID, tun);
client->start();
udpClient->start();
@@ -127,18 +94,14 @@ int main(int argc, char** argv) {
});
//ioThread.join();
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));
log("Client connected to " + host + ":" + port);
// 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

@@ -1,9 +1,9 @@
// tcp_client.cpp - TCP Client for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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>
#include <arpa/inet.h>
namespace ColumnLynx::Net::TCP {
void TCPClient::start() {
@@ -13,35 +13,22 @@ namespace ColumnLynx::Net::TCP {
if (!ec) {
asio::async_connect(mSocket, endpoints,
[this, self](asio::error_code ec, const tcp::endpoint&) {
if (!ec) {
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);
});
// 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
try {
asio::ip::make_address(mHost);
self->mIsHostDomain = false; // IPv4 or IPv6 literal
} catch (const asio::system_error&) {
self->mIsHostDomain = true; // hostname / domain
}
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);
@@ -59,9 +46,7 @@ namespace ColumnLynx::Net::TCP {
mStartHeartbeat();
} else {
if (!NetHelper::isExpectedDisconnect(ec)) {
Utils::error("Client connect failed: " + ec.message());
}
Utils::error("Client connect failed: " + ec.message());
}
});
} else {
@@ -92,14 +77,18 @@ namespace ColumnLynx::Net::TCP {
asio::error_code ec;
mHeartbeatTimer.cancel();
// Half-close: stop sending, keep reading until peer FIN
mHandler->socket().shutdown(tcp::socket::shutdown_send, ec);
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
if (ec) {
Utils::error("Error during socket shutdown: " + ec.message());
}
// Do not close immediately; rely on onDisconnect to finalize
Utils::log("Client initiated graceful disconnect (half-close).");
mHandler->socket().close(ec);
if (ec) {
Utils::error("Error during socket close: " + ec.message());
}
mConnected = false;
Utils::log("Client disconnected.");
}
}
@@ -150,14 +139,13 @@ namespace ColumnLynx::Net::TCP {
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)));
std::string hexServerPub = Utils::bytesToHexString(mServerPublicKey, 32);
Utils::log("Received server identity. Public Key: " + hexServerPub);
// Verify pubkey against whitelisted_keys
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys(mConfigDirPath);
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys();
if (std::find(whitelistedKeys.begin(), whitelistedKeys.end(), Utils::bytesToHexString(mServerPublicKey, 32)) == whitelistedKeys.end()) { // Key verification is handled in later steps of the handshake
if (!mInsecureMode) {
if (!(*mInsecureMode)) {
Utils::error("Server public key not in whitelisted_keys. Terminating connection.");
disconnect();
return;
@@ -281,12 +269,6 @@ 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

@@ -1,46 +1,15 @@
// udp_client.cpp - UDP Client for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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() {
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
// TODO: Add IPv6
auto endpoints = mResolver.resolve(asio::ip::udp::v4(), mHost, mPort);
mRemoteEndpoint = *endpoints.begin();
// Open socket using the resolved endpoint's protocol
mSocket.open(mRemoteEndpoint.protocol(), ec);
if (ec) {
Utils::error("UDP socket open failed: " + ec.message());
return;
}
mSocket.open(asio::ip::udp::v4());
Utils::log("UDP Client ready to send to " + mRemoteEndpoint.address().to_string() + ":" + std::to_string(mRemoteEndpoint.port()));
}
@@ -67,9 +36,10 @@ namespace ColumnLynx::Net::UDP {
reinterpret_cast<uint8_t*>(&hdr),
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
);
uint64_t sid = *mSessionIDRef;
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(mSessionIDRef.get()),
reinterpret_cast<uint8_t*>(mSessionIDRef.get()) + sizeof(uint64_t)
reinterpret_cast<uint8_t*>(&sid),
reinterpret_cast<uint8_t*>(&sid) + sizeof(sid)
);
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
@@ -120,11 +90,6 @@ namespace ColumnLynx::Net::UDP {
uint64_t sessionID;
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
if (sessionID != *mSessionIDRef) {
Utils::warn("Got packet that isn't for me! Dropping!");
return;
}
// Decrypt payload
std::vector<uint8_t> ciphertext(
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t),

View File

@@ -1,5 +1,5 @@
// libsodium_wrapper.cpp - Libsodium Wrapper for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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/libsodium_wrapper.hpp>
@@ -41,27 +41,4 @@ namespace ColumnLynx::Utils {
randombytes_buf(randbytes.data(), randbytes.size());
return randbytes;
}
bool LibSodiumWrapper::recomputeKeys(PrivateSeed privateSeed, PublicKey storedPubKey) {
int res = crypto_sign_seed_keypair(mPublicKey.data(), mPrivateKey.data(), privateSeed.data());
if (res != 0) {
return false;
}
// Convert to Curve25519 keys for encryption
res = crypto_sign_ed25519_pk_to_curve25519(mXPublicKey.data(), mPublicKey.data());
res = crypto_sign_ed25519_sk_to_curve25519(mXPrivateKey.data(), mPrivateKey.data());
if (res != 0) {
return false;
}
// Compare to stored for verification
if (sodium_memcmp(mPublicKey.data(), storedPubKey.data(), crypto_sign_PUBLICKEYBYTES) != 0) {
return false;
}
return true;
}
}

View File

@@ -1,100 +0,0 @@
// session_registry.cpp - Session Registry for ColumnLynx
// Copyright (C) 2026 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

@@ -1,5 +1,5 @@
// tcp_message_handler.cpp - TCP Message Handler for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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/tcp/tcp_message_handler.hpp>
@@ -43,19 +43,13 @@ 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 (!ec) {
if (!NetHelper::isExpectedDisconnect(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);
}
Utils::error("Header read failed: " + ec.message());
}
}
);
@@ -67,7 +61,7 @@ namespace ColumnLynx::Net::TCP {
asio::async_read(mSocket, asio::buffer(mBody),
[this, self](asio::error_code ec, std::size_t) {
if (!ec) {
if (!NetHelper::isExpectedDisconnect(ec)) {
std::string payload(mBody.begin(), mBody.end());
// Dispatch based on message type
@@ -77,10 +71,8 @@ namespace ColumnLynx::Net::TCP {
mReadHeader(); // Keep listening
} else {
if (!NetHelper::isExpectedDisconnect(ec)) {
Utils::error("Body read failed: " + ec.message());
}
// Connection closed, trigger disconnect handler
Utils::error("Body read failed: " + ec.message());
if (mOnDisconnect) {
mOnDisconnect(ec);
}

View File

@@ -1,65 +1,29 @@
// utils.cpp - Utility functions for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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/utils.hpp>
namespace ColumnLynx::Utils {
std::string unixMillisToISO8601(uint64_t unixMillis, bool local) {
using namespace std::chrono;
// Convert milliseconds since epoch to system_clock::time_point
system_clock::time_point tp = system_clock::time_point(milliseconds(unixMillis));
// Convert to time_t for localtime conversion
std::time_t tt = system_clock::to_time_t(tp);
std::tm localTm;
if (local) {
#ifdef _WIN32
localtime_s(&localTm, &tt);
#else
localtime_r(&tt, &localTm);
#endif
} else {
#ifdef _WIN32
gmtime_s(&localTm, &tt);
#else
gmtime_r(&tt, &localTm);
#endif
}
// Format the time to ISO 8601
char buffer[30];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", &localTm);
// Append milliseconds
auto ms = duration_cast<milliseconds>(tp.time_since_epoch()) % 1000;
char iso8601[34];
std::snprintf(iso8601, sizeof(iso8601), "%s.%03lld", buffer, static_cast<long long>(ms.count()));
return std::string(iso8601);
}
void log(const std::string &msg) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "\033[0m[" << unixMillisToISO8601(now) << " LOG] " << msg << std::endl;
std::cout << "\033[0m[" << std::to_string(now) << " LOG] " << msg << std::endl;
}
void warn(const std::string &msg) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cerr << "\033[33m[" << unixMillisToISO8601(now) << " WARN] " << msg << "\033[0m" << std::endl;
std::cerr << "\033[33m[" << std::to_string(now) << " WARN] " << msg << "\033[0m" << std::endl;
}
void error(const std::string &msg) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cerr << "\033[31m[" << unixMillisToISO8601(now) << " ERROR] " << msg << "\033[0m" << std::endl;
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[" << unixMillisToISO8601(now) << " DEBUG] " << msg << "\033[0m" << std::endl;
std::cerr << "\033[95m[" << std::to_string(now) << " DEBUG] " << msg << "\033[0m" << std::endl;
#else
return;
#endif
@@ -85,7 +49,7 @@ namespace ColumnLynx::Utils {
}
std::string getVersion() {
return "1.0.1";
return "a0.5";
}
unsigned short serverPort() {
@@ -137,19 +101,14 @@ namespace ColumnLynx::Utils {
return bytes;
}
std::vector<std::string> getWhitelistedKeys(std::string basePath) {
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(basePath + "whitelisted_keys");
if (!file.is_open()) {
warn("Failed to open whitelisted_keys file at path: " + basePath + "whitelisted_keys");
return out;
}
std::ifstream file("whitelisted_keys"); // TODO: This is hardcoded for now, make dynamic
std::string line;
while (std::getline(file, line)) {
@@ -159,15 +118,11 @@ namespace ColumnLynx::Utils {
return out;
}
std::unordered_map<std::string, std::string> getConfigMap(std::string path, std::vector<std::string> requiredKeys) {
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);
if (!file.is_open()) {
throw std::runtime_error("Failed to open config file at path: " + path);
}
std::string line;
while (std::getline(file, line)) {
@@ -190,14 +145,6 @@ namespace ColumnLynx::Utils {
config.insert({ key, val });
}
if (!requiredKeys.empty()) {
for (std::string x : requiredKeys) {
if (config.find(x) == config.end()) {
throw std::runtime_error("Config doesn't contain all required keys! (Missing: '" + x + "')");
}
}
}
return config;
}
}

View File

@@ -1,60 +1,11 @@
// virtual_interface.cpp - Virtual Interface for Network Communication
// Copyright (C) 2026 DcruBro
// 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.
#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)
@@ -72,12 +23,11 @@ namespace ColumnLynx::Net {
if (ioctl(mFd, TUNSETIFF, &ifr) < 0) {
close(mFd);
throw std::runtime_error("TUNSETIFF failed (try running with sudo): " + std::string(strerror(errno)));
throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno)));
}
#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)));
@@ -92,53 +42,39 @@ namespace ColumnLynx::Net {
sc.sc_family = AF_SYSTEM;
sc.ss_sysaddr = AF_SYS_CONTROL;
sc.sc_id = ctlInfo.ctl_id;
sc.sc_unit = 0; // 0 = auto-assign next utunX
sc.sc_unit = 0; // lynx0 (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 with sudo)");
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 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
// 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 = "utun0"; // Fallback (should not happen)
mIfName = "utunX";
}
Utils::log("VirtualInterface: opened macOS UTUN: " + mIfName);
#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");
// 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)
WINTUN_SESSION_HANDLE session =
WintunStartSession(adapter, 0x200000); // ring buffer size
if (!session)
throw std::runtime_error("Failed to start Wintun session");
mHandle = pWintunGetReadWaitEvent(mSession);
mFd = -1;
mHandle = WintunGetReadWaitEvent(session);
mFd = -1; // not used on Windows
mIfName = ifName;
#else
throw std::runtime_error("Unsupported platform");
@@ -151,82 +87,32 @@ namespace ColumnLynx::Net {
if (mFd >= 0)
close(mFd);
#elif defined(_WIN32)
if (mSession)
pWintunEndSession(mSession);
// 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__)
// Linux TUN: blocking read is fine, unblocks on fd close / EINTR
#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, just return empty
return {}; // Interrupted, return empty
}
throw std::runtime_error("read() failed: " + std::string(strerror(errno)));
}
buf.resize(n);
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)
DWORD size = 0;
BYTE* packet = pWintunReceivePacket(mSession, &size);
if (!packet)
return {};
std::vector<uint8_t> buf(packet, packet + size);
pWintunReleaseReceivePacket(mSession, packet);
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
@@ -234,52 +120,16 @@ namespace ColumnLynx::Net {
// ------------------------------ Write ------------------------------
void VirtualInterface::writePacket(const std::vector<uint8_t>& packet) {
#if defined(__linux__)
// Linux TUN expects raw IP 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(__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)
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);
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
}
@@ -305,52 +155,6 @@ 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);
// Wipe old routes
//snprintf(cmd, sizeof(cmd),
// "route -n delete -net %s",
// 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
// ------------------------------------------------------------
@@ -362,13 +166,6 @@ namespace ColumnLynx::Net {
std::string ipStr = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP);
// Wipe the current config
snprintf(cmd, sizeof(cmd),
"ip addr flush dev %s",
mIfName.c_str()
);
system(cmd);
snprintf(cmd, sizeof(cmd),
"ip addr add %s/%d peer %s dev %s",
ipStr.c_str(), prefixLen, peerStr.c_str(), mIfName.c_str());
@@ -391,32 +188,12 @@ namespace ColumnLynx::Net {
std::string ipStr = ipv4ToString(clientIP);
std::string peerStr = ipv4ToString(serverIP);
std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false);
Utils::debug("Prefix string: " + prefixStr);
std::string prefixStr = ipv4ToString(prefixLen);
// Reset
// Set netmask (/24 CIDR temporarily with raw command, improve later)
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);
// Set
snprintf(cmd, sizeof(cmd),
"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);
// Host bits are auto-normalized by the kernel on macOS, so we don't need to worry about them not being zeroed out.
snprintf(cmd, sizeof(cmd),
"route -n add -net %s/%d -interface %s",
ipStr.c_str(), prefixLen, mIfName.c_str());
"ifconfig lynx0 %s %s mtu %d netmask %s up",
ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
system(cmd);
Utils::log("Executed command: " + std::string(cmd));
@@ -433,66 +210,29 @@ namespace ColumnLynx::Net {
uint16_t mtu)
{
#ifdef _WIN32
// Interface alias → LUID → Index
std::wstring ifAlias(mIfName.begin(), mIfName.end());
std::string ip = ipv4ToString(clientIP);
std::string gw = ipv4ToString(serverIP);
std::string mask;
NET_LUID luid;
if (ConvertInterfaceAliasToLuid(ifAlias.c_str(), &luid) != NO_ERROR)
return false;
// Convert prefixLen → subnet mask
uint32_t maskInt = (prefixLen == 0) ? 0 : (0xFFFFFFFF << (32 - prefixLen));
mask = ipv4ToString(maskInt);
NET_IFINDEX ifIndex;
if (ConvertInterfaceLuidToIndex(&luid, &ifIndex) != NO_ERROR)
return false;
char cmd[256];
// ssign IPv4 address + prefix
MIB_UNICASTIPADDRESS_ROW addr;
InitializeUnicastIpAddressEntry(&addr);
// 1. Set the static IP + mask + gateway
snprintf(cmd, sizeof(cmd),
"netsh interface ip set address name=\"%s\" static %s %s %s",
mIfName.c_str(), ip.c_str(), mask.c_str(), gw.c_str()
);
system(cmd);
addr.InterfaceIndex = ifIndex;
addr.Address.si_family = AF_INET;
addr.Address.Ipv4.sin_addr.s_addr = htonl(clientIP);
addr.OnLinkPrefixLength = prefixLen;
addr.DadState = IpDadStatePreferred;
if (CreateUnicastIpAddressEntry(&addr) != NO_ERROR)
return false;
// Set MTU
MIB_IFROW ifRow;
ifRow.dwIndex = ifIndex;
if (GetIfEntry(&ifRow) != NO_ERROR)
return false;
ifRow.dwMtu = mtu;
if (SetIfEntry(&ifRow) != NO_ERROR)
return false;
// Add persistent route for VPN network via this interface
uint32_t mask =
(prefixLen == 0) ? 0 : (0xFFFFFFFFu << (32 - prefixLen));
uint32_t network = clientIP & mask;
MIB_IPFORWARD_ROW2 route;
InitializeIpForwardEntry(&route);
route.InterfaceIndex = ifIndex;
route.DestinationPrefix.Prefix.si_family = AF_INET;
route.DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = htonl(network);
route.DestinationPrefix.PrefixLength = prefixLen;
route.NextHop.si_family = AF_INET;
route.NextHop.Ipv4.sin_addr.s_addr = 0;
route.Metric = 1;
route.Protocol = static_cast<NL_ROUTE_PROTOCOL>(MIB_IPPROTO_NETMGMT);
route.ValidLifetime = 0xFFFFFFFF;
route.PreferredLifetime = 0xFFFFFFFF;
DWORD r = CreateIpForwardEntry2(&route);
if (r != NO_ERROR && r != ERROR_OBJECT_ALREADY_EXISTS)
return false;
// 2. Set MTU (separate command)
snprintf(cmd, sizeof(cmd),
"netsh interface ipv4 set subinterface \"%s\" mtu=%u store=persistent",
mIfName.c_str(), mtu
);
system(cmd);
return true;
#else

View File

@@ -1,11 +1,9 @@
// main.cpp - Server entry point for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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 <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>
@@ -16,10 +14,6 @@
#include <cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
#if defined(__WIN32__)
#include <windows.h>
#endif
using asio::ip::tcp;
using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net::TCP;
@@ -29,7 +23,20 @@ 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");
@@ -41,12 +48,7 @@ int main(int argc, char** argv) {
#else
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
#endif
#if defined(__WIN32__)
/* Get config dir in LOCALAPPDATA\ColumnLynx\ */
("config-dir", "Override config dir path", cxxopts::value<std::string>()->default_value("C:\\ProgramData\\ColumnLynx\\"));
#else
("config-dir", "Override config dir path", cxxopts::value<std::string>()->default_value("/etc/columnlynx"));
#endif
("config", "Override config file path", cxxopts::value<std::string>()->default_value("./server_config"));
PanicHandler::init();
@@ -55,7 +57,7 @@ int main(int argc, char** argv) {
if (optionsObj.count("help")) {
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) 2026, The ColumnLynx Contributors.\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;
}
@@ -66,31 +68,16 @@ 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
// Get the config path, ENV > CLI > /etc/columnlynx
std::string configPath = optionsObj["config-dir"].as<std::string>();
const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR");
if (envConfigPath != nullptr) {
configPath = std::string(envConfigPath);
}
if (configPath.back() != '/' && configPath.back() != '\\') {
#if defined(__WIN32__)
configPath += "\\";
#else
configPath += "/";
#endif
}
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config");
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(optionsObj["config"].as<std::string>());
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
log("Using virtual interface: " + tun->getName());
// Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory)
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
LibSodiumWrapper sodiumWrapper = LibSodiumWrapper();
auto itPubkey = config.find("SERVER_PUBLIC_KEY");
auto itPrivkey = config.find("SERVER_PRIVATE_KEY");
@@ -99,37 +86,32 @@ int main(int argc, char** argv) {
log("Loading keypair from config file.");
PublicKey pk;
PrivateSeed seed;
PrivateKey sk;
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), seed.size(), seed.begin());
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), sk.size(), sk.begin());
std::copy_n(Utils::hexStringToBytes(itPubkey->second).begin(), pk.size(), pk.begin());
if (!sodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute keypair from config file values!");
}
sodiumWrapper.setKeys(pk, sk);
} else {
#if defined(DEBUG)
warn("No keypair found in config file! Using random key.");
#else
throw std::runtime_error("No keypair found in config file! Cannot start server without keys.");
#endif
}
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
std::shared_ptr<bool> hostRunning = std::make_shared<bool>(true);
bool hostRunning = true;
asio::io_context io;
auto server = std::make_shared<TCPServer>(io, serverPort(), sodiumWrapper, hostRunning, configPath, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), hostRunning, ipv4Only, tun);
auto server = std::make_shared<TCPServer>(io, serverPort(), &sodiumWrapper, &hostRunning, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), &hostRunning, ipv4Only, tun);
asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](const std::error_code&, int) {
log("Received termination signal. Shutting down server gracefully.");
done = 1;
asio::post(io, [&]() {
*hostRunning = false;
hostRunning = false;
server->stop();
udpServer->stop();
});
@@ -147,37 +129,25 @@ 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;
}
const uint8_t* ip = packet.data();
uint32_t srcIP = ntohl(*(uint32_t*)(ip + 12)); // IPv4 source address offset
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset in IPv6-mapped header
// First, check if destination IP is a registered client (e.g., server responding to client or client-to-client)
auto dstSession = SessionRegistry::getInstance().getByIP(dstIP);
if (dstSession) {
// Destination is a registered client, forward to that client's session
udpServer->sendData(dstSession->sessionID, std::string(packet.begin(), packet.end()));
auto session = SessionRegistry::getInstance().getByIP(dstIP);
if (!session) {
Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipv4ToString(dstIP));
continue;
}
// Destination is not a registered client, check if source is (for external routing)
auto srcSession = SessionRegistry::getInstance().getByIP(srcIP);
if (srcSession) {
// Source is a registered client, write to TUN interface to forward to external destination
tun->writePacket(packet);
continue;
}
// Neither source nor destination is registered, drop the packet
Utils::warn("TUN: No session found for source IP " + VirtualInterface::ipv4ToString(srcIP) +
" or destination IP " + VirtualInterface::ipv4ToString(dstIP));
udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end()));
}
log("Shutting down server...");
/*hostRunning = false;
server->stop();
udpServer->stop();*/
io.stop();
if (ioThread.joinable()) {

View File

@@ -1,44 +1,25 @@
// tcp_connection.cpp - TCP Connection for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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() {
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) {
// 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());
}
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: " + mRemoteIP);
Utils::log("Client connected: " + mHandler->socket().remote_endpoint().address().to_string());
}
void TCPConnection::sendMessage(ServerMessageType type, const std::string& data) {
@@ -51,19 +32,23 @@ namespace ColumnLynx::Net::TCP {
mOnDisconnect = std::move(cb);
}
void TCPConnection::disconnect(bool echo) {
if (echo) {
mHandler->sendMessage(ServerMessageType::GRACEFUL_DISCONNECT, "Server initiated disconnect.");
}
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;
// 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());
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());
}
// Do not close immediately; final cleanup happens in onDisconnect
Utils::log("Initiated graceful disconnect (half-close) to " + mRemoteIP);
}
uint64_t TCPConnection::getSessionID() const {
@@ -107,7 +92,7 @@ namespace ColumnLynx::Net::TCP {
}
void TCPConnection::mHandleMessage(ClientMessageType type, const std::string& data) {
std::string& reqAddr = mRemoteIP;
std::string reqAddr = mHandler->socket().remote_endpoint().address().to_string();
switch (type) {
case ClientMessageType::HANDSHAKE_INIT: {
@@ -145,7 +130,7 @@ namespace ColumnLynx::Net::TCP {
Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size()));
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys(mConfigDirPath);
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);
@@ -217,18 +202,7 @@ namespace ColumnLynx::Net::TCP {
// 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::string networkString = mRawServerConfig->find("NETWORK")->second; // The load check guarantees that this value exists
uint8_t configMask = std::stoi(mRawServerConfig->find("SUBNET_MASK")->second); // Same deal here
uint32_t baseIP = Net::VirtualInterface::stringToIpv4(networkString);
if (baseIP == 0) {
Utils::warn("Your NETWORK value in the server configuration is malformed! I will not be able to accept connections! (Connection " + reqAddr + " was killed)");
disconnect();
return;
}
uint32_t clientIP = SessionRegistry::getInstance().getFirstAvailableIP(baseIP, configMask);
uint32_t clientIP = SessionRegistry::getInstance().getFirstAvailableIP();
if (clientIP == 0) {
Utils::warn("Out of available IPs! Disconnecting client " + reqAddr);
@@ -240,11 +214,13 @@ namespace ColumnLynx::Net::TCP {
tunConfig.version = Utils::protocolVersion();
tunConfig.prefixLength = 24;
tunConfig.mtu = 1420;
tunConfig.serverIP = htonl(baseIP + 1); // e.g. 10.10.0.1
tunConfig.clientIP = htonl(clientIP); // e.g. 10.10.0.X
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));
@@ -262,7 +238,6 @@ namespace ColumnLynx::Net::TCP {
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));
SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP);
} catch (const std::exception& e) {
Utils::error("Failed to decrypt HANDSHAKE_EXCHANGE_KEY from " + reqAddr + ": " + e.what());
@@ -287,11 +262,6 @@ 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;

View File

@@ -1,5 +1,5 @@
// tcp_server.cpp - TCP Server for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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_server.hpp>
@@ -35,8 +35,6 @@ namespace ColumnLynx::Net::TCP {
auto client = TCPConnection::create(
std::move(socket),
mSodiumWrapper,
&mRawServerConfig,
mConfigDirPath,
[this](std::shared_ptr<TCPConnection> c) {
mClients.erase(c);
Utils::log("Client removed.");

View File

@@ -1,5 +1,5 @@
// udp_server.cpp - UDP Server for ColumnLynx
// Copyright (C) 2026 DcruBro
// 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/udp/udp_server.hpp>