144 lines
5.1 KiB
Go
144 lines
5.1 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// srvPresenceRule verifies that SIP SRV records are published for the
|
|
// domain. It also surfaces NAPTR/SRV lookup errors and the
|
|
// "fell back to bare domain" notice, because they are all about SRV
|
|
// discovery posture.
|
|
type srvPresenceRule struct{}
|
|
|
|
func (r *srvPresenceRule) Name() string { return "sip.srv_present" }
|
|
func (r *srvPresenceRule) Description() string {
|
|
return "Verifies that _sip._udp / _sip._tcp / _sips._tcp SRV records are published and resolvable."
|
|
}
|
|
|
|
func (r *srvPresenceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadSIPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
|
|
var issues []Issue
|
|
totalSRV := len(data.SRV.UDP) + len(data.SRV.TCP) + len(data.SRV.SIPS)
|
|
|
|
if totalSRV == 0 && data.SRV.FallbackProbed {
|
|
issues = append(issues, Issue{
|
|
Code: CodeNoSRV,
|
|
Severity: SeverityCrit,
|
|
Message: "No SIP SRV records published for " + data.Domain + ".",
|
|
Fix: "Publish `_sip._tcp." + data.Domain + ". SRV 10 10 5060 sip." + data.Domain + ".` (and `_sips._tcp` on 5061 for TLS).",
|
|
})
|
|
}
|
|
|
|
for prefix, msg := range data.SRV.Errors {
|
|
if prefix == "naptr" {
|
|
issues = append(issues, Issue{
|
|
Code: CodeNAPTRServfail,
|
|
Severity: SeverityInfo,
|
|
Message: "NAPTR lookup for " + data.Domain + " failed: " + msg,
|
|
Fix: "This is optional. If you meant to expose a NAPTR, verify your authoritative resolver answers AUTH/NXDOMAIN cleanly.",
|
|
})
|
|
continue
|
|
}
|
|
issues = append(issues, Issue{
|
|
Code: CodeSRVServfail,
|
|
Severity: SeverityWarn,
|
|
Message: "SRV lookup for `" + prefix + data.Domain + "` failed: " + msg,
|
|
Fix: "Check zone serial and authoritative NS for this name.",
|
|
})
|
|
}
|
|
|
|
if data.SRV.FallbackProbed {
|
|
issues = append(issues, Issue{
|
|
Code: CodeFallbackProbed,
|
|
Severity: SeverityInfo,
|
|
Message: "No SIP SRV records: probing fell back to " + data.Domain + ":5060 / :5061.",
|
|
Fix: "Publish the SRV records expected by SIP clients and trunks.",
|
|
})
|
|
}
|
|
|
|
if len(issues) == 0 {
|
|
return []sdk.CheckState{passState("sip.srv_present.ok", "SIP SRV records are published and resolved cleanly.")}
|
|
}
|
|
return statesFromIssues(issues)
|
|
}
|
|
|
|
// transportDiversityRule flags SIP deployments that publish a single
|
|
// weak transport (UDP only) or omit the TLS transport entirely.
|
|
type transportDiversityRule struct{}
|
|
|
|
func (r *transportDiversityRule) Name() string { return "sip.transport_diversity" }
|
|
func (r *transportDiversityRule) Description() string {
|
|
return "Verifies that modern SIP transports (TCP, and ideally TLS) are published alongside legacy UDP."
|
|
}
|
|
|
|
func (r *transportDiversityRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadSIPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
|
|
var issues []Issue
|
|
|
|
if len(data.SRV.UDP) > 0 && len(data.SRV.TCP) == 0 && len(data.SRV.SIPS) == 0 && !data.SRV.FallbackProbed {
|
|
issues = append(issues, Issue{
|
|
Code: CodeOnlyUDP,
|
|
Severity: SeverityWarn,
|
|
Message: "Only _sip._udp is published; modern SIP trunks (Twilio, OVH, Orange…) prefer TCP/TLS.",
|
|
Fix: "Also publish `_sip._tcp." + data.Domain + ".` and ideally `_sips._tcp." + data.Domain + ".`.",
|
|
})
|
|
}
|
|
|
|
if wantTLS(opts) && len(data.SRV.SIPS) == 0 && (len(data.SRV.UDP) > 0 || len(data.SRV.TCP) > 0) && !data.SRV.FallbackProbed {
|
|
issues = append(issues, Issue{
|
|
Code: CodeNoTLS,
|
|
Severity: SeverityInfo,
|
|
Message: "No _sips._tcp SRV record, SIP signalling runs in the clear.",
|
|
Fix: "Publish `_sips._tcp." + data.Domain + ".` on port 5061 and terminate TLS on the server.",
|
|
})
|
|
}
|
|
|
|
if len(issues) == 0 {
|
|
return []sdk.CheckState{passState("sip.transport_diversity.ok", "A modern transport (TCP/TLS) is published.")}
|
|
}
|
|
return statesFromIssues(issues)
|
|
}
|
|
|
|
// srvTargetsResolvableRule flags SRV targets that do not resolve to any
|
|
// A or AAAA address.
|
|
type srvTargetsResolvableRule struct{}
|
|
|
|
func (r *srvTargetsResolvableRule) Name() string { return "sip.srv_targets_resolvable" }
|
|
func (r *srvTargetsResolvableRule) Description() string {
|
|
return "Verifies that every SRV target resolves to at least one A or AAAA address."
|
|
}
|
|
|
|
func (r *srvTargetsResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadSIPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
|
|
var issues []Issue
|
|
for _, ep := range data.Endpoints {
|
|
if !ep.Reachable && ep.ReachableErr == "" && ep.Error == "no A/AAAA records for target" {
|
|
issues = append(issues, Issue{
|
|
Code: CodeSRVTargetUnresolved,
|
|
Severity: SeverityCrit,
|
|
Message: "SRV target `" + ep.Target + "` has no A/AAAA.",
|
|
Fix: "Add A/AAAA records for `" + ep.Target + "` or change the SRV target.",
|
|
Endpoint: ep.Target,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(issues) == 0 {
|
|
return []sdk.CheckState{passState("sip.srv_targets_resolvable.ok", "All SRV targets resolve to at least one address.")}
|
|
}
|
|
return statesFromIssues(issues)
|
|
}
|