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
5 changes: 5 additions & 0 deletions docs/advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ fetch --inspect-dns example.com
fetch --inspect-dns --dns-server https://1.1.1.1/dns-query example.com
```

When the URL hostname is an IPv4 or IPv6 literal, DNS inspection does not
perform a resolver query. It reports `Status: IP literal — DNS not performed`,
and omits resolver, transport, security, and DNS timing fields. The command
returns success.

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. When system failover occurs, `Resolver` or `Resolvers` reports the nameserver(s) that actually answered. The default output is complete, and `-v` has no effect in DNS inspection mode. Use `-vv` for resolver and query internals, including the configured nameserver list, policy limits, normalization, caveats, responders, transport, duration, and failover attempts. For direct DNS lookups, when IDNA normalization changes the name, normal output includes `Query name` with the absolute punycode name sent to DNS. Single-label names also show their absolute query name; the root terminator is omitted for ordinary multi-label hostnames when it is the only difference. 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. If a UDP response is truncated, fetch retries the query over TCP and reports the normal protocol fallback as `Transport: UDP → TCP fallback`, not as a warning. Use `-vv` to see which record-type queries used the fallback.

At `-vv`, the `System resolver` section describes the direct system path. It shows the resolver configuration file, direct nameserver routing, and that search domains are not applied. On macOS it also reports that scoped, VPN, per-interface, and `/etc/resolver` routing is not applied. On other platforms it reports that OS resolver routing is not applied by direct queries. These caveats describe the direct DNS portion only; a platform fallback uses the OS resolver for addresses.
Expand Down
4 changes: 4 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,10 @@ fetch --inspect-dns example.com
fetch --inspect-dns --dns-server https://1.1.1.1/dns-query example.com
```

An IPv4 or IPv6 URL literal does not require DNS inspection. The output reports
`Status: IP literal — DNS not performed` and omits resolver, transport,
transport-security, and DNS timing fields. It returns success.

### `--proxy PROXY`

Route request through a proxy.
Expand Down
12 changes: 12 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,18 @@ func TestMain(t *testing.T) {
}
})

t.Run("dns inspection skips IP literals", func(t *testing.T) {
t.Parallel()
for _, target := range []string{"http://127.0.0.1", "http://[2001:db8::1]"} {
t.Run(target, func(t *testing.T) {
res := runFetch(t, fetchPath, "--inspect-dns", target)
assertExitCode(t, 0, res)
assertBufEmpty(t, res.stderr)
assertBufContains(t, res.stdout, "Status: IP literal — DNS not performed")
})
}
})

t.Run("dns over https", func(t *testing.T) {
t.Parallel()
server := startServer(func(w http.ResponseWriter, r *http.Request) {
Expand Down
19 changes: 18 additions & 1 deletion internal/dnsinspect/dnsinspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func InspectWithError(ctx context.Context, output, errorOutput *core.Printer, cf
defer cancel()

start := time.Now()
if net.ParseIP(host) != nil {
if isIPLiteral(host) {
renderIPLiteral(output, host)
return flushInspectionOutput(output, errorOutput)
}
Expand Down Expand Up @@ -1833,6 +1833,23 @@ func typeLabel(typ dnsmessage.Type) string {
}
}

// isIPLiteral reports IPv4, IPv6, and scoped IPv6 literals. URL.Hostname
// removes brackets from IPv6 authorities and decodes the zone separator, so
// check the address without its optional interface zone. A scoped IPv6
// literal is still an address that must not trigger DNS inspection.
func isIPLiteral(host string) bool {
if net.ParseIP(host) != nil {
return true
}
if zone := strings.IndexByte(host, '%'); zone > 0 && zone+1 < len(host) {
ip := net.ParseIP(host[:zone])
// A zone is valid only on an IPv6 spelling. IPv4-mapped IPv6
// addresses retain the colon syntax even though To4 reports true.
return ip != nil && strings.Contains(host[:zone], ":")
}
return false
}

func renderIPLiteral(p *core.Printer, host string) {
renderInspectionSection(p, "Lookup")
writeInspectionField(p, "Name", host)
Expand Down
70 changes: 52 additions & 18 deletions internal/dnsinspect/dnsinspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,26 +355,60 @@ func TestInspectNormalizesIDNForDNSQueries(t *testing.T) {
}

func TestInspectIPLiteralSkipsLookup(t *testing.T) {
p := core.TestPrinter(false)
status := Inspect(context.Background(), p, &Config{
URL: mustURL(t, "http://127.0.0.1"),
})
if status != 0 {
t.Fatalf("status = %d, want 0\n%s", status, p.Bytes())
tests := []struct {
name string
url string
want string
}{
{name: "IPv4", url: "http://127.0.0.1", want: "127.0.0.1"},
{name: "IPv6", url: "http://[2001:db8::1]", want: "2001:db8::1"},
{name: "scoped IPv6", url: "http://[fe80::1%25lo0]", want: "fe80::1%lo0"},
{name: "scoped IPv6 with encoded zone percent", url: "http://[fe80::1%25en%25foo]", want: "fe80::1%en%foo"},
{name: "scoped IPv4-mapped IPv6", url: "http://[::ffff:192.0.2.1%25lo0]", want: "::ffff:192.0.2.1%lo0"},
}
out := string(p.Bytes())
for _, want := range []string{
"Lookup\n",
"Name: 127.0.0.1",
"Status: IP literal — DNS not performed",
} {
if !strings.Contains(out, want) {
t.Fatalf("output missing %q:\n%s", want, out)
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := core.TestPrinter(false)
status := Inspect(context.Background(), p, &Config{URL: mustURL(t, tt.url)})
if status != 0 {
t.Fatalf("status = %d, want 0\n%s", status, p.Bytes())
}
out := string(p.Bytes())
for _, want := range []string{
"Lookup\n",
"Name: " + tt.want,
"Status: IP literal — DNS not performed",
} {
if !strings.Contains(out, want) {
t.Fatalf("output missing %q:\n%s", want, out)
}
}
for _, unwanted := range []string{"Resolver:", "Transport:", "Transport security:", "Timing:"} {
if strings.Contains(out, unwanted) {
t.Fatalf("IP literal output contains DNS field %q:\n%s", unwanted, out)
}
}
})
}
for _, unwanted := range []string{"Resolver:", "Transport:", "Transport security:", "Timing:"} {
if strings.Contains(out, unwanted) {
t.Fatalf("IP literal output contains DNS field %q:\n%s", unwanted, out)
}

func TestIsIPLiteral(t *testing.T) {
for _, tt := range []struct {
host string
want bool
}{
{host: "127.0.0.1", want: true},
{host: "2001:db8::1", want: true},
{host: "fe80::1%lo0", want: true},
{host: "fe80::1%en%foo", want: true},
{host: "127.0.0.1%bad", want: false},
{host: "::ffff:192.0.2.1%lo0", want: true},
{host: "example.com", want: false},
{host: "2001:db8::1%", want: false},
} {
if got := isIPLiteral(tt.host); got != tt.want {
t.Errorf("isIPLiteral(%q) = %t, want %t", tt.host, got, tt.want)
}
}
}
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 @@ -33,7 +33,7 @@ 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. `Transport security` describes the resolver connection only; it is not DNSSEC validation, which fetch does not perform. A truncated UDP response is retried over TCP and is reported as transport metadata (`Transport: UDP → TCP fallback`), not as a warning. Use `-vv` to identify the record-type queries that used this fallback.

For direct system DNS, `-vv` reports the configuration file, direct nameserver routing, and that search domains are not applied. On macOS, it also reports that scoped, VPN, per-interface, and `/etc/resolver` routing is not applied. On other platforms, it reports that OS resolver routing is not applied by direct queries. These limitations apply to the direct DNS path; platform-fallback addresses use the OS resolver. Do not discard useful stdout only because a partial inspection exits with status 1.
For an IPv4 or IPv6 URL literal, DNS is not performed. The result reports `Status: IP literal — DNS not performed`, omits resolver and transport fields, and exits successfully. For direct system DNS, `-vv` reports the configuration file, direct nameserver routing, and that search domains are not applied. On macOS, it also reports that scoped, VPN, per-interface, and `/etc/resolver` routing is not applied. On other platforms, it reports that OS resolver routing is not applied by direct queries. These limitations apply to the direct DNS path; platform-fallback addresses use the OS resolver. Do not discard useful stdout only because a partial inspection exits with status 1.

## TLS

Expand Down