// Package checker implements the STUN/TURN checker for happyDomain. // // The checker drives a target server through the STUN binding and TURN // allocation/relay protocols (RFC 5389, RFC 5766) using the Pion libraries, // then exposes a structured observation and a rich HTML report including // remediation guidance for the most common deployment mistakes. package checker import ( "net" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) // isPrivateAddr returns whether s parses as a private/loopback/link-local // address. Accepts either a bare IP or a host:port form. Hostnames return // false; the caller should resolve first if needed. func isPrivateAddr(s string) bool { ip := net.ParseIP(s) if ip == nil { host, _, err := net.SplitHostPort(s) if err != nil { return false } ip = net.ParseIP(host) } return isPrivate(ip) } // ObservationKeyStunTurn is the observation key for STUN/TURN test data. const ObservationKeyStunTurn sdk.ObservationKey = "stun_turn" // Transport identifies the L4/L4-secure transport used to reach an endpoint. type Transport string const ( TransportUDP Transport = "udp" TransportTCP Transport = "tcp" TransportTLS Transport = "tls" TransportDTLS Transport = "dtls" ) // Endpoint is a single resolved server target to probe. type Endpoint struct { URI string `json:"uri"` Host string `json:"host"` Port uint16 `json:"port"` Transport Transport `json:"transport"` Secure bool `json:"secure"` IsTURN bool `json:"is_turn"` // false: STUN-only scheme, true: TURN scheme Source string `json:"source"` // "uri" or "srv:_turn._udp.example.com" } // DialResult holds the raw outcome of establishing the L4(/secure) // connection to an endpoint. Collected without judgement. type DialResult struct { OK bool `json:"ok"` DurationMs int64 `json:"duration_ms"` RemoteAddr string `json:"remote_addr,omitempty"` Error string `json:"error,omitempty"` // TLS metadata populated when the underlying transport used TLS. TLSVersion string `json:"tls_version,omitempty"` TLSCipher string `json:"tls_cipher,omitempty"` TLSPeerCN string `json:"tls_peer_cn,omitempty"` // DTLSHandshake records whether a DTLS handshake was performed. DTLSHandshake bool `json:"dtls_handshake,omitempty"` } // STUNBindingObservation holds the raw outcome of a STUN Binding probe. type STUNBindingObservation struct { Attempted bool `json:"attempted"` OK bool `json:"ok"` RTTMs int64 `json:"rtt_ms"` ReflexiveAddr string `json:"reflexive_addr,omitempty"` IsPrivateMapped bool `json:"is_private_mapped,omitempty"` Error string `json:"error,omitempty"` } // TURNAllocateObservation holds the raw outcome of a TURN Allocate // attempt (either unauthenticated probe or authenticated attempt). type TURNAllocateObservation struct { Attempted bool `json:"attempted"` OK bool `json:"ok"` DurationMs int64 `json:"duration_ms"` RelayAddr string `json:"relay_addr,omitempty"` IsPrivateRelay bool `json:"is_private_relay,omitempty"` UnauthChallenge bool `json:"unauth_challenge,omitempty"` ErrorCode int `json:"error_code,omitempty"` ErrorReason string `json:"error_reason,omitempty"` Error string `json:"error,omitempty"` } // RelayEchoObservation holds the raw outcome of the relay echo probe. type RelayEchoObservation struct { Attempted bool `json:"attempted"` OK bool `json:"ok"` PeerAddr string `json:"peer_addr,omitempty"` Error string `json:"error,omitempty"` } // EndpointProbe holds the raw, unjudged observation for a single endpoint. type EndpointProbe struct { Endpoint Endpoint `json:"endpoint"` // ResolvedIPs lists the A/AAAA addresses we observed for this host // (populated when the endpoint was reached). Used for IPv6 coverage. ResolvedIPs []string `json:"resolved_ips,omitempty"` Dial DialResult `json:"dial"` STUNBinding STUNBindingObservation `json:"stun_binding"` TURNNoAuth TURNAllocateObservation `json:"turn_noauth"` TURNAuth TURNAllocateObservation `json:"turn_auth"` RelayEcho RelayEchoObservation `json:"relay_echo"` ChannelBindRun bool `json:"channel_bind_run,omitempty"` } // StunTurnData is the JSON-serializable observation payload. It now // carries only raw per-endpoint probe outcomes; rules do the judging. type StunTurnData struct { Zone string `json:"zone,omitempty"` Mode string `json:"mode,omitempty"` RequestedURI string `json:"requested_uri,omitempty"` HasCreds bool `json:"has_creds,omitempty"` ProbePeer string `json:"probe_peer,omitempty"` WarningRTTMs int64 `json:"warning_rtt_ms,omitempty"` CriticalRTT int64 `json:"critical_rtt_ms,omitempty"` CollectedAt time.Time `json:"collected_at"` Endpoints []EndpointProbe `json:"endpoints"` GlobalError string `json:"global_error,omitempty"` }