mirror of https://github.com/tailscale/tailscale/
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>pull/13500/head
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