package checker import ( "encoding/json" "fmt" "html/template" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) type tmplHint struct { StatusCSS string StatusText string Message string Fix string } type tmplObservation struct { Name string Status string // "ok" | "fail" | "info" (for neutral data badge) Detail string Error string } type tmplEndpoint struct { URI string Transport string Source string Open bool BadgeText string BadgeCSS string ResolvedIPs []string Observations []tmplObservation Hints []tmplHint } type tmplData struct { Zone string Mode string OverallText string OverallCSS string HeadlineFix string HeadlineDetail string GlobalError string GlobalHints []tmplHint // hints with no endpoint subject Endpoints []tmplEndpoint } var stunturnTemplate = template.Must(template.New("stunturn").Parse(` STUN/TURN Report

STUN/TURN check

{{.OverallText}}
{{if .Zone}}Zone: {{.Zone}} · {{end}} Mode: {{.Mode}} · {{len .Endpoints}} endpoint(s) probed
{{if .HeadlineFix}}
How to fix: {{.HeadlineFix}} {{if .HeadlineDetail}}
{{.HeadlineDetail}}
{{end}}
{{end}}
{{if .GlobalError}}
Discovery failed: {{.GlobalError}}
{{end}} {{if .GlobalHints}}

Global findings

{{range .GlobalHints}}
{{.StatusText}} {{.Message}} {{if .Fix}}
Fix: {{.Fix}}
{{end}}
{{end}}
{{end}} {{range .Endpoints}} {{.URI}} {{.BadgeText}}

Transport: {{.Transport}} · Source: {{.Source}} {{if .ResolvedIPs}} · Resolved: {{range $i, $ip := .ResolvedIPs}}{{if $i}}, {{end}}{{$ip}}{{end}}{{end}}

{{if .Hints}}
Findings
{{range .Hints}}
{{.StatusText}} {{.Message}} {{if .Fix}}
Fix: {{.Fix}}
{{end}}
{{end}} {{end}} {{if .Observations}}
Observations
{{range .Observations}} {{end}}
ProbeResultDetail
{{.Name}} {{.Status}} {{if .Detail}}{{.Detail}}{{end}} {{if .Error}}
⚠ {{.Error}}
{{end}}
{{end}}
{{end}} `)) // GetHTMLReport implements sdk.CheckerHTMLReporter. func (p *stunTurnProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { var d StunTurnData if err := json.Unmarshal(ctx.Data(), &d); err != nil { return "", fmt.Errorf("unmarshal stun/turn report: %w", err) } states := ctx.States() // Group hints by endpoint subject. type group struct { worst sdk.Status hints []tmplHint first *tmplHint // first failing hint (for headline/open logic) } byEp := make(map[string]*group) global := &group{worst: sdk.StatusOK} for _, st := range states { fix := "" if st.Meta != nil { if v, ok := st.Meta["fix"].(string); ok { fix = v } } css, txt := statusBadge(st.Status) h := tmplHint{ StatusCSS: css, StatusText: txt, Message: st.Message, Fix: fix, } var g *group if st.Subject == "" { g = global } else { g = byEp[st.Subject] if g == nil { g = &group{worst: sdk.StatusOK} byEp[st.Subject] = g } } g.hints = append(g.hints, h) if statusSeverity(st.Status) > statusSeverity(g.worst) { g.worst = st.Status } if g.first == nil && isFailing(st.Status) { hh := h g.first = &hh } } td := tmplData{ Zone: d.Zone, Mode: d.Mode, GlobalError: d.GlobalError, GlobalHints: global.hints, } worst := sdk.StatusOK if statusSeverity(global.worst) > statusSeverity(worst) { worst = global.worst } var headlineFix, headlineDetail string if global.first != nil && global.first.Fix != "" { headlineFix = global.first.Fix headlineDetail = global.first.Message } for _, ep := range d.Endpoints { subj := epSubject(ep.Endpoint) g := byEp[subj] te := tmplEndpoint{ URI: ep.Endpoint.URI, Transport: string(ep.Endpoint.Transport), Source: ep.Endpoint.Source, ResolvedIPs: ep.ResolvedIPs, Observations: observationsFor(ep), } epWorst := sdk.StatusOK if g != nil { te.Hints = g.hints epWorst = g.worst if statusSeverity(g.worst) > statusSeverity(worst) { worst = g.worst } if headlineFix == "" && g.first != nil && g.first.Fix != "" { headlineFix = g.first.Fix headlineDetail = fmt.Sprintf("[%s] %s", ep.Endpoint.URI, g.first.Message) } } te.BadgeCSS, te.BadgeText = statusBadge(epWorst) te.Open = isFailing(epWorst) td.Endpoints = append(td.Endpoints, te) } td.OverallCSS, td.OverallText = statusBadge(worst) td.HeadlineFix = headlineFix td.HeadlineDetail = headlineDetail var buf strings.Builder if err := stunturnTemplate.Execute(&buf, td); err != nil { return "", fmt.Errorf("render stun/turn report: %w", err) } return buf.String(), nil } // observationsFor renders the raw probe observations for an endpoint, // without any severity or fix guidance. This is the neutral, data-only // view used both as the default and as a fallback when no CheckStates // are available. func observationsFor(ep EndpointProbe) []tmplObservation { var out []tmplObservation // Dial dialName := fmt.Sprintf("dial:%s", ep.Endpoint.Transport) if ep.Dial.OK { detail := fmt.Sprintf("connected to %s in %d ms", ep.Dial.RemoteAddr, ep.Dial.DurationMs) if ep.Dial.TLSVersion != "" { detail += fmt.Sprintf("; TLS %s, %s, peer CN=%s", ep.Dial.TLSVersion, ep.Dial.TLSCipher, ep.Dial.TLSPeerCN) } if ep.Dial.DTLSHandshake { detail += "; DTLS handshake completed" } out = append(out, tmplObservation{Name: dialName, Status: "ok", Detail: detail}) } else { out = append(out, tmplObservation{Name: dialName, Status: "fail", Error: ep.Dial.Error}) return out } // STUN binding if ep.STUNBinding.Attempted { if ep.STUNBinding.OK { detail := fmt.Sprintf("reflexive address: %s (RTT %d ms)", ep.STUNBinding.ReflexiveAddr, ep.STUNBinding.RTTMs) if ep.STUNBinding.IsPrivateMapped { detail += " [private]" } out = append(out, tmplObservation{Name: "stun_binding", Status: "ok", Detail: detail}) } else { out = append(out, tmplObservation{Name: "stun_binding", Status: "fail", Error: ep.STUNBinding.Error}) } } // TURN no-auth probe if ep.TURNNoAuth.Attempted { switch { case ep.TURNNoAuth.OK: out = append(out, tmplObservation{Name: "turn_allocate_noauth", Status: "ok", Detail: "allocation accepted without authentication"}) case ep.TURNNoAuth.UnauthChallenge: out = append(out, tmplObservation{Name: "turn_allocate_noauth", Status: "ok", Detail: "server challenged the unauthenticated allocate (401)"}) default: out = append(out, tmplObservation{Name: "turn_allocate_noauth", Status: "info", Detail: fmt.Sprintf("code=%d %s", ep.TURNNoAuth.ErrorCode, ep.TURNNoAuth.ErrorReason)}) } } // TURN authenticated allocate if ep.TURNAuth.Attempted { if ep.TURNAuth.OK { detail := fmt.Sprintf("relay address: %s (%d ms)", ep.TURNAuth.RelayAddr, ep.TURNAuth.DurationMs) if ep.TURNAuth.IsPrivateRelay { detail += " [private]" } out = append(out, tmplObservation{Name: "turn_allocate_auth", Status: "ok", Detail: detail}) } else { out = append(out, tmplObservation{ Name: "turn_allocate_auth", Status: "fail", Detail: fmt.Sprintf("STUN error code: %d", ep.TURNAuth.ErrorCode), Error: ep.TURNAuth.Error, }) } } // Relay echo if ep.RelayEcho.Attempted { if ep.RelayEcho.OK { out = append(out, tmplObservation{Name: "turn_relay_echo", Status: "ok", Detail: fmt.Sprintf("CreatePermission + Send to %s succeeded", ep.RelayEcho.PeerAddr)}) } else { out = append(out, tmplObservation{Name: "turn_relay_echo", Status: "fail", Error: ep.RelayEcho.Error}) } } if ep.ChannelBindRun { out = append(out, tmplObservation{Name: "turn_channel_bind", Status: "info", Detail: "ChannelBind exercised implicitly by relay traffic"}) } return out } func statusBadge(s sdk.Status) (cssClass, label string) { switch s { case sdk.StatusOK: return "ok", "OK" case sdk.StatusInfo: return "info", "INFO" case sdk.StatusWarn: return "warn", "WARN" case sdk.StatusCrit: return "crit", "CRIT" case sdk.StatusError: return "error", "ERROR" case sdk.StatusUnknown: return "unknown", "UNKNOWN" } return "info", s.String() } func statusSeverity(s sdk.Status) int { switch s { case sdk.StatusOK: return 0 case sdk.StatusUnknown: return 1 case sdk.StatusInfo: return 2 case sdk.StatusWarn: return 3 case sdk.StatusCrit: return 4 case sdk.StatusError: return 5 } return -1 } func isFailing(s sdk.Status) bool { return s == sdk.StatusWarn || s == sdk.StatusCrit || s == sdk.StatusError }