This commit is contained in:
developer committed 2026-09-28 10:37:15 +00:00
1 parent d94e4cf6a8
commit b2cd7d4b91
13 files changed
+542 -26

No files matched your search

+58 -1
View File
@@ -2,7 +2,9 @@ package app
import (
"context"
"errors"
"fmt"
"net/netip"
"sort"
"strings"
"sync"
@@ -13,6 +15,7 @@ import (
"git.k8s-git-k6n.net/mats/fancy-dns/internal/api"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/blocklist"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/engine"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/metrics"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/qlog"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/resolver"
@@ -35,6 +38,7 @@ func (a *App) hooks() api.Hooks {
RefreshBlocklists: a.refreshBlocklists,
ToggleBlocklist: a.toggleBlocklist,
DomainCheck: a.domainCheck,
Resolve: a.resolveTest,
ListDomains: a.listDomains,
AddDomain: a.addDomain,
DeleteDomain: a.deleteDomain,
@@ -398,7 +402,60 @@ func (a *App) domainCheck(name string) (any, error) {
return out, nil
}
// listDomains pages the compiled rules. Walking a million-entry map on every
// resolveTest runs one query through the live pipeline. It goes through the
// engine rather than straight to an upstream: a test that skipped the
// pipeline would report a healthy upstream while the blocklist, the ACL or the
// local zones were what a client actually hit.
func (a *App) resolveTest(ctx context.Context, name, qtype string) (any, error) {
if a.engine == nil {
return nil, errors.New("the query engine is not running")
}
qt := dns.TypeA
if qtype = strings.TrimSpace(qtype); qtype != "" {
var ok bool
qt, ok = dns.StringToType[strings.ToUpper(qtype)]
if !ok {
return nil, fmt.Errorf("unknown record type %q", qtype)
}
}
q := new(dns.Msg)
q.SetQuestion(dns.Fqdn(name), qt)
q.RecursionDesired = true
res := a.engine.Handle(ctx, engine.Request{
Msg: q,
Remote: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
Transport: "api",
Start: time.Now(),
})
out := map[string]any{
"name": dns.Fqdn(name),
"type": dns.TypeToString[qt],
"rcode": "SERVFAIL",
"action": string(res.Action),
"duration_ms": res.Duration.Milliseconds(),
"upstream": res.Upstream,
"group": res.Group,
"cache": res.CacheKind,
"local": res.Local,
"truncated": res.Truncated,
"blocked": res.BlockSource != "",
"block_source": res.BlockSource,
"answers": []string{},
}
if res.Msg == nil {
out["error"] = "no response"
return out, nil
}
out["rcode"] = dns.RcodeToString[res.Msg.Rcode]
answers := make([]string, 0, len(res.Msg.Answer))
for _, rr := range res.Msg.Answer {
answers = append(answers, strings.TrimSpace(rr.String()))
}
out["answers"] = answers
return out, nil
}
// listDomains pages the compiled rules.
// request would be too slow, so the filter runs over a bounded sample unless
// the query is a prefix/exact lookup, which the LRU can answer directly.
func (a *App) listDomains(q api.DomainQuery) (any, error) {
+51
View File
@@ -608,6 +608,57 @@ func TestDomainAddInvalid(t *testing.T) {
// TestACLUpdateRoundTrip checks the clients editor's write path and that a
// saved group is enforced immediately.
// The console's "Test" button goes through the live pipeline, so this pins
// the whole path: a name the fake upstream answers must come back with the
// answer records, and one it does not must come back as a result (a 200 with
// an rcode) rather than as a failed request.
func TestDNSTestEndpoint(t *testing.T) {
h := newHarness(t, nil)
code, body := h.do(http.MethodPost, "/api/dns/test", `{"name":"www.example.test","type":"A"}`)
if code != http.StatusOK {
t.Fatalf("POST /api/dns/test = %d: %s", code, body)
}
var hit struct {
Name string `json:"name"`
Type string `json:"type"`
RCode string `json:"rcode"`
Action string `json:"action"`
Answers []string `json:"answers"`
}
if err := json.Unmarshal(body, &hit); err != nil {
t.Fatal(err)
}
if hit.Name != "www.example.test." || hit.Type != "A" {
t.Errorf("canonical name = %q type = %q", hit.Name, hit.Type)
}
if hit.RCode != "NOERROR" {
t.Fatalf("rcode = %q, want NOERROR: %s", hit.RCode, body)
}
if len(hit.Answers) != 1 || !strings.Contains(hit.Answers[0], "203.0.113.7") {
t.Errorf("answers = %v, want the upstream A record", hit.Answers)
}
// A name the upstream does not hold is still a successful test.
code, body = h.do(http.MethodPost, "/api/dns/test", `{"name":"nothing.example.com","type":"A"}`)
if code != http.StatusOK {
t.Fatalf("POST /api/dns/test (miss) = %d: %s", code, body)
}
if !strings.Contains(string(body), "NXDOMAIN") {
t.Errorf("a miss is not reported as a result: %s", body)
}
// An unknown record type is a bad request the caller can fix, so it comes
// back as an error in the payload rather than as a bogus lookup.
code, body = h.do(http.MethodPost, "/api/dns/test", `{"name":"www.example.test","type":"NOTATYPE"}`)
if code != http.StatusOK {
t.Fatalf("POST /api/dns/test (bad type) = %d: %s", code, body)
}
if !strings.Contains(string(body), "error") {
t.Errorf("a bad type is not reported: %s", body)
}
}
func TestACLUpdateRoundTrip(t *testing.T) {
h := newHarness(t, nil)
// Write a deny group through the API.