234 lines
9.9 KiB
Markdown
234 lines
9.9 KiB
Markdown
# Encrypted DNS Skill
|
||
|
||
[](https://github.com/windyboy/encrypted-dns-skill/actions/workflows/ci.yml)
|
||
|
||
An [Agent Skill](https://agentskills.io/specification) and deterministic Go CLI
|
||
for querying and diagnosing encrypted DNS resolvers.
|
||
|
||
The skill tells an agent when and how to perform encrypted DNS diagnostics;
|
||
`ednsdiag` performs the protocol exchange. Agents do not need to construct DoH
|
||
URLs, TLS sessions, or DNS wire messages themselves.
|
||
|
||
## Status
|
||
|
||
| Protocol | Status | Standard |
|
||
| --- | --- | --- |
|
||
| 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) | 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) |
|
||
|
||
Run `ednsdiag capabilities` instead of assuming a protocol is implemented.
|
||
|
||
## Why a Skill and a CLI?
|
||
|
||
- `SKILL.md` provides compact instructions, safety boundaries, and result
|
||
interpretation for an AI agent.
|
||
- `ednsdiag` provides repeatable wire-format DNS, HTTP, TLS, input validation,
|
||
and structured JSON output.
|
||
- Reference files keep protocol, provider, and security details grounded in
|
||
authoritative sources without bloating the agent's active context.
|
||
|
||
The CLI never silently downgrades to plaintext DNS, and it does not connect to
|
||
addresses returned in DNS answers.
|
||
|
||
## Requirements
|
||
|
||
- 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
|
||
|
||
## Install the Skill
|
||
|
||
Clone or copy this repository into a skill discovery directory supported by
|
||
your agent host. Keep the repository layout intact so `SKILL.md`, `references/`,
|
||
`schemas/`, and the Go source remain together.
|
||
|
||
For example, in a host that discovers project-local skills from `.agents/skills`:
|
||
|
||
```bash
|
||
git clone https://github.com/windyboy/encrypted-dns-skill.git \
|
||
.agents/skills/encrypted-dns-skill
|
||
```
|
||
|
||
Discovery paths differ between hosts. Follow the host's documentation rather
|
||
than moving only `SKILL.md`.
|
||
|
||
## Run from Source
|
||
|
||
No precompiled executable is required. Go can compile and run the command from
|
||
the repository root:
|
||
|
||
```bash
|
||
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`.
|
||
|
||
To build a reusable local executable:
|
||
|
||
```bash
|
||
go build -o ./bin/ednsdiag ./cmd/ednsdiag
|
||
./bin/ednsdiag capabilities
|
||
```
|
||
|
||
Do not download or execute an unverified third-party binary. This repository
|
||
does not currently publish release binaries.
|
||
|
||
## Usage
|
||
|
||
```text
|
||
ednsdiag capabilities
|
||
ednsdiag version
|
||
ednsdiag query <domain> [type] \
|
||
[--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 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 2–8 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 | 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.
|
||
|
||
## Result Semantics
|
||
|
||
Every query returns structured JSON compatible with
|
||
[`schemas/result-v1.schema.json`](schemas/result-v1.schema.json).
|
||
|
||
- `completed: true` means the encrypted protocol exchange completed; it does
|
||
not mean the DNS response was `NOERROR`.
|
||
- `dns.rcode` is the DNS result. `NXDOMAIN`, `SERVFAIL`, and `REFUSED` are DNS
|
||
outcomes, not transport failures.
|
||
- Empty `dns.answers` with `NOERROR` means NODATA.
|
||
- `transport.server_authenticated` reports resolver endpoint authentication.
|
||
- `dns.resolver_reports_dnssec_authenticated` reflects the resolver's AD bit;
|
||
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.
|
||
- DNS answers are data only; the tool does not make application connections to
|
||
returned addresses.
|
||
|
||
See [`references/security.md`](references/security.md) for the complete threat
|
||
model and privacy boundaries.
|
||
|
||
## Development
|
||
|
||
```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
|
||
[`references/standards.md`](references/standards.md), provider changes with
|
||
[`references/providers.md`](references/providers.md), and output with the v1
|
||
JSON schema.
|
||
|
||
## Scope
|
||
|
||
This project targets client-to-recursive encrypted DNS diagnostics. It is not a
|
||
system stub resolver, an authoritative DNS server, a hosted DNS record manager,
|
||
or a zone-transfer tool.
|
||
|
||
## License
|
||
|
||
Licensed under the [Apache License 2.0](LICENSE).
|