Initial commit
This commit is contained in:
commit
f6f102079f
19 changed files with 2222 additions and 0 deletions
204
checker/rule.go
Normal file
204
checker/rule.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
func Rule() sdk.CheckRule {
|
||||
return &sipRule{}
|
||||
}
|
||||
|
||||
type sipRule struct{}
|
||||
|
||||
func (r *sipRule) Name() string {
|
||||
return "sip_server"
|
||||
}
|
||||
|
||||
func (r *sipRule) Description() string {
|
||||
return "Checks DNS resolution, reachability and OPTIONS response of a SIP/VoIP server"
|
||||
}
|
||||
|
||||
func (r *sipRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
var data SIPData
|
||||
if err := obs.Get(ctx, ObservationKeySIP, &data); err != nil {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusError,
|
||||
Message: fmt.Sprintf("failed to load SIP observation: %v", err),
|
||||
Code: "sip.observation_error",
|
||||
}}
|
||||
}
|
||||
|
||||
issues := append([]Issue(nil), data.Issues...)
|
||||
related, _ := obs.GetRelated(ctx, TLSRelatedKey)
|
||||
issues = append(issues, tlsIssuesFromRelated(related)...)
|
||||
|
||||
byEndpoint := map[string][]Issue{}
|
||||
var zoneIssues []Issue
|
||||
for _, is := range issues {
|
||||
if is.Endpoint == "" {
|
||||
zoneIssues = append(zoneIssues, is)
|
||||
continue
|
||||
}
|
||||
byEndpoint[is.Endpoint] = append(byEndpoint[is.Endpoint], is)
|
||||
}
|
||||
|
||||
var out []sdk.CheckState
|
||||
out = append(out, zoneState(&data, zoneIssues))
|
||||
|
||||
for _, ep := range data.Endpoints {
|
||||
out = append(out, endpointState(ep, byEndpoint))
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusInfo,
|
||||
Message: "no SIP endpoint to evaluate",
|
||||
Code: "sip.no_endpoint",
|
||||
}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// zoneState summarises findings that are not tied to a specific endpoint:
|
||||
// SRV/NAPTR lookup errors, missing transports, overall coverage.
|
||||
func zoneState(data *SIPData, zoneIssues []Issue) sdk.CheckState {
|
||||
var transports []string
|
||||
if data.Coverage.WorkingUDP {
|
||||
transports = append(transports, "udp")
|
||||
}
|
||||
if data.Coverage.WorkingTCP {
|
||||
transports = append(transports, "tcp")
|
||||
}
|
||||
if data.Coverage.WorkingTLS {
|
||||
transports = append(transports, "tls")
|
||||
}
|
||||
|
||||
meta := map[string]any{
|
||||
"working_udp": data.Coverage.WorkingUDP,
|
||||
"working_tcp": data.Coverage.WorkingTCP,
|
||||
"working_tls": data.Coverage.WorkingTLS,
|
||||
"has_ipv4": data.Coverage.HasIPv4,
|
||||
"has_ipv6": data.Coverage.HasIPv6,
|
||||
"endpoints": len(data.Endpoints),
|
||||
"issue_count": len(data.Issues),
|
||||
}
|
||||
|
||||
worst, firstCrit, firstWarn, critMsgs, warnMsgs := reduceIssues(zoneIssues)
|
||||
|
||||
okMsg := fmt.Sprintf("SIP operational (%s, %d endpoints)", strings.Join(transports, "+"), len(data.Endpoints))
|
||||
return buildCheckState(worst, data.Domain, "sip.ok", okMsg, firstCrit, firstWarn, critMsgs, warnMsgs, meta)
|
||||
}
|
||||
|
||||
// endpointState produces one CheckState per probed endpoint.
|
||||
func endpointState(ep EndpointProbe, byEndpoint map[string][]Issue) sdk.CheckState {
|
||||
subject := string(ep.Transport) + "://" + endpointSubject(ep)
|
||||
|
||||
meta := map[string]any{
|
||||
"transport": string(ep.Transport),
|
||||
"target": ep.Target,
|
||||
"port": ep.Port,
|
||||
"address": ep.Address,
|
||||
"is_ipv6": ep.IsIPv6,
|
||||
"reachable": ep.Reachable,
|
||||
}
|
||||
if ep.TLSVersion != "" {
|
||||
meta["tls_version"] = ep.TLSVersion
|
||||
}
|
||||
if ep.OptionsRawCode != 0 {
|
||||
meta["options_status"] = ep.OptionsStatus
|
||||
meta["options_rtt_ms"] = ep.OptionsRTTMs
|
||||
}
|
||||
|
||||
// Match endpoint issues by either the address or the SRV target
|
||||
// (unresolvable-target issues key on ep.Target).
|
||||
var epIssues []Issue
|
||||
epIssues = append(epIssues, byEndpoint[ep.Address]...)
|
||||
if ep.Target != "" && ep.Target != ep.Address {
|
||||
epIssues = append(epIssues, byEndpoint[ep.Target]...)
|
||||
}
|
||||
|
||||
worst, firstCrit, firstWarn, critMsgs, warnMsgs := reduceIssues(epIssues)
|
||||
|
||||
okMsg := "OPTIONS " + ep.OptionsStatus
|
||||
if okMsg == "OPTIONS " {
|
||||
okMsg = "reachable"
|
||||
}
|
||||
return buildCheckState(worst, subject, "sip.endpoint.ok", okMsg, firstCrit, firstWarn, critMsgs, warnMsgs, meta)
|
||||
}
|
||||
|
||||
// endpointSubject prefers the resolved address; falls back to target:port
|
||||
// when no address was reached (e.g. unresolvable SRV target).
|
||||
func endpointSubject(ep EndpointProbe) string {
|
||||
if ep.Address != "" {
|
||||
return ep.Address
|
||||
}
|
||||
if ep.Target != "" {
|
||||
return net.JoinHostPort(ep.Target, strconv.Itoa(int(ep.Port)))
|
||||
}
|
||||
return strconv.Itoa(int(ep.Port))
|
||||
}
|
||||
|
||||
func buildCheckState(worst sdk.Status, subject, okCode, okMsg, firstCrit, firstWarn string, critMsgs, warnMsgs []string, meta map[string]any) sdk.CheckState {
|
||||
switch worst {
|
||||
case sdk.StatusOK:
|
||||
return sdk.CheckState{Status: sdk.StatusOK, Subject: subject, Code: okCode, Message: okMsg, Meta: meta}
|
||||
case sdk.StatusInfo:
|
||||
return sdk.CheckState{Status: sdk.StatusInfo, Subject: subject, Code: firstWarn, Message: joinTop(warnMsgs, 2), Meta: meta}
|
||||
case sdk.StatusWarn:
|
||||
return sdk.CheckState{Status: sdk.StatusWarn, Subject: subject, Code: firstWarn, Message: joinTop(warnMsgs, 2), Meta: meta}
|
||||
default:
|
||||
return sdk.CheckState{Status: sdk.StatusCrit, Subject: subject, Code: firstCrit, Message: joinTop(critMsgs, 2), Meta: meta}
|
||||
}
|
||||
}
|
||||
|
||||
// reduceIssues collapses a set of issues into a worst status, first codes
|
||||
// per severity, and separated message lists.
|
||||
// sdk.Status values are ordered numerically: OK < Info < Warn < Crit.
|
||||
func reduceIssues(issues []Issue) (worst sdk.Status, firstCrit, firstWarn string, critMsgs, warnMsgs []string) {
|
||||
worst = sdk.StatusOK
|
||||
for _, is := range issues {
|
||||
switch is.Severity {
|
||||
case SeverityCrit:
|
||||
if worst < sdk.StatusCrit {
|
||||
worst = sdk.StatusCrit
|
||||
}
|
||||
if firstCrit == "" {
|
||||
firstCrit = is.Code
|
||||
}
|
||||
critMsgs = append(critMsgs, is.Message)
|
||||
case SeverityWarn:
|
||||
if worst < sdk.StatusWarn {
|
||||
worst = sdk.StatusWarn
|
||||
}
|
||||
if firstWarn == "" {
|
||||
firstWarn = is.Code
|
||||
}
|
||||
warnMsgs = append(warnMsgs, is.Message)
|
||||
case SeverityInfo:
|
||||
if worst < sdk.StatusInfo {
|
||||
worst = sdk.StatusInfo
|
||||
}
|
||||
if firstWarn == "" {
|
||||
firstWarn = is.Code
|
||||
}
|
||||
warnMsgs = append(warnMsgs, is.Message)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func joinTop(msgs []string, n int) string {
|
||||
if len(msgs) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(msgs) <= n {
|
||||
return strings.Join(msgs, "; ")
|
||||
}
|
||||
return strings.Join(msgs[:n], "; ") + fmt.Sprintf(" (+%d more)", len(msgs)-n)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue