feat: DNS host selection - system resolver, --dns-upstream flag, root hints
- Replace hardcoded 127.0.0.1:53 in systemResolver() with actual system DNS from /etc/resolv.conf (falls back to 127.0.0.1:53 if unavailable) - Add Resolver field to RootDiscoveryConfig so callers can override the upstream resolver used during root server discovery - Add --dns-upstream flag (e.g. --dns-upstream 8.8.8.8:53) to exploredns CLI and DNSUpstream field to Config - Add internal/dns/hints.go with all 13 IANA root server IPv4/IPv6 addresses as embedded constants (RootHints []RootServer) - Update tests: fix real_exchange_test.go call site; add hints_test.go covering RootHints correctness and resolver helper functions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Copilot
multica-agent
parent
93959c9f11
commit
87898d5eb3
@@ -0,0 +1,157 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRootHintsCount(t *testing.T) {
|
||||
if len(RootHints) != 13 {
|
||||
t.Errorf("expected 13 root hints, got %d", len(RootHints))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsNames(t *testing.T) {
|
||||
wantNames := []string{
|
||||
"a.root-servers.net.",
|
||||
"b.root-servers.net.",
|
||||
"c.root-servers.net.",
|
||||
"d.root-servers.net.",
|
||||
"e.root-servers.net.",
|
||||
"f.root-servers.net.",
|
||||
"g.root-servers.net.",
|
||||
"h.root-servers.net.",
|
||||
"i.root-servers.net.",
|
||||
"j.root-servers.net.",
|
||||
"k.root-servers.net.",
|
||||
"l.root-servers.net.",
|
||||
"m.root-servers.net.",
|
||||
}
|
||||
for i, rs := range RootHints {
|
||||
if rs.Name != wantNames[i] {
|
||||
t.Errorf("RootHints[%d].Name = %q, want %q", i, rs.Name, wantNames[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsHaveIPv4(t *testing.T) {
|
||||
for _, rs := range RootHints {
|
||||
if len(rs.IPv4) == 0 {
|
||||
t.Errorf("root server %q has no IPv4 address", rs.Name)
|
||||
}
|
||||
for _, ip := range rs.IPv4 {
|
||||
if ip.To4() == nil {
|
||||
t.Errorf("root server %q: expected IPv4, got %v", rs.Name, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsHaveIPv6(t *testing.T) {
|
||||
for _, rs := range RootHints {
|
||||
if len(rs.IPv6) == 0 {
|
||||
t.Errorf("root server %q has no IPv6 address", rs.Name)
|
||||
}
|
||||
for _, ip := range rs.IPv6 {
|
||||
if ip.To4() != nil {
|
||||
t.Errorf("root server %q: expected IPv6, got IPv4-mappable %v", rs.Name, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsAllIPsIPv4Only(t *testing.T) {
|
||||
for _, rs := range RootHints {
|
||||
ips := rs.AllIPs(false)
|
||||
if len(ips) != len(rs.IPv4) {
|
||||
t.Errorf("root server %q: AllIPs(false) = %d, want %d", rs.Name, len(ips), len(rs.IPv4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsAllIPsBoth(t *testing.T) {
|
||||
for _, rs := range RootHints {
|
||||
ips := rs.AllIPs(true)
|
||||
want := len(rs.IPv4) + len(rs.IPv6)
|
||||
if len(ips) != want {
|
||||
t.Errorf("root server %q: AllIPs(true) = %d, want %d", rs.Name, len(ips), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsNoParseFail(t *testing.T) {
|
||||
// Ensure none of the IPs failed to parse (net.ParseIP returns nil on failure).
|
||||
for _, rs := range RootHints {
|
||||
for _, ip := range rs.IPv4 {
|
||||
if ip == nil {
|
||||
t.Errorf("root server %q has nil IPv4 (parse failed)", rs.Name)
|
||||
}
|
||||
}
|
||||
for _, ip := range rs.IPv6 {
|
||||
if ip == nil {
|
||||
t.Errorf("root server %q has nil IPv6 (parse failed)", rs.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHintsKnownAddress(t *testing.T) {
|
||||
// Spot-check a.root-servers.net. which has been stable for decades.
|
||||
for _, rs := range RootHints {
|
||||
if rs.Name == "a.root-servers.net." {
|
||||
want := net.ParseIP("198.41.0.4")
|
||||
if !rs.IPv4[0].Equal(want) {
|
||||
t.Errorf("a.root-servers.net. IPv4 = %v, want %v", rs.IPv4[0], want)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("a.root-servers.net. not found in RootHints")
|
||||
}
|
||||
|
||||
func TestResolverFromConfigExplicit(t *testing.T) {
|
||||
cfg := &RootDiscoveryConfig{Resolver: "8.8.8.8:53"}
|
||||
got := resolverFromConfig(cfg)
|
||||
if got != "8.8.8.8:53" {
|
||||
t.Errorf("resolverFromConfig = %q, want 8.8.8.8:53", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverFromConfigEmpty(t *testing.T) {
|
||||
cfg := &RootDiscoveryConfig{}
|
||||
got := resolverFromConfig(cfg)
|
||||
// Should return the system resolver; just check it's non-empty and contains a port.
|
||||
if got == "" {
|
||||
t.Error("resolverFromConfig with empty Resolver returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverFromConfigNil(t *testing.T) {
|
||||
got := resolverFromConfig(nil)
|
||||
if got == "" {
|
||||
t.Error("resolverFromConfig(nil) returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemResolverNonEmpty(t *testing.T) {
|
||||
got := systemResolver()
|
||||
if got == "" {
|
||||
t.Error("systemResolver() returned empty string")
|
||||
}
|
||||
// Must contain a colon (host:port format).
|
||||
host, port, err := splitHostPort(got)
|
||||
if err != nil {
|
||||
t.Errorf("systemResolver() = %q: not a valid host:port: %v", got, err)
|
||||
}
|
||||
if host == "" {
|
||||
t.Errorf("systemResolver() host is empty in %q", got)
|
||||
}
|
||||
if port == "" {
|
||||
t.Errorf("systemResolver() port is empty in %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// splitHostPort is a thin wrapper around net.SplitHostPort for test use.
|
||||
func splitHostPort(addr string) (host, port string, err error) {
|
||||
return net.SplitHostPort(addr)
|
||||
}
|
||||
Reference in New Issue
Block a user