Initial commit

This commit is contained in:
nemunaire 2026-04-08 04:22:00 +07:00
commit b259d9ef18
17 changed files with 809 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/checker-ns-restrictions
/checker-ns-restrictions.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-ns-restrictions .
FROM scratch
COPY --from=builder /checker-ns-restrictions /checker-ns-restrictions
EXPOSE 8080
ENTRYPOINT ["/checker-ns-restrictions"]

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-ns-restrictions
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

20
NOTICE Normal file
View file

@ -0,0 +1,20 @@
checker-ns-restrictions
Copyright (c) 2020-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
-------------------------------------------------------------------------------
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
You may obtain a copy of the Apache License 2.0 at:
http://www.apache.org/licenses/LICENSE-2.0
This product includes software developed as part of the miekg/dns project
(https://github.com/miekg/dns), licensed under the BSD 3-Clause License.

68
README.md Normal file
View file

@ -0,0 +1,68 @@
# checker-ns-restrictions
Authoritative nameserver security restrictions checker for [happyDomain](https://www.happydomain.org/).
For each nameserver of an `abstract.Origin` or `abstract.NSOnlyOrigin`
service, this checker verifies common security misconfigurations:
| Check | Severity on failure |
|--------------------------------|---------------------|
| AXFR zone transfer refused | CRITICAL |
| IXFR zone transfer refused | WARNING |
| Recursion not available (RA) | WARNING |
| ANY query handling (RFC 8482) | WARNING |
| Authoritative answer (AA bit) | INFO |
The checker resolves each NS host, then runs the five DNS probes against
every returned IPv4/IPv6 address. IPv6 targets are skipped gracefully if
the host has no IPv6 connectivity.
## Usage
### Standalone HTTP server
```bash
make
./checker-ns-restrictions -listen :8080
```
The server exposes the standard happyDomain external checker protocol
(`/health`, `/collect`, `/evaluate`, `/definition`).
### Docker
```bash
make docker
docker run -p 8080:8080 happydomain/checker-ns-restrictions
```
### happyDomain plugin
```bash
make plugin
# produces checker-ns-restrictions.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
```
## License
This project does **not** depend on the happyDomain core repository: the
few host types it needs (`ServiceMessage`, `abstract.Origin`,
`abstract.NSOnlyOrigin`) are mirrored as minimal local copies of their
JSON wire shapes. It only depends on
[`checker-sdk-go`](https://git.happydns.org/checker-sdk-go) (Apache 2.0)
and [`miekg/dns`](https://github.com/miekg/dns) (BSD 3-Clause).

46
checker/abstract.go Normal file
View file

@ -0,0 +1,46 @@
package checker
import (
"encoding/json"
"github.com/miekg/dns"
)
// Service type identifiers as exposed by happyDomain core.
const (
serviceTypeOrigin = "abstract.Origin"
serviceTypeNSOnlyOrigin = "abstract.NSOnlyOrigin"
)
// originPayload is a minimal local copy of services/abstract.Origin keeping
// only the field this checker reads. The JSON tag matches the upstream wire
// format ("ns").
type originPayload struct {
NameServers []*dns.NS `json:"ns"`
}
// nsOnlyOriginPayload is a minimal local copy of
// services/abstract.NSOnlyOrigin keeping only the field this checker reads.
type nsOnlyOriginPayload struct {
NameServers []*dns.NS `json:"ns"`
}
// nsFromService extracts the list of NS records from an Origin or
// NSOnlyOrigin service payload.
func nsFromService(svc *serviceMessage) []*dns.NS {
switch svc.Type {
case serviceTypeOrigin:
var o originPayload
if err := json.Unmarshal(svc.Service, &o); err != nil {
return nil
}
return o.NameServers
case serviceTypeNSOnlyOrigin:
var o nsOnlyOriginPayload
if err := json.Unmarshal(svc.Service, &o); err != nil {
return nil
}
return o.NameServers
}
return nil
}

164
checker/checks.go Normal file
View file

@ -0,0 +1,164 @@
package checker
import (
"context"
"fmt"
"net"
"time"
"github.com/miekg/dns"
)
// checkAXFR returns (ok bool, detail string).
// ok=false means the server accepted the zone transfer (CRITICAL).
func checkAXFR(ctx context.Context, domain, addr string) (bool, string) {
msg := new(dns.Msg)
msg.SetAxfr(dns.Fqdn(domain))
t := &dns.Transfer{}
t.DialTimeout = 5 * time.Second
t.ReadTimeout = 10 * time.Second
ch, err := t.In(msg, net.JoinHostPort(addr, "53"))
if err != nil {
return true, fmt.Sprintf("transfer refused: %s", err)
}
for env := range ch {
if env.Error != nil {
return true, fmt.Sprintf("transfer error: %s", env.Error)
}
for _, rr := range env.RR {
if rr.Header().Rrtype == dns.TypeSOA {
return false, "AXFR zone transfer accepted"
}
}
}
return true, "AXFR refused"
}
// checkIXFR returns (ok bool, detail string).
// ok=false means the server answered with records (WARN).
func checkIXFR(ctx context.Context, domain, addr string) (bool, string) {
msg := new(dns.Msg)
msg.SetIxfr(dns.Fqdn(domain), 0, "", "")
cl := &dns.Client{Net: "udp", Timeout: 5 * time.Second}
resp, _, err := cl.ExchangeContext(ctx, msg, net.JoinHostPort(addr, "53"))
if err != nil {
return true, fmt.Sprintf("query failed: %s", err)
}
if resp.Rcode != dns.RcodeSuccess {
return true, fmt.Sprintf("IXFR refused (rcode=%s)", dns.RcodeToString[resp.Rcode])
}
if len(resp.Answer) > 0 {
return false, fmt.Sprintf("IXFR accepted with %d answer(s)", len(resp.Answer))
}
return true, "IXFR refused or empty"
}
// checkNoRecursion returns (ok bool, detail string).
// ok=false means the server offers recursion (WARN).
func checkNoRecursion(ctx context.Context, domain, addr string) (bool, string) {
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(domain), dns.TypeSOA)
msg.RecursionDesired = true
cl := &dns.Client{Net: "udp", Timeout: 5 * time.Second}
resp, _, err := cl.ExchangeContext(ctx, msg, net.JoinHostPort(addr, "53"))
if err != nil {
return true, fmt.Sprintf("query failed: %s", err)
}
if resp.RecursionAvailable {
return false, "recursion available (RA bit set)"
}
return true, "recursion not available"
}
// checkANYHandled returns (ok bool, detail string).
// ok=false means the server returned a full record set for ANY (WARN).
// Per RFC 8482, servers should return HINFO or a minimal response.
func checkANYHandled(ctx context.Context, domain, addr string) (bool, string) {
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(domain), dns.TypeANY)
cl := &dns.Client{Net: "udp", Timeout: 5 * time.Second}
resp, _, err := cl.ExchangeContext(ctx, msg, net.JoinHostPort(addr, "53"))
if err != nil {
return true, fmt.Sprintf("query failed: %s", err)
}
if resp.Rcode != dns.RcodeSuccess {
return true, fmt.Sprintf("ANY refused (rcode=%s)", dns.RcodeToString[resp.Rcode])
}
if len(resp.Answer) == 1 {
if _, ok := resp.Answer[0].(*dns.HINFO); ok {
return true, "RFC 8482 compliant HINFO response"
}
}
if len(resp.Answer) == 0 {
return true, "ANY returned empty answer"
}
return false, fmt.Sprintf("ANY returned %d records (not RFC 8482 compliant)", len(resp.Answer))
}
// checkIsAuthoritative returns (ok bool, detail string).
// ok=false means the server is not authoritative for the zone (INFO).
func checkIsAuthoritative(ctx context.Context, domain, addr string) (bool, string) {
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn(domain), dns.TypeSOA)
cl := &dns.Client{Net: "udp", Timeout: 5 * time.Second}
resp, _, err := cl.ExchangeContext(ctx, msg, net.JoinHostPort(addr, "53"))
if err != nil {
return false, fmt.Sprintf("query failed: %s", err)
}
if resp.Authoritative {
return true, "server is authoritative (AA bit set)"
}
return false, "server is not authoritative (AA bit not set)"
}
// Stable check names. They are part of the JSON wire format of
// NSRestrictionsReport and used by individual rules to look up their
// corresponding entry, so they MUST NOT change without coordinating with
// the rule definitions.
const (
checkNameAXFR = "AXFR refused"
checkNameIXFR = "IXFR refused"
checkNameNoRecursion = "No recursion"
checkNameANYHandled = "ANY handled (RFC 8482)"
checkNameIsAuthoritative = "Is authoritative"
)
// checkServerAddr runs all NS security checks against a single IP address.
func checkServerAddr(ctx context.Context, domain, nsHost, addr string) NSServerResult {
result := NSServerResult{Name: nsHost, Address: addr}
type checkDef struct {
name string
fn func(context.Context, string, string) (bool, string)
}
checks := []checkDef{
{checkNameAXFR, checkAXFR},
{checkNameIXFR, checkIXFR},
{checkNameNoRecursion, checkNoRecursion},
{checkNameANYHandled, checkANYHandled},
{checkNameIsAuthoritative, checkIsAuthoritative},
}
for _, ch := range checks {
ok, detail := ch.fn(ctx, domain, addr)
result.Checks = append(result.Checks, NSCheckItem{Name: ch.name, OK: ok, Detail: detail})
}
return result
}

117
checker/collect.go Normal file
View file

@ -0,0 +1,117 @@
package checker
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"strings"
"syscall"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Collect performs the NS security restriction checks for the configured
// service and returns an NSRestrictionsReport.
func (p *nsProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
svc, err := serviceFromOptions(opts)
if err != nil {
return nil, err
}
if svc.Type != serviceTypeOrigin && svc.Type != serviceTypeNSOnlyOrigin {
return nil, fmt.Errorf("service is %s, expected %s or %s", svc.Type, serviceTypeOrigin, serviceTypeNSOnlyOrigin)
}
domainName := ""
if v, ok := opts["domainName"]; ok {
if s, ok := v.(string); ok {
domainName = s
}
}
if domainName == "" {
domainName = svc.Domain
}
if domainName == "" {
return nil, fmt.Errorf("domain name not provided and not present in service")
}
nameServers := nsFromService(svc)
if len(nameServers) == 0 {
return nil, fmt.Errorf("no nameservers found in service")
}
report := &NSRestrictionsReport{}
for _, ns := range nameServers {
nsHost := strings.TrimSuffix(ns.Ns, ".")
results := checkNameServer(ctx, domainName, nsHost)
report.Servers = append(report.Servers, results...)
}
return report, nil
}
// serviceFromOptions extracts a *serviceMessage from the options. It accepts
// either a direct value (in-process plugin path) or a JSON-decoded
// map[string]any (HTTP path) — both are normalized via a JSON round-trip.
func serviceFromOptions(opts sdk.CheckerOptions) (*serviceMessage, error) {
v, ok := opts["service"]
if !ok {
return nil, fmt.Errorf("service not defined")
}
raw, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal service option: %w", err)
}
var svc serviceMessage
if err := json.Unmarshal(raw, &svc); err != nil {
return nil, fmt.Errorf("failed to decode service option: %w", err)
}
return &svc, nil
}
// checkNameServer resolves nsHost and runs checks on each address.
func checkNameServer(ctx context.Context, domain, nsHost string) []NSServerResult {
addrs, err := net.LookupHost(nsHost)
if err != nil {
return []NSServerResult{{
Name: nsHost,
Address: "",
Checks: []NSCheckItem{{
Name: "DNS resolution",
OK: false,
Detail: fmt.Sprintf("lookup failed: %s", err),
}},
}}
}
var results []NSServerResult
for _, addr := range addrs {
// Skip IPv6 addresses when there is no IPv6 connectivity.
if ip := net.ParseIP(addr); ip != nil && ip.To4() == nil {
conn, err := net.DialTimeout("udp", net.JoinHostPort(addr, "53"), 3*time.Second)
if errors.Is(err, syscall.ENETUNREACH) {
results = append(results, NSServerResult{
Name: nsHost,
Address: addr,
Checks: []NSCheckItem{{
Name: "IPv6 connectivity",
OK: true,
Detail: "unable to test due to the lack of IPv6 connectivity",
}},
})
continue
}
if conn != nil {
conn.Close()
}
}
results = append(results, checkServerAddr(ctx, domain, nsHost, addr))
}
return results
}

51
checker/definition.go Normal file
View file

@ -0,0 +1,51 @@
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. Standalone binaries and plugin entrypoints 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 NS security restrictions
// checker.
func Definition() *sdk.CheckerDefinition {
return &sdk.CheckerDefinition{
ID: "ns_restrictions",
Name: "NS Security Restrictions",
Version: Version,
Availability: sdk.CheckerAvailability{
ApplyToService: true,
LimitToServices: []string{serviceTypeOrigin, serviceTypeNSOnlyOrigin},
},
ObservationKeys: []sdk.ObservationKey{ObservationKeyNSRestrictions},
Options: sdk.CheckerOptionsDocumentation{
ServiceOpts: []sdk.CheckerOptionDocumentation{
{
Id: "service",
Label: "Service",
AutoFill: sdk.AutoFillService,
},
{
Id: "domainName",
Label: "Domain name",
AutoFill: sdk.AutoFillDomainName,
},
},
},
Rules: Rules(),
Interval: &sdk.CheckIntervalSpec{
Min: 1 * time.Hour,
Max: 24 * time.Hour,
Default: 6 * 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 NS restrictions observation provider.
func Provider() sdk.ObservationProvider {
return &nsProvider{}
}
type nsProvider struct{}
func (p *nsProvider) Key() sdk.ObservationKey {
return ObservationKeyNSRestrictions
}
// Definition implements sdk.CheckerDefinitionProvider.
func (p *nsProvider) Definition() *sdk.CheckerDefinition {
return Definition()
}

136
checker/rule.go Normal file
View file

@ -0,0 +1,136 @@
package checker
import (
"context"
"fmt"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rules returns one rule per individual NS security check. Every rule
// reads the same shared observation produced by Collect and only looks
// at its own check entry, so a single network round trip feeds all rules.
func Rules() []sdk.CheckRule {
return []sdk.CheckRule{
&singleCheckRule{
ruleName: "ns_axfr_refused",
description: "Verifies that AXFR zone transfers are refused by every authoritative nameserver",
checkName: checkNameAXFR,
failStatus: sdk.StatusCrit,
code: "ns_axfr",
},
&singleCheckRule{
ruleName: "ns_ixfr_refused",
description: "Verifies that IXFR zone transfers are refused by every authoritative nameserver",
checkName: checkNameIXFR,
failStatus: sdk.StatusWarn,
code: "ns_ixfr",
},
&singleCheckRule{
ruleName: "ns_no_recursion",
description: "Verifies that authoritative nameservers do not advertise recursion (RA bit unset)",
checkName: checkNameNoRecursion,
failStatus: sdk.StatusWarn,
code: "ns_recursion",
},
&singleCheckRule{
ruleName: "ns_any_handled",
description: "Verifies that ANY queries are handled per RFC 8482 (HINFO or minimal answer)",
checkName: checkNameANYHandled,
failStatus: sdk.StatusWarn,
code: "ns_any",
},
&singleCheckRule{
ruleName: "ns_is_authoritative",
description: "Verifies that nameservers answer authoritatively (AA bit set) for the zone",
checkName: checkNameIsAuthoritative,
failStatus: sdk.StatusInfo,
code: "ns_authoritative",
},
}
}
// singleCheckRule evaluates one named check across all servers in the
// shared NSRestrictionsReport observation.
type singleCheckRule struct {
ruleName string
description string
checkName string
failStatus sdk.Status
code string
}
func (r *singleCheckRule) Name() string { return r.ruleName }
func (r *singleCheckRule) Description() string { return r.description }
func (r *singleCheckRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
var report NSRestrictionsReport
if err := obs.Get(ctx, ObservationKeyNSRestrictions, &report); err != nil {
return sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("Failed to get NS restrictions data: %v", err),
Code: r.code + "_error",
}
}
status := sdk.StatusOK
var summaryParts []string
failingServers := make([]map[string]string, 0)
for _, srv := range report.Servers {
item, found := findCheck(srv.Checks, r.checkName)
if !found {
// The collect step did not run this check on this server
// (e.g. IPv6 unreachable, DNS resolution failure). Surface
// the reason from whichever entry the server does have.
if len(srv.Checks) > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%s: skipped (%s)", serverLabel(srv), srv.Checks[0].Detail))
} else {
summaryParts = append(summaryParts, fmt.Sprintf("%s: skipped", serverLabel(srv)))
}
continue
}
if item.OK {
summaryParts = append(summaryParts, fmt.Sprintf("%s: OK", serverLabel(srv)))
continue
}
if status < r.failStatus {
status = r.failStatus
}
summaryParts = append(summaryParts, fmt.Sprintf("%s: FAIL (%s)", serverLabel(srv), item.Detail))
failingServers = append(failingServers, map[string]string{
"name": srv.Name,
"address": srv.Address,
"detail": item.Detail,
})
}
return sdk.CheckState{
Status: status,
Message: strings.Join(summaryParts, " | "),
Code: r.code + "_result",
Meta: map[string]any{
"check": r.checkName,
"failing_servers": failingServers,
},
}
}
func findCheck(items []NSCheckItem, name string) (NSCheckItem, bool) {
for _, it := range items {
if it.Name == name {
return it, true
}
}
return NSCheckItem{}, false
}
func serverLabel(srv NSServerResult) string {
if srv.Address == "" {
return srv.Name
}
return fmt.Sprintf("%s (%s)", srv.Name, srv.Address)
}

35
checker/types.go Normal file
View file

@ -0,0 +1,35 @@
package checker
import "encoding/json"
// ObservationKeyNSRestrictions is the observation key for NS security
// restrictions data.
const ObservationKeyNSRestrictions = "ns_restrictions"
// NSRestrictionsReport contains the results of NS security restriction checks.
type NSRestrictionsReport struct {
Servers []NSServerResult `json:"servers"`
}
// NSServerResult holds the check results for a single nameserver IP.
type NSServerResult struct {
Name string `json:"name"`
Address string `json:"address"`
Checks []NSCheckItem `json:"checks"`
}
// NSCheckItem represents one security check for an NS server.
type NSCheckItem struct {
Name string `json:"name"`
OK bool `json:"ok"`
Detail string `json:"detail,omitempty"`
}
// serviceMessage is a minimal local copy of happydns.ServiceMessage matching
// the JSON wire shape, so this plugin does not depend on the happyDomain core
// repository.
type serviceMessage struct {
Type string `json:"_svctype"`
Domain string `json:"_domain"`
Service json.RawMessage `json:"Service"`
}

16
go.mod Normal file
View file

@ -0,0 +1,16 @@
module git.happydns.org/checker-ns-restrictions
go 1.25.0
require (
git.happydns.org/checker-sdk-go v0.0.1
github.com/miekg/dns v1.1.72
)
require (
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/tools v0.40.0 // indirect
)

16
go.sum Normal file
View file

@ -0,0 +1,16 @@
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=
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=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=

30
main.go Normal file
View file

@ -0,0 +1,30 @@
package main
import (
"flag"
"log"
nsr "git.happydns.org/checker-ns-restrictions/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.
nsr.Version = Version
server := sdk.NewServer(nsr.Provider())
if err := server.ListenAndServe(*listenAddr); err != nil {
log.Fatalf("server error: %v", err)
}
}

27
plugin/plugin.go Normal file
View file

@ -0,0 +1,27 @@
// Command plugin is the happyDomain plugin entrypoint for the NS security
// restrictions checker.
//
// It is built as a Go plugin (`go build -buildmode=plugin`) and loaded at
// runtime by happyDomain.
package main
import (
nsr "git.happydns.org/checker-ns-restrictions/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-ns-restrictions.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) {
// Propagate the plugin's version to the checker package so it shows up
// in CheckerDefinition.Version.
nsr.Version = Version
return nsr.Definition(), nsr.Provider(), nil
}