From ff4b9a1fd5e60693596a9e6a47a1ce15a34eb600 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 16 Jul 2026 18:23:39 +0200 Subject: [PATCH 1/7] check_dns: misc improvements and fixes improve argument description text for norec,warning,critical,timeout fix some grammatical issues and typos return unknown when getting a timeout and exit immediately --- docs/checks/plugins/check_dns.md | 24 ++++++++--------- pkg/check_dns/check_dns.go | 41 +++++++++++++---------------- pkg/check_dns/nameserver_windows.go | 2 +- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index 477cb7a6..f06da5f2 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -48,7 +48,7 @@ Naemon Config ## Usage -```Usage: +```UNKNOWN - Usage: check_dns [OPTIONS] Application Options: @@ -56,20 +56,20 @@ Application Options: -s, --server= DNS server you want to use for the lookup -p, --port= Port number you want to use (default: 53) -q, --querytype= DNS record query type (default: A) - --norec Set not recursive mode + --norec Clears the Recursion Desired flag, makes DNS server answer only from its authoritative data + or cache and not ask other nameservers. -e, --expected-string= IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at once, you have to specify whole string - --search-path= Search paths is added to the domains before sending a DNS query. This can be specified - multiple times. - --resolv-conf-file= Path to the resolv.conf file to use. Is not used in Windows. Default is /etc/resolv.conf . - (default: /etc/resolv.conf) + --search-path= Search paths to add to domains before sending a DNS query. This can be specified multiple + times. + --resolv-conf-file= Path to the resolv.conf file to use. Is not used in Windows. (default: /etc/resolv.conf) -v, --verbose Show verbose output. - -w, --warning= Warning timeout in seconds, if getting a successfull DNS query takes longer than specified, - set return status to warning. - -c, --critical= Critical timeout in seconds, if getting a successfull DNS query takes longer than specified, - set return status to critical. - -t, --timeout= If the program cannot get a successfull DNS response until the specified timeout in seconds, - it exits with critical status. (default: 10) + -w, --warning= Return warning if elapsed time to get a successful DNS query exceeds this value in seconds. + Default is off. + -c, --critical= Return critical if elapsed time to get a successful DNS query exceeds this value in seconds. + Default ist off. + -t, --timeout= Exit early and return unknown if elapsed time to get a successful DNS query exceeds this + value in seconds. (default: 10) Help Options: -h, --help Show this help message diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index d7b22f98..e9543c29 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -21,8 +21,8 @@ import ( func Check(ctx context.Context, output io.Writer, args []string) int { opts, err := parseArgs(args) if err != nil { - fmt.Fprintf(output, "%s", err.Error()) - return 2 + fmt.Fprintf(output, "UNKNOWN - %s", err.Error()) + return int(checkers.UNKNOWN) } ckr := opts.run(ctx) @@ -38,14 +38,14 @@ type dnsOpts struct { Server string `short:"s" long:"server" description:"DNS server you want to use for the lookup"` Port int `short:"p" long:"port" default:"53" description:"Port number you want to use"` QueryType string `short:"q" long:"querytype" default:"A" description:"DNS record query type"` - Norec bool `long:"norec" description:"Set not recursive mode"` + Norec bool `long:"norec" description:"Clears the Recursion Desired flag, makes DNS server answer only from its authoritative data or cache and not ask other nameservers."` ExpectedString []string `short:"e" long:"expected-string" description:"IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at once, you have to specify whole string"` - SearchPaths []string `long:"search-path" description:"Search paths is added to the domains before sending a DNS query. This can be specified multiple times."` - ResolvConfFile string `long:"resolv-conf-file" default:"/etc/resolv.conf" description:"Path to the resolv.conf file to use. Is not used in Windows. Default is /etc/resolv.conf ."` + SearchPaths []string `long:"search-path" description:"Search paths to add to domains before sending a DNS query. This can be specified multiple times."` + ResolvConfFile string `long:"resolv-conf-file" default:"/etc/resolv.conf" description:"Path to the resolv.conf file to use. Is not used in Windows."` Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output."` - WarningTimeout *int `short:"w" long:"warning" description:"Warning timeout in seconds, if getting a successfull DNS query takes longer than specified, set return status to warning."` - CriticalTimeout *int `short:"c" long:"critical" description:"Critical timeout in seconds, if getting a successfull DNS query takes longer than specified, set return status to critical."` - Timeout int `short:"t" long:"timeout" default:"10" description:"If the program cannot get a successfull DNS response until the specified timeout in seconds, it exits with critical status."` + WarningTimeout *int `short:"w" long:"warning" description:"Return warning if elapsed time to get a successful DNS query exceeds this value in seconds. Default is off."` + CriticalTimeout *int `short:"c" long:"critical" description:"Return critical if elapsed time to get a successful DNS query exceeds this value in seconds. Default ist off."` + Timeout int `short:"t" long:"timeout" default:"10" description:"Exit early and return unknown if elapsed time to get a successful DNS query exceeds this value in seconds."` } func parseArgs(args []string) (*dnsOpts, error) { @@ -125,7 +125,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queryType, ok := dns.StringToType[strings.ToUpper(opts.QueryType)] if !ok { - return checkers.Critical(fmt.Sprintf("%s is invalid query type", opts.QueryType)) + return checkers.Critical(fmt.Sprintf("%s is an invalid query type", opts.QueryType)) } c := new(dns.Client) @@ -139,7 +139,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { var successfulHost string queryDNSChan := make(chan bool, 1) - queryDNSSuccessfull := false + queryDNSSuccessful := false dnsExchangeCount := 0 queryDNS := func() { @@ -163,7 +163,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { message.Id = dns.Id() r, duration, err = c.Exchange(message, nameserver) - dnsExchangeCount = dnsExchangeCount + 1 + dnsExchangeCount++ if err == nil { if len(r.Answer) == 0 { @@ -200,14 +200,13 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { select { case <-time.After(time.Duration(opts.Timeout) * time.Second): - return checkers.Critical(fmt.Sprintf("Did not get a successfull DNS query in timeout: %d seconds", opts.Timeout)) - case queryDNSSuccessfull = <-queryDNSChan: - break + return checkers.Unknown(fmt.Sprintf("Failed to get a result after %d seconds", opts.Timeout)) + case queryDNSSuccessful = <-queryDNSChan: } queryEndTimestamp := time.Now() queryDuration := queryEndTimestamp.Sub(queryBeginTimestamp) - if !queryDNSSuccessfull { + if !queryDNSSuccessful { return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) } @@ -226,10 +225,10 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { switch { case opts.CriticalTimeout != nil && queryDuration.Seconds() > float64(*opts.CriticalTimeout): - tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the critical threshold: %d", queryDuration.Seconds(), opts.CriticalTimeout)) + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the critical threshold: %d", queryDuration.Seconds(), *opts.CriticalTimeout)) escalateStatus(checkers.CRITICAL) case opts.WarningTimeout != nil && queryDuration.Seconds() > float64(*opts.WarningTimeout): - tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the warning threshold: %d", queryDuration.Seconds(), opts.WarningTimeout)) + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the warning threshold: %d", queryDuration.Seconds(), *opts.WarningTimeout)) escalateStatus(checkers.WARNING) default: tryLogTrace(fmt.Sprintf("DNS query took %f seconds, and it is lower than (if specified) warning threshold: %v and critical threshold: %v", @@ -261,7 +260,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { supportedQueryType := map[string]int{"A": 1, "AAAA": 1, "MX": 1, "CNAME": 1} _, ok := supportedQueryType[strings.ToUpper(opts.QueryType)] if !ok { - return checkers.Critical(fmt.Sprintf("%s is not supported query type. Only A, AAAA, MX, CNAME are supported query types.", opts.QueryType)) + return checkers.Critical(fmt.Sprintf("%s is not a supported query type. Only A, AAAA, MX, CNAME are supported query types.", opts.QueryType)) } expectedStringsContainOneAnswerAddress := slices.ContainsFunc(opts.ExpectedString, func(ex string) bool { @@ -287,10 +286,6 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { tryLogTrace(fmt.Sprintf("Expected strings: %v does not contain one of the strings from the DNS answer: %v , raising status to critical", opts.ExpectedString, answerCopy)) escalateStatus(checkers.CRITICAL) - default: - tryLogTrace(fmt.Sprintf("Could not comapre expected strings: %v with the strings from the DNS answer: %v , raising status to unknown", - opts.ExpectedString, answerCopy)) - escalateStatus(checkers.UNKNOWN) } } @@ -326,6 +321,6 @@ func dnsAnswer(answer dns.RR) (string, string, error) { case *dns.CNAME: return t.Target, "CNAME", nil default: - return "", "", fmt.Errorf("%T is not supported query type. Only A, AAAA, MX, CNAME is supported for expectation.", t) + return "", "", fmt.Errorf("%T is not a supported query type. Only A, AAAA, MX, CNAME are supported for expectation.", t) } } diff --git a/pkg/check_dns/nameserver_windows.go b/pkg/check_dns/nameserver_windows.go index 7a287039..7dc27220 100644 --- a/pkg/check_dns/nameserver_windows.go +++ b/pkg/check_dns/nameserver_windows.go @@ -15,7 +15,7 @@ import ( ) // ref: https://go.dev/src/net/interface_windows.go -// windows does not use a resolv.config file i.e dns.ClientConfig +// windows does not use a resolv.conf file i.e. dns.ClientConfig // this function is defined for consistency across platforms func adapterAddress(_ *dns.ClientConfig) (nameservers []string, err error) { var b []byte From 2ee8a238a9424b8bd6b7841829daea2ccdae611a Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 16 Jul 2026 18:30:41 +0200 Subject: [PATCH 2/7] check_dns: ai pass - add validator functions to the arguments and tests --- pkg/check_dns/check_dns.go | 37 ++++++++++- pkg/snclient/check_dns_test.go | 110 +++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index e9543c29..fd0e2a2d 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -53,7 +53,42 @@ func parseArgs(args []string) (*dnsOpts, error) { psr := flags.NewParser(opts, flags.HelpFlag|flags.PassDoubleDash) // default flags without flags.PrintErrors psr.Name = "check_dns" _, err := psr.ParseArgs(args) - return opts, err + if err != nil { + return opts, err + } + + return opts, opts.validate() +} + +func (opts *dnsOpts) validate() error { + if strings.TrimSpace(opts.Host) == "" { + return fmt.Errorf("host must not be empty") + } + if strings.TrimSpace(opts.QueryType) == "" { + return fmt.Errorf("query type must not be empty") + } + if opts.Port < 1 || opts.Port > 65535 { + return fmt.Errorf("port must be between 1 and 65535, got: %d", opts.Port) + } + if opts.Timeout <= 0 { + return fmt.Errorf("timeout must be a positive number of seconds, got: %d", opts.Timeout) + } + if opts.WarningTimeout != nil && *opts.WarningTimeout < 0 { + return fmt.Errorf("warning threshold must not be negative, got: %d", *opts.WarningTimeout) + } + if opts.CriticalTimeout != nil && *opts.CriticalTimeout < 0 { + return fmt.Errorf("critical threshold must not be negative, got: %d", *opts.CriticalTimeout) + } + if opts.WarningTimeout != nil && opts.CriticalTimeout != nil && *opts.WarningTimeout > *opts.CriticalTimeout { + return fmt.Errorf("warning threshold (%d) must not be higher than the critical threshold (%d)", *opts.WarningTimeout, *opts.CriticalTimeout) + } + for _, expected := range opts.ExpectedString { + if strings.TrimSpace(expected) == "" { + return fmt.Errorf("expected string must not be empty") + } + } + + return nil } func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index a80779b1..3d80132d 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -151,5 +151,115 @@ CheckBuiltinPlugins = enabled ) }) + t.Run("missing host argument", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - .*host.*`, + string(res.BuildPluginOutput()), + "missing host argument", + ) + }) + + t.Run("empty host argument", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", ""}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - host must not be empty`, + string(res.BuildPluginOutput()), + "empty host argument", + ) + }) + + t.Run("empty query type", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-q", " "}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - query type must not be empty`, + string(res.BuildPluginOutput()), + "empty query type", + ) + }) + + t.Run("invalid port", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-p", "70000"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - port must be between 1 and 65535, got: 70000`, + string(res.BuildPluginOutput()), + "invalid port", + ) + }) + + t.Run("zero timeout", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-t", "0"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - timeout must be a positive number of seconds, got: 0`, + string(res.BuildPluginOutput()), + "zero timeout", + ) + }) + + t.Run("negative timeout", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-t", "-5"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - timeout must be a positive number of seconds, got: -5`, + string(res.BuildPluginOutput()), + "negative timeout", + ) + }) + + t.Run("negative warning threshold", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-w", "-1"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - warning threshold must not be negative, got: -1`, + string(res.BuildPluginOutput()), + "negative warning threshold", + ) + }) + + t.Run("negative critical threshold", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-c", "-1"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - critical threshold must not be negative, got: -1`, + string(res.BuildPluginOutput()), + "negative critical threshold", + ) + }) + + t.Run("warning threshold higher than critical", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-w", "10", "-c", "5"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - warning threshold \(10\) must not be higher than the critical threshold \(5\)`, + string(res.BuildPluginOutput()), + "warning threshold higher than critical", + ) + }) + + t.Run("empty expected string", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", ""}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - expected string must not be empty`, + string(res.BuildPluginOutput()), + "empty expected string", + ) + }) + StopTestAgent(t, snc) } From b1d01a19820bcc554c4e39ef206d3280a8b0842d Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 17 Jul 2026 09:07:26 +0200 Subject: [PATCH 3/7] check_dns: use total time neede for all queries time for the 'time' metric, and append warning and critical thresholds to it. --- pkg/check_dns/check_dns.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index fd0e2a2d..55cd5764 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -230,7 +230,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queryDNSChan <- gotAnswer } - queryBeginTimestamp := time.Now() + queriesBeginTimestamp := time.Now() go queryDNS() select { @@ -238,8 +238,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { return checkers.Unknown(fmt.Sprintf("Failed to get a result after %d seconds", opts.Timeout)) case queryDNSSuccessful = <-queryDNSChan: } - queryEndTimestamp := time.Now() - queryDuration := queryEndTimestamp.Sub(queryBeginTimestamp) + queriesEndTimestamp := time.Now() + queriesDuration := queriesEndTimestamp.Sub(queriesBeginTimestamp) if !queryDNSSuccessful { return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) @@ -259,15 +259,15 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { } switch { - case opts.CriticalTimeout != nil && queryDuration.Seconds() > float64(*opts.CriticalTimeout): - tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the critical threshold: %d", queryDuration.Seconds(), *opts.CriticalTimeout)) + case opts.CriticalTimeout != nil && queriesDuration.Seconds() > float64(*opts.CriticalTimeout): + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the critical threshold: %d", queriesDuration.Seconds(), *opts.CriticalTimeout)) escalateStatus(checkers.CRITICAL) - case opts.WarningTimeout != nil && queryDuration.Seconds() > float64(*opts.WarningTimeout): - tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the warning threshold: %d", queryDuration.Seconds(), *opts.WarningTimeout)) + case opts.WarningTimeout != nil && queriesDuration.Seconds() > float64(*opts.WarningTimeout): + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the warning threshold: %d", queriesDuration.Seconds(), *opts.WarningTimeout)) escalateStatus(checkers.WARNING) default: tryLogTrace(fmt.Sprintf("DNS query took %f seconds, and it is lower than (if specified) warning threshold: %v and critical threshold: %v", - queryDuration.Seconds(), opts.WarningTimeout, opts.CriticalTimeout)) + queriesDuration.Seconds(), opts.WarningTimeout, opts.CriticalTimeout)) } answersWithoutHeaders := make([]string, 0) @@ -329,9 +329,23 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { escalateStatus(checkers.CRITICAL) } + timeMetric := fmt.Sprintf("time=%fs;%s;%s", queriesDuration.Seconds(), + func() string { + if opts.WarningTimeout != nil { + return fmt.Sprintf("%d", *opts.WarningTimeout) + } + return "" + }(), + func() string { + if opts.CriticalTimeout != nil { + return fmt.Sprintf("%d", *opts.CriticalTimeout) + } + return "" + }(), + ) + msg := "" if len(answersWithoutHeaders) > 0 && len(answerTypes) > 0 { - timeMetric := fmt.Sprintf("time=%fs;;", successfulDuration.Seconds()) msg = fmt.Sprintf("%s returns %s (%s) |%s\n", opts.Host, answersWithoutHeaders[0], answerTypes[0], timeMetric) } else { msg = fmt.Sprintf("%s (%s) returns no answer from %s\n", opts.Host, opts.QueryType, successfulNameserver) From e90ce3f1404110dec299b0f512af443ea1aafde5 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 17 Jul 2026 09:13:58 +0200 Subject: [PATCH 4/7] check_dns: improve norec argument documentation --- docs/checks/plugins/check_dns.md | 4 ++-- pkg/check_dns/check_dns.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index f06da5f2..93aa45d8 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -56,8 +56,8 @@ Application Options: -s, --server= DNS server you want to use for the lookup -p, --port= Port number you want to use (default: 53) -q, --querytype= DNS record query type (default: A) - --norec Clears the Recursion Desired flag, makes DNS server answer only from its authoritative data - or cache and not ask other nameservers. + --norec Clears the Recursion Desired flag, DNS server answers only from its authoritative data or + cache, does not ask other nameservers. -e, --expected-string= IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at once, you have to specify whole string --search-path= Search paths to add to domains before sending a DNS query. This can be specified multiple diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 55cd5764..4e334df4 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -38,7 +38,7 @@ type dnsOpts struct { Server string `short:"s" long:"server" description:"DNS server you want to use for the lookup"` Port int `short:"p" long:"port" default:"53" description:"Port number you want to use"` QueryType string `short:"q" long:"querytype" default:"A" description:"DNS record query type"` - Norec bool `long:"norec" description:"Clears the Recursion Desired flag, makes DNS server answer only from its authoritative data or cache and not ask other nameservers."` + Norec bool `long:"norec" description:"Clears the Recursion Desired flag, DNS server answers only from its authoritative data or cache, does not ask other nameservers."` ExpectedString []string `short:"e" long:"expected-string" description:"IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at once, you have to specify whole string"` SearchPaths []string `long:"search-path" description:"Search paths to add to domains before sending a DNS query. This can be specified multiple times."` ResolvConfFile string `long:"resolv-conf-file" default:"/etc/resolv.conf" description:"Path to the resolv.conf file to use. Is not used in Windows."` From 8a8f458008ce5a2e952f190d2efb468736c2dc28 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 17 Jul 2026 13:56:11 +0200 Subject: [PATCH 5/7] check_dns: ai assisted - rework output for cases where no valid response came save them in a slice with details, then generate an output that looks like this: dns lookup failed for host: 'host' : nameserver1: err1, err2, err3 nameserver2: err1, err2 tests written completely using ai add test function that creates local dns nameserver that always replies with a specific return code use it to check errored output when every query is errored/empty. --- pkg/check_dns/check_dns.go | 100 +++++++++++++++++++++++----- pkg/snclient/check_dns_test.go | 116 +++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 17 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 4e334df4..26d25777 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -3,6 +3,7 @@ package check_dns import ( "context" + "errors" "fmt" "io" "net" @@ -35,7 +36,7 @@ func Check(ctx context.Context, output io.Writer, args []string) int { // Apache-2.0 license type dnsOpts struct { Host string `short:"H" long:"host" required:"true" description:"The name or address you want to query"` - Server string `short:"s" long:"server" description:"DNS server you want to use for the lookup"` + Servers []string `short:"s" long:"server" description:"DNS servers to use for the lookup. This can be specified multiple times."` Port int `short:"p" long:"port" default:"53" description:"Port number you want to use"` QueryType string `short:"q" long:"querytype" default:"A" description:"DNS record query type"` Norec bool `long:"norec" description:"Clears the Recursion Desired flag, DNS server answers only from its authoritative data or cache, does not ask other nameservers."` @@ -117,8 +118,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { } var nameservers []string - if opts.Server != "" { - nameservers = []string{opts.Server} + if len(opts.Servers) > 0 { + nameservers = opts.Servers } else { nameservers, err = adapterAddress(clientConfig) if err != nil { @@ -165,7 +166,6 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { c := new(dns.Client) - var lastErr error var r *dns.Msg var duration time.Duration @@ -175,7 +175,15 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queryDNSChan := make(chan bool, 1) queryDNSSuccessful := false - dnsExchangeCount := 0 + + emptyResults := make([]emptyResult, 0) + recordEmptyResult := func(hostCandidate, nameserver, reason string) { + emptyResults = append(emptyResults, emptyResult{ + host: hostCandidate, + nameserver: nameserver, + reason: reason, + }) + } queryDNS := func() { gotAnswer := false @@ -198,12 +206,13 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { message.Id = dns.Id() r, duration, err = c.Exchange(message, nameserver) - dnsExchangeCount++ if err == nil { if len(r.Answer) == 0 { - tryLogTrace(fmt.Sprintf("DNS query returned empty result, continuing to next combination, host: %s, nameserver: %s, duration: %dms", - hostCandidate, nameserver, duration.Milliseconds())) + reason := emptyResultReason(r.Rcode) + recordEmptyResult(hostCandidate, nameserver, reason) + tryLogTrace(fmt.Sprintf("DNS query returned empty result (%s), continuing to next combination, host: %s, nameserver: %s, duration: %dms", + reason, hostCandidate, nameserver, duration.Milliseconds())) continue } @@ -217,9 +226,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { break } - tryLogTrace(fmt.Sprintf("DNS query failed, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds())) - - lastErr = err + recordEmptyResult(hostCandidate, nameserver, queryFailedReason(err)) + tryLogTrace(fmt.Sprintf("DNS query failed, host: %s, nameserver: %s, duration: %dms, error: %v", hostCandidate, nameserver, duration.Milliseconds(), err)) } if gotAnswer { @@ -241,12 +249,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queriesEndTimestamp := time.Now() queriesDuration := queriesEndTimestamp.Sub(queriesBeginTimestamp) - if !queryDNSSuccessful { - return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) - } - - if r == nil { - return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) + if !queryDNSSuccessful || r == nil { + return checkers.Critical(emptyResultsMessage(originalHost, nameservers, emptyResults)) } checkSt := checkers.OK @@ -373,3 +377,65 @@ func dnsAnswer(answer dns.RR) (string, string, error) { return "", "", fmt.Errorf("%T is not a supported query type. Only A, AAAA, MX, CNAME are supported for expectation.", t) } } + +// emptyResult keeps track of why a single DNS query combination did not return any answer +// the reason is either the rcode or the query error. +type emptyResult struct { + host string + nameserver string + reason string +} + +// emptyResultsMessage builds a message from the reasons (rcodes or errors) why the DNS queries returned no answer. +// The first line contains the unique reasons from the first nameserver, each further nameserver gets a line with its own unique reasons. +func emptyResultsMessage(host string, nameservers []string, results []emptyResult) string { + lines := make([]string, 0, len(nameservers)) + for _, nameserver := range nameservers { + reasons := make([]string, 0) + for _, result := range results { + if result.nameserver == nameserver && !slices.Contains(reasons, result.reason) { + reasons = append(reasons, result.reason) + } + } + if len(reasons) == 0 { + continue + } + if len(lines) == 0 { + lines = append(lines, fmt.Sprintf("dns lookup failed for host '%s':", strings.TrimSuffix(host, "."))) + } + lines = append(lines, fmt.Sprintf("%s: %s", nameserver, strings.Join(reasons, ", "))) + } + if len(lines) == 0 { + return "all DNS queries gave empty results or failed" + } + + return strings.Join(lines, "\n") +} + +func emptyResultReason(rcode int) string { + rcodeStr, ok := dns.RcodeToString[rcode] + if !ok { + rcodeStr = fmt.Sprintf("RCODE %d", rcode) + } + if rcode == dns.RcodeSuccess { + return fmt.Sprintf("no answer (%s)", rcodeStr) + } + + return rcodeStr +} + +func queryFailedReason(err error) string { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return "query failed: timeout" + } + + // unwrap to the innermost error to keep the reason short and free of nameserver addresses + for { + unwrapped := errors.Unwrap(err) + if unwrapped == nil { + return "query failed: " + err.Error() + } + err = unwrapped + } +} diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index 3d80132d..4b5350c4 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -3,11 +3,47 @@ package snclient import ( + "net" + "os" + "path/filepath" + "strconv" "testing" + "github.com/miekg/dns" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// startTestDNSServer starts a local DNS server on the given address +// it answers every query with the given rcode and no answer records. +// It returns the port the port it has started on as string +func startTestDNSServer(t *testing.T, listenAddr string, rcode int) string { + t.Helper() + + pc, err := net.ListenPacket("udp", listenAddr) + require.NoError(t, err) + + srv := &dns.Server{ + PacketConn: pc, + Handler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { + reply := new(dns.Msg) + reply.SetRcode(req, rcode) + _ = w.WriteMsg(reply) + }), + } + go func() { + _ = srv.ActivateAndServe() + }() + t.Cleanup(func() { + _ = srv.Shutdown() + }) + + udpAddr, ok := pc.LocalAddr().(*net.UDPAddr) + require.True(t, ok, "local addr is a udp addr") + + return strconv.Itoa(udpAddr.Port) +} + func TestCheckDNS(t *testing.T) { config := ` [/modules] @@ -151,6 +187,86 @@ CheckBuiltinPlugins = enabled ) }) + t.Run("nxdomain rcode", func(t *testing.T) { + port := startTestDNSServer(t, "127.0.0.1:0", dns.RcodeNameError) + res := snc.RunCheck("check_dns", []string{"-H", "nxdomain.example.com.", "-s", "127.0.0.1", "-p", port}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - dns lookup failed for host 'nxdomain\.example\.com':\n127\.0\.0\.1:`+port+`: NXDOMAIN$`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("servfail rcode", func(t *testing.T) { + port := startTestDNSServer(t, "127.0.0.1:0", dns.RcodeServerFailure) + res := snc.RunCheck("check_dns", []string{"-H", "servfail.example.com.", "-s", "127.0.0.1", "-p", port}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - dns lookup failed for host 'servfail\.example\.com':\n127\.0\.0\.1:`+port+`: SERVFAIL$`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("empty noerror answer", func(t *testing.T) { + port := startTestDNSServer(t, "127.0.0.1:0", dns.RcodeSuccess) + res := snc.RunCheck("check_dns", []string{"-H", "nodata.example.com.", "-s", "127.0.0.1", "-p", port}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - dns lookup failed for host 'nodata\.example\.com':\n127\.0\.0\.1:`+port+`: no answer \(NOERROR\)$`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("empty results unique across search paths", func(t *testing.T) { + port := startTestDNSServer(t, "127.0.0.1:0", dns.RcodeNameError) + res := snc.RunCheck("check_dns", []string{ + "-H", "myhost", + "--search-path", "one.example.com", "--search-path", "two.example.com", + "-s", "127.0.0.1", "-p", port, + }) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - dns lookup failed for host 'myhost':\n127\.0\.0\.1:`+port+`: NXDOMAIN$`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("empty results from multiple nameservers", func(t *testing.T) { + port := startTestDNSServer(t, "127.0.0.1:0", dns.RcodeServerFailure) + pc, err := net.ListenPacket("udp", "127.0.0.2:"+port) + if err != nil { + t.Skipf("cannot listen on 127.0.0.2: %s", err) + } + _ = pc.Close() + startTestDNSServer(t, "127.0.0.2:"+port, dns.RcodeNameError) + + resolvConf := filepath.Join(t.TempDir(), "resolv.conf") + require.NoError(t, os.WriteFile(resolvConf, []byte("nameserver 127.0.0.1\nnameserver 127.0.0.2\n"), 0o600)) + + res := snc.RunCheck("check_dns", []string{ + "-H", "multi.example.com.", + "--resolv-conf-file", resolvConf, + "-p", port, + }) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + output := string(res.BuildPluginOutput()) + assert.Regexpf( + t, + `^CRITICAL - dns lookup failed for host 'multi\.example\.com':\n127\.0\.0\.1:`+port+`: SERVFAIL`, + output, + "first nameserver gets its own line", + ) + assert.Containsf(t, output, "127.0.0.2:"+port+": NXDOMAIN", "second nameserver gets its own line") + }) + t.Run("missing host argument", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{}) assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") From 9493cde904d280149164e7a055493186103c4da7 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 17 Jul 2026 14:21:56 +0200 Subject: [PATCH 6/7] check_dns: ai assisted - add query-timeout argument. works on each individual query. adjust global timeout to 30 seconds, and query timeout to 5 seconds add tests, ai generated, checking query timeout parameter --- docs/checks/plugins/check_dns.md | 8 ++- pkg/check_dns/check_dns.go | 9 ++- pkg/snclient/check_dns_test.go | 100 ++++++++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index 93aa45d8..8925fae5 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -53,7 +53,7 @@ Naemon Config Application Options: -H, --host= The name or address you want to query - -s, --server= DNS server you want to use for the lookup + -s, --server= DNS servers to use for the lookup. This can be specified multiple times. -p, --port= Port number you want to use (default: 53) -q, --querytype= DNS record query type (default: A) --norec Clears the Recursion Desired flag, DNS server answers only from its authoritative data or @@ -68,8 +68,10 @@ Application Options: Default is off. -c, --critical= Return critical if elapsed time to get a successful DNS query exceeds this value in seconds. Default ist off. - -t, --timeout= Exit early and return unknown if elapsed time to get a successful DNS query exceeds this - value in seconds. (default: 10) + -t, --timeout= Global timeout in seconds. Exit early and return unknown if elapsed time to get a successful + DNS query exceeds this value. (default: 30) + -T, --query-timeout= Timeout for each single DNS query in seconds. If exceeded, the next query is tried instead of + exiting. (default: 5) Help Options: -h, --help Show this help message diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 26d25777..dc19cf64 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -46,7 +46,8 @@ type dnsOpts struct { Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output."` WarningTimeout *int `short:"w" long:"warning" description:"Return warning if elapsed time to get a successful DNS query exceeds this value in seconds. Default is off."` CriticalTimeout *int `short:"c" long:"critical" description:"Return critical if elapsed time to get a successful DNS query exceeds this value in seconds. Default ist off."` - Timeout int `short:"t" long:"timeout" default:"10" description:"Exit early and return unknown if elapsed time to get a successful DNS query exceeds this value in seconds."` + Timeout int `short:"t" long:"timeout" default:"30" description:"Global timeout in seconds. Exit early and return unknown if elapsed time to get a successful DNS query exceeds this value."` + QueryTimeout int `short:"T" long:"query-timeout" default:"5" description:"Timeout for each single DNS query in seconds. If exceeded, the next query is tried instead of exiting."` } func parseArgs(args []string) (*dnsOpts, error) { @@ -74,6 +75,9 @@ func (opts *dnsOpts) validate() error { if opts.Timeout <= 0 { return fmt.Errorf("timeout must be a positive number of seconds, got: %d", opts.Timeout) } + if opts.QueryTimeout <= 0 { + return fmt.Errorf("query timeout must be a positive number of seconds, got: %d", opts.QueryTimeout) + } if opts.WarningTimeout != nil && *opts.WarningTimeout < 0 { return fmt.Errorf("warning threshold must not be negative, got: %d", *opts.WarningTimeout) } @@ -164,7 +168,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { return checkers.Critical(fmt.Sprintf("%s is an invalid query type", opts.QueryType)) } - c := new(dns.Client) + // Timeout is a builtin cumulative timeout for dial, write and read, it is applied to every single Exchange i.e. DNS query. + c := &dns.Client{Timeout: time.Duration(opts.QueryTimeout) * time.Second} var r *dns.Msg var duration time.Duration diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index 4b5350c4..f6268c70 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -14,10 +14,10 @@ import ( "github.com/stretchr/testify/require" ) -// startTestDNSServer starts a local DNS server on the given address -// it answers every query with the given rcode and no answer records. -// It returns the port the port it has started on as string -func startTestDNSServer(t *testing.T, listenAddr string, rcode int) string { +// startTestDNSServerHandler starts a local DNS server on the given address +// using the given handler. +// It returns the port it has started on as string +func startTestDNSServerHandler(t *testing.T, listenAddr string, handler dns.Handler) string { t.Helper() pc, err := net.ListenPacket("udp", listenAddr) @@ -25,11 +25,7 @@ func startTestDNSServer(t *testing.T, listenAddr string, rcode int) string { srv := &dns.Server{ PacketConn: pc, - Handler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { - reply := new(dns.Msg) - reply.SetRcode(req, rcode) - _ = w.WriteMsg(reply) - }), + Handler: handler, } go func() { _ = srv.ActivateAndServe() @@ -44,6 +40,37 @@ func startTestDNSServer(t *testing.T, listenAddr string, rcode int) string { return strconv.Itoa(udpAddr.Port) } +// startTestDNSServer starts a local DNS server on the given address +// it answers every query with the given rcode and no answer records. +// It returns the port the port it has started on as string +func startTestDNSServer(t *testing.T, listenAddr string, rcode int) string { + t.Helper() + + return startTestDNSServerHandler(t, listenAddr, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { + reply := new(dns.Msg) + reply.SetRcode(req, rcode) + _ = w.WriteMsg(reply) + })) +} + +// startSilentDNSServer opens a udp listener on the given address which never +// responds, causing DNS query timeouts. +// It returns the port it has started on as string +func startSilentDNSServer(t *testing.T, listenAddr string) string { + t.Helper() + + pc, err := net.ListenPacket("udp", listenAddr) + require.NoError(t, err) + t.Cleanup(func() { + _ = pc.Close() + }) + + udpAddr, ok := pc.LocalAddr().(*net.UDPAddr) + require.True(t, ok, "local addr is a udp addr") + + return strconv.Itoa(udpAddr.Port) +} + func TestCheckDNS(t *testing.T) { config := ` [/modules] @@ -267,6 +294,50 @@ CheckBuiltinPlugins = enabled assert.Containsf(t, output, "127.0.0.2:"+port+": NXDOMAIN", "second nameserver gets its own line") }) + t.Run("query timeout on all nameservers", func(t *testing.T) { + port := startSilentDNSServer(t, "127.0.0.1:0") + res := snc.RunCheck("check_dns", []string{"-H", "silent.example.com.", "-s", "127.0.0.1", "-p", port, "-T", "1"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - dns lookup failed for host 'silent\.example\.com':\n127\.0\.0\.1:`+port+`: query failed: timeout$`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("query timeout continues with next nameserver", func(t *testing.T) { + port := startSilentDNSServer(t, "127.0.0.1:0") + pc, err := net.ListenPacket("udp", "127.0.0.2:"+port) + if err != nil { + t.Skipf("cannot listen on 127.0.0.2: %s", err) + } + _ = pc.Close() + startTestDNSServerHandler(t, "127.0.0.2:"+port, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { + reply := new(dns.Msg) + reply.SetReply(req) + rr, rrErr := dns.NewRR(req.Question[0].Name + " 60 IN A 1.2.3.4") + if rrErr == nil { + reply.Answer = append(reply.Answer, rr) + } + _ = w.WriteMsg(reply) + })) + + res := snc.RunCheck("check_dns", []string{ + "-H", "slow.example.com.", + "-s", "127.0.0.1", "-s", "127.0.0.2", + "-p", port, + "-T", "1", + }) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - slow\.example\.com\. returns 1\.2\.3\.4 \(A\)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + t.Run("missing host argument", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{}) assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") @@ -333,6 +404,17 @@ CheckBuiltinPlugins = enabled ) }) + t.Run("zero query timeout", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-T", "0"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") + assert.Regexpf( + t, + `^UNKNOWN - query timeout must be a positive number of seconds, got: 0`, + string(res.BuildPluginOutput()), + "zero query timeout", + ) + }) + t.Run("negative warning threshold", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-w", "-1"}) assert.Equalf(t, CheckExitUnknown, res.State, "state unknown") From 36cdb7625cfa431f14bce9d4b73ad0616ea7b247 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 17 Jul 2026 14:35:02 +0200 Subject: [PATCH 7/7] check_dns: citest fixes split up test functions, rename some variables --- pkg/check_dns/check_dns.go | 3 +- pkg/snclient/check_dns_test.go | 76 ++++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index dc19cf64..24a0da72 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -338,7 +338,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { escalateStatus(checkers.CRITICAL) } - timeMetric := fmt.Sprintf("time=%fs;%s;%s", queriesDuration.Seconds(), + timeMetric := fmt.Sprintf( + "time=%fs;%s;%s", queriesDuration.Seconds(), func() string { if opts.WarningTimeout != nil { return fmt.Sprintf("%d", *opts.WarningTimeout) diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index f6268c70..b6ca5f4a 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -20,11 +20,11 @@ import ( func startTestDNSServerHandler(t *testing.T, listenAddr string, handler dns.Handler) string { t.Helper() - pc, err := net.ListenPacket("udp", listenAddr) + packetConnection, err := net.ListenPacket("udp", listenAddr) require.NoError(t, err) srv := &dns.Server{ - PacketConn: pc, + PacketConn: packetConnection, Handler: handler, } go func() { @@ -34,7 +34,7 @@ func startTestDNSServerHandler(t *testing.T, listenAddr string, handler dns.Hand _ = srv.Shutdown() }) - udpAddr, ok := pc.LocalAddr().(*net.UDPAddr) + udpAddr, ok := packetConnection.LocalAddr().(*net.UDPAddr) require.True(t, ok, "local addr is a udp addr") return strconv.Itoa(udpAddr.Port) @@ -59,13 +59,13 @@ func startTestDNSServer(t *testing.T, listenAddr string, rcode int) string { func startSilentDNSServer(t *testing.T, listenAddr string) string { t.Helper() - pc, err := net.ListenPacket("udp", listenAddr) + packetConnection, err := net.ListenPacket("udp", listenAddr) require.NoError(t, err) t.Cleanup(func() { - _ = pc.Close() + _ = packetConnection.Close() }) - udpAddr, ok := pc.LocalAddr().(*net.UDPAddr) + udpAddr, ok := packetConnection.LocalAddr().(*net.UDPAddr) require.True(t, ok, "local addr is a udp addr") return strconv.Itoa(udpAddr.Port) @@ -89,6 +89,16 @@ CheckBuiltinPlugins = enabled ) }) + StopTestAgent(t, snc) +} + +func TestCheckDNSExpectedString(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled + ` + snc := StartTestAgent(t, config) + t.Run("expected string all match", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "94.185.89.33"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") @@ -133,6 +143,16 @@ CheckBuiltinPlugins = enabled ) }) + StopTestAgent(t, snc) +} + +func TestCheckDNSThresholds(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled + ` + snc := StartTestAgent(t, config) + t.Run("warning threshold not triggered", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-w", "999"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") @@ -155,6 +175,16 @@ CheckBuiltinPlugins = enabled ) }) + StopTestAgent(t, snc) +} + +func TestCheckDNSExpectedString2(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled + ` + snc := StartTestAgent(t, config) + t.Run("aaaa lookup", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-q", "AAAA"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") @@ -214,6 +244,16 @@ CheckBuiltinPlugins = enabled ) }) + StopTestAgent(t, snc) +} + +func TestCheckDNSNameserverErrors(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled + ` + snc := StartTestAgent(t, config) + t.Run("nxdomain rcode", func(t *testing.T) { port := startTestDNSServer(t, "127.0.0.1:0", dns.RcodeNameError) res := snc.RunCheck("check_dns", []string{"-H", "nxdomain.example.com.", "-s", "127.0.0.1", "-p", port}) @@ -294,6 +334,16 @@ CheckBuiltinPlugins = enabled assert.Containsf(t, output, "127.0.0.2:"+port+": NXDOMAIN", "second nameserver gets its own line") }) + StopTestAgent(t, snc) +} + +func TestCheckDNSTimeouts(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled + ` + snc := StartTestAgent(t, config) + t.Run("query timeout on all nameservers", func(t *testing.T) { port := startSilentDNSServer(t, "127.0.0.1:0") res := snc.RunCheck("check_dns", []string{"-H", "silent.example.com.", "-s", "127.0.0.1", "-p", port, "-T", "1"}) @@ -313,14 +363,14 @@ CheckBuiltinPlugins = enabled t.Skipf("cannot listen on 127.0.0.2: %s", err) } _ = pc.Close() - startTestDNSServerHandler(t, "127.0.0.2:"+port, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { + startTestDNSServerHandler(t, "127.0.0.2:"+port, dns.HandlerFunc(func(writer dns.ResponseWriter, req *dns.Msg) { reply := new(dns.Msg) reply.SetReply(req) rr, rrErr := dns.NewRR(req.Question[0].Name + " 60 IN A 1.2.3.4") if rrErr == nil { reply.Answer = append(reply.Answer, rr) } - _ = w.WriteMsg(reply) + _ = writer.WriteMsg(reply) })) res := snc.RunCheck("check_dns", []string{ @@ -338,6 +388,16 @@ CheckBuiltinPlugins = enabled ) }) + StopTestAgent(t, snc) +} + +func TestCheckDNSArgumentvalidation(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled + ` + snc := StartTestAgent(t, config) + t.Run("missing host argument", func(t *testing.T) { res := snc.RunCheck("check_dns", []string{}) assert.Equalf(t, CheckExitUnknown, res.State, "state unknown")