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:
@@ -0,0 +1,103 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user