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.")} }