Compare commits
28 Commits
b0.3
...
afe10bbb6e
| Author | SHA1 | Date | |
|---|---|---|---|
| afe10bbb6e | |||
| 60795c60d8 | |||
| b64d9c4498 | |||
| 604e4ace0f | |||
| 1136892c5d | |||
| 8c54250449 | |||
| 27bd2cd2ec | |||
| 14298453b3 | |||
| 316498c745 | |||
| 6d40dbe00d | |||
| 757d0d251d | |||
| e61a429f24 | |||
| db1b919981 | |||
| 2117b6a62b | |||
| 7d56f9db5d | |||
| 4609e85ca9 | |||
| 154296bcdc | |||
| 867b2c953a | |||
| 83693ed1da | |||
| 62335f3693 | |||
| 2d3d6afb07 | |||
| ccbacd1180 | |||
| cf3ec30492 | |||
| 1d34953f25 | |||
| b9903f5a8e | |||
| 1f5a0585f3 | |||
| 3dc5c04bf1 | |||
| 37ddb82d9a |
39
.github/workflows/sanitizers.yml
vendored
Normal file
39
.github/workflows/sanitizers.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Sanitizers
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SANITIZERS: "-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
|
||||||
|
ASAN_OPTIONS: "detect_leaks=1:abort_on_error=1"
|
||||||
|
UBSAN_OPTIONS: "print_stacktrace=1"
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y cmake build-essential clang
|
||||||
|
|
||||||
|
- name: Configure (CMake)
|
||||||
|
run: |
|
||||||
|
mkdir -p build-sanitizers
|
||||||
|
cd build-sanitizers
|
||||||
|
CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="$SANITIZERS" -DCMAKE_EXE_LINKER_FLAGS="$SANITIZERS" ..
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd build-sanitizers
|
||||||
|
cmake --build . -- -j
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
cd build-sanitizers
|
||||||
|
ctest --output-on-failure || (echo "ctest failed"; exit 1)
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,5 +12,6 @@ _deps
|
|||||||
CMakeUserPresets.json
|
CMakeUserPresets.json
|
||||||
|
|
||||||
build/
|
build/
|
||||||
|
build*/
|
||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## ASIO C++ Library
|
## ASIO C++ Library
|
||||||
- **Name:** ASIO (standalone)
|
- **Name:** ASIO (standalone)
|
||||||
- **Website:** https://think-async.com/Asio/
|
- **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:** Boost Software License, Version 1.0
|
||||||
- **License Text:** See `third_party/asio/LICENSE_1_0.txt`
|
- **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
|
## CXXOPTS C++ Library
|
||||||
- **Name:** cxxopts
|
- **Name:** cxxopts
|
||||||
- **Website:** https://github.com/jarro2783/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:** MIT License
|
||||||
- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt`
|
- **License Text:** See `third_party/cxxopts/LICENSE_1_0.txt`
|
||||||
|
|
||||||
## Wintun C++ Library
|
## Wintun C++ Library
|
||||||
- **Name:** wintun
|
- **Name:** wintun
|
||||||
- **Website:** https://www.wintun.net/
|
- **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:** MIT License OR GPL-2.0 License
|
||||||
- **License Text:** See `third_party/wintun/`
|
- **License Text:** See `third_party/wintun/`
|
||||||
- **Utilized Under:** MIT License
|
- **Utilized Under:** MIT License
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
# If MAJOR is 0, and MINOR > 0, Version is BETA
|
# If MAJOR is 0, and MINOR > 0, Version is BETA
|
||||||
|
|
||||||
project(ColumnLynx
|
project(ColumnLynx
|
||||||
VERSION 0.3.0
|
VERSION 1.1.1
|
||||||
LANGUAGES CXX
|
LANGUAGES CXX
|
||||||
)
|
)
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# General C++ setup
|
# General C++ setup
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -40,7 +42,7 @@ endif()
|
|||||||
if(WIN32)
|
if(WIN32)
|
||||||
add_compile_definitions(_WIN32_WINNT=0x0A00 NOMINMAX WIN32_LEAN_AND_MEAN)
|
add_compile_definitions(_WIN32_WINNT=0x0A00 NOMINMAX WIN32_LEAN_AND_MEAN)
|
||||||
elseif(UNIX)
|
elseif(UNIX)
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic -O3)
|
add_compile_options(-Wall -Wextra -Wpedantic -O1)
|
||||||
add_link_options(-pthread)
|
add_link_options(-pthread)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ endif()
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
Sodium
|
Sodium
|
||||||
GIT_REPOSITORY https://github.com/robinlinden/libsodium-cmake.git
|
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)
|
set(SODIUM_DISABLE_TESTS ON CACHE BOOL "" FORCE)
|
||||||
@@ -157,3 +159,42 @@ target_include_directories(server PRIVATE
|
|||||||
)
|
)
|
||||||
target_compile_definitions(server PRIVATE ASIO_STANDALONE)
|
target_compile_definitions(server PRIVATE ASIO_STANDALONE)
|
||||||
set_target_properties(server PROPERTIES OUTPUT_NAME "columnlynx_server")
|
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})
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Unit tests
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
option(BUILD_TESTS "Build unit tests" ON)
|
||||||
|
|
||||||
|
if(BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
file(GLOB_RECURSE TEST_SRC CONFIGURE_DEPENDS tests/*.cpp)
|
||||||
|
if(TEST_SRC)
|
||||||
|
foreach(TEST_FILE IN LISTS TEST_SRC)
|
||||||
|
get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
|
||||||
|
add_executable(${TEST_NAME} ${TEST_FILE})
|
||||||
|
target_link_libraries(${TEST_NAME} PRIVATE common sodium)
|
||||||
|
target_include_directories(${TEST_NAME} PRIVATE
|
||||||
|
${PROJECT_SOURCE_DIR}/include
|
||||||
|
${asio_SOURCE_DIR}/asio/include
|
||||||
|
${sodium_SOURCE_DIR}/src/libsodium/include
|
||||||
|
${sodium_BINARY_DIR}/src/libsodium/include
|
||||||
|
)
|
||||||
|
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
|
||||||
|
set_target_properties(${TEST_NAME} PROPERTIES OUTPUT_NAME "columnlynx_${TEST_NAME}")
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|||||||
159
README.md
159
README.md
@@ -18,22 +18,155 @@ This simplicity-focused design approach allows us to make an efficient, low-over
|
|||||||
|
|
||||||
## Configuration
|
## 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
|
||||||
|
|
||||||
"**server_config**" is a file that contains the server configuration, **one variable per line**. These are the current configuration available variables:
|
"**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_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
|
||||||
- **SERVER_PRIVATE_KEY** (Hex String): The private key to be used
|
- **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)
|
- **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)
|
- **SUBNET_MASK** (Integer): The subnet mask to be used (ensure proper length, it will not be checked)
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```
|
```
|
||||||
SERVER_PUBLIC_KEY=787B648046F10DDD0B77A6303BE42D859AA65C52F5708CC3C58EB5691F217C7B
|
SERVER_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
|
||||||
SERVER_PRIVATE_KEY=778604245F57B847E63BD85DE8208FF1A127FB559895195928C3987E246B77B8787B648046F10DDD0B77A6303BE42D859AA65C52F5708CC3C58EB5691F217C7B
|
SERVER_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
|
||||||
NETWORK=10.10.0.0
|
NETWORK=10.10.0.0
|
||||||
SUBNET_MASK=24
|
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_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_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
|
||||||
- **CLIENT_PRIVATE_KEY** (Hex String): The private key to be used
|
- **CLIENT_PRIVATE_KEY** (Hex String): The private key seed to be used
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```
|
```
|
||||||
CLIENT_PUBLIC_KEY=8CC8BE1A9D24639D0492EF143E84E2BD4C757C9B3B687E7035173EBFCA8FEDDA
|
CLIENT_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
|
||||||
CLIENT_PRIVATE_KEY=9B486A5B1509FA216F9EEFED85CACF2384E9D902A76CC979BFA143C18B869F5C8CC8BE1A9D24639D0492EF143E84E2BD4C757C9B3B687E7035173EBFCA8FEDDA
|
CLIENT_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
|
||||||
```
|
```
|
||||||
|
|
||||||
<hr></hr>
|
<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.
|
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**.
|
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
|
### 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>
|
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>
|
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***).
|
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**.
|
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 |
|
| Type | Length | Name | Description |
|
||||||
|:-----|:-------|:-----|:------------|
|
|:-----|:-------|:-----|:------------|
|
||||||
| uint8_t | 12 bytes | **Header** - Nonce | Random nonce to obfuscate encrypted contents |
|
| 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 |
|
| uint8_t | variable | Data | General data / payload |
|
||||||
|
|
||||||
## Misc.
|
## Misc.
|
||||||
|
|||||||
106
include/columnlynx/client/client_session.hpp
Normal file
106
include/columnlynx/client/client_session.hpp
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// 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);
|
||||||
|
if (mClientState->send_cnt == std::numeric_limits<uint64_t>::max()) {
|
||||||
|
Utils::error("ClientSession: send counter overflow detected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_client.hpp - TCP Client for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -11,11 +11,14 @@
|
|||||||
#include <columnlynx/common/utils.hpp>
|
#include <columnlynx/common/utils.hpp>
|
||||||
#include <columnlynx/common/libsodium_wrapper.hpp>
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <string>
|
||||||
#include <columnlynx/common/net/protocol_structs.hpp>
|
#include <columnlynx/common/net/protocol_structs.hpp>
|
||||||
#include <columnlynx/common/net/virtual_interface.hpp>
|
#include <columnlynx/common/net/virtual_interface.hpp>
|
||||||
|
#include <columnlynx/client/client_session.hpp>
|
||||||
|
|
||||||
using asio::ip::tcp;
|
using asio::ip::tcp;
|
||||||
|
|
||||||
@@ -24,28 +27,22 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
public:
|
public:
|
||||||
TCPClient(asio::io_context& ioContext,
|
TCPClient(asio::io_context& ioContext,
|
||||||
const std::string& host,
|
const std::string& host,
|
||||||
const std::string& port,
|
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)
|
|
||||||
:
|
:
|
||||||
mResolver(ioContext),
|
mResolver(ioContext),
|
||||||
mSocket(ioContext),
|
mSocket(ioContext),
|
||||||
mHost(host),
|
mHost(host),
|
||||||
mPort(port),
|
mPort(port),
|
||||||
mLibSodiumWrapper(sodiumWrapper),
|
|
||||||
mGlobalKeyRef(aesKey),
|
|
||||||
mSessionIDRef(sessionIDRef),
|
|
||||||
mInsecureMode(insecureMode),
|
|
||||||
mHeartbeatTimer(mSocket.get_executor()),
|
mHeartbeatTimer(mSocket.get_executor()),
|
||||||
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
|
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
|
||||||
mLastHeartbeatSent(std::chrono::steady_clock::now()),
|
mLastHeartbeatSent(std::chrono::steady_clock::now())
|
||||||
mTun(tun)
|
|
||||||
{
|
{
|
||||||
|
// Get initial client config
|
||||||
|
std::string configPath = ClientSession::getInstance().getConfigPath();
|
||||||
|
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper();
|
||||||
|
|
||||||
// Preload the config map
|
// Preload the config map
|
||||||
mRawClientConfig = Utils::getConfigMap("client_config");
|
mRawClientConfig = Utils::getConfigMap(configPath + "client_config");
|
||||||
|
|
||||||
auto itPubkey = mRawClientConfig.find("CLIENT_PUBLIC_KEY");
|
auto itPubkey = mRawClientConfig.find("CLIENT_PUBLIC_KEY");
|
||||||
auto itPrivkey = mRawClientConfig.find("CLIENT_PRIVATE_KEY");
|
auto itPrivkey = mRawClientConfig.find("CLIENT_PRIVATE_KEY");
|
||||||
@@ -54,16 +51,22 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
Utils::log("Loading keypair from config file.");
|
Utils::log("Loading keypair from config file.");
|
||||||
|
|
||||||
PublicKey pk;
|
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());
|
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));
|
Utils::debug("Newly-Loaded Public Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPublicKey(), 32));
|
||||||
} else {
|
} else {
|
||||||
|
#if defined(DEBUG)
|
||||||
Utils::warn("No keypair found in config file! Using random key.");
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,27 +90,22 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// TODO: Move ptrs to smart ptrs
|
// TODO: Move ptrs to smart ptrs
|
||||||
|
|
||||||
bool mConnected = false;
|
std::atomic<bool> mConnected{false};
|
||||||
bool mHandshakeComplete = false;
|
std::atomic<bool> mHandshakeComplete{false};
|
||||||
tcp::resolver mResolver;
|
tcp::resolver mResolver;
|
||||||
tcp::socket mSocket;
|
tcp::socket mSocket;
|
||||||
std::shared_ptr<MessageHandler> mHandler;
|
std::shared_ptr<MessageHandler> mHandler;
|
||||||
std::string mHost, mPort;
|
std::string mHost, mPort;
|
||||||
uint8_t mServerPublicKey[32]; // Assuming 256-bit public key
|
uint8_t mServerPublicKey[32]; // Assuming 256-bit public key
|
||||||
std::array<uint8_t, 32> mSubmittedChallenge{};
|
std::array<uint8_t, 32> mSubmittedChallenge{};
|
||||||
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper;
|
uint32_t mConnectionSessionID;
|
||||||
uint64_t mConnectionSessionID;
|
|
||||||
SymmetricKey mConnectionAESKey;
|
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;
|
asio::steady_timer mHeartbeatTimer;
|
||||||
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
|
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
|
||||||
std::chrono::steady_clock::time_point mLastHeartbeatSent;
|
std::chrono::steady_clock::time_point mLastHeartbeatSent;
|
||||||
int mMissedHeartbeats = 0;
|
int mMissedHeartbeats = 0;
|
||||||
bool mIsHostDomain;
|
bool mIsHostDomain;
|
||||||
Protocol::TunConfig mTunConfig;
|
Protocol::TunConfig mTunConfig;
|
||||||
std::shared_ptr<VirtualInterface> mTun = nullptr;
|
|
||||||
std::unordered_map<std::string, std::string> mRawClientConfig;
|
std::unordered_map<std::string, std::string> mRawClientConfig;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// udp_client.hpp - UDP Client for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -10,17 +10,15 @@
|
|||||||
#include <columnlynx/common/libsodium_wrapper.hpp>
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <columnlynx/common/net/virtual_interface.hpp>
|
#include <columnlynx/common/net/virtual_interface.hpp>
|
||||||
|
#include <columnlynx/client/client_session.hpp>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::UDP {
|
namespace ColumnLynx::Net::UDP {
|
||||||
class UDPClient {
|
class UDPClient {
|
||||||
public:
|
public:
|
||||||
UDPClient(asio::io_context& ioContext,
|
UDPClient(asio::io_context& ioContext,
|
||||||
const std::string& host,
|
const std::string& host,
|
||||||
const std::string& port,
|
const std::string& port)
|
||||||
std::shared_ptr<std::array<uint8_t, 32>> aesKeyRef,
|
: mSocket(ioContext), mResolver(ioContext), mHost(host), mPort(port)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
mStartReceive();
|
mStartReceive();
|
||||||
}
|
}
|
||||||
@@ -43,9 +41,6 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
asio::ip::udp::endpoint mRemoteEndpoint;
|
asio::ip::udp::endpoint mRemoteEndpoint;
|
||||||
std::string mHost;
|
std::string mHost;
|
||||||
std::string mPort;
|
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
|
std::array<uint8_t, 2048> mRecvBuffer; // Adjust size as needed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// libsodium_wrapper.hpp - Libsodium Wrapper for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
namespace ColumnLynx {
|
namespace ColumnLynx {
|
||||||
using PublicKey = std::array<uint8_t, crypto_sign_PUBLICKEYBYTES>; // Ed25519
|
using PublicKey = std::array<uint8_t, crypto_sign_PUBLICKEYBYTES>; // Ed25519
|
||||||
using PrivateKey = std::array<uint8_t, crypto_sign_SECRETKEYBYTES>; // 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 Signature = std::array<uint8_t, crypto_sign_BYTES>; // 64 bytes
|
||||||
using SymmetricKey = std::array<uint8_t, crypto_aead_chacha20poly1305_ietf_KEYBYTES>; // 32 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
|
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
|
// Helper section
|
||||||
|
|
||||||
// Generates a random 256-bit (32-byte) array
|
// Generates a random 256-bit (32-byte) array
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// protocol_structs.hpp - Network Protocol Structures
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// session_registry.hpp - Session Registry for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -27,8 +27,9 @@ namespace ColumnLynx::Net {
|
|||||||
std::chrono::steady_clock::time_point expires{}; // Time of expiry
|
std::chrono::steady_clock::time_point expires{}; // Time of expiry
|
||||||
uint32_t clientTunIP; // Assigned IP
|
uint32_t clientTunIP; // Assigned IP
|
||||||
uint32_t serverTunIP; // Server IP
|
uint32_t serverTunIP; // Server IP
|
||||||
uint64_t sessionID; // Session ID
|
uint32_t sessionID; // Session ID
|
||||||
Nonce base_nonce{};
|
Nonce base_nonce{};
|
||||||
|
uint32_t noncePrefix;
|
||||||
|
|
||||||
~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); }
|
~SessionState() { sodium_memzero(aesKey.data(), aesKey.size()); }
|
||||||
SessionState(const SessionState&) = delete;
|
SessionState(const SessionState&) = delete;
|
||||||
@@ -36,7 +37,7 @@ namespace ColumnLynx::Net {
|
|||||||
SessionState(SessionState&&) = default;
|
SessionState(SessionState&&) = default;
|
||||||
SessionState& operator=(SessionState&&) = default;
|
SessionState& operator=(SessionState&&) = default;
|
||||||
|
|
||||||
explicit SessionState(const SymmetricKey& k, std::chrono::seconds ttl = std::chrono::hours(24), 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;
|
expires = created + ttl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ namespace ColumnLynx::Net {
|
|||||||
void setUDPEndpoint(const asio::ip::udp::endpoint& ep) {
|
void setUDPEndpoint(const asio::ip::udp::endpoint& ep) {
|
||||||
udpEndpoint = 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 {
|
class SessionRegistry {
|
||||||
@@ -52,19 +58,19 @@ namespace ColumnLynx::Net {
|
|||||||
static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; }
|
static SessionRegistry& getInstance() { static SessionRegistry instance; return instance; }
|
||||||
|
|
||||||
// Insert or replace a session entry
|
// Insert or replace a session entry
|
||||||
void put(uint64_t sessionID, std::shared_ptr<SessionState> state);
|
void put(uint32_t sessionID, std::shared_ptr<SessionState> state);
|
||||||
|
|
||||||
// Lookup a session entry by session ID
|
// Lookup a session entry by session ID
|
||||||
std::shared_ptr<const SessionState> get(uint64_t sessionID) const;
|
std::shared_ptr<const SessionState> get(uint32_t sessionID) const;
|
||||||
|
|
||||||
// Lookup a session entry by IPv4
|
// Lookup a session entry by IPv4
|
||||||
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const;
|
std::shared_ptr<const SessionState> getByIP(uint32_t ip) const;
|
||||||
|
|
||||||
// Get a snapshot of the Session Registry
|
// Get a snapshot of the Session Registry
|
||||||
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> snapshot() const;
|
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> snapshot() const;
|
||||||
|
|
||||||
// Remove a session by ID
|
// Remove a session by ID
|
||||||
void erase(uint64_t sessionID);
|
void erase(uint32_t sessionID);
|
||||||
|
|
||||||
// Cleanup expired sessions
|
// Cleanup expired sessions
|
||||||
void cleanupExpired();
|
void cleanupExpired();
|
||||||
@@ -72,21 +78,23 @@ namespace ColumnLynx::Net {
|
|||||||
// Get the number of registered sessions
|
// Get the number of registered sessions
|
||||||
int size() const;
|
int size() const;
|
||||||
|
|
||||||
|
bool exists(uint32_t sessionID) const;
|
||||||
|
|
||||||
// IP management
|
// IP management
|
||||||
|
|
||||||
// Get the lowest available IPv4 address; Returns 0 if none available
|
// Get the lowest available IPv4 address; Returns 0 if none available
|
||||||
uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const;
|
uint32_t getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const;
|
||||||
|
|
||||||
// Lock IP to session ID; Do NOT call before put() - You will segfault!
|
// 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
|
// Unlock IP from session ID
|
||||||
void deallocIP(uint64_t sessionID);
|
void deallocIP(uint32_t sessionID);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::shared_mutex mMutex;
|
mutable std::shared_mutex mMutex;
|
||||||
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> mSessions;
|
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mSessions;
|
||||||
std::unordered_map<uint64_t, uint32_t> mSessionIPs;
|
std::unordered_map<uint32_t, uint32_t> mSessionIPs;
|
||||||
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mIPSessions;
|
std::unordered_map<uint32_t, std::shared_ptr<SessionState>> mIPSessions;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// net_helper.hpp - Network Helper Functions for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_message_handler.hpp - TCP Message Handler for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -39,6 +39,6 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
std::array<uint8_t, 3> mHeader{}; // [type][lenHigh][lenLow]
|
std::array<uint8_t, 3> mHeader{}; // [type][lenHigh][lenLow]
|
||||||
std::vector<uint8_t> mBody;
|
std::vector<uint8_t> mBody;
|
||||||
std::function<void(AnyMessageType, std::string)> mOnMessage;
|
std::function<void(AnyMessageType, std::string)> mOnMessage;
|
||||||
std::function<void(asio::error_code&)> mOnDisconnect;
|
std::function<void(const asio::error_code&)> mOnDisconnect;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_message_type.hpp - TCP Message Types for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// udp_message_type.hpp - UDP Message Types for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// virtual_interface.hpp - Virtual Interface for Network Communication
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
#include <locale>
|
#include <locale>
|
||||||
#include <codecvt>
|
#include <codecvt>
|
||||||
#include <wintun/wintun.h>
|
#include <wintun/wintun.h>
|
||||||
|
|
||||||
|
#include <iphlpapi.h>
|
||||||
|
#include <netioapi.h>
|
||||||
|
|
||||||
|
#pragma comment(lib, "iphlpapi.lib")
|
||||||
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace ColumnLynx::Net {
|
namespace ColumnLynx::Net {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// panic_handler.hpp - Panic Handler for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// utils.hpp - Utility functions for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -29,6 +30,9 @@ namespace ColumnLynx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace ColumnLynx::Utils {
|
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.
|
// General log function. Use for logging important information.
|
||||||
void log(const std::string &msg);
|
void log(const std::string &msg);
|
||||||
// General warning function. Use for logging important warnings.
|
// General warning function. Use for logging important warnings.
|
||||||
@@ -44,7 +48,7 @@ namespace ColumnLynx::Utils {
|
|||||||
std::string getVersion();
|
std::string getVersion();
|
||||||
unsigned short serverPort();
|
unsigned short serverPort();
|
||||||
unsigned char protocolVersion();
|
unsigned char protocolVersion();
|
||||||
std::vector<std::string> getWhitelistedKeys();
|
std::vector<std::string> getWhitelistedKeys(std::string basePath);
|
||||||
|
|
||||||
// Raw byte to hex string conversion helper
|
// Raw byte to hex string conversion helper
|
||||||
std::string bytesToHexString(const uint8_t* bytes, size_t length);
|
std::string bytesToHexString(const uint8_t* bytes, size_t length);
|
||||||
@@ -61,27 +65,6 @@ namespace ColumnLynx::Utils {
|
|||||||
return std::string(reinterpret_cast<const char*>(data), length);
|
return std::string(reinterpret_cast<const char*>(data), length);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline constexpr uint64_t cbswap64(uint64_t x) {
|
|
||||||
return ((x & 0x00000000000000FFULL) << 56) |
|
|
||||||
((x & 0x000000000000FF00ULL) << 40) |
|
|
||||||
((x & 0x0000000000FF0000ULL) << 24) |
|
|
||||||
((x & 0x00000000FF000000ULL) << 8) |
|
|
||||||
((x & 0x000000FF00000000ULL) >> 8) |
|
|
||||||
((x & 0x0000FF0000000000ULL) >> 24) |
|
|
||||||
((x & 0x00FF000000000000ULL) >> 40) |
|
|
||||||
((x & 0xFF00000000000000ULL) >> 56);
|
|
||||||
}
|
|
||||||
|
|
||||||
// host -> big-endian (for little-endian hosts) - 64 bit
|
|
||||||
inline constexpr uint64_t chtobe64(uint64_t x) {
|
|
||||||
return cbswap64(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// big-endian -> host (for little-endian hosts) - 64 bit
|
|
||||||
inline constexpr uint64_t cbe64toh(uint64_t x) {
|
|
||||||
return cbswap64(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
T cbswap128(const T& x) {
|
T cbswap128(const T& x) {
|
||||||
static_assert(sizeof(T) == 16, "cbswap128 requires a 128-bit type");
|
static_assert(sizeof(T) == 16, "cbswap128 requires a 128-bit type");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_connection.hpp - TCP Connection for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <columnlynx/common/net/session_registry.hpp>
|
#include <columnlynx/common/net/session_registry.hpp>
|
||||||
#include <columnlynx/common/net/protocol_structs.hpp>
|
#include <columnlynx/common/net/protocol_structs.hpp>
|
||||||
#include <columnlynx/common/net/virtual_interface.hpp>
|
#include <columnlynx/common/net/virtual_interface.hpp>
|
||||||
|
#include <columnlynx/server/server_session.hpp>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::TCP {
|
namespace ColumnLynx::Net::TCP {
|
||||||
class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
|
class TCPConnection : public std::enable_shared_from_this<TCPConnection> {
|
||||||
@@ -26,11 +27,9 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
static pointer create(
|
static pointer create(
|
||||||
asio::ip::tcp::socket socket,
|
asio::ip::tcp::socket socket,
|
||||||
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
|
|
||||||
std::unordered_map<std::string, std::string>* serverConfig,
|
|
||||||
std::function<void(pointer)> onDisconnect)
|
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);
|
conn->mOnDisconnect = std::move(onDisconnect);
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
@@ -45,16 +44,14 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
void disconnect(bool echo = true);
|
void disconnect(bool echo = true);
|
||||||
|
|
||||||
// Get the assigned session ID
|
// 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
|
// Get the assigned AES key; You should probably access this via the Session Registry instead
|
||||||
std::array<uint8_t, 32> getAESKey() const;
|
std::array<uint8_t, 32> getAESKey() const;
|
||||||
|
|
||||||
private:
|
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))),
|
mHandler(std::make_shared<MessageHandler>(std::move(socket))),
|
||||||
mLibSodiumWrapper(sodiumWrapper),
|
|
||||||
mRawServerConfig(serverConfig),
|
|
||||||
mHeartbeatTimer(mHandler->socket().get_executor()),
|
mHeartbeatTimer(mHandler->socket().get_executor()),
|
||||||
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
|
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
|
||||||
mLastHeartbeatSent(std::chrono::steady_clock::now())
|
mLastHeartbeatSent(std::chrono::steady_clock::now())
|
||||||
@@ -67,10 +64,8 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
std::shared_ptr<MessageHandler> mHandler;
|
std::shared_ptr<MessageHandler> mHandler;
|
||||||
std::function<void(std::shared_ptr<TCPConnection>)> mOnDisconnect;
|
std::function<void(std::shared_ptr<TCPConnection>)> mOnDisconnect;
|
||||||
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper;
|
|
||||||
std::unordered_map<std::string, std::string>* mRawServerConfig;
|
|
||||||
std::array<uint8_t, 32> mConnectionAESKey;
|
std::array<uint8_t, 32> mConnectionAESKey;
|
||||||
uint64_t mConnectionSessionID;
|
uint32_t mConnectionSessionID;
|
||||||
AsymPublicKey mConnectionPublicKey;
|
AsymPublicKey mConnectionPublicKey;
|
||||||
asio::steady_timer mHeartbeatTimer;
|
asio::steady_timer mHeartbeatTimer;
|
||||||
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
|
std::chrono::steady_clock::time_point mLastHeartbeatReceived;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_server.hpp - TCP Server for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -17,26 +17,23 @@
|
|||||||
#include <columnlynx/server/net/tcp/tcp_connection.hpp>
|
#include <columnlynx/server/net/tcp/tcp_connection.hpp>
|
||||||
#include <columnlynx/common/libsodium_wrapper.hpp>
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
#include <columnlynx/common/net/protocol_structs.hpp>
|
#include <columnlynx/common/net/protocol_structs.hpp>
|
||||||
|
#include <columnlynx/server/server_session.hpp>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::TCP {
|
namespace ColumnLynx::Net::TCP {
|
||||||
|
|
||||||
class TCPServer {
|
class TCPServer {
|
||||||
public:
|
public:
|
||||||
TCPServer(asio::io_context& ioContext,
|
TCPServer(asio::io_context& ioContext,
|
||||||
uint16_t port,
|
uint16_t port)
|
||||||
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
|
|
||||||
std::shared_ptr<bool> hostRunning, bool ipv4Only = false)
|
|
||||||
: mIoContext(ioContext),
|
: mIoContext(ioContext),
|
||||||
mAcceptor(ioContext),
|
mAcceptor(ioContext)
|
||||||
mSodiumWrapper(sodiumWrapper),
|
|
||||||
mHostRunning(hostRunning)
|
|
||||||
{
|
{
|
||||||
// Preload the config map
|
// Preload the config map
|
||||||
mRawServerConfig = Utils::getConfigMap("server_config", {"NETWORK", "SUBNET_MASK"});
|
|
||||||
|
|
||||||
asio::error_code ec_open, ec_v6only, ec_bind;
|
asio::error_code ec_open, ec_v6only, ec_bind;
|
||||||
|
|
||||||
if (!ipv4Only) {
|
bool isIPv4Only = ServerSession::getInstance().isIPv4Only();
|
||||||
|
|
||||||
|
if (!isIPv4Only) {
|
||||||
// Try IPv6 (dual-stack if supported)
|
// Try IPv6 (dual-stack if supported)
|
||||||
asio::ip::tcp::endpoint endpoint_v6(asio::ip::tcp::v6(), port);
|
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 IPv6 bind failed OR IPv6 open failed OR forced IPv4-only
|
||||||
if (ipv4Only || ec_open || ec_bind) {
|
if (isIPv4Only || ec_open || ec_bind) {
|
||||||
if (!ipv4Only)
|
if (!isIPv4Only)
|
||||||
Utils::warn("TCP: IPv6 unavailable (open=" + ec_open.message() +
|
Utils::warn("TCP: IPv6 unavailable (open=" + ec_open.message() +
|
||||||
", bind=" + ec_bind.message() +
|
", bind=" + ec_bind.message() +
|
||||||
"), falling back to IPv4 only");
|
"), falling back to IPv4 only");
|
||||||
@@ -81,9 +78,6 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
asio::io_context &mIoContext;
|
asio::io_context &mIoContext;
|
||||||
asio::ip::tcp::acceptor mAcceptor;
|
asio::ip::tcp::acceptor mAcceptor;
|
||||||
std::unordered_set<TCPConnection::pointer> mClients;
|
std::unordered_set<TCPConnection::pointer> mClients;
|
||||||
std::shared_ptr<Utils::LibSodiumWrapper> mSodiumWrapper;
|
|
||||||
std::shared_ptr<bool> mHostRunning;
|
|
||||||
std::unordered_map<std::string, std::string> mRawServerConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// udp_server.hpp - UDP Server for ColumnLynx
|
// 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.
|
// Distributed under the terms of the GNU General Public License, either version 2 only or version 3. See LICENSES/ for details.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -9,16 +9,18 @@
|
|||||||
#include <columnlynx/common/utils.hpp>
|
#include <columnlynx/common/utils.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <columnlynx/common/net/virtual_interface.hpp>
|
#include <columnlynx/common/net/virtual_interface.hpp>
|
||||||
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
|
#include <columnlynx/server/server_session.hpp>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::UDP {
|
namespace ColumnLynx::Net::UDP {
|
||||||
class UDPServer {
|
class UDPServer {
|
||||||
public:
|
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)
|
||||||
: mSocket(ioContext), mHostRunning(hostRunning), mTun(tun)
|
: mSocket(ioContext)
|
||||||
{
|
{
|
||||||
asio::error_code ec_open, ec_v6only, ec_bind;
|
asio::error_code ec_open, ec_v6only, ec_bind;
|
||||||
|
|
||||||
if (!ipv4Only) {
|
if (!mIpv4Only) {
|
||||||
asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port);
|
asio::ip::udp::endpoint endpoint_v6(asio::ip::udp::v6(), port);
|
||||||
|
|
||||||
// Try opening IPv6 socket
|
// Try opening IPv6 socket
|
||||||
@@ -34,8 +36,8 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to IPv4 if IPv6 is unusable
|
// Fallback to IPv4 if IPv6 is unusable
|
||||||
if (ipv4Only || ec_open || ec_bind) {
|
if (mIpv4Only || ec_open || ec_bind) {
|
||||||
if (!ipv4Only) {
|
if (!mIpv4Only) {
|
||||||
Utils::warn(
|
Utils::warn(
|
||||||
"UDP: IPv6 unavailable (open=" + ec_open.message() +
|
"UDP: IPv6 unavailable (open=" + ec_open.message() +
|
||||||
", bind=" + ec_bind.message() +
|
", bind=" + ec_bind.message() +
|
||||||
@@ -59,7 +61,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
// Send UDP data to an endpoint; Fetched via the Session Registry
|
// 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:
|
private:
|
||||||
// Start receiving UDP data
|
// Start receiving UDP data
|
||||||
@@ -70,7 +72,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
asio::ip::udp::socket mSocket;
|
asio::ip::udp::socket mSocket;
|
||||||
asio::ip::udp::endpoint mRemoteEndpoint;
|
asio::ip::udp::endpoint mRemoteEndpoint;
|
||||||
std::array<uint8_t, 2048> mRecvBuffer; // 2048 seems stable
|
std::array<uint8_t, 2048> mRecvBuffer; // 2048 seems stable
|
||||||
std::shared_ptr<bool> mHostRunning;
|
bool mIpv4Only = ServerSession::getInstance().isIPv4Only();
|
||||||
std::shared_ptr<VirtualInterface> mTun;
|
const std::shared_ptr<VirtualInterface> mTun = ServerSession::getInstance().getVirtualInterface();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
62
include/columnlynx/server/server_session.hpp
Normal file
62
include/columnlynx/server/server_session.hpp
Normal 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};
|
||||||
|
};
|
||||||
|
}
|
||||||
71
src/client/client_session.cpp
Normal file
71
src/client/client_session.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
// main.cpp - Client entry point for ColumnLynx
|
// 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.
|
// 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 <asio.hpp>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <filesystem>
|
||||||
#include <columnlynx/common/utils.hpp>
|
#include <columnlynx/common/utils.hpp>
|
||||||
#include <columnlynx/common/panic_handler.hpp>
|
#include <columnlynx/common/panic_handler.hpp>
|
||||||
#include <columnlynx/client/net/tcp/tcp_client.hpp>
|
#include <columnlynx/client/net/tcp/tcp_client.hpp>
|
||||||
#include <columnlynx/client/net/udp/udp_client.hpp>
|
#include <columnlynx/client/net/udp/udp_client.hpp>
|
||||||
#include <cxxopts.hpp>
|
#include <cxxopts.hpp>
|
||||||
#include <columnlynx/common/net/virtual_interface.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 asio::ip::tcp;
|
||||||
using namespace ColumnLynx::Utils;
|
using namespace ColumnLynx::Utils;
|
||||||
@@ -48,15 +55,23 @@ int main(int argc, char** argv) {
|
|||||||
#else
|
#else
|
||||||
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
|
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
|
||||||
#endif
|
#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);
|
auto optionsObj = options.parse(argc, argv);
|
||||||
if (optionsObj.count("help")) {
|
if (optionsObj.count("help")) {
|
||||||
std::cout << options.help() << std::endl;
|
std::cout << options.help() << std::endl;
|
||||||
std::cout << "This software is licensed under the GPLv2-only license OR the GPLv3 license.\n";
|
std::cout << "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";
|
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -72,20 +87,65 @@ int main(int argc, char** argv) {
|
|||||||
//WintunInitialize();
|
//WintunInitialize();
|
||||||
#endif
|
#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) {
|
||||||
|
// Validate and canonicalize environment-provided path
|
||||||
|
try {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path candidate(envConfigPath);
|
||||||
|
fs::path abs = fs::absolute(candidate, ec);
|
||||||
|
if (!ec) {
|
||||||
|
configPath = abs.string();
|
||||||
|
} else {
|
||||||
|
warn(std::string("Invalid COLUMNLYNX_CONFIG_DIR value: ") + envConfigPath + " - using default");
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
warn(std::string("Failed to canonicalize COLUMNLYNX_CONFIG_DIR: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>());
|
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
|
||||||
log("Using virtual interface: " + tun->getName());
|
log("Using virtual interface: " + tun->getName());
|
||||||
|
initialState.virtualInterface = tun;
|
||||||
|
|
||||||
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
||||||
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper->getPublicKey(), 32));
|
debug("Public Key: " + Utils::bytesToHexString(sodiumWrapper->getPublicKey(), 32));
|
||||||
debug("Private Key: " + Utils::bytesToHexString(sodiumWrapper->getPrivateKey(), 64));
|
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>>();
|
std::array<uint8_t, 32> aesKey = std::array<uint8_t, 32>();
|
||||||
aesKey->fill(0); // Defualt zeroed state until modified by handshake
|
aesKey.fill(0); // Defualt zeroed state until modified by handshake
|
||||||
std::shared_ptr<uint64_t> sessionID = std::make_shared<uint64_t>(0);
|
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;
|
asio::io_context io;
|
||||||
auto client = std::make_shared<ColumnLynx::Net::TCP::TCPClient>(io, host, port, sodiumWrapper, aesKey, sessionID, insecureMode, 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, aesKey, sessionID, tun);
|
auto udpClient = std::make_shared<ColumnLynx::Net::UDP::UDPClient>(io, host, port);
|
||||||
|
|
||||||
client->start();
|
client->start();
|
||||||
udpClient->start();
|
udpClient->start();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_client.cpp - TCP Client for ColumnLynx
|
// 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.
|
// 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 <columnlynx/client/net/tcp/tcp_client.hpp>
|
||||||
@@ -14,19 +14,25 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
asio::async_connect(mSocket, endpoints,
|
asio::async_connect(mSocket, endpoints,
|
||||||
[this, self](asio::error_code ec, const tcp::endpoint&) {
|
[this, self](asio::error_code ec, const tcp::endpoint&) {
|
||||||
if (!ec) {
|
if (!ec) {
|
||||||
mConnected = true;
|
mConnected.store(true, std::memory_order_relaxed);
|
||||||
Utils::log("Client connected.");
|
Utils::log("Client connected.");
|
||||||
mHandler = std::make_shared<MessageHandler>(std::move(mSocket));
|
mHandler = std::make_shared<MessageHandler>(std::move(mSocket));
|
||||||
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
|
mHandler->onMessage([weakSelf = weak_from_this()](AnyMessageType type, const std::string& data) {
|
||||||
mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
|
if (auto self = weakSelf.lock()) {
|
||||||
|
self->mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Close only after peer FIN to avoid RSTs
|
// Close only after peer FIN to avoid RSTs
|
||||||
mHandler->onDisconnect([this](const asio::error_code& ec) {
|
mHandler->onDisconnect([weakSelf = weak_from_this()](const asio::error_code& ec) {
|
||||||
asio::error_code ec2;
|
auto self = weakSelf.lock();
|
||||||
if (mHandler) {
|
if (!self) {
|
||||||
mHandler->socket().close(ec2);
|
return;
|
||||||
}
|
}
|
||||||
mConnected = false;
|
asio::error_code ec2;
|
||||||
|
if (self->mHandler) {
|
||||||
|
self->mHandler->socket().close(ec2);
|
||||||
|
}
|
||||||
|
self->mConnected.store(false, std::memory_order_relaxed);
|
||||||
Utils::log(std::string("Server disconnected: ") + ec.message());
|
Utils::log(std::string("Server disconnected: ") + ec.message());
|
||||||
});
|
});
|
||||||
mHandler->start();
|
mHandler->start();
|
||||||
@@ -46,10 +52,13 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
std::vector<uint8_t> payload;
|
std::vector<uint8_t> payload;
|
||||||
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
|
payload.reserve(1 + crypto_box_PUBLICKEYBYTES);
|
||||||
payload.push_back(Utils::protocolVersion());
|
payload.push_back(Utils::protocolVersion());
|
||||||
|
Utils::log("Using protocol version: " + std::to_string(Utils::protocolVersion()));
|
||||||
/*payload.insert(payload.end(),
|
/*payload.insert(payload.end(),
|
||||||
mLibSodiumWrapper->getXPublicKey(),
|
mLibSodiumWrapper->getXPublicKey(),
|
||||||
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
|
mLibSodiumWrapper->getXPublicKey() + crypto_box_PUBLICKEYBYTES
|
||||||
);*/
|
);*/
|
||||||
|
|
||||||
|
const auto& mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper();
|
||||||
payload.insert(payload.end(),
|
payload.insert(payload.end(),
|
||||||
mLibSodiumWrapper->getPublicKey(),
|
mLibSodiumWrapper->getPublicKey(),
|
||||||
mLibSodiumWrapper->getPublicKey() + crypto_sign_PUBLICKEYBYTES
|
mLibSodiumWrapper->getPublicKey() + crypto_sign_PUBLICKEYBYTES
|
||||||
@@ -71,7 +80,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TCPClient::sendMessage(ClientMessageType type, const std::string& data) {
|
void TCPClient::sendMessage(ClientMessageType type, const std::string& data) {
|
||||||
if (!mConnected) {
|
if (!mConnected.load(std::memory_order_relaxed)) {
|
||||||
Utils::error("Cannot send message, client not connected.");
|
Utils::error("Cannot send message, client not connected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,7 +93,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TCPClient::disconnect(bool echo) {
|
void TCPClient::disconnect(bool echo) {
|
||||||
if (mConnected && mHandler) {
|
if (mConnected.load(std::memory_order_relaxed) && mHandler) {
|
||||||
if (echo) {
|
if (echo) {
|
||||||
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
|
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
|
||||||
}
|
}
|
||||||
@@ -104,17 +113,17 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TCPClient::isHandshakeComplete() const {
|
bool TCPClient::isHandshakeComplete() const {
|
||||||
return mHandshakeComplete;
|
return mHandshakeComplete.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TCPClient::isConnected() const {
|
bool TCPClient::isConnected() const {
|
||||||
return mConnected;
|
return mConnected.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TCPClient::mStartHeartbeat() {
|
void TCPClient::mStartHeartbeat() {
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
|
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
|
||||||
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
|
mHeartbeatTimer.async_wait([self](const asio::error_code& ec) {
|
||||||
if (ec == asio::error::operation_aborted) {
|
if (ec == asio::error::operation_aborted) {
|
||||||
return; // Timer was cancelled
|
return; // Timer was cancelled
|
||||||
}
|
}
|
||||||
@@ -127,14 +136,14 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// Close sockets forcefully, server is dead
|
// Close sockets forcefully, server is dead
|
||||||
asio::error_code ec;
|
asio::error_code ec;
|
||||||
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
|
if (self->mHandler) {
|
||||||
mHandler->socket().close(ec);
|
self->mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
|
||||||
mConnected = false;
|
self->mHandler->socket().close(ec);
|
||||||
|
|
||||||
mGlobalKeyRef = nullptr;
|
|
||||||
if (mSessionIDRef) {
|
|
||||||
*mSessionIDRef = 0;
|
|
||||||
}
|
}
|
||||||
|
self->mConnected.store(false, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros
|
||||||
|
ClientSession::getInstance().setSessionID(0);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,13 +159,21 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) {
|
void TCPClient::mHandleMessage(ServerMessageType type, const std::string& data) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ServerMessageType::HANDSHAKE_IDENTIFY: {
|
case ServerMessageType::HANDSHAKE_IDENTIFY: {
|
||||||
Utils::log("Received server identity: " + data);
|
if (data.size() != sizeof(mServerPublicKey)) {
|
||||||
std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey)));
|
Utils::warn("HANDSHAKE_IDENTIFY has invalid size: " + std::to_string(data.size()));
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(mServerPublicKey, data.data(), sizeof(mServerPublicKey));
|
||||||
|
std::string hexServerPub = Utils::bytesToHexString(mServerPublicKey, 32);
|
||||||
|
Utils::log("Received server identity. Public Key: " + hexServerPub);
|
||||||
|
|
||||||
// Verify pubkey against whitelisted_keys
|
// 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 (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.");
|
Utils::error("Server public key not in whitelisted_keys. Terminating connection.");
|
||||||
disconnect();
|
disconnect();
|
||||||
return;
|
return;
|
||||||
@@ -177,7 +194,13 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
{
|
{
|
||||||
// Verify the signature
|
// Verify the signature
|
||||||
Signature sig{};
|
Signature sig{};
|
||||||
std::memcpy(sig.data(), data.data(), std::min(data.size(), sig.size()));
|
if (data.size() != sig.size()) {
|
||||||
|
Utils::warn("HANDSHAKE_CHALLENGE_RESPONSE has invalid size: " + std::to_string(data.size()));
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(sig.data(), data.data(), sig.size());
|
||||||
if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) {
|
if (Utils::LibSodiumWrapper::verifyMessage(mSubmittedChallenge.data(), mSubmittedChallenge.size(), sig, mServerPublicKey)) {
|
||||||
Utils::log("Challenge response verified successfully.");
|
Utils::log("Challenge response verified successfully.");
|
||||||
|
|
||||||
@@ -192,16 +215,14 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// Generate AES key and send confirmation
|
// Generate AES key and send confirmation
|
||||||
mConnectionAESKey = Utils::LibSodiumWrapper::generateRandom256Bit();
|
mConnectionAESKey = Utils::LibSodiumWrapper::generateRandom256Bit();
|
||||||
if (mGlobalKeyRef) { // Copy to the global reference
|
ClientSession::getInstance().setAESKey(mConnectionAESKey);
|
||||||
std::copy(mConnectionAESKey.begin(), mConnectionAESKey.end(), mGlobalKeyRef->begin());
|
|
||||||
}
|
|
||||||
AsymNonce nonce{};
|
AsymNonce nonce{};
|
||||||
randombytes_buf(nonce.data(), nonce.size());
|
randombytes_buf(nonce.data(), nonce.size());
|
||||||
|
|
||||||
// TODO: This is pretty redundant, it should return the required type directly
|
// TODO: This is pretty redundant, it should return the required type directly
|
||||||
std::array<uint8_t, 32> arrayPrivateKey;
|
std::array<uint8_t, 32> arrayPrivateKey;
|
||||||
std::copy(mLibSodiumWrapper->getXPrivateKey(),
|
std::copy(ClientSession::getInstance().getSodiumWrapper()->getXPrivateKey(),
|
||||||
mLibSodiumWrapper->getXPrivateKey() + 32,
|
ClientSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32,
|
||||||
arrayPrivateKey.begin());
|
arrayPrivateKey.begin());
|
||||||
|
|
||||||
std::vector<uint8_t> encr = Utils::LibSodiumWrapper::encryptAsymmetric(
|
std::vector<uint8_t> encr = Utils::LibSodiumWrapper::encryptAsymmetric(
|
||||||
@@ -244,24 +265,23 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID));
|
std::memcpy(&mConnectionSessionID, decrypted.data(), sizeof(mConnectionSessionID));
|
||||||
std::memcpy(&mTunConfig, decrypted.data() + sizeof(mConnectionSessionID), sizeof(Protocol::TunConfig));
|
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));
|
Utils::log("Connection established with Session ID: " + std::to_string(mConnectionSessionID));
|
||||||
|
|
||||||
if (mSessionIDRef) { // Copy to the global reference
|
ClientSession::getInstance().setSessionID(mConnectionSessionID);
|
||||||
*mSessionIDRef = mConnectionSessionID;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t clientIP = ntohl(mTunConfig.clientIP);
|
uint32_t clientIP = ntohl(mTunConfig.clientIP);
|
||||||
uint32_t serverIP = ntohl(mTunConfig.serverIP);
|
uint32_t serverIP = ntohl(mTunConfig.serverIP);
|
||||||
uint8_t prefixLen = mTunConfig.prefixLength;
|
uint8_t prefixLen = mTunConfig.prefixLength;
|
||||||
uint16_t mtu = mTunConfig.mtu;
|
uint16_t mtu = mTunConfig.mtu;
|
||||||
|
|
||||||
|
const auto& mTun = ClientSession::getInstance().getVirtualInterface();
|
||||||
if (mTun) {
|
if (mTun) {
|
||||||
mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
|
mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
|
||||||
}
|
}
|
||||||
|
|
||||||
mHandshakeComplete = true;
|
mHandshakeComplete.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -276,13 +296,13 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
break;
|
break;
|
||||||
case ServerMessageType::GRACEFUL_DISCONNECT:
|
case ServerMessageType::GRACEFUL_DISCONNECT:
|
||||||
Utils::log("Server is disconnecting: " + data);
|
Utils::log("Server is disconnecting: " + data);
|
||||||
if (mConnected) { // Prevent Recursion
|
if (mConnected.load(std::memory_order_relaxed)) { // Prevent Recursion
|
||||||
disconnect(false);
|
disconnect(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ServerMessageType::KILL_CONNECTION:
|
case ServerMessageType::KILL_CONNECTION:
|
||||||
Utils::warn("Server is killing the connection: " + data);
|
Utils::warn("Server is killing the connection: " + data);
|
||||||
if (mConnected) {
|
if (mConnected.load(std::memory_order_relaxed)) {
|
||||||
disconnect(false);
|
disconnect(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// udp_client.cpp - UDP Client for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/client/net/udp/udp_client.hpp>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::UDP {
|
namespace ColumnLynx::Net::UDP {
|
||||||
void UDPClient::start() {
|
void UDPClient::start() {
|
||||||
@@ -46,9 +48,14 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
|
|
||||||
void UDPClient::sendMessage(const std::string& data) {
|
void UDPClient::sendMessage(const std::string& data) {
|
||||||
UDPPacketHeader hdr{};
|
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!");
|
Utils::error("UDP Client AES key or Session ID reference is null!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,24 +64,28 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
|
|
||||||
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
|
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
|
||||||
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
|
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
|
||||||
*mAesKeyRef, hdr.nonce, "udp-data"
|
ClientSession::getInstance().getAESKey(), hdr.nonce, "udp-data"
|
||||||
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
|
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
|
||||||
);
|
);
|
||||||
|
|
||||||
std::vector<uint8_t> packet;
|
std::vector<uint8_t> packet;
|
||||||
packet.reserve(sizeof(UDPPacketHeader) + sizeof(uint64_t) + encryptedPayload.size());
|
packet.reserve(sizeof(UDPPacketHeader) + encryptedPayload.size());
|
||||||
packet.insert(packet.end(),
|
packet.insert(packet.end(),
|
||||||
reinterpret_cast<uint8_t*>(&hdr),
|
reinterpret_cast<uint8_t*>(&hdr),
|
||||||
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
|
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
|
||||||
);
|
);
|
||||||
|
uint32_t sessionID = static_cast<uint32_t>(ClientSession::getInstance().getSessionID());
|
||||||
|
uint32_t sessionIDNet = htonl(sessionID);
|
||||||
packet.insert(packet.end(),
|
packet.insert(packet.end(),
|
||||||
reinterpret_cast<uint8_t*>(mSessionIDRef.get()),
|
reinterpret_cast<uint8_t*>(&sessionIDNet),
|
||||||
reinterpret_cast<uint8_t*>(mSessionIDRef.get()) + sizeof(uint64_t)
|
reinterpret_cast<uint8_t*>(&sessionIDNet) + sizeof(uint32_t)
|
||||||
);
|
);
|
||||||
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
|
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
|
||||||
|
|
||||||
mSocket.send_to(asio::buffer(packet), mRemoteEndpoint);
|
mSocket.send_to(asio::buffer(packet), mRemoteEndpoint);
|
||||||
Utils::debug("Sent UDP packet of size " + std::to_string(packet.size()));
|
Utils::debug("Sent UDP packet of size " + std::to_string(packet.size()));
|
||||||
|
|
||||||
|
ClientSession::getInstance().incrementSendCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDPClient::stop() {
|
void UDPClient::stop() {
|
||||||
@@ -93,7 +104,12 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
if (ec) {
|
if (ec) {
|
||||||
if (ec == asio::error::operation_aborted) return; // Socket closed
|
if (ec == asio::error::operation_aborted) return; // Socket closed
|
||||||
// Other recv error
|
// Other recv error
|
||||||
mStartReceive();
|
Utils::warn("UDPClient receive error: " + ec.message());
|
||||||
|
// Back off briefly before restarting receive to avoid busy error loops
|
||||||
|
asio::post(mSocket.get_executor(), [this]() {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
mStartReceive();
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +123,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UDPClient::mHandlePacket(std::size_t bytes) {
|
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.");
|
Utils::warn("UDP Client received packet too small to process.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -117,27 +133,43 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader));
|
std::memcpy(&hdr, mRecvBuffer.data(), sizeof(UDPPacketHeader));
|
||||||
|
|
||||||
// Parse session ID
|
// Parse session ID
|
||||||
uint64_t sessionID;
|
uint32_t sessionIDNet;
|
||||||
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
|
std::memcpy(&sessionIDNet, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint32_t));
|
||||||
|
uint32_t sessionID = ntohl(sessionIDNet);
|
||||||
|
|
||||||
if (sessionID != *mSessionIDRef) {
|
if (sessionID != ClientSession::getInstance().getSessionID()) {
|
||||||
Utils::warn("Got packet that isn't for me! Dropping!");
|
Utils::warn("This packet that isn't for me! Dropping!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt payload
|
// Decrypt payload
|
||||||
|
// Extract ciphertext safely
|
||||||
|
size_t headerLen = sizeof(UDPPacketHeader) + sizeof(uint32_t);
|
||||||
|
if (bytes < headerLen) {
|
||||||
|
Utils::warn("UDP Client received packet too small after header check.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ciphertextLen = bytes - headerLen;
|
||||||
|
// Enforce reasonable maximum (UDP payload practical limit)
|
||||||
|
const size_t MAX_UDP_PAYLOAD = 65507; // 65535 - UDP/IP headers
|
||||||
|
if (ciphertextLen > MAX_UDP_PAYLOAD) {
|
||||||
|
Utils::warn("UDP Client received packet with excessive payload size: " + std::to_string(ciphertextLen));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> ciphertext(
|
std::vector<uint8_t> ciphertext(
|
||||||
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint64_t),
|
mRecvBuffer.begin() + headerLen,
|
||||||
mRecvBuffer.begin() + bytes
|
mRecvBuffer.begin() + headerLen + ciphertextLen
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mAesKeyRef == nullptr) {
|
if (ClientSession::getInstance().getAESKey().empty()) {
|
||||||
Utils::error("UDP Client AES key reference is null!");
|
Utils::error("UDP Client AES key reference is null!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> plaintext = Utils::LibSodiumWrapper::decryptMessage(
|
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))
|
//std::string(reinterpret_cast<const char*>(&mSessionIDRef), sizeof(uint64_t))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -149,6 +181,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
Utils::debug("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
|
Utils::debug("UDP Client received packet from " + mRemoteEndpoint.address().to_string() + " - Packet size: " + std::to_string(bytes));
|
||||||
|
|
||||||
// Write to TUN
|
// Write to TUN
|
||||||
|
const auto& mTunRef = ClientSession::getInstance().getVirtualInterface();
|
||||||
if (mTunRef) {
|
if (mTunRef) {
|
||||||
mTunRef->writePacket(plaintext);
|
mTunRef->writePacket(plaintext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// libsodium_wrapper.cpp - Libsodium Wrapper for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
@@ -41,4 +41,27 @@ namespace ColumnLynx::Utils {
|
|||||||
randombytes_buf(randbytes.data(), randbytes.size());
|
randombytes_buf(randbytes.data(), randbytes.size());
|
||||||
return randbytes;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
// session_registry.cpp - Session Registry for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/common/net/session_registry.hpp>
|
||||||
|
|
||||||
namespace ColumnLynx::Net {
|
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);
|
std::unique_lock lock(mMutex);
|
||||||
mSessions[sessionID] = std::move(state);
|
mSessions[sessionID] = std::move(state);
|
||||||
mIPSessions[mSessions[sessionID]->clientTunIP] = mSessions[sessionID];
|
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);
|
std::shared_lock lock(mMutex);
|
||||||
auto it = mSessions.find(sessionID);
|
auto it = mSessions.find(sessionID);
|
||||||
return (it == mSessions.end()) ? nullptr : it->second;
|
return (it == mSessions.end()) ? nullptr : it->second;
|
||||||
@@ -23,16 +23,35 @@ namespace ColumnLynx::Net {
|
|||||||
return (it == mIPSessions.end()) ? nullptr : it->second;
|
return (it == mIPSessions.end()) ? nullptr : it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_map<uint64_t, std::shared_ptr<SessionState>> SessionRegistry::snapshot() const {
|
std::unordered_map<uint32_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>> snap;
|
||||||
std::shared_lock lock(mMutex);
|
std::shared_lock lock(mMutex);
|
||||||
snap = mSessions;
|
snap = mSessions;
|
||||||
return snap;
|
return snap;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionRegistry::erase(uint64_t sessionID) {
|
void SessionRegistry::erase(uint32_t sessionID) {
|
||||||
std::unique_lock lock(mMutex);
|
std::unique_lock lock(mMutex);
|
||||||
mSessions.erase(sessionID);
|
auto it = mSessions.find(sessionID);
|
||||||
|
if (it != mSessions.end()) {
|
||||||
|
// If the session has a client IP mapping, remove it to avoid stale entries
|
||||||
|
if (it->second) {
|
||||||
|
uint32_t ip = it->second->clientTunIP;
|
||||||
|
auto ipIt = mIPSessions.find(ip);
|
||||||
|
if (ipIt != mIPSessions.end()) {
|
||||||
|
// Only erase if it points to the same session
|
||||||
|
if (ipIt->second == it->second) {
|
||||||
|
mIPSessions.erase(ipIt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any session->ip bookkeeping
|
||||||
|
mSessionIPs.erase(sessionID);
|
||||||
|
|
||||||
|
// Finally erase the session
|
||||||
|
mSessions.erase(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionRegistry::cleanupExpired() {
|
void SessionRegistry::cleanupExpired() {
|
||||||
@@ -60,6 +79,11 @@ namespace ColumnLynx::Net {
|
|||||||
return static_cast<int>(mSessions.size());
|
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 {
|
uint32_t SessionRegistry::getFirstAvailableIP(uint32_t baseIP, uint8_t mask) const {
|
||||||
std::shared_lock lock(mMutex);
|
std::shared_lock lock(mMutex);
|
||||||
|
|
||||||
@@ -77,17 +101,21 @@ namespace ColumnLynx::Net {
|
|||||||
return 0;
|
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);
|
std::unique_lock lock(mMutex);
|
||||||
mSessionIPs[sessionID] = ip;
|
mSessionIPs[sessionID] = ip;
|
||||||
|
|
||||||
/*if (mIPSessions.find(sessionID) == mIPSessions.end()) {
|
auto it = mSessions.find(sessionID);
|
||||||
Utils::debug("yikes");
|
if (it == mSessions.end() || !it->second) {
|
||||||
}*/
|
Utils::warn("SessionRegistry::lockIP called for unknown session " + std::to_string(sessionID));
|
||||||
mIPSessions[ip] = mSessions.find(sessionID)->second;
|
mSessionIPs.erase(sessionID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mIPSessions[ip] = it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionRegistry::deallocIP(uint64_t sessionID) {
|
void SessionRegistry::deallocIP(uint32_t sessionID) {
|
||||||
std::unique_lock lock(mMutex);
|
std::unique_lock lock(mMutex);
|
||||||
|
|
||||||
auto it = mSessionIPs.find(sessionID);
|
auto it = mSessionIPs.find(sessionID);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// tcp_message_handler.cpp - TCP Message Handler for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/common/net/tcp/tcp_message_handler.hpp>
|
||||||
#include <columnlynx/common/net/tcp/net_helper.hpp>
|
#include <columnlynx/common/net/tcp/net_helper.hpp>
|
||||||
#include <columnlynx/common/utils.hpp>
|
#include <columnlynx/common/utils.hpp>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::TCP {
|
namespace ColumnLynx::Net::TCP {
|
||||||
void MessageHandler::start() {
|
void MessageHandler::start() {
|
||||||
@@ -17,17 +18,23 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
return static_cast<uint8_t>(type);
|
return static_cast<uint8_t>(type);
|
||||||
}, type);
|
}, type);
|
||||||
|
|
||||||
std::vector<uint8_t> data;
|
auto data = std::make_shared<std::vector<uint8_t>>();
|
||||||
data.push_back(typeByte);
|
data->push_back(typeByte);
|
||||||
uint16_t length = payload.size();
|
// Ensure payload fits into protocol's 16-bit length field
|
||||||
|
if (payload.size() > static_cast<size_t>(std::numeric_limits<uint16_t>::max())) {
|
||||||
|
Utils::error("sendMessage(): payload too large (>65535 bytes)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
data.push_back(length >> 8);
|
uint16_t length = static_cast<uint16_t>(payload.size());
|
||||||
data.push_back(length & 0xFF);
|
|
||||||
|
|
||||||
data.insert(data.end(), payload.begin(), payload.end());
|
data->push_back(static_cast<uint8_t>(length >> 8));
|
||||||
|
data->push_back(static_cast<uint8_t>(length & 0xFF));
|
||||||
|
|
||||||
|
data->insert(data->end(), payload.begin(), payload.end());
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
asio::async_write(mSocket, asio::buffer(data),
|
asio::async_write(mSocket, asio::buffer(*data),
|
||||||
[self](asio::error_code ec, std::size_t) {
|
[self, data](asio::error_code ec, std::size_t) {
|
||||||
if (ec) {
|
if (ec) {
|
||||||
Utils::error("Send failed: " + ec.message());
|
Utils::error("Send failed: " + ec.message());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,66 @@
|
|||||||
// utils.cpp - Utility functions for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/common/utils.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
namespace ColumnLynx::Utils {
|
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) {
|
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();
|
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) {
|
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();
|
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) {
|
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();
|
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) {
|
void debug(const std::string &msg) {
|
||||||
#if DEBUG || _DEBUG
|
#if DEBUG || _DEBUG
|
||||||
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
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
|
#else
|
||||||
return;
|
return;
|
||||||
#endif
|
#endif
|
||||||
@@ -49,7 +86,7 @@ namespace ColumnLynx::Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string getVersion() {
|
std::string getVersion() {
|
||||||
return "b0.3";
|
return "1.1.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned short serverPort() {
|
unsigned short serverPort() {
|
||||||
@@ -57,7 +94,7 @@ namespace ColumnLynx::Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsigned char protocolVersion() {
|
unsigned char protocolVersion() {
|
||||||
return 1;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string bytesToHexString(const uint8_t* bytes, size_t length) {
|
std::string bytesToHexString(const uint8_t* bytes, size_t length) {
|
||||||
@@ -101,18 +138,56 @@ namespace ColumnLynx::Utils {
|
|||||||
return bytes;
|
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.
|
// 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.
|
// 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.
|
// I might redo this part.
|
||||||
|
|
||||||
std::vector<std::string> out;
|
std::vector<std::string> out;
|
||||||
|
|
||||||
std::ifstream file("whitelisted_keys"); // TODO: This is hardcoded for now, make dynamic
|
namespace fs = std::filesystem;
|
||||||
std::string line;
|
std::error_code ec;
|
||||||
|
|
||||||
|
fs::path base(basePath);
|
||||||
|
fs::path absBase = fs::absolute(base, ec);
|
||||||
|
if (ec) {
|
||||||
|
warn("getWhitelistedKeys(): failed to resolve base path: " + basePath + " - " + ec.message());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path whitelist = absBase / "whitelisted_keys";
|
||||||
|
if (!fs::exists(whitelist, ec) || ec) {
|
||||||
|
warn("getWhitelistedKeys(): whitelist file not found: " + whitelist.string());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize to avoid symlink tricks
|
||||||
|
fs::path canon = fs::canonical(whitelist, ec);
|
||||||
|
if (ec) {
|
||||||
|
warn("getWhitelistedKeys(): failed to canonicalize path: " + whitelist.string());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(canon);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
warn("getWhitelistedKeys(): failed to open whitelist file: " + canon.string());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
while (std::getline(file, line)) {
|
while (std::getline(file, line)) {
|
||||||
out.push_back(line);
|
// Trim whitespace
|
||||||
|
while (!line.empty() && isspace(static_cast<unsigned char>(line.back()))) line.pop_back();
|
||||||
|
size_t start = 0;
|
||||||
|
while (start < line.size() && isspace(static_cast<unsigned char>(line[start]))) ++start;
|
||||||
|
if (start >= line.size()) continue;
|
||||||
|
std::string key = line.substr(start);
|
||||||
|
|
||||||
|
// Convert to upper case to align with the bytesToHexString() output
|
||||||
|
for (size_t i = 0; i < key.length(); ++i) {
|
||||||
|
key[i] = static_cast<char>(toupper(static_cast<unsigned char>(key[i])));
|
||||||
|
}
|
||||||
|
out.push_back(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -122,7 +197,28 @@ namespace ColumnLynx::Utils {
|
|||||||
// TODO: Currently re-reads every time.
|
// TODO: Currently re-reads every time.
|
||||||
std::vector<std::string> readLines;
|
std::vector<std::string> readLines;
|
||||||
|
|
||||||
std::ifstream file(path);
|
namespace fs = std::filesystem;
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path p(path);
|
||||||
|
fs::path abs = fs::absolute(p, ec);
|
||||||
|
if (ec) {
|
||||||
|
throw std::runtime_error("getConfigMap(): failed to resolve path: " + path + " - " + ec.message());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs::exists(abs, ec) || ec) {
|
||||||
|
throw std::runtime_error("getConfigMap(): config file does not exist: " + abs.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path canon = fs::canonical(abs, ec);
|
||||||
|
if (ec) {
|
||||||
|
throw std::runtime_error("getConfigMap(): failed to canonicalize config path: " + abs.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(canon);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
throw std::runtime_error("Failed to open config file at path: " + canon.string());
|
||||||
|
}
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
|
|
||||||
while (std::getline(file, line)) {
|
while (std::getline(file, line)) {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
// virtual_interface.cpp - Virtual Interface for Network Communication
|
// 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.
|
// 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>
|
#include <columnlynx/common/net/virtual_interface.hpp>
|
||||||
|
|
||||||
|
#include <spawn.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
extern char **environ;
|
||||||
|
|
||||||
// This is all fucking voodoo dark magic.
|
// This is all fucking voodoo dark magic.
|
||||||
|
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
@@ -56,6 +61,33 @@ static void InitializeWintun()
|
|||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
|
|
||||||
namespace ColumnLynx::Net {
|
namespace ColumnLynx::Net {
|
||||||
|
|
||||||
|
// Run a command without invoking a shell. Arguments are passed directly
|
||||||
|
// to the underlying process to avoid shell injection vulnerabilities.
|
||||||
|
static bool runCommand(const std::vector<std::string>& args) {
|
||||||
|
if (args.empty()) return false;
|
||||||
|
|
||||||
|
std::vector<char*> argv;
|
||||||
|
argv.reserve(args.size() + 1);
|
||||||
|
for (const auto &s : args) {
|
||||||
|
argv.push_back(const_cast<char*>(s.c_str()));
|
||||||
|
}
|
||||||
|
argv.push_back(nullptr);
|
||||||
|
|
||||||
|
pid_t pid;
|
||||||
|
int rc = posix_spawnp(&pid, argv[0], nullptr, nullptr, argv.data(), environ);
|
||||||
|
if (rc != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
if (waitpid(pid, &status, 0) == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------ Constructor ------------------------------
|
// ------------------------------ Constructor ------------------------------
|
||||||
VirtualInterface::VirtualInterface(const std::string& ifName)
|
VirtualInterface::VirtualInterface(const std::string& ifName)
|
||||||
: mIfName(ifName), mFd(-1)
|
: mIfName(ifName), mFd(-1)
|
||||||
@@ -72,7 +104,7 @@ namespace ColumnLynx::Net {
|
|||||||
|
|
||||||
if (ioctl(mFd, TUNSETIFF, &ifr) < 0) {
|
if (ioctl(mFd, TUNSETIFF, &ifr) < 0) {
|
||||||
close(mFd);
|
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__)
|
#elif defined(__APPLE__)
|
||||||
@@ -96,7 +128,7 @@ namespace ColumnLynx::Net {
|
|||||||
|
|
||||||
if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) {
|
if (connect(mFd, (struct sockaddr*)&sc, sizeof(sc)) < 0) {
|
||||||
if (errno == EPERM)
|
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)));
|
throw std::runtime_error("connect(AF_SYS_CONTROL) failed: " + std::string(strerror(errno)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +213,8 @@ namespace ColumnLynx::Net {
|
|||||||
pfd.fd = mFd;
|
pfd.fd = mFd;
|
||||||
pfd.events = POLLIN;
|
pfd.events = POLLIN;
|
||||||
|
|
||||||
// timeout in ms; keep it small so shutdown is responsive
|
// timeout in ms; keep it small so shutdown is responsive. Reduced for lower latency.
|
||||||
int ret = poll(&pfd, 1, 200);
|
int ret = poll(&pfd, 1, 50);
|
||||||
|
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
// No data yet
|
// No data yet
|
||||||
@@ -307,25 +339,17 @@ namespace ColumnLynx::Net {
|
|||||||
|
|
||||||
void VirtualInterface::resetIP() {
|
void VirtualInterface::resetIP() {
|
||||||
#if defined(__linux__)
|
#if defined(__linux__)
|
||||||
char cmd[512];
|
runCommand({"ip", "addr", "flush", "dev", mIfName});
|
||||||
snprintf(cmd, sizeof(cmd),
|
|
||||||
"ip addr flush dev %s",
|
|
||||||
mIfName.c_str()
|
|
||||||
);
|
|
||||||
system(cmd);
|
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
char cmd[512];
|
runCommand({"ifconfig", mIfName, "inet", "0.0.0.0", "delete"});
|
||||||
snprintf(cmd, sizeof(cmd),
|
runCommand({"ifconfig", mIfName, "inet6", "::", "delete"});
|
||||||
"ifconfig %s inet 0.0.0.0 delete",
|
|
||||||
mIfName.c_str()
|
|
||||||
);
|
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
snprintf(cmd, sizeof(cmd),
|
// Wipe old routes
|
||||||
"ifconfig %s inet6 :: delete",
|
//snprintf(cmd, sizeof(cmd),
|
||||||
mIfName.c_str()
|
// "route -n delete -net %s",
|
||||||
);
|
// mIfName.c_str()
|
||||||
system(cmd);
|
//);
|
||||||
|
//system(cmd);
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
char cmd[512];
|
char cmd[512];
|
||||||
// Remove any persistent routes associated with this interface
|
// Remove any persistent routes associated with this interface
|
||||||
@@ -350,26 +374,19 @@ namespace ColumnLynx::Net {
|
|||||||
bool VirtualInterface::mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP,
|
bool VirtualInterface::mApplyLinuxIP(uint32_t clientIP, uint32_t serverIP,
|
||||||
uint8_t prefixLen, uint16_t mtu)
|
uint8_t prefixLen, uint16_t mtu)
|
||||||
{
|
{
|
||||||
char cmd[512];
|
|
||||||
|
|
||||||
std::string ipStr = ipv4ToString(clientIP);
|
std::string ipStr = ipv4ToString(clientIP);
|
||||||
std::string peerStr = ipv4ToString(serverIP);
|
std::string peerStr = ipv4ToString(serverIP);
|
||||||
|
|
||||||
// Wipe the current config
|
// Wipe the current config
|
||||||
snprintf(cmd, sizeof(cmd),
|
runCommand({"ip", "addr", "flush", "dev", mIfName});
|
||||||
"ip addr flush dev %s",
|
|
||||||
mIfName.c_str()
|
|
||||||
);
|
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
snprintf(cmd, sizeof(cmd),
|
// Add address with peer
|
||||||
"ip addr add %s/%d peer %s dev %s",
|
std::string addrArg = ipStr + "/" + std::to_string(prefixLen);
|
||||||
ipStr.c_str(), prefixLen, peerStr.c_str(), mIfName.c_str());
|
runCommand({"ip", "addr", "add", addrArg, "peer", peerStr, "dev", mIfName});
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
snprintf(cmd, sizeof(cmd),
|
// Bring link up and set MTU
|
||||||
"ip link set dev %s up mtu %d", mIfName.c_str(), mtu);
|
runCommand({"ip", "link", "set", "dev", mIfName, "up", "mtu", std::to_string(mtu)});
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -380,33 +397,23 @@ namespace ColumnLynx::Net {
|
|||||||
bool VirtualInterface::mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP,
|
bool VirtualInterface::mApplyMacOSIP(uint32_t clientIP, uint32_t serverIP,
|
||||||
uint8_t prefixLen, uint16_t mtu)
|
uint8_t prefixLen, uint16_t mtu)
|
||||||
{
|
{
|
||||||
char cmd[512];
|
|
||||||
|
|
||||||
std::string ipStr = ipv4ToString(clientIP);
|
std::string ipStr = ipv4ToString(clientIP);
|
||||||
std::string peerStr = ipv4ToString(serverIP);
|
std::string peerStr = ipv4ToString(serverIP);
|
||||||
std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false);
|
std::string prefixStr = ipv4ToString(prefixLengthToNetmask(prefixLen), false);
|
||||||
Utils::debug("Prefix string: " + prefixStr);
|
Utils::debug("Prefix string: " + prefixStr);
|
||||||
|
|
||||||
// Reset
|
// Reset IPv4 and IPv6 addresses
|
||||||
snprintf(cmd, sizeof(cmd),
|
runCommand({"ifconfig", mIfName, "inet", "0.0.0.0", "delete"});
|
||||||
"ifconfig %s inet 0.0.0.0 delete",
|
runCommand({"ifconfig", mIfName, "inet6", "::", "delete"});
|
||||||
mIfName.c_str()
|
|
||||||
);
|
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
snprintf(cmd, sizeof(cmd),
|
// Set address and netmask
|
||||||
"ifconfig %s inet6 :: delete",
|
std::string netArg = ipStr + " " + peerStr; // ifconfig expects ip peer
|
||||||
mIfName.c_str()
|
runCommand({"ifconfig", mIfName, "inet", ipStr, peerStr, "mtu", std::to_string(mtu), "netmask", prefixStr, "up"});
|
||||||
);
|
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
// Set
|
// Add route for the network
|
||||||
snprintf(cmd, sizeof(cmd),
|
std::string networkArg = ipStr + "/" + std::to_string(prefixLen);
|
||||||
"ifconfig %s inet %s %s mtu %d netmask %s up",
|
runCommand({"route", "-n", "add", "-net", networkArg, "-interface", mIfName});
|
||||||
mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
|
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
Utils::log("Executed command: " + std::string(cmd));
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -420,41 +427,66 @@ namespace ColumnLynx::Net {
|
|||||||
uint16_t mtu)
|
uint16_t mtu)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
std::string ip = ipv4ToString(clientIP);
|
// Interface alias → LUID → Index
|
||||||
std::string gw = ipv4ToString(serverIP);
|
std::wstring ifAlias(mIfName.begin(), mIfName.end());
|
||||||
std::string mask;
|
|
||||||
|
|
||||||
// Convert prefixLen → subnet mask
|
NET_LUID luid;
|
||||||
uint32_t maskInt = (prefixLen == 0) ? 0 : (0xFFFFFFFF << (32 - prefixLen));
|
if (ConvertInterfaceAliasToLuid(ifAlias.c_str(), &luid) != NO_ERROR)
|
||||||
mask = ipv4ToString(maskInt);
|
return false;
|
||||||
|
|
||||||
// Calculate network address from IP and mask
|
NET_IFINDEX ifIndex;
|
||||||
uint32_t networkInt = (clientIP & maskInt);
|
if (ConvertInterfaceLuidToIndex(&luid, &ifIndex) != NO_ERROR)
|
||||||
std::string network = ipv4ToString(networkInt);
|
return false;
|
||||||
|
|
||||||
char cmd[512];
|
// ssign IPv4 address + prefix
|
||||||
|
MIB_UNICASTIPADDRESS_ROW addr;
|
||||||
|
InitializeUnicastIpAddressEntry(&addr);
|
||||||
|
|
||||||
// 1. Set the static IP + mask + gateway
|
addr.InterfaceIndex = ifIndex;
|
||||||
snprintf(cmd, sizeof(cmd),
|
addr.Address.si_family = AF_INET;
|
||||||
"netsh interface ip set address name=\"%s\" static %s %s %s",
|
addr.Address.Ipv4.sin_addr.s_addr = htonl(clientIP);
|
||||||
mIfName.c_str(), ip.c_str(), mask.c_str(), gw.c_str()
|
addr.OnLinkPrefixLength = prefixLen;
|
||||||
);
|
addr.DadState = IpDadStatePreferred;
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
// 2. Set MTU (separate command)
|
if (CreateUnicastIpAddressEntry(&addr) != NO_ERROR)
|
||||||
snprintf(cmd, sizeof(cmd),
|
return false;
|
||||||
"netsh interface ipv4 set subinterface \"%s\" mtu=%u store=persistent",
|
|
||||||
mIfName.c_str(), mtu
|
|
||||||
);
|
|
||||||
system(cmd);
|
|
||||||
|
|
||||||
// 3. Add route for the VPN network to go through the TUN interface
|
// Set MTU
|
||||||
// This is critical: tells Windows to send packets destined for the server/network through the TUN interface
|
MIB_IFROW ifRow;
|
||||||
snprintf(cmd, sizeof(cmd),
|
ifRow.dwIndex = ifIndex;
|
||||||
"netsh routing ip add persistentroute dest=%s/%d name=\"%s\" nexthopcfg=%s",
|
|
||||||
network.c_str(), prefixLen, mIfName.c_str(), gw.c_str()
|
if (GetIfEntry(&ifRow) != NO_ERROR)
|
||||||
);
|
return false;
|
||||||
system(cmd);
|
|
||||||
|
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;
|
return true;
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// main.cpp - Server entry point for ColumnLynx
|
// 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.
|
// 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 <asio.hpp>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
#include <columnlynx/common/utils.hpp>
|
#include <columnlynx/common/utils.hpp>
|
||||||
#include <columnlynx/common/panic_handler.hpp>
|
#include <columnlynx/common/panic_handler.hpp>
|
||||||
#include <columnlynx/server/net/tcp/tcp_server.hpp>
|
#include <columnlynx/server/net/tcp/tcp_server.hpp>
|
||||||
@@ -15,6 +16,11 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <cxxopts.hpp>
|
#include <cxxopts.hpp>
|
||||||
#include <columnlynx/common/net/virtual_interface.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 asio::ip::tcp;
|
||||||
using namespace ColumnLynx::Utils;
|
using namespace ColumnLynx::Utils;
|
||||||
@@ -37,7 +43,12 @@ int main(int argc, char** argv) {
|
|||||||
#else
|
#else
|
||||||
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
|
("i,interface", "Override used interface", cxxopts::value<std::string>()->default_value("lynx0"))
|
||||||
#endif
|
#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();
|
PanicHandler::init();
|
||||||
|
|
||||||
@@ -46,7 +57,7 @@ int main(int argc, char** argv) {
|
|||||||
if (optionsObj.count("help")) {
|
if (optionsObj.count("help")) {
|
||||||
std::cout << options.help() << std::endl;
|
std::cout << options.help() << std::endl;
|
||||||
std::cout << "This software is licensed under the GPLv2-only license OR the GPLv3 license.\n";
|
std::cout << "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";
|
std::cout << "This software is provided under ABSOLUTELY NO WARRANTY, to the extent permitted by law.\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -60,11 +71,40 @@ int main(int argc, char** argv) {
|
|||||||
//WintunInitialize();
|
//WintunInitialize();
|
||||||
#endif
|
#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>());
|
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>());
|
||||||
log("Using virtual interface: " + tun->getName());
|
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)
|
// 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>();
|
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
||||||
|
|
||||||
@@ -75,33 +115,62 @@ int main(int argc, char** argv) {
|
|||||||
log("Loading keypair from config file.");
|
log("Loading keypair from config file.");
|
||||||
|
|
||||||
PublicKey pk;
|
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());
|
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 {
|
} else {
|
||||||
|
#if defined(DEBUG)
|
||||||
warn("No keypair found in config file! Using random key.");
|
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));
|
||||||
|
|
||||||
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;
|
asio::io_context io;
|
||||||
|
|
||||||
auto server = std::make_shared<TCPServer>(io, serverPort(), sodiumWrapper, hostRunning, ipv4Only);
|
auto server = std::make_shared<TCPServer>(io, serverPort());
|
||||||
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), hostRunning, ipv4Only, tun);
|
auto udpServer = std::make_shared<UDPServer>(io, serverPort());
|
||||||
|
|
||||||
|
// Schedule periodic cleanup of expired sessions every 5 minutes
|
||||||
|
auto cleanupTimer = std::make_shared<asio::steady_timer>(io);
|
||||||
|
auto cleanupHandler = std::make_shared<std::function<void(const asio::error_code&)>>();
|
||||||
|
*cleanupHandler = [cleanupTimer, cleanupHandler](const asio::error_code& ec) {
|
||||||
|
if (ec == asio::error::operation_aborted) return; // Timer cancelled
|
||||||
|
try {
|
||||||
|
SessionRegistry::getInstance().cleanupExpired();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
Utils::warn(std::string("SessionRegistry::cleanupExpired() threw: ") + e.what());
|
||||||
|
}
|
||||||
|
cleanupTimer->expires_after(std::chrono::minutes(5));
|
||||||
|
cleanupTimer->async_wait(*cleanupHandler);
|
||||||
|
};
|
||||||
|
cleanupTimer->expires_after(std::chrono::minutes(5));
|
||||||
|
cleanupTimer->async_wait(*cleanupHandler);
|
||||||
|
|
||||||
asio::signal_set signals(io, SIGINT, SIGTERM);
|
asio::signal_set signals(io, SIGINT, SIGTERM);
|
||||||
signals.async_wait([&](const std::error_code&, int) {
|
signals.async_wait([&](const std::error_code&, int) {
|
||||||
log("Received termination signal. Shutting down server gracefully.");
|
log("Received termination signal. Shutting down server gracefully.");
|
||||||
done = 1;
|
done = 1;
|
||||||
asio::post(io, [&]() {
|
asio::post(io, [&]() {
|
||||||
*hostRunning = false;
|
ServerSession::getInstance().setHostRunning(false);
|
||||||
server->stop();
|
server->stop();
|
||||||
udpServer->stop();
|
udpServer->stop();
|
||||||
|
// Cancel cleanup timer
|
||||||
|
cleanupTimer->cancel();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,16 +191,49 @@ int main(int argc, char** argv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t* ip = packet.data();
|
if (packet.size() < 20) {
|
||||||
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset in IPv6-mapped header
|
Utils::warn("TUN: Dropping packet smaller than IPv4 header (" + std::to_string(packet.size()) + " bytes)");
|
||||||
|
|
||||||
auto session = SessionRegistry::getInstance().getByIP(dstIP);
|
|
||||||
if (!session) {
|
|
||||||
Utils::warn("TUN: No session found for destination IP " + VirtualInterface::ipv4ToString(dstIP));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
udpServer->sendData(session->sessionID, std::string(packet.begin(), packet.end()));
|
const uint8_t* ip = packet.data();
|
||||||
|
uint8_t ipVersion = (ip[0] >> 4);
|
||||||
|
if (ipVersion != 4) {
|
||||||
|
Utils::debug("TUN: Non-IPv4 packet received (version=" + std::to_string(ipVersion) + "), skipping server IPv4 routing path.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t srcIPNet = 0;
|
||||||
|
uint32_t dstIPNet = 0;
|
||||||
|
std::memcpy(&srcIPNet, ip + 12, sizeof(srcIPNet)); // IPv4 source address offset
|
||||||
|
std::memcpy(&dstIPNet, ip + 16, sizeof(dstIPNet)); // IPv4 destination address offset
|
||||||
|
uint32_t srcIP = ntohl(srcIPNet);
|
||||||
|
uint32_t dstIP = ntohl(dstIPNet);
|
||||||
|
|
||||||
|
// 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, enforce MTU and forward to that client's session
|
||||||
|
const size_t MTU = 1420; // Enforce configured MTU; TODO: read from server config
|
||||||
|
if (packet.size() > MTU) {
|
||||||
|
Utils::warn("TUN: Dropping oversized packet (" + std::to_string(packet.size()) + " > MTU " + std::to_string(MTU) + ")");
|
||||||
|
} else {
|
||||||
|
udpServer->sendData(dstSession->sessionID, std::string(packet.begin(), packet.end()));
|
||||||
|
}
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Shutting down server...");
|
log("Shutting down server...");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_connection.cpp - TCP Connection for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/server/net/tcp/tcp_connection.hpp>
|
||||||
@@ -14,23 +14,31 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
Utils::warn("Failed to get remote endpoint: " + std::string(e.what()));
|
Utils::warn("Failed to get remote endpoint: " + std::string(e.what()));
|
||||||
}
|
}
|
||||||
|
|
||||||
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
|
mHandler->onMessage([weakSelf = weak_from_this()](AnyMessageType type, const std::string& data) {
|
||||||
mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
|
if (auto self = weakSelf.lock()) {
|
||||||
|
self->mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mHandler->onDisconnect([this](const asio::error_code& ec) {
|
mHandler->onDisconnect([weakSelf = weak_from_this()](const asio::error_code& ec) {
|
||||||
|
auto self = weakSelf.lock();
|
||||||
|
if (!self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Peer has closed; finalize locally without sending RST
|
// Peer has closed; finalize locally without sending RST
|
||||||
Utils::log("Client disconnected: " + mRemoteIP + " - " + ec.message());
|
Utils::log("Client disconnected: " + self->mRemoteIP + " - " + ec.message());
|
||||||
asio::error_code ec2;
|
asio::error_code ec2;
|
||||||
mHandler->socket().close(ec2);
|
if (self->mHandler) {
|
||||||
|
self->mHandler->socket().close(ec2);
|
||||||
|
}
|
||||||
|
|
||||||
SessionRegistry::getInstance().erase(mConnectionSessionID);
|
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
|
||||||
SessionRegistry::getInstance().deallocIP(mConnectionSessionID);
|
SessionRegistry::getInstance().deallocIP(self->mConnectionSessionID);
|
||||||
|
|
||||||
Utils::log("Closed connection to " + mRemoteIP);
|
Utils::log("Closed connection to " + self->mRemoteIP);
|
||||||
|
|
||||||
if (mOnDisconnect) {
|
if (self->mOnDisconnect) {
|
||||||
mOnDisconnect(shared_from_this());
|
self->mOnDisconnect(self);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
Utils::log("Initiated graceful disconnect (half-close) to " + mRemoteIP);
|
Utils::log("Initiated graceful disconnect (half-close) to " + mRemoteIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t TCPConnection::getSessionID() const {
|
uint32_t TCPConnection::getSessionID() const {
|
||||||
return mConnectionSessionID;
|
return mConnectionSessionID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +85,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
void TCPConnection::mStartHeartbeat() {
|
void TCPConnection::mStartHeartbeat() {
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
|
mHeartbeatTimer.expires_after(std::chrono::seconds(5));
|
||||||
mHeartbeatTimer.async_wait([this, self](const asio::error_code& ec) {
|
mHeartbeatTimer.async_wait([self](const asio::error_code& ec) {
|
||||||
if (ec == asio::error::operation_aborted) {
|
if (ec == asio::error::operation_aborted) {
|
||||||
return; // Timer was cancelled
|
return; // Timer was cancelled
|
||||||
}
|
}
|
||||||
@@ -90,10 +98,13 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// Remove socket forcefully, client is dead
|
// Remove socket forcefully, client is dead
|
||||||
asio::error_code ec;
|
asio::error_code ec;
|
||||||
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
|
if (self->mHandler) {
|
||||||
mHandler->socket().close(ec);
|
self->mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
|
||||||
|
self->mHandler->socket().close(ec);
|
||||||
|
}
|
||||||
|
|
||||||
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
|
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
|
||||||
|
SessionRegistry::getInstance().deallocIP(self->mConnectionSessionID);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,8 +124,8 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
case ClientMessageType::HANDSHAKE_INIT: {
|
case ClientMessageType::HANDSHAKE_INIT: {
|
||||||
Utils::log("Received HANDSHAKE_INIT from " + reqAddr);
|
Utils::log("Received HANDSHAKE_INIT from " + reqAddr);
|
||||||
|
|
||||||
if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) {
|
if (data.size() != 1 + crypto_sign_PUBLICKEYBYTES) {
|
||||||
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short.");
|
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " has invalid size: " + std::to_string(data.size()));
|
||||||
disconnect();
|
disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -130,7 +141,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + ".");
|
Utils::log("Client protocol version " + std::to_string(clientProtoVer) + " accepted from " + reqAddr + ".");
|
||||||
|
|
||||||
PublicKey signPk;
|
PublicKey signPk;
|
||||||
std::memcpy(signPk.data(), data.data() + 1, std::min(data.size() - 1, sizeof(signPk)));
|
std::memcpy(signPk.data(), data.data() + 1, sizeof(signPk));
|
||||||
|
|
||||||
// We can safely store this without further checking, the client will need to send the encrypted AES key in a way where they must possess the corresponding private key anyways.
|
// We can safely store this without further checking, the client will need to send the encrypted AES key in a way where they must possess the corresponding private key anyways.
|
||||||
int r = crypto_sign_ed25519_pk_to_curve25519(mConnectionPublicKey.data(), signPk.data()); // Store the client's public encryption key key (for identification)
|
int r = crypto_sign_ed25519_pk_to_curve25519(mConnectionPublicKey.data(), signPk.data()); // Store the client's public encryption key key (for identification)
|
||||||
@@ -145,7 +156,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
Utils::debug("Key attempted connect: " + Utils::bytesToHexString(signPk.data(), signPk.size()));
|
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()) {
|
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);
|
Utils::warn("Non-whitelisted client attempted to connect, terminating. Client IP: " + reqAddr);
|
||||||
@@ -156,20 +167,26 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
Utils::debug("Client " + reqAddr + " passed authorized_keys");
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case ClientMessageType::HANDSHAKE_CHALLENGE: {
|
case ClientMessageType::HANDSHAKE_CHALLENGE: {
|
||||||
Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr);
|
Utils::log("Received HANDSHAKE_CHALLENGE from " + reqAddr);
|
||||||
|
|
||||||
// Convert to byte array
|
// Convert to byte array - require exact size
|
||||||
|
if (data.size() != 32) {
|
||||||
|
Utils::warn("HANDSHAKE_CHALLENGE has invalid size: " + std::to_string(data.size()));
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t challengeData[32];
|
uint8_t challengeData[32];
|
||||||
std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData)));
|
std::memcpy(challengeData, data.data(), sizeof(challengeData));
|
||||||
|
|
||||||
// Sign the challenge
|
// Sign the challenge
|
||||||
Signature sig = Utils::LibSodiumWrapper::signMessage(
|
Signature sig = Utils::LibSodiumWrapper::signMessage(
|
||||||
challengeData, sizeof(challengeData),
|
challengeData, sizeof(challengeData),
|
||||||
mLibSodiumWrapper->getPrivateKey()
|
ServerSession::getInstance().getSodiumWrapper()->getPrivateKey()
|
||||||
);
|
);
|
||||||
|
|
||||||
mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response
|
mHandler->sendMessage(ServerMessageType::HANDSHAKE_CHALLENGE_RESPONSE, Utils::uint8ArrayToString(sig.data(), sig.size())); // Placeholder response
|
||||||
@@ -191,8 +208,8 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size());
|
std::memcpy(ciphertext.data(), data.data() + nonce.size(), ciphertext.size());
|
||||||
try {
|
try {
|
||||||
std::array<uint8_t, 32> arrayPrivateKey;
|
std::array<uint8_t, 32> arrayPrivateKey;
|
||||||
std::copy(mLibSodiumWrapper->getXPrivateKey(),
|
std::copy(ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey(),
|
||||||
mLibSodiumWrapper->getXPrivateKey() + 32,
|
ServerSession::getInstance().getSodiumWrapper()->getXPrivateKey() + 32,
|
||||||
arrayPrivateKey.begin());
|
arrayPrivateKey.begin());
|
||||||
|
|
||||||
// Decrypt the AES key using the client's public key and server's private key
|
// Decrypt the AES key using the client's public key and server's private key
|
||||||
@@ -211,14 +228,18 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
std::memcpy(mConnectionAESKey.data(), decrypted.data(), decrypted.size());
|
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)
|
||||||
randombytes_buf(&mConnectionSessionID, sizeof(mConnectionSessionID));
|
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)
|
// Encrypt the Session ID with the established AES key (using symmetric encryption, nonce can be all zeros for this purpose)
|
||||||
Nonce symNonce{}; // All zeros
|
Nonce symNonce{}; // All zeros
|
||||||
|
|
||||||
std::string networkString = mRawServerConfig->find("NETWORK")->second; // The load check guarantees that this value exists
|
const auto& serverConfig = ServerSession::getInstance().getRawServerConfig();
|
||||||
uint8_t configMask = std::stoi(mRawServerConfig->find("SUBNET_MASK")->second); // Same deal here
|
|
||||||
|
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);
|
uint32_t baseIP = Net::VirtualInterface::stringToIpv4(networkString);
|
||||||
|
|
||||||
@@ -245,11 +266,11 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
tunConfig.dns1 = htonl(0x08080808); // 8.8.8.8
|
tunConfig.dns1 = htonl(0x08080808); // 8.8.8.8
|
||||||
tunConfig.dns2 = 0;
|
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::vector<uint8_t> payload(sizeof(uint32_t) + sizeof(tunConfig));
|
||||||
std::memcpy(payload.data(), &sessionIDNet, sizeof(uint64_t));
|
std::memcpy(payload.data(), &sessionIDNet, sizeof(uint32_t));
|
||||||
std::memcpy(payload.data() + sizeof(uint64_t), &tunConfig, sizeof(tunConfig));
|
std::memcpy(payload.data() + sizeof(uint32_t), &tunConfig, sizeof(tunConfig));
|
||||||
|
|
||||||
std::vector<uint8_t> encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
|
std::vector<uint8_t> encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
|
||||||
payload.data(), payload.size(),
|
payload.data(), payload.size(),
|
||||||
@@ -261,6 +282,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
// Add to session registry
|
// Add to session registry
|
||||||
Utils::log("Handshake with " + reqAddr + " completed successfully. Session ID assigned (" + std::to_string(mConnectionSessionID) + ").");
|
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);
|
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().put(mConnectionSessionID, std::move(session));
|
||||||
SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP);
|
SessionRegistry::getInstance().lockIP(mConnectionSessionID, clientIP);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// tcp_server.cpp - TCP Server for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/server/net/tcp/tcp_server.hpp>
|
||||||
@@ -27,15 +27,13 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
}
|
}
|
||||||
Utils::error("Accept failed: " + ec.message());
|
Utils::error("Accept failed: " + ec.message());
|
||||||
// Try again only if still running
|
// Try again only if still running
|
||||||
if (mHostRunning && *mHostRunning && mAcceptor.is_open())
|
if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open())
|
||||||
mStartAccept();
|
mStartAccept();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto client = TCPConnection::create(
|
auto client = TCPConnection::create(
|
||||||
std::move(socket),
|
std::move(socket),
|
||||||
mSodiumWrapper,
|
|
||||||
&mRawServerConfig,
|
|
||||||
[this](std::shared_ptr<TCPConnection> c) {
|
[this](std::shared_ptr<TCPConnection> c) {
|
||||||
mClients.erase(c);
|
mClients.erase(c);
|
||||||
Utils::log("Client removed.");
|
Utils::log("Client removed.");
|
||||||
@@ -45,7 +43,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
client->start();
|
client->start();
|
||||||
Utils::log("Accepted new client connection.");
|
Utils::log("Accepted new client connection.");
|
||||||
|
|
||||||
if (mHostRunning && *mHostRunning && mAcceptor.is_open())
|
if (ServerSession::getInstance().isHostRunning() && mAcceptor.is_open())
|
||||||
mStartAccept();
|
mStartAccept();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// udp_server.cpp - UDP Server for ColumnLynx
|
// 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.
|
// 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>
|
#include <columnlynx/server/net/udp/udp_server.hpp>
|
||||||
@@ -16,26 +16,27 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
if (ec) {
|
if (ec) {
|
||||||
if (ec == asio::error::operation_aborted) return; // Socket closed
|
if (ec == asio::error::operation_aborted) return; // Socket closed
|
||||||
// Other recv error
|
// Other recv error
|
||||||
if (mHostRunning && *mHostRunning) mStartReceive();
|
if (ServerSession::getInstance().isHostRunning()) mStartReceive();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bytes > 0) mHandlePacket(bytes);
|
if (bytes > 0) mHandlePacket(bytes);
|
||||||
if (mHostRunning && *mHostRunning) mStartReceive();
|
if (ServerSession::getInstance().isHostRunning()) mStartReceive();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDPServer::mHandlePacket(std::size_t bytes) {
|
void UDPServer::mHandlePacket(std::size_t bytes) {
|
||||||
if (bytes < sizeof(UDPPacketHeader))
|
if (bytes < sizeof(UDPPacketHeader) + sizeof(uint32_t))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto* hdr = reinterpret_cast<UDPPacketHeader*>(mRecvBuffer.data());
|
const auto* hdr = reinterpret_cast<UDPPacketHeader*>(mRecvBuffer.data());
|
||||||
|
|
||||||
// Get plaintext session ID (assuming first 8 bytes after nonce (header))
|
// Get plaintext session ID (first 4 bytes after header, in network byte order)
|
||||||
uint64_t sessionID = 0;
|
uint32_t sessionIDNet = 0;
|
||||||
std::memcpy(&sessionID, mRecvBuffer.data() + sizeof(UDPPacketHeader), sizeof(uint64_t));
|
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);
|
std::vector<uint8_t> encryptedPayload(it, mRecvBuffer.begin() + bytes);
|
||||||
|
|
||||||
// Get associated session state
|
// Get associated session state
|
||||||
@@ -54,7 +55,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
encryptedPayload.data(), encryptedPayload.size(),
|
encryptedPayload.data(), encryptedPayload.size(),
|
||||||
session->aesKey,
|
session->aesKey,
|
||||||
hdr->nonce, "udp-data"
|
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");
|
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
|
// Find the IPv4/IPv6 endpoint for the session
|
||||||
std::shared_ptr<const SessionState> session = SessionRegistry::getInstance().get(sessionID);
|
std::shared_ptr<const SessionState> session = SessionRegistry::getInstance().get(sessionID);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -92,23 +93,45 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
|
|
||||||
// Prepare packet
|
// Prepare packet
|
||||||
UDPPacketHeader hdr{};
|
UDPPacketHeader hdr{};
|
||||||
randombytes_buf(hdr.nonce.data(), hdr.nonce.size());
|
uint8_t nonce[12];
|
||||||
|
uint32_t prefix = session->noncePrefix;
|
||||||
|
// Increment send counter with overflow protection
|
||||||
|
uint64_t sendCount = 0;
|
||||||
|
{
|
||||||
|
auto ptr = const_cast<SessionState*>(session.get());
|
||||||
|
uint64_t old = ptr->send_ctr.load(std::memory_order_relaxed);
|
||||||
|
for (;;) {
|
||||||
|
if (old == std::numeric_limits<uint64_t>::max()) {
|
||||||
|
Utils::error("UDP: send counter overflow for session " + std::to_string(sessionID));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ptr->send_ctr.compare_exchange_weak(old, old + 1, std::memory_order_relaxed)) {
|
||||||
|
sendCount = old;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// old updated by compare_exchange_weak, loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(
|
auto encryptedPayload = Utils::LibSodiumWrapper::encryptMessage(
|
||||||
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
|
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
|
||||||
session->aesKey, hdr.nonce, "udp-data"
|
session->aesKey, hdr.nonce, "udp-data"
|
||||||
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint64_t))
|
//std::string(reinterpret_cast<const char*>(&sessionID), sizeof(uint32_t))
|
||||||
);
|
);
|
||||||
|
|
||||||
std::vector<uint8_t> packet;
|
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(),
|
packet.insert(packet.end(),
|
||||||
reinterpret_cast<uint8_t*>(&hdr),
|
reinterpret_cast<uint8_t*>(&hdr),
|
||||||
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
|
reinterpret_cast<uint8_t*>(&hdr) + sizeof(UDPPacketHeader)
|
||||||
);
|
);
|
||||||
|
uint32_t sessionIDNet = htonl(sessionID);
|
||||||
packet.insert(packet.end(),
|
packet.insert(packet.end(),
|
||||||
reinterpret_cast<const uint8_t*>(&sessionID),
|
reinterpret_cast<const uint8_t*>(&sessionIDNet),
|
||||||
reinterpret_cast<const uint8_t*>(&sessionID) + sizeof(sessionID)
|
reinterpret_cast<const uint8_t*>(&sessionIDNet) + sizeof(sessionIDNet)
|
||||||
);
|
);
|
||||||
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
|
packet.insert(packet.end(), encryptedPayload.begin(), encryptedPayload.end());
|
||||||
|
|
||||||
|
|||||||
92
src/server/server_session.cpp
Normal file
92
src/server/server_session.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
tests/test_libsodium_wrapper.cpp
Normal file
42
tests/test_libsodium_wrapper.cpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Tests for LibSodiumWrapper: random, symmetric encrypt/decrypt, sign/verify
|
||||||
|
#include <iostream>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace ColumnLynx::Utils;
|
||||||
|
|
||||||
|
// Random bytes uniqueness
|
||||||
|
auto a = LibSodiumWrapper::generateRandom256Bit();
|
||||||
|
auto b = LibSodiumWrapper::generateRandom256Bit();
|
||||||
|
assert(a != b && "generateRandom256Bit() should produce different outputs (very likely)");
|
||||||
|
|
||||||
|
// Symmetric encrypt/decrypt roundtrip
|
||||||
|
ColumnLynx::SymmetricKey key = {};
|
||||||
|
for (size_t i = 0; i < key.size(); ++i) key[i] = static_cast<uint8_t>(i);
|
||||||
|
auto nonce = LibSodiumWrapper::generateNonce();
|
||||||
|
|
||||||
|
std::string plaintext = "The quick brown fox jumps over the lazy dog";
|
||||||
|
auto ct = LibSodiumWrapper::encryptMessage(reinterpret_cast<const uint8_t*>(plaintext.data()), plaintext.size(), key, nonce, "aad");
|
||||||
|
auto pt = LibSodiumWrapper::decryptMessage(ct.data(), ct.size(), key, nonce, "aad");
|
||||||
|
std::string recovered(pt.begin(), pt.end());
|
||||||
|
assert(recovered == plaintext && "decrypt should recover original plaintext");
|
||||||
|
|
||||||
|
// Sign and verify
|
||||||
|
ColumnLynx::PrivateKey sk{}; ColumnLynx::PublicKey pk{};
|
||||||
|
randombytes_buf(sk.data(), sk.size());
|
||||||
|
// naive keypair generation for test purposes: use libsodium functions via wrapper
|
||||||
|
// generate a real keypair using crypto_sign
|
||||||
|
if (crypto_sign_keypair(pk.data(), sk.data()) != 0) {
|
||||||
|
std::cerr << "Failed to generate keypair\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sig = LibSodiumWrapper::signMessage(plaintext, sk);
|
||||||
|
bool ok = LibSodiumWrapper::verifyMessage(plaintext, sig, pk);
|
||||||
|
assert(ok && "Signature should verify");
|
||||||
|
|
||||||
|
std::cout << "LibSodiumWrapper tests passed\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
49
tests/test_session_registry.cpp
Normal file
49
tests/test_session_registry.cpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Simple unit tests for SessionRegistry
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include <columnlynx/common/net/session_registry.hpp>
|
||||||
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace ColumnLynx::Net;
|
||||||
|
using namespace ColumnLynx::Utils;
|
||||||
|
|
||||||
|
auto ® = SessionRegistry::getInstance();
|
||||||
|
|
||||||
|
// Use a unique session id to avoid colliding with any running instance
|
||||||
|
const uint32_t sid = 0xDEADBEEF;
|
||||||
|
// ensure clean state
|
||||||
|
reg.erase(sid);
|
||||||
|
|
||||||
|
auto key = LibSodiumWrapper::generateRandom256Bit();
|
||||||
|
auto state = std::make_shared<SessionState>(key, std::chrono::hours(24), 0xC0A80101 /*192.168.1.1*/, 0, sid);
|
||||||
|
|
||||||
|
reg.put(sid, state);
|
||||||
|
|
||||||
|
assert(reg.exists(sid) && "Session should exist after put()");
|
||||||
|
|
||||||
|
auto got = reg.get(sid);
|
||||||
|
assert(got && got->sessionID == sid && "get() should return stored session");
|
||||||
|
|
||||||
|
auto byip = reg.getByIP(0xC0A80101);
|
||||||
|
assert(byip && byip->sessionID == sid && "getByIP() should find session by client IP");
|
||||||
|
|
||||||
|
// Erase and verify removed
|
||||||
|
reg.erase(sid);
|
||||||
|
assert(!reg.exists(sid) && "Session should not exist after erase()");
|
||||||
|
assert(reg.getByIP(0xC0A80101) == nullptr && "getByIP() should return nullptr after erase");
|
||||||
|
|
||||||
|
// Test cleanupExpired: insert an already-expired session
|
||||||
|
const uint32_t sid2 = 0xFEEDBEEF;
|
||||||
|
reg.erase(sid2);
|
||||||
|
auto expiredState = std::make_shared<SessionState>(key, std::chrono::seconds(0), 0xC0A80102, 0, sid2);
|
||||||
|
reg.put(sid2, expiredState);
|
||||||
|
// Force cleanup
|
||||||
|
reg.cleanupExpired();
|
||||||
|
assert(!reg.exists(sid2) && "Expired session should be removed by cleanupExpired()");
|
||||||
|
|
||||||
|
std::cout << "SessionRegistry tests passed\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
43
tests/test_session_registry_ip.cpp
Normal file
43
tests/test_session_registry_ip.cpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Tests for SessionRegistry IP allocation and lock/dealloc
|
||||||
|
#include <iostream>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include <columnlynx/common/net/session_registry.hpp>
|
||||||
|
#include <columnlynx/common/libsodium_wrapper.hpp>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace ColumnLynx::Net;
|
||||||
|
using namespace ColumnLynx::Utils;
|
||||||
|
|
||||||
|
auto ® = SessionRegistry::getInstance();
|
||||||
|
|
||||||
|
const uint32_t sid = 0xABCDEF01;
|
||||||
|
reg.erase(sid);
|
||||||
|
|
||||||
|
auto key = LibSodiumWrapper::generateRandom256Bit();
|
||||||
|
auto state = std::make_shared<SessionState>(key, std::chrono::hours(24), 0, 0, sid);
|
||||||
|
reg.put(sid, state);
|
||||||
|
|
||||||
|
// Lock IP
|
||||||
|
uint32_t ip = 0xC0A80201; // 192.168.2.1
|
||||||
|
reg.lockIP(sid, ip);
|
||||||
|
|
||||||
|
auto byip = reg.getByIP(ip);
|
||||||
|
assert(byip && byip->sessionID == sid && "lockIP should populate mIPSessions");
|
||||||
|
|
||||||
|
// deallocIP
|
||||||
|
reg.deallocIP(sid);
|
||||||
|
assert(reg.getByIP(ip) == nullptr && "deallocIP should remove mapping");
|
||||||
|
|
||||||
|
// getFirstAvailableIP: choose a small /30 range to limit hosts
|
||||||
|
uint32_t base = 0x0A000000; // 10.0.0.0
|
||||||
|
uint8_t mask = 30; // 2 usable hosts
|
||||||
|
uint32_t first = reg.getFirstAvailableIP(base, mask);
|
||||||
|
assert(first != 0 && "Should find available IP in empty registry");
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
reg.erase(sid);
|
||||||
|
|
||||||
|
std::cout << "SessionRegistry IP tests passed\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
30
tests/test_tcp_message_handler_static.cpp
Normal file
30
tests/test_tcp_message_handler_static.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Tests for TCP MessageHandler static helpers
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <columnlynx/common/net/tcp/tcp_message_handler.hpp>
|
||||||
|
#include <columnlynx/common/net/tcp/tcp_message_type.hpp>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace ColumnLynx::Net::TCP;
|
||||||
|
|
||||||
|
// server message special codes
|
||||||
|
auto t1 = MessageHandler::decodeMessageType(0xFE);
|
||||||
|
// Expect GRACEFUL_DISCONNECT mapped
|
||||||
|
// Compare by converting back to uint8
|
||||||
|
assert(MessageHandler::toUint8(t1) == 0xFE);
|
||||||
|
|
||||||
|
auto t2 = MessageHandler::decodeMessageType(0xFF);
|
||||||
|
assert(MessageHandler::toUint8(t2) == 0xFF);
|
||||||
|
|
||||||
|
// Client message range (>= 0xA0)
|
||||||
|
auto t3 = MessageHandler::decodeMessageType(0xA5);
|
||||||
|
assert(MessageHandler::toUint8(t3) == 0xA5);
|
||||||
|
|
||||||
|
// Server message range (< 0xA0) and not special
|
||||||
|
auto t4 = MessageHandler::decodeMessageType(0x10);
|
||||||
|
assert(MessageHandler::toUint8(t4) == 0x10);
|
||||||
|
|
||||||
|
std::cout << "TCP MessageHandler static helpers tests passed\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user