Files
zeekay d54b8709cb feat(hanzodns): provider-connector layer — native + Cloudflare over per-org KMS token
hanzodns becomes the one DNS control plane: authoritative zones served by its
own CoreDNS store AND third-party providers (Cloudflare) via the org's
KMS-sealed token. Both are first-class and coexist per-zone.

- Provider interface (provider.go) mirroring the platform dns-provider.ts:
  Type/ListZones/ZoneIDForName/ListRecords/Create/Update/DeleteRecord.
- Cloudflare connector (cloudflare.go) ported from cloudflare.ts — CF API v4
  CRUD, token only on the Authorization header, fail-closed, never logged.
- Per-org credential sourcing (kms.go): providerFor reads the sealed token from
  KMS by RELAYING the caller's own validated bearer to the KMS REST API
  (/v1/kms/orgs/{org}/secrets/integrations/cloudflare/api_token). hanzodns holds
  no standing cross-tenant credential — KMS enforces org isolation. org is the
  IAM  claim; auth-method-agnostic (apikey + oauth seal the same coord).
- Zone.provider dispatch (api.go): authoritative -> local store; cloudflare ->
  provider (source of truth) with a local-store mirror so CoreDNS serves it and
  listings stay consistent. OIDC wired into the auth chain so org + bearer reach
  the provider path.
- Durable authoritative zones (store_snapshot.go): opt-in file snapshot via
  HANZO_DNS_STATE_DIR — load on startup, atomic rewrite on every mutation, so a
  restart preserves native zones. Unset keeps the in-memory store.

Tests: CF connector, KMS bearer-relay (incl. foreign-org refusal), the full
create/list/delete dispatch for a Cloudflare zone, the native authoritative
path, and snapshot round-trip across a restart. go test -tags metrics green.
2026-07-18 10:05:09 -07:00

128 lines
2.5 KiB
Go

package hanzodns
import (
"context"
"net"
"net/http"
"os"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/reuseport"
)
var log = clog.NewWithPlugin("hanzodns")
func init() { plugin.Register("hanzodns", setup) }
func setup(c *caddy.Controller) error {
addr, err := parse(c)
if err != nil {
return plugin.Error("hanzodns", err)
}
store := NewStore()
// Durable backing for authoritative zones: when HANZO_DNS_STATE_DIR is set,
// the store loads its last snapshot and persists after every mutation, so a
// pod restart preserves native zones. Unset keeps the pure in-memory store.
if err := EnableSnapshot(store, os.Getenv("HANZO_DNS_STATE_DIR")); err != nil {
return plugin.Error("hanzodns", err)
}
h := &HanzoDNS{
Store: store,
Addr: addr,
}
srv := &apiServer{
addr: addr,
store: store,
}
c.OnStartup(srv.OnStartup)
c.OnRestart(srv.OnShutdown)
c.OnFinalShutdown(srv.OnShutdown)
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
h.Next = next
return h
})
return nil
}
func parse(c *caddy.Controller) (string, error) {
addr := ":8443"
for c.Next() {
args := c.RemainingArgs()
switch len(args) {
case 0:
case 1:
addr = args[0]
if _, _, err := net.SplitHostPort(addr); err != nil {
return "", err
}
default:
return "", c.ArgErr()
}
// No block directives for now.
for c.NextBlock() {
return "", c.ArgErr()
}
}
return addr, nil
}
// apiServer manages the lifecycle of the HTTP API server.
type apiServer struct {
addr string
store *Store
ln net.Listener
srv *http.Server
setup bool
}
const shutdownTimeout = 5 * time.Second
func (s *apiServer) OnStartup() error {
ln, err := reuseport.Listen("tcp", s.addr)
if err != nil {
return err
}
s.ln = ln
s.setup = true
mux := http.NewServeMux()
registerRoutes(mux, s.store)
s.srv = &http.Server{
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Infof("Starting Hanzo DNS API on %s", s.addr)
go func() { s.srv.Serve(s.ln) }()
return nil
}
func (s *apiServer) OnShutdown() error {
if !s.setup {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := s.srv.Shutdown(ctx); err != nil {
log.Errorf("Failed to stop Hanzo DNS API server: %s", err)
}
s.setup = false
return nil
}