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) }