Repository navigation
Feat/split transport - #2
Merged
Merged
Conversation
A queued request started from _release ran inside the finishing lane's zone, so each new lane's zone was parented on its predecessor. The predecessor's error handler captures its lane, keeping every retired HttpClient and ID bitmap reachable under sustained queued load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 8484 §5.1 requires DoH clients to reduce DNS TTLs by the response's HTTP Age. IoDohTransport now parses Age per RFC 9111 §5.1: the first member of a list-valued or repeated header, delta-seconds only, leading zeros allowed, saturating at 2^31. It is carried on DohHttpResponse.age, and DohResolver subtracts it from JSON answer TTLs, floored at zero, keeping it on DnsResponse.age. RFC 2181 §8 treats a received TTL with the most significant bit set as zero. JSON answers and issued local evidence previously kept values up to 0xffffffff as positive lifetimes. Wire bodies are unchanged; callers must apply the age themselves. BREAKING CHANGE: TTLs of 2^31 or more are now reported as 0, and DohHttpResponse and DnsResponse gain an age field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JSON permits unpaired surrogate escapes, and utf8.encode replaced them with U+FFFD, so the decoded octets differed from what the resolver sent. Reject them as malformed instead. DnsTxtRecord also accepted any int, so caller-built evidence passed to DnsValidationIssuer.issueTxt could carry values outside 0-255. Validate in the constructor so every record holds real TXT octets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DnsException was one class carrying a DnsError enum code, plus a responseCode that only meant something for two codes. It is now a sealed base class with a final subtype per failure, so callers can catch one case with an on-clause or switch exhaustively. CNAME failures share a sealed DnsAliasException parent. DnsQueryFailedException carries the DNS rcode and DnsTransportException an optional httpStatus. The unused noRecords code is gone; an authenticated empty answer still succeeds with no records. Errors also reached callers two ways: invalid names threw from startTxtLookup and invalid wire queries or URIs threw ArgumentError, while a closed transport or full queue failed the returned future. Starting an operation now never throws; all of these complete DnsOperation.result. The new DnsInvalidQueryException covers unusable wire queries and request URIs, including from validateDnsWireResponse and padDnsWireQuery. ArgumentError remains for invalid constructor arguments only. Every lookup now completes. The response handler caught only DnsException and FormatException while its finally block cancelled the deadline, so any other error left the lookup pending forever; it now maps to DnsMalformedResponseException. A custom transport that throws from start() fails the lookup instead of escaping. The resolver also stops cancelling transport operations that have already completed, so transports no longer need cancel() to be harmless after completion. BREAKING CHANGE: DnsError and DnsException.code/responseCode are removed in favour of DnsException subtypes; start methods no longer throw; invalid wire queries raise DnsInvalidQueryException, not ArgumentError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 8484 §4.1 says DoH clients SHOULD use DNS ID 0, but a connection refused any ID it had already carried, so every ID 0 query after the first opened a new connection and reuse never happened. The transport now gives each exchange a random ID unique on its connection, validates the reply against the query as sent, and restores the caller's ID in the returned body. A stale reply on a reused connection still fails the ID check rather than answering the next request. A connection retires once half its ID space is used, keeping random selection to about two draws. SIG(0) (RFC 2931 §3.1) and TSIG sign the header, so queries carrying either in their additional section are sent byte-for-byte with their own ID, which likewise never repeats on a connection. Padding already skipped them because the signature, not OPT, is the last record. A SIG(0)-signed response to an unsigned query (RFC 2931 §2.4) covers the response header and the request as sent, so startWire also takes preserveMessage: the query goes out unpadded with its own ID and the response is returned exactly as received. Lookups sharing a TLS connection reach the resolver together, so it can link them, and through SOCKS they also share one stream, defeating per-lookup isolation such as SocksCredentials.isolation. Reuse now defaults to on only for direct transports. The concurrency test assumed requests reached the server in start order, but concurrent TLS handshakes race, so it intermittently answered the wrong request and timed out. It now keys held requests by question name. BREAKING CHANGE: proxied transports no longer reuse connections unless reuseConnections: true is passed, servers see transport-chosen IDs unless the message is preserved, and DohWireTransport.startWire gains a preserveMessage parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An endpoint such as https://resolver.example/resolve?x=%FF passed construction, but startTxtLookup read endpoint.queryParameters on every call and threw FormatException before returning an operation, breaking the guarantee that starting never throws. The parameters are now decoded once in the constructor, which rejects undecodable endpoints with ArgumentError. queryParameters also kept only the last value of a repeated key, so tag=first&tag=second was sent as tag=second. queryParametersAll keeps every value, while the resolver's own keys still override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DoH JSON APIs encode TXT data differently: Google returns the text verbatim, Cloudflare returns quoted character-strings with escapes. The decoder guessed from a leading quote, so a Google TXT value starting with " was misparsed or rejected. A value cannot reveal its own form, so DohResolver now takes a DnsTxtFormat (raw or quoted), defaulting for known Google and Cloudflare hosts and required for any other endpoint. The quoted format accepts only quoted character-strings, which is what Cloudflare emits, and requires at least one, so empty or blank data is malformed while "" stays valid. A live test checks both providers decode identically. BREAKING CHANGE: decodeDnsTxtBytes and DnsResponse.authenticatedTxt take a DnsTxtFormat, and DohResolver requires txtFormat for endpoints other than Google and Cloudflare. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compression pointers may only target names the parser has walked, but it skipped the RDATA of types it did not know, so a later name pointing into an HTTPS TargetName or an IPSECKEY gateway was rejected while the uncompressed equivalent was accepted. The parser now walks the embedded names of NSAP-PTR, IPSECKEY, HIP, TALINK, SVCB and HTTPS, DSYNC, LP, TKEY, TSIG and AMTRELAY. Those names must themselves be uncompressed, as RFC 3597 requires for newer types, and the existing backward-only pointer rules are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wire bodies are returned as received, leaving every caller to apply the HTTP Age and the RFC 2181 high-bit rule itself. effectiveDnsTtl applies both to one TTL and now backs the JSON path too, so the rule lives in one place. adjustDnsWireTtls returns a copy of a wire response with every record TTL adjusted, skipping OPT, whose TTL field holds EDNS flags. RRSIG validation is unaffected because it uses the Original TTL, but SIG(0) and TSIG transaction signatures cover the whole message, so they must be verified before adjusting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tool/check.sh needed a POSIX shell, so it failed from cmd or PowerShell. tool/check.dart runs the same steps (pub get, format, analyze, test, publish dry-run) through the running Dart VM, so no PATH or .bat lookup is involved; it streams their output, works from any directory, and exits with the first failing step's code. The example now runs from a test, so dart test alone catches a broken example. CI only ran on Ubuntu, but the transport depends on dart:io sockets, TLS and timing. It now runs the checker on Linux, macOS and Windows for the minimum and stable SDKs, without fail-fast, and .gitattributes pins LF endings so Windows checkouts do not trip dart format or byte-exact tests. lints moves to ^5.0.0, the newest release supporting Dart 3.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds dartdoc to every public declaration, prioritising trust, cancellation, limits and lifecycle, and validated with dart doc. The README is reorganised around the trust model (including checking a DnsValidator by identity, since anyone can construct an issuer), per-provider JSON behaviour, transport limits, deadlines, connection reuse and shutdown, the error and cancellation contract, and TTL handling. The unreleased 0.0.1 changelog entry now describes the package as it stands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t slot Uri.parse accepts any digit string as a port and only throws when the port is read. IoDohTransport read it in _origin after creating the request and incrementing _active, so an endpoint such as https://dns.google:99999999999999999999/resolve threw a synchronous FormatException from start, left a request whose deadline later failed with a null-check error, and leaked a concurrency slot: with maxConcurrentRequests 1 every later lookup timed out. isUsableDohEndpoint now checks the scheme, host, user info, fragment and a readable port in 1-65535 before any state exists, and _start fails the operation with DnsInvalidQueryException. DohResolver applies the same check at construction so such an endpoint cannot make startTxtLookup throw either.
_name re-walked every backward pointer chain for each name it decoded and only remembered which offsets were valid targets. A valid message near the 65,535-octet limit can build progressively longer chains, so decompression was quadratic: one 60 KB reply cost about 11.9 million byte reads on the isolate that also runs deadlines and UI work. Each decoded name now caches the suffix starting at every label it owns, as views into one buffer, and a pointer appends that suffix in one step. Targets must still be the start of a label in a name decoded before the current one began, so loops stay impossible and the accepted set of messages is unchanged. compression_budget_test counts byte reads rather than time so the regression is deterministic.
…ception Sealing DnsException gave callers exhaustive switches but closed every subtype, and dnssec_resolver reports its bogus and indeterminate results through a DnsException subclass. It can no longer compile against this package. DnsAuthenticationUnavailableException is now a base class, the one open extension point: a local validator subclasses it to say why it found no secure answer, and a switch over DnsException still sees that as authentication being unavailable. Every other subtype stays final.
Reuse is off behind a proxy so a long-lived transport cannot let the resolver link unrelated lookups. A transport created per lookup with SocksCredentials.isolation gains nothing from that default: its queries already share one circuit, and a wire lookup makes several requests, so each costs a new Tor stream and TLS handshake. Measured on such a transport, reuse cut a Tor lookup from 7.2 s to 4.9 s. Say so next to the default.
…ilableException" This reverts commit 15ecf36.
Move the DNS wire builders covered by test/support/LICENSE into doh_transport under the package MIT LICENSE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.