Skip to content

dns: authenticate the resolver dial — verify chain and hostname (closes §11 gap) - #15

Merged
myleshorton merged 2 commits into
mainfrom
fisk/dns-verify-roots
Jul 29, 2026
Merged

dns: authenticate the resolver dial — verify chain and hostname (closes §11 gap)#15
myleshorton merged 2 commits into
mainfrom
fisk/dns-verify-roots

Conversation

@myleshorton

@myleshorton myleshorton commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes the design §11 gap that surfaced in review on #13: every DoH and DoT resolver dial was encrypted but not authenticated.

Stacked on #14 (base fisk/proxyless-search), because it changes an API #14 consumes. Retargets to main automatically when #14 merges.

The bug

Resolver::tls_strategy_with built on BootstrapStrategy::boring_chrome, which defaults to CertVerification::None — and flint-tls's connector only enables SslVerifyMode::PEER + set_verify_hostname in the Roots branch. So neither the chain nor the hostname was ever checked.

An on-path attacker could therefore terminate the handshake with any certificate and hand back forged answers over a perfectly encrypted channel — precisely the poisoning DoH exists to prevent. validate would catch a clumsy sentinel like Iran's 10.10.34.x, but never a plausible attacker-chosen address. flint-fronted already dialed with Roots; DNS was the outlier.

Verification is now applied explicitly rather than inherited. That inheritance was the bug, so relying on a better upstream default would leave the same trap for the next constructor.

The identity is the SNI

#13 left open "which identity to verify per addressing form". It resolves uniformly: the SNI is by definition the name whose certificate the server presents.

Entry form sni vs host Verified identity
raw-IP equal resolver hostname
CDN-edge equal resolver hostname
fronted different the front (whose cert actually arrives)

Two consequences worth calling out:

  • It works despite dialing a bare IP, because the identity checked is the hostname, not the address. That makes the "raw-IP works because the cert carries IP SANs" rationale in §6 unnecessary for our path — we never verify against the IP.
  • For a fronted entry, verifying the DoH :authority would simply fail; the front serves its own certificate. Authenticating the front and then addressing the real resolver via :authority inside the verified channel is the trust model fronting always relies on.

Verification failure hard-fails that resolver's attempt, which the pool race already treats as one candidate losing — so no separate down-ranking was needed.

Public signatures are unchanged

New DialPolicy { wire, roots } bundles shaping and trust, so trust is something a caller states rather than inherits, and the resolve signatures stay stable as knobs accumulate.

resolve / resolve_cached / resolve_one keep their shapes and gain verification for free; *_with variants take a policy for callers that need to pin anchors. That matters concretely:

  • flint-fronted resolves its fronts through resolve_cached (lib.rs:1288)
  • spark calls flint_dns::resolve directly (core/src/bootstrap/mod.rs:80)

Both get the fix with no code change. (resolve_one_shapedresolve_one_with is the one rename — one day old, single caller, unmerged.)

Mobile is why roots are pinnable

Empty roots falls back to X509_STORE_set_default_paths() — OpenSSL's compile-time paths plus SSL_CERT_FILE/SSL_CERT_DIR. Desktop has a working store; Android and iOS do not, since they keep trust roots where those paths cannot see them.

spark already solved this in core/src/ca_roots.rs, writing the bundled Mozilla set and pointing SSL_CERT_FILE at it — added for fronted TLS, for exactly this reason. So this change rides an existing mechanism rather than needing new platform work, which is the main reason it is low-risk to turn on.

I recorded the residual coupling as a new §11 item: flint cannot tell whether an embedder has done that setup, and a missing anchor set looks identical to a blocked network (unable to get local issuer certificate). Worth considering a cheap startup self-check so a misconfigured embedder fails loudly at init instead of masquerading as censorship.

flint-proxyless gets the same treatment

Its dials were already verified, but its roots were hardcoded empty — the same latent mobile problem. It now carries a DialPolicy per strategy plus deployment-wide Space::roots, so both legs of a proxyless connection are authenticated. The legs verify different identities: the resolver leg checks the resolver's SNI, the destination leg checks the destination host.

A cached entry never carries trust anchors of its own — trust comes from the current Space. A persisted (or disk-loaded) anchor set is not something a cache should get a vote on.

Verification

  • cargo fmt --all -- --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo test --workspace — clean, flint-dns 28 / flint-proxyless 13, including flint-fronted's resolver path
  • cargo doczero warnings on both crates
  • cargo check -p flint-dns -p flint-proxyless --features boring — clean; this is the configuration where verification actually executes

New tests: every TLS kind verifies with hostname == sni and empty anchors meaning the default store (never None); pinned anchors are carried through; a fronted entry verifies the front rather than the :authority; proxyless trust comes from the space, not the cache.

What this does not cover

No live assertion that each pool entry's certificate actually validates — CI has no egress, and the CDN-edge entries were "verified live" while verification was off, which proved the handshake completed and DoH answered, not that the chain was valid for cloudflare-dns.com. The pool races, so a few entries failing verification degrades rather than breaks, and an entry that was serving an invalid chain should now fail. Worth one live run against a real network before relying on it in the field.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BufGsK81otiqihvrZdfoJX

Summary by CodeRabbit

  • Security

    • Resolver TLS dials are now authenticated by verifying certificate chains and hostnames, using the expected SNI identity.
    • Fronted DoH connections verify the front endpoint’s presented identity rather than the original resolver host.
    • Resolution and cached resolution honor the same verification policy.
  • New Features

    • Added policy-aware resolution APIs for dialing, including cache-aware policy variants.
    • Added support for supplying custom trusted certificate roots per configuration space.
  • Documentation

    • Updated security design notes to reflect authenticated dialing and trust-anchor sourcing/requirements.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The resolver stack replaces wire-only shaping with DialPolicy, combining WirePlan and trusted certificate roots. DNS resolution, cached resolution, proxyless strategies, TLS verification, and documentation now use policy-aware dialing with hostname verification tied to resolver SNI.

Authenticated resolver dialing

Layer / File(s) Summary
DNS policy and resolution flow
crates/flint-dns/src/lib.rs, crates/flint-dns/src/pool.rs
Adds DialPolicy, exposes policy-aware resolution APIs, applies roots and resolver SNI during TLS verification, and adds authentication regression tests.
Proxyless trust propagation
crates/flint-proxyless/src/*.rs
Stores DialPolicy in strategies, carries trust roots from Space, and uses them for DNS resolution, probing, dialing, and cache resolution.
Resolver security model
docs/design.md
Documents authenticated resolver TLS, SNI identity validation, trust-root sourcing, and mobile trust-anchor handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Space
  participant Strategy
  participant Proxyless
  participant flint_dns
  participant TLS
  Space->>Strategy: Build DialPolicy with wire and roots
  Proxyless->>flint_dns: resolve_one_with(..., policy)
  flint_dns->>TLS: Create strategy with policy roots and resolver SNI
  TLS-->>Proxyless: Verified DNS connection
  Proxyless->>TLS: Dial using policy
  TLS-->>Proxyless: Verified resolver connection
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: authenticating resolver dials by verifying certificate chain and hostname.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fisk/dns-verify-roots

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

Pull request overview

This PR closes a security gap in flint-dns where DoH/DoT resolver dials were encrypted but not authenticated, by explicitly enabling certificate chain + hostname verification and making trust configuration an explicit input to resolution/dial paths.

Changes:

  • Introduces DialPolicy { wire, roots } to bundle handshake shaping with TLS trust anchors, and threads it through flint-dns resolution APIs.
  • Updates resolver TLS strategies to always use CertVerification::Roots (verifying the SNI as the identity), never inheriting boring_chrome’s CertVerification::None.
  • Updates flint-proxyless to carry trust roots at the Space level and ensure cached strategies never supply trust anchors.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs/design.md Updates the design doc to reflect authenticated resolver dials, SNI identity verification, and mobile root handling.
crates/flint-proxyless/src/lib.rs Switches proxyless strategies from WirePlan to DialPolicy and adds per-space trust roots.
crates/flint-proxyless/src/cache.rs Ensures cached entries only select a candidate; trust anchors always come from the current Space.
crates/flint-dns/src/pool.rs Adds DialPolicy and makes resolver TLS strategies explicitly verified with CertVerification::Roots.
crates/flint-dns/src/lib.rs Renames/shifts resolution APIs to accept DialPolicy and adds *_with variants for explicit policy control.
Comments suppressed due to low confidence (1)

docs/design.md:398

  • (spark's core/src/ca_roots.rs) looks like an in-repo path, but there is no such file here. Consider clarifying that this is a reference to the external spark embedder so the design doc remains self-contained.
- **Trust anchors on mobile.** Empty `DialPolicy::roots` falls back to OpenSSL's default paths, which
  are empty on Android/iOS unless the embedder points `SSL_CERT_FILE`/`SSL_CERT_DIR` at a bundled set
  (spark's `core/src/ca_roots.rs`). That coupling is implicit: flint cannot tell whether an embedder
  has done it, and gets an indistinguishable "unable to get local issuer certificate" either way. Open:

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/flint-dns/src/pool.rs Outdated
Comment thread docs/design.md Outdated
myleshorton added a commit that referenced this pull request Jul 29, 2026
…reference

Review catches (Copilot, PR #15), both verified and both correct.

`DialPolicy`'s docs described `roots` as verifying "the resolver's certificate",
but this same PR widened the type: flint-proxyless applies it to destination dials
too, so the resolver-specific wording was already stale when written. Reworded to
apply to whichever TLS peer the policy is used against, and noted explicitly that
the name reflects the dial it was introduced for rather than a restriction — both
legs of a proxyless connection are shaped and anchored from one value.

design.md pointed at `core/src/ca_roots.rs` twice as if it lived here. It does not
— flint has no `core/` directory at all; that file belongs to the spark embedder in
a separate repo. Both references now say so, so a reader does not go hunting for a
path this repository never contained.

Gate: fmt, clippy --workspace --all-targets -D warnings, cargo test --workspace,
and cargo doc all clean. Documentation only, no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BufGsK81otiqihvrZdfoJX
@myleshorton
myleshorton requested a review from Copilot July 29, 2026 19:28

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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

crates/flint-dns/src/lib.rs:149

  • tls_strategy_with now always builds CertVerification::Roots { hostname: resolver.sni }, and flint-tls rejects verified connections with an empty hostname (io::ErrorKind::InvalidInput). Since Resolver::doh/dot don’t prevent sni == "", this currently turns into a late failure inside dial with a less actionable error. Consider failing fast here with a clearer error when resolver.sni is empty.
fn tls_strategy(
    resolver: &Resolver,
    policy: &DialPolicy,
) -> io::Result<flint_dial::BootstrapStrategy> {
    resolver.tls_strategy_with(policy).ok_or_else(|| {
        io::Error::other(format!(
            "resolver {} has kind {:?}, which has no TLS dial strategy",
            resolver.name, resolver.kind
        ))
    })

@myleshorton
myleshorton changed the base branch from fisk/proxyless-search to main July 29, 2026 19:35
myleshorton and others added 2 commits July 29, 2026 14:36
…r inherit None

Closes the design §11 gap. `Resolver::tls_strategy_with` inherited
`CertVerification::None` from `BootstrapStrategy::boring_chrome`, so every DoH and
DoT resolver dial was encrypted but **unauthenticated**: an on-path attacker could
terminate the handshake with any certificate and hand back forged answers, which is
exactly the poisoning DoH exists to prevent. Bogon validation would catch a clumsy
sentinel like 10.10.34.x but never a plausible attacker-chosen address. flint-fronted
already dialed with `Roots`; DNS was the outlier.

Now every TLS resolver dial applies `CertVerification::Roots` explicitly — chain and
hostname — rather than inheriting a default. That inheritance *was* the bug, so the
application is deliberate rather than relying on a better default upstream.

**The identity is the SNI**, which resolves the open question uniformly instead of
needing a per-addressing-form policy. The SNI is by definition the name whose
certificate the server presents:

- raw-IP and CDN-edge entries have `sni == host`, so this is "verify the resolver
  hostname" — and it works despite dialing a bare IP because the identity checked is
  the hostname, not the address. That also makes the "raw-IP works because the cert
  carries IP SANs" rationale unnecessary for our path: we never verify against the IP.
- a fronted entry (`sni != host`) authenticates the front whose certificate actually
  arrives; verifying the DoH `:authority` there would simply fail. The real resolver
  is addressed by `:authority` inside the verified channel — the trust model fronting
  always relies on.

New `DialPolicy { wire, roots }` bundles shaping and trust, so trust is something a
caller states rather than inherits, and the resolve signatures stay stable as knobs
are added. Empty `roots` means the platform default store.

**Public signatures are unchanged.** `resolve`/`resolve_cached`/`resolve_one` keep
their shapes and gain verification for free; `*_with` variants accept a policy for
callers that need to pin anchors. That matters because flint-fronted resolves its
fronts through `resolve_cached` and spark calls `flint_dns::resolve` directly — both
get the fix without a code change. `resolve_one_shaped` becomes `resolve_one_with`
(one day old, single caller, unmerged).

Mobile is why roots are pinnable at all: empty roots fall back to
`X509_STORE_set_default_paths()`, which is empty on Android/iOS unless the embedder
points `SSL_CERT_FILE` at a bundled set. spark already does this in
`core/src/ca_roots.rs` for fronted TLS, so this change rides that same mechanism
rather than needing new platform work — recorded in §11 along with the open question
of whether flint should self-check anchors at init so a misconfigured embedder fails
loudly instead of looking like a blocked network.

flint-proxyless picks up the same treatment: its dials were already verified, but its
roots were hardcoded empty. It now carries a `DialPolicy` per strategy and
deployment-wide `Space::roots`, so both legs of a proxyless connection are
authenticated — noting the legs verify *different* identities (resolver SNI vs
destination host). A cached entry never carries trust anchors of its own: trust comes
from the current space, since a persisted (or disk-loaded) anchor set is not something
a cache should get a vote on.

Tests: verification is present on every TLS kind with hostname == SNI and empty
anchors meaning the default store; pinned anchors are carried through; a fronted entry
verifies the front rather than the `:authority`; proxyless trust comes from the space.
flint-dns 28 tests, flint-proxyless 13.

Gate: fmt, clippy --workspace --all-targets -D warnings, cargo test --workspace
(flint-fronted's resolver path included), cargo doc with zero warnings for both
crates, and cargo check --features boring — the configuration where verification
actually executes — all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BufGsK81otiqihvrZdfoJX
…reference

Review catches (Copilot, PR #15), both verified and both correct.

`DialPolicy`'s docs described `roots` as verifying "the resolver's certificate",
but this same PR widened the type: flint-proxyless applies it to destination dials
too, so the resolver-specific wording was already stale when written. Reworded to
apply to whichever TLS peer the policy is used against, and noted explicitly that
the name reflects the dial it was introduced for rather than a restriction — both
legs of a proxyless connection are shaped and anchored from one value.

design.md pointed at `core/src/ca_roots.rs` twice as if it lived here. It does not
— flint has no `core/` directory at all; that file belongs to the spark embedder in
a separate repo. Both references now say so, so a reader does not go hunting for a
path this repository never contained.

Gate: fmt, clippy --workspace --all-targets -D warnings, cargo test --workspace,
and cargo doc all clean. Documentation only, no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BufGsK81otiqihvrZdfoJX
@myleshorton
myleshorton force-pushed the fisk/dns-verify-roots branch from f28f7dd to f168b13 Compare July 29, 2026 19:36
@myleshorton

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@myleshorton
myleshorton requested a review from Copilot July 29, 2026 19:37
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 17 minutes.

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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

docs/design.md:265

  • In §6.1, the earlier “orthogonal to shaping” paragraph still references the pre-rename APIs (Resolver::strategy_with(wire) / resolve_one_shaped). Since this PR introduces DialPolicy and renames the composition seam to Resolver::tls_strategy_with(policy) / resolve_one_with, that paragraph should be updated to match so the design doc doesn’t point readers at non-existent functions.
**The resolver dial is authenticated, and the identity is the SNI.** Every TLS resolver dial uses
`CertVerification::Roots` — chain *and* hostname — never `None`. Encryption without authentication
would have bought very little: an active on-path MITM could terminate the handshake with any
certificate and return whatever answers it liked, and bogon validation catches a clumsy sentinel but
not a plausible attacker-chosen address. (`boring_chrome` still defaults to `CertVerification::None`,

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/design.md (1)

241-249: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale API names adjacent to the updated section.

This paragraph still says Resolver::strategy_with(wire) / resolve_one_shaped, but the actual current API (per pool.rs/lib.rs in this same PR) is Resolver::tls_strategy_with(policy) / resolve_one_with. It sits immediately before the newly-added §6.1 paragraphs that correctly use the current names, so the section is now internally inconsistent.

📝 Suggested fix
-inter-segment jitter). `Resolver::strategy_with(wire)` / `resolve_one_shaped` compose the two, so a
+inter-segment jitter). `Resolver::tls_strategy_with(policy)` / `resolve_one_with` compose the two, so a
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design.md` around lines 241 - 249, Update the API references in the
paragraph describing resolver and wire-shaping composition: replace
Resolver::strategy_with(wire) with Resolver::tls_strategy_with(policy) and
resolve_one_shaped with resolve_one_with. Keep the surrounding explanation of
orthogonal resolver and wire strategies unchanged.
🧹 Nitpick comments (1)
crates/flint-dns/src/pool.rs (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated BootstrapStrategy + CertVerification::Roots construction across crates. Both sites hand-build the same "shape with policy.wire, verify with CertVerification::Roots sourced from policy.roots" pattern, differing only in which identity string is verified. A shared DialPolicy constructor would centralize this security-critical pattern instead of keeping it duplicated in two crates.

  • crates/flint-dns/src/pool.rs#L183-195: extract a DialPolicy::strategy(&self, target: SocketAddr, identity: impl Into<String>) -> BootstrapStrategy helper that builds boring_chrome(...).with_wire(self.wire.clone()).with_verification(CertVerification::Roots{ roots_pem: self.roots.clone(), hostname }), and have tls_strategy_with call it with self.sni.clone().
  • crates/flint-proxyless/src/lib.rs#L379-386: replace verified()'s body with a call to the same DialPolicy::strategy(target, hostname) helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flint-dns/src/pool.rs` at line 1, Add a shared DialPolicy::strategy
method that accepts target and identity, constructs BootstrapStrategy using
boring_chrome, self.wire, and CertVerification::Roots from self.roots, then
update tls_strategy_with to call it with self.sni.clone(). Replace
flint-proxyless verified() construction with DialPolicy::strategy(target,
hostname), preserving each caller’s identity value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/design.md`:
- Around line 241-249: Update the API references in the paragraph describing
resolver and wire-shaping composition: replace Resolver::strategy_with(wire)
with Resolver::tls_strategy_with(policy) and resolve_one_shaped with
resolve_one_with. Keep the surrounding explanation of orthogonal resolver and
wire strategies unchanged.

---

Nitpick comments:
In `@crates/flint-dns/src/pool.rs`:
- Line 1: Add a shared DialPolicy::strategy method that accepts target and
identity, constructs BootstrapStrategy using boring_chrome, self.wire, and
CertVerification::Roots from self.roots, then update tls_strategy_with to call
it with self.sni.clone(). Replace flint-proxyless verified() construction with
DialPolicy::strategy(target, hostname), preserving each caller’s identity value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5004b5a4-9fd7-4e80-b198-5d252d864f1a

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc74b1 and f168b13.

📒 Files selected for processing (5)
  • crates/flint-dns/src/lib.rs
  • crates/flint-dns/src/pool.rs
  • crates/flint-proxyless/src/cache.rs
  • crates/flint-proxyless/src/lib.rs
  • docs/design.md

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/design.md (1)

241-249: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale API references left over from this PR's refactor.

This paragraph (in the same §6.1 subsection updated at lines 261-285) still says Resolver::strategy_with(wire) / resolve_one_shaped compose the DNS and shaping axes. resolve_one_shaped was removed by this PR's refactor of crates/flint-dns/src/lib.rs, and the actual method is Resolver::tls_strategy_with (via DialPolicy), not strategy_with. Worth fixing alongside the rest of this section's update.

📝 Proposed fix
 It is deliberately **independent of the wire-shaping axis** (§5). A resolver contributes addressing;
 a `WirePlan` contributes opening-handshake shape (record fragmentation, segment splitting,
-inter-segment jitter). `Resolver::strategy_with(wire)` / `resolve_one_shaped` compose the two, so a
+inter-segment jitter). `Resolver::tls_strategy_with(policy)` / `resolve_one_with` compose the two, so a
 DoH lookup can itself be carried over a fragmented, jittered ClientHello — the same shaping
 vocabulary used for a destination dial, aimed at the DNS dial. Keeping them orthogonal is what makes
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design.md` around lines 241 - 249, Update the §6.1 paragraph to remove
the stale `resolve_one_shaped` and `Resolver::strategy_with(wire)` references,
replacing them with the current `Resolver::tls_strategy_with`/`DialPolicy`
composition API. Preserve the description of orthogonal resolver and wire
shaping axes and the existing limitations for shapeable TLS kinds, plaintext
DNS, and the system resolver.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/design.md`:
- Around line 241-249: Update the §6.1 paragraph to remove the stale
`resolve_one_shaped` and `Resolver::strategy_with(wire)` references, replacing
them with the current `Resolver::tls_strategy_with`/`DialPolicy` composition
API. Preserve the description of orthogonal resolver and wire shaping axes and
the existing limitations for shapeable TLS kinds, plaintext DNS, and the system
resolver.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 211427cf-3372-4458-82dc-f8836231fe70

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc74b1 and f168b13.

📒 Files selected for processing (5)
  • crates/flint-dns/src/lib.rs
  • crates/flint-dns/src/pool.rs
  • crates/flint-proxyless/src/cache.rs
  • crates/flint-proxyless/src/lib.rs
  • docs/design.md

@myleshorton
myleshorton merged commit 18bd85b into main Jul 29, 2026
3 checks passed
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