Skip to content

Auto-update anyone_hosts from the .anyone DNS service (v2)#147

Open
jim-toth wants to merge 1 commit into
mainfrom
anyone-dns-autoupdate-v2
Open

Auto-update anyone_hosts from the .anyone DNS service (v2)#147
jim-toth wants to merge 1 commit into
mainfrom
anyone-dns-autoupdate-v2

Conversation

@jim-toth

@jim-toth jim-toth commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Automatic updating of the anyone_hosts DNS mapping file, reworked from #145 per its production-readiness review. The client fetches a fresh mapping from the .anyone DNS service nodes when new directory information arrives and on a periodic (jittered) schedule. Fetches are onion-routed to the DNS service's hidden service by .anyone name.

What changed vs. #145

Transport actually works now. v1 handed the .anyone name to connection_ap_make_link(), which attaches linked streams directly as general exit streams — the name was never resolved, so every fetch failed at the exit with RESOLVEFAILED. v2 adds connection_ap_make_link_onion() (connection_edge.c), which routes the linked stream through connection_ap_handshake_rewrite_and_attach() — the same path SOCKS client streams take — so the .anyone name is resolved via the mapping and attached to a hidden-service circuit, with entry_cfg.onion_traffic enabled for the internal fetch.

Response validation. A fetched file is installed only if it:

  • is non-empty and within DNSMappingFileMaxSize,
  • carries a valid signature from a trusted DNS signer — there is deliberately no configuration to weaken this (v1's verify/any modes are gone),
  • is unexpired per its valid-until (now actually enforced; previously parsed and discarded),
  • contains at least one mapping line (a signed-but-empty document can't wipe the installed file),
  • is not older by published time than the installed mapping (anti-rollback for replayed signed files).

Byte-identical refetches are accepted without rewriting the file. Validation/installation policy lives in the anyone module (anyone_hosts_handle_fetch_response); dirclient keeps only a thin dispatch.

Scope trimmed to 3 options (v1 had 6):

  • AnyoneHostsUpdate (0|1, default 1)
  • AnyoneHostsUpdateInterval (default 12 hours, floor 1 hour, ±10% jitter so fleets don't fetch in lockstep)
  • AnyoneHostsURL (repeatable override, tried before built-in defaults)

Dropped: AnyoneHostsUpdateTrigger (both triggers hardcoded — uniform client behavior over knobs), AnyoneHostsFetchPath (hardcoded /tld/anyone, deletes the header-smuggling validation), AnyoneHostsSignatureRequirement (strict always).

Review findings addressed:

  • Target rotation advances only on failure; a working server keeps serving future updates (v1 rotated off the good server after every success). Relevant since only DNS 1 is live today; DNS 2/3 defaults are placeholders.
  • Interval floor + jitter (v1 accepted a 1-second interval and fetched in lockstep).
  • The saved-file URL mining is gone — targets come from config + DEFAULT_ANON_DNS_MAPPING only, so fetched content can't steer future fetch targets.
  • Clients-only gate enforced once, inside the module (v1 had it in 3 places).
  • Dead _free_all() removed; .anyone suffix checks use strcmpend(); approx_time() used consistently.
  • The 600s stuck-fetch safety net is kept — verified necessary because directory_initiate_request() returns void and can fail before any connection exists.

Parser: anyone_hosts_parse_and_verify_ex() now exposes published/valid-until (mockable for tests); the old signature remains as a wrapper.

Testing

  • 12 unit tests: scheduling/backoff/rotation (mocked directory_initiate_request), address extraction, and response validation incl. anti-rollback and size cap (mocked signature verifier driven by body markers).
  • Full unit suite passes in the dev container.

Manual testing needed before merge (the untestable-in-unit part)

The routing path is exactly what unit tests cannot cover — this is where v1 silently failed:

  • With AnyoneHostsUpdate 1 on a live/stage network, verify a fetch launches and completes: anyone_hosts mtime/content updates from the live DNS node (DNS 1).
  • Verify the fetch rides an HS circuit (log Anyone dns address mapping found for the DNS node name, followed by descriptor fetch / rend circuit activity, no RESOLVEFAILED at an exit).
  • Verify fallback: point AnyoneHostsURL at a dead placeholder (DNS 2/3), confirm the next attempt (after the 1h retry gap) moves to the next target and eventually succeeds via DNS 1.
  • Verify a served unsigned/invalid-signature file is rejected and the installed file is untouched.
  • AnyoneHostsUpdate 0: no fetches scheduled or launched.
  • Config validation: interval below 1 hour and malformed AnyoneHostsURL entries are rejected with clear errors.

Reworked from PR #145 per its production-readiness review.

The client fetches a fresh anyone_hosts mapping when new directory
information arrives and on a periodic (jittered) schedule.  Fetches are
routed by .anyone name over hidden-service circuits: linked directory
streams for this purpose now go through the same rewrite/attach path as
SOCKS client streams (connection_ap_make_link_onion), which is where
.anyone names are resolved -- the v1 approach attached them as general
exit streams and could never complete.

Fetched files are installed only if they are non-empty, within
DNSMappingFileMaxSize, carry a valid signature from a trusted DNS signer
(not configurable), are unexpired per valid-until, contain at least one
mapping, and are not older (by published time) than the installed
mapping.  Byte-identical refetches skip the rewrite.

Configuration is deliberately minimal: AnyoneHostsUpdate,
AnyoneHostsUpdateInterval (floored at 1 hour, jittered +/-10%), and
AnyoneHostsURL.  Target selection tries configured URLs then built-in
defaults, advancing only past failing servers.
@jim-toth jim-toth marked this pull request as ready for review July 7, 2026 23:58
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.

1 participant