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.
This commit is contained in:
commit
6ad7d3f593
29 changed files with 2794 additions and 0 deletions
108
checker/rules_transport.go
Normal file
108
checker/rules_transport.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// turnTLSTransportRule evaluates whether a TLS-capable transport is
|
||||
// available and reports its version/cipher metadata. Kept separate from
|
||||
// the generic dial rule because TURN-TLS deployments often need dedicated
|
||||
// attention (port 5349, certificate covering the TURN hostname, …).
|
||||
type turnTLSTransportRule struct{}
|
||||
|
||||
func (r *turnTLSTransportRule) Name() string { return "stun_turn.tls_transport" }
|
||||
func (r *turnTLSTransportRule) Description() string {
|
||||
return "Verifies that at least one TLS/DTLS transport (stuns/turns) succeeds when present in the endpoint set."
|
||||
}
|
||||
|
||||
func (r *turnTLSTransportRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
var (
|
||||
secureCount, secureOK int
|
||||
states []sdk.CheckState
|
||||
)
|
||||
for _, ep := range data.Endpoints {
|
||||
if !ep.Endpoint.Secure {
|
||||
continue
|
||||
}
|
||||
secureCount++
|
||||
if ep.Dial.OK {
|
||||
secureOK++
|
||||
continue
|
||||
}
|
||||
states = append(states, sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Code: "stun_turn.tls_transport.handshake_failed",
|
||||
Subject: epSubject(ep.Endpoint),
|
||||
Message: ep.Dial.Error,
|
||||
Meta: map[string]any{
|
||||
"fix": "TLS/DTLS handshake failed. Reissue a certificate covering the TURN hostname and reload the server (coturn: `cert=`/`pkey=`).",
|
||||
},
|
||||
})
|
||||
}
|
||||
if secureCount == 0 {
|
||||
return []sdk.CheckState{skippedState("stun_turn.tls_transport.skipped", "No secure (stuns/turns) endpoint discovered.")}
|
||||
}
|
||||
if secureOK == 0 {
|
||||
// All secure endpoints failed, already emitted per-endpoint
|
||||
// crit states; return them as-is.
|
||||
return states
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return []sdk.CheckState{passState("stun_turn.tls_transport.ok", "TLS/DTLS handshake succeeded on every secure endpoint.")}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// ipv6CoverageRule verifies at least one endpoint resolves to an IPv6
|
||||
// address. It never marks an endpoint failed: missing AAAA records are a
|
||||
// coverage concern, not an error.
|
||||
type ipv6CoverageRule struct{}
|
||||
|
||||
func (r *ipv6CoverageRule) Name() string { return "stun_turn.ipv6_coverage" }
|
||||
func (r *ipv6CoverageRule) Description() string {
|
||||
return "Verifies at least one STUN/TURN hostname resolves to an IPv6 address."
|
||||
}
|
||||
|
||||
func (r *ipv6CoverageRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
if len(data.Endpoints) == 0 {
|
||||
return []sdk.CheckState{skippedState("stun_turn.ipv6_coverage.skipped", "No endpoint discovered.")}
|
||||
}
|
||||
var anyResolved, anyV6 bool
|
||||
for _, ep := range data.Endpoints {
|
||||
for _, ipStr := range ep.ResolvedIPs {
|
||||
anyResolved = true
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip != nil && ip.To4() == nil && strings.Contains(ipStr, ":") {
|
||||
anyV6 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if anyV6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !anyResolved {
|
||||
return []sdk.CheckState{skippedState("stun_turn.ipv6_coverage.skipped", "Hostname resolution data unavailable.")}
|
||||
}
|
||||
if !anyV6 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusWarn,
|
||||
Code: "stun_turn.ipv6_coverage.missing",
|
||||
Message: "No STUN/TURN endpoint resolves to an IPv6 address; IPv6-only clients will have no reachable server.",
|
||||
Meta: map[string]any{"fix": "Publish AAAA records for your STUN/TURN hostnames and ensure the server listens on IPv6."},
|
||||
}}
|
||||
}
|
||||
return []sdk.CheckState{passState("stun_turn.ipv6_coverage.ok", "At least one endpoint resolves to an IPv6 address.")}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue