// 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" )