├── docs ├── CNAME ├── _config.yml ├── images │ ├── cake.png │ ├── sdwan.png │ ├── alipay.png │ ├── wechat.png │ ├── cacao-network.png │ ├── cacao-register.png │ ├── cacao-admin-user.png │ ├── cacao-admin-setting.png │ └── cacao-network-another.png ├── deploy-cli-server.md ├── install-client-for-windows.md ├── install-client-for-macos.md ├── use-the-community-server.md ├── deploy-web-server.md ├── index.md ├── install-client-for-linux.md └── software-defined-wide-area-network.md ├── .dockerignore ├── .vscode ├── extensions.json ├── c_cpp_properties.json ├── settings.json ├── tasks.json └── launch.json ├── candy ├── include │ └── candy │ │ ├── candy.h │ │ ├── common.h │ │ ├── server.h │ │ └── client.h ├── src │ ├── utils │ │ ├── random.h │ │ ├── time.h │ │ ├── codecvt.h │ │ ├── random.cc │ │ ├── codecvt.cc │ │ ├── atomic.h │ │ └── time.cc │ ├── core │ │ ├── common.cc │ │ ├── message.cc │ │ ├── version.h │ │ ├── server.cc │ │ ├── server.h │ │ ├── message.h │ │ ├── client.h │ │ ├── client.cc │ │ ├── net.h │ │ └── net.cc │ ├── peer │ │ ├── message.cc │ │ ├── message.h │ │ ├── peer.h │ │ ├── manager.h │ │ └── peer.cc │ ├── tun │ │ ├── unknown.cc │ │ ├── tun.h │ │ ├── tun.cc │ │ ├── linux.cc │ │ ├── macos.cc │ │ └── windows.cc │ ├── candy │ │ ├── server.cc │ │ └── client.cc │ └── websocket │ │ ├── server.h │ │ ├── client.h │ │ ├── message.h │ │ ├── message.cc │ │ ├── client.cc │ │ └── server.cc ├── .vscode │ ├── c_cpp_properties.json │ └── settings.json └── CMakeLists.txt ├── candy.service ├── candy@.service ├── .clang-format ├── .gitignore ├── dockerfile ├── candy-cli ├── CMakeLists.txt └── src │ ├── config.h │ ├── main.cc │ └── config.cc ├── candy-service ├── CMakeLists.txt ├── README.md └── src │ └── main.cc ├── cmake ├── openssl │ └── CMakeLists.txt └── Fetch.cmake ├── LICENSE ├── candy.initd ├── scripts ├── standalone.json ├── search-deps.sh └── build-standalone.sh ├── README.md ├── .github └── workflows │ ├── check.yaml │ ├── standalone.yaml │ └── release.yaml ├── candy.cfg └── CMakeLists.txt /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.canets.org 2 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Candy 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .vscode 4 | 5 | build/* 6 | -------------------------------------------------------------------------------- /docs/images/cake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/cake.png -------------------------------------------------------------------------------- /docs/images/sdwan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/sdwan.png -------------------------------------------------------------------------------- /docs/images/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/alipay.png -------------------------------------------------------------------------------- /docs/images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/wechat.png -------------------------------------------------------------------------------- /docs/images/cacao-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/cacao-network.png -------------------------------------------------------------------------------- /docs/images/cacao-register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/cacao-register.png -------------------------------------------------------------------------------- /docs/images/cacao-admin-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/cacao-admin-user.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode.cpptools-extension-pack" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/images/cacao-admin-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/cacao-admin-setting.png -------------------------------------------------------------------------------- /docs/images/cacao-network-another.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanthora/candy/HEAD/docs/images/cacao-network-another.png -------------------------------------------------------------------------------- /docs/deploy-cli-server.md: -------------------------------------------------------------------------------- 1 | # 部署 CLI 服务端 2 | 3 | 根据帮助信息 `candy --help` 和配置文件描述部署. 4 | 5 | 非专业用户请[部署 Web 服务端](https://docs.canets.org/deploy-web-server). 6 | -------------------------------------------------------------------------------- /candy/include/candy/candy.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CANDY_H 3 | #define CANDY_CANDY_H 4 | 5 | #include "client.h" 6 | #include "common.h" 7 | #include "server.h" 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /candy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=A simple networking tool 3 | StartLimitIntervalSec=0 4 | 5 | [Service] 6 | ExecStart=/usr/bin/candy --no-timestamp -c /etc/candy.cfg 7 | Restart=always 8 | RestartSec=3 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /candy@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=A simple networking tool 3 | StartLimitIntervalSec=0 4 | 5 | [Service] 6 | ExecStart=/usr/bin/candy --no-timestamp -c /etc/candy.d/%i.cfg 7 | Restart=always 8 | RestartSec=3 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /candy/include/candy/common.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_COMMON_H 3 | #define CANDY_COMMON_H 4 | 5 | #include 6 | 7 | namespace candy { 8 | static const int VMAC_SIZE = 16; 9 | 10 | std::string version(); 11 | std::string create_vmac(); 12 | } // namespace candy 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | TabWidth: 8 5 | AccessModifierOffset: -4 6 | AllowShortFunctionsOnASingleLine: Empty 7 | AllowShortIfStatementsOnASingleLine: false 8 | AllowShortLoopsOnASingleLine: false 9 | ColumnLimit: 130 10 | IndentCaseLabels: false 11 | SortIncludes: true 12 | ... 13 | -------------------------------------------------------------------------------- /candy/src/utils/random.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_UTILS_RANDOM_H 3 | #define CANDY_UTILS_RANDOM_H 4 | 5 | #include 6 | #include 7 | 8 | namespace candy { 9 | 10 | uint32_t randomUint32(); 11 | std::string randomHexString(int length); 12 | 13 | } // namespace candy 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /candy/src/core/common.cc: -------------------------------------------------------------------------------- 1 | #include "candy/common.h" 2 | #include "core/version.h" 3 | #include "utils/random.h" 4 | #include 5 | 6 | namespace candy { 7 | 8 | std::string version() { 9 | return CANDY_VERSION; 10 | } 11 | 12 | std::string create_vmac() { 13 | return randomHexString(VMAC_SIZE); 14 | } 15 | 16 | } // namespace candy -------------------------------------------------------------------------------- /candy/src/utils/time.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_UTILS_TIME_H 3 | #define CANDY_UTILS_TIME_H 4 | 5 | #include 6 | #include 7 | 8 | namespace candy { 9 | 10 | int64_t unixTime(); 11 | int64_t bootTime(); 12 | 13 | std::string getCurrentTimeWithMillis(); 14 | 15 | } // namespace candy 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /candy/src/utils/codecvt.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_UTILS_CODECVT_H 3 | #define CANDY_UTILS_CODECVT_H 4 | 5 | #include 6 | 7 | namespace candy { 8 | 9 | std::string UTF16ToUTF8(const std::wstring &utf16Str); 10 | std::wstring UTF8ToUTF16(const std::string &utf8Str); 11 | 12 | } // namespace candy 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /candy/include/candy/server.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_SERVER_H 3 | #define CANDY_SERVER_H 4 | 5 | #include 6 | #include 7 | 8 | namespace candy { 9 | namespace server { 10 | 11 | bool run(const Poco::JSON::Object &config); 12 | bool shutdown(); 13 | 14 | } // namespace server 15 | } // namespace candy 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /candy/src/peer/message.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "peer/message.h" 3 | #include 4 | 5 | namespace candy { 6 | 7 | namespace PeerMsg { 8 | std::string Forward::create(const std::string &packet) { 9 | std::string data; 10 | data.push_back(PeerMsgKind::FORWARD); 11 | data += packet; 12 | return data; 13 | } 14 | } // namespace PeerMsg 15 | 16 | } // namespace candy 17 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/clang", 10 | "cStandard": "c17", 11 | "intelliSenseMode": "linux-clang-x64" 12 | } 13 | ], 14 | "version": 4 15 | } 16 | -------------------------------------------------------------------------------- /candy/.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/clang", 10 | "cStandard": "c17", 11 | "intelliSenseMode": "linux-clang-x64" 12 | } 13 | ], 14 | "version": 4 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.detectIndentation": false, 3 | "editor.tabSize": 4, 4 | "editor.formatOnSave": true, 5 | "editor.insertSpaces": true, 6 | "editor.formatOnSaveMode": "modifications", 7 | "files.insertFinalNewline": true, 8 | "json.format.enable": true, 9 | "C_Cpp.default.cppStandard": "c++23", 10 | "C_Cpp.autoAddFileAssociations": false, 11 | "C_Cpp.errorSquiggles": "disabled" 12 | } 13 | -------------------------------------------------------------------------------- /candy/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.detectIndentation": false, 3 | "editor.tabSize": 4, 4 | "editor.formatOnSave": true, 5 | "editor.insertSpaces": true, 6 | "editor.formatOnSaveMode": "modifications", 7 | "files.insertFinalNewline": true, 8 | "json.format.enable": true, 9 | "C_Cpp.default.cppStandard": "c++17", 10 | "C_Cpp.autoAddFileAssociations": false, 11 | "C_Cpp.errorSquiggles": "disabled" 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # CMake Build files 35 | .cache 36 | build 37 | -------------------------------------------------------------------------------- /candy/include/candy/client.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CLIENT_H 3 | #define CANDY_CLIENT_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace candy { 10 | namespace client { 11 | 12 | bool run(const std::string &id, const Poco::JSON::Object &config); 13 | bool shutdown(const std::string &id); 14 | std::optional status(const std::string &id); 15 | 16 | } // namespace client 17 | } // namespace candy 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /candy/src/core/message.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "core/message.h" 3 | 4 | namespace candy { 5 | 6 | Msg::Msg(MsgKind kind, std::string data) { 7 | this->kind = kind; 8 | this->data = std::move(data); 9 | } 10 | 11 | Msg::Msg(Msg &&packet) { 12 | kind = packet.kind; 13 | data = std::move(packet.data); 14 | } 15 | 16 | Msg &Msg::operator=(Msg &&packet) { 17 | kind = packet.kind; 18 | data = std::move(packet.data); 19 | return *this; 20 | } 21 | 22 | } // namespace candy 23 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS base 2 | RUN apk update 3 | RUN apk add spdlog openssl poco 4 | 5 | FROM base AS build 6 | RUN apk add git cmake ninja pkgconf g++ spdlog-dev openssl-dev poco-dev linux-headers 7 | COPY . candy 8 | RUN cd candy && cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build && cmake --install build 9 | 10 | FROM base AS product 11 | RUN install -D /dev/null /var/lib/candy/lost 12 | COPY --from=build /usr/local/bin/candy /usr/bin/candy 13 | COPY candy.cfg /etc/candy.cfg 14 | ENTRYPOINT ["/usr/bin/candy"] 15 | CMD ["-c", "/etc/candy.cfg"] 16 | -------------------------------------------------------------------------------- /candy/src/core/version.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CORE_VERSION_H 3 | #define CANDY_CORE_VERSION_H 4 | 5 | #include 6 | 7 | #if POCO_OS == POCO_OS_LINUX 8 | #define CANDY_SYSTEM "linux" 9 | #elif POCO_OS == POCO_OS_MAC_OS_X 10 | #define CANDY_SYSTEM "macos" 11 | #elif POCO_OS == POCO_OS_ANDROID 12 | #define CANDY_SYSTEM "android" 13 | #elif POCO_OS == POCO_OS_WINDOWS_NT 14 | #define CANDY_SYSTEM "windows" 15 | #else 16 | #define CANDY_SYSTEM "unknown" 17 | #endif 18 | 19 | #ifndef CANDY_VERSION 20 | #define CANDY_VERSION "unknown" 21 | #endif 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /candy-cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE SOURCES "src/*.cc") 2 | add_executable(candy-cli ${SOURCES}) 3 | 4 | target_include_directories(candy-cli PUBLIC 5 | $ 6 | $ 7 | ) 8 | 9 | set_target_properties(candy-cli PROPERTIES OUTPUT_NAME "candy") 10 | 11 | target_link_libraries(candy-cli PRIVATE spdlog::spdlog) 12 | target_link_libraries(candy-cli PRIVATE Poco::Foundation Poco::JSON) 13 | target_link_libraries(candy-cli PRIVATE Candy::Library) 14 | 15 | install(TARGETS candy-cli) 16 | 17 | add_executable(Candy::CLI ALIAS candy-cli) 18 | -------------------------------------------------------------------------------- /docs/install-client-for-windows.md: -------------------------------------------------------------------------------- 1 | # 安装 Windows 客户端 2 | 3 | ## 图形用户界面 4 | 5 | 对于 Windows 10 及以上的用户,请使用[图形界面版本](https://github.com/lanthora/cake/releases/latest).此版本支持同时配置多个网络. 6 | 7 | 在没有任何配置时,点击 "文件" => "新建" 将填充默认配置,点击 "保存" 后配置生效,客户端此时才开始连接服务端. 8 | 9 | 图形界面的配置与[默认配置](https://raw.githubusercontent.com/lanthora/candy/refs/heads/master/candy.cfg)对应. 10 | 11 | 日志保存在 `C:/ProgramData/Cake/logs`, 反馈 Windows 相关问题请带着日志和配置截图. 12 | 13 | ![cake](images/cake.png) 14 | 15 | ## 命令行 16 | 17 | 使用命令行版本请自行解决遇到的任何问题,我们不对 Windows 命令行提供任何技术支持. 18 | 19 | Windows 7 用户只能使用[命令行版本](https://github.com/lanthora/candy/releases/latest) 20 | -------------------------------------------------------------------------------- /docs/install-client-for-macos.md: -------------------------------------------------------------------------------- 1 | # 安装 macOS 客户端 2 | 3 | macOS 客户端通过 [Homebrew](https://brew.sh) 安装并提供服务. 4 | 5 | ## 安装 Homebrew 6 | 7 | ```bash 8 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 9 | ``` 10 | 11 | ## 添加第三方仓库 12 | 13 | ```bash 14 | brew tap lanthora/repo 15 | ``` 16 | 17 | ## 安装 Candy 18 | 19 | ```bash 20 | brew install candy 21 | ``` 22 | 23 | ## 修改配置 24 | 25 | 对于 M 系列处理器,配置文件在 `/opt/homebrew/etc/candy.cfg`, Intel 系列处理器,配置文件在 `/usr/local/etc/candy.cfg` 26 | 27 | 通过以下命令进行测试: 28 | 29 | ```bash 30 | sudo candy -c /path/to/candy.cfg 31 | ``` 32 | 33 | ## 启动服务 34 | 35 | 测试成功后以服务的形式运行. 36 | 37 | ```bash 38 | sudo brew services start lanthora/repo/candy 39 | ``` 40 | -------------------------------------------------------------------------------- /candy-service/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE SOURCES "src/*.cc") 2 | add_executable(candy-service ${SOURCES}) 3 | 4 | target_include_directories(candy-service PUBLIC 5 | $ 6 | $ 7 | ) 8 | 9 | set_target_properties(candy-service PROPERTIES OUTPUT_NAME "candy-service") 10 | 11 | target_link_libraries(candy-service PRIVATE spdlog::spdlog) 12 | target_link_libraries(candy-service PRIVATE Poco::Foundation Poco::Net Poco::JSON Poco::Util) 13 | target_link_libraries(candy-service PRIVATE Threads::Threads) 14 | target_link_libraries(candy-service PRIVATE Candy::Library) 15 | 16 | install(TARGETS candy-service) 17 | 18 | add_executable(Candy::Service ALIAS candy-service) 19 | -------------------------------------------------------------------------------- /candy/src/core/server.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "core/server.h" 3 | 4 | namespace candy { 5 | 6 | void Server::setWebSocket(const std::string &uri) { 7 | ws.setWebSocket(uri); 8 | } 9 | 10 | void Server::setPassword(const std::string &password) { 11 | ws.setPassword(password); 12 | } 13 | 14 | void Server::setDHCP(const std::string &cidr) { 15 | ws.setDHCP(cidr); 16 | } 17 | 18 | void Server::setSdwan(const std::string &sdwan) { 19 | ws.setSdwan(sdwan); 20 | } 21 | 22 | void Server::run() { 23 | running.store(true); 24 | ws.run(); 25 | running.wait(true); 26 | ws.shutdown(); 27 | } 28 | 29 | void Server::shutdown() { 30 | running.store(false); 31 | } 32 | 33 | } // namespace candy 34 | -------------------------------------------------------------------------------- /candy/src/core/server.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CORE_SERVER_H 3 | #define CANDY_CORE_SERVER_H 4 | 5 | #include "utils/atomic.h" 6 | #include "websocket/server.h" 7 | #include 8 | 9 | namespace candy { 10 | 11 | class Server { 12 | public: 13 | // 通过配置文件或命令行设置的参数 14 | void setWebSocket(const std::string &uri); 15 | void setPassword(const std::string &password); 16 | void setDHCP(const std::string &cidr); 17 | void setSdwan(const std::string &sdwan); 18 | 19 | // 启动服务端,非阻塞 20 | void run(); 21 | // 关闭客户端,阻塞,直到所有子模块退出 22 | void shutdown(); 23 | 24 | private: 25 | // 目前只有一个 WebSocket 服务端的子模块 26 | WebSocketServer ws; 27 | Utils::Atomic running; 28 | }; 29 | 30 | } // namespace candy 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "type": "cppbuild", 5 | "label": "C/C++: g++ build active file", 6 | "command": "/usr/bin/g++", 7 | "args": [ 8 | "-fdiagnostics-color=always", 9 | "-g", 10 | "${file}", 11 | "-o", 12 | "${fileDirname}/${fileBasenameNoExtension}" 13 | ], 14 | "options": { 15 | "cwd": "${fileDirname}" 16 | }, 17 | "problemMatcher": [ 18 | "$gcc" 19 | ], 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "detail": "Task generated by Debugger." 25 | } 26 | ], 27 | "version": "2.0.0" 28 | } 29 | -------------------------------------------------------------------------------- /cmake/openssl/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | if(POLICY CMP0135) 3 | cmake_policy(SET CMP0135 NEW) 4 | endif() 5 | project(openssl) 6 | 7 | include(ExternalProject) 8 | ExternalProject_Add( 9 | openssl 10 | PREFIX ${CMAKE_CURRENT_BINARY_DIR}/openssl 11 | URL https://www.openssl.org/source/openssl-3.5.4.tar.gz 12 | LOG_BUILD ON 13 | BUILD_IN_SOURCE YES 14 | CONFIGURE_COMMAND 15 | COMMAND sed -i "s/disable('static', 'pic', 'threads')/disable('static', 'pic')/" Configure 16 | COMMAND ./config --release no-unit-test no-shared ${TARGET_OPENSSL} CFLAGS=$ENV{CFLAGS} LDFLAGS=$ENV{LDFLAGS} 17 | BUILD_COMMAND "" 18 | INSTALL_COMMAND "" 19 | TEST_COMMAND "" 20 | ) 21 | -------------------------------------------------------------------------------- /cmake/Fetch.cmake: -------------------------------------------------------------------------------- 1 | macro(Fetch NAME GIT_REPOSITORY GIT_TAG) 2 | include(FetchContent) 3 | if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.28") 4 | FetchContent_Declare( 5 | ${NAME} 6 | GIT_REPOSITORY ${GIT_REPOSITORY} 7 | GIT_TAG ${GIT_TAG} 8 | EXCLUDE_FROM_ALL 9 | ) 10 | FetchContent_MakeAvailable(${NAME}) 11 | else() 12 | FetchContent_Declare( 13 | ${NAME} 14 | GIT_REPOSITORY ${GIT_REPOSITORY} 15 | GIT_TAG ${GIT_TAG} 16 | ) 17 | FetchContent_GetProperties(${NAME}) 18 | if(NOT ${NAME}_POPULATED) 19 | FetchContent_Populate(${NAME}) 20 | add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR} EXCLUDE_FROM_ALL) 21 | endif() 22 | endif() 23 | endmacro() 24 | -------------------------------------------------------------------------------- /candy/src/utils/random.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "utils/random.h" 3 | #include 4 | #include 5 | #include 6 | 7 | namespace { 8 | 9 | int randomHex() { 10 | std::random_device device; 11 | std::mt19937 engine(device()); 12 | std::uniform_int_distribution distrib(0, 15); 13 | return distrib(engine); 14 | } 15 | } // namespace 16 | 17 | namespace candy { 18 | 19 | uint32_t randomUint32() { 20 | std::random_device device; 21 | std::mt19937 engine(device()); 22 | std::uniform_int_distribution distrib; 23 | return distrib(engine); 24 | } 25 | 26 | std::string randomHexString(int length) { 27 | std::stringstream ss; 28 | for (int i = 0; i < length; i++) { 29 | ss << std::hex << randomHex(); 30 | } 31 | return ss.str(); 32 | } 33 | 34 | } // namespace candy 35 | -------------------------------------------------------------------------------- /candy/src/tun/unknown.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include 3 | 4 | #if POCO_OS != POCO_OS_LINUX && POCO_OS != POCO_OS_MAC_OS_X && POCO_OS != POCO_OS_WINDOWS_NT 5 | 6 | #include "tun/tun.h" 7 | 8 | namespace candy { 9 | 10 | Tun::Tun() {} 11 | 12 | Tun::~Tun() {} 13 | 14 | int Tun::setName(const std::string &name) { 15 | return -1; 16 | } 17 | 18 | int Tun::setAddress(const std::string &cidr) { 19 | return -1; 20 | } 21 | 22 | int Tun::setMTU(int mtu) { 23 | return -1; 24 | } 25 | 26 | int Tun::up() { 27 | return -1; 28 | } 29 | 30 | int Tun::down() { 31 | return -1; 32 | } 33 | 34 | int Tun::read(std::string &buffer) { 35 | return -1; 36 | } 37 | 38 | int Tun::write(const std::string &buffer) { 39 | return -1; 40 | } 41 | 42 | int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 43 | return -1; 44 | } 45 | 46 | } // namespace candy 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /candy/src/core/message.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CORE_MESSAGE_H 3 | #define CANDY_CORE_MESSAGE_H 4 | 5 | #include "core/net.h" 6 | #include 7 | #include 8 | 9 | namespace candy { 10 | 11 | enum class MsgKind { 12 | TIMEOUT, 13 | PACKET, 14 | TUNADDR, 15 | SYSRT, 16 | TRYP2P, 17 | PUBINFO, 18 | DISCOVERY, 19 | }; 20 | 21 | struct Msg { 22 | MsgKind kind; 23 | std::string data; 24 | 25 | Msg(const Msg &) = delete; 26 | Msg &operator=(const Msg &) = delete; 27 | 28 | Msg(MsgKind kind = MsgKind::TIMEOUT, std::string = ""); 29 | Msg(Msg &&packet); 30 | Msg &operator=(Msg &&packet); 31 | }; 32 | 33 | namespace CoreMsg { 34 | 35 | struct PubInfo { 36 | IP4 src; 37 | IP4 dst; 38 | IP4 ip; 39 | uint16_t port; 40 | bool local = false; 41 | }; 42 | 43 | } // namespace CoreMsg 44 | 45 | } // namespace candy 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /docs/use-the-community-server.md: -------------------------------------------------------------------------------- 1 | # 使用社区服务器 2 | 3 | 社区服务器支持用户级别的隔离,同时支持一个用户创建多个网络. 4 | 5 | __服务器将定期清理不活跃用户,请确保短期内至少有一台设备连接过服务器,或手动登录过服务器管理页面.__ 6 | 7 | ## 注册 8 | 9 | 在社区服务器[注册](https://canets.org/register),示例中的用户名为 `username`. 10 | 11 | ![](images/cacao-register.png) 12 | 13 | ## 使用默认网络 14 | 15 | 查看网络,可以注意到已经有一个名称为 @ 的默认网络,密码是 `ZrhaUcz1` 16 | 17 | ![](images/cacao-network.png) 18 | 19 | 连接到这个网络的客户端仅需要修改以下配置,关于配置文件的位置请参考客户端安装的相关文档: 20 | 21 | ```cfg 22 | websocket = "wss://canets.org/username" 23 | password = "ZrhaUcz1" 24 | ``` 25 | 26 | ## 多个网络 27 | 28 | 点击左上角 `Add` 可以创建多个网络,例如: 29 | 30 | ![](images/cacao-network-another.png) 31 | 32 | 这个新网络,网络名为 `netname`, 这会体现到 `websocket` 参数中; 密码为空; 网络范围是 `10.0.0.0/24`; 不允许广播; 且租期为 3 天, 即超过 3 天不活跃的客户端将被自动从网络中移除, 配置为 0 时表示不自动移除. 33 | 34 | 客户端的配置应该为: 35 | 36 | ```cfg 37 | websocket = "wss://canets.org/username/netname" 38 | password = "" 39 | ``` 40 | 41 | 如果要给某个客户端指定静态地址 `10.0.0.1/24`, 只需要修改配置中的: 42 | 43 | ```cfg 44 | tun = "10.0.0.1/24" 45 | ``` 46 | -------------------------------------------------------------------------------- /candy/src/candy/server.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "candy/server.h" 3 | #include "core/server.h" 4 | #include "utils/atomic.h" 5 | 6 | namespace candy { 7 | namespace server { 8 | 9 | namespace { 10 | Utils::Atomic running(true); 11 | std::shared_ptr server; 12 | } // namespace 13 | 14 | bool run(const Poco::JSON::Object &config) { 15 | while (running.load()) { 16 | std::this_thread::sleep_for(std::chrono::seconds(1)); 17 | server = std::make_shared(); 18 | server->setWebSocket(config.getValue("websocket")); 19 | server->setPassword(config.getValue("password")); 20 | server->setDHCP(config.getValue("dhcp")); 21 | server->setSdwan(config.getValue("sdwan")); 22 | server->run(); 23 | } 24 | return true; 25 | } 26 | 27 | bool shutdown() { 28 | running.store(false); 29 | server->shutdown(); 30 | return true; 31 | } 32 | 33 | } // namespace server 34 | } // namespace candy 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "(gdb) Launch", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/build/src/main/candy", 9 | "args": [ 10 | "-c", 11 | "candy.cfg" 12 | ], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [], 16 | "externalConsole": false, 17 | "MIMode": "gdb", 18 | "setupCommands": [ 19 | { 20 | "description": "Enable pretty-printing for gdb", 21 | "text": "-enable-pretty-printing", 22 | "ignoreFailures": true 23 | }, 24 | { 25 | "description": "Set Disassembly Flavor to Intel", 26 | "text": "-gdb-set disassembly-flavor intel", 27 | "ignoreFailures": true 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lanthora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /candy-cli/src/config.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CLI_CONFIG_H 3 | #define CANDY_CLI_CONFIG_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | struct arguments { 10 | int parse(int argc, char *argv[]); 11 | Poco::JSON::Object json(); 12 | 13 | private: 14 | void parseFile(std::string cfgFile); 15 | std::map fileToKvMap(const std::string &filename); 16 | 17 | std::string mode; 18 | std::string websocket; 19 | std::string password; 20 | bool noTimestamp = false; 21 | bool debug = false; 22 | 23 | std::string dhcp; 24 | std::string sdwan; 25 | 26 | std::string name; 27 | std::string tun; 28 | std::string stun; 29 | std::string localhost; 30 | int port = 0; 31 | int discovery = 0; 32 | int routeCost = 0; 33 | int mtu = 1400; 34 | }; 35 | 36 | int saveTunAddress(const std::string &name, const std::string &cidr); 37 | std::string loadTunAddress(const std::string &name); 38 | std::string virtualMac(const std::string &name); 39 | std::string storageDirectory(std::string subdir = ""); 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /candy.initd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | # Copyright 2024 Gentoo Authors 3 | # Distributed under the terms of the GNU General Public License v2 4 | 5 | name="candy daemon" 6 | description="A simple networking tool" 7 | CANDY_NAME=${SVCNAME##*.} 8 | if [ -n "${CANDY_NAME}" -a "${SVCNAME}" != "candy" ]; then 9 | CANDY_PIDFILE="/run/candy.${CANDY_NAME}.pid" 10 | CANDY_CONFIG="/etc/candy.d/${CANDY_NAME}.cfg" 11 | CANDY_LOG="/var/log/candy/${CANDY_NAME}.log" 12 | else 13 | CANDY_PIDFILE="/run/candy.pid" 14 | CANDY_CONFIG="/etc/candy.cfg" 15 | CANDY_LOG="/var/log/candy/candy.log" 16 | fi 17 | depend() { 18 | need net 19 | } 20 | 21 | start_pre() { 22 | if [ ! -d "/tmp/candy/" ]; then 23 | mkdir "/tmp/candy" 24 | fi 25 | if [ ! -L "/var/log/candy" ]; then 26 | ln -s "/tmp/candy" "/var/log/" 27 | fi 28 | } 29 | 30 | start() { 31 | ebegin "Starting Candy, Log File: ${CANDY_LOG}" 32 | start-stop-daemon --start --background \ 33 | --stdout "${CANDY_LOG}" --stderr "${CANDY_LOG}" \ 34 | --make-pidfile --pidfile "${CANDY_PIDFILE}" \ 35 | --exec /usr/bin/candy -- -c "${CANDY_CONFIG}" 36 | eend $? 37 | } 38 | 39 | stop() { 40 | ebegin "Stopping Candy" 41 | start-stop-daemon --stop \ 42 | --pidfile "${CANDY_PIDFILE}" 43 | eend $? 44 | } 45 | -------------------------------------------------------------------------------- /candy/src/utils/codecvt.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #if POCO_OS == POCO_OS_WINDOWS_NT 3 | #include "utils/codecvt.h" 4 | #include 5 | 6 | namespace candy { 7 | 8 | std::string UTF16ToUTF8(const std::wstring &utf16Str) { 9 | if (utf16Str.empty()) 10 | return ""; 11 | 12 | int utf8Size = WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, nullptr, 0, nullptr, nullptr); 13 | 14 | if (utf8Size == 0) { 15 | return ""; 16 | } 17 | 18 | std::string utf8Str(utf8Size, '\0'); 19 | WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, &utf8Str[0], utf8Size, nullptr, nullptr); 20 | 21 | utf8Str.resize(utf8Size - 1); 22 | return utf8Str; 23 | } 24 | 25 | std::wstring UTF8ToUTF16(const std::string &utf8Str) { 26 | if (utf8Str.empty()) 27 | return L""; 28 | 29 | int utf16Size = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0); 30 | 31 | if (utf16Size == 0) { 32 | return L""; 33 | } 34 | 35 | std::wstring utf16Str(utf16Size, L'\0'); 36 | MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &utf16Str[0], utf16Size); 37 | 38 | utf16Str.resize(utf16Size - 1); 39 | return utf16Str; 40 | } 41 | 42 | } // namespace candy 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /scripts/standalone.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | { 4 | "os": "linux", 5 | "arch": "aarch64" 6 | }, 7 | { 8 | "os": "linux", 9 | "arch": "arm" 10 | }, 11 | { 12 | "os": "linux", 13 | "arch": "armhf" 14 | }, 15 | { 16 | "os": "linux", 17 | "arch": "loongarch64" 18 | }, 19 | { 20 | "os": "linux", 21 | "arch": "mips" 22 | }, 23 | { 24 | "os": "linux", 25 | "arch": "mipssf" 26 | }, 27 | { 28 | "os": "linux", 29 | "arch": "mipsel" 30 | }, 31 | { 32 | "os": "linux", 33 | "arch": "mipselsf" 34 | }, 35 | { 36 | "os": "linux", 37 | "arch": "mips64" 38 | }, 39 | { 40 | "os": "linux", 41 | "arch": "mips64el" 42 | }, 43 | { 44 | "os": "linux", 45 | "arch": "riscv32" 46 | }, 47 | { 48 | "os": "linux", 49 | "arch": "riscv64" 50 | }, 51 | { 52 | "os": "linux", 53 | "arch": "x86_64" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /docs/deploy-web-server.md: -------------------------------------------------------------------------------- 1 | # 部署 Web 服务端 2 | 3 | ## 前置条件 4 | 5 | 知道如何部署 Web 服务,并能够申请证书后对外提供 HTTPS 服务. 6 | 7 | 否则使用明文传输将导致数据泄漏,存在安全隐患.此时建议使用社区服务器构建私有网络. 8 | 9 | ## 一键部署服务端 10 | 11 | ```bash 12 | docker run --name=cacao --detach --volume /var/lib/cacao:/var/lib/cacao --publish 8080:80 docker.io/lanthora/cacao:latest 13 | ``` 14 | 15 | ## 使用 16 | 17 | 假设你的域名为 `example.com`, 此时通过 `https://example.com` 应该能够正常访问服务.如果不是 `https` 请回到最开始解决前置条件. 18 | 19 | 服务器启动后的第一个注册用户默认被设置为管理员.管理员无法创建网络,且无权查看其他用户的网络. 20 | 21 | 管理员配置页面,能够配置是否允许注册,以及允许注册时的注册间隔(避免脚本小子刷注册用户).同时可以配置自动清理不活跃用户. 22 | 23 | ![](images/cacao-admin-setting.png) 24 | 25 | ### 单网络模式 26 | 27 | 在不允许注册时,管理员可以手动添加用户.其中名为 @ 的用户是一个特殊用户,这个用户只能创建一个名为 @ 的网络.用户名和网络名的作用在后面说明.先创建这个用户. 28 | 29 | ![](images/cacao-admin-user.png) 30 | 31 | 退出管理员,并以 @ 用户登录.此时已经默认添加了 @ 网络.默认网络生成了随机密码 `ZrhaUcz1` 32 | 33 | ![](images/cacao-network.png) 34 | 35 | 此时连接这个网络的客户端仅需要修改以下配置: 36 | 37 | ```cfg 38 | websocket = "wss://example.com" 39 | password = "ZrhaUcz1" 40 | ``` 41 | 42 | 除非你知道自己在做什么,否则请不要修改任何其他配置项. 43 | 44 | ### 多用户多网络模式 45 | 46 | 如果只是创建一个网络,单网络模式已经足够了.如果要允许多个用户使用,且每个用户可以创建多个网络.则可以使用多用户多网络模式. 47 | 48 | 假设由管理员创建或自行注册的普通用户名为 `${username}`, 这个用户拥有的一个网络名是 `${netname}`,那么客户端对应的配置仅需要修改为: 49 | 50 | ```cfg 51 | websocket = "wss://example.com/${username}/${netname}" 52 | ``` 53 | 54 | 当用户名或者网络名为 @ 时,在客户端的配置中需要留空.当用户名和网络名都为空时,就是所谓的单网络模式 55 | -------------------------------------------------------------------------------- /candy/src/utils/atomic.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_UTILS_ATOMIC_H 3 | #define CANDY_UTILS_ATOMIC_H 4 | 5 | #include 6 | 7 | namespace candy { 8 | namespace Utils { 9 | 10 | template class Atomic { 11 | public: 12 | explicit Atomic(T initial = T()) : value(initial) {} 13 | 14 | T load() const { 15 | std::lock_guard lock(mutex); 16 | return value; 17 | } 18 | 19 | void store(T new_value) { 20 | std::lock_guard lock(mutex); 21 | value = new_value; 22 | cv.notify_all(); 23 | } 24 | 25 | void wait(const T &expected) { 26 | std::unique_lock lock(mutex); 27 | cv.wait(lock, [this, &expected] { return value != expected; }); 28 | } 29 | 30 | template void wait_until(Predicate pred) { 31 | std::unique_lock lock(mutex); 32 | cv.wait(lock, pred); 33 | } 34 | 35 | void notify_one() { 36 | std::lock_guard lock(mutex); 37 | cv.notify_one(); 38 | } 39 | 40 | void notify_all() { 41 | std::lock_guard lock(mutex); 42 | cv.notify_all(); 43 | } 44 | 45 | private: 46 | T value; 47 | mutable std::mutex mutex; 48 | std::condition_variable cv; 49 | }; 50 | 51 | } // namespace Utils 52 | } // namespace candy 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /candy/src/utils/time.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "utils/time.h" 3 | #include "core/net.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace candy { 16 | 17 | int64_t unixTime() { 18 | using namespace std::chrono; 19 | return duration_cast(system_clock::now().time_since_epoch()).count(); 20 | } 21 | 22 | int64_t bootTime() { 23 | using namespace std::chrono; 24 | auto now = steady_clock::now(); 25 | return duration_cast(now.time_since_epoch()).count(); 26 | } 27 | 28 | std::string getCurrentTimeWithMillis() { 29 | auto now = std::chrono::system_clock::now(); 30 | 31 | auto ms_tp = std::chrono::time_point_cast(now); 32 | auto epoch = ms_tp.time_since_epoch(); 33 | auto value = std::chrono::duration_cast(epoch).count(); 34 | 35 | std::time_t now_time_t = std::chrono::system_clock::to_time_t(now); 36 | std::tm *ptm = std::localtime(&now_time_t); 37 | 38 | std::ostringstream oss; 39 | oss << std::put_time(ptm, "%Y-%m-%d %H:%M:%S"); 40 | oss << '.' << std::setfill('0') << std::setw(3) << (value % 1000); 41 | 42 | return oss.str(); 43 | } 44 | 45 | } // namespace candy 46 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Candy 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 | 一个高可用,低时延,反审查的组网工具. 12 | 13 | ## 如何使用 14 | 15 | - [安装 Windows 客户端](install-client-for-windows) 16 | - [安装 macOS 客户端](install-client-for-macos) 17 | - [安装 Linux 客户端](install-client-for-linux) 18 | - [部署 Web 服务端](deploy-web-server) 19 | - [部署 CLI 服务端](deploy-cli-server) 20 | - [使用社区服务器](use-the-community-server) 21 | - [多局域网组网](software-defined-wide-area-network) 22 | 23 | ## 相关项目 24 | 25 | - [Cacao](https://github.com/lanthora/cacao): WebUI 版的 Candy 服务器 26 | - [Cake](https://github.com/lanthora/cake): Qt 实现的 Candy GUI 桌面应用程序 27 | - [EasyTier](https://github.com/EasyTier/EasyTier): 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现 28 | 29 | ## 交流群 30 | 31 | - QQ: 768305206 32 | - TG: [Click to Join](https://t.me/CandyUserGroup) 33 | -------------------------------------------------------------------------------- /candy/src/tun/tun.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_TUN_TUN_H 3 | #define CANDY_TUN_TUN_H 4 | 5 | #include "core/message.h" 6 | #include "core/net.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace candy { 14 | 15 | class Client; 16 | 17 | class Tun { 18 | public: 19 | Tun(); 20 | ~Tun(); 21 | 22 | int setName(const std::string &name); 23 | int setMTU(int mtu); 24 | 25 | int run(Client *client); 26 | int wait(); 27 | 28 | IP4 getIP(); 29 | 30 | private: 31 | int setAddress(const std::string &cidr); 32 | 33 | // 处理来自 TUN 设备的数据 34 | int handleTunDevice(); 35 | 36 | // 处理来自消息队列的数据 37 | int handleTunQueue(); 38 | int handlePacket(Msg msg); 39 | int handleTunAddr(Msg msg); 40 | int handleSysRt(Msg msg); 41 | 42 | std::string tunAddress; 43 | std::thread tunThread; 44 | std::thread msgThread; 45 | 46 | private: 47 | int up(); 48 | int down(); 49 | 50 | int read(std::string &buffer); 51 | int write(const std::string &buffer); 52 | 53 | int setSysRtTable(const SysRouteEntry &entry); 54 | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop); 55 | 56 | std::shared_mutex sysRtMutex; 57 | std::list sysRtTable; 58 | 59 | private: 60 | std::any impl; 61 | 62 | private: 63 | Client &getClient(); 64 | Client *client; 65 | }; 66 | 67 | } // namespace candy 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /candy-cli/src/main.cc: -------------------------------------------------------------------------------- 1 | #include "candy/candy.h" 2 | #include "config.h" 3 | #include 4 | #include 5 | 6 | int main(int argc, char *argv[]) { 7 | arguments args; 8 | args.parse(argc, argv); 9 | auto config = args.json(); 10 | 11 | if (config.getValue("mode") == "client") { 12 | static const std::string id = "cli"; 13 | 14 | auto handler = [](int) -> void { candy::client::shutdown(id); }; 15 | 16 | signal(SIGINT, handler); 17 | signal(SIGTERM, handler); 18 | 19 | std::thread([&]() { 20 | while (true) { 21 | std::this_thread::sleep_for(std::chrono::seconds(1)); 22 | auto status = candy::client::status(id); 23 | if (status && (*status).has("address")) { 24 | std::string address = (*status).getValue("address"); 25 | if (!address.empty()) { 26 | saveTunAddress(config.getValue("name"), address); 27 | break; 28 | } 29 | } 30 | } 31 | }).detach(); 32 | 33 | candy::client::run(id, config); 34 | return 0; 35 | } 36 | 37 | if (config.getValue("mode") == "server") { 38 | auto handler = [](int) -> void { candy::server::shutdown(); }; 39 | 40 | signal(SIGINT, handler); 41 | signal(SIGTERM, handler); 42 | 43 | candy::server::run(config); 44 | return 0; 45 | } 46 | 47 | return -1; 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Candy 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 | 一个简单的组网工具. 12 | 13 | ## 如何使用 14 | 15 | - [安装 Windows 客户端](https://docs.canets.org/install-client-for-windows) 16 | - [安装 macOS 客户端](https://docs.canets.org/install-client-for-macos) 17 | - [安装 Linux 客户端](https://docs.canets.org/install-client-for-linux) 18 | - [部署 Web 服务端](https://docs.canets.org/deploy-web-server) 19 | - [部署 CLI 服务端](https://docs.canets.org/deploy-cli-server) 20 | - [使用社区服务器](https://docs.canets.org/use-the-community-server) 21 | - [多局域网组网](https://docs.canets.org/software-defined-wide-area-network) 22 | 23 | ## 相关项目 24 | 25 | - [Cacao](https://github.com/lanthora/cacao): WebUI 版的 Candy 服务器 26 | - [Cake](https://github.com/lanthora/cake): Qt 实现的 Candy GUI 桌面应用程序 27 | - [EasyTier](https://github.com/EasyTier/EasyTier): 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现 28 | 29 | ## 交流群 30 | 31 | - QQ: 768305206 32 | - TG: [Click to Join](https://t.me/CandyUserGroup) 33 | -------------------------------------------------------------------------------- /candy-service/README.md: -------------------------------------------------------------------------------- 1 | # candy-service 2 | 3 | Candy 客户端的另一个实现. 4 | 5 | - **无状态**: 进程本身不持久化任何数据, 进程重启后数据丢失,需要外部维护网络配置信息 6 | - **API 交互**: 对外提供 HTTP API 交互接口,可以远程控制和访问 7 | 8 | ## API 9 | 10 | ### 帮助 11 | 12 | Linux 13 | 14 | ```bash 15 | candy-service --help 16 | ``` 17 | 18 | Windows 19 | 20 | ```bat 21 | candy-service /help 22 | ``` 23 | 24 | 请求响应中的 **id** 用于标识网络连接, 通过不同标识可以同时加入多个网络, 这个标识用于查看状态和关闭网络. 25 | 26 | ### Run 27 | 28 | 启动参数的含义与[配置文件](../candy.cfg)相同,此外还有两个额外的配置项. 29 | 30 | - vmac: 用于标识唯一设备,当同一网络中有两台不同 vmac 的设备申请相同 IP 地址时, 后者会报告 IP 冲突. 为 16 个字符的随机数字字母字符串, 需要持久化存储, 建议在首次启动进程时生成. 31 | - expt: 期望使用的 IP 地址, 这个参数用于实现有优先分配曾经使用过的地址, 由客户端主动向服务器报告, 可以为空. 建议由服务端随机分配地址的情况下, 通过 `/api/status` 查看分配的地址并保存, 下次连接时携带这个地址. 32 | 33 | `POST /api/run` 34 | 35 | ```json 36 | { 37 | "id": "test", 38 | "config": { 39 | "mode": "client", 40 | "websocket": "wss://canets.org", 41 | "password": "", 42 | "name": "", 43 | "tun": "", 44 | "stun": "stun://stun.canets.org", 45 | "discovery": 300, 46 | "route": 5, 47 | "port": 0, 48 | "localhost": "", 49 | "mtu": 1400, 50 | "expt": "", 51 | "vmac": "16-char rand str" 52 | } 53 | } 54 | ``` 55 | 56 | ```json 57 | { 58 | "id": "test", 59 | "message": "success" 60 | } 61 | ``` 62 | 63 | ### Status 64 | 65 | `POST /api/status` 66 | 67 | ```json 68 | { 69 | "id": "test" 70 | } 71 | ``` 72 | 73 | ```json 74 | { 75 | "id": "test", 76 | "message": "success", 77 | "status": { 78 | "address": "192.168.202.1/24" 79 | } 80 | } 81 | ``` 82 | 83 | ### Shutdown 84 | 85 | `POST /api/shutdown` 86 | 87 | ```json 88 | { 89 | "id": "test" 90 | } 91 | ``` 92 | 93 | ```json 94 | { 95 | "id": "test", 96 | "message": "success" 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /scripts/search-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Define an array to store the processed dependencies 4 | declare -a processed 5 | 6 | # Define a function to get the whole list of dependencies recursively 7 | recursive_search_deps () { 8 | # Use ldd to list the dependencies and filter out the ones that are not absolute paths 9 | local list=$(ldd "$1" | awk '/=> \// {print $3}') 10 | 11 | # Loop through the dependencies 12 | for dep in $list; do 13 | # Check if the dependency has been processed before 14 | if [[ ! " ${processed[@]} " =~ " ${dep} " ]]; then 15 | # Check if the dependency contains /c/Windows in its path 16 | if [[ "$dep" =~ "/c/Windows" ]]; then 17 | # Ignore the dependency and continue the loop 18 | continue 19 | fi 20 | 21 | # Copy the dependency to the specified directory 22 | cp -n "$dep" "$2" 23 | # Output the copied file path and name 24 | echo "Copied $dep to $2" 25 | 26 | # Add the dependency to the processed array 27 | processed+=("$dep") 28 | 29 | # Recursively call the function to process the dependency's dependencies 30 | recursive_search_deps "$dep" "$2" 31 | fi 32 | done 33 | } 34 | 35 | # Check if the executable file is given as an argument 36 | if [ -z "$2" ]; then 37 | echo "Usage: $0 " 38 | exit 1 39 | fi 40 | 41 | # Create the directory if it does not exist 42 | if [ ! -d "$2" ]; then 43 | mkdir -p $2 44 | fi 45 | 46 | # Get the absolute path of the executable file 47 | exe=$(readlink -f "$1") 48 | 49 | # Copy the executable file to the target directory 50 | cp "$exe" "$2" 51 | exe=$2/$(basename "$exe") 52 | exe=$(readlink -f "$exe") 53 | 54 | # Call the function to get the whole list of dependencies recursively 55 | recursive_search_deps "$exe" "$2" 56 | -------------------------------------------------------------------------------- /candy/src/core/client.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CORE_CLIENT_H 3 | #define CANDY_CORE_CLIENT_H 4 | 5 | #include "core/message.h" 6 | #include "peer/manager.h" 7 | #include "tun/tun.h" 8 | #include "utils/atomic.h" 9 | #include "websocket/client.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace candy { 16 | 17 | class MsgQueue { 18 | public: 19 | Msg read(); 20 | void write(Msg msg); 21 | void clear(); 22 | 23 | private: 24 | std::queue msgQueue; 25 | std::mutex msgMutex; 26 | std::condition_variable msgCondition; 27 | }; 28 | 29 | class Client { 30 | public: 31 | void setName(const std::string &name); 32 | void setPassword(const std::string &password); 33 | void setWebSocket(const std::string &uri); 34 | void setTunAddress(const std::string &cidr); 35 | void setStun(const std::string &stun); 36 | void setDiscoveryInterval(int interval); 37 | void setRouteCost(int cost); 38 | void setPort(int port); 39 | void setLocalhost(std::string ip); 40 | void setMtu(int mtu); 41 | 42 | void setExptTunAddress(const std::string &cidr); 43 | void setVirtualMac(const std::string &vmac); 44 | 45 | void run(); 46 | bool isRunning(); 47 | void shutdown(); 48 | 49 | std::string getName() const; 50 | std::string getTunCidr() const; 51 | IP4 address(); 52 | 53 | private: 54 | Utils::Atomic running; 55 | 56 | public: 57 | MsgQueue &getTunMsgQueue(); 58 | MsgQueue &getPeerMsgQueue(); 59 | MsgQueue &getWsMsgQueue(); 60 | 61 | private: 62 | MsgQueue tunMsgQueue, peerMsgQueue, wsMsgQueue; 63 | 64 | Tun tun; 65 | PeerManager peerManager; 66 | WebSocketClient ws; 67 | 68 | private: 69 | std::string tunName; 70 | }; 71 | 72 | } // namespace candy 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /candy/src/peer/message.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_PEER_MESSAGE_H 3 | #define CANDY_PEER_MESSAGE_H 4 | 5 | #include "core/net.h" 6 | #include "utils/random.h" 7 | #include 8 | 9 | namespace candy { 10 | 11 | namespace PeerMsgKind { 12 | 13 | constexpr uint8_t HEARTBEAT = 0; 14 | constexpr uint8_t FORWARD = 1; 15 | constexpr uint8_t DELAY = 2; 16 | constexpr uint8_t ROUTE = 4; 17 | 18 | } // namespace PeerMsgKind 19 | 20 | struct __attribute__((packed)) StunRequest { 21 | uint8_t type[2] = {0x00, 0x01}; 22 | uint8_t length[2] = {0x00, 0x08}; 23 | uint8_t cookie[4] = {0x21, 0x12, 0xa4, 0x42}; 24 | uint32_t id[3] = {0x00}; 25 | struct __attribute__((packed)) { 26 | uint8_t type[2] = {0x00, 0x03}; 27 | uint8_t length[2] = {0x00, 0x04}; 28 | uint8_t notset[4] = {0x00}; 29 | } attr; 30 | 31 | StunRequest() { 32 | id[0] = randomUint32(); 33 | id[1] = randomUint32(); 34 | id[2] = randomUint32(); 35 | } 36 | }; 37 | 38 | struct __attribute__((packed)) StunResponse { 39 | uint16_t type; 40 | uint16_t length; 41 | uint32_t cookie; 42 | uint8_t id[12]; 43 | uint8_t attr[0]; 44 | }; 45 | 46 | namespace PeerMsg { 47 | 48 | struct __attribute__((packed)) Heartbeat { 49 | uint8_t kind; 50 | IP4 tunip; 51 | IP4 ip; 52 | uint16_t port; 53 | uint8_t ack; 54 | }; 55 | 56 | struct __attribute__((packed)) Forward { 57 | uint8_t type; 58 | IP4Header iph; 59 | 60 | static std::string create(const std::string &packet); 61 | }; 62 | 63 | struct __attribute__((packed)) Delay { 64 | uint8_t type; 65 | IP4 src; 66 | IP4 dst; 67 | int64_t timestamp; 68 | }; 69 | 70 | struct __attribute__((packed)) Route { 71 | uint8_t type; 72 | IP4 dst; 73 | IP4 next; 74 | int32_t rtt; 75 | }; 76 | 77 | } // namespace PeerMsg 78 | 79 | } // namespace candy 80 | 81 | #endif 82 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | format: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout 12 | uses: actions/checkout@v4 13 | - name: check format 14 | uses: jidicula/clang-format-action@v4.11.0 15 | with: 16 | check-path: 'src' 17 | exclude-regex: 'argparse.h' 18 | 19 | linux: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@v4 24 | - name: build 25 | run: docker build . 26 | 27 | macos: 28 | runs-on: macos-latest 29 | steps: 30 | - name: depends 31 | run: brew update && brew install fmt poco spdlog 32 | - name: checkout 33 | uses: actions/checkout@v4 34 | - name: build 35 | run: | 36 | if [ "$RUNNER_ARCH" == "ARM64" ]; then 37 | export CPATH=/opt/homebrew/include 38 | export LIBRARY_PATH=/opt/homebrew/lib 39 | else 40 | export CPATH=/usr/local/include 41 | export LIBRARY_PATH=/usr/local/lib 42 | fi 43 | cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo 44 | cmake --build build 45 | 46 | windows: 47 | runs-on: windows-latest 48 | steps: 49 | - name: depends 50 | uses: msys2/setup-msys2@v2 51 | with: 52 | msystem: MINGW64 53 | update: true 54 | install: >- 55 | mingw-w64-x86_64-cmake 56 | mingw-w64-x86_64-ninja 57 | mingw-w64-x86_64-gcc 58 | mingw-w64-x86_64-spdlog 59 | mingw-w64-x86_64-poco 60 | - name: checkout 61 | uses: actions/checkout@v4 62 | - name: cache 63 | uses: actions/cache@v4 64 | with: 65 | path: build 66 | key: ${{ hashFiles('CMakeLists.txt') }} 67 | - name: build 68 | shell: msys2 {0} 69 | run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build 70 | -------------------------------------------------------------------------------- /candy/src/websocket/server.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_WEBSOCKET_SERVER_H 3 | #define CANDY_WEBSOCKET_SERVER_H 4 | 5 | #include "core/net.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace candy { 14 | 15 | struct WsCtx { 16 | Poco::Net::WebSocket *ws; 17 | 18 | std::string buffer; 19 | int status; 20 | 21 | IP4 ip; 22 | std::string vmac; 23 | 24 | void sendFrame(const std::string &frame, int flags = Poco::Net::WebSocket::FRAME_BINARY); 25 | }; 26 | 27 | struct SysRoute { 28 | // 通过地址和掩码确定策略下发给哪些客户端 29 | Address dev; 30 | // 系统路由策略中的地址掩码和下一跳 31 | Address dst; 32 | IP4 next; 33 | }; 34 | 35 | class WebSocketServer { 36 | public: 37 | int setWebSocket(const std::string &uri); 38 | int setPassword(const std::string &password); 39 | int setDHCP(const std::string &cidr); 40 | int setSdwan(const std::string &sdwan); 41 | int run(); 42 | int shutdown(); 43 | 44 | private: 45 | std::string host; 46 | uint16_t port; 47 | std::string password; 48 | Address dhcp; 49 | std::list routes; 50 | 51 | private: 52 | void handleMsg(WsCtx &ctx); 53 | void handleAuthMsg(WsCtx &ctx); 54 | void handleForwardMsg(WsCtx &ctx); 55 | void handleExptTunMsg(WsCtx &ctx); 56 | void handleUdp4ConnMsg(WsCtx &ctx); 57 | void handleVMacMsg(WsCtx &ctx); 58 | void handleDiscoveryMsg(WsCtx &ctx); 59 | void HandleGeneralMsg(WsCtx &ctx); 60 | 61 | // 更新客户端系统路由 62 | void updateSysRoute(WsCtx &ctx); 63 | 64 | // 保存 IP 到对应连接指针的映射 65 | std::unordered_map ipCtxMap; 66 | // 操作 map 时需要加锁,以确保操作时指针有效 67 | std::shared_mutex ipCtxMutex; 68 | 69 | bool running; 70 | 71 | private: 72 | // 开始监听,新的请求将调用 handleWebsocket 73 | int listen(); 74 | // 同步的处理每个客户独的请求,函数返回后连接将断开 75 | void handleWebsocket(Poco::Net::WebSocket &ws); 76 | 77 | std::shared_ptr httpServer; 78 | }; 79 | 80 | } // namespace candy 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /candy/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(candy-library) 2 | 3 | file(GLOB_RECURSE SOURCES "src/*.cc") 4 | target_sources(candy-library PRIVATE ${SOURCES}) 5 | 6 | target_include_directories(candy-library PUBLIC 7 | $ 8 | $ 9 | $ 10 | ) 11 | 12 | if (${CANDY_STATIC_OPENSSL}) 13 | target_link_libraries(candy-library PRIVATE ${OPENSSL_LIB_CRYPTO} ${OPENSSL_LIB_SSL}) 14 | else() 15 | target_link_libraries(candy-library PRIVATE OpenSSL::SSL OpenSSL::Crypto) 16 | endif() 17 | 18 | target_link_libraries(candy-library PRIVATE spdlog::spdlog) 19 | target_link_libraries(candy-library PRIVATE Poco::Foundation Poco::JSON Poco::Net Poco::NetSSL) 20 | target_link_libraries(candy-library PRIVATE Threads::Threads) 21 | 22 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") 23 | target_link_libraries(candy-library PRIVATE ws2_32) 24 | endif() 25 | 26 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") 27 | target_link_libraries(candy-library PRIVATE iphlpapi) 28 | target_link_libraries(candy-library PRIVATE ws2_32) 29 | 30 | set(WINTUN_VERSION 0.14.1) 31 | set(WINTUN_ZIP wintun-${WINTUN_VERSION}.zip) 32 | set(WINTUN_URL https://www.wintun.net/builds/${WINTUN_ZIP}) 33 | 34 | if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP}) 35 | file(DOWNLOAD ${WINTUN_URL} ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP} STATUS DOWNLOAD_STATUS) 36 | list(GET DOWNLOAD_STATUS 0 STATUS_CODE) 37 | list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE) 38 | 39 | if(${STATUS_CODE} EQUAL 0) 40 | message(STATUS "wintun download success") 41 | else() 42 | message(FATAL_ERROR "wintun download failed: ${ERROR_MESSAGE}") 43 | endif() 44 | else() 45 | message(STATUS "use wintun cache") 46 | endif() 47 | 48 | file(ARCHIVE_EXTRACT INPUT ${CMAKE_CURRENT_BINARY_DIR}/${WINTUN_ZIP}) 49 | 50 | include_directories(${CMAKE_CURRENT_BINARY_DIR}/wintun/include) 51 | endif() 52 | 53 | set_target_properties(candy-library PROPERTIES OUTPUT_NAME "candy") 54 | 55 | add_library(Candy::Library ALIAS candy-library) 56 | -------------------------------------------------------------------------------- /candy/src/peer/peer.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_PEER_PEER_H 3 | #define CANDY_PEER_PEER_H 4 | 5 | #include "core/net.h" 6 | #include "utils/random.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace candy { 18 | 19 | class PeerManager; 20 | 21 | constexpr int32_t RTT_LIMIT = INT32_MAX; 22 | constexpr int32_t RETRY_MIN = 30; 23 | constexpr int32_t RETRY_MAX = 3600; 24 | 25 | using Poco::Net::SocketAddress; 26 | 27 | enum class PeerState { 28 | INIT, 29 | PREPARING, 30 | SYNCHRONIZING, 31 | CONNECTING, 32 | CONNECTED, 33 | WAITING, 34 | FAILED, 35 | }; 36 | 37 | class Peer { 38 | public: 39 | Peer(const IP4 &addr, PeerManager *peerManager); 40 | ~Peer(); 41 | 42 | void tick(); 43 | void tryConnecct(); 44 | void handleStunResponse(); 45 | void handlePubInfo(IP4 ip, uint16_t port, bool local = false); 46 | 47 | void handleHeartbeatMessage(const SocketAddress &address, uint8_t heartbeatAck); 48 | int sendEncrypted(const std::string &buffer); 49 | std::optional isConnected() const; 50 | 51 | int32_t rtt = RTT_LIMIT; 52 | uint32_t tickCount = randomUint32(); 53 | 54 | private: 55 | PeerManager &getManager(); 56 | PeerManager *peerManager; 57 | 58 | std::optional encrypt(const std::string &plaintext); 59 | std::shared_ptr encryptCtx; 60 | std::mutex encryptCtxMutex; 61 | std::string key; 62 | 63 | std::string stateString() const; 64 | std::string stateString(PeerState state) const; 65 | bool updateState(PeerState state); 66 | void resetState(); 67 | bool checkActivityWithin(std::chrono::system_clock::duration duration); 68 | PeerState state = PeerState::INIT; 69 | uint8_t ack = 0; 70 | int32_t retry = RETRY_MIN; 71 | std::chrono::system_clock::time_point lastActiveTime; 72 | 73 | int send(const std::string &buffer); 74 | void sendHeartbeatMessage(); 75 | void sendDelayMessage(); 76 | 77 | std::optional wide, local, real; 78 | std::shared_mutex socketAddressMutex; 79 | 80 | IP4 addr; 81 | }; 82 | 83 | } // namespace candy 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /.github/workflows/standalone.yaml: -------------------------------------------------------------------------------- 1 | name: standalone 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [ published ] 7 | pull_request: 8 | branches: [master] 9 | paths: 10 | - 'scripts/build-standalone.sh' 11 | - 'scripts/standalone.json' 12 | 13 | jobs: 14 | configure: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | matrix: ${{ steps.fetch.outputs.matrix }} 18 | steps: 19 | - name: Checkout to repository 20 | uses: actions/checkout@v4 21 | - name: fetch matrix data 22 | id: fetch 23 | run: echo "matrix=$(jq -c . < scripts/standalone.json)" >> $GITHUB_OUTPUT 24 | build: 25 | runs-on: ubuntu-latest 26 | needs: configure 27 | strategy: 28 | fail-fast: false 29 | matrix: ${{ fromJson(needs.configure.outputs.matrix) }} 30 | env: 31 | WORKSPACE: "/opt" 32 | steps: 33 | - name: checkout 34 | uses: actions/checkout@v4 35 | - name: Install UPX 36 | uses: crazy-max/ghaction-upx@v3 37 | with: 38 | install-only: true 39 | - name: cache 40 | uses: actions/cache@v4 41 | with: 42 | path: ${{ env.WORKSPACE }}/toolchains 43 | key: ${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('scripts/build-standalone.sh') }} 44 | - name: Cross compile 45 | run: | 46 | ./scripts/build-standalone.sh 47 | env: 48 | CANDY_WORKSPACE: ${{ env.WORKSPACE }} 49 | CANDY_OS: ${{ matrix.os }} 50 | CANDY_ARCH: ${{ matrix.arch }} 51 | CANDY_STRIP: "0" 52 | CANDY_UPX: "0" 53 | CANDY_TGZ: "1" 54 | - name: upload 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: candy-${{ matrix.os }}-${{ matrix.arch }} 58 | path: ${{ env.WORKSPACE }}/output/${{ matrix.os }}-${{ matrix.arch }} 59 | - name: prepare package 60 | shell: bash 61 | if: github.event_name == 'release' 62 | run: | 63 | GIT_TAG=${{ github.event.release.tag_name }} 64 | PKG_PATH=${{ env.WORKSPACE }}/output/candy_${GIT_TAG#v}+${{ matrix.os }}_${{ matrix.arch }}.tar.gz 65 | mv ${{ env.WORKSPACE }}/output/candy-${{ matrix.os }}-${{ matrix.arch }}.tar.gz $PKG_PATH 66 | echo "PKG_PATH=$PKG_PATH" >> $GITHUB_ENV 67 | - name: release 68 | uses: softprops/action-gh-release@v2 69 | if: github.event_name == 'release' 70 | with: 71 | files: ${{ env.PKG_PATH }} 72 | -------------------------------------------------------------------------------- /candy/src/websocket/client.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_WEBSOCKET_CLIENT_H 3 | #define CANDY_WEBSOCKET_CLIENT_H 4 | 5 | #include "core/message.h" 6 | #include "core/net.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace candy { 14 | 15 | class Client; 16 | 17 | class WebSocketClient { 18 | public: 19 | int setName(const std::string &name); 20 | int setPassword(const std::string &password); 21 | int setWsServerUri(const std::string &uri); 22 | int setExptTunAddress(const std::string &cidr); 23 | int setAddress(const std::string &cidr); 24 | int setVirtualMac(const std::string &vmac); 25 | int setTunUpdateCallback(std::function callback); 26 | 27 | std::string getTunCidr() const; 28 | 29 | int run(Client *client); 30 | int wait(); 31 | 32 | private: 33 | void handleWsQueue(); 34 | void handlePacket(Msg msg); 35 | void handlePubInfo(Msg msg); 36 | void handleDiscovery(Msg msg); 37 | 38 | std::thread msgThread; 39 | 40 | int handleWsConn(); 41 | void handleWsMsg(std::string buffer); 42 | void handleForwardMsg(std::string buffer); 43 | void handleExptTunMsg(std::string buffer); 44 | void handleUdp4ConnMsg(std::string buffer); 45 | void handleDiscoveryMsg(std::string buffer); 46 | void handleRouteMsg(std::string buffer); 47 | void handleGeneralMsg(std::string buffer); 48 | std::thread wsThread; 49 | 50 | void sendFrame(const std::string &buffer, int flags = Poco::Net::WebSocket::FRAME_BINARY); 51 | void sendFrame(const void *buffer, int length, int flags = Poco::Net::WebSocket::FRAME_BINARY); 52 | 53 | void sendVirtualMacMsg(); 54 | void sendExptTunMsg(); 55 | void sendAuthMsg(); 56 | void sendDiscoveryMsg(IP4 dst); 57 | 58 | std::function addressUpdateCallback; 59 | 60 | private: 61 | std::string hostName(); 62 | void sendPingMessage(); 63 | 64 | private: 65 | int connect(); 66 | int disconnect(); 67 | 68 | std::shared_ptr ws; 69 | std::string pingMessage; 70 | int64_t timestamp; 71 | 72 | private: 73 | std::string wsServerUri; 74 | std::string exptTunCidr; 75 | std::string tunCidr; 76 | std::string vmac; 77 | std::string name; 78 | std::string password; 79 | 80 | Client &getClient(); 81 | Client *client; 82 | }; 83 | 84 | } // namespace candy 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /docs/install-client-for-linux.md: -------------------------------------------------------------------------------- 1 | # 安装 Linux 客户端 2 | 3 | 我们针对不同 Linux 发行版提供了多种格式的安装包.对于暂未支持的发行版,可以选择容器部署或者静态链接的可执行文件. 4 | 我们致力于支持所有架构的 Linux 系统. 5 | 6 | ## Docker 7 | 8 | 镜像已上传 [Docker Hub](https://hub.docker.com/r/lanthora/candy) 和 [Github Packages](https://github.com/lanthora/candy/pkgs/container/candy). 9 | 10 | 获取最新镜像 11 | 12 | ```bash 13 | docker pull docker.io/lanthora/candy:latest 14 | ``` 15 | 16 | 容器需要管理员权限读取设备创建虚拟网卡并设置路由,同时需要 Host 网络命名空间共享虚拟网卡. 17 | 18 | 以默认配置文件启动将加入社区网络.指定的参数为 `--rm` 当进程结束时会自动销毁容器,且日志会在控制台输出,这有利于初次运行调试. 19 | 20 | ```bash 21 | docker run --rm --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy docker.io/lanthora/candy:latest 22 | ``` 23 | 24 | 以自定义配置文件启动.请在[默认配置](https://raw.githubusercontent.com/lanthora/candy/refs/heads/master/candy.cfg)基础上自定义配置文件. 25 | 26 | ```bash 27 | docker run --rm --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy --volume /path/to/candy.cfg:/etc/candy.cfg docker.io/lanthora/candy:latest 28 | ``` 29 | 30 | 一切正常后,以守护进程的形式启动. 31 | 32 | ```bash 33 | docker run --detach --restart=always --privileged=true --net=host --volume /var/lib/candy:/var/lib/candy --volume /path/to/candy.cfg:/etc/candy.cfg docker.io/lanthora/candy:latest 34 | ``` 35 | 36 | ## Arch Linux 37 | 38 | 使用 [AUR](https://aur.archlinux.org/packages/candy) 或者 [archlinuxcn](https://github.com/archlinuxcn/repo/tree/master/archlinuxcn/candy) 仓库 39 | 40 | ```bash 41 | # AUR 42 | paru candy 43 | # archlinuxcn 44 | pacman -S candy 45 | ``` 46 | 47 | ## Gentoo 48 | 49 | ```bash 50 | emerge --sync gentoo && emerge -av candy 51 | ``` 52 | 53 | ## 单文件可执行程序 54 | 55 | 当上述所有方式都不适用时,尝试[单文件可执行程序](https://github.com/lanthora/candy/releases/latest). 56 | 57 | 该程序由[交叉编译脚本](https://github.com/lanthora/candy/tree/master/scripts/build-standalone.sh)构建. 58 | 59 | 如果你的系统在使用 Systemd 管理进程.请复制以下文件到指定目录. 60 | 61 | ```bash 62 | cp candy.service /usr/lib/systemd/system/candy.service 63 | cp candy@.service /usr/lib/systemd/system/candy@.service 64 | cp candy.cfg /etc/candy.cfg 65 | ``` 66 | 67 | 然后按照后续进程管理的方式管理进程. 68 | 69 | 判断 Systemd 的方法: 检查 `ps -p 1 -o comm=` 输出的内容里是否为 systemd 70 | 71 | ## 进程管理 72 | 73 | 各发行版安装后自带 Service 文件,强烈建议使用 Systemd 管理进程,不要使用自己编写的脚本. 74 | 75 | 对于自定义配置的用户,可以通过以下方式启动进程,不要修改默认配置. 76 | 77 | ```bash 78 | mkdir /etc/candy.d 79 | # 复制一份默认配置,并修改.文件名为 one.cfg 80 | cp /etc/candy.cfg /etc/candy.d/one.cfg 81 | # 以 one.cfg 为配置启动进程 82 | systemctl start candy@one 83 | 84 | # 复制一份默认配置,并修改.文件名为 two.cfg 85 | # 需要注意不同配置文件中的 name 字段不能重复 86 | cp /etc/candy.cfg /etc/candy.d/two.cfg 87 | # 以 two.cfg 为配置启动进程 88 | systemctl start candy@two 89 | ``` 90 | -------------------------------------------------------------------------------- /candy/src/websocket/message.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_WEBSOCKET_MESSAGE_H 3 | #define CANDY_WEBSOCKET_MESSAGE_H 4 | 5 | #include "core/net.h" 6 | #include 7 | 8 | namespace candy { 9 | 10 | namespace WsMsgKind { 11 | constexpr uint8_t AUTH = 0; 12 | constexpr uint8_t FORWARD = 1; 13 | constexpr uint8_t EXPTTUN = 2; 14 | constexpr uint8_t UDP4CONN = 3; 15 | constexpr uint8_t VMAC = 4; 16 | constexpr uint8_t DISCOVERY = 5; 17 | constexpr uint8_t ROUTE = 6; 18 | constexpr uint8_t GENERAL = 255; 19 | } // namespace WsMsgKind 20 | 21 | namespace GeSubType { 22 | constexpr uint8_t LOCALUDP4CONN = 0; 23 | } 24 | 25 | namespace WsMsg { 26 | 27 | struct __attribute__((packed)) Auth { 28 | uint8_t type; 29 | IP4 ip; 30 | int64_t timestamp; 31 | uint8_t hash[SHA256_DIGEST_LENGTH]; 32 | 33 | Auth(IP4 ip); 34 | void updateHash(const std::string &password); 35 | bool check(const std::string &password); 36 | }; 37 | 38 | struct __attribute__((packed)) Forward { 39 | uint8_t type; 40 | IP4Header iph; 41 | 42 | Forward(); 43 | }; 44 | 45 | struct __attribute__((packed)) ExptTun { 46 | uint8_t type; 47 | int64_t timestamp; 48 | char cidr[32] = {0}; 49 | uint8_t hash[SHA256_DIGEST_LENGTH]; 50 | 51 | ExptTun(const std::string &cidr); 52 | void updateHash(const std::string &password); 53 | bool check(const std::string &password); 54 | }; 55 | 56 | struct __attribute__((packed)) Conn { 57 | uint8_t type; 58 | IP4 src; 59 | IP4 dst; 60 | IP4 ip; 61 | uint16_t port; 62 | 63 | Conn(); 64 | }; 65 | 66 | struct __attribute__((packed)) VMac { 67 | uint8_t type; 68 | uint8_t vmac[16]; 69 | int64_t timestamp; 70 | uint8_t hash[SHA256_DIGEST_LENGTH]; 71 | 72 | VMac(const std::string &vmac); 73 | void updateHash(const std::string &password); 74 | bool check(const std::string &password); 75 | }; 76 | 77 | struct __attribute__((packed)) Discovery { 78 | uint8_t type; 79 | IP4 src; 80 | IP4 dst; 81 | 82 | Discovery(); 83 | }; 84 | 85 | struct __attribute__((packed)) SysRoute { 86 | uint8_t type; 87 | uint8_t size; 88 | uint16_t reserved; 89 | SysRouteEntry rtTable[0]; 90 | }; 91 | 92 | struct __attribute__((packed)) General { 93 | uint8_t type; 94 | uint8_t subtype; 95 | uint16_t extra; 96 | IP4 src; 97 | IP4 dst; 98 | 99 | General(); 100 | }; 101 | 102 | struct __attribute__((packed)) ConnLocal { 103 | General ge; 104 | IP4 ip; 105 | uint16_t port; 106 | 107 | ConnLocal(); 108 | }; 109 | 110 | } // namespace WsMsg 111 | } // namespace candy 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /candy.cfg: -------------------------------------------------------------------------------- 1 | ############################## Client and Server ############################## 2 | # [Required] Working mode, "client" or "server" 3 | mode = "client" 4 | 5 | # [Required] The address that the server listens on 6 | # Server only supports ws and needs to provide wss through an external web 7 | # service. Client supports ws and wss. 8 | websocket = "wss://canets.org" 9 | 10 | # [Optional] Password used to verify identity 11 | # Only the hashed content of the password and timestamp is transmitted on the 12 | # network, and the password cannot be obtained from the message. 13 | #password = "this is the password" 14 | 15 | # [Optional] Show debug log 16 | #debug = false 17 | 18 | ################################# Server Only ################################# 19 | # [Optional] The range of addresses automatically assigned by the server 20 | # Server address allocation is not enabled by default, and the client needs to 21 | # configure a static address through tun. 22 | #dhcp = "192.168.202.0/24" 23 | 24 | # [Optional] software-defined wide area network 25 | # IP packets entering 192.168.202.1/32 with the destination address 172.17.0.0/16 26 | # will be forwarded to 192.168.202.2. Multiple rules are separated by semicolons. 27 | # Extraneous whitespace characters are prohibited. 28 | #sdwan = "192.168.202.1/32,172.17.0.0/16,192.168.202.2;192.168.202.2/32,172.16.0.0/16,192.168.202.1" 29 | 30 | ################################# Client Only ################################# 31 | # [Optional] Network interface name 32 | # Used to differentiate networks when running multiple clients. 33 | #name = "" 34 | 35 | # [Optional] Static address 36 | # If dhcp is not configured, tun must be configured. When there is an address 37 | # conflict, the previous client will be kicked out. 38 | #tun = "192.168.202.1/24" 39 | 40 | # [Optional] STUN server address 41 | stun = "stun://stun.canets.org" 42 | 43 | # [Optional] Active discovery interval 44 | # Periodically sends broadcasts to try to establish P2P with devices on the 45 | # network. The default configuration is 0, which means disabled. 46 | discovery = 300 47 | 48 | # [Optional] The cost of routing through this machine 49 | # Use all nodes in the network to establish the link with the lowest latency. 50 | # This configuration represents the cost of using this node as a relay. The 51 | # default configuration is 0 which means disabled. 52 | route = 5 53 | 54 | # [Optional] Local UDP port used for P2P 55 | # The default configuration is 0, which means it is allocated by the operating 56 | # system. This configuration can be used when the firewall is strict and can 57 | # only open specific ports. 58 | #port = 0 59 | 60 | # [Optional] Local IPv4 address used for peering connections 61 | # By default the IPv4 address of the local physical network card will be 62 | # detected. When there are multiple physical network cards, the detection 63 | # results may not be the best. You can specify it manually. 64 | #localhost = "127.0.0.1" 65 | 66 | # [Optional] Maximum Transmission Unit 67 | #mtu=1400 68 | -------------------------------------------------------------------------------- /docs/software-defined-wide-area-network.md: -------------------------------------------------------------------------------- 1 | # 多局域网组网 2 | 3 | ## 需求 4 | 5 | 在多地有多个局域网时,希望能够让本局域网内的设备通过其他局域网的地址直接访问对方的设备,并且无需在所有设备上部署 Candy 客户端. 6 | 7 | ## 示例 8 | 9 | 首先你需要: 10 | 11 | - 有一个独立的网络.可以自建服务端或者使用社区服务器 12 | - 在网关 (Gateway) 上部署 Candy 并成功加入自己创建的网络 13 | 14 | 以 LAN A 为例解释表格含义. 15 | 16 | - 局域网 (Network) 地址为 `172.16.1.0/24`, 这个地址不能与 B,C 冲突 17 | - 网关 (Gateway) 可以是路由器,也可以是局域网中任意一台 Linux 系统,但需要能够部署 Candy 客户端,假设它在局域网中的地址是 `172.16.1.1`. 通过给局域网中的设备配置路由,确保流量能够进入网关 18 | - Candy 客户端部署在网关上,它在虚拟网络中的地址是 `192.168.202.1` 19 | 20 | | LAN | A | B | C | 21 | | :------ | :------------ | :------------ | :------------ | 22 | | Network | 172.16.1.0/24 | 172.16.2.0/24 | 172.16.3.0/24 | 23 | | Gateway | 172.16.1.1 | 172.16.2.1 | 172.16.3.1 | 24 | | Candy | 192.168.202.1 | 192.168.202.2 | 192.168.202.3 | 25 | 26 | 当 `172.16.1.0/24` 的设备访问 `172.16.2.0/24` 的设备时,希望流量可以通过以下方式送达: 27 | 28 | ```txt 29 | 172.16.1.0/24 <=> 172.16.1.1 <=> 192.168.202.1 <=> 192.168.202.2 <=> 172.16.2.1 <=> 172.16.2.0/24 30 | ``` 31 | 32 | ### 流量转发到网关 (172.16.1.0/24 => 172.16.1.1) 33 | 34 | 如果网关是路由器,不需要任何操作,流量就应该能够进入网关.否则需要在非网关设备上配置流量转发到网关的路由. 35 | 36 | 给 172.16.1.0/24 的设备配置路由: 37 | 38 | - dst: 172.16.2.0/24; gw: 172.16.1.1 39 | - dst: 172.16.3.0/24; gw: 172.16.1.1 40 | 41 | 需要用同样的方式给另外两个局域网做配置. 42 | 43 | ### 允许网关转发流量 (172.16.1.1 <=> 192.168.202.1) 44 | 45 | #### Linux 46 | 47 | 如果你的网关是路由器,应该能够轻易的配置出允许转发.否则需要手动添加转发相关的配置. 48 | 49 | 开启内核流量转发功能 50 | 51 | ```bash 52 | sysctl -w net.ipv4.ip_forward=1 53 | ``` 54 | 55 | 开启动态伪装并接受转发报文. 56 | 57 | ```bash 58 | iptables -t nat -A POSTROUTING -j MASQUERADE 59 | iptables -A FORWARD -j ACCEPT 60 | ``` 61 | 62 | #### Windows 63 | 64 | 查看的网卡名,应该与配置文件中写的名称相同,对于 GUI 版本客户端的默认配置网卡名应该为 `candy` 65 | 66 | ```ps 67 | Get-NetAdapter 68 | ``` 69 | 70 | 允许转发,注意要把网卡名替换成上一步查出来的网卡名 71 | 72 | ```ps 73 | Set-NetIPInterface -ifAlias 'candy' -Forwarding Enabled 74 | ``` 75 | 76 | #### macOS 77 | 78 | 应该不会有人拿 macOS 做网关吧, Windows 应该都没有多少人,有需要再补充这部分文档 79 | 80 | ### 创建虚拟链路 (172.16.1.0/24 <=> 172.16.2.0/24) 81 | 82 | 所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.1.0/24` 的 IP 报文时,将其转发到 `192.168.202.1`; 83 | 所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.2.0/24` 的 IP 报文时,将其转发到 `192.168.202.2`; 84 | 所有 Candy 客户端 `192.168.202.0/24` 收到发往 `172.16.3.0/24` 的 IP 报文时,将其转发到 `192.168.202.3`; 85 | 86 | 策略会发下给属于 `192.168.202.0/24` 网络的客户端,上面的配置下发给了虚拟网络中的所有设备,能够满足大部分用户场景. 87 | 88 | 此外支持更细粒度的控制供用户选择,例如 `192.168.202.1/32` 就表示仅把路由策略下发给 `192.168.202.1` 这台设备. 89 | 90 | #### Cacao 配置 91 | 92 | 如果你在使用 Cacao 服务端(例如社区服务端),配置如下. 93 | 94 | ![sdwan](images/sdwan.png) 95 | 96 | #### Candy 配置 97 | 98 | 如果你在使用命令行版本的 Candy 服务端,等效配置如下. 99 | 100 | ```ini 101 | sdwan = "192.168.202.0/24,172.16.1.0/24,192.168.202.1;192.168.202.0/24,172.16.2.0/24,192.168.202.2;192.168.202.0/24,172.16.3.0/24,192.168.202.3;" 102 | ``` 103 | 104 | ### 测试 105 | 106 | 此时局域网设备之间应当可以相互 ping 通. 107 | 108 | ## 常见问题 109 | 110 | ### 能 ping 通网关,但 ping 不通网关下的目标设备 111 | 112 | - 检查 iptables 配置的动态伪装是否生效.如果生效,抓包可以看到发往目标设备的源地址已经改成了网关地址 113 | - 检查目标设备防火墙.例如 Windows 系统防火墙默认禁止 ping, 此时直接尝试访问 Windows 提供出的服务,例如远程桌面, SSH, Web 服务等 114 | 115 | ### 能 ping 通目标设备,但不能访问服务 116 | 117 | - 检查 iptables 配置的动态伪装是否生效.动态伪装不生效的情况下,某种路由配置规则也可以实现 ping 通目标设备,但是防火墙会拦截对应报文. 118 | 119 | ### 关于源进源出 120 | 121 | 通过合理的路由配置和对防火墙策略的调整,在不使用动态伪装的情况下,可以做到在目标设备看到请求的真实源地址.想要达成这个效果需要有足够的计算机网络知识储备,请自行探索. 122 | 123 | -------------------------------------------------------------------------------- /candy/src/core/client.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "core/client.h" 3 | #include "core/message.h" 4 | #include 5 | #include 6 | 7 | namespace candy { 8 | 9 | Msg MsgQueue::read() { 10 | std::unique_lock lock(msgMutex); 11 | if (!msgCondition.wait_for(lock, std::chrono::seconds(1), [this] { return !msgQueue.empty(); })) { 12 | return Msg(MsgKind::TIMEOUT); 13 | } 14 | 15 | Msg msg = std::move(msgQueue.front()); 16 | msgQueue.pop(); 17 | return msg; 18 | } 19 | 20 | void MsgQueue::write(Msg msg) { 21 | { 22 | std::unique_lock lock(this->msgMutex); 23 | msgQueue.push(std::move(msg)); 24 | } 25 | msgCondition.notify_one(); 26 | } 27 | 28 | void MsgQueue::clear() { 29 | std::unique_lock lock(this->msgMutex); 30 | while (!msgQueue.empty()) { 31 | msgQueue.pop(); 32 | } 33 | } 34 | 35 | void Client::setName(const std::string &name) { 36 | this->tunName = name; 37 | tun.setName(name); 38 | ws.setName(name); 39 | } 40 | 41 | std::string Client::getName() const { 42 | return this->tunName; 43 | } 44 | 45 | std::string Client::getTunCidr() const { 46 | return ws.getTunCidr(); 47 | } 48 | 49 | IP4 Client::address() { 50 | return this->tun.getIP(); 51 | } 52 | 53 | MsgQueue &Client::getTunMsgQueue() { 54 | return this->tunMsgQueue; 55 | } 56 | 57 | MsgQueue &Client::getPeerMsgQueue() { 58 | return this->peerMsgQueue; 59 | } 60 | 61 | MsgQueue &Client::getWsMsgQueue() { 62 | return this->wsMsgQueue; 63 | } 64 | 65 | void Client::setPassword(const std::string &password) { 66 | ws.setPassword(password); 67 | peerManager.setPassword(password); 68 | } 69 | 70 | void Client::setWebSocket(const std::string &uri) { 71 | ws.setWsServerUri(uri); 72 | } 73 | 74 | void Client::setTunAddress(const std::string &cidr) { 75 | ws.setAddress(cidr); 76 | } 77 | 78 | void Client::setExptTunAddress(const std::string &cidr) { 79 | ws.setExptTunAddress(cidr); 80 | } 81 | 82 | void Client::setVirtualMac(const std::string &vmac) { 83 | ws.setVirtualMac(vmac); 84 | } 85 | 86 | void Client::setStun(const std::string &stun) { 87 | peerManager.setStun(stun); 88 | } 89 | 90 | void Client::setDiscoveryInterval(int interval) { 91 | peerManager.setDiscoveryInterval(interval); 92 | } 93 | 94 | void Client::setRouteCost(int cost) { 95 | peerManager.setRouteCost(cost); 96 | } 97 | 98 | void Client::setPort(int port) { 99 | peerManager.setPort(port); 100 | } 101 | 102 | void Client::setLocalhost(std::string ip) { 103 | peerManager.setLocalhost(ip); 104 | } 105 | 106 | void Client::setMtu(int mtu) { 107 | tun.setMTU(mtu); 108 | } 109 | 110 | void Client::run() { 111 | this->running.store(true); 112 | 113 | if (ws.run(this)) { 114 | return; 115 | } 116 | if (tun.run(this)) { 117 | return; 118 | } 119 | if (peerManager.run(this)) { 120 | return; 121 | } 122 | 123 | ws.wait(); 124 | tun.wait(); 125 | peerManager.wait(); 126 | 127 | wsMsgQueue.clear(); 128 | tunMsgQueue.clear(); 129 | peerMsgQueue.clear(); 130 | } 131 | 132 | bool Client::isRunning() { 133 | return this->running.load(); 134 | } 135 | 136 | void Client::shutdown() { 137 | this->running.store(false); 138 | } 139 | 140 | } // namespace candy 141 | -------------------------------------------------------------------------------- /candy/src/core/net.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_CORE_NET_H 3 | #define CANDY_CORE_NET_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace candy { 12 | 13 | template typename std::enable_if::value, T>::type byteswap(T value) { 14 | static_assert(std::is_integral::value, "byteswap requires integral type"); 15 | 16 | union { 17 | T value; 18 | uint8_t bytes[sizeof(T)]; 19 | } src, dst; 20 | 21 | src.value = value; 22 | for (size_t i = 0; i < sizeof(T); i++) { 23 | dst.bytes[i] = src.bytes[sizeof(T) - i - 1]; 24 | } 25 | return dst.value; 26 | } 27 | 28 | template T ntoh(T v) { 29 | static_assert(std::is_integral::value, "ntoh requires integral type"); 30 | 31 | uint8_t *bytes = reinterpret_cast(&v); 32 | bool isLittleEndian = true; 33 | { 34 | uint16_t test = 0x0001; 35 | isLittleEndian = (*reinterpret_cast(&test) == 0x01); 36 | } 37 | 38 | if (isLittleEndian) { 39 | return byteswap(v); 40 | } 41 | return v; 42 | } 43 | 44 | template T hton(T v) { 45 | return ntoh(v); 46 | } 47 | 48 | class __attribute__((packed)) IP4 { 49 | public: 50 | IP4(const std::string &ip = "0.0.0.0"); 51 | IP4 operator=(const std::string &ip); 52 | IP4 operator&(IP4 another) const; 53 | IP4 operator|(IP4 another) const; 54 | IP4 operator^(IP4 another) const; 55 | IP4 operator~() const; 56 | bool operator==(IP4 another) const; 57 | operator std::string() const; 58 | operator uint32_t() const; 59 | IP4 next() const; 60 | int fromString(const std::string &ip); 61 | std::string toString() const; 62 | int fromPrefix(int prefix); 63 | int toPrefix(); 64 | bool empty() const; 65 | void reset(); 66 | 67 | private: 68 | std::array raw; 69 | }; 70 | 71 | struct __attribute__((packed)) IP4Header { 72 | uint8_t version_ihl; 73 | uint8_t tos; 74 | uint16_t tot_len; 75 | uint16_t id; 76 | uint16_t frag_off; 77 | uint8_t ttl; 78 | uint8_t protocol; 79 | uint16_t check; 80 | IP4 saddr; 81 | IP4 daddr; 82 | 83 | bool isIPv4(); 84 | bool isIPIP(); 85 | }; 86 | 87 | struct __attribute__((packed)) SysRouteEntry { 88 | IP4 dst; 89 | IP4 mask; 90 | IP4 nexthop; 91 | }; 92 | 93 | /* 用于表示地址和掩码的组合,用于判断主机是否属于某个网络 */ 94 | class Address { 95 | public: 96 | Address(); 97 | Address(const std::string &cidr); 98 | 99 | IP4 &Host(); 100 | IP4 &Mask(); 101 | IP4 Net(); 102 | 103 | // 当前网络内的下一个地址 104 | Address Next(); 105 | 106 | // 判断是否是有效的主机地址 107 | bool isValid(); 108 | 109 | int fromCidr(const std::string &cidr); 110 | std::string toCidr(); 111 | 112 | bool empty() const { 113 | return host.empty() && mask.empty(); 114 | } 115 | 116 | private: 117 | IP4 host; 118 | IP4 mask; 119 | }; 120 | 121 | } // namespace candy 122 | 123 | namespace std { 124 | using candy::IP4; 125 | template <> struct hash { 126 | size_t operator()(const IP4 &ip) const noexcept { 127 | return hash{}(ip); 128 | } 129 | }; 130 | } // namespace std 131 | 132 | namespace { 133 | 134 | constexpr std::size_t AES_256_GCM_IV_LEN = 12; 135 | constexpr std::size_t AES_256_GCM_TAG_LEN = 16; 136 | constexpr std::size_t AES_256_GCM_KEY_LEN = 32; 137 | 138 | } // namespace 139 | 140 | #endif 141 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | release: 7 | types: [ published ] 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v4 15 | - name: setup qemu 16 | uses: docker/setup-qemu-action@v3 17 | - name: setup docker buildx 18 | uses: docker/setup-buildx-action@v3 19 | - name: login docker hub 20 | uses: docker/login-action@v3 21 | with: 22 | registry: docker.io 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - name: login github container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - name: setup version 32 | if: github.event_name == 'release' 33 | run: | 34 | GIT_TAG=${{ github.event.release.tag_name }} 35 | echo "IMAGE_TAG=${GIT_TAG#v}" >> $GITHUB_ENV 36 | - name: build and push 37 | uses: docker/build-push-action@v5 38 | if: github.event_name == 'release' 39 | with: 40 | context: . 41 | provenance: false 42 | platforms: linux/amd64,linux/arm64,linux/arm/v7 43 | push: true 44 | tags: | 45 | docker.io/${{ secrets.DOCKERHUB_USERNAME }}/candy:${{ env.IMAGE_TAG }} 46 | docker.io/${{ secrets.DOCKERHUB_USERNAME }}/candy:latest 47 | ghcr.io/${{ github.actor }}/candy:${{ env.IMAGE_TAG }} 48 | ghcr.io/${{ github.actor }}/candy:latest 49 | 50 | windows: 51 | runs-on: windows-latest 52 | steps: 53 | - name: setup msys2 54 | uses: msys2/setup-msys2@v2 55 | with: 56 | msystem: MINGW64 57 | update: true 58 | install: >- 59 | mingw-w64-x86_64-cmake 60 | mingw-w64-x86_64-ninja 61 | mingw-w64-x86_64-gcc 62 | mingw-w64-x86_64-spdlog 63 | mingw-w64-x86_64-poco 64 | - name: checkout 65 | uses: actions/checkout@v4 66 | - name: cache 67 | uses: actions/cache@v4 68 | with: 69 | path: build 70 | key: ${{ hashFiles('CMakeLists.txt') }} 71 | - name: build 72 | shell: msys2 {0} 73 | run: | 74 | cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build 75 | mkdir artifact 76 | cp candy.cfg artifact 77 | cp build/candy/wintun/bin/amd64/wintun.dll artifact 78 | scripts/search-deps.sh build/candy-cli/candy.exe artifact 79 | scripts/search-deps.sh build/candy-service/candy-service.exe artifact 80 | - name: set release package name 81 | shell: bash 82 | if: github.event_name == 'release' 83 | run: | 84 | GIT_TAG=${{ github.event.release.tag_name }} 85 | echo "PKGNAME=candy_${GIT_TAG#v}+windows_amd64" >> $GITHUB_ENV 86 | - name: upload artifact 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: windows-${{ github.event.release.tag_name || github.sha }} 90 | path: artifact 91 | - name: zip release 92 | uses: thedoctor0/zip-release@0.7.5 93 | if: github.event_name == 'release' 94 | with: 95 | type: 'zip' 96 | filename: ${{ env.PKGNAME }}.zip 97 | directory: artifact 98 | - name: upload release 99 | uses: softprops/action-gh-release@v2 100 | if: github.event_name == 'release' 101 | with: 102 | files: artifact/${{ env.PKGNAME }}.zip 103 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(Candy VERSION 6.1.4) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | add_compile_definitions(CANDY_VERSION="${PROJECT_VERSION}") 8 | 9 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) 10 | set(CMAKE_SKIP_BUILD_RPATH TRUE) 11 | 12 | if (${CANDY_STATIC}) 13 | set(CANDY_STATIC_OPENSSL 1) 14 | set(CANDY_STATIC_SPDLOG 1) 15 | set(CANDY_STATIC_NLOHMANN_JSON 1) 16 | set(CANDY_STATIC_POCO 1) 17 | endif() 18 | 19 | find_package(PkgConfig REQUIRED) 20 | include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Fetch.cmake) 21 | 22 | if (${CANDY_STATIC_OPENSSL}) 23 | execute_process( 24 | COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/openssl 25 | ) 26 | execute_process( 27 | COMMAND ${CMAKE_COMMAND} -DTARGET_OPENSSL=${TARGET_OPENSSL} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/openssl 28 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/openssl 29 | RESULT_VARIABLE result 30 | ) 31 | if(NOT result EQUAL "0") 32 | message(FATAL_ERROR "OpenSSL CMake failed") 33 | endif() 34 | 35 | execute_process( 36 | COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/openssl 37 | RESULT_VARIABLE result 38 | ) 39 | if(NOT result EQUAL "0") 40 | message(FATAL_ERROR "OpenSSL Download or Configure failed") 41 | endif() 42 | 43 | include(ProcessorCount) 44 | ProcessorCount(nproc) 45 | if(nproc EQUAL 0) 46 | set(nproc 1) 47 | endif() 48 | set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/openssl/openssl/src/openssl) 49 | execute_process( 50 | COMMAND make -j${nproc} 51 | WORKING_DIRECTORY ${OPENSSL_ROOT_DIR} 52 | RESULT_VARIABLE result 53 | ) 54 | if(NOT result EQUAL "0") 55 | message(FATAL_ERROR "OpenSSL Build failed") 56 | endif() 57 | 58 | set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/openssl/openssl/src/openssl) 59 | set(OPENSSL_INCLUDE ${OPENSSL_ROOT_DIR}/include) 60 | set(OPENSSL_LIB_CRYPTO ${OPENSSL_ROOT_DIR}/libcrypto.a) 61 | set(OPENSSL_LIB_SSL ${OPENSSL_ROOT_DIR}/libssl.a) 62 | include_directories(${OPENSSL_INCLUDE}) 63 | else() 64 | find_package(OpenSSL REQUIRED) 65 | endif() 66 | 67 | if (${CANDY_STATIC_SPDLOG}) 68 | Fetch(spdlog "https://github.com/gabime/spdlog.git" "v1.15.3") 69 | else() 70 | find_package(spdlog REQUIRED) 71 | endif() 72 | 73 | if (${CANDY_STATIC_POCO}) 74 | set(ENABLE_DATA OFF CACHE BOOL "" FORCE) 75 | set(ENABLE_DATA_MYSQL OFF CACHE BOOL "" FORCE) 76 | set(ENABLE_DATA_POSTGRESQL OFF CACHE BOOL "" FORCE) 77 | set(ENABLE_DATA_SQLITE OFF CACHE BOOL "" FORCE) 78 | set(ENABLE_DATA_ODBC OFF CACHE BOOL "" FORCE) 79 | set(ENABLE_MONGODB OFF CACHE BOOL "" FORCE) 80 | set(ENABLE_REDIS OFF CACHE BOOL "" FORCE) 81 | set(ENABLE_ENCODINGS OFF CACHE BOOL "" FORCE) 82 | set(ENABLE_PROMETHEUS OFF CACHE BOOL "" FORCE) 83 | set(ENABLE_PAGECOMPILER OFF CACHE BOOL "" FORCE) 84 | set(ENABLE_PAGECOMPILER_FILE2PAGE OFF CACHE BOOL "" FORCE) 85 | set(ENABLE_ACTIVERECORD OFF CACHE BOOL "" FORCE) 86 | set(ENABLE_ACTIVERECORD_COMPILER OFF CACHE BOOL "" FORCE) 87 | set(ENABLE_ZIP OFF CACHE BOOL "" FORCE) 88 | set(ENABLE_JWT OFF CACHE BOOL "" FORCE) 89 | Fetch(poco "https://github.com/pocoproject/poco.git" "poco-1.13.3-release") 90 | else() 91 | find_package(Poco REQUIRED COMPONENTS Foundation XML JSON Net NetSSL Util) 92 | endif() 93 | 94 | set(THREADS_PREFER_PTHREAD_FLAG ON) 95 | find_package(Threads REQUIRED) 96 | 97 | add_subdirectory(candy) 98 | add_subdirectory(candy-cli) 99 | add_subdirectory(candy-service) 100 | -------------------------------------------------------------------------------- /candy/src/core/net.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "core/net.h" 3 | #include 4 | #include 5 | #include 6 | 7 | namespace candy { 8 | 9 | IP4::IP4(const std::string &ip) { 10 | fromString(ip); 11 | } 12 | 13 | IP4 IP4::operator=(const std::string &ip) { 14 | fromString(ip); 15 | return *this; 16 | } 17 | 18 | IP4::operator std::string() const { 19 | return toString(); 20 | } 21 | 22 | IP4::operator uint32_t() const { 23 | uint32_t val = 0; 24 | std::memcpy(&val, raw.data(), sizeof(val)); 25 | return val; 26 | } 27 | 28 | IP4 IP4::operator|(IP4 another) const { 29 | for (int i = 0; i < raw.size(); ++i) { 30 | another.raw[i] |= raw[i]; 31 | } 32 | return another; 33 | } 34 | 35 | IP4 IP4::operator^(IP4 another) const { 36 | for (int i = 0; i < raw.size(); ++i) { 37 | another.raw[i] ^= raw[i]; 38 | } 39 | return another; 40 | } 41 | 42 | IP4 IP4::operator~() const { 43 | IP4 retval; 44 | for (int i = 0; i < raw.size(); ++i) { 45 | retval.raw[i] |= ~raw[i]; 46 | } 47 | return retval; 48 | } 49 | 50 | bool IP4::operator==(IP4 another) const { 51 | return raw == another.raw; 52 | } 53 | 54 | IP4 IP4::operator&(IP4 another) const { 55 | for (int i = 0; i < raw.size(); ++i) { 56 | another.raw[i] &= raw[i]; 57 | } 58 | return another; 59 | } 60 | 61 | IP4 IP4::next() const { 62 | IP4 ip; 63 | uint32_t t = hton(ntoh(uint32_t(*this)) + 1); 64 | std::memcpy(&ip, &t, sizeof(ip)); 65 | return ip; 66 | } 67 | 68 | int IP4::fromString(const std::string &ip) { 69 | memcpy(raw.data(), Poco::Net::IPAddress(ip).addr(), 4); 70 | return 0; 71 | } 72 | 73 | std::string IP4::toString() const { 74 | return Poco::Net::IPAddress(raw.data(), sizeof(raw)).toString(); 75 | } 76 | 77 | int IP4::fromPrefix(int prefix) { 78 | std::memset(raw.data(), 0, sizeof(raw)); 79 | for (int i = 0; i < prefix; ++i) { 80 | raw[i / 8] |= (0x80 >> (i % 8)); 81 | } 82 | return 0; 83 | } 84 | 85 | int IP4::toPrefix() { 86 | int i; 87 | for (i = 0; i < 32; ++i) { 88 | if (!(raw[i / 8] & (0x80 >> (i % 8)))) { 89 | break; 90 | } 91 | } 92 | return i; 93 | } 94 | 95 | bool IP4::empty() const { 96 | return raw[0] == 0 && raw[1] == 0 && raw[2] == 0 && raw[3] == 0; 97 | } 98 | 99 | void IP4::reset() { 100 | this->raw.fill(0); 101 | } 102 | 103 | bool IP4Header::isIPv4() { 104 | return (this->version_ihl >> 4) == 4; 105 | } 106 | 107 | bool IP4Header::isIPIP() { 108 | return this->protocol == 0x04; 109 | } 110 | 111 | Address::Address() {} 112 | 113 | Address::Address(const std::string &cidr) { 114 | if (!cidr.empty()) { 115 | fromCidr(cidr); 116 | } 117 | } 118 | 119 | IP4 &Address::Host() { 120 | return this->host; 121 | } 122 | 123 | IP4 &Address::Mask() { 124 | return this->mask; 125 | } 126 | 127 | IP4 Address::Net() { 128 | return Host() & Mask(); 129 | } 130 | 131 | Address Address::Next() { 132 | Address next; 133 | next.mask = this->mask; 134 | next.host = (Net() | (~Mask() & this->host.next())); 135 | return next; 136 | } 137 | 138 | bool Address::isValid() { 139 | if ((~mask & host) == 0) { 140 | return false; 141 | } 142 | if (~(mask | host) == 0) { 143 | return false; 144 | } 145 | return true; 146 | } 147 | 148 | int Address::fromCidr(const std::string &cidr) { 149 | try { 150 | std::size_t pos = cidr.find('/'); 151 | host.fromString(cidr.substr(0UL, pos)); 152 | mask.fromPrefix(std::stoi(cidr.substr(pos + 1))); 153 | } catch (std::exception &e) { 154 | spdlog::warn("address parse cidr failed: {}: {}", e.what(), cidr); 155 | return -1; 156 | } 157 | return 0; 158 | } 159 | 160 | std::string Address::toCidr() { 161 | return host.toString() + "/" + std::to_string(mask.toPrefix()); 162 | } 163 | 164 | } // namespace candy 165 | -------------------------------------------------------------------------------- /candy/src/websocket/message.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "websocket/message.h" 3 | #include "utils/time.h" 4 | 5 | namespace candy { 6 | namespace WsMsg { 7 | 8 | Auth::Auth(IP4 ip) { 9 | this->type = WsMsgKind::AUTH; 10 | this->ip = ip; 11 | this->timestamp = hton(unixTime()); 12 | } 13 | 14 | void Auth::updateHash(const std::string &password) { 15 | std::string data; 16 | data.append(password); 17 | data.append((char *)&ip, sizeof(ip)); 18 | data.append((char *)×tamp, sizeof(timestamp)); 19 | SHA256((unsigned char *)data.data(), data.size(), this->hash); 20 | } 21 | 22 | bool Auth::check(const std::string &password) { 23 | int64_t localTime = unixTime(); 24 | int64_t remoteTime = ntoh(this->timestamp); 25 | if (std::abs(localTime - remoteTime) > 300) { 26 | spdlog::warn("auth header timestamp check failed: server {} client {}", localTime, remoteTime); 27 | } 28 | 29 | uint8_t reported[SHA256_DIGEST_LENGTH]; 30 | std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH); 31 | 32 | updateHash(password); 33 | 34 | if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) { 35 | spdlog::warn("auth header hash check failed"); 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | Forward::Forward() { 42 | this->type = WsMsgKind::FORWARD; 43 | } 44 | 45 | ExptTun::ExptTun(const std::string &cidr) { 46 | this->type = WsMsgKind::EXPTTUN; 47 | this->timestamp = hton(unixTime()); 48 | std::strcpy(this->cidr, cidr.c_str()); 49 | } 50 | 51 | void ExptTun::updateHash(const std::string &password) { 52 | std::string data; 53 | data.append(password); 54 | data.append((char *)&this->timestamp, sizeof(this->timestamp)); 55 | SHA256((unsigned char *)data.data(), data.size(), this->hash); 56 | } 57 | 58 | bool ExptTun::check(const std::string &password) { 59 | int64_t localTime = unixTime(); 60 | int64_t remoteTime = ntoh(this->timestamp); 61 | if (std::abs(localTime - remoteTime) > 300) { 62 | spdlog::warn("expected address header timestamp check failed: server {} client {}", localTime, remoteTime); 63 | } 64 | 65 | uint8_t reported[SHA256_DIGEST_LENGTH]; 66 | std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH); 67 | 68 | updateHash(password); 69 | 70 | if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) { 71 | spdlog::warn("expected address header hash check failed"); 72 | return false; 73 | } 74 | return true; 75 | } 76 | 77 | Conn::Conn() { 78 | this->type = WsMsgKind::UDP4CONN; 79 | } 80 | 81 | VMac::VMac(const std::string &vmac) { 82 | this->type = WsMsgKind::VMAC; 83 | this->timestamp = hton(unixTime()); 84 | if (vmac.length() >= sizeof(this->vmac)) { 85 | memcpy(this->vmac, vmac.c_str(), sizeof(this->vmac)); 86 | } else { 87 | memset(this->vmac, 0, sizeof(this->vmac)); 88 | } 89 | } 90 | 91 | void VMac::updateHash(const std::string &password) { 92 | std::string data; 93 | data.append(password); 94 | data.append((char *)&this->vmac, sizeof(this->vmac)); 95 | data.append((char *)&this->timestamp, sizeof(this->timestamp)); 96 | SHA256((unsigned char *)data.data(), data.size(), this->hash); 97 | } 98 | 99 | bool VMac::check(const std::string &password) { 100 | int64_t localTime = unixTime(); 101 | int64_t remoteTime = ntoh(this->timestamp); 102 | if (std::abs(localTime - remoteTime) > 300) { 103 | spdlog::warn("vmac message timestamp check failed: server {} client {}", localTime, remoteTime); 104 | } 105 | 106 | uint8_t reported[SHA256_DIGEST_LENGTH]; 107 | std::memcpy(reported, this->hash, SHA256_DIGEST_LENGTH); 108 | 109 | updateHash(password); 110 | 111 | if (std::memcmp(reported, this->hash, SHA256_DIGEST_LENGTH)) { 112 | spdlog::warn("vmac message hash check failed"); 113 | return false; 114 | } 115 | return true; 116 | } 117 | 118 | Discovery::Discovery() { 119 | this->type = WsMsgKind::DISCOVERY; 120 | } 121 | 122 | General::General() { 123 | this->type = WsMsgKind::GENERAL; 124 | } 125 | 126 | ConnLocal::ConnLocal() { 127 | this->ge.subtype = GeSubType::LOCALUDP4CONN; 128 | this->ge.extra = 0; 129 | } 130 | } // namespace WsMsg 131 | } // namespace candy 132 | -------------------------------------------------------------------------------- /candy/src/candy/client.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "candy/client.h" 3 | #include "core/client.h" 4 | #include "utils/atomic.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace candy { 15 | namespace client { 16 | 17 | namespace { 18 | using Utils::Atomic; 19 | 20 | class Instance { 21 | public: 22 | bool is_running() { 23 | return this->running.load(); 24 | } 25 | 26 | void exit() { 27 | this->running.store(false); 28 | if (auto client = this->client.lock()) { 29 | client->shutdown(); 30 | } 31 | } 32 | 33 | Poco::JSON::Object status() { 34 | Poco::JSON::Object data; 35 | if (auto client = this->client.lock()) { 36 | data.set("address", client->getTunCidr()); 37 | } 38 | return data; 39 | } 40 | 41 | std::shared_ptr create_client() { 42 | auto client = std::make_shared(); 43 | this->client = client; 44 | return client; 45 | } 46 | 47 | private: 48 | Atomic running = Atomic(true); 49 | std::weak_ptr client; 50 | }; 51 | 52 | std::map> instance_map; 53 | std::shared_mutex instance_mutex; 54 | 55 | std::optional> try_create_instance(const std::string &id) { 56 | std::unique_lock lock(instance_mutex); 57 | auto it = instance_map.find(id); 58 | if (it != instance_map.end()) { 59 | spdlog::warn("instance already exists: id={}", id); 60 | return std::nullopt; 61 | } 62 | auto manager = std::make_shared(); 63 | instance_map.emplace(id, manager); 64 | return manager; 65 | } 66 | 67 | bool try_erase_instance(const std::string &id) { 68 | std::unique_lock lock(instance_mutex); 69 | return instance_map.erase(id) > 0; 70 | } 71 | 72 | } // namespace 73 | 74 | bool run(const std::string &id, const Poco::JSON::Object &config) { 75 | auto instance = try_create_instance(id); 76 | if (!instance) { 77 | return false; 78 | } 79 | 80 | auto toString = [](const Poco::JSON::Object &obj) -> std::string { 81 | std::ostringstream oss; 82 | Poco::JSON::Stringifier::stringify(obj, oss); 83 | return oss.str(); 84 | }; 85 | 86 | spdlog::info("run enter: id={} config={}", id, toString(config)); 87 | while ((*instance)->is_running()) { 88 | std::this_thread::sleep_for(std::chrono::seconds(1)); 89 | auto client = (*instance)->create_client(); 90 | client->setName(config.getValue("name")); 91 | client->setPassword(config.getValue("password")); 92 | client->setWebSocket(config.getValue("websocket")); 93 | client->setTunAddress(config.getValue("tun")); 94 | client->setVirtualMac(config.getValue("vmac")); 95 | client->setExptTunAddress(config.getValue("expt")); 96 | client->setStun(config.getValue("stun")); 97 | client->setDiscoveryInterval(config.getValue("discovery")); 98 | client->setRouteCost(config.getValue("route")), client->setMtu(config.getValue("mtu")); 99 | client->setPort(config.getValue("port")); 100 | client->setLocalhost(config.getValue("localhost")); 101 | client->run(); 102 | } 103 | spdlog::info("run exit: id={} ", id); 104 | 105 | return try_erase_instance(id); 106 | } 107 | 108 | bool shutdown(const std::string &id) { 109 | std::shared_lock lock(instance_mutex); 110 | auto it = instance_map.find(id); 111 | if (it == instance_map.end()) { 112 | spdlog::warn("instance not found: id={}", id); 113 | return false; 114 | } 115 | if (auto instance = it->second) { 116 | instance->exit(); 117 | } 118 | return true; 119 | } 120 | 121 | std::optional status(const std::string &id) { 122 | std::shared_lock lock(instance_mutex); 123 | auto it = instance_map.find(id); 124 | if (it != instance_map.end()) { 125 | if (auto instance = it->second) { 126 | return instance->status(); 127 | } 128 | } 129 | return std::nullopt; 130 | } 131 | 132 | } // namespace client 133 | } // namespace candy 134 | -------------------------------------------------------------------------------- /candy/src/peer/manager.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #ifndef CANDY_PEER_MANAGER_H 3 | #define CANDY_PEER_MANAGER_H 4 | 5 | #include "core/message.h" 6 | #include "core/net.h" 7 | #include "peer/message.h" 8 | #include "peer/peer.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace candy { 20 | 21 | using Poco::Net::SocketAddress; 22 | 23 | class Client; 24 | 25 | struct Stun { 26 | std::string uri; 27 | SocketAddress address; 28 | bool needed = false; 29 | IP4 ip; 30 | uint16_t port; 31 | 32 | bool enabled() { 33 | return !this->address.host().isWildcard(); 34 | } 35 | 36 | int update() { 37 | try { 38 | if (!this->uri.empty()) { 39 | Poco::URI uri(this->uri); 40 | if (!uri.getPort()) { 41 | uri.setPort(3478); 42 | } 43 | this->address = Poco::Net::SocketAddress(uri.getHost(), uri.getPort()); 44 | } 45 | return 0; 46 | } catch (std::exception &e) { 47 | spdlog::warn("set stun server address failed: {}", e.what()); 48 | return -1; 49 | } 50 | } 51 | }; 52 | 53 | struct PeerRouteEntry { 54 | IP4 dst; 55 | IP4 next; 56 | int32_t rtt; 57 | 58 | PeerRouteEntry(IP4 dst = IP4(), IP4 next = IP4(), int32_t rtt = RTT_LIMIT) : dst(dst), next(next), rtt(rtt) {} 59 | }; 60 | 61 | class PeerManager { 62 | public: 63 | int setPassword(const std::string &password); 64 | int setStun(const std::string &stun); 65 | int setDiscoveryInterval(int interval); 66 | int setRouteCost(int cost); 67 | int setPort(int port); 68 | int setLocalhost(const std::string &ip); 69 | 70 | int run(Client *client); 71 | int wait(); 72 | 73 | std::string getPassword(); 74 | 75 | private: 76 | std::string password; 77 | IP4 localhost; 78 | 79 | public: 80 | int sendPubInfo(CoreMsg::PubInfo info); 81 | IP4 getTunIp(); 82 | int updateRtTable(PeerRouteEntry entry); 83 | 84 | private: 85 | // 处理来自消息队列的数据 86 | int handlePeerQueue(); 87 | int handlePacket(Msg msg); 88 | int handleTunAddr(Msg msg); 89 | int handleTryP2P(Msg msg); 90 | int handlePubInfo(Msg msg); 91 | 92 | std::thread msgThread; 93 | 94 | int sendPacket(IP4 dst, const Msg &msg); 95 | int sendPacketDirect(IP4 dst, const Msg &msg); 96 | int sendPacketRelay(IP4 dst, const Msg &msg); 97 | 98 | Address tunAddr; 99 | 100 | int startTickThread(); 101 | int tick(); 102 | std::thread tickThread; 103 | uint64_t tickTick = randomUint32(); 104 | 105 | std::shared_mutex ipPeerMutex; 106 | std::unordered_map ipPeerMap; 107 | 108 | void showRtChange(const PeerRouteEntry &entry); 109 | int sendRtMessage(IP4 dst, int32_t rtt); 110 | 111 | std::shared_mutex rtTableMutex; 112 | std::unordered_map rtTableMap; 113 | 114 | public: 115 | Stun stun; 116 | std::atomic localP2PDisabled; 117 | 118 | private: 119 | int initSocket(); 120 | void sendStunRequest(); 121 | void handleStunResponse(std::string buffer); 122 | void handleMessage(std::string buffer, const SocketAddress &address); 123 | void handleHeartbeatMessage(std::string buffer, const SocketAddress &address); 124 | void handleForwardMessage(std::string buffer, const SocketAddress &address); 125 | void handleDelayMessage(std::string buffer, const SocketAddress &address); 126 | void handleRouteMessage(std::string buffer, const SocketAddress &address); 127 | int poll(); 128 | 129 | std::optional decrypt(const std::string &ciphertext); 130 | std::shared_ptr decryptCtx; 131 | std::mutex decryptCtxMutex; 132 | std::string key; 133 | 134 | // 默认监听端口,如果不配置,随机监听 135 | uint16_t listenPort = 0; 136 | 137 | public: 138 | std::mutex socketMutex; 139 | Poco::Net::DatagramSocket socket; 140 | int sendTo(const void *buffer, int length, const SocketAddress &address); 141 | int getDiscoveryInterval() const; 142 | bool clientRelayEnabled() const; 143 | 144 | private: 145 | std::thread pollThread; 146 | 147 | int discoveryInterval = 0; 148 | int routeCost = 0; 149 | 150 | Client &getClient(); 151 | Client *client; 152 | }; 153 | 154 | } // namespace candy 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /scripts/build-standalone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ -z $CANDY_WORKSPACE ];then echo "CANDY_WORKSPACE is not exist";exit 1;fi 4 | 5 | if [[ -z $TARGET || -z $TARGET_OPENSSL ]];then 6 | if [ -z $CANDY_ARCH ];then echo "CANDY_ARCH is not exist";exit 1;fi 7 | if [ -z $CANDY_OS ];then echo "CANDY_OS is not exist";exit 1;fi 8 | echo "CANDY_ARCH: $CANDY_ARCH" 9 | echo "CANDY_OS: $CANDY_OS" 10 | if [[ "$CANDY_OS" == "linux" ]]; then 11 | if [[ "$CANDY_ARCH" == "aarch64" ]]; then TARGET="aarch64-unknown-linux-musl";TARGET_OPENSSL="linux-aarch64";UPX=1 12 | elif [[ "$CANDY_ARCH" == "arm" ]]; then TARGET="arm-unknown-linux-musleabi";TARGET_OPENSSL="linux-armv4";UPX=1 13 | elif [[ "$CANDY_ARCH" == "armhf" ]]; then TARGET="arm-unknown-linux-musleabihf";TARGET_OPENSSL="linux-armv4";UPX=1 14 | elif [[ "$CANDY_ARCH" == "loongarch64" ]]; then TARGET="loongarch64-unknown-linux-musl";TARGET_OPENSSL="linux64-loongarch64";UPX=0 15 | elif [[ "$CANDY_ARCH" == "mips" ]]; then TARGET="mips-unknown-linux-musl";TARGET_OPENSSL="linux-mips32";UPX=1 16 | elif [[ "$CANDY_ARCH" == "mipssf" ]]; then TARGET="mips-unknown-linux-muslsf";TARGET_OPENSSL="linux-mips32";UPX=1 17 | elif [[ "$CANDY_ARCH" == "mipsel" ]]; then TARGET="mipsel-unknown-linux-musl";TARGET_OPENSSL="linux-mips32";UPX=1 18 | elif [[ "$CANDY_ARCH" == "mipselsf" ]]; then TARGET="mipsel-unknown-linux-muslsf";TARGET_OPENSSL="linux-mips32";UPX=1 19 | elif [[ "$CANDY_ARCH" == "mips64" ]]; then TARGET="mips64-unknown-linux-musl";TARGET_OPENSSL="linux64-mips64";UPX=0 20 | elif [[ "$CANDY_ARCH" == "mips64el" ]]; then TARGET="mips64el-unknown-linux-musl";TARGET_OPENSSL="linux64-mips64";UPX=0 21 | elif [[ "$CANDY_ARCH" == "riscv32" ]]; then TARGET="riscv32-unknown-linux-musl";TARGET_OPENSSL="linux32-riscv32";UPX=0 22 | elif [[ "$CANDY_ARCH" == "riscv64" ]]; then TARGET="riscv64-unknown-linux-musl";TARGET_OPENSSL="linux64-riscv64";UPX=0 23 | elif [[ "$CANDY_ARCH" == "x86_64" ]]; then TARGET="x86_64-unknown-linux-musl";TARGET_OPENSSL="linux-x86_64";UPX=1 24 | else echo "Unknown CANDY_ARCH: $CANDY_ARCH";exit 1;fi 25 | elif [[ "$CANDY_OS" == "macos" ]]; then 26 | echo "macos is not supported yet";exit 1 27 | elif [[ "$CANDY_OS" == "windows" ]]; then 28 | echo "windows is not supported yet";exit 1 29 | else echo "Unknown CANDY_OS: $CANDY_OS";exit 1;fi 30 | fi 31 | 32 | echo "CANDY_WORKSPACE: $CANDY_WORKSPACE" 33 | echo "TARGET: $TARGET" 34 | echo "TARGET_OPENSSL: $TARGET_OPENSSL" 35 | 36 | TOOLCHAINS="$CANDY_WORKSPACE/toolchains" 37 | COMPILER_ROOT="$TOOLCHAINS/$TARGET" 38 | 39 | if [ ! -d "$COMPILER_ROOT" ]; then 40 | mkdir -p $TOOLCHAINS 41 | VERSION=20250206 42 | wget -c https://github.com/musl-cross/musl-cross/releases/download/$VERSION/$TARGET.tar.xz -P $TOOLCHAINS 43 | tar xvf $COMPILER_ROOT.tar.xz -C $TOOLCHAINS 44 | fi 45 | 46 | export CC="$COMPILER_ROOT/bin/$TARGET-gcc" 47 | export CXX="$COMPILER_ROOT/bin/$TARGET-g++" 48 | export AR="$COMPILER_ROOT/bin/$TARGET-ar" 49 | export LD="$COMPILER_ROOT/bin/$TARGET-ld" 50 | export RANLIB="$COMPILER_ROOT/bin/$TARGET-ranlib" 51 | export STRIP="$COMPILER_ROOT/bin/$TARGET-strip" 52 | export CFLAGS="-I $COMPILER_ROOT/$TARGET/include" 53 | export LDFLAGS="-static -Wl,--whole-archive -latomic -Wl,--no-whole-archive -L $COMPILER_ROOT/$TARGET/lib" 54 | 55 | if [[ $CANDY_OS && $CANDY_ARCH ]];then 56 | BUILD_DIR="$CANDY_WORKSPACE/build/$CANDY_OS-$CANDY_ARCH" 57 | OUTPUT_DIR="$CANDY_WORKSPACE/output/$CANDY_OS-$CANDY_ARCH" 58 | else 59 | BUILD_DIR="$CANDY_WORKSPACE/build/$TARGET" 60 | OUTPUT_DIR="$CANDY_WORKSPACE/output/$TARGET" 61 | fi 62 | 63 | if which ninja >/dev/null 2>&1;then GENERATOR="Ninja";else GENERATOR="Unix Makefiles";fi 64 | SOURCE_DIR="$(dirname $(readlink -f "$0"))/../" 65 | cmake -G "$GENERATOR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DCANDY_STATIC=1 -DTARGET_OPENSSL=$TARGET_OPENSSL $SOURCE_DIR 66 | cmake --build $BUILD_DIR --parallel $(nproc) 67 | mkdir -p $OUTPUT_DIR 68 | cp $BUILD_DIR/candy-cli/candy $OUTPUT_DIR/candy 69 | cp $BUILD_DIR/candy-service/candy-service $OUTPUT_DIR/candy-service 70 | 71 | if [[ $CANDY_STRIP && $CANDY_STRIP -eq 1 ]];then 72 | $STRIP $OUTPUT_DIR/candy 73 | $STRIP $OUTPUT_DIR/candy-service 74 | fi 75 | 76 | if [[ $CANDY_UPX && $CANDY_UPX -eq 1 && $UPX -eq 1 ]];then 77 | upx --lzma --best -q $OUTPUT_DIR/candy 78 | upx --lzma --best -q $OUTPUT_DIR/candy-service 79 | fi 80 | 81 | if [[ $CANDY_TGZ && $CANDY_TGZ -eq 1 && $CANDY_OS && $CANDY_ARCH ]];then 82 | cp $SOURCE_DIR/{candy.cfg,candy.service,candy@.service,candy.initd} $OUTPUT_DIR 83 | tar zcvf $CANDY_WORKSPACE/output/candy-$CANDY_OS-$CANDY_ARCH.tar.gz -C $OUTPUT_DIR . 84 | fi 85 | -------------------------------------------------------------------------------- /candy/src/tun/tun.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "tun/tun.h" 3 | #include "core/client.h" 4 | #include "core/message.h" 5 | #include "core/net.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace candy { 11 | 12 | int Tun::run(Client *client) { 13 | this->client = client; 14 | this->msgThread = std::thread([&] { 15 | spdlog::debug("start thread: tun msg"); 16 | while (getClient().isRunning()) { 17 | if (handleTunQueue()) { 18 | break; 19 | } 20 | } 21 | getClient().shutdown(); 22 | spdlog::debug("stop thread: tun msg"); 23 | }); 24 | return 0; 25 | } 26 | 27 | int Tun::wait() { 28 | if (this->tunThread.joinable()) { 29 | this->tunThread.join(); 30 | } 31 | if (this->msgThread.joinable()) { 32 | this->msgThread.join(); 33 | } 34 | { 35 | std::unique_lock lock(this->sysRtMutex); 36 | this->sysRtTable.clear(); 37 | } 38 | return 0; 39 | } 40 | 41 | int Tun::handleTunDevice() { 42 | std::string buffer; 43 | int error = read(buffer); 44 | if (error <= 0) { 45 | return 0; 46 | } 47 | if (buffer.length() < sizeof(IP4Header)) { 48 | return 0; 49 | } 50 | IP4Header *header = (IP4Header *)buffer.data(); 51 | if (!header->isIPv4()) { 52 | return 0; 53 | } 54 | 55 | IP4 nextHop = [&]() { 56 | std::shared_lock lock(this->sysRtMutex); 57 | for (auto const &rt : sysRtTable) { 58 | if ((header->daddr & rt.mask) == rt.dst) { 59 | return rt.nexthop; 60 | } 61 | } 62 | return IP4(); 63 | }(); 64 | if (!nextHop.empty()) { 65 | buffer.insert(0, sizeof(IP4Header), 0); 66 | header = (IP4Header *)buffer.data(); 67 | header->protocol = 0x04; 68 | header->saddr = getIP(); 69 | header->daddr = nextHop; 70 | } 71 | 72 | if (header->daddr == getIP()) { 73 | write(buffer); 74 | return 0; 75 | } 76 | 77 | this->client->getPeerMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer))); 78 | return 0; 79 | } 80 | 81 | int Tun::handleTunQueue() { 82 | Msg msg = this->client->getTunMsgQueue().read(); 83 | switch (msg.kind) { 84 | case MsgKind::TIMEOUT: 85 | break; 86 | case MsgKind::PACKET: 87 | handlePacket(std::move(msg)); 88 | break; 89 | case MsgKind::TUNADDR: 90 | if (handleTunAddr(std::move(msg))) { 91 | return -1; 92 | } 93 | break; 94 | case MsgKind::SYSRT: 95 | handleSysRt(std::move(msg)); 96 | break; 97 | default: 98 | spdlog::warn("unexcepted tun message type: {}", static_cast(msg.kind)); 99 | break; 100 | } 101 | return 0; 102 | } 103 | 104 | int Tun::handlePacket(Msg msg) { 105 | if (msg.data.size() < sizeof(IP4Header)) { 106 | spdlog::warn("invalid IPv4 packet: {:n}", spdlog::to_hex(msg.data)); 107 | return 0; 108 | } 109 | IP4Header *header = (IP4Header *)msg.data.data(); 110 | if (header->isIPIP()) { 111 | msg.data.erase(0, sizeof(IP4Header)); 112 | header = (IP4Header *)msg.data.data(); 113 | } 114 | write(msg.data); 115 | return 0; 116 | } 117 | 118 | int Tun::handleTunAddr(Msg msg) { 119 | if (setAddress(msg.data)) { 120 | return -1; 121 | } 122 | 123 | if (up()) { 124 | spdlog::critical("tun up failed"); 125 | return -1; 126 | } 127 | 128 | this->tunThread = std::thread([&] { 129 | spdlog::debug("start thread: tun"); 130 | while (getClient().isRunning()) { 131 | if (handleTunDevice()) { 132 | break; 133 | } 134 | } 135 | getClient().shutdown(); 136 | spdlog::debug("stop thread: tun"); 137 | 138 | if (down()) { 139 | spdlog::critical("tun down failed"); 140 | return; 141 | } 142 | }); 143 | 144 | return 0; 145 | } 146 | 147 | int Tun::handleSysRt(Msg msg) { 148 | SysRouteEntry *rt = (SysRouteEntry *)msg.data.data(); 149 | if (rt->nexthop != getIP()) { 150 | spdlog::info("route: {}/{} via {}", rt->dst.toString(), rt->mask.toPrefix(), rt->nexthop.toString()); 151 | if (setSysRtTable(*rt)) { 152 | return -1; 153 | } 154 | } 155 | return 0; 156 | } 157 | 158 | int Tun::setSysRtTable(const SysRouteEntry &entry) { 159 | std::unique_lock lock(this->sysRtMutex); 160 | this->sysRtTable.push_back(entry); 161 | return setSysRtTable(entry.dst, entry.mask, entry.nexthop); 162 | } 163 | 164 | Client &Tun::getClient() { 165 | return *this->client; 166 | } 167 | 168 | } // namespace candy 169 | -------------------------------------------------------------------------------- /candy/src/tun/linux.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include 3 | #if POCO_OS == POCO_OS_LINUX 4 | 5 | #include "core/net.h" 6 | #include "tun/tun.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace candy { 19 | 20 | class LinuxTun { 21 | public: 22 | int setName(const std::string &name) { 23 | this->name = name.empty() ? "candy" : "candy-" + name; 24 | return 0; 25 | } 26 | 27 | int setIP(IP4 ip) { 28 | this->ip = ip; 29 | return 0; 30 | } 31 | 32 | IP4 getIP() { 33 | return this->ip; 34 | } 35 | 36 | int setMask(IP4 mask) { 37 | this->mask = mask; 38 | return 0; 39 | } 40 | 41 | int setMTU(int mtu) { 42 | this->mtu = mtu; 43 | return 0; 44 | } 45 | 46 | // 配置网卡,设置路由 47 | int up() { 48 | this->tunFd = open("/dev/net/tun", O_RDWR); 49 | if (this->tunFd < 0) { 50 | spdlog::critical("open /dev/net/tun failed: {}", strerror(errno)); 51 | close(this->tunFd); 52 | return -1; 53 | } 54 | int flags = fcntl(this->tunFd, F_GETFL, 0); 55 | if (flags < 0) { 56 | spdlog::error("get tun flags failed: {}", strerror(errno)); 57 | close(this->tunFd); 58 | return -1; 59 | } 60 | flags |= O_NONBLOCK; 61 | if (fcntl(this->tunFd, F_SETFL, flags) < 0) { 62 | spdlog::error("set non-blocking tun failed: {}", strerror(errno)); 63 | close(this->tunFd); 64 | return -1; 65 | } 66 | 67 | // 设置设备名 68 | struct ifreq ifr; 69 | memset(&ifr, 0, sizeof(ifr)); 70 | strncpy(ifr.ifr_name, this->name.c_str(), IFNAMSIZ); 71 | ifr.ifr_flags = IFF_TUN | IFF_NO_PI; 72 | if (ioctl(this->tunFd, TUNSETIFF, &ifr) == -1) { 73 | spdlog::critical("set tun interface failed: {}", strerror(errno)); 74 | close(this->tunFd); 75 | return -1; 76 | } 77 | 78 | // 创建 socket, 并通过这个 socket 更新网卡的其他配置 79 | struct sockaddr_in *addr; 80 | addr = (struct sockaddr_in *)&ifr.ifr_addr; 81 | addr->sin_family = AF_INET; 82 | int sockfd = socket(addr->sin_family, SOCK_DGRAM, 0); 83 | if (sockfd == -1) { 84 | spdlog::critical("create socket failed"); 85 | close(this->tunFd); 86 | return -1; 87 | } 88 | 89 | // 设置地址 90 | addr->sin_addr.s_addr = this->ip; 91 | if (ioctl(sockfd, SIOCSIFADDR, (caddr_t)&ifr) == -1) { 92 | spdlog::critical("set ip address failed: ip {}", this->ip.toString()); 93 | close(sockfd); 94 | close(this->tunFd); 95 | return -1; 96 | } 97 | 98 | // 设置掩码 99 | addr->sin_addr.s_addr = this->mask; 100 | if (ioctl(sockfd, SIOCSIFNETMASK, (caddr_t)&ifr) == -1) { 101 | spdlog::critical("set mask failed: mask {}", this->mask.toString()); 102 | close(sockfd); 103 | close(this->tunFd); 104 | return -1; 105 | } 106 | 107 | // 设置 MTU 108 | ifr.ifr_mtu = this->mtu; 109 | if (ioctl(sockfd, SIOCSIFMTU, (caddr_t)&ifr) == -1) { 110 | spdlog::critical("set mtu failed: mtu {}", this->mtu); 111 | close(sockfd); 112 | close(this->tunFd); 113 | return -1; 114 | } 115 | 116 | // 设置 flags 117 | if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) { 118 | spdlog::critical("get interface flags failed"); 119 | close(sockfd); 120 | close(this->tunFd); 121 | return -1; 122 | } 123 | ifr.ifr_flags |= IFF_UP | IFF_RUNNING; 124 | if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) { 125 | spdlog::critical("set interface flags failed"); 126 | close(sockfd); 127 | close(this->tunFd); 128 | return -1; 129 | } 130 | 131 | close(sockfd); 132 | 133 | return 0; 134 | } 135 | 136 | int down() { 137 | close(this->tunFd); 138 | return 0; 139 | } 140 | 141 | int read(std::string &buffer) { 142 | buffer.resize(this->mtu); 143 | int n = ::read(this->tunFd, buffer.data(), buffer.size()); 144 | if (n >= 0) { 145 | buffer.resize(n); 146 | return n; 147 | } 148 | 149 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 150 | struct timeval timeout = {.tv_sec = 1}; 151 | fd_set set; 152 | 153 | FD_ZERO(&set); 154 | FD_SET(this->tunFd, &set); 155 | 156 | select(this->tunFd + 1, &set, NULL, NULL, &timeout); 157 | return 0; 158 | } 159 | spdlog::warn("tun read failed: {}", strerror(errno)); 160 | return -1; 161 | } 162 | 163 | int write(const std::string &buffer) { 164 | return ::write(this->tunFd, buffer.c_str(), buffer.size()); 165 | } 166 | 167 | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 168 | int sockfd = socket(AF_INET, SOCK_DGRAM, 0); 169 | if (sockfd == -1) { 170 | spdlog::error("set route failed: create socket failed"); 171 | return -1; 172 | } 173 | 174 | struct sockaddr_in *addr; 175 | struct rtentry route; 176 | memset(&route, 0, sizeof(route)); 177 | 178 | addr = (struct sockaddr_in *)&route.rt_dst; 179 | addr->sin_family = AF_INET; 180 | addr->sin_addr.s_addr = dst; 181 | 182 | addr = (struct sockaddr_in *)&route.rt_genmask; 183 | addr->sin_family = AF_INET; 184 | addr->sin_addr.s_addr = mask; 185 | 186 | addr = (struct sockaddr_in *)&route.rt_gateway; 187 | addr->sin_family = AF_INET; 188 | addr->sin_addr.s_addr = nexthop; 189 | 190 | route.rt_flags = RTF_UP | RTF_GATEWAY; 191 | if (ioctl(sockfd, SIOCADDRT, &route) == -1) { 192 | spdlog::error("set route failed: ioctl failed"); 193 | close(sockfd); 194 | return -1; 195 | } 196 | 197 | close(sockfd); 198 | return 0; 199 | } 200 | 201 | private: 202 | std::string name; 203 | IP4 ip; 204 | IP4 mask; 205 | int mtu; 206 | int timeout; 207 | int tunFd; 208 | }; 209 | 210 | } // namespace candy 211 | 212 | namespace candy { 213 | 214 | Tun::Tun() { 215 | this->impl = std::make_shared(); 216 | } 217 | 218 | Tun::~Tun() { 219 | this->impl.reset(); 220 | } 221 | 222 | int Tun::setName(const std::string &name) { 223 | std::shared_ptr tun; 224 | 225 | tun = std::any_cast>(this->impl); 226 | tun->setName(name); 227 | return 0; 228 | } 229 | 230 | int Tun::setAddress(const std::string &cidr) { 231 | std::shared_ptr tun; 232 | Address address; 233 | 234 | if (address.fromCidr(cidr)) { 235 | return -1; 236 | } 237 | spdlog::info("client address: {}", address.toCidr()); 238 | tun = std::any_cast>(this->impl); 239 | if (tun->setIP(address.Host())) { 240 | return -1; 241 | } 242 | if (tun->setMask(address.Mask())) { 243 | return -1; 244 | } 245 | this->tunAddress = cidr; 246 | return 0; 247 | } 248 | 249 | IP4 Tun::getIP() { 250 | std::shared_ptr tun; 251 | tun = std::any_cast>(this->impl); 252 | return tun->getIP(); 253 | } 254 | 255 | int Tun::setMTU(int mtu) { 256 | std::shared_ptr tun; 257 | tun = std::any_cast>(this->impl); 258 | if (tun->setMTU(mtu)) { 259 | return -1; 260 | } 261 | return 0; 262 | } 263 | 264 | int Tun::up() { 265 | std::shared_ptr tun; 266 | tun = std::any_cast>(this->impl); 267 | return tun->up(); 268 | } 269 | 270 | int Tun::down() { 271 | std::shared_ptr tun; 272 | tun = std::any_cast>(this->impl); 273 | return tun->down(); 274 | } 275 | 276 | int Tun::read(std::string &buffer) { 277 | std::shared_ptr tun; 278 | tun = std::any_cast>(this->impl); 279 | return tun->read(buffer); 280 | } 281 | 282 | int Tun::write(const std::string &buffer) { 283 | std::shared_ptr tun; 284 | tun = std::any_cast>(this->impl); 285 | return tun->write(buffer); 286 | } 287 | 288 | int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 289 | std::shared_ptr tun; 290 | tun = std::any_cast>(this->impl); 291 | return tun->setSysRtTable(dst, mask, nexthop); 292 | } 293 | 294 | } // namespace candy 295 | 296 | #endif 297 | -------------------------------------------------------------------------------- /candy-cli/src/config.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "config.h" 3 | #include "argparse.h" 4 | #include "candy/candy.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | Poco::JSON::Object arguments::json() { 18 | Poco::JSON::Object config; 19 | config.set("mode", this->mode); 20 | config.set("websocket", this->websocket); 21 | config.set("password", this->password); 22 | 23 | if (this->mode == "client") { 24 | config.set("name", this->name); 25 | config.set("tun", this->tun); 26 | config.set("stun", this->stun); 27 | config.set("localhost", this->localhost); 28 | config.set("discovery", this->discovery); 29 | config.set("route", this->routeCost); 30 | config.set("mtu", this->mtu); 31 | config.set("port", this->port); 32 | config.set("vmac", virtualMac(this->name)); 33 | config.set("expt", loadTunAddress(this->name)); 34 | } 35 | 36 | if (this->mode == "server") { 37 | config.set("dhcp", this->dhcp); 38 | config.set("sdwan", this->sdwan); 39 | } 40 | 41 | return config; 42 | } 43 | 44 | int arguments::parse(int argc, char *argv[]) { 45 | argparse::ArgumentParser program("candy", candy::version()); 46 | 47 | program.add_argument("-c", "--config").help("config file path"); 48 | program.add_argument("-m", "--mode").help("working mode"); 49 | program.add_argument("-w", "--websocket").help("websocket address"); 50 | program.add_argument("-p", "--password").help("authorization password"); 51 | program.add_argument("-d", "--dhcp").help("dhcp address range"); 52 | program.add_argument("--sdwan").help("software-defined wide area network"); 53 | program.add_argument("-n", "--name").help("network interface name"); 54 | program.add_argument("-t", "--tun").help("static address"); 55 | program.add_argument("-s", "--stun").help("stun address"); 56 | program.add_argument("--port").help("p2p listen port").scan<'i', int>(); 57 | program.add_argument("--mtu").help("maximum transmission unit").scan<'i', int>(); 58 | program.add_argument("-r", "--route").help("routing cost").scan<'i', int>(); 59 | program.add_argument("--discovery").help("discovery interval").scan<'i', int>(); 60 | program.add_argument("--localhost").help("local ip"); 61 | 62 | program.add_argument("--no-timestamp").implicit_value(true); 63 | program.add_argument("--debug").implicit_value(true); 64 | 65 | try { 66 | program.parse_args(argc, argv); 67 | if (program.is_used("--config")) { 68 | parseFile(program.get("--config")); 69 | } 70 | 71 | if (program.is_used("--mode")) { 72 | this->mode = program.get("--mode"); 73 | } 74 | 75 | program.set_if_used("--mode", this->mode); 76 | program.set_if_used("--websocket", this->websocket); 77 | program.set_if_used("--password", this->password); 78 | program.set_if_used("--no-timestamp", this->noTimestamp); 79 | program.set_if_used("--debug", this->debug); 80 | program.set_if_used("--dhcp", this->dhcp); 81 | program.set_if_used("--sdwan", this->sdwan); 82 | program.set_if_used("--name", this->name); 83 | program.set_if_used("--tun", this->tun); 84 | program.set_if_used("--stun", this->stun); 85 | program.set_if_used("--localhost", this->localhost); 86 | program.set_if_used("--port", this->port); 87 | program.set_if_used("--mtu", this->mtu); 88 | program.set_if_used("--discovery", this->discovery); 89 | program.set_if_used("--route", this->routeCost); 90 | 91 | bool needShowUsage = [&]() { 92 | if (this->mode != "client" && this->mode != "server") 93 | return true; 94 | if (this->websocket.empty()) 95 | return true; 96 | 97 | return false; 98 | }(); 99 | 100 | if (needShowUsage) { 101 | std::cout << program.usage() << std::endl; 102 | exit(1); 103 | } 104 | 105 | if (this->noTimestamp) { 106 | spdlog::set_pattern("[%^%l%$] %v"); 107 | } 108 | if (this->debug) { 109 | spdlog::set_level(spdlog::level::debug); 110 | } 111 | return 0; 112 | } catch (const std::exception &e) { 113 | std::cout << program.usage() << std::endl; 114 | exit(1); 115 | } 116 | } 117 | 118 | void arguments::parseFile(std::string cfgFile) { 119 | try { 120 | std::map> cfgHandlers = { 121 | {"mode", [&](const std::string &value) { this->mode = value; }}, 122 | {"websocket", [&](const std::string &value) { this->websocket = value; }}, 123 | {"password", [&](const std::string &value) { this->password = value; }}, 124 | {"debug", [&](const std::string &value) { this->debug = (value == "true"); }}, 125 | {"dhcp", [&](const std::string &value) { this->dhcp = value; }}, 126 | {"sdwan", [&](const std::string &value) { this->sdwan = value; }}, 127 | {"tun", [&](const std::string &value) { this->tun = value; }}, 128 | {"stun", [&](const std::string &value) { this->stun = value; }}, 129 | {"name", [&](const std::string &value) { this->name = value; }}, 130 | {"discovery", [&](const std::string &value) { this->discovery = std::stoi(value); }}, 131 | {"route", [&](const std::string &value) { this->routeCost = std::stoi(value); }}, 132 | {"port", [&](const std::string &value) { this->port = std::stoi(value); }}, 133 | {"mtu", [&](const std::string &value) { this->mtu = std::stoi(value); }}, 134 | {"localhost", [&](const std::string &value) { this->localhost = value; }}, 135 | }; 136 | auto trim = [](std::string str) { 137 | if (str.length() >= 2 && str.front() == '\"' && str.back() == '\"') { 138 | return str.substr(1, str.length() - 2); 139 | } 140 | return str; 141 | }; 142 | auto configs = fileToKvMap(cfgFile); 143 | for (auto cfg : configs) { 144 | auto handler = cfgHandlers.find(cfg.first); 145 | if (handler != cfgHandlers.end()) { 146 | handler->second(trim(cfg.second)); 147 | } else { 148 | spdlog::warn("unknown config: {}={}", cfg.first, cfg.second); 149 | } 150 | } 151 | } catch (std::exception &e) { 152 | spdlog::error("parse config file failed: {}", e.what()); 153 | exit(1); 154 | } 155 | } 156 | 157 | std::map arguments::fileToKvMap(const std::string &filename) { 158 | std::map config; 159 | std::ifstream file(filename); 160 | std::string line; 161 | 162 | while (std::getline(file, line)) { 163 | line = Poco::trimLeft(line); 164 | if (line.empty() || line.front() == '#') 165 | continue; 166 | line.erase(line.find_last_not_of(" \t;") + 1); 167 | std::size_t delimiterPos = line.find('='); 168 | if (delimiterPos != std::string::npos) { 169 | std::string key = Poco::trim(line.substr(0, delimiterPos)); 170 | std::string value = Poco::trim(line.substr(delimiterPos + 1)); 171 | config[key] = value; 172 | } 173 | } 174 | return config; 175 | } 176 | 177 | int saveTunAddress(const std::string &name, const std::string &cidr) { 178 | try { 179 | std::string cache = storageDirectory("address/"); 180 | cache += name.empty() ? "__noname__" : name; 181 | std::filesystem::create_directories(std::filesystem::path(cache).parent_path()); 182 | std::ofstream ofs(cache); 183 | if (ofs.is_open()) { 184 | ofs << cidr; 185 | ofs.close(); 186 | } 187 | return 0; 188 | } catch (std::exception &e) { 189 | spdlog::critical("save latest address failed: {}", e.what()); 190 | return -1; 191 | } 192 | } 193 | 194 | std::string loadTunAddress(const std::string &name) { 195 | std::string cache = storageDirectory("address/"); 196 | cache += name.empty() ? "__noname__" : name; 197 | std::ifstream ifs(cache); 198 | if (ifs.is_open()) { 199 | std::stringstream ss; 200 | ss << ifs.rdbuf(); 201 | ifs.close(); 202 | return ss.str(); 203 | } 204 | return "0.0.0.0/0"; 205 | } 206 | 207 | std::string virtualMacHelper(std::string name = "") { 208 | try { 209 | std::string path = storageDirectory("vmac/"); 210 | path += name.empty() ? "__noname__" : name; 211 | char buffer[candy::VMAC_SIZE]; 212 | std::stringstream ss; 213 | std::ifstream ifs(path); 214 | if (ifs.is_open()) { 215 | ifs.read(buffer, sizeof(buffer)); 216 | if (ifs) { 217 | for (int i = 0; i < (int)sizeof(buffer); i++) { 218 | ss << std::hex << buffer[i]; 219 | } 220 | } 221 | ifs.close(); 222 | return ss.str(); 223 | } 224 | return ""; 225 | } catch (std::exception &e) { 226 | return ""; 227 | } 228 | } 229 | 230 | std::string initVirtualMac() { 231 | try { 232 | std::string path = storageDirectory("vmac/__noname__"); 233 | std::filesystem::create_directories(std::filesystem::path(path).parent_path()); 234 | std::string vmac = candy::create_vmac(); 235 | std::ofstream ofs(path); 236 | if (ofs.is_open()) { 237 | ofs << vmac; 238 | ofs.close(); 239 | } 240 | return vmac; 241 | } catch (std::exception &e) { 242 | spdlog::critical("init vmac failed: {}", e.what()); 243 | return ""; 244 | } 245 | } 246 | 247 | std::string virtualMac(const std::string &name) { 248 | std::string path; 249 | // 兼容老版本,优先获取与配置网卡名对应的 vmac 250 | path = virtualMacHelper(name); 251 | if (!path.empty()) { 252 | return path; 253 | } 254 | // 获取网卡名无关的全局 vmac 255 | path = virtualMacHelper(); 256 | if (!path.empty()) { 257 | return path; 258 | } 259 | // 初次启动,生成全局 vmac 260 | return initVirtualMac(); 261 | } 262 | 263 | bool starts_with(const std::string &str, const std::string &prefix) { 264 | return str.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), str.begin()); 265 | } 266 | 267 | #if POCO_OS == POCO_OS_WINDOWS_NT 268 | std::string storageDirectory(std::string subdir) { 269 | return "C:/ProgramData/Candy/" + subdir; 270 | } 271 | #else 272 | std::string storageDirectory(std::string subdir) { 273 | return "/var/lib/candy/" + subdir; 274 | } 275 | #endif 276 | -------------------------------------------------------------------------------- /candy-service/src/main.cc: -------------------------------------------------------------------------------- 1 | #include "candy/client.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | std::mutex threadMutex; 29 | std::map threadMap; 30 | 31 | class BaseJSONHandler : public Poco::Net::HTTPRequestHandler { 32 | protected: 33 | Poco::JSON::Object::Ptr readRequest(Poco::Net::HTTPServerRequest &request) { 34 | Poco::JSON::Parser parser; 35 | Poco::Dynamic::Var result = parser.parse(request.stream()); 36 | return result.extract(); 37 | } 38 | 39 | void sendResponse(Poco::Net::HTTPServerResponse &response, const Poco::JSON::Object::Ptr &json) { 40 | response.setChunkedTransferEncoding(true); 41 | response.setContentType("application/json"); 42 | Poco::JSON::Stringifier::stringify(json, response.send()); 43 | } 44 | }; 45 | 46 | class RunHandler : public BaseJSONHandler { 47 | public: 48 | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override { 49 | if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) { 50 | response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); 51 | return; 52 | } 53 | 54 | auto json = readRequest(request); 55 | auto id = json->getValue("id"); 56 | auto config = json->getObject("config"); 57 | json->remove("config"); 58 | 59 | std::lock_guard lock(threadMutex); 60 | auto it = threadMap.find(id); 61 | if (it != threadMap.end()) { 62 | json->set("message", "id already exists"); 63 | } else { 64 | auto thread = std::thread([=]() { candy::client::run(id, *config); }); 65 | threadMap.insert({id, std::move(thread)}); 66 | json->set("message", "success"); 67 | } 68 | 69 | sendResponse(response, json); 70 | } 71 | }; 72 | 73 | class StatusHandler : public BaseJSONHandler { 74 | public: 75 | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override { 76 | if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) { 77 | response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); 78 | return; 79 | } 80 | 81 | auto json = readRequest(request); 82 | auto id = json->getValue("id"); 83 | 84 | std::lock_guard lock(threadMutex); 85 | auto it = threadMap.find(id); 86 | if (it != threadMap.end()) { 87 | if (auto status = candy::client::status(id)) { 88 | json->set("status", *status); 89 | json->set("message", "success"); 90 | } else { 91 | json->set("message", "unable to get status"); 92 | } 93 | } else { 94 | json->set("message", "id does not exist"); 95 | } 96 | 97 | sendResponse(response, json); 98 | } 99 | }; 100 | 101 | class ShutdownHandler : public BaseJSONHandler { 102 | public: 103 | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) override { 104 | if (request.getMethod() != Poco::Net::HTTPRequest::HTTP_POST) { 105 | response.setStatus(Poco::Net::HTTPResponse::HTTP_METHOD_NOT_ALLOWED); 106 | return; 107 | } 108 | 109 | auto json = readRequest(request); 110 | auto id = json->getValue("id"); 111 | candy::client::shutdown(id); 112 | 113 | std::lock_guard lock(threadMutex); 114 | auto it = threadMap.find(id); 115 | if (it != threadMap.end()) { 116 | it->second.detach(); 117 | threadMap.erase(it); 118 | json->set("message", "success"); 119 | } else { 120 | json->set("message", "id does not exist"); 121 | } 122 | 123 | sendResponse(response, json); 124 | } 125 | }; 126 | 127 | class JSONRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory { 128 | public: 129 | Poco::Net::HTTPRequestHandler *createRequestHandler(const Poco::Net::HTTPServerRequest &request) override { 130 | const std::string &uri = request.getURI(); 131 | 132 | if (uri == "/api/run") { 133 | return new RunHandler; 134 | } else if (uri == "/api/status") { 135 | return new StatusHandler; 136 | } else if (uri == "/api/shutdown") { 137 | return new ShutdownHandler; 138 | } 139 | 140 | return nullptr; 141 | } 142 | }; 143 | 144 | class CandyServiceApp : public Poco::Util::ServerApplication { 145 | protected: 146 | std::string bindAddress; 147 | int port = 0; 148 | bool helpRequested = false; 149 | std::string logdir; 150 | std::string loglevel; 151 | 152 | void initialize(Poco::Util::Application &self) override { 153 | loadConfiguration(); 154 | Poco::Util::ServerApplication::initialize(self); 155 | } 156 | 157 | void defineOptions(Poco::Util::OptionSet &options) override { 158 | Poco::Util::ServerApplication::defineOptions(options); 159 | 160 | options.addOption(Poco::Util::Option("help", "", "Display help information") 161 | .required(false) 162 | .repeatable(false) 163 | .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleHelp))); 164 | 165 | options.addOption(Poco::Util::Option("bind", "", "Bind address and port (address:port)") 166 | .required(false) 167 | .repeatable(false) 168 | .argument("address:port") 169 | .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleBind))); 170 | options.addOption(Poco::Util::Option("logdir", "", "Specify log directory") 171 | .required(false) 172 | .repeatable(false) 173 | .argument("path") 174 | .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleLogDir))); 175 | options.addOption(Poco::Util::Option("loglevel", "", "Specify log level") 176 | .required(false) 177 | .repeatable(false) 178 | .argument("level") 179 | .callback(Poco::Util::OptionCallback(this, &CandyServiceApp::handleLogLevel))); 180 | } 181 | 182 | void handleHelp(const std::string &name, const std::string &value) { 183 | helpRequested = true; 184 | displayHelp(); 185 | stopOptionsProcessing(); 186 | } 187 | 188 | void handleBind(const std::string &name, const std::string &value) { 189 | size_t pos = value.find(':'); 190 | if (pos == std::string::npos) { 191 | std::cerr << "Invalid bind format. Use address:port (e.g., 0.0.0.0:26817)" << std::endl; 192 | std::exit(EXIT_FAILURE); 193 | } 194 | 195 | bindAddress = value.substr(0, pos); 196 | try { 197 | port = std::stoi(value.substr(pos + 1)); 198 | } catch (const std::exception &e) { 199 | std::cerr << "Invalid port number: " << e.what() << std::endl; 200 | std::exit(EXIT_FAILURE); 201 | } 202 | } 203 | 204 | void handleLogDir(const std::string &name, const std::string &dir) { 205 | this->logdir = dir; 206 | } 207 | 208 | void handleLogLevel(const std::string &name, const std::string &level) { 209 | this->loglevel = level; 210 | } 211 | 212 | void displayHelp() { 213 | Poco::Util::HelpFormatter helpFormatter(options()); 214 | helpFormatter.setCommand(commandName()); 215 | helpFormatter.format(std::cout); 216 | } 217 | 218 | int main(const std::vector &args) override { 219 | if (helpRequested) { 220 | return Poco::Util::Application::EXIT_OK; 221 | } 222 | 223 | if (!logdir.empty()) { 224 | Poco::File(logdir).createDirectories(); 225 | auto logger = spdlog::rotating_logger_mt("app", logdir + "/app.log", 10 * 1024 * 1024, 5, true); 226 | spdlog::set_default_logger(logger); 227 | } 228 | 229 | if (!loglevel.empty()) { 230 | spdlog::set_level(spdlog::level::from_str(loglevel)); 231 | } 232 | 233 | if (bindAddress.empty()) { 234 | bindAddress = "localhost"; 235 | port = 26817; 236 | } 237 | 238 | try { 239 | Poco::Net::ServerSocket socket; 240 | socket.bind(Poco::Net::SocketAddress(bindAddress, port)); 241 | socket.listen(); 242 | 243 | Poco::Net::HTTPServerParams *params = new Poco::Net::HTTPServerParams; 244 | params->setMaxQueued(10); 245 | params->setMaxThreads(1); 246 | 247 | Poco::Net::HTTPServer server(new JSONRequestHandlerFactory, socket, params); 248 | 249 | server.start(); 250 | spdlog::info("bind: {}:{}", bindAddress, port); 251 | 252 | waitForTerminationRequest(); 253 | spdlog::info("exit signal detected"); 254 | 255 | server.stop(); 256 | 257 | std::lock_guard lock(threadMutex); 258 | for (auto &[id, thread] : threadMap) { 259 | candy::client::shutdown(id); 260 | if (thread.joinable()) { 261 | thread.join(); 262 | } 263 | } 264 | 265 | } catch (const Poco::Exception &exc) { 266 | std::cerr << "Fatal error: " << exc.displayText() << std::endl; 267 | return Poco::Util::Application::EXIT_SOFTWARE; 268 | } catch (const std::exception &e) { 269 | std::cerr << "Fatal error: " << e.what() << std::endl; 270 | return Poco::Util::Application::EXIT_SOFTWARE; 271 | } 272 | 273 | return Poco::Util::Application::EXIT_OK; 274 | } 275 | }; 276 | 277 | int main(int argc, char **argv) { 278 | CandyServiceApp app; 279 | return app.run(argc, argv); 280 | } 281 | -------------------------------------------------------------------------------- /candy/src/tun/macos.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include 3 | #if POCO_OS == POCO_OS_MAC_OS_X 4 | 5 | #include "core/net.h" 6 | #include "tun/tun.h" 7 | #include 8 | #include 9 | #include 10 | // clang-format off 11 | #include 12 | #include 13 | // clang-format on 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace candy { 28 | 29 | class MacTun { 30 | public: 31 | int setName(const std::string &name) { 32 | this->name = name.empty() ? "candy" : "candy-" + name; 33 | return 0; 34 | } 35 | 36 | int setIP(IP4 ip) { 37 | this->ip = ip; 38 | return 0; 39 | } 40 | 41 | IP4 getIP() { 42 | return this->ip; 43 | } 44 | 45 | int setMask(IP4 mask) { 46 | this->mask = mask; 47 | return 0; 48 | } 49 | 50 | int setMTU(int mtu) { 51 | this->mtu = mtu; 52 | return 0; 53 | } 54 | 55 | int up() { 56 | // 创建设备,操作系统不允许自定义设备名,只能由内核分配 57 | this->tunFd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); 58 | if (this->tunFd < 0) { 59 | spdlog::critical("create socket failed: {}", strerror(errno)); 60 | return -1; 61 | } 62 | int flags = fcntl(this->tunFd, F_GETFL, 0); 63 | if (flags < 0) { 64 | spdlog::error("get tun flags failed: {}", strerror(errno)); 65 | close(this->tunFd); 66 | return -1; 67 | } 68 | flags |= O_NONBLOCK; 69 | if (fcntl(this->tunFd, F_SETFL, flags) < 0) { 70 | spdlog::error("set non-blocking tun failed: {}", strerror(errno)); 71 | close(this->tunFd); 72 | return -1; 73 | } 74 | 75 | struct ctl_info info; 76 | memset(&info, 0, sizeof(info)); 77 | strncpy(info.ctl_name, UTUN_CONTROL_NAME, MAX_KCTL_NAME); 78 | if (ioctl(this->tunFd, CTLIOCGINFO, &info) == -1) { 79 | spdlog::critical("get control id failed: {}", strerror(errno)); 80 | close(this->tunFd); 81 | return -1; 82 | } 83 | 84 | struct sockaddr_ctl ctl; 85 | memset(&ctl, 0, sizeof(ctl)); 86 | ctl.sc_len = sizeof(ctl); 87 | ctl.sc_family = AF_SYSTEM; 88 | ctl.ss_sysaddr = AF_SYS_CONTROL; 89 | ctl.sc_id = info.ctl_id; 90 | ctl.sc_unit = 0; 91 | if (connect(this->tunFd, (struct sockaddr *)&ctl, sizeof(ctl)) == -1) { 92 | spdlog::critical("connect to control failed: {}", strerror(errno)); 93 | close(this->tunFd); 94 | return -1; 95 | } 96 | 97 | socklen_t ifname_len = sizeof(ifname); 98 | if (getsockopt(this->tunFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &ifname_len) == -1) { 99 | spdlog::critical("get interface name failed: {}", strerror(errno)); 100 | close(this->tunFd); 101 | return -1; 102 | } 103 | 104 | spdlog::debug("created utun interface: {}", ifname); 105 | 106 | struct ifreq ifr; 107 | memset(&ifr, 0, sizeof(ifr)); 108 | strncpy(ifr.ifr_name, ifname, IFNAMSIZ); 109 | 110 | // 创建 socket, 并通过这个 socket 更新网卡的其他配置 111 | struct sockaddr_in *addr; 112 | addr = (struct sockaddr_in *)&ifr.ifr_addr; 113 | addr->sin_family = AF_INET; 114 | int sockfd = socket(addr->sin_family, SOCK_DGRAM, 0); 115 | if (sockfd == -1) { 116 | spdlog::critical("create socket failed"); 117 | close(this->tunFd); 118 | return -1; 119 | } 120 | 121 | // 设置地址和掩码 122 | struct ifaliasreq areq; 123 | memset(&areq, 0, sizeof(areq)); 124 | strncpy(areq.ifra_name, ifname, IFNAMSIZ); 125 | ((struct sockaddr_in *)&areq.ifra_addr)->sin_family = AF_INET; 126 | ((struct sockaddr_in *)&areq.ifra_addr)->sin_len = sizeof(areq.ifra_addr); 127 | ((struct sockaddr_in *)&areq.ifra_addr)->sin_addr.s_addr = this->ip; 128 | 129 | ((struct sockaddr_in *)&areq.ifra_mask)->sin_family = AF_INET; 130 | ((struct sockaddr_in *)&areq.ifra_mask)->sin_len = sizeof(areq.ifra_mask); 131 | ((struct sockaddr_in *)&areq.ifra_mask)->sin_addr.s_addr = this->mask; 132 | 133 | ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_family = AF_INET; 134 | ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_len = sizeof(areq.ifra_broadaddr); 135 | ((struct sockaddr_in *)&areq.ifra_broadaddr)->sin_addr.s_addr = (this->ip & this->mask); 136 | 137 | if (ioctl(sockfd, SIOCAIFADDR, (void *)&areq) == -1) { 138 | spdlog::critical("set ip mask failed: {}: ip {} mask {}", strerror(errno), this->ip.toString(), 139 | this->mask.toString()); 140 | close(sockfd); 141 | close(this->tunFd); 142 | return -1; 143 | } 144 | 145 | // 设置 MTU 146 | ifr.ifr_mtu = this->mtu; 147 | if (ioctl(sockfd, SIOCSIFMTU, &ifr) == -1) { 148 | spdlog::critical("set mtu failed: mtu {}", this->mtu); 149 | close(sockfd); 150 | close(this->tunFd); 151 | return -1; 152 | } 153 | 154 | // 设置 flags 155 | if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) == -1) { 156 | spdlog::critical("get interface flags failed"); 157 | close(sockfd); 158 | close(this->tunFd); 159 | return -1; 160 | } 161 | ifr.ifr_flags |= IFF_UP | IFF_RUNNING; 162 | if (ioctl(sockfd, SIOCSIFFLAGS, &ifr) == -1) { 163 | spdlog::critical("set interface flags failed"); 164 | close(sockfd); 165 | close(this->tunFd); 166 | return -1; 167 | } 168 | close(sockfd); 169 | 170 | // 设置路由 171 | if (setSysRtTable(this->ip & this->mask, this->mask, this->ip)) { 172 | close(this->tunFd); 173 | return -1; 174 | } 175 | return 0; 176 | } 177 | 178 | int down() { 179 | close(this->tunFd); 180 | return 0; 181 | } 182 | 183 | int read(std::string &buffer) { 184 | buffer.resize(this->mtu); 185 | struct iovec iov[2]; 186 | iov[0].iov_base = &this->packetinfo; 187 | iov[0].iov_len = sizeof(this->packetinfo); 188 | iov[1].iov_base = buffer.data(); 189 | iov[1].iov_len = buffer.size(); 190 | 191 | int n = ::readv(this->tunFd, iov, sizeof(iov) / sizeof(iov[0])); 192 | if (n >= 0) { 193 | buffer.resize(n - sizeof(this->packetinfo)); 194 | return n; 195 | } 196 | 197 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 198 | struct timeval timeout = {.tv_sec = 1}; 199 | fd_set set; 200 | 201 | FD_ZERO(&set); 202 | FD_SET(this->tunFd, &set); 203 | 204 | select(this->tunFd + 1, &set, NULL, NULL, &timeout); 205 | return 0; 206 | } 207 | 208 | spdlog::warn("tun read failed: error {}", n); 209 | return -1; 210 | } 211 | 212 | int write(const std::string &buffer) { 213 | struct iovec iov[2]; 214 | iov[0].iov_base = &this->packetinfo; 215 | iov[0].iov_len = sizeof(this->packetinfo); 216 | iov[1].iov_base = (void *)buffer.data(); 217 | iov[1].iov_len = buffer.size(); 218 | return ::writev(this->tunFd, iov, sizeof(iov) / sizeof(iov[0])) - sizeof(sizeof(this->packetinfo)); 219 | } 220 | 221 | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 222 | struct { 223 | struct rt_msghdr msghdr; 224 | struct sockaddr_in addr[3]; 225 | } msg; 226 | 227 | memset(&msg, 0, sizeof(msg)); 228 | msg.msghdr.rtm_msglen = sizeof(msg); 229 | msg.msghdr.rtm_version = RTM_VERSION; 230 | msg.msghdr.rtm_type = RTM_ADD; 231 | msg.msghdr.rtm_addrs = RTA_DST | RTA_GATEWAY | RTA_NETMASK; 232 | msg.msghdr.rtm_flags = RTF_UP | RTA_GATEWAY; 233 | for (int idx = 0; idx < (int)(sizeof(msg.addr) / sizeof(msg.addr[0])); ++idx) { 234 | msg.addr[idx].sin_len = sizeof(msg.addr[0]); 235 | msg.addr[idx].sin_family = AF_INET; 236 | } 237 | msg.addr[0].sin_addr.s_addr = dst; 238 | msg.addr[1].sin_addr.s_addr = nexthop; 239 | msg.addr[2].sin_addr.s_addr = mask; 240 | 241 | int routefd = socket(AF_ROUTE, SOCK_RAW, 0); 242 | if (routefd < 0) { 243 | spdlog::error("create route fd failed: {}", strerror(routefd)); 244 | return -1; 245 | } 246 | if (::write(routefd, &msg, sizeof(msg)) == -1) { 247 | spdlog::error("add route failed: {}", strerror(errno)); 248 | close(routefd); 249 | return -1; 250 | } 251 | close(routefd); 252 | return 0; 253 | } 254 | 255 | private: 256 | std::string name; 257 | char ifname[IFNAMSIZ] = {0}; 258 | IP4 ip; 259 | IP4 mask; 260 | int mtu; 261 | int timeout; 262 | int tunFd; 263 | 264 | uint8_t packetinfo[4] = {0x00, 0x00, 0x00, 0x02}; 265 | }; 266 | 267 | } // namespace candy 268 | 269 | namespace candy { 270 | 271 | Tun::Tun() { 272 | this->impl = std::make_shared(); 273 | } 274 | 275 | Tun::~Tun() { 276 | this->impl.reset(); 277 | } 278 | 279 | int Tun::setName(const std::string &name) { 280 | std::shared_ptr tun; 281 | 282 | tun = std::any_cast>(this->impl); 283 | tun->setName(name); 284 | return 0; 285 | } 286 | 287 | int Tun::setAddress(const std::string &cidr) { 288 | std::shared_ptr tun; 289 | Address address; 290 | 291 | if (address.fromCidr(cidr)) { 292 | return -1; 293 | } 294 | spdlog::info("client address: {}", address.toCidr()); 295 | tun = std::any_cast>(this->impl); 296 | if (tun->setIP(address.Host())) { 297 | return -1; 298 | } 299 | if (tun->setMask(address.Mask())) { 300 | return -1; 301 | } 302 | return 0; 303 | } 304 | 305 | IP4 Tun::getIP() { 306 | std::shared_ptr tun; 307 | tun = std::any_cast>(this->impl); 308 | return tun->getIP(); 309 | } 310 | 311 | int Tun::setMTU(int mtu) { 312 | std::shared_ptr tun; 313 | tun = std::any_cast>(this->impl); 314 | if (tun->setMTU(mtu)) { 315 | return -1; 316 | } 317 | return 0; 318 | } 319 | 320 | int Tun::up() { 321 | std::shared_ptr tun; 322 | tun = std::any_cast>(this->impl); 323 | return tun->up(); 324 | } 325 | 326 | int Tun::down() { 327 | std::shared_ptr tun; 328 | tun = std::any_cast>(this->impl); 329 | return tun->down(); 330 | } 331 | 332 | int Tun::read(std::string &buffer) { 333 | std::shared_ptr tun; 334 | tun = std::any_cast>(this->impl); 335 | return tun->read(buffer); 336 | } 337 | 338 | int Tun::write(const std::string &buffer) { 339 | std::shared_ptr tun; 340 | tun = std::any_cast>(this->impl); 341 | return tun->write(buffer); 342 | } 343 | 344 | int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 345 | std::shared_ptr tun; 346 | tun = std::any_cast>(this->impl); 347 | return tun->setSysRtTable(dst, mask, nexthop); 348 | } 349 | 350 | } // namespace candy 351 | 352 | #endif 353 | -------------------------------------------------------------------------------- /candy/src/tun/windows.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include 3 | #if POCO_OS == POCO_OS_WINDOWS_NT 4 | 5 | #include "core/net.h" 6 | #include "tun/tun.h" 7 | #include "utils/codecvt.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | // clang-format off 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | // clang-format on 27 | #pragma GCC diagnostic push 28 | #pragma GCC diagnostic ignored "-Wunknown-pragmas" 29 | #include 30 | #pragma GCC diagnostic pop 31 | 32 | namespace candy { 33 | 34 | WINTUN_CREATE_ADAPTER_FUNC *WintunCreateAdapter; 35 | WINTUN_CLOSE_ADAPTER_FUNC *WintunCloseAdapter; 36 | WINTUN_OPEN_ADAPTER_FUNC *WintunOpenAdapter; 37 | WINTUN_GET_ADAPTER_LUID_FUNC *WintunGetAdapterLUID; 38 | WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC *WintunGetRunningDriverVersion; 39 | WINTUN_DELETE_DRIVER_FUNC *WintunDeleteDriver; 40 | WINTUN_SET_LOGGER_FUNC *WintunSetLogger; 41 | WINTUN_START_SESSION_FUNC *WintunStartSession; 42 | WINTUN_END_SESSION_FUNC *WintunEndSession; 43 | WINTUN_GET_READ_WAIT_EVENT_FUNC *WintunGetReadWaitEvent; 44 | WINTUN_RECEIVE_PACKET_FUNC *WintunReceivePacket; 45 | WINTUN_RELEASE_RECEIVE_PACKET_FUNC *WintunReleaseReceivePacket; 46 | WINTUN_ALLOCATE_SEND_PACKET_FUNC *WintunAllocateSendPacket; 47 | WINTUN_SEND_PACKET_FUNC *WintunSendPacket; 48 | 49 | class Holder { 50 | public: 51 | static bool Ok() { 52 | static Holder instance; 53 | return instance.wintun; 54 | } 55 | 56 | private: 57 | Holder() { 58 | this->wintun = LoadLibraryExW(L"wintun.dll", NULL, LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); 59 | if (!this->wintun) { 60 | spdlog::critical("load wintun.dll failed"); 61 | return; 62 | } 63 | #pragma GCC diagnostic push 64 | #pragma GCC diagnostic ignored "-Wstrict-aliasing" 65 | #define X(Name) ((*(FARPROC *)&Name = GetProcAddress(this->wintun, #Name)) == NULL) 66 | if (X(WintunCreateAdapter) || X(WintunCloseAdapter) || X(WintunOpenAdapter) || X(WintunGetAdapterLUID) || 67 | X(WintunGetRunningDriverVersion) || X(WintunDeleteDriver) || X(WintunSetLogger) || X(WintunStartSession) || 68 | X(WintunEndSession) || X(WintunGetReadWaitEvent) || X(WintunReceivePacket) || X(WintunReleaseReceivePacket) || 69 | X(WintunAllocateSendPacket) || X(WintunSendPacket)) 70 | #undef X 71 | #pragma GCC diagnostic pop 72 | { 73 | spdlog::critical("get function from wintun.dll failed"); 74 | FreeLibrary(this->wintun); 75 | this->wintun = NULL; 76 | return; 77 | } 78 | } 79 | 80 | ~Holder() { 81 | if (this->wintun) { 82 | WintunDeleteDriver(); 83 | FreeLibrary(this->wintun); 84 | this->wintun = NULL; 85 | } 86 | } 87 | 88 | HMODULE wintun = NULL; 89 | }; 90 | 91 | class WindowsTun { 92 | public: 93 | int setName(const std::string &name) { 94 | this->name = name.empty() ? "candy" : name; 95 | return 0; 96 | } 97 | 98 | int setIP(IP4 ip) { 99 | this->ip = ip; 100 | return 0; 101 | } 102 | 103 | IP4 getIP() { 104 | return this->ip; 105 | } 106 | 107 | int setPrefix(uint32_t prefix) { 108 | this->prefix = prefix; 109 | return 0; 110 | } 111 | 112 | int setMTU(int mtu) { 113 | this->mtu = mtu; 114 | return 0; 115 | } 116 | 117 | int up() { 118 | if (!Holder::Ok()) { 119 | spdlog::critical("init wintun failed"); 120 | return -1; 121 | } 122 | 123 | GUID Guid; 124 | std::string data = "CandyGuid" + this->name; 125 | unsigned char hash[SHA256_DIGEST_LENGTH]; 126 | SHA256((unsigned char *)data.c_str(), data.size(), hash); 127 | memcpy(&Guid, hash, sizeof(Guid)); 128 | this->adapter = WintunCreateAdapter(UTF8ToUTF16(this->name).c_str(), L"Candy", &Guid); 129 | if (!this->adapter) { 130 | spdlog::critical("create wintun adapter failed: {}", GetLastError()); 131 | return -1; 132 | } 133 | int Error; 134 | MIB_UNICASTIPADDRESS_ROW AddressRow; 135 | InitializeUnicastIpAddressEntry(&AddressRow); 136 | WintunGetAdapterLUID(this->adapter, &AddressRow.InterfaceLuid); 137 | AddressRow.Address.Ipv4.sin_family = AF_INET; 138 | AddressRow.Address.Ipv4.sin_addr.S_un.S_addr = this->ip; 139 | AddressRow.OnLinkPrefixLength = this->prefix; 140 | AddressRow.DadState = IpDadStatePreferred; 141 | Error = CreateUnicastIpAddressEntry(&AddressRow); 142 | if (Error != ERROR_SUCCESS) { 143 | spdlog::critical("create unicast ip address entry failed: {}", Error); 144 | return -1; 145 | } 146 | 147 | MIB_IPINTERFACE_ROW Interface = {0}; 148 | Interface.Family = AF_INET; 149 | Interface.InterfaceLuid = AddressRow.InterfaceLuid; 150 | Error = GetIpInterfaceEntry(&Interface); 151 | if (Error != NO_ERROR) { 152 | spdlog::critical("get ip interface entry failed: {}", Error); 153 | return -1; 154 | } 155 | this->ifindex = Interface.InterfaceIndex; 156 | Interface.SitePrefixLength = 0; 157 | Interface.NlMtu = this->mtu; 158 | Error = SetIpInterfaceEntry(&Interface); 159 | if (Error != NO_ERROR) { 160 | spdlog::critical("set ip interface entry failed: {}", Error); 161 | return -1; 162 | } 163 | 164 | this->session = WintunStartSession(this->adapter, WINTUN_MIN_RING_CAPACITY); 165 | if (!this->session) { 166 | spdlog::critical("start wintun session failed: {}", GetLastError()); 167 | return -1; 168 | } 169 | return 0; 170 | } 171 | 172 | int down() { 173 | while (!routes.empty()) { 174 | DeleteIpForwardEntry(&routes.top()); 175 | routes.pop(); 176 | } 177 | 178 | if (this->session) { 179 | WintunEndSession(this->session); 180 | this->session = NULL; 181 | } 182 | if (this->adapter) { 183 | WintunCloseAdapter(this->adapter); 184 | this->adapter = NULL; 185 | } 186 | return 0; 187 | } 188 | 189 | int read(std::string &buffer) { 190 | if (this->session) { 191 | DWORD size; 192 | BYTE *packet = WintunReceivePacket(this->session, &size); 193 | if (packet) { 194 | buffer.assign((char *)packet, size); 195 | WintunReleaseReceivePacket(this->session, packet); 196 | return size; 197 | } 198 | if (GetLastError() == ERROR_NO_MORE_ITEMS) { 199 | WaitForSingleObject(WintunGetReadWaitEvent(this->session), 1000); 200 | return 0; 201 | } 202 | spdlog::error("wintun read failed: {}", GetLastError()); 203 | } 204 | return -1; 205 | } 206 | 207 | int write(const std::string &buffer) { 208 | if (this->session) { 209 | BYTE *packet = WintunAllocateSendPacket(this->session, buffer.size()); 210 | if (packet) { 211 | memcpy(packet, buffer.c_str(), buffer.size()); 212 | WintunSendPacket(this->session, packet); 213 | return buffer.size(); 214 | } 215 | if (GetLastError() == ERROR_BUFFER_OVERFLOW) { 216 | return 0; 217 | } 218 | spdlog::error("wintun write failed: {}", GetLastError()); 219 | } 220 | return -1; 221 | } 222 | 223 | int setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 224 | MIB_IPFORWARDROW route; 225 | 226 | route.dwForwardDest = dst; 227 | route.dwForwardMask = mask; 228 | route.dwForwardNextHop = nexthop; 229 | route.dwForwardIfIndex = this->ifindex; 230 | 231 | route.dwForwardProto = MIB_IPPROTO_NETMGMT; 232 | route.dwForwardNextHopAS = 0; 233 | route.dwForwardAge = INFINITE; 234 | route.dwForwardType = MIB_IPROUTE_TYPE_INDIRECT; 235 | route.dwForwardMetric1 = route.dwForwardType + 1; 236 | route.dwForwardMetric2 = MIB_IPROUTE_METRIC_UNUSED; 237 | route.dwForwardMetric3 = MIB_IPROUTE_METRIC_UNUSED; 238 | route.dwForwardMetric4 = MIB_IPROUTE_METRIC_UNUSED; 239 | route.dwForwardMetric5 = MIB_IPROUTE_METRIC_UNUSED; 240 | 241 | DWORD result = CreateIpForwardEntry(&route); 242 | if (result == NO_ERROR) { 243 | routes.push(route); 244 | } else { 245 | spdlog::error("add route failed: {}", result); 246 | } 247 | 248 | return 0; 249 | } 250 | 251 | private: 252 | std::string name; 253 | IP4 ip; 254 | uint32_t prefix; 255 | int mtu; 256 | int timeout; 257 | NET_IFINDEX ifindex; 258 | std::stack routes; 259 | 260 | WINTUN_ADAPTER_HANDLE adapter = NULL; 261 | WINTUN_SESSION_HANDLE session = NULL; 262 | }; 263 | 264 | } // namespace candy 265 | 266 | namespace candy { 267 | 268 | Tun::Tun() { 269 | this->impl = std::make_shared(); 270 | } 271 | 272 | Tun::~Tun() { 273 | this->impl.reset(); 274 | } 275 | 276 | int Tun::setName(const std::string &name) { 277 | std::shared_ptr tun; 278 | 279 | tun = std::any_cast>(this->impl); 280 | tun->setName(name); 281 | return 0; 282 | } 283 | 284 | int Tun::setAddress(const std::string &cidr) { 285 | std::shared_ptr tun; 286 | Address address; 287 | 288 | if (address.fromCidr(cidr)) { 289 | return -1; 290 | } 291 | spdlog::info("client address: {}", address.toCidr()); 292 | tun = std::any_cast>(this->impl); 293 | if (tun->setIP(address.Host())) { 294 | return -1; 295 | } 296 | if (tun->setPrefix(address.Mask().toPrefix())) { 297 | return -1; 298 | } 299 | return 0; 300 | } 301 | 302 | IP4 Tun::getIP() { 303 | std::shared_ptr tun; 304 | tun = std::any_cast>(this->impl); 305 | return tun->getIP(); 306 | } 307 | 308 | int Tun::setMTU(int mtu) { 309 | std::shared_ptr tun; 310 | tun = std::any_cast>(this->impl); 311 | if (tun->setMTU(mtu)) { 312 | return -1; 313 | } 314 | return 0; 315 | } 316 | 317 | int Tun::up() { 318 | std::shared_ptr tun; 319 | tun = std::any_cast>(this->impl); 320 | return tun->up(); 321 | } 322 | 323 | int Tun::down() { 324 | std::shared_ptr tun; 325 | tun = std::any_cast>(this->impl); 326 | return tun->down(); 327 | } 328 | 329 | int Tun::read(std::string &buffer) { 330 | std::shared_ptr tun; 331 | tun = std::any_cast>(this->impl); 332 | return tun->read(buffer); 333 | } 334 | 335 | int Tun::write(const std::string &buffer) { 336 | std::shared_ptr tun; 337 | tun = std::any_cast>(this->impl); 338 | return tun->write(buffer); 339 | } 340 | 341 | int Tun::setSysRtTable(IP4 dst, IP4 mask, IP4 nexthop) { 342 | std::shared_ptr tun; 343 | tun = std::any_cast>(this->impl); 344 | return tun->setSysRtTable(dst, mask, nexthop); 345 | } 346 | 347 | } // namespace candy 348 | 349 | #endif 350 | -------------------------------------------------------------------------------- /candy/src/peer/peer.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "peer/peer.h" 3 | #include "core/client.h" 4 | #include "core/message.h" 5 | #include "peer/manager.h" 6 | #include "peer/peer.h" 7 | #include "utils/time.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace { 17 | 18 | using namespace Poco::Net; 19 | 20 | bool isLocalNetwork(const SocketAddress &addr) { 21 | IPAddress ip = addr.host(); 22 | 23 | if (ip.isV4()) { 24 | return ip.isSiteLocal() || ip.isLinkLocal() || ip.isSiteLocalMC(); 25 | } else if (ip.isV6()) { 26 | spdlog::error("unexpected ipv6 local address"); 27 | } 28 | 29 | return false; 30 | } 31 | 32 | } // namespace 33 | 34 | namespace candy { 35 | 36 | Peer::Peer(const IP4 &addr, PeerManager *peerManager) : peerManager(peerManager), addr(addr) { 37 | std::string data; 38 | data.append(this->peerManager->getPassword()); 39 | auto leaddr = hton(uint32_t(this->addr)); 40 | data.append((char *)&leaddr, sizeof(leaddr)); 41 | 42 | this->key.resize(SHA256_DIGEST_LENGTH); 43 | SHA256((unsigned char *)data.data(), data.size(), (unsigned char *)this->key.data()); 44 | 45 | this->encryptCtx = std::shared_ptr(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); 46 | } 47 | 48 | Peer::~Peer() {} 49 | 50 | void Peer::tryConnecct() { 51 | if (this->state == PeerState::INIT) { 52 | updateState(PeerState::PREPARING); 53 | } 54 | } 55 | 56 | PeerManager &Peer::getManager() { 57 | return *this->peerManager; 58 | } 59 | 60 | std::optional Peer::encrypt(const std::string &plaintext) { 61 | int len = 0; 62 | int ciphertextLen = 0; 63 | unsigned char ciphertext[1500] = {0}; 64 | unsigned char iv[AES_256_GCM_IV_LEN] = {0}; 65 | unsigned char tag[AES_256_GCM_TAG_LEN] = {0}; 66 | 67 | if (!RAND_bytes(iv, AES_256_GCM_IV_LEN)) { 68 | spdlog::debug("generate random iv failed"); 69 | return std::nullopt; 70 | } 71 | 72 | std::lock_guard lock(this->encryptCtxMutex); 73 | auto ctx = this->encryptCtx.get(); 74 | 75 | if (!EVP_CIPHER_CTX_reset(ctx)) { 76 | spdlog::debug("encrypt reset cipher context failed"); 77 | return std::nullopt; 78 | } 79 | if (!EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (unsigned char *)key.data(), iv)) { 80 | spdlog::debug("encrypt initialize cipher context failed"); 81 | return std::nullopt; 82 | } 83 | if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_GCM_IV_LEN, NULL)) { 84 | spdlog::debug("set iv length failed"); 85 | return std::nullopt; 86 | } 87 | if (!EVP_EncryptUpdate(ctx, ciphertext, &len, (unsigned char *)plaintext.data(), plaintext.size())) { 88 | spdlog::debug("encrypt update failed"); 89 | return std::nullopt; 90 | } 91 | ciphertextLen = len; 92 | if (!EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) { 93 | spdlog::debug("encrypt final failed"); 94 | return std::nullopt; 95 | } 96 | ciphertextLen += len; 97 | if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, AES_256_GCM_TAG_LEN, tag)) { 98 | spdlog::debug("get tag failed"); 99 | return std::nullopt; 100 | } 101 | 102 | std::string result; 103 | result.append((char *)iv, AES_256_GCM_IV_LEN); 104 | result.append((char *)tag, AES_256_GCM_TAG_LEN); 105 | result.append((char *)ciphertext, ciphertextLen); 106 | return result; 107 | } 108 | 109 | int Peer::sendEncrypted(const std::string &data) { 110 | if (auto buffer = encrypt(data)) { 111 | return send(*buffer); 112 | } 113 | return -1; 114 | } 115 | 116 | bool Peer::checkActivityWithin(std::chrono::system_clock::duration duration) { 117 | return std::chrono::system_clock::now() - lastActiveTime < duration; 118 | } 119 | 120 | std::optional Peer::isConnected() const { 121 | if (this->state == PeerState::CONNECTED) { 122 | return this->rtt; 123 | } 124 | return std::nullopt; 125 | } 126 | 127 | bool Peer::updateState(PeerState state) { 128 | this->lastActiveTime = std::chrono::system_clock::now(); 129 | 130 | if (this->state == state) { 131 | return false; 132 | } 133 | 134 | spdlog::debug("state: {} {} => {}", this->addr.toString(), stateString(), stateString(state)); 135 | 136 | if (state == PeerState::INIT || state == PeerState::WAITING || state == PeerState::FAILED) { 137 | resetState(); 138 | } 139 | 140 | if (this->state == PeerState::WAITING && state == PeerState::INIT) { 141 | this->retry = std::min(this->retry * 2, RETRY_MAX); 142 | } else if (state == PeerState::INIT || state == PeerState::FAILED) { 143 | this->retry = RETRY_MIN; 144 | } 145 | 146 | this->state = state; 147 | return true; 148 | } 149 | 150 | std::string Peer::stateString() const { 151 | return this->stateString(this->state); 152 | } 153 | 154 | std::string Peer::stateString(PeerState state) const { 155 | switch (state) { 156 | case PeerState::INIT: 157 | return "INIT"; 158 | case PeerState::PREPARING: 159 | return "PREPARING"; 160 | case PeerState::SYNCHRONIZING: 161 | return "SYNCHRONIZING"; 162 | case PeerState::CONNECTING: 163 | return "CONNECTING"; 164 | case PeerState::CONNECTED: 165 | return "CONNECTED"; 166 | case PeerState::WAITING: 167 | return "WAITING"; 168 | case PeerState::FAILED: 169 | return "FAILED"; 170 | default: 171 | return "UNKNOWN"; 172 | } 173 | } 174 | 175 | void Peer::handlePubInfo(IP4 ip, uint16_t port, bool local) { 176 | { 177 | std::unique_lock lock(this->socketAddressMutex); 178 | if (local) { 179 | this->local = SocketAddress(ip.toString(), port); 180 | return; 181 | } 182 | 183 | this->wide = SocketAddress(ip.toString(), port); 184 | } 185 | 186 | if (this->state == PeerState::CONNECTED) { 187 | return; 188 | } 189 | 190 | if (this->state == PeerState::SYNCHRONIZING) { 191 | updateState(PeerState::CONNECTING); 192 | return; 193 | } 194 | 195 | if (this->state != PeerState::CONNECTING) { 196 | updateState(PeerState::PREPARING); 197 | CoreMsg::PubInfo info = {.dst = this->addr, .local = true}; 198 | getManager().sendPubInfo(info); 199 | return; 200 | } 201 | } 202 | 203 | void Peer::handleStunResponse() { 204 | if (this->state != PeerState::PREPARING) { 205 | return; 206 | } 207 | if (this->wide == std::nullopt) { 208 | updateState(PeerState::SYNCHRONIZING); 209 | } else { 210 | updateState(PeerState::CONNECTING); 211 | } 212 | CoreMsg::PubInfo info = {.dst = this->addr}; 213 | getManager().sendPubInfo(info); 214 | } 215 | 216 | void Peer::tick() { 217 | switch (this->state) { 218 | case PeerState::INIT: 219 | break; 220 | case PeerState::PREPARING: 221 | if (getManager().stun.enabled() && checkActivityWithin(std::chrono::seconds(10))) { 222 | getManager().stun.needed = true; 223 | } else { 224 | updateState(PeerState::FAILED); 225 | } 226 | break; 227 | case PeerState::SYNCHRONIZING: 228 | if (checkActivityWithin(std::chrono::seconds(10))) { 229 | sendHeartbeatMessage(); 230 | } else { 231 | updateState(PeerState::FAILED); 232 | } 233 | break; 234 | case PeerState::CONNECTING: 235 | if (checkActivityWithin(std::chrono::seconds(10))) { 236 | sendHeartbeatMessage(); 237 | } else { 238 | updateState(PeerState::WAITING); 239 | } 240 | break; 241 | case PeerState::CONNECTED: 242 | if (checkActivityWithin(std::chrono::seconds(3))) { 243 | sendHeartbeatMessage(); 244 | if (getManager().clientRelayEnabled() && tickCount % 60 == 0) { 245 | sendDelayMessage(); 246 | } 247 | } else { 248 | updateState(PeerState::INIT); 249 | if (getManager().clientRelayEnabled()) { 250 | getManager().updateRtTable(PeerRouteEntry(addr, addr, RTT_LIMIT)); 251 | } 252 | } 253 | break; 254 | case PeerState::WAITING: 255 | if (!checkActivityWithin(std::chrono::seconds(this->retry))) { 256 | updateState(PeerState::INIT); 257 | } 258 | break; 259 | case PeerState::FAILED: 260 | break; 261 | default: 262 | break; 263 | } 264 | ++tickCount; 265 | } 266 | 267 | void Peer::handleHeartbeatMessage(const SocketAddress &address, uint8_t heartbeatAck) { 268 | if (this->state == PeerState::INIT || this->state == PeerState::WAITING || this->state == PeerState::FAILED) { 269 | spdlog::debug("heartbeat peer state invalid: {} {}", this->addr.toString(), stateString()); 270 | return; 271 | } 272 | 273 | if (!isLocalNetwork(address)) { 274 | this->wide = address; 275 | } else if (!getManager().localP2PDisabled) { 276 | this->local = address; 277 | } else { 278 | return; 279 | } 280 | 281 | { 282 | std::unique_lock lock(this->socketAddressMutex); 283 | if (!this->real || isLocalNetwork(address) || !isLocalNetwork(*this->real)) { 284 | this->real = address; 285 | } 286 | } 287 | 288 | if (!this->ack) { 289 | this->ack = 1; 290 | } 291 | 292 | if (heartbeatAck && updateState(PeerState::CONNECTED)) { 293 | sendDelayMessage(); 294 | } 295 | } 296 | 297 | int Peer::send(const std::string &buffer) { 298 | try { 299 | std::shared_lock lock(this->socketAddressMutex); 300 | if (this->real) { 301 | if (buffer.size() == getManager().sendTo(buffer.data(), buffer.size(), *this->real)) { 302 | return 0; 303 | } 304 | } 305 | } catch (std::exception &e) { 306 | spdlog::debug("peer send failed: {}", e.what()); 307 | } 308 | return -1; 309 | } 310 | 311 | void Peer::sendHeartbeatMessage() { 312 | PeerMsg::Heartbeat heartbeat; 313 | heartbeat.kind = PeerMsgKind::HEARTBEAT; 314 | heartbeat.tunip = getManager().getTunIp(); 315 | heartbeat.ack = this->ack; 316 | 317 | if (auto buffer = encrypt(std::string((char *)&heartbeat, sizeof(heartbeat)))) { 318 | using Poco::Net::SocketAddress; 319 | std::shared_lock lock(this->socketAddressMutex); 320 | if (this->real && (this->state == PeerState::CONNECTED)) { 321 | heartbeat.ip = this->real->host().toString(); 322 | heartbeat.port = this->real->port(); 323 | getManager().sendTo(buffer->data(), buffer->size(), *this->real); 324 | } 325 | 326 | if (this->wide && (this->state == PeerState::CONNECTING)) { 327 | heartbeat.ip = this->wide->host().toString(); 328 | heartbeat.port = this->wide->port(); 329 | getManager().sendTo(buffer->data(), buffer->size(), *this->wide); 330 | } 331 | 332 | if (this->local && (this->state == PeerState::PREPARING || this->state == PeerState::SYNCHRONIZING || 333 | this->state == PeerState::CONNECTING)) { 334 | heartbeat.ip = this->local->host().toString(); 335 | heartbeat.port = this->local->port(); 336 | getManager().sendTo(buffer->data(), buffer->size(), *this->local); 337 | } 338 | } 339 | } 340 | 341 | void Peer::sendDelayMessage() { 342 | PeerMsg::Delay delay; 343 | delay.type = PeerMsgKind::DELAY; 344 | delay.src = getManager().getTunIp(); 345 | delay.dst = this->addr; 346 | delay.timestamp = hton(bootTime()); 347 | sendEncrypted(std::string((char *)&delay, sizeof(delay))); 348 | } 349 | 350 | void Peer::resetState() { 351 | std::unique_lock lock(this->socketAddressMutex); 352 | this->wide = std::nullopt; 353 | this->local = std::nullopt; 354 | this->real = std::nullopt; 355 | this->ack = 0; 356 | this->rtt = RTT_LIMIT; 357 | } 358 | 359 | } // namespace candy 360 | -------------------------------------------------------------------------------- /candy/src/websocket/client.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "websocket/client.h" 3 | #include "core/client.h" 4 | #include "core/message.h" 5 | #include "core/net.h" 6 | #include "core/version.h" 7 | #include "utils/time.h" 8 | #include "websocket/message.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace candy { 20 | 21 | int WebSocketClient::setName(const std::string &name) { 22 | this->name = name; 23 | return 0; 24 | } 25 | 26 | int WebSocketClient::setPassword(const std::string &password) { 27 | this->password = password; 28 | return 0; 29 | } 30 | 31 | int WebSocketClient::setWsServerUri(const std::string &uri) { 32 | this->wsServerUri = uri; 33 | return 0; 34 | } 35 | 36 | int WebSocketClient::setExptTunAddress(const std::string &cidr) { 37 | this->exptTunCidr = cidr; 38 | return 0; 39 | } 40 | 41 | int WebSocketClient::setAddress(const std::string &cidr) { 42 | this->tunCidr = cidr; 43 | return 0; 44 | } 45 | 46 | int WebSocketClient::setVirtualMac(const std::string &vmac) { 47 | this->vmac = vmac; 48 | return 0; 49 | } 50 | 51 | std::string WebSocketClient::getTunCidr() const { 52 | return this->tunCidr; 53 | } 54 | 55 | int WebSocketClient::run(Client *client) { 56 | this->client = client; 57 | 58 | if (connect()) { 59 | spdlog::critical("websocket client connect failed"); 60 | return -1; 61 | } 62 | 63 | sendVirtualMacMsg(); 64 | if (this->tunCidr.empty()) { 65 | sendExptTunMsg(); 66 | } else { 67 | sendAuthMsg(); 68 | } 69 | 70 | this->msgThread = std::thread([&] { 71 | spdlog::debug("start thread: websocket client msg"); 72 | while (getClient().isRunning()) { 73 | handleWsQueue(); 74 | } 75 | getClient().shutdown(); 76 | spdlog::debug("stop thread: websocket client msg"); 77 | }); 78 | 79 | this->wsThread = std::thread([&] { 80 | spdlog::debug("start thread: websocket client ws"); 81 | while (getClient().isRunning()) { 82 | if (handleWsConn()) { 83 | break; 84 | } 85 | } 86 | getClient().shutdown(); 87 | spdlog::debug("stop thread: websocket client ws"); 88 | }); 89 | 90 | return 0; 91 | } 92 | 93 | int WebSocketClient::wait() { 94 | if (this->msgThread.joinable()) { 95 | this->msgThread.join(); 96 | } 97 | if (this->wsThread.joinable()) { 98 | this->wsThread.join(); 99 | } 100 | return 0; 101 | } 102 | 103 | void WebSocketClient::handleWsQueue() { 104 | Msg msg = this->client->getWsMsgQueue().read(); 105 | switch (msg.kind) { 106 | case MsgKind::TIMEOUT: 107 | break; 108 | case MsgKind::PACKET: 109 | handlePacket(std::move(msg)); 110 | break; 111 | case MsgKind::PUBINFO: 112 | handlePubInfo(std::move(msg)); 113 | break; 114 | case MsgKind::DISCOVERY: 115 | handleDiscovery(std::move(msg)); 116 | break; 117 | default: 118 | spdlog::warn("unexcepted websocket message type: {}", static_cast(msg.kind)); 119 | break; 120 | } 121 | } 122 | 123 | void WebSocketClient::handlePacket(Msg msg) { 124 | IP4Header *header = (IP4Header *)msg.data.data(); 125 | 126 | msg.data.insert(0, 1, WsMsgKind::FORWARD); 127 | sendFrame(msg.data); 128 | } 129 | 130 | void WebSocketClient::handlePubInfo(Msg msg) { 131 | CoreMsg::PubInfo *info = (CoreMsg::PubInfo *)(msg.data.data()); 132 | if (info->local) { 133 | WsMsg::ConnLocal buffer; 134 | buffer.ge.src = info->src; 135 | buffer.ge.dst = info->dst; 136 | buffer.ip = info->ip; 137 | buffer.port = hton(info->port); 138 | sendFrame(&buffer, sizeof(buffer)); 139 | } else { 140 | WsMsg::Conn buffer; 141 | buffer.src = info->src; 142 | buffer.dst = info->dst; 143 | buffer.ip = info->ip; 144 | buffer.port = hton(info->port); 145 | sendFrame(&buffer, sizeof(buffer)); 146 | } 147 | } 148 | 149 | void WebSocketClient::handleDiscovery(Msg msg) { 150 | sendDiscoveryMsg(IP4("255.255.255.255")); 151 | } 152 | 153 | int WebSocketClient::handleWsConn() { 154 | try { 155 | std::string buffer; 156 | int flags = 0; 157 | 158 | // receiveFrame 会对 ws 加锁,影响写操作,需要先确定可读 159 | if (!this->ws->poll(Poco::Timespan(1, 0), Poco::Net::Socket::SELECT_READ)) { 160 | if (bootTime() - this->timestamp > 30000) { 161 | spdlog::warn("websocket pong timeout"); 162 | return -1; 163 | } 164 | if (bootTime() - this->timestamp > 15000) { 165 | sendPingMessage(); 166 | } 167 | return 0; 168 | } 169 | 170 | buffer.resize(1500); 171 | int length = this->ws->receiveFrame(buffer.data(), buffer.size(), flags); 172 | if (length == 0 && flags == 0) { 173 | spdlog::info("abnormal disconnect"); 174 | return -1; 175 | } 176 | if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PING) { 177 | this->timestamp = bootTime(); 178 | flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PONG; 179 | sendFrame(buffer.data(), length, flags); 180 | return 0; 181 | } 182 | if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PONG) { 183 | this->timestamp = bootTime(); 184 | return 0; 185 | } 186 | if ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_CLOSE) { 187 | spdlog::info("websocket close: {}", buffer); 188 | return -1; 189 | } 190 | if (length > 0) { 191 | this->timestamp = bootTime(); 192 | buffer.resize(length); 193 | handleWsMsg(std::move(buffer)); 194 | return 0; 195 | } 196 | spdlog::warn("handle ws conn failed: unexpected empty message"); 197 | return -1; 198 | } catch (std::exception &e) { 199 | spdlog::warn("handle ws conn failed: {}", e.what()); 200 | return -1; 201 | } 202 | } 203 | 204 | void WebSocketClient::handleWsMsg(std::string buffer) { 205 | uint8_t msgKind = buffer.front(); 206 | switch (msgKind) { 207 | case WsMsgKind::FORWARD: 208 | handleForwardMsg(std::move(buffer)); 209 | break; 210 | case WsMsgKind::EXPTTUN: 211 | handleExptTunMsg(std::move(buffer)); 212 | break; 213 | case WsMsgKind::UDP4CONN: 214 | handleUdp4ConnMsg(std::move(buffer)); 215 | break; 216 | case WsMsgKind::DISCOVERY: 217 | handleDiscoveryMsg(std::move(buffer)); 218 | break; 219 | case WsMsgKind::ROUTE: 220 | handleRouteMsg(std::move(buffer)); 221 | break; 222 | case WsMsgKind::GENERAL: 223 | handleGeneralMsg(std::move(buffer)); 224 | break; 225 | default: 226 | spdlog::debug("unknown websocket message kind: {}", msgKind); 227 | break; 228 | } 229 | } 230 | 231 | void WebSocketClient::handleForwardMsg(std::string buffer) { 232 | if (buffer.size() < sizeof(WsMsg::Forward)) { 233 | spdlog::warn("invalid forward message: {:n}", spdlog::to_hex(buffer)); 234 | return; 235 | } 236 | // 移除一个字节的类型 237 | buffer.erase(0, 1); 238 | // 尝试与源地址建立对等连接 239 | IP4Header *header = (IP4Header *)buffer.data(); 240 | // 每次通过服务端转发收到报文都触发一次尝试 P2P 连接, 用于暗示通过服务端转发是个非常耗时的操作 241 | this->client->getPeerMsgQueue().write(Msg(MsgKind::TRYP2P, header->saddr.toString())); 242 | // 最后把报文移动到 TUN 模块, 因为有移动操作所以必须在最后执行 243 | this->client->getTunMsgQueue().write(Msg(MsgKind::PACKET, std::move(buffer))); 244 | } 245 | 246 | void WebSocketClient::handleExptTunMsg(std::string buffer) { 247 | if (buffer.size() < sizeof(WsMsg::ExptTun)) { 248 | spdlog::warn("invalid expt tun message: {:n}", spdlog::to_hex(buffer)); 249 | return; 250 | } 251 | WsMsg::ExptTun *header = (WsMsg::ExptTun *)buffer.data(); 252 | Address exptTun(header->cidr); 253 | this->tunCidr = exptTun.toCidr(); 254 | sendAuthMsg(); 255 | } 256 | 257 | void WebSocketClient::handleUdp4ConnMsg(std::string buffer) { 258 | if (buffer.size() < sizeof(WsMsg::Conn)) { 259 | spdlog::warn("invalid udp4conn message: {:n}", spdlog::to_hex(buffer)); 260 | return; 261 | } 262 | WsMsg::Conn *header = (WsMsg::Conn *)buffer.data(); 263 | CoreMsg::PubInfo info = {.src = header->src, .dst = header->dst, .ip = header->ip, .port = ntoh(header->port)}; 264 | this->client->getPeerMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info)))); 265 | } 266 | 267 | void WebSocketClient::handleDiscoveryMsg(std::string buffer) { 268 | if (buffer.size() < sizeof(WsMsg::Discovery)) { 269 | spdlog::warn("invalid discovery message: {:n}", spdlog::to_hex(buffer)); 270 | return; 271 | } 272 | WsMsg::Discovery *header = (WsMsg::Discovery *)buffer.data(); 273 | if (header->dst == IP4("255.255.255.255")) { 274 | sendDiscoveryMsg(header->src); 275 | } 276 | this->client->getPeerMsgQueue().write(Msg(MsgKind::TRYP2P, header->src.toString())); 277 | } 278 | 279 | void WebSocketClient::handleRouteMsg(std::string buffer) { 280 | if (buffer.size() < sizeof(WsMsg::SysRoute)) { 281 | spdlog::warn("invalid route message: {:n}", spdlog::to_hex(buffer)); 282 | return; 283 | } 284 | WsMsg::SysRoute *header = (WsMsg::SysRoute *)buffer.data(); 285 | SysRouteEntry *rt = header->rtTable; 286 | for (uint8_t idx = 0; idx < header->size; ++idx) { 287 | this->client->getTunMsgQueue().write(Msg(MsgKind::SYSRT, std::string((char *)(rt + idx), sizeof(SysRouteEntry)))); 288 | this->client->getPeerMsgQueue().write(Msg(MsgKind::SYSRT)); 289 | } 290 | } 291 | 292 | void WebSocketClient::handleGeneralMsg(std::string buffer) { 293 | if (buffer.size() < sizeof(WsMsg::ConnLocal)) { 294 | spdlog::warn("invalid udp4conn local message: {:n}", spdlog::to_hex(buffer)); 295 | return; 296 | } 297 | WsMsg::ConnLocal *header = (WsMsg::ConnLocal *)buffer.data(); 298 | CoreMsg::PubInfo info = { 299 | .src = header->ge.src, 300 | .dst = header->ge.dst, 301 | .ip = header->ip, 302 | .port = ntoh(header->port), 303 | .local = true, 304 | }; 305 | this->client->getPeerMsgQueue().write(Msg(MsgKind::PUBINFO, std::string((char *)(&info), sizeof(info)))); 306 | } 307 | 308 | void WebSocketClient::sendFrame(const std::string &buffer, int flags) { 309 | sendFrame(buffer.c_str(), buffer.size(), flags); 310 | } 311 | 312 | void WebSocketClient::sendFrame(const void *buffer, int length, int flags) { 313 | if (this->ws) { 314 | try { 315 | this->ws->sendFrame(buffer, length, flags); 316 | } catch (std::exception &e) { 317 | spdlog::critical("websocket send frame failed: {}", e.what()); 318 | } 319 | } 320 | } 321 | 322 | void WebSocketClient::sendVirtualMacMsg() { 323 | WsMsg::VMac buffer(this->vmac); 324 | buffer.updateHash(this->password); 325 | sendFrame(&buffer, sizeof(buffer)); 326 | } 327 | 328 | void WebSocketClient::sendExptTunMsg() { 329 | Address exptTun(this->exptTunCidr); 330 | WsMsg::ExptTun buffer(exptTun.toCidr()); 331 | buffer.updateHash(this->password); 332 | sendFrame(&buffer, sizeof(buffer)); 333 | } 334 | 335 | void WebSocketClient::sendAuthMsg() { 336 | Address address(this->tunCidr); 337 | WsMsg::Auth buffer(address.Host()); 338 | buffer.updateHash(this->password); 339 | sendFrame(&buffer, sizeof(buffer)); 340 | this->client->getTunMsgQueue().write(Msg(MsgKind::TUNADDR, address.toCidr())); 341 | this->client->getPeerMsgQueue().write(Msg(MsgKind::TUNADDR, address.toCidr())); 342 | sendPingMessage(); 343 | } 344 | 345 | void WebSocketClient::sendDiscoveryMsg(IP4 dst) { 346 | Address address(this->tunCidr); 347 | 348 | WsMsg::Discovery buffer; 349 | buffer.dst = dst; 350 | buffer.src = address.Host(); 351 | 352 | sendFrame(&buffer, sizeof(buffer)); 353 | } 354 | 355 | std::string WebSocketClient::hostName() { 356 | char hostname[64] = {0}; 357 | if (!gethostname(hostname, sizeof(hostname))) { 358 | return std::string(hostname, strnlen(hostname, sizeof(hostname))); 359 | } 360 | return ""; 361 | } 362 | 363 | void WebSocketClient::sendPingMessage() { 364 | int flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PING; 365 | sendFrame(pingMessage, flags); 366 | } 367 | 368 | int WebSocketClient::connect() { 369 | std::shared_ptr uri; 370 | try { 371 | uri = std::make_shared(wsServerUri); 372 | } catch (std::exception &e) { 373 | spdlog::critical("invalid websocket server: {}: {}", wsServerUri, e.what()); 374 | return -1; 375 | } 376 | 377 | try { 378 | const std::string path = uri->getPath().empty() ? "/" : uri->getPath(); 379 | Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, path, Poco::Net::HTTPMessage::HTTP_1_1); 380 | Poco::Net::HTTPResponse response; 381 | if (uri->getScheme() == "wss") { 382 | using Poco::Net::Context; 383 | Context::Ptr context = new Context(Context::TLS_CLIENT_USE, "", "", "", Context::VERIFY_NONE); 384 | Poco::Net::HTTPSClientSession cs(uri->getHost(), uri->getPort(), context); 385 | this->ws = std::make_shared(cs, request, response); 386 | } else if (uri->getScheme() == "ws") { 387 | Poco::Net::HTTPClientSession cs(uri->getHost(), uri->getPort()); 388 | this->ws = std::make_shared(cs, request, response); 389 | } else { 390 | spdlog::critical("invalid websocket scheme: {}", wsServerUri); 391 | return -1; 392 | } 393 | this->timestamp = bootTime(); 394 | this->pingMessage = fmt::format("candy::{}::{}::{}", CANDY_SYSTEM, CANDY_VERSION, hostName()); 395 | spdlog::debug("client info: {}", this->pingMessage); 396 | return 0; 397 | } catch (std::exception &e) { 398 | spdlog::critical("websocket connect failed: {}", e.what()); 399 | return -1; 400 | } 401 | } 402 | 403 | int WebSocketClient::disconnect() { 404 | try { 405 | if (this->ws) { 406 | this->ws->shutdown(); 407 | this->ws->close(); 408 | this->ws.reset(); 409 | } 410 | } catch (std::exception &e) { 411 | spdlog::debug("websocket disconnect failed: {}", e.what()); 412 | } 413 | return 0; 414 | } 415 | 416 | Client &WebSocketClient::getClient() { 417 | return *this->client; 418 | } 419 | 420 | } // namespace candy 421 | -------------------------------------------------------------------------------- /candy/src/websocket/server.cc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #include "websocket/server.h" 3 | #include "core/net.h" 4 | #include "utils/time.h" 5 | #include "websocket/message.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | /** 24 | * Poco 的 WebSocket 服务端接口有点难用,简单封装一下,并对外提供一个回调函数,回调函数的参数表示独立的 25 | * WebSocket客户端,函数返回会释放连接 26 | */ 27 | namespace { 28 | 29 | using WebSocketHandler = std::function; 30 | 31 | class HTTPRequestHandler : public Poco::Net::HTTPRequestHandler { 32 | public: 33 | HTTPRequestHandler(WebSocketHandler wsHandler) : wsHandler(wsHandler) {} 34 | void handleRequest(Poco::Net::HTTPServerRequest &request, Poco::Net::HTTPServerResponse &response) { 35 | try { 36 | Poco::Net::WebSocket ws(request, response); 37 | wsHandler(ws); 38 | ws.close(); 39 | } catch (const std::exception &e) { 40 | response.setStatus(Poco::Net::HTTPResponse::HTTP_FORBIDDEN); 41 | response.setReason("Forbidden"); 42 | response.setContentLength(0); 43 | response.send(); 44 | } 45 | } 46 | 47 | private: 48 | WebSocketHandler wsHandler; 49 | }; 50 | 51 | class HTTPRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory { 52 | public: 53 | HTTPRequestHandlerFactory(WebSocketHandler wsHandler) : wsHandler(wsHandler) {} 54 | 55 | Poco::Net::HTTPRequestHandler *createRequestHandler(const Poco::Net::HTTPServerRequest &request) { 56 | return new HTTPRequestHandler(wsHandler); 57 | } 58 | 59 | private: 60 | WebSocketHandler wsHandler; 61 | }; 62 | 63 | }; // namespace 64 | 65 | namespace candy { 66 | 67 | void WsCtx::sendFrame(const std::string &frame, int flags) { 68 | this->ws->sendFrame(frame.data(), frame.size(), flags); 69 | } 70 | 71 | int WebSocketServer::setWebSocket(const std::string &uri) { 72 | try { 73 | Poco::URI parser(uri); 74 | if (parser.getScheme() != "ws") { 75 | spdlog::critical("websocket server only support ws"); 76 | return -1; 77 | } 78 | this->host = parser.getHost(); 79 | this->port = parser.getPort(); 80 | return 0; 81 | } catch (std::exception &e) { 82 | spdlog::critical("invalid websocket uri: {}: {}", uri, e.what()); 83 | return -1; 84 | } 85 | } 86 | 87 | int WebSocketServer::setPassword(const std::string &password) { 88 | this->password = password; 89 | return 0; 90 | } 91 | 92 | int WebSocketServer::setDHCP(const std::string &cidr) { 93 | if (cidr.empty()) { 94 | return 0; 95 | } 96 | return this->dhcp.fromCidr(cidr); 97 | } 98 | 99 | int WebSocketServer::setSdwan(const std::string &sdwan) { 100 | if (sdwan.empty()) { 101 | return 0; 102 | } 103 | std::string route; 104 | std::stringstream stream(sdwan); 105 | while (std::getline(stream, route, ';')) { 106 | std::string addr; 107 | SysRoute rt; 108 | std::stringstream ss(route); 109 | // dev 110 | if (!std::getline(ss, addr, ',') || rt.dev.fromCidr(addr) || rt.dev.Host() != rt.dev.Net()) { 111 | spdlog::critical("invalid route device: {}", route); 112 | return -1; 113 | } 114 | // dst 115 | if (!std::getline(ss, addr, ',') || rt.dst.fromCidr(addr) || rt.dst.Host() != rt.dst.Net()) { 116 | spdlog::critical("invalid route dest: {}", route); 117 | return -1; 118 | } 119 | // next 120 | if (!std::getline(ss, addr, ',') || rt.next.fromString(addr)) { 121 | spdlog::critical("invalid route nexthop: {}", route); 122 | return -1; 123 | } 124 | spdlog::info("route: dev={} dst={} next={}", rt.dev.toCidr(), rt.dst.toCidr(), rt.next.toString()); 125 | this->routes.push_back(rt); 126 | } 127 | return 0; 128 | } 129 | 130 | int WebSocketServer::run() { 131 | listen(); 132 | return 0; 133 | } 134 | 135 | int WebSocketServer::shutdown() { 136 | this->running = false; 137 | if (this->httpServer) { 138 | this->httpServer->stopAll(); 139 | } 140 | this->routes.clear(); 141 | return 0; 142 | } 143 | 144 | void WebSocketServer::handleMsg(WsCtx &ctx) { 145 | uint8_t msgKind = ctx.buffer.front(); 146 | switch (msgKind) { 147 | case WsMsgKind::AUTH: 148 | handleAuthMsg(ctx); 149 | break; 150 | case WsMsgKind::FORWARD: 151 | handleForwardMsg(ctx); 152 | break; 153 | case WsMsgKind::EXPTTUN: 154 | handleExptTunMsg(ctx); 155 | break; 156 | case WsMsgKind::UDP4CONN: 157 | handleUdp4ConnMsg(ctx); 158 | break; 159 | case WsMsgKind::VMAC: 160 | handleVMacMsg(ctx); 161 | break; 162 | case WsMsgKind::DISCOVERY: 163 | handleDiscoveryMsg(ctx); 164 | break; 165 | case WsMsgKind::GENERAL: 166 | HandleGeneralMsg(ctx); 167 | break; 168 | } 169 | } 170 | 171 | void WebSocketServer::handleAuthMsg(WsCtx &ctx) { 172 | if (ctx.buffer.length() < sizeof(WsMsg::Auth)) { 173 | spdlog::warn("invalid auth message: len {}", ctx.buffer.length()); 174 | ctx.status = -1; 175 | return; 176 | } 177 | 178 | WsMsg::Auth *header = (WsMsg::Auth *)ctx.buffer.data(); 179 | if (!header->check(this->password)) { 180 | spdlog::warn("auth header check failed: buffer {:n}", spdlog::to_hex(ctx.buffer)); 181 | ctx.status = -1; 182 | return; 183 | } 184 | 185 | ctx.ip = header->ip; 186 | 187 | { 188 | std::unique_lock lock(ipCtxMutex); 189 | auto it = ipCtxMap.find(header->ip); 190 | if (it != ipCtxMap.end()) { 191 | it->second->status = -1; 192 | spdlog::info("reconnect: {}", it->second->ip.toString()); 193 | } else { 194 | spdlog::info("connect: {}", ctx.ip.toString()); 195 | } 196 | ipCtxMap[header->ip] = &ctx; 197 | } 198 | 199 | updateSysRoute(ctx); 200 | } 201 | 202 | void WebSocketServer::handleForwardMsg(WsCtx &ctx) { 203 | if (ctx.ip.empty()) { 204 | spdlog::debug("unauthorized forward websocket client"); 205 | ctx.status = -1; 206 | return; 207 | } 208 | 209 | if (ctx.buffer.length() < sizeof(WsMsg::Forward)) { 210 | spdlog::debug("invalid forawrd message: len {}", ctx.buffer.length()); 211 | ctx.status = -1; 212 | return; 213 | } 214 | 215 | WsMsg::Forward *header = (WsMsg::Forward *)ctx.buffer.data(); 216 | 217 | { 218 | std::shared_lock lock(this->ipCtxMutex); 219 | auto it = this->ipCtxMap.find(header->iph.daddr); 220 | if (it != this->ipCtxMap.end()) { 221 | it->second->sendFrame(ctx.buffer); 222 | return; 223 | } 224 | } 225 | 226 | bool broadcast = [&] { 227 | // 多播地址 228 | if ((header->iph.daddr & IP4("240.0.0.0")) == IP4("224.0.0.0")) { 229 | return true; 230 | } 231 | // 广播 232 | if (header->iph.daddr == IP4("255.255.255.255")) { 233 | return true; 234 | } 235 | // 服务端没有配置动态分配地址的范围,没法检查是否为定向广播 236 | if (this->dhcp.empty()) { 237 | return false; 238 | } 239 | // 网络号不同,不是定向广播 240 | if ((this->dhcp.Mask() & header->iph.daddr) != this->dhcp.Net()) { 241 | return false; 242 | } 243 | // 主机号部分不全为 1,不是定向广播 244 | if (~((header->iph.daddr & ~this->dhcp.Mask()) ^ this->dhcp.Mask())) { 245 | return false; 246 | } 247 | return true; 248 | }(); 249 | 250 | if (broadcast) { 251 | std::shared_lock lock(this->ipCtxMutex); 252 | for (auto c : this->ipCtxMap) { 253 | if (c.second->ip != ctx.ip) { 254 | c.second->sendFrame(ctx.buffer); 255 | } 256 | } 257 | return; 258 | } 259 | 260 | spdlog::debug("forward failed: source {} dest {}", header->iph.saddr.toString(), header->iph.daddr.toString()); 261 | return; 262 | } 263 | 264 | void WebSocketServer::handleExptTunMsg(WsCtx &ctx) { 265 | if (ctx.buffer.length() < sizeof(WsMsg::ExptTun)) { 266 | spdlog::warn("invalid dynamic address message: len {}", ctx.buffer.length()); 267 | ctx.status = -1; 268 | return; 269 | } 270 | WsMsg::ExptTun *header = (WsMsg::ExptTun *)ctx.buffer.data(); 271 | if (!header->check(this->password)) { 272 | spdlog::warn("dynamic address header check failed: buffer {:n}", spdlog::to_hex(ctx.buffer)); 273 | ctx.status = -1; 274 | return; 275 | } 276 | if (this->dhcp.empty()) { 277 | spdlog::warn("unable to allocate dynamic address"); 278 | ctx.status = -1; 279 | return; 280 | } 281 | Address exptTun; 282 | if (exptTun.fromCidr(header->cidr)) { 283 | spdlog::warn("dynamic address header cidr invalid: buffer {:n}", spdlog::to_hex(ctx.buffer)); 284 | ctx.status = -1; 285 | return; 286 | } 287 | // 判断能否直接使用申请的地址 288 | bool direct = [&]() { 289 | if (dhcp.Net() != exptTun.Net()) { 290 | return false; 291 | } 292 | std::shared_lock lock(this->ipCtxMutex); 293 | auto oldCtx = this->ipCtxMap.find(exptTun.Host()); 294 | if (oldCtx == this->ipCtxMap.end()) { 295 | return true; 296 | } 297 | return ctx.vmac == oldCtx->second->vmac; 298 | }(); 299 | if (!direct) { 300 | exptTun = this->dhcp; 301 | std::shared_lock lock(this->ipCtxMutex); 302 | do { 303 | exptTun = exptTun.Next(); 304 | if (exptTun.Host() == this->dhcp.Host()) { 305 | spdlog::warn("all addresses in the network are assigned"); 306 | ctx.status = -1; 307 | return; 308 | } 309 | } while (!exptTun.isValid() && this->ipCtxMap.find(exptTun.Host()) != this->ipCtxMap.end()); 310 | this->dhcp = exptTun; 311 | } 312 | header->timestamp = hton(unixTime()); 313 | std::strcpy(header->cidr, exptTun.toCidr().c_str()); 314 | header->updateHash(this->password); 315 | ctx.sendFrame(ctx.buffer); 316 | } 317 | 318 | void WebSocketServer::handleUdp4ConnMsg(WsCtx &ctx) { 319 | if (ctx.ip.empty()) { 320 | spdlog::debug("unauthorized peer websocket client"); 321 | ctx.status = -1; 322 | return; 323 | } 324 | 325 | if (ctx.buffer.length() < sizeof(WsMsg::Conn)) { 326 | spdlog::warn("invalid peer conn message: len {}", ctx.buffer.length()); 327 | ctx.status = -1; 328 | return; 329 | } 330 | 331 | WsMsg::Conn *header = (WsMsg::Conn *)ctx.buffer.data(); 332 | if (ctx.ip != header->src) { 333 | spdlog::debug("peer source address does not match: auth {} source {}", ctx.ip.toString(), header->src.toString()); 334 | ctx.status = -1; 335 | return; 336 | } 337 | std::shared_lock lock(this->ipCtxMutex); 338 | auto it = this->ipCtxMap.find(header->dst); 339 | if (it == this->ipCtxMap.end()) { 340 | spdlog::debug("peer dest address not logged in: source {} dst {}", header->src.toString(), header->dst.toString()); 341 | return; 342 | } 343 | it->second->sendFrame(ctx.buffer); 344 | return; 345 | } 346 | 347 | void WebSocketServer::handleVMacMsg(WsCtx &ctx) { 348 | if (ctx.buffer.length() < sizeof(WsMsg::VMac)) { 349 | spdlog::warn("invalid vmac message: len {}", ctx.buffer.length()); 350 | ctx.status = -1; 351 | return; 352 | } 353 | 354 | WsMsg::VMac *header = (WsMsg::VMac *)ctx.buffer.data(); 355 | if (!header->check(this->password)) { 356 | spdlog::warn("vmac message check failed: buffer {:n}", spdlog::to_hex(ctx.buffer)); 357 | ctx.status = -1; 358 | return; 359 | } 360 | 361 | ctx.vmac.assign((char *)header->vmac, sizeof(header->vmac)); 362 | return; 363 | } 364 | 365 | void WebSocketServer::handleDiscoveryMsg(WsCtx &ctx) { 366 | if (ctx.ip.empty()) { 367 | spdlog::debug("unauthorized discovery websocket client"); 368 | ctx.status = -1; 369 | return; 370 | } 371 | 372 | if (ctx.buffer.length() < sizeof(WsMsg::Discovery)) { 373 | spdlog::debug("invalid discovery message: len {}", ctx.buffer.length()); 374 | ctx.status = -1; 375 | return; 376 | } 377 | 378 | WsMsg::Discovery *header = (WsMsg::Discovery *)ctx.buffer.data(); 379 | if (ctx.ip != header->src) { 380 | spdlog::debug("discovery source address does not match: auth {} source {}", ctx.ip.toString(), header->src.toString()); 381 | ctx.status = -1; 382 | return; 383 | } 384 | 385 | std::shared_lock lock(this->ipCtxMutex); 386 | if (header->dst == IP4("255.255.255.255")) { 387 | for (auto c : this->ipCtxMap) { 388 | if (c.first != header->src) { 389 | c.second->sendFrame(ctx.buffer); 390 | } 391 | } 392 | return; 393 | } 394 | auto it = this->ipCtxMap.find(header->dst); 395 | if (it != this->ipCtxMap.end()) { 396 | it->second->sendFrame(ctx.buffer); 397 | return; 398 | } 399 | } 400 | 401 | void WebSocketServer::HandleGeneralMsg(WsCtx &ctx) { 402 | if (ctx.ip.empty()) { 403 | spdlog::debug("unauthorized general websocket client"); 404 | ctx.status = -1; 405 | return; 406 | } 407 | 408 | if (ctx.buffer.length() < sizeof(WsMsg::General)) { 409 | spdlog::debug("invalid general message: len {}", ctx.buffer.length()); 410 | ctx.status = -1; 411 | return; 412 | } 413 | 414 | WsMsg::General *header = (WsMsg::General *)ctx.buffer.data(); 415 | 416 | if (ctx.ip != header->src) { 417 | spdlog::debug("general source address does not match: auth {} source {}", ctx.ip.toString(), header->src.toString()); 418 | ctx.status = -1; 419 | return; 420 | } 421 | 422 | std::shared_lock lock(this->ipCtxMutex); 423 | if (header->dst == IP4("255.255.255.255")) { 424 | for (auto c : this->ipCtxMap) { 425 | if (c.first != header->src) { 426 | c.second->sendFrame(ctx.buffer); 427 | } 428 | } 429 | return; 430 | } 431 | auto it = this->ipCtxMap.find(header->dst); 432 | if (it != this->ipCtxMap.end()) { 433 | it->second->sendFrame(ctx.buffer); 434 | return; 435 | } 436 | } 437 | 438 | void WebSocketServer::updateSysRoute(WsCtx &ctx) { 439 | ctx.buffer.resize(sizeof(WsMsg::SysRoute)); 440 | WsMsg::SysRoute *header = (WsMsg::SysRoute *)ctx.buffer.data(); 441 | memset(header, 0, sizeof(WsMsg::SysRoute)); 442 | header->type = WsMsgKind::ROUTE; 443 | 444 | for (auto rt : this->routes) { 445 | if ((rt.dev.Mask() & ctx.ip) == rt.dev.Host()) { 446 | SysRouteEntry item; 447 | item.dst = rt.dst.Net(); 448 | item.mask = rt.dst.Mask(); 449 | item.nexthop = rt.next; 450 | ctx.buffer.append((char *)(&item), sizeof(item)); 451 | header->size += 1; 452 | } 453 | // 100 条路由报文大小是 1204 字节,超过 100 条后分批发送 454 | if (header->size > 100) { 455 | ctx.sendFrame(ctx.buffer); 456 | ctx.buffer.resize(sizeof(WsMsg::SysRoute)); 457 | header->size = 0; 458 | } 459 | } 460 | 461 | if (header->size > 0) { 462 | ctx.sendFrame(ctx.buffer); 463 | } 464 | } 465 | 466 | int WebSocketServer::listen() { 467 | try { 468 | Poco::Net::ServerSocket socket(Poco::Net::SocketAddress(host, port)); 469 | 470 | Poco::Net::HTTPServerParams *params = new Poco::Net::HTTPServerParams(); 471 | params->setMaxThreads(0x00FFFFFF); 472 | 473 | this->running = true; 474 | WebSocketHandler wsHandler = [this](Poco::Net::WebSocket &ws) { handleWebsocket(ws); }; 475 | this->httpServer = std::make_shared(new HTTPRequestHandlerFactory(wsHandler), socket, params); 476 | this->httpServer->start(); 477 | spdlog::info("listen on: {}:{}", host, port); 478 | return 0; 479 | } catch (std::exception &e) { 480 | spdlog::critical("listen failed: {}", e.what()); 481 | return -1; 482 | } 483 | } 484 | 485 | void WebSocketServer::handleWebsocket(Poco::Net::WebSocket &ws) { 486 | ws.setReceiveTimeout(Poco::Timespan(1, 0)); 487 | WsCtx ctx = {.ws = &ws}; 488 | 489 | int flags = 0; 490 | int length = 0; 491 | std::string buffer; 492 | while (this->running && ctx.status == 0) { 493 | try { 494 | buffer.resize(1500); 495 | length = ws.receiveFrame(buffer.data(), buffer.size(), flags); 496 | int frameOp = flags & Poco::Net::WebSocket::FRAME_OP_BITMASK; 497 | 498 | // 响应 Ping 报文 499 | if (frameOp == Poco::Net::WebSocket::FRAME_OP_PING) { 500 | flags = (int)Poco::Net::WebSocket::FRAME_FLAG_FIN | (int)Poco::Net::WebSocket::FRAME_OP_PONG; 501 | ws.sendFrame(buffer.data(), buffer.size(), flags); 502 | continue; 503 | } 504 | 505 | // 客户端主动关闭连接 506 | if ((length == 0 && flags == 0) || frameOp == Poco::Net::WebSocket::FRAME_OP_CLOSE) { 507 | break; 508 | } 509 | 510 | if (frameOp == Poco::Net::WebSocket::FRAME_OP_BINARY && length > 0) { 511 | // 调整 buffer 为真实大小并移动到 ctx 512 | buffer.resize(length); 513 | ctx.buffer = std::move(buffer); 514 | 515 | // 处理客户端请求 516 | handleMsg(ctx); 517 | 518 | // 重新初始化 buffer 519 | buffer = std::string(); 520 | } 521 | } catch (Poco::TimeoutException const &e) { 522 | // 超时异常,不做处理 523 | continue; 524 | } catch (std::exception &e) { 525 | // 未知异常,退出这个客户端 526 | spdlog::debug("handle websocket failed: {}", e.what()); 527 | break; 528 | } 529 | } 530 | 531 | { 532 | std::unique_lock lock(ipCtxMutex); 533 | auto it = ipCtxMap.find(ctx.ip); 534 | if (it != ipCtxMap.end() && it->second == &ctx) { 535 | ipCtxMap.erase(it); 536 | spdlog::info("disconnect: {}", ctx.ip.toString()); 537 | } 538 | } 539 | } 540 | 541 | } // namespace candy 542 | --------------------------------------------------------------------------------