163 lines
5.9 KiB
Go
163 lines
5.9 KiB
Go
// Package checker implements the SIP / VoIP server checker for
|
|
// happyDomain.
|
|
//
|
|
// It probes a domain's SIP deployment end-to-end (NAPTR + SRV
|
|
// resolution per RFC 3263, reachability on UDP / TCP / TLS, SIP
|
|
// OPTIONS ping per RFC 3261) and reports actionable findings.
|
|
//
|
|
// TLS certificate chain / SAN / expiry / cipher posture is
|
|
// intentionally out of scope, the forthcoming checker-tls covers
|
|
// that. SIPS endpoints are published as "tls" discovery endpoints
|
|
// so checker-tls can probe them; its findings are folded back into
|
|
// this report via GetRelated("tls_probes"). See
|
|
// happydomain3/docs/checker-discovery-endpoint.md.
|
|
package checker
|
|
|
|
import (
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
const ObservationKeySIP sdk.ObservationKey = "sip"
|
|
|
|
// Transport identifies one of the three SIP transports we probe.
|
|
type Transport string
|
|
|
|
const (
|
|
TransportUDP Transport = "udp"
|
|
TransportTCP Transport = "tcp"
|
|
TransportTLS Transport = "tls" // SIPS, direct TLS on connect
|
|
)
|
|
|
|
// SIPData is the full observation stored per run. It is a pure record of
|
|
// what was observed, no severity or pass/fail judgment is encoded here;
|
|
// those are derived by the rules (see issues.go / rules_*.go).
|
|
type SIPData struct {
|
|
Domain string `json:"domain"`
|
|
RunAt string `json:"run_at"`
|
|
NAPTR []NAPTRRecord `json:"naptr,omitempty"`
|
|
SRV SRVLookup `json:"srv"`
|
|
Endpoints []EndpointProbe `json:"endpoints"`
|
|
}
|
|
|
|
// NAPTRRecord is a subset of a NAPTR record enough to reason about
|
|
// SIP service resolution.
|
|
type NAPTRRecord struct {
|
|
Service string `json:"service"` // e.g. "SIP+D2T"
|
|
Regexp string `json:"regexp,omitempty"`
|
|
Replacement string `json:"replacement,omitempty"`
|
|
Flags string `json:"flags,omitempty"`
|
|
Order uint16 `json:"order"`
|
|
Preference uint16 `json:"preference"`
|
|
}
|
|
|
|
// SRVLookup groups the SRV records found per transport plus per-prefix
|
|
// lookup errors and a fallback marker when no SRV was published.
|
|
type SRVLookup struct {
|
|
UDP []SRVRecord `json:"udp,omitempty"`
|
|
TCP []SRVRecord `json:"tcp,omitempty"`
|
|
SIPS []SRVRecord `json:"sips,omitempty"`
|
|
// Errors per-set, keyed by SRV prefix ("_sip._udp.", …).
|
|
Errors map[string]string `json:"errors,omitempty"`
|
|
// FallbackProbed is true when no SRV was published and we probed
|
|
// the bare domain on 5060 / 5061.
|
|
FallbackProbed bool `json:"fallback_probed,omitempty"`
|
|
}
|
|
|
|
// SRVRecord captures one SRV plus the addresses it resolves to.
|
|
type SRVRecord struct {
|
|
Target string `json:"target"`
|
|
Port uint16 `json:"port"`
|
|
Priority uint16 `json:"priority"`
|
|
Weight uint16 `json:"weight"`
|
|
IPv4 []string `json:"ipv4,omitempty"`
|
|
IPv6 []string `json:"ipv6,omitempty"`
|
|
}
|
|
|
|
// EndpointProbe is the result of probing one (transport, target, address).
|
|
type EndpointProbe struct {
|
|
Transport Transport `json:"transport"`
|
|
SRVPrefix string `json:"srv_prefix"`
|
|
Target string `json:"target"`
|
|
Port uint16 `json:"port"`
|
|
Address string `json:"address"`
|
|
IsIPv6 bool `json:"is_ipv6,omitempty"`
|
|
|
|
Reachable bool `json:"reachable"`
|
|
ReachableErr string `json:"reachable_err,omitempty"`
|
|
|
|
TLSVersion string `json:"tls_version,omitempty"`
|
|
TLSCipher string `json:"tls_cipher,omitempty"`
|
|
|
|
OptionsSent bool `json:"options_sent,omitempty"`
|
|
OptionsStatus string `json:"options_status,omitempty"` // e.g. "200 OK"
|
|
OptionsRawCode int `json:"options_raw_code,omitempty"`
|
|
OptionsRTTMs int64 `json:"options_rtt_ms,omitempty"`
|
|
ServerHeader string `json:"server_header,omitempty"`
|
|
UserAgent string `json:"user_agent,omitempty"`
|
|
AllowMethods []string `json:"allow_methods,omitempty"`
|
|
ContactURI string `json:"contact_uri,omitempty"`
|
|
|
|
ElapsedMS int64 `json:"elapsed_ms"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// OK reports whether this probe counts as a working SIP endpoint
|
|
// (reachable + 2xx answer to OPTIONS).
|
|
func (e EndpointProbe) OK() bool {
|
|
return e.Reachable && e.OptionsSent && e.OptionsRawCode >= 200 && e.OptionsRawCode < 300
|
|
}
|
|
|
|
// Coverage is a roll-up of the per-endpoint results. All fields reflect
|
|
// what was *reachable* during this run, not what was merely published in
|
|
// DNS: HasIPv6 is true only if at least one AAAA-resolved endpoint
|
|
// accepted a connection. A target with AAAA but firewalled off will not
|
|
// light up HasIPv6.
|
|
type Coverage struct {
|
|
HasIPv4 bool `json:"has_ipv4"`
|
|
HasIPv6 bool `json:"has_ipv6"`
|
|
WorkingUDP bool `json:"working_udp"`
|
|
WorkingTCP bool `json:"working_tcp"`
|
|
WorkingTLS bool `json:"working_tls"`
|
|
AnyWorking bool `json:"any_working"`
|
|
}
|
|
|
|
// Issue is a structured finding. The rule reduces issues to a worst
|
|
// severity; the report renders them as an actionable fix list.
|
|
type Issue struct {
|
|
Code string `json:"code"`
|
|
Severity string `json:"severity"` // "info" | "warn" | "crit"
|
|
Message string `json:"message"`
|
|
Fix string `json:"fix,omitempty"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
}
|
|
|
|
// Severities. Match checker-xmpp conventions for cross-checker
|
|
// consistency.
|
|
const (
|
|
SeverityInfo = "info"
|
|
SeverityWarn = "warn"
|
|
SeverityCrit = "crit"
|
|
)
|
|
|
|
// Issue codes. Keep short, stable, prefixed with "sip." so downstream
|
|
// consumers can filter.
|
|
const (
|
|
CodeNoSRV = "sip.no_srv"
|
|
CodeOnlyUDP = "sip.srv.only_udp"
|
|
CodeNoTLS = "sip.srv.no_tls"
|
|
CodeSRVServfail = "sip.srv.servfail"
|
|
CodeSRVTargetUnresolved = "sip.srv.target_unresolvable"
|
|
CodeNAPTRServfail = "sip.naptr.servfail"
|
|
|
|
CodeTCPUnreachable = "sip.tcp.unreachable"
|
|
CodeUDPUnreachable = "sip.udp.unreachable"
|
|
CodeTLSHandshake = "sip.tls.handshake_failed"
|
|
CodeOptionsNoAnswer = "sip.options.no_response"
|
|
CodeOptionsNon2xx = "sip.options.non_2xx"
|
|
CodeOptionsNoAllow = "sip.options.no_allow"
|
|
CodeOptionsNoInvite = "sip.options.no_invite"
|
|
|
|
CodeFallbackProbed = "sip.fallback_probed"
|
|
CodeNoIPv6 = "sip.no_ipv6"
|
|
CodeAllDown = "sip.all_endpoints_down"
|
|
)
|