Skip to content

feat: add count-based upstream verification for cached records#691

Open
iCasture wants to merge 1 commit into
NewFuture:masterfrom
iCasture:cache-upstream-refresh-control
Open

feat: add count-based upstream verification for cached records#691
iCasture wants to merge 1 commit into
NewFuture:masterfrom
iCasture:cache-upstream-refresh-control

Conversation

@iCasture

Copy link
Copy Markdown

When cache is enabled, DDNS currently skips upstream checks as long as the resolved local IP has not changed. This means external changes to the upstream DNS record may remain unsynchronized.

This change adds cache_verify_every to force one upstream verification after N consecutive cache hits for the same provider and record. The default remains 0, so existing behavior is unchanged unless the option is configured.

The verification counters are persisted in the existing cache file under cache_meta.cache_verify_counts, and the option is supported in CLI, JSON config, environment variables, and v4.1 multi-provider configs.

Tests and related docs were updated accordingly.

Add `cache_verify_every` to trigger upstream verification after
consecutive cache hits for the same provider and record.

Persist verification counters in `cache_meta.cache_verify_counts`,
wire the option through CLI, config loading, schema, and docs,
and add coverage for the new behavior.
@NewFuture

Copy link
Copy Markdown
Owner

How about use some config like cacheTTL (time-based expiration), and managed status in the Cache module?

@iCasture

Copy link
Copy Markdown
Author

Thanks, that makes sense. I’ll spend some time on this soon and rework the PR.

However, I also noticed another cache-related issue that should probably be fixed as part of the same change.

When cache is simply enabled with cache: true, the default cache file name is derived from the config hash, so different provider configs usually end up using different cache files. In that case, this problem is less likely to show up.

However, when the cache file path is explicitly configured, for example:

"cache": "/tmp/example.cache"

then with the v4.1 multi-provider config format, multiple provider entries may write into the same cache file. The current cache key is only based on domain:record_type, so two records with the same domain and record type, but representing different upstream DNS records, can overwrite each other.

For example:

{
  "$schema": "https://ddns.newfuture.cc/schema/v4.1.json",
  "ssl": true,
  "cache": "/tmp/example.cache",
  "providers": [
    {
      "provider": "alidns",
      "id": "ACCESSKEY_ID",
      "token": "ACCESSKEY_SECRET",
      "ipv4": ["example.com"],
      "index4": ["regex:200\\.10\\..*"],
      "line": "telecom"
    },
    {
      "provider": "alidns",
      "id": "ACCESSKEY_ID",
      "token": "ACCESSKEY_SECRET",
      "ipv4": ["example.com"],
      "index4": ["regex:200\\.20\\..*"],
      "line": "unicom"
    }
  ]
}

In AliDNS, these are actually two different A records: one for the telecom line pointing to 200.10.x.x, and one for the unicom line pointing to 200.20.x.x.

But the current cache file may end up like this:

{
  "example.com:A": "200.20.1.1"
}

So the telecom record and the unicom record are sharing the same cache key and one can overwrite the other. The cache should really be able to store both records separately.

There is a similar issue for TTL as well. If only the TTL in the config is changed, the old cached value may keep the record from being updated until the cache expires after 72 hours, or until the cache file is manually removed.

So I think the current cache key is missing some identity information. For cache_ttl, we also need to store the last successful upstream verification time, and that state cannot safely be keyed only by domain:record_type either.

I see two possible ways to handle this:

  1. Introduce a new cache file format, for example:

    {
        "version": 2,
        "records": {
            "sha256:aabb...": {
                "identity": {
                    "provider": "alidns",
                    "endpoint": null,
                    "auth_scope": "sha256:ab12...",
                    "domain": "example.com",
                    "record_type": "A",
                    "line": "telecom"
                },
                "verified_state": {
                    "address": "200.10.1.1",
                    "ttl": null,
                    "proxied": null,  // For Cloudflare
                    "extra": {}
                },
                "updated_at": 1770000000.123,
                "verified_at": 1770000000.123
            }
        }
    }

    Here the record key (e.g. sha256:aabb...) is derived from a canonicalized identity. The identity field itself is mainly kept for readability and debugging.

  2. Keep the legacy domain:record_type entries for backward compatibility, and introduce a new cache_meta.records section keyed by the identity hash.

    The Cache module would first look up cache_meta.records[identity_hash] and compare its verified_state with the expected state derived from the current config. If no matching entry exists, it can fall back to the legacy domain:record_type value. After a successful verification or update, the new-style entry would be written back.

Which approach do you prefer, or do you have a better idea?

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.

2 participants