summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/tailscale/localclient.go7
-rw-r--r--cmd/tailscale/cli/dns-stream.go72
-rw-r--r--cmd/tailscale/cli/dns.go9
-rw-r--r--ipn/localapi/localapi.go25
-rw-r--r--net/dns/resolver/forwarder.go23
5 files changed, 129 insertions, 7 deletions
diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go
index df51dc1ca..73f27d50f 100644
--- a/client/tailscale/localclient.go
+++ b/client/tailscale/localclient.go
@@ -1470,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)
diff --git a/cmd/tailscale/cli/dns-stream.go b/cmd/tailscale/cli/dns-stream.go
new file mode 100644
index 000000000..ea5ddb2cf
--- /dev/null
+++ b/cmd/tailscale/cli/dns-stream.go
@@ -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)
+ }
+}
diff --git a/cmd/tailscale/cli/dns.go b/cmd/tailscale/cli/dns.go
index 042ce1a94..18fa0f8fa 100644
--- a/cmd/tailscale/cli/dns.go
+++ b/cmd/tailscale/cli/dns.go
@@ -35,8 +35,13 @@ var dnsCmd = &ffcli.Command{
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.",
},
-
- // TODO: implement `tailscale log` here
+ {
+ 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
},
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index ec9d434e7..bf8cba2bc 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -92,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,
@@ -584,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)
diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go
index 5fe4cc631..4820e7743 100644
--- a/net/dns/resolver/forwarder.go
+++ b/net/dns/resolver/forwarder.go
@@ -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://") {
@@ -1003,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)