diff --git a/Dockerfile b/Dockerfile index 591dc3d..64f0039 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-delegation . +RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-delegation . FROM scratch COPY --from=builder /checker-delegation /checker-delegation diff --git a/Makefile b/Makefile index a257412..3b94d28 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,12 @@ CHECKER_SOURCES := main.go $(wildcard checker/*.go) GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION) -.PHONY: all plugin docker test clean +.PHONY: all plugin docker clean all: $(CHECKER_NAME) $(CHECKER_NAME): $(CHECKER_SOURCES) - go build -tags standalone -ldflags "$(GO_LDFLAGS)" -o $@ . + go build -ldflags "$(GO_LDFLAGS)" -o $@ . plugin: $(CHECKER_NAME).so @@ -21,8 +21,5 @@ $(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 diff --git a/README.md b/README.md deleted file mode 100644 index baa57dc..0000000 --- a/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# checker-delegation - -DNS delegation checker for [happyDomain](https://www.happydomain.org/). - -Audits the delegation of a zone: NS consistency between parent and child, -glue correctness, DS / DNSKEY hand-off, TCP reachability, SOA serial drift, -and authoritativeness of each delegated server. Applies to services of type -`abstract.Delegation`. - -## Usage - -### Standalone HTTP server - -```bash -# Build and run -make -./checker-delegation -listen :8080 -``` - -The server exposes: - -- `GET /health`, health check -- `POST /collect`, collect delegation observations (happyDomain external checker protocol) - -### Docker - -```bash -make docker -docker run -p 8080:8080 happydomain/checker-delegation -``` - -### happyDomain plugin - -```bash -make plugin -# produces checker-delegation.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 delegation checker to the URL of -the running checker-delegation server (e.g., -`http://checker-delegation:8080`). happyDomain will delegate observation -collection to this endpoint. - -## Options - -| Option | Type | Default | Description | -|---------------------|------|---------|---------------------------------------------------------------------------------------------------| -| `requireDS` | bool | `false` | When enabled, missing DS records at the parent are treated as critical (otherwise informational). | -| `requireTCP` | bool | `true` | When enabled, name servers that fail to answer over TCP are reported as critical (otherwise warning). | -| `minNameServers` | uint | `2` | Below this count, the delegation is reported as a warning (RFC 1034 recommends at least 2). | -| `allowGlueMismatch` | bool | `false` | When disabled, glue/address mismatches between parent and child are reported as critical. | - -## Protocol - -### POST /collect - -Request: -```json -{ - "key": "delegation", - "target": {"userId": "...", "domainId": "..."}, - "options": { - "domain_name": "example.com.", - "subdomain": "www", - "service": { "_svctype": "abstract.Delegation", "Service": { "ns": [...], "ds": [...] } } - } -} -``` - -Response: -```json -{ - "data": { - "delegated_fqdn": "www.example.com.", - "parent_zone": "example.com.", - "parent_ns": ["a.iana-servers.net.", "b.iana-servers.net."], - "advertised_ns": ["ns1.example.net.", "ns2.example.net."], - "advertised_glue": {}, - "parent_ds": [], - "child_serials": {"ns1.example.net.:53": 2026042401}, - "findings": [ - { - "code": "delegation_ns_mismatch", - "severity": "crit", - "message": "NS RRset at parent does not match declared service: missing=[ns3.example.net] extra=[]", - "server": "a.iana-servers.net.:53" - } - ] - } -} -``` - -Findings carry a stable `code` (e.g. `delegation_lame`, -`delegation_missing_glue`, `delegation_ds_mismatch`, -`delegation_soa_serial_drift`, `delegation_dnskey_no_match`, …) so that -downstream rules can match on them deterministically. - -## License - -This project is licensed under the **MIT License** (see `LICENSE`), in -line with the rest of the happyDomain checker ecosystem. - -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. diff --git a/checker/collect.go b/checker/collect.go index 925ecb2..7d25897 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( @@ -164,7 +185,7 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption if len(dsMissing) > 0 || len(dsExtra) > 0 { sev := SeverityCrit if len(declaredDS) == 0 { - // Service does not declare any DS but parent has some, warn only. + // Service does not declare any DS but parent has some — warn only. sev = SeverityWarn } data.Findings = append(data.Findings, DelegationFinding{ diff --git a/checker/definition.go b/checker/definition.go index e6b9c37..e400932 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( diff --git a/checker/dns.go b/checker/dns.go index e810eed..9b68043 100644 --- a/checker/dns.go +++ b/checker/dns.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( @@ -19,7 +40,7 @@ const dnsTimeout = 5 * time.Second // dnsExchange sends a single query to the given server using the requested // transport ("" for UDP, "tcp"). The server address must already include a -// port. RecursionDesired is forced off, this checker only talks to +// port. RecursionDesired is forced off — this checker only talks to // authoritative servers. func dnsExchange(ctx context.Context, proto, server string, q dns.Question, edns bool) (*dns.Msg, error) { client := dns.Client{Net: proto, Timeout: dnsTimeout} @@ -77,7 +98,7 @@ func resolveHost(ctx context.Context, host string) ([]string, error) { // resolver returns NOERROR with an answer. // // If hintParent is non-empty, it is used as the assumed parent and we only -// resolve its NS, this matches happyDomain's data model where the parent +// resolve its NS — this matches happyDomain's data model where the parent // zone is known. func findParentZone(ctx context.Context, fqdn, hintParent string) (zone string, servers []string, err error) { zone = dns.Fqdn(hintParent) diff --git a/checker/evaluate.go b/checker/evaluate.go index b8cf56d..761d841 100644 --- a/checker/evaluate.go +++ b/checker/evaluate.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( @@ -9,7 +30,7 @@ import ( // Evaluate folds findings into a single CheckState. The status is the // highest severity observed: any Crit makes the whole result Crit, any Warn // makes it Warn, otherwise OK. -func Evaluate(data *DelegationData) []sdk.CheckState { +func Evaluate(data *DelegationData) sdk.CheckState { status := sdk.StatusOK var crit, warn, info int for _, f := range data.Findings { @@ -37,7 +58,7 @@ func Evaluate(data *DelegationData) []sdk.CheckState { msg = fmt.Sprintf("Delegation of %s: %d critical, %d warning, %d info", data.DelegatedFQDN, crit, warn, info) } - return []sdk.CheckState{{ + return sdk.CheckState{ Status: status, Message: msg, Code: "delegation_result", @@ -49,5 +70,5 @@ func Evaluate(data *DelegationData) []sdk.CheckState { "parent_ds": data.ParentDS, "child_serials": data.ChildSerials, }, - }} + } } diff --git a/checker/interactive.go b/checker/interactive.go deleted file mode 100644 index ec46c23..0000000 --- a/checker/interactive.go +++ /dev/null @@ -1,139 +0,0 @@ -//go:build standalone - -package checker - -import ( - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "strings" - - "github.com/miekg/dns" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// RenderForm exposes a single "delegated domain" input for the standalone -// /check route. The parent zone is derived by stripping the left-most label -// and the declared NS / DS sets are resolved via the system resolver. -func (p *delegationProvider) RenderForm() []sdk.CheckerOptionField { - return []sdk.CheckerOptionField{ - { - Id: "domain", - Type: "string", - Label: "Delegated domain", - Placeholder: "sub.example.com", - Required: true, - Description: "Fully-qualified name of the delegated zone to check.", - }, - } -} - -// ParseForm turns the submitted domain into the CheckerOptions that Collect -// expects: the parent zone, the sub-label, and a synthesized -// abstract.Delegation service populated with the NS / DS records resolved -// via the system resolver. -func (p *delegationProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { - domain := strings.TrimSpace(r.FormValue("domain")) - if domain == "" { - return nil, errors.New("domain is required") - } - fqdn := dns.Fqdn(domain) - labels := dns.SplitDomainName(fqdn) - if len(labels) < 2 { - return nil, fmt.Errorf("%q has no parent zone", domain) - } - parentZone := strings.Join(labels[1:], ".") - subdomain := labels[0] - - resolver, err := interactiveResolver() - if err != nil { - return nil, err - } - - nsRecords, err := lookupDelegationNS(resolver, fqdn) - if err != nil { - return nil, fmt.Errorf("NS lookup for %s: %w", domain, err) - } - if len(nsRecords) == 0 { - return nil, fmt.Errorf("no NS records found for %s", domain) - } - dsRecords, err := lookupDelegationDS(resolver, fqdn) - if err != nil { - return nil, fmt.Errorf("DS lookup for %s: %w", domain, err) - } - - body, err := json.Marshal(delegationService{NameServers: nsRecords, DS: dsRecords}) - if err != nil { - return nil, fmt.Errorf("marshal delegation service: %w", err) - } - - svc := serviceMessage{ - Type: "abstract.Delegation", - Domain: parentZone, - Service: body, - } - - return sdk.CheckerOptions{ - "domain_name": parentZone, - "subdomain": subdomain, - "service": svc, - }, nil -} - -func interactiveResolver() (string, error) { - cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf") - if err != nil || len(cfg.Servers) == 0 { - return net.JoinHostPort("1.1.1.1", "53"), nil - } - return net.JoinHostPort(cfg.Servers[0], cfg.Port), nil -} - -func lookupDelegationNS(resolver, fqdn string) ([]*dns.NS, error) { - msg := new(dns.Msg) - msg.SetQuestion(fqdn, dns.TypeNS) - msg.RecursionDesired = true - - c := new(dns.Client) - in, _, err := c.Exchange(msg, resolver) - if err != nil { - return nil, err - } - if in.Rcode != dns.RcodeSuccess && in.Rcode != dns.RcodeNameError { - return nil, fmt.Errorf("rcode %s", dns.RcodeToString[in.Rcode]) - } - - var out []*dns.NS - for _, rr := range in.Answer { - if ns, ok := rr.(*dns.NS); ok { - out = append(out, ns) - } - } - return out, nil -} - -func lookupDelegationDS(resolver, fqdn string) ([]*dns.DS, error) { - msg := new(dns.Msg) - msg.SetQuestion(fqdn, dns.TypeDS) - msg.RecursionDesired = true - msg.SetEdns0(4096, true) - - c := new(dns.Client) - in, _, err := c.Exchange(msg, resolver) - if err != nil { - return nil, err - } - if in.Rcode != dns.RcodeSuccess && in.Rcode != dns.RcodeNameError { - return nil, fmt.Errorf("rcode %s", dns.RcodeToString[in.Rcode]) - } - - var out []*dns.DS - for _, rr := range in.Answer { - if ds, ok := rr.(*dns.DS); ok { - out = append(out, ds) - } - } - return out, nil -} diff --git a/checker/provider.go b/checker/provider.go index c612fa3..c6a6e9c 100644 --- a/checker/provider.go +++ b/checker/provider.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( diff --git a/checker/rule.go b/checker/rule.go index 444d421..4200d09 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( @@ -33,14 +54,14 @@ func (r *delegationRule) ValidateOptions(opts sdk.CheckerOptions) error { return nil } -func (r *delegationRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { +func (r *delegationRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState { var data DelegationData if err := obs.Get(ctx, ObservationKeyDelegation, &data); err != nil { - return []sdk.CheckState{{ + return sdk.CheckState{ Status: sdk.StatusError, Message: fmt.Sprintf("Failed to get delegation data: %v", err), Code: "delegation_error", - }} + } } return Evaluate(&data) } diff --git a/checker/types.go b/checker/types.go index a6c76e5..d164212 100644 --- a/checker/types.go +++ b/checker/types.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package checker import ( diff --git a/go.mod b/go.mod index 3ec3aab..ebea955 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.happydns.org/checker-delegation go 1.25.0 require ( - git.happydns.org/checker-sdk-go v1.3.0 + git.happydns.org/checker-sdk-go v0.0.1 github.com/miekg/dns v1.1.72 ) diff --git a/go.sum b/go.sum index 092728a..eab57b3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.happydns.org/checker-sdk-go v1.3.0 h1:FG2kIhlJCzI0m35EhxSgn4UWc9M4ha6aZTeoChu4l7A= -git.happydns.org/checker-sdk-go v1.3.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +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= diff --git a/main.go b/main.go index 465e3cf..492a553 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package main import ( @@ -5,7 +26,7 @@ import ( "log" delegation "git.happydns.org/checker-delegation/checker" - "git.happydns.org/checker-sdk-go/checker/server" + sdk "git.happydns.org/checker-sdk-go/checker" ) var listenAddr = flag.String("listen", ":8080", "HTTP listen address") @@ -21,8 +42,8 @@ func main() { delegation.Version = Version - srv := server.New(delegation.Provider()) - if err := srv.ListenAndServe(*listenAddr); err != nil { + server := sdk.NewServer(delegation.Provider()) + if err := server.ListenAndServe(*listenAddr); err != nil { log.Fatalf("server error: %v", err) } } diff --git a/plugin/plugin.go b/plugin/plugin.go index 7d2e37d..5fde216 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,3 +1,24 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + // Command plugin is the happyDomain plugin entrypoint for the delegation // checker. It is built as a Go plugin (`go build -buildmode=plugin`) and // loaded at runtime by happyDomain.