checker-xmpp/checker/issues.go

206 lines
6.9 KiB
Go

package checker
import "strings"
// deriveIssues walks a raw XMPPData and returns the full list of findings
// that rules (and the HTML report) may surface.
//
// It does not mutate data. It is intentionally pure so that rules can
// recompute their slice of the findings without having to stash anything
// into the observation payload. wantC2S / wantS2S restrict mode-scoped
// checks (SASL / direct-TLS / working-endpoint-coverage); SRV and per-
// endpoint findings are always emitted.
func deriveIssues(data *XMPPData, wantC2S, wantS2S bool) []Issue {
var issues []Issue
// 1. No SRV published.
if data.SRV.FallbackProbed {
issues = append(issues, Issue{
Code: CodeNoSRV,
Severity: SeverityCrit,
Message: "No XMPP SRV records found for " + data.Domain + ".",
Fix: "Publish _xmpp-client._tcp." + data.Domain + " and _xmpp-server._tcp." + data.Domain + " SRV records.",
})
}
// 2. Legacy _jabber.
if len(data.SRV.Jabber) > 0 {
issues = append(issues, Issue{
Code: CodeLegacyJabber,
Severity: SeverityWarn,
Message: "Obsolete _jabber._tcp SRV record still published.",
Fix: "Remove _jabber._tcp records; _xmpp-client._tcp supersedes them.",
})
}
// 3. SRV lookup errors (real DNS failures, not NXDOMAIN).
for prefix, msg := range data.SRV.Errors {
issues = append(issues, Issue{
Code: CodeSRVServfail,
Severity: SeverityWarn,
Message: "DNS lookup failed for " + prefix + data.Domain + ": " + msg,
Fix: "Check the authoritative DNS servers for this domain.",
})
}
// 4. Endpoint-level issues.
allDown := true
sawSCRAM := map[XMPPMode]bool{}
sawSCRAMPlus := map[XMPPMode]bool{}
sawPlainOnly := map[XMPPMode]bool{}
sawAnyWorking := map[XMPPMode]bool{}
for _, ep := range data.Endpoints {
if ep.TCPConnected && ep.STARTTLSUpgraded {
allDown = false
sawAnyWorking[ep.Mode] = true
}
if ep.TCPConnected && ep.StreamOpened && !ep.DirectTLS {
if !ep.STARTTLSOffered {
issues = append(issues, Issue{
Code: CodeStartTLSMissing,
Severity: SeverityCrit,
Message: "STARTTLS not advertised on " + ep.Address + " (" + ep.SRVPrefix + ").",
Fix: "Enable STARTTLS in the XMPP server configuration and require it for all connections.",
Endpoint: ep.Address,
})
} else if !ep.STARTTLSRequired {
issues = append(issues, Issue{
Code: CodeStartTLSNotRequired,
Severity: SeverityWarn,
Message: "STARTTLS offered but not <required/> on " + ep.Address + ".",
Fix: "Set the server to require TLS (e.g. `c2s_require_encryption = true` in Prosody, `starttls_required` in ejabberd).",
Endpoint: ep.Address,
})
}
}
if ep.TCPConnected && !ep.STARTTLSUpgraded && ep.STARTTLSOffered && ep.Error != "" {
issues = append(issues, Issue{
Code: CodeStartTLSFailed,
Severity: SeverityCrit,
Message: "STARTTLS handshake failed on " + ep.Address + ": " + ep.Error + ".",
Fix: "Run the TLS checker on this port for cert and cipher details.",
Endpoint: ep.Address,
})
}
if !ep.TCPConnected && ep.Error != "" {
issues = append(issues, Issue{
Code: CodeTCPUnreachable,
Severity: SeverityWarn,
Message: "Cannot reach " + ep.Address + ": " + ep.Error + ".",
Fix: "Verify firewall rules and that the XMPP server is listening on this address.",
Endpoint: ep.Address,
})
}
// SASL posture (c2s only).
if ep.Mode == ModeClient && ep.STARTTLSUpgraded && len(ep.SASLMechanisms) > 0 {
hasSCRAM := false
hasSCRAMPlus := false
hasPlain := false
nonPlain := false
for _, m := range ep.SASLMechanisms {
u := strings.ToUpper(m)
if strings.HasPrefix(u, "SCRAM-") {
hasSCRAM = true
if strings.HasSuffix(u, "-PLUS") {
hasSCRAMPlus = true
}
}
if u == "PLAIN" {
hasPlain = true
} else {
nonPlain = true
}
}
if hasSCRAM {
sawSCRAM[ep.Mode] = true
}
if hasSCRAMPlus {
sawSCRAMPlus[ep.Mode] = true
}
if hasPlain && !nonPlain {
sawPlainOnly[ep.Mode] = true
}
}
// S2S auth posture, only meaningful if we actually parsed the
// post-TLS features.
if ep.Mode == ModeServer && ep.STARTTLSUpgraded {
if !ep.FeaturesRead {
issues = append(issues, Issue{
Code: CodeS2SProbeIncomplete,
Severity: SeverityInfo,
Message: "Could not read post-TLS stream features on " + ep.Address + "; server may require an authenticated origin for s2s.",
Fix: "This is often benign for well-run public servers. Try from a real federating host if in doubt.",
Endpoint: ep.Address,
})
} else if !ep.DialbackOffered && !ep.SASLExternal {
issues = append(issues, Issue{
Code: CodeS2SNoAuth,
Severity: SeverityCrit,
Message: "No dialback or SASL EXTERNAL advertised on " + ep.Address + " after TLS; federation will fail.",
Fix: "Enable server-to-server dialback, or provision a cert usable for SASL EXTERNAL.",
Endpoint: ep.Address,
})
}
}
}
if len(data.Endpoints) > 0 && allDown {
issues = append(issues, Issue{
Code: CodeAllEndpointsDown,
Severity: SeverityCrit,
Message: "None of the XMPP endpoints could complete STARTTLS.",
Fix: "Verify the server is running and reachable on the published SRV ports.",
})
}
if wantC2S && sawAnyWorking[ModeClient] {
if !sawSCRAM[ModeClient] {
issues = append(issues, Issue{
Code: CodeSASLNoSCRAM,
Severity: SeverityWarn,
Message: "No SCRAM-SHA-* SASL mechanism offered on c2s.",
Fix: "Enable SCRAM-SHA-256 (and SCRAM-SHA-1 for compatibility).",
})
}
if !sawSCRAMPlus[ModeClient] {
issues = append(issues, Issue{
Code: CodeSASLNoSCRAMPlus,
Severity: SeverityInfo,
Message: "No SCRAM-SHA-*-PLUS offered (channel binding).",
Fix: "Enable SCRAM-SHA-256-PLUS to protect against TLS MITM.",
})
}
if sawPlainOnly[ModeClient] {
issues = append(issues, Issue{
Code: CodeSASLPlainOnly,
Severity: SeverityCrit,
Message: "Only SASL PLAIN is offered on c2s.",
Fix: "Enable SCRAM-SHA-256 so credentials are not sent as a password-equivalent hash.",
})
}
}
// IPv6 coverage.
if data.Coverage.HasIPv4 && !data.Coverage.HasIPv6 {
issues = append(issues, Issue{
Code: CodeNoIPv6,
Severity: SeverityInfo,
Message: "No IPv6 endpoint reachable.",
Fix: "Publish AAAA records for the SRV targets.",
})
}
// XEP-0368 direct TLS coverage.
if wantC2S && sawAnyWorking[ModeClient] && len(data.SRV.ClientSecure) == 0 {
issues = append(issues, Issue{
Code: CodeNoDirectTLS,
Severity: SeverityInfo,
Message: "No XEP-0368 direct-TLS SRV record (_xmpps-client._tcp) published.",
Fix: "Publish _xmpps-client._tcp SRV records pointing at port 5223 to allow TLS from the first byte.",
})
}
_ = wantS2S // kept for signature symmetry; s2s-specific rules are expressed via per-endpoint mode checks above
return issues
}