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>
This commit is contained in:
Andrea Gottardo
2024-09-24 13:18:45 -07:00
committed by GitHub
parent a98f75b783
commit 8a6f48b455
13 changed files with 396 additions and 7 deletions

View File

@@ -38,6 +38,7 @@ import (
"go4.org/mem"
"go4.org/netipx"
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
"gvisor.dev/gvisor/pkg/tcpip"
"tailscale.com/appc"
"tailscale.com/client/tailscale/apitype"
@@ -606,6 +607,50 @@ func (b *LocalBackend) GetDNSOSConfig() (dns.OSConfig, error) {
return manager.GetBaseConfig()
}
// QueryDNS performs a DNS query for name and queryType using the built-in DNS resolver, and returns
// the raw DNS response and the resolvers that are were able to handle the query (the internal forwarder
// may race multiple resolvers).
func (b *LocalBackend) QueryDNS(name string, queryType dnsmessage.Type) (res []byte, resolvers []*dnstype.Resolver, err error) {
manager, ok := b.sys.DNSManager.GetOK()
if !ok {
return nil, nil, errors.New("DNS manager not available")
}
fqdn, err := dnsname.ToFQDN(name)
if err != nil {
b.logf("DNSQuery: failed to parse FQDN %q: %v", name, err)
return nil, nil, err
}
n, err := dnsmessage.NewName(fqdn.WithTrailingDot())
if err != nil {
b.logf("DNSQuery: failed to parse name %q: %v", name, err)
return nil, nil, err
}
from := netip.MustParseAddrPort("127.0.0.1:0")
db := dnsmessage.NewBuilder(nil, dnsmessage.Header{
OpCode: 0,
RecursionDesired: true,
ID: 1,
})
db.StartQuestions()
db.Question(dnsmessage.Question{
Name: n,
Type: queryType,
Class: dnsmessage.ClassINET,
})
q, err := db.Finish()
if err != nil {
b.logf("DNSQuery: failed to build query: %v", err)
return nil, nil, err
}
res, err = manager.Query(b.ctx, q, "tcp", from)
if err != nil {
b.logf("DNSQuery: failed to query %q: %v", name, err)
return nil, nil, err
}
rr := manager.Resolver().GetUpstreamResolvers(fqdn)
return res, rr, nil
}
// GetComponentDebugLogging gets the time that component's debug logging is
// enabled until, or the zero time if component's time is not currently
// enabled.

View File

@@ -32,6 +32,7 @@ import (
"time"
"github.com/google/uuid"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/clientupdate"
"tailscale.com/drive"
@@ -49,6 +50,7 @@ import (
"tailscale.com/taildrop"
"tailscale.com/tka"
"tailscale.com/tstime"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
@@ -99,6 +101,7 @@ var handler = map[string]localAPIHandler{
"dev-set-state-store": (*Handler).serveDevSetStateStore,
"dial": (*Handler).serveDial,
"dns-osconfig": (*Handler).serveDNSOSConfig,
"dns-query": (*Handler).serveDNSQuery,
"drive/fileserver-address": (*Handler).serveDriveServerAddr,
"drive/shares": (*Handler).serveShares,
"file-targets": (*Handler).serveFileTargets,
@@ -2746,6 +2749,49 @@ func (h *Handler) serveDNSOSConfig(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
}
// serveDNSQuery provides the ability to perform DNS queries using the internal
// DNS forwarder. This is useful for debugging and testing purposes.
// URL parameters:
// - name: the domain name to query
// - type: the DNS record type to query as a number (default if empty: A = '1')
//
// The response if successful is a DNSQueryResponse JSON object.
func (h *Handler) serveDNSQuery(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
// Require write access for privacy reasons.
if !h.PermitWrite {
http.Error(w, "dns-query access denied", http.StatusForbidden)
return
}
q := r.URL.Query()
name := q.Get("name")
queryType := q.Get("type")
qt := dnsmessage.TypeA
if queryType != "" {
t, err := dnstype.DNSMessageTypeForString(queryType)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
qt = t
}
res, rrs, err := h.b.QueryDNS(name, qt)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&apitype.DNSQueryResponse{
Bytes: res,
Resolvers: rrs,
})
}
// serveDriveServerAddr handles updates of the Taildrive file server address.
func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" {