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.
61 lines
1.7 KiB
Go
61 lines
1.7 KiB
Go
package checker
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/pion/turn/v4"
|
|
)
|
|
|
|
// stunBindingResult holds the outcome of a STUN Binding test.
|
|
type stunBindingResult struct {
|
|
RTT time.Duration
|
|
ReflexiveAddr net.Addr
|
|
IsPrivateMapped bool
|
|
Err error
|
|
}
|
|
|
|
// runSTUNBinding sends a STUN Binding Request to the remote and returns the
|
|
// reflexive (XOR-MAPPED) address along with the RTT. We construct a tiny
|
|
// turn.Client with no credentials; its SendBindingRequestTo path drives a
|
|
// vanilla STUN exchange (RFC 5389) and works on UDP/TCP/TLS/DTLS through
|
|
// the dialed PacketConn we hand it.
|
|
func runSTUNBinding(d *dialedConn, timeout time.Duration) stunBindingResult {
|
|
cfg := &turn.ClientConfig{
|
|
Conn: d.pc,
|
|
STUNServerAddr: d.remoteAddr.String(),
|
|
RTO: timeout,
|
|
Software: "happyDomain-checker-stun-turn",
|
|
}
|
|
client, err := turn.NewClient(cfg)
|
|
if err != nil {
|
|
return stunBindingResult{Err: fmt.Errorf("turn.NewClient: %w", err)}
|
|
}
|
|
defer client.Close()
|
|
if err := client.Listen(); err != nil {
|
|
return stunBindingResult{Err: fmt.Errorf("client.Listen: %w", err)}
|
|
}
|
|
start := time.Now()
|
|
addr, err := client.SendBindingRequestTo(d.remoteAddr)
|
|
if err != nil {
|
|
return stunBindingResult{Err: err}
|
|
}
|
|
res := stunBindingResult{
|
|
RTT: time.Since(start),
|
|
ReflexiveAddr: addr,
|
|
}
|
|
if udpAddr, ok := addr.(*net.UDPAddr); ok {
|
|
res.IsPrivateMapped = isPrivate(udpAddr.IP)
|
|
} else if tcpAddr, ok := addr.(*net.TCPAddr); ok {
|
|
res.IsPrivateMapped = isPrivate(tcpAddr.IP)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func isPrivate(ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified()
|
|
}
|