Compare commits
17 Commits
dev
...
31cb3d0a02
| Author | SHA1 | Date | |
|---|---|---|---|
| 31cb3d0a02 | |||
| 0299b03d9c | |||
| 204f89006f | |||
| 57d260976c | |||
| 4fa26d51d0 | |||
| e1118ccafe | |||
| 00f72e1a64 | |||
| 3cd99243ad | |||
| 8f536abe77 | |||
| 3eadd41a00 | |||
| 714aa52f98 | |||
| a2ecc589f8 | |||
| 640a751f9b | |||
| a08dba5b59 | |||
| 4ba59fb23f | |||
| 9e5e728438 | |||
| d20bee9e60 |
39
.github/workflows/sanitizers.yml
vendored
39
.github/workflows/sanitizers.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
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,6 +12,5 @@ _deps
|
|||||||
CMakeUserPresets.json
|
CMakeUserPresets.json
|
||||||
|
|
||||||
build/
|
build/
|
||||||
build*/
|
|
||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
# If MAJOR is 0, and MINOR > 0, Version is BETA
|
# If MAJOR is 0, and MINOR > 0, Version is BETA
|
||||||
|
|
||||||
project(ColumnLynx
|
project(ColumnLynx
|
||||||
VERSION 1.2.0
|
VERSION 1.1.0
|
||||||
LANGUAGES CXX
|
LANGUAGES CXX
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,28 +173,3 @@ install(FILES
|
|||||||
LICENSES/GPL-2.0-only.txt
|
LICENSES/GPL-2.0-only.txt
|
||||||
LICENSES/GPL-3.0.txt
|
LICENSES/GPL-3.0.txt
|
||||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME})
|
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()
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -55,22 +55,7 @@ openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32
|
|||||||
# Output example: 1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
|
# Output example: 1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
|
||||||
```
|
```
|
||||||
|
|
||||||
Write the hex output into two separate files in the matching config directory:
|
You can then set these keys accordingly in the **server_config** and **client_config** files.
|
||||||
|
|
||||||
- `public.key` for the public key
|
|
||||||
- `private.key` for the private key seed
|
|
||||||
|
|
||||||
The files should contain only the hex ASCII characters, optionally followed by a trailing newline. Hex parsing is case-insensitive.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
printf '%s' '1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e' > /etc/columnlynx/public.key
|
|
||||||
printf '%s' '9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e' > /etc/columnlynx/private.key
|
|
||||||
chmod 600 /etc/columnlynx/private.key
|
|
||||||
```
|
|
||||||
|
|
||||||
On Unix-like systems, the software will warn if the private key file is too permissive and recommend tightening it with `chmod 600`.
|
|
||||||
|
|
||||||
### Server Setup (Linux Server ONLY)
|
### Server Setup (Linux Server ONLY)
|
||||||
|
|
||||||
@@ -172,19 +157,20 @@ sudo nft add rule nat postroute ip saddr 10.10.0.0/24 oifname "eth0" masquerade
|
|||||||
|
|
||||||
"**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 - Used for verification
|
||||||
|
- **SERVER_PRIVATE_KEY** (Hex String): The private key seed to be used
|
||||||
- **NETWORK** (IPv4 Format): The network IPv4 to be used (Server Interface still needs to be configured manually)
|
- **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=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
|
||||||
|
SERVER_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
|
||||||
NETWORK=10.10.0.0
|
NETWORK=10.10.0.0
|
||||||
SUBNET_MASK=24
|
SUBNET_MASK=24
|
||||||
```
|
```
|
||||||
|
|
||||||
The server keypair must now live in the same directory as `server_config`, stored in `public.key` and `private.key`.
|
|
||||||
`server_config` no longer stores key material.
|
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
"**whitelisted_keys**" is a file that **public keys of clients that are allowed to connect to the server, one key per line**.
|
"**whitelisted_keys**" is a file that **public keys of clients that are allowed to connect to the server, one key per line**.
|
||||||
@@ -198,9 +184,17 @@ The server keypair must now live in the same directory as `server_config`, store
|
|||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|
||||||
"**client_config**" is a file that contains the client configuration, **one variable per line**. Key material is no longer stored here; if you do not have any client-only settings yet, this file can stay empty.
|
"**client_config**" is a file that contains the client configuration, **one variable per line**. These are the current configuration available variables:
|
||||||
|
|
||||||
The client keypair must now live in the same directory as `client_config`, stored in `public.key` and `private.key`.
|
- **CLIENT_PUBLIC_KEY** (Hex String): The public key to be used - Used for verification
|
||||||
|
- **CLIENT_PRIVATE_KEY** (Hex String): The private key seed to be used
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
CLIENT_PUBLIC_KEY=1c9d4f7a3b2e8a6d0f5c9b1e4d8a7f3c6e2b1a9d5f4c8e0a7b3d6c9f2e
|
||||||
|
CLIENT_PRIVATE_KEY=9f3a2b6c0f8e4d1a7c3e9a4b5d2f8c6e1a9d0b7e3f4c2a8e6d5b1f0a3c4e
|
||||||
|
```
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,6 @@ namespace ColumnLynx {
|
|||||||
|
|
||||||
void incrementSendCount() {
|
void incrementSendCount() {
|
||||||
std::unique_lock lock(mMutex);
|
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++;
|
mClientState->send_cnt++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
#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 <string>
|
#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>
|
||||||
@@ -26,7 +26,48 @@ 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)
|
||||||
|
:
|
||||||
|
mResolver(ioContext),
|
||||||
|
mSocket(ioContext),
|
||||||
|
mHost(host),
|
||||||
|
mPort(port),
|
||||||
|
mHeartbeatTimer(mSocket.get_executor()),
|
||||||
|
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
|
||||||
|
mLastHeartbeatSent(std::chrono::steady_clock::now())
|
||||||
|
{
|
||||||
|
// Get initial client config
|
||||||
|
std::string configPath = ClientSession::getInstance().getConfigPath();
|
||||||
|
std::shared_ptr<Utils::LibSodiumWrapper> mLibSodiumWrapper = ClientSession::getInstance().getSodiumWrapper();
|
||||||
|
|
||||||
|
// Preload the config map
|
||||||
|
mRawClientConfig = Utils::getConfigMap(configPath + "client_config");
|
||||||
|
|
||||||
|
auto itPubkey = mRawClientConfig.find("CLIENT_PUBLIC_KEY");
|
||||||
|
auto itPrivkey = mRawClientConfig.find("CLIENT_PRIVATE_KEY");
|
||||||
|
|
||||||
|
if (itPubkey != mRawClientConfig.end() && itPrivkey != mRawClientConfig.end()) {
|
||||||
|
Utils::log("Loading keypair from config file.");
|
||||||
|
|
||||||
|
PublicKey pk;
|
||||||
|
PrivateSeed seed;
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
if (!mLibSodiumWrapper->recomputeKeys(seed, pk)) {
|
||||||
|
throw std::runtime_error("Failed to recompute keypair from config file values!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::debug("Newly-Loaded Public Key: " + Utils::bytesToHexString(mLibSodiumWrapper->getPublicKey(), 32));
|
||||||
|
} else {
|
||||||
|
#if defined(DEBUG)
|
||||||
|
Utils::warn("No keypair found in config file! Using random key.");
|
||||||
|
#else
|
||||||
|
throw std::runtime_error("No keypair found in config file! Cannot start client without keys.");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Starts the TCP Client and initiaties the handshake
|
// Starts the TCP Client and initiaties the handshake
|
||||||
void start();
|
void start();
|
||||||
@@ -48,8 +89,8 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// TODO: Move ptrs to smart ptrs
|
// TODO: Move ptrs to smart ptrs
|
||||||
|
|
||||||
std::atomic<bool> mConnected{false};
|
bool mConnected = false;
|
||||||
std::atomic<bool> mHandshakeComplete{false};
|
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;
|
||||||
@@ -64,5 +105,6 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
int mMissedHeartbeats = 0;
|
int mMissedHeartbeats = 0;
|
||||||
bool mIsHostDomain;
|
bool mIsHostDomain;
|
||||||
Protocol::TunConfig mTunConfig;
|
Protocol::TunConfig mTunConfig;
|
||||||
|
std::unordered_map<std::string, std::string> mRawClientConfig;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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(const asio::error_code&)> mOnDisconnect;
|
std::function<void(asio::error_code&)> mOnDisconnect;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <bits/stdc++.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
@@ -79,15 +79,4 @@ namespace ColumnLynx::Utils {
|
|||||||
|
|
||||||
// Returns the config file in an unordered_map format. This purely reads the config file, you still need to parse it manually.
|
// Returns the config file in an unordered_map format. This purely reads the config file, you still need to parse it manually.
|
||||||
std::unordered_map<std::string, std::string> getConfigMap(std::string path, std::vector<std::string> requiredKeys = {});
|
std::unordered_map<std::string, std::string> getConfigMap(std::string path, std::vector<std::string> requiredKeys = {});
|
||||||
|
|
||||||
// Load a hex-encoded file, validate its byte length, and return the decoded bytes.
|
|
||||||
std::vector<uint8_t> loadHexBytesFromFile(const std::string& path, size_t expectedBytes, const std::string& description = "key", bool warnOnInsecurePermissions = false);
|
|
||||||
|
|
||||||
template <size_t N>
|
|
||||||
inline std::array<uint8_t, N> loadHexArrayFromFile(const std::string& path, const std::string& description = "key", bool warnOnInsecurePermissions = false) {
|
|
||||||
auto bytes = loadHexBytesFromFile(path, N, description, warnOnInsecurePermissions);
|
|
||||||
std::array<uint8_t, N> out{};
|
|
||||||
std::copy_n(bytes.begin(), N, out.begin());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
#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>
|
||||||
@@ -91,20 +90,7 @@ int main(int argc, char** argv) {
|
|||||||
std::string configPath = optionsObj["config-dir"].as<std::string>();
|
std::string configPath = optionsObj["config-dir"].as<std::string>();
|
||||||
const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR");
|
const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR");
|
||||||
if (envConfigPath != nullptr) {
|
if (envConfigPath != nullptr) {
|
||||||
// Validate and canonicalize environment-provided path
|
configPath = std::string(envConfigPath);
|
||||||
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 (configPath.back() != '/' && configPath.back() != '\\') {
|
||||||
@@ -127,28 +113,6 @@ int main(int argc, char** argv) {
|
|||||||
initialState.virtualInterface = tun;
|
initialState.virtualInterface = tun;
|
||||||
|
|
||||||
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
||||||
const std::string clientPublicKeyPath = configPath + "public.key";
|
|
||||||
const std::string clientPrivateKeyPath = configPath + "private.key";
|
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
bool clientKeyFilesPresent = fs::exists(clientPublicKeyPath) && fs::exists(clientPrivateKeyPath);
|
|
||||||
if (clientKeyFilesPresent) {
|
|
||||||
Utils::log("Loading client keypair from key files.");
|
|
||||||
|
|
||||||
PublicKey pk = Utils::loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(clientPublicKeyPath, "client public key");
|
|
||||||
PrivateSeed seed = Utils::loadHexArrayFromFile<crypto_sign_SEEDBYTES>(clientPrivateKeyPath, "client private key", true);
|
|
||||||
|
|
||||||
if (!sodiumWrapper->recomputeKeys(seed, pk)) {
|
|
||||||
throw std::runtime_error("Failed to recompute client keypair from key files!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#if defined(DEBUG)
|
|
||||||
Utils::warn("No client keypair files found! Using random key.");
|
|
||||||
#else
|
|
||||||
throw std::runtime_error("No client keypair files found! Cannot start client without keys.");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
initialState.sodiumWrapper = sodiumWrapper;
|
||||||
|
|||||||
@@ -6,20 +6,6 @@
|
|||||||
//#include <arpa/inet.h>
|
//#include <arpa/inet.h>
|
||||||
|
|
||||||
namespace ColumnLynx::Net::TCP {
|
namespace ColumnLynx::Net::TCP {
|
||||||
TCPClient::TCPClient(asio::io_context& ioContext, const std::string& host, const std::string& port)
|
|
||||||
: mResolver(ioContext),
|
|
||||||
mSocket(ioContext),
|
|
||||||
mHost(host),
|
|
||||||
mPort(port),
|
|
||||||
mHeartbeatTimer(mSocket.get_executor()),
|
|
||||||
mLastHeartbeatReceived(std::chrono::steady_clock::now()),
|
|
||||||
mLastHeartbeatSent(std::chrono::steady_clock::now())
|
|
||||||
{
|
|
||||||
if (!ClientSession::getInstance().getSodiumWrapper()) {
|
|
||||||
throw std::runtime_error("ClientSession sodium wrapper is not initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TCPClient::start() {
|
void TCPClient::start() {
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
mResolver.async_resolve(mHost, mPort,
|
mResolver.async_resolve(mHost, mPort,
|
||||||
@@ -28,25 +14,19 @@ 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.store(true, std::memory_order_relaxed);
|
mConnected = true;
|
||||||
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([weakSelf = weak_from_this()](AnyMessageType type, const std::string& data) {
|
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
|
||||||
if (auto self = weakSelf.lock()) {
|
mHandleMessage(static_cast<ServerMessageType>(MessageHandler::toUint8(type)), data);
|
||||||
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([weakSelf = weak_from_this()](const asio::error_code& ec) {
|
mHandler->onDisconnect([this](const asio::error_code& ec) {
|
||||||
auto self = weakSelf.lock();
|
|
||||||
if (!self) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
asio::error_code ec2;
|
asio::error_code ec2;
|
||||||
if (self->mHandler) {
|
if (mHandler) {
|
||||||
self->mHandler->socket().close(ec2);
|
mHandler->socket().close(ec2);
|
||||||
}
|
}
|
||||||
self->mConnected.store(false, std::memory_order_relaxed);
|
mConnected = false;
|
||||||
Utils::log(std::string("Server disconnected: ") + ec.message());
|
Utils::log(std::string("Server disconnected: ") + ec.message());
|
||||||
});
|
});
|
||||||
mHandler->start();
|
mHandler->start();
|
||||||
@@ -94,7 +74,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.load(std::memory_order_relaxed)) {
|
if (!mConnected) {
|
||||||
Utils::error("Cannot send message, client not connected.");
|
Utils::error("Cannot send message, client not connected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -107,7 +87,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TCPClient::disconnect(bool echo) {
|
void TCPClient::disconnect(bool echo) {
|
||||||
if (mConnected.load(std::memory_order_relaxed) && mHandler) {
|
if (mConnected && mHandler) {
|
||||||
if (echo) {
|
if (echo) {
|
||||||
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
|
mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye");
|
||||||
}
|
}
|
||||||
@@ -127,17 +107,17 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TCPClient::isHandshakeComplete() const {
|
bool TCPClient::isHandshakeComplete() const {
|
||||||
return mHandshakeComplete.load(std::memory_order_relaxed);
|
return mHandshakeComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TCPClient::isConnected() const {
|
bool TCPClient::isConnected() const {
|
||||||
return mConnected.load(std::memory_order_relaxed);
|
return mConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
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([self](const asio::error_code& ec) {
|
mHeartbeatTimer.async_wait([this, 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
|
||||||
}
|
}
|
||||||
@@ -150,11 +130,9 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// Close sockets forcefully, server is dead
|
// Close sockets forcefully, server is dead
|
||||||
asio::error_code ec;
|
asio::error_code ec;
|
||||||
if (self->mHandler) {
|
mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
|
||||||
self->mHandler->socket().shutdown(tcp::socket::shutdown_both, ec);
|
mHandler->socket().close(ec);
|
||||||
self->mHandler->socket().close(ec);
|
mConnected = false;
|
||||||
}
|
|
||||||
self->mConnected.store(false, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros
|
ClientSession::getInstance().setAESKey({}); // Clear AES key with all zeros
|
||||||
ClientSession::getInstance().setSessionID(0);
|
ClientSession::getInstance().setSessionID(0);
|
||||||
@@ -173,13 +151,7 @@ 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: {
|
||||||
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);
|
std::string hexServerPub = Utils::bytesToHexString(mServerPublicKey, 32);
|
||||||
Utils::log("Received server identity. Public Key: " + hexServerPub);
|
Utils::log("Received server identity. Public Key: " + hexServerPub);
|
||||||
|
|
||||||
@@ -208,13 +180,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
{
|
{
|
||||||
// Verify the signature
|
// Verify the signature
|
||||||
Signature sig{};
|
Signature sig{};
|
||||||
if (data.size() != sig.size()) {
|
std::memcpy(sig.data(), data.data(), std::min(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.");
|
||||||
|
|
||||||
@@ -295,7 +261,7 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
|
mTun->configureIP(clientIP, serverIP, prefixLen, mtu);
|
||||||
}
|
}
|
||||||
|
|
||||||
mHandshakeComplete.store(true, std::memory_order_relaxed);
|
mHandshakeComplete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -310,13 +276,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.load(std::memory_order_relaxed)) { // Prevent Recursion
|
if (mConnected) { // 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.load(std::memory_order_relaxed)) {
|
if (mConnected) {
|
||||||
disconnect(false);
|
disconnect(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
// 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() {
|
||||||
@@ -75,7 +73,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
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 sessionID = static_cast<uint32_t>(ClientSession::getInstance().getSessionID());
|
||||||
uint32_t sessionIDNet = htonl(sessionID);
|
uint32_t sessionIDNet = sessionID;
|
||||||
packet.insert(packet.end(),
|
packet.insert(packet.end(),
|
||||||
reinterpret_cast<uint8_t*>(&sessionIDNet),
|
reinterpret_cast<uint8_t*>(&sessionIDNet),
|
||||||
reinterpret_cast<uint8_t*>(&sessionIDNet) + sizeof(uint32_t)
|
reinterpret_cast<uint8_t*>(&sessionIDNet) + sizeof(uint32_t)
|
||||||
@@ -104,12 +102,7 @@ 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
|
||||||
Utils::warn("UDPClient receive error: " + ec.message());
|
mStartReceive();
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,24 +136,9 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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() + headerLen,
|
mRecvBuffer.begin() + sizeof(UDPPacketHeader) + sizeof(uint32_t),
|
||||||
mRecvBuffer.begin() + headerLen + ciphertextLen
|
mRecvBuffer.begin() + bytes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ClientSession::getInstance().getAESKey().empty()) {
|
if (ClientSession::getInstance().getAESKey().empty()) {
|
||||||
|
|||||||
@@ -32,26 +32,7 @@ namespace ColumnLynx::Net {
|
|||||||
|
|
||||||
void SessionRegistry::erase(uint32_t sessionID) {
|
void SessionRegistry::erase(uint32_t sessionID) {
|
||||||
std::unique_lock lock(mMutex);
|
std::unique_lock lock(mMutex);
|
||||||
auto it = mSessions.find(sessionID);
|
mSessions.erase(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() {
|
||||||
@@ -104,15 +85,11 @@ namespace ColumnLynx::Net {
|
|||||||
void SessionRegistry::lockIP(uint32_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;
|
||||||
|
|
||||||
auto it = mSessions.find(sessionID);
|
/*if (mIPSessions.find(sessionID) == mIPSessions.end()) {
|
||||||
if (it == mSessions.end() || !it->second) {
|
Utils::debug("yikes");
|
||||||
Utils::warn("SessionRegistry::lockIP called for unknown session " + std::to_string(sessionID));
|
}*/
|
||||||
mSessionIPs.erase(sessionID);
|
mIPSessions[ip] = mSessions.find(sessionID)->second;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mIPSessions[ip] = it->second;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionRegistry::deallocIP(uint32_t sessionID) {
|
void SessionRegistry::deallocIP(uint32_t sessionID) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#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() {
|
||||||
@@ -18,23 +17,17 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
return static_cast<uint8_t>(type);
|
return static_cast<uint8_t>(type);
|
||||||
}, type);
|
}, type);
|
||||||
|
|
||||||
auto data = std::make_shared<std::vector<uint8_t>>();
|
std::vector<uint8_t> data;
|
||||||
data->push_back(typeByte);
|
data.push_back(typeByte);
|
||||||
// Ensure payload fits into protocol's 16-bit length field
|
uint16_t length = payload.size();
|
||||||
if (payload.size() > static_cast<size_t>(std::numeric_limits<uint16_t>::max())) {
|
|
||||||
Utils::error("sendMessage(): payload too large (>65535 bytes)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t length = static_cast<uint16_t>(payload.size());
|
data.push_back(length >> 8);
|
||||||
|
data.push_back(length & 0xFF);
|
||||||
|
|
||||||
data->push_back(static_cast<uint8_t>(length >> 8));
|
data.insert(data.end(), payload.begin(), payload.end());
|
||||||
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, data](asio::error_code ec, std::size_t) {
|
[self](asio::error_code ec, std::size_t) {
|
||||||
if (ec) {
|
if (ec) {
|
||||||
Utils::error("Send failed: " + ec.message());
|
Utils::error("Send failed: " + ec.message());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
// 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>
|
|
||||||
#include <cctype>
|
|
||||||
|
|
||||||
namespace ColumnLynx::Utils {
|
namespace ColumnLynx::Utils {
|
||||||
std::string unixMillisToISO8601(uint64_t unixMillis, bool local) {
|
std::string unixMillisToISO8601(uint64_t unixMillis, bool local) {
|
||||||
@@ -87,7 +85,7 @@ namespace ColumnLynx::Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string getVersion() {
|
std::string getVersion() {
|
||||||
return "1.2.0";
|
return "1.1.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned short serverPort() {
|
unsigned short serverPort() {
|
||||||
@@ -146,49 +144,19 @@ namespace ColumnLynx::Utils {
|
|||||||
|
|
||||||
std::vector<std::string> out;
|
std::vector<std::string> out;
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
std::ifstream file(basePath + "whitelisted_keys");
|
||||||
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()) {
|
if (!file.is_open()) {
|
||||||
warn("getWhitelistedKeys(): failed to open whitelist file: " + canon.string());
|
warn("Failed to open whitelisted_keys file at path: " + basePath + "whitelisted_keys");
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(file, line)) {
|
while (std::getline(file, 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
|
// Convert to upper case to align with the bytesToHexString() output
|
||||||
for (size_t i = 0; i < key.length(); ++i) {
|
for (int i = 0; i < line.length(); i++) {
|
||||||
key[i] = static_cast<char>(toupper(static_cast<unsigned char>(key[i])));
|
line[i] = toupper(line[i]);
|
||||||
}
|
}
|
||||||
out.push_back(key);
|
out.push_back(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -198,26 +166,9 @@ 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;
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
std::ifstream file(path);
|
||||||
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()) {
|
if (!file.is_open()) {
|
||||||
throw std::runtime_error("Failed to open config file at path: " + canon.string());
|
throw std::runtime_error("Failed to open config file at path: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
@@ -252,78 +203,4 @@ namespace ColumnLynx::Utils {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> loadHexBytesFromFile(const std::string& path, size_t expectedBytes, const std::string& description, bool warnOnInsecurePermissions) {
|
|
||||||
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("loadHexBytesFromFile(): failed to resolve path: " + path + " - " + ec.message());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs::exists(abs, ec) || ec) {
|
|
||||||
throw std::runtime_error("loadHexBytesFromFile(): file does not exist: " + abs.string());
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path canon = fs::canonical(abs, ec);
|
|
||||||
if (ec) {
|
|
||||||
throw std::runtime_error("loadHexBytesFromFile(): failed to canonicalize path: " + abs.string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
if (warnOnInsecurePermissions) {
|
|
||||||
ec.clear();
|
|
||||||
fs::file_status status = fs::status(canon, ec);
|
|
||||||
if (ec) {
|
|
||||||
warn("loadHexBytesFromFile(): failed to inspect permissions for " + canon.string() + " - " + ec.message());
|
|
||||||
} else {
|
|
||||||
auto perms = status.permissions();
|
|
||||||
if ((perms & (fs::perms::group_all | fs::perms::others_all)) != fs::perms::none) {
|
|
||||||
warn(description + " file permissions are too permissive: " + canon.string() + " (recommend chmod 600)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs::is_regular_file(status)) {
|
|
||||||
warn(description + " path is not a regular file: " + canon.string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
std::ifstream file(canon);
|
|
||||||
if (!file.is_open()) {
|
|
||||||
throw std::runtime_error("Failed to open " + description + " file at path: " + canon.string());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string hex((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
||||||
|
|
||||||
auto trim = [](std::string& s) {
|
|
||||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
|
|
||||||
s.pop_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t start = 0;
|
|
||||||
while (start < s.size() && std::isspace(static_cast<unsigned char>(s[start]))) {
|
|
||||||
++start;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start > 0) {
|
|
||||||
s.erase(0, start);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
trim(hex);
|
|
||||||
|
|
||||||
if (hex.empty()) {
|
|
||||||
throw std::runtime_error(description + " file is empty: " + canon.string());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> bytes = hexStringToBytes(hex);
|
|
||||||
if (bytes.size() != expectedBytes) {
|
|
||||||
throw std::runtime_error(description + " file must contain exactly " + std::to_string(expectedBytes * 2) + " hex characters (" + std::to_string(expectedBytes) + " bytes), got " + std::to_string(bytes.size()) + " bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,6 @@
|
|||||||
|
|
||||||
#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)
|
||||||
@@ -61,33 +56,6 @@ 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)
|
||||||
@@ -213,8 +181,8 @@ static bool runCommand(const std::vector<std::string>& args) {
|
|||||||
pfd.fd = mFd;
|
pfd.fd = mFd;
|
||||||
pfd.events = POLLIN;
|
pfd.events = POLLIN;
|
||||||
|
|
||||||
// timeout in ms; keep it small so shutdown is responsive. Reduced for lower latency.
|
// timeout in ms; keep it small so shutdown is responsive
|
||||||
int ret = poll(&pfd, 1, 50);
|
int ret = poll(&pfd, 1, 200);
|
||||||
|
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
// No data yet
|
// No data yet
|
||||||
@@ -339,10 +307,25 @@ static bool runCommand(const std::vector<std::string>& args) {
|
|||||||
|
|
||||||
void VirtualInterface::resetIP() {
|
void VirtualInterface::resetIP() {
|
||||||
#if defined(__linux__)
|
#if defined(__linux__)
|
||||||
runCommand({"ip", "addr", "flush", "dev", mIfName});
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ip addr flush dev %s",
|
||||||
|
mIfName.c_str()
|
||||||
|
);
|
||||||
|
system(cmd);
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
runCommand({"ifconfig", mIfName, "inet", "0.0.0.0", "delete"});
|
char cmd[512];
|
||||||
runCommand({"ifconfig", mIfName, "inet6", "::", "delete"});
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ifconfig %s inet 0.0.0.0 delete",
|
||||||
|
mIfName.c_str()
|
||||||
|
);
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ifconfig %s inet6 :: delete",
|
||||||
|
mIfName.c_str()
|
||||||
|
);
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
// Wipe old routes
|
// Wipe old routes
|
||||||
//snprintf(cmd, sizeof(cmd),
|
//snprintf(cmd, sizeof(cmd),
|
||||||
@@ -374,19 +357,26 @@ static bool runCommand(const std::vector<std::string>& args) {
|
|||||||
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
|
||||||
runCommand({"ip", "addr", "flush", "dev", mIfName});
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ip addr flush dev %s",
|
||||||
|
mIfName.c_str()
|
||||||
|
);
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
// Add address with peer
|
snprintf(cmd, sizeof(cmd),
|
||||||
std::string addrArg = ipStr + "/" + std::to_string(prefixLen);
|
"ip addr add %s/%d peer %s dev %s",
|
||||||
runCommand({"ip", "addr", "add", addrArg, "peer", peerStr, "dev", mIfName});
|
ipStr.c_str(), prefixLen, peerStr.c_str(), mIfName.c_str());
|
||||||
|
system(cmd);
|
||||||
// Bring link up and set MTU
|
|
||||||
runCommand({"ip", "link", "set", "dev", mIfName, "up", "mtu", std::to_string(mtu)});
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ip link set dev %s up mtu %d", mIfName.c_str(), mtu);
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -397,23 +387,39 @@ static bool runCommand(const std::vector<std::string>& args) {
|
|||||||
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 IPv4 and IPv6 addresses
|
// Reset
|
||||||
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);
|
||||||
|
|
||||||
// Set address and netmask
|
snprintf(cmd, sizeof(cmd),
|
||||||
std::string netArg = ipStr + " " + peerStr; // ifconfig expects ip peer
|
"ifconfig %s inet6 :: delete",
|
||||||
runCommand({"ifconfig", mIfName, "inet", ipStr, peerStr, "mtu", std::to_string(mtu), "netmask", prefixStr, "up"});
|
mIfName.c_str()
|
||||||
|
);
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
// Add route for the network
|
// Set
|
||||||
std::string networkArg = ipStr + "/" + std::to_string(prefixLen);
|
snprintf(cmd, sizeof(cmd),
|
||||||
runCommand({"route", "-n", "add", "-net", networkArg, "-interface", mIfName});
|
"ifconfig %s inet %s %s mtu %d netmask %s up",
|
||||||
|
mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
|
// Host bits are auto-normalized by the kernel on macOS, so we don't need to worry about them not being zeroed out.
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"route -n add -net %s/%d -interface %s",
|
||||||
|
ipStr.c_str(), prefixLen, mIfName.c_str());
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
|
Utils::log("Executed command: " + std::string(cmd));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
|
||||||
#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>
|
||||||
@@ -92,9 +90,10 @@ int main(int argc, char** argv) {
|
|||||||
serverState.configPath = configPath;
|
serverState.configPath = configPath;
|
||||||
|
|
||||||
#if defined(DEBUG)
|
#if defined(DEBUG)
|
||||||
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" });
|
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" });
|
||||||
#else
|
#else
|
||||||
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config", { "NETWORK", "SUBNET_MASK" });
|
// 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
|
#endif
|
||||||
|
|
||||||
serverState.serverConfig = config;
|
serverState.serverConfig = config;
|
||||||
@@ -105,28 +104,30 @@ int main(int argc, char** argv) {
|
|||||||
// Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs)
|
// Store a reference to the tun in the serverState, it will increment and keep a safe reference (we love shared_ptrs)
|
||||||
serverState.virtualInterface = tun;
|
serverState.virtualInterface = tun;
|
||||||
|
|
||||||
|
// Generate a temporary keypair, replace with actual CA signed keys later (Note, these are stored in memory)
|
||||||
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
std::shared_ptr<LibSodiumWrapper> sodiumWrapper = std::make_shared<LibSodiumWrapper>();
|
||||||
|
|
||||||
const std::string serverPublicKeyPath = configPath + "public.key";
|
auto itPubkey = config.find("SERVER_PUBLIC_KEY");
|
||||||
const std::string serverPrivateKeyPath = configPath + "private.key";
|
auto itPrivkey = config.find("SERVER_PRIVATE_KEY");
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
if (itPubkey != config.end() && itPrivkey != config.end()) {
|
||||||
bool serverKeyFilesPresent = fs::exists(serverPublicKeyPath) && fs::exists(serverPrivateKeyPath);
|
log("Loading keypair from config file.");
|
||||||
if (serverKeyFilesPresent) {
|
|
||||||
log("Loading server keypair from key files.");
|
|
||||||
|
|
||||||
PublicKey pk = Utils::loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(serverPublicKeyPath, "server public key");
|
PublicKey pk;
|
||||||
PrivateSeed seed = Utils::loadHexArrayFromFile<crypto_sign_SEEDBYTES>(serverPrivateKeyPath, "server private key", true);
|
PrivateSeed seed;
|
||||||
|
|
||||||
|
std::copy_n(Utils::hexStringToBytes(itPrivkey->second).begin(), seed.size(), seed.begin());
|
||||||
|
std::copy_n(Utils::hexStringToBytes(itPubkey->second).begin(), pk.size(), pk.begin());
|
||||||
|
|
||||||
if (!sodiumWrapper->recomputeKeys(seed, pk)) {
|
if (!sodiumWrapper->recomputeKeys(seed, pk)) {
|
||||||
throw std::runtime_error("Failed to recompute keypair from key files!");
|
throw std::runtime_error("Failed to recompute keypair from config file values!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
#if defined(DEBUG)
|
#if defined(DEBUG)
|
||||||
warn("No server keypair files found! Using random key.");
|
warn("No keypair found in config file! Using random key.");
|
||||||
#else
|
#else
|
||||||
throw std::runtime_error("No server keypair files found! Cannot start server without keys.");
|
throw std::runtime_error("No keypair found in config file! Cannot start server without keys.");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Server public key: " + bytesToHexString(sodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES));
|
log("Server public key: " + bytesToHexString(sodiumWrapper->getPublicKey(), crypto_sign_PUBLICKEYBYTES));
|
||||||
@@ -143,22 +144,6 @@ int main(int argc, char** argv) {
|
|||||||
auto server = std::make_shared<TCPServer>(io, serverPort());
|
auto server = std::make_shared<TCPServer>(io, serverPort());
|
||||||
auto udpServer = std::make_shared<UDPServer>(io, serverPort());
|
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.");
|
||||||
@@ -167,8 +152,6 @@ int main(int argc, char** argv) {
|
|||||||
ServerSession::getInstance().setHostRunning(false);
|
ServerSession::getInstance().setHostRunning(false);
|
||||||
server->stop();
|
server->stop();
|
||||||
udpServer->stop();
|
udpServer->stop();
|
||||||
// Cancel cleanup timer
|
|
||||||
cleanupTimer->cancel();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,35 +172,15 @@ int main(int argc, char** argv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packet.size() < 20) {
|
|
||||||
Utils::warn("TUN: Dropping packet smaller than IPv4 header (" + std::to_string(packet.size()) + " bytes)");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t* ip = packet.data();
|
const uint8_t* ip = packet.data();
|
||||||
uint8_t ipVersion = (ip[0] >> 4);
|
uint32_t srcIP = ntohl(*(uint32_t*)(ip + 12)); // IPv4 source address offset
|
||||||
if (ipVersion != 4) {
|
uint32_t dstIP = ntohl(*(uint32_t*)(ip + 16)); // IPv4 destination address offset
|
||||||
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)
|
// 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);
|
auto dstSession = SessionRegistry::getInstance().getByIP(dstIP);
|
||||||
if (dstSession) {
|
if (dstSession) {
|
||||||
// Destination is a registered client, enforce MTU and forward to that client's session
|
// Destination is a registered client, forward to that client's session
|
||||||
const size_t MTU = 1420; // Enforce configured MTU; TODO: read from server config
|
udpServer->sendData(dstSession->sessionID, std::string(packet.begin(), packet.end()));
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,31 +14,23 @@ 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([weakSelf = weak_from_this()](AnyMessageType type, const std::string& data) {
|
mHandler->onMessage([this](AnyMessageType type, const std::string& data) {
|
||||||
if (auto self = weakSelf.lock()) {
|
mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
|
||||||
self->mHandleMessage(static_cast<ClientMessageType>(MessageHandler::toUint8(type)), data);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mHandler->onDisconnect([weakSelf = weak_from_this()](const asio::error_code& ec) {
|
mHandler->onDisconnect([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: " + self->mRemoteIP + " - " + ec.message());
|
Utils::log("Client disconnected: " + mRemoteIP + " - " + ec.message());
|
||||||
asio::error_code ec2;
|
asio::error_code ec2;
|
||||||
if (self->mHandler) {
|
mHandler->socket().close(ec2);
|
||||||
self->mHandler->socket().close(ec2);
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
|
SessionRegistry::getInstance().erase(mConnectionSessionID);
|
||||||
SessionRegistry::getInstance().deallocIP(self->mConnectionSessionID);
|
SessionRegistry::getInstance().deallocIP(mConnectionSessionID);
|
||||||
|
|
||||||
Utils::log("Closed connection to " + self->mRemoteIP);
|
Utils::log("Closed connection to " + mRemoteIP);
|
||||||
|
|
||||||
if (self->mOnDisconnect) {
|
if (mOnDisconnect) {
|
||||||
self->mOnDisconnect(self);
|
mOnDisconnect(shared_from_this());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +77,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([self](const asio::error_code& ec) {
|
mHeartbeatTimer.async_wait([this, 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
|
||||||
}
|
}
|
||||||
@@ -98,13 +90,10 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
|
|
||||||
// Remove socket forcefully, client is dead
|
// Remove socket forcefully, client is dead
|
||||||
asio::error_code ec;
|
asio::error_code ec;
|
||||||
if (self->mHandler) {
|
mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
|
||||||
self->mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec);
|
mHandler->socket().close(ec);
|
||||||
self->mHandler->socket().close(ec);
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
|
SessionRegistry::getInstance().erase(self->mConnectionSessionID);
|
||||||
SessionRegistry::getInstance().deallocIP(self->mConnectionSessionID);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -124,8 +113,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_sign_PUBLICKEYBYTES) {
|
if (data.size() < 1 + crypto_box_PUBLICKEYBYTES) {
|
||||||
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " has invalid size: " + std::to_string(data.size()));
|
Utils::warn("HANDSHAKE_INIT from " + reqAddr + " is too short.");
|
||||||
disconnect();
|
disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -141,7 +130,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, sizeof(signPk));
|
std::memcpy(signPk.data(), data.data() + 1, std::min(data.size() - 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)
|
||||||
@@ -173,15 +162,9 @@ namespace ColumnLynx::Net::TCP {
|
|||||||
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 - require exact size
|
// Convert to byte array
|
||||||
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(), sizeof(challengeData));
|
std::memcpy(challengeData, data.data(), std::min(data.size(), sizeof(challengeData)));
|
||||||
|
|
||||||
// Sign the challenge
|
// Sign the challenge
|
||||||
Signature sig = Utils::LibSodiumWrapper::signMessage(
|
Signature sig = Utils::LibSodiumWrapper::signMessage(
|
||||||
|
|||||||
@@ -95,23 +95,7 @@ namespace ColumnLynx::Net::UDP {
|
|||||||
UDPPacketHeader hdr{};
|
UDPPacketHeader hdr{};
|
||||||
uint8_t nonce[12];
|
uint8_t nonce[12];
|
||||||
uint32_t prefix = session->noncePrefix;
|
uint32_t prefix = session->noncePrefix;
|
||||||
// Increment send counter with overflow protection
|
uint64_t sendCount = const_cast<SessionState*>(session.get())->send_ctr.fetch_add(1, std::memory_order_relaxed);
|
||||||
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, &prefix, sizeof(uint32_t)); // Prefix nonce
|
||||||
memcpy(nonce + sizeof(uint32_t), &sendCount, sizeof(uint64_t)); // Use send count as nonce suffix to ensure uniqueness
|
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());
|
std::copy_n(nonce, 12, hdr.nonce.data());
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Tests for hex key file loading helpers
|
|
||||||
#include <cassert>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include <columnlynx/common/utils.hpp>
|
|
||||||
#include <sodium.h>
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace ColumnLynx::Utils;
|
|
||||||
|
|
||||||
fs::path tempDir = fs::temp_directory_path() / "columnlynx_key_loader_test";
|
|
||||||
fs::remove_all(tempDir);
|
|
||||||
fs::create_directories(tempDir);
|
|
||||||
|
|
||||||
auto publicKeyPath = tempDir / "public.key";
|
|
||||||
auto privateKeyPath = tempDir / "private.key";
|
|
||||||
|
|
||||||
{
|
|
||||||
std::ofstream pub(publicKeyPath);
|
|
||||||
pub << "00112233445566778899aabbccddeeff00112233445566778899AABBCCDDEEFF";
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::ofstream priv(privateKeyPath);
|
|
||||||
priv << "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
auto pk = loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(publicKeyPath.string(), "public key");
|
|
||||||
auto sk = loadHexArrayFromFile<crypto_sign_SEEDBYTES>(privateKeyPath.string(), "private key", true);
|
|
||||||
|
|
||||||
assert(pk[0] == 0x00 && pk[1] == 0x11 && pk.back() == 0xFF);
|
|
||||||
assert(sk[0] == 0xFF && sk[1] == 0xEE && sk.back() == 0x00);
|
|
||||||
|
|
||||||
bool threwMissing = false;
|
|
||||||
try {
|
|
||||||
(void)loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>((tempDir / "missing.key").string(), "missing key");
|
|
||||||
} catch (const std::exception&) {
|
|
||||||
threwMissing = true;
|
|
||||||
}
|
|
||||||
assert(threwMissing && "Missing key file should throw");
|
|
||||||
|
|
||||||
auto badLengthPath = tempDir / "bad-length.key";
|
|
||||||
{
|
|
||||||
std::ofstream bad(badLengthPath);
|
|
||||||
bad << "abcd";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool threwBadLength = false;
|
|
||||||
try {
|
|
||||||
(void)loadHexArrayFromFile<crypto_sign_PUBLICKEYBYTES>(badLengthPath.string(), "bad length key");
|
|
||||||
} catch (const std::exception&) {
|
|
||||||
threwBadLength = true;
|
|
||||||
}
|
|
||||||
assert(threwBadLength && "Wrong-length key file should throw");
|
|
||||||
|
|
||||||
std::cout << "Key file loader tests passed\n";
|
|
||||||
fs::remove_all(tempDir);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// 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