* 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>
12 KiB
Rust V5 client development
ddns-rs is the Rust implementation for DDNS V5, integrated on the v5 branch.
Python maintenance continues on master / v4; Rust feature PRs should target
v5. The current development version is 5.0.0-alpha1. It supports one-shot
updates and Web/MCP services, is not published, and does not replace the stable
Python ddns command or default installer.
Current support
| Capability | Status |
|---|---|
| All Python DNS providers and documented compatibility aliases (Cloudflare, AliDNS/ESA, DNSPod, EdgeOne, ClouDNS, DNS.COM, HE.net, Huawei DNS, NameSilo, No-IP, Callback, West.cn, and Debug) | Supported |
| IPv4/IPv6 and every existing address rule type | Supported; regex: uses Rust syntax |
CLI, DDNS_*, local/remote/multiple configs, v4.1 providers |
Supported |
| JSON comments and restricted Python data literals | Supported |
| Cache, proxy fallback, retries, TLS, and custom CA files | Supported |
| Linux amd64/arm64 Docker image | Build and offline smoke coverage; preparation saves an OCI artifact without registry publication; web provides in-process scheduling |
| Linux x64/arm64, macOS x64/arm64, Windows x64 binaries | Preparation saves ddns-rs-* and .sha256 Actions artifacts; Linux uses static musl targets |
| Web dashboard, configuration management, and in-process scheduling | Supported; one local config, interval 1..1440 minutes |
MCP stdio, standalone HTTP, and Web /mcp |
Supported; modern protocol and legacy stdio compatibility |
task and OS task detection/takeover |
Not ported; manually disable old Python/host tasks |
V5 branch and release preparation
Keep the rust/ layout, ddns-rs command, and existing configuration boundaries;
do not remove Python yet. The versions in rust/Cargo.toml and rust/Cargo.lock
must match. Changes to the version files, preparation workflow, embedded Web
assets/field model, or Rust Dockerfile on v5 automatically run Prepare Rust V5
without requiring a tag. It also accepts manual preparation on v5 or canonical
v5.* tags matching Cargo, then produces verified native and two-platform OCI
artifacts. It does not create Releases, upload release assets, or push images.
Preparation accepts only V5 prereleases with an alpha, beta, or rc suffix,
not stable versions.
V5 documentation builds do not deploy over the stable site. The Python publisher
retained on this branch rejects v5.* inputs; stable-branch publishing
configuration is unchanged.
There is no installable V5 release yet; use source or CI artifacts. Before the
first public V5 tag, isolate Python/Rust version selection in installers and
the download service, and separately confirm the Rust publication workflow.
Do not use repository-wide latest / beta downloads as V5 install channels yet.
Build and run
Use the current stable Rust toolchain:
cargo build --manifest-path rust/Cargo.toml --release --locked
rust/target/release/ddns-rs --help
rust/target/release/ddns-rs -c config.json
The Windows artifact is rust\target\release\ddns-rs.exe.
The experimental install-rust.sh runs --version on the temporary binary
before moving it into place. An artifact that cannot run leaves the existing
ddns-rs untouched, even with --force.
Web and 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 accepts -c FILE, --host HOST, --port PORT, --http-token TOKEN,
repeatable --http-origin ORIGIN, --interval MINUTES, and --open (open the
browser). Without an explicit subcommand, top-level --interval or a root
configuration interval selects Web mode. The in-process scheduler accepts
1..1440 minutes and runs only while the process is alive.
The dashboard reads, validates, and saves the full configuration, manages backups/restores, reports status, and triggers synchronization. Web and MCP use the same service. They require a single local configuration file, not remote or multiple configuration sources. Web validation uses the compatible Rust runtime; it does not promise full Python runtime parity.
mcp defaults to newline-delimited stdio. --transport http serves standalone
Streamable HTTP at /mcp; Web exposes the same endpoint on its own listener.
Modern MCP uses protocol 2026-07-28; legacy 2025-11-25 is supported over stdio
only. Both expose the Python tool names get_ddns_status and
update_dns_records.
HTTP listeners default to loopback. A non-loopback bind requires
--http-token TOKEN; clients authenticate with Authorization: Bearer TOKEN.
Use repeated --http-origin ORIGIN flags for additional allowed browser origins.
Origin checks and authentication apply to both Web and MCP HTTP. Do not expose
plain HTTP on an untrusted network; use a trusted TLS reverse proxy or tunnel.
Status responses exclude credentials, but the authenticated configuration API
intentionally returns the full configuration, including provider credentials.
Treat listener tokens, configuration files, and backups as secrets. With no
listener token on loopback, local access is still a trust boundary.
The listener token grants sensitive configuration access and DNS update authority,
not read-only monitoring. Web updates execute cmd:/shell: rules from the
authenticated local configuration with the process user's permissions; grant
access only to trusted administrators.
The binary embeds the existing web/ dashboard assets and
ddns/config/field-model.json at compile time; no frontend server or Python
runtime is needed to serve them. Rebuild after changing these inputs. Source
builds require the repository layout, not an isolated copy of rust/.
Migration limitation: task, OS scheduler detection, and automatic takeover
are not implemented. Manually disable old Python/host systemd, cron, launchd, or
schtasks jobs before enabling Rust scheduled updates to avoid duplicate runs.
The Web scheduler does not install or replace a host service. Existing Rust
regex: and separate-cache limits still apply. log_format and log_datefmt
are accepted for configuration compatibility, not Python formatting parity.
Architecture
cli.rs: compatible options, aliases, and list rules without a CLI framework.config/: environment, JSONC, restricted Python literals, v4.1 expansion, and precedence.http.rs: synchronousureq/rustls transport, proxies, retries, TLS, and redaction.ip.rs: IPv4/IPv6 discovery and rule fallback.cache.rs: a versioned cache independent from the Python cache.provider/: shared CRUD orchestration and all DNS providers.update.rs: deterministic execution with continued attempts and aggregated failures.
Compatibility handling stays at the CLI and configuration boundary. Inside the
core, providers use the ProviderId enum and aliases are parsed once. Providers
use trait objects only for runtime dispatch and borrow the synchronous HTTP
client; each update request borrows its domain, address, and provider extras
instead of copying the full configuration per domain. Provider-specific
extra values remain dynamic JSON because their fields are defined by each DNS
API.
The configuration layer rejects only empty domain entries, allowing debug
and callback to use single labels such as localhost. Provider-specific
domain constraints are handled by each provider.
Integer log levels support both --log-level -5 and --log-level=-5.
Cloudflare and Tencent Cloud lookups use Result<Option<_>> to distinguish absence
from request failure. Only valid empty lists or known lookup-miss error codes permit further lookup or
creation. Cloudflare and Tencent Cloud mutation responses must contain a valid
result identifier; malformed responses are not cached as successful updates.
An enum selects the service and API version for Tencent DNSPod, EdgeOne
acceleration, and EdgeOne DNS instead of freely combining strings and booleans.
NameSilo and ClouDNS try the next candidate zone only for documented zone-lookup
misses. Authentication, permission, HTTP, and malformed-response errors propagate
instead of becoming "zone not found". ClouDNS, Huawei, and NameSilo record lookups
reject malformed collections and records missing matching fields to avoid
duplicate creation; ClouDNS still accepts both {} and [] as empty collections.
Huawei signing and requests use the same RFC3986 query string, encoding spaces
as %20 rather than + without a second form-encoding pass. Form encoding for
other providers is unchanged.
AliDNS and ESA send updates when extra is supplied even if the address and TTL
are unchanged. The existing unchanged-record shortcut remains when no extras
are supplied.
Both DNSPod endpoints merge extra after standard mutation parameters, preserving
its override priority. IP extraction validates complete addresses with the
standard-library IpAddr type and supports compact labels such as
IP:2001:db8::1 and address:192.0.2.1; malformed IPv6 literals are not truncated
into usable fragments.
regex: matches from the start of the extracted original address, preserving
IPv6 zero padding, case, and compression for matching. For example, ^2001:0db8:
matches 2001:0db8::1; the returned value remains a validated IpAddr.
local and numeric indices exclude loopback and unspecified addresses within
the requested family before indexing from 0 in the original order. No usable
address or an out-of-range index returns an error; private and link-local
addresses remain selectable.
Production code forbids unsafe code and does not use an async runtime, a generic error framework, or a mock framework. Provider tests inject an HTTP client, and integration tests only access local fixtures.
Validation
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 also builds, tests, and packages binaries on Linux x64/arm64, Windows x64, and macOS x64/arm64; it builds and smoke-tests the Docker image offline on Linux x64/arm64.
Security boundaries
ssl=autowarns and retries once without certificate verification only after a classified certificate-validation error. Preferssl=truein production.cmd:andshell:in remote configurations execute local commands. Load only trusted configuration URLs.regex:uses Rustregexsyntax and does not support Python look-around or backreferences. Incompatible patterns return an explicit error.- A reused custom
cachepath writes Rust data to the sibling<path>.ddns-rsfile and never overwrites the Python cache. - The Python-literal parser accepts only dictionaries, lists/tuples, strings,
numbers,
True,False, andNone; it cannot execute expressions. - Logs and errors replace registered IDs, tokens, and their percent-encoded and form-encoded forms
with the constant
***marker, without retaining credential prefixes or suffixes. Cache files contain no credentials. - DNS.COM signed request bodies are not logged, echoed current signatures are redacted from API error messages, and JSON errors omit the raw response. HTTP errors for sensitive requests retain only the status code.
- URL diagnostics hide userinfo, non-root paths, query values, and fragments to protect credentials embedded in callback or remote-configuration paths. The actual request URL is unchanged.
Parity roadmap
- Port systemd, cron, launchd, and schtasks support.
- Validate Web/MCP compatibility in long-running deployments and platform migrations.
- Consider renaming the binary to
ddnsonly after complete parity and a separate stability period.