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
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ vdns lookup example.com google.com github.com
# Propagation check
vdns prop cloudflare.com

# Compare two domains
vdns compare google.com cloudflare.com

# Benchmark resolver speed
vdns benchmark

# Watch mode
vdns lookup example.com --watch
vdns prop example.com --until-consistent
Expand Down Expand Up @@ -74,6 +80,8 @@ vdns lookup example.com # DNS lookup with DNSSEC validation
| `vdns lookup <domain> [domain...]` | DNS record lookup | Local / Remote |
| `vdns a/aaaa/mx/ns/txt/cname/soa/caa/srv <domain>` | Shorthand lookups | Local / Remote |
| `vdns prop <domain> [domain...]` | DNS propagation check | Local / Remote |
| `vdns compare <domain1> <domain2>` | Side-by-side DNS comparison | Local / Remote |
| `vdns benchmark [domain]` | Measure resolver response times | Local |
| `vdns whois <domain>` | WHOIS/RDAP data | Remote |
| `vdns avail <domain>` | Domain availability | Remote |
| `vdns ssl <domain>` | SSL/TLS certificate | Remote |
Expand Down Expand Up @@ -128,6 +136,28 @@ vdns prop example.com google.com

Results are displayed sequentially with clear separation.

## Compare

Compare DNS records between two domains side by side:

```bash
vdns compare google.com cloudflare.com
vdns compare example.com example.org -t A,MX,NS
```

Matching records are shown plain, differences are highlighted. Works in both local and remote mode.

## Benchmark

Measure response times for all public resolvers from your machine:

```bash
vdns benchmark # Query google.com by default
vdns benchmark example.com # Query a specific domain
```

Results are sorted by latency and color-coded. Also shows your detected system resolver.

## Local History

Lookup results are stored locally by default. Browse, search, export, or clear:
Expand Down Expand Up @@ -209,13 +239,16 @@ vdns config remove-profile work

## Propagation Resolvers

Propagation checks query these public resolvers:
Propagation checks and benchmarks query these public resolvers:

- Google DNS (8.8.8.8, 8.8.4.4)
- Cloudflare DNS (1.1.1.1, 1.0.0.1)
- Quad9 (9.9.9.9)
- Quad9 (9.9.9.9, 149.112.112.112)
- OpenDNS (208.67.222.222, 208.67.220.220)
- Control D (76.76.2.0)
- Control D (76.76.2.0, 76.76.10.0)
- AdGuard DNS (94.140.14.14, 94.140.15.15)
- CleanBrowsing (185.228.168.9, 185.228.169.9)
- Level3 (4.2.2.1)

## Shell Completions

Expand Down
113 changes: 113 additions & 0 deletions cmd/vdns/cmd/benchmark.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package cmd

import (
"fmt"
"net"
"os"
"sort"
"strings"

"github.com/Vector-DNS/vdns-cli/internal/dns"
"github.com/Vector-DNS/vdns-cli/internal/output"
"github.com/Vector-DNS/vdns-cli/internal/validate"
"github.com/spf13/cobra"
)

var benchmarkCmd = &cobra.Command{
Use: "benchmark [domain]",
Short: "Measure DNS resolver response times",
Long: "Benchmarks all public resolvers by querying a domain and measuring response time. Defaults to querying google.com.",
Aliases: []string{"bench"},
Args: cobra.MaximumNArgs(1),
RunE: runBenchmark,
}

func init() {
rootCmd.AddCommand(benchmarkCmd)
}

func runBenchmark(cmd *cobra.Command, args []string) error {
pr := output.NewPrinter(jsonOutput, noColor)

domain := "google.com"
if len(args) == 1 {
vr, err := validate.Domain(args[0])
if err != nil {
pr.Error(err.Error())
return err
}
domain = vr.ASCII
}

results := dns.Benchmark(domain, dns.DefaultResolvers)

if pr.IsJSON() {
return pr.JSON(results)
}

fmt.Println()
fmt.Fprintf(os.Stdout, " %s\n", pr.Bold("DNS Resolver Benchmark"))
fmt.Fprintf(os.Stdout, " %s %s\n", pr.Dim("Query:"), domain)

sysResolver := dns.GetSystemResolver()
if dns.IsDefaultResolver() {
fmt.Fprintf(os.Stdout, " %s %s %s\n", pr.Dim("Resolver:"), sysResolver, pr.Dim("(default fallback)"))
} else {
fmt.Fprintf(os.Stdout, " %s %s\n", pr.Dim("System resolver:"), sysResolver)
}
fmt.Println()

sort.Slice(results, func(i, j int) bool {
if results[i].Success != results[j].Success {
return results[i].Success
}
return results[i].LatencyMs < results[j].LatencyMs
})

nameW := len("Resolver")
for _, r := range results {
name := fmt.Sprintf("%s (%s)", r.Resolver.Name, resolverIP(r.Resolver))
if len(name) > nameW {
nameW = len(name)
}
}

header := "Resolver" + strings.Repeat(" ", nameW-len("Resolver"))
fmt.Fprintf(os.Stdout, " %s %s\n", pr.Dim(header), pr.Dim("Latency"))
fmt.Fprintf(os.Stdout, " %s\n", pr.Dim(strings.Repeat("-", nameW+12)))

for _, r := range results {
name := fmt.Sprintf("%s (%s)", r.Resolver.Name, resolverIP(r.Resolver))
padded := name + strings.Repeat(" ", nameW-len(name))

if !r.Success {
fmt.Fprintf(os.Stdout, " %s %s\n",
padded,
pr.Color(output.Red, "FAILED: "+r.Error))
continue
}

color := output.Green
if r.LatencyMs > 100 {
color = output.Yellow
}
if r.LatencyMs > 300 {
color = output.Red
}

fmt.Fprintf(os.Stdout, " %s %s\n",
padded,
pr.Color(color, fmt.Sprintf("%dms", r.LatencyMs)))
}

fmt.Println()
return nil
}

func resolverIP(r dns.Resolver) string {
host, _, err := net.SplitHostPort(r.Address)
if err != nil {
return r.Address
}
return host
}
Loading
Loading