Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86b5207a8f | |||
| 4079a92868 | |||
| 8a62846170 | |||
| 2af16d3ab9 | |||
| 0fee494294 | |||
| d19bda771d | |||
| e4b6481d32 | |||
| 2bd0ae99bd |
23 changed files with 925 additions and 89 deletions
22
.drone-manifest.yml
Normal file
22
.drone-manifest.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
- image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
- image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
187
.drone.yml
Normal file
187
.drone.yml
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: checker build
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: checker build tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_SEMVER}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-matrix
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub (tag)
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-matrix
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_SEMVER}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-arm64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: checker build
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: checker build tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_SEMVER}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-matrix
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub (tag)
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-matrix
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_SEMVER}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: docker-manifest
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: .drone-manifest.yml
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
|
|
@ -6,9 +6,13 @@ 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 .
|
||||
RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /checker-matrix /checker-matrix
|
||||
USER 65534:65534
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD ["/checker-matrix", "-healthcheck"]
|
||||
ENTRYPOINT ["/checker-matrix"]
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -6,12 +6,12 @@ CHECKER_SOURCES := main.go $(wildcard checker/*.go)
|
|||
|
||||
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
|
||||
|
||||
.PHONY: all plugin docker clean
|
||||
.PHONY: all plugin docker test clean
|
||||
|
||||
all: $(CHECKER_NAME)
|
||||
|
||||
$(CHECKER_NAME): $(CHECKER_SOURCES)
|
||||
go build -ldflags "$(GO_LDFLAGS)" -o $@ .
|
||||
go build -tags standalone -ldflags "$(GO_LDFLAGS)" -o $@ .
|
||||
|
||||
plugin: $(CHECKER_NAME).so
|
||||
|
||||
|
|
@ -21,5 +21,8 @@ $(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go)
|
|||
docker:
|
||||
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
|
||||
|
||||
test:
|
||||
go test -tags standalone ./...
|
||||
|
||||
clean:
|
||||
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -54,6 +54,17 @@ 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.
|
||||
|
||||
## Rules
|
||||
|
||||
| Code | Description | Severity |
|
||||
|------------------------------|---------------------------------------------------------------------------------------------------|---------------------|
|
||||
| `matrix.connection_reachable`| Checks that every discovered federation endpoint accepts an inbound connection. | CRITICAL |
|
||||
| `matrix.federation_ok` | Reports the overall federation status returned by the Matrix Federation Tester. | CRITICAL |
|
||||
| `matrix.srv_records` | Checks that the Matrix SRV lookup (`_matrix-fed._tcp` / `_matrix._tcp`) succeeded or was skipped. | CRITICAL |
|
||||
| `matrix.tls_checks` | Reviews the TLS posture on every reachable federation endpoint (chain, hostname, Ed25519 key). | CRITICAL |
|
||||
| `matrix.version` | Checks that the homeserver responds to `/_matrix/federation/v1/version` with name and version. | WARNING |
|
||||
| `matrix.well_known` | Checks that `/.well-known/matrix/server` (if published) is valid and points at the server_name. | CRITICAL |
|
||||
|
||||
## Options
|
||||
|
||||
| Scope | Id | Description |
|
||||
|
|
|
|||
|
|
@ -4,12 +4,23 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTesterURI = "https://federationtester.matrix.org/api/report?server_name=%s"
|
||||
collectHTTPTimeout = 30 * time.Second
|
||||
maxResponseBodySize = 5 << 20 // 5 MiB
|
||||
)
|
||||
|
||||
var collectHTTPClient = &http.Client{Timeout: collectHTTPTimeout}
|
||||
|
||||
func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
||||
domain, _ := opts["serviceDomain"].(string)
|
||||
if domain == "" {
|
||||
|
|
@ -19,15 +30,20 @@ func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (
|
|||
|
||||
testerURI, _ := opts["federationTesterServer"].(string)
|
||||
if testerURI == "" {
|
||||
testerURI = "https://federationtester.matrix.org/api/report?server_name=%s"
|
||||
testerURI = defaultTesterURI
|
||||
}
|
||||
if !strings.Contains(testerURI, "%s") {
|
||||
return nil, fmt.Errorf("federationTesterServer must contain a %%s placeholder for the domain")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(testerURI, domain), nil)
|
||||
reqURL := fmt.Sprintf(testerURI, url.QueryEscape(domain))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to build the request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := collectHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to perform the test: %w", err)
|
||||
}
|
||||
|
|
@ -38,7 +54,7 @@ func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (
|
|||
}
|
||||
|
||||
var data MatrixFederationData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, maxResponseBodySize)).Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode federation tester response: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
91
checker/collect_test.go
Normal file
91
checker/collect_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
func TestCollectMissingDomain(t *testing.T) {
|
||||
p := &matrixProvider{}
|
||||
if _, err := p.Collect(context.Background(), sdk.CheckerOptions{}); err == nil {
|
||||
t.Fatal("expected error when serviceDomain is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectSuccess(t *testing.T) {
|
||||
const body = `{
|
||||
"WellKnownResult": {"m.server": "matrix.example.org:8448", "result": ""},
|
||||
"DNSResult": {"SRVSkipped": false, "SRVRecords": [{"Target": "matrix.example.org.", "Port": 8448, "Priority": 10, "Weight": 5}]},
|
||||
"ConnectionReports": {"1.2.3.4:8448": {"Checks": {"AllChecksOK": true, "MatchingServerName": true, "FutureValidUntilTS": true, "HasEd25519Key": true, "AllEd25519ChecksOK": true, "ValidCertificates": true}}},
|
||||
"ConnectionErrors": {},
|
||||
"Version": {"name": "Synapse", "version": "1.100.0"},
|
||||
"FederationOK": true
|
||||
}`
|
||||
|
||||
var gotURL string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotURL = r.URL.String()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := &matrixProvider{}
|
||||
out, err := p.Collect(context.Background(), sdk.CheckerOptions{
|
||||
"serviceDomain": "example.org.",
|
||||
"federationTesterServer": srv.URL + "/api/report?server_name=%s",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(gotURL, "server_name=example.org") {
|
||||
t.Errorf("unexpected URL %q", gotURL)
|
||||
}
|
||||
data, ok := out.(*MatrixFederationData)
|
||||
if !ok || data == nil {
|
||||
t.Fatalf("expected *MatrixFederationData, got %T", out)
|
||||
}
|
||||
if !data.FederationOK {
|
||||
t.Error("expected FederationOK=true")
|
||||
}
|
||||
if data.Version.Name != "Synapse" {
|
||||
t.Errorf("unexpected version name %q", data.Version.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectNon2xx(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := &matrixProvider{}
|
||||
_, err := p.Collect(context.Background(), sdk.CheckerOptions{
|
||||
"serviceDomain": "example.org",
|
||||
"federationTesterServer": srv.URL + "/?s=%s",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error on 502 response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectMalformedJSON(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write([]byte("not json"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := &matrixProvider{}
|
||||
_, err := p.Collect(context.Background(), sdk.CheckerOptions{
|
||||
"serviceDomain": "example.org",
|
||||
"federationTesterServer": srv.URL + "/?s=%s",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected decode error")
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import (
|
|||
var Version = "built-in"
|
||||
|
||||
// Definition returns the CheckerDefinition for the matrix federation checker.
|
||||
func Definition() *sdk.CheckerDefinition {
|
||||
func (p *matrixProvider) Definition() *sdk.CheckerDefinition {
|
||||
return &sdk.CheckerDefinition{
|
||||
ID: "matrixim",
|
||||
Name: "Matrix Federation Tester",
|
||||
|
|
@ -50,9 +50,7 @@ func Definition() *sdk.CheckerDefinition {
|
|||
},
|
||||
},
|
||||
},
|
||||
Rules: []sdk.CheckRule{
|
||||
Rule(),
|
||||
},
|
||||
Rules: Rules(),
|
||||
Interval: &sdk.CheckIntervalSpec{
|
||||
Min: 5 * time.Minute,
|
||||
Max: 7 * 24 * time.Hour,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//go:build standalone
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
|
|
@ -8,7 +10,7 @@ import (
|
|||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// RenderForm implements sdk.CheckerInteractive.
|
||||
// RenderForm implements server.Interactive.
|
||||
func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField {
|
||||
return []sdk.CheckerOptionField{
|
||||
{
|
||||
|
|
@ -29,11 +31,11 @@ func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField {
|
|||
}
|
||||
}
|
||||
|
||||
// ParseForm implements sdk.CheckerInteractive.
|
||||
// ParseForm implements server.Interactive.
|
||||
func (p *matrixProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) {
|
||||
domain := strings.TrimSpace(r.FormValue("serviceDomain"))
|
||||
if domain == "" {
|
||||
return nil, errors.New("Matrix domain is required")
|
||||
return nil, errors.New("matrix domain is required")
|
||||
}
|
||||
|
||||
opts := sdk.CheckerOptions{
|
||||
|
|
|
|||
|
|
@ -14,8 +14,3 @@ type matrixProvider struct{}
|
|||
func (p *matrixProvider) Key() sdk.ObservationKey {
|
||||
return ObservationKeyMatrix
|
||||
}
|
||||
|
||||
// Definition implements sdk.CheckerDefinitionProvider.
|
||||
func (p *matrixProvider) Definition() *sdk.CheckerDefinition {
|
||||
return Definition()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
|
|
@ -326,7 +327,13 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) {
|
|||
}
|
||||
|
||||
// Hosts
|
||||
for name, h := range r.DNSResult.Hosts {
|
||||
hostNames := make([]string, 0, len(r.DNSResult.Hosts))
|
||||
for name := range r.DNSResult.Hosts {
|
||||
hostNames = append(hostNames, name)
|
||||
}
|
||||
sort.Strings(hostNames)
|
||||
for _, name := range hostNames {
|
||||
h := r.DNSResult.Hosts[name]
|
||||
data.Hosts = append(data.Hosts, matrixHostData{
|
||||
Name: name,
|
||||
CName: h.CName,
|
||||
|
|
@ -335,7 +342,13 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) {
|
|||
}
|
||||
|
||||
// Successful connections
|
||||
for addr, cr := range r.ConnectionReports {
|
||||
connAddrs := make([]string, 0, len(r.ConnectionReports))
|
||||
for addr := range r.ConnectionReports {
|
||||
connAddrs = append(connAddrs, addr)
|
||||
}
|
||||
sort.Strings(connAddrs)
|
||||
for _, addr := range connAddrs {
|
||||
cr := r.ConnectionReports[addr]
|
||||
conn := matrixConnectionData{
|
||||
Address: addr,
|
||||
TLSVersion: cr.Cipher.Version,
|
||||
|
|
@ -363,10 +376,15 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) {
|
|||
}
|
||||
|
||||
// Failed connections
|
||||
for addr, ce := range r.ConnectionErrors {
|
||||
errAddrs := make([]string, 0, len(r.ConnectionErrors))
|
||||
for addr := range r.ConnectionErrors {
|
||||
errAddrs = append(errAddrs, addr)
|
||||
}
|
||||
sort.Strings(errAddrs)
|
||||
for _, addr := range errAddrs {
|
||||
data.ConnectionErrors = append(data.ConnectionErrors, matrixConnErrData{
|
||||
Address: addr,
|
||||
Message: ce.Message,
|
||||
Message: r.ConnectionErrors[addr].Message,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
101
checker/rule.go
101
checker/rule.go
|
|
@ -3,79 +3,54 @@ 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{}
|
||||
// Rules returns the full list of CheckRules exposed by the Matrix checker.
|
||||
// Each rule covers a single concern so the UI can show a clear checklist
|
||||
// rather than a single monolithic pass/fail line.
|
||||
func Rules() []sdk.CheckRule {
|
||||
return []sdk.CheckRule{
|
||||
&federationOKRule{},
|
||||
&wellKnownRule{},
|
||||
&srvRecordsRule{},
|
||||
&connectionReachableRule{},
|
||||
&tlsChecksRule{},
|
||||
&versionRule{},
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// loadMatrixData fetches the Matrix observation. On error returns a
|
||||
// CheckState the caller should emit to short-circuit its rule.
|
||||
func loadMatrixData(ctx context.Context, obs sdk.ObservationGetter) (*MatrixFederationData, *sdk.CheckState) {
|
||||
var data MatrixFederationData
|
||||
if err := obs.Get(ctx, ObservationKeyMatrix, &data); err != nil {
|
||||
return []sdk.CheckState{{
|
||||
return nil, &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)
|
||||
Code: "matrix.observation_error",
|
||||
}
|
||||
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",
|
||||
}}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func passState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusOK, Message: message, Code: code}
|
||||
}
|
||||
|
||||
func infoState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusInfo, Message: message, Code: code}
|
||||
}
|
||||
|
||||
func warnState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusWarn, Message: message, Code: code}
|
||||
}
|
||||
|
||||
func critState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusCrit, Message: message, Code: code}
|
||||
}
|
||||
|
||||
func unknownState(code, message string) sdk.CheckState {
|
||||
return sdk.CheckState{Status: sdk.StatusUnknown, Message: message, Code: code}
|
||||
}
|
||||
|
|
|
|||
47
checker/rules_connection.go
Normal file
47
checker/rules_connection.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// connectionReachableRule checks that every federation endpoint returned
|
||||
// by DNS accepted the TLS connection the tester attempted.
|
||||
type connectionReachableRule struct{}
|
||||
|
||||
func (r *connectionReachableRule) Name() string { return "matrix.connection_reachable" }
|
||||
func (r *connectionReachableRule) Description() string {
|
||||
return "Checks that every discovered federation endpoint accepts an inbound connection."
|
||||
}
|
||||
|
||||
func (r *connectionReachableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadMatrixData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
if len(data.ConnectionErrors) == 0 && len(data.ConnectionReports) == 0 {
|
||||
return []sdk.CheckState{unknownState("matrix.connection_reachable.unknown", "No endpoint was probed by the federation tester.")}
|
||||
}
|
||||
|
||||
if len(data.ConnectionErrors) == 0 {
|
||||
return []sdk.CheckState{passState("matrix.connection_reachable.ok", fmt.Sprintf("All %d endpoint(s) accepted the connection.", len(data.ConnectionReports)))}
|
||||
}
|
||||
|
||||
addrs := make([]string, 0, len(data.ConnectionErrors))
|
||||
for addr := range data.ConnectionErrors {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
sort.Strings(addrs)
|
||||
|
||||
out := make([]sdk.CheckState, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
st := critState("matrix.connection_reachable.fail", data.ConnectionErrors[addr].Message)
|
||||
st.Subject = addr
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
67
checker/rules_federation.go
Normal file
67
checker/rules_federation.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// federationOKRule reflects the overall FederationOK flag reported by the
|
||||
// Matrix Federation Tester. Other rules isolate specific concerns; this
|
||||
// rule is the global verdict so callers get a single-line answer to
|
||||
// "does this homeserver federate?".
|
||||
type federationOKRule struct{}
|
||||
|
||||
func (r *federationOKRule) Name() string { return "matrix.federation_ok" }
|
||||
func (r *federationOKRule) Description() string {
|
||||
return "Reports the overall federation status returned by the Matrix Federation Tester."
|
||||
}
|
||||
|
||||
func (r *federationOKRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadMatrixData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
domain, _ := opts["serviceDomain"].(string)
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
|
||||
if data.FederationOK {
|
||||
version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version)
|
||||
st := passState("matrix.federation_ok.ok", "Matrix federation is working.")
|
||||
if version != "" {
|
||||
st.Message = fmt.Sprintf("Matrix federation is working (running %s).", version)
|
||||
st.Meta = map[string]any{"version": version}
|
||||
}
|
||||
return []sdk.CheckState{st}
|
||||
}
|
||||
|
||||
var statusLine string
|
||||
switch {
|
||||
case data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "":
|
||||
statusLine = fmt.Sprintf("%s; %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result)
|
||||
case len(data.ConnectionErrors) > 0:
|
||||
srvs := make([]string, 0, len(data.ConnectionErrors))
|
||||
for srv := range data.ConnectionErrors {
|
||||
srvs = append(srvs, srv)
|
||||
}
|
||||
sort.Strings(srvs)
|
||||
var msg strings.Builder
|
||||
for _, srv := range srvs {
|
||||
if msg.Len() > 0 {
|
||||
msg.WriteString("; ")
|
||||
}
|
||||
msg.WriteString(srv)
|
||||
msg.WriteString(": ")
|
||||
msg.WriteString(data.ConnectionErrors[srv].Message)
|
||||
}
|
||||
statusLine = fmt.Sprintf("Connection errors: %s", msg.String())
|
||||
default:
|
||||
statusLine = fmt.Sprintf("Federation broken. Check https://federationtester.matrix.org/#%s", domain)
|
||||
}
|
||||
|
||||
return []sdk.CheckState{critState("matrix.federation_ok.fail", statusLine)}
|
||||
}
|
||||
48
checker/rules_srv.go
Normal file
48
checker/rules_srv.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// srvRecordsRule checks _matrix-fed._tcp / _matrix._tcp SRV delegation: was
|
||||
// the lookup successful, and does it yield at least one record (or was it
|
||||
// legitimately skipped because of a CNAME/well-known path)?
|
||||
type srvRecordsRule struct{}
|
||||
|
||||
func (r *srvRecordsRule) Name() string { return "matrix.srv_records" }
|
||||
func (r *srvRecordsRule) Description() string {
|
||||
return "Checks that the Matrix SRV lookup (_matrix-fed._tcp / _matrix._tcp) succeeded or was legitimately skipped."
|
||||
}
|
||||
|
||||
func (r *srvRecordsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadMatrixData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
dns := data.DNSResult
|
||||
|
||||
if dns.SRVError != nil {
|
||||
return []sdk.CheckState{critState("matrix.srv_records.error", fmt.Sprintf("SRV lookup error: %s", dns.SRVError.Message))}
|
||||
}
|
||||
|
||||
if dns.SRVSkipped {
|
||||
msg := "SRV lookup skipped by the federation tester."
|
||||
if dns.SRVCName != "" {
|
||||
msg = fmt.Sprintf("SRV lookup skipped (CNAME: %s).", dns.SRVCName)
|
||||
}
|
||||
return []sdk.CheckState{unknownState("matrix.srv_records.skipped", msg)}
|
||||
}
|
||||
|
||||
if len(dns.SRVRecords) == 0 {
|
||||
return []sdk.CheckState{infoState(
|
||||
"matrix.srv_records.absent",
|
||||
"No Matrix SRV records published (federation may still work via well-known).",
|
||||
)}
|
||||
}
|
||||
|
||||
return []sdk.CheckState{passState("matrix.srv_records.ok", fmt.Sprintf("%d SRV record(s) published.", len(dns.SRVRecords)))}
|
||||
}
|
||||
178
checker/rules_test.go
Normal file
178
checker/rules_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
type stubObs struct {
|
||||
data *MatrixFederationData
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error {
|
||||
if s.err != nil {
|
||||
return s.err
|
||||
}
|
||||
b, err := json.Marshal(s.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
|
||||
func (s stubObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func eval(t *testing.T, rule sdk.CheckRule, data *MatrixFederationData, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
t.Helper()
|
||||
return rule.Evaluate(context.Background(), stubObs{data: data}, opts)
|
||||
}
|
||||
|
||||
func TestFederationOKRulePass(t *testing.T) {
|
||||
data := &MatrixFederationData{FederationOK: true}
|
||||
data.Version.Name = "Synapse"
|
||||
data.Version.Version = "1.100.0"
|
||||
got := eval(t, &federationOKRule{}, data, sdk.CheckerOptions{"serviceDomain": "example.org."})
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("expected single OK state, got %+v", got)
|
||||
}
|
||||
if !strings.Contains(got[0].Message, "Synapse 1.100.0") {
|
||||
t.Errorf("expected version in message, got %q", got[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFederationOKRuleFailDeterministicOrder(t *testing.T) {
|
||||
data := &MatrixFederationData{}
|
||||
data.ConnectionErrors = map[string]struct {
|
||||
Message string `json:"Message"`
|
||||
}{
|
||||
"z.example:8448": {Message: "boom z"},
|
||||
"a.example:8448": {Message: "boom a"},
|
||||
"m.example:8448": {Message: "boom m"},
|
||||
}
|
||||
first := eval(t, &federationOKRule{}, data, nil)[0].Message
|
||||
for range 5 {
|
||||
if eval(t, &federationOKRule{}, data, nil)[0].Message != first {
|
||||
t.Fatal("federation_ok message not stable across runs")
|
||||
}
|
||||
}
|
||||
idxA := strings.Index(first, "a.example")
|
||||
idxM := strings.Index(first, "m.example")
|
||||
idxZ := strings.Index(first, "z.example")
|
||||
if !(idxA < idxM && idxM < idxZ) {
|
||||
t.Errorf("expected sorted order a<m<z, got %q", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionReachableUnknownWhenNoEndpoints(t *testing.T) {
|
||||
got := eval(t, &connectionReachableRule{}, &MatrixFederationData{}, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("expected single Unknown state, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionReachableSortedFailures(t *testing.T) {
|
||||
data := &MatrixFederationData{}
|
||||
data.ConnectionErrors = map[string]struct {
|
||||
Message string `json:"Message"`
|
||||
}{
|
||||
"b:1": {Message: "b err"},
|
||||
"a:1": {Message: "a err"},
|
||||
}
|
||||
got := eval(t, &connectionReachableRule{}, data, nil)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 states, got %d", len(got))
|
||||
}
|
||||
if got[0].Subject != "a:1" || got[1].Subject != "b:1" {
|
||||
t.Errorf("subjects not sorted: %q, %q", got[0].Subject, got[1].Subject)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSRVRecordsSkipped(t *testing.T) {
|
||||
data := &MatrixFederationData{}
|
||||
data.DNSResult.SRVSkipped = true
|
||||
data.DNSResult.SRVCName = "matrix.example.org."
|
||||
got := eval(t, &srvRecordsRule{}, data, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("expected Unknown, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionRulePass(t *testing.T) {
|
||||
data := &MatrixFederationData{}
|
||||
data.Version.Name = "Dendrite"
|
||||
data.Version.Version = "0.13.0"
|
||||
got := eval(t, &versionRule{}, data, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("expected OK, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionRuleError(t *testing.T) {
|
||||
data := &MatrixFederationData{}
|
||||
data.Version.Error = "connection refused"
|
||||
got := eval(t, &versionRule{}, data, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusWarn {
|
||||
t.Fatalf("expected Warn, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWellKnownAbsent(t *testing.T) {
|
||||
got := eval(t, &wellKnownRule{}, &MatrixFederationData{}, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("expected Info, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSChecksDedupAndSorted(t *testing.T) {
|
||||
data := &MatrixFederationData{}
|
||||
data.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"`
|
||||
}{
|
||||
"b:8448": {Errors: []string{"server name does not match certificate"}},
|
||||
"a:8448": {Errors: []string{"server name does not match certificate", "server name does not match certificate"}},
|
||||
}
|
||||
|
||||
got := eval(t, &tlsChecksRule{}, data, nil)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 states, got %d", len(got))
|
||||
}
|
||||
if got[0].Subject != "a:8448" || got[1].Subject != "b:8448" {
|
||||
t.Errorf("subjects not sorted: %q, %q", got[0].Subject, got[1].Subject)
|
||||
}
|
||||
if strings.Count(got[0].Message, "server name does not match certificate") != 1 {
|
||||
t.Errorf("expected dedup, got %q", got[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMatrixDataObservationError(t *testing.T) {
|
||||
rule := &federationOKRule{}
|
||||
got := rule.Evaluate(context.Background(), stubObs{err: context.Canceled}, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusError {
|
||||
t.Fatalf("expected Error state, got %+v", got)
|
||||
}
|
||||
}
|
||||
90
checker/rules_tls.go
Normal file
90
checker/rules_tls.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// tlsChecksRule reviews the TLS-level findings the federation tester
|
||||
// reports for every endpoint it managed to reach: certificate validity,
|
||||
// matching server name, future expiry, presence of an Ed25519 key, and so
|
||||
// on. One CheckState is emitted per reachable endpoint so the UI can pin
|
||||
// the outcome on the exact address.
|
||||
type tlsChecksRule struct{}
|
||||
|
||||
func (r *tlsChecksRule) Name() string { return "matrix.tls_checks" }
|
||||
func (r *tlsChecksRule) Description() string {
|
||||
return "Reviews the TLS posture on every reachable federation endpoint (certificate chain, hostname match, Ed25519 key, …)."
|
||||
}
|
||||
|
||||
func (r *tlsChecksRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadMatrixData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
if len(data.ConnectionReports) == 0 {
|
||||
return []sdk.CheckState{unknownState("matrix.tls_checks.skipped", "No endpoint reached: TLS posture could not be assessed.")}
|
||||
}
|
||||
|
||||
addrs := make([]string, 0, len(data.ConnectionReports))
|
||||
for addr := range data.ConnectionReports {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
sort.Strings(addrs)
|
||||
|
||||
out := make([]sdk.CheckState, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
cr := data.ConnectionReports[addr]
|
||||
var problems []string
|
||||
seen := make(map[string]struct{})
|
||||
add := func(p string) {
|
||||
if p == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[p]; ok {
|
||||
return
|
||||
}
|
||||
seen[p] = struct{}{}
|
||||
problems = append(problems, p)
|
||||
}
|
||||
if !cr.Checks.MatchingServerName {
|
||||
add("server name does not match certificate")
|
||||
}
|
||||
if !cr.Checks.FutureValidUntilTS {
|
||||
add("certificate expired or near expiry")
|
||||
}
|
||||
if !cr.Checks.ValidCertificates {
|
||||
add("certificate chain is invalid")
|
||||
}
|
||||
if !cr.Checks.HasEd25519Key {
|
||||
add("no Ed25519 signing key advertised")
|
||||
}
|
||||
if !cr.Checks.AllEd25519ChecksOK {
|
||||
add("Ed25519 key verification failed")
|
||||
}
|
||||
for _, e := range cr.Errors {
|
||||
add(e)
|
||||
}
|
||||
|
||||
if len(problems) == 0 && cr.Checks.AllChecksOK {
|
||||
st := passState("matrix.tls_checks.ok", "All TLS checks passed.")
|
||||
st.Subject = addr
|
||||
out = append(out, st)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := "TLS checks failed."
|
||||
if len(problems) > 0 {
|
||||
msg = fmt.Sprintf("TLS checks failed: %s.", strings.Join(problems, "; "))
|
||||
}
|
||||
st := critState("matrix.tls_checks.fail", msg)
|
||||
st.Subject = addr
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
40
checker/rules_version.go
Normal file
40
checker/rules_version.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// versionRule reports whether the federation tester could fetch the
|
||||
// homeserver version string. The test probe reaches /_matrix/federation/v1/version,
|
||||
// so a failure here hints at a federation-path problem even when the rest
|
||||
// of the federation handshake looks healthy.
|
||||
type versionRule struct{}
|
||||
|
||||
func (r *versionRule) Name() string { return "matrix.version" }
|
||||
func (r *versionRule) Description() string {
|
||||
return "Checks that the homeserver responds to /_matrix/federation/v1/version and reports its name and version."
|
||||
}
|
||||
|
||||
func (r *versionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadMatrixData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
if data.Version.Error != "" {
|
||||
return []sdk.CheckState{warnState("matrix.version.error", fmt.Sprintf("Homeserver /version probe failed: %s", data.Version.Error))}
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version)
|
||||
if version == "" {
|
||||
return []sdk.CheckState{infoState("matrix.version.unknown", "Homeserver did not return a version string.")}
|
||||
}
|
||||
|
||||
st := passState("matrix.version.ok", fmt.Sprintf("Homeserver running %s.", version))
|
||||
st.Meta = map[string]any{"version": version}
|
||||
return []sdk.CheckState{st}
|
||||
}
|
||||
43
checker/rules_wellknown.go
Normal file
43
checker/rules_wellknown.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// wellKnownRule checks the /.well-known/matrix/server delegation: was a
|
||||
// delegation published, did it resolve, and does it point back at the
|
||||
// expected server_name?
|
||||
type wellKnownRule struct{}
|
||||
|
||||
func (r *wellKnownRule) Name() string { return "matrix.well_known" }
|
||||
func (r *wellKnownRule) Description() string {
|
||||
return "Checks that /.well-known/matrix/server (if published) is valid and points at the expected server_name."
|
||||
}
|
||||
|
||||
func (r *wellKnownRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadMatrixData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
wk := data.WellKnownResult
|
||||
|
||||
// Nothing published: the host may rely on SRV only. Mark informational.
|
||||
if wk.Server == "" && wk.Result == "" {
|
||||
return []sdk.CheckState{infoState("matrix.well_known.absent", "No /.well-known/matrix/server delegation published (federation may still work via SRV).")}
|
||||
}
|
||||
|
||||
// Published but the tester flagged an error string.
|
||||
if wk.Server == "" && wk.Result != "" {
|
||||
if strings.Contains(strings.ToLower(wk.Result), "no .well-known") {
|
||||
return []sdk.CheckState{unknownState("matrix.well_known.absent", "No /.well-known/matrix/server delegation found (federation may still work via SRV).")}
|
||||
}
|
||||
return []sdk.CheckState{critState("matrix.well_known.error", fmt.Sprintf("Well-known delegation error: %s", wk.Result))}
|
||||
}
|
||||
|
||||
return []sdk.CheckState{passState("matrix.well_known.ok", fmt.Sprintf("Well-known delegation resolves to %s.", wk.Server))}
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -2,4 +2,4 @@ module git.happydns.org/checker-matrix
|
|||
|
||||
go 1.25.0
|
||||
|
||||
require git.happydns.org/checker-sdk-go v1.2.0
|
||||
require git.happydns.org/checker-sdk-go v1.5.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1,2 +1,2 @@
|
|||
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-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY=
|
||||
git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
||||
|
|
|
|||
6
main.go
6
main.go
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
|
||||
matrix "git.happydns.org/checker-matrix/checker"
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
"git.happydns.org/checker-sdk-go/checker/server"
|
||||
)
|
||||
|
||||
// Version is the standalone binary's version. It defaults to "custom-build"
|
||||
|
|
@ -23,8 +23,8 @@ func main() {
|
|||
// CheckerDefinition.Version.
|
||||
matrix.Version = Version
|
||||
|
||||
server := sdk.NewServer(matrix.Provider())
|
||||
if err := server.ListenAndServe(*listenAddr); err != nil {
|
||||
srv := server.New(matrix.Provider())
|
||||
if err := srv.ListenAndServe(*listenAddr); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,6 @@ var Version = "custom-build"
|
|||
// 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
|
||||
prvd := matrix.Provider()
|
||||
return prvd.(sdk.CheckerDefinitionProvider).Definition(), prvd, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue