Initial commit
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>
This commit is contained in:
commit
fa516fdaad
23 changed files with 1869 additions and 0 deletions
85
checker/rule.go
Normal file
85
checker/rule.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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 state
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue