feat(ednsdiag): add DoQ/DoH3/DNSCrypt transports, proxy support, probe & compare

This commit is contained in:
windyboy
2026-08-14 18:17:26 +08:00
parent 8303d78caf
commit 70aea6cd72
21 changed files with 1665 additions and 236 deletions
+86 -12
View File
@@ -15,9 +15,9 @@ URLs, TLS sessions, or DNS wire messages themselves.
| --- | --- | --- |
| DNS over HTTPS (DoH) | Available (GET and POST) | [RFC 8484](https://www.rfc-editor.org/rfc/rfc8484.html) |
| DNS over TLS (DoT) | Available (strict authentication) | [RFC 7858](https://www.rfc-editor.org/rfc/rfc7858.html), [RFC 8310](https://www.rfc-editor.org/rfc/rfc8310.html) |
| DNS over QUIC (DoQ) | Planned | [RFC 9250](https://www.rfc-editor.org/rfc/rfc9250.html) |
| DoH over HTTP/3 (DoH3) | Planned | RFC 8484 over HTTP/3 |
| DNSCrypt | Planned | [DNSCrypt protocol specification](https://github.com/DNSCrypt/dnscrypt-protocol) |
| DNS over QUIC (DoQ) | Available | [RFC 9250](https://www.rfc-editor.org/rfc/rfc9250.html) |
| DoH over HTTP/3 (DoH3) | Available (GET and POST) | RFC 8484 over HTTP/3 |
| DNSCrypt | Available (v2 over UDP) | [DNSCrypt protocol specification](https://github.com/DNSCrypt/dnscrypt-protocol) |
| Oblivious DoH (ODoH) | Research | [RFC 9230](https://www.rfc-editor.org/rfc/rfc9230.html) |
| Anonymized DNSCrypt | Research | [Anonymized DNSCrypt specification](https://github.com/DNSCrypt/dnscrypt-protocol/blob/master/ANONYMIZED-DNSCRYPT.txt) |
@@ -37,7 +37,7 @@ addresses returned in DNS answers.
## Requirements
- Go 1.26 or later when running or building from source
- Go 1.26.6 or later when running or building from source
- Network access to the selected encrypted DNS resolver
- A host that supports the [Agent Skills package format](https://agentskills.io/specification) when using the repository as a Skill
@@ -66,6 +66,14 @@ the repository root:
go run ./cmd/ednsdiag capabilities
go run ./cmd/ednsdiag query example.com A --protocol doh --provider cloudflare
go run ./cmd/ednsdiag query gmail.com MX --protocol dot --provider google --timeout 5s
HTTPS_PROXY=http://127.0.0.1:8080 go run ./cmd/ednsdiag query example.com A --protocol doh
go run ./cmd/ednsdiag query example.com A --protocol dot --proxy http://127.0.0.1:8080
go run ./cmd/ednsdiag query example.com AAAA --protocol doq --provider adguard
go run ./cmd/ednsdiag query example.com HTTPS --protocol doh3 --provider cloudflare
go run ./cmd/ednsdiag query example.com A --protocol dnscrypt --provider adguard
go run ./cmd/ednsdiag probe example.com A --protocol dot --provider cloudflare
go run ./cmd/ednsdiag compare example.com A \
--target doh:cloudflare --target dot:google
```
The first run may download the modules pinned in `go.mod` and `go.sum`.
@@ -86,23 +94,51 @@ does not currently publish release binaries.
ednsdiag capabilities
ednsdiag version
ednsdiag query <domain> [type] \
[--protocol doh|dot] \
[--protocol doh|dot|doq|doh3|dnscrypt|odoh|anonymized-dnscrypt] \
[--provider cloudflare|google|quad9|adguard] \
[--method post|get] \
[--proxy http://host:port] \
[--timeout 5s]
ednsdiag probe <domain> [type] [query options]
ednsdiag compare <domain> [type] \
--target protocol:provider[:method] \
--target protocol:provider[:method] \
[--proxy http://host:port] \
[--attempt-timeout 5s] [--timeout 30s] [--max-attempts 4]
```
Defaults are `A`, `doh`, `cloudflare`, `post`, and `5s`. `--method` applies
only to DoH. The timeout must be between `250ms` and `30s`.
only to DoH and DoH3. The timeout must be between `250ms` and `30s`.
Research protocols are accepted as inputs so automation receives a structured
`unsupported` result and exit code `4`; they are never silently substituted.
DoH and DoT honor Go's standard `HTTPS_PROXY`/`https_proxy` and
`NO_PROXY`/`no_proxy` environment variables. `--proxy` overrides environment
selection and accepts an `http://` or `https://` proxy URL, including optional
Basic-auth userinfo. DoT uses HTTP CONNECT before its resolver TLS handshake.
DoH3, DoQ, and DNSCrypt are UDP/QUIC transports and cannot use this TCP CONNECT
proxy; an explicit proxy combined with one of those protocols is rejected.
The same `--proxy` is shared by every DoH/DoT target in a `compare` operation.
See Go's official [`ProxyFromEnvironment` documentation](https://pkg.go.dev/net/http#ProxyFromEnvironment)
for environment-variable and `NO_PROXY` matching rules.
`compare` accepts 28 unique, allowlisted targets, bounded by `--max-attempts`.
Its total timeout is `250ms``60s`; each attempt timeout is `250ms``30s` and
cannot exceed the total. Comparison targets start concurrently, results retain
the requested target order, and answers are never merged.
Supported record types are `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SOA`,
`CAA`, `SRV`, `PTR`, `HTTPS`, and `SVCB`. For `PTR`, pass an IP address; the CLI
constructs the reverse name. Other IP literals and local names remain blocked.
Built-in resolver profiles:
| Provider | Profile |
| --- | --- |
| Cloudflare | Unfiltered |
| Google | Unfiltered |
| Quad9 | Security-filtered |
| AdGuard | Ad- and security-filtered |
| Provider | Profile | DoH | DoT | DoQ | DoH3 | DNSCrypt |
| --- | --- | --- | --- | --- | --- | --- |
| Cloudflare | Unfiltered | Yes | Yes | No | Yes | No |
| Google | Unfiltered | Yes | Yes | No | Yes | No |
| Quad9 | Security-filtered | Yes | Yes | No | No | No |
| AdGuard | Ad- and security-filtered | Yes | Yes | Yes | No | Yes |
Filtering policies can affect DNS answers. Results always identify the
provider and profile used.
@@ -122,11 +158,37 @@ Every query returns structured JSON compatible with
it is not local DNSSEC validation.
- `transport.bootstrap: system_resolver` means the operating system resolver
was used to locate the encrypted resolver endpoint.
- `transport.proxy`, when present, is the HTTP(S) proxy endpoint actually
selected for DoH or DoT. Embedded credentials are never reported.
- DNSCrypt reports `bootstrap: stamp_ip`, the authenticated provider name,
resolver certificate serial, and selected crypto construction.
- DoH and DoH3 subtract a valid HTTP `Age` value from returned answer TTLs and
report it as `transport.http_age_seconds`.
- Truncated or non-representable DNS answers are protocol failures rather than
partial `completed: true` results.
Human-readable usage errors go to stderr. Machine-readable operational results
go to stdout. Stable exit codes are:
| Code | Meaning |
| --- | --- |
| `0` | The requested encrypted DNS operation completed; inspect `dns.rcode`. |
| `1` | Local or internal failure. |
| `2` | Invalid input or CLI usage. |
| `3` | Transport or DNS protocol failure. |
| `4` | Known but unsupported capability or provider/protocol combination. |
See [`references/contracts.md`](references/contracts.md) for the complete v1
command and result contract.
## Security Model
- DoH uses standard `application/dns-message` wire messages.
- DoT verifies the PKIX certificate chain and configured authentication domain.
- DoT advertises ALPN `dot`; an empty selection is accepted and reported, while
selection of a different application protocol is rejected.
- DNSCrypt validates the resolver stamp, Ed25519-signed certificate, validity
interval, provider identity, and encrypted response before accepting DNS data.
- Plaintext fallback is prohibited.
- DNS errors are not retried through another protocol as transport failures.
- Provider and protocol results remain separate.
@@ -140,7 +202,19 @@ model and privacy boundaries.
```bash
go test ./...
go test -race ./...
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
python3 scripts/validate_skill.py .
```
Public endpoint interoperability tests are opt-in and skip cleanly when the
host cannot reach the network:
```bash
EDNSDIAG_PUBLIC_INTEROP=1 go test ./internal/edns \
-run '^TestPublicCloudflareDo[HT]Interoperability$' -count=1 -v
```
Protocol behavior must remain aligned with