diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07d44d8 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/checker/abstract.go b/checker/abstract.go index 6c42667..c8de1e5 100644 --- a/checker/abstract.go +++ b/checker/abstract.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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/checks.go b/checker/checks.go index 534073f..89bb952 100644 --- a/checker/checks.go +++ b/checker/checks.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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/collect.go b/checker/collect.go index 1a49be8..7ab9990 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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/definition.go b/checker/definition.go index 42370bb..1db8583 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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/interactive.go b/checker/interactive.go new file mode 100644 index 0000000..d7de7c8 --- /dev/null +++ b/checker/interactive.go @@ -0,0 +1,103 @@ +package checker + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" + "github.com/miekg/dns" +) + +// RenderForm implements sdk.CheckerInteractive. It lists the minimal human +// inputs needed to bootstrap a check when this checker runs standalone +// (outside of a happyDomain host). +func (p *nsProvider) RenderForm() []sdk.CheckerOptionField { + return []sdk.CheckerOptionField{ + { + Id: "domain", + Type: "string", + Label: "Domain name", + Placeholder: "example.com", + Required: true, + Description: "Zone to probe. Its NS records will be resolved and each nameserver tested.", + }, + } +} + +// ParseForm implements sdk.CheckerInteractive. It resolves the NS records +// for the requested domain via DNS and assembles the CheckerOptions that +// Collect expects — replacing the AutoFill work that happyDomain would +// otherwise perform. +func (p *nsProvider) 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) + + nsRecords, err := resolveNS(fqdn) + if err != nil { + return nil, fmt.Errorf("could not resolve NS records for %s: %w", domain, err) + } + if len(nsRecords) == 0 { + return nil, fmt.Errorf("no NS records found for %s", domain) + } + + payload, err := json.Marshal(originPayload{NameServers: nsRecords}) + if err != nil { + return nil, fmt.Errorf("failed to encode origin payload: %w", err) + } + + svc := serviceMessage{ + Type: serviceTypeOrigin, + Domain: "", + Service: payload, + } + + return sdk.CheckerOptions{ + "service": svc, + "domainName": strings.TrimSuffix(fqdn, "."), + }, nil +} + +// resolveNS queries the system resolver for the NS records of fqdn and +// returns them as miekg *dns.NS records so they match the shape produced +// by happyDomain's Origin service payload. +func resolveNS(fqdn string) ([]*dns.NS, error) { + c := new(dns.Client) + m := new(dns.Msg) + m.SetQuestion(fqdn, dns.TypeNS) + m.RecursionDesired = true + + config, err := dns.ClientConfigFromFile("/etc/resolv.conf") + if err != nil || config == nil || len(config.Servers) == 0 { + config = &dns.ClientConfig{Servers: []string{"1.1.1.1", "8.8.8.8"}, Port: "53"} + } + + var lastErr error + for _, server := range config.Servers { + in, _, err := c.Exchange(m, server+":"+config.Port) + if err != nil { + lastErr = err + continue + } + if in.Rcode != dns.RcodeSuccess { + lastErr = fmt.Errorf("DNS response code %s", dns.RcodeToString[in.Rcode]) + continue + } + var records []*dns.NS + for _, rr := range in.Answer { + if ns, ok := rr.(*dns.NS); ok { + records = append(records, ns) + } + } + return records, nil + } + if lastErr == nil { + lastErr = errors.New("no resolver available") + } + return nil, lastErr +} diff --git a/checker/provider.go b/checker/provider.go index b7a8d13..6dfe8a7 100644 --- a/checker/provider.go +++ b/checker/provider.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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 85d1e5f..94abc35 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -1,30 +1,8 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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 ( "context" "fmt" - "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) @@ -85,66 +63,65 @@ type singleCheckRule struct { 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 { +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{ + 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) - checked := false - + out := make([]sdk.CheckState, 0, len(report.Servers)) 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 - } - - checked = true - - 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{ + meta := map[string]any{ + "check": r.checkName, "name": srv.Name, "address": srv.Address, - "detail": item.Detail, - }) + } + + item, found := findCheck(srv.Checks, r.checkName) + if !found { + message := "check not performed" + if len(srv.Checks) > 0 { + message = fmt.Sprintf("skipped: %s", srv.Checks[0].Detail) + } + out = append(out, sdk.CheckState{ + Status: sdk.StatusUnknown, + Message: message, + Code: r.code + "_skipped", + Subject: serverLabel(srv), + Meta: meta, + }) + continue + } + + state := sdk.CheckState{ + Code: r.code + "_result", + Subject: serverLabel(srv), + Meta: meta, + Message: item.Detail, + } + if item.OK { + state.Status = sdk.StatusOK + if state.Message == "" { + state.Message = "OK" + } + } else { + state.Status = r.failStatus + } + out = append(out, state) } - if !checked { - status = sdk.StatusUnknown - } - - return sdk.CheckState{ - Status: status, - Message: strings.Join(summaryParts, " | "), - Code: r.code + "_result", - Meta: map[string]any{ - "check": r.checkName, - "failing_servers": failingServers, - }, + if len(out) == 0 { + return []sdk.CheckState{{ + Status: sdk.StatusUnknown, + Message: "no nameserver to evaluate", + Code: r.code + "_result", + }} } + return out } func findCheck(items []NSCheckItem, name string) (NSCheckItem, bool) { diff --git a/checker/types.go b/checker/types.go index 2d976a5..d84f327 100644 --- a/checker/types.go +++ b/checker/types.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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 "encoding/json" diff --git a/go.mod b/go.mod index 98a5e52..906fafe 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.happydns.org/checker-ns-restrictions go 1.25.0 require ( - git.happydns.org/checker-sdk-go v0.0.1 + git.happydns.org/checker-sdk-go v1.2.0 github.com/miekg/dns v1.1.72 ) diff --git a/go.sum b/go.sum index eab57b3..d64ca7c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -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= +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= 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 c3f555d..2ea54aa 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,3 @@ -// This file is part of the happyDomain (R) project. -// Copyright (c) 2020-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 ( @@ -29,14 +8,14 @@ import ( sdk "git.happydns.org/checker-sdk-go/checker" ) -var listenAddr = flag.String("listen", ":8080", "HTTP listen address") - // 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()