Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs.dns_query): Add IP field(s) #12519

Merged
merged 3 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
## IP -- return IP of the first A and AAAA answer
## "all IPs" -- return IP of the all A and AAAA answers with index
srebhan marked this conversation as resolved.
Show resolved Hide resolved
# 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 "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["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