Compare commits

...

No commits in common. "8d2ff95be9df46b7cc6cc65074a311646069bedc" and "158b453759adf212d858e516b1c353a91126cf9d" have entirely different histories.

12 changed files with 214 additions and 168 deletions

21
LICENSE
View file

@ -1,21 +0,0 @@
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.

View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import (

View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import (

View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import (

View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import (

View file

@ -1,103 +0,0 @@
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
}

View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import (

View file

@ -1,8 +1,30 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import (
"context"
"fmt"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
@ -63,65 +85,66 @@ 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",
}}
}
}
out := make([]sdk.CheckState, 0, len(report.Servers))
for _, srv := range report.Servers {
meta := map[string]any{
"check": r.checkName,
"name": srv.Name,
"address": srv.Address,
}
status := sdk.StatusOK
var summaryParts []string
failingServers := make([]map[string]string, 0)
checked := false
for _, srv := range report.Servers {
item, found := findCheck(srv.Checks, r.checkName)
if !found {
message := "check not performed"
// 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 {
message = fmt.Sprintf("skipped: %s", srv.Checks[0].Detail)
summaryParts = append(summaryParts, fmt.Sprintf("%s: skipped (%s)", serverLabel(srv), srv.Checks[0].Detail))
} else {
summaryParts = append(summaryParts, fmt.Sprintf("%s: skipped", serverLabel(srv)))
}
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,
}
checked = true
if item.OK {
state.Status = sdk.StatusOK
if state.Message == "" {
state.Message = "OK"
}
} else {
state.Status = r.failStatus
summaryParts = append(summaryParts, fmt.Sprintf("%s: OK", serverLabel(srv)))
continue
}
out = append(out, state)
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,
})
}
if len(out) == 0 {
return []sdk.CheckState{{
Status: sdk.StatusUnknown,
Message: "no nameserver to evaluate",
Code: r.code + "_result",
}}
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,
},
}
return out
}
func findCheck(items []NSCheckItem, name string) (NSCheckItem, bool) {

View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package checker
import "encoding/json"

2
go.mod
View file

@ -3,7 +3,7 @@ module git.happydns.org/checker-ns-restrictions
go 1.25.0
require (
git.happydns.org/checker-sdk-go v1.2.0
git.happydns.org/checker-sdk-go v0.0.1
github.com/miekg/dns v1.1.72
)

4
go.sum
View file

@ -1,5 +1,5 @@
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 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=

25
main.go
View file

@ -1,3 +1,24 @@
// 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 <contact@happydomain.org>.
//
// 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 <https://www.gnu.org/licenses/>.
package main
import (
@ -8,14 +29,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()