cli: add `tailscale dns query` (#13368)
Updates tailscale/tailscale#13326 Adds a CLI subcommand to perform DNS queries using the internal DNS forwarder and observe its internals (namely, which upstream resolvers are being used). Signed-off-by: Andrea Gottardo <andrea@gottardo.me>main
parent
a98f75b783
commit
8a6f48b455
@ -0,0 +1,163 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli |
||||
|
||||
import ( |
||||
"context" |
||||
"flag" |
||||
"fmt" |
||||
"net/netip" |
||||
"os" |
||||
"text/tabwriter" |
||||
|
||||
"golang.org/x/net/dns/dnsmessage" |
||||
"tailscale.com/types/dnstype" |
||||
) |
||||
|
||||
func runDNSQuery(ctx context.Context, args []string) error { |
||||
if len(args) < 1 { |
||||
return flag.ErrHelp |
||||
} |
||||
name := args[0] |
||||
queryType := "A" |
||||
if len(args) >= 2 { |
||||
queryType = args[1] |
||||
} |
||||
fmt.Printf("DNS query for %q (%s) using internal resolver:\n", name, queryType) |
||||
fmt.Println() |
||||
bytes, resolvers, err := localClient.QueryDNS(ctx, name, queryType) |
||||
if err != nil { |
||||
fmt.Printf("failed to query DNS: %v\n", err) |
||||
return nil |
||||
} |
||||
|
||||
if len(resolvers) == 1 { |
||||
fmt.Printf("Forwarding to resolver: %v\n", makeResolverString(*resolvers[0])) |
||||
} else { |
||||
fmt.Println("Multiple resolvers available:") |
||||
for _, r := range resolvers { |
||||
fmt.Printf(" - %v\n", makeResolverString(*r)) |
||||
} |
||||
} |
||||
fmt.Println() |
||||
var p dnsmessage.Parser |
||||
header, err := p.Start(bytes) |
||||
if err != nil { |
||||
fmt.Printf("failed to parse DNS response: %v\n", err) |
||||
return err |
||||
} |
||||
fmt.Printf("Response code: %v\n", header.RCode.String()) |
||||
fmt.Println() |
||||
p.SkipAllQuestions() |
||||
if header.RCode != dnsmessage.RCodeSuccess { |
||||
fmt.Println("No answers were returned.") |
||||
return nil |
||||
} |
||||
answers, err := p.AllAnswers() |
||||
if err != nil { |
||||
fmt.Printf("failed to parse DNS answers: %v\n", err) |
||||
return err |
||||
} |
||||
if len(answers) == 0 { |
||||
fmt.Println(" (no answers found)") |
||||
} |
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) |
||||
fmt.Fprintln(w, "Name\tTTL\tClass\tType\tBody") |
||||
fmt.Fprintln(w, "----\t---\t-----\t----\t----") |
||||
for _, a := range answers { |
||||
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", a.Header.Name.String(), a.Header.TTL, a.Header.Class.String(), a.Header.Type.String(), makeAnswerBody(a)) |
||||
} |
||||
w.Flush() |
||||
|
||||
fmt.Println() |
||||
return nil |
||||
} |
||||
|
||||
// makeAnswerBody returns a string with the DNS answer body in a human-readable format.
|
||||
func makeAnswerBody(a dnsmessage.Resource) string { |
||||
switch a.Header.Type { |
||||
case dnsmessage.TypeA: |
||||
return makeABody(a.Body) |
||||
case dnsmessage.TypeAAAA: |
||||
return makeAAAABody(a.Body) |
||||
case dnsmessage.TypeCNAME: |
||||
return makeCNAMEBody(a.Body) |
||||
case dnsmessage.TypeMX: |
||||
return makeMXBody(a.Body) |
||||
case dnsmessage.TypeNS: |
||||
return makeNSBody(a.Body) |
||||
case dnsmessage.TypeOPT: |
||||
return makeOPTBody(a.Body) |
||||
case dnsmessage.TypePTR: |
||||
return makePTRBody(a.Body) |
||||
case dnsmessage.TypeSRV: |
||||
return makeSRVBody(a.Body) |
||||
case dnsmessage.TypeTXT: |
||||
return makeTXTBody(a.Body) |
||||
default: |
||||
return a.Body.GoString() |
||||
} |
||||
} |
||||
|
||||
func makeABody(a dnsmessage.ResourceBody) string { |
||||
if a, ok := a.(*dnsmessage.AResource); ok { |
||||
return netip.AddrFrom4(a.A).String() |
||||
} |
||||
return "" |
||||
} |
||||
func makeAAAABody(aaaa dnsmessage.ResourceBody) string { |
||||
if a, ok := aaaa.(*dnsmessage.AAAAResource); ok { |
||||
return netip.AddrFrom16(a.AAAA).String() |
||||
} |
||||
return "" |
||||
} |
||||
func makeCNAMEBody(cname dnsmessage.ResourceBody) string { |
||||
if c, ok := cname.(*dnsmessage.CNAMEResource); ok { |
||||
return c.CNAME.String() |
||||
} |
||||
return "" |
||||
} |
||||
func makeMXBody(mx dnsmessage.ResourceBody) string { |
||||
if m, ok := mx.(*dnsmessage.MXResource); ok { |
||||
return fmt.Sprintf("%s (Priority=%d)", m.MX, m.Pref) |
||||
} |
||||
return "" |
||||
} |
||||
func makeNSBody(ns dnsmessage.ResourceBody) string { |
||||
if n, ok := ns.(*dnsmessage.NSResource); ok { |
||||
return n.NS.String() |
||||
} |
||||
return "" |
||||
} |
||||
func makeOPTBody(opt dnsmessage.ResourceBody) string { |
||||
if o, ok := opt.(*dnsmessage.OPTResource); ok { |
||||
return o.GoString() |
||||
} |
||||
return "" |
||||
} |
||||
func makePTRBody(ptr dnsmessage.ResourceBody) string { |
||||
if p, ok := ptr.(*dnsmessage.PTRResource); ok { |
||||
return p.PTR.String() |
||||
} |
||||
return "" |
||||
} |
||||
func makeSRVBody(srv dnsmessage.ResourceBody) string { |
||||
if s, ok := srv.(*dnsmessage.SRVResource); ok { |
||||
return fmt.Sprintf("Target=%s, Port=%d, Priority=%d, Weight=%d", s.Target.String(), s.Port, s.Priority, s.Weight) |
||||
} |
||||
return "" |
||||
} |
||||
func makeTXTBody(txt dnsmessage.ResourceBody) string { |
||||
if t, ok := txt.(*dnsmessage.TXTResource); ok { |
||||
return fmt.Sprintf("%q", t.TXT) |
||||
} |
||||
return "" |
||||
} |
||||
func makeResolverString(r dnstype.Resolver) string { |
||||
if len(r.BootstrapResolution) > 0 { |
||||
return fmt.Sprintf("%s (bootstrap: %v)", r.Addr, r.BootstrapResolution) |
||||
} |
||||
return fmt.Sprintf("%s", r.Addr) |
||||
} |
||||
@ -0,0 +1,84 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dnstype |
||||
|
||||
import ( |
||||
"errors" |
||||
"strings" |
||||
|
||||
"golang.org/x/net/dns/dnsmessage" |
||||
) |
||||
|
||||
// StringForType returns the string representation of a dnsmessage.Type.
|
||||
// For example, StringForType(dnsmessage.TypeA) returns "A".
|
||||
func StringForDNSMessageType(t dnsmessage.Type) string { |
||||
switch t { |
||||
case dnsmessage.TypeAAAA: |
||||
return "AAAA" |
||||
case dnsmessage.TypeALL: |
||||
return "ALL" |
||||
case dnsmessage.TypeA: |
||||
return "A" |
||||
case dnsmessage.TypeCNAME: |
||||
return "CNAME" |
||||
case dnsmessage.TypeHINFO: |
||||
return "HINFO" |
||||
case dnsmessage.TypeMINFO: |
||||
return "MINFO" |
||||
case dnsmessage.TypeMX: |
||||
return "MX" |
||||
case dnsmessage.TypeNS: |
||||
return "NS" |
||||
case dnsmessage.TypeOPT: |
||||
return "OPT" |
||||
case dnsmessage.TypePTR: |
||||
return "PTR" |
||||
case dnsmessage.TypeSOA: |
||||
return "SOA" |
||||
case dnsmessage.TypeSRV: |
||||
return "SRV" |
||||
case dnsmessage.TypeTXT: |
||||
return "TXT" |
||||
case dnsmessage.TypeWKS: |
||||
return "WKS" |
||||
} |
||||
return "UNKNOWN" |
||||
} |
||||
|
||||
// DNSMessageTypeForString returns the dnsmessage.Type for the given string.
|
||||
// For example, DNSMessageTypeForString("A") returns dnsmessage.TypeA.
|
||||
func DNSMessageTypeForString(s string) (t dnsmessage.Type, err error) { |
||||
s = strings.TrimSpace(strings.ToUpper(s)) |
||||
switch s { |
||||
case "AAAA": |
||||
return dnsmessage.TypeAAAA, nil |
||||
case "ALL": |
||||
return dnsmessage.TypeALL, nil |
||||
case "A": |
||||
return dnsmessage.TypeA, nil |
||||
case "CNAME": |
||||
return dnsmessage.TypeCNAME, nil |
||||
case "HINFO": |
||||
return dnsmessage.TypeHINFO, nil |
||||
case "MINFO": |
||||
return dnsmessage.TypeMINFO, nil |
||||
case "MX": |
||||
return dnsmessage.TypeMX, nil |
||||
case "NS": |
||||
return dnsmessage.TypeNS, nil |
||||
case "OPT": |
||||
return dnsmessage.TypeOPT, nil |
||||
case "PTR": |
||||
return dnsmessage.TypePTR, nil |
||||
case "SOA": |
||||
return dnsmessage.TypeSOA, nil |
||||
case "SRV": |
||||
return dnsmessage.TypeSRV, nil |
||||
case "TXT": |
||||
return dnsmessage.TypeTXT, nil |
||||
case "WKS": |
||||
return dnsmessage.TypeWKS, nil |
||||
} |
||||
return 0, errors.New("unknown DNS message type: " + s) |
||||
} |
||||
Loading…
Reference in new issue