Files
fancy-dns/internal/kubesource/types.go
T
matsandClaude Sonnet 5.5 88479b9a12 Add AddressOverride and DomainRule objects for what lived in a ConfigMap and the config file
The address redirects page rewrote an RPZ ConfigMap through the resolver's own
service account, and the domains page rewrote allow_entries/block_entries in the
config file. Both become cluster objects like the rest.

AddressOverride is one name answered with fixed addresses (the name and
everything under it), exactly what an RPZ "name A address" line did. DomainRule
is a block or allow list of inline names. The resolver reads both with the
existing watch: overrides are rendered into the managed RPZ source the blocklist
manager already has, and rule names join the config's inline entries in the
merged overlay, so the file's config is never written to.

Names are checked to be DNS names or wildcard patterns before they are used,
because they are joined into line-oriented parser input and a newline or a
comment character in one would otherwise add a rule of its own.

The console edits both: the address-overrides and domains endpoints now write
objects. The console keeps its added rules in one DomainRule per action
(inline-block, inline-allow) instead of an object per name, and removes a name
from whichever DomainRule holds it, wildcard or not. An end-to-end test drives
the pages' endpoints and checks the resolver's answers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:08:08 +02:00

106 lines
4.6 KiB
Go

// Package kubesource turns fancy-dns custom resources (DNSZone, ClientGroup,
// BlockList) into the runtime's own configuration types. It is read-only: the
// runtime watches the cluster and never writes to it.
//
// Objects are decoded from unstructured data into the plain structs below, so
// there is no generated clientset or deepcopy code to keep in step with the
// CRD manifests in deploy/k8s/crds.
package kubesource
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
)
// APIGroup and Version name the custom resources.
const (
APIGroup = "fancydns.k6n.net"
Version = "v1alpha1"
)
// The resources the runtime watches.
var (
ZoneGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "dnszones"}
GroupGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "clientgroups"}
ListGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "blocklists"}
OverrideGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "addressoverrides"}
RuleGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "domainrules"}
allGVRs = []schema.GroupVersionResource{ZoneGVR, GroupGVR, ListGVR, OverrideGVR, RuleGVR}
)
// DNSZoneSpec declares one authoritative zone. The zone's name is spec.zone,
// not metadata.name, because a DNS name is not always a legal object name.
type DNSZoneSpec struct {
Zone string `json:"zone"`
// TTL is the zone's $TTL and SOA minimum; 0 means the writer's default.
TTL uint32 `json:"ttl,omitempty"`
// SOA is the SOA rdata ("ns1.example.com. hostmaster.example.com. ..."),
// or empty to synthesise one from the first apex NS record.
SOA string `json:"soa,omitempty"`
Records []ZoneRecord `json:"records,omitempty"`
}
// ZoneRecord is one resource record. Value is presentation-format RDATA.
type ZoneRecord struct {
Name string `json:"name"`
Type string `json:"type"`
TTL uint32 `json:"ttl,omitempty"`
Value string `json:"value"`
}
// ClientGroupSpec mirrors config.Group. The group's name is metadata.name.
// Groups are evaluated first-match-wins, so Priority orders them: lower runs
// first, ties break on name.
type ClientGroupSpec struct {
Priority int32 `json:"priority,omitempty"`
Comment string `json:"comment,omitempty"`
Match config.Match `json:"match"`
Policy string `json:"policy"`
AllowDomains []string `json:"allow_domains,omitempty"`
DenyDomains []string `json:"deny_domains,omitempty"`
SkipBlocklist bool `json:"skip_blocklist,omitempty"`
SkipCache bool `json:"skip_cache,omitempty"`
RateLimit int `json:"rate_limit,omitempty"`
MaxConcurrent int `json:"max_concurrent,omitempty"`
LogQueries bool `json:"log_queries,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
}
// BlockListSpec mirrors config.ListSource for a remote list. There is no path
// field on purpose: a namespace-scoped object must not be able to name a file
// the runtime reads or writes. file:// URLs are refused for the same reason.
type BlockListSpec struct {
URL string `json:"url"`
Format string `json:"format,omitempty"`
Mirror []string `json:"mirror,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Refresh string `json:"refresh,omitempty"`
Timeout string `json:"timeout,omitempty"`
MaxBytes int64 `json:"max_bytes,omitempty"`
Categories []string `json:"categories,omitempty"`
Comment string `json:"comment,omitempty"`
InsecureSkipTLSVerify bool `json:"insecure_skip_tls_verify,omitempty"`
}
// AddressOverrideSpec answers a name, and everything under it, with fixed
// addresses instead of asking upstream: "tornberg.me -> 10.10.3.10" for split
// horizon DNS. It is what an RPZ "name A address" line does, one object per
// name.
type AddressOverrideSpec struct {
Domain string `json:"domain"`
// Addresses are IPv4 and IPv6 literals; each becomes an A or AAAA answer.
Addresses []string `json:"addresses"`
}
// DomainRuleSpec is an inline allow or block rule list, the objects behind the
// console's Domains page. A name may carry a wildcard prefix: "*.example.com"
// is the subdomains, "**.example.com" the name and its subdomains.
type DomainRuleSpec struct {
// Action is "block" or "allow". An allow rule rescues a name that any
// blocklist would block.
Action string `json:"action"`
Names []string `json:"names"`
Comment string `json:"comment,omitempty"`
}