206 lines
6.9 KiB
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
|
|
}
|