Files

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
}