Skip to content

Commit

Permalink
feat(inputs.dns_query): Add IP field(s) (#12519)
Browse files Browse the repository at this point in the history
  • Loading branch information
srebhan authored Jan 20, 2023
1 parent bfb26a8 commit 4102260
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 204 deletions.
10 changes: 8 additions & 2 deletions plugins/inputs/dns_query/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Dns server port.
# port = 53

## Query timeout in seconds.
# timeout = 2
## Query timeout
# timeout = "2s"

## Include the specified additional properties in the resulting metric.
## The following values are supported:
## "first_ip" -- return IP of the first A and AAAA answer
## "all_ips" -- return IPs of all A and AAAA answers
# include_fields = []
```

## Metrics
Expand Down
198 changes: 113 additions & 85 deletions plugins/inputs/dns_query/dns_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/miekg/dns"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
)

Expand All @@ -27,117 +28,147 @@ const (
)

type DNSQuery struct {
// Domains or subdomains to query
Domains []string
Domains []string `toml:"domains"`
Network string `toml:"network"`
Servers []string `toml:"servers"`
RecordType string `toml:"record_type"`
Port int `toml:"port"`
Timeout config.Duration `toml:"timeout"`
IncludeFields []string `toml:"include_fields"`

fieldEnabled map[string]bool
}

// Network protocol name
Network string
func (*DNSQuery) SampleConfig() string {
return sampleConfig
}

// Server to query
Servers []string
func (d *DNSQuery) Init() error {
// Convert the included fields into a lookup-table
d.fieldEnabled = make(map[string]bool, len(d.IncludeFields))
for _, f := range d.IncludeFields {
switch f {
case "first_ip", "all_ips":
default:
return fmt.Errorf("invalid field %q included", f)
}
d.fieldEnabled[f] = true
}

// Record type
RecordType string `toml:"record_type"`
// Set defaults
if d.Network == "" {
d.Network = "udp"
}

// DNS server port number
Port int
if d.RecordType == "" {
d.RecordType = "NS"
}

// Dns query timeout in seconds. 0 means no timeout
Timeout int
}
if len(d.Domains) == 0 {
d.Domains = []string{"."}
d.RecordType = "NS"
}

func (*DNSQuery) SampleConfig() string {
return sampleConfig
if d.Port < 1 {
d.Port = 53
}

return nil
}

func (d *DNSQuery) Gather(acc telegraf.Accumulator) error {
var wg sync.WaitGroup
d.setDefaultValues()

for _, domain := range d.Domains {
for _, server := range d.Servers {
wg.Add(1)
go func(domain, server string) {
fields := make(map[string]interface{}, 2)
tags := map[string]string{
"server": server,
"domain": domain,
"record_type": d.RecordType,
}
defer wg.Done()

dnsQueryTime, rcode, err := d.getDNSQueryTime(domain, server)
if rcode >= 0 {
tags["rcode"] = dns.RcodeToString[rcode]
fields["rcode_value"] = rcode
}
if err == nil {
setResult(Success, fields, tags)
fields["query_time_ms"] = dnsQueryTime
} else if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
setResult(Timeout, fields, tags)
} else if err != nil {
setResult(Error, fields, tags)
acc.AddError(err)
fields, tags, err := d.query(domain, server)
if err != nil {
if opErr, ok := err.(*net.OpError); !ok || !opErr.Timeout() {
acc.AddError(err)
}
}

acc.AddFields("dns_query", fields, tags)

wg.Done()
}(domain, server)
}
}

wg.Wait()

return nil
}

func (d *DNSQuery) setDefaultValues() {
if d.Network == "" {
d.Network = "udp"
func (d *DNSQuery) query(domain string, server string) (map[string]interface{}, map[string]string, error) {
tags := map[string]string{
"server": server,
"domain": domain,
"record_type": d.RecordType,
"result": "error",
}

if len(d.RecordType) == 0 {
d.RecordType = "NS"
fields := map[string]interface{}{
"query_time_ms": float64(0),
"result_code": uint64(Error),
}

if len(d.Domains) == 0 {
d.Domains = []string{"."}
d.RecordType = "NS"
c := dns.Client{
ReadTimeout: time.Duration(d.Timeout),
Net: d.Network,
}

if d.Port == 0 {
d.Port = 53
recordType, err := d.parseRecordType()
if err != nil {
return fields, tags, err
}

if d.Timeout == 0 {
d.Timeout = 2
}
}
var msg dns.Msg
msg.SetQuestion(dns.Fqdn(domain), recordType)
msg.RecursionDesired = true

func (d *DNSQuery) getDNSQueryTime(domain string, server string) (float64, int, error) {
dnsQueryTime := float64(0)
addr := net.JoinHostPort(server, strconv.Itoa(d.Port))
r, rtt, err := c.Exchange(&msg, addr)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
tags["result"] = "timeout"
fields["result_code"] = uint64(Timeout)
return fields, tags, err
}
return fields, tags, err
}

c := new(dns.Client)
c.ReadTimeout = time.Duration(d.Timeout) * time.Second
c.Net = d.Network
// Fill valid fields
tags["rcode"] = dns.RcodeToString[r.Rcode]
fields["rcode_value"] = r.Rcode
fields["query_time_ms"] = float64(rtt.Nanoseconds()) / 1e6

m := new(dns.Msg)
recordType, err := d.parseRecordType()
if err != nil {
return dnsQueryTime, -1, err
// Handle the failure case
if r.Rcode != dns.RcodeSuccess {
return fields, tags, fmt.Errorf("invalid answer (%s) from %s after %s query for %s", dns.RcodeToString[r.Rcode], server, d.RecordType, domain)
}
m.SetQuestion(dns.Fqdn(domain), recordType)
m.RecursionDesired = true

r, rtt, err := c.Exchange(m, net.JoinHostPort(server, strconv.Itoa(d.Port)))
if err != nil {
return dnsQueryTime, -1, err
// Success
tags["result"] = "success"
fields["result_code"] = uint64(Success)

if d.fieldEnabled["first_ip"] {
for _, record := range r.Answer {
if ip, found := extractIP(record); found {
fields["ip"] = ip
break
}
}
}
if r.Rcode != dns.RcodeSuccess {
return dnsQueryTime, r.Rcode, fmt.Errorf("Invalid answer (%s) from %s after %s query for %s", dns.RcodeToString[r.Rcode], server, d.RecordType, domain)
if d.fieldEnabled["all_ips"] {
for i, record := range r.Answer {
if ip, found := extractIP(record); found {
fields["ip_"+strconv.Itoa(i)] = ip
}
}
}
dnsQueryTime = float64(rtt.Nanoseconds()) / 1e6
return dnsQueryTime, r.Rcode, nil

return fields, tags, nil
}

func (d *DNSQuery) parseRecordType() (uint16, error) {
Expand Down Expand Up @@ -168,29 +199,26 @@ func (d *DNSQuery) parseRecordType() (uint16, error) {
case "TXT":
recordType = dns.TypeTXT
default:
err = fmt.Errorf("Record type %s not recognized", d.RecordType)
err = fmt.Errorf("record type %s not recognized", d.RecordType)
}

return recordType, err
}

func setResult(result ResultType, fields map[string]interface{}, tags map[string]string) {
var tag string
switch result {
case Success:
tag = "success"
case Timeout:
tag = "timeout"
case Error:
tag = "error"
func extractIP(record dns.RR) (string, bool) {
if r, ok := record.(*dns.A); ok {
return r.A.String(), true
}

tags["result"] = tag
fields["result_code"] = uint64(result)
if r, ok := record.(*dns.AAAA); ok {
return r.AAAA.String(), true
}
return "", false
}

func init() {
inputs.Add("dns_query", func() telegraf.Input {
return &DNSQuery{}
return &DNSQuery{
Timeout: config.Duration(2 * time.Second),
}
})
}
Loading

0 comments on commit 4102260

Please sign in to comment.