checker-stun-turn/checker/interactive.go
Pierre-Olivier Mercier 7c7706fe3f 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.
2026-04-26 19:55:05 +07:00

77 lines
2.1 KiB
Go

//go:build standalone
package checker
import (
"errors"
"net/http"
"strconv"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// RenderForm exposes the inputs needed to drive a STUN/TURN check from
// the standalone HTML form. The fields mirror the canonical option
// documentation in Definition() (RunOpts then UserOpts) so the form
// stays in lock-step with the JSON schema served at /definition.
func (p *stunTurnProvider) RenderForm() []sdk.CheckerOptionField {
def := p.Definition()
fields := make([]sdk.CheckerOptionField, 0, len(def.Options.RunOpts)+len(def.Options.UserOpts))
fields = append(fields, def.Options.RunOpts...)
fields = append(fields, def.Options.UserOpts...)
return fields
}
// ParseForm turns the submitted form into a CheckerOptions. At least one
// of zone or serverURI must be provided so discoverEndpoints has
// something to work with.
func (p *stunTurnProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) {
zone := strings.TrimSpace(r.FormValue("zone"))
zone = strings.TrimSuffix(zone, ".")
uri := strings.TrimSpace(r.FormValue("serverURI"))
if zone == "" && uri == "" {
return nil, errors.New("either zone or serverURI is required")
}
opts := sdk.CheckerOptions{}
if zone != "" {
opts["zone"] = zone
}
if uri != "" {
opts["serverURI"] = uri
}
if v := strings.TrimSpace(r.FormValue("mode")); v != "" {
switch v {
case "auto", "stun", "turn":
opts["mode"] = v
default:
return nil, errors.New("mode must be one of: auto, stun, turn")
}
}
for _, k := range []string{"username", "credential", "sharedSecret", "realm", "transports", "probePeer"} {
if v := strings.TrimSpace(r.FormValue(k)); v != "" {
opts[k] = v
}
}
if v := r.FormValue("testChannelBind"); v != "" {
opts["testChannelBind"] = v == "true" || v == "on" || v == "1"
}
for _, k := range []string{"warningRTT", "criticalRTT", "timeout"} {
v := strings.TrimSpace(r.FormValue(k))
if v == "" {
continue
}
n, err := strconv.ParseUint(v, 10, 32)
if err != nil {
return nil, errors.New(k + " must be a non-negative integer")
}
opts[k] = int(n)
}
return opts, nil
}