diff --git a/CMakeLists.txt b/CMakeLists.txt index dad3553678a5a1a124be4a95a5edad463a07d758..077c4d4ad927784af80538318f7c5f6f6371476b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ project(dhtnet LANGUAGES CXX DESCRIPTION "A C++ library for NAT traversal and secure communication") +option(BUILD_TOOLS "Build dnc" ON) + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(CTest) @@ -135,6 +137,16 @@ install(TARGETS dhtnet) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/dhtnet) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dhtnet.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +if (BUILD_TOOLS) + add_executable(dnc + tools/dnc/main.cpp + tools/dnc/dnc.cpp + tools/common.cpp) + target_link_libraries(dnc PRIVATE dhtnet fmt::fmt) + target_include_directories(dnc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tools) + install(TARGETS dnc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() + if (BUILD_TESTING AND NOT MSVC) pkg_search_module(Cppunit REQUIRED IMPORTED_TARGET cppunit) add_executable(tests_certstore tests/certstore.cpp) diff --git a/tools/common.cpp b/tools/common.cpp new file mode 100644 index 0000000000000000000000000000000000000000..101fbb9d4c752c06bff21882f73be205407e5bb6 --- /dev/null +++ b/tools/common.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2004-2023 Savoir-faire Linux Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +#include "certstore.h" +#include <opendht/crypto.h> +#include "connectionmanager.h" +#include "common.h" +#include "fileutils.h" +#include "ice_transport.h" + +#include <iostream> +#include <string> +#include <filesystem> +#include <unistd.h> +#include <fcntl.h> +#include <asio.hpp> + +namespace dhtnet { + +std::shared_ptr<asio::posix::stream_descriptor> stdinDescriptor; + +dht::crypto::Identity +loadIdentity(bool isServer) +{ + std::string idDir = std::string(getenv("HOME")) + "/.dhtnetTools"; + if (isServer){ + + if (!std::filesystem::exists(idDir)) { + std::filesystem::create_directory(idDir); + } + + try { + std::filesystem::directory_iterator endIter; + for (std::filesystem::directory_iterator iter(idDir); iter != endIter; ++iter) { + if (iter->path().extension() == ".pem") { + auto privateKey = std::make_unique<dht::crypto::PrivateKey>( + fileutils::loadFile(std::filesystem::path(iter->path()))); + // Generate certificate + auto certificate = std::make_unique<dht::crypto::Certificate>( + dht::crypto::Certificate::generate(*privateKey, "dhtnc")); + // return + return dht::crypto::Identity(std::move(privateKey), std::move(certificate)); + } + } + } catch (const std::exception& e) { + fmt::print(stderr, "Error loadind key from .dhtnetTools: {}\n", e.what()); + } + } + auto ca = dht::crypto::generateIdentity("ca"); + auto id = dht::crypto::generateIdentity("dhtnc", ca); + idDir += "/id"; + if (isServer) + dht::crypto::saveIdentity(id, idDir); + return id; +} + +std::unique_ptr<ConnectionManager::Config> +connectionManagerConfig(dht::crypto::Identity identity, + const std::string& bootstrap_ip_add, + const std::string& bootstrap_port, + std::shared_ptr<Logger> logger, + tls::CertificateStore& certStore, + std::shared_ptr<asio::io_context> ioContext, + IceTransportFactory& iceFactory) +{ + std::filesystem::create_directories(std::string(getenv("HOME")) + "/.dhtnetTools/certstore"); + + // DHT node creation: To make a connection manager at first a DHT node should be created + + dht::DhtRunner::Config dhtConfig; + dhtConfig.dht_config.id = identity; + dhtConfig.threaded = true; + dhtConfig.peer_discovery = false; + dhtConfig.peer_publish = false; + dht::DhtRunner::Context dhtContext; + dhtContext.identityAnnouncedCb = [logger](bool ok) { + if (logger) + logger->debug("Identity announced {}\n", ok); + }; + dhtContext.certificateStore = [&](const dht::InfoHash& pk_id) { + std::vector<std::shared_ptr<dht::crypto::Certificate>> ret; + if (auto cert = certStore.getCertificate(pk_id.toString())) + ret.emplace_back(std::move(cert)); + return ret; + }; + auto runner = std::make_shared<dht::DhtRunner>(); + runner->run(dhtConfig, std::move(dhtContext)); + runner->bootstrap(bootstrap_ip_add, bootstrap_port); + + // DHT node creation end: + // ConnectionManager creation: + auto config = std::make_unique<ConnectionManager::Config>(); + config->dht = runner; + config->id = identity; + config->ioContext = ioContext; + config->certStore = &certStore; + config->factory = &iceFactory; + config->cachePath = std::string(getenv("HOME")) + "/.dhtnetTools"; + + return std::move(config); +} +template <typename T> +void readFromPipe(std::shared_ptr<ChannelSocket> socket, T input, Buffer buffer) +{ + asio::async_read(*input, + asio::buffer(*buffer), + asio::transfer_at_least(1), + [socket, input, buffer](const asio::error_code& error, size_t bytesRead) { + if (!error) { + // Process the data received in the buffer + std::error_code ec; + // print the data to stdout + socket->write(buffer->data(), bytesRead, ec); + if (!ec) { + // Continue reading more data + readFromPipe(socket, input, buffer); + } else { + fmt::print(stderr, "Error writing to socket: {}\n", ec.message()); + // logger->error("Error writing to socket: {}", ec.message()); + } + } else if(error != asio::error::eof) { + fmt::print(stderr, "Error reading from stdin: {}\n", error.message()); + // logger->error("Error reading from stdin: {}", error.message()); + } + }); +} + +template void readFromPipe(std::shared_ptr<ChannelSocket> socket, std::shared_ptr<asio::posix::stream_descriptor> input, Buffer buffer); +template void readFromPipe(std::shared_ptr<ChannelSocket> socket, std::shared_ptr<asio::ip::tcp::socket> input, Buffer buffer); + + + + +} // namespace dhtnet \ No newline at end of file diff --git a/tools/common.h b/tools/common.h new file mode 100644 index 0000000000000000000000000000000000000000..19b7771edf5d949de9a838744d63a293f540d3a5 --- /dev/null +++ b/tools/common.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2004-2023 Savoir-faire Linux Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +#include <opendht/crypto.h> + + +namespace dhtnet { + +using Buffer = std::shared_ptr<std::vector<uint8_t>>; + +/** + * Attempt to retrieve the identity from the .ssh directory, and if none is found, generate a new + * certification. + * @return dht::crypto::Identity + */ + +dht::crypto::Identity loadIdentity(bool isServer); +// add certstore to the config +std::unique_ptr<ConnectionManager::Config> connectionManagerConfig(dht::crypto::Identity identity, + const std::string& bootstrap_ip_add, + const std::string& bootstrap_port, + std::shared_ptr<Logger> logger, + tls::CertificateStore& certStore, + std::shared_ptr<asio::io_context> ioContext, + IceTransportFactory& iceFactory); +// add ioContext to readFromStdin + +template <typename T> +void readFromPipe(std::shared_ptr<ChannelSocket> socket, T input, Buffer buffer); + +} // namespace dhtnet \ No newline at end of file diff --git a/tools/dnc/dnc.cpp b/tools/dnc/dnc.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dddc7b1a6878f735ac9206dd717f8b231d2e9922 --- /dev/null +++ b/tools/dnc/dnc.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2004-2023 Savoir-faire Linux Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +#include "dnc.h" +#include "certstore.h" +#include "connectionmanager.h" +#include "fileutils.h" +#include "../common.h" + +#include <opendht/log.h> +#include <opendht/crypto.h> +#include <asio.hpp> + +#include <fcntl.h> +#include <unistd.h> + +#include <iostream> +#include <chrono> +#include <string> +#include <string_view> +#include <filesystem> +#include <memory> + +namespace dhtnet { +std::pair<std::string, std::string> +Dnc::parseName(const std::string_view name) +{ + // Find the position of the first ':' character after "nc//" + size_t ip_add_start = name.find("nc//") + 6; // Adding 5 to skip "nc//" + size_t colonPos = name.find(':', ip_add_start); + + if (colonPos == std::string::npos) { + // Return an empty pair if ':' is not found + return std::make_pair("", ""); + } + + std::string ip_add(name.substr(ip_add_start, colonPos - ip_add_start)); + std::string port(name.substr(colonPos + 1)); + + return std::make_pair(ip_add, port); +} + + +// Build a server +Dnc::Dnc(dht::crypto::Identity identity, + const std::string& bootstrap_ip_add, + const std::string& bootstrap_port) + : logger(dht::log::getStdLogger()) + , certStore(std::string(getenv("HOME")) + "/.dhtnetTools/certstore", logger) + +{ + ioContext = std::make_shared<asio::io_context>(); + ioContextRunner = std::thread([context = ioContext, logger = logger] { + try { + auto work = asio::make_work_guard(*context); + context->run(); + } catch (const std::exception& ex) { + if (logger) + logger->error("Error in ioContextRunner: {}", ex.what()); + } + }); + + auto config = connectionManagerConfig(identity, bootstrap_ip_add, bootstrap_port, logger, certStore, ioContext, iceFactory); + // create a connection manager + connectionManager = std::make_unique<ConnectionManager>(move(config)); + + connectionManager->onDhtConnected(identity.first->getPublicKey()); + connectionManager->onICERequest([this](const dht::Hash<32>&) { // handle ICE request + if (logger) + logger->debug("ICE request received"); + return true; + }); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + + connectionManager->onChannelRequest( + [&](const std::shared_ptr<dht::crypto::Certificate>&, const std::string& name) { + // handle channel request + if (logger) + logger->debug("Channel request received"); + return true; + }); + + connectionManager->onConnectionReady([&](const DeviceId&, + const std::string& name, + std::shared_ptr<ChannelSocket> mtlxSocket) { + if (name.empty()) { + // Handle the empty input case here + return; + } + try { + auto parsedName = parseName(name); + asio::ip::tcp::resolver resolver(*ioContext); + asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(parsedName.first, + parsedName.second); + + // Create a TCP socket + auto socket = std::make_shared<asio::ip::tcp::socket>(*ioContext); + asio::async_connect( + *socket, + endpoints, + [this, socket, mtlxSocket](const std::error_code& error, + const asio::ip::tcp::endpoint& ep) { + if (!error) { + if (logger) + logger->debug("Connected!"); + mtlxSocket->setOnRecv([socket, this](const uint8_t* data, size_t size) { + auto data_copy = std::make_shared<std::vector<uint8_t>>(data, + data + size); + asio::async_write(*socket, + asio::buffer(*data_copy), + [data_copy, this](const std::error_code& error, + std::size_t bytesWritten) { + if (error) { + if (logger) + logger->error("Write error: {}", + error.message()); + } + }); + return size; + }); + // Create a buffer to read data into + auto buffer = std::make_shared<std::vector<uint8_t>>(65536); + + readFromPipe(mtlxSocket, socket, buffer); + } else { + if (logger) + logger->error("Connection error: {}", error.message()); + } + }); + + } catch (std::exception& e) { + if (logger) + logger->error("Exception: {}", e.what()); + } + }); +} +// Build a client +Dnc::Dnc(dht::crypto::Identity identity, + const std::string& bootstrap_ip_add, + const std::string& bootstrap_port, + dht::InfoHash peer_id, + int port, + const std::string& ip_add) + : Dnc(identity, bootstrap_ip_add, bootstrap_port) +{ + std::condition_variable cv; + auto name = fmt::format("nc://{:s}:{:d}", ip_add, port); + connectionManager->connectDevice(peer_id, + name, + [&](std::shared_ptr<ChannelSocket> socket, + const dht::InfoHash&) { + if (socket) { + socket->setOnRecv( + [this, socket](const uint8_t* data, size_t size) { + std::cout.write((const char*) data, size); + std::cout.flush(); + return size; + }); + // Create a buffer to read data into + auto buffer = std::make_shared<std::vector<uint8_t>>(65536); + + // Create a shared_ptr to the stream_descriptor + auto stdinPipe = std::make_shared<asio::posix::stream_descriptor>(*ioContext, + ::dup(STDIN_FILENO)); + readFromPipe(socket, stdinPipe, buffer); + + socket->onShutdown([this]() { + if (logger) + logger->error("Exit program"); + std::exit(EXIT_FAILURE); + }); + } + }); + + connectionManager->onConnectionReady([&](const DeviceId&, + const std::string& name, + std::shared_ptr<ChannelSocket> mtlxSocket) { + if (logger) + logger->debug("Connected!"); + }); +} + +void +Dnc::run() +{ + ioContext->run(); +} + + +Dnc::~Dnc() +{ + ioContext->stop(); + ioContextRunner.join(); +} +} // namespace dhtnet diff --git a/tools/dnc/dnc.h b/tools/dnc/dnc.h new file mode 100644 index 0000000000000000000000000000000000000000..0c8c7c1cb84d3925ac7327f2ed9dc3dda21c2a90 --- /dev/null +++ b/tools/dnc/dnc.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2004-2023 Savoir-faire Linux Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +#include "connectionmanager.h" +#include "multiplexed_socket.h" +#include "ice_transport_factory.h" +#include "certstore.h" + +#include <asio.hpp> + +namespace dhtnet { +/** + * Attempt to retrieve the identity from the .ssh directory, and if none is found, generate a new + * certification. + * @return dht::crypto::Identity + */ + +class Dnc +{ +public: + // Build a server + Dnc(dht::crypto::Identity identity, + const std::string& bootstrap_ip_add, + const std::string& bootstrap_port); + // Build a client + Dnc(dht::crypto::Identity identity, + const std::string& bootstrap_ip_add, + const std::string& bootstrap_port, + dht::InfoHash peer_id, + int port, + const std::string& ip_add); + ~Dnc(); + void run(); + +private: + std::unique_ptr<ConnectionManager> connectionManager; + std::shared_ptr<Logger> logger; + tls::CertificateStore certStore; + IceTransportFactory iceFactory; + std::shared_ptr<asio::io_context> ioContext; + std::thread ioContextRunner; + + std::shared_ptr<asio::posix::stream_descriptor> stdinDescriptor; + + std::pair<std::string, std::string> parseName(const std::string_view name); +}; + +} // namespace dhtnet diff --git a/tools/dnc/main.cpp b/tools/dnc/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e0eeeeb55ac3a353195c80e56fa1cf03d475653e --- /dev/null +++ b/tools/dnc/main.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2004-2023 Savoir-faire Linux Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +#include "dnc.h" +#include "common.h" +#include <string> +#include <vector> +#include <iostream> +#include <unistd.h> +#include <getopt.h> + +#include <netinet/in.h> + +struct dhtnc_params +{ + bool help {false}; + bool version {false}; + bool listen {false}; + std::string ip_add {}; + std::string bootstrap_ip {}; + std::string bootstrap_port {}; + in_port_t port {}; + dht::InfoHash peer_id {}; +}; + +static const constexpr struct option long_options[] + = {{"help", no_argument, nullptr, 'h'}, + {"version", no_argument, nullptr, 'v'}, + {"port", required_argument, nullptr, 'p'}, + {"ip", required_argument, nullptr, 'i'}, + {"listen", no_argument, nullptr, 'l'}, + {"bootstrap_ip", required_argument, nullptr, 'b'}, + {"bootstrap_port", required_argument, nullptr, 'P'}, + {nullptr, 0, nullptr, 0}}; + +dhtnc_params +parse_args(int argc, char** argv) +{ + dhtnc_params params; + int opt; + while ((opt = getopt_long(argc, argv, "hvp:i:", long_options, nullptr)) != -1) { + switch (opt) { + case 'h': + params.help = true; + break; + case 'v': + params.version = true; + break; + case 'p': + params.port = std::stoi(optarg); + break; + case 'i': + params.ip_add = optarg; + break; + case 'l': + params.listen = true; + break; + case 'b': + params.bootstrap_ip = optarg; + break; + case 'P': + params.bootstrap_port = optarg; + break; + default: + std::cerr << "Invalid option" << std::endl; + exit(EXIT_FAILURE); + } + } + + // If not listening, the peer_id argument is required + if (!params.listen) { + if (optind < argc) { + params.peer_id = dht::InfoHash(argv[optind]); + optind++; // Move to the next argument + } else { + std::cerr << "Error: Missing peer_id argument.\n"; + exit(EXIT_FAILURE); + } + } + + // default values + if (params.port == 0) + params.port = 22; + if (params.ip_add.empty()) + params.ip_add = "127.0.0.1"; + if (params.bootstrap_ip.empty()) + params.bootstrap_ip = "bootstrap.jami.net"; + if (params.bootstrap_port.empty()) + params.bootstrap_port = "4222"; + return params; +} + +static void +setSipLogLevel() +{ + char* envvar = getenv("SIPLOGLEVEL"); + + int level = 0; + + if (envvar != nullptr) { + level = std::stoi(envvar); + + // From 0 (min) to 6 (max) + level = std::max(0, std::min(level, 6)); + } + + pj_log_set_level(level); + pj_log_set_log_func([](int level, const char* data, int /*len*/) { + }); +} + +int +main(int argc, char** argv) +{ + setSipLogLevel(); + auto params = parse_args(argc, argv); + + std::unique_ptr<dhtnet::Dnc> dhtnc; + if (params.listen) { + auto identity = dhtnet::loadIdentity(true); + // create dnc instance + dhtnc = std::make_unique<dhtnet::Dnc>(identity, params.bootstrap_ip, params.bootstrap_port); + fmt::print("DhtNC 1.1\n"); + fmt::print("Loaded identity: {}\n", identity.second->getId()); + } else { + auto identity = dhtnet::loadIdentity(false); + dhtnc = std::make_unique<dhtnet::Dnc>(identity, + params.bootstrap_ip, + params.bootstrap_port, + params.peer_id, + params.port, + params.ip_add); + fmt::print("DhtNC 1.0\n"); + fmt::print("Loaded identity: {}\n", identity.second->getId()); + } + dhtnc->run(); +}