Skip to content

Commit 8823f7d

Browse files
authored
feat: host collector for DNS (#1617)
* add struct for host dns collector * add miekg/dns * add more logs * nit * new field names * use Hostnames instead of Names * misc update * make schemas * no error when there is no resolv.conf * query all searches * add summary.json file * merge summary into result file * query AAAA and CNAME as well * update schema for hostnames to be required
1 parent d73082a commit 8823f7d

10 files changed

+302
-2
lines changed

config/crds/troubleshoot.sh_hostcollectors.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,19 @@ spec:
12781278
required:
12791279
- path
12801280
type: object
1281+
dns:
1282+
properties:
1283+
collectorName:
1284+
type: string
1285+
exclude:
1286+
type: BoolString
1287+
hostnames:
1288+
items:
1289+
type: string
1290+
type: array
1291+
required:
1292+
- hostnames
1293+
type: object
12811294
filesystemPerformance:
12821295
description: |-
12831296
FilesystemPerformance benchmarks sequential write latency on a single file.

config/crds/troubleshoot.sh_hostpreflights.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,19 @@ spec:
12781278
required:
12791279
- path
12801280
type: object
1281+
dns:
1282+
properties:
1283+
collectorName:
1284+
type: string
1285+
exclude:
1286+
type: BoolString
1287+
hostnames:
1288+
items:
1289+
type: string
1290+
type: array
1291+
required:
1292+
- hostnames
1293+
type: object
12811294
filesystemPerformance:
12821295
description: |-
12831296
FilesystemPerformance benchmarks sequential write latency on a single file.

config/crds/troubleshoot.sh_supportbundles.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19850,6 +19850,19 @@ spec:
1985019850
required:
1985119851
- path
1985219852
type: object
19853+
dns:
19854+
properties:
19855+
collectorName:
19856+
type: string
19857+
exclude:
19858+
type: BoolString
19859+
hostnames:
19860+
items:
19861+
type: string
19862+
type: array
19863+
required:
19864+
- hostnames
19865+
type: object
1985319866
filesystemPerformance:
1985419867
description: |-
1985519868
FilesystemPerformance benchmarks sequential write latency on a single file.

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ require (
2727
github.com/mattn/go-isatty v0.0.20
2828
github.com/mholt/archiver/v3 v3.5.1
2929
github.com/microsoft/go-mssqldb v1.7.2
30+
github.com/miekg/dns v1.1.57
3031
github.com/opencontainers/image-spec v1.1.0
3132
github.com/pkg/errors v0.9.1
3233
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
@@ -124,6 +125,7 @@ require (
124125
go.opentelemetry.io/otel/metric v1.30.0 // indirect
125126
go.opentelemetry.io/otel/trace v1.30.0 // indirect
126127
go.uber.org/multierr v1.11.0 // indirect
128+
golang.org/x/tools v0.22.0 // indirect
127129
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
128130
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
129131
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect

pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ type HostJournald struct {
218218
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
219219
}
220220

221+
type HostDNS struct {
222+
HostCollectorMeta `json:",inline" yaml:",inline"`
223+
Hostnames []string `json:"hostnames" yaml:"hostnames"`
224+
}
225+
221226
type HostCollect struct {
222227
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
223228
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -245,6 +250,7 @@ type HostCollect struct {
245250
HostKernelConfigs *HostKernelConfigs `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"`
246251
HostJournald *HostJournald `json:"journald,omitempty" yaml:"journald,omitempty"`
247252
HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"`
253+
HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"`
248254
}
249255

250256
// GetName gets the name of the collector

pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/collect/dns.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222

2323
const (
2424
dnsUtilsImage = "registry.k8s.io/e2e-test-images/agnhost:2.39"
25-
nonResolvableDomain = "non-existent-domain"
25+
nonResolvableDomain = "*"
2626
)
2727

2828
type CollectDNS struct {
@@ -166,7 +166,7 @@ func troubleshootDNSFromPod(client kubernetes.Interface, ctx context.Context, no
166166
echo "=== dig kubernetes ==="
167167
dig +search +short kubernetes
168168
echo "=== dig non-existent-domain ==="
169-
dig +short %s
169+
dig +search +short %s
170170
exit 0
171171
`, nonResolvableDomain)}
172172

pkg/collect/host_collector.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
6767
return &CollectHostJournald{collector.HostJournald, bundlePath}, true
6868
case collector.HostCGroups != nil:
6969
return &CollectHostCGroups{collector.HostCGroups, bundlePath}, true
70+
case collector.HostDNS != nil:
71+
return &CollectHostDNS{collector.HostDNS, bundlePath}, true
7072
default:
7173
return nil, false
7274
}

pkg/collect/host_dns.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package collect
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/miekg/dns"
12+
"github.com/pkg/errors"
13+
"github.com/replicatedhq/troubleshoot/internal/util"
14+
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
15+
"k8s.io/klog/v2"
16+
)
17+
18+
type CollectHostDNS struct {
19+
hostCollector *troubleshootv1beta2.HostDNS
20+
BundlePath string
21+
}
22+
23+
type DNSResult struct {
24+
Query DNSQuery `json:"query"`
25+
ResolvedFromSearch string `json:"resolvedFromSearch"`
26+
}
27+
28+
type DNSQuery map[string][]DNSEntry
29+
30+
type DNSEntry struct {
31+
Server string `json:"server"`
32+
Search string `json:"search"`
33+
Name string `json:"name"`
34+
Answer string `json:"answer"`
35+
Record string `json:"record"`
36+
}
37+
38+
const (
39+
HostDNSPath = "host-collectors/dns/"
40+
resolvConf = "/etc/resolv.conf"
41+
)
42+
43+
func (c *CollectHostDNS) Title() string {
44+
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "dns")
45+
}
46+
47+
func (c *CollectHostDNS) IsExcluded() (bool, error) {
48+
return isExcluded(c.hostCollector.Exclude)
49+
}
50+
51+
func (c *CollectHostDNS) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
52+
53+
names := c.hostCollector.Hostnames
54+
if len(names) == 0 {
55+
return nil, errors.New("hostnames is required")
56+
}
57+
58+
// first, get DNS config from /etc/resolv.conf
59+
dnsConfig, err := getDNSConfig()
60+
if err != nil {
61+
return nil, errors.Wrap(err, "failed to read DNS resolve config")
62+
}
63+
64+
// query DNS for each name
65+
dnsEntries := make(map[string][]DNSEntry)
66+
dnsResult := DNSResult{Query: dnsEntries}
67+
allResolvedSearches := []string{}
68+
69+
for _, name := range names {
70+
entries, resolvedSearches, err := resolveName(name, dnsConfig)
71+
if err != nil {
72+
klog.V(2).Infof("Failed to resolve name %s: %v", name, err)
73+
}
74+
dnsEntries[name] = entries
75+
allResolvedSearches = append(allResolvedSearches, resolvedSearches...)
76+
}
77+
78+
// deduplicate resolved searches
79+
dnsResult.ResolvedFromSearch = strings.Join(util.Dedup(allResolvedSearches), ", ")
80+
81+
// convert dnsResult to a JSON string
82+
dnsResultJSON, err := json.MarshalIndent(dnsResult, "", " ")
83+
if err != nil {
84+
return nil, errors.Wrap(err, "failed to marshal DNS query result to JSON")
85+
}
86+
87+
output := NewResult()
88+
outputFile := c.getOutputFilePath("result.json")
89+
output.SaveResult(c.BundlePath, outputFile, bytes.NewBuffer(dnsResultJSON))
90+
91+
// write /etc/resolv.conf to a file
92+
resolvConfData, err := getResolvConf()
93+
if err != nil {
94+
klog.V(2).Infof("failed to read DNS resolve config: %v", err)
95+
} else {
96+
outputFile = c.getOutputFilePath("resolv.conf")
97+
output.SaveResult(c.BundlePath, outputFile, bytes.NewBuffer(resolvConfData))
98+
}
99+
100+
return output, nil
101+
}
102+
103+
func (c *CollectHostDNS) getOutputFilePath(name string) string {
104+
// normalize title to be used as a directory name, replace spaces with underscores
105+
title := strings.ReplaceAll(c.Title(), " ", "_")
106+
return filepath.Join(HostDNSPath, title, name)
107+
}
108+
109+
func getDNSConfig() (*dns.ClientConfig, error) {
110+
file, err := os.Open(resolvConf)
111+
if err != nil {
112+
return nil, err
113+
}
114+
defer file.Close()
115+
116+
config, err := dns.ClientConfigFromFile(file.Name())
117+
if err != nil {
118+
return nil, fmt.Errorf("failed to parse resolv.conf: %v", err)
119+
}
120+
121+
return config, nil
122+
}
123+
124+
func resolveName(name string, config *dns.ClientConfig) ([]DNSEntry, []string, error) {
125+
126+
results := []DNSEntry{}
127+
resolvedSearches := []string{}
128+
129+
// get a name list based on the config
130+
queryList := config.NameList(name)
131+
klog.V(2).Infof("DNS query list: %v", queryList)
132+
133+
// for each name in the list, query all the servers
134+
// we will query all search domains for each name
135+
for _, query := range queryList {
136+
for _, server := range config.Servers {
137+
klog.V(2).Infof("Querying DNS server %s for name %s", server, query)
138+
139+
entry := queryDNS(name, query, server+":"+config.Port)
140+
results = append(results, entry)
141+
142+
if entry.Search != "" {
143+
resolvedSearches = append(resolvedSearches, entry.Search)
144+
}
145+
}
146+
}
147+
return results, resolvedSearches, nil
148+
}
149+
150+
func getResolvConf() ([]byte, error) {
151+
data, err := os.ReadFile(resolvConf)
152+
if err != nil {
153+
return nil, err
154+
}
155+
return data, nil
156+
}
157+
158+
func queryDNS(name, query, server string) DNSEntry {
159+
recordTypes := []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeCNAME}
160+
entry := DNSEntry{Name: query, Server: server, Answer: ""}
161+
162+
for _, rec := range recordTypes {
163+
m := &dns.Msg{}
164+
m.SetQuestion(dns.Fqdn(query), rec)
165+
in, err := dns.Exchange(m, server)
166+
167+
if err != nil {
168+
klog.Errorf("failed to query DNS server %s for name %s: %v", server, query, err)
169+
continue
170+
}
171+
172+
if len(in.Answer) == 0 {
173+
continue
174+
}
175+
176+
entry.Answer = in.Answer[0].String()
177+
178+
// remember the search domain that resolved the query
179+
// e.g. foo.test.com -> test.com
180+
entry.Search = strings.Replace(query, name, "", 1)
181+
182+
// populate record detail
183+
switch rec {
184+
case dns.TypeA:
185+
record, ok := in.Answer[0].(*dns.A)
186+
if ok {
187+
entry.Record = record.A.String()
188+
}
189+
case dns.TypeAAAA:
190+
record, ok := in.Answer[0].(*dns.AAAA)
191+
if ok {
192+
entry.Record = record.AAAA.String()
193+
}
194+
case dns.TypeCNAME:
195+
record, ok := in.Answer[0].(*dns.CNAME)
196+
if ok {
197+
entry.Record = record.Target
198+
}
199+
}
200+
201+
// break on the first successful query
202+
break
203+
}
204+
return entry
205+
}

schemas/supportbundle-troubleshoot-v1beta2.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18986,6 +18986,26 @@
1898618986
}
1898718987
}
1898818988
},
18989+
"dns": {
18990+
"type": "object",
18991+
"required": [
18992+
"hostnames"
18993+
],
18994+
"properties": {
18995+
"collectorName": {
18996+
"type": "string"
18997+
},
18998+
"exclude": {
18999+
"oneOf": [{"type": "string"},{"type": "boolean"}]
19000+
},
19001+
"hostnames": {
19002+
"type": "array",
19003+
"items": {
19004+
"type": "string"
19005+
}
19006+
}
19007+
}
19008+
},
1898919009
"filesystemPerformance": {
1899019010
"description": "FilesystemPerformance benchmarks sequential write latency on a single file.\nThe optional background IOPS feature attempts to mimic real-world conditions by running read and\nwrite workloads prior to and during benchmark execution.",
1899119011
"type": "object",

0 commit comments

Comments
 (0)