40 Commits

Author SHA1 Message Date
6484f8f5c6 Merge branch 'beta' 2026-02-13 13:06:18 +01:00
31cb3d0a02 Merge branch 'dev' into beta 2026-02-13 13:05:59 +01:00
1136892c5d Log proto version on client 2026-02-13 13:05:34 +01:00
918b80931e Merge branch 'beta' - Version 1.1.0
This version introduces protocol version 2:
 - UDP Headers stripped to 16 Bytes, allowing more throughput
 - Nonce is derived, but unique every packet - allows faster sending
2026-02-10 19:35:19 +01:00
0299b03d9c Merge branch 'dev' into beta - Version 1.1.0
This version introduces protocol version 2:
 - UDP Headers stripped to 16 Bytes, allowing more throughput
 - Nonce is derived, but unique every packet - allows faster sending
2026-02-10 19:33:56 +01:00
8c54250449 enforce unique ids 2026-02-10 19:32:26 +01:00
27bd2cd2ec endianness 2026-02-10 19:24:07 +01:00
14298453b3 TESTING: protocol version 2 2026-02-10 13:27:15 +01:00
316498c745 TESTING: Move server stuff to a server session config for central/global resources 2026-02-09 14:20:26 +01:00
6d40dbe00d version update 2026-02-09 07:32:39 +01:00
757d0d251d Moved client data passing to a dedicated ClientSession class instead of passing through a bunch of pointers at init 2026-02-08 19:20:27 +01:00
07458c348b Merge branch 'beta' 2026-01-18 19:55:13 +01:00
204f89006f Merge branch 'dev' into beta 2026-01-18 19:54:59 +01:00
e61a429f24 Fix lowercase whitelisted_keys not being recognized as entries 2026-01-18 19:54:49 +01:00
833629486e Merge branch 'beta'; Version 1.0.1 2026-01-18 19:48:02 +01:00
57d260976c Merge branch 'dev' into beta 2026-01-18 19:47:48 +01:00
db1b919981 Fix CMakeLists for PKGBUILD 2026-01-18 19:47:15 +01:00
2117b6a62b Fix lowercase whitelisted_keys not being recognized as entries 2026-01-17 15:33:19 +01:00
cbfbcaa153 Merge branch 'beta' 2026-01-11 20:32:46 +01:00
4fa26d51d0 Merge branch 'dev' into beta 2026-01-11 20:32:28 +01:00
7d56f9db5d Update year 2026-01-10 19:10:37 +01:00
4609e85ca9 Logging uses ISO8601 instead of Unix Millis 2026-01-04 19:38:05 +01:00
154296bcdc README Update 2026-01-03 16:36:10 +01:00
867b2c953a Patch 1.0.1 START 2026-01-01 21:51:16 +01:00
83693ed1da Update README to be more clear about IPv4 forwarding 2026-01-01 21:48:42 +01:00
62335f3693 Test fixing writing bug 2026-01-01 21:27:13 +01:00
2d3d6afb07 Test: fix forwarding 2026-01-01 21:18:51 +01:00
e101f5ddd6 Merge branch 'beta' - Version 1.0.0 2026-01-01 17:21:28 +01:00
e1118ccafe Merge branch 'dev' into beta 2026-01-01 17:21:15 +01:00
ccbacd1180 interface on win32 - v1 2026-01-01 17:15:58 +01:00
cf3ec30492 interface on win32 - v1 2026-01-01 17:11:02 +01:00
1d34953f25 interface on win32 - v1 2026-01-01 17:08:35 +01:00
c715a43a10 Merge branch 'beta' - Version 1.0.0 2026-01-01 16:33:21 +01:00
00f72e1a64 Merge branch 'dev' into beta - Version 1.0.0 2026-01-01 16:32:59 +01:00
b9903f5a8e Version 1.0.0 2026-01-01 16:32:48 +01:00
3cd99243ad Version 1.0.0 2026-01-01 16:32:14 +01:00
8f536abe77 Merge branch 'dev' into beta - Version 1.0.0 2026-01-01 16:23:37 +01:00
1f5a0585f3 Version 1.0.0 2026-01-01 16:22:17 +01:00
3dc5c04bf1 Auto set route on macos 2025-12-31 21:12:23 +01:00
37ddb82d9a Refactor args 2025-12-30 03:39:35 +01:00
34 changed files with 980 additions and 274 deletions

View File

@@ -3,7 +3,7 @@
## ASIO C++ Library
- **Name:** ASIO (standalone)
- **Website:** https://think-async.com/Asio/
- **Copyright:** (c) 2003-2025 Christopher M. Kohlhoff
- **Copyright:** (c) 2003-2026 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-2025 Christopher M. Kohlhoff
- **Copyright:** (c) 2014-2026 Christopher M. Kohlhoff
- **License:** MIT License
- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt`
## Wintun C++ Library
- **Name:** wintun
- **Website:** https://www.wintun.net/
- **Copyright:** (c) 2018-2025 WireGuard LLC
- **Copyright:** (c) 2018-2026 WireGuard LLC
- **License:** MIT License OR GPL-2.0 License
- **License Text:** See `third_party/wintun/`
- **Utilized Under:** MIT License

View File

@@ -6,10 +6,12 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx
VERSION 0.3.0
VERSION 1.1.0
LANGUAGES CXX
)
include(GNUInstallDirs)
# ---------------------------------------------------------
# General C++ setup
# ---------------------------------------------------------
@@ -40,7 +42,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 -O1)
add_link_options(-pthread)
endif()
@@ -50,7 +52,7 @@ endif()
FetchContent_Declare(
Sodium
GIT_REPOSITORY https://github.com/robinlinden/libsodium-cmake.git
GIT_TAG e5b985ad0dd235d8c4307ea3a385b45e76c74c6a # Last updated at 2025-04-13
GIT_TAG e5b985ad0dd235d8c4307ea3a385b45e76c74c6a
)
set(SODIUM_DISABLE_TESTS ON CACHE BOOL "" FORCE)
@@ -157,3 +159,17 @@ target_include_directories(server PRIVATE
)
target_compile_definitions(server PRIVATE ASIO_STANDALONE)
set_target_properties(server PROPERTIES OUTPUT_NAME "columnlynx_server")
# ---------------------------------------------------------
# Install rules
# ---------------------------------------------------------
install(TARGETS
client
server
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES
LICENSES/GPL-2.0-only.txt
LICENSES/GPL-3.0.txt
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME})

159
README.md
View File

@@ -18,22 +18,155 @@ 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 the same directory as the working directory**.
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
```
### 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
- **SERVER_PRIVATE_KEY** (Hex String): The private key to be used
- **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)
**Example:**
```
SERVER_PUBLIC_KEY=787B648046F10DDD0B77A6303BE42D859AA65C52F5708CC3C58EB5691F217C7B
SERVER_PRIVATE_KEY=778604245F57B847E63BD85DE8208FF1A127FB559895195928C3987E246B77B8787B648046F10DDD0B77A6303BE42D859AA65C52F5708CC3C58EB5691F217C7B
SERVER_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
SERVER_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
NETWORK=10.10.0.0
SUBNET_MASK=24
```
@@ -53,14 +186,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
- **CLIENT_PRIVATE_KEY** (Hex String): The private key to be used
- **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
**Example:**
```
CLIENT_PUBLIC_KEY=8CC8BE1A9D24639D0492EF143E84E2BD4C757C9B3B687E7035173EBFCA8FEDDA
CLIENT_PRIVATE_KEY=9B486A5B1509FA216F9EEFED85CACF2384E9D902A76CC979BFA143C18B869F5C8CC8BE1A9D24639D0492EF143E84E2BD4C757C9B3B687E7035173EBFCA8FEDDA
CLIENT_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
CLIENT_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
```
<hr></hr>
@@ -80,6 +213,8 @@ ColumnLynx makes use of both **TCP** and **UDP**. **TCP** is used for the initia
It operates on port **48042** for both TCP and UDP.
Current protocol version is **2**.
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
@@ -98,7 +233,7 @@ The Client now generates a random aesKey (32 bytes long)
C: HANDSHAKE_EXCHANGE_KEY <aesKey Encrypted with Server Public Key>
The Server now assigns a local 8 byte session ID in the Session Registry.
The Server now assigns a local 4 byte session ID in the Session Registry.
S: HANDSHAKE_EXCHANGE_KEY_CONFIRM <Assigned SessionID>
```
@@ -109,7 +244,7 @@ The **Client** and **Server** have now securely exchanged a symmetric **AES Key*
Packet exchange and the general data tunneling is done via **Standard UDP** (*see the **UDP Packet** in **Data***).
The **header** of the sent packet always includes a **random 12 byte nonce** used to obscure the **encrypted payload / data** and the **Session ID** assigned by the server to the client (8 bytes). This makes the header **20 bytes long**.
The **header** of the sent packet always includes a **12 byte nonce** derived from a random **4 byte base nonce** and the **send count** to ensure a unique nonce, used to obscure the **encrypted payload / data** and the **Session ID** assigned by the server to the client (4 bytes). This makes the header **16 bytes long**.
The **payload / data** of the sent packet is **always encrypted** using the exchanged **AES Key** and obscured using the **random nonce**.
@@ -165,7 +300,7 @@ The **Data** is generally just the **raw underlying packet** forwarded to the se
| Type | Length | Name | Description |
|:-----|:-------|:-----|:------------|
| uint8_t | 12 bytes | **Header** - Nonce | Random nonce to obfuscate encrypted contents |
| uint64_t | 8 bytes | **Header** - Session ID | The unique and random session identifier for the client |
| uint32_t | 4 bytes | **Header** - Session ID | The unique and random session identifier for the client |
| uint8_t | variable | Data | General data / payload |
## Misc.

View File

@@ -0,0 +1,102 @@
// client_session.hpp - Client Session data 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.
#pragma once
#include <memory>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <shared_mutex>
namespace ColumnLynx {
struct ClientState {
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper;
SymmetricKey aesKey;
bool insecureMode;
std::string configPath;
std::shared_ptr<Net::VirtualInterface> virtualInterface;
uint32_t sessionID;
uint64_t recv_cnt;
uint64_t send_cnt;
uint32_t noncePrefix;
~ClientState() { sodium_memzero(aesKey.data(), aesKey.size()); }
ClientState(const ClientState&) = delete;
ClientState& operator=(const ClientState&) = delete;
ClientState(ClientState&&) = default;
ClientState& operator=(ClientState&&) = default;
explicit ClientState() = default;
explicit ClientState(std::shared_ptr<Utils::LibSodiumWrapper> sodium, SymmetricKey& k, bool insecure,
std::string& config, std::shared_ptr<Net::VirtualInterface> tun, uint32_t session, uint64_t recv, uint64_t send)
: sodiumWrapper(sodium), aesKey(k), insecureMode(insecure), configPath(config), virtualInterface(tun), sessionID(session), recv_cnt(recv), send_cnt(send) {}
};
class ClientSession {
public:
// Return a reference to the Client Session instance
static ClientSession& getInstance() { static ClientSession instance; return instance; }
// Return the current client state
std::shared_ptr<ClientState> getClientState() const;
// Set the client state
void setClientState(std::shared_ptr<ClientState> state);
// Get the wrapper for libsodium
const std::shared_ptr<Utils::LibSodiumWrapper>& getSodiumWrapper() const;
// Get the AES key
const SymmetricKey& getAESKey() const;
// Get whether insecure mode is enabled
bool isInsecureMode() const;
// Get the config path
const std::string& getConfigPath() const;
// Get the virtual interface
const std::shared_ptr<Net::VirtualInterface>& getVirtualInterface() const;
// Get the session ID
uint32_t getSessionID() const;
uint64_t getRecvCount() const {
std::shared_lock lock(mMutex);
return mClientState->recv_cnt;
}
uint64_t getSendCount() const {
std::shared_lock lock(mMutex);
return mClientState->send_cnt;
}
uint32_t getNoncePrefix() const {
std::shared_lock lock(mMutex);
return mClientState->noncePrefix;
}
// Setters
void setSodiumWrapper(std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper);
void setAESKey(const SymmetricKey& aesKey);
void setInsecureMode(bool insecureMode);
void setConfigPath(const std::string& configPath);
void setVirtualInterface(std::shared_ptr<Net::VirtualInterface> virtualInterface);
void setSessionID(uint32_t sessionID);
void incrementRecvCount() {
std::unique_lock lock(mMutex);
mClientState->recv_cnt++;
}
void incrementSendCount() {
std::unique_lock lock(mMutex);
mClientState->send_cnt++;
}
void setNoncePrefix(uint32_t prefix) {
std::unique_lock lock(mMutex);
mClientState->noncePrefix = prefix;
}
private:
mutable std::shared_mutex mMutex;
std::shared_ptr<struct ClientState> mClientState{nullptr};
};
}

View File

@@ -1,5 +1,5 @@
// tcp_client.hpp - TCP Client for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -14,8 +14,10 @@
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <string>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <columnlynx/client/client_session.hpp>
using asio::ip::tcp;
@@ -24,28 +26,22 @@ namespace ColumnLynx::Net::TCP {
public:
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::shared_ptr<VirtualInterface> tun = nullptr)
const std::string& port)
:
mResolver(ioContext),
mSocket(ioContext),
mHost(host),
mPort(port),
mLibSodiumWrapper(sodiumWrapper),
mGlobalKeyRef(aesKey),
mSessionIDRef(sessionIDRef),
mInsecureMode(insecureMode),
mHeartbeatTimer(mSocket.get_executor()),
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
mLastHeartbeatSent(std::chrono::steady_clock::now()),
mTun(tun)
mLastHeartbeatSent(std::chrono::steady_clock::now())
{
// Get initial client config
std::string configPath = ClientSession::getInstance().getConfigPath();
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper();
// Preload the config map
mRawClientConfig = Utils::getConfigMap("client_config");
mRawClientConfig = Utils::getConfigMap(configPath + "client_config");
auto itPubkey = mRawClientConfig.find("CLIENT_PUBLIC_KEY");
auto itPrivkey = mRawClientConfig.find("CLIENT_PRIVATE_KEY");
@@ -54,16 +50,22 @@ namespace ColumnLynx::Net::TCP {
Utils::log("Loading keypair from config file.");
PublicKey pk;
PrivateKey sk;
PrivateSeed seed;
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(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(itPubkey->second).begin(), pk.size(), pk.begin());
mLibSodiumWrapper->setKeys(pk, sk);
if (!mLibSodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute keypair from config file values!");
}
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
}
}
@@ -95,19 +97,14 @@ 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;
uint64_t mConnectionSessionID;
uint32_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
asio::steady_timer mHeartbeatTimer;
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
std::chrono::steady_clock::time_point mLastHeartbeatSent;
int mMissedHeartbeats = 0;
bool mIsHostDomain;
Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr;
std::unordered_map<std::string, std::string> mRawClientConfig;
};
}

View File

@@ -1,5 +1,5 @@
// udp_client.hpp - UDP Client for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -10,17 +10,15 @@
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <columnlynx/client/client_session.hpp>
namespace ColumnLynx::Net::UDP {
class UDPClient {
public:
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::shared_ptr<VirtualInterface> tunRef = nullptr)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port), mAesKeyRef(aesKeyRef), mSessionIDRef(sessionIDRef), mTunRef(tunRef)
const std::string& port)
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port)
{
mStartReceive();
}
@@ -43,9 +41,6 @@ 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::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) 2025 DcruBro
// 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.
#pragma once
@@ -17,6 +17,7 @@
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
@@ -53,6 +54,9 @@ 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) 2025 DcruBro
// 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.
#pragma once

View File

@@ -1,5 +1,5 @@
// session_registry.hpp - Session Registry for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -27,8 +27,9 @@ namespace ColumnLynx::Net {
std::chrono::steady_clock::time_point expires{}; // Time of expiry
uint32_t clientTunIP; // Assigned IP
uint32_t serverTunIP; // Server IP
uint64_t sessionID; // Session ID
uint32_t sessionID; // Session ID
Nonce base_nonce{};
uint32_t noncePrefix;
~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); }
SessionState(const SessionState&) = delete;
@@ -36,7 +37,7 @@ namespace ColumnLynx::Net {
SessionState(SessionState&&) = default;
SessionState& operator=(SessionState&&) = default;
explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), uint32_t clientIP = 0, uint32_t serverIP = 0, uint64_t id = 0) : aesKey(k), clientTunIP(clientIP), serverTunIP(serverIP), sessionID(id) {
explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), uint32_t clientIP = 0, uint32_t serverIP = 0, uint32_t id = 0) : aesKey(k), clientTunIP(clientIP), serverTunIP(serverIP), sessionID(id) {
expires = created + ttl;
}
@@ -44,6 +45,11 @@ namespace ColumnLynx::Net {
void setUDPEndpoint(const asio::ip::udp::endpoint& ep) {
udpEndpoint = ep;
}
void setBaseNonce() {
Utils::debug("Generating random base nonce for session " + std::to_string(sessionID));
randombytes_buf(base_nonce.data(), base_nonce.size());
}
};
class SessionRegistry {
@@ -52,19 +58,19 @@ 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(uint32_t sessionID, std::shared_ptr<SessionState> state);
// Lookup a session entry by session ID
std::shared_ptr<const SessionState> get(uint64_t sessionID) const;
std::shared_ptr<const SessionState> get(uint32_t sessionID) const;
// Lookup a session entry by IPv4
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const;
// Get a snapshot of the Session Registry
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const;
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> snapshot() const;
// Remove a session by ID
void erase(uint64_t sessionID);
void erase(uint32_t sessionID);
// Cleanup expired sessions
void cleanupExpired();
@@ -72,21 +78,23 @@ namespace ColumnLynx::Net {
// Get the number of registered sessions
int size() const;
bool exists(uint32_t sessionID) const;
// IP management
// Get the lowest available IPv4 address; Returns 0 if none available
uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const;
// Lock IP to session ID; Do NOT call before put() - You will segfault!
void lockIP(uint64_t sessionID, uint32_t ip);
void lockIP(uint32_t sessionID, uint32_t ip);
// Unlock IP from session ID
void deallocIP(uint64_t sessionID);
void deallocIP(uint32_t sessionID);
private:
mutable std::shared_mutex mMutex;
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> mSessions;
std::unordered_map<uint64_t, uint32_t> mSessionIPs;
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mSessions;
std::unordered_map<uint32_t, uint32_t> mSessionIPs;
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mIPSessions;
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// virtual_interface.hpp - Virtual Interface for Network Communication
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -36,6 +36,12 @@
#include <locale>
#include <codecvt>
#include <wintun/wintun.h>
#include <iphlpapi.h>
#include <netioapi.h>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
#endif
namespace ColumnLynx::Net {

View File

@@ -1,5 +1,5 @@
// panic_handler.hpp - Panic Handler for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once

View File

@@ -1,5 +1,5 @@
// utils.hpp - Utility functions for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -15,6 +15,7 @@
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
#include <bits/stdc++.h>
#ifdef _WIN32
#include <winsock2.h>
@@ -29,6 +30,9 @@ namespace ColumnLynx {
}
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.
@@ -44,7 +48,7 @@ namespace ColumnLynx::Utils {
std::string getVersion();
unsigned short serverPort();
unsigned char protocolVersion();
std::vector<std::string> getWhitelistedKeys();
std::vector<std::string> getWhitelistedKeys(std::string basePath);
// Raw byte to hex string conversion helper
std::string bytesToHexString(const uint8_t* bytes, size_t length);
@@ -61,27 +65,6 @@ namespace ColumnLynx::Utils {
return std::string(reinterpret_cast<const char*>(data), length);
}
inline constexpr uint64_t cbswap64(uint64_t x) {
return ((x & 0x00000000000000FFULL) << 56) |
((x & 0x000000000000FF00ULL) << 40) |
((x & 0x0000000000FF0000ULL) << 24) |
((x & 0x00000000FF000000ULL) << 8) |
((x & 0x000000FF00000000ULL) >> 8) |
((x & 0x0000FF0000000000ULL) >> 24) |
((x & 0x00FF000000000000ULL) >> 40) |
((x & 0xFF00000000000000ULL) >> 56);
}
// host -> big-endian (for little-endian hosts) - 64 bit
inline constexpr uint64_t chtobe64(uint64_t x) {
return cbswap64(x);
}
// big-endian -> host (for little-endian hosts) - 64 bit
inline constexpr uint64_t cbe64toh(uint64_t x) {
return cbswap64(x);
}
template <typename T>
T cbswap128(const T& x) {
static_assert(sizeof(T) == 16, "cbswap128 requires a 128-bit type");

View File

@@ -1,5 +1,5 @@
// tcp_connection.hpp - TCP Connection for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -18,6 +18,7 @@
#include <columnlynx/common/net/session_registry.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <columnlynx/server/server_session.hpp>
namespace ColumnLynx::Net::TCP {
class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
@@ -26,11 +27,9 @@ 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::function<void(pointer)> onDisconnect)
{
auto conn = pointer(new TCPConnection(std::move(socket), sodiumWrapper, serverConfig));
auto conn = pointer(new TCPConnection(std::move(socket)));
conn->mOnDisconnect = std::move(onDisconnect);
return conn;
}
@@ -45,16 +44,14 @@ namespace ColumnLynx::Net::TCP {
void disconnect(bool echo = true);
// Get the assigned session ID
uint64_t getSessionID() const;
uint32_t getSessionID() const;
// Get the assigned AES key; You should probably access this via the Session Registry instead
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)
TCPConnection(asio::ip::tcp::socket socket)
:
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())
@@ -67,10 +64,8 @@ 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;
std::array<uint8_t, 32> mConnectionAESKey;
uint64_t mConnectionSessionID;
uint32_t mConnectionSessionID;
AsymPublicKey mConnectionPublicKey;
asio::steady_timer mHeartbeatTimer;
std::chrono::steady_clock::time_point mLastHeartbeatReceived;

View File

@@ -1,5 +1,5 @@
// tcp_server.hpp - TCP Server for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -17,26 +17,23 @@
#include <columnlynx/server/net/tcp/tcp_connection.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/common/net/protocol_structs.hpp>
#include <columnlynx/server/server_session.hpp>
namespace ColumnLynx::Net::TCP {
class TCPServer {
public:
TCPServer(asio::io_context& ioContext,
uint16_t port,
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
std::shared_ptr<bool> hostRunning, bool ipv4Only = false)
uint16_t port)
: mIoContext(ioContext),
mAcceptor(ioContext),
mSodiumWrapper(sodiumWrapper),
mHostRunning(hostRunning)
mAcceptor(ioContext)
{
// Preload the config map
mRawServerConfig = Utils::getConfigMap("server_config", {"NETWORK", "SUBNET_MASK"});
asio::error_code ec_open, ec_v6only, ec_bind;
if (!ipv4Only) {
bool isIPv4Only = ServerSession::getInstance().isIPv4Only();
if (!isIPv4Only) {
// Try IPv6 (dual-stack if supported)
asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port);
@@ -52,8 +49,8 @@ namespace ColumnLynx::Net::TCP {
}
// If IPv6 bind failed OR IPv6 open failed OR forced IPv4-only
if (ipv4Only || ec_open || ec_bind) {
if (!ipv4Only)
if (isIPv4Only || ec_open || ec_bind) {
if (!isIPv4Only)
Utils::warn("TCP: IPv6 unavailable (open=" + ec_open.message() +
", bind=" + ec_bind.message() +
"), falling back to IPv4 only");
@@ -81,9 +78,6 @@ 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;
std::unordered_map<std::string, std::string> mRawServerConfig;
};
}

View File

@@ -1,5 +1,5 @@
// udp_server.hpp - UDP Server for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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.
#pragma once
@@ -9,16 +9,18 @@
#include <columnlynx/common/utils.hpp>
#include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <columnlynx/server/server_session.hpp>
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)
: mSocket(ioContext), mHostRunning(hostRunning), mTun(tun)
UDPServer(asio::io_context& ioContext, uint16_t port)
: mSocket(ioContext)
{
asio::error_code ec_open, ec_v6only, ec_bind;
if (!ipv4Only) {
if (!mIpv4Only) {
asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port);
// Try opening IPv6 socket
@@ -34,8 +36,8 @@ namespace ColumnLynx::Net::UDP {
}
// Fallback to IPv4 if IPv6 is unusable
if (ipv4Only || ec_open || ec_bind) {
if (!ipv4Only) {
if (mIpv4Only || ec_open || ec_bind) {
if (!mIpv4Only) {
Utils::warn(
"UDP: IPv6 unavailable (open=" + ec_open.message() +
", bind=" + ec_bind.message() +
@@ -59,7 +61,7 @@ namespace ColumnLynx::Net::UDP {
void stop();
// Send UDP data to an endpoint; Fetched via the Session Registry
void sendData(const uint64_t sessionID, const std::string& data);
void sendData(uint32_t sessionID, const std::string& data);
private:
// Start receiving UDP data
@@ -70,7 +72,7 @@ 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::shared_ptr<VirtualInterface> mTun;
bool mIpv4Only = ServerSession::getInstance().isIPv4Only();
const std::shared_ptr<VirtualInterface> mTun = ServerSession::getInstance().getVirtualInterface();
};
}

View File

@@ -0,0 +1,62 @@
// server_session.hpp - Client Session data 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.
#pragma once
#include <memory>
#include <columnlynx/common/libsodium_wrapper.hpp>
#include <array>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <shared_mutex>
namespace ColumnLynx {
struct ServerState {
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper;
std::shared_ptr<Net::VirtualInterface> virtualInterface;
std::string configPath;
std::unordered_map<std::string, std::string> serverConfig;
bool ipv4Only;
bool hostRunning;
~ServerState() = default;
ServerState(const ServerState&) = delete;
ServerState& operator=(const ServerState&) = delete;
ServerState(ServerState&&) = default;
ServerState& operator=(ServerState&&) = default;
explicit ServerState() = default;
};
class ServerSession {
public:
// Return a reference to the Server Session instance
static ServerSession& getInstance() { static ServerSession instance; return instance; }
// Return the current server state
std::shared_ptr<ServerState> getServerState() const;
// Set the server state
void setServerState(std::shared_ptr<ServerState> state);
// Getters
std::shared_ptr<Utils::LibSodiumWrapper> getSodiumWrapper() const;
const std::string& getConfigPath() const;
const std::unordered_map<std::string, std::string>& getRawServerConfig() const;
const std::shared_ptr<Net::VirtualInterface>& getVirtualInterface() const;
bool isIPv4Only() const;
bool isHostRunning() const;
// Setters
void setSodiumWrapper(std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper);
void setConfigPath(const std::string& configPath);
void setRawServerConfig(const std::unordered_map<std::string, std::string>& config);
void setVirtualInterface(std::shared_ptr<Net::VirtualInterface> tun);
void setIPv4Only(bool ipv4Only);
void setHostRunning(bool hostRunning);
private:
mutable std::shared_mutex mMutex;
std::shared_ptr<struct ServerState> mServerState{nullptr};
};
}

View File

@@ -0,0 +1,71 @@
// client_session.cpp - Client Session data 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/client/client_session.hpp>
namespace ColumnLynx {
std::shared_ptr<ClientState> ClientSession::getClientState() const {
std::shared_lock lock(mMutex);
return mClientState;
}
void ClientSession::setClientState(std::shared_ptr<ClientState> state) {
std::unique_lock lock(mMutex);
mClientState = state;
}
const std::shared_ptr<Utils::LibSodiumWrapper>& ClientSession::getSodiumWrapper() const {
return getClientState()->sodiumWrapper;
}
const SymmetricKey& ClientSession::getAESKey() const {
return getClientState()->aesKey;
}
bool ClientSession::isInsecureMode() const {
return getClientState()->insecureMode;
}
const std::string& ClientSession::getConfigPath() const {
return getClientState()->configPath;
}
const std::shared_ptr<Net::VirtualInterface>& ClientSession::getVirtualInterface() const {
return getClientState()->virtualInterface;
}
uint32_t ClientSession::getSessionID() const {
return getClientState()->sessionID;
}
void ClientSession::setSodiumWrapper(std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper) {
std::unique_lock lock(mMutex);
mClientState->sodiumWrapper = sodiumWrapper;
}
void ClientSession::setAESKey(const SymmetricKey& aesKey) {
std::unique_lock lock(mMutex);
mClientState->aesKey = aesKey;
}
void ClientSession::setInsecureMode(bool insecureMode) {
std::unique_lock lock(mMutex);
mClientState->insecureMode = insecureMode;
}
void ClientSession::setConfigPath(const std::string& configPath) {
std::unique_lock lock(mMutex);
mClientState->configPath = configPath;
}
void ClientSession::setVirtualInterface(std::shared_ptr<Net::VirtualInterface> virtualInterface) {
std::unique_lock lock(mMutex);
mClientState->virtualInterface = virtualInterface;
}
void ClientSession::setSessionID(uint32_t sessionID) {
std::unique_lock lock(mMutex);
mClientState->sessionID = sessionID;
}
}

View File

@@ -1,5 +1,5 @@
// main.cpp - Client entry point for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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 <asio.hpp>
@@ -11,6 +11,12 @@
#include <columnlynx/client/net/udp/udp_client.hpp>
#include <cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <columnlynx/client/client_session.hpp>
#include <thread>
#if defined(__WIN32__)
#include <windows.h>
#endif
using asio::ip::tcp;
using namespace ColumnLynx::Utils;
@@ -48,15 +54,23 @@ int main(int argc, char** argv) {
#else
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
#endif
("allow-selfsigned", "Allow self-signed certificates", cxxopts::value<bool>()->default_value("false"));
("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
bool insecureMode = options.parse(argc, argv).count("allow-selfsigned") > 0;
bool insecureMode = options.parse(argc, argv).count("ignore-whitelist") > 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) 2025, The ColumnLynx Contributors.\n";
std::cout << "Copyright (C) 2026, The ColumnLynx Contributors.\n";
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
return 0;
}
@@ -72,20 +86,52 @@ int main(int argc, char** argv) {
//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
}
struct ClientState initialState{};
initialState.configPath = configPath;
initialState.insecureMode = insecureMode;
initialState.send_cnt = 0;
initialState.recv_cnt = 0;
randombytes_buf(&initialState.noncePrefix, sizeof(uint32_t)); // Randomize nonce prefix
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
log("Using virtual interface: " + tun->getName());
initialState.virtualInterface = tun;
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));
initialState.sodiumWrapper = sodiumWrapper;
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);
std::array<uint8_t, 32> aesKey = std::array<uint8_t, 32>();
aesKey.fill(0); // Defualt zeroed state until modified by handshake
uint32_t sessionID = 0;
initialState.aesKey = aesKey;
initialState.sessionID = sessionID;
ColumnLynx::ClientSession::getInstance().setClientState(std::make_shared<ClientState>(std::move(initialState))); // Set initial state
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!");
}
asio::io_context io;
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);
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port); // TODO: Move to ClientSession state
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port);
client->start();
udpClient->start();

View File

@@ -1,5 +1,5 @@
// tcp_client.cpp - TCP Client for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/client/net/tcp/tcp_client.hpp>
@@ -46,10 +46,13 @@ namespace ColumnLynx::Net::TCP {
std::vector<uint8_t> payload;
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
payload.push_back(Utils::protocolVersion());
Utils::log("Using protocol version: " + std::to_string(Utils::protocolVersion()));
/*payload.insert(payload.end(),
mLibSodiumWrapper->getXPublicKey(),
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
);*/
const auto& mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper();
payload.insert(payload.end(),
mLibSodiumWrapper->getPublicKey(),
mLibSodiumWrapper->getPublicKey() + crypto_sign_PUBLICKEYBYTES
@@ -131,10 +134,8 @@ namespace ColumnLynx::Net::TCP {
mHandler->socket().close(ec);
mConnected = false;
mGlobalKeyRef = nullptr;
if (mSessionIDRef) {
*mSessionIDRef = 0;
}
ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros
ClientSession::getInstance().setSessionID(0);
return;
}
@@ -150,13 +151,15 @@ 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();
const std::string& configPath = ClientSession::getInstance().getConfigPath();
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys(configPath);
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 (!ClientSession::getInstance().isInsecureMode()) {
Utils::error("Server public key not in whitelisted_keys. Terminating connection.");
disconnect();
return;
@@ -192,16 +195,14 @@ namespace ColumnLynx::Net::TCP {
// Generate AES key and send confirmation
mConnectionAESKey = Utils::LibSodiumWrapper::generateRandom256Bit();
if (mGlobalKeyRef) { // Copy to the global reference
std::copy(mConnectionAESKey.begin(), mConnectionAESKey.end(), mGlobalKeyRef->begin());
}
ClientSession::getInstance().setAESKey(mConnectionAESKey);
AsymNonce nonce{};
randombytes_buf(nonce.data(), nonce.size());
// TODO: This is pretty redundant, it should return the required type directly
std::array<uint8_t, 32> arrayPrivateKey;
std::copy(mLibSodiumWrapper->getXPrivateKey(),
mLibSodiumWrapper->getXPrivateKey() + 32,
std::copy(ClientSession::getInstance().getSodiumWrapper()->getXPrivateKey(),
ClientSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32,
arrayPrivateKey.begin());
std::vector<uint8_t> encr = Utils::LibSodiumWrapper::encryptAsymmetric(
@@ -244,19 +245,18 @@ namespace ColumnLynx::Net::TCP {
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID));
std::memcpy(&mTunConfig, decrypted.data() + sizeof(mConnectionSessionID), sizeof(Protocol::TunConfig));
mConnectionSessionID = Utils::cbe64toh(mConnectionSessionID);
mConnectionSessionID = ntohl(mConnectionSessionID);
Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID));
if (mSessionIDRef) { // Copy to the global reference
*mSessionIDRef = mConnectionSessionID;
}
ClientSession::getInstance().setSessionID(mConnectionSessionID);
uint32_t clientIP = ntohl(mTunConfig.clientIP);
uint32_t serverIP = ntohl(mTunConfig.serverIP);
uint8_t prefixLen = mTunConfig.prefixLength;
uint16_t mtu = mTunConfig.mtu;
const auto& mTun = ClientSession::getInstance().getVirtualInterface();
if (mTun) {
mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
}

View File

@@ -1,5 +1,5 @@
// udp_client.cpp - UDP Client for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/client/net/udp/udp_client.hpp>
@@ -46,9 +46,14 @@ namespace ColumnLynx::Net::UDP {
void UDPClient::sendMessage(const std::string& data) {
UDPPacketHeader hdr{};
randombytes_buf(hdr.nonce.data(), hdr.nonce.size());
uint8_t nonce[12];
uint32_t prefix = ClientSession::getInstance().getNoncePrefix();
uint64_t sendCount = ClientSession::getInstance().getSendCount();
memcpy(nonce, &prefix, sizeof(uint32_t)); // Prefix nonce with client-specific random value
memcpy(nonce + sizeof(uint32_t), &sendCount, sizeof(uint64_t)); // Use send count as nonce suffix to ensure uniqueness
std::copy_n(nonce, 12, hdr.nonce.data());
if (mAesKeyRef == nullptr || mSessionIDRef == nullptr) {
if (ClientSession::getInstance().getAESKey().empty() || ClientSession::getInstance().getSessionID() == 0) {
Utils::error("UDP Client AES key or Session ID reference is null!");
return;
}
@@ -57,24 +62,28 @@ namespace ColumnLynx::Net::UDP {
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
*mAesKeyRef, hdr.nonce, "udp-data"
ClientSession::getInstance().getAESKey(), hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
);
std::vector<uint8_t> packet;
packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size());
packet.reserve(sizeof(UDPPacketHeader) + encryptedPayload.size());
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&hdr),
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
);
uint32_t sessionID = static_cast<uint32_t>(ClientSession::getInstance().getSessionID());
uint32_t sessionIDNet = sessionID;
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(mSessionIDRef.get()),
reinterpret_cast<uint8_t*>(mSessionIDRef.get()) + sizeof(uint64_t)
reinterpret_cast<uint8_t*>(&sessionIDNet),
reinterpret_cast<uint8_t*>(&sessionIDNet) + sizeof(uint32_t)
);
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
mSocket.send_to(asio::buffer(packet), mRemoteEndpoint);
Utils::debug("Sent UDP packet of size " + std::to_string(packet.size()));
ClientSession::getInstance().incrementSendCount();
}
void UDPClient::stop() {
@@ -107,7 +116,7 @@ namespace ColumnLynx::Net::UDP {
}
void UDPClient::mHandlePacket(std::size_t bytes) {
if (bytes < sizeof(UDPPacketHeader) + sizeof(uint64_t)) {
if (bytes < sizeof(UDPPacketHeader) + sizeof(uint32_t)) {
Utils::warn("UDP Client received packet too small to process.");
return;
}
@@ -117,27 +126,28 @@ namespace ColumnLynx::Net::UDP {
std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader));
// Parse session ID
uint64_t sessionID;
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
uint32_t sessionIDNet;
std::memcpy(&sessionIDNet, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint32_t));
uint32_t sessionID = ntohl(sessionIDNet);
if (sessionID != *mSessionIDRef) {
Utils::warn("Got packet that isn't for me! Dropping!");
if (sessionID != ClientSession::getInstance().getSessionID()) {
Utils::warn("This packet that isn't for me! Dropping!");
return;
}
// Decrypt payload
std::vector<uint8_t> ciphertext(
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t),
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t),
mRecvBuffer.begin() + bytes
);
if (mAesKeyRef == nullptr) {
if (ClientSession::getInstance().getAESKey().empty()) {
Utils::error("UDP Client AES key reference is null!");
return;
}
std::vector<uint8_t> plaintext = Utils::LibSodiumWrapper::decryptMessage(
ciphertext.data(), ciphertext.size(), *mAesKeyRef, hdr.nonce, "udp-data"
ciphertext.data(), ciphertext.size(), ClientSession::getInstance().getAESKey(), hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
);
@@ -149,6 +159,7 @@ namespace ColumnLynx::Net::UDP {
Utils::debug("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
// Write to TUN
const auto& mTunRef = ClientSession::getInstance().getVirtualInterface();
if (mTunRef) {
mTunRef->writePacket(plaintext);
}

View File

@@ -1,5 +1,5 @@
// libsodium_wrapper.cpp - Libsodium Wrapper for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/libsodium_wrapper.hpp>
@@ -41,4 +41,27 @@ 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,17 +1,17 @@
// session_registry.cpp - Session Registry for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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) {
void SessionRegistry::put(uint32_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_ptr<const SessionState> SessionRegistry::get(uint32_t sessionID) const {
std::shared_lock lock(mMutex);
auto it = mSessions.find(sessionID);
return (it == mSessions.end()) ? nullptr : it->second;
@@ -23,14 +23,14 @@ namespace ColumnLynx::Net {
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::unordered_map<uint32_t, std::shared_ptr<SessionState>> SessionRegistry::snapshot() const {
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> snap;
std::shared_lock lock(mMutex);
snap = mSessions;
return snap;
}
void SessionRegistry::erase(uint64_t sessionID) {
void SessionRegistry::erase(uint32_t sessionID) {
std::unique_lock lock(mMutex);
mSessions.erase(sessionID);
}
@@ -60,6 +60,11 @@ namespace ColumnLynx::Net {
return static_cast<int>(mSessions.size());
}
bool SessionRegistry::exists(uint32_t sessionID) const {
std::shared_lock lock(mMutex);
return mSessions.find(sessionID) != mSessions.end();
}
uint32_t SessionRegistry::getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const {
std::shared_lock lock(mMutex);
@@ -77,7 +82,7 @@ namespace ColumnLynx::Net {
return 0;
}
void SessionRegistry::lockIP(uint64_t sessionID, uint32_t ip) {
void SessionRegistry::lockIP(uint32_t sessionID, uint32_t ip) {
std::unique_lock lock(mMutex);
mSessionIPs[sessionID] = ip;
@@ -87,7 +92,7 @@ namespace ColumnLynx::Net {
mIPSessions[ip] = mSessions.find(sessionID)->second;
}
void SessionRegistry::deallocIP(uint64_t sessionID) {
void SessionRegistry::deallocIP(uint32_t sessionID) {
std::unique_lock lock(mMutex);
auto it = mSessionIPs.find(sessionID);

View File

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

View File

@@ -1,29 +1,65 @@
// utils.cpp - Utility functions for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/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[" << std::to_string(now) << " LOG] " << msg << std::endl;
std::cout << "\033[0m[" << unixMillisToISO8601(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[" << std::to_string(now) << " WARN] " << msg << "\033[0m" << std::endl;
std::cerr << "\033[33m[" << unixMillisToISO8601(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[" << std::to_string(now) << " ERROR] " << msg << "\033[0m" << std::endl;
std::cerr << "\033[31m[" << unixMillisToISO8601(now) << " ERROR] " << msg << "\033[0m" << std::endl;
}
void debug(const std::string &msg) {
#if DEBUG || _DEBUG
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::cerr << "\033[95m[" << std::to_string(now) << " DEBUG] " << msg << "\033[0m" << std::endl;
std::cerr << "\033[95m[" << unixMillisToISO8601(now) << " DEBUG] " << msg << "\033[0m" << std::endl;
#else
return;
#endif
@@ -49,7 +85,7 @@ namespace ColumnLynx::Utils {
}
std::string getVersion() {
return "b0.3";
return "1.1.0";
}
unsigned short serverPort() {
@@ -57,7 +93,7 @@ namespace ColumnLynx::Utils {
}
unsigned char protocolVersion() {
return 1;
return 2;
}
std::string bytesToHexString(const uint8_t* bytes, size_t length) {
@@ -101,17 +137,25 @@ namespace ColumnLynx::Utils {
return bytes;
}
std::vector<std::string> getWhitelistedKeys() {
std::vector<std::string> getWhitelistedKeys(std::string basePath) {
// Currently re-reads the file every time, should be fine.
// Advantage of it is that you don't need to reload the server binary after adding/removing keys. Disadvantage is re-reading the file every time.
// I might redo this part.
std::vector<std::string> out;
std::ifstream file("whitelisted_keys"); // TODO: This is hardcoded for now, make dynamic
std::string line;
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::string line;
while (std::getline(file, line)) {
// Convert to upper case to align with the bytesToHexString() output
for (int i = 0; i < line.length(); i++) {
line[i] = toupper(line[i]);
}
out.push_back(line);
}
@@ -123,6 +167,10 @@ namespace ColumnLynx::Utils {
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)) {

View File

@@ -1,5 +1,5 @@
// virtual_interface.cpp - Virtual Interface for Network Communication
// Copyright (C) 2025 DcruBro
// 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/virtual_interface.hpp>
@@ -72,7 +72,7 @@ namespace ColumnLynx::Net {
if (ioctl(mFd, TUNSETIFF, &ifr) < 0) {
close(mFd);
throw std::runtime_error("TUNSETIFF failed: " + std::string(strerror(errno)));
throw std::runtime_error("TUNSETIFF failed (try running with sudo): " + std::string(strerror(errno)));
}
#elif defined(__APPLE__)
@@ -96,7 +96,7 @@ namespace ColumnLynx::Net {
if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) {
if (errno == EPERM)
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: Insufficient permissions (try running as root)");
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: Insufficient permissions (try running with sudo)");
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno)));
}
@@ -326,6 +326,13 @@ namespace ColumnLynx::Net {
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
@@ -406,6 +413,12 @@ namespace ColumnLynx::Net {
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());
system(cmd);
Utils::log("Executed command: " + std::string(cmd));
return true;
@@ -420,41 +433,66 @@ namespace ColumnLynx::Net {
uint16_t mtu)
{
#ifdef _WIN32
std::string ip = ipv4ToString(clientIP);
std::string gw = ipv4ToString(serverIP);
std::string mask;
// Interface alias → LUID → Index
std::wstring ifAlias(mIfName.begin(), mIfName.end());
// Convert prefixLen → subnet mask
uint32_t maskInt = (prefixLen == 0) ? 0 : (0xFFFFFFFF << (32 - prefixLen));
mask = ipv4ToString(maskInt);
NET_LUID luid;
if (ConvertInterfaceAliasToLuid(ifAlias.c_str(), &luid) != NO_ERROR)
return false;
// Calculate network address from IP and mask
uint32_t networkInt = (clientIP & maskInt);
std::string network = ipv4ToString(networkInt);
NET_IFINDEX ifIndex;
if (ConvertInterfaceLuidToIndex(&luid, &ifIndex) != NO_ERROR)
return false;
char cmd[512];
// 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;
// 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);
if (CreateUnicastIpAddressEntry(&addr) != NO_ERROR)
return false;
// 3. Add route for the VPN network to go through the TUN interface
// This is critical: tells Windows to send packets destined for the server/network through the TUN interface
snprintf(cmd, sizeof(cmd),
"netsh routing ip add persistentroute dest=%s/%d name=\"%s\" nexthopcfg=%s",
network.c_str(), prefixLen, mIfName.c_str(), gw.c_str()
);
system(cmd);
// 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;
return true;
#else

View File

@@ -1,5 +1,5 @@
// main.cpp - Server entry point for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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 <asio.hpp>
@@ -15,6 +15,11 @@
#include <unordered_map>
#include <cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp>
#include <columnlynx/server/server_session.hpp>
#if defined(__WIN32__)
#include <windows.h>
#endif
using asio::ip::tcp;
using namespace ColumnLynx::Utils;
@@ -37,7 +42,12 @@ int main(int argc, char** argv) {
#else
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
#endif
("config", "Override config file path", cxxopts::value<std::string>()->default_value("./server_config"));
#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
PanicHandler::init();
@@ -46,7 +56,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) 2025, The ColumnLynx Contributors.\n";
std::cout << "Copyright (C) 2026, The ColumnLynx Contributors.\n";
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
return 0;
}
@@ -60,11 +70,40 @@ int main(int argc, char** argv) {
//WintunInitialize();
#endif
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(optionsObj["config"].as<std::string>());
struct ServerState serverState{};
// 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
}
serverState.configPath = configPath;
#if defined(DEBUG)
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" });
#else
// A production server should never use random keys. If the config file cannot be read or does not contain keys, the server will fail to start.
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK", "SERVER_PUBLIC_KEY", "SERVER_PRIVATE_KEY" });
#endif
serverState.serverConfig = config;
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
log("Using virtual interface: " + tun->getName());
// Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs)
serverState.virtualInterface = tun;
// 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>();
@@ -75,31 +114,42 @@ int main(int argc, char** argv) {
log("Loading keypair from config file.");
PublicKey pk;
PrivateKey sk;
PrivateSeed seed;
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), sk.size(), sk.begin());
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), seed.size(), seed.begin());
std::copy_n(Utils::hexStringToBytes(itPubkey->second).begin(), pk.size(), pk.begin());
sodiumWrapper->setKeys(pk, sk);
if (!sodiumWrapper->recomputeKeys(seed, pk)) {
throw std::runtime_error("Failed to recompute keypair from config file values!");
}
} 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));
std::shared_ptr<bool> hostRunning = std::make_shared<bool>(true);
serverState.sodiumWrapper = sodiumWrapper;
serverState.ipv4Only = ipv4Only;
serverState.hostRunning = true;
// Store the global state; from now on, it should only be accessed through the ServerSession singleton, which will ensure thread safety with its internal mutex
ServerSession::getInstance().setServerState(std::make_shared<ServerState>(std::move(serverState)));
asio::io_context io;
auto server = std::make_shared<TCPServer>(io, serverPort(), sodiumWrapper, hostRunning, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), hostRunning, ipv4Only, tun);
auto server = std::make_shared<TCPServer>(io, serverPort());
auto udpServer = std::make_shared<UDPServer>(io, serverPort());
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;
ServerSession::getInstance().setHostRunning(false);
server->stop();
udpServer->stop();
});
@@ -123,15 +173,28 @@ int main(int argc, char** argv) {
}
const uint8_t* ip = packet.data();
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset in IPv6-mapped header
uint32_t srcIP = ntohl(*(uint32_t*)(ip + 12)); // IPv4 source address offset
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset
auto session = SessionRegistry::getInstance().getByIP(dstIP);
if (!session) {
Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipv4ToString(dstIP));
// 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()));
continue;
}
udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end()));
// 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));
}
log("Shutting down server...");

View File

@@ -1,5 +1,5 @@
// tcp_connection.cpp - TCP Connection for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/server/net/tcp/tcp_connection.hpp>
@@ -66,7 +66,7 @@ namespace ColumnLynx::Net::TCP {
Utils::log("Initiated graceful disconnect (half-close) to " + mRemoteIP);
}
uint64_t TCPConnection::getSessionID() const {
uint32_t TCPConnection::getSessionID() const {
return mConnectionSessionID;
}
@@ -145,7 +145,7 @@ namespace ColumnLynx::Net::TCP {
Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size()));
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys();
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys(ServerSession::getInstance().getConfigPath());
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);
@@ -156,7 +156,7 @@ namespace ColumnLynx::Net::TCP {
Utils::debug("Client " + reqAddr + " passed authorized_keys");
mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(mLibSodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist
mHandler->sendMessage(ServerMessageType::HANDSHAKE_IDENTIFY, Utils::uint8ArrayToString(ServerSession::getInstance().getSodiumWrapper()->getPublicKey(), crypto_sign_PUBLICKEYBYTES)); // This public key should always exist
break;
}
case ClientMessageType::HANDSHAKE_CHALLENGE: {
@@ -169,7 +169,7 @@ namespace ColumnLynx::Net::TCP {
// Sign the challenge
Signature sig = Utils::LibSodiumWrapper::signMessage(
challengeData, sizeof(challengeData),
mLibSodiumWrapper->getPrivateKey()
ServerSession::getInstance().getSodiumWrapper()->getPrivateKey()
);
mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response
@@ -191,8 +191,8 @@ namespace ColumnLynx::Net::TCP {
std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size());
try {
std::array<uint8_t, 32> arrayPrivateKey;
std::copy(mLibSodiumWrapper->getXPrivateKey(),
mLibSodiumWrapper->getXPrivateKey() + 32,
std::copy(ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey(),
ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32,
arrayPrivateKey.begin());
// Decrypt the AES key using the client's public key and server's private key
@@ -211,14 +211,18 @@ namespace ColumnLynx::Net::TCP {
std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size());
// Make a Session ID
// Make a Session ID - unique and not zero (zero is reserved for invalid sessions)
do {
randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID));
} while (SessionRegistry::getInstance().exists(mConnectionSessionID) || mConnectionSessionID == 0); // Regenerate if it already exists or is zero (zero is reserved for invalid sessions)
// 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
const auto& serverConfig = ServerSession::getInstance().getRawServerConfig();
std::string networkString = serverConfig.find("NETWORK")->second; // The load check guarantees that this value exists
uint8_t configMask = std::stoi(serverConfig.find("SUBNET_MASK")->second); // Same deal here
uint32_t baseIP = Net::VirtualInterface::stringToIpv4(networkString);
@@ -245,11 +249,11 @@ namespace ColumnLynx::Net::TCP {
tunConfig.dns1 = htonl(0x08080808); // 8.8.8.8
tunConfig.dns2 = 0;
uint64_t sessionIDNet = Utils::chtobe64(mConnectionSessionID);
uint32_t sessionIDNet = htonl(mConnectionSessionID);
std::vector<uint8_t> payload(sizeof(uint64_t) + sizeof(tunConfig));
std::memcpy(payload.data(), &sessionIDNet, sizeof(uint64_t));
std::memcpy(payload.data() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig));
std::vector<uint8_t> payload(sizeof(uint32_t) + sizeof(tunConfig));
std::memcpy(payload.data(), &sessionIDNet, sizeof(uint32_t));
std::memcpy(payload.data() + sizeof(uint32_t), &tunConfig, sizeof(tunConfig));
std::vector<uint8_t> encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
payload.data(), payload.size(),
@@ -261,6 +265,7 @@ namespace ColumnLynx::Net::TCP {
// Add to session registry
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned (" + std::to_string(mConnectionSessionID) + ").");
auto session = std::make_shared<SessionState>(mConnectionAESKey, std::chrono::hours(12), clientIP, htonl(0x0A0A0001), mConnectionSessionID);
session->setBaseNonce(); // Set it
SessionRegistry::getInstance().put(mConnectionSessionID, std::move(session));
SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP);

View File

@@ -1,5 +1,5 @@
// tcp_server.cpp - TCP Server for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/server/net/tcp/tcp_server.hpp>
@@ -27,15 +27,13 @@ namespace ColumnLynx::Net::TCP {
}
Utils::error("Accept failed: " + ec.message());
// Try again only if still running
if (mHostRunning && *mHostRunning && mAcceptor.is_open())
if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open())
mStartAccept();
return;
}
auto client = TCPConnection::create(
std::move(socket),
mSodiumWrapper,
&mRawServerConfig,
[this](std::shared_ptr<TCPConnection> c) {
mClients.erase(c);
Utils::log("Client removed.");
@@ -45,7 +43,7 @@ namespace ColumnLynx::Net::TCP {
client->start();
Utils::log("Accepted new client connection.");
if (mHostRunning && *mHostRunning && mAcceptor.is_open())
if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open())
mStartAccept();
}
);

View File

@@ -1,5 +1,5 @@
// udp_server.cpp - UDP Server for ColumnLynx
// Copyright (C) 2025 DcruBro
// 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/server/net/udp/udp_server.hpp>
@@ -16,26 +16,27 @@ namespace ColumnLynx::Net::UDP {
if (ec) {
if (ec == asio::error::operation_aborted) return; // Socket closed
// Other recv error
if (mHostRunning && *mHostRunning) mStartReceive();
if (ServerSession::getInstance().isHostRunning()) mStartReceive();
return;
}
if (bytes > 0) mHandlePacket(bytes);
if (mHostRunning && *mHostRunning) mStartReceive();
if (ServerSession::getInstance().isHostRunning()) mStartReceive();
}
);
}
void UDPServer::mHandlePacket(std::size_t bytes) {
if (bytes < sizeof(UDPPacketHeader))
if (bytes < sizeof(UDPPacketHeader) + sizeof(uint32_t))
return;
const auto* hdr = reinterpret_cast<UDPPacketHeader*>(mRecvBuffer.data());
// Get plaintext session ID (assuming first 8 bytes after nonce (header))
uint64_t sessionID = 0;
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
// Get plaintext session ID (first 4 bytes after header, in network byte order)
uint32_t sessionIDNet = 0;
std::memcpy(&sessionIDNet, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint32_t));
uint32_t sessionID = sessionIDNet; // ntohl(sessionIDNet); --- IGNORE ---
auto it = mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t);
auto it = mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t);
std::vector<uint8_t> encryptedPayload(it, mRecvBuffer.begin() + bytes);
// Get associated session state
@@ -54,7 +55,7 @@ namespace ColumnLynx::Net::UDP {
encryptedPayload.data(), encryptedPayload.size(),
session->aesKey,
hdr->nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint64_t))
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint32_t))
);
Utils::debug("Passed decryption");
@@ -76,7 +77,7 @@ namespace ColumnLynx::Net::UDP {
}
}
void UDPServer::sendData(const uint64_t sessionID, const std::string& data) {
void UDPServer::sendData(uint32_t sessionID, const std::string& data) {
// Find the IPv4/IPv6 endpoint for the session
std::shared_ptr<const SessionState> session = SessionRegistry::getInstance().get(sessionID);
if (!session) {
@@ -92,23 +93,29 @@ namespace ColumnLynx::Net::UDP {
// Prepare packet
UDPPacketHeader hdr{};
randombytes_buf(hdr.nonce.data(), hdr.nonce.size());
uint8_t nonce[12];
uint32_t prefix = session->noncePrefix;
uint64_t sendCount = const_cast<SessionState*>(session.get())->send_ctr.fetch_add(1, std::memory_order_relaxed);
memcpy(nonce, &prefix, sizeof(uint32_t)); // Prefix nonce
memcpy(nonce + sizeof(uint32_t), &sendCount, sizeof(uint64_t)); // Use send count as nonce suffix to ensure uniqueness
std::copy_n(nonce, 12, hdr.nonce.data());
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
session->aesKey, hdr.nonce, "udp-data"
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint64_t))
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint32_t))
);
std::vector<uint8_t> packet;
packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size());
packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint32_t) + encryptedPayload.size());
packet.insert(packet.end(),
reinterpret_cast<uint8_t*>(&hdr),
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
);
uint32_t sessionIDNet = htonl(sessionID);
packet.insert(packet.end(),
reinterpret_cast<const uint8_t*>(&sessionID),
reinterpret_cast<const uint8_t*>(&sessionID) + sizeof(sessionID)
reinterpret_cast<const uint8_t*>(&sessionIDNet),
reinterpret_cast<const uint8_t*>(&sessionIDNet) + sizeof(sessionIDNet)
);
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());

View File

@@ -0,0 +1,92 @@
// server_session.cpp - Client Session data 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/server/server_session.hpp>
namespace ColumnLynx {
std::shared_ptr<ServerState> ServerSession::getServerState() const {
std::shared_lock lock(mMutex);
return mServerState;
}
void ServerSession::setServerState(std::shared_ptr<ServerState> state) {
std::unique_lock lock(mMutex);
mServerState = std::move(state);
}
std::shared_ptr<Utils::LibSodiumWrapper> ServerSession::getSodiumWrapper() const {
std::shared_lock lock(mMutex);
return mServerState ? mServerState->sodiumWrapper : nullptr;
}
const std::string& ServerSession::getConfigPath() const {
static const std::string emptyString;
std::shared_ptr<ServerState> state = getServerState();
return state ? state->configPath : emptyString;
}
const std::unordered_map<std::string, std::string>& ServerSession::getRawServerConfig() const {
static const std::unordered_map<std::string, std::string> emptyMap;
std::shared_ptr<ServerState> state = getServerState();
return state ? state->serverConfig : emptyMap;
}
const std::shared_ptr<Net::VirtualInterface>& ServerSession::getVirtualInterface() const {
static const std::shared_ptr<Net::VirtualInterface> nullTun = nullptr;
std::shared_ptr<ServerState> state = getServerState();
return state ? state->virtualInterface : nullTun;
}
bool ServerSession::isIPv4Only() const {
std::shared_ptr<ServerState> state = getServerState();
return state ? state->ipv4Only : false;
}
bool ServerSession::isHostRunning() const {
std::shared_ptr<ServerState> state = getServerState();
return state ? state->hostRunning : false;
}
void ServerSession::setSodiumWrapper(std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper) {
std::unique_lock lock(mMutex);
if (!mServerState)
mServerState = std::make_shared<ServerState>();
mServerState->sodiumWrapper = std::move(sodiumWrapper);
}
void ServerSession::setConfigPath(const std::string& configPath) {
std::unique_lock lock(mMutex);
if (!mServerState)
mServerState = std::make_shared<ServerState>();
mServerState->configPath = configPath;
}
void ServerSession::setRawServerConfig(const std::unordered_map<std::string, std::string>& config) {
std::unique_lock lock(mMutex);
if (!mServerState)
mServerState = std::make_shared<ServerState>();
mServerState->serverConfig = config;
}
void ServerSession::setVirtualInterface(std::shared_ptr<Net::VirtualInterface> tun) {
std::unique_lock lock(mMutex);
if (!mServerState)
mServerState = std::make_shared<ServerState>();
mServerState->virtualInterface = std::move(tun);
}
void ServerSession::setIPv4Only(bool ipv4Only) {
std::unique_lock lock(mMutex);
if (!mServerState)
mServerState = std::make_shared<ServerState>();
mServerState->ipv4Only = ipv4Only;
}
void ServerSession::setHostRunning(bool hostRunning) {
std::unique_lock lock(mMutex);
if (!mServerState)
mServerState = std::make_shared<ServerState>();
mServerState->hostRunning = hostRunning;
}
}