package checker import ( "context" "fmt" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) // stunBindingRule verifies that the STUN Binding request succeeds on every // reachable endpoint (returns a reflexive address). type stunBindingRule struct{} func (r *stunBindingRule) Name() string { return "stun_turn.stun_binding" } func (r *stunBindingRule) Description() string { return "Verifies that the STUN Binding request receives a XOR-MAPPED-ADDRESS reply." } func (r *stunBindingRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } var states []sdk.CheckState seen := false for _, ep := range data.Endpoints { if !ep.Dial.OK || !ep.STUNBinding.Attempted { continue } seen = true if ep.STUNBinding.OK { continue } states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "stun_turn.stun_binding.failed", Subject: epSubject(ep.Endpoint), Message: ep.STUNBinding.Error, Meta: map[string]any{ "fix": "Server did not answer the STUN Binding Request. Check that the STUN service is actually listening on this transport, and that no middlebox is filtering RFC 5389 traffic.", }, }) } if !seen { return []sdk.CheckState{skippedState("stun_turn.stun_binding.skipped", "No endpoint completed a dial, STUN binding not evaluated.")} } if len(states) == 0 { return []sdk.CheckState{passState("stun_turn.stun_binding.ok", "STUN Binding succeeded on every reachable endpoint.")} } return states } // stunReflexivePublicRule flags servers that return a private/loopback // reflexive address (typically a TURN server behind NAT with missing // external-ip configuration). type stunReflexivePublicRule struct{} func (r *stunReflexivePublicRule) Name() string { return "stun_turn.reflexive_public" } func (r *stunReflexivePublicRule) Description() string { return "Flags endpoints that return a private/loopback reflexive address (server unaware of its public IP)." } func (r *stunReflexivePublicRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } var states []sdk.CheckState seen := false for _, ep := range data.Endpoints { if !ep.STUNBinding.OK { continue } seen = true if !ep.STUNBinding.IsPrivateMapped { continue } states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "stun_turn.reflexive_public.private", Subject: epSubject(ep.Endpoint), Message: fmt.Sprintf("server returned a private/loopback IP: %s", ep.STUNBinding.ReflexiveAddr), Meta: map[string]any{ "fix": "Server appears to be behind NAT and unaware of its public IP. Set `external-ip=` (coturn) or the equivalent on your TURN server.", }, }) } if !seen { return []sdk.CheckState{skippedState("stun_turn.reflexive_public.skipped", "No successful STUN Binding to evaluate.")} } if len(states) == 0 { return []sdk.CheckState{passState("stun_turn.reflexive_public.ok", "Every reflexive address is public.")} } return states } // stunLatencyRule folds the warningRTT / criticalRTT thresholds the old // Collect hard-coded into a dedicated rule. type stunLatencyRule struct{} func (r *stunLatencyRule) Name() string { return "stun_turn.stun_latency" } func (r *stunLatencyRule) Description() string { return "Compares the STUN Binding RTT against the configured warning/critical thresholds." } func (r *stunLatencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } warn := time.Duration(sdk.GetIntOption(opts, "warningRTT", int(data.WarningRTTMs))) * time.Millisecond crit := time.Duration(sdk.GetIntOption(opts, "criticalRTT", int(data.CriticalRTT))) * time.Millisecond if warn <= 0 { warn = 200 * time.Millisecond } if crit <= 0 { crit = 1000 * time.Millisecond } var states []sdk.CheckState seen := false for _, ep := range data.Endpoints { if !ep.STUNBinding.OK { continue } seen = true rtt := time.Duration(ep.STUNBinding.RTTMs) * time.Millisecond switch { case rtt > crit: states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "stun_turn.stun_latency.critical", Subject: epSubject(ep.Endpoint), Message: fmt.Sprintf("STUN RTT %dms exceeds critical threshold %dms", ep.STUNBinding.RTTMs, crit.Milliseconds()), Meta: map[string]any{"fix": "Server is very slow to respond. Check server load, network path, and consider deploying closer to your users."}, }) case rtt > warn: states = append(states, sdk.CheckState{ Status: sdk.StatusWarn, Code: "stun_turn.stun_latency.high", Subject: epSubject(ep.Endpoint), Message: fmt.Sprintf("STUN RTT %dms exceeds warning threshold %dms", ep.STUNBinding.RTTMs, warn.Milliseconds()), Meta: map[string]any{"fix": "Latency is high enough to noticeably degrade interactive RTC. Consider a server geographically closer to your users."}, }) } } if !seen { return []sdk.CheckState{skippedState("stun_turn.stun_latency.skipped", "No successful STUN Binding to evaluate.")} } if len(states) == 0 { return []sdk.CheckState{passState("stun_turn.stun_latency.ok", "STUN RTT within acceptable thresholds on every endpoint.")} } return states }