From 79627098725b151c33f982ce0e085554e82eb570 Mon Sep 17 00:00:00 2001 From: Tao Date: Fri, 13 Jun 2025 22:24:38 +0800 Subject: [PATCH 1/4] feat: support recursive query from authoritative servers --- cli/cli.go | 2 ++ ipv6.go | 33 +++++++++++++++++++++++++++ main.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++---- root.go | 26 ++++++++++++++++++++++ root_test.go | 21 ++++++++++++++++++ 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 ipv6.go create mode 100644 root.go create mode 100644 root_test.go diff --git a/cli/cli.go b/cli/cli.go index 297eef6..202a90e 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -97,6 +97,8 @@ type Flags struct { UDPBuffer uint16 `long:"udp-buffer" description:"Set EDNS0 UDP size in query" default:"1232"` Verbose bool `short:"v" long:"verbose" description:"Show verbose log messages"` Trace bool `long:"trace" description:"Show trace log messages"` + Recursive bool `long:"recursive" description:"Do recursive query from authentic servers"` + ForceIPv4 bool `short:"4" long:"ipv4" description:"Force use the ipv4 address"` ShowVersion bool `short:"V" long:"version" description:"Show version and exit"` } diff --git a/ipv6.go b/ipv6.go new file mode 100644 index 0000000..19f0c76 --- /dev/null +++ b/ipv6.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "net" +) + +func supportIPv6() (bool, error) { + interfaces, err := net.Interfaces() + if err != nil { + return false, fmt.Errorf("failed to get network interfaces: %w", err) + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { + continue + } + + addrs, err := iface.Addrs() + if err != nil { + continue // Continue if we can't get addresses + } + + for _, addr := range addrs { + n, ok := addr.(*net.IPNet) + if ok && n.IP.To4() == nil && n.IP.IsGlobalUnicast() { + return true, nil + } + } + } + + return false, nil +} diff --git a/main.go b/main.go index 3509fb2..fddc59b 100644 --- a/main.go +++ b/main.go @@ -225,7 +225,7 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation if opts.Verbose { log.SetLevel(log.DebugLevel) - } else if opts.Trace { + } else if opts.Trace || opts.Recursive { log.SetLevel(log.TraceLevel) opts.ShowAll = true } @@ -334,7 +334,27 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation if len(opts.Server) == 0 { opts.Server = make([]string, 1) - if os.Getenv(defaultServerVar) != "" { + if opts.Recursive { + ipv4s, ipv6s, err := getRootHints() + if err != nil { + return fmt.Errorf("unable to load root hints: %s", err) + } + + hasIPv6, err := supportIPv6() + if err != nil { + return fmt.Errorf("unable to detect ipv6 support: %s", err) + } + + if !hasIPv6 { + opts.ForceIPv4 = true + } + + if opts.ForceIPv4 || !hasIPv6 { + opts.Server = ipv4s[:1] + } else { + opts.Server = ipv6s[:1] + } + } else if os.Getenv(defaultServerVar) != "" { opts.Server[0] = os.Getenv(defaultServerVar) log.Debugf("Using %s from %s environment variable", opts.Server, defaultServerVar) } else { @@ -414,11 +434,23 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation } msgs := createQuery(opts, rrTypesSlice) + if opts.Recursive { + if len(msgs) > 1 { + return fmt.Errorf("Only query one type in recursive mode") + } + + opts.Timeout = 10 * time.Minute // FIXME(tao) + } + errChan := make(chan error) + servers := opts.Server + go func() { + recursive: + var replies []*dns.Msg var entries []*output.Entry - for _, serverStr := range opts.Server { + for _, serverStr := range servers { // Parse server address and transport type server, transportType, err := parseServer(serverStr) if err != nil { @@ -442,7 +474,6 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation } startTime := time.Now() - var replies []*dns.Msg for _, msg := range msgs { if txp == nil { errChan <- fmt.Errorf("transport is nil") @@ -531,6 +562,30 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation errChan <- fmt.Errorf("invalid output format") } + if opts.Recursive && len(replies) > 0 { + if r := replies[0]; len(r.Answer) == 0 { + servers = []string{} + if opts.ForceIPv4 { + for _, extra := range r.Extra { + if a, ok := extra.(*dns.A); ok { + servers = append(servers, a.A.String()) + break + } + } + } else { + for _, extra := range r.Extra { + if a, ok := extra.(*dns.AAAA); ok { + servers = append(servers, a.AAAA.String()) + break + } + } + } + if len(servers) > 0 { + goto recursive + } + } + } + errChan <- nil }() diff --git a/root.go b/root.go new file mode 100644 index 0000000..bc41f1a --- /dev/null +++ b/root.go @@ -0,0 +1,26 @@ +package main + +import ( + "net/http" + + "github.com/miekg/dns" +) + +func getRootHints() (ip4s, ip6s []string, err error) { + resp, err := http.Get("https://www.internic.net/domain/named.root") + if err != nil { + return + } + defer resp.Body.Close() + + p := dns.NewZoneParser(resp.Body, "", "") + + for rr, ok := p.Next(); ok; rr, ok = p.Next() { + if a, ok := rr.(*dns.A); ok { + ip4s = append(ip4s, a.A.String()) + } else if a, ok := rr.(*dns.AAAA); ok { + ip6s = append(ip6s, a.AAAA.String()) + } + } + return +} diff --git a/root_test.go b/root_test.go new file mode 100644 index 0000000..dfd67e8 --- /dev/null +++ b/root_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" +) + +func TestGetRootHints(t *testing.T) { + ip4s, ip6s, err := getRootHints() + + if err != nil { + t.Fatal(err) + } + + if len(ip4s) == 0 { + t.Fatal("ipv4 is empty") + } + + if len(ip6s) == 0 { + t.Fatal("ipv6 is empty") + } +} From b1043497f6b0bf189768027d207bff26bee3b6f2 Mon Sep 17 00:00:00 2001 From: Tao Date: Sat, 14 Jun 2025 20:55:28 +0800 Subject: [PATCH 2/4] support ns delegation without extra --- main.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index fddc59b..f1cfc4b 100644 --- a/main.go +++ b/main.go @@ -349,7 +349,7 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation opts.ForceIPv4 = true } - if opts.ForceIPv4 || !hasIPv6 { + if opts.ForceIPv4 { opts.Server = ipv4s[:1] } else { opts.Server = ipv6s[:1] @@ -580,6 +580,16 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation } } } + + if len(servers) == 0 { + for _, ns := range r.Ns { + if a, ok := ns.(*dns.NS); ok { + servers = append(servers, a.Ns) + break + } + } + } + if len(servers) > 0 { goto recursive } From 6d4ad2633eb4119fdb37e60a445b5a52f8a37538 Mon Sep 17 00:00:00 2001 From: Tao Date: Fri, 19 Sep 2025 15:06:39 +0800 Subject: [PATCH 3/4] extract getRecursiveServers func --- main.go | 64 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/main.go b/main.go index f1cfc4b..45d8491 100644 --- a/main.go +++ b/main.go @@ -563,36 +563,9 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation } if opts.Recursive && len(replies) > 0 { - if r := replies[0]; len(r.Answer) == 0 { - servers = []string{} - if opts.ForceIPv4 { - for _, extra := range r.Extra { - if a, ok := extra.(*dns.A); ok { - servers = append(servers, a.A.String()) - break - } - } - } else { - for _, extra := range r.Extra { - if a, ok := extra.(*dns.AAAA); ok { - servers = append(servers, a.AAAA.String()) - break - } - } - } - - if len(servers) == 0 { - for _, ns := range r.Ns { - if a, ok := ns.(*dns.NS); ok { - servers = append(servers, a.Ns) - break - } - } - } - - if len(servers) > 0 { - goto recursive - } + servers = getRecursiveServers(replies) + if len(servers) > 0 { + goto recursive } } @@ -609,6 +582,37 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation return nil } +func getRecursiveServers(replies []*dns.Msg) (servers []string) { + if r := replies[0]; len(r.Answer) == 0 { + servers = []string{} + if opts.ForceIPv4 { + for _, extra := range r.Extra { + if a, ok := extra.(*dns.A); ok { + servers = append(servers, a.A.String()) + break + } + } + } else { + for _, extra := range r.Extra { + if a, ok := extra.(*dns.AAAA); ok { + servers = append(servers, a.AAAA.String()) + break + } + } + } + + if len(servers) == 0 { + for _, ns := range r.Ns { + if a, ok := ns.(*dns.NS); ok { + servers = append(servers, a.Ns) + break + } + } + } + } + return +} + func main() { clearOpts() if err := driver(os.Args[1:], os.Stdout); err != nil { From 5f7c2fe83575d1f4f8c48db4fed4681764e84aea Mon Sep 17 00:00:00 2001 From: Tao Date: Fri, 19 Sep 2025 15:14:06 +0800 Subject: [PATCH 4/4] extract initRootServer func --- main.go | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/main.go b/main.go index 45d8491..7c36b64 100644 --- a/main.go +++ b/main.go @@ -335,24 +335,8 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation opts.Server = make([]string, 1) if opts.Recursive { - ipv4s, ipv6s, err := getRootHints() - if err != nil { - return fmt.Errorf("unable to load root hints: %s", err) - } - - hasIPv6, err := supportIPv6() - if err != nil { - return fmt.Errorf("unable to detect ipv6 support: %s", err) - } - - if !hasIPv6 { - opts.ForceIPv4 = true - } - - if opts.ForceIPv4 { - opts.Server = ipv4s[:1] - } else { - opts.Server = ipv6s[:1] + if err = initRootServer(); err != nil { + return err } } else if os.Getenv(defaultServerVar) != "" { opts.Server[0] = os.Getenv(defaultServerVar) @@ -582,6 +566,29 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation return nil } +func initRootServer() error { + ipv4s, ipv6s, err := getRootHints() + if err != nil { + return fmt.Errorf("unable to load root hints: %s", err) + } + + hasIPv6, err := supportIPv6() + if err != nil { + return fmt.Errorf("unable to detect ipv6 support: %s", err) + } + + if !hasIPv6 { + opts.ForceIPv4 = true + } + + if opts.ForceIPv4 { + opts.Server = ipv4s[:1] + } else { + opts.Server = ipv6s[:1] + } + return nil +} + func getRecursiveServers(replies []*dns.Msg) (servers []string) { if r := replies[0]; len(r.Answer) == 0 { servers = []string{}