Adds a happyDomain checker that probes STUN/TURN servers end-to-end:
DNS/SRV discovery, UDP/TCP/TLS/DTLS dial, STUN binding + reflexive-addr
sanity, open-relay detection, authenticated TURN Allocate (long-term
creds or REST-API HMAC), public-relay check, CreatePermission + Send
round-trip through the relay, and optional ChannelBind.
Failing sub-tests carry a remediation string (`Fix`) that the HTML
report surfaces as a yellow headline callout and inline next to each
row. Mapping covers the most common coturn misconfigurations
(external-ip, relay-ip, lt-cred-mech, min-port/max-port, cert issues,
401 nonce drift, 441/442/486/508 allocation errors).
Implements sdk.EndpointDiscoverer (checker/discovery.go): every
stuns:/turns:/DTLS endpoint observed during Collect is published as a
DiscoveredEndpoint{Type: "tls"|"dtls"} so a downstream TLS checker can
verify certificates without re-parsing the observation.
Backed by pion/stun/v3 + pion/turn/v4 + pion/dtls/v3; SDK pinned to a
local replace until the EndpointDiscoverer interface ships in a tagged
release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// Rule returns the single aggregated STUN/TURN check rule.
|
|
func Rule() sdk.CheckRule {
|
|
return &stunTurnRule{}
|
|
}
|
|
|
|
type stunTurnRule struct{}
|
|
|
|
func (r *stunTurnRule) Name() string { return "stun_turn" }
|
|
func (r *stunTurnRule) Description() string {
|
|
return "Validates STUN binding and TURN allocation against the configured server(s)."
|
|
}
|
|
|
|
func (r *stunTurnRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
var data StunTurnData
|
|
if err := obs.Get(ctx, ObservationKeyStunTurn, &data); err != nil {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusError,
|
|
Message: fmt.Sprintf("failed to get STUN/TURN observation: %v", err),
|
|
Code: "stun_turn_obs_error",
|
|
}}
|
|
}
|
|
|
|
if data.GlobalError != "" {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusError,
|
|
Message: data.GlobalError,
|
|
Code: "stun_turn_discovery_error",
|
|
}}
|
|
}
|
|
if len(data.Endpoints) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusError,
|
|
Message: "no endpoints to probe",
|
|
Code: "stun_turn_no_endpoints",
|
|
}}
|
|
}
|
|
|
|
worst := SubTestOK
|
|
var firstFailLine string
|
|
for _, ep := range data.Endpoints {
|
|
w := ep.Worst()
|
|
if statusRank(w) > statusRank(worst) {
|
|
worst = w
|
|
}
|
|
if firstFailLine == "" {
|
|
if f := ep.FirstFailure(); f != nil {
|
|
parts := []string{
|
|
fmt.Sprintf("[%s] %s", ep.Endpoint.URI, f.Name),
|
|
}
|
|
if f.Detail != "" {
|
|
parts = append(parts, f.Detail)
|
|
}
|
|
if f.Error != "" {
|
|
parts = append(parts, f.Error)
|
|
}
|
|
firstFailLine = strings.Join(parts, ": ")
|
|
if f.Fix != "" {
|
|
firstFailLine += ". Fix: " + f.Fix
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
state := sdk.CheckState{
|
|
Status: toSDKStatus(worst),
|
|
Code: fmt.Sprintf("stun_turn_%s", worst),
|
|
Meta: map[string]any{
|
|
"endpoint_count": len(data.Endpoints),
|
|
},
|
|
}
|
|
if firstFailLine != "" {
|
|
state.Message = firstFailLine
|
|
} else {
|
|
state.Message = fmt.Sprintf("All %d endpoint(s) healthy", len(data.Endpoints))
|
|
}
|
|
return []sdk.CheckState{state}
|
|
}
|