cmd/tailscale/tsdnsjsonv0: extract a new package for tailscale dns --json (#20017)

This patch extracts all the DNS related JSON handling from the
cmd/tailscale/jsonoutput package into a new tsdnsjsonv0 package.

It adds package documentation for tsdnsjsonv0 with a big WARNING that
this is an unstable format with no backwards compatibility guarantees.
When we stabilize this format, we should spin off a new tsdnsjsonv1
package that uses jsonoutput.ResponseEnvelope to declare version 1.

Updates #13326
Updates #18750

Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
Simon Law
2026-07-27 16:59:29 -04:00
committed by GitHub
parent 358c975aea
commit f3ec43d7dd
9 changed files with 93 additions and 74 deletions

View File

@@ -16,7 +16,7 @@
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/cmd/tailscale/jsonoutput"
"tailscale.com/cmd/tailscale/tsdnsjsonv0"
)
var dnsQueryArgs struct {
@@ -74,7 +74,7 @@ func runDNSQuery(ctx context.Context, args []string) error {
return fmt.Errorf("failed to query DNS: %w", err)
}
data := &jsonoutput.DNSQueryResult{
data := &tsdnsjsonv0.QueryResponse{
Name: name,
QueryType: queryType,
}
@@ -97,9 +97,9 @@ func runDNSQuery(ctx context.Context, args []string) error {
if err != nil {
return fmt.Errorf("failed to parse DNS answers: %w", err)
}
data.Answers = make([]jsonoutput.DNSAnswer, 0, len(answers))
data.Answers = make([]tsdnsjsonv0.Answer, 0, len(answers))
for _, a := range answers {
data.Answers = append(data.Answers, jsonoutput.DNSAnswer{
data.Answers = append(data.Answers, tsdnsjsonv0.Answer{
Name: a.Header.Name.String(),
TTL: a.Header.TTL,
Class: a.Header.Class.String(),
@@ -121,7 +121,7 @@ func runDNSQuery(ctx context.Context, args []string) error {
return nil
}
func formatDNSQueryText(data *jsonoutput.DNSQueryResult) string {
func formatDNSQueryText(data *tsdnsjsonv0.QueryResponse) string {
var sb strings.Builder
fmt.Fprintf(&sb, "DNS query for %q (%s) using internal resolver:\n", data.Name, data.QueryType)
@@ -159,8 +159,8 @@ func formatDNSQueryText(data *jsonoutput.DNSQueryResult) string {
return sb.String()
}
// formatResolverString formats a jsonoutput.DNSResolverInfo for human-readable text output.
func formatResolverString(r jsonoutput.DNSResolverInfo) string {
// formatResolverString formats a [tsdnsjsonv0.ResolverInfo] for human-readable text output.
func formatResolverString(r tsdnsjsonv0.ResolverInfo) string {
if len(r.BootstrapResolution) > 0 {
return fmt.Sprintf("%s (bootstrap: %v)", r.Addr, r.BootstrapResolution)
}

View File

@@ -14,7 +14,7 @@
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/cmd/tailscale/jsonoutput"
"tailscale.com/cmd/tailscale/tsdnsjsonv0"
"tailscale.com/types/dnstype"
)
@@ -85,9 +85,9 @@
json bool
}
// makeDNSResolverInfo converts a dnstype.Resolver to a jsonoutput.DNSResolverInfo.
func makeDNSResolverInfo(r *dnstype.Resolver) jsonoutput.DNSResolverInfo {
info := jsonoutput.DNSResolverInfo{Addr: r.Addr}
// makeDNSResolverInfo converts a [dnstype.Resolver] to a [tsdnsjsonv0.ResolverInfo].
func makeDNSResolverInfo(r *dnstype.Resolver) tsdnsjsonv0.ResolverInfo {
info := tsdnsjsonv0.ResolverInfo{Addr: r.Addr}
if r.BootstrapResolution != nil {
info.BootstrapResolution = make([]string, 0, len(r.BootstrapResolution))
for _, a := range r.BootstrapResolution {
@@ -108,12 +108,12 @@ func runDNSStatus(ctx context.Context, args []string) error {
return err
}
data := &jsonoutput.DNSStatusResult{
data := &tsdnsjsonv0.StatusResponse{
TailscaleDNS: prefs.CorpDNS,
}
if s.CurrentTailnet != nil {
data.CurrentTailnet = &jsonoutput.DNSTailnetInfo{
data.CurrentTailnet = &tsdnsjsonv0.TailnetInfo{
MagicDNSEnabled: s.CurrentTailnet.MagicDNSEnabled,
MagicDNSSuffix: s.CurrentTailnet.MagicDNSSuffix,
SelfDNSName: s.Self.DNSName,
@@ -128,7 +128,7 @@ func runDNSStatus(ctx context.Context, args []string) error {
data.Resolvers = append(data.Resolvers, makeDNSResolverInfo(r))
}
data.SplitDNSRoutes = make(map[string][]jsonoutput.DNSResolverInfo)
data.SplitDNSRoutes = make(map[string][]tsdnsjsonv0.ResolverInfo)
for k, v := range dnsConfig.Routes {
for _, r := range v {
data.SplitDNSRoutes[k] = append(data.SplitDNSRoutes[k], makeDNSResolverInfo(r))
@@ -150,7 +150,7 @@ func runDNSStatus(ctx context.Context, args []string) error {
data.CertDomains = dnsConfig.CertDomains
for _, er := range dnsConfig.ExtraRecords {
data.ExtraRecords = append(data.ExtraRecords, jsonoutput.DNSExtraRecord{
data.ExtraRecords = append(data.ExtraRecords, tsdnsjsonv0.ExtraRecord{
Name: er.Name,
Type: er.Type,
Value: er.Value,
@@ -167,7 +167,7 @@ func runDNSStatus(ctx context.Context, args []string) error {
data.SystemDNSError = err.Error()
}
} else if osCfg != nil {
data.SystemDNS = &jsonoutput.DNSSystemConfig{
data.SystemDNS = &tsdnsjsonv0.SystemConfig{
Nameservers: osCfg.Nameservers,
SearchDomains: osCfg.SearchDomains,
MatchDomains: osCfg.MatchDomains,
@@ -187,7 +187,7 @@ func runDNSStatus(ctx context.Context, args []string) error {
return nil
}
func formatDNSStatusText(data *jsonoutput.DNSStatusResult, all bool) string {
func formatDNSStatusText(data *tsdnsjsonv0.StatusResponse, all bool) string {
var sb strings.Builder
fmt.Fprintf(&sb, "\n")

View File

@@ -187,6 +187,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
tailscale.com/cmd/tailscale/jsonoutput from tailscale.com/cmd/tailscale/cli
tailscale.com/cmd/tailscale/tsdnsjsonv0 from tailscale.com/cmd/tailscale/cli
tailscale.com/cmd/tailscale/tsroutecheckjsonv0 from tailscale.com/cmd/tailscale/cli
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlhttp from tailscale.com/control/ts2021

View File

@@ -0,0 +1,14 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tsdnsjsonv0 provides types for unmarshalling the JSON output of the
// "tailscale dns --json" command:
//
// - [QueryResponse] will unmarshal the output of "tailscale dns query --json=1"
// - [StatusResponse] will unmarshal the output of "tailscale dns status --json=1".
//
// # WARNING: unstable
//
// Format is "v0" and is subject to change.
// There is no guarantee of backwards or forwards compatibility.
package tsdnsjsonv0

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package jsonoutput_test
package tsdnsjsonv0_test
import (
"encoding/json"
@@ -10,10 +10,10 @@
"os"
"os/exec"
"tailscale.com/cmd/tailscale/jsonoutput"
"tailscale.com/cmd/tailscale/tsdnsjsonv0"
)
func ExampleDNSQueryResult() {
func ExampleQueryResponse() {
cmd := exec.Command("tailscale", "dns", "query", "--json", "hello.ts.net")
out, err := cmd.Output()
if err != nil {
@@ -23,9 +23,9 @@ func ExampleDNSQueryResult() {
panic(err)
}
var dnsQuery jsonoutput.DNSQueryResult
if err := json.Unmarshal(out, &dnsQuery); err != nil {
var resp tsdnsjsonv0.QueryResponse
if err := json.Unmarshal(out, &resp); err != nil {
panic(err)
}
fmt.Printf("{type: %s, name: %q}\n", dnsQuery.QueryType, dnsQuery.Name)
fmt.Printf("{type: %s, name: %q}\n", resp.QueryType, resp.Name)
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package jsonoutput_test
package tsdnsjsonv0_test
import (
"encoding/json"
@@ -10,10 +10,10 @@
"os"
"os/exec"
"tailscale.com/cmd/tailscale/jsonoutput"
"tailscale.com/cmd/tailscale/tsdnsjsonv0"
)
func ExampleDNSStatusResult() {
func ExampleStatusResponse() {
cmd := exec.Command("tailscale", "dns", "status", "--json")
out, err := cmd.Output()
if err != nil {
@@ -23,9 +23,9 @@ func ExampleDNSStatusResult() {
panic(err)
}
var dnsStatus jsonoutput.DNSStatusResult
if err := json.Unmarshal(out, &dnsStatus); err != nil {
var resp tsdnsjsonv0.StatusResponse
if err := json.Unmarshal(out, &resp); err != nil {
panic(err)
}
fmt.Printf("{accept-dns: %t, resolvers: %q}\n", dnsStatus.TailscaleDNS, dnsStatus.Resolvers)
fmt.Printf("{accept-dns: %t, resolvers: %q}\n", resp.TailscaleDNS, resp.Resolvers)
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsdnsjsonv0
// Answer is a single DNS resource record from a query response.
type Answer struct {
Name string
TTL uint32
Class string // e.g. "ClassINET"
Type string // e.g. "TypeA", "TypeAAAA"
Body string // human-readable record data
}
// QueryResponse is the result of a DNS query via the Tailscale
// internal forwarder (100.100.100.100). It is the output of:
//
// $ tailscale dns query --json NAME
type QueryResponse struct {
Name string
QueryType string // e.g. "A", "AAAA"
Resolvers []ResolverInfo `json:",omitzero"`
ResponseCode string // e.g. "RCodeSuccess", "RCodeNameError"
Answers []Answer `json:",omitzero"`
}

View File

@@ -1,10 +1,17 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package jsonoutput
package tsdnsjsonv0
// DNSResolverInfo is the JSON form of [tailscale.com/types/dnstype.Resolver].
type DNSResolverInfo struct {
// ExtraRecord is the JSON form of [tailscale.com/tailcfg.DNSRecord].
type ExtraRecord struct {
Name string
Type string `json:",omitempty"` // empty means A or AAAA, depending on Value
Value string // typically an IP address
}
// ResolverInfo is the JSON form of [tailscale.com/types/dnstype.Resolver].
type ResolverInfo struct {
// Addr is a plain IP, IP:port, DoH URL, or HTTP-over-WireGuard URL.
Addr string
@@ -13,16 +20,9 @@ type DNSResolverInfo struct {
BootstrapResolution []string `json:",omitempty"`
}
// DNSExtraRecord is the JSON form of [tailscale.com/tailcfg.DNSRecord].
type DNSExtraRecord struct {
Name string
Type string `json:",omitempty"` // empty means A or AAAA, depending on Value
Value string // typically an IP address
}
// DNSSystemConfig is the OS DNS configuration as observed by Tailscale,
// SystemConfig is the OS DNS configuration as observed by Tailscale,
// mirroring [tailscale.com/net/dns.OSConfig].
type DNSSystemConfig struct {
type SystemConfig struct {
Nameservers []string `json:",omitzero"`
SearchDomains []string `json:",omitzero"`
@@ -32,10 +32,10 @@ type DNSSystemConfig struct {
MatchDomains []string `json:",omitzero"`
}
// DNSTailnetInfo describes MagicDNS configuration for the tailnet,
// TailnetInfo describes MagicDNS configuration for the tailnet,
// combining [tailscale.com/ipn/ipnstate.TailnetStatus]
// and [tailscale.com/ipn/ipnstate.PeerStatus].
type DNSTailnetInfo struct {
type TailnetInfo struct {
// MagicDNSEnabled is whether MagicDNS is enabled for the
// tailnet. The device may still not use it if
// --accept-dns=false.
@@ -50,30 +50,30 @@ type DNSTailnetInfo struct {
SelfDNSName string `json:",omitempty"`
}
// DNSStatusResult is the full DNS status collected from the local
// StatusResponse is the full DNS status collected from the local
// Tailscale daemon. It is the output of:
//
// $ tailscale dns status --json
type DNSStatusResult struct {
type StatusResponse struct {
// TailscaleDNS is whether the Tailscale DNS configuration is
// installed on this device (the --accept-dns setting).
TailscaleDNS bool
// CurrentTailnet describes MagicDNS configuration for the tailnet.
CurrentTailnet *DNSTailnetInfo `json:",omitzero"` // nil if not connected
CurrentTailnet *TailnetInfo `json:",omitzero"` // nil if not connected
// Resolvers are the DNS resolvers, in preference order. If
// empty, the system defaults are used.
Resolvers []DNSResolverInfo `json:",omitzero"`
Resolvers []ResolverInfo `json:",omitzero"`
// SplitDNSRoutes maps domain suffixes to dedicated resolvers.
// An empty resolver slice means the suffix is handled by
// Tailscale's built-in resolver (100.100.100.100).
SplitDNSRoutes map[string][]DNSResolverInfo `json:",omitzero"`
SplitDNSRoutes map[string][]ResolverInfo `json:",omitzero"`
// FallbackResolvers are like Resolvers but only used when
// split DNS needs explicit default resolvers.
FallbackResolvers []DNSResolverInfo `json:",omitzero"`
FallbackResolvers []ResolverInfo `json:",omitzero"`
SearchDomains []string `json:",omitzero"`
@@ -87,7 +87,7 @@ type DNSStatusResult struct {
CertDomains []string `json:",omitzero"`
// ExtraRecords contains extra DNS records in the MagicDNS config.
ExtraRecords []DNSExtraRecord `json:",omitzero"`
ExtraRecords []ExtraRecord `json:",omitzero"`
// ExitNodeFilteredSet are DNS suffixes this node won't resolve
// when acting as an exit node DNS proxy. Period-prefixed
@@ -95,27 +95,6 @@ type DNSStatusResult struct {
// lowercase, no trailing dots.
ExitNodeFilteredSet []string `json:",omitzero"`
SystemDNS *DNSSystemConfig `json:",omitzero"` // nil if unavailable
SystemDNSError string `json:",omitempty"`
}
// DNSAnswer is a single DNS resource record from a query response.
type DNSAnswer struct {
Name string
TTL uint32
Class string // e.g. "ClassINET"
Type string // e.g. "TypeA", "TypeAAAA"
Body string // human-readable record data
}
// DNSQueryResult is the result of a DNS query via the Tailscale
// internal forwarder (100.100.100.100). It is the output of:
//
// $ tailscale dns query --json NAME
type DNSQueryResult struct {
Name string
QueryType string // e.g. "A", "AAAA"
Resolvers []DNSResolverInfo `json:",omitzero"`
ResponseCode string // e.g. "RCodeSuccess", "RCodeNameError"
Answers []DNSAnswer `json:",omitzero"`
SystemDNS *SystemConfig `json:",omitzero"` // nil if unavailable
SystemDNSError string `json:",omitempty"`
}

View File

@@ -55,7 +55,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscaled
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
tailscale.com/cmd/tailscale/jsonoutput from tailscale.com/cmd/tailscale/cli
tailscale.com/cmd/tailscale/tsdnsjsonv0 from tailscale.com/cmd/tailscale/cli
tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+