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 }