docs: comprehensive documentation for ExploreDNS #14
@@ -1,25 +1,255 @@
|
|||||||
# ExploreDNS
|
# ExploreDNS
|
||||||
|
|
||||||
DNS reconnaissance and exploration tool.
|
ExploreDNS is a command-line DNS reconnaissance and exploration tool that performs iterative DNS traversal from the root servers down to the target domain — the same way resolvers do, but with full visibility into every step.
|
||||||
|
|
||||||
## Build
|
Unlike a standard resolver query, ExploreDNS shows you the entire delegation path: which root server was queried, which TLD server was referred to, which authoritative server finally answered, and the probability that each path was followed. It also fingerprints nameservers via `version.bind` and detects CNAME chains, CNAME loops, and circular referrals.
|
||||||
|
|
||||||
|
Inspired by [dnstraverse](https://github.com/jeremyevans/dnstraverse).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Full delegation path visibility** — every referral hop from root to authoritative
|
||||||
|
- **CNAME chain following** and **loop detection**
|
||||||
|
- **DNAME synthesis** — generates implicit CNAME targets from DNAME records
|
||||||
|
- **Server fingerprinting** — queries `version.bind` CHAOS TXT on all encountered servers
|
||||||
|
- **JSON output** — machine-readable structured output for scripting and pipelines
|
||||||
|
- **Fast mode** — shared glue cache across branches (on by default)
|
||||||
|
- **Configurable transport** — UDP with TCP fallback, or TCP-only
|
||||||
|
- **All 13 root servers** — optionally query every root for maximum coverage
|
||||||
|
- **IPv6 support** — follow AAAA glue records when available
|
||||||
|
- **Probability weighting** — each result carries a probability so you know how authoritative it is
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### go install (recommended)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
go install github.com/hits/ExploreDNS/cmd/exploredns@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/hits/ExploreDNS.git
|
||||||
|
cd ExploreDNS
|
||||||
go build -o bin/exploredns ./cmd/exploredns
|
go build -o bin/exploredns ./cmd/exploredns
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the Makefile:
|
Or via the Makefile:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
The binary is placed at `bin/exploredns`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./bin/exploredns <domain>
|
# Basic A record traversal
|
||||||
|
exploredns www.example.com
|
||||||
|
|
||||||
|
# Query MX records
|
||||||
|
exploredns --type MX example.com
|
||||||
|
|
||||||
|
# JSON output for scripting
|
||||||
|
exploredns --json www.example.com | jq .summary
|
||||||
|
|
||||||
|
# Use all 13 root servers
|
||||||
|
exploredns --all-root-servers www.example.com
|
||||||
|
|
||||||
|
# TCP only (no UDP)
|
||||||
|
exploredns --allow-tcp --always-tcp www.example.com
|
||||||
|
|
||||||
|
# Quiet mode — results only, no headers
|
||||||
|
exploredns --quiet --no-show-progress --no-show-resolves --no-show-servers www.example.com
|
||||||
|
|
||||||
|
# Debug mode (shows config and traversal steps)
|
||||||
|
exploredns --debug www.example.com
|
||||||
|
|
||||||
|
# Library-level debug
|
||||||
|
exploredns -dd www.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic traversal
|
||||||
|
|
||||||
|
```
|
||||||
|
$ exploredns www.example.com
|
||||||
|
ExploreDNS - exploring: www.example.com (type: A)
|
||||||
|
1 198.41.0.4 (A)
|
||||||
|
2 192.5.6.30 (A)
|
||||||
|
3 199.43.135.53 (A)
|
||||||
|
4 93.184.216.34 (A)
|
||||||
|
4 100% answered with 93.184.216.34
|
||||||
|
|
||||||
|
The following servers were encountered:
|
||||||
|
a.root-servers.net: 198.41.0.4
|
||||||
|
a.iana-servers.net: 199.43.135.53
|
||||||
|
a.gtld-servers.net: 192.5.6.30
|
||||||
|
|
||||||
|
Results:
|
||||||
|
4 100% answered with 93.184.216.34
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
100% answered with 93.184.216.34
|
||||||
|
```
|
||||||
|
|
||||||
|
### MX records
|
||||||
|
|
||||||
|
```sh
|
||||||
|
exploredns --type MX gmail.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON output
|
||||||
|
|
||||||
|
```sh
|
||||||
|
exploredns --json www.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"domain": "www.example.com",
|
||||||
|
"query_type": "A",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"depth": 3,
|
||||||
|
"probability": 1,
|
||||||
|
"response_type": "answer",
|
||||||
|
"server": "93.184.216.34",
|
||||||
|
"answers": ["www.example.com. 3600 IN A 93.184.216.34"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": [
|
||||||
|
{"name": "a.root-servers.net", "ips": ["198.41.0.4"]},
|
||||||
|
{"name": "a.gtld-servers.net", "ips": ["192.5.6.30"]},
|
||||||
|
{"name": "a.iana-servers.net", "ips": ["199.43.135.53"]}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"answers": [{"rdata": "93.184.216.34", "probability": 1}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### All root servers
|
||||||
|
|
||||||
|
```sh
|
||||||
|
exploredns --all-root-servers www.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses all 13 root servers. Each branch carries a probability of approximately 1/13.
|
||||||
|
|
||||||
|
### Verbose mode
|
||||||
|
|
||||||
|
```sh
|
||||||
|
exploredns -v www.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Adds bailiwick information to each progress line.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage:
|
||||||
|
exploredns [flags] <domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Options
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--type` | `A` | Record type: `A`, `AAAA`, `NS`, `CNAME`, `MX`, `TXT`, `SOA`, `PTR`, `ANY` |
|
||||||
|
| `--root-server` | _(auto)_ | Override the root server IP address |
|
||||||
|
| `--all-root-servers` | `false` | Use all 13 root servers |
|
||||||
|
| `--root-aaaa` | `false` | Include IPv6 addresses of root servers |
|
||||||
|
| `--follow-aaaa` | `false` | Follow only AAAA glue records for referrals |
|
||||||
|
|
||||||
|
### Transport Options
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--udp-size` | `2048` | EDNS0 UDP payload size (512–4096) |
|
||||||
|
| `--allow-tcp` | `true` | TCP fallback when UDP response is truncated |
|
||||||
|
| `--always-tcp` | `false` | Always use TCP (requires `--allow-tcp`) |
|
||||||
|
| `--retries` | `2` | Number of retry attempts per query (0–10) |
|
||||||
|
|
||||||
|
### Traversal Options
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--max-depth` | `20` | Maximum referral depth (1–100) |
|
||||||
|
| `--fast` | `true` | Fast mode: sibling branches share a glue cache |
|
||||||
|
|
||||||
|
### Output Options
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--verbose`, `-v` | `false` | Verbose output (shows bailiwick on each line) |
|
||||||
|
| `--debug`, `-d` | `false` | Application debug output to stderr |
|
||||||
|
| `-dd` | `false` | Library-level debug (equivalent to `-d -d`) |
|
||||||
|
| `--quiet`, `-q` | `false` | Suppress introductory banner |
|
||||||
|
| `--json` | `false` | Output results as a single JSON document |
|
||||||
|
| `--show-progress` / `--no-show-progress` | on | Per-referral traversal progress lines |
|
||||||
|
| `--show-resolves` / `--no-show-resolves` | on | Nameserver address resolution detail |
|
||||||
|
| `--show-servers` / `--no-show-servers` | on | Encountered servers list |
|
||||||
|
| `--show-versions` / `--no-show-versions` | on | Server version.bind fingerprints |
|
||||||
|
| `--show-all-stats` / `--no-show-all-stats` | on | Per-result stats as they arrive |
|
||||||
|
| `--show-results` / `--no-show-results` | on | Terminal results section |
|
||||||
|
| `--show-summary-results` / `--no-show-summary-results` | on | Probability summary section |
|
||||||
|
|
||||||
|
> **Tip:** Set `NO_COLOR=1` in your environment to disable ANSI colour codes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Sections
|
||||||
|
|
||||||
|
### Traversal progress
|
||||||
|
|
||||||
|
```
|
||||||
|
1 198.41.0.4 (A)
|
||||||
|
2 192.5.6.30 (A)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line shows the hop number, the server IP, and the query type. Indentation reflects delegation depth.
|
||||||
|
|
||||||
|
### Servers list
|
||||||
|
|
||||||
|
Lists every nameserver encountered, mapped to its IP address, with optional `version.bind` fingerprints.
|
||||||
|
|
||||||
|
### Results
|
||||||
|
|
||||||
|
Terminal outcomes (answers, NXDOMAIN, SERVFAIL, etc.) for each leaf of the traversal tree.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Aggregates terminal results by response type, weighted by probability. Answer results are grouped by RDATA value.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/exploredns/ CLI entry point and flag parsing
|
||||||
|
internal/config/ Config struct, validation, and usage text
|
||||||
|
internal/dns/ DNS query primitives, response decoding, root discovery
|
||||||
|
internal/traverse/ Iterative DNS tree traversal engine
|
||||||
|
internal/fingerprint/ DNS server version fingerprinting
|
||||||
|
internal/output/ Text and JSON output formatters
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -28,16 +258,13 @@ make lint # run go vet
|
|||||||
make clean # remove build artifacts
|
make clean # remove build artifacts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
To run tests with the race detector:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -race ./...
|
||||||
```
|
```
|
||||||
cmd/exploredns/ CLI entry point
|
|
||||||
internal/dns/ DNS query operations
|
---
|
||||||
internal/traverse/ DNS tree traversal
|
|
||||||
internal/fingerprint/ DNS server fingerprinting
|
|
||||||
internal/output/ Result formatting and output
|
|
||||||
internal/config/ Configuration management
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+54
-13
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sentinel errors returned by validation and parsing functions.
|
||||||
var (
|
var (
|
||||||
ErrInvalidQueryType = errors.New("invalid query type")
|
ErrInvalidQueryType = errors.New("invalid query type")
|
||||||
ErrInvalidUDPSize = errors.New("UDP size must be between 512 and 4096")
|
ErrInvalidUDPSize = errors.New("UDP size must be between 512 and 4096")
|
||||||
@@ -20,22 +21,41 @@ var (
|
|||||||
ErrInvalidRootServer = errors.New("invalid root server IP address")
|
ErrInvalidRootServer = errors.New("invalid root server IP address")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config holds all runtime settings for ExploreDNS.
|
||||||
|
// Populate it from CLI flags, then call Validate before use.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
QueryType string
|
// QueryType is the DNS record type to query (e.g. "A", "MX", "TXT").
|
||||||
RootServer string
|
QueryType string
|
||||||
|
// RootServer overrides the root server IP used to begin traversal.
|
||||||
|
// Empty string means auto-discover via the system resolver.
|
||||||
|
RootServer string
|
||||||
|
// AllRootServers queries all 13 DNS root servers instead of one.
|
||||||
AllRootServers bool
|
AllRootServers bool
|
||||||
RootAAAA bool
|
// RootAAAA includes IPv6 addresses of root servers when true.
|
||||||
FollowAAAA bool
|
RootAAAA bool
|
||||||
UDPSize int
|
// FollowAAAA restricts referral following to AAAA glue records only.
|
||||||
AllowTCP bool
|
FollowAAAA bool
|
||||||
AlwaysTCP bool
|
// UDPSize is the EDNS0 advertised UDP payload size (512–4096 bytes).
|
||||||
MaxDepth int
|
UDPSize int
|
||||||
Retries int
|
// AllowTCP enables TCP fallback when a UDP response is truncated.
|
||||||
Fast bool
|
AllowTCP bool
|
||||||
Verbose bool
|
// AlwaysTCP forces all queries over TCP (requires AllowTCP = true).
|
||||||
Debug int
|
AlwaysTCP bool
|
||||||
Quiet bool
|
// MaxDepth limits the traversal depth (1–100).
|
||||||
|
MaxDepth int
|
||||||
|
// Retries is the number of times a failed query is retried (0–10).
|
||||||
|
Retries int
|
||||||
|
// Fast enables the shared-cache fast mode. When true, sibling branches
|
||||||
|
// inherit glue discovered by earlier branches, trading accuracy for speed.
|
||||||
|
Fast bool
|
||||||
|
// Verbose enables verbose output lines.
|
||||||
|
Verbose bool
|
||||||
|
// Debug controls debug verbosity: 0 = off, 1 = app debug, 2 = library debug.
|
||||||
|
Debug int
|
||||||
|
// Quiet suppresses supplementary informational output.
|
||||||
|
Quiet bool
|
||||||
|
|
||||||
|
// Output visibility flags — each controls a section of the output.
|
||||||
ShowProgress bool
|
ShowProgress bool
|
||||||
ShowResolves bool
|
ShowResolves bool
|
||||||
ShowServers bool
|
ShowServers bool
|
||||||
@@ -45,6 +65,8 @@ type Config struct {
|
|||||||
ShowSummaryResults bool
|
ShowSummaryResults bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseQueryType converts a case-insensitive query type string (e.g. "A", "MX")
|
||||||
|
// to its numeric DNS type constant. Returns ErrInvalidQueryType for unknown types.
|
||||||
func ParseQueryType(s string) (uint16, error) {
|
func ParseQueryType(s string) (uint16, error) {
|
||||||
s = strings.ToUpper(s)
|
s = strings.ToUpper(s)
|
||||||
switch s {
|
switch s {
|
||||||
@@ -71,6 +93,8 @@ func ParseQueryType(s string) (uint16, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseUDPSize parses a string as a UDP buffer size.
|
||||||
|
// Returns ErrInvalidUDPSize if the string is not an integer or is outside 512–4096.
|
||||||
func ParseUDPSize(s string) (int, error) {
|
func ParseUDPSize(s string) (int, error) {
|
||||||
var size int
|
var size int
|
||||||
if _, err := fmt.Sscanf(s, "%d", &size); err != nil {
|
if _, err := fmt.Sscanf(s, "%d", &size); err != nil {
|
||||||
@@ -82,6 +106,8 @@ func ParseUDPSize(s string) (int, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseMaxDepth parses a string as a traversal depth.
|
||||||
|
// Returns ErrInvalidMaxDepth if the string is not an integer or is outside 1–100.
|
||||||
func ParseMaxDepth(s string) (int, error) {
|
func ParseMaxDepth(s string) (int, error) {
|
||||||
var depth int
|
var depth int
|
||||||
if _, err := fmt.Sscanf(s, "%d", &depth); err != nil {
|
if _, err := fmt.Sscanf(s, "%d", &depth); err != nil {
|
||||||
@@ -93,6 +119,8 @@ func ParseMaxDepth(s string) (int, error) {
|
|||||||
return depth, nil
|
return depth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseRetries parses a string as a retry count.
|
||||||
|
// Returns ErrInvalidRetries if the string is not an integer or is outside 0–10.
|
||||||
func ParseRetries(s string) (int, error) {
|
func ParseRetries(s string) (int, error) {
|
||||||
var retries int
|
var retries int
|
||||||
if _, err := fmt.Sscanf(s, "%d", &retries); err != nil {
|
if _, err := fmt.Sscanf(s, "%d", &retries); err != nil {
|
||||||
@@ -104,6 +132,9 @@ func ParseRetries(s string) (int, error) {
|
|||||||
return retries, nil
|
return retries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks that all Config fields are within their accepted ranges and
|
||||||
|
// that flag combinations are valid (e.g. AlwaysTCP requires AllowTCP).
|
||||||
|
// Returns the first validation error encountered, or nil.
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
if _, err := ParseQueryType(c.QueryType); err != nil {
|
if _, err := ParseQueryType(c.QueryType); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -128,6 +159,8 @@ func (c *Config) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDomain returns the domain name from the positional CLI arguments.
|
||||||
|
// Returns ErrMissingDomain when args is empty.
|
||||||
func (c *Config) GetDomain(args []string) (string, error) {
|
func (c *Config) GetDomain(args []string) (string, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "", ErrMissingDomain
|
return "", ErrMissingDomain
|
||||||
@@ -135,6 +168,9 @@ func (c *Config) GetDomain(args []string) (string, error) {
|
|||||||
return args[0], nil
|
return args[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseRootServer parses the RootServer field as an IP address.
|
||||||
|
// Returns nil, nil when RootServer is empty (auto-discover mode).
|
||||||
|
// Returns ErrInvalidRootServer when the string is non-empty but not a valid IP.
|
||||||
func (c *Config) ParseRootServer() (net.IP, error) {
|
func (c *Config) ParseRootServer() (net.IP, error) {
|
||||||
if c.RootServer == "" {
|
if c.RootServer == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -158,6 +194,8 @@ func ParseDebugLevel(d, dd bool) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintUsage writes a grouped help message to stderr, listing every flag with
|
||||||
|
// its description. It is registered as flag.Usage by main.
|
||||||
func PrintUsage() {
|
func PrintUsage() {
|
||||||
fmt.Fprintf(os.Stderr, "ExploreDNS - DNS reconnaissance and exploration tool\n\n")
|
fmt.Fprintf(os.Stderr, "ExploreDNS - DNS reconnaissance and exploration tool\n\n")
|
||||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
||||||
@@ -211,6 +249,9 @@ func PrintUsage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a Config populated with sensible defaults:
|
||||||
|
// query type A, UDP size 2048, max depth 20, 2 retries, fast mode on,
|
||||||
|
// TCP fallback allowed, and all output sections visible.
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Package config defines the ExploreDNS runtime configuration, flag parsing
|
||||||
|
// helpers, input validation, and usage text.
|
||||||
|
//
|
||||||
|
// Config holds all settings that control traversal behaviour and output.
|
||||||
|
// DefaultConfig returns a ready-to-use Config with sensible defaults.
|
||||||
|
// After populating Config from CLI flags, call Validate to ensure
|
||||||
|
// all field values are within their accepted ranges.
|
||||||
|
//
|
||||||
|
// Helper functions such as ParseQueryType, ParseDebugLevel, and
|
||||||
|
// ParseRootServer are pure converters; they never read flag state.
|
||||||
|
package config
|
||||||
+34
-10
@@ -7,17 +7,20 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResponseClassification categorises a DNS response at a high level,
|
||||||
|
// independent of the raw RCODE.
|
||||||
type ResponseClassification int
|
type ResponseClassification int
|
||||||
|
|
||||||
|
// Classification constants, in order from most to least specific.
|
||||||
const (
|
const (
|
||||||
ResponseAnswer ResponseClassification = iota
|
ResponseAnswer ResponseClassification = iota // answer section contains records
|
||||||
ResponseReferral
|
ResponseReferral // non-authoritative NS referral
|
||||||
ResponseNODATA
|
ResponseNODATA // NOERROR with empty answer section
|
||||||
ResponseNXDOMAIN
|
ResponseNXDOMAIN // name does not exist (RCODE 3)
|
||||||
ResponseSERVFAIL
|
ResponseSERVFAIL // server failure (RCODE 2)
|
||||||
ResponseREFUSED
|
ResponseREFUSED // query refused (RCODE 5)
|
||||||
ResponseNOTIMPL
|
ResponseNOTIMPL // not implemented (RCODE 4)
|
||||||
ResponseOther
|
ResponseOther // any other RCODE
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rc ResponseClassification) String() string {
|
func (rc ResponseClassification) String() string {
|
||||||
@@ -41,6 +44,7 @@ func (rc ResponseClassification) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecodedResponse holds the extracted, structured fields from a raw *dns.Msg.
|
||||||
type DecodedResponse struct {
|
type DecodedResponse struct {
|
||||||
Rcode int
|
Rcode int
|
||||||
RcodeName string
|
RcodeName string
|
||||||
@@ -51,8 +55,10 @@ type DecodedResponse struct {
|
|||||||
Answers []dns.RR
|
Answers []dns.RR
|
||||||
Authority []dns.RR
|
Authority []dns.RR
|
||||||
Additional []dns.RR
|
Additional []dns.RR
|
||||||
CNAMEChain []string
|
// CNAMEChain contains the ordered CNAME targets from the answer section.
|
||||||
DNAMEMappings []DNAMEMapping
|
CNAMEChain []string
|
||||||
|
// DNAMEMappings contains any DNAME records for redirect synthesis.
|
||||||
|
DNAMEMappings []DNAMEMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNAMEMapping holds a DNAME record's owner and target for redirect synthesis.
|
// DNAMEMapping holds a DNAME record's owner and target for redirect synthesis.
|
||||||
@@ -61,6 +67,8 @@ type DNAMEMapping struct {
|
|||||||
Target string // e.g., "example.net."
|
Target string // e.g., "example.net."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecodeResponse converts a raw *dns.Msg into a DecodedResponse.
|
||||||
|
// Returns nil when msg is nil.
|
||||||
func DecodeResponse(msg *dns.Msg) *DecodedResponse {
|
func DecodeResponse(msg *dns.Msg) *DecodedResponse {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -171,10 +179,13 @@ func SynthesizeCNAMEFromDNAME(queryName, dnameOwner, dnameTarget string) string
|
|||||||
return prefix + target
|
return prefix + target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsTruncated reports whether msg has the TC (truncated) bit set.
|
||||||
func IsTruncated(msg *dns.Msg) bool {
|
func IsTruncated(msg *dns.Msg) bool {
|
||||||
return msg != nil && msg.Truncated
|
return msg != nil && msg.Truncated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RcodeName returns the string representation of the RCODE in msg (e.g. "NOERROR").
|
||||||
|
// Returns "UNKNOWN" when msg is nil.
|
||||||
func RcodeName(msg *dns.Msg) string {
|
func RcodeName(msg *dns.Msg) string {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return "UNKNOWN"
|
return "UNKNOWN"
|
||||||
@@ -182,6 +193,7 @@ func RcodeName(msg *dns.Msg) string {
|
|||||||
return dns.RcodeToString[msg.Rcode]
|
return dns.RcodeToString[msg.Rcode]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractAnswers returns the answer section of msg, or nil when msg is nil.
|
||||||
func ExtractAnswers(msg *dns.Msg) []dns.RR {
|
func ExtractAnswers(msg *dns.Msg) []dns.RR {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -189,6 +201,7 @@ func ExtractAnswers(msg *dns.Msg) []dns.RR {
|
|||||||
return msg.Answer
|
return msg.Answer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractAuthority returns the authority section of msg, or nil when msg is nil.
|
||||||
func ExtractAuthority(msg *dns.Msg) []dns.RR {
|
func ExtractAuthority(msg *dns.Msg) []dns.RR {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -196,6 +209,8 @@ func ExtractAuthority(msg *dns.Msg) []dns.RR {
|
|||||||
return msg.Ns
|
return msg.Ns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractCNAMEChain returns the ordered list of CNAME targets from the answer section.
|
||||||
|
// Returns nil when msg is nil.
|
||||||
func ExtractCNAMEChain(msg *dns.Msg) []string {
|
func ExtractCNAMEChain(msg *dns.Msg) []string {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -203,6 +218,8 @@ func ExtractCNAMEChain(msg *dns.Msg) []string {
|
|||||||
return extractCNAMEChain(msg)
|
return extractCNAMEChain(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsReferral reports whether msg is a non-authoritative NS referral
|
||||||
|
// (NOERROR, empty answer section, NS records in authority, AA=false).
|
||||||
func IsReferral(msg *dns.Msg) bool {
|
func IsReferral(msg *dns.Msg) bool {
|
||||||
if msg == nil || msg.Rcode != dns.RcodeSuccess || len(msg.Answer) > 0 {
|
if msg == nil || msg.Rcode != dns.RcodeSuccess || len(msg.Answer) > 0 {
|
||||||
return false
|
return false
|
||||||
@@ -210,6 +227,8 @@ func IsReferral(msg *dns.Msg) bool {
|
|||||||
return hasNSRecords(msg.Ns) && !msg.Authoritative
|
return hasNSRecords(msg.Ns) && !msg.Authoritative
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNODATA reports whether msg is a NODATA response
|
||||||
|
// (NOERROR with an empty answer section and no referral).
|
||||||
func IsNODATA(msg *dns.Msg) bool {
|
func IsNODATA(msg *dns.Msg) bool {
|
||||||
if msg == nil || msg.Rcode != dns.RcodeSuccess {
|
if msg == nil || msg.Rcode != dns.RcodeSuccess {
|
||||||
return false
|
return false
|
||||||
@@ -223,6 +242,8 @@ func IsNODATA(msg *dns.Msg) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasCNAMEChain reports whether msg contains at least one CNAME record in its
|
||||||
|
// answer section.
|
||||||
func HasCNAMEChain(msg *dns.Msg) bool {
|
func HasCNAMEChain(msg *dns.Msg) bool {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
return false
|
return false
|
||||||
@@ -230,6 +251,9 @@ func HasCNAMEChain(msg *dns.Msg) bool {
|
|||||||
return len(extractCNAMEChain(msg)) > 0
|
return len(extractCNAMEChain(msg)) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatRecord returns a human-readable representation of a DNS resource record
|
||||||
|
// in the form: NAME TTL CLASS TYPE RDATA.
|
||||||
|
// Returns an empty string when rr is nil.
|
||||||
func FormatRecord(rr dns.RR) string {
|
func FormatRecord(rr dns.RR) string {
|
||||||
if rr == nil {
|
if rr == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1 +1,11 @@
|
|||||||
|
// Package dns provides low-level DNS query primitives, response decoding, root
|
||||||
|
// server discovery, and type constants used throughout ExploreDNS.
|
||||||
|
//
|
||||||
|
// The package is organised around four concerns:
|
||||||
|
//
|
||||||
|
// - types.go — DNS record-type constants and helpers (QNameType, etc.)
|
||||||
|
// - query.go — Query / IterativeQuery and retry/back-off logic
|
||||||
|
// - resolver.go — Resolver interface plus BasicResolver and CachingResolver
|
||||||
|
// - decode.go — DecodeResponse, ResponseClassification, and related helpers
|
||||||
|
// - roots.go — DiscoverRoots for bootstrapping iterative traversal
|
||||||
package dns
|
package dns
|
||||||
|
|||||||
+23
-4
@@ -9,14 +9,22 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// QueryConfig controls the transport-level behaviour of DNS queries.
|
||||||
type QueryConfig struct {
|
type QueryConfig struct {
|
||||||
UDPSize int
|
// UDPSize is the EDNS0 advertised UDP payload size in bytes.
|
||||||
Timeout time.Duration
|
UDPSize int
|
||||||
Retries int
|
// Timeout is the per-attempt network timeout.
|
||||||
UseTCP bool
|
Timeout time.Duration
|
||||||
|
// Retries is the total number of query attempts (first try + retries - 1 extra attempts).
|
||||||
|
Retries int
|
||||||
|
// UseTCP forces all queries over TCP when true.
|
||||||
|
UseTCP bool
|
||||||
|
// AllowTCP enables automatic TCP retry when a UDP response is truncated.
|
||||||
AllowTCP bool
|
AllowTCP bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultQueryConfig returns a QueryConfig with sensible defaults:
|
||||||
|
// 2048-byte UDP buffer, 5-second timeout, 3 retries, UDP with TCP fallback.
|
||||||
func DefaultQueryConfig() *QueryConfig {
|
func DefaultQueryConfig() *QueryConfig {
|
||||||
return &QueryConfig{
|
return &QueryConfig{
|
||||||
UDPSize: DefaultEDNS0UDPSize(),
|
UDPSize: DefaultEDNS0UDPSize(),
|
||||||
@@ -27,8 +35,13 @@ func DefaultQueryConfig() *QueryConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExchangeFunc is a function that sends a DNS message to server and returns
|
||||||
|
// the response. Injecting an ExchangeFunc in tests avoids real network calls.
|
||||||
type ExchangeFunc func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error)
|
type ExchangeFunc func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error)
|
||||||
|
|
||||||
|
// Query sends a standard (recursion-desired) DNS query to server for name/qtype
|
||||||
|
// using the provided cfg. cfg may be nil; DefaultQueryConfig is used in that case.
|
||||||
|
// Retries with exponential back-off are performed on transient errors.
|
||||||
func Query(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error) {
|
func Query(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultQueryConfig()
|
cfg = DefaultQueryConfig()
|
||||||
@@ -75,6 +88,8 @@ func realExchange(ctx context.Context, server string, msg *dns.Msg, useTCP bool)
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryWithExchange is the testable core of Query. It uses exchangeFn instead
|
||||||
|
// of the real network, enabling deterministic unit tests.
|
||||||
func QueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig, exchangeFn ExchangeFunc) (*dns.Msg, error) {
|
func QueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig, exchangeFn ExchangeFunc) (*dns.Msg, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultQueryConfig()
|
cfg = DefaultQueryConfig()
|
||||||
@@ -127,6 +142,9 @@ func QueryWithExchange(ctx context.Context, server net.IP, name string, qtype ui
|
|||||||
return nil, fmt.Errorf("query %s %s failed after %d retries: %w", name, QNameType(qtype), cfg.Retries, lastErr)
|
return nil, fmt.Errorf("query %s %s failed after %d retries: %w", name, QNameType(qtype), cfg.Retries, lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IterativeQuery sends a non-recursive (RD=false) DNS query intended for
|
||||||
|
// authoritative nameservers. Use this during iterative traversal to prevent
|
||||||
|
// resolvers from answering on behalf of the authoritative server.
|
||||||
func IterativeQuery(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error) {
|
func IterativeQuery(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultQueryConfig()
|
cfg = DefaultQueryConfig()
|
||||||
@@ -137,6 +155,7 @@ func IterativeQuery(ctx context.Context, server net.IP, name string, qtype uint1
|
|||||||
return IterativeQueryWithExchange(ctx, server, name, qtype, cfg, realExchange)
|
return IterativeQueryWithExchange(ctx, server, name, qtype, cfg, realExchange)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IterativeQueryWithExchange is the testable core of IterativeQuery.
|
||||||
func IterativeQueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig, exchangeFn ExchangeFunc) (*dns.Msg, error) {
|
func IterativeQueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig, exchangeFn ExchangeFunc) (*dns.Msg, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultQueryConfig()
|
cfg = DefaultQueryConfig()
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Resolver is the interface for sending DNS queries.
|
||||||
|
// Implementations may cache, mock, or delegate to the real network.
|
||||||
type Resolver interface {
|
type Resolver interface {
|
||||||
Query(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error)
|
Query(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BasicResolver implements Resolver by issuing real UDP/TCP DNS queries.
|
||||||
type BasicResolver struct{}
|
type BasicResolver struct{}
|
||||||
|
|
||||||
|
// NewBasicResolver returns a BasicResolver ready for use.
|
||||||
func NewBasicResolver() *BasicResolver {
|
func NewBasicResolver() *BasicResolver {
|
||||||
return &BasicResolver{}
|
return &BasicResolver{}
|
||||||
}
|
}
|
||||||
@@ -40,6 +44,10 @@ func (e *cacheEntry) expired() bool {
|
|||||||
return time.Now().After(e.expireAt)
|
return time.Now().After(e.expireAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CachingResolver wraps another Resolver and caches successful responses.
|
||||||
|
// Responses are evicted based on the minimum DNS TTL in the message, with a
|
||||||
|
// configurable fallback default TTL for zero-TTL responses.
|
||||||
|
// All operations are safe for concurrent use.
|
||||||
type CachingResolver struct {
|
type CachingResolver struct {
|
||||||
inner Resolver
|
inner Resolver
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -47,6 +55,9 @@ type CachingResolver struct {
|
|||||||
defaultTTL time.Duration
|
defaultTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCachingResolver creates a CachingResolver wrapping inner.
|
||||||
|
// When inner is nil, a BasicResolver is used.
|
||||||
|
// Optional opts may be supplied to customise the default TTL (see WithDefaultTTL).
|
||||||
func NewCachingResolver(inner Resolver, opts ...CachingResolverOption) *CachingResolver {
|
func NewCachingResolver(inner Resolver, opts ...CachingResolverOption) *CachingResolver {
|
||||||
if inner == nil {
|
if inner == nil {
|
||||||
inner = NewBasicResolver()
|
inner = NewBasicResolver()
|
||||||
@@ -119,6 +130,7 @@ func (cr *CachingResolver) store(key cacheKey, msg *dns.Msg) {
|
|||||||
cr.mu.Unlock()
|
cr.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len returns the current number of entries in the cache, including expired ones.
|
||||||
func (cr *CachingResolver) Len() int {
|
func (cr *CachingResolver) Len() int {
|
||||||
cr.mu.RLock()
|
cr.mu.RLock()
|
||||||
n := len(cr.cache)
|
n := len(cr.cache)
|
||||||
@@ -126,12 +138,14 @@ func (cr *CachingResolver) Len() int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear removes all entries from the cache.
|
||||||
func (cr *CachingResolver) Clear() {
|
func (cr *CachingResolver) Clear() {
|
||||||
cr.mu.Lock()
|
cr.mu.Lock()
|
||||||
cr.cache = make(map[cacheKey]*cacheEntry)
|
cr.cache = make(map[cacheKey]*cacheEntry)
|
||||||
cr.mu.Unlock()
|
cr.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurgeExpired removes all expired entries from the cache and returns the count removed.
|
||||||
func (cr *CachingResolver) PurgeExpired() int {
|
func (cr *CachingResolver) PurgeExpired() int {
|
||||||
cr.mu.Lock()
|
cr.mu.Lock()
|
||||||
count := 0
|
count := 0
|
||||||
@@ -145,12 +159,15 @@ func (cr *CachingResolver) PurgeExpired() int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CachingResolverOption is a functional option for NewCachingResolver.
|
||||||
type CachingResolverOption func(*cachingResolverConfig)
|
type CachingResolverOption func(*cachingResolverConfig)
|
||||||
|
|
||||||
type cachingResolverConfig struct {
|
type cachingResolverConfig struct {
|
||||||
defaultTTL time.Duration
|
defaultTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDefaultTTL sets the TTL used for cache entries when the DNS response
|
||||||
|
// contains no TTL information (or when TTL is zero).
|
||||||
func WithDefaultTTL(d time.Duration) CachingResolverOption {
|
func WithDefaultTTL(d time.Duration) CachingResolverOption {
|
||||||
return func(c *cachingResolverConfig) {
|
return func(c *cachingResolverConfig) {
|
||||||
c.defaultTTL = d
|
c.defaultTTL = d
|
||||||
|
|||||||
+19
-2
@@ -10,12 +10,18 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RootServer holds the name and IP addresses of a DNS root nameserver.
|
||||||
type RootServer struct {
|
type RootServer struct {
|
||||||
|
// Name is the FQDN of the root nameserver (e.g. "a.root-servers.net.").
|
||||||
Name string
|
Name string
|
||||||
|
// IPv4 holds the IPv4 addresses for the server.
|
||||||
IPv4 []net.IP
|
IPv4 []net.IP
|
||||||
|
// IPv6 holds the IPv6 addresses for the server.
|
||||||
IPv6 []net.IP
|
IPv6 []net.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllIPs returns all IP addresses for the server.
|
||||||
|
// When includeAAAA is false, only IPv4 addresses are returned.
|
||||||
func (rs *RootServer) AllIPs(includeAAAA bool) []net.IP {
|
func (rs *RootServer) AllIPs(includeAAAA bool) []net.IP {
|
||||||
var ips []net.IP
|
var ips []net.IP
|
||||||
ips = append(ips, rs.IPv4...)
|
ips = append(ips, rs.IPv4...)
|
||||||
@@ -25,12 +31,19 @@ func (rs *RootServer) AllIPs(includeAAAA bool) []net.IP {
|
|||||||
return ips
|
return ips
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RootDiscoveryConfig controls how DiscoverRoots selects root servers.
|
||||||
type RootDiscoveryConfig struct {
|
type RootDiscoveryConfig struct {
|
||||||
Server string
|
// Server overrides which root server is used. An empty string means
|
||||||
AllRoots bool
|
// auto-select the first root server returned by the system resolver.
|
||||||
|
Server string
|
||||||
|
// AllRoots queries all 13 root servers instead of just one.
|
||||||
|
AllRoots bool
|
||||||
|
// IncludeAAAA includes IPv6 addresses of root servers when true.
|
||||||
IncludeAAAA bool
|
IncludeAAAA bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultRootDiscoveryConfig returns a RootDiscoveryConfig that auto-selects a
|
||||||
|
// single IPv4-only root server.
|
||||||
func DefaultRootDiscoveryConfig() *RootDiscoveryConfig {
|
func DefaultRootDiscoveryConfig() *RootDiscoveryConfig {
|
||||||
return &RootDiscoveryConfig{
|
return &RootDiscoveryConfig{
|
||||||
AllRoots: false,
|
AllRoots: false,
|
||||||
@@ -38,6 +51,10 @@ func DefaultRootDiscoveryConfig() *RootDiscoveryConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiscoverRoots discovers DNS root servers to use as traversal starting points.
|
||||||
|
// When cfg.Server is set, that specific root server is used.
|
||||||
|
// When cfg.AllRoots is true, all 13 root servers are returned.
|
||||||
|
// Otherwise, a single root server is selected from the system resolver's NS response.
|
||||||
func DiscoverRoots(ctx context.Context, cfg *RootDiscoveryConfig) ([]RootServer, error) {
|
func DiscoverRoots(ctx context.Context, cfg *RootDiscoveryConfig) ([]RootServer, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultRootDiscoveryConfig()
|
cfg = DefaultRootDiscoveryConfig()
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DNS record-type constants mirroring github.com/miekg/dns values.
|
||||||
|
// Using local aliases keeps the rest of ExploreDNS independent of the
|
||||||
|
// upstream library's type system.
|
||||||
const (
|
const (
|
||||||
TypeA uint16 = dns.TypeA
|
TypeA uint16 = dns.TypeA
|
||||||
TypeAAAA uint16 = dns.TypeAAAA
|
TypeAAAA uint16 = dns.TypeAAAA
|
||||||
@@ -17,6 +20,7 @@ const (
|
|||||||
TypeANY uint16 = dns.TypeANY
|
TypeANY uint16 = dns.TypeANY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// QNameTypes maps numeric DNS type constants to their canonical uppercase names.
|
||||||
var QNameTypes = map[uint16]string{
|
var QNameTypes = map[uint16]string{
|
||||||
TypeA: "A",
|
TypeA: "A",
|
||||||
TypeAAAA: "AAAA",
|
TypeAAAA: "AAAA",
|
||||||
@@ -30,6 +34,8 @@ var QNameTypes = map[uint16]string{
|
|||||||
TypeANY: "ANY",
|
TypeANY: "ANY",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QNameType returns the string name for a numeric DNS query type.
|
||||||
|
// Falls back to the upstream dns.TypeToString map for types not in QNameTypes.
|
||||||
func QNameType(qtype uint16) string {
|
func QNameType(qtype uint16) string {
|
||||||
if name, ok := QNameTypes[qtype]; ok {
|
if name, ok := QNameTypes[qtype]; ok {
|
||||||
return name
|
return name
|
||||||
@@ -37,10 +43,12 @@ func QNameType(qtype uint16) string {
|
|||||||
return dns.TypeToString[qtype]
|
return dns.TypeToString[qtype]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultEDNS0UDPSize returns the default EDNS0 advertised UDP payload size (2048 bytes).
|
||||||
func DefaultEDNS0UDPSize() int {
|
func DefaultEDNS0UDPSize() int {
|
||||||
return 2048
|
return 2048
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MinEDNS0UDPSize returns the minimum accepted EDNS0 UDP payload size (512 bytes).
|
||||||
func MinEDNS0UDPSize() int {
|
func MinEDNS0UDPSize() int {
|
||||||
return 512
|
return 512
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Package fingerprint identifies DNS server software by sending a
|
||||||
|
// version.bind CHAOS TXT query to each server IP address.
|
||||||
|
//
|
||||||
|
// The Fingerprinter type caches results so repeated queries for the same IP
|
||||||
|
// are answered from memory without a network round-trip.
|
||||||
|
// FingerprintAll queries a list of IPs concurrently and returns a map of
|
||||||
|
// IP string → version string. Servers that do not support version.bind, or
|
||||||
|
// that time out, map to the empty string.
|
||||||
|
package fingerprint
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Package output formats ExploreDNS traversal results for human-readable text
|
||||||
|
// and machine-readable JSON output.
|
||||||
|
//
|
||||||
|
// The Formatter interface is the single point of contact for the traversal
|
||||||
|
// engine. Two implementations are provided:
|
||||||
|
//
|
||||||
|
// - text formatter (default) — coloured, human-readable output.
|
||||||
|
// - JSON formatter (--json flag) — structured JSON suitable for piping.
|
||||||
|
//
|
||||||
|
// NewFormatter selects the right implementation based on Config.Format.
|
||||||
|
// RunTraversal is the high-level entry point: it attaches event hooks,
|
||||||
|
// executes the traversal, fingerprints encountered servers, and writes the
|
||||||
|
// final summary and flush.
|
||||||
|
package output
|
||||||
@@ -8,34 +8,53 @@ import (
|
|||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"github.com/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Format selects the output format.
|
||||||
type Format int
|
type Format int
|
||||||
|
|
||||||
|
// Output format constants.
|
||||||
const (
|
const (
|
||||||
FormatText Format = iota
|
FormatText Format = iota // human-readable coloured text (default)
|
||||||
FormatJSON
|
FormatJSON // machine-readable JSON
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config carries settings that control what the formatter emits.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Format Format
|
// Format selects text or JSON output.
|
||||||
Domain string
|
Format Format
|
||||||
QueryType string
|
// Domain is the queried domain name, included in JSON output.
|
||||||
ShowProgress bool
|
Domain string
|
||||||
ShowResolves bool
|
// QueryType is the record type queried, included in JSON output.
|
||||||
ShowServers bool
|
QueryType string
|
||||||
ShowVersions bool
|
// ShowProgress enables per-referral progress lines.
|
||||||
ShowAllStats bool
|
ShowProgress bool
|
||||||
ShowResults bool
|
// ShowResolves enables nameserver resolution detail lines.
|
||||||
|
ShowResolves bool
|
||||||
|
// ShowServers enables the server list section.
|
||||||
|
ShowServers bool
|
||||||
|
// ShowVersions enables version.bind fingerprint display alongside servers.
|
||||||
|
ShowVersions bool
|
||||||
|
// ShowAllStats enables per-result stats as they arrive (in text mode).
|
||||||
|
ShowAllStats bool
|
||||||
|
// ShowResults enables the terminal-results section.
|
||||||
|
ShowResults bool
|
||||||
|
// ShowSummaryResults enables the probability summary section.
|
||||||
ShowSummaryResults bool
|
ShowSummaryResults bool
|
||||||
Verbose bool
|
// Verbose enables additional detail in progress lines.
|
||||||
Quiet bool
|
Verbose bool
|
||||||
Color bool
|
// Quiet suppresses the introductory banner line.
|
||||||
Debug int
|
Quiet bool
|
||||||
|
// Color enables ANSI terminal colour codes in text output.
|
||||||
|
Color bool
|
||||||
|
// Debug controls debug verbosity for the formatter itself.
|
||||||
|
Debug int
|
||||||
|
|
||||||
// Fingerprints maps server IP strings to their version.bind version strings.
|
// Fingerprints maps server IP strings to their version.bind version strings.
|
||||||
// Populated by RunTraversal when ShowVersions and ShowServers are both true.
|
// Populated by RunTraversal when ShowVersions and ShowServers are both true.
|
||||||
Fingerprints map[string]string
|
Fingerprints map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a Config with all output sections enabled, text format,
|
||||||
|
// and color determined by the NO_COLOR environment variable.
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Format: FormatText,
|
Format: FormatText,
|
||||||
@@ -50,14 +69,23 @@ func DefaultConfig() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Formatter is the interface that both the text and JSON output backends implement.
|
||||||
|
// Each method is called by the traversal hooks or by RunTraversal.
|
||||||
type Formatter interface {
|
type Formatter interface {
|
||||||
|
// WriteProgress is called at EventStart for each Referral.
|
||||||
WriteProgress(event traverse.TraversalEvent) error
|
WriteProgress(event traverse.TraversalEvent) error
|
||||||
|
// WriteResolve is called at EventStart for nameserver resolution sub-steps.
|
||||||
WriteResolve(event traverse.TraversalEvent) error
|
WriteResolve(event traverse.TraversalEvent) error
|
||||||
|
// WriteResult is called at EventComplete for each TraversalResult.
|
||||||
WriteResult(result traverse.TraversalResult) error
|
WriteResult(result traverse.TraversalResult) error
|
||||||
|
// WriteSummary is called once after all results are collected.
|
||||||
WriteSummary(results []traverse.TraversalResult) error
|
WriteSummary(results []traverse.TraversalResult) error
|
||||||
|
// Flush finalises output (e.g. writes buffered JSON to the writer).
|
||||||
Flush() error
|
Flush() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFormatter returns the appropriate Formatter (text or JSON) based on cfg.Format.
|
||||||
|
// A nil cfg uses DefaultConfig. A nil w uses os.Stdout.
|
||||||
func NewFormatter(cfg *Config, w io.Writer) Formatter {
|
func NewFormatter(cfg *Config, w io.Writer) Formatter {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultConfig()
|
cfg = DefaultConfig()
|
||||||
@@ -71,6 +99,8 @@ func NewFormatter(cfg *Config, w io.Writer) Formatter {
|
|||||||
return newTextFormatter(cfg, w)
|
return newTextFormatter(cfg, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttachHooks creates a TraverserHooks that routes traversal events to formatter
|
||||||
|
// according to cfg visibility settings. Returns nil when cfg or formatter is nil.
|
||||||
func AttachHooks(cfg *Config, formatter Formatter) *traverse.TraverserHooks {
|
func AttachHooks(cfg *Config, formatter Formatter) *traverse.TraverserHooks {
|
||||||
if cfg == nil || formatter == nil {
|
if cfg == nil || formatter == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import (
|
|||||||
"github.com/hits/ExploreDNS/internal/traverse"
|
"github.com/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RunTraversal is the high-level entry point that wires together a Traverser,
|
||||||
|
// a Config, and a Formatter. It:
|
||||||
|
//
|
||||||
|
// 1. Attaches output hooks to traverser so events are formatted in real time.
|
||||||
|
// 2. Calls traverser.Traverse(ctx, domain) to perform the traversal.
|
||||||
|
// 3. Optionally fingerprints encountered servers (when ShowVersions && ShowServers).
|
||||||
|
// 4. Writes the summary and flushes the formatter.
|
||||||
|
//
|
||||||
|
// Returns all TraversalResults and any error from the traversal or output.
|
||||||
func RunTraversal(ctx context.Context, traverser *traverse.Traverser, cfg *Config, formatter Formatter, domain string) ([]traverse.TraversalResult, error) {
|
func RunTraversal(ctx context.Context, traverser *traverse.Traverser, cfg *Config, formatter Formatter, domain string) ([]traverse.TraversalResult, error) {
|
||||||
if traverser == nil {
|
if traverser == nil {
|
||||||
return nil, fmt.Errorf("traverser is required")
|
return nil, fmt.Errorf("traverser is required")
|
||||||
|
|||||||
@@ -21,11 +21,18 @@ type answerEntry struct {
|
|||||||
RRs []string
|
RRs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SummaryStats holds the aggregated probability statistics computed from a set
|
||||||
|
// of traversal results.
|
||||||
type SummaryStats struct {
|
type SummaryStats struct {
|
||||||
ByType map[string]float64
|
// ByType maps ResponseType strings (e.g. "nxdomain", "servfail") to their
|
||||||
|
// cumulative probability weight.
|
||||||
|
ByType map[string]float64
|
||||||
|
// Answers contains per-RDATA probability statistics for answer results.
|
||||||
Answers []answerEntry
|
Answers []answerEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ComputeSummary aggregates terminal TraversalResults into a SummaryStats.
|
||||||
|
// Returns nil when there are no terminal results to summarise.
|
||||||
func ComputeSummary(results []traverse.TraversalResult) *SummaryStats {
|
func ComputeSummary(results []traverse.TraversalResult) *SummaryStats {
|
||||||
stats := &SummaryStats{
|
stats := &SummaryStats{
|
||||||
ByType: make(map[string]float64),
|
ByType: make(map[string]float64),
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import (
|
|||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InfoCache is a two-level (parent/child) concurrent cache for NS records and
|
||||||
|
// glue addresses discovered during traversal.
|
||||||
|
//
|
||||||
|
// Lookups walk the parent chain: a child cache falls back to its parent when
|
||||||
|
// no local entry is found. Writes always go to the local cache, never to the
|
||||||
|
// parent. This makes it safe to give sibling branches separate child caches
|
||||||
|
// that share the root cache read-only in fast mode.
|
||||||
type InfoCache struct {
|
type InfoCache struct {
|
||||||
parent *InfoCache
|
parent *InfoCache
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -15,6 +22,8 @@ type InfoCache struct {
|
|||||||
glue map[string][]net.IP
|
glue map[string][]net.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewInfoCache creates an InfoCache with an optional parent.
|
||||||
|
// Pass nil for a root-level cache with no parent.
|
||||||
func NewInfoCache(parent *InfoCache) *InfoCache {
|
func NewInfoCache(parent *InfoCache) *InfoCache {
|
||||||
return &InfoCache{
|
return &InfoCache{
|
||||||
parent: parent,
|
parent: parent,
|
||||||
@@ -23,6 +32,8 @@ func NewInfoCache(parent *InfoCache) *InfoCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StoreNS records the nameserver names for zone in the local cache.
|
||||||
|
// Duplicate names within a zone are deduplicated.
|
||||||
func (c *InfoCache) StoreNS(zone string, nameservers []string) {
|
func (c *InfoCache) StoreNS(zone string, nameservers []string) {
|
||||||
if len(nameservers) == 0 {
|
if len(nameservers) == 0 {
|
||||||
return
|
return
|
||||||
@@ -40,6 +51,8 @@ func (c *InfoCache) StoreNS(zone string, nameservers []string) {
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupNS returns the cached nameserver names for zone,
|
||||||
|
// walking the parent chain when no local entry is found.
|
||||||
func (c *InfoCache) LookupNS(zone string) []string {
|
func (c *InfoCache) LookupNS(zone string) []string {
|
||||||
zone = normalize(zone)
|
zone = normalize(zone)
|
||||||
if names := c.localNS(zone); len(names) > 0 {
|
if names := c.localNS(zone); len(names) > 0 {
|
||||||
@@ -63,6 +76,8 @@ func (c *InfoCache) localNS(zone string) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StoreGlue records the IP addresses for a nameserver hostname in the local cache.
|
||||||
|
// Duplicate addresses are deduplicated.
|
||||||
func (c *InfoCache) StoreGlue(name string, addrs []net.IP) {
|
func (c *InfoCache) StoreGlue(name string, addrs []net.IP) {
|
||||||
if len(addrs) == 0 {
|
if len(addrs) == 0 {
|
||||||
return
|
return
|
||||||
@@ -80,6 +95,8 @@ func (c *InfoCache) StoreGlue(name string, addrs []net.IP) {
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupGlue returns the cached IP addresses for a nameserver hostname,
|
||||||
|
// walking the parent chain when no local entry is found.
|
||||||
func (c *InfoCache) LookupGlue(name string) []net.IP {
|
func (c *InfoCache) LookupGlue(name string) []net.IP {
|
||||||
name = normalize(name)
|
name = normalize(name)
|
||||||
if addrs := c.localGlue(name); len(addrs) > 0 {
|
if addrs := c.localGlue(name); len(addrs) > 0 {
|
||||||
@@ -103,16 +120,20 @@ func (c *InfoCache) localGlue(name string) []net.IP {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Child creates a new InfoCache that inherits from c.
|
||||||
|
// The child reads from c when a local lookup misses, but never writes to c.
|
||||||
func (c *InfoCache) Child() *InfoCache {
|
func (c *InfoCache) Child() *InfoCache {
|
||||||
return NewInfoCache(c)
|
return NewInfoCache(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NSCount returns the number of zone→nameservers entries in the local cache.
|
||||||
func (c *InfoCache) NSCount() int {
|
func (c *InfoCache) NSCount() int {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
return len(c.ns)
|
return len(c.ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GlueCount returns the number of nameserver→addresses entries in the local cache.
|
||||||
func (c *InfoCache) GlueCount() int {
|
func (c *InfoCache) GlueCount() int {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
package traverse
|
package traverse
|
||||||
|
|
||||||
|
// EventStage indicates whether a TraversalEvent is fired at the start or
|
||||||
|
// completion of processing a Referral.
|
||||||
type EventStage int
|
type EventStage int
|
||||||
|
|
||||||
|
// Event stage constants.
|
||||||
const (
|
const (
|
||||||
EventStart EventStage = iota
|
EventStart EventStage = iota // fired when a Referral is about to be queried
|
||||||
EventComplete
|
EventComplete // fired after the Response has been produced
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TraversalEvent carries context for a single traversal event delivered to
|
||||||
|
// the OnEvent hook.
|
||||||
type TraversalEvent struct {
|
type TraversalEvent struct {
|
||||||
Stage EventStage
|
// Stage is EventStart or EventComplete.
|
||||||
Result TraversalResult
|
Stage EventStage
|
||||||
|
// Result carries the Referral and (for EventComplete) the Response.
|
||||||
|
Result TraversalResult
|
||||||
|
// IsResolve is true when the event relates to a nameserver address
|
||||||
|
// resolution sub-traversal rather than the main traversal.
|
||||||
IsResolve bool
|
IsResolve bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EventHandler is the function signature for traversal event callbacks.
|
||||||
type EventHandler func(TraversalEvent)
|
type EventHandler func(TraversalEvent)
|
||||||
|
|
||||||
|
// TraverserHooks holds the optional event callback for a Traverser.
|
||||||
|
// Assigning OnEvent enables progress and result notifications.
|
||||||
type TraverserHooks struct {
|
type TraverserHooks struct {
|
||||||
OnEvent EventHandler
|
OnEvent EventHandler
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import (
|
|||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResolutionState tracks whether a Referral's nameserver addresses have been resolved.
|
||||||
type ResolutionState int
|
type ResolutionState int
|
||||||
|
|
||||||
|
// Resolution state constants.
|
||||||
const (
|
const (
|
||||||
StateUnresolved ResolutionState = iota
|
StateUnresolved ResolutionState = iota // nameserver addresses are not yet known
|
||||||
StateResolving
|
StateResolving // address resolution is in progress
|
||||||
StateResolved
|
StateResolved // addresses are available in Addresses
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s ResolutionState) String() string {
|
func (s ResolutionState) String() string {
|
||||||
@@ -32,19 +34,32 @@ func (s ResolutionState) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Referral represents a pending DNS query: a (name, qtype) pair delegated to a
|
||||||
|
// set of nameserver addresses. Referrals form a linked-list chain through
|
||||||
|
// Parent, enabling loop and depth detection.
|
||||||
type Referral struct {
|
type Referral struct {
|
||||||
Name string
|
// Name is the fully-qualified domain name being queried.
|
||||||
Qtype uint16
|
Name string
|
||||||
Qclass uint16
|
// Qtype is the DNS record type being queried.
|
||||||
|
Qtype uint16
|
||||||
|
// Qclass is the DNS class (always ClassINET in practice).
|
||||||
|
Qclass uint16
|
||||||
|
// Bailiwick is the zone that delegated this referral.
|
||||||
Bailiwick string
|
Bailiwick string
|
||||||
|
|
||||||
|
// Addresses holds the resolved IP addresses for this nameserver referral.
|
||||||
Addresses []net.IP
|
Addresses []net.IP
|
||||||
State ResolutionState
|
// State tracks whether Addresses have been resolved.
|
||||||
|
State ResolutionState
|
||||||
|
|
||||||
|
// NSName is the nameserver hostname (before IP resolution).
|
||||||
NSName string
|
NSName string
|
||||||
|
// Parent is the Referral that triggered this one, or nil for the root.
|
||||||
Parent *Referral
|
Parent *Referral
|
||||||
Depth int
|
// Depth is the number of referral hops from the root.
|
||||||
Prob float64
|
Depth int
|
||||||
|
// Prob is the probability weight for this branch (product of 1/fanout at each step).
|
||||||
|
Prob float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// idnaLookup is the IDN lookup profile used to convert internationalised domain
|
// idnaLookup is the IDN lookup profile used to convert internationalised domain
|
||||||
@@ -70,6 +85,9 @@ func toASCII(name string) string {
|
|||||||
return ascii
|
return ascii
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewReferral creates a Referral for (name, qtype) within bailiwick, at the
|
||||||
|
// given depth and probability. The name and bailiwick are normalised to
|
||||||
|
// lowercase FQDN, and internationalised labels are converted to punycode.
|
||||||
func NewReferral(name string, qtype uint16, bailiwick string, depth int, prob float64, parent *Referral) *Referral {
|
func NewReferral(name string, qtype uint16, bailiwick string, depth int, prob float64, parent *Referral) *Referral {
|
||||||
return &Referral{
|
return &Referral{
|
||||||
Name: miekgdns.Fqdn(strings.ToLower(toASCII(name))),
|
Name: miekgdns.Fqdn(strings.ToLower(toASCII(name))),
|
||||||
@@ -83,6 +101,8 @@ func NewReferral(name string, qtype uint16, bailiwick string, depth int, prob fl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InBailiwick reports whether name is within this referral's bailiwick zone.
|
||||||
|
// A root bailiwick ("." or "") is treated as matching everything.
|
||||||
func (r *Referral) InBailiwick(name string) bool {
|
func (r *Referral) InBailiwick(name string) bool {
|
||||||
if r.Bailiwick == "" || r.Bailiwick == "." {
|
if r.Bailiwick == "" || r.Bailiwick == "." {
|
||||||
return true
|
return true
|
||||||
@@ -91,10 +111,12 @@ func (r *Referral) InBailiwick(name string) bool {
|
|||||||
return miekgdns.IsSubDomain(r.Bailiwick, fqdn)
|
return miekgdns.IsSubDomain(r.Bailiwick, fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasAddresses reports whether at least one nameserver IP address is known.
|
||||||
func (r *Referral) HasAddresses() bool {
|
func (r *Referral) HasAddresses() bool {
|
||||||
return len(r.Addresses) > 0
|
return len(r.Addresses) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAddresses stores addrs and updates State accordingly.
|
||||||
func (r *Referral) SetAddresses(addrs []net.IP) {
|
func (r *Referral) SetAddresses(addrs []net.IP) {
|
||||||
r.Addresses = addrs
|
r.Addresses = addrs
|
||||||
if len(addrs) > 0 {
|
if len(addrs) > 0 {
|
||||||
@@ -104,6 +126,8 @@ func (r *Referral) SetAddresses(addrs []net.IP) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CircularReferralError is returned when a referral chain revisits a nameserver,
|
||||||
|
// indicating a circular delegation.
|
||||||
type CircularReferralError struct {
|
type CircularReferralError struct {
|
||||||
Name string
|
Name string
|
||||||
Chain []string
|
Chain []string
|
||||||
@@ -113,6 +137,8 @@ func (e *CircularReferralError) Error() string {
|
|||||||
return fmt.Sprintf("circular referral detected for %s: %v", e.Name, e.Chain)
|
return fmt.Sprintf("circular referral detected for %s: %v", e.Name, e.Chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnresolvableNameserverError is returned when a nameserver hostname cannot be
|
||||||
|
// resolved to any IP address.
|
||||||
type UnresolvableNameserverError struct {
|
type UnresolvableNameserverError struct {
|
||||||
Name string
|
Name string
|
||||||
Reason string
|
Reason string
|
||||||
@@ -122,6 +148,9 @@ func (e *UnresolvableNameserverError) Error() string {
|
|||||||
return fmt.Sprintf("unresolvable nameserver %s: %s", e.Name, e.Reason)
|
return fmt.Sprintf("unresolvable nameserver %s: %s", e.Name, e.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve attempts to resolve the nameserver addresses for this Referral by
|
||||||
|
// performing a fresh iterative traversal. It stores the found addresses and
|
||||||
|
// updates State. Returns an error when resolution fails.
|
||||||
func (r *Referral) Resolve(ctx context.Context, traverser *Traverser, cache *InfoCache, visited map[string]bool, depth int) error {
|
func (r *Referral) Resolve(ctx context.Context, traverser *Traverser, cache *InfoCache, visited map[string]bool, depth int) error {
|
||||||
if r.HasAddresses() {
|
if r.HasAddresses() {
|
||||||
r.State = StateResolved
|
r.State = StateResolved
|
||||||
|
|||||||
@@ -7,19 +7,21 @@ import (
|
|||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResponseType classifies the outcome of querying a single Referral.
|
||||||
type ResponseType int
|
type ResponseType int
|
||||||
|
|
||||||
|
// Response type constants used to drive traversal logic.
|
||||||
const (
|
const (
|
||||||
RespReferral ResponseType = iota
|
RespReferral ResponseType = iota // server returned an NS referral
|
||||||
RespAnswer
|
RespAnswer // server returned a final answer
|
||||||
RespCNAMEFollow
|
RespCNAMEFollow // answer contains a CNAME requiring further traversal
|
||||||
RespNODATA
|
RespNODATA // NOERROR with no matching records
|
||||||
RespNXDOMAIN
|
RespNXDOMAIN // name does not exist
|
||||||
RespSERVFAIL
|
RespSERVFAIL // server failure
|
||||||
RespREFUSED
|
RespREFUSED // query refused
|
||||||
RespNOTIMPL
|
RespNOTIMPL // query type not implemented
|
||||||
RespCNAMELoop
|
RespCNAMELoop // CNAME chain revisits a name already in the chain
|
||||||
RespError
|
RespError // transport or decoding error
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rt ResponseType) String() string {
|
func (rt ResponseType) String() string {
|
||||||
@@ -49,15 +51,25 @@ func (rt ResponseType) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Response is the result of querying a single Referral against a specific
|
||||||
|
// nameserver. It contains the decoded DNS message, the classified ResponseType,
|
||||||
|
// and any error message for display.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Referral *Referral
|
// Referral is the query this response corresponds to.
|
||||||
Server net.IP
|
Referral *Referral
|
||||||
Cache *InfoCache
|
// Server is the nameserver IP that was queried.
|
||||||
Decoded *dns.DecodedResponse
|
Server net.IP
|
||||||
Type ResponseType
|
// Cache is the InfoCache used during processing (for glue resolution).
|
||||||
|
Cache *InfoCache
|
||||||
|
// Decoded holds the structured DNS response fields.
|
||||||
|
Decoded *dns.DecodedResponse
|
||||||
|
// Type is the high-level classification of this response.
|
||||||
|
Type ResponseType
|
||||||
|
// ErrorMessage is a human-readable description when Type is RespError or RespCNAMELoop.
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewResponse creates a Response for the given Referral and server.
|
||||||
func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
|
func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
|
||||||
return &Response{
|
return &Response{
|
||||||
Referral: ref,
|
Referral: ref,
|
||||||
@@ -66,6 +78,9 @@ func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process decodes msg, classifies it, and populates r.Type and r.Decoded.
|
||||||
|
// Synthesises CNAME records from DNAME mappings when no explicit CNAME is present.
|
||||||
|
// Returns r for chaining.
|
||||||
func (r *Response) Process(msg *miekgdns.Msg) *Response {
|
func (r *Response) Process(msg *miekgdns.Msg) *Response {
|
||||||
if msg == nil {
|
if msg == nil {
|
||||||
r.Type = RespError
|
r.Type = RespError
|
||||||
@@ -133,6 +148,10 @@ func (r *Response) hasFinalAnswer() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChildReferrals returns the set of child Referrals implied by a referral
|
||||||
|
// response. It extracts nameservers from the authority section, resolves
|
||||||
|
// any glue from the additional section, and sets Prob proportionally.
|
||||||
|
// Returns nil when Type != RespReferral.
|
||||||
func (r *Response) ChildReferrals() []*Referral {
|
func (r *Response) ChildReferrals() []*Referral {
|
||||||
if r.Type != RespReferral {
|
if r.Type != RespReferral {
|
||||||
return nil
|
return nil
|
||||||
@@ -177,6 +196,8 @@ func (r *Response) ChildReferrals() []*Referral {
|
|||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CNAMEFollowReferral constructs a follow-up Referral targeting the last CNAME
|
||||||
|
// in the chain. Returns nil when Type != RespCNAMEFollow.
|
||||||
func (r *Response) CNAMEFollowReferral() *Referral {
|
func (r *Response) CNAMEFollowReferral() *Referral {
|
||||||
if r.Type != RespCNAMEFollow || len(r.Decoded.CNAMEChain) == 0 {
|
if r.Type != RespCNAMEFollow || len(r.Decoded.CNAMEChain) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -229,6 +250,8 @@ func (r *Response) resolveGlue(child *Referral) {
|
|||||||
r.Cache.StoreGlue(nsName, child.Addresses)
|
r.Cache.StoreGlue(nsName, child.Addresses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsTerminal reports whether this response ends a traversal branch (no further
|
||||||
|
// referrals or CNAME follows are expected).
|
||||||
func (r *Response) IsTerminal() bool {
|
func (r *Response) IsTerminal() bool {
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError:
|
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError:
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package traverse
|
package traverse
|
||||||
|
|
||||||
|
// DefaultMaxDepth is the maximum traversal depth used when no explicit depth is configured.
|
||||||
const DefaultMaxDepth = 20
|
const DefaultMaxDepth = 20
|
||||||
|
|
||||||
|
// Stack is a depth-limited LIFO queue of Referrals used to drive iterative
|
||||||
|
// DNS traversal. Pushing a Referral whose Depth exceeds maxDepth fails
|
||||||
|
// silently and returns false, preventing unbounded traversal.
|
||||||
type Stack struct {
|
type Stack struct {
|
||||||
items []*Referral
|
items []*Referral
|
||||||
maxDepth int
|
maxDepth int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStack creates a Stack with the given maximum depth.
|
||||||
|
// When maxDepth is ≤ 0, DefaultMaxDepth is used.
|
||||||
func NewStack(maxDepth int) *Stack {
|
func NewStack(maxDepth int) *Stack {
|
||||||
if maxDepth <= 0 {
|
if maxDepth <= 0 {
|
||||||
maxDepth = DefaultMaxDepth
|
maxDepth = DefaultMaxDepth
|
||||||
@@ -17,6 +23,8 @@ func NewStack(maxDepth int) *Stack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push adds r to the stack. Returns false (and does not add) when r is nil
|
||||||
|
// or r.Depth >= maxDepth.
|
||||||
func (s *Stack) Push(r *Referral) bool {
|
func (s *Stack) Push(r *Referral) bool {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return false
|
return false
|
||||||
@@ -28,6 +36,7 @@ func (s *Stack) Push(r *Referral) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pop removes and returns the top Referral, or nil when the stack is empty.
|
||||||
func (s *Stack) Pop() *Referral {
|
func (s *Stack) Pop() *Referral {
|
||||||
if len(s.items) == 0 {
|
if len(s.items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -38,6 +47,7 @@ func (s *Stack) Pop() *Referral {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peek returns the top Referral without removing it, or nil when empty.
|
||||||
func (s *Stack) Peek() *Referral {
|
func (s *Stack) Peek() *Referral {
|
||||||
if len(s.items) == 0 {
|
if len(s.items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -45,14 +55,17 @@ func (s *Stack) Peek() *Referral {
|
|||||||
return s.items[len(s.items)-1]
|
return s.items[len(s.items)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len returns the current number of items in the stack.
|
||||||
func (s *Stack) Len() int {
|
func (s *Stack) Len() int {
|
||||||
return len(s.items)
|
return len(s.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxDepth returns the configured maximum depth for this stack.
|
||||||
func (s *Stack) MaxDepth() int {
|
func (s *Stack) MaxDepth() int {
|
||||||
return s.maxDepth
|
return s.maxDepth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEmpty reports whether the stack has no items.
|
||||||
func (s *Stack) IsEmpty() bool {
|
func (s *Stack) IsEmpty() bool {
|
||||||
return len(s.items) == 0
|
return len(s.items) == 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,17 @@
|
|||||||
|
// Package traverse implements iterative DNS tree traversal.
|
||||||
|
//
|
||||||
|
// Starting from the DNS root, the traverser follows referrals depth-first until
|
||||||
|
// it reaches a terminal response (answer, NXDOMAIN, SERVFAIL, etc.) for the
|
||||||
|
// queried name and type. CNAME chains are followed automatically; CNAME loops
|
||||||
|
// are detected and reported as errors.
|
||||||
|
//
|
||||||
|
// Key types:
|
||||||
|
//
|
||||||
|
// - Traverser — entry point; call NewTraverser then Traverse.
|
||||||
|
// - TraverserConfig — controls max depth, query type, root discovery and fast mode.
|
||||||
|
// - Referral — a pending query to a set of nameserver addresses.
|
||||||
|
// - Response — the classified, decoded result of querying a Referral.
|
||||||
|
// - InfoCache — a two-level (parent/child) cache for NS records and glue.
|
||||||
|
// - Stack — depth-limited LIFO work queue used by Traverser.
|
||||||
|
// - TraverserHooks — callback interface for progress and result events.
|
||||||
package traverse
|
package traverse
|
||||||
|
|||||||
@@ -11,13 +11,21 @@ import (
|
|||||||
miekgdns "github.com/miekg/dns"
|
miekgdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TraverserConfig controls the behaviour of a Traverser.
|
||||||
type TraverserConfig struct {
|
type TraverserConfig struct {
|
||||||
MaxDepth int
|
// MaxDepth caps the traversal depth. Referrals at or beyond this depth
|
||||||
QueryType uint16
|
// are rejected and reported as errors.
|
||||||
RootConfig *dns.RootDiscoveryConfig
|
MaxDepth int
|
||||||
|
// QueryType is the DNS record type requested at each step (e.g. dns.TypeA).
|
||||||
|
QueryType uint16
|
||||||
|
// RootConfig controls how root servers are discovered at startup.
|
||||||
|
RootConfig *dns.RootDiscoveryConfig
|
||||||
|
// QueryConfig controls UDP/TCP transport settings for each DNS query.
|
||||||
QueryConfig *dns.QueryConfig
|
QueryConfig *dns.QueryConfig
|
||||||
RootAddrs []net.IP
|
// RootAddrs may be supplied directly to skip root discovery.
|
||||||
Hooks *TraverserHooks
|
RootAddrs []net.IP
|
||||||
|
// Hooks receives events during traversal (progress, resolve, result).
|
||||||
|
Hooks *TraverserHooks
|
||||||
// Fast controls cache sharing across branches. When true (default), child
|
// Fast controls cache sharing across branches. When true (default), child
|
||||||
// branches inherit glue discovered by earlier branches via the shared root
|
// branches inherit glue discovered by earlier branches via the shared root
|
||||||
// cache, trading accuracy for speed. When false, each branch gets a
|
// cache, trading accuracy for speed. When false, each branch gets a
|
||||||
@@ -26,6 +34,8 @@ type TraverserConfig struct {
|
|||||||
Fast bool
|
Fast bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultTraverserConfig returns a TraverserConfig with sensible defaults:
|
||||||
|
// max depth 20, query type A, fast mode on.
|
||||||
func DefaultTraverserConfig() *TraverserConfig {
|
func DefaultTraverserConfig() *TraverserConfig {
|
||||||
return &TraverserConfig{
|
return &TraverserConfig{
|
||||||
MaxDepth: DefaultMaxDepth,
|
MaxDepth: DefaultMaxDepth,
|
||||||
@@ -37,11 +47,17 @@ func DefaultTraverserConfig() *TraverserConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TraversalResult pairs a Referral (the query that was attempted) with the
|
||||||
|
// Response (the outcome of that query). Response may be nil for referrals
|
||||||
|
// that were never processed (e.g. depth-limit rejections).
|
||||||
type TraversalResult struct {
|
type TraversalResult struct {
|
||||||
Referral *Referral
|
Referral *Referral
|
||||||
Response *Response
|
Response *Response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traverser performs iterative DNS traversal from the root down to the target
|
||||||
|
// domain, following referrals and CNAME chains.
|
||||||
|
// Create one with NewTraverser; call Traverse to run a traversal.
|
||||||
type Traverser struct {
|
type Traverser struct {
|
||||||
config *TraverserConfig
|
config *TraverserConfig
|
||||||
exchange dns.ExchangeFunc
|
exchange dns.ExchangeFunc
|
||||||
@@ -50,6 +66,8 @@ type Traverser struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTraverser creates a Traverser using cfg.
|
||||||
|
// When cfg is nil, DefaultTraverserConfig is used.
|
||||||
func NewTraverser(cfg *TraverserConfig) *Traverser {
|
func NewTraverser(cfg *TraverserConfig) *Traverser {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultTraverserConfig()
|
cfg = DefaultTraverserConfig()
|
||||||
@@ -62,10 +80,12 @@ func NewTraverser(cfg *TraverserConfig) *Traverser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExchange injects a custom exchange function, primarily for testing.
|
||||||
func (t *Traverser) SetExchange(fn dns.ExchangeFunc) {
|
func (t *Traverser) SetExchange(fn dns.ExchangeFunc) {
|
||||||
t.exchange = fn
|
t.exchange = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHooks attaches traversal event hooks to the Traverser.
|
||||||
func (t *Traverser) SetHooks(hooks *TraverserHooks) {
|
func (t *Traverser) SetHooks(hooks *TraverserHooks) {
|
||||||
if t.config == nil {
|
if t.config == nil {
|
||||||
t.config = DefaultTraverserConfig()
|
t.config = DefaultTraverserConfig()
|
||||||
@@ -73,6 +93,10 @@ func (t *Traverser) SetHooks(hooks *TraverserHooks) {
|
|||||||
t.config.Hooks = hooks
|
t.config.Hooks = hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traverse performs an iterative DNS traversal for name, starting from the root
|
||||||
|
// servers. It returns all TraversalResults, including intermediate referrals
|
||||||
|
// and terminal outcomes. Hooks are called for each event during the traversal.
|
||||||
|
// The context can be used to cancel a long-running traversal.
|
||||||
func (t *Traverser) Traverse(ctx context.Context, name string) ([]TraversalResult, error) {
|
func (t *Traverser) Traverse(ctx context.Context, name string) ([]TraversalResult, error) {
|
||||||
name = miekgdns.Fqdn(name)
|
name = miekgdns.Fqdn(name)
|
||||||
|
|
||||||
@@ -251,6 +275,9 @@ func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveNS resolves a nameserver hostname to its IP addresses by performing
|
||||||
|
// a fresh iterative traversal for that name, using cache to avoid repeated
|
||||||
|
// queries and visited to detect circular referrals.
|
||||||
func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCache, visited map[string]bool, depth int) ([]net.IP, error) {
|
func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCache, visited map[string]bool, depth int) ([]net.IP, error) {
|
||||||
if cache != nil {
|
if cache != nil {
|
||||||
if addrs := cache.LookupGlue(nsName); len(addrs) > 0 {
|
if addrs := cache.LookupGlue(nsName); len(addrs) > 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user