package checker import ( "context" "fmt" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // getData is shared by every rule to pull the observation out of the store. // Any error is surfaced as a StatusError CheckState with a uniform code. func getData(ctx context.Context, obs sdk.ObservationGetter) (*SRVData, *sdk.CheckState) { var d SRVData if err := obs.Get(ctx, ObservationKeySRV, &d); err != nil { return nil, &sdk.CheckState{ Status: sdk.StatusError, Message: fmt.Sprintf("Failed to load SRV observation: %v", err), Code: "srv_obs_error", } } return &d, nil } // ── Rule: SRV records are present ───────────────────────────────────────────── type rulePresent struct{} func RulePresent() sdk.CheckRule { return &rulePresent{} } func (rulePresent) Name() string { return "srv_records_present" } func (rulePresent) Description() string { return "At least one SRV record is published for this service." } func (rulePresent) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } if len(d.Records) == 0 { return []sdk.CheckState{{Status: sdk.StatusCrit, Code: "srv_missing", Message: "No SRV records published."}} } return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_present", Message: fmt.Sprintf("%d SRV record(s) published.", len(d.Records))}} } // ── Rule: Null target ("." means service explicitly unavailable) ────────────── type ruleNullTarget struct{} func RuleNullTarget() sdk.CheckRule { return &ruleNullTarget{} } func (ruleNullTarget) Name() string { return "srv_null_target" } func (ruleNullTarget) Description() string { return "Detects SRV records with target \".\", which signals the service is intentionally not available." } func (ruleNullTarget) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } var nulls []string for _, r := range d.Records { if r.IsNullTarget { nulls = append(nulls, r.Owner) } } if len(nulls) == 0 { return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_no_null", Message: "No null-target SRV records."}} } if len(nulls) == len(d.Records) { return []sdk.CheckState{{Status: sdk.StatusWarn, Code: "srv_all_null", Message: fmt.Sprintf("All %d SRV records use null target (\".\"): service explicitly disabled.", len(nulls))}} } return []sdk.CheckState{{Status: sdk.StatusInfo, Code: "srv_some_null", Message: fmt.Sprintf("%d record(s) have null target: %s", len(nulls), strings.Join(nulls, ", "))}} } // ── Rule: SRV target must not be a CNAME (RFC 2782) ─────────────────────────── type ruleTargetNotCNAME struct{} func RuleTargetNotCNAME() sdk.CheckRule { return &ruleTargetNotCNAME{} } func (ruleTargetNotCNAME) Name() string { return "srv_target_not_cname" } func (ruleTargetNotCNAME) Description() string { return "RFC 2782: SRV targets must resolve directly to A/AAAA, not through a CNAME." } func (ruleTargetNotCNAME) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } var bad []string for _, r := range d.Records { if r.IsNullTarget { continue } if r.IsCNAME { bad = append(bad, r.Target) } } if len(bad) == 0 { return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_targets_not_cname", Message: "All SRV targets resolve directly (no CNAME)."}} } return []sdk.CheckState{{Status: sdk.StatusWarn, Code: "srv_targets_are_cname", Message: fmt.Sprintf("RFC 2782 violation — SRV target(s) are CNAMEs: %s", strings.Join(bad, ", "))}} } // ── Rule: targets resolve to at least one IP ────────────────────────────────── type ruleTargetsResolve struct{} func RuleTargetsResolve() sdk.CheckRule { return &ruleTargetsResolve{} } func (ruleTargetsResolve) Name() string { return "srv_targets_resolve" } func (ruleTargetsResolve) Description() string { return "Every SRV target resolves to at least one A/AAAA address." } func (ruleTargetsResolve) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } var failed []string var checked int for _, r := range d.Records { if r.IsNullTarget { continue } checked++ if len(r.Addresses) == 0 { failed = append(failed, fmt.Sprintf("%s (%s)", r.Target, r.ResolveError)) } } if checked == 0 { return []sdk.CheckState{{Status: sdk.StatusInfo, Code: "srv_no_targets", Message: "No resolvable targets to test."}} } if len(failed) == 0 { return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_all_resolve", Message: fmt.Sprintf("All %d target(s) resolve.", checked)}} } return []sdk.CheckState{{Status: sdk.StatusCrit, Code: "srv_resolve_fail", Message: fmt.Sprintf("Target(s) failed DNS resolution: %s", strings.Join(failed, "; "))}} } // ── Rule: TCP reachable ─────────────────────────────────────────────────────── type ruleTCPReachable struct{} func RuleTCPReachable() sdk.CheckRule { return &ruleTCPReachable{} } func (ruleTCPReachable) Name() string { return "srv_tcp_reachable" } func (ruleTCPReachable) Description() string { return "Every TCP SRV target:port accepts a TCP connection." } func (ruleTCPReachable) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } var total, ok int var failed []string for _, r := range d.Records { if r.IsNullTarget || r.Proto != "tcp" { continue } for _, pr := range r.Probes { if pr.Proto != "tcp" { continue } total++ if pr.Connected { ok++ } else { failed = append(failed, fmt.Sprintf("%s: %s", pr.Address, pr.Error)) } } } if total == 0 { return []sdk.CheckState{{Status: sdk.StatusInfo, Code: "srv_tcp_na", Message: "No TCP targets to test."}} } if ok == total { return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_tcp_ok", Message: fmt.Sprintf("All %d TCP target(s) reachable.", total)}} } if ok == 0 { return []sdk.CheckState{{Status: sdk.StatusCrit, Code: "srv_tcp_all_down", Message: fmt.Sprintf("All %d TCP target(s) unreachable: %s", total, strings.Join(failed, "; "))}} } return []sdk.CheckState{{Status: sdk.StatusWarn, Code: "srv_tcp_partial", Message: fmt.Sprintf("%d/%d TCP target(s) unreachable: %s", total-ok, total, strings.Join(failed, "; "))}} } // ── Rule: UDP reachable (best-effort) ───────────────────────────────────────── type ruleUDPReachable struct{} func RuleUDPReachable() sdk.CheckRule { return &ruleUDPReachable{} } func (ruleUDPReachable) Name() string { return "srv_udp_reachable" } func (ruleUDPReachable) Description() string { return "UDP SRV targets do not return ICMP port-unreachable." } func (ruleUDPReachable) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } var total, ok int var failed []string for _, r := range d.Records { if r.IsNullTarget || r.Proto != "udp" { continue } for _, pr := range r.Probes { if pr.Proto != "udp" { continue } total++ if pr.Connected { ok++ } else { failed = append(failed, fmt.Sprintf("%s: %s", pr.Address, pr.Error)) } } } if total == 0 { return []sdk.CheckState{{Status: sdk.StatusInfo, Code: "srv_udp_na", Message: "No UDP targets to test."}} } if ok == total { return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_udp_ok", Message: fmt.Sprintf("All %d UDP target(s) reachable.", total)}} } return []sdk.CheckState{{Status: sdk.StatusWarn, Code: "srv_udp_issue", Message: fmt.Sprintf("%d/%d UDP target(s) reported port unreachable: %s", total-ok, total, strings.Join(failed, "; "))}} } // ── Rule: redundancy (more than one usable target) ──────────────────────────── type ruleRedundancy struct{} func RuleRedundancy() sdk.CheckRule { return &ruleRedundancy{} } func (ruleRedundancy) Name() string { return "srv_redundancy" } func (ruleRedundancy) Description() string { return "At least two distinct SRV targets exist (avoids single point of failure)." } func (ruleRedundancy) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { d, cs := getData(ctx, obs) if cs != nil { return []sdk.CheckState{*cs} } targets := map[string]bool{} for _, r := range d.Records { if r.IsNullTarget { continue } targets[r.Target] = true } if len(targets) >= 2 { return []sdk.CheckState{{Status: sdk.StatusOK, Code: "srv_redundant", Message: fmt.Sprintf("%d distinct targets.", len(targets))}} } if len(targets) == 1 { return []sdk.CheckState{{Status: sdk.StatusInfo, Code: "srv_single_target", Message: "Single SRV target: no redundancy at DNS level."}} } return []sdk.CheckState{{Status: sdk.StatusInfo, Code: "srv_no_targets_redundancy", Message: "No usable SRV targets."}} }