Add a customizable HTTP/HTTPS block page for sinkholed queries

Serves a page explaining why a domain was blocked instead of leaving a
sinkholed client with a dead connection. Binds its own HTTP/HTTPS
listeners with self-signed, per-hostname TLS certs generated on the
fly, re-evaluates the requesting client against the policy engine per
request, and renders an HTML template editable from Settings with a
live preview.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTKpGMQzpfDsvedu1hvSUf
This commit is contained in:
2026-08-17 00:24:00 -05:00
co-authored by Claude Sonnet 5
parent 566a5fd3ed
commit 4895c8fd1e
13 changed files with 948 additions and 16 deletions
+27 -13
View File
@@ -12,6 +12,7 @@ import (
"github.com/owen/vibedns/internal/auditlog"
"github.com/owen/vibedns/internal/auth"
"github.com/owen/vibedns/internal/backup"
"github.com/owen/vibedns/internal/blockpage"
"github.com/owen/vibedns/internal/cache"
"github.com/owen/vibedns/internal/config"
"github.com/owen/vibedns/internal/database"
@@ -30,19 +31,20 @@ const keyCSRFSecret = "security.csrf_key"
// App wires every component together and exposes the operations the
// management interface performs.
type App struct {
Boot config.Bootstrap
DB *database.DB
Runtime *runtimecfg.Manager
Cache *cache.Cache
Resolver *resolver.Resolver
Limiter *ratelimit.Limiter
Metrics *metrics.Metrics
QueryLog *querylog.Logger
Audit *auditlog.Logger
Auth *auth.Authenticator
Backups *backup.Manager
DNS *dnsengine.Server
Log *slog.Logger
Boot config.Bootstrap
DB *database.DB
Runtime *runtimecfg.Manager
Cache *cache.Cache
Resolver *resolver.Resolver
Limiter *ratelimit.Limiter
Metrics *metrics.Metrics
QueryLog *querylog.Logger
Audit *auditlog.Logger
Auth *auth.Authenticator
Backups *backup.Manager
DNS *dnsengine.Server
BlockPage *blockpage.Server
Log *slog.Logger
cancel context.CancelFunc
started time.Time
@@ -88,6 +90,7 @@ func New(ctx context.Context, boot config.Bootstrap, db *database.DB, log *slog.
QueryLog: a.QueryLog,
Log: log,
})
a.BlockPage = blockpage.New(rt, log)
// Every subsystem picks up new settings from the same reload event, so a
// change in the UI takes effect without a restart.
@@ -203,12 +206,23 @@ func (a *App) Start(ctx context.Context) error {
a.cancel()
return err
}
if settings := a.Runtime.Settings(); settings.BlockPage.Enabled {
if err := a.BlockPage.Start(settings.BlockPage.HTTPListen, settings.BlockPage.HTTPSListen); err != nil {
a.cancel()
_ = a.DNS.Shutdown(context.Background())
return err
}
}
return nil
}
// Shutdown stops the DNS listeners and drains the background workers.
func (a *App) Shutdown(ctx context.Context) error {
err := a.DNS.Shutdown(ctx)
if bpErr := a.BlockPage.Shutdown(ctx); bpErr != nil && err == nil {
err = bpErr
}
if a.cancel != nil {
a.cancel()
}