From d9488b8f1f602fb8bf1aa78bd81d689b8e47f161 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Tue, 7 Jul 2026 12:21:18 +0200 Subject: [PATCH 01/14] wip: check-dns-improve-queries add the ability to specify /etc/resolv.conf add the ability to append search domains to hostname todo: implement dns queries over multiple nameservers, currently it only uses the first one in config implement dns queries over multiple search domains, currently it only uses the first one in config implement warning critical timeouts --- pkg/check_dns/check_dns.go | 104 ++++++++++++++++++++++++---- pkg/check_dns/nameserver.go | 65 ++++++++++++----- pkg/check_dns/nameserver_windows.go | 26 +++++-- pkg/snclient/logger.go | 4 ++ 4 files changed, 158 insertions(+), 41 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 993534f2..5a199421 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -6,8 +6,10 @@ import ( "fmt" "io" "net" + "runtime" "strconv" "strings" + "time" "github.com/mackerelio/checkers" "github.com/miekg/dns" @@ -36,6 +38,8 @@ type dnsOpts struct { QueryType string `short:"q" long:"querytype" default:"A" description:"DNS record query type"` Norec bool `long:"norec" description:"Set not recursive mode"` 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"` } func parseArgs(args []string) (*dnsOpts, error) { @@ -47,17 +51,51 @@ func parseArgs(args []string) (*dnsOpts, error) { } func (opts *dnsOpts) run() *checkers.Checker { - var nameserver string + var err error + var clientConfig *dns.ClientConfig + + switch runtime.GOOS { + case "linux", "dawrin", "bsd": + clientConfig, err = dns.ClientConfigFromFile(opts.ResolvConfFile) + if err != nil { + return checkers.Critical(err.Error()) + } + default: + } + + var nameservers []string if opts.Server != "" { - nameserver = opts.Server + nameservers = []string{opts.Server} } else { - nameserver, err = adapterAddress() + nameservers, err = adapterAddress(clientConfig) if err != nil { return checkers.Critical(err.Error()) } } - nameserver = net.JoinHostPort(nameserver, strconv.Itoa(opts.Port)) + for i, _ := range nameservers { + nameservers[i] = net.JoinHostPort(nameservers[i], strconv.Itoa(opts.Port)) + } + + var searchPaths []string + if len(opts.SearchPaths) > 0 { + searchPaths = opts.SearchPaths + } else { + searchPaths = getSearchPaths(clientConfig) + } + + var hostCandidates []string + originalHost := opts.Host + if dns.IsFqdn(originalHost) { + hostCandidates = append(hostCandidates, dns.Fqdn(originalHost)) + } else { + for _, searchPath := range searchPaths { + candidate := dns.Fqdn(originalHost + "." + searchPath) + hostCandidates = append(hostCandidates, candidate) + } + // try the bare host as FQDN as well without a searchPath + hostCandidates = append(hostCandidates, dns.Fqdn(originalHost)) + } queryType, ok := dns.StringToType[strings.ToUpper(opts.QueryType)] if !ok { @@ -65,18 +103,54 @@ func (opts *dnsOpts) run() *checkers.Checker { } c := new(dns.Client) - m := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - RecursionDesired: !opts.Norec, - Opcode: dns.OpcodeQuery, - }, - Question: []dns.Question{{Name: dns.Fqdn(opts.Host), Qtype: queryType, Qclass: dns.StringToClass["IN"]}}, + + var lastErr error + var r *dns.Msg + var duration time.Duration + + var successfulNameserver string // the server that answered successfully + var successfulDuration time.Duration + var successfulHost string + + for _, hostCandidate := range hostCandidates { + for _, nameserver := range nameservers { + message := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + RecursionDesired: !opts.Norec, + Opcode: dns.OpcodeQuery, + }, + Question: []dns.Question{ + { + Name: hostCandidate, + Qtype: queryType, + Qclass: dns.StringToClass["IN"], + }, + }, + } + message.Id = dns.Id() + + r, duration, err = c.Exchange(message, nameserver) + + if err == nil { + successfulNameserver = nameserver + successfulHost = hostCandidate + successfulDuration = duration + + break + } + + lastErr = err + } + + if r != nil { + break + } } - m.Id = dns.Id() - r, _, err := c.Exchange(m, nameserver) - if err != nil { - return checkers.Critical(err.Error()) + if r == nil { + return checkers.Critical(fmt.Sprintf("all attempts failed, last error: %v", lastErr)) + } else { + } checkSt := checkers.OK @@ -135,7 +209,7 @@ func (opts *dnsOpts) run() *checkers.Checker { msg = fmt.Sprintf("%s returns %s (%s)\n", opts.Host, res, dnsType) } } else { - msg = fmt.Sprintf("%s (%s) returns no answer from %s\n", opts.Host, opts.QueryType, nameserver) + msg = fmt.Sprintf("%s (%s) returns no answer from %s\n", opts.Host, opts.QueryType, successfulNameserver) } msg += fmt.Sprintf("HEADER-> %s\n", r.MsgHdr.String()) for _, answer := range r.Answer { diff --git a/pkg/check_dns/nameserver.go b/pkg/check_dns/nameserver.go index 849d7e21..dce70d4c 100644 --- a/pkg/check_dns/nameserver.go +++ b/pkg/check_dns/nameserver.go @@ -10,28 +10,55 @@ import ( "github.com/miekg/dns" ) -func adapterAddress() (string, error) { - conf, err := dns.ClientConfigFromFile("/etc/resolv.conf") - if err != nil { - return "", err - } +func adapterAddress(conf *dns.ClientConfig) (nameservers []string, err error) { if len(conf.Servers) == 0 { - return "", fmt.Errorf("no valid nameserver found") + return nameservers, fmt.Errorf("no valid nameserver found") } - nameserver := conf.Servers[0] - // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L148-L153 - // if the nameserver is from /etc/resolv.conf the [ and ] are already - // added, thereby breaking net.ParseIP. Check for this and don't - // fully qualify such a name - if nameserver[0] == '[' && nameserver[len(nameserver)-1] == ']' { - nameserver = nameserver[1 : len(nameserver)-1] + + parseNameserver := func(nameserver string) (string, error) { + // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L148-L153 + // if the nameserver is from /etc/resolv.conf the [ and ] are already + // added, thereby breaking net.ParseIP. Check for this and don't + // fully qualify such a name + if nameserver[0] == '[' && nameserver[len(nameserver)-1] == ']' { + nameserver = nameserver[1 : len(nameserver)-1] + } + + // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L154-L158 + if net.ParseIP(nameserver) == nil { + nameserver = dns.Fqdn(nameserver) + } + if net.ParseIP(nameserver) == nil { + return "", fmt.Errorf("invalid nameserver: %s", nameserver) + } + + return nameserver, nil + } + + parsedNameservers := make([]string, len(conf.Servers)) + for _, nameserver := range conf.Servers { + parsedNameserver, err := parseNameserver(nameserver) + + if err != nil { + parsedNameservers = append(parsedNameservers, parsedNameserver) + } } - // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L154-L158 - if net.ParseIP(nameserver) == nil { - nameserver = dns.Fqdn(nameserver) + + if len(parsedNameservers) == 0 { + return nameservers, fmt.Errorf("could not parse any nameserver") } - if net.ParseIP(nameserver) == nil { - return "", fmt.Errorf("invalid nameserver: %s", nameserver) + + return parsedNameservers, nil +} + +func AppendSearchPathsIfExists(host string, conf *dns.ClientConfig) string { + if len(conf.Search) > 0 { + return host + "." + conf.Search[0] } - return nameserver, nil + + return host +} + +func getSearchPaths(conf *dns.ClientConfig) []string { + return conf.Search } diff --git a/pkg/check_dns/nameserver_windows.go b/pkg/check_dns/nameserver_windows.go index a45d1fe0..ec6a944d 100644 --- a/pkg/check_dns/nameserver_windows.go +++ b/pkg/check_dns/nameserver_windows.go @@ -15,7 +15,9 @@ import ( ) // ref: https://go.dev/src/net/interface_windows.go -func adapterAddress() (string, error) { +// windows does not use a resolv.config file i.e dns.ClientConfig +// this function is defined for consistency across platforms +func adapterAddress(_ *dns.ClientConfig) (nameservers []string, err error) { var b []byte l := uint32(15000) // recommended initial size for { @@ -23,15 +25,15 @@ func adapterAddress() (string, error) { err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) if err == nil { if l == 0 { - return "", nil + return nameservers, nil } break } if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { - return "", os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) + return nameservers, os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) } if l <= uint32(len(b)) { - return "", os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) + return nameservers, os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) } } var aas []*windows.IpAdapterAddresses @@ -39,7 +41,7 @@ func adapterAddress() (string, error) { aas = append(aas, aa) } if len(aas) == 0 { - return "", fmt.Errorf("no valid nameserver found") + return nameservers, fmt.Errorf("no valid nameserver found") } nameserver := aas[0].FirstDnsServerAddress.Address.IP().String() // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L154-L158 @@ -47,7 +49,17 @@ func adapterAddress() (string, error) { nameserver = dns.Fqdn(nameserver) } if net.ParseIP(nameserver) == nil { - return "", fmt.Errorf("invalid nameserver: %s", nameserver) + return nameservers, fmt.Errorf("invalid nameserver: %s", nameserver) } - return nameserver, nil + return []string{nameserver}, nil +} + +// windows does not use a resolv.config file i.e dns.ClientConfig +// this function is defined for consistency across platforms +func AppendSearchPathsIfExists(host string, _ *dns.ClientConfig) string { + return host +} + +func getSearchPaths(_ *dns.ClientConfig) []string { + return []string{} } diff --git a/pkg/snclient/logger.go b/pkg/snclient/logger.go index 567e629d..04c2463f 100644 --- a/pkg/snclient/logger.go +++ b/pkg/snclient/logger.go @@ -339,6 +339,10 @@ func logTraceASCIIMap(data any) { } } +func getSnclientLogger() *factorlog.FactorLog { + return log +} + // LogWriter implements the io.Writer interface and simply logs everything with given level. type LogWriter struct { level string From c0b48f31d1e5908ba878ff1aebe4452071460e64 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 10 Jul 2026 16:55:14 +0200 Subject: [PATCH 02/14] add support for passsing snclient logger instace to builtin checks through context object --- pkg/snclient/builtin_check.go | 4 ++++ pkg/snclient/logger.go | 4 ---- pkg/utils/context.go | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 pkg/utils/context.go diff --git a/pkg/snclient/builtin_check.go b/pkg/snclient/builtin_check.go index 00e0122f..2f321ecb 100644 --- a/pkg/snclient/builtin_check.go +++ b/pkg/snclient/builtin_check.go @@ -8,6 +8,8 @@ import ( "os" "regexp" "strings" + + "github.com/consol-monitoring/snclient/pkg/utils" ) type CheckBuiltin struct { @@ -65,6 +67,8 @@ func (l *CheckBuiltin) Check(ctx context.Context, snc *Agent, check *CheckData, args = append(args, "-v") } + ctx = utils.ContextWithLogger(ctx, log) + output := bytes.NewBuffer(nil) rc := l.check(ctx, output, args) check.result.Output = output.String() diff --git a/pkg/snclient/logger.go b/pkg/snclient/logger.go index 04c2463f..567e629d 100644 --- a/pkg/snclient/logger.go +++ b/pkg/snclient/logger.go @@ -339,10 +339,6 @@ func logTraceASCIIMap(data any) { } } -func getSnclientLogger() *factorlog.FactorLog { - return log -} - // LogWriter implements the io.Writer interface and simply logs everything with given level. type LogWriter struct { level string diff --git a/pkg/utils/context.go b/pkg/utils/context.go new file mode 100644 index 00000000..60e0927e --- /dev/null +++ b/pkg/utils/context.go @@ -0,0 +1,21 @@ +package utils + +import ( + "context" + + "github.com/kdar/factorlog" +) + +type loggerCtxKey struct{} + +func ContextWithLogger(ctx context.Context, log *factorlog.FactorLog) context.Context { + return context.WithValue(ctx, loggerCtxKey{}, log) +} + +func LoggerFromContext(ctx context.Context) *factorlog.FactorLog { + if log, ok := ctx.Value(loggerCtxKey{}).(*factorlog.FactorLog); ok { + return log + } + + return nil +} From 7664200d0a9cf5bcd6f57488cecc769833e25dac Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 10 Jul 2026 16:56:24 +0200 Subject: [PATCH 03/14] add trace messages when -v , -vv and -vvv arguments are appended to the builtin check --- pkg/snclient/builtin_check.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/snclient/builtin_check.go b/pkg/snclient/builtin_check.go index 2f321ecb..c98452b1 100644 --- a/pkg/snclient/builtin_check.go +++ b/pkg/snclient/builtin_check.go @@ -61,10 +61,13 @@ func (l *CheckBuiltin) Check(ctx context.Context, snc *Agent, check *CheckData, switch { case snc.flags.Verbose >= 3: args = append(args, "-vvv") + log.Tracef("adding -vvv to the builtin check arguments") case snc.flags.Verbose >= 2: args = append(args, "-vv") + log.Tracef("adding -vv to the builtin check arguments") case snc.flags.Verbose >= 1: args = append(args, "-v") + log.Tracef("adding -v to the builtin check arguments") } ctx = utils.ContextWithLogger(ctx, log) From 287f6c5d0508281e915c946edc45c7408f83f6d8 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 10 Jul 2026 17:08:44 +0200 Subject: [PATCH 04/14] check_dns: implement multiple nameserver and search path support previously the code would parse a resolv.conf and pick the first option, not trying to combine all possible names and search paths. the code now checks multiple combinations, until it gets an non-empty answer add a custom argument ResolvConfFile to point at a different resolv.conf add logging support, takes the logger passed through context. use it to print logs if verbose mode is enabled rework handling comparions required to check for expected strings from argumnets. use slice comparion functions --- pkg/check_dns/check_dns.go | 110 +++++++++++++++++++++++------------- pkg/check_dns/nameserver.go | 4 +- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 5a199421..39f9b680 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -7,10 +7,12 @@ import ( "io" "net" "runtime" + "slices" "strconv" "strings" "time" + "github.com/consol-monitoring/snclient/pkg/utils" "github.com/mackerelio/checkers" "github.com/miekg/dns" "github.com/sni/go-flags" @@ -23,7 +25,7 @@ func Check(ctx context.Context, output io.Writer, args []string) int { return 2 } - ckr := opts.run() + ckr := opts.run(ctx) fmt.Fprintf(output, "%s - %s", ckr.Status, strings.TrimSpace(ckr.Message)) return int(ckr.Status) @@ -40,6 +42,7 @@ type dnsOpts struct { 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"` + Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output"` } func parseArgs(args []string) (*dnsOpts, error) { @@ -50,11 +53,13 @@ func parseArgs(args []string) (*dnsOpts, error) { return opts, err } -func (opts *dnsOpts) run() *checkers.Checker { +func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { var err error var clientConfig *dns.ClientConfig + logger := utils.LoggerFromContext(ctx) + switch runtime.GOOS { case "linux", "dawrin", "bsd": clientConfig, err = dns.ClientConfigFromFile(opts.ResolvConfFile) @@ -76,6 +81,9 @@ func (opts *dnsOpts) run() *checkers.Checker { for i, _ := range nameservers { nameservers[i] = net.JoinHostPort(nameservers[i], strconv.Itoa(opts.Port)) } + if logger != nil && opts.Verbose { + logger.Tracef("DNS nameservers: %v ", nameservers) + } var searchPaths []string if len(opts.SearchPaths) > 0 { @@ -83,6 +91,9 @@ func (opts *dnsOpts) run() *checkers.Checker { } else { searchPaths = getSearchPaths(clientConfig) } + if logger != nil && opts.Verbose { + logger.Tracef("DNS search paths: %v ", searchPaths) + } var hostCandidates []string originalHost := opts.Host @@ -96,6 +107,9 @@ func (opts *dnsOpts) run() *checkers.Checker { // try the bare host as FQDN as well without a searchPath hostCandidates = append(hostCandidates, dns.Fqdn(originalHost)) } + if logger != nil && opts.Verbose { + logger.Tracef("DNS host candidates: %v ", hostCandidates) + } queryType, ok := dns.StringToType[strings.ToUpper(opts.QueryType)] if !ok { @@ -108,7 +122,7 @@ func (opts *dnsOpts) run() *checkers.Checker { var r *dns.Msg var duration time.Duration - var successfulNameserver string // the server that answered successfully + var successfulNameserver string var successfulDuration time.Duration var successfulHost string @@ -132,28 +146,52 @@ func (opts *dnsOpts) run() *checkers.Checker { r, duration, err = c.Exchange(message, nameserver) if err == nil { + + if len(r.Answer) == 0 { + if logger != nil && opts.Verbose { + logger.Tracef("DNS query returned empty result, continuing to next combination, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds()) + } + + continue + } + successfulNameserver = nameserver successfulHost = hostCandidate successfulDuration = duration + if logger != nil && opts.Verbose { + logger.Debugf("successfully queried DNS, host: %s, nameserver: %s, duration: %dms", successfulHost, successfulNameserver, successfulDuration.Milliseconds()) + } + break } - lastErr = err - } + if logger != nil && opts.Verbose { + logger.Tracef("DNS query failed, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds()) + } - if r != nil { - break + lastErr = err } } if r == nil { return checkers.Critical(fmt.Sprintf("all attempts failed, last error: %v", lastErr)) - } else { - } checkSt := checkers.OK + + answersWithoutHeaders := make([]string, 0) + answerTypes := make([]string, 0) + for _, answer := range r.Answer { + answerWithoutHeader, answerType, err := dnsAnswer(answer) + if err != nil { + return checkers.Critical(err.Error()) + } + answersWithoutHeaders = append(answersWithoutHeaders, answerWithoutHeader) + answerTypes = append(answerTypes, answerType) + } + + // Special handling of returned DNS addresses VS expected DNS addresses, with set comparisons /** if DNS server return 1.1.1.1, 2.2.2.2 1: -e 1.1.1.1 -e 2.2.2.2 -> OK @@ -169,30 +207,26 @@ func (opts *dnsOpts) run() *checkers.Checker { if !ok { return checkers.Critical(fmt.Sprintf("%s is not supported query type. Only A, AAAA, MX, CNAME are supported query types.", opts.QueryType)) } - match := 0 - for _, expectedString := range opts.ExpectedString { - for _, answer := range r.Answer { - anserWithoutHeader, _, err := dnsAnswer(answer) - if err != nil { - return checkers.Critical(err.Error()) - } - if anserWithoutHeader == expectedString { - match += 1 - } - } - } - if match == len(r.Answer) { - if len(opts.ExpectedString) == len(r.Answer) { // case 1 - checkSt = checkers.OK - } else { // case 2 - checkSt = checkers.WARNING - } - } else { - if match > 0 { // case 3,4 - checkSt = checkers.WARNING - } else { // case 5,6 - checkSt = checkers.CRITICAL - } + + expectedStringsContainOneAnswerAddress := slices.ContainsFunc(opts.ExpectedString, func(ex string) bool { + return slices.Contains(answersWithoutHeaders, ex) + }) + + answerCopy := slices.Clone(answersWithoutHeaders) + expectedCopy := slices.Clone(opts.ExpectedString) + slices.Sort(answerCopy) + slices.Sort(expectedCopy) + expectedStringsAndAnswersAreSame := slices.Equal(answerCopy, expectedCopy) + + switch { + case expectedStringsAndAnswersAreSame: + checkSt = checkers.OK + case expectedStringsContainOneAnswerAddress: + checkSt = checkers.WARNING + case !expectedStringsContainOneAnswerAddress: + checkSt = checkers.CRITICAL + default: + checkSt = checkers.UNKNOWN } } @@ -201,16 +235,12 @@ func (opts *dnsOpts) run() *checkers.Checker { } msg := "" - if len(r.Answer) > 0 { - res, dnsType, err := dnsAnswer(r.Answer[0]) - if err != nil { - msg = err.Error() - } else { - msg = fmt.Sprintf("%s returns %s (%s)\n", opts.Host, res, dnsType) - } + if len(answersWithoutHeaders) > 0 && len(answerTypes) > 0 { + msg = fmt.Sprintf("%s returns %s (%s)\n", opts.Host, answersWithoutHeaders[0], answerTypes[0]) } else { msg = fmt.Sprintf("%s (%s) returns no answer from %s\n", opts.Host, opts.QueryType, successfulNameserver) } + msg += fmt.Sprintf("HEADER-> %s\n", r.MsgHdr.String()) for _, answer := range r.Answer { msg += fmt.Sprintf("ANSWER-> %s\n", answer) diff --git a/pkg/check_dns/nameserver.go b/pkg/check_dns/nameserver.go index dce70d4c..c6f7ab13 100644 --- a/pkg/check_dns/nameserver.go +++ b/pkg/check_dns/nameserver.go @@ -35,11 +35,11 @@ func adapterAddress(conf *dns.ClientConfig) (nameservers []string, err error) { return nameserver, nil } - parsedNameservers := make([]string, len(conf.Servers)) + parsedNameservers := make([]string, 0, len(conf.Servers)) for _, nameserver := range conf.Servers { parsedNameserver, err := parseNameserver(nameserver) - if err != nil { + if err == nil { parsedNameservers = append(parsedNameservers, parsedNameserver) } } From 54fce58809641ef9170ee93edf8b508d05b1668e Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Fri, 10 Jul 2026 17:21:33 +0200 Subject: [PATCH 05/14] check_dns: ai assisted fixes and linter fixes fix typo in platform check "darwin" properly exit the DNS query loop after getting an answer suppress linter from complaining about reutned context regenerate docs --- docs/checks/plugins/check_dns.md | 21 +++++++++++++-------- pkg/check_dns/check_dns.go | 12 +++++++++--- pkg/utils/context.go | 1 + 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index 0999fbf2..01fbb5f5 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -52,14 +52,19 @@ Naemon Config check_dns [OPTIONS] Application Options: - -H, --host= The name or address you want to query - -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 - -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 + -H, --host= The name or address you want to query + -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 + -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) + -v, --verbose Show verbose output Help Options: - -h, --help Show this help message + -h, --help Show this help message ``` diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 39f9b680..a3b792ab 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -54,14 +54,13 @@ func parseArgs(args []string) (*dnsOpts, error) { } func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { - var err error var clientConfig *dns.ClientConfig logger := utils.LoggerFromContext(ctx) switch runtime.GOOS { - case "linux", "dawrin", "bsd": + case "linux", "darwin", "freebsd": clientConfig, err = dns.ClientConfigFromFile(opts.ResolvConfFile) if err != nil { return checkers.Critical(err.Error()) @@ -78,7 +77,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { return checkers.Critical(err.Error()) } } - for i, _ := range nameservers { + for i := range nameservers { nameservers[i] = net.JoinHostPort(nameservers[i], strconv.Itoa(opts.Port)) } if logger != nil && opts.Verbose { @@ -126,6 +125,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { var successfulDuration time.Duration var successfulHost string + var gotAnswer bool + for _, hostCandidate := range hostCandidates { for _, nameserver := range nameservers { message := &dns.Msg{ @@ -158,6 +159,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { successfulNameserver = nameserver successfulHost = hostCandidate successfulDuration = duration + gotAnswer = true if logger != nil && opts.Verbose { logger.Debugf("successfully queried DNS, host: %s, nameserver: %s, duration: %dms", successfulHost, successfulNameserver, successfulDuration.Milliseconds()) @@ -172,6 +174,10 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { lastErr = err } + + if gotAnswer { + break + } } if r == nil { diff --git a/pkg/utils/context.go b/pkg/utils/context.go index 60e0927e..408f4963 100644 --- a/pkg/utils/context.go +++ b/pkg/utils/context.go @@ -8,6 +8,7 @@ import ( type loggerCtxKey struct{} +//nolint:ireturn // context helpers must return context.Context by convention func ContextWithLogger(ctx context.Context, log *factorlog.FactorLog) context.Context { return context.WithValue(ctx, loggerCtxKey{}, log) } From d3ff931b1f6ea3acf73a4a173c41299ad94bc707 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 13 Jul 2026 11:34:26 +0200 Subject: [PATCH 06/14] check_dns: added warning, critical thresholds and timeouts warning and critical thresholds on the total time for the DNS queries, until a successfull non-empty answer is found. the program still returns the query results , but raises a warning/critical status as well warning/critical thresholds use the total time needed, just like montioring-plugins check_dns, which launches nslookup executable. this program does the dns queries itself, multiples of them if multiple nameservers and search domains are configured added also timeout, which stops the program and returns critical directly when exceeded. default is 10 seconds. --- pkg/check_dns/check_dns.go | 159 +++++++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 50 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index a3b792ab..9e463132 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -34,15 +34,18 @@ func Check(ctx context.Context, output io.Writer, args []string) int { // adopted from https://raw.githubusercontent.com/mackerelio/go-check-plugins/master/check-dns/lib/ // 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"` - 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"` - 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"` - Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output"` + 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"` + 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"` + 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 ."` + Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output."` + WarningTimeout *int `short:"w" long:"warning" description:"Warning timeout, if getting a successfull DNS query takes longer than specified, set return status to warning."` + CriticalTimeout *int `short:"c" long:"critical" description:"Critical timeout, 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, it exit with critical status."` } func parseArgs(args []string) (*dnsOpts, error) { @@ -58,6 +61,16 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { var clientConfig *dns.ClientConfig logger := utils.LoggerFromContext(ctx) + tryLogTrace := func(logline string) { + if logger != nil && opts.Verbose { + logger.Trace(logline) + } + } + tryLogDebug := func(logline string) { + if logger != nil && opts.Verbose { + logger.Debug(logline) + } + } switch runtime.GOOS { case "linux", "darwin", "freebsd": @@ -125,59 +138,76 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { var successfulDuration time.Duration var successfulHost string - var gotAnswer bool - - for _, hostCandidate := range hostCandidates { - for _, nameserver := range nameservers { - message := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - RecursionDesired: !opts.Norec, - Opcode: dns.OpcodeQuery, - }, - Question: []dns.Question{ - { - Name: hostCandidate, - Qtype: queryType, - Qclass: dns.StringToClass["IN"], + queryDNSChan := make(chan bool, 1) + queryDNSSuccessfull := false + + queryDNS := func() { + + gotAnswer := false + + for _, hostCandidate := range hostCandidates { + for _, nameserver := range nameservers { + message := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + RecursionDesired: !opts.Norec, + Opcode: dns.OpcodeQuery, }, - }, - } - message.Id = dns.Id() + Question: []dns.Question{ + { + Name: hostCandidate, + Qtype: queryType, + Qclass: dns.StringToClass["IN"], + }, + }, + } + message.Id = dns.Id() - r, duration, err = c.Exchange(message, nameserver) + r, duration, err = c.Exchange(message, nameserver) - if err == nil { + 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())) - if len(r.Answer) == 0 { - if logger != nil && opts.Verbose { - logger.Tracef("DNS query returned empty result, continuing to next combination, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds()) + continue } - continue + successfulNameserver = nameserver + successfulHost = hostCandidate + successfulDuration = duration + gotAnswer = true + tryLogDebug(fmt.Sprintf("successfully queried DNS, host: %s, nameserver: %s, duration: %dms", successfulHost, successfulNameserver, successfulDuration.Milliseconds())) + + break } - successfulNameserver = nameserver - successfulHost = hostCandidate - successfulDuration = duration - gotAnswer = true + tryLogTrace(fmt.Sprintf("DNS query failed, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds())) - if logger != nil && opts.Verbose { - logger.Debugf("successfully queried DNS, host: %s, nameserver: %s, duration: %dms", successfulHost, successfulNameserver, successfulDuration.Milliseconds()) - } + lastErr = err + } + if gotAnswer { break } + } - if logger != nil && opts.Verbose { - logger.Tracef("DNS query failed, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds()) - } + queryDNSChan <- gotAnswer + } - lastErr = err - } + queryBeginTimestamp := time.Now() + go queryDNS() - if gotAnswer { - break - } + 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 + } + queryEndTimestamp := time.Now() + queryDuration := queryEndTimestamp.Sub(queryBeginTimestamp) + + if !queryDNSSuccessfull { + return checkers.Critical(fmt.Sprintf("all attempts failed, last error: %v", lastErr)) } if r == nil { @@ -186,6 +216,25 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { checkSt := checkers.OK + escalateStatus := func(newStatus checkers.Status) { + if newStatus > checkSt { + checkSt = newStatus + tryLogTrace(fmt.Sprintf("status escalated to %s", checkSt.String())) + } + } + + 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)) + 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)) + 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)) + } + answersWithoutHeaders := make([]string, 0) answerTypes := make([]string, 0) for _, answer := range r.Answer { @@ -227,17 +276,27 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { switch { case expectedStringsAndAnswersAreSame: checkSt = checkers.OK + tryLogTrace(fmt.Sprintf("Expected strings: %v and strings from the DNS answer: %v , are the same", + opts.ExpectedString, answerCopy)) case expectedStringsContainOneAnswerAddress: - checkSt = checkers.WARNING + tryLogTrace(fmt.Sprintf("Expected strings: %v contain one of the strings from the DNS answer: %v , but they are not the same, raising status to warning", + opts.ExpectedString, answerCopy)) + escalateStatus(checkers.WARNING) case !expectedStringsContainOneAnswerAddress: - checkSt = checkers.CRITICAL + 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: - checkSt = checkers.UNKNOWN + 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) } } if r.MsgHdr.Rcode != dns.RcodeSuccess { + tryLogTrace("DNS does not have success return code, raising status to critical") checkSt = checkers.CRITICAL + escalateStatus(checkers.UNKNOWN) } msg := "" From de62e8227f19212cf02e3e667e3b5b6e4b580e65 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 13 Jul 2026 12:02:44 +0200 Subject: [PATCH 07/14] check_dns: ai pass - add more tests using labs.consol.de and one.one.one.one address test expected string functionality , MX and AAAA records --- pkg/snclient/check_dns_test.go | 165 +++++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 8 deletions(-) diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index e81cc618..ca827927 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -15,14 +15,163 @@ CheckBuiltinPlugins = enabled ` snc := StartTestAgent(t, config) - res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de"}) - assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf( - t, - `^OK - labs\.consol\.de returns 94\.185\.89\.33`, - string(res.BuildPluginOutput()), - "output matches", - ) + t.Run("basic a lookup", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + 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") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string extra expected", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "94.185.89.33", "-e", "1.2.3.4"}) + assert.Equalf(t, CheckExitWarning, res.State, "state warning") + assert.Regexpf( + t, + `^WARNING - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string missing expected", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "94.185.89.33", "-e", "1.2.3.4", "-e", "5.6.7.8"}) + assert.Equalf(t, CheckExitWarning, res.State, "state warning") + assert.Regexpf( + t, + `^WARNING - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string none match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "1.2.3.4"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("mx query", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "consol.de", "-q", "MX"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - consol\.de returns mail\.consol\.de\. \(MX\)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + 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") + assert.Regexpf( + t, + `^OK`, + string(res.BuildPluginOutput()), + "not warned", + ) + }) + + t.Run("critical threshold triggered", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-c", "0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL`, + string(res.BuildPluginOutput()), + "critical threshold triggered", + ) + }) + + t.Run("norec mode", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "--norec"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + 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") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 2a03:3680:0:2::21 \(AAAA\)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("aaaa expected string match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-q", "AAAA", "-e", "2a03:3680:0:2::21"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 2a03:3680:0:2::21`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("multiple answers all match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "one.one.one.one", "-e", "1.1.1.1", "-e", "1.0.0.1"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - one\.one\.one\.one returns (1\.1\.1\.1|1\.0\.0\.1)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + // Resolves cloudflares one.one.one.one using whatever nameserver is configured, not the 1.1.1.1 DNS namesever + + t.Run("multiple answers partial match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "one.one.one.one", "-e", "1.1.1.1"}) + assert.Equalf(t, CheckExitWarning, res.State, "state warning") + assert.Regexpf( + t, + `^WARNING - one\.one\.one\.one returns (1\.1\.1\.1|1\.0\.0\.1)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + // Resolves cloudflares one.one.one.one using whatever nameserver is configured, not the 1.1.1.1 DNS namesever + + t.Run("multiple answers none match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "one.one.one.one", "-e", "8.8.8.8"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - one\.one\.one\.one returns (1\.1\.1\.1|1\.0\.0\.1)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) StopTestAgent(t, snc) } From c64f1e07d34e3457d0ec28c71a62870fd5aab8c2 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 13 Jul 2026 12:11:47 +0200 Subject: [PATCH 08/14] check_dns: removed MX and norec test, those fail in github CI --- pkg/snclient/check_dns_test.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index ca827927..a80779b1 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -70,17 +70,6 @@ CheckBuiltinPlugins = enabled ) }) - t.Run("mx query", func(t *testing.T) { - res := snc.RunCheck("check_dns", []string{"-H", "consol.de", "-q", "MX"}) - assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf( - t, - `^OK - consol\.de returns mail\.consol\.de\. \(MX\)`, - string(res.BuildPluginOutput()), - "output matches", - ) - }) - 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") @@ -103,17 +92,6 @@ CheckBuiltinPlugins = enabled ) }) - t.Run("norec mode", func(t *testing.T) { - res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "--norec"}) - assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf( - t, - `^OK - labs\.consol\.de returns 94\.185\.89\.33`, - string(res.BuildPluginOutput()), - "output matches", - ) - }) - 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") From f66e6aa9aeb2b2af7223467dae98f80b39db96af Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 13 Jul 2026 12:12:00 +0200 Subject: [PATCH 09/14] check_dns: regenerate docs, citest fixes --- docs/checks/plugins/check_dns.md | 10 ++++++++-- pkg/check_dns/check_dns.go | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index 01fbb5f5..3cbf0eec 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -61,9 +61,15 @@ Application Options: 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 + --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) - -v, --verbose Show verbose output + -v, --verbose Show verbose output. + -w, --warning= Warning timeout, if getting a successfull DNS query takes longer than specified, set return + status to warning. + -c, --critical= Critical timeout, 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, it exit + with critical status. (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 9e463132..d745aae1 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -142,7 +142,6 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queryDNSSuccessfull := false queryDNS := func() { - gotAnswer := false for _, hostCandidate := range hostCandidates { From f2e9f0f8e6ed4f42c2b1be28762ff4e8615ac2b1 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 13 Jul 2026 17:25:55 +0200 Subject: [PATCH 10/14] check_dns: add successfull exhange duration as 'time' metric --- pkg/check_dns/check_dns.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index d745aae1..5b564664 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -300,7 +300,8 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { msg := "" if len(answersWithoutHeaders) > 0 && len(answerTypes) > 0 { - msg = fmt.Sprintf("%s returns %s (%s)\n", opts.Host, answersWithoutHeaders[0], 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 fe9ecc451d0677261c6b4605dddb288ce7bdd9ae Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 13 Jul 2026 17:27:16 +0200 Subject: [PATCH 11/14] check_dns: count exhanges and add them to the error messages --- pkg/check_dns/check_dns.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 5b564664..b68e019f 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -140,6 +140,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queryDNSChan := make(chan bool, 1) queryDNSSuccessfull := false + dnsExchangeCount := 0 queryDNS := func() { gotAnswer := false @@ -162,6 +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 if err == nil { if len(r.Answer) == 0 { @@ -206,11 +208,11 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { queryDuration := queryEndTimestamp.Sub(queryBeginTimestamp) if !queryDNSSuccessfull { - return checkers.Critical(fmt.Sprintf("all attempts failed, last error: %v", lastErr)) + 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 attempts failed, last error: %v", lastErr)) + return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) } checkSt := checkers.OK From 55fed37f274251a1a4e16771990432e02012dcc5 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 16 Jul 2026 13:05:06 +0200 Subject: [PATCH 12/14] check_dns: fix return status when dns response header code is not success it should escalate to critical --- pkg/check_dns/check_dns.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index b68e019f..47afc295 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -296,8 +296,7 @@ func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { if r.MsgHdr.Rcode != dns.RcodeSuccess { tryLogTrace("DNS does not have success return code, raising status to critical") - checkSt = checkers.CRITICAL - escalateStatus(checkers.UNKNOWN) + escalateStatus(checkers.CRITICAL) } msg := "" From 6524d50a7e25510c76994ef18de34b5f0aca1e5d Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 16 Jul 2026 13:05:39 +0200 Subject: [PATCH 13/14] check_dns: clarify timeout options of the check --- docs/checks/plugins/check_dns.md | 12 ++++++------ pkg/check_dns/check_dns.go | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index 3cbf0eec..477cb7a6 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -64,12 +64,12 @@ Application Options: --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) -v, --verbose Show verbose output. - -w, --warning= Warning timeout, if getting a successfull DNS query takes longer than specified, set return - status to warning. - -c, --critical= Critical timeout, 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, it exit - with critical status. (default: 10) + -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) 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 47afc295..d7b22f98 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -43,9 +43,9 @@ type dnsOpts struct { 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 ."` Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output."` - WarningTimeout *int `short:"w" long:"warning" description:"Warning timeout, if getting a successfull DNS query takes longer than specified, set return status to warning."` - CriticalTimeout *int `short:"c" long:"critical" description:"Critical timeout, 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, it exit with critical status."` + 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."` } func parseArgs(args []string) (*dnsOpts, error) { From f479ee54038fc589183f1cb5077eca7bcee34352 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 16 Jul 2026 13:12:10 +0200 Subject: [PATCH 14/14] check_dns: remove unused function AppendSearchPathsIfExists it was used when only the first search path was used. the code now uses all search paths --- pkg/check_dns/nameserver.go | 8 -------- pkg/check_dns/nameserver_windows.go | 6 ------ 2 files changed, 14 deletions(-) diff --git a/pkg/check_dns/nameserver.go b/pkg/check_dns/nameserver.go index c6f7ab13..ebc9a974 100644 --- a/pkg/check_dns/nameserver.go +++ b/pkg/check_dns/nameserver.go @@ -51,14 +51,6 @@ func adapterAddress(conf *dns.ClientConfig) (nameservers []string, err error) { return parsedNameservers, nil } -func AppendSearchPathsIfExists(host string, conf *dns.ClientConfig) string { - if len(conf.Search) > 0 { - return host + "." + conf.Search[0] - } - - return host -} - func getSearchPaths(conf *dns.ClientConfig) []string { return conf.Search } diff --git a/pkg/check_dns/nameserver_windows.go b/pkg/check_dns/nameserver_windows.go index ec6a944d..7a287039 100644 --- a/pkg/check_dns/nameserver_windows.go +++ b/pkg/check_dns/nameserver_windows.go @@ -54,12 +54,6 @@ func adapterAddress(_ *dns.ClientConfig) (nameservers []string, err error) { return []string{nameserver}, nil } -// windows does not use a resolv.config file i.e dns.ClientConfig -// this function is defined for consistency across platforms -func AppendSearchPathsIfExists(host string, _ *dns.ClientConfig) string { - return host -} - func getSearchPaths(_ *dns.ClientConfig) []string { return []string{} }