Showing preview only (318K chars total). Download the full file or copy to clipboard to get everything.
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
<p>
<a href="https://github.com/lanthora/candy/releases/latest"><img src="https://img.shields.io/github/release/lanthora/candy" /></a>
<a href="https://github.com/lanthora/candy/actions/workflows/release.yaml"><img src="https://img.shields.io/github/actions/workflow/status/lanthora/candy/release.yaml" /></a>
<a href="https://github.com/lanthora/candy/graphs/contributors"><img src="https://img.shields.io/github/contributors-anon/lanthora/candy" /></a>
<a href="https://github.com/lanthora/candy/issues"><img src="https://img.shields.io/github/issues-raw/lanthora/candy" /></a>
<a href="https://github.com/lanthora/candy/pulls"><img src="https://img.shields.io/github/issues-pr-raw/lanthora/candy" /></a>
</p>
一个简单的组网工具.
## 如何使用
- [安装 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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
$<INSTALL_INTERFACE:include>
)
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 <Poco/JSON/Object.h>
#include <optional>
#include <string>
namespace candy {
namespace client {
bool run(const std::string &id, const Poco::JSON::Object &config);
bool shutdown(const std::string &id);
std::optional<Poco::JSON::Object> 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 <string>
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 <Poco/JSON/Object.h>
#include <string>
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 <Poco/JSON/Object.h>
#include <Poco/JSON/Stringifier.h>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <spdlog/spdlog.h>
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<Client> create_client() {
auto client = std::make_shared<Client>();
this->client = client;
return client;
}
private:
Atomic<bool> running = Atomic(true);
std::weak_ptr<Client> client;
};
std::map<std::string, std::shared_ptr<Instance>> instance_map;
std::shared_mutex instance_mutex;
std::optional<std::shared_ptr<Instance>> 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>();
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<std::string>("name"));
client->setPassword(config.getValue<std::string>("password"));
client->setWebSocket(config.getValue<std::string>("websocket"));
client->setTunAddress(config.getValue<std::string>("tun"));
client->setVirtualMac(config.getValue<std::string>("vmac"));
client->setExptTunAddress(config.getValue<std::string>("expt"));
client->setStun(config.getValue<std::string>("stun"));
client->setDiscoveryInterval(config.getValue<int>("discovery"));
client->setRouteCost(config.getValue<int>("route")), client->setMtu(config.getValue<int>("mtu"));
client->setPort(config.getValue<int>("port"));
client->setLocalhost(config.getValue<std::string>("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<Poco::JSON::Object> 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<bool> running(true);
std::shared_ptr<Server> 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>();
server->setWebSocket(config.getValue<std::string>("websocket"));
server->setPassword(config.getValue<std::string>("password"));
server->setDHCP(config.getValue<std::string>("dhcp"));
server->setSdwan(config.getValue<std::string>("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 <Poco/String.h>
#include <chrono>
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 <condition_variable>
#include <mutex>
#include <queue>
#include <string>
namespace candy {
class MsgQueue {
public:
Msg read();
void write(Msg msg);
void clear();
private:
std::queue<Msg> 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<bool> 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 <string>
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 <cstring>
#include <string>
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 <Poco/Net/IPAddress.h>
#include <cstring>
#include <exception>
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 <array>
#include <cstdint>
#include <spdlog/spdlog.h>
#include <string>
#include <type_traits>
namespace candy {
template <typename T> typename std::enable_if<std::is_integral<T>::value, T>::type byteswap(T value) {
static_assert(std::is_integral<T>::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 <typename T> T ntoh(T v) {
static_assert(std::is_integral<T>::value, "ntoh requires integral type");
uint8_t *bytes = reinterpret_cast<uint8_t *>(&v);
bool isLittleEndian = true;
{
uint16_t test = 0x0001;
isLittleEndian = (*reinterpret_cast<uint8_t *>(&test) == 0x01);
}
if (isLittleEndian) {
return byteswap(v);
}
return v;
}
template <typename T> 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<uint8_t, 4> 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<IP4> {
size_t operator()(const IP4 &ip) const noexcept {
return hash<uint32_t>{}(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 <string>
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<bool> 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 <Poco/Platform.h>
#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 <Poco/Net/NetException.h>
#include <Poco/Net/NetworkInterface.h>
#include <Poco/Timespan.h>
#include <openssl/sha.h>
#include <shared_mutex>
#include <spdlog/fmt/bin_to_hex.h>
#include <spdlog/spdlog.h>
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<int>(msg.kind));
break;
}
} catch (const Poco::Exception &e) {
spdlog::warn("peer manager handle queue failed: msg_kind={}, error={}", static_cast<int>(msg.kind), e.message());
return 0;
} catch (const std::exception &e) {
spdlog::warn("peer manager handle queue failed: msg_kind={}, error={}", static_cast<int>(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>(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<std::string> 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 <Poco/Net/DatagramSocket.h>
#include <Poco/Net/ServerSocket.h>
#include <Poco/Net/StreamSocket.h>
#include <Poco/URI.h>
#include <shared_mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
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<IP4, Peer> ipPeerMap;
void showRtChange(const PeerRouteEntry &entry);
int sendRtMessage(IP4 dst, int32_t rtt);
std::shared_mutex rtTableMutex;
std::unordered_map<IP4, PeerRouteEntry> rtTableMap;
public:
Stun stun;
std::atomic<bool> 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<std::string> decrypt(const std::string &ciphertext);
std::shared_ptr<EVP_CIPHER_CTX> 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 <string>
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 <cstdint>
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 <Poco/Net/IPAddress.h>
#include <Poco/Net/SocketAddress.h>
#include <algorithm>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <spdlog/spdlog.h>
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>(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<std::string> 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<int32_t> 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 <Poco/Net/SocketAddress.h>
#include <chrono>
#include <cstdint>
#include <map>
#include <memory>
#include <openssl/evp.h>
#include <optional>
#include <shared_mutex>
#include <string>
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<int32_t> isConnected() const;
int32_t rtt = RTT_LIMIT;
uint32_t tickCount = randomUint32();
private:
PeerManager &getManager();
PeerManager *peerManager;
std::optional<std::string> encrypt(const std::string &plaintext);
std::shared_ptr<EVP_CIPHER_CTX> 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<SocketAddress> wide, local, real;
std::shared_mutex socketAddressMutex;
IP4 addr;
};
} // namespace candy
#endif
================================================
FILE: candy/src/tun/linux.cc
================================================
// SPDX-License-Identifier: MIT
#include <Poco/Platform.h>
#if POCO_OS == POCO_OS_LINUX
#include "core/net.h"
#include "tun/tun.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <linux/if_tun.h>
#include <memory>
#include <net/if.h>
#include <net/route.h>
#include <spdlog/spdlog.h>
#include <string>
#include <sys/ioctl.h>
#include <unistd.h>
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<LinuxTun>();
}
Tun::~Tun() {
this->impl.reset();
}
int Tun::setName(const std::string &name) {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
tun->setName(name);
return 0;
}
int Tun::setAddress(const std::string &cidr) {
std::shared_ptr<LinuxTun> tun;
Address address;
if (address.fromCidr(cidr)) {
return -1;
}
spdlog::info("client address: {}", address.toCidr());
tun = std::any_cast<std::shared_ptr<LinuxTun>>(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<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
return tun->getIP();
}
int Tun::setMTU(int mtu) {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
if (tun->setMTU(mtu)) {
return -1;
}
return 0;
}
int Tun::up() {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
return tun->up();
}
int Tun::down() {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
return tun->down();
}
int Tun::read(std::string &buffer) {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
return tun->read(buffer);
}
int Tun::write(const std::string &buffer) {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
return tun->write(buffer);
}
int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
std::shared_ptr<LinuxTun> tun;
tun = std::any_cast<std::shared_ptr<LinuxTun>>(this->impl);
return tun->setSysRtTable(dst, mask, nexthop);
}
} // namespace candy
#endif
================================================
FILE: candy/src/tun/macos.cc
================================================
// SPDX-License-Identifier: MIT
#include <Poco/Platform.h>
#if POCO_OS == POCO_OS_MAC_OS_X
#include "core/net.h"
#include "tun/tun.h"
#include <errno.h>
#include <fcntl.h>
#include <memory>
// clang-format off
#include <sys/socket.h>
#include <net/if.h>
// clang-format on
#include <net/if_utun.h>
#include <net/route.h>
#include <netinet/in.h>
#include <spdlog/fmt/bin_to_hex.h>
#include <spdlog/spdlog.h>
#include <string>
#include <sys/ioctl.h>
#include <sys/kern_control.h>
#include <sys/select.h>
#include <sys/sys_domain.h>
#include <sys/uio.h>
#include <unistd.h>
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<MacTun>();
}
Tun::~Tun() {
this->impl.reset();
}
int Tun::setName(const std::string &name) {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
tun->setName(name);
return 0;
}
int Tun::setAddress(const std::string &cidr) {
std::shared_ptr<MacTun> tun;
Address address;
if (address.fromCidr(cidr)) {
return -1;
}
spdlog::info("client address: {}", address.toCidr());
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
if (tun->setIP(address.Host())) {
return -1;
}
if (tun->setMask(address.Mask())) {
return -1;
}
return 0;
}
IP4 Tun::getIP() {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
return tun->getIP();
}
int Tun::setMTU(int mtu) {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
if (tun->setMTU(mtu)) {
return -1;
}
return 0;
}
int Tun::up() {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
return tun->up();
}
int Tun::down() {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
return tun->down();
}
int Tun::read(std::string &buffer) {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
return tun->read(buffer);
}
int Tun::write(const std::string &buffer) {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(this->impl);
return tun->write(buffer);
}
int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
std::shared_ptr<MacTun> tun;
tun = std::any_cast<std::shared_ptr<MacTun>>(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 <mutex>
#include <shared_mutex>
#include <spdlog/fmt/bin_to_hex.h>
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<int>(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 <any>
#include <list>
#include <shared_mutex>
#include <string>
#include <thread>
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<SysRouteEntry> sysRtTable;
private:
std::any impl;
private:
Client &getClient();
Client *client;
};
} // namespace candy
#endif
================================================
FILE: candy/src/tun/unknown.cc
================================================
// SPDX-License-Identifier: MIT
#include <Poco/Platform.h>
#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 <Poco/Platform.h>
#if POCO_OS == POCO_OS_WINDOWS_NT
#include "core/net.h"
#include "tun/tun.h"
#include "utils/codecvt.h"
#include <memory>
#include <openssl/sha.h>
#include <spdlog/fmt/bin_to_hex.h>
#include <spdlog/spdlog.h>
#include <stack>
#include <string>
// clang-format off
#include <winsock2.h>
#include <windows.h>
#include <ws2ipdef.h>
#include <iphlpapi.h>
#include <guiddef.h>
#include <mstcpip.h>
#include <winternl.h>
#include <netioapi.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
// clang-format on
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunknown-pragmas"
#include <wintun.h>
#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<MIB_IPFORWARDROW> routes;
WINTUN_ADAPTER_HANDLE adapter = NULL;
WINTUN_SESSION_HANDLE session = NULL;
};
} // namespace candy
namespace candy {
Tun::Tun() {
this->impl = std::make_shared<WindowsTun>();
}
Tun::~Tun() {
this->impl.reset();
}
int Tun::setName(const std::string &name) {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
tun->setName(name);
return 0;
}
int Tun::setAddress(const std::string &cidr) {
std::shared_ptr<WindowsTun> tun;
Address address;
if (address.fromCidr(cidr)) {
return -1;
}
spdlog::info("client address: {}", address.toCidr());
tun = std::any_cast<std::shared_ptr<WindowsTun>>(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<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
return tun->getIP();
}
int Tun::setMTU(int mtu) {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
if (tun->setMTU(mtu)) {
return -1;
}
return 0;
}
int Tun::up() {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
return tun->up();
}
int Tun::down() {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
return tun->down();
}
int Tun::read(std::string &buffer) {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
return tun->read(buffer);
}
int Tun::write(const std::string &buffer) {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(this->impl);
return tun->write(buffer);
}
int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
std::shared_ptr<WindowsTun> tun;
tun = std::any_cast<std::shared_ptr<WindowsTun>>(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 <condition_variable>
namespace candy {
namespace Utils {
template <typename T> class Atomic {
public:
explicit Atomic(T initial = T()) : value(initial) {}
T load() const {
std::lock_guard<std::mutex> lock(mutex);
return value;
}
void store(T new_value) {
std::lock_guard<std::mutex> lock(mutex);
value = new_value;
cv.notify_all();
}
void wait(const T &expected) {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [this, &expected] { return value != expected; });
}
template <typename Predicate> void wait_until(Predicate pred) {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, pred);
}
void notify_one() {
std::lock_guard<std::mutex> lock(mutex);
cv.notify_one();
}
void notify_all() {
std::lock_guard<std::mutex> 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 <Poco/Platform.h>
#if POCO_OS == POCO_OS_WINDOWS_NT
#include "utils/codecvt.h"
#include <windows.h>
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 <string>
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 <iostream>
#include <random>
#include <sstream>
namespace {
int randomHex() {
std::random_device device;
std::mt19937 engine(device());
std::uniform_int_distribution<int> distrib(0, 15);
return distrib(engine);
}
} // namespace
namespace candy {
uint32_t randomUint32() {
std::random_device device;
std::mt19937 engine(device());
std::uniform_int_distribution<uint32_t> 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 <cstdint>
#include <string>
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 <Poco/Net/DatagramSocket.h>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <spdlog/fmt/bin_to_hex.h>
#include <spdlog/spdlog.h>
#include <sstream>
#include <string>
#include <unistd.h>
namespace candy {
int64_t unixTime() {
using namespace std::chrono;
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
}
int64_t bootTime() {
using namespace std::chrono;
auto now = steady_clock::now();
return duration_cast<milliseconds>(now.time_since_epoch()).count();
}
std::string getCurrentTimeWithMillis() {
auto now = std::chrono::system_clock::now();
auto ms_tp = std::chrono::time_point_cast<std::chrono::milliseconds>(now);
auto epoch = ms_tp.time_since_epoch();
auto value = std::chrono::duration_cast<std::chrono::milliseconds>(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 <cstdint>
#include <string>
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 <Poco/Net/HTTPMessage.h>
#include <Poco/Net/HTTPRequest.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/Net/HTTPSClientSession.h>
#include <Poco/Timespan.h>
#include <Poco/URI.h>
#include <memory>
#include <spdlog/fmt/bin_to_hex.h>
#include <spdlog/spdlog.h>
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<int>(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<Poco::URI> uri;
try {
uri = std::make_shared<Poco::URI>(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<Poco::Net::WebSocket>(cs, request, response);
} else if (uri->getScheme() == "ws") {
Poco::Net::HTTPClientSession cs(uri->getHost(), uri->getPort());
this->ws = std::make_shared<Poco::Net::WebSocket>(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 <Poco/Net/WebSocket.h>
#include <functional>
#include <memory>
#include <string>
#include <thread>
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<int(const std::string &)> 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<int(const std::string &)> addressUpdateCallback;
private:
std::string hostName();
void sendPingMessage();
private:
int connect();
int disconnect();
std::shared_ptr<Poco::Net::WebSocket> 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 <openssl/sha.h>
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 <Poco/Net/HTTPRequestHandler.h>
#include <Poco/Net/HTTPRequestHandlerFactory.h>
#include <Poco/Net/HTTPServerRequest.h>
#include <Poco/Net/HTTPServerResponse.h>
#include <Poco/Net/ServerSocket.h>
#include <Poco/Net/WebSocket.h>
#include <Poco/Timespan.h>
#include <Poco/URI.h>
#include <exception>
#include <functional>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <spdlog/fmt/bin_to_hex.h>
#include <spdlog/spdlog.h>
#include <sstream>
/**
* Poco 的 WebSocket 服务端接口有点难用,简单封装一下,并对外提供一个回调函数,回调函数的参数表示独立的
* WebSocket客户端,函数返回会释放连接
*/
namespace {
using WebSocketHandler = std::function<void(Poco::Net::WebSocket &ws)>;
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<Poco::Net::HTTPServer>(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 <Poco/Net/HTTPServer.h>
#include <Poco/Net/WebSocket.h>
#include <list>
#include <memory>
#include <shared_mutex>
#include <string>
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<SysRoute> 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<IP4, WsCtx *> ipCtxMap;
// 操作 map 时需要加锁,以确保操作时指针有效
std::shared_mutex ipCtxMutex;
bool running;
private:
// 开始监听,新的请求将调用 handleWebsocket
int listen();
// 同步的处理每个客户独的请求,函数返回后连接将断开
void handleWebsocket(Poco::Net::WebSocket &ws);
std::shared_ptr<Poco::Net::HTTPServer> 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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
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 <http://opensource.org/licenses/MIT>.
SPDX-License-Identifier: MIT
Copyright (c) 2019-2022 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>
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 <cerrno>
#ifndef ARGPARSE_MODULE_USE_STD_MODULE
#include <algorithm>
#include <any>
#include <array>
#include <charconv>
#include <cstdlib>
#include <functional>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <limits>
#include <list>
#include <map>
#include <numeric>
#include <optional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#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 <typename T, typename = void> struct HasContainerTraits : std::false_type {};
template <> struct HasContainerTraits<std::string> : std::false_type {};
template <> struct HasContainerTraits<std::string_view> : std::false_type {};
template <typename T>
struct HasContainerTraits<T, std::void_t<typename T::value_type, decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end()), decltype(std::declval<T>().size())>>
: std::true_type {};
template <typename T> inline constexpr bool IsContainer = HasContainerTraits<T>::value;
template <typename T, typename = void> struct HasStreamableTraits : std::false_type {};
template <typename T>
struct HasStreamableTraits<T, std::void_t<decltype(std::declval<std::ostream &>() << std::declval<T>())>> : std::true_type {};
template <typename T> inline constexpr bool IsStreamable = HasStreamableTraits<T>::value;
constexpr std::size_t repr_max_container_size = 5;
template <typename T> std::string repr(T const &val) {
if constexpr (std::is_same_v<T, bool>) {
return val ? "true" : "false";
} else if constexpr (std::is_convertible_v<T, std::string_view>) {
return '"' + std::string{std::string_view{val}} + '"';
} else if constexpr (IsContainer<T>) {
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<typename T::iterator::difference_type>(
std::min<std::size_t>(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<T>) {
std::stringstream out;
out << val;
return out.str();
} else {
return "<not representable>";
}
}
namespace {
template <typename T> constexpr bool standard_signed_integer = false;
template <> constexpr bool standard_signed_integer<signed char> = true;
template <> constexpr bool standard_signed_integer<short int> = true;
template <> constexpr bool standard_signed_integer<int> = true;
template <> constexpr bool standard_signed_integer<long int> = true;
template <> constexpr bool standard_signed_integer<long long int> = true;
template <typename T> constexpr bool standard_unsigned_integer = false;
template <> constexpr bool standard_unsigned_integer<unsigned char> = true;
template <> constexpr bool standard_unsigned_integer<unsigned short int> = true;
template <> constexpr bool standard_unsigned_integer<unsigned int> = true;
template <> constexpr bool standard_unsigned_integer<unsigned long int> = true;
template <> constexpr bool standard_unsigned_integer<unsigned long long int> = true;
} // namespace
constexpr int radix_2 = 2;
constexpr int radix_8 = 8;
constexpr int radix_10 = 10;
constexpr int radix_16 = 16;
template <typename T> constexpr bool standard_integer = standard_signed_integer<T> || standard_unsigned_integer<T>;
template <class F, class Tuple, class Extra, std::size_t... I>
constexpr decltype(auto) apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, std::index_sequence<I...> /*unused*/) {
return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))..., std::forward<Extra>(x));
}
template <class F, class Tuple, class Extra> constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) {
return details::apply_plus_one_impl(std::forward<F>(f), std::forward<Tuple>(t), std::forward<Extra>(x),
std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}
constexpr auto pointer_range(std::string_view s) noexcept {
return std::tuple(s.data(), s.data() + s.size());
}
template <class CharT, class Traits>
constexpr bool starts_with(std::basic_string_view<CharT, Traits> prefix, std::basic_string_view<CharT, Traits> 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 <class T, auto Param> 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 <class T, auto Param = 0> struct parse_number {
auto operator()(std::string_view s) -> T {
return do_from_chars<T, Param>(s);
}
};
template <class T> struct parse_number<T, radix_2> {
auto operator()(std::string_view s) -> T {
if (auto [ok, rest] = consume_binary_prefix(s); ok) {
return do_from_chars<T, radix_2>(rest);
}
throw std::invalid_argument{"pattern not found"};
}
};
template <class T> struct parse_number<T, radix_16> {
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<T, radix_16>(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<T, radix_16>(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 <class T> struct parse_number<T> {
auto operator()(std::string_view s) -> T {
auto [ok, rest] = consume_hex_prefix(s);
if (ok) {
try {
return do_from_chars<T, radix_16>(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<T, radix_2>(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<T, radix_8>(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<T, radix_10>(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 <class T> inline const auto generic_strtod = nullptr;
template <> inline const auto generic_strtod<float> = ARGPARSE_CUSTOM_STRTOF;
template <> inline const auto generic_strtod<double> = ARGPARSE_CUSTOM_STRTOD;
template <> inline const auto generic_strtod<long double> = ARGPARSE_CUSTOM_STRTOLD;
} // namespace
template <class T> inline auto do_strtod(std::string const &s) -> T {
if (isspace(static_cast<unsigned char>(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<T>(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 <class T> struct parse_number<T, chars_format::general> {
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<T>(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 <class T> struct parse_number<T, chars_format::hex> {
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<T>(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 <class T> struct parse_number<T, chars_format::binary> {
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<T>(s);
}
};
template <class T> struct parse_number<T, chars_format::scientific> {
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<T>(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 <class T> struct parse_number<T, chars_format::fixed> {
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<T>(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 <typename StrIt> 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 <typename T> struct can_invoke_to_string {
template <typename U> static auto test(int) -> decltype(std::to_string(std::declval<U>()), std::true_type{});
template <typename U> static auto test(...) -> std::false_type;
static constexpr bool value = decltype(test<T>(0))::value;
};
template <typename T> struct IsChoiceTypeSupported {
using CleanType = typename std::decay<T>::type;
static const bool value = std::is_integral<CleanType>::value || std::is_same<CleanType, std::string>::value ||
std::is_same<CleanType, std::string_view>::value || std::is_same<CleanType, const char *>::value;
};
template <typename StringType> std::size_t get_levenshtein_distance(const StringType &s1, const StringType &s2) {
std::vector<std::vector<std::size_t>> dp(s1.size() + 1, std::vector<std::size_t>(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 <typename ValueType>
std::string get_most_similar_string(const std::map<std::string, ValueType> &map, const std::string &input) {
std::string most_similar{};
std::size_t min_distance = std::numeric_limits<std::size_t>::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<default_arguments>(static_cast<std::underlying_type<default_arguments>::type>(a) &
static_cast<std::underlying_type<default_arguments>::type>(b));
}
class ArgumentParser;
class Argument {
friend class ArgumentParser;
friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) -> std::ostream &;
template <std::size_t N, std::size_t... I>
explicit Argument(std::string_view prefix_chars, std::array<std::string_view, N> &&a, std::index_sequence<I...> /*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 <std::size_t N>
explicit Argument(std::string_view prefix_chars, std::array<std::string_view, N> &&a)
: Argument(prefix_chars, std::move(a), std::make_index_sequence<N>{}) {}
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 <typename T> 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<T, std::string_view>) {
m_default_value_str = std::string{std::string_view{value}};
} else if constexpr (details::can_invoke_to_string<T>::value) {
m_default_value_str = std::to_string(value);
}
m_default_value = std::forward<T>(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 <class F, class... Args>
auto action(F &&callable,
Args &&...bound_args) -> std::enable_if_t<std::is_invocable_v<F, Args..., std::string const>, Argument &> {
using action_type =
std::conditional_t<std::is_void_v<std::invoke_result_t<F, Args..., std::string const>>, void_action, valued_action>;
if constexpr (sizeof...(Args) == 0) {
m_action.emplace<action_type>(std::forward<F>(callable));
} else {
m_action.emplace<action_type>(
[f = std::forward<F>(callable), tup = std::make_tuple(std::forward<Args>(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<bool>(m_default_value);
}
action([&var](const auto & /*unused*/) { var = true; });
return *this;
}
template <typename T, typename std::enable_if<std::is_integral<T>::value>::type * = nullptr> auto &store_into(T &var) {
if (m_default_value.has_value()) {
var = std::any_cast<T>(m_default_value);
}
action([&var](const auto &s) { var = details::parse_number<T, details::radix_10>()(s); });
return *this;
}
auto &store_into(double &var) {
if (m_default_value.has_value()) {
var = std::any_cast<double>(m_default_value);
}
action([&var](const auto &s) { var = details::parse_number<double, details::chars_format::general>()(s); });
return *this;
}
auto &store_into(std::string &var) {
if (m_default_value.has_value()) {
var = std::any_cast<std::string>(m_default_value);
}
action([&var](const std::string &s) { var = s; });
return *this;
}
auto &store_into(std::vector<std::string> &var) {
if (m_default_value.has_value()) {
var = std::any_cast<std::vector<std::string>>(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<int> &var) {
if (m_default_value.has_value()) {
var = std::any_cast<std::vector<int>>(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<int, details::radix_10>()(s));
});
return *this;
}
auto &store_into(std::set<std::string> &var) {
if (m_default_value.has_value()) {
var = std::any_cast<std::set<std::string>>(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<int> &var) {
if (m_default_value.has_value()) {
var = std::any_cast<std::set<int>>(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<int, details::radix_10>()(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 <char Shape, typename T> auto scan() -> std::enable_if_t<std::is_arithmetic_v<T>, Argument &> {
static_assert(!(std::is_const_v<T> || std::is_volatile_v<T>), "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<T>) {
action(details::parse_number<T, details::radix_10>());
} else if constexpr (is_one_of(Shape, 'i') && details::standard_integer<T>) {
action(details::parse_number<T>());
} else if constexpr (is_one_of(Shape, 'u') && details::standard_unsigned_integer<T>) {
action(details::parse_number<T, details::radix_10>());
} else if constexpr (is_one_of(Shape, 'b') && details::standard_unsigned_integer<T>) {
action(details::parse_number<T, details::radix_2>());
} else if constexpr (is_one_of(Shape, 'o') && details::standard_unsigned_integer<T>) {
action(details::parse_number<T, details::radix_8>());
} else if constexpr (is_one_of(Shape, 'x', 'X') && details::standard_unsigned_integer<T>) {
action(details::parse_number<T, details::radix_16>());
} else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_v<T>) {
action(details::parse_number<T, details::chars_format::hex>());
} else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_v<T>) {
action(details::parse_number<T, details::chars_format::scientific>());
} else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_v<T>) {
action(details::parse_number<T, details::chars_format::fixed>());
} else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_v<T>) {
action(details::parse_number<T, details::chars_format::general>());
} 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<std::size_t>::max)()};
break;
case nargs_pattern::at_least_one:
m_num_args_range = NArgsRange{1, (std::numeric_limits<std::size_t>::max)()};
break;
}
return *this;
}
Argument &remaining() {
m_accepts_optional_like_value = true;
return nargs(nargs_pattern::any);
}
template <typename T> void add_choice(T &&choice) {
static_assert(details::IsChoiceTypeSupported<T>::value, "Only string or integer type supported for choice");
static_assert(std::is_convertible_v<T, std::string_view> || details::can_invoke_to_string<T>::value,
"Choice is not convertible to string_type");
if (!m_choices.has_value()) {
m_choices = std::vector<std::string>{};
}
if constexpr (std::is_convertible_v<T, std::string_view>) {
m_choices.value().push_back(std::string{std::string_view{std::forward<T>(choice)}});
} else if constexpr (details::can_invoke_to_string<T>::value) {
m_choices.value().push_back(std::to_string(std::forward<T>(choice)));
}
}
Argument &choices() {
if (!m_choices.has_value()) {
throw std::runtime_error("Zero choices provided");
}
return *this;
}
template <typename T, typename... U> Argument &choices(T &&first, U &&...rest) {
add_choice(std::forward<T>(first));
choices(std::forward<U>(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 <typename Iterator> 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 <typename Iterator>
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::size_t>(std::distance(start, end))) >= num_args_min) {
if (num_args_max < dist) {
end = std::next(start, static_cast<typename Iterator::difference_type>(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::size_t>(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::size_t>(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()) {
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
SYMBOL INDEX (215 symbols across 44 files)
FILE: candy-cli/src/argparse.h
function namespace (line 74) | namespace argparse {
function chars_format (line 179) | enum class chars_format { scientific = 0xf1, fixed = 0xf2, hex = 0xf4, b...
function throw (line 267) | throw std::invalid_argument{"pattern '" + std::string(s) + "' not identi...
function throw (line 326) | throw std::invalid_argument{"pattern '" + s + "' not found"};
function throw (line 338) | throw std::invalid_argument{"pattern '" + s + "' does not match to the e...
function throw (line 341) | throw std::range_error{"'" + s + "' not representable"};
function throw (line 349) | throw std::invalid_argument{"chars_format::general does not parse hexflo...
function throw (line 352) | throw std::invalid_argument{"chars_format::general does not parse binflo...
function throw (line 368) | throw std::invalid_argument{"chars_format::hex parses hexfloat"};
function throw (line 371) | throw std::invalid_argument{"chars_format::hex does not parse binfloat"};
function throw (line 387) | throw std::invalid_argument{"chars_format::binary does not parse hexfloa...
function throw (line 390) | throw std::invalid_argument{"chars_format::binary parses binfloat"};
function throw (line 400) | throw std::invalid_argument{"chars_format::scientific does not parse hex...
function throw (line 403) | throw std::invalid_argument{"chars_format::scientific does not parse bin...
function throw (line 406) | throw std::invalid_argument{"chars_format::scientific requires exponent ...
function throw (line 422) | throw std::invalid_argument{"chars_format::fixed does not parse hexfloat"};
function throw (line 425) | throw std::invalid_argument{"chars_format::fixed does not parse binfloat"};
function throw (line 428) | throw std::invalid_argument{"chars_format::fixed does not parse exponent...
function string (line 441) | string join(StrIt first, StrIt last, const std::string &separator) {
function get_levenshtein_distance (line 469) | size_t get_levenshtein_distance(const StringType &s1, const StringType &...
function string (line 490) | string get_most_similar_string(const std::map<std::string, ValueType> &m...
function else (line 558) | else if constexpr (details::can_invoke_to_string<T>::value) {
function string (line 593) | string const>, Argument &> {
function else (line 714) | else if constexpr (is_one_of(Shape, 'u') && details::standard_unsigned_i...
function else (line 716) | else if constexpr (is_one_of(Shape, 'b') && details::standard_unsigned_i...
function else (line 718) | else if constexpr (is_one_of(Shape, 'o') && details::standard_unsigned_i...
function else (line 720) | else if constexpr (is_one_of(Shape, 'x', 'X') && details::standard_unsig...
function else (line 722) | else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_...
function else (line 724) | else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_...
function else (line 726) | else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_...
function else (line 728) | else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_...
function throw (line 808) | throw std::runtime_error(std::string{"Invalid default value "} + m_defau...
function throw (line 826) | throw std::runtime_error(std::string{"Invalid argument "} + details::rep...
type ActionApply (line 879) | struct ActionApply {
function pos (line 1033) | auto pos = std::string::size_type{}
function prev (line 1034) | auto prev = std::string::size_type{}
function else (line 1070) | else if (argument.m_is_required) {
function is_positional (line 1110) | static bool is_positional(std::string_view name, std::string_view prefix...
function contains (line 1138) | bool contains(std::size_t value) const {
function is_optional (line 1361) | static bool is_optional(std::string_view name, std::string_view prefix_c...
function set_usage_newline_counter (line 1416) | void set_usage_newline_counter(int i) {
function set_group_idx (line 1420) | void set_group_idx(std::size_t i) {
function NArgsRange (line 1438) | NArgsRange m_num_args_range{1, 1};
function class (line 1451) | class ArgumentParser {
function explicit (line 1499) | explicit operator bool() const {
function class (line 1523) | class MutuallyExclusiveGroup {
function deal_with_options_of_group (line 1893) | const auto deal_with_options_of_group = [&](std::size_t group_idx) {
function index_argument (line 2326) | void index_argument(argument_it it) {
FILE: candy-cli/src/config.cc
function saveTunAddress (line 177) | int saveTunAddress(const std::string &name, const std::string &cidr) {
function loadTunAddress (line 194) | std::string loadTunAddress(const std::string &name) {
function virtualMacHelper (line 207) | std::string virtualMacHelper(std::string name = "") {
function initVirtualMac (line 230) | std::string initVirtualMac() {
function virtualMac (line 247) | std::string virtualMac(const std::string &name) {
function starts_with (line 263) | bool starts_with(const std::string &str, const std::string &prefix) {
function storageDirectory (line 268) | std::string storageDirectory(std::string subdir) {
function storageDirectory (line 272) | std::string storageDirectory(std::string subdir) {
FILE: candy-cli/src/config.h
type arguments (line 9) | struct arguments {
FILE: candy-cli/src/main.cc
function main (line 6) | int main(int argc, char *argv[]) {
FILE: candy-service/src/main.cc
class BaseJSONHandler (line 31) | class BaseJSONHandler : public Poco::Net::HTTPRequestHandler {
method readRequest (line 33) | Poco::JSON::Object::Ptr readRequest(Poco::Net::HTTPServerRequest &requ...
method sendResponse (line 39) | void sendResponse(Poco::Net::HTTPServerResponse &response, const Poco:...
class RunHandler (line 46) | class RunHandler : public BaseJSONHandler {
method handleRequest (line 48) | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::H...
class StatusHandler (line 73) | class StatusHandler : public BaseJSONHandler {
method handleRequest (line 75) | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::H...
class ShutdownHandler (line 101) | class ShutdownHandler : public BaseJSONHandler {
method handleRequest (line 103) | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::H...
class JSONRequestHandlerFactory (line 127) | class JSONRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFa...
class CandyServiceApp (line 144) | class CandyServiceApp : public Poco::Util::ServerApplication {
method initialize (line 152) | void initialize(Poco::Util::Application &self) override {
method defineOptions (line 157) | void defineOptions(Poco::Util::OptionSet &options) override {
method handleHelp (line 182) | void handleHelp(const std::string &name, const std::string &value) {
method handleBind (line 188) | void handleBind(const std::string &name, const std::string &value) {
method handleLogDir (line 204) | void handleLogDir(const std::string &name, const std::string &dir) {
method handleLogLevel (line 208) | void handleLogLevel(const std::string &name, const std::string &level) {
method displayHelp (line 212) | void displayHelp() {
method main (line 218) | int main(const std::vector<std::string> &args) override {
function main (line 277) | int main(int argc, char **argv) {
FILE: candy/include/candy/client.h
function namespace (line 9) | namespace candy {
FILE: candy/include/candy/common.h
function namespace (line 7) | namespace candy {
FILE: candy/include/candy/server.h
function namespace (line 8) | namespace candy {
FILE: candy/src/candy/client.cc
type candy (line 14) | namespace candy {
type client (line 15) | namespace client {
class Instance (line 20) | class Instance {
method is_running (line 22) | bool is_running() {
method exit (line 26) | void exit() {
method status (line 33) | Poco::JSON::Object status() {
method create_client (line 41) | std::shared_ptr<Client> create_client() {
function try_create_instance (line 55) | std::optional<std::shared_ptr<Instance>> try_create_instance(const s...
function try_erase_instance (line 67) | bool try_erase_instance(const std::string &id) {
function run (line 74) | bool run(const std::string &id, const Poco::JSON::Object &config) {
function shutdown (line 108) | bool shutdown(const std::string &id) {
function status (line 121) | std::optional<Poco::JSON::Object> status(const std::string &id) {
FILE: candy/src/candy/server.cc
type candy (line 6) | namespace candy {
type server (line 7) | namespace server {
function run (line 14) | bool run(const Poco::JSON::Object &config) {
function shutdown (line 27) | bool shutdown() {
FILE: candy/src/core/client.cc
type candy (line 7) | namespace candy {
function Msg (line 9) | Msg MsgQueue::read() {
function IP4 (line 49) | IP4 Client::address() {
function MsgQueue (line 53) | MsgQueue &Client::getTunMsgQueue() {
function MsgQueue (line 57) | MsgQueue &Client::getPeerMsgQueue() {
function MsgQueue (line 61) | MsgQueue &Client::getWsMsgQueue() {
FILE: candy/src/core/client.h
function namespace (line 15) | namespace candy {
FILE: candy/src/core/common.cc
type candy (line 6) | namespace candy {
function version (line 8) | std::string version() {
function create_vmac (line 12) | std::string create_vmac() {
FILE: candy/src/core/message.cc
type candy (line 4) | namespace candy {
function Msg (line 16) | Msg &Msg::operator=(Msg &&packet) {
FILE: candy/src/core/message.h
function MsgKind (line 11) | enum class MsgKind {
FILE: candy/src/core/net.cc
type candy (line 7) | namespace candy {
function IP4 (line 13) | IP4 IP4::operator=(const std::string &ip) {
function IP4 (line 28) | IP4 IP4::operator|(IP4 another) const {
function IP4 (line 35) | IP4 IP4::operator^(IP4 another) const {
function IP4 (line 42) | IP4 IP4::operator~() const {
function IP4 (line 54) | IP4 IP4::operator&(IP4 another) const {
function IP4 (line 61) | IP4 IP4::next() const {
function IP4 (line 119) | IP4 &Address::Host() {
function IP4 (line 123) | IP4 &Address::Mask() {
function IP4 (line 127) | IP4 Address::Net() {
function Address (line 131) | Address Address::Next() {
FILE: candy/src/core/net.h
function namespace (line 11) | namespace candy {
function class (line 48) | class __attribute__((packed)) IP4 {
function namespace (line 123) | namespace std {
FILE: candy/src/core/server.cc
type candy (line 4) | namespace candy {
FILE: candy/src/core/server.h
function namespace (line 9) | namespace candy {
FILE: candy/src/peer/manager.cc
type candy (line 16) | namespace candy {
function IP4 (line 196) | IP4 PeerManager::getTunIp() {
function Client (line 642) | Client &PeerManager::getClient() {
FILE: candy/src/peer/manager.h
function namespace (line 19) | namespace candy {
type PeerRouteEntry (line 53) | struct PeerRouteEntry {
FILE: candy/src/peer/message.cc
type candy (line 5) | namespace candy {
type PeerMsg (line 7) | namespace PeerMsg {
FILE: candy/src/peer/message.h
function namespace (line 9) | namespace candy {
FILE: candy/src/peer/peer.cc
function isLocalNetwork (line 20) | bool isLocalNetwork(const SocketAddress &addr) {
type candy (line 34) | namespace candy {
function PeerManager (line 56) | PeerManager &Peer::getManager() {
FILE: candy/src/peer/peer.h
function PeerState (line 27) | enum class PeerState {
FILE: candy/src/tun/linux.cc
type candy (line 18) | namespace candy {
class LinuxTun (line 20) | class LinuxTun {
method setName (line 22) | int setName(const std::string &name) {
method setIP (line 27) | int setIP(IP4 ip) {
method IP4 (line 32) | IP4 getIP() {
method setMask (line 36) | int setMask(IP4 mask) {
method setMTU (line 41) | int setMTU(int mtu) {
method up (line 47) | int up() {
method down (line 136) | int down() {
method read (line 141) | int read(std::string &buffer) {
method write (line 163) | int write(const std::string &buffer) {
method setSysRtTable (line 167) | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
function IP4 (line 249) | IP4 Tun::getIP() {
type candy (line 212) | namespace candy {
class LinuxTun (line 20) | class LinuxTun {
method setName (line 22) | int setName(const std::string &name) {
method setIP (line 27) | int setIP(IP4 ip) {
method IP4 (line 32) | IP4 getIP() {
method setMask (line 36) | int setMask(IP4 mask) {
method setMTU (line 41) | int setMTU(int mtu) {
method up (line 47) | int up() {
method down (line 136) | int down() {
method read (line 141) | int read(std::string &buffer) {
method write (line 163) | int write(const std::string &buffer) {
method setSysRtTable (line 167) | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
function IP4 (line 249) | IP4 Tun::getIP() {
FILE: candy/src/tun/macos.cc
type candy (line 27) | namespace candy {
class MacTun (line 29) | class MacTun {
method setName (line 31) | int setName(const std::string &name) {
method setIP (line 36) | int setIP(IP4 ip) {
method IP4 (line 41) | IP4 getIP() {
method setMask (line 45) | int setMask(IP4 mask) {
method setMTU (line 50) | int setMTU(int mtu) {
method up (line 55) | int up() {
method down (line 178) | int down() {
method read (line 183) | int read(std::string &buffer) {
method write (line 212) | int write(const std::string &buffer) {
method setSysRtTable (line 221) | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
function IP4 (line 305) | IP4 Tun::getIP() {
type candy (line 269) | namespace candy {
class MacTun (line 29) | class MacTun {
method setName (line 31) | int setName(const std::string &name) {
method setIP (line 36) | int setIP(IP4 ip) {
method IP4 (line 41) | IP4 getIP() {
method setMask (line 45) | int setMask(IP4 mask) {
method setMTU (line 50) | int setMTU(int mtu) {
method up (line 55) | int up() {
method down (line 178) | int down() {
method read (line 183) | int read(std::string &buffer) {
method write (line 212) | int write(const std::string &buffer) {
method setSysRtTable (line 221) | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
function IP4 (line 305) | IP4 Tun::getIP() {
FILE: candy/src/tun/tun.cc
type candy (line 10) | namespace candy {
function Client (line 164) | Client &Tun::getClient() {
FILE: candy/src/tun/tun.h
function namespace (line 13) | namespace candy {
FILE: candy/src/tun/unknown.cc
type candy (line 8) | namespace candy {
FILE: candy/src/tun/windows.cc
type candy (line 32) | namespace candy {
class Holder (line 49) | class Holder {
method Ok (line 51) | static bool Ok() {
method Holder (line 57) | Holder() {
class WindowsTun (line 91) | class WindowsTun {
method setName (line 93) | int setName(const std::string &name) {
method setIP (line 98) | int setIP(IP4 ip) {
method IP4 (line 103) | IP4 getIP() {
method setPrefix (line 107) | int setPrefix(uint32_t prefix) {
method setMTU (line 112) | int setMTU(int mtu) {
method up (line 117) | int up() {
method down (line 172) | int down() {
method read (line 189) | int read(std::string &buffer) {
method write (line 207) | int write(const std::string &buffer) {
method setSysRtTable (line 223) | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
function IP4 (line 302) | IP4 Tun::getIP() {
type candy (line 266) | namespace candy {
class Holder (line 49) | class Holder {
method Ok (line 51) | static bool Ok() {
method Holder (line 57) | Holder() {
class WindowsTun (line 91) | class WindowsTun {
method setName (line 93) | int setName(const std::string &name) {
method setIP (line 98) | int setIP(IP4 ip) {
method IP4 (line 103) | IP4 getIP() {
method setPrefix (line 107) | int setPrefix(uint32_t prefix) {
method setMTU (line 112) | int setMTU(int mtu) {
method up (line 117) | int up() {
method down (line 172) | int down() {
method read (line 189) | int read(std::string &buffer) {
method write (line 207) | int write(const std::string &buffer) {
method setSysRtTable (line 223) | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) {
function IP4 (line 302) | IP4 Tun::getIP() {
FILE: candy/src/utils/atomic.h
function namespace (line 7) | namespace candy {
FILE: candy/src/utils/codecvt.cc
type candy (line 6) | namespace candy {
function UTF16ToUTF8 (line 8) | std::string UTF16ToUTF8(const std::wstring &utf16Str) {
function UTF8ToUTF16 (line 25) | std::wstring UTF8ToUTF16(const std::string &utf8Str) {
FILE: candy/src/utils/codecvt.h
function namespace (line 7) | namespace candy {
FILE: candy/src/utils/random.cc
function randomHex (line 9) | int randomHex() {
type candy (line 17) | namespace candy {
function randomUint32 (line 19) | uint32_t randomUint32() {
function randomHexString (line 26) | std::string randomHexString(int length) {
FILE: candy/src/utils/random.h
function namespace (line 8) | namespace candy {
FILE: candy/src/utils/time.cc
type candy (line 15) | namespace candy {
function unixTime (line 17) | int64_t unixTime() {
function bootTime (line 22) | int64_t bootTime() {
function getCurrentTimeWithMillis (line 28) | std::string getCurrentTimeWithMillis() {
FILE: candy/src/utils/time.h
function namespace (line 8) | namespace candy {
FILE: candy/src/websocket/client.cc
type candy (line 19) | namespace candy {
function Client (line 421) | Client &WebSocketClient::getClient() {
FILE: candy/src/websocket/client.h
function namespace (line 13) | namespace candy {
FILE: candy/src/websocket/message.cc
type candy (line 5) | namespace candy {
type WsMsg (line 6) | namespace WsMsg {
FILE: candy/src/websocket/message.h
function namespace (line 8) | namespace candy {
FILE: candy/src/websocket/server.cc
class HTTPRequestHandler (line 31) | class HTTPRequestHandler : public Poco::Net::HTTPRequestHandler {
method HTTPRequestHandler (line 33) | HTTPRequestHandler(WebSocketHandler wsHandler) : wsHandler(wsHandler) {}
method handleRequest (line 34) | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::H...
class HTTPRequestHandlerFactory (line 51) | class HTTPRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFa...
method HTTPRequestHandlerFactory (line 53) | HTTPRequestHandlerFactory(WebSocketHandler wsHandler) : wsHandler(wsHa...
type candy (line 65) | namespace candy {
FILE: candy/src/websocket/server.h
function namespace (line 13) | namespace candy {
Condensed preview — 86 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (320K chars).
[
{
"path": ".clang-format",
"chars": 258,
"preview": "---\nBasedOnStyle: LLVM\nIndentWidth: 4\nTabWidth: 8\nAccessModifierOffset: -4\nAllowShortFunctionsOnASingleLine: Empty\nAllow"
},
{
"path": ".dockerignore",
"chars": 30,
"preview": ".git\n.github\n.vscode\n\nbuild/*\n"
},
{
"path": ".github/workflows/check.yaml",
"chars": 1680,
"preview": "name: check\n\non:\n pull_request:\n branches: [master]\n\njobs:\n format:\n runs-on: ubuntu-latest\n steps:\n - n"
},
{
"path": ".github/workflows/release.yaml",
"chars": 3159,
"preview": "name: release\n\non:\n push:\n branches: [ master ]\n release:\n types: [ published ]\n\njobs:\n docker:\n runs-on: ub"
},
{
"path": ".github/workflows/standalone.yaml",
"chars": 2193,
"preview": "name: standalone\n\non:\n workflow_dispatch:\n release:\n types: [ published ]\n pull_request:\n branches: [master]\n "
},
{
"path": ".gitignore",
"chars": 304,
"preview": "# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic"
},
{
"path": ".vscode/c_cpp_properties.json",
"chars": 340,
"preview": "{\n \"configurations\": [\n {\n \"name\": \"Linux\",\n \"includePath\": [\n \"${workspa"
},
{
"path": ".vscode/extensions.json",
"chars": 79,
"preview": "{\n \"recommendations\": [\n \"ms-vscode.cpptools-extension-pack\"\n ]\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 966,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"(gdb) Launch\",\n \"type\": \"cpp"
},
{
"path": ".vscode/settings.json",
"chars": 377,
"preview": "{\n \"editor.detectIndentation\": false,\n \"editor.tabSize\": 4,\n \"editor.formatOnSave\": true,\n \"editor.insertSpa"
},
{
"path": ".vscode/tasks.json",
"chars": 714,
"preview": "{\n \"tasks\": [\n {\n \"type\": \"cppbuild\",\n \"label\": \"C/C++: g++ build active file\",\n "
},
{
"path": "CMakeLists.txt",
"chars": 3265,
"preview": "cmake_minimum_required(VERSION 3.16)\nproject(Candy VERSION 6.1.7)\n\nset(CMAKE_CXX_STANDARD 17)\nset(CMAKE_CXX_STANDARD_REQ"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2023 lanthora\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 1678,
"preview": "# Candy\n\n<p>\n<a href=\"https://github.com/lanthora/candy/releases/latest\"><img src=\"https://img.shields.io/github/release"
},
{
"path": "candy/.vscode/c_cpp_properties.json",
"chars": 340,
"preview": "{\n \"configurations\": [\n {\n \"name\": \"Linux\",\n \"includePath\": [\n \"${workspa"
},
{
"path": "candy/.vscode/settings.json",
"chars": 377,
"preview": "{\n \"editor.detectIndentation\": false,\n \"editor.tabSize\": 4,\n \"editor.formatOnSave\": true,\n \"editor.insertSpa"
},
{
"path": "candy/CMakeLists.txt",
"chars": 1957,
"preview": "add_library(candy-library)\n\nfile(GLOB_RECURSE SOURCES \"src/*.cc\")\ntarget_sources(candy-library PRIVATE ${SOURCES})\n\ntarg"
},
{
"path": "candy/include/candy/candy.h",
"chars": 145,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CANDY_H\n#define CANDY_CANDY_H\n\n#include \"client.h\"\n#include \"common.h\"\n#in"
},
{
"path": "candy/include/candy/client.h",
"chars": 408,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CLIENT_H\n#define CANDY_CLIENT_H\n\n#include <Poco/JSON/Object.h>\n#include <o"
},
{
"path": "candy/include/candy/common.h",
"chars": 229,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_COMMON_H\n#define CANDY_COMMON_H\n\n#include <string>\n\nnamespace candy {\nstat"
},
{
"path": "candy/include/candy/server.h",
"chars": 279,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_SERVER_H\n#define CANDY_SERVER_H\n\n#include <Poco/JSON/Object.h>\n#include <s"
},
{
"path": "candy/src/candy/client.cc",
"chars": 3876,
"preview": "// SPDX-License-Identifier: MIT\n#include \"candy/client.h\"\n#include \"core/client.h\"\n#include \"utils/atomic.h\"\n#include <P"
},
{
"path": "candy/src/candy/server.cc",
"chars": 876,
"preview": "// SPDX-License-Identifier: MIT\n#include \"candy/server.h\"\n#include \"core/server.h\"\n#include \"utils/atomic.h\"\n\nnamespace "
},
{
"path": "candy/src/core/client.cc",
"chars": 2676,
"preview": "// SPDX-License-Identifier: MIT\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include <Poco/String.h>\n#include <ch"
},
{
"path": "candy/src/core/client.h",
"chars": 1539,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_CLIENT_H\n#define CANDY_CORE_CLIENT_H\n\n#include \"core/message.h\"\n#incl"
},
{
"path": "candy/src/core/common.cc",
"chars": 259,
"preview": "#include \"candy/common.h\"\n#include \"core/version.h\"\n#include \"utils/random.h\"\n#include <string>\n\nnamespace candy {\n\nstd:"
},
{
"path": "candy/src/core/message.cc",
"chars": 405,
"preview": "// SPDX-License-Identifier: MIT\n#include \"core/message.h\"\n\nnamespace candy {\n\nMsg::Msg(MsgKind kind, std::string data) {"
},
{
"path": "candy/src/core/message.h",
"chars": 709,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_MESSAGE_H\n#define CANDY_CORE_MESSAGE_H\n\n#include \"core/net.h\"\n#includ"
},
{
"path": "candy/src/core/net.cc",
"chars": 3206,
"preview": "// SPDX-License-Identifier: MIT\n#include \"core/net.h\"\n#include <Poco/Net/IPAddress.h>\n#include <cstring>\n#include <excep"
},
{
"path": "candy/src/core/net.h",
"chars": 2886,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_NET_H\n#define CANDY_CORE_NET_H\n\n#include <array>\n#include <cstdint>\n#"
},
{
"path": "candy/src/core/server.cc",
"chars": 579,
"preview": "// SPDX-License-Identifier: MIT\n#include \"core/server.h\"\n\nnamespace candy {\n\nvoid Server::setWebSocket(const std::string"
},
{
"path": "candy/src/core/server.h",
"chars": 628,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_SERVER_H\n#define CANDY_CORE_SERVER_H\n\n#include \"utils/atomic.h\"\n#incl"
},
{
"path": "candy/src/core/version.h",
"chars": 485,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CORE_VERSION_H\n#define CANDY_CORE_VERSION_H\n\n#include <Poco/Platform.h>\n\n#"
},
{
"path": "candy/src/peer/manager.cc",
"chars": 22199,
"preview": "// SPDX-License-Identifier: MIT\n#include \"peer/manager.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"c"
},
{
"path": "candy/src/peer/manager.h",
"chars": 3856,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_PEER_MANAGER_H\n#define CANDY_PEER_MANAGER_H\n\n#include \"core/message.h\"\n#in"
},
{
"path": "candy/src/peer/message.cc",
"chars": 321,
"preview": "// SPDX-License-Identifier: MIT\n#include \"peer/message.h\"\n#include <string>\n\nnamespace candy {\n\nnamespace PeerMsg {\nstd:"
},
{
"path": "candy/src/peer/message.h",
"chars": 1556,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_PEER_MESSAGE_H\n#define CANDY_PEER_MESSAGE_H\n\n#include \"core/net.h\"\n#includ"
},
{
"path": "candy/src/peer/peer.cc",
"chars": 10745,
"preview": "// SPDX-License-Identifier: MIT\n#include \"peer/peer.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"peer"
},
{
"path": "candy/src/peer/peer.h",
"chars": 1987,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_PEER_PEER_H\n#define CANDY_PEER_PEER_H\n\n#include \"core/net.h\"\n#include \"uti"
},
{
"path": "candy/src/tun/linux.cc",
"chars": 7580,
"preview": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_LINUX\n\n#include \"core/net.h\"\n#include "
},
{
"path": "candy/src/tun/macos.cc",
"chars": 10145,
"preview": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_MAC_OS_X\n\n#include \"core/net.h\"\n#inclu"
},
{
"path": "candy/src/tun/tun.cc",
"chars": 4015,
"preview": "// SPDX-License-Identifier: MIT\n#include \"tun/tun.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#include \"core/n"
},
{
"path": "candy/src/tun/tun.h",
"chars": 1175,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_TUN_TUN_H\n#define CANDY_TUN_TUN_H\n\n#include \"core/message.h\"\n#include \"cor"
},
{
"path": "candy/src/tun/unknown.cc",
"chars": 689,
"preview": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n\n#if POCO_OS != POCO_OS_LINUX && POCO_OS != POCO_OS_MAC_OS_X "
},
{
"path": "candy/src/tun/windows.cc",
"chars": 10222,
"preview": "// SPDX-License-Identifier: MIT\n#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_WINDOWS_NT\n\n#include \"core/net.h\"\n#inc"
},
{
"path": "candy/src/utils/atomic.h",
"chars": 1165,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_ATOMIC_H\n#define CANDY_UTILS_ATOMIC_H\n\n#include <condition_variable>"
},
{
"path": "candy/src/utils/codecvt.cc",
"chars": 1045,
"preview": "#include <Poco/Platform.h>\n#if POCO_OS == POCO_OS_WINDOWS_NT\n#include \"utils/codecvt.h\"\n#include <windows.h>\n\nnamespace "
},
{
"path": "candy/src/utils/codecvt.h",
"chars": 270,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_CODECVT_H\n#define CANDY_UTILS_CODECVT_H\n\n#include <string>\n\nnamespac"
},
{
"path": "candy/src/utils/random.cc",
"chars": 706,
"preview": "// SPDX-License-Identifier: MIT\n#include \"utils/random.h\"\n#include <iostream>\n#include <random>\n#include <sstream>\n\nname"
},
{
"path": "candy/src/utils/random.h",
"chars": 244,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_RANDOM_H\n#define CANDY_UTILS_RANDOM_H\n\n#include <cstdint>\n#include <"
},
{
"path": "candy/src/utils/time.cc",
"chars": 1253,
"preview": "// SPDX-License-Identifier: MIT\n#include \"utils/time.h\"\n#include \"core/net.h\"\n#include <Poco/Net/DatagramSocket.h>\n#incl"
},
{
"path": "candy/src/utils/time.h",
"chars": 255,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_UTILS_TIME_H\n#define CANDY_UTILS_TIME_H\n\n#include <cstdint>\n#include <stri"
},
{
"path": "candy/src/websocket/client.cc",
"chars": 13378,
"preview": "// SPDX-License-Identifier: MIT\n#include \"websocket/client.h\"\n#include \"core/client.h\"\n#include \"core/message.h\"\n#includ"
},
{
"path": "candy/src/websocket/client.h",
"chars": 2150,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_WEBSOCKET_CLIENT_H\n#define CANDY_WEBSOCKET_CLIENT_H\n\n#include \"core/messag"
},
{
"path": "candy/src/websocket/message.cc",
"chars": 3718,
"preview": "// SPDX-License-Identifier: MIT\n#include \"websocket/message.h\"\n#include \"utils/time.h\"\n\nnamespace candy {\nnamespace WsMs"
},
{
"path": "candy/src/websocket/message.h",
"chars": 2132,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_WEBSOCKET_MESSAGE_H\n#define CANDY_WEBSOCKET_MESSAGE_H\n\n#include \"core/net."
},
{
"path": "candy/src/websocket/server.cc",
"chars": 16309,
"preview": "// SPDX-License-Identifier: MIT\n#include \"websocket/server.h\"\n#include \"core/net.h\"\n#include \"utils/time.h\"\n#include \"we"
},
{
"path": "candy/src/websocket/server.h",
"chars": 1759,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_WEBSOCKET_SERVER_H\n#define CANDY_WEBSOCKET_SERVER_H\n\n#include \"core/net.h\""
},
{
"path": "candy-cli/CMakeLists.txt",
"chars": 526,
"preview": "file(GLOB_RECURSE SOURCES \"src/*.cc\")\nadd_executable(candy-cli ${SOURCES})\n\ntarget_include_directories(candy-cli PUBLIC "
},
{
"path": "candy-cli/src/argparse.h",
"chars": 91383,
"preview": "/*\n __ _ _ __ __ _ _ __ __ _ _ __ ___ ___\n / _` | '__/ _` | '_ \\ / _` | '__/ __|/ _ \\ Argument Parser for Modern C++"
},
{
"path": "candy-cli/src/config.cc",
"chars": 9799,
"preview": "// SPDX-License-Identifier: MIT\n#include \"config.h\"\n#include \"argparse.h\"\n#include \"candy/candy.h\"\n#include <Poco/JSON/O"
},
{
"path": "candy-cli/src/config.h",
"chars": 964,
"preview": "// SPDX-License-Identifier: MIT\n#ifndef CANDY_CLI_CONFIG_H\n#define CANDY_CLI_CONFIG_H\n\n#include <Poco/JSON/Object.h>\n#in"
},
{
"path": "candy-cli/src/main.cc",
"chars": 1363,
"preview": "#include \"candy/candy.h\"\n#include \"config.h\"\n#include <signal.h>\n#include <spdlog/spdlog.h>\n\nint main(int argc, char *ar"
},
{
"path": "candy-service/CMakeLists.txt",
"chars": 653,
"preview": "file(GLOB_RECURSE SOURCES \"src/*.cc\")\nadd_executable(candy-service ${SOURCES})\n\ntarget_include_directories(candy-service"
},
{
"path": "candy-service/README.md",
"chars": 1288,
"preview": "# candy-service\n\nCandy 客户端的另一个实现.\n\n- **无状态**: 进程本身不持久化任何数据, 进程重启后数据丢失,需要外部维护网络配置信息\n- **API 交互**: 对外提供 HTTP API 交互接口,可以远程"
},
{
"path": "candy-service/src/main.cc",
"chars": 9807,
"preview": "#include \"candy/client.h\"\n#include <Poco/Exception.h>\n#include <Poco/File.h>\n#include <Poco/JSON/Object.h>\n#include <Poc"
},
{
"path": "candy.cfg",
"chars": 2768,
"preview": "############################## Client and Server ##############################\n# [Required] Working mode, \"client\" or \""
},
{
"path": "candy.initd",
"chars": 1051,
"preview": "#!/sbin/openrc-run\n# Copyright 2024 Gentoo Authors\n# Distributed under the terms of the GNU General Public License v2\n\nn"
},
{
"path": "candy.service",
"chars": 203,
"preview": "[Unit]\nDescription=A simple networking tool\nStartLimitIntervalSec=0\n\n[Service]\nExecStart=/usr/bin/candy --no-timestamp -"
},
{
"path": "candy@.service",
"chars": 208,
"preview": "[Unit]\nDescription=A simple networking tool\nStartLimitIntervalSec=0\n\n[Service]\nExecStart=/usr/bin/candy --no-timestamp -"
},
{
"path": "cmake/Fetch.cmake",
"chars": 742,
"preview": "macro(Fetch NAME GIT_REPOSITORY GIT_TAG)\n include(FetchContent)\n if(${CMAKE_VERSION} VERSION_GREATER_EQUAL \"3.28\")"
},
{
"path": "cmake/openssl/CMakeLists.txt",
"chars": 742,
"preview": "cmake_minimum_required(VERSION 3.16)\nif(POLICY CMP0135)\n cmake_policy(SET CMP0135 NEW)\nendif()\nproject(openssl)\n\ninclud"
},
{
"path": "dockerfile",
"chars": 518,
"preview": "FROM alpine AS base\nRUN apk update\nRUN apk add spdlog openssl poco\n\nFROM base AS build\nRUN apk add git cmake ninja pkgco"
},
{
"path": "docs/CNAME",
"chars": 16,
"preview": "docs.canets.org\n"
},
{
"path": "docs/_config.yml",
"chars": 13,
"preview": "title: Candy\n"
},
{
"path": "docs/deploy-cli-server.md",
"chars": 111,
"preview": "# 部署 CLI 服务端\n\n根据帮助信息 `candy --help` 和配置文件描述部署.\n\n非专业用户请[部署 Web 服务端](https://docs.canets.org/deploy-web-server).\n"
},
{
"path": "docs/deploy-web-server.md",
"chars": 1108,
"preview": "# 部署 Web 服务端\n\n## 前置条件\n\n知道如何部署 Web 服务,并能够申请证书后对外提供 HTTPS 服务.\n\n否则使用明文传输将导致数据泄漏,存在安全隐患.此时建议使用社区服务器构建私有网络.\n\n## 一键部署服务端\n\n```b"
},
{
"path": "docs/index.md",
"chars": 1519,
"preview": "# Candy\n\n<p>\n<a href=\"https://github.com/lanthora/candy/releases/latest\"><img src=\"https://img.shields.io/github/release"
},
{
"path": "docs/install-client-for-linux.md",
"chars": 2173,
"preview": "# 安装 Linux 客户端\n\n我们针对不同 Linux 发行版提供了多种格式的安装包.对于暂未支持的发行版,可以选择容器部署或者静态链接的可执行文件.\n我们致力于支持所有架构的 Linux 系统.\n\n## Docker\n\n镜像已上传 [D"
},
{
"path": "docs/install-client-for-macos.md",
"chars": 528,
"preview": "# 安装 macOS 客户端\n\nmacOS 客户端通过 [Homebrew](https://brew.sh) 安装并提供服务.\n\n## 安装 Homebrew\n\n```bash\n/bin/bash -c \"$(curl -fsSL htt"
},
{
"path": "docs/install-client-for-windows.md",
"chars": 507,
"preview": "# 安装 Windows 客户端\n\n## 图形用户界面\n\n对于 Windows 10 及以上的用户,请使用[图形界面版本](https://github.com/lanthora/cake/releases/latest).此版本支持同时配"
},
{
"path": "docs/software-defined-wide-area-network.md",
"chars": 2756,
"preview": "# 多局域网组网\n\n## 需求\n\n在多地有多个局域网时,希望能够让本局域网内的设备通过其他局域网的地址直接访问对方的设备,并且无需在所有设备上部署 Candy 客户端.\n\n## 示例\n\n首先你需要:\n\n- 有一个独立的网络.可以自建服务端或"
},
{
"path": "docs/use-the-community-server.md",
"chars": 769,
"preview": "# 使用社区服务器\n\n社区服务器支持用户级别的隔离,同时支持一个用户创建多个网络.\n\n__服务器将定期清理不活跃用户,请确保短期内至少有一台设备连接过服务器,或手动登录过服务器管理页面.__\n\n## 注册\n\n在社区服务器[注册](https"
},
{
"path": "scripts/build-standalone.sh",
"chars": 4413,
"preview": "#!/bin/bash -e\n\nif [ -z $CANDY_WORKSPACE ];then echo \"CANDY_WORKSPACE is not exist\";exit 1;fi\n\nif [[ -z $TARGET || -z $T"
},
{
"path": "scripts/search-deps.sh",
"chars": 1566,
"preview": "#!/bin/bash -e\n\n# Define an array to store the processed dependencies\ndeclare -a processed\n\n# Define a function to get t"
},
{
"path": "scripts/standalone.json",
"chars": 1033,
"preview": "{\n \"include\": [\n {\n \"os\": \"linux\",\n \"arch\": \"aarch64\"\n },\n {\n \""
}
]
About this extraction
This page contains the full source code of the lanthora/candy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 86 files (288.4 KB), approximately 78.8k tokens, and a symbol index with 215 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.