checker: add domain length validation and refactor rules into per-concern checks
This commit is contained in:
parent
df0d429150
commit
946ec446d2
15 changed files with 716 additions and 308 deletions
206
checker/issues.go
Normal file
206
checker/issues.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue