Compare commits
2 Commits
icio/testw
...
angott/dns
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
025ceed735 | ||
|
|
7c02dcf93a |
@@ -4,7 +4,10 @@
|
||||
// Package apitype contains types for the Tailscale LocalAPI and control plane API.
|
||||
package apitype
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
)
|
||||
|
||||
// LocalAPIHost is the Host header value used by the LocalAPI.
|
||||
const LocalAPIHost = "local-tailscaled.sock"
|
||||
@@ -65,3 +68,11 @@ type DNSOSConfig struct {
|
||||
SearchDomains []string
|
||||
MatchDomains []string
|
||||
}
|
||||
|
||||
// DNSQueryResponse is the response to a DNS query request sent via LocalAPI.
|
||||
type DNSQueryResponse struct {
|
||||
// Bytes is the raw DNS response bytes.
|
||||
Bytes []byte
|
||||
// Resolvers is the list of resolvers that the forwarder deemed able to resolve the query.
|
||||
Resolvers []*dnstype.Resolver
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
@@ -813,6 +814,8 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
|
||||
return decodeJSON[*ipn.Prefs](body)
|
||||
}
|
||||
|
||||
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
||||
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
||||
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
|
||||
if err != nil {
|
||||
@@ -825,6 +828,21 @@ func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig
|
||||
return &osCfg, nil
|
||||
}
|
||||
|
||||
// QueryDNS executes a DNS query for a name (`google.com.`) and query type (`CNAME`).
|
||||
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
|
||||
// (often just one, but can be more if we raced multiple resolvers).
|
||||
func (lc *LocalClient) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
|
||||
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var res apitype.DNSQueryResponse
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid query response: %w", err)
|
||||
}
|
||||
return res.Bytes, res.Resolvers, nil
|
||||
}
|
||||
|
||||
// StartLoginInteractive starts an interactive login.
|
||||
func (lc *LocalClient) StartLoginInteractive(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
|
||||
@@ -1452,6 +1470,13 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin
|
||||
return decodeJSON[*ipnstate.DebugDERPRegionReport](body)
|
||||
}
|
||||
|
||||
// DebugEnvknob sets a envknob for debugging purposes.
|
||||
func (lc *LocalClient) DebugEnvknob(ctx context.Context, key, value string) error {
|
||||
v := url.Values{"key": {key}, "value": {value}}
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/debug-envknob?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// DebugPacketFilterRules returns the packet filter rules for the current device.
|
||||
func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-packet-filter-rules", 200, nil)
|
||||
|
||||
@@ -128,7 +128,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/tsweb from tailscale.com/cmd/derper
|
||||
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
|
||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/tailcfg+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
|
||||
@@ -91,7 +91,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
|
||||
163
cmd/tailscale/cli/dns-query.go
Normal file
163
cmd/tailscale/cli/dns-query.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func runDNSStatus(ctx context.Context, args []string) error {
|
||||
fmt.Print("\n")
|
||||
fmt.Println("Split DNS Routes:")
|
||||
if len(dnsConfig.Routes) == 0 {
|
||||
fmt.Println(" (no routes configured: split DNS might not be in use)")
|
||||
fmt.Println(" (no routes configured: split DNS disabled)")
|
||||
}
|
||||
for _, k := range slices.Sorted(maps.Keys(dnsConfig.Routes)) {
|
||||
v := dnsConfig.Routes[k]
|
||||
|
||||
72
cmd/tailscale/cli/dns-stream.go
Normal file
72
cmd/tailscale/cli/dns-stream.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func runDNSStream(ctx context.Context, args []string) error {
|
||||
fmt.Printf(`Privacy warning! To stream DNS queries, this tool will set these Tailscale debug flags, which would normally be disabled by default:
|
||||
|
||||
- TS_DEBUG_DNS_FORWARD_SEND=true
|
||||
- TS_DEBUG_DNS_INCLUDE_NAMES=true
|
||||
|
||||
TS_DEBUG_DNS_FORWARD_SEND instructs Tailscale to log DNS queries and responses as they are handled by the internal DNS forwarder.
|
||||
|
||||
TS_DEBUG_DNS_INCLUDE_NAMES instructs Tailscale to include queried and resolved DNS hostnames in the logs.
|
||||
|
||||
Unless the 'TS_NO_LOGS_NO_SUPPORT' flag was previously set, logs are uploaded to Tailscale for diagnostic and debugging purposes, which can be a concern in privacy-sensitive environments.
|
||||
|
||||
If you are concerned about the privacy implications of this, run this tool with the '--no-names' flag, which will avoid logging hostnames.`)
|
||||
fmt.Printf("\n\n")
|
||||
fmt.Println("Press Enter to start streaming DNS logs, or Ctrl+C to quit this tool.")
|
||||
|
||||
buf := bufio.NewReader(os.Stdin)
|
||||
_, err := buf.ReadBytes('\n')
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = localClient.DebugEnvknob(ctx, "TS_DEBUG_DNS_FORWARD_SEND", "true")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to set TS_DEBUG_DNS_FORWARD_SEND=true: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
err = localClient.DebugEnvknob(ctx, "TS_DEBUG_DNS_INCLUDE_NAMES", "true")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to set TS_DEBUG_DNS_INCLUDE_NAMES=true: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
logs, err := localClient.TailDaemonLogs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Streaming DNS logs. Press Ctrl+C to stop.")
|
||||
|
||||
d := json.NewDecoder(logs)
|
||||
for {
|
||||
var line struct {
|
||||
Text string `json:"text"`
|
||||
Verbose int `json:"v"`
|
||||
Time string `json:"client_time"`
|
||||
}
|
||||
err := d.Decode(&line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
text := strings.TrimSpace(line.Text)
|
||||
dnsPrefix := "dns: resolver: forward: "
|
||||
if !strings.HasPrefix(text, dnsPrefix) {
|
||||
continue
|
||||
}
|
||||
text = strings.TrimPrefix(text, dnsPrefix)
|
||||
fmt.Println(text)
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,20 @@ var dnsCmd = &ffcli.Command{
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
|
||||
// TODO: implement `tailscale query` here
|
||||
|
||||
// TODO: implement `tailscale log` here
|
||||
{
|
||||
Name: "query",
|
||||
ShortUsage: "tailscale dns query <name> [a|aaaa|cname|mx|ns|opt|ptr|srv|txt]",
|
||||
Exec: runDNSQuery,
|
||||
ShortHelp: "Perform a DNS query",
|
||||
LongHelp: "The 'tailscale dns query' subcommand performs a DNS query for the specified name using the internal DNS forwarder (100.100.100.100).\n\nIt also provides information about the resolver(s) used to resolve the query.",
|
||||
},
|
||||
{
|
||||
Name: "stream",
|
||||
ShortUsage: "tailscale dns stream",
|
||||
Exec: runDNSStream,
|
||||
ShortHelp: "Stream DNS queries and responses",
|
||||
LongHelp: "The 'tailscale dns stream' subcommand streams DNS queries and responses to and from the internal DNS forwarder, which is useful for debugging DNS issues.",
|
||||
},
|
||||
|
||||
// The above work is tracked in https://github.com/tailscale/tailscale/issues/13326
|
||||
},
|
||||
|
||||
@@ -134,7 +134,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tsweb/varz from tailscale.com/util/usermetric
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg+
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
@@ -90,6 +92,7 @@ var handler = map[string]localAPIHandler{
|
||||
"debug-capture": (*Handler).serveDebugCapture,
|
||||
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
||||
"debug-dial-types": (*Handler).serveDebugDialTypes,
|
||||
"debug-envknob": (*Handler).serveDebugEnvKnob,
|
||||
"debug-log": (*Handler).serveDebugLog,
|
||||
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
|
||||
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
|
||||
@@ -99,6 +102,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,
|
||||
@@ -581,6 +585,30 @@ func (h *Handler) serveUserMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
usermetric.Handler(w, r)
|
||||
}
|
||||
|
||||
// serveDebugEnvKnob allows the remote LocalAPI user to set the value of an envknob.
|
||||
func (h *Handler) serveDebugEnvKnob(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug-envknob access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if k := r.FormValue("key"); k != "" {
|
||||
if kv := r.FormValue("value"); kv != "" {
|
||||
envknob.Setenv(k, kv)
|
||||
io.WriteString(w, fmt.Sprintf("set %q to %q\n", k, kv))
|
||||
} else {
|
||||
http.Error(w, fmt.Sprintf("missing envknob value for envknob key %q", k), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "must provide an envknob key", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
@@ -2746,6 +2774,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" {
|
||||
|
||||
@@ -486,8 +486,9 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
|
||||
}
|
||||
|
||||
var (
|
||||
verboseDNSForward = envknob.RegisterBool("TS_DEBUG_DNS_FORWARD_SEND")
|
||||
skipTCPRetry = envknob.RegisterBool("TS_DNS_FORWARD_SKIP_TCP_RETRY")
|
||||
verboseDNSForward = envknob.RegisterBool("TS_DEBUG_DNS_FORWARD_SEND")
|
||||
verboseDNSIncludeNames = envknob.RegisterBool("TS_DEBUG_DNS_INCLUDE_NAMES")
|
||||
skipTCPRetry = envknob.RegisterBool("TS_DNS_FORWARD_SKIP_TCP_RETRY")
|
||||
|
||||
// For correlating log messages in the send() function; only used when
|
||||
// verboseDNSForward() is true.
|
||||
@@ -501,9 +502,17 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
if verboseDNSForward() {
|
||||
id := forwarderCount.Add(1)
|
||||
domain, typ, _ := nameFromQuery(fq.packet)
|
||||
f.logf("forwarder.send(%q, %d, %v, %d) [%d] ...", rr.name.Addr, fq.txid, typ, len(domain), id)
|
||||
if verboseDNSIncludeNames() {
|
||||
f.logf("forwarder.send(%q, %d, %v, %q) [%d] ...", rr.name.Addr, fq.txid, typ, domain.WithoutTrailingDot(), id)
|
||||
} else {
|
||||
f.logf("forwarder.send(%q, %d, %v, %d) [%d] ...", rr.name.Addr, fq.txid, typ, len(domain), id)
|
||||
}
|
||||
defer func() {
|
||||
f.logf("forwarder.send(%q, %d, %v, %d) [%d] = %v, %v", rr.name.Addr, fq.txid, typ, len(domain), id, len(ret), err)
|
||||
if verboseDNSIncludeNames() {
|
||||
f.logf("forwarder.send(%q, %d, %v, %q) [%d] = %v, %v", rr.name.Addr, fq.txid, typ, domain.WithoutTrailingDot(), id, len(ret), err)
|
||||
} else {
|
||||
f.logf("forwarder.send(%q, %d, %v, %d) [%d] = %v, %v", rr.name.Addr, fq.txid, typ, len(domain), id, len(ret), err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "http://") {
|
||||
@@ -820,7 +829,7 @@ func (f *forwarder) sendTCP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// resolvers returns the resolvers to use for domain.
|
||||
// Resolvers returns the resolvers to use for domain.
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
f.mu.Lock()
|
||||
routes := f.routes
|
||||
@@ -834,6 +843,17 @@ func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
return cloudHostFallback // or nil if no fallback
|
||||
}
|
||||
|
||||
// GetUpstreamResolvers returns the resolvers that would be used to resolve
|
||||
// the given FQDN.
|
||||
func (f *forwarder) GetUpstreamResolvers(name dnsname.FQDN) []*dnstype.Resolver {
|
||||
resolvers := f.resolvers(name)
|
||||
upstreamResolvers := make([]*dnstype.Resolver, 0, len(resolvers))
|
||||
for _, r := range resolvers {
|
||||
upstreamResolvers = append(upstreamResolvers, r.name)
|
||||
}
|
||||
return upstreamResolvers
|
||||
}
|
||||
|
||||
// forwardQuery is information and state about a forwarded DNS query that's
|
||||
// being sent to 1 or more upstreams.
|
||||
//
|
||||
@@ -992,7 +1012,11 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
return fmt.Errorf("waiting to send response: %w", ctx.Err())
|
||||
case responseChan <- packet{v, query.family, query.addr}:
|
||||
if verboseDNSForward() {
|
||||
f.logf("response(%d, %v, %d) = %d, nil", fq.txid, typ, len(domain), len(v))
|
||||
if verboseDNSIncludeNames() {
|
||||
f.logf("forwarder.response(%d, %v, %q) = %d, nil", fq.txid, typ, domain.WithTrailingDot(), len(v))
|
||||
} else {
|
||||
f.logf("forwarder.response(%d, %v, %d) = %d, nil", fq.txid, typ, len(domain), len(v))
|
||||
}
|
||||
}
|
||||
metricDNSFwdSuccess.Add(1)
|
||||
f.health.SetHealthy(dnsForwarderFailing)
|
||||
|
||||
@@ -337,6 +337,12 @@ func (r *Resolver) Query(ctx context.Context, bs []byte, family string, from net
|
||||
return out, err
|
||||
}
|
||||
|
||||
// GetUpstreamResolvers returns the resolvers that would be used to resolve
|
||||
// the given FQDN.
|
||||
func (r *Resolver) GetUpstreamResolvers(name dnsname.FQDN) []*dnstype.Resolver {
|
||||
return r.forwarder.GetUpstreamResolvers(name)
|
||||
}
|
||||
|
||||
// parseExitNodeQuery parses a DNS request packet.
|
||||
// It returns nil if it's malformed or lacking a question.
|
||||
func parseExitNodeQuery(q []byte) *response {
|
||||
|
||||
84
types/dnstype/messagetypes-string.go
Normal file
84
types/dnstype/messagetypes-string.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user