135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package monitor
|
|
|
|
import "context"
|
|
import "encoding/json"
|
|
import "fmt"
|
|
import "net"
|
|
import "sort"
|
|
import "strings"
|
|
|
|
import "codit/internal/models"
|
|
|
|
type dnsCheckConfig struct {
|
|
RecordType string `json:"record_type"`
|
|
Expected string `json:"expected"`
|
|
}
|
|
|
|
// checkDNS resolves the target name for the configured record type (stdlib
|
|
// resolver) and optionally asserts an expected value is present. A DNS lookup
|
|
// is a query to the system resolver, not a connection to the target, so the
|
|
// egress policy does not apply here.
|
|
func checkDNS(ctx context.Context, m models.Monitor) models.MonitorCheckResult {
|
|
var cfg dnsCheckConfig
|
|
var name string
|
|
var recordType string
|
|
var values []string
|
|
var resolved string
|
|
var err error
|
|
|
|
cfg = parseDNSConfig(m.Config)
|
|
name = strings.TrimSpace(m.Target)
|
|
if name == "" {
|
|
return failResult("dns target requires a name")
|
|
}
|
|
recordType = strings.ToUpper(strings.TrimSpace(cfg.RecordType))
|
|
if recordType == "" {
|
|
recordType = "A"
|
|
}
|
|
|
|
values, err = lookupDNS(ctx, recordType, name)
|
|
if err != nil {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, Message: fmt.Sprintf("%s lookup failed: %v", recordType, err)}
|
|
}
|
|
if len(values) == 0 {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, Message: fmt.Sprintf("no %s records for %s", recordType, name)}
|
|
}
|
|
sort.Strings(values)
|
|
resolved = strings.Join(values, ", ")
|
|
if strings.TrimSpace(cfg.Expected) != "" && !matchesExpected(values, cfg.Expected) {
|
|
return models.MonitorCheckResult{OK: false, State: models.MonitorStateDown, ResolvedValue: resolved, Message: fmt.Sprintf("expected %q not found in %s records", cfg.Expected, recordType)}
|
|
}
|
|
return models.MonitorCheckResult{OK: true, State: models.MonitorStateUp, ResolvedValue: resolved, Message: fmt.Sprintf("%d %s record(s)", len(values), recordType)}
|
|
}
|
|
|
|
func lookupDNS(ctx context.Context, recordType string, name string) ([]string, error) {
|
|
var resolver *net.Resolver = net.DefaultResolver
|
|
var values []string
|
|
var err error
|
|
|
|
switch recordType {
|
|
case "A", "AAAA":
|
|
var network string
|
|
var ips []net.IP
|
|
var ip net.IP
|
|
if recordType == "A" {
|
|
network = "ip4"
|
|
} else {
|
|
network = "ip6"
|
|
}
|
|
ips, err = resolver.LookupIP(ctx, network, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ip = range ips {
|
|
values = append(values, ip.String())
|
|
}
|
|
case "CNAME":
|
|
var cname string
|
|
cname, err = resolver.LookupCNAME(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cname != "" {
|
|
values = append(values, strings.TrimSuffix(cname, "."))
|
|
}
|
|
case "MX":
|
|
var mxs []*net.MX
|
|
var mx *net.MX
|
|
mxs, err = resolver.LookupMX(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, mx = range mxs {
|
|
values = append(values, fmt.Sprintf("%d %s", mx.Pref, strings.TrimSuffix(mx.Host, ".")))
|
|
}
|
|
case "NS":
|
|
var nss []*net.NS
|
|
var ns *net.NS
|
|
nss, err = resolver.LookupNS(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ns = range nss {
|
|
values = append(values, strings.TrimSuffix(ns.Host, "."))
|
|
}
|
|
case "TXT":
|
|
values, err = resolver.LookupTXT(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported record type %q", recordType)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func matchesExpected(values []string, expected string) bool {
|
|
var v string
|
|
expected = strings.TrimSpace(expected)
|
|
for _, v = range values {
|
|
if strings.Contains(v, expected) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseDNSConfig(raw string) dnsCheckConfig {
|
|
var cfg dnsCheckConfig
|
|
if strings.TrimSpace(raw) == "" {
|
|
return cfg
|
|
}
|
|
_ = json.Unmarshal([]byte(raw), &cfg)
|
|
return cfg
|
|
}
|