Compare commits

...

3 commits

Author SHA1 Message Date
744a75b25d checker: resolve relative NS labels using the zone origin
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
NS hostnames stored in happyDomain's abstract.Origin service are encoded
relative to the zone apex (e.g. "ns0" instead of "ns0.example.com.").
normalizeNSList was calling dns.Fqdn() directly, turning "ns0" into the
useless single-label "ns0." rather than the correct FQDN.

Expose domain_name in ServiceOpts so happyDomain auto-fills the zone
apex at service scope, then use sdk.JoinRelative in normalizeNSList to
absolutize relative labels.  Absolute FQDNs (trailing dot) are kept
as-is so external nameservers round-trip safely.
2026-05-16 21:37:05 +08:00
0903357221 Enforce prober/evaluator boundary in ObservationData
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-15 17:43:27 +08:00
a0dcac59bd Add CI/CD pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-10 19:00:19 +08:00
6 changed files with 231 additions and 13 deletions

22
.drone-manifest.yml Normal file
View file

@ -0,0 +1,22 @@
image: happydomain/checker-authoritative-consistency:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: happydomain/checker-authoritative-consistency:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: happydomain/checker-authoritative-consistency:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: happydomain/checker-authoritative-consistency:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

187
.drone.yml Normal file
View 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-authoritative-consistency
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-authoritative-consistency
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-authoritative-consistency
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-authoritative-consistency
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

View file

@ -32,7 +32,7 @@ func (p *authoritativeConsistencyProvider) Collect(ctx context.Context, opts sdk
data := &ObservationData{
Zone: dns.Fqdn(zone),
HasSOA: svc.SOA != nil,
DeclaredNS: normalizeNSList(svc.NameServers),
DeclaredNS: normalizeNSList(svc.NameServers, zone),
Results: map[string]*NSResult{},
}
if svc.SOA != nil {
@ -167,13 +167,17 @@ func loadZone(opts sdk.CheckerOptions, svc *originService) (string, error) {
return "", fmt.Errorf("no zone name provided (missing 'domain_name' option and SOA header)")
}
func normalizeNSList(ns []*dns.NS) []string {
func normalizeNSList(ns []*dns.NS, origin string) []string {
out := make([]string, 0, len(ns))
for _, n := range ns {
if n == nil {
continue
}
out = append(out, strings.ToLower(dns.Fqdn(n.Ns)))
name := n.Ns
if !strings.HasSuffix(name, ".") {
name = sdk.JoinRelative(name, strings.TrimSuffix(origin, "."))
}
out = append(out, strings.ToLower(dns.Fqdn(name)))
}
sort.Strings(out)
return out

View file

@ -80,13 +80,15 @@ func TestDiffStringSets_Equal(t *testing.T) {
}
func TestNormalizeNSList(t *testing.T) {
// Relative labels (no trailing dot) are joined with the zone origin.
// Absolute FQDNs (trailing dot) are kept as-is.
in := []*dns.NS{
{Ns: "NS2.Example.COM"},
{Ns: "ns2"},
nil,
{Ns: "ns1.example.com."},
{Ns: "NS1.example.com"},
{Ns: "ns1"},
}
got := normalizeNSList(in)
got := normalizeNSList(in, "example.com.")
want := []string{"ns1.example.com.", "ns1.example.com.", "ns2.example.com."}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)

View file

@ -110,6 +110,11 @@ func (p *authoritativeConsistencyProvider) Definition() *sdk.CheckerDefinition {
Label: "Origin service",
AutoFill: sdk.AutoFillService,
},
{
Id: "domain_name",
Label: "Zone name",
AutoFill: sdk.AutoFillDomainName,
},
},
},
Rules: Rules(),

View file

@ -3,6 +3,7 @@ package checker
import (
"encoding/json"
"fmt"
"slices"
"github.com/miekg/dns"
)
@ -77,11 +78,9 @@ type NSResult struct {
// Dedupes identical messages and caps the list with a sentinel summary.
func (n *NSResult) appendError(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
for _, e := range n.Errors {
if e == msg {
if slices.Contains(n.Errors, msg) {
return
}
}
if len(n.Errors) >= maxNSResultErrors {
n.suppressedErrors++
sentinel := fmt.Sprintf("(%d more error(s) suppressed)", n.suppressedErrors)
@ -108,7 +107,6 @@ type ObservationData struct {
// Union of DeclaredNS and ParentNS, de-duplicated.
Probed []string `json:"probed,omitempty"`
Results map[string]*NSResult `json:"results,omitempty"`
Findings []Finding `json:"findings"`
}
// Local mirror of happyDomain's services/abstract.Origin. Duplicated on