Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fetch --inspect-dns example.com
fetch --inspect-dns --dns-server https://1.1.1.1/dns-query example.com
```

Without `--dns-server`, inspection queries the nameservers listed in the system resolver configuration (`/etc/resolv.conf`) directly, including on macOS. It reports every record type (A, AAAA, CNAME, TXT, MX, NS, SOA, SRV, CAA, SVCB, and HTTPS) with per-record TTLs, but does not apply macOS scoped, per-interface, VPN, or `/etc/resolver` routing. On platforms without a usable resolver file (notably Windows), or when the name is resolved only through OS mechanisms (the hosts file, NSS modules, or mDNS), it uses the platform resolver for A and AAAA records without per-record TTLs. Platform-resolver records show their source and `TTL unavailable` individually. If direct DNS returns no address records, platform-resolver addresses are added while any records already returned by direct DNS remain visible. The `Lookup` section identifies this mixed resolver path and reports the platform fallback. With an explicit resolver, inspection queries the same record types concurrently. The default output uses `Lookup` and `Records` sections with the inspected name, resolver path, transport, transport security, source, status, result counts, query counts, and duration. Each record shows its normalized, fully qualified owner name before its value. Inspection output is written to stdout; invocation warnings and setup/configuration errors are written to stderr. If a query fails, successful records are retained, a `Failures` section reports the incomplete record types on stdout, and the command exits with status 1.
Without `--dns-server`, inspection queries the nameservers listed in the system resolver configuration (`/etc/resolv.conf`) directly, including on macOS. It reports every record type (A, AAAA, CNAME, TXT, MX, NS, SOA, SRV, CAA, SVCB, and HTTPS) with per-record TTLs, but does not apply macOS scoped, per-interface, VPN, or `/etc/resolver` routing. On platforms without a usable resolver file (notably Windows), or when the name is resolved only through OS mechanisms (the hosts file, NSS modules, or mDNS), it uses the platform resolver for A and AAAA records without per-record TTLs. Platform-resolver records show their source and `TTL unavailable` individually. If direct DNS returns no address records, platform-resolver addresses are added while any records already returned by direct DNS remain visible. The `Lookup` section identifies this mixed resolver path and reports the platform fallback. With an explicit resolver, inspection queries the same record types concurrently. The default output uses `Lookup` and `Records` sections with the inspected name, resolver path, transport, transport security, source, status, result counts, query counts, and duration. Each record shows its normalized, fully qualified owner name before its value. Inspection output is written to stdout; invocation warnings and setup/configuration errors are written to stderr. If a query fails, successful records are retained, a `Failures` section reports the incomplete record types on stdout, and the command exits with status 1. `Transport security` describes encryption and certificate verification between fetch and the resolver; it does not indicate DNSSEC validation, which fetch does not perform.

### Configuration File

Expand Down
2 changes: 1 addition & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ platform bootstrap and negotiate the standard `doq` ALPN.

### `--inspect-dns`

Inspect DNS resolution for the URL hostname only (no HTTP request is made). Without `--dns-server`, it queries the nameservers listed in the system resolver configuration (`/etc/resolv.conf`) directly, including on macOS. It reports every record type (A, AAAA, CNAME, TXT, MX, NS, SOA, SRV, CAA, SVCB, and HTTPS) with per-record TTLs, but does not apply macOS scoped, per-interface, VPN, or `/etc/resolver` routing. On platforms without a usable resolver file (notably Windows), or when the name is resolved only through OS mechanisms (the hosts file, NSS modules, or mDNS), it uses the platform resolver for A and AAAA records without per-record TTLs. Platform-resolver records show their source and `TTL unavailable` individually. If direct DNS returns no address records, platform-resolver addresses are added while any records already returned by direct DNS remain visible. The `Lookup` section identifies this mixed resolver path and reports the platform fallback. With an explicit resolver it queries the same record types concurrently. The default output uses `Lookup` and `Records` sections and includes the inspected name, resolver path, transport, transport security, source, status, result counts, query counts, and duration. Each record shows its normalized, fully qualified owner name before its value. Inspection output is written to stdout; invocation warnings and setup/configuration errors are written to stderr. If one query fails, successful records remain visible, a `Failures` section identifies the incomplete record types on stdout, and the command exits with status 1.
Inspect DNS resolution for the URL hostname only (no HTTP request is made). Without `--dns-server`, it queries the nameservers listed in the system resolver configuration (`/etc/resolv.conf`) directly, including on macOS. It reports every record type (A, AAAA, CNAME, TXT, MX, NS, SOA, SRV, CAA, SVCB, and HTTPS) with per-record TTLs, but does not apply macOS scoped, per-interface, VPN, or `/etc/resolver` routing. On platforms without a usable resolver file (notably Windows), or when the name is resolved only through OS mechanisms (the hosts file, NSS modules, or mDNS), it uses the platform resolver for A and AAAA records without per-record TTLs. Platform-resolver records show their source and `TTL unavailable` individually. If direct DNS returns no address records, platform-resolver addresses are added while any records already returned by direct DNS remain visible. The `Lookup` section identifies this mixed resolver path and reports the platform fallback. With an explicit resolver it queries the same record types concurrently. The default output uses `Lookup` and `Records` sections and includes the inspected name, resolver path, transport, transport security, source, status, result counts, query counts, and duration. Each record shows its normalized, fully qualified owner name before its value. Inspection output is written to stdout; invocation warnings and setup/configuration errors are written to stderr. If one query fails, successful records remain visible, a `Failures` section identifies the incomplete record types on stdout, and the command exits with status 1. `Transport security` describes encryption and certificate verification between fetch and the resolver; it does not indicate DNSSEC validation, which fetch does not perform.

```sh
fetch --inspect-dns example.com
Expand Down
42 changes: 31 additions & 11 deletions internal/dnsinspect/dnsinspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ func lookup(ctx context.Context, cfg *Config, host string, start time.Time) (*re
host: host,
resolver: target.label,
transport: inspectionTransport(cfg, server),
security: resolverSecurity(cfg, server),
security: resolverTransportSecurity(cfg, server),
source: inspectionSource(server),
records: make(map[string][]record),
silent: cfg.Silent,
Expand Down Expand Up @@ -1502,24 +1502,44 @@ func dnsLabelPresentation(label []byte) string {
return b.String()
}

func resolverSecurity(cfg *Config, server *url.URL) string {
// resolverTransportSecurity reports protection for the connection to the
// resolver. It intentionally says nothing about DNSSEC: fetch does not
// validate DNSSEC chains locally.
func resolverTransportSecurity(cfg *Config, server *url.URL) string {
if server == nil {
return "platform resolver (OS-managed security)"
}

// Endpoint is the authoritative representation used by production callers.
// Derive the result from VerifyTLS instead of the display URL so --insecure
// is reflected without changing endpoint configuration. An HTTPS transport
// used by an explicit local test endpoint can intentionally have TLS
// verification disabled and is therefore plaintext, not unverified TLS.
if cfg.Endpoint != nil {
security := cfg.Endpoint.Security
if cfg.Insecure && security == resolver.SecurityVerifiedEncrypted {
security = resolver.SecurityUnverifiedEncrypt
if cfg.Endpoint.VerifyTLS {
if cfg.Insecure {
return string(resolver.SecurityUnverifiedEncrypt)
}
return string(resolver.SecurityVerifiedEncrypted)
}
return string(security)
}
if cfg.Insecure && (strings.EqualFold(server.Scheme, "https") || strings.EqualFold(server.Scheme, "http")) {
return string(resolver.SecurityUnverifiedEncrypt)
return string(resolver.SecurityPlaintext)
}
if strings.EqualFold(server.Scheme, "https") {

// DNSServer remains supported for older internal callers. Account for all
// resolver URL schemes here; treating only https:// as encrypted would
// misreport legacy DoT and DoQ configurations.
scheme := strings.ToLower(server.Scheme)
switch scheme {
case "tls", "dot", "quic", "doq", "https":
if cfg.Insecure {
return string(resolver.SecurityUnverifiedEncrypt)
}
return string(resolver.SecurityVerifiedEncrypted)
default:
// UDP, TCP, and plain HTTP expose DNS on the wire. --insecure has no
// certificate verification to disable for these transports.
return string(resolver.SecurityPlaintext)
}
return string(resolver.SecurityPlaintext)
}

func resolverTarget(server *url.URL) resolverTargetInfo {
Expand Down
69 changes: 69 additions & 0 deletions internal/dnsinspect/dnsinspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,75 @@ func TestFlushInspectionOutputIgnoresBrokenPipe(t *testing.T) {
}
}

func TestResolverTransportSecurityReportsTransportProtection(t *testing.T) {
tests := []struct {
name string
endpoint string
insecure bool
transport string
security string
}{
{name: "UDP", endpoint: "udp://192.0.2.53", transport: "UDP", security: "plaintext"},
{name: "TCP", endpoint: "tcp://192.0.2.53", transport: "TCP", security: "plaintext"},
{name: "DoT", endpoint: "dot://resolver.example", transport: "TLS (DoT)", security: "verified TLS"},
{name: "DoQ", endpoint: "doq://resolver.example", transport: "QUIC (DoQ)", security: "verified TLS"},
{name: "DoH", endpoint: "https://resolver.example/dns-query", transport: "HTTPS (DoH)", security: "verified TLS"},
{name: "DoT insecure", endpoint: "dot://resolver.example", insecure: true, transport: "TLS (DoT)", security: "encrypted, certificate verification disabled"},
{name: "DoQ insecure", endpoint: "doq://resolver.example", insecure: true, transport: "QUIC (DoQ)", security: "encrypted, certificate verification disabled"},
{name: "DoH insecure", endpoint: "https://resolver.example/dns-query", insecure: true, transport: "HTTPS (DoH)", security: "encrypted, certificate verification disabled"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep, err := resolver.ParseEndpoint(tt.endpoint)
if err != nil {
t.Fatal(err)
}
cfg := &Config{Endpoint: ep, Insecure: tt.insecure}
if got := inspectionTransport(cfg, ep.URL()); got != tt.transport {
t.Errorf("inspectionTransport() = %q, want %q", got, tt.transport)
}
if got := displaySecurity(resolverTransportSecurity(cfg, ep.URL())); got != tt.security {
t.Errorf("resolver transport security = %q, want %q", got, tt.security)
}
})
}
}

func TestResolverTransportSecuritySupportsLegacyResolverURLs(t *testing.T) {
tests := []struct {
name string
scheme string
security string
}{
{name: "UDP", scheme: "", security: "plaintext"},
{name: "TCP", scheme: "tcp", security: "plaintext"},
{name: "DoT", scheme: "tls", security: "verified TLS"},
{name: "DoT alias", scheme: "dot", security: "verified TLS"},
{name: "DoQ", scheme: "quic", security: "verified TLS"},
{name: "DoQ alias", scheme: "doq", security: "verified TLS"},
{name: "DoH", scheme: "https", security: "verified TLS"},
{name: "HTTP", scheme: "http", security: "plaintext"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := mustURL(t, "//resolver.example:53")
server.Scheme = tt.scheme
cfg := &Config{Insecure: tt.name == "HTTP"}
if got := displaySecurity(resolverTransportSecurity(cfg, server)); got != tt.security {
t.Errorf("resolver transport security = %q, want %q", got, tt.security)
}
})
}
}

func TestResolverTransportSecurityReportsPlatformResolver(t *testing.T) {
if got, want := displaySecurity(resolverTransportSecurity(&Config{}, nil)), "OS-managed / unknown to fetch"; got != want {
t.Fatalf("platform resolver security = %q, want %q", got, want)
}
}

func TestInspectDOHShowsAAndAAAATTLs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("type") {
Expand Down
2 changes: 1 addition & 1 deletion skills/fetch/references/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ shows its normalized, fully qualified owner name before its value. Successful
records remain visible when one query fails; the `Lookup` section reports an
incomplete status and the `Failures` section identifies the failed types. The
command exits nonzero. Inspection output, including the `Failures` section, goes
to stdout. Invocation warnings and setup/configuration errors go to stderr.
to stdout. Invocation warnings and setup/configuration errors go to stderr. `Transport security` describes the resolver connection only; it is not DNSSEC validation, which fetch does not perform.

## TLS

Expand Down
Loading