Skip to content

Reliability fixes: SIGTERM, failed-write reporting, fail-closed config (#302, #292) - #303

Open
timothymiller wants to merge 9 commits into
masterfrom
fix/reliability-2.2.1
Open

timothymiller wants to merge 9 commits into
masterfrom
fix/reliability-2.2.1

Conversation

@timothymiller

Copy link
Copy Markdown
Owner

Reliability fixes for the next patch release. Fixes #302 and #292 (startup diagnostics); #294 is a docs fix in the companion docs PR.

Behavior changes

  • SIGTERM handled. docker stop / kubectl delete pod / systemctl stop now run DELETE_ON_STOP and send the exit heartbeat instead of being SIGKILLed after the grace period. Sleeps are interruptible.
  • Failed Cloudflare writes are failures. set_ips and the legacy client return/report failure when a create, update or delete is rejected; previously monitors stayed green while DNS went stale. A failed record listing no longer looks like "no records" (which could create duplicates).
  • Fail closed on bad config. Invalid PROXIED, MANAGED_RECORDS_COMMENT_REGEX, MANAGED_WAF_LIST_ITEMS_COMMENT_REGEX, WAF_LISTS, TTL, DETECTION_TIMEOUT, UPDATE_TIMEOUT are startup errors. An invalid managed-records regex used to mean "manage every record"; an invalid PROXIED silently disabled proxying.
  • Minimum interval. @every below 30s is clamped; legacy ttl: 1 + --repeat polls every 300s instead of every second.
  • Exit code 1 for failed single-shot runs.
  • Uptime Kuma Heartsbeats Not Being Sent #302: the query string Uptime Kuma's UI includes in push URLs is stripped.
  • API GET failed: Invalid request headers #292: API tokens are trimmed, surrounding quotes stripped (with a warning), and malformed tokens rejected at startup with a message that never prints the token. Legacy entries without credentials are a startup error.
  • Secrets redacted in notification/heartbeat failure logs; failures now include the HTTP status. Auth's Debug is redacted.
  • WAF lists are created when missing (using WAF_LIST_DESCRIPTION, previously unused) and item listing follows cursor pagination.
  • Legacy purgeUnknownRecords on definitive IP absence deletes only the configured subdomains' records — it previously deleted every A/AAAA record in the zone.
  • Telegram messages are sent as plain text (Markdown mode rejected _/*).

Other fixes

  • Record listings filter by name server-side (zones with >100 records could miss the target).
  • Zone IDs cached per domain.
  • cloudflare.doh, ipify, url: providers pinned to the requested address family; more non-global ranges rejected.
  • Removed TestDdnsClient (duplicate of the legacy client used only by its own tests), fixed racy env-var tests, cargo build --locked in the Dockerfile, clippy fixes.

Testing

cargo test: 406 passing (regression tests added for each fix above). cargo clippy --all-targets warnings reduced to style lints (too-many-arguments, TTL naming, complex types).

Notes for review

  • tokio gains the sync feature (watch channel for shutdown).
  • Users relying on the old fail-open behavior (invalid regex/PROXIED) will now see the container exit at startup with an explicit error.

- Treat SIGTERM like SIGINT so docker/kubectl/systemd stops run DELETE_ON_STOP
  and the exit heartbeat; make all waits interruptible.
- Exit with status 1 when a single-shot run fails.
- Reject invalid PROXIED, managed-comment regexes, WAF_LISTS entries, TTL and
  timeouts at startup instead of silently falling back (an invalid regex used
  to mean "manage every record").
- Clamp UPDATE_CRON @every to a 30s minimum; legacy TTL auto no longer polls
  every second.
- Strip quotes from API tokens and reject malformed ones with a clear message
  (#292); legacy entries without credentials are a startup error.
- Substitute longest CF_DDNS_* names first and warn on unset references.
- Strip the query string Uptime Kuma's UI includes in push URLs so pings
  are no longer malformed (#302).
- Log heartbeat and notification failures with the HTTP status or transport
  error; URLs are redacted to their scheme since they embed tokens.
- Respect QUIET/EMOJI for notifier and heartbeat warnings.
- Send Telegram messages as plain text; Markdown parse mode rejected
  messages containing '_' or '*'.
- Redact credentials in Auth's Debug output.
- set_ips returns Failed when a create, update or delete is rejected, so
  heartbeats and notifications no longer report success with stale DNS.
- A failed record listing is a failure instead of an empty list, which used
  to trigger duplicate creates.
- Filter DNS record listings by name server-side; zones with more than 100
  records could miss the target.
- Cache zone IDs per domain instead of looking them up every cycle.
- Follow cursor pagination for WAF list items; route item deletion through
  the shared API helper so errors are logged.
- Create a missing WAF list using WAF_LIST_DESCRIPTION.
- DELETE_ON_STOP reports per-domain failures and the exit heartbeat reflects
  them (previously two exit heartbeats were sent).
- Check create/update/delete results and report failures in heartbeats,
  notifications and the exit code (update_legacy always returned success).
- Look up records by name server-side and match case-insensitively.
- A failed listing no longer looks like a missing record.
- When a deterministic provider reports no address and purgeUnknownRecords is
  on, delete only the configured subdomains' records instead of every A/AAAA
  record in the zone.
- Treat a transient detection failure as an unsuccessful cycle.
Only cloudflare.trace used a family-bound client; the others could connect
over the wrong family on dual-stack hosts and fail every cycle. Also reject
multicast, reserved, benchmarking, 0.0.0.0/8 and IPv4-mapped addresses.
…r build

- Delete TestDdnsClient from main.rs: a third copy of the legacy update logic
  that only tested itself; LegacyDdnsClient is covered in updater.rs.
- Add tests for the interruptible shutdown sleep.
- Serialize config tests that write shared env vars (intermittent failures).
- Build the image with --locked.
API errors now name the failed operation instead of printing the request URL,
which contains zone and account IDs; transport errors drop the URL too. WAF
lists are described by name only. Addresses CodeQL cleartext-logging alerts.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several failure paths can still report success, create WAF lists after lookup errors, accept invalid configuration, or expose notification credentials.

7 open findings
What changed in this PR

Improves shutdown handling, configuration validation, Cloudflare update reliability, and monitoring diagnostics.

Changes:

  • Adds graceful SIGTERM shutdown and interruptible scheduling.
  • Fails closed on invalid configuration and reports Cloudflare write failures.
  • Improves WAF handling, IP detection, heartbeat reporting, and secret redaction.
File Description
Cargo.toml Enables Tokio watch channels.
Dockerfile Enforces locked release builds.
src/​cloudflare.rs Adds caching, WAF creation/pagination, and failure propagation.
src/​config.rs Strengthens validation and token normalization.
src/​domain.rs Simplifies domain matching.
src/​main.rs Adds graceful shutdown and exit-status handling.
src/​notifier.rs Improves heartbeat errors, redaction, and Telegram delivery.
src/​pp.rs Makes output configuration cloneable.
src/​provider.rs Pins address families and rejects additional non-global ranges.
src/​updater.rs Improves legacy updates, deletion safety, and failure reporting.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/notifier.rs
Comment on lines +90 to +94
pub fn redact_url(url: &str) -> String {
match url.split_once("://") {
Some((scheme, _)) => format!("{scheme}://***"),
None => "***".to_string(),
}
Comment thread src/cloudflare.rs
) -> SetResult {
let list_meta = match self.find_waf_list(waf_list, ppfmt).await {
Some(meta) => meta,
let (list_meta, existing_items) = match self.find_waf_list(waf_list, ppfmt).await {
Comment thread src/cloudflare.rs
Comment on lines 876 to +878
let list_meta = match self.find_waf_list(waf_list, ppfmt).await {
Some(meta) => meta,
None => return,
None => return true,
Comment thread src/config.rs
Comment on lines +492 to +500
let has_token = !auth.api_token.trim().is_empty() && auth.api_token != "api_token_here";
if has_token {
auth.api_token =
normalize_token(&auth.api_token, &format!("cloudflare[{i}].authentication.api_token"), &ppfmt)?;
} else if auth.api_key.is_none() {
return Err(format!(
"config.json: cloudflare[{i}] has no api_token or api_key credentials"
));
}
Comment thread src/config.rs
Comment on lines +623 to +628
let ttl_val = match getenv("TTL") {
Some(s) => s
.parse::<i64>()
.map_err(|_| format!("Invalid TTL: '{s}'. Use 1 (auto) or a number of seconds."))?,
None => 1,
};
Comment thread src/updater.rs
Comment on lines +385 to 387
ok &= ddns
.delete_entries(ip_type.record_type(), &legacy.cloudflare)
.await;
Comment thread src/updater.rs
let _: Option<serde_json::Value> = self
.cf_api(&del_endpoint, "DELETE", entry, None::<&()>.as_ref())
.await;
ok &= self.delete_record(entry, dup_id).await;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Uptime Kuma Heartsbeats Not Being Sent

2 participants