1 parent
98b65945f6
commit
0b6dcbd547
13 files changed
+417
-68
No files matched your search
+25
-6
@@ -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
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user