Files
New Futureandopenhands e9ad33e88b Feat/v5 web mcp (#745)
* feat(rust): add Web dashboard and MCP services

Share configuration, status and synchronization between Web and MCP; add authenticated HTTP, stdio lifecycle support, in-process scheduling and offline regression tests. Exercise native tests on the existing Windows/macOS/Linux CI matrix.

Co-authored-by: openhands <openhands@all-hands.dev>

* ci(rust): allow manual cross-platform validation

Enable workflow_dispatch for feature-branch CI without requiring PR creation. Preserve matrices, permissions, publishing boundaries and trusted-branch cache policy.

Co-authored-by: openhands <openhands@all-hands.dev>

* ci(rust): validate feature branch pushes

Run the existing Rust cross-platform matrix for feat/** pushes while retaining trusted-branch-only cache writes and unchanged release triggers.

Co-authored-by: openhands <openhands@all-hands.dev>

* ci(rust): remove temporary branch trigger

The v5 pull request now exists and runs the complete native matrix, so restore the original trusted push branches while retaining Web/MCP paths and tests.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-09-12 12:09:26 +08:00

11 KiB
Raw Permalink Blame History

Rust V5 客户端开发

ddns-rs 是 DDNS 下一主版本 V5 的 Rust 实现,集成分支为 v5。 Python 版本继续在 master / v4 维护;Rust 功能 PR 应以 v5 为目标。 当前开发版本为 5.0.0-alpha1,支持单次同步及 Web/MCP 服务,尚未发布,也不替换稳定的 Python ddns 命令或默认安装方式。

当前支持

能力 状态
全部 Python DNS Provider 及其兼容别名(Cloudflare、AliDNS/ESA、DNSPod、EdgeOne、ClouDNS、DNS.COM、HE.net、华为云、NameSilo、No-IP、Callback、West.cn、Debug) 已支持
IPv4/IPv6 与全部现有 IP 规则类型 已支持;regex: 使用 Rust 语法
CLI、DDNS_*、本地/远程/多配置、v4.1 providers 已支持
JSON 注释与受限 Python 数据字面量 已支持
缓存、代理回退、重试、TLS 与自定义 CA 已支持
Linux amd64/arm64 Docker 镜像 构建和离线冒烟测试;准备流程保存 OCI 工件,不推送公共镜像;web 提供进程内调度
Linux x64/arm64、macOS x64/arm64、Windows x64 二进制 准备流程保存 ddns-rs-* 与 .sha256 Actions 工件;Linux 使用静态 musl 目标
Web 控制台、配置管理与进程内调度 已支持;单个本地配置,间隔 1..1440 分钟
MCP stdio、独立 HTTP、Web /mcp 已支持;现代协议及旧版 stdio 兼容
task 与系统任务检测/接管 尚未迁移;必须手动停用旧 Python/主机任务

V5 分支与发布准备

保留 rust/ 布局、ddns-rs 命令及现有配置兼容边界,暂不删除 Python 实现。 rust/Cargo.toml 与 rust/Cargo.lock 的版本必须一致。v5 上的版本文件或 准备工作流、内嵌 Web 资源/字段模型或 Rust Dockerfile 发生变更时, Prepare Rust V5 自动构建,无需创建标签。它也接受 v5 分支上的手动准备,或与 Cargo 版本一致的规范 v5.* 标签,并生成经过 校验的原生工件和双架构 OCI 工件;不会创建 Release、上传发布资产或推送镜像。 准备流程仅接受带 alpha、beta 或 rc 后缀的 V5 预发布版本,不接受稳定版本。 V5 文档构建也不会部署覆盖稳定站点;本分支保留的 Python 发布入口拒绝 v5.*, 不改动稳定分支的发布配置。

当前没有可安装的 V5 发布版本,应使用源码或 CI 工件。在首次创建公开 V5 标签前, 还需隔离安装器和下载服务的 Python/Rust 版本选择,并单独确认 Rust 发布流程; 暂不要使用仓库级 latest / beta 下载作为 V5 安装入口。

构建与运行

使用当前 stable Rust:

cargo build --manifest-path rust/Cargo.toml --release --locked
rust/target/release/ddns-rs --help
rust/target/release/ddns-rs -c config.json

Windows 产物为 rust\target\release\ddns-rs.exe。

实验性 install-rust.sh 在移动临时二进制到安装目录前执行 --version; 新程序无法运行时,即使使用 --force 也不会覆盖已有的 ddns-rs。

Web 与 MCP

ddns-rs web -c config.json --host 127.0.0.1 --port 8000 --interval 5 --open
ddns-rs mcp -c config.json
ddns-rs mcp -c config.json --transport http --host 127.0.0.1 --port 8001

web 接受 -c FILE、--host HOST、--port PORT、--http-token TOKEN、 可重复的 --http-origin ORIGIN、--interval MINUTES 和 --open(打开浏览器)。 未指定子命令时,顶层 --interval 或配置根级 interval 会选择 Web 模式。 进程内调度间隔为 1..1440 分钟,仅在进程存活期间运行。

控制台支持读取、校验和保存完整配置,管理备份/恢复、查看状态并触发同步。 Web 与 MCP 复用同一个服务,仅接受单个本地配置文件,不接受远程或多配置来源。 Web 使用现有兼容的 Rust 运行时校验配置,不代表与 Python 运行时完全等价。

mcp 默认使用按行分隔的 stdio。--transport http 在 /mcp 提供独立的 Streamable HTTP;Web 监听器也提供相同端点。现代 MCP 协议版本为 2026-07-28;旧版 2025-11-25 仅通过 stdio 兼容。两种服务均提供与 Python 相同的 get_ddns_status、update_dns_records 工具。

HTTP 默认监听回环地址。非回环监听必须设置 --http-token TOKEN,客户端使用 Authorization: Bearer TOKEN 鉴权。可重复传入 --http-origin ORIGIN 添加允许的 浏览器来源。Web 和 MCP HTTP 均执行来源检查与鉴权。不要将明文 HTTP 暴露于 不可信网络,应使用可信的 TLS 反向代理或隧道。状态响应不包含凭据,但受监听器 鉴权保护的配置 API 会按设计返回完整配置,包括 Provider 凭据。监听 token、配置和 备份均应视为机密;即使不设置回环监听 token,本机访问仍是信任边界。 监听 token 授予敏感配置访问与 DNS 更新权限,不是只读监控凭据。Web 同步会以 进程用户权限执行经鉴权管理的本地配置中的 cmd:/shell: 规则,因此只应向可信 管理员授予访问权限。

二进制在编译时内嵌现有 web/ 控制台资源和 ddns/config/field-model.json, 提供页面无需前端服务器或 Python 运行时。修改这些输入后须重新构建;源码构建需 保留仓库布局,不能只复制孤立的 rust/ 目录。

迁移限制: 尚未实现 task、系统任务检测及自动接管。启用 Rust 定时同步前, 必须手动停用旧 Python/主机的 systemd、cron、launchd 或 schtasks 任务,避免重复运行。 Web 调度器不会安装或替代主机服务。已有 Rust regex: 与独立缓存限制仍然适用; log_format、log_datefmt 仅为配置兼容而接受,不保证 Python 日志格式等价。

架构

  • cli.rs:兼容参数、别名和列表规则,不依赖 CLI 框架。
  • config/:环境变量、JSONC、受限 Python 字面量、v4.1 展平和优先级合并。
  • http.rs:基于 ureq/rustls 的同步传输、代理、重试、TLS 和脱敏。
  • ip.rs:IPv4/IPv6 地址发现与规则回退。
  • cache.rs:独立于 Python 的版本化缓存和原子写入。
  • provider/:公共 CRUD 编排及全部 DNS Provider。
  • update.rs:逐配置、地址族和域名执行,继续后续项并汇总失败。

兼容性处理限制在 CLI 与配置加载边界;进入核心后,Provider 使用 ProviderId 枚举,别名只解析一次。Provider 通过 trait object 做运行时分发, 借用同步 HTTP 客户端;单次更新请求借用域名、地址和 Provider 扩展配置,避免 为每个域名复制整份配置。Provider 特有的 extra 仍保留动态 JSON 值,因为其 字段由各 DNS API 定义。 域名列表在配置层只拒绝空条目,允许 debug、callback 使用 localhost 等单标签; 具体域名约束由各 Provider 处理。 整数日志级别支持 --log-level -5 和 --log-level=-5 两种写法。

Cloudflare 和腾讯云查询用 Result<Option<_>> 区分记录不存在与请求失败;只有正常空列表或已知的 查询未命中错误码才允许继续查找或创建。Cloudflare 和腾讯云写入响应必须包含 有效的结果标识,畸形响应不会被当作成功写入缓存。腾讯云的 DNSPod、EdgeOne 加速和 EdgeOne DNS 模式由枚举确定服务与 API 版本,避免自由组合字符串和布尔状态。

NameSilo 和 ClouDNS 仅在域名查询返回文档定义的未命中响应时尝试下一个候选域名; 鉴权、权限、HTTP 和畸形响应错误直接上报,不会被替换为“域名不存在”。 ClouDNS、华为云与 NameSilo 的记录查询会拒绝畸形集合或缺少匹配字段的记录, 避免误建重复记录;ClouDNS 的 {} 和 [] 均保留为空集合。 华为云查询签名与实际请求使用相同的 RFC3986 查询字符串,空格编码为 %20 而非 +,发送时不再二次表单编码;其他 Provider 的表单编码保持不变。

AliDNS 和 ESA 在提供 extra 时仍发送更新请求,即使地址和 TTL 没有变化; 未提供扩展参数时保留原有的跳过无变化更新行为。

DNSPod 中国版和国际版在标准写入参数之后合并 extra,保留其覆盖优先级。 IP 文本提取使用标准库 IpAddr 校验完整地址,支持 IP:2001:db8::1、 address:192.0.2.1 等紧凑文本标签;无效 IPv6 不会被截断成可用片段。 regex: 从提取的原始地址开头匹配,保留 IPv6 的零填充、大小写和压缩写法, 例如 ^2001:0db8: 可以匹配 2001:0db8::1;返回值仍为校验后的 IpAddr。 local 和数字索引先按地址族排除回环与未指定地址,再按原有顺序从 0 开始索引; 没有可用地址或索引越界时明确报错。私有地址和链路本地地址仍可选取。

生产代码不使用 unsafe,不引入异步运行时、通用错误框架或 mock 框架。Provider 测试通过注入 HTTP 客户端完成,集成测试只访问本机 fixture。

验证

cargo fmt --manifest-path rust/Cargo.toml --check
cargo clippy --manifest-path rust/Cargo.toml --locked --all-targets --all-features -- -D warnings
cargo test --manifest-path rust/Cargo.toml --locked

CI 还会在 Linux x64/arm64、Windows x64、macOS x64/arm64 构建、测试并打包 二进制;Linux x64/arm64 还会构建和离线冒烟测试 Docker 镜像。

安全边界

  • ssl=auto 会在确认属于证书校验错误时警告并重试一次不校验证书的连接;生产环境优先使用 ssl=true。
  • 远程配置中的 cmd: 与 shell: 会执行本机命令,只能加载可信 URL。
  • regex: 使用 Rust regex 语法,不支持 Python 环视和反向引用;不兼容模式会返回明确错误。
  • 复用配置中的自定义 cache 路径时,Rust 写入同级 <path>.ddns-rs,不会覆盖 Python 缓存。
  • Python 字面量解析器只接受字典、列表/元组、字符串、数字、True、False 和 None,不会执行表达式。
  • 日志与错误信息将已登记的 ID、token 及其百分号编码、表单编码形式替换为固定的 ***,不保留凭据首尾字符;缓存不保存凭据。
  • DNS.COM 签名请求体不写入日志,API 错误消息中的当前签名会脱敏,JSON 解析错误不回显原始响应;敏感请求的 HTTP 错误只保留状态码。
  • 诊断中的 URL 会隐藏用户信息、非根路径、查询值与片段,避免 Callback 或远程配置的路径凭据泄漏;实际发送的 URL 不变。

后续顺序

  1. 迁移 systemd、cron、launchd、schtasks。
  2. 验证 Web/MCP 在长期运行与平台迁移中的兼容性。
  3. 达到完整等价并稳定运行后再讨论改名为 ddns。