5 Commits

Author SHA1 Message Date
3cd99243ad Version 1.0.0 2026-01-01 16:32:14 +01:00
8f536abe77 Merge branch 'dev' into beta - Version 1.0.0 2026-01-01 16:23:37 +01:00
1f5a0585f3 Version 1.0.0 2026-01-01 16:22:17 +01:00
3dc5c04bf1 Auto set route on macos 2025-12-31 21:12:23 +01:00
37ddb82d9a Refactor args 2025-12-30 03:39:35 +01:00
16 changed files with 214 additions and 40 deletions

View File

@@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.16)
# If MAJOR is 0, and MINOR > 0, Version is BETA # If MAJOR is 0, and MINOR > 0, Version is BETA
project(ColumnLynx project(ColumnLynx
VERSION 0.3.0 VERSION 1.0.0
LANGUAGES CXX LANGUAGES CXX
) )

View File

@@ -18,22 +18,71 @@ 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.
### Creating the Tun Interface (Linux Server ONLY)
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
```
### 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 +102,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>

View File

@@ -14,6 +14,7 @@
#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>
@@ -29,6 +30,7 @@ namespace ColumnLynx::Net::TCP {
std::shared_ptr<std::array<uint8_t, 32>> aesKey, std::shared_ptr<std::array<uint8_t, 32>> aesKey,
std::shared_ptr<uint64_t> sessionIDRef, std::shared_ptr<uint64_t> sessionIDRef,
bool insecureMode, bool insecureMode,
std::string& configPath,
std::shared_ptr<VirtualInterface> tun = nullptr) std::shared_ptr<VirtualInterface> tun = nullptr)
: :
mResolver(ioContext), mResolver(ioContext),
@@ -42,10 +44,11 @@ namespace ColumnLynx::Net::TCP {
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) mTun(tun),
mConfigDirPath(configPath)
{ {
// 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 +57,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
} }
} }
@@ -109,5 +118,6 @@ namespace ColumnLynx::Net::TCP {
Protocol::TunConfig mTunConfig; Protocol::TunConfig mTunConfig;
std::shared_ptr<VirtualInterface> mTun = nullptr; std::shared_ptr<VirtualInterface> mTun = nullptr;
std::unordered_map<std::string, std::string> mRawClientConfig; std::unordered_map<std::string, std::string> mRawClientConfig;
std::string mConfigDirPath;
}; };
} }

View File

@@ -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

View File

@@ -46,7 +46,7 @@ namespace ColumnLynx::Net {
bool configureIP(uint32_t clientIP, uint32_t serverIP, bool configureIP(uint32_t clientIP, uint32_t serverIP,
uint8_t prefixLen, uint16_t mtu); uint8_t prefixLen, uint16_t mtu);
void resetIP(); void resetIP();
std::vector<uint8_t> readPacket(); std::vector<uint8_t> readPacket();

View File

@@ -44,7 +44,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);

View File

@@ -28,9 +28,10 @@ namespace ColumnLynx::Net::TCP {
asio::ip::tcp::socket socket, asio::ip::tcp::socket socket,
std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper, std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper,
std::unordered_map<std::string, std::string>* serverConfig, std::unordered_map<std::string, std::string>* serverConfig,
std::string configDirPath,
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), sodiumWrapper, serverConfig, configDirPath));
conn->mOnDisconnect = std::move(onDisconnect); conn->mOnDisconnect = std::move(onDisconnect);
return conn; return conn;
} }
@@ -50,14 +51,15 @@ namespace ColumnLynx::Net::TCP {
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, std::shared_ptr<Utils::LibSodiumWrapper> sodiumWrapper, std::unordered_map<std::string, std::string>* serverConfig, std::string configDirPath)
: :
mHandler(std::make_shared<MessageHandler>(std::move(socket))), mHandler(std::make_shared<MessageHandler>(std::move(socket))),
mLibSodiumWrapper(sodiumWrapper), mLibSodiumWrapper(sodiumWrapper),
mRawServerConfig(serverConfig), 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()),
mConfigDirPath(configDirPath)
{} {}
// Start the heartbeat routine // Start the heartbeat routine
@@ -77,5 +79,6 @@ namespace ColumnLynx::Net::TCP {
std::chrono::steady_clock::time_point mLastHeartbeatSent; std::chrono::steady_clock::time_point mLastHeartbeatSent;
int mMissedHeartbeats = 0; int mMissedHeartbeats = 0;
std::string mRemoteIP; // Cached remote IP to avoid calling remote_endpoint() on closed sockets std::string mRemoteIP; // Cached remote IP to avoid calling remote_endpoint() on closed sockets
std::string mConfigDirPath;
}; };
} }

View File

@@ -25,14 +25,17 @@ namespace ColumnLynx::Net::TCP {
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<Utils::LibSodiumWrapper> sodiumWrapper,
std::shared_ptr<bool> hostRunning, bool ipv4Only = false) std::shared_ptr<bool> hostRunning,
std::string& configPath,
bool ipv4Only = false)
: mIoContext(ioContext), : mIoContext(ioContext),
mAcceptor(ioContext), mAcceptor(ioContext),
mSodiumWrapper(sodiumWrapper), mSodiumWrapper(sodiumWrapper),
mHostRunning(hostRunning) mHostRunning(hostRunning),
mConfigDirPath(configPath)
{ {
// Preload the config map // Preload the config map
mRawServerConfig = Utils::getConfigMap("server_config", {"NETWORK", "SUBNET_MASK"}); mRawServerConfig = Utils::getConfigMap(configPath + "server_config", {"NETWORK", "SUBNET_MASK"});
asio::error_code ec_open, ec_v6only, ec_bind; asio::error_code ec_open, ec_v6only, ec_bind;
@@ -84,6 +87,7 @@ namespace ColumnLynx::Net::TCP {
std::shared_ptr<Utils::LibSodiumWrapper> mSodiumWrapper; std::shared_ptr<Utils::LibSodiumWrapper> mSodiumWrapper;
std::shared_ptr<bool> mHostRunning; std::shared_ptr<bool> mHostRunning;
std::unordered_map<std::string, std::string> mRawServerConfig; std::unordered_map<std::string, std::string> mRawServerConfig;
std::string mConfigDirPath;
}; };
} }

View File

@@ -12,6 +12,10 @@
#include <cxxopts.hpp> #include <cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp> #include <columnlynx/common/net/virtual_interface.hpp>
#if defined(__WIN32__)
#include <windows.h>
#endif
using asio::ip::tcp; using asio::ip::tcp;
using namespace ColumnLynx::Utils; using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net; using namespace ColumnLynx::Net;
@@ -48,9 +52,17 @@ 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")) {
@@ -72,6 +84,21 @@ 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) {
configPath = std::string(envConfigPath);
}
if (configPath.back() != '/' && configPath.back() != '\\') {
#if defined(__WIN32__)
configPath += "\\";
#else
configPath += "/";
#endif
}
std::shared_ptr<VirtualInterface> tun = std::make_shared<VirtualInterface>(optionsObj["interface"].as<std::string>()); 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());
@@ -84,7 +111,7 @@ int main(int argc, char** argv) {
std::shared_ptr<uint64_t> sessionID = std::make_shared<uint64_t>(0); std::shared_ptr<uint64_t> sessionID = std::make_shared<uint64_t>(0);
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, sodiumWrapper, aesKey, sessionID, insecureMode, configPath, tun);
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, aesKey, sessionID, tun);
client->start(); client->start();

View File

@@ -150,11 +150,12 @@ 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);
std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey))); std::memcpy(mServerPublicKey, data.data(), std::min(data.size(), sizeof(mServerPublicKey)));
std::string hexServerPub = Utils::bytesToHexString(mServerPublicKey, 32);
Utils::log("Received server identity. Public Key: " + hexServerPub);
// Verify pubkey against whitelisted_keys // Verify pubkey against whitelisted_keys
std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys(); std::vector<std::string> whitelistedKeys = Utils::getWhitelistedKeys(mConfigDirPath);
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 (!mInsecureMode) {
Utils::error("Server public key not in whitelisted_keys. Terminating connection."); Utils::error("Server public key not in whitelisted_keys. Terminating connection.");

View File

@@ -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;
}
} }

View File

@@ -49,7 +49,7 @@ namespace ColumnLynx::Utils {
} }
std::string getVersion() { std::string getVersion() {
return "b0.3"; return "1.0.0";
} }
unsigned short serverPort() { unsigned short serverPort() {
@@ -101,14 +101,19 @@ 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 std::ifstream file(basePath + "whitelisted_keys");
if (!file.is_open()) {
warn("Failed to open whitelisted_keys file at path: " + basePath + "whitelisted_keys");
return out;
}
std::string line; std::string line;
while (std::getline(file, line)) { while (std::getline(file, line)) {
@@ -123,6 +128,10 @@ namespace ColumnLynx::Utils {
std::vector<std::string> readLines; std::vector<std::string> readLines;
std::ifstream file(path); std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Failed to open config file at path: " + path);
}
std::string line; std::string line;
while (std::getline(file, line)) { while (std::getline(file, line)) {

View File

@@ -72,7 +72,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 +96,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)));
} }
@@ -326,6 +326,13 @@ namespace ColumnLynx::Net {
mIfName.c_str() mIfName.c_str()
); );
system(cmd); system(cmd);
// Wipe old routes
//snprintf(cmd, sizeof(cmd),
// "route -n delete -net %s",
// mIfName.c_str()
//);
//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
@@ -406,6 +413,12 @@ namespace ColumnLynx::Net {
mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str()); mIfName.c_str(), ipStr.c_str(), peerStr.c_str(), mtu, prefixStr.c_str());
system(cmd); system(cmd);
// 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)); Utils::log("Executed command: " + std::string(cmd));
return true; return true;

View File

@@ -16,6 +16,10 @@
#include <cxxopts.hpp> #include <cxxopts.hpp>
#include <columnlynx/common/net/virtual_interface.hpp> #include <columnlynx/common/net/virtual_interface.hpp>
#if defined(__WIN32__)
#include <windows.h>
#endif
using asio::ip::tcp; using asio::ip::tcp;
using namespace ColumnLynx::Utils; using namespace ColumnLynx::Utils;
using namespace ColumnLynx::Net::TCP; using namespace ColumnLynx::Net::TCP;
@@ -37,7 +41,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();
@@ -60,7 +69,22 @@ 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>()); // Get the config path, ENV > CLI > /etc/columnlynx
std::string configPath = optionsObj["config-dir"].as<std::string>();
const char* envConfigPath = std::getenv("COLUMNLYNX_CONFIG_DIR");
if (envConfigPath != nullptr) {
configPath = std::string(envConfigPath);
}
if (configPath.back() != '/' && configPath.back() != '\\') {
#if defined(__WIN32__)
configPath += "\\";
#else
configPath += "/";
#endif
}
std::unordered_map<std::string, std::string> config = Utils::getConfigMap(configPath + "server_config");
std::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());
@@ -75,14 +99,20 @@ 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));
@@ -91,7 +121,7 @@ int main(int argc, char** argv) {
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(), sodiumWrapper, hostRunning, configPath, ipv4Only);
auto udpServer = std::make_shared<UDPServer>(io, serverPort(), hostRunning, ipv4Only, tun); auto udpServer = std::make_shared<UDPServer>(io, serverPort(), hostRunning, ipv4Only, tun);
asio::signal_set signals(io, SIGINT, SIGTERM); asio::signal_set signals(io, SIGINT, SIGTERM);

View File

@@ -145,7 +145,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(mConfigDirPath);
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);

View File

@@ -36,6 +36,7 @@ namespace ColumnLynx::Net::TCP {
std::move(socket), std::move(socket),
mSodiumWrapper, mSodiumWrapper,
&mRawServerConfig, &mRawServerConfig,
mConfigDirPath,
[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.");