// Package checker implements the XMPP server checker for happyDomain. // // It probes a domain's XMPP deployment (SRV discovery, STARTTLS, // stream features, SASL mechanisms, dialback / SASL EXTERNAL, // XEP-0368 direct TLS) and reports actionable findings. // // TLS certificate chain / SAN / expiry / cipher posture is intentionally // out of scope; a dedicated TLS checker covers that. package checker import ( sdk "git.happydns.org/checker-sdk-go/checker" ) const ObservationKeyXMPP sdk.ObservationKey = "xmpp" type XMPPMode string const ( ModeClient XMPPMode = "c2s" ModeServer XMPPMode = "s2s" ModeBoth XMPPMode = "both" ) var validModes = []string{string(ModeClient), string(ModeServer), string(ModeBoth)} // XMPPData is the full observation stored per run. type XMPPData struct { Domain string `json:"domain"` RunAt string `json:"run_at"` SRV SRVLookup `json:"srv"` Endpoints []EndpointProbe `json:"endpoints"` Coverage ReachabilitySpan `json:"coverage"` Issues []Issue `json:"issues"` } type SRVLookup struct { Client []SRVRecord `json:"client,omitempty"` Server []SRVRecord `json:"server,omitempty"` ClientSecure []SRVRecord `json:"client_secure,omitempty"` ServerSecure []SRVRecord `json:"server_secure,omitempty"` Jabber []SRVRecord `json:"jabber,omitempty"` // Errors per-set (keyed by record type like "_xmpp-client._tcp"). Errors map[string]string `json:"errors,omitempty"` // FallbackProbed is true when no SRV was published and we probed the bare domain. FallbackProbed bool `json:"fallback_probed,omitempty"` } type SRVRecord struct { Target string `json:"target"` Port uint16 `json:"port"` Priority uint16 `json:"priority"` Weight uint16 `json:"weight"` // IPv4 and IPv6 addresses resolved for the target (at probe time). IPv4 []string `json:"ipv4,omitempty"` IPv6 []string `json:"ipv6,omitempty"` } // EndpointProbe is the result of probing one (mode, host, port, address) tuple. type EndpointProbe struct { Mode XMPPMode `json:"mode"` SRVPrefix string `json:"srv_prefix"` Target string `json:"target"` Port uint16 `json:"port"` Address string `json:"address"` IsIPv6 bool `json:"is_ipv6,omitempty"` DirectTLS bool `json:"direct_tls,omitempty"` // What happened. TCPConnected bool `json:"tcp_connected"` StreamOpened bool `json:"stream_opened"` STARTTLSOffered bool `json:"starttls_offered"` STARTTLSRequired bool `json:"starttls_required"` STARTTLSUpgraded bool `json:"starttls_upgraded"` TLSVersion string `json:"tls_version,omitempty"` TLSCipher string `json:"tls_cipher,omitempty"` // Post-TLS features. FeaturesRead bool `json:"features_read,omitempty"` SASLMechanisms []string `json:"sasl_mechanisms,omitempty"` DialbackOffered bool `json:"dialback_offered,omitempty"` SASLExternal bool `json:"sasl_external,omitempty"` StreamFrom string `json:"stream_from,omitempty"` ElapsedMS int64 `json:"elapsed_ms"` Error string `json:"error,omitempty"` } type ReachabilitySpan struct { HasIPv4 bool `json:"has_ipv4"` HasIPv6 bool `json:"has_ipv6"` // WorkingC2S is true when at least one c2s endpoint completed TLS + advertised SASL. WorkingC2S bool `json:"working_c2s"` // WorkingS2S is true when at least one s2s endpoint completed TLS + advertised dialback or SASL EXTERNAL. WorkingS2S bool `json:"working_s2s"` } // Issue is a structured finding attached to the observation so the rule and // the HTML report can both consume them without re-deriving logic. type Issue struct { Code string `json:"code"` Severity string `json:"severity"` // "info" | "warn" | "crit" Message string `json:"message"` Fix string `json:"fix,omitempty"` Endpoint string `json:"endpoint,omitempty"` } // Severities (string for stable JSON, independent of sdk.Status numeric values). const ( SeverityInfo = "info" SeverityWarn = "warn" SeverityCrit = "crit" ) // Issue codes. const ( CodeNoSRV = "xmpp.no_srv" CodeSRVServfail = "xmpp.srv.servfail" CodeStartTLSMissing = "xmpp.starttls.missing" CodeStartTLSNotRequired = "xmpp.starttls.not_required" CodeStartTLSFailed = "xmpp.starttls.handshake_failed" CodeTCPUnreachable = "xmpp.tcp.unreachable" CodeSASLPlainOnly = "xmpp.sasl.plain_only" CodeSASLNoSCRAM = "xmpp.sasl.no_scram" CodeSASLNoSCRAMPlus = "xmpp.sasl.no_scram_plus" CodeS2SNoAuth = "xmpp.s2s.no_auth" CodeS2SProbeIncomplete = "xmpp.s2s.probe_incomplete" CodeLegacyJabber = "xmpp.legacy_jabber" CodeNoIPv6 = "xmpp.no_ipv6" CodeNoDirectTLS = "xmpp.no_direct_tls" CodeAllEndpointsDown = "xmpp.all_endpoints_down" )