Repository: lanthora/candy Branch: master Commit: fa41f0172719 Files: 86 Total size: 288.4 KB Directory structure: gitextract_0f5sny5p/ ├── .clang-format ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── check.yaml │ ├── release.yaml │ └── standalone.yaml ├── .gitignore ├── .vscode/ │ ├── c_cpp_properties.json │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── candy/ │ ├── .vscode/ │ │ ├── c_cpp_properties.json │ │ └── settings.json │ ├── CMakeLists.txt │ ├── include/ │ │ └── candy/ │ │ ├── candy.h │ │ ├── client.h │ │ ├── common.h │ │ └── server.h │ └── src/ │ ├── candy/ │ │ ├── client.cc │ │ └── server.cc │ ├── core/ │ │ ├── client.cc │ │ ├── client.h │ │ ├── common.cc │ │ ├── message.cc │ │ ├── message.h │ │ ├── net.cc │ │ ├── net.h │ │ ├── server.cc │ │ ├── server.h │ │ └── version.h │ ├── peer/ │ │ ├── manager.cc │ │ ├── manager.h │ │ ├── message.cc │ │ ├── message.h │ │ ├── peer.cc │ │ └── peer.h │ ├── tun/ │ │ ├── linux.cc │ │ ├── macos.cc │ │ ├── tun.cc │ │ ├── tun.h │ │ ├── unknown.cc │ │ └── windows.cc │ ├── utils/ │ │ ├── atomic.h │ │ ├── codecvt.cc │ │ ├── codecvt.h │ │ ├── random.cc │ │ ├── random.h │ │ ├── time.cc │ │ └── time.h │ └── websocket/ │ ├── client.cc │ ├── client.h │ ├── message.cc │ ├── message.h │ ├── server.cc │ └── server.h ├── candy-cli/ │ ├── CMakeLists.txt │ └── src/ │ ├── argparse.h │ ├── config.cc │ ├── config.h │ └── main.cc ├── candy-service/ │ ├── CMakeLists.txt │ ├── README.md │ └── src/ │ └── main.cc ├── candy.cfg ├── candy.initd ├── candy.service ├── candy@.service ├── cmake/ │ ├── Fetch.cmake │ └── openssl/ │ └── CMakeLists.txt ├── dockerfile ├── docs/ │ ├── CNAME │ ├── _config.yml │ ├── deploy-cli-server.md │ ├── deploy-web-server.md │ ├── index.md │ ├── install-client-for-linux.md │ ├── install-client-for-macos.md │ ├── install-client-for-windows.md │ ├── software-defined-wide-area-network.md │ └── use-the-community-server.md └── scripts/ ├── build-standalone.sh ├── search-deps.sh └── standalone.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- BasedOnStyle: LLVM IndentWidth: 4 TabWidth: 8 AccessModifierOffset: -4 AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false ColumnLimit: 130 IndentCaseLabels: false SortIncludes: true ... ================================================ FILE: .dockerignore ================================================ .git .github .vscode build/* ================================================ FILE: .github/workflows/check.yaml ================================================ name: check on: pull_request: branches: [master] jobs: format: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: check format uses: jidicula/clang-format-action@v4.11.0 with: check-path: 'src' exclude-regex: 'argparse.h' linux: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: build run: docker build . macos: runs-on: macos-latest steps: - name: depends run: brew update && brew install fmt poco spdlog - name: checkout uses: actions/checkout@v4 - name: build run: | if [ "$RUNNER_ARCH" == "ARM64" ]; then export CPATH=/opt/homebrew/include export LIBRARY_PATH=/opt/homebrew/lib else export CPATH=/usr/local/include export LIBRARY_PATH=/usr/local/lib fi cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build windows: runs-on: windows-latest steps: - name: depends uses: msys2/setup-msys2@v2 with: msystem: MINGW64 update: true install: >- mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-gcc mingw-w64-x86_64-spdlog mingw-w64-x86_64-poco - name: checkout uses: actions/checkout@v4 - name: cache uses: actions/cache@v4 with: path: build key: ${{ hashFiles('CMakeLists.txt') }} - name: build shell: msys2 {0} run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build ================================================ FILE: .github/workflows/release.yaml ================================================ name: release on: push: branches: [ master ] release: types: [ published ] jobs: docker: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: setup qemu uses: docker/setup-qemu-action@v3 - name: setup docker buildx uses: docker/setup-buildx-action@v3 - name: login docker hub uses: docker/login-action@v3 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: login github container registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: setup version if: github.event_name == 'release' run: | GIT_TAG=${{ github.event.release.tag_name }} echo "IMAGE_TAG=${GIT_TAG#v}" >> $GITHUB_ENV - name: build and push uses: docker/build-push-action@v5 if: github.event_name == 'release' with: context: . provenance: false platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: | docker.io/${{ secrets.DOCKERHUB_USERNAME }}/candy:${{ env.IMAGE_TAG }} docker.io/${{ secrets.DOCKERHUB_USERNAME }}/candy:latest ghcr.io/${{ github.actor }}/candy:${{ env.IMAGE_TAG }} ghcr.io/${{ github.actor }}/candy:latest windows: runs-on: windows-latest steps: - name: setup msys2 uses: msys2/setup-msys2@v2 with: msystem: MINGW64 update: true install: >- mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-gcc mingw-w64-x86_64-spdlog mingw-w64-x86_64-poco - name: checkout uses: actions/checkout@v4 - name: cache uses: actions/cache@v4 with: path: build key: ${{ hashFiles('CMakeLists.txt') }} - name: build shell: msys2 {0} run: | cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build mkdir artifact cp candy.cfg artifact cp build/candy/wintun/bin/amd64/wintun.dll artifact scripts/search-deps.sh build/candy-cli/candy.exe artifact scripts/search-deps.sh build/candy-service/candy-service.exe artifact - name: set release package name shell: bash if: github.event_name == 'release' run: | GIT_TAG=${{ github.event.release.tag_name }} echo "PKGNAME=candy_${GIT_TAG#v}+windows_amd64" >> $GITHUB_ENV - name: upload artifact uses: actions/upload-artifact@v4 with: name: windows-${{ github.event.release.tag_name || github.sha }} path: artifact - name: zip release uses: thedoctor0/zip-release@0.7.5 if: github.event_name == 'release' with: type: 'zip' filename: ${{ env.PKGNAME }}.zip directory: artifact - name: upload release uses: softprops/action-gh-release@v2 if: github.event_name == 'release' with: files: artifact/${{ env.PKGNAME }}.zip ================================================ FILE: .github/workflows/standalone.yaml ================================================ name: standalone on: workflow_dispatch: release: types: [ published ] pull_request: branches: [master] paths: - 'scripts/build-standalone.sh' - 'scripts/standalone.json' jobs: configure: runs-on: ubuntu-latest outputs: matrix: ${{ steps.fetch.outputs.matrix }} steps: - name: Checkout to repository uses: actions/checkout@v4 - name: fetch matrix data id: fetch run: echo "matrix=$(jq -c . < scripts/standalone.json)" >> $GITHUB_OUTPUT build: runs-on: ubuntu-latest needs: configure strategy: fail-fast: false matrix: ${{ fromJson(needs.configure.outputs.matrix) }} env: WORKSPACE: "/opt" steps: - name: checkout uses: actions/checkout@v4 - name: Install UPX uses: crazy-max/ghaction-upx@v3 with: install-only: true - name: cache uses: actions/cache@v4 with: path: ${{ env.WORKSPACE }}/toolchains key: ${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('scripts/build-standalone.sh') }} - name: Cross compile run: | ./scripts/build-standalone.sh env: CANDY_WORKSPACE: ${{ env.WORKSPACE }} CANDY_OS: ${{ matrix.os }} CANDY_ARCH: ${{ matrix.arch }} CANDY_STRIP: "0" CANDY_UPX: "0" CANDY_TGZ: "1" - name: upload uses: actions/upload-artifact@v4 with: name: candy-${{ matrix.os }}-${{ matrix.arch }} path: ${{ env.WORKSPACE }}/output/${{ matrix.os }}-${{ matrix.arch }} - name: prepare package shell: bash if: github.event_name == 'release' run: | GIT_TAG=${{ github.event.release.tag_name }} PKG_PATH=${{ env.WORKSPACE }}/output/candy_${GIT_TAG#v}+${{ matrix.os }}_${{ matrix.arch }}.tar.gz mv ${{ env.WORKSPACE }}/output/candy-${{ matrix.os }}-${{ matrix.arch }}.tar.gz $PKG_PATH echo "PKG_PATH=$PKG_PATH" >> $GITHUB_ENV - name: release uses: softprops/action-gh-release@v2 if: github.event_name == 'release' with: files: ${{ env.PKG_PATH }} ================================================ FILE: .gitignore ================================================ # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # CMake Build files .cache build ================================================ FILE: .vscode/c_cpp_properties.json ================================================ { "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**" ], "defines": [], "compilerPath": "/usr/bin/clang", "cStandard": "c17", "intelliSenseMode": "linux-clang-x64" } ], "version": 4 } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "ms-vscode.cpptools-extension-pack" ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/build/src/main/candy", "args": [ "-c", "candy.cfg" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true }, { "description": "Set Disassembly Flavor to Intel", "text": "-gdb-set disassembly-flavor intel", "ignoreFailures": true } ] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.detectIndentation": false, "editor.tabSize": 4, "editor.formatOnSave": true, "editor.insertSpaces": true, "editor.formatOnSaveMode": "modifications", "files.insertFinalNewline": true, "json.format.enable": true, "C_Cpp.default.cppStandard": "c++23", "C_Cpp.autoAddFileAssociations": false, "C_Cpp.errorSquiggles": "disabled" } ================================================ FILE: .vscode/tasks.json ================================================ { "tasks": [ { "type": "cppbuild", "label": "C/C++: g++ build active file", "command": "/usr/bin/g++", "args": [ "-fdiagnostics-color=always", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build", "isDefault": true }, "detail": "Task generated by Debugger." } ], "version": "2.0.0" } ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16) project(Candy VERSION 6.1.7) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_definitions(CANDY_VERSION="${PROJECT_VERSION}") set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(CMAKE_SKIP_BUILD_RPATH TRUE) if (${CANDY_STATIC}) set(CANDY_STATIC_OPENSSL 1) set(CANDY_STATIC_SPDLOG 1) set(CANDY_STATIC_NLOHMANN_JSON 1) set(CANDY_STATIC_POCO 1) endif() find_package(PkgConfig REQUIRED) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Fetch.cmake) if (${CANDY_STATIC_OPENSSL}) execute_process( COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/openssl ) execute_process( COMMAND ${CMAKE_COMMAND} -DTARGET_OPENSSL=${TARGET_OPENSSL} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/openssl WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/openssl RESULT_VARIABLE result ) if(NOT result EQUAL "0") message(FATAL_ERROR "OpenSSL CMake failed") endif() execute_process( COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/openssl RESULT_VARIABLE result ) if(NOT result EQUAL "0") message(FATAL_ERROR "OpenSSL Download or Configure failed") endif() include(ProcessorCount) ProcessorCount(nproc) if(nproc EQUAL 0) set(nproc 1) endif() set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/openssl/openssl/src/openssl) execute_process( COMMAND make -j${nproc} WORKING_DIRECTORY ${OPENSSL_ROOT_DIR} RESULT_VARIABLE result ) if(NOT result EQUAL "0") message(FATAL_ERROR "OpenSSL Build failed") endif() set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/openssl/openssl/src/openssl) set(OPENSSL_INCLUDE ${OPENSSL_ROOT_DIR}/include) set(OPENSSL_LIB_CRYPTO ${OPENSSL_ROOT_DIR}/libcrypto.a) set(OPENSSL_LIB_SSL ${OPENSSL_ROOT_DIR}/libssl.a) include_directories(${OPENSSL_INCLUDE}) else() find_package(OpenSSL REQUIRED) endif() if (${CANDY_STATIC_SPDLOG}) Fetch(spdlog "https://github.com/gabime/spdlog.git" "v1.15.3") else() find_package(spdlog REQUIRED) endif() if (${CANDY_STATIC_POCO}) set(ENABLE_DATA OFF CACHE BOOL "" FORCE) set(ENABLE_DATA_MYSQL OFF CACHE BOOL "" FORCE) set(ENABLE_DATA_POSTGRESQL OFF CACHE BOOL "" FORCE) set(ENABLE_DATA_SQLITE OFF CACHE BOOL "" FORCE) set(ENABLE_DATA_ODBC OFF CACHE BOOL "" FORCE) set(ENABLE_MONGODB OFF CACHE BOOL "" FORCE) set(ENABLE_REDIS OFF CACHE BOOL "" FORCE) set(ENABLE_ENCODINGS OFF CACHE BOOL "" FORCE) set(ENABLE_PROMETHEUS OFF CACHE BOOL "" FORCE) set(ENABLE_PAGECOMPILER OFF CACHE BOOL "" FORCE) set(ENABLE_PAGECOMPILER_FILE2PAGE OFF CACHE BOOL "" FORCE) set(ENABLE_ACTIVERECORD OFF CACHE BOOL "" FORCE) set(ENABLE_ACTIVERECORD_COMPILER OFF CACHE BOOL "" FORCE) set(ENABLE_ZIP OFF CACHE BOOL "" FORCE) set(ENABLE_JWT OFF CACHE BOOL "" FORCE) Fetch(poco "https://github.com/pocoproject/poco.git" "poco-1.13.3-release") else() find_package(Poco REQUIRED COMPONENTS Foundation XML JSON Net NetSSL Util) endif() set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) add_subdirectory(candy) add_subdirectory(candy-cli) add_subdirectory(candy-service) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 lanthora Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Candy

一个简单的组网工具. ## 如何使用 - [安装 Windows 客户端](https://docs.canets.org/install-client-for-windows) - [安装 macOS 客户端](https://docs.canets.org/install-client-for-macos) - [安装 Linux 客户端](https://docs.canets.org/install-client-for-linux) - [部署 Web 服务端](https://docs.canets.org/deploy-web-server) - [部署 CLI 服务端](https://docs.canets.org/deploy-cli-server) - [使用社区服务器](https://docs.canets.org/use-the-community-server) - [多局域网组网](https://docs.canets.org/software-defined-wide-area-network) ## 相关项目 - [Cacao](https://github.com/lanthora/cacao): WebUI 版的 Candy 服务器 - [Cake](https://github.com/lanthora/cake): Qt 实现的 Candy GUI 桌面应用程序 - [Candy Android](https://github.com/Jercrox/Candy_Android_Client): Android 客户端 - [EasyTier](https://github.com/EasyTier/EasyTier): 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现 - [candygo](https://github.com/SoraKasvgano/candygo): 一个简单的与candy原项目配置文件兼容的go版本 ## 交流群 - QQ: 768305206 - TG: [Click to Join](https://t.me/CandyUserGroup) ================================================ FILE: candy/.vscode/c_cpp_properties.json ================================================ { "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**" ], "defines": [], "compilerPath": "/usr/bin/clang", "cStandard": "c17", "intelliSenseMode": "linux-clang-x64" } ], "version": 4 } ================================================ FILE: candy/.vscode/settings.json ================================================ { "editor.detectIndentation": false, "editor.tabSize": 4, "editor.formatOnSave": true, "editor.insertSpaces": true, "editor.formatOnSaveMode": "modifications", "files.insertFinalNewline": true, "json.format.enable": true, "C_Cpp.default.cppStandard": "c++17", "C_Cpp.autoAddFileAssociations": false, "C_Cpp.errorSquiggles": "disabled" } ================================================ FILE: candy/CMakeLists.txt ================================================ add_library(candy-library) file(GLOB_RECURSE SOURCES "src/*.cc") target_sources(candy-library PRIVATE ${SOURCES}) target_include_directories(candy-library PUBLIC $ $ $ ) if (${CANDY_STATIC_OPENSSL}) target_link_libraries(candy-library PRIVATE ${OPENSSL_LIB_CRYPTO} ${OPENSSL_LIB_SSL}) else() target_link_libraries(candy-library PRIVATE OpenSSL::SSL OpenSSL::Crypto) endif() target_link_libraries(candy-library PRIVATE spdlog::spdlog) target_link_libraries(candy-library PRIVATE Poco::Foundation Poco::JSON Poco::Net Poco::NetSSL) target_link_libraries(candy-library PRIVATE Threads::Threads) if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") target_link_libraries(candy-library PRIVATE ws2_32) endif() if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") target_link_libraries(candy-library PRIVATE iphlpapi) target_link_libraries(candy-library PRIVATE ws2_32) set(WINTUN_VERSION 0.14.1) set(WINTUN_ZIP wintun-${WINTUN_VERSION}.zip) set(WINTUN_URL https://www.wintun.net/builds/${WINTUN_ZIP}) if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP}) file(DOWNLOAD ${WINTUN_URL} ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP} STATUS DOWNLOAD_STATUS) list(GET DOWNLOAD_STATUS 0 STATUS_CODE) list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE) if(${STATUS_CODE} EQUAL 0) message(STATUS "wintun download success") else() message(FATAL_ERROR "wintun download failed: ${ERROR_MESSAGE}") endif() else() message(STATUS "use wintun cache") endif() file(ARCHIVE_EXTRACT INPUT ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP}) include_directories(${CMAKE_CURRENT_BINARY_DIR}/wintun/include) endif() set_target_properties(candy-library PROPERTIES OUTPUT_NAME "candy") add_library(Candy::Library ALIAS candy-library) ================================================ FILE: candy/include/candy/candy.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CANDY_H #define CANDY_CANDY_H #include "client.h" #include "common.h" #include "server.h" #endif ================================================ FILE: candy/include/candy/client.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CLIENT_H #define CANDY_CLIENT_H #include #include #include namespace candy { namespace client { bool run(const std::string &id, const Poco::JSON::Object &config); bool shutdown(const std::string &id); std::optional status(const std::string &id); } // namespace client } // namespace candy #endif ================================================ FILE: candy/include/candy/common.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_COMMON_H #define CANDY_COMMON_H #include namespace candy { static const int VMAC_SIZE = 16; std::string version(); std::string create_vmac(); } // namespace candy #endif ================================================ FILE: candy/include/candy/server.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_SERVER_H #define CANDY_SERVER_H #include #include namespace candy { namespace server { bool run(const Poco::JSON::Object &config); bool shutdown(); } // namespace server } // namespace candy #endif ================================================ FILE: candy/src/candy/client.cc ================================================ // SPDX-License-Identifier: MIT #include "candy/client.h" #include "core/client.h" #include "utils/atomic.h" #include #include #include #include #include #include #include #include namespace candy { namespace client { namespace { using Utils::Atomic; class Instance { public: bool is_running() { return this->running.load(); } void exit() { this->running.store(false); if (auto client = this->client.lock()) { client->shutdown(); } } Poco::JSON::Object status() { Poco::JSON::Object data; if (auto client = this->client.lock()) { data.set("address", client->getTunCidr()); } return data; } std::shared_ptr create_client() { auto client = std::make_shared(); this->client = client; return client; } private: Atomic running = Atomic(true); std::weak_ptr client; }; std::map> instance_map; std::shared_mutex instance_mutex; std::optional> try_create_instance(const std::string &id) { std::unique_lock lock(instance_mutex); auto it = instance_map.find(id); if (it != instance_map.end()) { spdlog::warn("instance already exists: id={}", id); return std::nullopt; } auto manager = std::make_shared(); instance_map.emplace(id, manager); return manager; } bool try_erase_instance(const std::string &id) { std::unique_lock lock(instance_mutex); return instance_map.erase(id) > 0; } } // namespace bool run(const std::string &id, const Poco::JSON::Object &config) { auto instance = try_create_instance(id); if (!instance) { return false; } auto toString = [](const Poco::JSON::Object &obj) -> std::string { std::ostringstream oss; Poco::JSON::Stringifier::stringify(obj, oss); return oss.str(); }; spdlog::info("run enter: id={} config={}", id, toString(config)); while ((*instance)->is_running()) { std::this_thread::sleep_for(std::chrono::seconds(1)); auto client = (*instance)->create_client(); client->setName(config.getValue("name")); client->setPassword(config.getValue("password")); client->setWebSocket(config.getValue("websocket")); client->setTunAddress(config.getValue("tun")); client->setVirtualMac(config.getValue("vmac")); client->setExptTunAddress(config.getValue("expt")); client->setStun(config.getValue("stun")); client->setDiscoveryInterval(config.getValue("discovery")); client->setRouteCost(config.getValue("route")), client->setMtu(config.getValue("mtu")); client->setPort(config.getValue("port")); client->setLocalhost(config.getValue("localhost")); client->run(); } spdlog::info("run exit: id={} ", id); return try_erase_instance(id); } bool shutdown(const std::string &id) { std::shared_lock lock(instance_mutex); auto it = instance_map.find(id); if (it == instance_map.end()) { spdlog::warn("instance not found: id={}", id); return false; } if (auto instance = it->second) { instance->exit(); } return true; } std::optional status(const std::string &id) { std::shared_lock lock(instance_mutex); auto it = instance_map.find(id); if (it != instance_map.end()) { if (auto instance = it->second) { return instance->status(); } } return std::nullopt; } } // namespace client } // namespace candy ================================================ FILE: candy/src/candy/server.cc ================================================ // SPDX-License-Identifier: MIT #include "candy/server.h" #include "core/server.h" #include "utils/atomic.h" namespace candy { namespace server { namespace { Utils::Atomic running(true); std::shared_ptr server; } // namespace bool run(const Poco::JSON::Object &config) { while (running.load()) { std::this_thread::sleep_for(std::chrono::seconds(1)); server = std::make_shared(); server->setWebSocket(config.getValue("websocket")); server->setPassword(config.getValue("password")); server->setDHCP(config.getValue("dhcp")); server->setSdwan(config.getValue("sdwan")); server->run(); } return true; } bool shutdown() { running.store(false); server->shutdown(); return true; } } // namespace server } // namespace candy ================================================ FILE: candy/src/core/client.cc ================================================ // SPDX-License-Identifier: MIT #include "core/client.h" #include "core/message.h" #include #include namespace candy { Msg MsgQueue::read() { std::unique_lock lock(msgMutex); if (!msgCondition.wait_for(lock, std::chrono::seconds(1), [this] { return !msgQueue.empty(); })) { return Msg(MsgKind::TIMEOUT); } Msg msg = std::move(msgQueue.front()); msgQueue.pop(); return msg; } void MsgQueue::write(Msg msg) { { std::unique_lock lock(this->msgMutex); msgQueue.push(std::move(msg)); } msgCondition.notify_one(); } void MsgQueue::clear() { std::unique_lock lock(this->msgMutex); while (!msgQueue.empty()) { msgQueue.pop(); } } void Client::setName(const std::string &name) { this->tunName = name; tun.setName(name); ws.setName(name); } std::string Client::getName() const { return this->tunName; } std::string Client::getTunCidr() const { return ws.getTunCidr(); } IP4 Client::address() { return this->tun.getIP(); } MsgQueue &Client::getTunMsgQueue() { return this->tunMsgQueue; } MsgQueue &Client::getPeerMsgQueue() { return this->peerMsgQueue; } MsgQueue &Client::getWsMsgQueue() { return this->wsMsgQueue; } void Client::setPassword(const std::string &password) { ws.setPassword(password); peerManager.setPassword(password); } void Client::setWebSocket(const std::string &uri) { ws.setWsServerUri(uri); } void Client::setTunAddress(const std::string &cidr) { ws.setAddress(cidr); } void Client::setExptTunAddress(const std::string &cidr) { ws.setExptTunAddress(cidr); } void Client::setVirtualMac(const std::string &vmac) { ws.setVirtualMac(vmac); } void Client::setStun(const std::string &stun) { peerManager.setStun(stun); } void Client::setDiscoveryInterval(int interval) { peerManager.setDiscoveryInterval(interval); } void Client::setRouteCost(int cost) { peerManager.setRouteCost(cost); } void Client::setPort(int port) { peerManager.setPort(port); } void Client::setLocalhost(std::string ip) { peerManager.setLocalhost(ip); } void Client::setMtu(int mtu) { tun.setMTU(mtu); } void Client::run() { this->running.store(true); if (ws.run(this)) { return; } if (tun.run(this)) { return; } if (peerManager.run(this)) { return; } ws.wait(); tun.wait(); peerManager.wait(); wsMsgQueue.clear(); tunMsgQueue.clear(); peerMsgQueue.clear(); } bool Client::isRunning() { return this->running.load(); } void Client::shutdown() { this->running.store(false); } } // namespace candy ================================================ FILE: candy/src/core/client.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CORE_CLIENT_H #define CANDY_CORE_CLIENT_H #include "core/message.h" #include "peer/manager.h" #include "tun/tun.h" #include "utils/atomic.h" #include "websocket/client.h" #include #include #include #include namespace candy { class MsgQueue { public: Msg read(); void write(Msg msg); void clear(); private: std::queue msgQueue; std::mutex msgMutex; std::condition_variable msgCondition; }; class Client { public: void setName(const std::string &name); void setPassword(const std::string &password); void setWebSocket(const std::string &uri); void setTunAddress(const std::string &cidr); void setStun(const std::string &stun); void setDiscoveryInterval(int interval); void setRouteCost(int cost); void setPort(int port); void setLocalhost(std::string ip); void setMtu(int mtu); void setExptTunAddress(const std::string &cidr); void setVirtualMac(const std::string &vmac); void run(); bool isRunning(); void shutdown(); std::string getName() const; std::string getTunCidr() const; IP4 address(); private: Utils::Atomic running; public: MsgQueue &getTunMsgQueue(); MsgQueue &getPeerMsgQueue(); MsgQueue &getWsMsgQueue(); private: MsgQueue tunMsgQueue, peerMsgQueue, wsMsgQueue; Tun tun; PeerManager peerManager; WebSocketClient ws; private: std::string tunName; }; } // namespace candy #endif ================================================ FILE: candy/src/core/common.cc ================================================ #include "candy/common.h" #include "core/version.h" #include "utils/random.h" #include namespace candy { std::string version() { return CANDY_VERSION; } std::string create_vmac() { return randomHexString(VMAC_SIZE); } } // namespace candy ================================================ FILE: candy/src/core/message.cc ================================================ // SPDX-License-Identifier: MIT #include "core/message.h" namespace candy { Msg::Msg(MsgKind kind, std::string data) { this->kind = kind; this->data = std::move(data); } Msg::Msg(Msg &&packet) { kind = packet.kind; data = std::move(packet.data); } Msg &Msg::operator=(Msg &&packet) { kind = packet.kind; data = std::move(packet.data); return *this; } } // namespace candy ================================================ FILE: candy/src/core/message.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CORE_MESSAGE_H #define CANDY_CORE_MESSAGE_H #include "core/net.h" #include #include namespace candy { enum class MsgKind { TIMEOUT, PACKET, TUNADDR, SYSRT, TRYP2P, PUBINFO, DISCOVERY, }; struct Msg { MsgKind kind; std::string data; Msg(const Msg &) = delete; Msg &operator=(const Msg &) = delete; Msg(MsgKind kind = MsgKind::TIMEOUT, std::string = ""); Msg(Msg &&packet); Msg &operator=(Msg &&packet); }; namespace CoreMsg { struct PubInfo { IP4 src; IP4 dst; IP4 ip; uint16_t port; bool local = false; }; } // namespace CoreMsg } // namespace candy #endif ================================================ FILE: candy/src/core/net.cc ================================================ // SPDX-License-Identifier: MIT #include "core/net.h" #include #include #include namespace candy { IP4::IP4(const std::string &ip) { fromString(ip); } IP4 IP4::operator=(const std::string &ip) { fromString(ip); return *this; } IP4::operator std::string() const { return toString(); } IP4::operator uint32_t() const { uint32_t val = 0; std::memcpy(&val, raw.data(), sizeof(val)); return val; } IP4 IP4::operator|(IP4 another) const { for (int i = 0; i < raw.size(); ++i) { another.raw[i] |= raw[i]; } return another; } IP4 IP4::operator^(IP4 another) const { for (int i = 0; i < raw.size(); ++i) { another.raw[i] ^= raw[i]; } return another; } IP4 IP4::operator~() const { IP4 retval; for (int i = 0; i < raw.size(); ++i) { retval.raw[i] |= ~raw[i]; } return retval; } bool IP4::operator==(IP4 another) const { return raw == another.raw; } IP4 IP4::operator&(IP4 another) const { for (int i = 0; i < raw.size(); ++i) { another.raw[i] &= raw[i]; } return another; } IP4 IP4::next() const { IP4 ip; uint32_t t = hton(ntoh(uint32_t(*this)) + 1); std::memcpy(&ip, &t, sizeof(ip)); return ip; } int IP4::fromString(const std::string &ip) { memcpy(raw.data(), Poco::Net::IPAddress(ip).addr(), 4); return 0; } std::string IP4::toString() const { return Poco::Net::IPAddress(raw.data(), sizeof(raw)).toString(); } int IP4::fromPrefix(int prefix) { std::memset(raw.data(), 0, sizeof(raw)); for (int i = 0; i < prefix; ++i) { raw[i / 8] |= (0x80 >> (i % 8)); } return 0; } int IP4::toPrefix() { int i; for (i = 0; i < 32; ++i) { if (!(raw[i / 8] & (0x80 >> (i % 8)))) { break; } } return i; } bool IP4::empty() const { return raw[0] == 0 && raw[1] == 0 && raw[2] == 0 && raw[3] == 0; } void IP4::reset() { this->raw.fill(0); } bool IP4Header::isIPv4() { return (this->version_ihl >> 4) == 4; } bool IP4Header::isIPIP() { return this->protocol == 0x04; } Address::Address() {} Address::Address(const std::string &cidr) { if (!cidr.empty()) { fromCidr(cidr); } } IP4 &Address::Host() { return this->host; } IP4 &Address::Mask() { return this->mask; } IP4 Address::Net() { return Host() & Mask(); } Address Address::Next() { Address next; next.mask = this->mask; next.host = (Net() | (~Mask() & this->host.next())); return next; } bool Address::isValid() { if ((~mask & host) == 0) { return false; } if (~(mask | host) == 0) { return false; } return true; } int Address::fromCidr(const std::string &cidr) { try { std::size_t pos = cidr.find('/'); host.fromString(cidr.substr(0UL, pos)); mask.fromPrefix(std::stoi(cidr.substr(pos + 1))); } catch (std::exception &e) { spdlog::warn("address parse cidr failed: {}: {}", e.what(), cidr); return -1; } return 0; } std::string Address::toCidr() { return host.toString() + "/" + std::to_string(mask.toPrefix()); } } // namespace candy ================================================ FILE: candy/src/core/net.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CORE_NET_H #define CANDY_CORE_NET_H #include #include #include #include #include namespace candy { template typename std::enable_if::value, T>::type byteswap(T value) { static_assert(std::is_integral::value, "byteswap requires integral type"); union { T value; uint8_t bytes[sizeof(T)]; } src, dst; src.value = value; for (size_t i = 0; i < sizeof(T); i++) { dst.bytes[i] = src.bytes[sizeof(T) - i - 1]; } return dst.value; } template T ntoh(T v) { static_assert(std::is_integral::value, "ntoh requires integral type"); uint8_t *bytes = reinterpret_cast(&v); bool isLittleEndian = true; { uint16_t test = 0x0001; isLittleEndian = (*reinterpret_cast(&test) == 0x01); } if (isLittleEndian) { return byteswap(v); } return v; } template T hton(T v) { return ntoh(v); } class __attribute__((packed)) IP4 { public: IP4(const std::string &ip = "0.0.0.0"); IP4 operator=(const std::string &ip); IP4 operator&(IP4 another) const; IP4 operator|(IP4 another) const; IP4 operator^(IP4 another) const; IP4 operator~() const; bool operator==(IP4 another) const; operator std::string() const; operator uint32_t() const; IP4 next() const; int fromString(const std::string &ip); std::string toString() const; int fromPrefix(int prefix); int toPrefix(); bool empty() const; void reset(); private: std::array raw; }; struct __attribute__((packed)) IP4Header { uint8_t version_ihl; uint8_t tos; uint16_t tot_len; uint16_t id; uint16_t frag_off; uint8_t ttl; uint8_t protocol; uint16_t check; IP4 saddr; IP4 daddr; bool isIPv4(); bool isIPIP(); }; struct __attribute__((packed)) SysRouteEntry { IP4 dst; IP4 mask; IP4 nexthop; }; /* 用于表示地址和掩码的组合,用于判断主机是否属于某个网络 */ class Address { public: Address(); Address(const std::string &cidr); IP4 &Host(); IP4 &Mask(); IP4 Net(); // 当前网络内的下一个地址 Address Next(); // 判断是否是有效的主机地址 bool isValid(); int fromCidr(const std::string &cidr); std::string toCidr(); bool empty() const { return host.empty() && mask.empty(); } private: IP4 host; IP4 mask; }; } // namespace candy namespace std { using candy::IP4; template <> struct hash { size_t operator()(const IP4 &ip) const noexcept { return hash{}(ip); } }; } // namespace std namespace { constexpr std::size_t AES_256_GCM_IV_LEN = 12; constexpr std::size_t AES_256_GCM_TAG_LEN = 16; constexpr std::size_t AES_256_GCM_KEY_LEN = 32; } // namespace #endif ================================================ FILE: candy/src/core/server.cc ================================================ // SPDX-License-Identifier: MIT #include "core/server.h" namespace candy { void Server::setWebSocket(const std::string &uri) { ws.setWebSocket(uri); } void Server::setPassword(const std::string &password) { ws.setPassword(password); } void Server::setDHCP(const std::string &cidr) { ws.setDHCP(cidr); } void Server::setSdwan(const std::string &sdwan) { ws.setSdwan(sdwan); } void Server::run() { running.store(true); ws.run(); running.wait(true); ws.shutdown(); } void Server::shutdown() { running.store(false); } } // namespace candy ================================================ FILE: candy/src/core/server.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CORE_SERVER_H #define CANDY_CORE_SERVER_H #include "utils/atomic.h" #include "websocket/server.h" #include namespace candy { class Server { public: // 通过配置文件或命令行设置的参数 void setWebSocket(const std::string &uri); void setPassword(const std::string &password); void setDHCP(const std::string &cidr); void setSdwan(const std::string &sdwan); // 启动服务端,非阻塞 void run(); // 关闭客户端,阻塞,直到所有子模块退出 void shutdown(); private: // 目前只有一个 WebSocket 服务端的子模块 WebSocketServer ws; Utils::Atomic running; }; } // namespace candy #endif ================================================ FILE: candy/src/core/version.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CORE_VERSION_H #define CANDY_CORE_VERSION_H #include #if POCO_OS == POCO_OS_LINUX #define CANDY_SYSTEM "linux" #elif POCO_OS == POCO_OS_MAC_OS_X #define CANDY_SYSTEM "macos" #elif POCO_OS == POCO_OS_ANDROID #define CANDY_SYSTEM "android" #elif POCO_OS == POCO_OS_WINDOWS_NT #define CANDY_SYSTEM "windows" #else #define CANDY_SYSTEM "unknown" #endif #ifndef CANDY_VERSION #define CANDY_VERSION "unknown" #endif #endif ================================================ FILE: candy/src/peer/manager.cc ================================================ // SPDX-License-Identifier: MIT #include "peer/manager.h" #include "core/client.h" #include "core/message.h" #include "core/net.h" #include "peer/message.h" #include "utils/time.h" #include #include #include #include #include #include #include namespace candy { int PeerManager::setPassword(const std::string &password) { this->password = password; return 0; } int PeerManager::setStun(const std::string &stun) { this->stun.uri = stun; return 0; } int PeerManager::setDiscoveryInterval(int interval) { this->discoveryInterval = interval; return 0; } int PeerManager::setRouteCost(int cost) { if (cost < 0) { this->routeCost = 0; } else if (cost > 1000) { this->routeCost = 1000; } else { this->routeCost = cost; } return 0; } int PeerManager::setPort(int port) { if (port > 0 && port <= UINT16_MAX) { this->listenPort = port; } return 0; } int PeerManager::setLocalhost(const std::string &ip) { this->localhost.fromString(ip); return 0; } int PeerManager::run(Client *client) { this->client = client; this->localP2PDisabled = false; if (this->stun.update()) { spdlog::critical("update stun failed"); return -1; } this->msgThread = std::thread([&] { spdlog::debug("start thread: peer manager msg"); while (getClient().isRunning()) { if (handlePeerQueue()) { break; } } getClient().shutdown(); spdlog::debug("stop thread: peer manager msg"); }); return 0; } int PeerManager::wait() { if (this->msgThread.joinable()) { this->msgThread.join(); } if (this->tickThread.joinable()) { this->tickThread.join(); } if (this->pollThread.joinable()) { this->pollThread.join(); } this->socket.close(); { std::unique_lock lock(this->rtTableMutex); this->rtTableMap.clear(); } { std::unique_lock lock(this->ipPeerMutex); this->ipPeerMap.clear(); } return 0; } std::string PeerManager::getPassword() { return this->password; } int PeerManager::handlePeerQueue() { Msg msg = getClient().getPeerMsgQueue().read(); try { switch (msg.kind) { case MsgKind::TIMEOUT: break; case MsgKind::PACKET: handlePacket(std::move(msg)); break; case MsgKind::TUNADDR: if (startTickThread()) { return -1; } if (handleTunAddr(std::move(msg))) { return -1; } break; case MsgKind::SYSRT: this->localP2PDisabled = true; break; case MsgKind::TRYP2P: handleTryP2P(std::move(msg)); break; case MsgKind::PUBINFO: handlePubInfo(std::move(msg)); break; default: spdlog::warn("unexcepted peer message type: {}", static_cast(msg.kind)); break; } } catch (const Poco::Exception &e) { spdlog::warn("peer manager handle queue failed: msg_kind={}, error={}", static_cast(msg.kind), e.message()); return 0; } catch (const std::exception &e) { spdlog::warn("peer manager handle queue failed: msg_kind={}, error={}", static_cast(msg.kind), e.what()); return 0; } return 0; } int PeerManager::sendPacket(IP4 dst, const Msg &msg) { if (!sendPacketRelay(dst, msg)) { return 0; } if (!sendPacketDirect(dst, msg)) { return 0; } return -1; } int PeerManager::sendPacketDirect(IP4 dst, const Msg &msg) { std::shared_lock ipPeerLock(this->ipPeerMutex); auto it = this->ipPeerMap.find(dst); if (it != this->ipPeerMap.end()) { auto &peer = it->second; if (peer.isConnected()) { return peer.sendEncrypted(PeerMsg::Forward::create(msg.data)); } } return -1; } int PeerManager::sendPacketRelay(IP4 dst, const Msg &msg) { { std::shared_lock rtTableLock(this->rtTableMutex); auto it = this->rtTableMap.find(dst); if (it == this->rtTableMap.end()) { return -1; } dst = it->second.next; } return sendPacketDirect(dst, msg); } int PeerManager::sendPubInfo(CoreMsg::PubInfo info) { info.src = getClient().address(); if (info.local) { info.ip = this->localhost; info.port = this->socket.address().port(); } else { info.ip = this->stun.ip; info.port = this->stun.port; } getClient().getWsMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info)))); return 0; } IP4 PeerManager::getTunIp() { return this->tunAddr.Host(); } int PeerManager::handlePacket(Msg msg) { auto header = (IP4Header *)msg.data.data(); if (!sendPacket(header->daddr, msg)) { return 0; } getClient().getWsMsgQueue().write(std::move(msg)); return 0; } int PeerManager::handleTunAddr(Msg msg) { if (this->tunAddr.fromCidr(msg.data)) { spdlog::error("set tun addr failed: {}", msg.data); return -1; } std::string data; data.append(this->password); auto leaddr = hton(uint32_t(this->tunAddr.Host())); data.append((char *)&leaddr, sizeof(leaddr)); this->key.resize(SHA256_DIGEST_LENGTH); SHA256((unsigned char *)data.data(), data.size(), (unsigned char *)this->key.data()); return 0; } int PeerManager::handleTryP2P(Msg msg) { IP4 src(msg.data); { std::shared_lock lock(this->ipPeerMutex); auto it = this->ipPeerMap.find(src); if (it != this->ipPeerMap.end()) { it->second.tryConnecct(); return 0; } } { std::unique_lock lock(this->ipPeerMutex); auto it = this->ipPeerMap.emplace(std::piecewise_construct, std::forward_as_tuple(src), std::forward_as_tuple(src, this)); if (it.second) { it.first->second.tryConnecct(); return 0; } } spdlog::warn("can not find peer: {}", src.toString()); return 0; } int PeerManager::handlePubInfo(Msg msg) { auto info = (CoreMsg::PubInfo *)(msg.data.data()); if (info->src == getClient().address() || info->dst != getClient().address()) { spdlog::warn("invalid public info: src=[{}] dst=[{}]", info->src.toString(), info->dst.toString()); return 0; } try { { std::shared_lock lock(this->ipPeerMutex); auto it = this->ipPeerMap.find(info->src); if (it != this->ipPeerMap.end()) { it->second.handlePubInfo(info->ip, info->port, info->local); } } { std::unique_lock lock(this->ipPeerMutex); auto it = this->ipPeerMap.emplace(std::piecewise_construct, std::forward_as_tuple(info->src), std::forward_as_tuple(info->src, this)); if (it.second) { it.first->second.handlePubInfo(info->ip, info->port, info->local); return 0; } } } catch (const Poco::Exception &e) { spdlog::warn("peer manager handle pubinfo failed: src={}, ip={}, port={}, error={}", info->src.toString(), info->ip.toString(), info->port, e.message()); return 0; } catch (const std::exception &e) { spdlog::warn("peer manager handle pubinfo failed: src={}, error={}", info->src.toString(), e.what()); return 0; } return 0; } int PeerManager::startTickThread() { if (this->localhost.empty()) { try { for (const auto &iface : Poco::Net::NetworkInterface::list()) { if (iface.supportsIPv4() && !iface.isLoopback() && !iface.isPointToPoint() && iface.type() != iface.NI_TYPE_OTHER) { auto firstAddress = iface.firstAddress(Poco::Net::IPAddress::IPv4); memcpy(&this->localhost, firstAddress.addr(), sizeof(this->localhost)); spdlog::debug("localhost: {}", this->localhost.toString()); break; } } } catch (std::exception &e) { spdlog::warn("local ip failed: {}", e.what()); } } if (this->initSocket()) { return -1; } this->tickThread = std::thread([&] { spdlog::debug("start thread: peer manager tick"); while (getClient().isRunning()) { auto wake_time = std::chrono::system_clock::now() + std::chrono::seconds(1); if (tick()) { break; } std::this_thread::sleep_until(wake_time); } getClient().shutdown(); spdlog::debug("stop thread: peer manager tick"); }); return 0; } int PeerManager::tick() { if (this->discoveryInterval && this->stun.enabled()) { if ((++tickTick % discoveryInterval == 0)) { getClient().getWsMsgQueue().write(Msg(MsgKind::DISCOVERY)); } } { std::shared_lock ipPeerLock(this->ipPeerMutex); for (auto &[ip, peer] : this->ipPeerMap) { peer.tick(); } } if (this->stun.needed) { sendStunRequest(); this->stun.needed = false; } return 0; } int PeerManager::initSocket() { using Poco::Net::AddressFamily; using Poco::Net::SocketAddress; try { this->socket.bind(SocketAddress(AddressFamily::IPv4, this->listenPort)); this->socket.setSendBufferSize(16 * 1024 * 1024); this->socket.setReceiveBufferSize(16 * 1024 * 1024); spdlog::debug("listen port: {}", this->socket.address().port()); } catch (Poco::Net::NetException &e) { spdlog::critical("peer socket init failed: {}: {}", e.what(), e.message()); return -1; } this->decryptCtx = std::shared_ptr(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); this->pollThread = std::thread([&]() { spdlog::debug("start thread: peer manager poll"); while (getClient().isRunning()) { if (poll()) { break; } } getClient().shutdown(); spdlog::debug("stop thread: peer manager poll"); }); return 0; } void PeerManager::sendStunRequest() { try { StunRequest request; if (sendTo(&request, sizeof(request), this->stun.address) != sizeof(request)) { spdlog::warn("the stun request was not completely sent"); } } catch (std::exception &e) { spdlog::debug("send stun request failed: {}", e.what()); } } void PeerManager::handleStunResponse(std::string buffer) { if (buffer.length() < sizeof(StunResponse)) { spdlog::debug("invalid stun response length: {}", buffer.length()); return; } auto response = (StunResponse *)buffer.c_str(); if (ntoh(response->type) != 0x0101) { spdlog::debug("invalid stun reponse type: {}", ntoh(response->type)); return; } int pos = 0; uint32_t ip = 0; uint16_t port = 0; uint8_t *attr = response->attr; while (pos < ntoh(response->length)) { // mapped address if (ntoh(*(uint16_t *)(attr + pos)) == 0x0001) { pos += 6; // 跳过 2 字节类型, 2 字节长度, 1 字节保留, 1 字节IP版本号,指向端口号 port = ntoh(*(uint16_t *)(attr + pos)); pos += 2; // 跳过2字节端口号,指向地址 ip = *(uint32_t *)(attr + pos); break; } // xor mapped address if (ntoh(*(uint16_t *)(attr + pos)) == 0x0020) { pos += 6; // 跳过 2 字节类型, 2 字节长度, 1 字节保留, 1 字节IP版本号,指向端口号 port = ntoh(*(uint16_t *)(attr + pos)) ^ 0x2112; pos += 2; // 跳过2字节端口号,指向地址 ip = (*(uint32_t *)(attr + pos)) ^ hton(0x2112a442); break; } // 跳过 2 字节类型,指向属性长度 pos += 2; // 跳过 2 字节长度和用该属性其他内容 pos += 2 + ntoh(*(uint16_t *)(attr + pos)); } if (!ip || !port) { spdlog::warn("stun response parse failed: {:n}", spdlog::to_hex(buffer)); return; } memcpy(&this->stun.ip, &ip, sizeof(this->stun.ip)); this->stun.port = port; std::shared_lock lock(this->ipPeerMutex); for (auto &[tun, peer] : this->ipPeerMap) { peer.handleStunResponse(); } return; } void PeerManager::handleMessage(std::string buffer, const SocketAddress &address) { switch (buffer.front()) { case PeerMsgKind::HEARTBEAT: handleHeartbeatMessage(std::move(buffer), address); break; case PeerMsgKind::FORWARD: handleForwardMessage(std::move(buffer), address); break; case PeerMsgKind::DELAY: if (clientRelayEnabled()) { handleDelayMessage(std::move(buffer), address); } break; case PeerMsgKind::ROUTE: if (clientRelayEnabled()) { handleRouteMessage(std::move(buffer), address); } break; default: spdlog::info("udp4 unknown message: {}", address.toString()); break; } } void PeerManager::handleHeartbeatMessage(std::string buffer, const SocketAddress &address) { if (buffer.size() < sizeof(PeerMsg::Heartbeat)) { spdlog::debug("udp4 heartbeat failed: len {} address {}", buffer.length(), address.toString()); return; } auto heartbeat = (PeerMsg::Heartbeat *)buffer.c_str(); std::shared_lock lock(this->ipPeerMutex); auto it = this->ipPeerMap.find(heartbeat->tunip); if (it == this->ipPeerMap.end()) { spdlog::debug("udp4 heartbeat find peer failed: tun ip {}", heartbeat->tunip.toString()); return; } it->second.handleHeartbeatMessage(address, heartbeat->ack); } void PeerManager::handleForwardMessage(std::string buffer, const SocketAddress &address) { if (buffer.size() < sizeof(PeerMsg::Forward)) { spdlog::warn("invalid forward message: {:n}", spdlog::to_hex(buffer)); return; } buffer.erase(0, 1); auto header = (IP4Header *)buffer.data(); if (header->daddr == getTunIp()) { getClient().getTunMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer))); } else { getClient().getPeerMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer))); } } void PeerManager::handleDelayMessage(std::string buffer, const SocketAddress &address) { if (buffer.size() < sizeof(PeerMsg::Delay)) { spdlog::warn("invalid delay message: {:n}", spdlog::to_hex(buffer)); return; } auto header = (PeerMsg::Delay *)buffer.data(); if (header->dst == getTunIp()) { std::shared_lock ipPeerLock(this->ipPeerMutex); auto it = this->ipPeerMap.find(header->src); if (it != this->ipPeerMap.end()) { auto &peer = it->second; if (peer.isConnected()) { peer.sendEncrypted(buffer); } } return; } if (header->src == getTunIp()) { std::shared_lock ipPeerLock(this->ipPeerMutex); auto it = this->ipPeerMap.find(header->dst); if (it != this->ipPeerMap.end()) { auto &peer = it->second; peer.rtt = bootTime() - ntoh(header->timestamp); updateRtTable(PeerRouteEntry(header->dst, header->dst, peer.rtt)); } return; } } void PeerManager::handleRouteMessage(std::string buffer, const SocketAddress &address) { if (!routeCost) { return; } if (buffer.size() < sizeof(PeerMsg::Route)) { spdlog::warn("invalid delay message: {:n}", spdlog::to_hex(buffer)); return; } auto header = (PeerMsg::Route *)buffer.data(); if (header->dst != getTunIp()) { updateRtTable(PeerRouteEntry(header->dst, header->next, ntoh(header->rtt))); } } int PeerManager::poll() { using Poco::Net::Socket; using Poco::Net::SocketAddress; try { if (this->socket.poll(Poco::Timespan(1, 0), Poco::Net::Socket::SELECT_READ)) { std::string buffer(1500, 0); SocketAddress address; auto size = this->socket.receiveFrom(buffer.data(), buffer.size(), address); if (size > 0) { buffer.resize(size); if (this->stun.address == address) { handleStunResponse(buffer); } else if (auto plaintext = decrypt(buffer)) { handleMessage(std::move(*plaintext), address); } } } } catch (Poco::Net::ConnectionResetException &e) { // 忽略 UDP 的连接 Reset, Windows 特有的问题 } catch (std::exception &e) { spdlog::warn("peer_manager poll failed: {}", e.what()); return -1; } return 0; } std::optional PeerManager::decrypt(const std::string &ciphertext) { int len = 0; int plaintextLen = 0; unsigned char *enc = NULL; unsigned char plaintext[1500] = {0}; unsigned char iv[AES_256_GCM_IV_LEN] = {0}; unsigned char tag[AES_256_GCM_TAG_LEN] = {0}; if (this->key.size() != AES_256_GCM_KEY_LEN) { spdlog::debug("invalid key length: {}", this->key.size()); return std::nullopt; } if (ciphertext.size() < AES_256_GCM_IV_LEN + AES_256_GCM_TAG_LEN) { spdlog::debug("invalid ciphertext length: {}", ciphertext.size()); return std::nullopt; } std::lock_guard lock(this->decryptCtxMutex); auto ctx = this->decryptCtx.get(); if (!EVP_CIPHER_CTX_reset(ctx)) { spdlog::debug("decrypt reset cipher context failed"); return std::nullopt; } enc = (unsigned char *)ciphertext.data(); memcpy(iv, enc, AES_256_GCM_IV_LEN); memcpy(tag, enc + AES_256_GCM_IV_LEN, AES_256_GCM_TAG_LEN); enc += AES_256_GCM_IV_LEN + AES_256_GCM_TAG_LEN; if (!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (unsigned char *)key.data(), iv)) { spdlog::debug("initialize cipher context failed"); return std::nullopt; } if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_GCM_IV_LEN, NULL)) { spdlog::debug("set iv length failed"); return std::nullopt; } if (!EVP_DecryptUpdate(ctx, plaintext, &len, enc, ciphertext.size() - AES_256_GCM_IV_LEN - AES_256_GCM_TAG_LEN)) { spdlog::debug("decrypt update failed"); return std::nullopt; } if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, AES_256_GCM_TAG_LEN, tag)) { spdlog::debug("set tag failed"); return std::nullopt; } plaintextLen = len; if (!EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) { spdlog::debug("decrypt final failed"); return std::nullopt; } plaintextLen += len; std::string result; result.append((char *)plaintext, plaintextLen); return result; } int PeerManager::sendTo(const void *buffer, int length, const SocketAddress &address) { std::lock_guard lock(this->socketMutex); return this->socket.sendTo(buffer, length, address); } int PeerManager::getDiscoveryInterval() const { return this->discoveryInterval; } bool PeerManager::clientRelayEnabled() const { return this->routeCost > 0; } Client &PeerManager::getClient() { return *this->client; } void PeerManager::showRtChange(const PeerRouteEntry &entry) { std::string rtt = (entry.rtt == RTT_LIMIT) ? "[deleted]" : std::to_string(entry.rtt); spdlog::debug("route: dst={} next={} delay={}", entry.dst.toString(), entry.next.toString(), rtt); } int PeerManager::sendRtMessage(IP4 dst, int32_t rtt) { PeerMsg::Route message; message.type = PeerMsgKind::ROUTE; message.dst = dst; message.next = getTunIp(); if (rtt != RTT_LIMIT) { rtt += routeCost; } message.rtt = ntoh(rtt); for (auto &[_, peer] : this->ipPeerMap) { if (peer.isConnected()) { peer.sendEncrypted(std::string((char *)&message, sizeof(message))); } } return 0; } int PeerManager::updateRtTable(PeerRouteEntry entry) { bool isDirect = (entry.dst == entry.next); bool isDelete = (entry.rtt < 0 || entry.rtt > 1000); std::unique_lock lock(this->rtTableMutex); auto oldEntry = this->rtTableMap.find(entry.dst); if (isDirect && isDelete) { for (auto it = this->rtTableMap.begin(); it != this->rtTableMap.end();) { if (it->second.next == entry.next) { it->second.rtt = RTT_LIMIT; sendRtMessage(it->second.dst, it->second.rtt); showRtChange(it->second); it = this->rtTableMap.erase(it); continue; } ++it; } return 0; } if (isDirect && !isDelete) { if (oldEntry == this->rtTableMap.end() || oldEntry->second.next == entry.next || oldEntry->second.rtt > entry.rtt) { this->rtTableMap[entry.dst] = entry; sendRtMessage(entry.dst, entry.rtt); showRtChange(entry); } return 0; } if (!isDirect && isDelete) { if (oldEntry != this->rtTableMap.end() && oldEntry->second.next == entry.next) { oldEntry->second.rtt = RTT_LIMIT; sendRtMessage(oldEntry->second.dst, oldEntry->second.rtt); showRtChange(oldEntry->second); this->rtTableMap.erase(oldEntry); } return 0; } if (!isDirect && !isDelete) { auto directEntry = this->rtTableMap.find(entry.next); if (directEntry == this->rtTableMap.end()) { return 0; } int32_t rttNow = directEntry->second.rtt + entry.rtt; if (oldEntry == this->rtTableMap.end() || oldEntry->second.next == entry.next || oldEntry->second.rtt > rttNow) { entry.rtt = rttNow; this->rtTableMap[entry.dst] = entry; sendRtMessage(entry.dst, entry.rtt); showRtChange(entry); return 0; } return 0; } return 0; } } // namespace candy ================================================ FILE: candy/src/peer/manager.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_PEER_MANAGER_H #define CANDY_PEER_MANAGER_H #include "core/message.h" #include "core/net.h" #include "peer/message.h" #include "peer/peer.h" #include #include #include #include #include #include #include #include #include namespace candy { using Poco::Net::SocketAddress; class Client; struct Stun { std::string uri; SocketAddress address; bool needed = false; IP4 ip; uint16_t port; bool enabled() { return !this->address.host().isWildcard(); } int update() { try { if (!this->uri.empty()) { Poco::URI uri(this->uri); if (!uri.getPort()) { uri.setPort(3478); } this->address = Poco::Net::SocketAddress(uri.getHost(), uri.getPort()); } return 0; } catch (std::exception &e) { spdlog::warn("set stun server address failed: {}", e.what()); return -1; } } }; struct PeerRouteEntry { IP4 dst; IP4 next; int32_t rtt; PeerRouteEntry(IP4 dst = IP4(), IP4 next = IP4(), int32_t rtt = RTT_LIMIT) : dst(dst), next(next), rtt(rtt) {} }; class PeerManager { public: int setPassword(const std::string &password); int setStun(const std::string &stun); int setDiscoveryInterval(int interval); int setRouteCost(int cost); int setPort(int port); int setLocalhost(const std::string &ip); int run(Client *client); int wait(); std::string getPassword(); private: std::string password; IP4 localhost; public: int sendPubInfo(CoreMsg::PubInfo info); IP4 getTunIp(); int updateRtTable(PeerRouteEntry entry); private: // 处理来自消息队列的数据 int handlePeerQueue(); int handlePacket(Msg msg); int handleTunAddr(Msg msg); int handleTryP2P(Msg msg); int handlePubInfo(Msg msg); std::thread msgThread; int sendPacket(IP4 dst, const Msg &msg); int sendPacketDirect(IP4 dst, const Msg &msg); int sendPacketRelay(IP4 dst, const Msg &msg); Address tunAddr; int startTickThread(); int tick(); std::thread tickThread; uint64_t tickTick = randomUint32(); std::shared_mutex ipPeerMutex; std::unordered_map ipPeerMap; void showRtChange(const PeerRouteEntry &entry); int sendRtMessage(IP4 dst, int32_t rtt); std::shared_mutex rtTableMutex; std::unordered_map rtTableMap; public: Stun stun; std::atomic localP2PDisabled; private: int initSocket(); void sendStunRequest(); void handleStunResponse(std::string buffer); void handleMessage(std::string buffer, const SocketAddress &address); void handleHeartbeatMessage(std::string buffer, const SocketAddress &address); void handleForwardMessage(std::string buffer, const SocketAddress &address); void handleDelayMessage(std::string buffer, const SocketAddress &address); void handleRouteMessage(std::string buffer, const SocketAddress &address); int poll(); std::optional decrypt(const std::string &ciphertext); std::shared_ptr decryptCtx; std::mutex decryptCtxMutex; std::string key; // 默认监听端口,如果不配置,随机监听 uint16_t listenPort = 0; public: std::mutex socketMutex; Poco::Net::DatagramSocket socket; int sendTo(const void *buffer, int length, const SocketAddress &address); int getDiscoveryInterval() const; bool clientRelayEnabled() const; private: std::thread pollThread; int discoveryInterval = 0; int routeCost = 0; Client &getClient(); Client *client; }; } // namespace candy #endif ================================================ FILE: candy/src/peer/message.cc ================================================ // SPDX-License-Identifier: MIT #include "peer/message.h" #include namespace candy { namespace PeerMsg { std::string Forward::create(const std::string &packet) { std::string data; data.push_back(PeerMsgKind::FORWARD); data += packet; return data; } } // namespace PeerMsg } // namespace candy ================================================ FILE: candy/src/peer/message.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_PEER_MESSAGE_H #define CANDY_PEER_MESSAGE_H #include "core/net.h" #include "utils/random.h" #include namespace candy { namespace PeerMsgKind { constexpr uint8_t HEARTBEAT = 0; constexpr uint8_t FORWARD = 1; constexpr uint8_t DELAY = 2; constexpr uint8_t ROUTE = 4; } // namespace PeerMsgKind struct __attribute__((packed)) StunRequest { uint8_t type[2] = {0x00, 0x01}; uint8_t length[2] = {0x00, 0x08}; uint8_t cookie[4] = {0x21, 0x12, 0xa4, 0x42}; uint32_t id[3] = {0x00}; struct __attribute__((packed)) { uint8_t type[2] = {0x00, 0x03}; uint8_t length[2] = {0x00, 0x04}; uint8_t notset[4] = {0x00}; } attr; StunRequest() { id[0] = randomUint32(); id[1] = randomUint32(); id[2] = randomUint32(); } }; struct __attribute__((packed)) StunResponse { uint16_t type; uint16_t length; uint32_t cookie; uint8_t id[12]; uint8_t attr[0]; }; namespace PeerMsg { struct __attribute__((packed)) Heartbeat { uint8_t kind; IP4 tunip; IP4 ip; uint16_t port; uint8_t ack; }; struct __attribute__((packed)) Forward { uint8_t type; IP4Header iph; static std::string create(const std::string &packet); }; struct __attribute__((packed)) Delay { uint8_t type; IP4 src; IP4 dst; int64_t timestamp; }; struct __attribute__((packed)) Route { uint8_t type; IP4 dst; IP4 next; int32_t rtt; }; } // namespace PeerMsg } // namespace candy #endif ================================================ FILE: candy/src/peer/peer.cc ================================================ // SPDX-License-Identifier: MIT #include "peer/peer.h" #include "core/client.h" #include "core/message.h" #include "peer/manager.h" #include "peer/peer.h" #include "utils/time.h" #include #include #include #include #include #include #include namespace { using namespace Poco::Net; bool isLocalNetwork(const SocketAddress &addr) { IPAddress ip = addr.host(); if (ip.isV4()) { return ip.isSiteLocal() || ip.isLinkLocal() || ip.isSiteLocalMC(); } else if (ip.isV6()) { spdlog::error("unexpected ipv6 local address"); } return false; } } // namespace namespace candy { Peer::Peer(const IP4 &addr, PeerManager *peerManager) : peerManager(peerManager), addr(addr) { std::string data; data.append(this->peerManager->getPassword()); auto leaddr = hton(uint32_t(this->addr)); data.append((char *)&leaddr, sizeof(leaddr)); this->key.resize(SHA256_DIGEST_LENGTH); SHA256((unsigned char *)data.data(), data.size(), (unsigned char *)this->key.data()); this->encryptCtx = std::shared_ptr(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); } Peer::~Peer() {} void Peer::tryConnecct() { if (this->state == PeerState::INIT) { updateState(PeerState::PREPARING); } } PeerManager &Peer::getManager() { return *this->peerManager; } std::optional Peer::encrypt(const std::string &plaintext) { int len = 0; int ciphertextLen = 0; unsigned char ciphertext[1500] = {0}; unsigned char iv[AES_256_GCM_IV_LEN] = {0}; unsigned char tag[AES_256_GCM_TAG_LEN] = {0}; if (!RAND_bytes(iv, AES_256_GCM_IV_LEN)) { spdlog::debug("generate random iv failed"); return std::nullopt; } std::lock_guard lock(this->encryptCtxMutex); auto ctx = this->encryptCtx.get(); if (!EVP_CIPHER_CTX_reset(ctx)) { spdlog::debug("encrypt reset cipher context failed"); return std::nullopt; } if (!EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (unsigned char *)key.data(), iv)) { spdlog::debug("encrypt initialize cipher context failed"); return std::nullopt; } if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_GCM_IV_LEN, NULL)) { spdlog::debug("set iv length failed"); return std::nullopt; } if (!EVP_EncryptUpdate(ctx, ciphertext, &len, (unsigned char *)plaintext.data(), plaintext.size())) { spdlog::debug("encrypt update failed"); return std::nullopt; } ciphertextLen = len; if (!EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) { spdlog::debug("encrypt final failed"); return std::nullopt; } ciphertextLen += len; if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AES_256_GCM_TAG_LEN, tag)) { spdlog::debug("get tag failed"); return std::nullopt; } std::string result; result.append((char *)iv, AES_256_GCM_IV_LEN); result.append((char *)tag, AES_256_GCM_TAG_LEN); result.append((char *)ciphertext, ciphertextLen); return result; } int Peer::sendEncrypted(const std::string &data) { if (auto buffer = encrypt(data)) { return send(*buffer); } return -1; } bool Peer::checkActivityWithin(std::chrono::system_clock::duration duration) { return std::chrono::system_clock::now() - lastActiveTime < duration; } std::optional Peer::isConnected() const { if (this->state == PeerState::CONNECTED) { return this->rtt; } return std::nullopt; } bool Peer::updateState(PeerState state) { this->lastActiveTime = std::chrono::system_clock::now(); if (this->state == state) { return false; } spdlog::debug("state: {} {} => {}", this->addr.toString(), stateString(), stateString(state)); if (state == PeerState::INIT || state == PeerState::WAITING || state == PeerState::FAILED) { resetState(); } if (this->state == PeerState::WAITING && state == PeerState::INIT) { this->retry = std::min(this->retry * 2, RETRY_MAX); } else if (state == PeerState::INIT || state == PeerState::FAILED) { this->retry = RETRY_MIN; } this->state = state; return true; } std::string Peer::stateString() const { return this->stateString(this->state); } std::string Peer::stateString(PeerState state) const { switch (state) { case PeerState::INIT: return "INIT"; case PeerState::PREPARING: return "PREPARING"; case PeerState::SYNCHRONIZING: return "SYNCHRONIZING"; case PeerState::CONNECTING: return "CONNECTING"; case PeerState::CONNECTED: return "CONNECTED"; case PeerState::WAITING: return "WAITING"; case PeerState::FAILED: return "FAILED"; default: return "UNKNOWN"; } } void Peer::handlePubInfo(IP4 ip, uint16_t port, bool local) { try { std::unique_lock lock(this->socketAddressMutex); if (local) { this->local = SocketAddress(ip.toString(), port); return; } this->wide = SocketAddress(ip.toString(), port); } catch (const Poco::Exception &e) { spdlog::warn("peer handle pubinfo failed: ip={}, port={}, error={}", ip.toString(), port, e.message()); return; } if (this->state == PeerState::CONNECTED) { return; } if (this->state == PeerState::SYNCHRONIZING) { updateState(PeerState::CONNECTING); return; } if (this->state != PeerState::CONNECTING) { updateState(PeerState::PREPARING); CoreMsg::PubInfo info = {.dst = this->addr, .local = true}; getManager().sendPubInfo(info); return; } } void Peer::handleStunResponse() { if (this->state != PeerState::PREPARING) { return; } if (this->wide == std::nullopt) { updateState(PeerState::SYNCHRONIZING); } else { updateState(PeerState::CONNECTING); } CoreMsg::PubInfo info = {.dst = this->addr}; getManager().sendPubInfo(info); } void Peer::tick() { switch (this->state) { case PeerState::INIT: break; case PeerState::PREPARING: if (getManager().stun.enabled() && checkActivityWithin(std::chrono::seconds(10))) { getManager().stun.needed = true; } else { updateState(PeerState::FAILED); } break; case PeerState::SYNCHRONIZING: if (checkActivityWithin(std::chrono::seconds(10))) { sendHeartbeatMessage(); } else { updateState(PeerState::FAILED); } break; case PeerState::CONNECTING: if (checkActivityWithin(std::chrono::seconds(10))) { sendHeartbeatMessage(); } else { updateState(PeerState::WAITING); } break; case PeerState::CONNECTED: if (checkActivityWithin(std::chrono::seconds(3))) { sendHeartbeatMessage(); if (getManager().clientRelayEnabled() && tickCount % 60 == 0) { sendDelayMessage(); } } else { updateState(PeerState::INIT); if (getManager().clientRelayEnabled()) { getManager().updateRtTable(PeerRouteEntry(addr, addr, RTT_LIMIT)); } } break; case PeerState::WAITING: if (!checkActivityWithin(std::chrono::seconds(this->retry))) { updateState(PeerState::INIT); } break; case PeerState::FAILED: break; default: break; } ++tickCount; } void Peer::handleHeartbeatMessage(const SocketAddress &address, uint8_t heartbeatAck) { if (this->state == PeerState::INIT || this->state == PeerState::WAITING || this->state == PeerState::FAILED) { spdlog::debug("heartbeat peer state invalid: {} {}", this->addr.toString(), stateString()); return; } if (!isLocalNetwork(address)) { this->wide = address; } else if (!getManager().localP2PDisabled) { this->local = address; } else { return; } { std::unique_lock lock(this->socketAddressMutex); if (!this->real || isLocalNetwork(address) || !isLocalNetwork(*this->real)) { this->real = address; } } if (!this->ack) { this->ack = 1; } if (heartbeatAck && updateState(PeerState::CONNECTED)) { sendDelayMessage(); } } int Peer::send(const std::string &buffer) { try { std::shared_lock lock(this->socketAddressMutex); if (this->real) { if (buffer.size() == getManager().sendTo(buffer.data(), buffer.size(), *this->real)) { return 0; } } } catch (std::exception &e) { spdlog::debug("peer send failed: {}", e.what()); } return -1; } void Peer::sendHeartbeatMessage() { PeerMsg::Heartbeat heartbeat; heartbeat.kind = PeerMsgKind::HEARTBEAT; heartbeat.tunip = getManager().getTunIp(); heartbeat.ack = this->ack; if (auto buffer = encrypt(std::string((char *)&heartbeat, sizeof(heartbeat)))) { using Poco::Net::SocketAddress; std::shared_lock lock(this->socketAddressMutex); if (this->real && (this->state == PeerState::CONNECTED)) { heartbeat.ip = this->real->host().toString(); heartbeat.port = this->real->port(); getManager().sendTo(buffer->data(), buffer->size(), *this->real); } if (this->wide && (this->state == PeerState::CONNECTING)) { heartbeat.ip = this->wide->host().toString(); heartbeat.port = this->wide->port(); getManager().sendTo(buffer->data(), buffer->size(), *this->wide); } if (this->local && (this->state == PeerState::PREPARING || this->state == PeerState::SYNCHRONIZING || this->state == PeerState::CONNECTING)) { heartbeat.ip = this->local->host().toString(); heartbeat.port = this->local->port(); getManager().sendTo(buffer->data(), buffer->size(), *this->local); } } } void Peer::sendDelayMessage() { PeerMsg::Delay delay; delay.type = PeerMsgKind::DELAY; delay.src = getManager().getTunIp(); delay.dst = this->addr; delay.timestamp = hton(bootTime()); sendEncrypted(std::string((char *)&delay, sizeof(delay))); } void Peer::resetState() { std::unique_lock lock(this->socketAddressMutex); this->wide = std::nullopt; this->local = std::nullopt; this->real = std::nullopt; this->ack = 0; this->rtt = RTT_LIMIT; } } // namespace candy ================================================ FILE: candy/src/peer/peer.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_PEER_PEER_H #define CANDY_PEER_PEER_H #include "core/net.h" #include "utils/random.h" #include #include #include #include #include #include #include #include #include namespace candy { class PeerManager; constexpr int32_t RTT_LIMIT = INT32_MAX; constexpr int32_t RETRY_MIN = 30; constexpr int32_t RETRY_MAX = 3600; using Poco::Net::SocketAddress; enum class PeerState { INIT, PREPARING, SYNCHRONIZING, CONNECTING, CONNECTED, WAITING, FAILED, }; class Peer { public: Peer(const IP4 &addr, PeerManager *peerManager); ~Peer(); void tick(); void tryConnecct(); void handleStunResponse(); void handlePubInfo(IP4 ip, uint16_t port, bool local = false); void handleHeartbeatMessage(const SocketAddress &address, uint8_t heartbeatAck); int sendEncrypted(const std::string &buffer); std::optional isConnected() const; int32_t rtt = RTT_LIMIT; uint32_t tickCount = randomUint32(); private: PeerManager &getManager(); PeerManager *peerManager; std::optional encrypt(const std::string &plaintext); std::shared_ptr encryptCtx; std::mutex encryptCtxMutex; std::string key; std::string stateString() const; std::string stateString(PeerState state) const; bool updateState(PeerState state); void resetState(); bool checkActivityWithin(std::chrono::system_clock::duration duration); PeerState state = PeerState::INIT; uint8_t ack = 0; int32_t retry = RETRY_MIN; std::chrono::system_clock::time_point lastActiveTime; int send(const std::string &buffer); void sendHeartbeatMessage(); void sendDelayMessage(); std::optional wide, local, real; std::shared_mutex socketAddressMutex; IP4 addr; }; } // namespace candy #endif ================================================ FILE: candy/src/tun/linux.cc ================================================ // SPDX-License-Identifier: MIT #include #if POCO_OS == POCO_OS_LINUX #include "core/net.h" #include "tun/tun.h" #include #include #include #include #include #include #include #include #include #include namespace candy { class LinuxTun { public: int setName(const std::string &name) { this->name = name.empty() ? "candy" : "candy-" + name; return 0; } int setIP(IP4 ip) { this->ip = ip; return 0; } IP4 getIP() { return this->ip; } int setMask(IP4 mask) { this->mask = mask; return 0; } int setMTU(int mtu) { this->mtu = mtu; return 0; } // 配置网卡,设置路由 int up() { this->tunFd = open("/dev/net/tun", O_RDWR); if (this->tunFd < 0) { spdlog::critical("open /dev/net/tun failed: {}", strerror(errno)); close(this->tunFd); return -1; } int flags = fcntl(this->tunFd, F_GETFL, 0); if (flags < 0) { spdlog::error("get tun flags failed: {}", strerror(errno)); close(this->tunFd); return -1; } flags |= O_NONBLOCK; if (fcntl(this->tunFd, F_SETFL, flags) < 0) { spdlog::error("set non-blocking tun failed: {}", strerror(errno)); close(this->tunFd); return -1; } // 设置设备名 struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, this->name.c_str(), IFNAMSIZ); ifr.ifr_flags = IFF_TUN | IFF_NO_PI; if (ioctl(this->tunFd, TUNSETIFF, &ifr) == -1) { spdlog::critical("set tun interface failed: {}", strerror(errno)); close(this->tunFd); return -1; } // 创建 socket, 并通过这个 socket 更新网卡的其他配置 struct sockaddr_in *addr; addr = (struct sockaddr_in *)&ifr.ifr_addr; addr->sin_family = AF_INET; int sockfd = socket(addr->sin_family, SOCK_DGRAM, 0); if (sockfd == -1) { spdlog::critical("create socket failed"); close(this->tunFd); return -1; } // 设置地址 addr->sin_addr.s_addr = this->ip; if (ioctl(sockfd, SIOCSIFADDR, (caddr_t)&ifr) == -1) { spdlog::critical("set ip address failed: ip {}", this->ip.toString()); close(sockfd); close(this->tunFd); return -1; } // 设置掩码 addr->sin_addr.s_addr = this->mask; if (ioctl(sockfd, SIOCSIFNETMASK, (caddr_t)&ifr) == -1) { spdlog::critical("set mask failed: mask {}", this->mask.toString()); close(sockfd); close(this->tunFd); return -1; } // 设置 MTU ifr.ifr_mtu = this->mtu; if (ioctl(sockfd, SIOCSIFMTU, (caddr_t)&ifr) == -1) { spdlog::critical("set mtu failed: mtu {}", this->mtu); close(sockfd); close(this->tunFd); return -1; } // 设置 flags if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) { spdlog::critical("get interface flags failed"); close(sockfd); close(this->tunFd); return -1; } ifr.ifr_flags |= IFF_UP | IFF_RUNNING; if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) { spdlog::critical("set interface flags failed"); close(sockfd); close(this->tunFd); return -1; } close(sockfd); return 0; } int down() { close(this->tunFd); return 0; } int read(std::string &buffer) { buffer.resize(this->mtu); int n = ::read(this->tunFd, buffer.data(), buffer.size()); if (n >= 0) { buffer.resize(n); return n; } if (errno == EAGAIN || errno == EWOULDBLOCK) { struct timeval timeout = {.tv_sec = 1}; fd_set set; FD_ZERO(&set); FD_SET(this->tunFd, &set); select(this->tunFd + 1, &set, NULL, NULL, &timeout); return 0; } spdlog::warn("tun read failed: {}", strerror(errno)); return -1; } int write(const std::string &buffer) { return ::write(this->tunFd, buffer.c_str(), buffer.size()); } int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { spdlog::error("set route failed: create socket failed"); return -1; } struct sockaddr_in *addr; struct rtentry route; memset(&route, 0, sizeof(route)); addr = (struct sockaddr_in *)&route.rt_dst; addr->sin_family = AF_INET; addr->sin_addr.s_addr = dst; addr = (struct sockaddr_in *)&route.rt_genmask; addr->sin_family = AF_INET; addr->sin_addr.s_addr = mask; addr = (struct sockaddr_in *)&route.rt_gateway; addr->sin_family = AF_INET; addr->sin_addr.s_addr = nexthop; route.rt_flags = RTF_UP | RTF_GATEWAY; if (ioctl(sockfd, SIOCADDRT, &route) == -1) { spdlog::error("set route failed: ioctl failed"); close(sockfd); return -1; } close(sockfd); return 0; } private: std::string name; IP4 ip; IP4 mask; int mtu; int timeout; int tunFd; }; } // namespace candy namespace candy { Tun::Tun() { this->impl = std::make_shared(); } Tun::~Tun() { this->impl.reset(); } int Tun::setName(const std::string &name) { std::shared_ptr tun; tun = std::any_cast>(this->impl); tun->setName(name); return 0; } int Tun::setAddress(const std::string &cidr) { std::shared_ptr tun; Address address; if (address.fromCidr(cidr)) { return -1; } spdlog::info("client address: {}", address.toCidr()); tun = std::any_cast>(this->impl); if (tun->setIP(address.Host())) { return -1; } if (tun->setMask(address.Mask())) { return -1; } this->tunAddress = cidr; return 0; } IP4 Tun::getIP() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->getIP(); } int Tun::setMTU(int mtu) { std::shared_ptr tun; tun = std::any_cast>(this->impl); if (tun->setMTU(mtu)) { return -1; } return 0; } int Tun::up() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->up(); } int Tun::down() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->down(); } int Tun::read(std::string &buffer) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->read(buffer); } int Tun::write(const std::string &buffer) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->write(buffer); } int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->setSysRtTable(dst, mask, nexthop); } } // namespace candy #endif ================================================ FILE: candy/src/tun/macos.cc ================================================ // SPDX-License-Identifier: MIT #include #if POCO_OS == POCO_OS_MAC_OS_X #include "core/net.h" #include "tun/tun.h" #include #include #include // clang-format off #include #include // clang-format on #include #include #include #include #include #include #include #include #include #include #include #include namespace candy { class MacTun { public: int setName(const std::string &name) { this->name = name.empty() ? "candy" : "candy-" + name; return 0; } int setIP(IP4 ip) { this->ip = ip; return 0; } IP4 getIP() { return this->ip; } int setMask(IP4 mask) { this->mask = mask; return 0; } int setMTU(int mtu) { this->mtu = mtu; return 0; } int up() { // 创建设备,操作系统不允许自定义设备名,只能由内核分配 this->tunFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (this->tunFd < 0) { spdlog::critical("create socket failed: {}", strerror(errno)); return -1; } int flags = fcntl(this->tunFd, F_GETFL, 0); if (flags < 0) { spdlog::error("get tun flags failed: {}", strerror(errno)); close(this->tunFd); return -1; } flags |= O_NONBLOCK; if (fcntl(this->tunFd, F_SETFL, flags) < 0) { spdlog::error("set non-blocking tun failed: {}", strerror(errno)); close(this->tunFd); return -1; } struct ctl_info info; memset(&info, 0, sizeof(info)); strncpy(info.ctl_name, UTUN_CONTROL_NAME, MAX_KCTL_NAME); if (ioctl(this->tunFd, CTLIOCGINFO, &info) == -1) { spdlog::critical("get control id failed: {}", strerror(errno)); close(this->tunFd); return -1; } struct sockaddr_ctl ctl; memset(&ctl, 0, sizeof(ctl)); ctl.sc_len = sizeof(ctl); ctl.sc_family = AF_SYSTEM; ctl.ss_sysaddr = AF_SYS_CONTROL; ctl.sc_id = info.ctl_id; ctl.sc_unit = 0; if (connect(this->tunFd, (struct sockaddr *)&ctl, sizeof(ctl)) == -1) { spdlog::critical("connect to control failed: {}", strerror(errno)); close(this->tunFd); return -1; } socklen_t ifname_len = sizeof(ifname); if (getsockopt(this->tunFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &ifname_len) == -1) { spdlog::critical("get interface name failed: {}", strerror(errno)); close(this->tunFd); return -1; } spdlog::debug("created utun interface: {}", ifname); struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, ifname, IFNAMSIZ); // 创建 socket, 并通过这个 socket 更新网卡的其他配置 struct sockaddr_in *addr; addr = (struct sockaddr_in *)&ifr.ifr_addr; addr->sin_family = AF_INET; int sockfd = socket(addr->sin_family, SOCK_DGRAM, 0); if (sockfd == -1) { spdlog::critical("create socket failed"); close(this->tunFd); return -1; } // 设置地址和掩码 struct ifaliasreq areq; memset(&areq, 0, sizeof(areq)); strncpy(areq.ifra_name, ifname, IFNAMSIZ); ((struct sockaddr_in *)&areq.ifra_addr)->sin_family = AF_INET; ((struct sockaddr_in *)&areq.ifra_addr)->sin_len = sizeof(areq.ifra_addr); ((struct sockaddr_in *)&areq.ifra_addr)->sin_addr.s_addr = this->ip; ((struct sockaddr_in *)&areq.ifra_mask)->sin_family = AF_INET; ((struct sockaddr_in *)&areq.ifra_mask)->sin_len = sizeof(areq.ifra_mask); ((struct sockaddr_in *)&areq.ifra_mask)->sin_addr.s_addr = this->mask; ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_family = AF_INET; ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_len = sizeof(areq.ifra_broadaddr); ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_addr.s_addr = (this->ip & this->mask); if (ioctl(sockfd, SIOCAIFADDR, (void *)&areq) == -1) { spdlog::critical("set ip mask failed: {}: ip {} mask {}", strerror(errno), this->ip.toString(), this->mask.toString()); close(sockfd); close(this->tunFd); return -1; } // 设置 MTU ifr.ifr_mtu = this->mtu; if (ioctl(sockfd, SIOCSIFMTU, &ifr) == -1) { spdlog::critical("set mtu failed: mtu {}", this->mtu); close(sockfd); close(this->tunFd); return -1; } // 设置 flags if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) { spdlog::critical("get interface flags failed"); close(sockfd); close(this->tunFd); return -1; } ifr.ifr_flags |= IFF_UP | IFF_RUNNING; if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) { spdlog::critical("set interface flags failed"); close(sockfd); close(this->tunFd); return -1; } close(sockfd); // 设置路由 if (setSysRtTable(this->ip & this->mask, this->mask, this->ip)) { close(this->tunFd); return -1; } return 0; } int down() { close(this->tunFd); return 0; } int read(std::string &buffer) { buffer.resize(this->mtu); struct iovec iov[2]; iov[0].iov_base = &this->packetinfo; iov[0].iov_len = sizeof(this->packetinfo); iov[1].iov_base = buffer.data(); iov[1].iov_len = buffer.size(); int n = ::readv(this->tunFd, iov, sizeof(iov) / sizeof(iov[0])); if (n >= 0) { buffer.resize(n - sizeof(this->packetinfo)); return n; } if (errno == EAGAIN || errno == EWOULDBLOCK) { struct timeval timeout = {.tv_sec = 1}; fd_set set; FD_ZERO(&set); FD_SET(this->tunFd, &set); select(this->tunFd + 1, &set, NULL, NULL, &timeout); return 0; } spdlog::warn("tun read failed: error {}", n); return -1; } int write(const std::string &buffer) { struct iovec iov[2]; iov[0].iov_base = &this->packetinfo; iov[0].iov_len = sizeof(this->packetinfo); iov[1].iov_base = (void *)buffer.data(); iov[1].iov_len = buffer.size(); return ::writev(this->tunFd, iov, sizeof(iov) / sizeof(iov[0])) - sizeof(sizeof(this->packetinfo)); } int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { struct { struct rt_msghdr msghdr; struct sockaddr_in addr[3]; } msg; memset(&msg, 0, sizeof(msg)); msg.msghdr.rtm_msglen = sizeof(msg); msg.msghdr.rtm_version = RTM_VERSION; msg.msghdr.rtm_type = RTM_ADD; msg.msghdr.rtm_addrs = RTA_DST | RTA_GATEWAY | RTA_NETMASK; msg.msghdr.rtm_flags = RTF_UP | RTA_GATEWAY; for (int idx = 0; idx < (int)(sizeof(msg.addr) / sizeof(msg.addr[0])); ++idx) { msg.addr[idx].sin_len = sizeof(msg.addr[0]); msg.addr[idx].sin_family = AF_INET; } msg.addr[0].sin_addr.s_addr = dst; msg.addr[1].sin_addr.s_addr = nexthop; msg.addr[2].sin_addr.s_addr = mask; int routefd = socket(AF_ROUTE, SOCK_RAW, 0); if (routefd < 0) { spdlog::error("create route fd failed: {}", strerror(routefd)); return -1; } if (::write(routefd, &msg, sizeof(msg)) == -1) { spdlog::error("add route failed: {}", strerror(errno)); close(routefd); return -1; } close(routefd); return 0; } private: std::string name; char ifname[IFNAMSIZ] = {0}; IP4 ip; IP4 mask; int mtu; int timeout; int tunFd; uint8_t packetinfo[4] = {0x00, 0x00, 0x00, 0x02}; }; } // namespace candy namespace candy { Tun::Tun() { this->impl = std::make_shared(); } Tun::~Tun() { this->impl.reset(); } int Tun::setName(const std::string &name) { std::shared_ptr tun; tun = std::any_cast>(this->impl); tun->setName(name); return 0; } int Tun::setAddress(const std::string &cidr) { std::shared_ptr tun; Address address; if (address.fromCidr(cidr)) { return -1; } spdlog::info("client address: {}", address.toCidr()); tun = std::any_cast>(this->impl); if (tun->setIP(address.Host())) { return -1; } if (tun->setMask(address.Mask())) { return -1; } return 0; } IP4 Tun::getIP() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->getIP(); } int Tun::setMTU(int mtu) { std::shared_ptr tun; tun = std::any_cast>(this->impl); if (tun->setMTU(mtu)) { return -1; } return 0; } int Tun::up() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->up(); } int Tun::down() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->down(); } int Tun::read(std::string &buffer) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->read(buffer); } int Tun::write(const std::string &buffer) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->write(buffer); } int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->setSysRtTable(dst, mask, nexthop); } } // namespace candy #endif ================================================ FILE: candy/src/tun/tun.cc ================================================ // SPDX-License-Identifier: MIT #include "tun/tun.h" #include "core/client.h" #include "core/message.h" #include "core/net.h" #include #include #include namespace candy { int Tun::run(Client *client) { this->client = client; this->msgThread = std::thread([&] { spdlog::debug("start thread: tun msg"); while (getClient().isRunning()) { if (handleTunQueue()) { break; } } getClient().shutdown(); spdlog::debug("stop thread: tun msg"); }); return 0; } int Tun::wait() { if (this->tunThread.joinable()) { this->tunThread.join(); } if (this->msgThread.joinable()) { this->msgThread.join(); } { std::unique_lock lock(this->sysRtMutex); this->sysRtTable.clear(); } return 0; } int Tun::handleTunDevice() { std::string buffer; int error = read(buffer); if (error <= 0) { return 0; } if (buffer.length() < sizeof(IP4Header)) { return 0; } IP4Header *header = (IP4Header *)buffer.data(); if (!header->isIPv4()) { return 0; } IP4 nextHop = [&]() { std::shared_lock lock(this->sysRtMutex); for (auto const &rt : sysRtTable) { if ((header->daddr & rt.mask) == rt.dst) { return rt.nexthop; } } return IP4(); }(); if (!nextHop.empty()) { buffer.insert(0, sizeof(IP4Header), 0); header = (IP4Header *)buffer.data(); header->protocol = 0x04; header->saddr = getIP(); header->daddr = nextHop; } if (header->daddr == getIP()) { write(buffer); return 0; } this->client->getPeerMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer))); return 0; } int Tun::handleTunQueue() { Msg msg = this->client->getTunMsgQueue().read(); switch (msg.kind) { case MsgKind::TIMEOUT: break; case MsgKind::PACKET: handlePacket(std::move(msg)); break; case MsgKind::TUNADDR: if (handleTunAddr(std::move(msg))) { return -1; } break; case MsgKind::SYSRT: handleSysRt(std::move(msg)); break; default: spdlog::warn("unexcepted tun message type: {}", static_cast(msg.kind)); break; } return 0; } int Tun::handlePacket(Msg msg) { if (msg.data.size() < sizeof(IP4Header)) { spdlog::warn("invalid IPv4 packet: {:n}", spdlog::to_hex(msg.data)); return 0; } IP4Header *header = (IP4Header *)msg.data.data(); if (header->isIPIP()) { msg.data.erase(0, sizeof(IP4Header)); header = (IP4Header *)msg.data.data(); } write(msg.data); return 0; } int Tun::handleTunAddr(Msg msg) { if (setAddress(msg.data)) { return -1; } if (up()) { spdlog::critical("tun up failed"); return -1; } this->tunThread = std::thread([&] { spdlog::debug("start thread: tun"); while (getClient().isRunning()) { if (handleTunDevice()) { break; } } getClient().shutdown(); spdlog::debug("stop thread: tun"); if (down()) { spdlog::critical("tun down failed"); return; } }); return 0; } int Tun::handleSysRt(Msg msg) { SysRouteEntry *rt = (SysRouteEntry *)msg.data.data(); if (rt->nexthop != getIP()) { spdlog::info("route: {}/{} via {}", rt->dst.toString(), rt->mask.toPrefix(), rt->nexthop.toString()); if (setSysRtTable(*rt)) { return -1; } } return 0; } int Tun::setSysRtTable(const SysRouteEntry &entry) { std::unique_lock lock(this->sysRtMutex); this->sysRtTable.push_back(entry); return setSysRtTable(entry.dst, entry.mask, entry.nexthop); } Client &Tun::getClient() { return *this->client; } } // namespace candy ================================================ FILE: candy/src/tun/tun.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_TUN_TUN_H #define CANDY_TUN_TUN_H #include "core/message.h" #include "core/net.h" #include #include #include #include #include namespace candy { class Client; class Tun { public: Tun(); ~Tun(); int setName(const std::string &name); int setMTU(int mtu); int run(Client *client); int wait(); IP4 getIP(); private: int setAddress(const std::string &cidr); // 处理来自 TUN 设备的数据 int handleTunDevice(); // 处理来自消息队列的数据 int handleTunQueue(); int handlePacket(Msg msg); int handleTunAddr(Msg msg); int handleSysRt(Msg msg); std::string tunAddress; std::thread tunThread; std::thread msgThread; private: int up(); int down(); int read(std::string &buffer); int write(const std::string &buffer); int setSysRtTable(const SysRouteEntry &entry); int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop); std::shared_mutex sysRtMutex; std::list sysRtTable; private: std::any impl; private: Client &getClient(); Client *client; }; } // namespace candy #endif ================================================ FILE: candy/src/tun/unknown.cc ================================================ // SPDX-License-Identifier: MIT #include #if POCO_OS != POCO_OS_LINUX && POCO_OS != POCO_OS_MAC_OS_X && POCO_OS != POCO_OS_WINDOWS_NT #include "tun/tun.h" namespace candy { Tun::Tun() {} Tun::~Tun() {} int Tun::setName(const std::string &name) { return -1; } int Tun::setAddress(const std::string &cidr) { return -1; } int Tun::setMTU(int mtu) { return -1; } int Tun::up() { return -1; } int Tun::down() { return -1; } int Tun::read(std::string &buffer) { return -1; } int Tun::write(const std::string &buffer) { return -1; } int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { return -1; } } // namespace candy #endif ================================================ FILE: candy/src/tun/windows.cc ================================================ // SPDX-License-Identifier: MIT #include #if POCO_OS == POCO_OS_WINDOWS_NT #include "core/net.h" #include "tun/tun.h" #include "utils/codecvt.h" #include #include #include #include #include #include // clang-format off #include #include #include #include #include #include #include #include #include #include #include // clang-format on #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunknown-pragmas" #include #pragma GCC diagnostic pop namespace candy { WINTUN_CREATE_ADAPTER_FUNC *WintunCreateAdapter; WINTUN_CLOSE_ADAPTER_FUNC *WintunCloseAdapter; WINTUN_OPEN_ADAPTER_FUNC *WintunOpenAdapter; WINTUN_GET_ADAPTER_LUID_FUNC *WintunGetAdapterLUID; WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC *WintunGetRunningDriverVersion; WINTUN_DELETE_DRIVER_FUNC *WintunDeleteDriver; WINTUN_SET_LOGGER_FUNC *WintunSetLogger; WINTUN_START_SESSION_FUNC *WintunStartSession; WINTUN_END_SESSION_FUNC *WintunEndSession; WINTUN_GET_READ_WAIT_EVENT_FUNC *WintunGetReadWaitEvent; WINTUN_RECEIVE_PACKET_FUNC *WintunReceivePacket; WINTUN_RELEASE_RECEIVE_PACKET_FUNC *WintunReleaseReceivePacket; WINTUN_ALLOCATE_SEND_PACKET_FUNC *WintunAllocateSendPacket; WINTUN_SEND_PACKET_FUNC *WintunSendPacket; class Holder { public: static bool Ok() { static Holder instance; return instance.wintun; } private: Holder() { this->wintun = LoadLibraryExW(L"wintun.dll", NULL, LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); if (!this->wintun) { spdlog::critical("load wintun.dll failed"); return; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstrict-aliasing" #define X(Name) ((*(FARPROC *)&Name = GetProcAddress(this->wintun, #Name)) == NULL) if (X(WintunCreateAdapter) || X(WintunCloseAdapter) || X(WintunOpenAdapter) || X(WintunGetAdapterLUID) || X(WintunGetRunningDriverVersion) || X(WintunDeleteDriver) || X(WintunSetLogger) || X(WintunStartSession) || X(WintunEndSession) || X(WintunGetReadWaitEvent) || X(WintunReceivePacket) || X(WintunReleaseReceivePacket) || X(WintunAllocateSendPacket) || X(WintunSendPacket)) #undef X #pragma GCC diagnostic pop { spdlog::critical("get function from wintun.dll failed"); FreeLibrary(this->wintun); this->wintun = NULL; return; } } ~Holder() { if (this->wintun) { WintunDeleteDriver(); FreeLibrary(this->wintun); this->wintun = NULL; } } HMODULE wintun = NULL; }; class WindowsTun { public: int setName(const std::string &name) { this->name = name.empty() ? "candy" : name; return 0; } int setIP(IP4 ip) { this->ip = ip; return 0; } IP4 getIP() { return this->ip; } int setPrefix(uint32_t prefix) { this->prefix = prefix; return 0; } int setMTU(int mtu) { this->mtu = mtu; return 0; } int up() { if (!Holder::Ok()) { spdlog::critical("init wintun failed"); return -1; } GUID Guid; std::string data = "CandyGuid" + this->name; unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256((unsigned char *)data.c_str(), data.size(), hash); memcpy(&Guid, hash, sizeof(Guid)); this->adapter = WintunCreateAdapter(UTF8ToUTF16(this->name).c_str(), L"Candy", &Guid); if (!this->adapter) { spdlog::critical("create wintun adapter failed: {}", GetLastError()); return -1; } int Error; MIB_UNICASTIPADDRESS_ROW AddressRow; InitializeUnicastIpAddressEntry(&AddressRow); WintunGetAdapterLUID(this->adapter, &AddressRow.InterfaceLuid); AddressRow.Address.Ipv4.sin_family = AF_INET; AddressRow.Address.Ipv4.sin_addr.S_un.S_addr = this->ip; AddressRow.OnLinkPrefixLength = this->prefix; AddressRow.DadState = IpDadStatePreferred; Error = CreateUnicastIpAddressEntry(&AddressRow); if (Error != ERROR_SUCCESS) { spdlog::critical("create unicast ip address entry failed: {}", Error); return -1; } MIB_IPINTERFACE_ROW Interface = {0}; Interface.Family = AF_INET; Interface.InterfaceLuid = AddressRow.InterfaceLuid; Error = GetIpInterfaceEntry(&Interface); if (Error != NO_ERROR) { spdlog::critical("get ip interface entry failed: {}", Error); return -1; } this->ifindex = Interface.InterfaceIndex; Interface.SitePrefixLength = 0; Interface.NlMtu = this->mtu; Error = SetIpInterfaceEntry(&Interface); if (Error != NO_ERROR) { spdlog::critical("set ip interface entry failed: {}", Error); return -1; } this->session = WintunStartSession(this->adapter, WINTUN_MIN_RING_CAPACITY); if (!this->session) { spdlog::critical("start wintun session failed: {}", GetLastError()); return -1; } return 0; } int down() { while (!routes.empty()) { DeleteIpForwardEntry(&routes.top()); routes.pop(); } if (this->session) { WintunEndSession(this->session); this->session = NULL; } if (this->adapter) { WintunCloseAdapter(this->adapter); this->adapter = NULL; } return 0; } int read(std::string &buffer) { if (this->session) { DWORD size; BYTE *packet = WintunReceivePacket(this->session, &size); if (packet) { buffer.assign((char *)packet, size); WintunReleaseReceivePacket(this->session, packet); return size; } if (GetLastError() == ERROR_NO_MORE_ITEMS) { WaitForSingleObject(WintunGetReadWaitEvent(this->session), 1000); return 0; } spdlog::error("wintun read failed: {}", GetLastError()); } return -1; } int write(const std::string &buffer) { if (this->session) { BYTE *packet = WintunAllocateSendPacket(this->session, buffer.size()); if (packet) { memcpy(packet, buffer.c_str(), buffer.size()); WintunSendPacket(this->session, packet); return buffer.size(); } if (GetLastError() == ERROR_BUFFER_OVERFLOW) { return 0; } spdlog::error("wintun write failed: {}", GetLastError()); } return -1; } int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { MIB_IPFORWARDROW route; route.dwForwardDest = dst; route.dwForwardMask = mask; route.dwForwardNextHop = nexthop; route.dwForwardIfIndex = this->ifindex; route.dwForwardProto = MIB_IPPROTO_NETMGMT; route.dwForwardNextHopAS = 0; route.dwForwardAge = INFINITE; route.dwForwardType = MIB_IPROUTE_TYPE_INDIRECT; route.dwForwardMetric1 = route.dwForwardType + 1; route.dwForwardMetric2 = MIB_IPROUTE_METRIC_UNUSED; route.dwForwardMetric3 = MIB_IPROUTE_METRIC_UNUSED; route.dwForwardMetric4 = MIB_IPROUTE_METRIC_UNUSED; route.dwForwardMetric5 = MIB_IPROUTE_METRIC_UNUSED; DWORD result = CreateIpForwardEntry(&route); if (result == NO_ERROR) { routes.push(route); } else { spdlog::error("add route failed: {}", result); } return 0; } private: std::string name; IP4 ip; uint32_t prefix; int mtu; int timeout; NET_IFINDEX ifindex; std::stack routes; WINTUN_ADAPTER_HANDLE adapter = NULL; WINTUN_SESSION_HANDLE session = NULL; }; } // namespace candy namespace candy { Tun::Tun() { this->impl = std::make_shared(); } Tun::~Tun() { this->impl.reset(); } int Tun::setName(const std::string &name) { std::shared_ptr tun; tun = std::any_cast>(this->impl); tun->setName(name); return 0; } int Tun::setAddress(const std::string &cidr) { std::shared_ptr tun; Address address; if (address.fromCidr(cidr)) { return -1; } spdlog::info("client address: {}", address.toCidr()); tun = std::any_cast>(this->impl); if (tun->setIP(address.Host())) { return -1; } if (tun->setPrefix(address.Mask().toPrefix())) { return -1; } return 0; } IP4 Tun::getIP() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->getIP(); } int Tun::setMTU(int mtu) { std::shared_ptr tun; tun = std::any_cast>(this->impl); if (tun->setMTU(mtu)) { return -1; } return 0; } int Tun::up() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->up(); } int Tun::down() { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->down(); } int Tun::read(std::string &buffer) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->read(buffer); } int Tun::write(const std::string &buffer) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->write(buffer); } int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { std::shared_ptr tun; tun = std::any_cast>(this->impl); return tun->setSysRtTable(dst, mask, nexthop); } } // namespace candy #endif ================================================ FILE: candy/src/utils/atomic.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_UTILS_ATOMIC_H #define CANDY_UTILS_ATOMIC_H #include namespace candy { namespace Utils { template class Atomic { public: explicit Atomic(T initial = T()) : value(initial) {} T load() const { std::lock_guard lock(mutex); return value; } void store(T new_value) { std::lock_guard lock(mutex); value = new_value; cv.notify_all(); } void wait(const T &expected) { std::unique_lock lock(mutex); cv.wait(lock, [this, &expected] { return value != expected; }); } template void wait_until(Predicate pred) { std::unique_lock lock(mutex); cv.wait(lock, pred); } void notify_one() { std::lock_guard lock(mutex); cv.notify_one(); } void notify_all() { std::lock_guard lock(mutex); cv.notify_all(); } private: T value; mutable std::mutex mutex; std::condition_variable cv; }; } // namespace Utils } // namespace candy #endif ================================================ FILE: candy/src/utils/codecvt.cc ================================================ #include #if POCO_OS == POCO_OS_WINDOWS_NT #include "utils/codecvt.h" #include namespace candy { std::string UTF16ToUTF8(const std::wstring &utf16Str) { if (utf16Str.empty()) return ""; int utf8Size = WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, nullptr, 0, nullptr, nullptr); if (utf8Size == 0) { return ""; } std::string utf8Str(utf8Size, '\0'); WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, &utf8Str[0], utf8Size, nullptr, nullptr); utf8Str.resize(utf8Size - 1); return utf8Str; } std::wstring UTF8ToUTF16(const std::string &utf8Str) { if (utf8Str.empty()) return L""; int utf16Size = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0); if (utf16Size == 0) { return L""; } std::wstring utf16Str(utf16Size, L'\0'); MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &utf16Str[0], utf16Size); utf16Str.resize(utf16Size - 1); return utf16Str; } } // namespace candy #endif ================================================ FILE: candy/src/utils/codecvt.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_UTILS_CODECVT_H #define CANDY_UTILS_CODECVT_H #include namespace candy { std::string UTF16ToUTF8(const std::wstring &utf16Str); std::wstring UTF8ToUTF16(const std::string &utf8Str); } // namespace candy #endif ================================================ FILE: candy/src/utils/random.cc ================================================ // SPDX-License-Identifier: MIT #include "utils/random.h" #include #include #include namespace { int randomHex() { std::random_device device; std::mt19937 engine(device()); std::uniform_int_distribution distrib(0, 15); return distrib(engine); } } // namespace namespace candy { uint32_t randomUint32() { std::random_device device; std::mt19937 engine(device()); std::uniform_int_distribution distrib; return distrib(engine); } std::string randomHexString(int length) { std::stringstream ss; for (int i = 0; i < length; i++) { ss << std::hex << randomHex(); } return ss.str(); } } // namespace candy ================================================ FILE: candy/src/utils/random.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_UTILS_RANDOM_H #define CANDY_UTILS_RANDOM_H #include #include namespace candy { uint32_t randomUint32(); std::string randomHexString(int length); } // namespace candy #endif ================================================ FILE: candy/src/utils/time.cc ================================================ // SPDX-License-Identifier: MIT #include "utils/time.h" #include "core/net.h" #include #include #include #include #include #include #include #include #include #include namespace candy { int64_t unixTime() { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } int64_t bootTime() { using namespace std::chrono; auto now = steady_clock::now(); return duration_cast(now.time_since_epoch()).count(); } std::string getCurrentTimeWithMillis() { auto now = std::chrono::system_clock::now(); auto ms_tp = std::chrono::time_point_cast(now); auto epoch = ms_tp.time_since_epoch(); auto value = std::chrono::duration_cast(epoch).count(); std::time_t now_time_t = std::chrono::system_clock::to_time_t(now); std::tm *ptm = std::localtime(&now_time_t); std::ostringstream oss; oss << std::put_time(ptm, "%Y-%m-%d %H:%M:%S"); oss << '.' << std::setfill('0') << std::setw(3) << (value % 1000); return oss.str(); } } // namespace candy ================================================ FILE: candy/src/utils/time.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_UTILS_TIME_H #define CANDY_UTILS_TIME_H #include #include namespace candy { int64_t unixTime(); int64_t bootTime(); std::string getCurrentTimeWithMillis(); } // namespace candy #endif ================================================ FILE: candy/src/websocket/client.cc ================================================ // SPDX-License-Identifier: MIT #include "websocket/client.h" #include "core/client.h" #include "core/message.h" #include "core/net.h" #include "core/version.h" #include "utils/time.h" #include "websocket/message.h" #include #include #include #include #include #include #include #include #include namespace candy { int WebSocketClient::setName(const std::string &name) { this->name = name; return 0; } int WebSocketClient::setPassword(const std::string &password) { this->password = password; return 0; } int WebSocketClient::setWsServerUri(const std::string &uri) { this->wsServerUri = uri; return 0; } int WebSocketClient::setExptTunAddress(const std::string &cidr) { this->exptTunCidr = cidr; return 0; } int WebSocketClient::setAddress(const std::string &cidr) { this->tunCidr = cidr; return 0; } int WebSocketClient::setVirtualMac(const std::string &vmac) { this->vmac = vmac; return 0; } std::string WebSocketClient::getTunCidr() const { return this->tunCidr; } int WebSocketClient::run(Client *client) { this->client = client; if (connect()) { spdlog::critical("websocket client connect failed"); return -1; } sendVirtualMacMsg(); if (this->tunCidr.empty()) { sendExptTunMsg(); } else { sendAuthMsg(); } this->msgThread = std::thread([&] { spdlog::debug("start thread: websocket client msg"); while (getClient().isRunning()) { handleWsQueue(); } getClient().shutdown(); spdlog::debug("stop thread: websocket client msg"); }); this->wsThread = std::thread([&] { spdlog::debug("start thread: websocket client ws"); while (getClient().isRunning()) { if (handleWsConn()) { break; } } getClient().shutdown(); spdlog::debug("stop thread: websocket client ws"); }); return 0; } int WebSocketClient::wait() { if (this->msgThread.joinable()) { this->msgThread.join(); } if (this->wsThread.joinable()) { this->wsThread.join(); } return 0; } void WebSocketClient::handleWsQueue() { Msg msg = this->client->getWsMsgQueue().read(); switch (msg.kind) { case MsgKind::TIMEOUT: break; case MsgKind::PACKET: handlePacket(std::move(msg)); break; case MsgKind::PUBINFO: handlePubInfo(std::move(msg)); break; case MsgKind::DISCOVERY: handleDiscovery(std::move(msg)); break; default: spdlog::warn("unexcepted websocket message type: {}", static_cast(msg.kind)); break; } } void WebSocketClient::handlePacket(Msg msg) { IP4Header *header = (IP4Header *)msg.data.data(); msg.data.insert(0, 1, WsMsgKind::FORWARD); sendFrame(msg.data); } void WebSocketClient::handlePubInfo(Msg msg) { CoreMsg::PubInfo *info = (CoreMsg::PubInfo *)(msg.data.data()); if (info->local) { WsMsg::ConnLocal buffer; buffer.ge.src = info->src; buffer.ge.dst = info->dst; buffer.ip = info->ip; buffer.port = hton(info->port); sendFrame(&buffer, sizeof(buffer)); } else { WsMsg::Conn buffer; buffer.src = info->src; buffer.dst = info->dst; buffer.ip = info->ip; buffer.port = hton(info->port); sendFrame(&buffer, sizeof(buffer)); } } void WebSocketClient::handleDiscovery(Msg msg) { sendDiscoveryMsg(IP4("255.255.255.255")); } int WebSocketClient::handleWsConn() { try { std::string buffer; int flags = 0; if (!this->ws->poll(Poco::Timespan(1, 0), Poco::Net::Socket::SELECT_READ | Poco::Net::Socket::SELECT_ERROR)) { if (bootTime() - this->timestamp > 30000) { spdlog::warn("websocket pong timeout"); return -1; } if (bootTime() - this->timestamp > 15000) { sendPingMessage(); } return 0; } if (this->ws->getError()) { spdlog::warn("websocket connection error: {}", this->ws->getError()); return -1; } buffer.resize(1500); int length = this->ws->receiveFrame(buffer.data(), buffer.size(), flags); if (length == 0 && flags == 0) { spdlog::info("abnormal disconnect"); return -1; } if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PING) { this->timestamp = bootTime(); flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PONG; sendFrame(buffer.data(), length, flags); return 0; } if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PONG) { this->timestamp = bootTime(); return 0; } if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_CLOSE) { spdlog::info("websocket close: {}", buffer); return -1; } if (length > 0) { this->timestamp = bootTime(); buffer.resize(length); handleWsMsg(std::move(buffer)); return 0; } return 0; } catch (std::exception &e) { spdlog::warn("handle ws conn failed: {}", e.what()); return -1; } } void WebSocketClient::handleWsMsg(std::string buffer) { uint8_t msgKind = buffer.front(); switch (msgKind) { case WsMsgKind::FORWARD: handleForwardMsg(std::move(buffer)); break; case WsMsgKind::EXPTTUN: handleExptTunMsg(std::move(buffer)); break; case WsMsgKind::UDP4CONN: handleUdp4ConnMsg(std::move(buffer)); break; case WsMsgKind::DISCOVERY: handleDiscoveryMsg(std::move(buffer)); break; case WsMsgKind::ROUTE: handleRouteMsg(std::move(buffer)); break; case WsMsgKind::GENERAL: handleGeneralMsg(std::move(buffer)); break; default: spdlog::debug("unknown websocket message kind: {}", msgKind); break; } } void WebSocketClient::handleForwardMsg(std::string buffer) { if (buffer.size() < sizeof(WsMsg::Forward)) { spdlog::warn("invalid forward message: {:n}", spdlog::to_hex(buffer)); return; } // 移除一个字节的类型 buffer.erase(0, 1); // 尝试与源地址建立对等连接 IP4Header *header = (IP4Header *)buffer.data(); // 每次通过服务端转发收到报文都触发一次尝试 P2P 连接, 用于暗示通过服务端转发是个非常耗时的操作 this->client->getPeerMsgQueue().write(Msg(MsgKind::TRYP2P, header->saddr.toString())); // 最后把报文移动到 TUN 模块, 因为有移动操作所以必须在最后执行 this->client->getTunMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer))); } void WebSocketClient::handleExptTunMsg(std::string buffer) { if (buffer.size() < sizeof(WsMsg::ExptTun)) { spdlog::warn("invalid expt tun message: {:n}", spdlog::to_hex(buffer)); return; } WsMsg::ExptTun *header = (WsMsg::ExptTun *)buffer.data(); Address exptTun(header->cidr); this->tunCidr = exptTun.toCidr(); sendAuthMsg(); } void WebSocketClient::handleUdp4ConnMsg(std::string buffer) { if (buffer.size() < sizeof(WsMsg::Conn)) { spdlog::warn("invalid udp4conn message: {:n}", spdlog::to_hex(buffer)); return; } WsMsg::Conn *header = (WsMsg::Conn *)buffer.data(); CoreMsg::PubInfo info = {.src = header->src, .dst = header->dst, .ip = header->ip, .port = ntoh(header->port)}; this->client->getPeerMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info)))); } void WebSocketClient::handleDiscoveryMsg(std::string buffer) { if (buffer.size() < sizeof(WsMsg::Discovery)) { spdlog::warn("invalid discovery message: {:n}", spdlog::to_hex(buffer)); return; } WsMsg::Discovery *header = (WsMsg::Discovery *)buffer.data(); if (header->dst == IP4("255.255.255.255")) { sendDiscoveryMsg(header->src); } this->client->getPeerMsgQueue().write(Msg(MsgKind::TRYP2P, header->src.toString())); } void WebSocketClient::handleRouteMsg(std::string buffer) { if (buffer.size() < sizeof(WsMsg::SysRoute)) { spdlog::warn("invalid route message: {:n}", spdlog::to_hex(buffer)); return; } WsMsg::SysRoute *header = (WsMsg::SysRoute *)buffer.data(); SysRouteEntry *rt = header->rtTable; for (uint8_t idx = 0; idx < header->size; ++idx) { this->client->getTunMsgQueue().write(Msg(MsgKind::SYSRT, std::string((char *)(rt + idx), sizeof(SysRouteEntry)))); this->client->getPeerMsgQueue().write(Msg(MsgKind::SYSRT)); } } void WebSocketClient::handleGeneralMsg(std::string buffer) { if (buffer.size() < sizeof(WsMsg::ConnLocal)) { spdlog::warn("invalid udp4conn local message: {:n}", spdlog::to_hex(buffer)); return; } WsMsg::ConnLocal *header = (WsMsg::ConnLocal *)buffer.data(); CoreMsg::PubInfo info = { .src = header->ge.src, .dst = header->ge.dst, .ip = header->ip, .port = ntoh(header->port), .local = true, }; this->client->getPeerMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info)))); } void WebSocketClient::sendFrame(const std::string &buffer, int flags) { sendFrame(buffer.c_str(), buffer.size(), flags); } void WebSocketClient::sendFrame(const void *buffer, int length, int flags) { if (this->ws) { try { this->ws->sendFrame(buffer, length, flags); } catch (std::exception &e) { spdlog::critical("websocket send frame failed: {}", e.what()); } } } void WebSocketClient::sendVirtualMacMsg() { WsMsg::VMac buffer(this->vmac); buffer.updateHash(this->password); sendFrame(&buffer, sizeof(buffer)); } void WebSocketClient::sendExptTunMsg() { Address exptTun(this->exptTunCidr); WsMsg::ExptTun buffer(exptTun.toCidr()); buffer.updateHash(this->password); sendFrame(&buffer, sizeof(buffer)); } void WebSocketClient::sendAuthMsg() { Address address(this->tunCidr); WsMsg::Auth buffer(address.Host()); buffer.updateHash(this->password); sendFrame(&buffer, sizeof(buffer)); this->client->getTunMsgQueue().write(Msg(MsgKind::TUNADDR, address.toCidr())); this->client->getPeerMsgQueue().write(Msg(MsgKind::TUNADDR, address.toCidr())); sendPingMessage(); } void WebSocketClient::sendDiscoveryMsg(IP4 dst) { Address address(this->tunCidr); WsMsg::Discovery buffer; buffer.dst = dst; buffer.src = address.Host(); sendFrame(&buffer, sizeof(buffer)); } std::string WebSocketClient::hostName() { char hostname[64] = {0}; if (!gethostname(hostname, sizeof(hostname))) { return std::string(hostname, strnlen(hostname, sizeof(hostname))); } return ""; } void WebSocketClient::sendPingMessage() { int flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PING; sendFrame(pingMessage, flags); } int WebSocketClient::connect() { std::shared_ptr uri; try { uri = std::make_shared(wsServerUri); } catch (std::exception &e) { spdlog::critical("invalid websocket server: {}: {}", wsServerUri, e.what()); return -1; } try { const std::string path = uri->getPath().empty() ? "/" : uri->getPath(); Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, path, Poco::Net::HTTPMessage::HTTP_1_1); Poco::Net::HTTPResponse response; if (uri->getScheme() == "wss") { using Poco::Net::Context; Context::Ptr context = new Context(Context::TLS_CLIENT_USE, "", "", "", Context::VERIFY_NONE); Poco::Net::HTTPSClientSession cs(uri->getHost(), uri->getPort(), context); this->ws = std::make_shared(cs, request, response); } else if (uri->getScheme() == "ws") { Poco::Net::HTTPClientSession cs(uri->getHost(), uri->getPort()); this->ws = std::make_shared(cs, request, response); } else { spdlog::critical("invalid websocket scheme: {}", wsServerUri); return -1; } // Blocking mode may cause receiveFrame to hang and use 100% CPU this->ws->setBlocking(false); this->timestamp = bootTime(); this->pingMessage = fmt::format("candy::{}::{}::{}", CANDY_SYSTEM, CANDY_VERSION, hostName()); spdlog::debug("client info: {}", this->pingMessage); return 0; } catch (std::exception &e) { spdlog::critical("websocket connect failed: {}", e.what()); return -1; } } int WebSocketClient::disconnect() { try { if (this->ws) { this->ws->shutdown(); this->ws->close(); this->ws.reset(); } } catch (std::exception &e) { spdlog::debug("websocket disconnect failed: {}", e.what()); } return 0; } Client &WebSocketClient::getClient() { return *this->client; } } // namespace candy ================================================ FILE: candy/src/websocket/client.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_WEBSOCKET_CLIENT_H #define CANDY_WEBSOCKET_CLIENT_H #include "core/message.h" #include "core/net.h" #include #include #include #include #include namespace candy { class Client; class WebSocketClient { public: int setName(const std::string &name); int setPassword(const std::string &password); int setWsServerUri(const std::string &uri); int setExptTunAddress(const std::string &cidr); int setAddress(const std::string &cidr); int setVirtualMac(const std::string &vmac); int setTunUpdateCallback(std::function callback); std::string getTunCidr() const; int run(Client *client); int wait(); private: void handleWsQueue(); void handlePacket(Msg msg); void handlePubInfo(Msg msg); void handleDiscovery(Msg msg); std::thread msgThread; int handleWsConn(); void handleWsMsg(std::string buffer); void handleForwardMsg(std::string buffer); void handleExptTunMsg(std::string buffer); void handleUdp4ConnMsg(std::string buffer); void handleDiscoveryMsg(std::string buffer); void handleRouteMsg(std::string buffer); void handleGeneralMsg(std::string buffer); std::thread wsThread; void sendFrame(const std::string &buffer, int flags = Poco::Net::WebSocket::FRAME_BINARY); void sendFrame(const void *buffer, int length, int flags = Poco::Net::WebSocket::FRAME_BINARY); void sendVirtualMacMsg(); void sendExptTunMsg(); void sendAuthMsg(); void sendDiscoveryMsg(IP4 dst); std::function addressUpdateCallback; private: std::string hostName(); void sendPingMessage(); private: int connect(); int disconnect(); std::shared_ptr ws; std::string pingMessage; int64_t timestamp; private: std::string wsServerUri; std::string exptTunCidr; std::string tunCidr; std::string vmac; std::string name; std::string password; Client &getClient(); Client *client; }; } // namespace candy #endif ================================================ FILE: candy/src/websocket/message.cc ================================================ // SPDX-License-Identifier: MIT #include "websocket/message.h" #include "utils/time.h" namespace candy { namespace WsMsg { Auth::Auth(IP4 ip) { this->type = WsMsgKind::AUTH; this->ip = ip; this->timestamp = hton(unixTime()); } void Auth::updateHash(const std::string &password) { std::string data; data.append(password); data.append((char *)&ip, sizeof(ip)); data.append((char *)×tamp, sizeof(timestamp)); SHA256((unsigned char *)data.data(), data.size(), this->hash); } bool Auth::check(const std::string &password) { int64_t localTime = unixTime(); int64_t remoteTime = ntoh(this->timestamp); if (std::abs(localTime - remoteTime) > 300) { spdlog::warn("auth header timestamp check failed: server {} client {}", localTime, remoteTime); } uint8_t reported[SHA256_DIGEST_LENGTH]; std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH); updateHash(password); if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) { spdlog::warn("auth header hash check failed"); return false; } return true; } Forward::Forward() { this->type = WsMsgKind::FORWARD; } ExptTun::ExptTun(const std::string &cidr) { this->type = WsMsgKind::EXPTTUN; this->timestamp = hton(unixTime()); std::strcpy(this->cidr, cidr.c_str()); } void ExptTun::updateHash(const std::string &password) { std::string data; data.append(password); data.append((char *)&this->timestamp, sizeof(this->timestamp)); SHA256((unsigned char *)data.data(), data.size(), this->hash); } bool ExptTun::check(const std::string &password) { int64_t localTime = unixTime(); int64_t remoteTime = ntoh(this->timestamp); if (std::abs(localTime - remoteTime) > 300) { spdlog::warn("expected address header timestamp check failed: server {} client {}", localTime, remoteTime); } uint8_t reported[SHA256_DIGEST_LENGTH]; std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH); updateHash(password); if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) { spdlog::warn("expected address header hash check failed"); return false; } return true; } Conn::Conn() { this->type = WsMsgKind::UDP4CONN; } VMac::VMac(const std::string &vmac) { this->type = WsMsgKind::VMAC; this->timestamp = hton(unixTime()); if (vmac.length() >= sizeof(this->vmac)) { memcpy(this->vmac, vmac.c_str(), sizeof(this->vmac)); } else { memset(this->vmac, 0, sizeof(this->vmac)); } } void VMac::updateHash(const std::string &password) { std::string data; data.append(password); data.append((char *)&this->vmac, sizeof(this->vmac)); data.append((char *)&this->timestamp, sizeof(this->timestamp)); SHA256((unsigned char *)data.data(), data.size(), this->hash); } bool VMac::check(const std::string &password) { int64_t localTime = unixTime(); int64_t remoteTime = ntoh(this->timestamp); if (std::abs(localTime - remoteTime) > 300) { spdlog::warn("vmac message timestamp check failed: server {} client {}", localTime, remoteTime); } uint8_t reported[SHA256_DIGEST_LENGTH]; std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH); updateHash(password); if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) { spdlog::warn("vmac message hash check failed"); return false; } return true; } Discovery::Discovery() { this->type = WsMsgKind::DISCOVERY; } General::General() { this->type = WsMsgKind::GENERAL; } ConnLocal::ConnLocal() { this->ge.subtype = GeSubType::LOCALUDP4CONN; this->ge.extra = 0; } } // namespace WsMsg } // namespace candy ================================================ FILE: candy/src/websocket/message.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_WEBSOCKET_MESSAGE_H #define CANDY_WEBSOCKET_MESSAGE_H #include "core/net.h" #include namespace candy { namespace WsMsgKind { constexpr uint8_t AUTH = 0; constexpr uint8_t FORWARD = 1; constexpr uint8_t EXPTTUN = 2; constexpr uint8_t UDP4CONN = 3; constexpr uint8_t VMAC = 4; constexpr uint8_t DISCOVERY = 5; constexpr uint8_t ROUTE = 6; constexpr uint8_t GENERAL = 255; } // namespace WsMsgKind namespace GeSubType { constexpr uint8_t LOCALUDP4CONN = 0; } namespace WsMsg { struct __attribute__((packed)) Auth { uint8_t type; IP4 ip; int64_t timestamp; uint8_t hash[SHA256_DIGEST_LENGTH]; Auth(IP4 ip); void updateHash(const std::string &password); bool check(const std::string &password); }; struct __attribute__((packed)) Forward { uint8_t type; IP4Header iph; Forward(); }; struct __attribute__((packed)) ExptTun { uint8_t type; int64_t timestamp; char cidr[32] = {0}; uint8_t hash[SHA256_DIGEST_LENGTH]; ExptTun(const std::string &cidr); void updateHash(const std::string &password); bool check(const std::string &password); }; struct __attribute__((packed)) Conn { uint8_t type; IP4 src; IP4 dst; IP4 ip; uint16_t port; Conn(); }; struct __attribute__((packed)) VMac { uint8_t type; uint8_t vmac[16]; int64_t timestamp; uint8_t hash[SHA256_DIGEST_LENGTH]; VMac(const std::string &vmac); void updateHash(const std::string &password); bool check(const std::string &password); }; struct __attribute__((packed)) Discovery { uint8_t type; IP4 src; IP4 dst; Discovery(); }; struct __attribute__((packed)) SysRoute { uint8_t type; uint8_t size; uint16_t reserved; SysRouteEntry rtTable[0]; }; struct __attribute__((packed)) General { uint8_t type; uint8_t subtype; uint16_t extra; IP4 src; IP4 dst; General(); }; struct __attribute__((packed)) ConnLocal { General ge; IP4 ip; uint16_t port; ConnLocal(); }; } // namespace WsMsg } // namespace candy #endif ================================================ FILE: candy/src/websocket/server.cc ================================================ // SPDX-License-Identifier: MIT #include "websocket/server.h" #include "core/net.h" #include "utils/time.h" #include "websocket/message.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * Poco 的 WebSocket 服务端接口有点难用,简单封装一下,并对外提供一个回调函数,回调函数的参数表示独立的 * WebSocket客户端,函数返回会释放连接 */ namespace { using WebSocketHandler = std::function; class HTTPRequestHandler : public Poco::Net::HTTPRequestHandler { public: HTTPRequestHandler(WebSocketHandler wsHandler) : wsHandler(wsHandler) {} void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) { try { Poco::Net::WebSocket ws(request, response); wsHandler(ws); ws.close(); } catch (const std::exception &e) { response.setStatus(Poco::Net::HTTPResponse::HTTP_FORBIDDEN); response.setReason("Forbidden"); response.setContentLength(0); response.send(); } } private: WebSocketHandler wsHandler; }; class HTTPRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory { public: HTTPRequestHandlerFactory(WebSocketHandler wsHandler) : wsHandler(wsHandler) {} Poco::Net::HTTPRequestHandler *createRequestHandler(const Poco::Net::HTTPServerRequest &request) { return new HTTPRequestHandler(wsHandler); } private: WebSocketHandler wsHandler; }; }; // namespace namespace candy { void WsCtx::sendFrame(const std::string &frame, int flags) { this->ws->sendFrame(frame.data(), frame.size(), flags); } int WebSocketServer::setWebSocket(const std::string &uri) { try { Poco::URI parser(uri); if (parser.getScheme() != "ws") { spdlog::critical("websocket server only support ws"); return -1; } this->host = parser.getHost(); this->port = parser.getPort(); return 0; } catch (std::exception &e) { spdlog::critical("invalid websocket uri: {}: {}", uri, e.what()); return -1; } } int WebSocketServer::setPassword(const std::string &password) { this->password = password; return 0; } int WebSocketServer::setDHCP(const std::string &cidr) { if (cidr.empty()) { return 0; } return this->dhcp.fromCidr(cidr); } int WebSocketServer::setSdwan(const std::string &sdwan) { if (sdwan.empty()) { return 0; } std::string route; std::stringstream stream(sdwan); while (std::getline(stream, route, ';')) { std::string addr; SysRoute rt; std::stringstream ss(route); // dev if (!std::getline(ss, addr, ',') || rt.dev.fromCidr(addr) || rt.dev.Host() != rt.dev.Net()) { spdlog::critical("invalid route device: {}", route); return -1; } // dst if (!std::getline(ss, addr, ',') || rt.dst.fromCidr(addr) || rt.dst.Host() != rt.dst.Net()) { spdlog::critical("invalid route dest: {}", route); return -1; } // next if (!std::getline(ss, addr, ',') || rt.next.fromString(addr)) { spdlog::critical("invalid route nexthop: {}", route); return -1; } spdlog::info("route: dev={} dst={} next={}", rt.dev.toCidr(), rt.dst.toCidr(), rt.next.toString()); this->routes.push_back(rt); } return 0; } int WebSocketServer::run() { listen(); return 0; } int WebSocketServer::shutdown() { this->running = false; if (this->httpServer) { this->httpServer->stopAll(); } this->routes.clear(); return 0; } void WebSocketServer::handleMsg(WsCtx &ctx) { uint8_t msgKind = ctx.buffer.front(); switch (msgKind) { case WsMsgKind::AUTH: handleAuthMsg(ctx); break; case WsMsgKind::FORWARD: handleForwardMsg(ctx); break; case WsMsgKind::EXPTTUN: handleExptTunMsg(ctx); break; case WsMsgKind::UDP4CONN: handleUdp4ConnMsg(ctx); break; case WsMsgKind::VMAC: handleVMacMsg(ctx); break; case WsMsgKind::DISCOVERY: handleDiscoveryMsg(ctx); break; case WsMsgKind::GENERAL: HandleGeneralMsg(ctx); break; } } void WebSocketServer::handleAuthMsg(WsCtx &ctx) { if (ctx.buffer.length() < sizeof(WsMsg::Auth)) { spdlog::warn("invalid auth message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::Auth *header = (WsMsg::Auth *)ctx.buffer.data(); if (!header->check(this->password)) { spdlog::warn("auth header check failed: buffer {:n}", spdlog::to_hex(ctx.buffer)); ctx.status = -1; return; } ctx.ip = header->ip; { std::unique_lock lock(ipCtxMutex); auto it = ipCtxMap.find(header->ip); if (it != ipCtxMap.end()) { it->second->status = -1; spdlog::info("reconnect: {}", it->second->ip.toString()); } else { spdlog::info("connect: {}", ctx.ip.toString()); } ipCtxMap[header->ip] = &ctx; } updateSysRoute(ctx); } void WebSocketServer::handleForwardMsg(WsCtx &ctx) { if (ctx.ip.empty()) { spdlog::debug("unauthorized forward websocket client"); ctx.status = -1; return; } if (ctx.buffer.length() < sizeof(WsMsg::Forward)) { spdlog::debug("invalid forawrd message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::Forward *header = (WsMsg::Forward *)ctx.buffer.data(); { std::shared_lock lock(this->ipCtxMutex); auto it = this->ipCtxMap.find(header->iph.daddr); if (it != this->ipCtxMap.end()) { it->second->sendFrame(ctx.buffer); return; } } bool broadcast = [&] { // 多播地址 if ((header->iph.daddr & IP4("240.0.0.0")) == IP4("224.0.0.0")) { return true; } // 广播 if (header->iph.daddr == IP4("255.255.255.255")) { return true; } // 服务端没有配置动态分配地址的范围,没法检查是否为定向广播 if (this->dhcp.empty()) { return false; } // 网络号不同,不是定向广播 if ((this->dhcp.Mask() & header->iph.daddr) != this->dhcp.Net()) { return false; } // 主机号部分不全为 1,不是定向广播 if (~((header->iph.daddr & ~this->dhcp.Mask()) ^ this->dhcp.Mask())) { return false; } return true; }(); if (broadcast) { std::shared_lock lock(this->ipCtxMutex); for (auto c : this->ipCtxMap) { if (c.second->ip != ctx.ip) { c.second->sendFrame(ctx.buffer); } } return; } spdlog::debug("forward failed: source {} dest {}", header->iph.saddr.toString(), header->iph.daddr.toString()); return; } void WebSocketServer::handleExptTunMsg(WsCtx &ctx) { if (ctx.buffer.length() < sizeof(WsMsg::ExptTun)) { spdlog::warn("invalid dynamic address message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::ExptTun *header = (WsMsg::ExptTun *)ctx.buffer.data(); if (!header->check(this->password)) { spdlog::warn("dynamic address header check failed: buffer {:n}", spdlog::to_hex(ctx.buffer)); ctx.status = -1; return; } if (this->dhcp.empty()) { spdlog::warn("unable to allocate dynamic address"); ctx.status = -1; return; } Address exptTun; if (exptTun.fromCidr(header->cidr)) { spdlog::warn("dynamic address header cidr invalid: buffer {:n}", spdlog::to_hex(ctx.buffer)); ctx.status = -1; return; } // 判断能否直接使用申请的地址 bool direct = [&]() { if (dhcp.Net() != exptTun.Net()) { return false; } std::shared_lock lock(this->ipCtxMutex); auto oldCtx = this->ipCtxMap.find(exptTun.Host()); if (oldCtx == this->ipCtxMap.end()) { return true; } return ctx.vmac == oldCtx->second->vmac; }(); if (!direct) { exptTun = this->dhcp; std::shared_lock lock(this->ipCtxMutex); do { exptTun = exptTun.Next(); if (exptTun.Host() == this->dhcp.Host()) { spdlog::warn("all addresses in the network are assigned"); ctx.status = -1; return; } } while (!exptTun.isValid() && this->ipCtxMap.find(exptTun.Host()) != this->ipCtxMap.end()); this->dhcp = exptTun; } header->timestamp = hton(unixTime()); std::strcpy(header->cidr, exptTun.toCidr().c_str()); header->updateHash(this->password); ctx.sendFrame(ctx.buffer); } void WebSocketServer::handleUdp4ConnMsg(WsCtx &ctx) { if (ctx.ip.empty()) { spdlog::debug("unauthorized peer websocket client"); ctx.status = -1; return; } if (ctx.buffer.length() < sizeof(WsMsg::Conn)) { spdlog::warn("invalid peer conn message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::Conn *header = (WsMsg::Conn *)ctx.buffer.data(); if (ctx.ip != header->src) { spdlog::debug("peer source address does not match: auth {} source {}", ctx.ip.toString(), header->src.toString()); ctx.status = -1; return; } std::shared_lock lock(this->ipCtxMutex); auto it = this->ipCtxMap.find(header->dst); if (it == this->ipCtxMap.end()) { spdlog::debug("peer dest address not logged in: source {} dst {}", header->src.toString(), header->dst.toString()); return; } it->second->sendFrame(ctx.buffer); return; } void WebSocketServer::handleVMacMsg(WsCtx &ctx) { if (ctx.buffer.length() < sizeof(WsMsg::VMac)) { spdlog::warn("invalid vmac message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::VMac *header = (WsMsg::VMac *)ctx.buffer.data(); if (!header->check(this->password)) { spdlog::warn("vmac message check failed: buffer {:n}", spdlog::to_hex(ctx.buffer)); ctx.status = -1; return; } ctx.vmac.assign((char *)header->vmac, sizeof(header->vmac)); return; } void WebSocketServer::handleDiscoveryMsg(WsCtx &ctx) { if (ctx.ip.empty()) { spdlog::debug("unauthorized discovery websocket client"); ctx.status = -1; return; } if (ctx.buffer.length() < sizeof(WsMsg::Discovery)) { spdlog::debug("invalid discovery message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::Discovery *header = (WsMsg::Discovery *)ctx.buffer.data(); if (ctx.ip != header->src) { spdlog::debug("discovery source address does not match: auth {} source {}", ctx.ip.toString(), header->src.toString()); ctx.status = -1; return; } std::shared_lock lock(this->ipCtxMutex); if (header->dst == IP4("255.255.255.255")) { for (auto c : this->ipCtxMap) { if (c.first != header->src) { c.second->sendFrame(ctx.buffer); } } return; } auto it = this->ipCtxMap.find(header->dst); if (it != this->ipCtxMap.end()) { it->second->sendFrame(ctx.buffer); return; } } void WebSocketServer::HandleGeneralMsg(WsCtx &ctx) { if (ctx.ip.empty()) { spdlog::debug("unauthorized general websocket client"); ctx.status = -1; return; } if (ctx.buffer.length() < sizeof(WsMsg::General)) { spdlog::debug("invalid general message: len {}", ctx.buffer.length()); ctx.status = -1; return; } WsMsg::General *header = (WsMsg::General *)ctx.buffer.data(); if (ctx.ip != header->src) { spdlog::debug("general source address does not match: auth {} source {}", ctx.ip.toString(), header->src.toString()); ctx.status = -1; return; } std::shared_lock lock(this->ipCtxMutex); if (header->dst == IP4("255.255.255.255")) { for (auto c : this->ipCtxMap) { if (c.first != header->src) { c.second->sendFrame(ctx.buffer); } } return; } auto it = this->ipCtxMap.find(header->dst); if (it != this->ipCtxMap.end()) { it->second->sendFrame(ctx.buffer); return; } } void WebSocketServer::updateSysRoute(WsCtx &ctx) { ctx.buffer.resize(sizeof(WsMsg::SysRoute)); WsMsg::SysRoute *header = (WsMsg::SysRoute *)ctx.buffer.data(); memset(header, 0, sizeof(WsMsg::SysRoute)); header->type = WsMsgKind::ROUTE; for (auto rt : this->routes) { if ((rt.dev.Mask() & ctx.ip) == rt.dev.Host()) { SysRouteEntry item; item.dst = rt.dst.Net(); item.mask = rt.dst.Mask(); item.nexthop = rt.next; ctx.buffer.append((char *)(&item), sizeof(item)); header->size += 1; } // 100 条路由报文大小是 1204 字节,超过 100 条后分批发送 if (header->size > 100) { ctx.sendFrame(ctx.buffer); ctx.buffer.resize(sizeof(WsMsg::SysRoute)); header->size = 0; } } if (header->size > 0) { ctx.sendFrame(ctx.buffer); } } int WebSocketServer::listen() { try { Poco::Net::ServerSocket socket(Poco::Net::SocketAddress(host, port)); Poco::Net::HTTPServerParams *params = new Poco::Net::HTTPServerParams(); params->setMaxThreads(0x00FFFFFF); this->running = true; WebSocketHandler wsHandler = [this](Poco::Net::WebSocket &ws) { handleWebsocket(ws); }; this->httpServer = std::make_shared(new HTTPRequestHandlerFactory(wsHandler), socket, params); this->httpServer->start(); spdlog::info("listen on: {}:{}", host, port); return 0; } catch (std::exception &e) { spdlog::critical("listen failed: {}", e.what()); return -1; } } void WebSocketServer::handleWebsocket(Poco::Net::WebSocket &ws) { ws.setReceiveTimeout(Poco::Timespan(1, 0)); WsCtx ctx = {.ws = &ws}; int flags = 0; int length = 0; std::string buffer; while (this->running && ctx.status == 0) { try { buffer.resize(1500); length = ws.receiveFrame(buffer.data(), buffer.size(), flags); int frameOp = flags & Poco::Net::WebSocket::FRAME_OP_BITMASK; // 响应 Ping 报文 if (frameOp == Poco::Net::WebSocket::FRAME_OP_PING) { flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PONG; ws.sendFrame(buffer.data(), buffer.size(), flags); continue; } // 客户端主动关闭连接 if ((length == 0 && flags == 0) || frameOp == Poco::Net::WebSocket::FRAME_OP_CLOSE) { break; } if (frameOp == Poco::Net::WebSocket::FRAME_OP_BINARY && length > 0) { // 调整 buffer 为真实大小并移动到 ctx buffer.resize(length); ctx.buffer = std::move(buffer); // 处理客户端请求 handleMsg(ctx); // 重新初始化 buffer buffer = std::string(); } } catch (Poco::TimeoutException const &e) { // 超时异常,不做处理 continue; } catch (std::exception &e) { // 未知异常,退出这个客户端 spdlog::debug("handle websocket failed: {}", e.what()); break; } } { std::unique_lock lock(ipCtxMutex); auto it = ipCtxMap.find(ctx.ip); if (it != ipCtxMap.end() && it->second == &ctx) { ipCtxMap.erase(it); spdlog::info("disconnect: {}", ctx.ip.toString()); } } } } // namespace candy ================================================ FILE: candy/src/websocket/server.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_WEBSOCKET_SERVER_H #define CANDY_WEBSOCKET_SERVER_H #include "core/net.h" #include #include #include #include #include #include namespace candy { struct WsCtx { Poco::Net::WebSocket *ws; std::string buffer; int status; IP4 ip; std::string vmac; void sendFrame(const std::string &frame, int flags = Poco::Net::WebSocket::FRAME_BINARY); }; struct SysRoute { // 通过地址和掩码确定策略下发给哪些客户端 Address dev; // 系统路由策略中的地址掩码和下一跳 Address dst; IP4 next; }; class WebSocketServer { public: int setWebSocket(const std::string &uri); int setPassword(const std::string &password); int setDHCP(const std::string &cidr); int setSdwan(const std::string &sdwan); int run(); int shutdown(); private: std::string host; uint16_t port; std::string password; Address dhcp; std::list routes; private: void handleMsg(WsCtx &ctx); void handleAuthMsg(WsCtx &ctx); void handleForwardMsg(WsCtx &ctx); void handleExptTunMsg(WsCtx &ctx); void handleUdp4ConnMsg(WsCtx &ctx); void handleVMacMsg(WsCtx &ctx); void handleDiscoveryMsg(WsCtx &ctx); void HandleGeneralMsg(WsCtx &ctx); // 更新客户端系统路由 void updateSysRoute(WsCtx &ctx); // 保存 IP 到对应连接指针的映射 std::unordered_map ipCtxMap; // 操作 map 时需要加锁,以确保操作时指针有效 std::shared_mutex ipCtxMutex; bool running; private: // 开始监听,新的请求将调用 handleWebsocket int listen(); // 同步的处理每个客户独的请求,函数返回后连接将断开 void handleWebsocket(Poco::Net::WebSocket &ws); std::shared_ptr httpServer; }; } // namespace candy #endif ================================================ FILE: candy-cli/CMakeLists.txt ================================================ file(GLOB_RECURSE SOURCES "src/*.cc") add_executable(candy-cli ${SOURCES}) target_include_directories(candy-cli PUBLIC $ $ ) set_target_properties(candy-cli PROPERTIES OUTPUT_NAME "candy") target_link_libraries(candy-cli PRIVATE spdlog::spdlog) target_link_libraries(candy-cli PRIVATE Poco::Foundation Poco::JSON) target_link_libraries(candy-cli PRIVATE Candy::Library) install(TARGETS candy-cli) add_executable(Candy::CLI ALIAS candy-cli) ================================================ FILE: candy-cli/src/argparse.h ================================================ /* __ _ _ __ __ _ _ __ __ _ _ __ ___ ___ / _` | '__/ _` | '_ \ / _` | '__/ __|/ _ \ Argument Parser for Modern C++ | (_| | | | (_| | |_) | (_| | | \__ \ __/ http://github.com/p-ranav/argparse \__,_|_| \__, | .__/ \__,_|_| |___/\___| |___/|_| Licensed under the MIT License . SPDX-License-Identifier: MIT Copyright (c) 2019-2022 Pranav Srinivas Kumar and other contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #pragma once #include #ifndef ARGPARSE_MODULE_USE_STD_MODULE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif #ifndef ARGPARSE_CUSTOM_STRTOF #define ARGPARSE_CUSTOM_STRTOF strtof #endif #ifndef ARGPARSE_CUSTOM_STRTOD #define ARGPARSE_CUSTOM_STRTOD strtod #endif #ifndef ARGPARSE_CUSTOM_STRTOLD #define ARGPARSE_CUSTOM_STRTOLD strtold #endif namespace argparse { namespace details { // namespace for helper methods template struct HasContainerTraits : std::false_type {}; template <> struct HasContainerTraits : std::false_type {}; template <> struct HasContainerTraits : std::false_type {}; template struct HasContainerTraits().begin()), decltype(std::declval().end()), decltype(std::declval().size())>> : std::true_type {}; template inline constexpr bool IsContainer = HasContainerTraits::value; template struct HasStreamableTraits : std::false_type {}; template struct HasStreamableTraits() << std::declval())>> : std::true_type {}; template inline constexpr bool IsStreamable = HasStreamableTraits::value; constexpr std::size_t repr_max_container_size = 5; template std::string repr(T const &val) { if constexpr (std::is_same_v) { return val ? "true" : "false"; } else if constexpr (std::is_convertible_v) { return '"' + std::string{std::string_view{val}} + '"'; } else if constexpr (IsContainer) { std::stringstream out; out << "{"; const auto size = val.size(); if (size > 1) { out << repr(*val.begin()); std::for_each(std::next(val.begin()), std::next(val.begin(), static_cast( std::min(size, repr_max_container_size) - 1)), [&out](const auto &v) { out << " " << repr(v); }); if (size <= repr_max_container_size) { out << " "; } else { out << "..."; } } if (size > 0) { out << repr(*std::prev(val.end())); } out << "}"; return out.str(); } else if constexpr (IsStreamable) { std::stringstream out; out << val; return out.str(); } else { return ""; } } namespace { template constexpr bool standard_signed_integer = false; template <> constexpr bool standard_signed_integer = true; template <> constexpr bool standard_signed_integer = true; template <> constexpr bool standard_signed_integer = true; template <> constexpr bool standard_signed_integer = true; template <> constexpr bool standard_signed_integer = true; template constexpr bool standard_unsigned_integer = false; template <> constexpr bool standard_unsigned_integer = true; template <> constexpr bool standard_unsigned_integer = true; template <> constexpr bool standard_unsigned_integer = true; template <> constexpr bool standard_unsigned_integer = true; template <> constexpr bool standard_unsigned_integer = true; } // namespace constexpr int radix_2 = 2; constexpr int radix_8 = 8; constexpr int radix_10 = 10; constexpr int radix_16 = 16; template constexpr bool standard_integer = standard_signed_integer || standard_unsigned_integer; template constexpr decltype(auto) apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, std::index_sequence /*unused*/) { return std::invoke(std::forward(f), std::get(std::forward(t))..., std::forward(x)); } template constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) { return details::apply_plus_one_impl(std::forward(f), std::forward(t), std::forward(x), std::make_index_sequence>>{}); } constexpr auto pointer_range(std::string_view s) noexcept { return std::tuple(s.data(), s.data() + s.size()); } template constexpr bool starts_with(std::basic_string_view prefix, std::basic_string_view s) noexcept { return s.substr(0, prefix.size()) == prefix; } enum class chars_format { scientific = 0xf1, fixed = 0xf2, hex = 0xf4, binary = 0xf8, general = fixed | scientific }; struct ConsumeBinaryPrefixResult { bool is_binary; std::string_view rest; }; constexpr auto consume_binary_prefix(std::string_view s) -> ConsumeBinaryPrefixResult { if (starts_with(std::string_view{"0b"}, s) || starts_with(std::string_view{"0B"}, s)) { s.remove_prefix(2); return {true, s}; } return {false, s}; } struct ConsumeHexPrefixResult { bool is_hexadecimal; std::string_view rest; }; using namespace std::literals; constexpr auto consume_hex_prefix(std::string_view s) -> ConsumeHexPrefixResult { if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { s.remove_prefix(2); return {true, s}; } return {false, s}; } template inline auto do_from_chars(std::string_view s) -> T { T x{0}; auto [first, last] = pointer_range(s); auto [ptr, ec] = std::from_chars(first, last, x, Param); if (ec == std::errc()) { if (ptr == last) { return x; } throw std::invalid_argument{"pattern '" + std::string(s) + "' does not match to the end"}; } if (ec == std::errc::invalid_argument) { throw std::invalid_argument{"pattern '" + std::string(s) + "' not found"}; } if (ec == std::errc::result_out_of_range) { throw std::range_error{"'" + std::string(s) + "' not representable"}; } return x; // unreachable } template struct parse_number { auto operator()(std::string_view s) -> T { return do_from_chars(s); } }; template struct parse_number { auto operator()(std::string_view s) -> T { if (auto [ok, rest] = consume_binary_prefix(s); ok) { return do_from_chars(rest); } throw std::invalid_argument{"pattern not found"}; } }; template struct parse_number { auto operator()(std::string_view s) -> T { if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { if (auto [ok, rest] = consume_hex_prefix(s); ok) { try { return do_from_chars(rest); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + std::string(s) + "' as hexadecimal: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + std::string(s) + "' as hexadecimal: " + err.what()); } } } else { // Allow passing hex numbers without prefix // Shape 'x' already has to be specified try { return do_from_chars(s); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + std::string(s) + "' as hexadecimal: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + std::string(s) + "' as hexadecimal: " + err.what()); } } throw std::invalid_argument{"pattern '" + std::string(s) + "' not identified as hexadecimal"}; } }; template struct parse_number { auto operator()(std::string_view s) -> T { auto [ok, rest] = consume_hex_prefix(s); if (ok) { try { return do_from_chars(rest); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + std::string(s) + "' as hexadecimal: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + std::string(s) + "' as hexadecimal: " + err.what()); } } auto [ok_binary, rest_binary] = consume_binary_prefix(s); if (ok_binary) { try { return do_from_chars(rest_binary); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + std::string(s) + "' as binary: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + std::string(s) + "' as binary: " + err.what()); } } if (starts_with("0"sv, s)) { try { return do_from_chars(rest); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + std::string(s) + "' as octal: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + std::string(s) + "' as octal: " + err.what()); } } try { return do_from_chars(rest); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + std::string(s) + "' as decimal integer: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + std::string(s) + "' as decimal integer: " + err.what()); } } }; namespace { template inline const auto generic_strtod = nullptr; template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOF; template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOD; template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOLD; } // namespace template inline auto do_strtod(std::string const &s) -> T { if (isspace(static_cast(s[0])) || s[0] == '+') { throw std::invalid_argument{"pattern '" + s + "' not found"}; } auto [first, last] = pointer_range(s); char *ptr; errno = 0; auto x = generic_strtod(first, &ptr); if (errno == 0) { if (ptr == last) { return x; } throw std::invalid_argument{"pattern '" + s + "' does not match to the end"}; } if (errno == ERANGE) { throw std::range_error{"'" + s + "' not representable"}; } return x; // unreachable } template struct parse_number { auto operator()(std::string const &s) -> T { if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { throw std::invalid_argument{"chars_format::general does not parse hexfloat"}; } if (auto r = consume_binary_prefix(s); r.is_binary) { throw std::invalid_argument{"chars_format::general does not parse binfloat"}; } try { return do_strtod(s); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + s + "' as number: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + s + "' as number: " + err.what()); } } }; template struct parse_number { auto operator()(std::string const &s) -> T { if (auto r = consume_hex_prefix(s); !r.is_hexadecimal) { throw std::invalid_argument{"chars_format::hex parses hexfloat"}; } if (auto r = consume_binary_prefix(s); r.is_binary) { throw std::invalid_argument{"chars_format::hex does not parse binfloat"}; } try { return do_strtod(s); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + s + "' as hexadecimal: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + s + "' as hexadecimal: " + err.what()); } } }; template struct parse_number { auto operator()(std::string const &s) -> T { if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { throw std::invalid_argument{"chars_format::binary does not parse hexfloat"}; } if (auto r = consume_binary_prefix(s); !r.is_binary) { throw std::invalid_argument{"chars_format::binary parses binfloat"}; } return do_strtod(s); } }; template struct parse_number { auto operator()(std::string const &s) -> T { if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { throw std::invalid_argument{"chars_format::scientific does not parse hexfloat"}; } if (auto r = consume_binary_prefix(s); r.is_binary) { throw std::invalid_argument{"chars_format::scientific does not parse binfloat"}; } if (s.find_first_of("eE") == std::string::npos) { throw std::invalid_argument{"chars_format::scientific requires exponent part"}; } try { return do_strtod(s); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + s + "' as scientific notation: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + s + "' as scientific notation: " + err.what()); } } }; template struct parse_number { auto operator()(std::string const &s) -> T { if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { throw std::invalid_argument{"chars_format::fixed does not parse hexfloat"}; } if (auto r = consume_binary_prefix(s); r.is_binary) { throw std::invalid_argument{"chars_format::fixed does not parse binfloat"}; } if (s.find_first_of("eE") != std::string::npos) { throw std::invalid_argument{"chars_format::fixed does not parse exponent part"}; } try { return do_strtod(s); } catch (const std::invalid_argument &err) { throw std::invalid_argument("Failed to parse '" + s + "' as fixed notation: " + err.what()); } catch (const std::range_error &err) { throw std::range_error("Failed to parse '" + s + "' as fixed notation: " + err.what()); } } }; template std::string join(StrIt first, StrIt last, const std::string &separator) { if (first == last) { return ""; } std::stringstream value; value << *first; ++first; while (first != last) { value << separator << *first; ++first; } return value.str(); } template struct can_invoke_to_string { template static auto test(int) -> decltype(std::to_string(std::declval()), std::true_type{}); template static auto test(...) -> std::false_type; static constexpr bool value = decltype(test(0))::value; }; template struct IsChoiceTypeSupported { using CleanType = typename std::decay::type; static const bool value = std::is_integral::value || std::is_same::value || std::is_same::value || std::is_same::value; }; template std::size_t get_levenshtein_distance(const StringType &s1, const StringType &s2) { std::vector> dp(s1.size() + 1, std::vector(s2.size() + 1, 0)); for (std::size_t i = 0; i <= s1.size(); ++i) { for (std::size_t j = 0; j <= s2.size(); ++j) { if (i == 0) { dp[i][j] = j; } else if (j == 0) { dp[i][j] = i; } else if (s1[i - 1] == s2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}); } } } return dp[s1.size()][s2.size()]; } template std::string get_most_similar_string(const std::map &map, const std::string &input) { std::string most_similar{}; std::size_t min_distance = std::numeric_limits::max(); for (const auto &entry : map) { std::size_t distance = get_levenshtein_distance(entry.first, input); if (distance < min_distance) { min_distance = distance; most_similar = entry.first; } } return most_similar; } } // namespace details enum class nargs_pattern { optional, any, at_least_one }; enum class default_arguments : unsigned int { none = 0, help = 1, version = 2, all = help | version, }; inline default_arguments operator&(const default_arguments &a, const default_arguments &b) { return static_cast(static_cast::type>(a) & static_cast::type>(b)); } class ArgumentParser; class Argument { friend class ArgumentParser; friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) -> std::ostream &; template explicit Argument(std::string_view prefix_chars, std::array &&a, std::index_sequence /*unused*/) : m_accepts_optional_like_value(false), m_is_optional((is_optional(a[I], prefix_chars) || ...)), m_is_required(false), m_is_repeatable(false), m_is_used(false), m_is_hidden(false), m_prefix_chars(prefix_chars) { ((void)m_names.emplace_back(a[I]), ...); std::sort(m_names.begin(), m_names.end(), [](const auto &lhs, const auto &rhs) { return lhs.size() == rhs.size() ? lhs < rhs : lhs.size() < rhs.size(); }); } public: template explicit Argument(std::string_view prefix_chars, std::array &&a) : Argument(prefix_chars, std::move(a), std::make_index_sequence{}) {} Argument &help(std::string help_text) { m_help = std::move(help_text); return *this; } Argument &metavar(std::string metavar) { m_metavar = std::move(metavar); return *this; } template Argument &default_value(T &&value) { m_num_args_range = NArgsRange{0, m_num_args_range.get_max()}; m_default_value_repr = details::repr(value); if constexpr (std::is_convertible_v) { m_default_value_str = std::string{std::string_view{value}}; } else if constexpr (details::can_invoke_to_string::value) { m_default_value_str = std::to_string(value); } m_default_value = std::forward(value); return *this; } Argument &default_value(const char *value) { return default_value(std::string(value)); } Argument &required() { m_is_required = true; return *this; } Argument &implicit_value(std::any value) { m_implicit_value = std::move(value); m_num_args_range = NArgsRange{0, 0}; return *this; } // This is shorthand for: // program.add_argument("foo") // .default_value(false) // .implicit_value(true) Argument &flag() { default_value(false); implicit_value(true); return *this; } template auto action(F &&callable, Args &&...bound_args) -> std::enable_if_t, Argument &> { using action_type = std::conditional_t>, void_action, valued_action>; if constexpr (sizeof...(Args) == 0) { m_action.emplace(std::forward(callable)); } else { m_action.emplace( [f = std::forward(callable), tup = std::make_tuple(std::forward(bound_args)...)]( std::string const &opt) mutable { return details::apply_plus_one(f, tup, opt); }); } return *this; } auto &store_into(bool &var) { flag(); if (m_default_value.has_value()) { var = std::any_cast(m_default_value); } action([&var](const auto & /*unused*/) { var = true; }); return *this; } template ::value>::type * = nullptr> auto &store_into(T &var) { if (m_default_value.has_value()) { var = std::any_cast(m_default_value); } action([&var](const auto &s) { var = details::parse_number()(s); }); return *this; } auto &store_into(double &var) { if (m_default_value.has_value()) { var = std::any_cast(m_default_value); } action([&var](const auto &s) { var = details::parse_number()(s); }); return *this; } auto &store_into(std::string &var) { if (m_default_value.has_value()) { var = std::any_cast(m_default_value); } action([&var](const std::string &s) { var = s; }); return *this; } auto &store_into(std::vector &var) { if (m_default_value.has_value()) { var = std::any_cast>(m_default_value); } action([this, &var](const std::string &s) { if (!m_is_used) { var.clear(); } m_is_used = true; var.push_back(s); }); return *this; } auto &store_into(std::vector &var) { if (m_default_value.has_value()) { var = std::any_cast>(m_default_value); } action([this, &var](const std::string &s) { if (!m_is_used) { var.clear(); } m_is_used = true; var.push_back(details::parse_number()(s)); }); return *this; } auto &store_into(std::set &var) { if (m_default_value.has_value()) { var = std::any_cast>(m_default_value); } action([this, &var](const std::string &s) { if (!m_is_used) { var.clear(); } m_is_used = true; var.insert(s); }); return *this; } auto &store_into(std::set &var) { if (m_default_value.has_value()) { var = std::any_cast>(m_default_value); } action([this, &var](const std::string &s) { if (!m_is_used) { var.clear(); } m_is_used = true; var.insert(details::parse_number()(s)); }); return *this; } auto &append() { m_is_repeatable = true; return *this; } // Cause the argument to be invisible in usage and help auto &hidden() { m_is_hidden = true; return *this; } template auto scan() -> std::enable_if_t, Argument &> { static_assert(!(std::is_const_v || std::is_volatile_v), "T should not be cv-qualified"); auto is_one_of = [](char c, auto... x) constexpr { return ((c == x) || ...); }; if constexpr (is_one_of(Shape, 'd') && details::standard_integer) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'i') && details::standard_integer) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'u') && details::standard_unsigned_integer) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'b') && details::standard_unsigned_integer) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'o') && details::standard_unsigned_integer) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'x', 'X') && details::standard_unsigned_integer) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_v) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_v) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_v) { action(details::parse_number()); } else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_v) { action(details::parse_number()); } else { static_assert(alignof(T) == 0, "No scan specification for T"); } return *this; } Argument &nargs(std::size_t num_args) { m_num_args_range = NArgsRange{num_args, num_args}; return *this; } Argument &nargs(std::size_t num_args_min, std::size_t num_args_max) { m_num_args_range = NArgsRange{num_args_min, num_args_max}; return *this; } Argument &nargs(nargs_pattern pattern) { switch (pattern) { case nargs_pattern::optional: m_num_args_range = NArgsRange{0, 1}; break; case nargs_pattern::any: m_num_args_range = NArgsRange{0, (std::numeric_limits::max)()}; break; case nargs_pattern::at_least_one: m_num_args_range = NArgsRange{1, (std::numeric_limits::max)()}; break; } return *this; } Argument &remaining() { m_accepts_optional_like_value = true; return nargs(nargs_pattern::any); } template void add_choice(T &&choice) { static_assert(details::IsChoiceTypeSupported::value, "Only string or integer type supported for choice"); static_assert(std::is_convertible_v || details::can_invoke_to_string::value, "Choice is not convertible to string_type"); if (!m_choices.has_value()) { m_choices = std::vector{}; } if constexpr (std::is_convertible_v) { m_choices.value().push_back(std::string{std::string_view{std::forward(choice)}}); } else if constexpr (details::can_invoke_to_string::value) { m_choices.value().push_back(std::to_string(std::forward(choice))); } } Argument &choices() { if (!m_choices.has_value()) { throw std::runtime_error("Zero choices provided"); } return *this; } template Argument &choices(T &&first, U &&...rest) { add_choice(std::forward(first)); choices(std::forward(rest)...); return *this; } void find_default_value_in_choices_or_throw() const { const auto &choices = m_choices.value(); if (m_default_value.has_value()) { if (std::find(choices.begin(), choices.end(), m_default_value_str) == choices.end()) { // provided arg not in list of allowed choices // report error std::string choices_as_csv = std::accumulate(choices.begin(), choices.end(), std::string(), [](const std::string &a, const std::string &b) { return a + (a.empty() ? "" : ", ") + b; }); throw std::runtime_error(std::string{"Invalid default value "} + m_default_value_repr + " - allowed options: {" + choices_as_csv + "}"); } } } template void find_value_in_choices_or_throw(Iterator it) const { const auto &choices = m_choices.value(); if (std::find(choices.begin(), choices.end(), *it) == choices.end()) { // provided arg not in list of allowed choices // report error std::string choices_as_csv = std::accumulate(choices.begin(), choices.end(), std::string(), [](const std::string &a, const std::string &b) { return a + (a.empty() ? "" : ", ") + b; }); throw std::runtime_error(std::string{"Invalid argument "} + details::repr(*it) + " - allowed options: {" + choices_as_csv + "}"); } } /* The dry_run parameter can be set to true to avoid running the actions, * and setting m_is_used. This may be used by a pre-processing step to do * a first iteration over arguments. */ template Iterator consume(Iterator start, Iterator end, std::string_view used_name = {}, bool dry_run = false) { if (!m_is_repeatable && m_is_used) { throw std::runtime_error(std::string("Duplicate argument ").append(used_name)); } m_used_name = used_name; if (m_choices.has_value()) { // Check each value in (start, end) and make sure // it is in the list of allowed choices/options std::size_t i = 0; auto max_number_of_args = m_num_args_range.get_max(); for (auto it = start; it != end; ++it) { if (i == max_number_of_args) { break; } find_value_in_choices_or_throw(it); i += 1; } } const auto num_args_max = m_num_args_range.get_max(); const auto num_args_min = m_num_args_range.get_min(); std::size_t dist = 0; if (num_args_max == 0) { if (!dry_run) { m_values.emplace_back(m_implicit_value); std::visit([](const auto &f) { f({}); }, m_action); m_is_used = true; } return start; } if ((dist = static_cast(std::distance(start, end))) >= num_args_min) { if (num_args_max < dist) { end = std::next(start, static_cast(num_args_max)); } if (!m_accepts_optional_like_value) { end = std::find_if(start, end, std::bind(is_optional, std::placeholders::_1, m_prefix_chars)); dist = static_cast(std::distance(start, end)); if (dist < num_args_min) { throw std::runtime_error("Too few arguments for '" + std::string(m_used_name) + "'."); } } struct ActionApply { void operator()(valued_action &f) { std::transform(first, last, std::back_inserter(self.m_values), f); } void operator()(void_action &f) { std::for_each(first, last, f); if (!self.m_default_value.has_value()) { if (!self.m_accepts_optional_like_value) { self.m_values.resize(static_cast(std::distance(first, last))); } } } Iterator first, last; Argument &self; }; if (!dry_run) { std::visit(ActionApply{start, end, *this}, m_action); m_is_used = true; } return end; } if (m_default_value.has_value()) { if (!dry_run) { m_is_used = true; } return start; } throw std::runtime_error("Too few arguments for '" + std::string(m_used_name) + "'."); } /* * @throws std::runtime_error if argument values are not valid */ void validate() const { if (m_is_optional) { if (!m_is_used && !m_default_value.has_value() && m_is_required) { throw_required_arg_not_used_error(); } if (m_is_used && m_is_required && m_values.empty()) { throw_required_arg_no_value_provided_error(); } } else { if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) { throw_nargs_range_validation_error(); } } if (m_choices.has_value()) { // Make sure the default value (if provided) // is in the list of choices find_default_value_in_choices_or_throw(); } } std::string get_names_csv(char separator = ',') const { return std::accumulate(m_names.begin(), m_names.end(), std::string{""}, [&](const std::string &result, const std::string &name) { return result.empty() ? name : result + separator + name; }); } std::string get_usage_full() const { std::stringstream usage; usage << get_names_csv('/'); const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; if (m_num_args_range.get_max() > 0) { usage << " " << metavar; if (m_num_args_range.get_max() > 1) { usage << "..."; } } return usage.str(); } std::string get_inline_usage() const { std::stringstream usage; // Find the longest variant to show in the usage string std::string longest_name = m_names.front(); for (const auto &s : m_names) { if (s.size() > longest_name.size()) { longest_name = s; } } if (!m_is_required) { usage << "["; } usage << longest_name; const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; if (m_num_args_range.get_max() > 0) { usage << " " << metavar; if (m_num_args_range.get_max() > 1 && m_metavar.find("> <") == std::string::npos) { usage << "..."; } } if (!m_is_required) { usage << "]"; } if (m_is_repeatable) { usage << "..."; } return usage.str(); } std::size_t get_arguments_length() const { std::size_t names_size = std::accumulate(std::begin(m_names), std::end(m_names), std::size_t(0), [](const auto &sum, const auto &s) { return sum + s.size(); }); if (is_positional(m_names.front(), m_prefix_chars)) { // A set metavar means this replaces the names if (!m_metavar.empty()) { // Indent and metavar return 2 + m_metavar.size(); } // Indent and space-separated return 2 + names_size + (m_names.size() - 1); } // Is an option - include both names _and_ metavar // size = text + (", " between names) std::size_t size = names_size + 2 * (m_names.size() - 1); if (!m_metavar.empty() && m_num_args_range == NArgsRange{1, 1}) { size += m_metavar.size() + 1; } return size + 2; // indent } friend std::ostream &operator<<(std::ostream &stream, const Argument &argument) { std::stringstream name_stream; name_stream << " "; // indent if (argument.is_positional(argument.m_names.front(), argument.m_prefix_chars)) { if (!argument.m_metavar.empty()) { name_stream << argument.m_metavar; } else { name_stream << details::join(argument.m_names.begin(), argument.m_names.end(), " "); } } else { name_stream << details::join(argument.m_names.begin(), argument.m_names.end(), ", "); // If we have a metavar, and one narg - print the metavar if (!argument.m_metavar.empty() && argument.m_num_args_range == NArgsRange{1, 1}) { name_stream << " " << argument.m_metavar; } else if (!argument.m_metavar.empty() && argument.m_num_args_range.get_min() == argument.m_num_args_range.get_max() && argument.m_metavar.find("> <") != std::string::npos) { name_stream << " " << argument.m_metavar; } } // align multiline help message auto stream_width = stream.width(); auto name_padding = std::string(name_stream.str().size(), ' '); auto pos = std::string::size_type{}; auto prev = std::string::size_type{}; auto first_line = true; auto hspace = " "; // minimal space between name and help message stream << name_stream.str(); std::string_view help_view(argument.m_help); while ((pos = argument.m_help.find('\n', prev)) != std::string::npos) { auto line = help_view.substr(prev, pos - prev + 1); if (first_line) { stream << hspace << line; first_line = false; } else { stream.width(stream_width); stream << name_padding << hspace << line; } prev += pos - prev + 1; } if (first_line) { stream << hspace << argument.m_help; } else { auto leftover = help_view.substr(prev, argument.m_help.size() - prev); if (!leftover.empty()) { stream.width(stream_width); stream << name_padding << hspace << leftover; } } // print nargs spec if (!argument.m_help.empty()) { stream << " "; } stream << argument.m_num_args_range; bool add_space = false; if (argument.m_default_value.has_value() && argument.m_num_args_range != NArgsRange{0, 0}) { stream << "[default: " << argument.m_default_value_repr << "]"; add_space = true; } else if (argument.m_is_required) { stream << "[required]"; add_space = true; } if (argument.m_is_repeatable) { if (add_space) { stream << " "; } stream << "[may be repeated]"; } stream << "\n"; return stream; } template bool operator!=(const T &rhs) const { return !(*this == rhs); } /* * Compare to an argument value of known type * @throws std::logic_error in case of incompatible types */ template bool operator==(const T &rhs) const { if constexpr (!details::IsContainer) { return get() == rhs; } else { using ValueType = typename T::value_type; auto lhs = get(); return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs), [](const auto &a, const auto &b) { return std::any_cast(a) == b; }); } } /* * positional: * _empty_ * '-' * '-' decimal-literal * !'-' anything */ static bool is_positional(std::string_view name, std::string_view prefix_chars) { auto first = lookahead(name); if (first == eof) { return true; } if (prefix_chars.find(static_cast(first)) != std::string_view::npos) { name.remove_prefix(1); if (name.empty()) { return true; } return is_decimal_literal(name); } return true; } private: class NArgsRange { std::size_t m_min; std::size_t m_max; public: NArgsRange(std::size_t minimum, std::size_t maximum) : m_min(minimum), m_max(maximum) { if (minimum > maximum) { throw std::logic_error("Range of number of arguments is invalid"); } } bool contains(std::size_t value) const { return value >= m_min && value <= m_max; } bool is_exact() const { return m_min == m_max; } bool is_right_bounded() const { return m_max < (std::numeric_limits::max)(); } std::size_t get_min() const { return m_min; } std::size_t get_max() const { return m_max; } // Print help message friend auto operator<<(std::ostream &stream, const NArgsRange &range) -> std::ostream & { if (range.m_min == range.m_max) { if (range.m_min != 0 && range.m_min != 1) { stream << "[nargs: " << range.m_min << "] "; } } else { if (range.m_max == (std::numeric_limits::max)()) { stream << "[nargs: " << range.m_min << " or more] "; } else { stream << "[nargs=" << range.m_min << ".." << range.m_max << "] "; } } return stream; } bool operator==(const NArgsRange &rhs) const { return rhs.m_min == m_min && rhs.m_max == m_max; } bool operator!=(const NArgsRange &rhs) const { return !(*this == rhs); } }; void throw_nargs_range_validation_error() const { std::stringstream stream; if (!m_used_name.empty()) { stream << m_used_name << ": "; } else { stream << m_names.front() << ": "; } if (m_num_args_range.is_exact()) { stream << m_num_args_range.get_min(); } else if (m_num_args_range.is_right_bounded()) { stream << m_num_args_range.get_min() << " to " << m_num_args_range.get_max(); } else { stream << m_num_args_range.get_min() << " or more"; } stream << " argument(s) expected. " << m_values.size() << " provided."; throw std::runtime_error(stream.str()); } void throw_required_arg_not_used_error() const { std::stringstream stream; stream << m_names.front() << ": required."; throw std::runtime_error(stream.str()); } void throw_required_arg_no_value_provided_error() const { std::stringstream stream; stream << m_used_name << ": no value provided."; throw std::runtime_error(stream.str()); } static constexpr int eof = std::char_traits::eof(); static auto lookahead(std::string_view s) -> int { if (s.empty()) { return eof; } return static_cast(static_cast(s[0])); } /* * decimal-literal: * '0' * nonzero-digit digit-sequence_opt * integer-part fractional-part * fractional-part * integer-part '.' exponent-part_opt * integer-part exponent-part * * integer-part: * digit-sequence * * fractional-part: * '.' post-decimal-point * * post-decimal-point: * digit-sequence exponent-part_opt * * exponent-part: * 'e' post-e * 'E' post-e * * post-e: * sign_opt digit-sequence * * sign: one of * '+' '-' */ static bool is_decimal_literal(std::string_view s) { auto is_digit = [](auto c) constexpr { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return true; default: return false; } }; // precondition: we have consumed or will consume at least one digit auto consume_digits = [=](std::string_view sd) { // NOLINTNEXTLINE(readability-qualified-auto) auto it = std::find_if_not(std::begin(sd), std::end(sd), is_digit); return sd.substr(static_cast(it - std::begin(sd))); }; switch (lookahead(s)) { case '0': { s.remove_prefix(1); if (s.empty()) { return true; } goto integer_part; } case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { s = consume_digits(s); if (s.empty()) { return true; } goto integer_part_consumed; } case '.': { s.remove_prefix(1); goto post_decimal_point; } default: return false; } integer_part: s = consume_digits(s); integer_part_consumed: switch (lookahead(s)) { case '.': { s.remove_prefix(1); if (is_digit(lookahead(s))) { goto post_decimal_point; } else { goto exponent_part_opt; } } case 'e': case 'E': { s.remove_prefix(1); goto post_e; } default: return false; } post_decimal_point: if (is_digit(lookahead(s))) { s = consume_digits(s); goto exponent_part_opt; } return false; exponent_part_opt: switch (lookahead(s)) { case eof: return true; case 'e': case 'E': { s.remove_prefix(1); goto post_e; } default: return false; } post_e: switch (lookahead(s)) { case '-': case '+': s.remove_prefix(1); } if (is_digit(lookahead(s))) { s = consume_digits(s); return s.empty(); } return false; } static bool is_optional(std::string_view name, std::string_view prefix_chars) { return !is_positional(name, prefix_chars); } /* * Get argument value given a type * @throws std::logic_error in case of incompatible types */ template T get() const { if (!m_values.empty()) { if constexpr (details::IsContainer) { return any_cast_container(m_values); } else { return std::any_cast(m_values.front()); } } if (m_default_value.has_value()) { return std::any_cast(m_default_value); } if constexpr (details::IsContainer) { if (!m_accepts_optional_like_value) { return any_cast_container(m_values); } } throw std::logic_error("No value provided for '" + m_names.back() + "'."); } /* * Get argument value given a type. * @pre The object has no default value. * @returns The stored value if any, std::nullopt otherwise. */ template auto present() const -> std::optional { if (m_default_value.has_value()) { throw std::logic_error("Argument with default value always presents"); } if (m_values.empty()) { return std::nullopt; } if constexpr (details::IsContainer) { return any_cast_container(m_values); } return std::any_cast(m_values.front()); } template static auto any_cast_container(const std::vector &operand) -> T { using ValueType = typename T::value_type; T result; std::transform(std::begin(operand), std::end(operand), std::back_inserter(result), [](const auto &value) { return std::any_cast(value); }); return result; } void set_usage_newline_counter(int i) { m_usage_newline_counter = i; } void set_group_idx(std::size_t i) { m_group_idx = i; } std::vector m_names; std::string_view m_used_name; std::string m_help; std::string m_metavar; std::any m_default_value; std::string m_default_value_repr; std::optional m_default_value_str; // used for checking default_value against choices std::any m_implicit_value; std::optional> m_choices{std::nullopt}; using valued_action = std::function; using void_action = std::function; std::variant m_action{std::in_place_type, [](const std::string &value) { return value; }}; std::vector m_values; NArgsRange m_num_args_range{1, 1}; // Bit field of bool values. Set default value in ctor. bool m_accepts_optional_like_value : 1; bool m_is_optional : 1; bool m_is_required : 1; bool m_is_repeatable : 1; bool m_is_used : 1; bool m_is_hidden : 1; // if set, does not appear in usage or help std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars int m_usage_newline_counter = 0; std::size_t m_group_idx = 0; }; class ArgumentParser { public: explicit ArgumentParser(std::string program_name = {}, std::string version = "1.0", default_arguments add_args = default_arguments::all, bool exit_on_default_arguments = true, std::ostream &os = std::cout) : m_program_name(std::move(program_name)), m_version(std::move(version)), m_exit_on_default_arguments(exit_on_default_arguments), m_parser_path(m_program_name) { if ((add_args & default_arguments::help) == default_arguments::help) { add_argument("-h", "--help") .action([&](const auto & /*unused*/) { os << help().str(); if (m_exit_on_default_arguments) { std::exit(0); } }) .default_value(false) .help("shows help message and exits") .implicit_value(true) .nargs(0); } if ((add_args & default_arguments::version) == default_arguments::version) { add_argument("-v", "--version") .action([&](const auto & /*unused*/) { os << m_version << std::endl; if (m_exit_on_default_arguments) { std::exit(0); } }) .default_value(false) .help("prints version information and exits") .implicit_value(true) .nargs(0); } } ~ArgumentParser() = default; // ArgumentParser is meant to be used in a single function. // Setup everything and parse arguments in one place. // // ArgumentParser internally uses std::string_views, // references, iterators, etc. // Many of these elements become invalidated after a copy or move. ArgumentParser(const ArgumentParser &other) = delete; ArgumentParser &operator=(const ArgumentParser &other) = delete; ArgumentParser(ArgumentParser &&) noexcept = delete; ArgumentParser &operator=(ArgumentParser &&) = delete; explicit operator bool() const { auto arg_used = std::any_of(m_argument_map.cbegin(), m_argument_map.cend(), [](auto &it) { return it.second->m_is_used; }); auto subparser_used = std::any_of(m_subparser_used.cbegin(), m_subparser_used.cend(), [](auto &it) { return it.second; }); return m_is_parsed && (arg_used || subparser_used); } // Parameter packing // Call add_argument with variadic number of string arguments template Argument &add_argument(Targs... f_args) { using array_of_sv = std::array; auto argument = m_optional_arguments.emplace(std::cend(m_optional_arguments), m_prefix_chars, array_of_sv{f_args...}); if (!argument->m_is_optional) { m_positional_arguments.splice(std::cend(m_positional_arguments), m_optional_arguments, argument); } argument->set_usage_newline_counter(m_usage_newline_counter); argument->set_group_idx(m_group_names.size()); index_argument(argument); return *argument; } class MutuallyExclusiveGroup { friend class ArgumentParser; public: MutuallyExclusiveGroup() = delete; explicit MutuallyExclusiveGroup(ArgumentParser &parent, bool required = false) : m_parent(parent), m_required(required), m_elements({}) {} MutuallyExclusiveGroup(const MutuallyExclusiveGroup &other) = delete; MutuallyExclusiveGroup &operator=(const MutuallyExclusiveGroup &other) = delete; MutuallyExclusiveGroup(MutuallyExclusiveGroup &&other) noexcept : m_parent(other.m_parent), m_required(other.m_required), m_elements(std::move(other.m_elements)) { other.m_elements.clear(); } template Argument &add_argument(Targs... f_args) { auto &argument = m_parent.add_argument(std::forward(f_args)...); m_elements.push_back(&argument); argument.set_usage_newline_counter(m_parent.m_usage_newline_counter); argument.set_group_idx(m_parent.m_group_names.size()); return argument; } private: ArgumentParser &m_parent; bool m_required{false}; std::vector m_elements{}; }; MutuallyExclusiveGroup &add_mutually_exclusive_group(bool required = false) { m_mutually_exclusive_groups.emplace_back(*this, required); return m_mutually_exclusive_groups.back(); } // Parameter packed add_parents method // Accepts a variadic number of ArgumentParser objects template ArgumentParser &add_parents(const Targs &...f_args) { for (const ArgumentParser &parent_parser : {std::ref(f_args)...}) { for (const auto &argument : parent_parser.m_positional_arguments) { auto it = m_positional_arguments.insert(std::cend(m_positional_arguments), argument); index_argument(it); } for (const auto &argument : parent_parser.m_optional_arguments) { auto it = m_optional_arguments.insert(std::cend(m_optional_arguments), argument); index_argument(it); } } return *this; } // Ask for the next optional arguments to be displayed on a separate // line in usage() output. Only effective if set_usage_max_line_width() is // also used. ArgumentParser &add_usage_newline() { ++m_usage_newline_counter; return *this; } // Ask for the next optional arguments to be displayed in a separate section // in usage() and help (<< *this) output. // For usage(), this is only effective if set_usage_max_line_width() is // also used. ArgumentParser &add_group(std::string group_name) { m_group_names.emplace_back(std::move(group_name)); return *this; } ArgumentParser &add_description(std::string description) { m_description = std::move(description); return *this; } ArgumentParser &add_epilog(std::string epilog) { m_epilog = std::move(epilog); return *this; } // Add a un-documented/hidden alias for an argument. // Ideally we'd want this to be a method of Argument, but Argument // does not own its owing ArgumentParser. ArgumentParser &add_hidden_alias_for(Argument &arg, std::string_view alias) { for (auto it = m_optional_arguments.begin(); it != m_optional_arguments.end(); ++it) { if (&(*it) == &arg) { m_argument_map.insert_or_assign(std::string(alias), it); return *this; } } throw std::logic_error("Argument is not an optional argument of this parser"); } /* Getter for arguments and subparsers. * @throws std::logic_error in case of an invalid argument or subparser name */ template T &at(std::string_view name) { if constexpr (std::is_same_v) { return (*this)[name]; } else { std::string str_name(name); auto subparser_it = m_subparser_map.find(str_name); if (subparser_it != m_subparser_map.end()) { return subparser_it->second->get(); } throw std::logic_error("No such subparser: " + str_name); } } ArgumentParser &set_prefix_chars(std::string prefix_chars) { m_prefix_chars = std::move(prefix_chars); return *this; } ArgumentParser &set_assign_chars(std::string assign_chars) { m_assign_chars = std::move(assign_chars); return *this; } /* Call parse_args_internal - which does all the work * Then, validate the parsed arguments * This variant is used mainly for testing * @throws std::runtime_error in case of any invalid argument */ void parse_args(const std::vector &arguments) { parse_args_internal(arguments); // Check if all arguments are parsed for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { argument->validate(); } // Check each mutually exclusive group and make sure // there are no constraint violations for (const auto &group : m_mutually_exclusive_groups) { auto mutex_argument_used{false}; Argument *mutex_argument_it{nullptr}; for (Argument *arg : group.m_elements) { if (!mutex_argument_used && arg->m_is_used) { mutex_argument_used = true; mutex_argument_it = arg; } else if (mutex_argument_used && arg->m_is_used) { // Violation throw std::runtime_error("Argument '" + arg->get_usage_full() + "' not allowed with '" + mutex_argument_it->get_usage_full() + "'"); } } if (!mutex_argument_used && group.m_required) { // at least one argument from the group is // required std::string argument_names{}; std::size_t i = 0; std::size_t size = group.m_elements.size(); for (Argument *arg : group.m_elements) { if (i + 1 == size) { // last argument_names += std::string("'") + arg->get_usage_full() + std::string("' "); } else { argument_names += std::string("'") + arg->get_usage_full() + std::string("' or "); } i += 1; } throw std::runtime_error("One of the arguments " + argument_names + "is required"); } } } /* Call parse_known_args_internal - which does all the work * Then, validate the parsed arguments * This variant is used mainly for testing * @throws std::runtime_error in case of any invalid argument */ std::vector parse_known_args(const std::vector &arguments) { auto unknown_arguments = parse_known_args_internal(arguments); // Check if all arguments are parsed for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { argument->validate(); } return unknown_arguments; } /* Main entry point for parsing command-line arguments using this * ArgumentParser * @throws std::runtime_error in case of any invalid argument */ // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) void parse_args(int argc, const char *const argv[]) { parse_args({argv, argv + argc}); } /* Main entry point for parsing command-line arguments using this * ArgumentParser * @throws std::runtime_error in case of any invalid argument */ // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) auto parse_known_args(int argc, const char *const argv[]) { return parse_known_args({argv, argv + argc}); } /* Getter for options with default values. * @throws std::logic_error if parse_args() has not been previously called * @throws std::logic_error if there is no such option * @throws std::logic_error if the option has no value * @throws std::bad_any_cast if the option is not of type T */ template T get(std::string_view arg_name) const { if (!m_is_parsed) { throw std::logic_error("Nothing parsed, no arguments are available."); } return (*this)[arg_name].get(); } /* Getter for options without default values. * @pre The option has no default value. * @throws std::logic_error if there is no such option * @throws std::bad_any_cast if the option is not of type T */ template auto present(std::string_view arg_name) const -> std::optional { return (*this)[arg_name].present(); } /* Getter that returns true for user-supplied options. Returns false if not * user-supplied, even with a default value. */ auto is_used(std::string_view arg_name) const { return (*this)[arg_name].m_is_used; } /* Getter that returns true if a subcommand is used. */ auto is_subcommand_used(std::string_view subcommand_name) const { return m_subparser_used.at(std::string(subcommand_name)); } /* Getter that returns true if a subcommand is used. */ auto is_subcommand_used(const ArgumentParser &subparser) const { return is_subcommand_used(subparser.m_program_name); } /* Indexing operator. Return a reference to an Argument object * Used in conjunction with Argument.operator== e.g., parser["foo"] == true * @throws std::logic_error in case of an invalid argument name */ Argument &operator[](std::string_view arg_name) const { std::string name(arg_name); auto it = m_argument_map.find(name); if (it != m_argument_map.end()) { return *(it->second); } if (!is_valid_prefix_char(arg_name.front())) { const auto legal_prefix_char = get_any_valid_prefix_char(); const auto prefix = std::string(1, legal_prefix_char); // "-" + arg_name name = prefix + name; it = m_argument_map.find(name); if (it != m_argument_map.end()) { return *(it->second); } // "--" + arg_name name = prefix + name; it = m_argument_map.find(name); if (it != m_argument_map.end()) { return *(it->second); } } throw std::logic_error("No such argument: " + std::string(arg_name)); } // Print help message friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) -> std::ostream & { stream.setf(std::ios_base::left); auto longest_arg_length = parser.get_length_of_longest_argument(); stream << parser.usage() << "\n\n"; if (!parser.m_description.empty()) { stream << parser.m_description << "\n\n"; } const bool has_visible_positional_args = std::find_if(parser.m_positional_arguments.begin(), parser.m_positional_arguments.end(), [](const auto &argument) { return !argument.m_is_hidden; }) != parser.m_positional_arguments.end(); if (has_visible_positional_args) { stream << "Positional arguments:\n"; } for (const auto &argument : parser.m_positional_arguments) { if (!argument.m_is_hidden) { stream.width(static_cast(longest_arg_length)); stream << argument; } } if (!parser.m_optional_arguments.empty()) { stream << (!has_visible_positional_args ? "" : "\n") << "Optional arguments:\n"; } for (const auto &argument : parser.m_optional_arguments) { if (argument.m_group_idx == 0 && !argument.m_is_hidden) { stream.width(static_cast(longest_arg_length)); stream << argument; } } for (size_t i_group = 0; i_group < parser.m_group_names.size(); ++i_group) { stream << "\n" << parser.m_group_names[i_group] << " (detailed usage):\n"; for (const auto &argument : parser.m_optional_arguments) { if (argument.m_group_idx == i_group + 1 && !argument.m_is_hidden) { stream.width(static_cast(longest_arg_length)); stream << argument; } } } bool has_visible_subcommands = std::any_of(parser.m_subparser_map.begin(), parser.m_subparser_map.end(), [](auto &p) { return !p.second->get().m_suppress; }); if (has_visible_subcommands) { stream << (parser.m_positional_arguments.empty() ? (parser.m_optional_arguments.empty() ? "" : "\n") : "\n") << "Subcommands:\n"; for (const auto &[command, subparser] : parser.m_subparser_map) { if (subparser->get().m_suppress) { continue; } stream << std::setw(2) << " "; stream << std::setw(static_cast(longest_arg_length - 2)) << command; stream << " " << subparser->get().m_description << "\n"; } } if (!parser.m_epilog.empty()) { stream << '\n'; stream << parser.m_epilog << "\n\n"; } return stream; } // Format help message auto help() const -> std::stringstream { std::stringstream out; out << *this; return out; } // Sets the maximum width for a line of the Usage message ArgumentParser &set_usage_max_line_width(size_t w) { this->m_usage_max_line_width = w; return *this; } // Asks to display arguments of mutually exclusive group on separate lines in // the Usage message ArgumentParser &set_usage_break_on_mutex() { this->m_usage_break_on_mutex = true; return *this; } // Format usage part of help only auto usage() const -> std::string { std::stringstream stream; std::string curline("Usage: "); curline += this->m_program_name; const bool multiline_usage = this->m_usage_max_line_width < std::numeric_limits::max(); const size_t indent_size = curline.size(); const auto deal_with_options_of_group = [&](std::size_t group_idx) { bool found_options = false; // Add any options inline here const MutuallyExclusiveGroup *cur_mutex = nullptr; int usage_newline_counter = -1; for (const auto &argument : this->m_optional_arguments) { if (argument.m_is_hidden) { continue; } if (multiline_usage) { if (argument.m_group_idx != group_idx) { continue; } if (usage_newline_counter != argument.m_usage_newline_counter) { if (usage_newline_counter >= 0) { if (curline.size() > indent_size) { stream << curline << std::endl; curline = std::string(indent_size, ' '); } } usage_newline_counter = argument.m_usage_newline_counter; } } found_options = true; const std::string arg_inline_usage = argument.get_inline_usage(); const MutuallyExclusiveGroup *arg_mutex = get_belonging_mutex(&argument); if ((cur_mutex != nullptr) && (arg_mutex == nullptr)) { curline += ']'; if (this->m_usage_break_on_mutex) { stream << curline << std::endl; curline = std::string(indent_size, ' '); } } else if ((cur_mutex == nullptr) && (arg_mutex != nullptr)) { if ((this->m_usage_break_on_mutex && curline.size() > indent_size) || curline.size() + 3 + arg_inline_usage.size() > this->m_usage_max_line_width) { stream << curline << std::endl; curline = std::string(indent_size, ' '); } curline += " ["; } else if ((cur_mutex != nullptr) && (arg_mutex != nullptr)) { if (cur_mutex != arg_mutex) { curline += ']'; if (this->m_usage_break_on_mutex || curline.size() + 3 + arg_inline_usage.size() > this->m_usage_max_line_width) { stream << curline << std::endl; curline = std::string(indent_size, ' '); } curline += " ["; } else { curline += '|'; } } cur_mutex = arg_mutex; if (curline.size() + 1 + arg_inline_usage.size() > this->m_usage_max_line_width) { stream << curline << std::endl; curline = std::string(indent_size, ' '); curline += " "; } else if (cur_mutex == nullptr) { curline += " "; } curline += arg_inline_usage; } if (cur_mutex != nullptr) { curline += ']'; } return found_options; }; const bool found_options = deal_with_options_of_group(0); if (found_options && multiline_usage && !this->m_positional_arguments.empty()) { stream << curline << std::endl; curline = std::string(indent_size, ' '); } // Put positional arguments after the optionals for (const auto &argument : this->m_positional_arguments) { if (argument.m_is_hidden) { continue; } const std::string pos_arg = !argument.m_metavar.empty() ? argument.m_metavar : argument.m_names.front(); if (curline.size() + 1 + pos_arg.size() > this->m_usage_max_line_width) { stream << curline << std::endl; curline = std::string(indent_size, ' '); } curline += " "; if (argument.m_num_args_range.get_min() == 0 && !argument.m_num_args_range.is_right_bounded()) { curline += "["; curline += pos_arg; curline += "]..."; } else if (argument.m_num_args_range.get_min() == 1 && !argument.m_num_args_range.is_right_bounded()) { curline += pos_arg; curline += "..."; } else { curline += pos_arg; } } if (multiline_usage) { // Display options of other groups for (std::size_t i = 0; i < m_group_names.size(); ++i) { stream << curline << std::endl << std::endl; stream << m_group_names[i] << ":" << std::endl; curline = std::string(indent_size, ' '); deal_with_options_of_group(i + 1); } } stream << curline; // Put subcommands after positional arguments if (!m_subparser_map.empty()) { stream << " {"; std::size_t i{0}; for (const auto &[command, subparser] : m_subparser_map) { if (subparser->get().m_suppress) { continue; } if (i == 0) { stream << command; } else { stream << "," << command; } ++i; } stream << "}"; } return stream.str(); } // Printing the one and only help message // I've stuck with a simple message format, nothing fancy. [[deprecated("Use cout << program; instead. See also help().")]] std::string print_help() const { auto out = help(); std::cout << out.rdbuf(); return out.str(); } void add_subparser(ArgumentParser &parser) { parser.m_parser_path = m_program_name + " " + parser.m_program_name; auto it = m_subparsers.emplace(std::cend(m_subparsers), parser); m_subparser_map.insert_or_assign(parser.m_program_name, it); m_subparser_used.insert_or_assign(parser.m_program_name, false); } void set_suppress(bool suppress) { m_suppress = suppress; } template void set_if_used(const std::string key, T &val) { if (is_used(key)) { val = get(key); } } protected: const MutuallyExclusiveGroup *get_belonging_mutex(const Argument *arg) const { for (const auto &mutex : m_mutually_exclusive_groups) { if (std::find(mutex.m_elements.begin(), mutex.m_elements.end(), arg) != mutex.m_elements.end()) { return &mutex; } } return nullptr; } bool is_valid_prefix_char(char c) const { return m_prefix_chars.find(c) != std::string::npos; } char get_any_valid_prefix_char() const { return m_prefix_chars[0]; } /* * Pre-process this argument list. Anything starting with "--", that * contains an =, where the prefix before the = has an entry in the * options table, should be split. */ std::vector preprocess_arguments(const std::vector &raw_arguments) const { std::vector arguments{}; for (const auto &arg : raw_arguments) { const auto argument_starts_with_prefix_chars = [this](const std::string &a) -> bool { if (!a.empty()) { const auto legal_prefix = [this](char c) -> bool { return m_prefix_chars.find(c) != std::string::npos; }; // Windows-style // if '/' is a legal prefix char // then allow single '/' followed by argument name, followed by an // assign char, e.g., ':' e.g., 'test.exe /A:Foo' const auto windows_style = legal_prefix('/'); if (windows_style) { if (legal_prefix(a[0])) { return true; } } else { // Slash '/' is not a legal prefix char // For all other characters, only support long arguments // i.e., the argument must start with 2 prefix chars, e.g, // '--foo' e,g, './test --foo=Bar -DARG=yes' if (a.size() > 1) { return (legal_prefix(a[0]) && legal_prefix(a[1])); } } } return false; }; // Check that: // - We don't have an argument named exactly this // - The argument starts with a prefix char, e.g., "--" // - The argument contains an assign char, e.g., "=" auto assign_char_pos = arg.find_first_of(m_assign_chars); if (m_argument_map.find(arg) == m_argument_map.end() && argument_starts_with_prefix_chars(arg) && assign_char_pos != std::string::npos) { // Get the name of the potential option, and check it exists std::string opt_name = arg.substr(0, assign_char_pos); if (m_argument_map.find(opt_name) != m_argument_map.end()) { // This is the name of an option! Split it into two parts arguments.push_back(std::move(opt_name)); arguments.push_back(arg.substr(assign_char_pos + 1)); continue; } } // If we've fallen through to here, then it's a standard argument arguments.push_back(arg); } return arguments; } /* * @throws std::runtime_error in case of any invalid argument */ void parse_args_internal(const std::vector &raw_arguments) { auto arguments = preprocess_arguments(raw_arguments); if (m_program_name.empty() && !arguments.empty()) { m_program_name = arguments.front(); } auto end = std::end(arguments); auto positional_argument_it = std::begin(m_positional_arguments); for (auto it = std::next(std::begin(arguments)); it != end;) { const auto ¤t_argument = *it; if (Argument::is_positional(current_argument, m_prefix_chars)) { if (positional_argument_it == std::end(m_positional_arguments)) { // Check sub-parsers auto subparser_it = m_subparser_map.find(current_argument); if (subparser_it != m_subparser_map.end()) { // build list of remaining args const auto unprocessed_arguments = std::vector(it, end); // invoke subparser m_is_parsed = true; m_subparser_used[current_argument] = true; return subparser_it->second->get().parse_args(unprocessed_arguments); } if (m_positional_arguments.empty()) { // Ask the user if they argument they provided was a typo // for some sub-parser, // e.g., user provided `git totes` instead of `git notes` if (!m_subparser_map.empty()) { throw std::runtime_error( "Failed to parse '" + current_argument + "', did you mean '" + std::string{details::get_most_similar_string(m_subparser_map, current_argument)} + "'"); } // Ask the user if they meant to use a specific optional argument if (!m_optional_arguments.empty()) { for (const auto &opt : m_optional_arguments) { if (!opt.m_implicit_value.has_value()) { // not a flag, requires a value if (!opt.m_is_used) { throw std::runtime_error("Zero positional arguments expected, did you mean " + opt.get_usage_full()); } } } throw std::runtime_error("Zero positional arguments expected"); } else { throw std::runtime_error("Zero positional arguments expected"); } } else { throw std::runtime_error("Maximum number of positional arguments " "exceeded, failed to parse '" + current_argument + "'"); } } auto argument = positional_argument_it++; // Deal with the situation of ... if (argument->m_num_args_range.get_min() == 1 && argument->m_num_args_range.get_max() == (std::numeric_limits::max)() && positional_argument_it != std::end(m_positional_arguments) && std::next(positional_argument_it) == std::end(m_positional_arguments) && positional_argument_it->m_num_args_range.get_min() == 1 && positional_argument_it->m_num_args_range.get_max() == 1) { if (std::next(it) != end) { positional_argument_it->consume(std::prev(end), end); end = std::prev(end); } else { throw std::runtime_error("Missing " + positional_argument_it->m_names.front()); } } it = argument->consume(it, end); continue; } auto arg_map_it = m_argument_map.find(current_argument); if (arg_map_it != m_argument_map.end()) { auto argument = arg_map_it->second; it = argument->consume(std::next(it), end, arg_map_it->first); } else if (const auto &compound_arg = current_argument; compound_arg.size() > 1 && is_valid_prefix_char(compound_arg[0]) && !is_valid_prefix_char(compound_arg[1])) { ++it; for (std::size_t j = 1; j < compound_arg.size(); j++) { auto hypothetical_arg = std::string{'-', compound_arg[j]}; auto arg_map_it2 = m_argument_map.find(hypothetical_arg); if (arg_map_it2 != m_argument_map.end()) { auto argument = arg_map_it2->second; it = argument->consume(it, end, arg_map_it2->first); } else { throw std::runtime_error("Unknown argument: " + current_argument); } } } else { throw std::runtime_error("Unknown argument: " + current_argument); } } m_is_parsed = true; } /* * Like parse_args_internal but collects unused args into a vector */ std::vector parse_known_args_internal(const std::vector &raw_arguments) { auto arguments = preprocess_arguments(raw_arguments); std::vector unknown_arguments{}; if (m_program_name.empty() && !arguments.empty()) { m_program_name = arguments.front(); } auto end = std::end(arguments); auto positional_argument_it = std::begin(m_positional_arguments); for (auto it = std::next(std::begin(arguments)); it != end;) { const auto ¤t_argument = *it; if (Argument::is_positional(current_argument, m_prefix_chars)) { if (positional_argument_it == std::end(m_positional_arguments)) { // Check sub-parsers auto subparser_it = m_subparser_map.find(current_argument); if (subparser_it != m_subparser_map.end()) { // build list of remaining args const auto unprocessed_arguments = std::vector(it, end); // invoke subparser m_is_parsed = true; m_subparser_used[current_argument] = true; return subparser_it->second->get().parse_known_args_internal(unprocessed_arguments); } // save current argument as unknown and go to next argument unknown_arguments.push_back(current_argument); ++it; } else { // current argument is the value of a positional argument // consume it auto argument = positional_argument_it++; it = argument->consume(it, end); } continue; } auto arg_map_it = m_argument_map.find(current_argument); if (arg_map_it != m_argument_map.end()) { auto argument = arg_map_it->second; it = argument->consume(std::next(it), end, arg_map_it->first); } else if (const auto &compound_arg = current_argument; compound_arg.size() > 1 && is_valid_prefix_char(compound_arg[0]) && !is_valid_prefix_char(compound_arg[1])) { ++it; for (std::size_t j = 1; j < compound_arg.size(); j++) { auto hypothetical_arg = std::string{'-', compound_arg[j]}; auto arg_map_it2 = m_argument_map.find(hypothetical_arg); if (arg_map_it2 != m_argument_map.end()) { auto argument = arg_map_it2->second; it = argument->consume(it, end, arg_map_it2->first); } else { unknown_arguments.push_back(current_argument); break; } } } else { // current argument is an optional-like argument that is unknown // save it and move to next argument unknown_arguments.push_back(current_argument); ++it; } } m_is_parsed = true; return unknown_arguments; } // Used by print_help. std::size_t get_length_of_longest_argument() const { if (m_argument_map.empty()) { return 0; } std::size_t max_size = 0; for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { max_size = std::max(max_size, argument->get_arguments_length()); } for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) { max_size = std::max(max_size, command.size()); } return max_size; } using argument_it = std::list::iterator; using mutex_group_it = std::vector::iterator; using argument_parser_it = std::list>::iterator; void index_argument(argument_it it) { for (const auto &name : std::as_const(it->m_names)) { m_argument_map.insert_or_assign(name, it); } } std::string m_program_name; std::string m_version; std::string m_description; std::string m_epilog; bool m_exit_on_default_arguments = true; std::string m_prefix_chars{"-"}; std::string m_assign_chars{"="}; bool m_is_parsed = false; std::list m_positional_arguments; std::list m_optional_arguments; std::map m_argument_map; std::string m_parser_path; std::list> m_subparsers; std::map m_subparser_map; std::map m_subparser_used; std::vector m_mutually_exclusive_groups; bool m_suppress = false; std::size_t m_usage_max_line_width = std::numeric_limits::max(); bool m_usage_break_on_mutex = false; int m_usage_newline_counter = 0; std::vector m_group_names; }; } // namespace argparse ================================================ FILE: candy-cli/src/config.cc ================================================ // SPDX-License-Identifier: MIT #include "config.h" #include "argparse.h" #include "candy/candy.h" #include #include #include #include #include #include #include #include #include #include #include Poco::JSON::Object arguments::json() { Poco::JSON::Object config; config.set("mode", this->mode); config.set("websocket", this->websocket); config.set("password", this->password); if (this->mode == "client") { config.set("name", this->name); config.set("tun", this->tun); config.set("stun", this->stun); config.set("localhost", this->localhost); config.set("discovery", this->discovery); config.set("route", this->routeCost); config.set("mtu", this->mtu); config.set("port", this->port); config.set("vmac", virtualMac(this->name)); config.set("expt", loadTunAddress(this->name)); } if (this->mode == "server") { config.set("dhcp", this->dhcp); config.set("sdwan", this->sdwan); } return config; } int arguments::parse(int argc, char *argv[]) { argparse::ArgumentParser program("candy", candy::version()); program.add_argument("-c", "--config").help("config file path"); program.add_argument("-m", "--mode").help("working mode"); program.add_argument("-w", "--websocket").help("websocket address"); program.add_argument("-p", "--password").help("authorization password"); program.add_argument("-d", "--dhcp").help("dhcp address range"); program.add_argument("--sdwan").help("software-defined wide area network"); program.add_argument("-n", "--name").help("network interface name"); program.add_argument("-t", "--tun").help("static address"); program.add_argument("-s", "--stun").help("stun address"); program.add_argument("--port").help("p2p listen port").scan<'i', int>(); program.add_argument("--mtu").help("maximum transmission unit").scan<'i', int>(); program.add_argument("-r", "--route").help("routing cost").scan<'i', int>(); program.add_argument("--discovery").help("discovery interval").scan<'i', int>(); program.add_argument("--localhost").help("local ip"); program.add_argument("--no-timestamp").implicit_value(true); program.add_argument("--debug").implicit_value(true); try { program.parse_args(argc, argv); if (program.is_used("--config")) { parseFile(program.get("--config")); } if (program.is_used("--mode")) { this->mode = program.get("--mode"); } program.set_if_used("--mode", this->mode); program.set_if_used("--websocket", this->websocket); program.set_if_used("--password", this->password); program.set_if_used("--no-timestamp", this->noTimestamp); program.set_if_used("--debug", this->debug); program.set_if_used("--dhcp", this->dhcp); program.set_if_used("--sdwan", this->sdwan); program.set_if_used("--name", this->name); program.set_if_used("--tun", this->tun); program.set_if_used("--stun", this->stun); program.set_if_used("--localhost", this->localhost); program.set_if_used("--port", this->port); program.set_if_used("--mtu", this->mtu); program.set_if_used("--discovery", this->discovery); program.set_if_used("--route", this->routeCost); bool needShowUsage = [&]() { if (this->mode != "client" && this->mode != "server") return true; if (this->websocket.empty()) return true; return false; }(); if (needShowUsage) { std::cout << program.usage() << std::endl; exit(1); } if (this->noTimestamp) { spdlog::set_pattern("[%^%l%$] %v"); } if (this->debug) { spdlog::set_level(spdlog::level::debug); } return 0; } catch (const std::exception &e) { std::cout << program.usage() << std::endl; exit(1); } } void arguments::parseFile(std::string cfgFile) { try { std::map> cfgHandlers = { {"mode", [&](const std::string &value) { this->mode = value; }}, {"websocket", [&](const std::string &value) { this->websocket = value; }}, {"password", [&](const std::string &value) { this->password = value; }}, {"debug", [&](const std::string &value) { this->debug = (value == "true"); }}, {"dhcp", [&](const std::string &value) { this->dhcp = value; }}, {"sdwan", [&](const std::string &value) { this->sdwan = value; }}, {"tun", [&](const std::string &value) { this->tun = value; }}, {"stun", [&](const std::string &value) { this->stun = value; }}, {"name", [&](const std::string &value) { this->name = value; }}, {"discovery", [&](const std::string &value) { this->discovery = std::stoi(value); }}, {"route", [&](const std::string &value) { this->routeCost = std::stoi(value); }}, {"port", [&](const std::string &value) { this->port = std::stoi(value); }}, {"mtu", [&](const std::string &value) { this->mtu = std::stoi(value); }}, {"localhost", [&](const std::string &value) { this->localhost = value; }}, }; auto trim = [](std::string str) { if (str.length() >= 2 && str.front() == '\"' && str.back() == '\"') { return str.substr(1, str.length() - 2); } return str; }; auto configs = fileToKvMap(cfgFile); for (auto cfg : configs) { auto handler = cfgHandlers.find(cfg.first); if (handler != cfgHandlers.end()) { handler->second(trim(cfg.second)); } else { spdlog::warn("unknown config: {}={}", cfg.first, cfg.second); } } } catch (std::exception &e) { spdlog::error("parse config file failed: {}", e.what()); exit(1); } } std::map arguments::fileToKvMap(const std::string &filename) { std::map config; std::ifstream file(filename); std::string line; while (std::getline(file, line)) { line = Poco::trimLeft(line); if (line.empty() || line.front() == '#') continue; line.erase(line.find_last_not_of(" \t;") + 1); std::size_t delimiterPos = line.find('='); if (delimiterPos != std::string::npos) { std::string key = Poco::trim(line.substr(0, delimiterPos)); std::string value = Poco::trim(line.substr(delimiterPos + 1)); config[key] = value; } } return config; } int saveTunAddress(const std::string &name, const std::string &cidr) { try { std::string cache = storageDirectory("address/"); cache += name.empty() ? "__noname__" : name; std::filesystem::create_directories(std::filesystem::path(cache).parent_path()); std::ofstream ofs(cache); if (ofs.is_open()) { ofs << cidr; ofs.close(); } return 0; } catch (std::exception &e) { spdlog::critical("save latest address failed: {}", e.what()); return -1; } } std::string loadTunAddress(const std::string &name) { std::string cache = storageDirectory("address/"); cache += name.empty() ? "__noname__" : name; std::ifstream ifs(cache); if (ifs.is_open()) { std::stringstream ss; ss << ifs.rdbuf(); ifs.close(); return ss.str(); } return "0.0.0.0/0"; } std::string virtualMacHelper(std::string name = "") { try { std::string path = storageDirectory("vmac/"); path += name.empty() ? "__noname__" : name; char buffer[candy::VMAC_SIZE]; std::stringstream ss; std::ifstream ifs(path); if (ifs.is_open()) { ifs.read(buffer, sizeof(buffer)); if (ifs) { for (int i = 0; i < (int)sizeof(buffer); i++) { ss << std::hex << buffer[i]; } } ifs.close(); return ss.str(); } return ""; } catch (std::exception &e) { return ""; } } std::string initVirtualMac() { try { std::string path = storageDirectory("vmac/__noname__"); std::filesystem::create_directories(std::filesystem::path(path).parent_path()); std::string vmac = candy::create_vmac(); std::ofstream ofs(path); if (ofs.is_open()) { ofs << vmac; ofs.close(); } return vmac; } catch (std::exception &e) { spdlog::critical("init vmac failed: {}", e.what()); return ""; } } std::string virtualMac(const std::string &name) { std::string path; // 兼容老版本,优先获取与配置网卡名对应的 vmac path = virtualMacHelper(name); if (!path.empty()) { return path; } // 获取网卡名无关的全局 vmac path = virtualMacHelper(); if (!path.empty()) { return path; } // 初次启动,生成全局 vmac return initVirtualMac(); } bool starts_with(const std::string &str, const std::string &prefix) { return str.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), str.begin()); } #if POCO_OS == POCO_OS_WINDOWS_NT std::string storageDirectory(std::string subdir) { return "C:/ProgramData/Candy/" + subdir; } #else std::string storageDirectory(std::string subdir) { return "/var/lib/candy/" + subdir; } #endif ================================================ FILE: candy-cli/src/config.h ================================================ // SPDX-License-Identifier: MIT #ifndef CANDY_CLI_CONFIG_H #define CANDY_CLI_CONFIG_H #include #include #include struct arguments { int parse(int argc, char *argv[]); Poco::JSON::Object json(); private: void parseFile(std::string cfgFile); std::map fileToKvMap(const std::string &filename); std::string mode; std::string websocket; std::string password; bool noTimestamp = false; bool debug = false; std::string dhcp; std::string sdwan; std::string name; std::string tun; std::string stun; std::string localhost; int port = 0; int discovery = 0; int routeCost = 0; int mtu = 1400; }; int saveTunAddress(const std::string &name, const std::string &cidr); std::string loadTunAddress(const std::string &name); std::string virtualMac(const std::string &name); std::string storageDirectory(std::string subdir = ""); #endif ================================================ FILE: candy-cli/src/main.cc ================================================ #include "candy/candy.h" #include "config.h" #include #include int main(int argc, char *argv[]) { arguments args; args.parse(argc, argv); auto config = args.json(); if (config.getValue("mode") == "client") { static const std::string id = "cli"; auto handler = [](int) -> void { candy::client::shutdown(id); }; signal(SIGINT, handler); signal(SIGTERM, handler); std::thread([&]() { while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); auto status = candy::client::status(id); if (status && (*status).has("address")) { std::string address = (*status).getValue("address"); if (!address.empty()) { saveTunAddress(config.getValue("name"), address); break; } } } }).detach(); candy::client::run(id, config); return 0; } if (config.getValue("mode") == "server") { auto handler = [](int) -> void { candy::server::shutdown(); }; signal(SIGINT, handler); signal(SIGTERM, handler); candy::server::run(config); return 0; } return -1; } ================================================ FILE: candy-service/CMakeLists.txt ================================================ file(GLOB_RECURSE SOURCES "src/*.cc") add_executable(candy-service ${SOURCES}) target_include_directories(candy-service PUBLIC $ $ ) set_target_properties(candy-service PROPERTIES OUTPUT_NAME "candy-service") target_link_libraries(candy-service PRIVATE spdlog::spdlog) target_link_libraries(candy-service PRIVATE Poco::Foundation Poco::Net Poco::JSON Poco::Util) target_link_libraries(candy-service PRIVATE Threads::Threads) target_link_libraries(candy-service PRIVATE Candy::Library) install(TARGETS candy-service) add_executable(Candy::Service ALIAS candy-service) ================================================ FILE: candy-service/README.md ================================================ # candy-service Candy 客户端的另一个实现. - **无状态**: 进程本身不持久化任何数据, 进程重启后数据丢失,需要外部维护网络配置信息 - **API 交互**: 对外提供 HTTP API 交互接口,可以远程控制和访问 ## API ### 帮助 Linux ```bash candy-service --help ``` Windows ```bat candy-service /help ``` 请求响应中的 **id** 用于标识网络连接, 通过不同标识可以同时加入多个网络, 这个标识用于查看状态和关闭网络. ### Run 启动参数的含义与[配置文件](../candy.cfg)相同,此外还有两个额外的配置项. - vmac: 用于标识唯一设备,当同一网络中有两台不同 vmac 的设备申请相同 IP 地址时, 后者会报告 IP 冲突. 为 16 个字符的随机数字字母字符串, 需要持久化存储, 建议在首次启动进程时生成. - expt: 期望使用的 IP 地址, 这个参数用于实现有优先分配曾经使用过的地址, 由客户端主动向服务器报告, 可以为空. 建议由服务端随机分配地址的情况下, 通过 `/api/status` 查看分配的地址并保存, 下次连接时携带这个地址. `POST /api/run` ```json { "id": "test", "config": { "mode": "client", "websocket": "wss://canets.org", "password": "", "name": "", "tun": "", "stun": "stun://stun.canets.org", "discovery": 300, "route": 5, "port": 0, "localhost": "", "mtu": 1400, "expt": "", "vmac": "16-char rand str" } } ``` ```json { "id": "test", "message": "success" } ``` ### Status `POST /api/status` ```json { "id": "test" } ``` ```json { "id": "test", "message": "success", "status": { "address": "192.168.202.1/24" } } ``` ### Shutdown `POST /api/shutdown` ```json { "id": "test" } ``` ```json { "id": "test", "message": "success" } ``` ================================================ FILE: candy-service/src/main.cc ================================================ #include "candy/client.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include std::mutex threadMutex; std::map threadMap; class BaseJSONHandler : public Poco::Net::HTTPRequestHandler { protected: Poco::JSON::Object::Ptr readRequest(Poco::Net::HTTPServerRequest &request) { Poco::JSON::Parser parser; Poco::Dynamic::Var result = parser.parse(request.stream()); return result.extract(); } void sendResponse(Poco::Net::HTTPServerResponse &response, const Poco::JSON::Object::Ptr &json) { response.setChunkedTransferEncoding(true); response.setContentType("application/json"); Poco::JSON::Stringifier::stringify(json, response.send()); } }; class RunHandler : public BaseJSONHandler { public: void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override { if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) { response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); return; } auto json = readRequest(request); auto id = json->getValue("id"); auto config = json->getObject("config"); json->remove("config"); std::lock_guard lock(threadMutex); auto it = threadMap.find(id); if (it != threadMap.end()) { json->set("message", "id already exists"); } else { auto thread = std::thread([=]() { candy::client::run(id, *config); }); threadMap.insert({id, std::move(thread)}); json->set("message", "success"); } sendResponse(response, json); } }; class StatusHandler : public BaseJSONHandler { public: void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override { if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) { response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); return; } auto json = readRequest(request); auto id = json->getValue("id"); std::lock_guard lock(threadMutex); auto it = threadMap.find(id); if (it != threadMap.end()) { if (auto status = candy::client::status(id)) { json->set("status", *status); json->set("message", "success"); } else { json->set("message", "unable to get status"); } } else { json->set("message", "id does not exist"); } sendResponse(response, json); } }; class ShutdownHandler : public BaseJSONHandler { public: void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override { if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) { response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); return; } auto json = readRequest(request); auto id = json->getValue("id"); candy::client::shutdown(id); std::lock_guard lock(threadMutex); auto it = threadMap.find(id); if (it != threadMap.end()) { it->second.detach(); threadMap.erase(it); json->set("message", "success"); } else { json->set("message", "id does not exist"); } sendResponse(response, json); } }; class JSONRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory { public: Poco::Net::HTTPRequestHandler *createRequestHandler(const Poco::Net::HTTPServerRequest &request) override { const std::string &uri = request.getURI(); if (uri == "/api/run") { return new RunHandler; } else if (uri == "/api/status") { return new StatusHandler; } else if (uri == "/api/shutdown") { return new ShutdownHandler; } return nullptr; } }; class CandyServiceApp : public Poco::Util::ServerApplication { protected: std::string bindAddress; int port = 0; bool helpRequested = false; std::string logdir; std::string loglevel; void initialize(Poco::Util::Application &self) override { loadConfiguration(); Poco::Util::ServerApplication::initialize(self); } void defineOptions(Poco::Util::OptionSet &options) override { Poco::Util::ServerApplication::defineOptions(options); options.addOption(Poco::Util::Option("help", "", "Display help information") .required(false) .repeatable(false) .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleHelp))); options.addOption(Poco::Util::Option("bind", "", "Bind address and port (address:port)") .required(false) .repeatable(false) .argument("address:port") .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleBind))); options.addOption(Poco::Util::Option("logdir", "", "Specify log directory") .required(false) .repeatable(false) .argument("path") .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleLogDir))); options.addOption(Poco::Util::Option("loglevel", "", "Specify log level") .required(false) .repeatable(false) .argument("level") .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleLogLevel))); } void handleHelp(const std::string &name, const std::string &value) { helpRequested = true; displayHelp(); stopOptionsProcessing(); } void handleBind(const std::string &name, const std::string &value) { size_t pos = value.find(':'); if (pos == std::string::npos) { std::cerr << "Invalid bind format. Use address:port (e.g., 0.0.0.0:26817)" << std::endl; std::exit(EXIT_FAILURE); } bindAddress = value.substr(0, pos); try { port = std::stoi(value.substr(pos + 1)); } catch (const std::exception &e) { std::cerr << "Invalid port number: " << e.what() << std::endl; std::exit(EXIT_FAILURE); } } void handleLogDir(const std::string &name, const std::string &dir) { this->logdir = dir; } void handleLogLevel(const std::string &name, const std::string &level) { this->loglevel = level; } void displayHelp() { Poco::Util::HelpFormatter helpFormatter(options()); helpFormatter.setCommand(commandName()); helpFormatter.format(std::cout); } int main(const std::vector &args) override { if (helpRequested) { return Poco::Util::Application::EXIT_OK; } if (!logdir.empty()) { Poco::File(logdir).createDirectories(); auto logger = spdlog::rotating_logger_mt("app", logdir + "/app.log", 10 * 1024 * 1024, 5, true); spdlog::set_default_logger(logger); } if (!loglevel.empty()) { spdlog::set_level(spdlog::level::from_str(loglevel)); } if (bindAddress.empty()) { bindAddress = "localhost"; port = 26817; } try { Poco::Net::ServerSocket socket; socket.bind(Poco::Net::SocketAddress(bindAddress, port)); socket.listen(); Poco::Net::HTTPServerParams *params = new Poco::Net::HTTPServerParams; params->setMaxQueued(10); params->setMaxThreads(1); Poco::Net::HTTPServer server(new JSONRequestHandlerFactory, socket, params); server.start(); spdlog::info("bind: {}:{}", bindAddress, port); waitForTerminationRequest(); spdlog::info("exit signal detected"); server.stop(); std::lock_guard lock(threadMutex); for (auto &[id, thread] : threadMap) { candy::client::shutdown(id); if (thread.joinable()) { thread.join(); } } } catch (const Poco::Exception &exc) { std::cerr << "Fatal error: " << exc.displayText() << std::endl; return Poco::Util::Application::EXIT_SOFTWARE; } catch (const std::exception &e) { std::cerr << "Fatal error: " << e.what() << std::endl; return Poco::Util::Application::EXIT_SOFTWARE; } return Poco::Util::Application::EXIT_OK; } }; int main(int argc, char **argv) { CandyServiceApp app; return app.run(argc, argv); } ================================================ FILE: candy.cfg ================================================ ############################## Client and Server ############################## # [Required] Working mode, "client" or "server" mode = "client" # [Required] The address that the server listens on # Server only supports ws and needs to provide wss through an external web # service. Client supports ws and wss. websocket = "wss://canets.org" # [Optional] Password used to verify identity # Only the hashed content of the password and timestamp is transmitted on the # network, and the password cannot be obtained from the message. #password = "this is the password" # [Optional] Show debug log #debug = false ################################# Server Only ################################# # [Optional] The range of addresses automatically assigned by the server # Server address allocation is not enabled by default, and the client needs to # configure a static address through tun. #dhcp = "192.168.202.0/24" # [Optional] software-defined wide area network # IP packets entering 192.168.202.1/32 with the destination address 172.17.0.0/16 # will be forwarded to 192.168.202.2. Multiple rules are separated by semicolons. # Extraneous whitespace characters are prohibited. #sdwan = "192.168.202.1/32,172.17.0.0/16,192.168.202.2;192.168.202.2/32,172.16.0.0/16,192.168.202.1" ################################# Client Only ################################# # [Optional] Network interface name # Used to differentiate networks when running multiple clients. #name = "" # [Optional] Static address # If dhcp is not configured, tun must be configured. When there is an address # conflict, the previous client will be kicked out. #tun = "192.168.202.1/24" # [Optional] STUN server address stun = "stun://stun.canets.org" # [Optional] Active discovery interval # Periodically sends broadcasts to try to establish P2P with devices on the # network. The default configuration is 0, which means disabled. discovery = 300 # [Optional] The cost of routing through this machine # Use all nodes in the network to establish the link with the lowest latency. # This configuration represents the cost of using this node as a relay. The # default configuration is 0 which means disabled. route = 5 # [Optional] Local UDP port used for P2P # The default configuration is 0, which means it is allocated by the operating # system. This configuration can be used when the firewall is strict and can # only open specific ports. #port = 0 # [Optional] Local IPv4 address used for peering connections # By default the IPv4 address of the local physical network card will be # detected. When there are multiple physical network cards, the detection # results may not be the best. You can specify it manually. #localhost = "127.0.0.1" # [Optional] Maximum Transmission Unit #mtu=1400 ================================================ FILE: candy.initd ================================================ #!/sbin/openrc-run # Copyright 2024 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 name="candy daemon" description="A simple networking tool" CANDY_NAME=${SVCNAME##*.} if [ -n "${CANDY_NAME}" -a "${SVCNAME}" != "candy" ]; then CANDY_PIDFILE="/run/candy.${CANDY_NAME}.pid" CANDY_CONFIG="/etc/candy.d/${CANDY_NAME}.cfg" CANDY_LOG="/var/log/candy/${CANDY_NAME}.log" else CANDY_PIDFILE="/run/candy.pid" CANDY_CONFIG="/etc/candy.cfg" CANDY_LOG="/var/log/candy/candy.log" fi depend() { need net } start_pre() { if [ ! -d "/tmp/candy/" ]; then mkdir "/tmp/candy" fi if [ ! -L "/var/log/candy" ]; then ln -s "/tmp/candy" "/var/log/" fi } start() { ebegin "Starting Candy, Log File: ${CANDY_LOG}" start-stop-daemon --start --background \ --stdout "${CANDY_LOG}" --stderr "${CANDY_LOG}" \ --make-pidfile --pidfile "${CANDY_PIDFILE}" \ --exec /usr/bin/candy -- -c "${CANDY_CONFIG}" eend $? } stop() { ebegin "Stopping Candy" start-stop-daemon --stop \ --pidfile "${CANDY_PIDFILE}" eend $? } ================================================ FILE: candy.service ================================================ [Unit] Description=A simple networking tool StartLimitIntervalSec=0 [Service] ExecStart=/usr/bin/candy --no-timestamp -c /etc/candy.cfg Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ================================================ FILE: candy@.service ================================================ [Unit] Description=A simple networking tool StartLimitIntervalSec=0 [Service] ExecStart=/usr/bin/candy --no-timestamp -c /etc/candy.d/%i.cfg Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ================================================ FILE: cmake/Fetch.cmake ================================================ macro(Fetch NAME GIT_REPOSITORY GIT_TAG) include(FetchContent) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.28") FetchContent_Declare( ${NAME} GIT_REPOSITORY ${GIT_REPOSITORY} GIT_TAG ${GIT_TAG} EXCLUDE_FROM_ALL ) FetchContent_MakeAvailable(${NAME}) else() FetchContent_Declare( ${NAME} GIT_REPOSITORY ${GIT_REPOSITORY} GIT_TAG ${GIT_TAG} ) FetchContent_GetProperties(${NAME}) if(NOT ${NAME}_POPULATED) FetchContent_Populate(${NAME}) add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR} EXCLUDE_FROM_ALL) endif() endif() endmacro() ================================================ FILE: cmake/openssl/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16) if(POLICY CMP0135) cmake_policy(SET CMP0135 NEW) endif() project(openssl) include(ExternalProject) ExternalProject_Add( openssl PREFIX ${CMAKE_CURRENT_BINARY_DIR}/openssl URL https://www.openssl.org/source/openssl-3.5.4.tar.gz LOG_BUILD ON BUILD_IN_SOURCE YES CONFIGURE_COMMAND COMMAND sed -i "s/disable('static', 'pic', 'threads')/disable('static', 'pic')/" Configure COMMAND ./config --release no-unit-test no-shared ${TARGET_OPENSSL} CFLAGS=$ENV{CFLAGS} LDFLAGS=$ENV{LDFLAGS} BUILD_COMMAND "" INSTALL_COMMAND "" TEST_COMMAND "" ) ================================================ FILE: dockerfile ================================================ FROM alpine AS base RUN apk update RUN apk add spdlog openssl poco FROM base AS build RUN apk add git cmake ninja pkgconf g++ spdlog-dev openssl-dev poco-dev linux-headers COPY . candy RUN cd candy && cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build && cmake --install build FROM base AS product RUN install -D /dev/null /var/lib/candy/lost COPY --from=build /usr/local/bin/candy /usr/bin/candy COPY candy.cfg /etc/candy.cfg ENTRYPOINT ["/usr/bin/candy"] CMD ["-c", "/etc/candy.cfg"] ================================================ FILE: docs/CNAME ================================================ docs.canets.org ================================================ FILE: docs/_config.yml ================================================ title: Candy ================================================ FILE: docs/deploy-cli-server.md ================================================ # 部署 CLI 服务端 根据帮助信息 `candy --help` 和配置文件描述部署. 非专业用户请[部署 Web 服务端](https://docs.canets.org/deploy-web-server). ================================================ FILE: docs/deploy-web-server.md ================================================ # 部署 Web 服务端 ## 前置条件 知道如何部署 Web 服务,并能够申请证书后对外提供 HTTPS 服务. 否则使用明文传输将导致数据泄漏,存在安全隐患.此时建议使用社区服务器构建私有网络. ## 一键部署服务端 ```bash docker run --name=cacao --detach --volume /var/lib/cacao:/var/lib/cacao --publish 8080:80 docker.io/lanthora/cacao:latest ``` ## 使用 假设你的域名为 `example.com`, 此时通过 `https://example.com` 应该能够正常访问服务.如果不是 `https` 请回到最开始解决前置条件. 服务器启动后的第一个注册用户默认被设置为管理员.管理员无法创建网络,且无权查看其他用户的网络. 管理员配置页面,能够配置是否允许注册,以及允许注册时的注册间隔(避免脚本小子刷注册用户).同时可以配置自动清理不活跃用户. ![](images/cacao-admin-setting.png) ### 单网络模式 在不允许注册时,管理员可以手动添加用户.其中名为 @ 的用户是一个特殊用户,这个用户只能创建一个名为 @ 的网络.用户名和网络名的作用在后面说明.先创建这个用户. ![](images/cacao-admin-user.png) 退出管理员,并以 @ 用户登录.此时已经默认添加了 @ 网络.默认网络生成了随机密码 `ZrhaUcz1` ![](images/cacao-network.png) 此时连接这个网络的客户端仅需要修改以下配置: ```cfg websocket = "wss://example.com" password = "ZrhaUcz1" ``` 除非你知道自己在做什么,否则请不要修改任何其他配置项. ### 多用户多网络模式 如果只是创建一个网络,单网络模式已经足够了.如果要允许多个用户使用,且每个用户可以创建多个网络.则可以使用多用户多网络模式. 假设由管理员创建或自行注册的普通用户名为 `${username}`, 这个用户拥有的一个网络名是 `${netname}`,那么客户端对应的配置仅需要修改为: ```cfg websocket = "wss://example.com/${username}/${netname}" ``` 当用户名或者网络名为 @ 时,在客户端的配置中需要留空.当用户名和网络名都为空时,就是所谓的单网络模式 ================================================ FILE: docs/index.md ================================================ # Candy

一个高可用,低时延,反审查的组网工具. ## 如何使用 - [安装 Windows 客户端](install-client-for-windows) - [安装 macOS 客户端](install-client-for-macos) - [安装 Linux 客户端](install-client-for-linux) - [部署 Web 服务端](deploy-web-server) - [部署 CLI 服务端](deploy-cli-server) - [使用社区服务器](use-the-community-server) - [多局域网组网](software-defined-wide-area-network) ## 相关项目 - [Cacao](https://github.com/lanthora/cacao): WebUI 版的 Candy 服务器 - [Cake](https://github.com/lanthora/cake): Qt 实现的 Candy GUI 桌面应用程序 - [Candy Android](https://github.com/Jercrox/Candy_Android_Client): Android 客户端 - [EasyTier](https://github.com/EasyTier/EasyTier): 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现 - [candygo](https://github.com/SoraKasvgano/candygo): 一个简单的与candy原项目配置文件兼容的go版本 ## 交流群 - QQ: 768305206 - TG: [Click to Join](https://t.me/CandyUserGroup) ================================================ FILE: docs/install-client-for-linux.md ================================================ # 安装 Linux 客户端 我们针对不同 Linux 发行版提供了多种格式的安装包.对于暂未支持的发行版,可以选择容器部署或者静态链接的可执行文件. 我们致力于支持所有架构的 Linux 系统. ## Docker 镜像已上传 [Docker Hub](https://hub.docker.com/r/lanthora/candy) 和 [Github Packages](https://github.com/lanthora/candy/pkgs/container/candy). 获取最新镜像 ```bash docker pull docker.io/lanthora/candy:latest ``` 容器需要管理员权限读取设备创建虚拟网卡并设置路由,同时需要 Host 网络命名空间共享虚拟网卡. 以默认配置文件启动将加入社区网络.指定的参数为 `--rm` 当进程结束时会自动销毁容器,且日志会在控制台输出,这有利于初次运行调试. ```bash docker run --rm --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy docker.io/lanthora/candy:latest ``` 以自定义配置文件启动.请在[默认配置](https://raw.githubusercontent.com/lanthora/candy/refs/heads/master/candy.cfg)基础上自定义配置文件. ```bash docker run --rm --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy --volume /path/to/candy.cfg:/etc/candy.cfg docker.io/lanthora/candy:latest ``` 一切正常后,以守护进程的形式启动. ```bash docker run --detach --restart=always --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy --volume /path/to/candy.cfg:/etc/candy.cfg docker.io/lanthora/candy:latest ``` ## Arch Linux 使用 [AUR](https://aur.archlinux.org/packages/candy) 或者 [archlinuxcn](https://github.com/archlinuxcn/repo/tree/master/archlinuxcn/candy) 仓库 ```bash # AUR paru candy # archlinuxcn pacman -S candy ``` ## Gentoo ```bash emerge --sync gentoo && emerge -av candy ``` ## 单文件可执行程序 当上述所有方式都不适用时,尝试[单文件可执行程序](https://github.com/lanthora/candy/releases/latest). 该程序由[交叉编译脚本](https://github.com/lanthora/candy/tree/master/scripts/build-standalone.sh)构建. 如果你的系统在使用 Systemd 管理进程.请复制以下文件到指定目录. ```bash cp candy.service /usr/lib/systemd/system/candy.service cp candy@.service /usr/lib/systemd/system/candy@.service cp candy.cfg /etc/candy.cfg ``` 然后按照后续进程管理的方式管理进程. 判断 Systemd 的方法: 检查 `ps -p 1 -o comm=` 输出的内容里是否为 systemd ## 进程管理 各发行版安装后自带 Service 文件,强烈建议使用 Systemd 管理进程,不要使用自己编写的脚本. 对于自定义配置的用户,可以通过以下方式启动进程,不要修改默认配置. ```bash mkdir /etc/candy.d # 复制一份默认配置,并修改.文件名为 one.cfg cp /etc/candy.cfg /etc/candy.d/one.cfg # 以 one.cfg 为配置启动进程 systemctl start candy@one # 复制一份默认配置,并修改.文件名为 two.cfg # 需要注意不同配置文件中的 name 字段不能重复 cp /etc/candy.cfg /etc/candy.d/two.cfg # 以 two.cfg 为配置启动进程 systemctl start candy@two ``` ================================================ FILE: docs/install-client-for-macos.md ================================================ # 安装 macOS 客户端 macOS 客户端通过 [Homebrew](https://brew.sh) 安装并提供服务. ## 安装 Homebrew ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` ## 添加第三方仓库 ```bash brew tap lanthora/repo ``` ## 安装 Candy ```bash brew install candy ``` ## 修改配置 对于 M 系列处理器,配置文件在 `/opt/homebrew/etc/candy.cfg`, Intel 系列处理器,配置文件在 `/usr/local/etc/candy.cfg` 通过以下命令进行测试: ```bash sudo candy -c /path/to/candy.cfg ``` ## 启动服务 测试成功后以服务的形式运行. ```bash sudo brew services start lanthora/repo/candy ``` ================================================ FILE: docs/install-client-for-windows.md ================================================ # 安装 Windows 客户端 ## 图形用户界面 对于 Windows 10 及以上的用户,请使用[图形界面版本](https://github.com/lanthora/cake/releases/latest).此版本支持同时配置多个网络. 在没有任何配置时,点击 "文件" => "新建" 将填充默认配置,点击 "保存" 后配置生效,客户端此时才开始连接服务端. 图形界面的配置与[默认配置](https://raw.githubusercontent.com/lanthora/candy/refs/heads/master/candy.cfg)对应. 日志保存在 `C:/ProgramData/Cake/logs`, 反馈 Windows 相关问题请带着日志和配置截图. ![cake](images/cake.png) ## 命令行 使用命令行版本请自行解决遇到的任何问题,我们不对 Windows 命令行提供任何技术支持. Windows 7 用户只能使用[命令行版本](https://github.com/lanthora/candy/releases/latest) ================================================ FILE: docs/software-defined-wide-area-network.md ================================================ # 多局域网组网 ## 需求 在多地有多个局域网时,希望能够让本局域网内的设备通过其他局域网的地址直接访问对方的设备,并且无需在所有设备上部署 Candy 客户端. ## 示例 首先你需要: - 有一个独立的网络.可以自建服务端或者使用社区服务器 - 在网关 (Gateway) 上部署 Candy 并成功加入自己创建的网络 以 LAN A 为例解释表格含义. - 局域网 (Network) 地址为 `172.16.1.0/24`, 这个地址不能与 B,C 冲突 - 网关 (Gateway) 可以是路由器,也可以是局域网中任意一台 Linux 系统,但需要能够部署 Candy 客户端,假设它在局域网中的地址是 `172.16.1.1`. 通过给局域网中的设备配置路由,确保流量能够进入网关 - Candy 客户端部署在网关上,它在虚拟网络中的地址是 `192.168.202.1` | LAN | A | B | C | | :------ | :------------ | :------------ | :------------ | | Network | 172.16.1.0/24 | 172.16.2.0/24 | 172.16.3.0/24 | | Gateway | 172.16.1.1 | 172.16.2.1 | 172.16.3.1 | | Candy | 192.168.202.1 | 192.168.202.2 | 192.168.202.3 | 当 `172.16.1.0/24` 的设备访问 `172.16.2.0/24` 的设备时,希望流量可以通过以下方式送达: ```txt 172.16.1.0/24 <=> 172.16.1.1 <=> 192.168.202.1 <=> 192.168.202.2 <=> 172.16.2.1 <=> 172.16.2.0/24 ``` ### 流量转发到网关 (172.16.1.0/24 => 172.16.1.1) 如果网关是路由器,不需要任何操作,流量就应该能够进入网关.否则需要在非网关设备上配置流量转发到网关的路由. 给 172.16.1.0/24 的设备配置路由: - dst: 172.16.2.0/24; gw: 172.16.1.1 - dst: 172.16.3.0/24; gw: 172.16.1.1 需要用同样的方式给另外两个局域网做配置. ### 允许网关转发流量 (172.16.1.1 <=> 192.168.202.1) #### Linux 如果你的网关是路由器,应该能够轻易的配置出允许转发.否则需要手动添加转发相关的配置. 开启内核流量转发功能 ```bash sysctl -w net.ipv4.ip_forward=1 ``` 开启动态伪装并接受转发报文. ```bash iptables -t nat -A POSTROUTING -j MASQUERADE iptables -A FORWARD -j ACCEPT ``` #### Windows 查看的网卡名,应该与配置文件中写的名称相同,对于 GUI 版本客户端的默认配置网卡名应该为 `candy` ```ps Get-NetAdapter ``` 允许转发,注意要把网卡名替换成上一步查出来的网卡名 ```ps Set-NetIPInterface -ifAlias 'candy' -Forwarding Enabled ``` #### macOS 应该不会有人拿 macOS 做网关吧, Windows 应该都没有多少人,有需要再补充这部分文档 ### 创建虚拟链路 (172.16.1.0/24 <=> 172.16.2.0/24) 所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.1.0/24` 的 IP 报文时,将其转发到 `192.168.202.1`; 所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.2.0/24` 的 IP 报文时,将其转发到 `192.168.202.2`; 所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.3.0/24` 的 IP 报文时,将其转发到 `192.168.202.3`; 策略会发下给属于 `192.168.202.0/24` 网络的客户端,上面的配置下发给了虚拟网络中的所有设备,能够满足大部分用户场景. 此外支持更细粒度的控制供用户选择,例如 `192.168.202.1/32` 就表示仅把路由策略下发给 `192.168.202.1` 这台设备. #### Cacao 配置 如果你在使用 Cacao 服务端(例如社区服务端),配置如下. ![sdwan](images/sdwan.png) #### Candy 配置 如果你在使用命令行版本的 Candy 服务端,等效配置如下. ```ini sdwan = "192.168.202.0/24,172.16.1.0/24,192.168.202.1;192.168.202.0/24,172.16.2.0/24,192.168.202.2;192.168.202.0/24,172.16.3.0/24,192.168.202.3;" ``` ### 测试 此时局域网设备之间应当可以相互 ping 通. ## 常见问题 ### 能 ping 通网关,但 ping 不通网关下的目标设备 - 检查 iptables 配置的动态伪装是否生效.如果生效,抓包可以看到发往目标设备的源地址已经改成了网关地址 - 检查目标设备防火墙.例如 Windows 系统防火墙默认禁止 ping, 此时直接尝试访问 Windows 提供出的服务,例如远程桌面, SSH, Web 服务等 ### 能 ping 通目标设备,但不能访问服务 - 检查 iptables 配置的动态伪装是否生效.动态伪装不生效的情况下,某种路由配置规则也可以实现 ping 通目标设备,但是防火墙会拦截对应报文. ### 关于源进源出 通过合理的路由配置和对防火墙策略的调整,在不使用动态伪装的情况下,可以做到在目标设备看到请求的真实源地址.想要达成这个效果需要有足够的计算机网络知识储备,请自行探索. ================================================ FILE: docs/use-the-community-server.md ================================================ # 使用社区服务器 社区服务器支持用户级别的隔离,同时支持一个用户创建多个网络. __服务器将定期清理不活跃用户,请确保短期内至少有一台设备连接过服务器,或手动登录过服务器管理页面.__ ## 注册 在社区服务器[注册](https://canets.org/register),示例中的用户名为 `username`. ![](images/cacao-register.png) ## 使用默认网络 查看网络,可以注意到已经有一个名称为 @ 的默认网络,密码是 `ZrhaUcz1` ![](images/cacao-network.png) 连接到这个网络的客户端仅需要修改以下配置,关于配置文件的位置请参考客户端安装的相关文档: ```cfg websocket = "wss://canets.org/username" password = "ZrhaUcz1" ``` ## 多个网络 点击左上角 `Add` 可以创建多个网络,例如: ![](images/cacao-network-another.png) 这个新网络,网络名为 `netname`, 这会体现到 `websocket` 参数中; 密码为空; 网络范围是 `10.0.0.0/24`; 不允许广播; 且租期为 3 天, 即超过 3 天不活跃的客户端将被自动从网络中移除, 配置为 0 时表示不自动移除. 客户端的配置应该为: ```cfg websocket = "wss://canets.org/username/netname" password = "" ``` 如果要给某个客户端指定静态地址 `10.0.0.1/24`, 只需要修改配置中的: ```cfg tun = "10.0.0.1/24" ``` ================================================ FILE: scripts/build-standalone.sh ================================================ #!/bin/bash -e if [ -z $CANDY_WORKSPACE ];then echo "CANDY_WORKSPACE is not exist";exit 1;fi if [[ -z $TARGET || -z $TARGET_OPENSSL ]];then if [ -z $CANDY_ARCH ];then echo "CANDY_ARCH is not exist";exit 1;fi if [ -z $CANDY_OS ];then echo "CANDY_OS is not exist";exit 1;fi echo "CANDY_ARCH: $CANDY_ARCH" echo "CANDY_OS: $CANDY_OS" if [[ "$CANDY_OS" == "linux" ]]; then if [[ "$CANDY_ARCH" == "aarch64" ]]; then TARGET="aarch64-unknown-linux-musl";TARGET_OPENSSL="linux-aarch64";UPX=1 elif [[ "$CANDY_ARCH" == "arm" ]]; then TARGET="arm-unknown-linux-musleabi";TARGET_OPENSSL="linux-armv4";UPX=1 elif [[ "$CANDY_ARCH" == "armhf" ]]; then TARGET="arm-unknown-linux-musleabihf";TARGET_OPENSSL="linux-armv4";UPX=1 elif [[ "$CANDY_ARCH" == "loongarch64" ]]; then TARGET="loongarch64-unknown-linux-musl";TARGET_OPENSSL="linux64-loongarch64";UPX=0 elif [[ "$CANDY_ARCH" == "mips" ]]; then TARGET="mips-unknown-linux-musl";TARGET_OPENSSL="linux-mips32";UPX=1 elif [[ "$CANDY_ARCH" == "mipssf" ]]; then TARGET="mips-unknown-linux-muslsf";TARGET_OPENSSL="linux-mips32";UPX=1 elif [[ "$CANDY_ARCH" == "mipsel" ]]; then TARGET="mipsel-unknown-linux-musl";TARGET_OPENSSL="linux-mips32";UPX=1 elif [[ "$CANDY_ARCH" == "mipselsf" ]]; then TARGET="mipsel-unknown-linux-muslsf";TARGET_OPENSSL="linux-mips32";UPX=1 elif [[ "$CANDY_ARCH" == "mips64" ]]; then TARGET="mips64-unknown-linux-musl";TARGET_OPENSSL="linux64-mips64";UPX=0 elif [[ "$CANDY_ARCH" == "mips64el" ]]; then TARGET="mips64el-unknown-linux-musl";TARGET_OPENSSL="linux64-mips64";UPX=0 elif [[ "$CANDY_ARCH" == "riscv32" ]]; then TARGET="riscv32-unknown-linux-musl";TARGET_OPENSSL="linux32-riscv32";UPX=0 elif [[ "$CANDY_ARCH" == "riscv64" ]]; then TARGET="riscv64-unknown-linux-musl";TARGET_OPENSSL="linux64-riscv64";UPX=0 elif [[ "$CANDY_ARCH" == "x86_64" ]]; then TARGET="x86_64-unknown-linux-musl";TARGET_OPENSSL="linux-x86_64";UPX=1 else echo "Unknown CANDY_ARCH: $CANDY_ARCH";exit 1;fi elif [[ "$CANDY_OS" == "macos" ]]; then echo "macos is not supported yet";exit 1 elif [[ "$CANDY_OS" == "windows" ]]; then echo "windows is not supported yet";exit 1 else echo "Unknown CANDY_OS: $CANDY_OS";exit 1;fi fi echo "CANDY_WORKSPACE: $CANDY_WORKSPACE" echo "TARGET: $TARGET" echo "TARGET_OPENSSL: $TARGET_OPENSSL" TOOLCHAINS="$CANDY_WORKSPACE/toolchains" COMPILER_ROOT="$TOOLCHAINS/$TARGET" if [ ! -d "$COMPILER_ROOT" ]; then mkdir -p $TOOLCHAINS VERSION=20250206 wget -c https://github.com/musl-cross/musl-cross/releases/download/$VERSION/$TARGET.tar.xz -P $TOOLCHAINS tar xvf $COMPILER_ROOT.tar.xz -C $TOOLCHAINS fi export CC="$COMPILER_ROOT/bin/$TARGET-gcc" export CXX="$COMPILER_ROOT/bin/$TARGET-g++" export AR="$COMPILER_ROOT/bin/$TARGET-ar" export LD="$COMPILER_ROOT/bin/$TARGET-ld" export RANLIB="$COMPILER_ROOT/bin/$TARGET-ranlib" export STRIP="$COMPILER_ROOT/bin/$TARGET-strip" export CFLAGS="-I $COMPILER_ROOT/$TARGET/include" export LDFLAGS="-static -Wl,--whole-archive -latomic -Wl,--no-whole-archive -L $COMPILER_ROOT/$TARGET/lib" if [[ $CANDY_OS && $CANDY_ARCH ]];then BUILD_DIR="$CANDY_WORKSPACE/build/$CANDY_OS-$CANDY_ARCH" OUTPUT_DIR="$CANDY_WORKSPACE/output/$CANDY_OS-$CANDY_ARCH" else BUILD_DIR="$CANDY_WORKSPACE/build/$TARGET" OUTPUT_DIR="$CANDY_WORKSPACE/output/$TARGET" fi if which ninja >/dev/null 2>&1;then GENERATOR="Ninja";else GENERATOR="Unix Makefiles";fi SOURCE_DIR="$(dirname $(readlink -f "$0"))/../" cmake -G "$GENERATOR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DCANDY_STATIC=1 -DTARGET_OPENSSL=$TARGET_OPENSSL $SOURCE_DIR cmake --build $BUILD_DIR --parallel $(nproc) mkdir -p $OUTPUT_DIR cp $BUILD_DIR/candy-cli/candy $OUTPUT_DIR/candy cp $BUILD_DIR/candy-service/candy-service $OUTPUT_DIR/candy-service if [[ $CANDY_STRIP && $CANDY_STRIP -eq 1 ]];then $STRIP $OUTPUT_DIR/candy $STRIP $OUTPUT_DIR/candy-service fi if [[ $CANDY_UPX && $CANDY_UPX -eq 1 && $UPX -eq 1 ]];then upx --lzma --best -q $OUTPUT_DIR/candy upx --lzma --best -q $OUTPUT_DIR/candy-service fi if [[ $CANDY_TGZ && $CANDY_TGZ -eq 1 && $CANDY_OS && $CANDY_ARCH ]];then cp $SOURCE_DIR/{candy.cfg,candy.service,candy@.service,candy.initd} $OUTPUT_DIR tar zcvf $CANDY_WORKSPACE/output/candy-$CANDY_OS-$CANDY_ARCH.tar.gz -C $OUTPUT_DIR . fi ================================================ FILE: scripts/search-deps.sh ================================================ #!/bin/bash -e # Define an array to store the processed dependencies declare -a processed # Define a function to get the whole list of dependencies recursively recursive_search_deps () { # Use ldd to list the dependencies and filter out the ones that are not absolute paths local list=$(ldd "$1" | awk '/=> \// {print $3}') # Loop through the dependencies for dep in $list; do # Check if the dependency has been processed before if [[ ! " ${processed[@]} " =~ " ${dep} " ]]; then # Check if the dependency contains /c/Windows in its path if [[ "$dep" =~ "/c/Windows" ]]; then # Ignore the dependency and continue the loop continue fi # Copy the dependency to the specified directory cp -n "$dep" "$2" # Output the copied file path and name echo "Copied $dep to $2" # Add the dependency to the processed array processed+=("$dep") # Recursively call the function to process the dependency's dependencies recursive_search_deps "$dep" "$2" fi done } # Check if the executable file is given as an argument if [ -z "$2" ]; then echo "Usage: $0 " exit 1 fi # Create the directory if it does not exist if [ ! -d "$2" ]; then mkdir -p $2 fi # Get the absolute path of the executable file exe=$(readlink -f "$1") # Copy the executable file to the target directory cp "$exe" "$2" exe=$2/$(basename "$exe") exe=$(readlink -f "$exe") # Call the function to get the whole list of dependencies recursively recursive_search_deps "$exe" "$2" ================================================ FILE: scripts/standalone.json ================================================ { "include": [ { "os": "linux", "arch": "aarch64" }, { "os": "linux", "arch": "arm" }, { "os": "linux", "arch": "armhf" }, { "os": "linux", "arch": "loongarch64" }, { "os": "linux", "arch": "mips" }, { "os": "linux", "arch": "mipssf" }, { "os": "linux", "arch": "mipsel" }, { "os": "linux", "arch": "mipselsf" }, { "os": "linux", "arch": "mips64" }, { "os": "linux", "arch": "mips64el" }, { "os": "linux", "arch": "riscv32" }, { "os": "linux", "arch": "riscv64" }, { "os": "linux", "arch": "x86_64" } ] }