Skip to content

Delegation checker and automatic limit_domains management - #1235

Open
peterthomassen wants to merge 15 commits into
mainfrom
20260816_delegation_checks
Open

Delegation checker and automatic limit_domains management#1235
peterthomassen wants to merge 15 commits into
mainfrom
20260816_delegation_checks

Conversation

@peterthomassen

@peterthomassen peterthomassen commented Aug 16, 2026

Copy link
Copy Markdown
Member

Different approach than #1174

Testing using production database on local machine takes about 79 minutes with 4 threads, 47 minutes with 8 threads, 44 minutes with 16 threads, and 39 minute with 64 threads. 8 threads is probably best: the more threads, the more resolution errors (maybe rate limits at TLD auths or something).

@peterthomassen
peterthomassen force-pushed the 20260816_delegation_checks branch from adabf3d to 245e4e0 Compare August 17, 2026 01:50
@peterthomassen peterthomassen changed the title Delegation checker Delegation checker and automatic limit_domains management Aug 17, 2026
@peterthomassen

peterthomassen commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Extended PR with automatic domain limit management. Effective domain limit is number of domains securely delegated to us (s, includes .dedyn.io domains), plus the square root of external domains securely delegated to us (e, without .dedyn.io domains; but: at least 1). This way, the headroom for creating more domains without immediately securing them (DS record) grows sensibly:

limit = s + max(1, h)    with  h = round(√e)

For users without .dedyn.io domains, this is equivalent to (e + sqrt(e)); .dedyn.io domains are added on top (effectively not influencing the limit besides offsetting it).

Illustrations:

Account without dedyn.io domain

All secure domains s are external.

Secure domains s Headroom h = round(√s) Effective limit
0 0 1
1 1 2
2 1 3
3 2 5
4 2 6
5 2 7
7 3 10
10 3 13
12 3 15
15 4 19
20 4 24
25 5 30
30 5 35
40 6 46
50 7 57
75 9 84
100 10 110

Account with dedyn.io domain

Secure domains s include the .dedyn.io domain. The limit is effectively raised by 1 for the first few domains. From ~10 domains on, the situation is the same as above.

Secure domains s Headroom h = round(√(s−1)) Effective limit
1 0 2
2 1 3
3 1 4
4 2 6
5 2 7
7 2 9
10 3 13
12 3 15
15 4 19
20 4 24
25 5 30
30 5 35
40 6 46
50 7 57
75 9 84
100 10 110

@peterthomassen
peterthomassen marked this pull request as ready for review August 17, 2026 01:57
@peterthomassen
peterthomassen force-pushed the 20260816_delegation_checks branch 5 times, most recently from 6560072 to 889ad0e Compare August 18, 2026 01:16
@peterthomassen

Copy link
Copy Markdown
Member Author

I've performed a detailed/thorough review, and applied various improvements / rearranged commits. I did not find any problematic issues.

The first two commits initially might seem unrelated, but they are needed so that the later limit calculation logic works reliably.

Perhaps we should drop the commits containing the plans.

@nils-wisiol ready for final review

@nils-wisiol

nils-wisiol commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

PR 1235 Review: Delegation Checks and Automatic Domain Limits

Compared: origin/main...origin/pr/1235

Checklist

  • High: Fix delegation false positives for nested/co-hosted zones. api/desecapi/delegation.py:214, api/desecapi/delegation.py:326

    The checker queries parent nameserver addresses directly, but accepts NS RRsets
    from both answer and authority sections. If the same authoritative server hosts
    both parent and child zones, an authoritative child-zone answer can be mistaken
    for parent-side delegation. For quota purposes, only parent referral data should
    count. Add a co-hosted parent/child regression test.

  • High: Reserve execution capacity for ad-hoc delegation checks. docker-compose.yml:303, api/desecapi/tasks.py:27, api/desecapi/dns.py:154, api/desecapi/unbound.py

    One celery-delegation worker consumes both delegation_adhoc and
    delegation_bulk with concurrency 4 and --prefetch-multiplier 1. That avoids
    deep prefetching, but does not bound user wait time when all slots are occupied
    by bulk checks. Run separate worker pools for delegation_adhoc and
    delegation_bulk; AMQP priority can be an extra ordering hint, but not the
    latency guarantee.

    Capacity note: with about 50k non-local-public-suffix domains, cron checks
    roughly 50,000 / 11 = 4,545 domains every 2h. With four workers, the system
    needs to average better than about 6.3s per domain to avoid falling behind.

  • High: Prevent duplicate bulk-check backlog. api/cronhook/crontab:4, api/desecapi/management/commands/check-delegation.py:124, api/desecapi/tasks.py:116

    Cron enqueues stale checks every 2h:

    23 */2 * * * manage.py check-delegation --stale 79200 --concurrency 0
    

    Workers re-check staleness before DNS work, but if the queue falls behind,
    later cron runs can enqueue the same still-stale domains again. Celery does not
    provide enqueue-time uniqueness for "one pending check per domain". Add an
    app-level pending marker, e.g. delegation_check_pending or a pending task id,
    set it atomically when enqueueing, skip already-pending domains, and clear it
    after the task's freshness re-check completes. A Redis/RabbitMQ lock keyed by
    domain id and queue type would also work, with a TTL longer than worst-case task
    runtime.

  • Medium: Expose per-domain delegation status through the API. api/desecapi/models/delegation.py:71, api/desecapi/serializers/users.py:32, api/desecapi/serializers/domains.py:22

    The PR stores per-domain security status, nameserver status, nameserver list,
    EDE, rcode, and timestamps internally, but public responses only expose
    aggregate secure_domains and effective limit_domains on the user object.
    Users need to know which domain is blocking quota, whether a re-check is
    pending, what failed, when it last ran, and what to fix.

  • Medium: Align docs with the self-service limit flow. docs/auth/account.rst:270, docs/dns/domains.rst:162, api/desecapi/permissions.py:169

    The account docs mention limits growing with secure delegation, but do not
    document the formula, freshness, status meanings, expected wait time, or how to
    inspect/fix specific domains. The domain docs still primarily say to contact
    support when hitting the limit, while the API error says the system is
    re-checking delegation.

@nils-wisiol

nils-wisiol commented Sep 1, 2026

Copy link
Copy Markdown
Contributor
  • does ad hoc queue have precedence over bulk items so that users do not get blocked behind bulk workload?
  • can we increase concurrency on the workers (for ~free) to increase delegation checker workload headroom? AI memory estimate: delegation worker PSS ~= 95 MiB + 12.5-13 MiB per concurrency slot
  • close on LPS domain limit - implement a solution to increase it per account?
    • PR status quo: hardcoded LPS limit of 1 plus total domain limit as set by the per-account limit
    • make it backwards compatible by disabling new logic when per-account limit is not null
    • plan to introduce separate LPS and regular domain limits later

DomainSerializer rejects a name below one of our local public suffixes that
is not an immediate child of it, such as sub.example.dedyn.io, with the
name_too_deep code. Signup reports that message as it stands instead of
replacing it with the generic name_unavailable one.
Creating a second domain under one of our own public suffixes is now
rejected, unless support has pinned the account's domain limit; accounts
that already hold several keep them. DomainQuerySet gains
under_local_public_suffix(), built on a new under_local_public_suffix_q()
that matches at any depth below one of our own public suffixes.
Describes how a hosted zone is checked for being correctly delegated to
deSEC and correctly secured, and how the outcomes are recorded: what is
measured and from where, where the code lives, what is stored, and what
is deliberately deferred.
New compose service running a validating recursive resolver, built from
source for its flush_delegation command. It sits alone with api on the new
rearapi_unbound network, serving DNS on port 53 and remote control on 8953.
desecapi.unbound speaks unbound's remote control protocol: flush_delegation()
drops a name's delegation from the resolver's cache, and query() sends it a
recursive, DNSSEC-enabled query. Settings gain UNBOUND_HOST, UNBOUND_PORT and
UNBOUND_CONTROL_PORT.
New DelegationCheck model holding the outcome of one check: the two status
dimensions, the nameservers seen, and the rcode and EDE of the response that
settled the security status. DelegationCheck.objects.record() bumps the
confirmation time when an outcome agrees with the domain's current one, and
inserts a new row otherwise. Domain gains current_delegation_check, pointing
at the newest one.
desecapi.delegation.check() returns the nameservers a name is delegated to
and whether that delegation is secured, as a DelegationCheckResult that
touches no database. It flushes the delegation from the resolver's cache,
locates the parent zone, reads the delegation from the parent's nameservers,
and takes the security status from our resolver's AD bit. desecapi.dns gains
query_server() for the non-recursive queries to authoritative servers.
Checks the named domains, all of them (--all), or those not checked within a
given number of seconds (--stale), records the results, and prints one line
per domain. --include-local extends bulk selection to locally registrable
domains, which are otherwise skipped; --dry-run prints without recording;
--concurrency sizes the thread pool.
Describes how the domain limit becomes a function of the domains a user has
securely delegated to us, how checks are scheduled and triggered, which
decisions were taken along the way, and what is deferred.
A null User.limit_domains -- now the default -- makes the enforced limit
s + max(DOMAIN_LIMIT_INSECURE_HEADROOM, round(sqrt(e))), with s the user's
securely delegated domains and e the externally delegated ones; an explicit
value still pins it. LIMIT_USER_DOMAIN_COUNT_DEFAULT is renamed to
DOMAIN_LIMIT_INSECURE_HEADROOM, its environment variable unchanged, and now
floors the headroom rather than the limit. Domain.secure_delegation_since,
maintained by DelegationCheck.objects.record(), is what those counts read. The
account endpoint keeps reporting limit_domains as the enforced number and gains
secure_domains. Migration 0048 adds the field, backfills it from the checks
already recorded, and makes limit_domains nullable by default.
Support can pin an account's domain limit by setting a number; there was no
way back. "auto" writes null, which is what the computed limit is spelled as,
and the command reports the value that results rather than the null it wrote.
New celery-delegation service, a second instance of the api image consuming
the delegation_bulk and delegation_adhoc queues at one domain per task.
desecapi.tasks holds check_domain_delegation, which re-checks staleness before
measuring and leaves the domain for later when the resolver is unavailable,
and plan_user_delegation_checks, which puts a user's not-yet-secure domains on
the ad-hoc queue, capped at 20 per plan and one plan per user per minute.
check-delegation with --concurrency 0 enqueues one task per selected domain
on the bulk queue instead of checking them inline, reusing the same selection
and passing --stale on so the workers can re-check it; --dry-run reports what
would be enqueued. Negative values are rejected.
A cron entry in the api container runs check-delegation --stale 79200
--concurrency 0 every two hours, which gets each domain re-checked about once
a day.
Creating a domain, and being denied one by the domain limit, both enqueue
checks for the user's domains that are not yet known to be secure. Bounded to
one plan per user per minute and 20 domains per plan; neither trigger raises.
The limit message now says that a re-check is under way.
@peterthomassen
peterthomassen force-pushed the 20260816_delegation_checks branch from 889ad0e to 0c9716f Compare September 1, 2026 11:20
@peterthomassen

Copy link
Copy Markdown
Member Author
  • does ad hoc queue have precedence over bulk items so that users do not get blocked behind bulk workload?

It does not take precedence, but workers consume both queues round-robin, and bulk tasks are enqueued per domain. Thus, when an ad-hoc check task is added, a worker will pick it up quickly, then go back to pending bulk tasks.

Effectively, adhoc queue gets half the slots as long as it is not empty. Nothing to be gained by separating workers.

Hard cut-off for a check is 30s, expected time for any slot to free up is < 2s.

  • can we increase concurrency on the workers (for ~free) to increase delegation checker workload headroom? AI memory estimate: delegation worker PSS ~= 95 MiB + 12.5-13 MiB per concurrency slot

Increased from 4 to 8. Potential constraints are TLD nameserver rate limits (probably far away), queuing within unbound (we give it $(nproc) workers, that's currently 8), and memory impact. We are not in a rush, so I think 8 is fine.

  • close on LPS domain limit - implement a solution to increase it per account?

    • PR status quo: hardcoded LPS limit of 1 plus total domain limit as set by the per-account limit
    • make it backwards compatible by disabling new logic when per-account limit is not null
    • plan to introduce separate LPS and regular domain limits later

Done.

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