overrides
build / build-push (push) Successful in 29s

This commit is contained in:
mats committed 2026-09-28 16:25:55 +02:00
1 parent 98b65945f6
commit 0b6dcbd547
13 files changed
+417 -68

No files matched your search

+25 -6
View File
@@ -56,12 +56,13 @@ type App struct {
cfg *config.Store
ver string
blocks *blocklist.Manager
cache *cache.Manager
cstore *cache.Store
logger *qlog.Logger
aclH *acl.Holder
local *localzone.Holder
blocks *blocklist.Manager
overrideMu sync.Mutex
cache *cache.Manager
cstore *cache.Store
logger *qlog.Logger
aclH *acl.Holder
local *localzone.Holder
// zones is the Kubernetes-sourced zone client.
zones *kubezones.Client
// managedZones is the console-managed zone directory: the only zone data
@@ -316,6 +317,23 @@ func (a *App) startBlocklist() {
Config: a.cfg,
Logger: a.log,
})
if a.zones != nil {
ctx := context.Background()
content, exists, err := a.zones.Data(ctx, "address-overrides.rpz")
if err == nil && !exists {
if seed, readErr := os.ReadFile("/etc/fancy-dns/rpz/address-overrides.rpz"); readErr == nil {
content = string(seed)
if saveErr := a.zones.SaveData(ctx, "address-overrides.rpz", content); saveErr != nil {
a.log.Warn("seed address overrides failed", "error", saveErr)
}
}
}
if err == nil {
a.blocks.SetManagedRPZ(content)
} else {
a.log.Warn("load address overrides failed", "error", err)
}
}
// Compile once before serving so the first queries are not unprotected.
if _, err := a.blocks.RefreshAll(context.Background()); err != nil {
a.markDegraded("blocklist: " + err.Error())
@@ -576,6 +594,7 @@ func (a *App) startCluster(ctx context.Context, cfg *config.Config) {
// probes and the metrics gauge.
func (a *App) startBackground(ctx context.Context, cfg *config.Config) {
go a.watchLocalZones(ctx)
go a.watchAddressOverrides(ctx)
// Cache snapshots.
if a.cstore != nil {
interval := cfg.Cache.Persist.Interval.D()
+164 -38
View File
@@ -26,47 +26,173 @@ import (
// components. Everything the UI can read or change goes through here.
func (a *App) hooks() api.Hooks {
return api.Hooks{
Store: a.cfg,
Logger: a.log,
Version: a.ver,
StartedAt: a.startedAt,
Dashboard: a.dashboard,
Status: a.status,
Health: a.health,
BlocklistState: a.blocklistState,
BlocklistStats: a.blocklistStats,
RefreshBlocklists: a.refreshBlocklists,
ToggleBlocklist: a.toggleBlocklist,
DomainCheck: a.domainCheck,
Resolve: a.resolveTest,
ListDomains: a.listDomains,
AddDomain: a.addDomain,
DeleteDomain: a.deleteDomain,
ListZones: a.listZones,
AddZoneRecord: a.addZoneRecord,
PutZoneRecord: a.putZoneRecord,
DeleteZoneRecord: a.deleteZoneRecord,
DeleteZone: a.deleteZone,
AddZoneDelegation: a.addZoneDelegation,
DeleteZoneDelegation: a.deleteZoneDelegation,
LocalZones: a.listLocalZones,
SaveLocalZone: a.saveLocalZone,
DeleteLocalZone: a.deleteLocalZone,
GetACL: a.getACL,
PutACL: a.putACL,
GetUpstream: a.getUpstreams,
PutUpstream: a.putUpstream,
SearchLog: a.searchLog,
LogStats: a.logStats,
SubscribeLog: a.subscribeLog,
ClearLog: a.clearLog,
CacheStats: a.cacheStats,
CacheTop: a.cacheTop,
FlushCache: a.flushCache,
Flush: a.flush,
Store: a.cfg,
Logger: a.log,
Version: a.ver,
StartedAt: a.startedAt,
Dashboard: a.dashboard,
Status: a.status,
Health: a.health,
BlocklistState: a.blocklistState,
BlocklistStats: a.blocklistStats,
RefreshBlocklists: a.refreshBlocklists,
ToggleBlocklist: a.toggleBlocklist,
DomainCheck: a.domainCheck,
Resolve: a.resolveTest,
ResolveUpstream: a.resolveTestUpstream,
AddressOverrides: a.listAddressOverrides,
SaveAddressOverride: a.saveAddressOverride,
DeleteAddressOverride: a.deleteAddressOverride,
ListDomains: a.listDomains,
AddDomain: a.addDomain,
DeleteDomain: a.deleteDomain,
ListZones: a.listZones,
AddZoneRecord: a.addZoneRecord,
PutZoneRecord: a.putZoneRecord,
DeleteZoneRecord: a.deleteZoneRecord,
DeleteZone: a.deleteZone,
AddZoneDelegation: a.addZoneDelegation,
DeleteZoneDelegation: a.deleteZoneDelegation,
LocalZones: a.listLocalZones,
SaveLocalZone: a.saveLocalZone,
DeleteLocalZone: a.deleteLocalZone,
GetACL: a.getACL,
PutACL: a.putACL,
GetUpstream: a.getUpstreams,
PutUpstream: a.putUpstream,
SearchLog: a.searchLog,
LogStats: a.logStats,
SubscribeLog: a.subscribeLog,
ClearLog: a.clearLog,
CacheStats: a.cacheStats,
CacheTop: a.cacheTop,
FlushCache: a.flushCache,
Flush: a.flush,
}
}
func (a *App) resolveTestUpstream(ctx context.Context, name, qtype, upstream string) (any, error) {
if a.pool == nil {
return nil, errors.New("upstream pool is not running")
}
up := a.pool.Get(strings.TrimSpace(upstream))
if up == nil {
return nil, fmt.Errorf("unknown upstream %q", upstream)
}
qt := dns.TypeA
if qtype != "" {
var ok bool
qt, ok = dns.StringToType[strings.ToUpper(strings.TrimSpace(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
started := time.Now()
resp, addr, err := a.pool.Exchange(ctx, q, []string{up.Addr()}, 5*time.Second)
if err != nil {
return nil, err
}
out := map[string]any{"name": dns.Fqdn(name), "type": dns.TypeToString[qt], "rcode": "SERVFAIL", "action": "upstream", "duration_ms": time.Since(started).Milliseconds(), "upstream": addr, "group": "", "cache": "bypass", "local": false, "truncated": false, "blocked": false, "block_source": "", "answers": []string{}}
if resp != nil {
out["rcode"] = dns.RcodeToString[resp.Rcode]
ans := []string{}
for _, rr := range resp.Answer {
ans = append(ans, strings.TrimSpace(rr.String()))
}
out["answers"] = ans
}
return out, nil
}
type addressOverride struct {
Domain string `json:"domain"`
Address string `json:"address"`
}
func (a *App) listAddressOverrides(ctx context.Context) (any, error) {
if a.zones == nil {
return map[string]any{"items": []addressOverride{}, "writable": false}, nil
}
raw, ok, err := a.zones.Data(ctx, "address-overrides.rpz")
if err != nil {
return nil, err
}
items := parseAddressOverrides(raw)
return map[string]any{"items": items, "writable": true, "initialized": ok}, nil
}
func parseAddressOverrides(raw string) []addressOverride {
out := []addressOverride{}
for _, line := range strings.Split(raw, "\n") {
f := strings.Fields(line)
if len(f) >= 3 && !strings.HasPrefix(f[0], "$") && (strings.EqualFold(f[1], "A") || strings.EqualFold(f[1], "AAAA")) {
out = append(out, addressOverride{Domain: strings.TrimSuffix(f[0], "."), Address: f[2]})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Domain < out[j].Domain })
return out
}
func (a *App) saveAddressOverride(ctx context.Context, domain, address string) error {
return a.mutateAddressOverride(ctx, domain, address, false)
}
func (a *App) deleteAddressOverride(ctx context.Context, domain string) error {
return a.mutateAddressOverride(ctx, domain, "", true)
}
func (a *App) mutateAddressOverride(ctx context.Context, domain, address string, remove bool) error {
a.overrideMu.Lock()
defer a.overrideMu.Unlock()
if a.zones == nil {
return errors.New("address overrides are writable only in Kubernetes")
}
domain = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
if _, ok := dns.IsDomainName(dns.Fqdn(domain)); !ok || domain == "" {
return errors.New("enter a valid domain name")
}
ip, err := netip.ParseAddr(strings.TrimSpace(address))
if !remove && err != nil {
return errors.New("enter a valid IPv4 or IPv6 address")
}
raw, _, err := a.zones.Data(ctx, "address-overrides.rpz")
if err != nil {
return err
}
items := parseAddressOverrides(raw)
next := make([]addressOverride, 0, len(items)+1)
for _, it := range items {
if it.Domain != domain {
next = append(next, it)
}
}
if !remove {
next = append(next, addressOverride{Domain: domain, Address: ip.String()})
}
sort.Slice(next, func(i, j int) bool { return next[i].Domain < next[j].Domain })
var b strings.Builder
b.WriteString("$TTL 60\n")
for _, it := range next {
typ := "A"
if strings.Contains(it.Address, ":") {
typ = "AAAA"
}
fmt.Fprintf(&b, "%s %s %s\n", it.Domain, typ, it.Address)
}
if err := a.zones.SaveData(ctx, "address-overrides.rpz", b.String()); err != nil {
return err
}
if a.blocks != nil {
a.blocks.SetManagedRPZ(b.String())
if _, err := a.blocks.RefreshAll(ctx); err != nil {
return err
}
}
if a.cache != nil {
a.cache.Flush()
}
return nil
}
func (a *App) listLocalZones(ctx context.Context) ([]api.ZoneFile, error) {
if a.zones == nil {
return nil, fmt.Errorf("local zone editor is available only in Kubernetes")
+35
View File
@@ -0,0 +1,35 @@
package app
import (
"context"
"time"
)
func (a *App) watchAddressOverrides(ctx context.Context) {
if a.zones == nil || a.blocks == nil {
return
}
t := time.NewTicker(2 * time.Second)
defer t.Stop()
var last string
for {
select {
case <-ctx.Done():
return
case <-t.C:
content, ok, err := a.zones.Data(ctx, "address-overrides.rpz")
if err != nil || !ok || content == last {
continue
}
a.overrideMu.Lock()
a.blocks.SetManagedRPZ(content)
if _, err := a.blocks.RefreshAll(ctx); err == nil {
if a.cache != nil {
a.cache.Flush()
}
last = content
}
a.overrideMu.Unlock()
}
}
}