基于 Boost.Asio/C++ 的 EasyTier 异地组网后端 daemon 程序,通过本地 Socket 向前端提供 JSON IPC 管理接口。本项目主要用于 QtEasyTier 相关项目的后端守护进程。
EasyTier 是一款去中心化的异地组网和内网穿透工具。由于 EasyTier 后端(FFI)在执行创建 TUN 网卡等操作时需要高权限(Linux 下 root、Windows 下管理员),而前端 UI 不宜以高权限运行,因此设计了本独立后端 daemon。
qtet-daemon 通过 FFI 调用 EasyTier 的 Rust 核心库(Linux 下为 libeasytier_ffi.so,Windows 下为 easytier_ffi.dll,macOS 下为 libeasytier_ffi.dylib),封装并暴露其管理功能,向上层前端提供 JSON IPC 接口。daemon 本身不实现组网逻辑——它仅仅是 EasyTier Rust 核心的管理代理。
本程序会在程序二进制文件目录下维护 <socket名称>.json,用于保存自动回连开关和最近成功启动的实例配置。
职责边界:本 daemon 专注于管理层面(实例生命周期、状态监控、配置管理、前端接口暴露),不直接操作数据平面(TCP/UDP 连接、数据读写)。
- CMake 3.19+
- Boost.Asio(header-only 使用,仅需 Boost 头文件)
- 支持 C++17 的编译器(Linux/macOS 用 GCC/Clang,Windows 用 MinGW 或 LLVM-MinGW,暂不支持 MSVC)
libeasytier_ffi动态库无需手动安装——CMake 配置时自动下载对应平台的预编译包(EasyTier FFI v2.6.4)- 可选:Ninja(不安装则使用 CMake 平台默认生成器)
Linux / macOS:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)Windows(PowerShell):
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 8 # -j 参数也可省略CMake 选项:
-DDAEMON_SOCKET_NAME=...:自定义 IPC socket 名称,默认为qtet-daemon.sock-DDOWNLOAD_FFI_FROM_CNB=OFF:切换 FFI 下载源,默认从 CNB 下载,关闭后改从 GitHub Releases 下载
生成产物统一输出到 build/Output/:
| 目标 | 路径 |
|---|---|
| daemon | build/Output/qtet-daemon(Windows 下为 qtet-daemon.exe) |
| CLI 客户端 | build/Output/cli-client(Windows 下为 cli-client.exe) |
| 测试 | build/Output/tst_* |
FFI 动态库会随构建自动复制到
build/Output/,daemon 通过 RPATH(构建目录)或$ORIGIN(安装目录)在自身目录下查找,无需手动配置库路径。
cmake --install build --prefix /usr/local安装规则会把 daemon 可执行文件与 FFI 动态库一并安装到同一目录,可直接运行。
Linux:
# 启动 daemon(需要 root 权限,因为要创建 TUN 网卡)
sudo -E ./build/Output/qtet-daemon
# 通过 CLI 客户端调用 daemon(无需 root)
./build/Output/cli-client list_instancesWindows:以管理员身份运行 build\Output\qtet-daemon.exe(创建 TUN 网卡需要管理员权限);CLI 客户端 build\Output\cli-client.exe 调用无需管理员。
如果 daemon 在非特权模式下运行,
run_network_instance等需要 TUN 设备的操作会失败,但 IPC 通信功能正常。
Windows 下可将 daemon 注册为系统服务,随系统自动启动(SERVICE_AUTO_START),无需手动运行。服务内部名为 qtet-daemon.sock(等于 IPC socket 名,自定义构建时同步变化),显示名为 QtEasyTier Daemon Service。
# 注册服务(服务命令行自动携带 --daemon,防止程序崩溃退出)
qtet-daemon.exe --install
# 启动 / 停止服务
qtet-daemon.exe --start
qtet-daemon.exe --stop
# 卸载服务(运行中会先停止)
qtet-daemon.exe --uninstall
# 查询服务状态
sc query qtet-daemon.sock说明:
--install/--start/--stop/--uninstall需要管理员权限;未以管理员身份运行时程序会自动弹出 UAC 提权对话框并重跑自身,无需手动开管理员终端- 普通前台模式(无参数运行)与
--daemon模式不做提权判断,以任意权限运行- 服务管理命令只操作服务控制管理器,不初始化 FFI、不监听 socket,执行完即退出
--daemon由服务控制管理器拉起时以服务模式运行;手动在控制台运行qtet-daemon.exe --daemon时会回退为前台模式,便于调试- 服务模式下日志无控制台可见;服务以 LocalSystem 账户运行,需确保
easytier_ffi.dll与qtet-daemon.exe同目录
qtet-daemon/
├── CMakeLists.txt # 顶层 CMake
├── README.md
├── AGENTS.md # 开发上下文文档
│
├── cmake/
│ └── download_ffi.cmake # EasyTier FFI 自动下载与导入
│
├── src/
│ ├── main.cpp # 应用入口
│ ├── CMakeLists.txt
│ ├── base64.h / .cpp # Base64 编解码辅助
│ ├── app_paths.h / .cpp # 可执行文件所在目录定位(与 CWD 无关)
│ ├── log.h # 轻量日志(LOG_INFO/LOG_WARNING/LOG_ERROR)
│ ├── daemon/
│ │ ├── Protocol.h / .cpp # 通信协议层(JSON 序列化 + 长度前缀帧)
│ │ ├── IpcServer.h / .cpp # IPC 服务端(Boost.Asio 本地 Socket 多会话模型)
│ │ └── ReconnectStore.h / .cpp # 自动回连配置的 JSON 持久化
│ ├── ffis/
│ │ └── FfiDispatcher.h / .cpp # FFI 调度层(方法→EasyTierFFI)
│ ├── ipc/ # 本地 Socket 平台抽象(POSIX/Windows 双实现)
│ │ ├── LocalSocket.h / .cpp # 流式本地 Socket(LocalSocketPosix/Windows.cpp)
│ │ ├── LocalAcceptor.h / .cpp # 监听接受器 + 单实例探测
│ │ └── LocalEndpoint.h # 端点路径推导(Unix socket 路径 / 命名管道名)
│ └── windows-register/ # Windows 服务注册与控制(SCM API,仅 WIN32 编译)
│ └── WindowsService.h / .cpp # --daemon 服务入口 + 服务管理命令
│
├── tests/
│ ├── CMakeLists.txt
│ ├── tst_base64.cpp # Base64 单元测试
│ ├── tst_log.cpp # 日志单元测试
│ ├── tst_app_paths.cpp # 路径定位单元测试
│ ├── tst_protocol.cpp # 协议层单元测试
│ ├── tst_dispatcher.cpp # 调度层单元测试
│ ├── tst_reconnect_store.cpp # 自动回连配置持久化测试
│ └── tst_integration.cpp # 端到端集成测试(仅非 Windows 构建)
│
├── examples/
│ ├── CMakeLists.txt
│ └── cli-client/
│ ├── CMakeLists.txt
│ └── main.cpp # 示例 CLI 前端
│
└── easytier/ # EasyTier FFI 头文件(上游依赖,不修改;
# 动态库在 configure 时下载到 build 目录)
└── include/
├── easytier_ffi.h # C FFI 头文件
└── EasyTierFFI.h # C++ 封装头文件
本 daemon 使用三层协议栈与前端通信:传输层 → 帧协议 → 消息格式。
| 项 | 值 |
|---|---|
| 传输方式 | 本地 Socket(Linux/macOS 为 Unix Domain Socket,Windows 为命名管道),端点路径语义与 Qt QLocalSocket/QLocalServer 对齐 |
| Socket 名称 | qtet-daemon.sock(Linux 下实际路径为 /tmp/qtet-daemon.sock,若设置了 TMPDIR 环境变量则位于其下;Windows 下为 \\.\pipe\qtet-daemon.sock) |
前端只需使用标准 QLocalSocket 客户端连接名为 qtet-daemon.sock 的 socket 即可。在 Qt 中:
QLocalSocket sock;
sock.connectToServer("qtet-daemon.sock");在其他语言(Python 示例,仅 Linux):
import socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/tmp/qtet-daemon.sock")若 daemon 以自定义 socket 名称构建(
-DDAEMON_SOCKET_NAME=...),上述路径需同步替换。
TCP 是流式协议,不保证消息边界。本协议使用4 字节大端长度前缀进行帧分隔:
┌──────────────────────────────────────────────┐
│ Payload Length (4 bytes, Big-Endian uint32) │
├──────────────────────────────────────────────┤
│ Payload (JSON bytes, UTF-8) │
└──────────────────────────────────────────────┘
编码规则(写帧):
1. 获取 JSON 字节数 N(uint32)
2. 构造 4 字节大端长度头:byte[0]=(N>>24)&0xFF, byte[1]=(N>>16)&0xFF,
byte[2]=(N>>8)&0xFF, byte[3]=N&0xFF
3. 写入 socket:长度头 + JSON 字节
4. flush socket
解码规则(读帧):
1. 从 socket 读取所有可用数据,追加到接收缓冲区
2. 如果缓冲区 < 4 字节,等待更多数据
3. 从前 4 字节解码 payload 长度 N(大端序)
4. 如果缓冲区 < 4+N 字节,等待更多数据
5. 取出缓冲区 [4, 4+N) 作为完整帧 payload
6. 从缓冲区移除 [0, 4+N)
伪代码实现:
function writeFrame(sock, payload):
len = payload.length
header = bytes([
(len >> 24) & 0xFF,
(len >> 16) & 0xFF,
(len >> 8) & 0xFF,
len & 0xFF
])
sock.sendall(header + payload)
function readFrame(sock, buffer):
buffer += sock.recv(65536)
if len(buffer) < 4:
return null
len = (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]
if len(buffer) < 4 + len:
return null
payload = buffer[4 : 4+len]
buffer = buffer[4+len : ]
return payload
每帧 payload 是一个完整的 JSON 文档(UTF-8 编码,Compact 格式无换行)。
请求格式:
{
"id": 1,
"method": "方法名",
"params": {
"参数1": "值1",
"参数2": "值2"
}
}| 字段 | 类型 | 说明 |
|---|---|---|
id |
integer | 请求序号,前端自行维护,响应中会原样返回 |
method |
string | 方法名,对应 EasyTierFFI 函数名(snake_case) |
params |
object | 方法参数;是否需要 Base64 取决于具体字段,见下方 API 参考 |
成功响应:
{
"id": 1,
"method": "方法名",
"result": {
"字段1": "值1"
}
}错误响应:
{
"id": 1,
"method": "方法名",
"error": {
"message": "错误描述"
}
}| 字段 | 类型 | 说明 |
|---|---|---|
id |
integer | 与请求相同 |
method |
string | 与请求相同 |
result |
object | 成功时的返回数据——与 error 互斥 |
error |
object | 失败时的错误信息,含 message 字段——与 result 互斥 |
Base64 只用于当前接口中明确标注的字段,主要是 TOML 配置、JSON payload、状态 JSON 和错误消息等可能包含复杂文本内容的字段。其他普通标识符、实例名称、整数、布尔值和数组按原始 JSON 类型传输。
调用方应以本节和 API 参考中的字段说明为准,不要简单按 C++ 层 std::string 类型推断是否需要 Base64。
需要 Base64 编码的参数:
| 方法 | 参数字段 |
|---|---|
parse_config |
cfg_str |
run_network_instance |
cfg_str |
call_json_rpc |
payload |
start_config_server_client |
config_server_url, hostname, machine_id |
需要 Base64 编码的返回值字段:
| 方法 | 返回字段 |
|---|---|
collect_network_infos |
instances[].value |
call_json_rpc |
response |
get_error_msg |
errors[] |
get_last_error_msg |
message |
编码与解码:
发送:Base64Encode(原始UTF-8字符串)
例如:echo -n "Hello" | base64 → "SGVsbG8="
接收:Base64Decode(接收到的字符串)
例如:echo -n "SGVsbG8=" | base64 -d → "Hello"
当前不需要 Base64 编码的字段:
| 方法 | 字段 |
|---|---|
delete_network_instance |
inst_names[] |
list_instances |
instances[].key, instances[].value |
call_json_rpc |
service_name, method_name, domain_name |
set_auto_reconnect |
enabled |
get_auto_reconnect |
无参数 |
poll_config_events |
events[] |
| 所有方法 | id, method, 整数参数、布尔参数 |
约定:以下示例中,所有
<base64>标记的值表示实际传输时需替换为 Base64 编码后的字符串。
验证 TOML 配置格式合法性,不启动实例。
请求:
{
"id": 1,
"method": "parse_config",
"params": {
"cfg_str": "<Base64编码的TOML内容>"
}
}成功响应:
{
"id": 1,
"method": "parse_config",
"result": {}
}失败响应:
{
"id": 1,
"method": "parse_config",
"error": {
"message": "Invalid TOML syntax at line 3"
}
}启动一个 EasyTier 网络实例。同名实例会被覆盖,实例在后台线程运行。启动成功后,daemon 会尝试从 TOML 根域读取 instance_name;如果读取成功,会把传入的 Base64 cfg_str 写入自动回连缓存。同名缓存会被更新。
请求:
{
"id": 2,
"method": "run_network_instance",
"params": {
"cfg_str": "<Base64编码的TOML配置>"
}
}成功响应:
{
"id": 2,
"method": "run_network_instance",
"result": {}
}示例 TOML 配置(Base64 编码前):
instance_name = "my-net" [instance] hostname = "node1" [[peer]] uri = "tcp://example.com:11010"
删除指定的网络实例。空数组不执行任何操作,未知名称会被忽略。删除成功后,daemon 会从自动回连缓存中移除 inst_names[] 对应的实例。
请求:
{
"id": 3,
"method": "delete_network_instance",
"params": {
"inst_names": ["my-net", "old-net"]
}
}成功响应:
{
"id": 3,
"method": "delete_network_instance",
"result": {}
}| 参数 | 类型 | 说明 |
|---|---|---|
inst_names |
string[] | 要删除的实例名称列表 |
仅返回实例名称和 ID,不含详细状态 JSON。最多返回 64 个实例。
请求:
{
"id": 4,
"method": "list_instances",
"params": {}
}成功响应:
{
"id": 4,
"method": "list_instances",
"result": {
"instances": [
{
"key": "my-net",
"value": "abc123def"
}
]
}
}| 返回字段 | 说明 |
|---|---|
instances[].key |
实例名称 |
instances[].value |
实例 ID |
返回实例名称和详细状态信息。instances[].key 为原始实例名称;instances[].value 是 JSON 状态字符串,Base64 编码后返回。
请求:
{
"id": 5,
"method": "collect_network_infos",
"params": {
"max_length": 64
}
}| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_length |
integer | 64 | 最大查询实例数 |
成功响应:
{
"id": 5,
"method": "collect_network_infos",
"result": {
"count": 1,
"instances": [
{
"key": "my-net",
"value": "<Base64编码的JSON状态信息>"
}
]
}
}
value字段解码后为 EasyTier 的原生 JSON 状态对象,包含节点列表、连接状态、延迟等信息。
调用 EasyTier 核心内部的 JSON RPC 服务。service_name、method_name、domain_name 按普通 JSON 字符串传输;payload 需 Base64 编码,response 为 Base64 编码返回。
请求:
{
"id": 6,
"method": "call_json_rpc",
"params": {
"service_name": "service",
"method_name": "method",
"domain_name": "",
"payload": "<Base64编码的请求JSON>"
}
}| 参数 | 类型 | 说明 |
|---|---|---|
service_name |
string | 目标 RPC 服务名 |
method_name |
string | 目标 RPC 方法名 |
domain_name |
string | 域名,可为空字符串 |
payload |
string | 请求 JSON 字符串(Base64) |
成功响应:
{
"id": 6,
"method": "call_json_rpc",
"result": {
"response": "<Base64编码的响应JSON>"
}
}连接到远程配置服务器,接收远程下发的实例启停事件。事件通过 poll_config_events 轮询获取。
请求:
{
"id": 7,
"method": "start_config_server_client",
"params": {
"config_server_url": "<Base64编码的URL>",
"hostname": "<Base64编码的主机名>",
"machine_id": "<Base64编码的机器ID>",
"secure_mode": false
}
}| 参数 | 类型 | 说明 |
|---|---|---|
config_server_url |
string | 配置服务器 URL(Base64) |
hostname |
string | 主机名,可为空(Base64) |
machine_id |
string | 机器唯一标识(Base64) |
secure_mode |
boolean | 是否使用安全模式 |
断开与远程配置服务器的连接。
请求:
{
"id": 8,
"method": "stop_config_server_client",
"params": {}
}查询是否已连接到配置服务器。
请求:
{
"id": 9,
"method": "is_config_server_client_connected",
"params": {}
}成功响应:
{
"id": 9,
"method": "is_config_server_client_connected",
"result": {
"connected": false
}
}返回 EasyTier FFI 中累积的错误信息列表。每条错误消息为 Base64 编码。
请求:
{
"id": 10,
"method": "get_error_msg",
"params": {}
}成功响应:
{
"id": 10,
"method": "get_error_msg",
"result": {
"errors": [
"<Base64编码的错误消息1>",
"<Base64编码的错误消息2>"
]
}
}返回最近一条错误信息(Base64 编码)。无错误时返回 "Unknown error" 的 Base64。
请求:
{
"id": 11,
"method": "get_last_error_msg",
"params": {}
}成功响应:
{
"id": 11,
"method": "get_last_error_msg",
"result": {
"message": "<Base64编码的错误消息>"
}
}注意:此方法是 IPC 层的附加方法,用于读取配置服务器回调暂存的事件队列。
轮询获取远程配置服务器推送的待处理事件。调用 start_config_server_client 后,远程下发的事件会缓存在内部队列中,需通过此方法取出。
请求:
{
"id": 13,
"method": "poll_config_events",
"params": {}
}成功响应:
{
"id": 13,
"method": "poll_config_events",
"result": {
"events": [
"{...事件JSON...}",
"{...事件JSON...}"
]
}
}如果无待处理事件,
events为空数组。
设置 daemon 下次启动时是否自动回连缓存实例。开关保存在 <socket名称>.json 的根域 autoReconnect 字段中,默认值为 false。
请求:
{
"id": 14,
"method": "set_auto_reconnect",
"params": {
"enabled": true
}
}成功响应:
{
"id": 14,
"method": "set_auto_reconnect",
"result": {
"autoReconnect": true
}
}查询当前是否启用自动回连。
请求:
{
"id": 15,
"method": "get_auto_reconnect",
"params": {}
}成功响应:
{
"id": 15,
"method": "get_auto_reconnect",
"result": {
"autoReconnect": true
}
}配置文件位于 daemon 可执行文件所在目录(与运行时工作目录无关),文件名为 <socket名称>.json。默认 socket 名为 qtet-daemon.sock 时,文件名为 qtet-daemon.sock.json。
{
"autoReconnect": false,
"instances": [
{
"instance_name": "my-net",
"cfg_str": "<Base64编码的TOML配置>"
}
]
}当 autoReconnect 为 true 时,daemon 每次启动都会读取 instances 并逐个尝试回连缓存配置:回连成功的实例保留缓存项(下次启动继续回连),回连失败的实例删除对应缓存项(避免反复重试坏配置)。autoReconnect 开关本身不会被自动关闭。
- 多客户端并行连接:daemon 支持多个前端客户端同时连接,每个客户端独立成会话(独立帧缓冲与 FFI 调度器),会话之间互不阻塞。
- 单实例运行:若 socket 已被其他 daemon 实例监听,新启动的实例会检测到并直接退出(不会移除或覆盖正在运行的实例的 socket)。
- 会话内请求串行执行:同一会话同一时刻最多只有一个 FFI 调用在途,调用期间到达的帧累积在接收缓冲区,当前调用完成后按序处理(不会丢弃);不同会话的 FFI 调用可在线程池中并行执行。
- 非法帧断连:零长度或超过单帧上限(16 MiB)的帧视为非法,daemon 会直接断开对应客户端,防止接收缓冲区被恶意撑大。
- 无鉴权:本地 socket 对所有本地用户开放(Windows 命名管道授予 Everyone 读写权限,等价于 Qt
QLocalServer::WorldAccessOption语义),允许非 root/非管理员前端连接高权限运行的 daemon。socket 上没有任何认证或加密,任何本地用户都可发起请求,请仅在本机可信环境运行。 - 事件为内存队列:配置服务器推送的事件缓存在进程内队列中,daemon 重启后丢失,需通过
poll_config_events及时轮询取出。
examples/cli-client/ 提供了一个最小命令行前端,演示如何通过 IPC 协议与 daemon 通信。
# 无参数运行:列出所有可用方法
./build/Output/cli-client
# 查看命令行用法
./build/Output/cli-client --help
# 列出运行中的实例
./build/Output/cli-client list_instances
# 查询配置服务器连接状态
./build/Output/cli-client is_config_server_client_connected
# 获取最近一条错误
./build/Output/cli-client get_last_error_msg
# 传递 JSON 参数
./build/Output/cli-client delete_network_instance '{"inst_names":["my-net"]}'# 运行所有测试
ctest --test-dir build --output-on-failure
# 单独运行某个测试
ctest --test-dir build -R tst_protocol --output-on-failure