feat: implement DNS server fingerprinting (HAN-384)
CI / test (pull_request) Failing after 2m24s

- Add internal/fingerprint package with Fingerprinter type
  - Sends version.bind CHAOS TXT query to each server
  - Caches results per IP to avoid redundant queries
  - Concurrent batch fingerprinting via FingerprintAll
  - Gracefully handles non-responding servers (returns empty string)
  - Injectable exchange function for testability

- Wire fingerprinting into RunTraversal
  - Triggered when both ShowVersions and ShowServers are enabled
  - Collects unique server IPs from traversal results
  - Stores results in output.Config.Fingerprints before WriteSummary

- Display versions in text output (writeServers)
  - Appends version string after IP when ShowVersions is true
  - No output change when version is unknown

- Include version in JSON output (jsonServer.Version field)
  - omitempty: field absent when version is unknown

- Add comprehensive fingerprint tests (11 test cases)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Gary Hansen
2026-06-08 02:47:23 +10:00
co-authored by Copilot multica-agent
parent 35be7539b4
commit 3e7580b919
6 changed files with 395 additions and 7 deletions
+26
View File
@@ -3,7 +3,9 @@ package output
import (
"context"
"fmt"
"net"
"github.com/hits/ExploreDNS/internal/fingerprint"
"github.com/hits/ExploreDNS/internal/traverse"
)
@@ -25,6 +27,13 @@ func RunTraversal(ctx context.Context, traverser *traverse.Traverser, cfg *Confi
return results, err
}
// Fingerprint servers when both ShowVersions and ShowServers are enabled.
// Gating on ShowServers avoids unnecessary network calls when versions
// would not be displayed anyway.
if cfg.ShowVersions && cfg.ShowServers {
cfg.Fingerprints = fingerprint.New().FingerprintAll(ctx, collectUniqueServerIPs(results))
}
if err := formatter.WriteSummary(results); err != nil {
return results, err
}
@@ -34,3 +43,20 @@ func RunTraversal(ctx context.Context, traverser *traverse.Traverser, cfg *Confi
return results, nil
}
// collectUniqueServerIPs returns the set of unique server IPs seen in results.
func collectUniqueServerIPs(results []traverse.TraversalResult) []net.IP {
seen := make(map[string]bool)
var ips []net.IP
for _, r := range results {
if r.Response == nil || r.Response.Server == nil {
continue
}
key := r.Response.Server.String()
if !seen[key] {
seen[key] = true
ips = append(ips, r.Response.Server)
}
}
return ips
}