208 lines
8 KiB
Go
208 lines
8 KiB
Go
package checker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// maxNSResultErrors caps the per-NS error list so a flaky server with many
|
|
// addresses cannot bloat the JSON observation payload. Once the cap is
|
|
// reached, further errors are dropped and a single sentinel entry records the
|
|
// number of suppressed messages.
|
|
const maxNSResultErrors = 16
|
|
|
|
// ObservationKey is the observation key for observation data.
|
|
const ObservationKey = "authoritative-consistency"
|
|
|
|
// Severity classifies a finding emitted by the authoritative-consistency checker.
|
|
type Severity string
|
|
|
|
const (
|
|
SeverityInfo Severity = "info"
|
|
SeverityWarn Severity = "warn"
|
|
SeverityCrit Severity = "crit"
|
|
)
|
|
|
|
// Finding codes: stable machine-readable identifiers used by the UI to
|
|
// localize and link to remediation docs.
|
|
const (
|
|
CodeSerialDrift = "authoritative_consistency_serial_drift"
|
|
CodeSerialStaleVsSaved = "authoritative_consistency_serial_stale_vs_saved"
|
|
CodeSerialAheadOfSaved = "authoritative_consistency_serial_ahead_of_saved"
|
|
CodeNSUnreachable = "authoritative_consistency_ns_unreachable"
|
|
CodeNSUDPFailed = "authoritative_consistency_ns_udp_failed"
|
|
CodeNSTCPFailed = "authoritative_consistency_ns_tcp_failed"
|
|
CodeNSUnresolvable = "authoritative_consistency_ns_unresolvable"
|
|
CodeLame = "authoritative_consistency_lame"
|
|
CodeNoSOA = "authoritative_consistency_no_soa"
|
|
CodeNSRRsetDrift = "authoritative_consistency_ns_rrset_drift"
|
|
CodeNSRRsetMismatchConfig = "authoritative_consistency_ns_rrset_mismatch_config"
|
|
CodeParentDrift = "authoritative_consistency_parent_drift"
|
|
CodeParentQueryFailed = "authoritative_consistency_parent_query_failed"
|
|
CodeSOAFieldsDrift = "authoritative_consistency_soa_fields_drift"
|
|
CodeSlowNS = "authoritative_consistency_slow_ns"
|
|
CodeEDNSUnsupported = "authoritative_consistency_edns_unsupported"
|
|
CodeTooFewNS = "authoritative_consistency_too_few_ns"
|
|
CodeNoNS = "authoritative_consistency_no_ns"
|
|
)
|
|
|
|
// Finding describes a single observation produced while running the
|
|
// checker testsuite.
|
|
type Finding struct {
|
|
// Code is a stable machine-readable identifier (e.g.
|
|
// "authoritative_consistency_serial_drift").
|
|
Code string `json:"code"`
|
|
|
|
// Severity grades the finding.
|
|
Severity Severity `json:"severity"`
|
|
|
|
// Message is a human-readable explanation.
|
|
Message string `json:"message"`
|
|
|
|
// Server is the name server the finding applies to, when the issue is
|
|
// scoped to a specific NS. Empty for zone-wide findings.
|
|
Server string `json:"server,omitempty"`
|
|
|
|
// Addr is the IP:port actually queried when the issue was raised. Useful
|
|
// to distinguish IPv4/IPv6 problems on the same NS name.
|
|
Addr string `json:"addr,omitempty"`
|
|
}
|
|
|
|
// NSResult is the per-name-server view of the zone, populated during Collect.
|
|
// It carries every signal the evaluator needs to decide whether the zone is
|
|
// propagated correctly, plus what the UI needs to render an actionable
|
|
// report.
|
|
type NSResult struct {
|
|
// Name is the NS hostname (FQDN, lowercase).
|
|
Name string `json:"name"`
|
|
|
|
// Addresses is the list of A/AAAA addresses tried for this NS.
|
|
Addresses []string `json:"addresses,omitempty"`
|
|
|
|
// ResolveError is set when no address could be resolved for this NS.
|
|
ResolveError string `json:"resolve_error,omitempty"`
|
|
|
|
// UDPReachable is true when the NS answered at least once over UDP/53.
|
|
UDPReachable bool `json:"udp_reachable"`
|
|
|
|
// TCPReachable is true when the NS answered at least once over TCP/53.
|
|
TCPReachable bool `json:"tcp_reachable"`
|
|
|
|
// Authoritative is true when at least one authoritative (AA=1) answer
|
|
// was received for the zone.
|
|
Authoritative bool `json:"authoritative"`
|
|
|
|
// Serial is the SOA serial returned by this NS (0 when not reachable or
|
|
// the answer does not carry a SOA).
|
|
Serial uint32 `json:"serial,omitempty"`
|
|
|
|
// SOA is the full SOA RR returned by this NS, useful for per-field
|
|
// comparison in the report.
|
|
SOA *dns.SOA `json:"soa,omitempty"`
|
|
|
|
// NSRRset is the NS RRset this server returns for the zone (lowercase
|
|
// FQDNs).
|
|
NSRRset []string `json:"ns_rrset,omitempty"`
|
|
|
|
// EDNSSupported is true when the NS answered correctly to an EDNS0
|
|
// query.
|
|
EDNSSupported bool `json:"edns_supported"`
|
|
|
|
// LatencyMs is the duration (milliseconds) of the SOA query used for
|
|
// the reachability test. 0 when not reachable.
|
|
LatencyMs int64 `json:"latency_ms,omitempty"`
|
|
|
|
// Errors collects low-level query errors encountered while probing this
|
|
// NS. Exposed to help operators debug network/firewall issues. Capped
|
|
// at maxNSResultErrors entries; appendError is the only intended writer.
|
|
Errors []string `json:"errors,omitempty"`
|
|
|
|
// suppressedErrors counts the messages that were dropped after the cap
|
|
// was reached. Reflected back into Errors as a sentinel line so the
|
|
// operator knows the list is truncated.
|
|
suppressedErrors int
|
|
}
|
|
|
|
// appendError records a probe error on the NS result, deduplicating identical
|
|
// messages and capping the total to maxNSResultErrors. Suppressed entries are
|
|
// summarised in a trailing sentinel.
|
|
func (n *NSResult) appendError(format string, args ...any) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
for _, e := range n.Errors {
|
|
if e == msg {
|
|
return
|
|
}
|
|
}
|
|
if len(n.Errors) >= maxNSResultErrors {
|
|
n.suppressedErrors++
|
|
sentinel := fmt.Sprintf("(%d more error(s) suppressed)", n.suppressedErrors)
|
|
// Replace the previous sentinel in place when present.
|
|
if last := len(n.Errors) - 1; last >= 0 && len(n.Errors[last]) > 0 && n.Errors[last][0] == '(' {
|
|
n.Errors[last] = sentinel
|
|
return
|
|
}
|
|
n.Errors = append(n.Errors, sentinel)
|
|
return
|
|
}
|
|
n.Errors = append(n.Errors, msg)
|
|
}
|
|
|
|
// ObservationData is the observation payload stored by the checker. It
|
|
// carries every finding emitted by the testsuite plus the raw observed state
|
|
// from each authoritative server.
|
|
type ObservationData struct {
|
|
// Zone is the FQDN of the zone under test.
|
|
Zone string `json:"zone"`
|
|
|
|
// HasSOA indicates whether the service declares a SOA record (Origin
|
|
// versus NSOnlyOrigin). Drives which tests run.
|
|
HasSOA bool `json:"has_soa"`
|
|
|
|
// DeclaredSerial is the SOA serial saved in happyDomain for this zone.
|
|
// Zero when the service is an NSOnlyOrigin.
|
|
DeclaredSerial uint32 `json:"declared_serial,omitempty"`
|
|
|
|
// DeclaredNS is the list of NS hostnames declared by the service,
|
|
// lowercased and FQDN-normalized.
|
|
DeclaredNS []string `json:"declared_ns,omitempty"`
|
|
|
|
// ParentNS is the list of NS hostnames returned by the parent zone's
|
|
// referral, when parent discovery is enabled. Empty when the parent
|
|
// query is disabled or failed (see ParentQueryError).
|
|
ParentNS []string `json:"parent_ns,omitempty"`
|
|
|
|
// ParentQueryError is set when the parent referral query failed.
|
|
ParentQueryError string `json:"parent_query_error,omitempty"`
|
|
|
|
// Probed is the final list of NS names that were actually probed
|
|
// (union of DeclaredNS and ParentNS, de-duplicated).
|
|
Probed []string `json:"probed,omitempty"`
|
|
|
|
// Results holds the per-NS probe results, keyed by NS hostname.
|
|
Results map[string]*NSResult `json:"results,omitempty"`
|
|
|
|
// Findings is the list of issues / observations produced by the run,
|
|
// ordered by (severity desc, code asc, server asc).
|
|
Findings []Finding `json:"findings"`
|
|
}
|
|
|
|
// originService is the minimal local mirror of happyDomain's
|
|
// `services/abstract.Origin` type. It is duplicated on purpose so that this
|
|
// checker does not have to import the (heavy) happyDomain server module
|
|
// just to decode the service payload. github.com/miekg/dns marshals
|
|
// dns.SOA / dns.NS to JSON in the same shape happyDomain uses.
|
|
type originService struct {
|
|
SOA *dns.SOA `json:"soa,omitempty"`
|
|
NameServers []*dns.NS `json:"ns"`
|
|
}
|
|
|
|
// serviceMessage is the minimal local mirror of happyDomain's ServiceMessage
|
|
// envelope. We only need the embedded service JSON and the type tag; the
|
|
// rest of the meta fields are ignored.
|
|
type serviceMessage struct {
|
|
Type string `json:"_svctype"`
|
|
Domain string `json:"_domain"`
|
|
Service json.RawMessage `json:"Service"`
|
|
}
|