Files
vibedns/internal/blockpage/cert.go
T
owenandClaude Sonnet 5 4895c8fd1e 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
2026-08-17 00:24:00 -05:00

104 lines
3.0 KiB
Go

package blockpage
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"sync"
"time"
)
// certCap bounds how many per-hostname certificates are kept in memory. A
// client that churns through SNI names (deliberately or not) could otherwise
// grow the cache without limit; when the cap is hit the whole cache is
// cleared and rebuilt on demand, which just costs a few CPU-cheap regenerations.
const certCap = 20_000
// certStore lazily generates and caches a self-signed TLS certificate for
// each hostname a client connects with over SNI. Signing each leaf with a
// name that matches what was requested means the browser's warning is the
// expected "this certificate is not trusted" one, not also a confusing
// hostname mismatch.
type certStore struct {
mu sync.Mutex
certs map[string]*tls.Certificate
}
func newCertStore() certStore {
return certStore{certs: make(map[string]*tls.Certificate)}
}
// get implements tls.Config.GetCertificate.
func (c *certStore) get(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
name := hello.ServerName
if name == "" {
name = "block-page.invalid"
}
c.mu.Lock()
if cert, ok := c.certs[name]; ok {
c.mu.Unlock()
return cert, nil
}
c.mu.Unlock()
cert, err := generateCert(name)
if err != nil {
return nil, err
}
c.mu.Lock()
if len(c.certs) >= certCap {
c.certs = make(map[string]*tls.Certificate)
}
c.certs[name] = cert
c.mu.Unlock()
return cert, nil
}
// generateCert creates a fresh, self-signed ECDSA leaf certificate for name.
func generateCert(name string) (*tls.Certificate, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate block page certificate key: %w", err)
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 62))
if err != nil {
return nil, fmt.Errorf("generate block page certificate serial: %w", err)
}
tmpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: name, Organization: []string{"VibeDNS Block Page"}},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
if ip := net.ParseIP(name); ip != nil {
tmpl.IPAddresses = []net.IP{ip}
} else {
tmpl.DNSNames = []string{name}
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
return nil, fmt.Errorf("create block page certificate: %w", err)
}
return &tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}, nil
}
// parseLeaf parses the leaf certificate's DER bytes, for tests that need to
// inspect what generateCert produced.
func parseLeaf(cert *tls.Certificate) (*x509.Certificate, error) {
return x509.ParseCertificate(cert.Certificate[0])
}