Initial commit

This commit is contained in:
nemunaire 2026-04-08 04:18:24 +07:00
commit 2bb91d33d4
16 changed files with 878 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
checker-matrix
*.so

14
Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM golang:1.25-alpine AS builder
ARG CHECKER_VERSION=custom-build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix .
FROM scratch
COPY --from=builder /checker-matrix /checker-matrix
EXPOSE 8080
ENTRYPOINT ["/checker-matrix"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 The happyDomain Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

25
Makefile Normal file
View file

@ -0,0 +1,25 @@
CHECKER_NAME := checker-matrix
CHECKER_IMAGE := happydomain/$(CHECKER_NAME)
CHECKER_VERSION ?= custom-build
CHECKER_SOURCES := main.go $(wildcard checker/*.go)
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
.PHONY: all plugin docker clean
all: $(CHECKER_NAME)
$(CHECKER_NAME): $(CHECKER_SOURCES)
go build -ldflags "$(GO_LDFLAGS)" -o $@ .
plugin: $(CHECKER_NAME).so
$(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go)
go build -buildmode=plugin -ldflags "$(GO_LDFLAGS)" -o $@ ./plugin/
docker:
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
clean:
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so

26
NOTICE Normal file
View file

@ -0,0 +1,26 @@
checker-matrix
Copyright (c) 2026 The happyDomain Authors
This product is licensed under the MIT License (see LICENSE).
-------------------------------------------------------------------------------
Third-party notices
-------------------------------------------------------------------------------
This product includes software developed as part of the checker-sdk-go
project (https://git.happydns.org/happyDomain/checker-sdk-go), licensed
under the Apache License, Version 2.0:
checker-sdk-go
Copyright 2020-2026 The happyDomain Authors
This product includes software developed as part of the happyDomain
project (https://happydomain.org).
Portions of this code were originally written for the happyDomain
server (licensed under AGPL-3.0 and a commercial license) and are
made available there under the Apache License, Version 2.0 to enable
a permissively licensed ecosystem of checker plugins.
You may obtain a copy of the Apache License 2.0 at:
http://www.apache.org/licenses/LICENSE-2.0

71
README.md Normal file
View file

@ -0,0 +1,71 @@
# checker-matrix
Matrix federation checker for [happyDomain](https://www.happydomain.org/).
Queries a [Matrix Federation Tester](https://federationtester.matrix.org/)
instance to verify that a Matrix homeserver is correctly federating, stores
the full report as an observation, and renders a rich HTML summary
(connections, certificates, well-known, DNS/SRV resolution).
## Usage
### Standalone HTTP server
```bash
make
./checker-matrix -listen :8080
```
The server exposes the standard happyDomain external checker endpoints
(`/health`, `/definition`, `/collect`, `/evaluate`, `/html-report`).
### Docker
```bash
make docker
docker run -p 8080:8080 happydomain/checker-matrix
```
### happyDomain plugin
```bash
make plugin
# produces checker-matrix.so, loadable by happyDomain as a Go plugin
```
The plugin exposes a `NewCheckerPlugin` symbol returning the checker
definition and observation provider, which happyDomain registers in its
global registries at load time.
### Versioning
The binary, plugin, and Docker image embed a version string overridable
at build time:
```bash
make CHECKER_VERSION=1.2.3
make plugin CHECKER_VERSION=1.2.3
make docker CHECKER_VERSION=1.2.3
```
### happyDomain remote endpoint
Set the `endpoint` admin option for the `matrixim` checker to the URL of
the running checker-matrix server (e.g., `http://checker-matrix:8080`).
happyDomain will delegate observation collection to this endpoint.
## Options
| Scope | Id | Description |
| ----- | ------------------------ | -------------------------------------------------------------------------- |
| Run | `serviceDomain` | Matrix domain to test (auto-filled, default `matrix.org`) |
| Admin | `federationTesterServer` | Federation Tester URL template (default: `https://federationtester.matrix.org/api/report?server_name=%s`) |
The checker only applies to services of type `abstract.MatrixIM`.
## License
This project is licensed under the **MIT License** (see `LICENSE`). The
third-party Apache-2.0 attributions for `checker-sdk-go` are recorded in
`NOTICE` and must accompany any binary or source redistribution of this
project.

46
checker/collect.go Normal file
View file

@ -0,0 +1,46 @@
package checker
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
domain, _ := opts["serviceDomain"].(string)
if domain == "" {
return nil, fmt.Errorf("serviceDomain is required")
}
domain = strings.TrimSuffix(domain, ".")
testerURI, _ := opts["federationTesterServer"].(string)
if testerURI == "" {
testerURI = "https://federationtester.matrix.org/api/report?server_name=%s"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(testerURI, domain), nil)
if err != nil {
return nil, fmt.Errorf("unable to build the request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to perform the test: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("federation tester returned status %d; check https://federationtester.matrix.org/#%s", resp.StatusCode, domain)
}
var data MatrixFederationData
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, fmt.Errorf("failed to decode federation tester response: %w", err)
}
return &data, nil
}

62
checker/definition.go Normal file
View file

@ -0,0 +1,62 @@
package checker
import (
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Version is the checker version reported in CheckerDefinition.Version.
//
// It defaults to "built-in", which is appropriate when the checker package is
// imported directly (built-in or plugin mode). Standalone binaries (like
// main.go) should override this from their own Version variable at the start
// of main(), which makes it easy for CI to inject a version with a single
// -ldflags "-X main.Version=..." flag instead of targeting the nested
// package path.
var Version = "built-in"
// Definition returns the CheckerDefinition for the matrix federation checker.
func Definition() *sdk.CheckerDefinition {
return &sdk.CheckerDefinition{
ID: "matrixim",
Name: "Matrix Federation Tester",
Version: Version,
Availability: sdk.CheckerAvailability{
ApplyToService: true,
LimitToServices: []string{"abstract.MatrixIM"},
},
HasHTMLReport: true,
ObservationKeys: []sdk.ObservationKey{ObservationKeyMatrix},
Options: sdk.CheckerOptionsDocumentation{
RunOpts: []sdk.CheckerOptionDocumentation{
{
Id: "serviceDomain",
Type: "string",
Label: "Matrix domain",
Placeholder: "matrix.org",
Default: "matrix.org",
AutoFill: sdk.AutoFillDomainName,
Required: true,
},
},
AdminOpts: []sdk.CheckerOptionDocumentation{
{
Id: "federationTesterServer",
Type: "string",
Label: "Federation Tester Server",
Placeholder: "https://federationtester.matrix.org/api/report?server_name=%s",
Default: "https://federationtester.matrix.org/api/report?server_name=%s",
},
},
},
Rules: []sdk.CheckRule{
Rule(),
},
Interval: &sdk.CheckIntervalSpec{
Min: 5 * time.Minute,
Max: 7 * 24 * time.Hour,
Default: 24 * time.Hour,
},
}
}

21
checker/provider.go Normal file
View file

@ -0,0 +1,21 @@
package checker
import (
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Provider returns a new matrix federation observation provider.
func Provider() sdk.ObservationProvider {
return &matrixProvider{}
}
type matrixProvider struct{}
func (p *matrixProvider) Key() sdk.ObservationKey {
return ObservationKeyMatrix
}
// Definition implements sdk.CheckerDefinitionProvider.
func (p *matrixProvider) Definition() *sdk.CheckerDefinition {
return Definition()
}

376
checker/report.go Normal file
View file

@ -0,0 +1,376 @@
package checker
import (
"encoding/json"
"fmt"
"html/template"
"strings"
)
// ── HTML report ───────────────────────────────────────────────────────────────
type matrixCertData struct {
SubjectCommonName string
IssuerCommonName string
SHA256Fingerprint string
DNSNames []string
}
type matrixConnectionData struct {
Address string
TLSVersion string
CipherSuite string
Certs []matrixCertData
AllChecksOK bool
CheckDetails []matrixCheckItem
Errors []string
Open bool
}
type matrixCheckItem struct {
Label string
OK bool
}
type matrixConnErrData struct {
Address string
Message string
}
type matrixSRVRecord struct {
Target string
Port uint16
Priority uint16
Weight uint16
}
type matrixHostData struct {
Name string
CName string
Addrs []string
}
type matrixTemplateData struct {
FederationOK bool
Version string
VersionError string
WellKnownServer string
WellKnownResult string
SRVSkipped bool
SRVCName string
SRVRecords []matrixSRVRecord
SRVError string
Hosts []matrixHostData
Addrs []string
Connections []matrixConnectionData
ConnectionErrors []matrixConnErrData
}
var matrixHTMLTemplate = template.Must(
template.New("matrix").Parse(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Matrix Federation Report</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
:root {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
line-height: 1.5;
color: #1f2937;
background: #f3f4f6;
}
body { margin: 0; padding: 1rem; }
code { font-family: ui-monospace, monospace; font-size: .9em; }
h2 { font-size: 1rem; font-weight: 700; margin: 0 0 .6rem; }
h3 { font-size: .9rem; font-weight: 600; margin: 0 0 .4rem; }
.hd {
background: #fff;
border-radius: 10px;
padding: 1rem 1.25rem;
margin-bottom: .75rem;
box-shadow: 0 1px 3px rgba(0,0,0,.08);
}
.hd h1 { margin: 0 0 .4rem; font-size: 1.15rem; font-weight: 700; }
.badge {
display: inline-flex; align-items: center;
padding: .2em .65em;
border-radius: 9999px;
font-size: .78rem; font-weight: 700;
letter-spacing: .02em;
}
.ok { background: #d1fae5; color: #065f46; }
.fail { background: #fee2e2; color: #991b1b; }
.version { color: #6b7280; font-size: .82rem; margin-top: .35rem; }
.section {
background: #fff;
border-radius: 8px;
padding: .85rem 1rem;
margin-bottom: .6rem;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
}
details {
background: #fff;
border-radius: 8px;
margin-bottom: .45rem;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
overflow: hidden;
}
.section details {
box-shadow: none;
border-radius: 6px;
border: 1px solid #e5e7eb;
margin-bottom: .4rem;
}
summary {
display: flex; align-items: center; gap: .5rem;
padding: .65rem 1rem;
cursor: pointer;
user-select: none;
list-style: none;
}
summary::-webkit-details-marker { display: none; }
summary::before {
content: "▶";
font-size: .65rem;
color: #9ca3af;
transition: transform .15s;
flex-shrink: 0;
}
details[open] > summary::before { transform: rotate(90deg); }
.conn-addr { font-weight: 600; flex: 1; font-size: .9rem; font-family: ui-monospace, monospace; }
.details-body { padding: .6rem 1rem .85rem; border-top: 1px solid #f3f4f6; }
table { border-collapse: collapse; width: 100%; font-size: .85rem; }
th, td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid #f3f4f6; }
th { font-weight: 600; color: #6b7280; }
.check-ok { color: #059669; }
.check-fail { color: #dc2626; }
.errmsg { color: #dc2626; font-size: .85rem; margin: .25rem 0 0; }
.note { color: #6b7280; font-size: .85rem; }
ul { margin: .25rem 0; padding-left: 1.2rem; }
li { margin-bottom: .15rem; }
</style>
</head>
<body>
<div class="hd">
<h1>Matrix Federation</h1>
{{if .FederationOK}}
<span class="badge ok">Federation OK</span>
{{- else}}
<span class="badge fail">Federation FAIL</span>
{{- end}}
{{if .Version}}<div class="version">Server: <code>{{.Version}}</code>{{if .VersionError}} &mdash; {{.VersionError}}{{end}}</div>{{end}}
</div>
{{if .Connections}}
<div class="section">
<h2>Connections ({{len .Connections}})</h2>
{{range .Connections}}
<details{{if .Open}} open{{end}}>
<summary>
<span class="conn-addr">{{.Address}}</span>
{{if .AllChecksOK}}<span class="badge ok">All checks OK</span>{{else}}<span class="badge fail">Checks failed</span>{{end}}
</summary>
<div class="details-body">
{{if or .TLSVersion .CipherSuite}}
<h3>TLS</h3>
<p class="note">{{.TLSVersion}}{{if and .TLSVersion .CipherSuite}} &mdash; {{end}}{{.CipherSuite}}</p>
{{end}}
{{if .Certs}}
<h3>Certificates</h3>
<table>
<tr><th>Subject</th><th>Issuer</th><th>DNS Names</th><th>Fingerprint (SHA-256)</th></tr>
{{range .Certs}}
<tr>
<td><code>{{.SubjectCommonName}}</code></td>
<td><code>{{.IssuerCommonName}}</code></td>
<td>{{range .DNSNames}}<code>{{.}}</code> {{end}}</td>
<td><code>{{.SHA256Fingerprint}}</code></td>
</tr>
{{end}}
</table>
{{end}}
{{if .CheckDetails}}
<h3 style="margin-top:.7rem">Checks</h3>
<table>
{{range .CheckDetails}}
<tr>
<td>{{if .OK}}<span class="check-ok">&#10003;</span>{{else}}<span class="check-fail">&#10007;</span>{{end}}</td>
<td>{{.Label}}</td>
</tr>
{{end}}
</table>
{{end}}
{{range .Errors}}<p class="errmsg">&#9888; {{.}}</p>{{end}}
</div>
</details>
{{end}}
</div>
{{end}}
{{if .ConnectionErrors}}
<div class="section">
<h2>Connection Errors ({{len .ConnectionErrors}})</h2>
{{range .ConnectionErrors}}
<p><code>{{.Address}}</code><br><span class="errmsg">{{.Message}}</span></p>
{{end}}
</div>
{{end}}
<div class="section">
<h2>Well-Known</h2>
{{if .WellKnownServer}}
<p>Server: <code>{{.WellKnownServer}}</code></p>
{{else if .WellKnownResult}}
<p class="note">{{.WellKnownResult}}</p>
{{else}}
<p class="note">Not found.</p>
{{end}}
</div>
<div class="section">
<h2>DNS Resolution</h2>
{{if .SRVSkipped}}
<p class="note">SRV lookup skipped{{if .SRVCName}} (CNAME: <code>{{.SRVCName}}</code>){{end}}</p>
{{else if .SRVError}}
<p class="errmsg">SRV error: {{.SRVError}}</p>
{{else if .SRVRecords}}
<h3>SRV Records</h3>
<table>
<tr><th>Target</th><th>Port</th><th>Priority</th><th>Weight</th></tr>
{{range .SRVRecords}}
<tr>
<td><code>{{.Target}}</code></td>
<td>{{.Port}}</td>
<td>{{.Priority}}</td>
<td>{{.Weight}}</td>
</tr>
{{end}}
</table>
{{else}}
<p class="note">No SRV records found.</p>
{{end}}
{{if .Hosts}}
<h3 style="margin-top:.6rem">Resolved Hosts</h3>
{{range .Hosts}}
<p style="margin:.25rem 0">
<code>{{.Name}}</code>
{{if .CName}} &rarr; <code>{{.CName}}</code>{{end}}
{{if .Addrs}}: {{range .Addrs}}<code>{{.}}</code> {{end}}{{end}}
</p>
{{end}}
{{else if .Addrs}}
<h3 style="margin-top:.6rem">Addresses</h3>
<ul>{{range .Addrs}}<li><code>{{.}}</code></li>{{end}}</ul>
{{end}}
</div>
</body>
</html>`),
)
// GetHTMLReport implements sdk.CheckerHTMLReporter.
func (p *matrixProvider) GetHTMLReport(raw json.RawMessage) (string, error) {
var r MatrixFederationData
if err := json.Unmarshal(raw, &r); err != nil {
return "", fmt.Errorf("failed to unmarshal matrix report: %w", err)
}
data := matrixTemplateData{
FederationOK: r.FederationOK,
WellKnownServer: r.WellKnownResult.Server,
WellKnownResult: r.WellKnownResult.Result,
SRVSkipped: r.DNSResult.SRVSkipped,
SRVCName: r.DNSResult.SRVCName,
Addrs: r.DNSResult.Addrs,
}
// Version
if r.Version.Name != "" || r.Version.Version != "" {
data.Version = strings.TrimSpace(r.Version.Name + " " + r.Version.Version)
}
data.VersionError = r.Version.Error
// SRV records
for _, s := range r.DNSResult.SRVRecords {
data.SRVRecords = append(data.SRVRecords, matrixSRVRecord{
Target: s.Target,
Port: s.Port,
Priority: s.Priority,
Weight: s.Weight,
})
}
// SRV error
if r.DNSResult.SRVError != nil {
data.SRVError = r.DNSResult.SRVError.Message
}
// Hosts
for name, h := range r.DNSResult.Hosts {
data.Hosts = append(data.Hosts, matrixHostData{
Name: name,
CName: h.CName,
Addrs: h.Addrs,
})
}
// Successful connections
for addr, cr := range r.ConnectionReports {
conn := matrixConnectionData{
Address: addr,
TLSVersion: cr.Cipher.Version,
CipherSuite: cr.Cipher.CipherSuite,
AllChecksOK: cr.Checks.AllChecksOK,
Errors: cr.Errors,
Open: !cr.Checks.AllChecksOK,
}
for _, cert := range cr.Certificates {
conn.Certs = append(conn.Certs, matrixCertData{
SubjectCommonName: cert.SubjectCommonName,
IssuerCommonName: cert.IssuerCommonName,
SHA256Fingerprint: cert.SHA256Fingerprint,
DNSNames: cert.DNSNames,
})
}
conn.CheckDetails = []matrixCheckItem{
{"Matching server name", cr.Checks.MatchingServerName},
{"Certificate valid until future", cr.Checks.FutureValidUntilTS},
{"Valid certificates", cr.Checks.ValidCertificates},
{"Has Ed25519 key", cr.Checks.HasEd25519Key},
{"All Ed25519 checks OK", cr.Checks.AllEd25519ChecksOK},
}
data.Connections = append(data.Connections, conn)
}
// Failed connections
for addr, ce := range r.ConnectionErrors {
data.ConnectionErrors = append(data.ConnectionErrors, matrixConnErrData{
Address: addr,
Message: ce.Message,
})
}
var buf strings.Builder
if err := matrixHTMLTemplate.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to render matrix HTML report: %w", err)
}
return buf.String(), nil
}

81
checker/rule.go Normal file
View file

@ -0,0 +1,81 @@
package checker
import (
"context"
"fmt"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rule returns a new matrix federation check rule.
func Rule() sdk.CheckRule {
return &matrixRule{}
}
type matrixRule struct{}
func (r *matrixRule) Name() string {
return "matrix_federation"
}
func (r *matrixRule) Description() string {
return "Checks whether Matrix federation is working correctly"
}
func (r *matrixRule) ValidateOptions(opts sdk.CheckerOptions) error {
return nil
}
func (r *matrixRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
var data MatrixFederationData
if err := obs.Get(ctx, ObservationKeyMatrix, &data); err != nil {
return sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("Failed to get Matrix federation data: %v", err),
Code: "matrix_federation_error",
}
}
domain, _ := opts["serviceDomain"].(string)
domain = strings.TrimSuffix(domain, ".")
if data.FederationOK {
version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version)
return sdk.CheckState{
Status: sdk.StatusOK,
Message: fmt.Sprintf("Running %s", version),
Code: "matrix_federation_ok",
Meta: map[string]any{
"version": version,
},
}
}
var statusLine string
if data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "" {
statusLine = fmt.Sprintf("%s OR %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result)
} else if len(data.ConnectionErrors) > 0 {
var msg strings.Builder
for srv, cerr := range data.ConnectionErrors {
if msg.Len() > 0 {
msg.WriteString("; ")
}
msg.WriteString(srv)
msg.WriteString(": ")
msg.WriteString(cerr.Message)
}
statusLine = fmt.Sprintf("Connection errors: %s", msg.String())
} else if data.WellKnownResult.Server != domain {
statusLine = fmt.Sprintf("Bad homeserver_name: got %s, expected %s", data.WellKnownResult.Server, domain)
} else {
statusLine = fmt.Sprintf("Federation broken. Check https://federationtester.matrix.org/#%s", domain)
}
return sdk.CheckState{
Status: sdk.StatusCrit,
Message: statusLine,
Code: "matrix_federation_fail",
}
}

72
checker/types.go Normal file
View file

@ -0,0 +1,72 @@
// Package checker implements the Matrix federation checker for happyDomain.
//
// It queries a Matrix Federation Tester instance (by default the public one
// hosted at https://federationtester.matrix.org) to verify that a Matrix
// homeserver is correctly federating, then exposes the result both as an
// observation and as a rich HTML report.
package checker
import (
sdk "git.happydns.org/checker-sdk-go/checker"
)
// ObservationKeyMatrix is the observation key for Matrix federation test data.
const ObservationKeyMatrix sdk.ObservationKey = "matrix_federation"
// MatrixFederationData is the full payload returned by the Matrix Federation
// Tester API and stored as the observation.
type MatrixFederationData struct {
WellKnownResult struct {
Server string `json:"m.server"`
Result string `json:"result"`
CacheExpiresAt int64 `json:"CacheExpiresAt"`
} `json:"WellKnownResult"`
DNSResult struct {
SRVSkipped bool `json:"SRVSkipped"`
SRVCName string `json:"SRVCName"`
SRVRecords []struct {
Target string `json:"Target"`
Port uint16 `json:"Port"`
Priority uint16 `json:"Priority"`
Weight uint16 `json:"Weight"`
} `json:"SRVRecords"`
SRVError *struct {
Message string `json:"Message"`
} `json:"SRVError"`
Hosts map[string]struct {
CName string `json:"CName"`
Addrs []string `json:"Addrs"`
} `json:"Hosts"`
Addrs []string `json:"Addrs"`
} `json:"DNSResult"`
ConnectionReports map[string]struct {
Certificates []struct {
SubjectCommonName string `json:"SubjectCommonName"`
IssuerCommonName string `json:"IssuerCommonName"`
SHA256Fingerprint string `json:"SHA256Fingerprint"`
DNSNames []string `json:"DNSNames"`
} `json:"Certificates"`
Cipher struct {
Version string `json:"Version"`
CipherSuite string `json:"CipherSuite"`
} `json:"Cipher"`
Checks struct {
AllChecksOK bool `json:"AllChecksOK"`
MatchingServerName bool `json:"MatchingServerName"`
FutureValidUntilTS bool `json:"FutureValidUntilTS"`
HasEd25519Key bool `json:"HasEd25519Key"`
AllEd25519ChecksOK bool `json:"AllEd25519ChecksOK"`
ValidCertificates bool `json:"ValidCertificates"`
} `json:"Checks"`
Errors []string `json:"Errors"`
} `json:"ConnectionReports"`
ConnectionErrors map[string]struct {
Message string `json:"Message"`
} `json:"ConnectionErrors"`
Version struct {
Name string `json:"name"`
Version string `json:"version"`
Error string `json:"error,omitempty"`
} `json:"Version"`
FederationOK bool `json:"FederationOK"`
}

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module git.happydns.org/checker-matrix
go 1.25.0
require git.happydns.org/checker-sdk-go v0.0.1

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
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=

30
main.go Normal file
View file

@ -0,0 +1,30 @@
package main
import (
"flag"
"log"
matrix "git.happydns.org/checker-matrix/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Version is the standalone binary's version. It defaults to "custom-build"
// and is meant to be overridden by the CI at link time:
//
// go build -ldflags "-X main.Version=1.2.3" .
var Version = "custom-build"
var listenAddr = flag.String("listen", ":8080", "HTTP listen address")
func main() {
flag.Parse()
// Propagate the binary version to the checker package so it shows up in
// CheckerDefinition.Version.
matrix.Version = Version
server := sdk.NewServer(matrix.Provider())
if err := server.ListenAndServe(*listenAddr); err != nil {
log.Fatalf("server error: %v", err)
}
}

24
plugin/plugin.go Normal file
View file

@ -0,0 +1,24 @@
// Command plugin is the happyDomain plugin entrypoint for the matrix checker.
//
// It is built as a Go plugin (`go build -buildmode=plugin`) and loaded at
// runtime by happyDomain.
package main
import (
matrix "git.happydns.org/checker-matrix/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Version is the plugin's version. It defaults to "custom-build" and is
// meant to be overridden by the CI at link time:
//
// go build -buildmode=plugin -ldflags "-X main.Version=1.2.3" -o checker-matrix.so ./plugin
var Version = "custom-build"
// NewCheckerPlugin is the symbol resolved by happyDomain when loading the
// .so file. It returns the checker definition and the observation provider
// that the host will register in its global registries.
func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) {
matrix.Version = Version
return matrix.Definition(), matrix.Provider(), nil
}