Compare commits

..

No commits in common. "2c2fb0712989a94b0075eb8dcea91eece2976eca" and "bda66e47ebbcd735e5341ad401ff7f0f4ef8117c" have entirely different histories.

15 changed files with 255 additions and 160 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
checker-xmpp
checker-xmpp.so

View file

@ -8,17 +8,31 @@ STARTTLS, SASL mechanisms, federation auth (dialback / SASL EXTERNAL),
and XEP-0368 direct-TLS. Produces an actionable HTML report with a
remediation panel surfacing the most common real-world failures.
TLS certificate chain / SAN / expiry / cipher posture is **out of scope**
a dedicated TLS checker handles that. This checker only confirms that
TLS certificate chain / SAN / expiry / cipher posture is **out of scope**:
a dedicated TLS checker handles that. This checker only confirms that
STARTTLS completes and records the negotiated TLS version/cipher for
context.
When a TLS checker runs against the endpoints we publish via
`EndpointDiscoverer`, its observations are automatically folded into our
rule aggregation and HTML report via the SDK's `GetRelated` /
`CheckerHTMLReporterCtx` composition path — so a bad cert on an XMPP
endpoint shows up on the XMPP service page, not only in a separate TLS
view. The expected observation key is `tls_probes`.
We publish each probed endpoint as a `DiscoveryEntry` of type
`tls.endpoint.v1` so that `checker-tls` (or any other consumer of that
contract) can run TLS posture checks against them without redoing the
SRV lookup. The entries are produced through
`git.happydns.org/checker-tls/contract`, with `SNI` set to the bare JID
domain; XMPP certificates must be valid for the source domain (RFC 6120
§13.7.2.1), which is typically different from the SRV target hostname.
`RequireSTARTTLS` is carried over from the STARTTLS-required posture we
actually observed during probing, so an operator who requires STARTTLS
will see a CRIT on the TLS side, not a WARN, if the server later drops
it.
The TLS checker's resulting observations (under the `tls_probes` key)
are folded back into our rule aggregation and HTML report via the SDK's
`ObservationGetter.GetRelated` / `ReportContext.Related` path: a bad
certificate on an XMPP endpoint shows up on the XMPP service page, not
only in a separate TLS view. The matching between a probe and its XMPP
endpoint is done on `RelatedObservation.Ref`, which carries the same
value as `DiscoveryEntry.Ref` we emitted (computed deterministically by
`contract.Ref`).
## What it checks

View file

@ -24,6 +24,14 @@ const (
tlsNS = "urn:ietf:params:xml:ns:xmpp-tls"
)
func tlsProbeConfig(serverName string) *tls.Config {
return &tls.Config{
ServerName: serverName,
InsecureSkipVerify: true, //nolint:gosec: cert validation is the TLS checker's job
MinVersion: tls.VersionTLS10,
}
}
// Collect runs the full XMPP probe for a domain.
func (p *xmppProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
domain, _ := sdk.GetOption[string](opts, "domain")
@ -126,8 +134,8 @@ func probeSet(ctx context.Context, data *XMPPData, domain string, mode XMPPMode,
}
type probeAddr struct {
ip string
isV6 bool
ip string
isV6 bool
}
func addressesForProbe(rec SRVRecord) []probeAddr {
@ -162,7 +170,7 @@ func probeEndpoint(ctx context.Context, domain string, mode XMPPMode, prefix str
dialCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
dialer := &net.Dialer{Timeout: timeout}
dialer := &net.Dialer{}
rawConn, err := dialer.DialContext(dialCtx, "tcp", result.Address)
if err != nil {
result.Error = "tcp: " + err.Error()
@ -175,12 +183,8 @@ func probeEndpoint(ctx context.Context, domain string, mode XMPPMode, prefix str
var conn net.Conn = rawConn
if directTLS {
tlsConn := tls.Client(rawConn, &tls.Config{
ServerName: domain,
InsecureSkipVerify: true, //nolint:gosec — cert validation is the TLS checker's job
MinVersion: tls.VersionTLS10,
})
if err := tlsConn.HandshakeContext(dialCtx); err != nil {
tlsConn := tls.Client(rawConn, tlsProbeConfig(domain))
if err := tlsConn.Handshake(); err != nil {
result.Error = "tls-handshake: " + err.Error()
return result
}
@ -202,7 +206,6 @@ func probeEndpoint(ctx context.Context, domain string, mode XMPPMode, prefix str
return result
}
// STARTTLS path.
dec, from, err := openStream(conn, domain, ns, mode == ModeServer)
if err != nil {
result.Error = "stream: " + err.Error()
@ -222,13 +225,12 @@ func probeEndpoint(ctx context.Context, domain string, mode XMPPMode, prefix str
}
if !result.STARTTLSOffered {
// Record any features seen in plaintext, but do not proceed we
// Record any features seen in plaintext, but do not proceed; we
// intentionally refuse to send SASL over a non-TLS channel.
applyFeatures(&result, feats)
return result
}
// Request STARTTLS.
if _, err := io.WriteString(conn, `<starttls xmlns='`+tlsNS+`'/>`); err != nil {
result.Error = "starttls-write: " + err.Error()
return result
@ -238,14 +240,9 @@ func probeEndpoint(ctx context.Context, domain string, mode XMPPMode, prefix str
return result
}
// Upgrade.
tlsConn := tls.Client(rawConn, &tls.Config{
ServerName: domain,
InsecureSkipVerify: true, //nolint:gosec — cert validation is the TLS checker's job
MinVersion: tls.VersionTLS10,
})
tlsConn := tls.Client(rawConn, tlsProbeConfig(domain))
_ = tlsConn.SetDeadline(time.Now().Add(timeout))
if err := tlsConn.HandshakeContext(dialCtx); err != nil {
if err := tlsConn.Handshake(); err != nil {
result.Error = "tls-handshake: " + err.Error()
return result
}
@ -283,8 +280,6 @@ func applyFeatures(ep *EndpointProbe, feats *streamFeatures) {
}
}
// ─── Stream / XML plumbing ────────────────────────────────────────────────────
type streamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
StartTLS *startTLSEl
@ -400,8 +395,6 @@ func expectProceed(dec *xml.Decoder) error {
}
}
// ─── DNS ──────────────────────────────────────────────────────────────────────
func lookupSRV(ctx context.Context, r *net.Resolver, prefix, domain string) ([]SRVRecord, error) {
name := prefix + dns.Fqdn(domain)
_, records, err := r.LookupSRV(ctx, "", "", name)
@ -446,8 +439,6 @@ func resolveAllInto(ctx context.Context, r *net.Resolver, records []SRVRecord) {
}
}
// ─── Coverage + issues ────────────────────────────────────────────────────────
func computeCoverage(data *XMPPData) {
for _, ep := range data.Endpoints {
if ep.TCPConnected {
@ -517,9 +508,6 @@ func deriveIssues(data *XMPPData, wantC2S, _ bool) []Issue {
sawAnyWorking := map[XMPPMode]bool{}
for _, ep := range data.Endpoints {
if ep.DirectTLS {
// direct-TLS endpoints don't contribute to STARTTLS-missing logic
}
if ep.TCPConnected && ep.STARTTLSUpgraded {
allDown = false
sawAnyWorking[ep.Mode] = true
@ -591,7 +579,7 @@ func deriveIssues(data *XMPPData, wantC2S, _ bool) []Issue {
sawPlainOnly[ep.Mode] = true
}
}
// S2S auth posture only meaningful if we actually parsed the
// S2S auth posture, only meaningful if we actually parsed the
// post-TLS features. Many public servers don't respond fully to
// anonymous s2s probes; in that case we emit a probe_incomplete
// info instead of falsely asserting "no auth".
@ -600,7 +588,7 @@ func deriveIssues(data *XMPPData, wantC2S, _ bool) []Issue {
issues = append(issues, Issue{
Code: CodeS2SProbeIncomplete,
Severity: SeverityInfo,
Message: "Could not read post-TLS stream features on " + ep.Address + " server may require an authenticated origin for s2s.",
Message: "Could not read post-TLS stream features on " + ep.Address + "; server may require an authenticated origin for s2s.",
Fix: "This is often benign for well-run public servers. Try from a real federating host if in doubt.",
Endpoint: ep.Address,
})
@ -608,7 +596,7 @@ func deriveIssues(data *XMPPData, wantC2S, _ bool) []Issue {
issues = append(issues, Issue{
Code: CodeS2SNoAuth,
Severity: SeverityCrit,
Message: "No dialback or SASL EXTERNAL advertised on " + ep.Address + " after TLS federation will fail.",
Message: "No dialback or SASL EXTERNAL advertised on " + ep.Address + " after TLS; federation will fail.",
Fix: "Enable server-to-server dialback, or provision a cert usable for SASL EXTERNAL.",
Endpoint: ep.Address,
})

View file

@ -4,6 +4,8 @@ import (
"encoding/xml"
"strings"
"testing"
tlsct "git.happydns.org/checker-tls/contract"
)
func TestReadFeatures_WithStartTLSAndSCRAM(t *testing.T) {
@ -196,7 +198,7 @@ func TestComputeCoverage_Mixed(t *testing.T) {
}
}
func TestDiscoverEndpoints_AllSets(t *testing.T) {
func TestDiscoverEntries_AllSets(t *testing.T) {
d := &XMPPData{
Domain: "example.com",
SRV: SRVLookup{
@ -212,59 +214,70 @@ func TestDiscoverEndpoints_AllSets(t *testing.T) {
},
}
p := &xmppProvider{}
eps, err := p.DiscoverEndpoints(d)
raw, err := p.DiscoverEntries(d)
if err != nil {
t.Fatalf("DiscoverEndpoints: %v", err)
t.Fatalf("DiscoverEntries: %v", err)
}
if len(eps) != 4 {
t.Fatalf("expected 4 endpoints (legacy jabber excluded), got %d: %+v", len(eps), eps)
if len(raw) != 4 {
t.Fatalf("expected 4 entries (legacy jabber excluded), got %d: %+v", len(raw), raw)
}
by := map[string]int{}
for i, e := range eps {
by[e.Type+":"+e.Host+":"+itoa(int(e.Port))] = i
if e.SNI != "example.com" {
t.Errorf("endpoint %d: SNI=%q, want example.com", i, e.SNI)
type signature struct {
starttls string
host string
port uint16
}
by := map[signature]tlsct.TLSEndpoint{}
for i, e := range raw {
if e.Type != tlsct.Type {
t.Errorf("entry %d: Type=%q, want %q", i, e.Type, tlsct.Type)
}
ep, err := tlsct.ParseEntry(e)
if err != nil {
t.Fatalf("entry %d: ParseEntry: %v", i, err)
}
if ep.SNI != "example.com" {
t.Errorf("entry %d: SNI=%q, want example.com", i, ep.SNI)
}
by[signature{ep.STARTTLS, ep.Host, ep.Port}] = ep
}
c2s := eps[by["starttls-xmpp-client:xmpp.example.com:5222"]]
if c2s.Meta["starttls"] != "required" {
t.Errorf("c2s starttls meta = %v, want required", c2s.Meta)
c2s, ok := by[signature{"xmpp-client", "xmpp.example.com", 5222}]
if !ok {
t.Fatal("missing c2s entry")
}
s2s := eps[by["starttls-xmpp-server:xmpp.example.com:5269"]]
if s2s.Meta["starttls"] != "opportunistic" {
t.Errorf("s2s starttls meta = %v, want opportunistic", s2s.Meta)
if !c2s.RequireSTARTTLS {
t.Errorf("c2s RequireSTARTTLS = false, want true")
}
direct := eps[by["tls:xmpp.example.com:5223"]]
if direct.Meta != nil {
t.Errorf("direct-TLS endpoint should not carry starttls meta, got %v", direct.Meta)
s2s, ok := by[signature{"xmpp-server", "xmpp.example.com", 5269}]
if !ok {
t.Fatal("missing s2s entry")
}
if s2s.RequireSTARTTLS {
t.Errorf("s2s RequireSTARTTLS = true, want false (opportunistic)")
}
directClient, ok := by[signature{"", "xmpp.example.com", 5223}]
if !ok {
t.Fatal("missing direct-TLS client entry")
}
if directClient.STARTTLS != "" || directClient.RequireSTARTTLS {
t.Errorf("direct-TLS entry should carry no STARTTLS posture, got %+v", directClient)
}
}
func TestDiscoverEndpoints_WrongType(t *testing.T) {
func TestDiscoverEntries_WrongType(t *testing.T) {
p := &xmppProvider{}
eps, err := p.DiscoverEndpoints("not an XMPPData")
eps, err := p.DiscoverEntries("not an XMPPData")
if err != nil {
t.Fatalf("expected nil error for wrong type, got %v", err)
}
if eps != nil {
t.Fatalf("expected nil endpoints for wrong type, got %v", eps)
t.Fatalf("expected nil entries for wrong type, got %v", eps)
}
}
func itoa(i int) string {
if i == 0 {
return "0"
}
var buf [6]byte
pos := len(buf)
for i > 0 {
pos--
buf[pos] = byte('0' + i%10)
i /= 10
}
return string(buf[pos:])
}
func containsCode(is []Issue, code string) bool {
for _, i := range is {
if i.Code == code {

66
checker/interactive.go Normal file
View file

@ -0,0 +1,66 @@
package checker
import (
"errors"
"net/http"
"strconv"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// RenderForm implements sdk.CheckerInteractive.
func (p *xmppProvider) RenderForm() []sdk.CheckerOptionField {
return []sdk.CheckerOptionField{
{
Id: "domain",
Type: "string",
Label: "Domain",
Placeholder: "example.com",
Required: true,
},
{
Id: "mode",
Type: "string",
Label: "Mode",
Default: "both",
Choices: []string{"c2s", "s2s", "both"},
},
{
Id: "timeout",
Type: "number",
Label: "Per-endpoint timeout (seconds)",
Default: 10,
},
}
}
// ParseForm implements sdk.CheckerInteractive.
func (p *xmppProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) {
domain := strings.TrimSpace(r.FormValue("domain"))
domain = strings.TrimSuffix(domain, ".")
if domain == "" {
return nil, errors.New("domain is required")
}
opts := sdk.CheckerOptions{"domain": domain}
if mode := strings.TrimSpace(r.FormValue("mode")); mode != "" {
switch mode {
case "c2s", "s2s", "both":
opts["mode"] = mode
default:
return nil, errors.New("mode must be one of: c2s, s2s, both")
}
}
if to := strings.TrimSpace(r.FormValue("timeout")); to != "" {
v, err := strconv.ParseFloat(to, 64)
if err != nil {
return nil, errors.New("timeout must be a number")
}
opts["timeout"] = v
}
return opts, nil
}

View file

@ -1,9 +1,11 @@
package checker
import (
"net"
"strconv"
sdk "git.happydns.org/checker-sdk-go/checker"
tlsct "git.happydns.org/checker-tls/contract"
)
func Provider() sdk.ObservationProvider {
@ -21,17 +23,18 @@ func (p *xmppProvider) Definition() *sdk.CheckerDefinition {
return Definition()
}
// DiscoverEndpoints implements sdk.EndpointDiscoverer.
// DiscoverEntries implements sdk.DiscoveryPublisher.
//
// It publishes the (host, port) pairs of every SRV target we found, so a
// downstream TLS checker can verify the certificate chain / SAN / expiry on
// each one without re-doing the SRV lookup. The XMPP checker itself does not
// perform certificate verification — that posture lives in the TLS checker.
// It publishes TLS endpoint contract entries for every SRV target we found,
// so a downstream TLS checker can verify the certificate chain / SAN /
// expiry on each one without re-doing the SRV lookup. The XMPP checker
// itself does not perform certificate verification; that posture lives in
// the TLS checker.
//
// SNI is set to the bare JID domain rather than the SRV target, because XMPP
// certificates must be valid for the source domain (RFC 6120 §13.7.2.1),
// which is typically different from the SRV target hostname.
func (p *xmppProvider) DiscoverEndpoints(data any) ([]sdk.DiscoveredEndpoint, error) {
func (p *xmppProvider) DiscoverEntries(data any) ([]sdk.DiscoveryEntry, error) {
d, ok := data.(*XMPPData)
if !ok || d == nil {
return nil, nil
@ -45,33 +48,42 @@ func (p *xmppProvider) DiscoverEndpoints(data any) ([]sdk.DiscoveredEndpoint, er
}
}
var out []sdk.DiscoveredEndpoint
emit := func(epType string, recs []SRVRecord, directTLS bool) {
var out []sdk.DiscoveryEntry
emit := func(proto string, recs []SRVRecord, directTLS bool) error {
for _, r := range recs {
ep := sdk.DiscoveredEndpoint{
Type: epType,
ep := tlsct.TLSEndpoint{
Host: r.Target,
Port: r.Port,
SNI: d.Domain,
}
if !directTLS {
mode := "opportunistic"
if starttlsRequired[endpointKey(r.Target, r.Port)] {
mode = "required"
}
ep.Meta = map[string]any{"starttls": mode}
ep.STARTTLS = proto
ep.RequireSTARTTLS = starttlsRequired[endpointKey(r.Target, r.Port)]
}
out = append(out, ep)
entry, err := tlsct.NewEntry(ep)
if err != nil {
return err
}
out = append(out, entry)
}
return nil
}
if err := emit("xmpp-client", d.SRV.Client, false); err != nil {
return nil, err
}
if err := emit("xmpp-server", d.SRV.Server, false); err != nil {
return nil, err
}
if err := emit("", d.SRV.ClientSecure, true); err != nil {
return nil, err
}
if err := emit("", d.SRV.ServerSecure, true); err != nil {
return nil, err
}
emit("starttls-xmpp-client", d.SRV.Client, false)
emit("starttls-xmpp-server", d.SRV.Server, false)
emit("tls", d.SRV.ClientSecure, true)
emit("tls", d.SRV.ServerSecure, true)
return out, nil
}
func endpointKey(host string, port uint16) string {
return host + ":" + strconv.Itoa(int(port))
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10))
}

View file

@ -95,7 +95,7 @@ var reportTpl = template.Must(template.New("xmpp").Funcs(template.FuncMap{
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XMPP Report {{.Domain}}</title>
<title>XMPP Report: {{.Domain}}</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
:root {
@ -179,7 +179,7 @@ th { font-weight: 600; color: #6b7280; }
<body>
<div class="hd">
<h1>XMPP <code>{{.Domain}}</code></h1>
<h1>XMPP: <code>{{.Domain}}</code></h1>
<span class="badge {{.StatusClass}}">{{.StatusLabel}}</span>
<div class="meta">
{{if .WorkingC2S}}<span class="badge ok" style="margin-right:.25rem">c2s OK</span>{{else}}<span class="badge fail" style="margin-right:.25rem">c2s FAIL</span>{{end}}
@ -206,7 +206,7 @@ th { font-weight: 600; color: #6b7280; }
<div class="section">
<h2>DNS / SRV</h2>
{{if .FallbackProbed}}
<p class="note">No SRV records published fell back to probing the bare domain on default ports.</p>
<p class="note">No SRV records published; fell back to probing the bare domain on default ports.</p>
{{else if .SRV}}
<table>
<tr><th>Record</th><th>Target</th><th>Port</th><th>Prio/Weight</th><th>IPv4</th><th>IPv6</th></tr>

View file

@ -31,14 +31,14 @@ func (r *xmppRule) ValidateOptions(opts sdk.CheckerOptions) error {
return nil
}
func (r *xmppRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
func (r *xmppRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
var data XMPPData
if err := obs.Get(ctx, ObservationKeyXMPP, &data); err != nil {
return sdk.CheckState{
return []sdk.CheckState{{
Status: sdk.StatusError,
Message: fmt.Sprintf("failed to load XMPP observation: %v", err),
Code: "xmpp.observation_error",
}
}}
}
issues := append([]Issue(nil), data.Issues...)
@ -88,7 +88,7 @@ func (r *xmppRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts
if worst < sdk.StatusCrit {
worst = sdk.StatusCrit
}
missing := []string{}
var missing []string
if wantC2S && !data.Coverage.WorkingC2S {
missing = append(missing, "c2s")
}
@ -102,36 +102,36 @@ func (r *xmppRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts
}
meta := map[string]any{
"working_c2s": data.Coverage.WorkingC2S,
"working_s2s": data.Coverage.WorkingS2S,
"has_ipv4": data.Coverage.HasIPv4,
"has_ipv6": data.Coverage.HasIPv6,
"endpoints": len(data.Endpoints),
"issue_count": len(data.Issues),
"working_c2s": data.Coverage.WorkingC2S,
"working_s2s": data.Coverage.WorkingS2S,
"has_ipv4": data.Coverage.HasIPv4,
"has_ipv6": data.Coverage.HasIPv6,
"endpoints": len(data.Endpoints),
"issue_count": len(data.Issues),
}
switch worst {
case sdk.StatusOK:
return sdk.CheckState{
return []sdk.CheckState{{
Status: sdk.StatusOK,
Message: fmt.Sprintf("XMPP operational (c2s=%v, s2s=%v, %d endpoints)", data.Coverage.WorkingC2S, data.Coverage.WorkingS2S, len(data.Endpoints)),
Code: "xmpp.ok",
Meta: meta,
}
}}
case sdk.StatusWarn:
return sdk.CheckState{
return []sdk.CheckState{{
Status: sdk.StatusWarn,
Message: "XMPP works with warnings: " + joinTop(warnMsgs, 2),
Code: firstWarnCode,
Meta: meta,
}
}}
default:
return sdk.CheckState{
return []sdk.CheckState{{
Status: sdk.StatusCrit,
Message: "XMPP broken: " + joinTop(critMsgs, 2),
Code: firstCritCode,
Meta: meta,
}
}}
}
}

View file

@ -2,7 +2,6 @@ package checker
import (
"encoding/json"
"log"
"net"
"strconv"
"strings"
@ -17,7 +16,7 @@ import (
const TLSRelatedKey sdk.ObservationKey = "tls_probes"
// tlsProbeView is our local, permissive view of a TLS checker's payload.
// We read only the fields we need and tolerate missing ones the TLS
// We read only the fields we need and tolerate missing ones; the TLS
// checker's full schema is owned by that checker.
type tlsProbeView struct {
Host string `json:"host,omitempty"`
@ -53,17 +52,16 @@ func (v *tlsProbeView) address() string {
//
// Two payload shapes are accepted:
//
// 1. {"probes": {"<endpointId>": <probe>, …}} — the current convention
// used by checker-tls. The consumer picks its own probe via
// r.EndpointID so one observation does not leak into another's report.
// 2. <probe> a single top-level probe object, kept for back-compat.
// 1. {"probes": {"<ref>": <probe>, …}}: the current convention used by
// checker-tls. The consumer picks its own probe via r.Ref so one
// observation does not leak into another's report.
// 2. <probe>: a single top-level probe object, kept for back-compat.
func parseTLSRelated(r sdk.RelatedObservation) *tlsProbeView {
log.Printf("xmpp parseTLSRelated: endpointID=%q data=%s", r.EndpointID, r.Data)
var keyed struct {
Probes map[string]tlsProbeView `json:"probes"`
}
if err := json.Unmarshal(r.Data, &keyed); err == nil && keyed.Probes != nil {
if p, ok := keyed.Probes[r.EndpointID]; ok {
if p, ok := keyed.Probes[r.Ref]; ok {
return &p
}
return nil

View file

@ -41,7 +41,7 @@ func mkTLSObs(t *testing.T, payload any) sdk.RelatedObservation {
Key: TLSRelatedKey,
Data: b,
CollectedAt: time.Now(),
EndpointID: "ep-1",
Ref: "ep-1",
}
}

View file

@ -5,7 +5,7 @@
// XEP-0368 direct TLS) and reports actionable findings.
//
// TLS certificate chain / SAN / expiry / cipher posture is intentionally
// out of scope a dedicated TLS checker covers that.
// out of scope; a dedicated TLS checker covers that.
package checker
import (
@ -23,12 +23,12 @@ const (
// XMPPData is the full observation stored per run.
type XMPPData struct {
Domain string `json:"domain"`
RunAt string `json:"run_at"`
SRV SRVLookup `json:"srv"`
Endpoints []EndpointProbe `json:"endpoints"`
Coverage ReachabilitySpan `json:"coverage"`
Issues []Issue `json:"issues"`
Domain string `json:"domain"`
RunAt string `json:"run_at"`
SRV SRVLookup `json:"srv"`
Endpoints []EndpointProbe `json:"endpoints"`
Coverage ReachabilitySpan `json:"coverage"`
Issues []Issue `json:"issues"`
}
type SRVLookup struct {
@ -55,17 +55,17 @@ type SRVRecord struct {
// EndpointProbe is the result of probing one (mode, host, port, address) tuple.
type EndpointProbe struct {
Mode XMPPMode `json:"mode"`
SRVPrefix string `json:"srv_prefix"`
Target string `json:"target"`
Port uint16 `json:"port"`
Address string `json:"address"`
IsIPv6 bool `json:"is_ipv6,omitempty"`
DirectTLS bool `json:"direct_tls,omitempty"`
Mode XMPPMode `json:"mode"`
SRVPrefix string `json:"srv_prefix"`
Target string `json:"target"`
Port uint16 `json:"port"`
Address string `json:"address"`
IsIPv6 bool `json:"is_ipv6,omitempty"`
DirectTLS bool `json:"direct_tls,omitempty"`
// What happened.
TCPConnected bool `json:"tcp_connected"`
StreamOpened bool `json:"stream_opened"`
TCPConnected bool `json:"tcp_connected"`
StreamOpened bool `json:"stream_opened"`
STARTTLSOffered bool `json:"starttls_offered"`
STARTTLSRequired bool `json:"starttls_required"`
@ -114,20 +114,19 @@ const (
// Issue codes.
const (
CodeNoSRV = "xmpp.no_srv"
CodeSRVServfail = "xmpp.srv.servfail"
CodeStartTLSMissing = "xmpp.starttls.missing"
CodeNoSRV = "xmpp.no_srv"
CodeSRVServfail = "xmpp.srv.servfail"
CodeStartTLSMissing = "xmpp.starttls.missing"
CodeStartTLSNotRequired = "xmpp.starttls.not_required"
CodeStartTLSFailed = "xmpp.starttls.handshake_failed"
CodeStreamOpenFailed = "xmpp.stream.open_failed"
CodeTCPUnreachable = "xmpp.tcp.unreachable"
CodeSASLPlainOnly = "xmpp.sasl.plain_only"
CodeSASLNoSCRAM = "xmpp.sasl.no_scram"
CodeSASLNoSCRAMPlus = "xmpp.sasl.no_scram_plus"
CodeS2SNoAuth = "xmpp.s2s.no_auth"
CodeS2SProbeIncomplete = "xmpp.s2s.probe_incomplete"
CodeLegacyJabber = "xmpp.legacy_jabber"
CodeNoIPv6 = "xmpp.no_ipv6"
CodeNoDirectTLS = "xmpp.no_direct_tls"
CodeAllEndpointsDown = "xmpp.all_endpoints_down"
CodeStartTLSFailed = "xmpp.starttls.handshake_failed"
CodeTCPUnreachable = "xmpp.tcp.unreachable"
CodeSASLPlainOnly = "xmpp.sasl.plain_only"
CodeSASLNoSCRAM = "xmpp.sasl.no_scram"
CodeSASLNoSCRAMPlus = "xmpp.sasl.no_scram_plus"
CodeS2SNoAuth = "xmpp.s2s.no_auth"
CodeS2SProbeIncomplete = "xmpp.s2s.probe_incomplete"
CodeLegacyJabber = "xmpp.legacy_jabber"
CodeNoIPv6 = "xmpp.no_ipv6"
CodeNoDirectTLS = "xmpp.no_direct_tls"
CodeAllEndpointsDown = "xmpp.all_endpoints_down"
)

3
go.mod
View file

@ -3,7 +3,8 @@ module git.happydns.org/checker-xmpp
go 1.25.0
require (
git.happydns.org/checker-sdk-go v0.0.1
git.happydns.org/checker-sdk-go v1.2.0
git.happydns.org/checker-tls v0.2.0
github.com/miekg/dns v1.1.72
)

6
go.sum
View file

@ -1,5 +1,7 @@
git.happydns.org/checker-sdk-go v0.0.1 h1:4RxCJr73HWKxjOyU/6NJMO8lXJmH0gMLA68EzTqLbQI=
git.happydns.org/checker-sdk-go v0.0.1/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
git.happydns.org/checker-sdk-go v1.2.0 h1:v4MpKAz0W3PwP+bxx3pya8w893sVH5xTD1of1cc0TV8=
git.happydns.org/checker-sdk-go v1.2.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
git.happydns.org/checker-tls v0.2.0 h1:2dYpcePBylUc3le76fFlLbxraiLpGESmOhx4NfD7REM=
git.happydns.org/checker-tls v0.2.0/go.mod h1:0ZSG0CTP007SHBPE7qInESVIOcW+xgucHUhHgj6MeZ8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=

View file

@ -4,8 +4,8 @@ import (
"flag"
"log"
xmpp "git.happydns.org/checker-xmpp/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
xmpp "git.happydns.org/checker-xmpp/checker"
)
// Version is the standalone binary's version. It defaults to "custom-build"

View file

@ -5,8 +5,8 @@
package main
import (
xmpp "git.happydns.org/checker-xmpp/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
xmpp "git.happydns.org/checker-xmpp/checker"
)
var Version = "custom-build"