Add DNS filtering with dnsmasq address=/ directives for wildcard blocking
Adds Pi-hole-style DNS filtering using dnsmasq's native address=/ directives, which block domains and all subdomains. Users subscribe to blocklists by URL or upload files, with suggested lists from Hagezi, Steven Black, and OISD. Background runner refreshes lists on a configurable interval (default 24h).
This commit is contained in:
115
internal/dnsfilter/runner.go
Normal file
115
internal/dnsfilter/runner.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Runner manages the background blocklist update goroutine.
|
||||
type Runner struct {
|
||||
mu sync.RWMutex
|
||||
manager *Manager
|
||||
cancel context.CancelFunc
|
||||
trigger chan struct{}
|
||||
running bool
|
||||
onUpdate func() // called after lists are downloaded and compiled
|
||||
}
|
||||
|
||||
// NewRunner creates a new DNS filter update runner.
|
||||
func NewRunner(manager *Manager, onUpdate func()) *Runner {
|
||||
return &Runner{
|
||||
manager: manager,
|
||||
trigger: make(chan struct{}, 1),
|
||||
onUpdate: onUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
// Start launches the background update goroutine.
|
||||
// Safe to call multiple times — cancels the previous run first.
|
||||
func (r *Runner) Start(parentCtx context.Context, intervalHours int) {
|
||||
r.mu.Lock()
|
||||
if r.cancel != nil {
|
||||
r.cancel()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
r.cancel = cancel
|
||||
r.running = true
|
||||
r.mu.Unlock()
|
||||
|
||||
if intervalHours <= 0 {
|
||||
intervalHours = 24
|
||||
}
|
||||
|
||||
go r.run(ctx, intervalHours)
|
||||
}
|
||||
|
||||
// Stop cancels the running goroutine.
|
||||
func (r *Runner) Stop() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.cancel != nil {
|
||||
r.cancel()
|
||||
r.cancel = nil
|
||||
}
|
||||
r.running = false
|
||||
}
|
||||
|
||||
// IsRunning returns whether the runner is active.
|
||||
func (r *Runner) IsRunning() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.running
|
||||
}
|
||||
|
||||
// Trigger requests an immediate update (non-blocking).
|
||||
func (r *Runner) Trigger() {
|
||||
select {
|
||||
case r.trigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) run(ctx context.Context, intervalHours int) {
|
||||
interval := time.Duration(intervalHours) * time.Hour
|
||||
|
||||
slog.Info("dns-filter: runner started", "interval", interval)
|
||||
|
||||
// Immediate update on start
|
||||
r.updateAndCompile(ctx)
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
r.mu.Lock()
|
||||
r.running = false
|
||||
r.mu.Unlock()
|
||||
slog.Info("dns-filter: runner stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
r.updateAndCompile(ctx)
|
||||
case <-r.trigger:
|
||||
r.updateAndCompile(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) updateAndCompile(ctx context.Context) {
|
||||
if err := r.manager.UpdateLists(ctx); err != nil {
|
||||
slog.Error("dns-filter: update failed", "error", err)
|
||||
}
|
||||
|
||||
if _, err := r.manager.Compile(); err != nil {
|
||||
slog.Error("dns-filter: compile failed", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.onUpdate != nil {
|
||||
r.onUpdate()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user