Replace the CoreDNS precursor with a purpose-built authoritative DNS server. Zones and records (A, AAAA, CNAME) are stored in SQLite and managed via synchronized gRPC + REST APIs authenticated through MCIAS. Non-authoritative queries are forwarded to upstream resolvers with in-memory caching. Key components: - DNS server (miekg/dns) with authoritative zone handling and forwarding - gRPC + REST management APIs with MCIAS auth (mcdsl integration) - SQLite storage with CNAME exclusivity enforcement and auto SOA serials - 30 tests covering database CRUD, DNS resolution, and caching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
279 lines
6.7 KiB
Go
279 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
|
|
mcdsldb "git.wntrmute.dev/kyle/mcdsl/db"
|
|
|
|
"git.wntrmute.dev/kyle/mcns/internal/config"
|
|
"git.wntrmute.dev/kyle/mcns/internal/db"
|
|
mcnsdns "git.wntrmute.dev/kyle/mcns/internal/dns"
|
|
"git.wntrmute.dev/kyle/mcns/internal/grpcserver"
|
|
"git.wntrmute.dev/kyle/mcns/internal/server"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
root := &cobra.Command{
|
|
Use: "mcns",
|
|
Short: "Metacircular Networking Service",
|
|
Version: version,
|
|
}
|
|
|
|
root.AddCommand(serverCmd())
|
|
root.AddCommand(statusCmd())
|
|
root.AddCommand(snapshotCmd())
|
|
|
|
if err := root.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func serverCmd() *cobra.Command {
|
|
var configPath string
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "server",
|
|
Short: "Start the DNS and API servers",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
return runServer(configPath)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&configPath, "config", "c", "mcns.toml", "path to configuration file")
|
|
return cmd
|
|
}
|
|
|
|
func runServer(configPath string) error {
|
|
cfg, err := config.Load(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: parseLogLevel(cfg.Log.Level),
|
|
}))
|
|
|
|
// Open and migrate the database.
|
|
database, err := db.Open(cfg.Database.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("open database: %w", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
if err := database.Migrate(); err != nil {
|
|
return fmt.Errorf("migrate database: %w", err)
|
|
}
|
|
|
|
// Create auth client for MCIAS integration.
|
|
authClient, err := mcdslauth.New(mcdslauth.Config{
|
|
ServerURL: cfg.MCIAS.ServerURL,
|
|
CACert: cfg.MCIAS.CACert,
|
|
ServiceName: cfg.MCIAS.ServiceName,
|
|
Tags: cfg.MCIAS.Tags,
|
|
}, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("create auth client: %w", err)
|
|
}
|
|
|
|
// Start DNS server.
|
|
dnsServer := mcnsdns.New(database, cfg.DNS.Upstreams, logger)
|
|
|
|
// Build REST API router.
|
|
router := server.NewRouter(server.Deps{
|
|
DB: database,
|
|
Auth: authClient,
|
|
Logger: logger,
|
|
})
|
|
|
|
// TLS configuration.
|
|
cert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
|
|
if err != nil {
|
|
return fmt.Errorf("load TLS cert: %w", err)
|
|
}
|
|
tlsCfg := &tls.Config{
|
|
MinVersion: tls.VersionTLS13,
|
|
Certificates: []tls.Certificate{cert},
|
|
}
|
|
|
|
// HTTP server.
|
|
httpServer := &http.Server{
|
|
Addr: cfg.Server.ListenAddr,
|
|
Handler: router,
|
|
TLSConfig: tlsCfg,
|
|
ReadTimeout: cfg.Server.ReadTimeout.Duration,
|
|
WriteTimeout: cfg.Server.WriteTimeout.Duration,
|
|
IdleTimeout: cfg.Server.IdleTimeout.Duration,
|
|
}
|
|
|
|
// Start gRPC server if configured.
|
|
var grpcSrv *grpcserver.Server
|
|
var grpcLis net.Listener
|
|
if cfg.Server.GRPCAddr != "" {
|
|
grpcSrv, err = grpcserver.New(cfg.Server.TLSCert, cfg.Server.TLSKey, grpcserver.Deps{
|
|
DB: database,
|
|
Authenticator: authClient,
|
|
}, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("create gRPC server: %w", err)
|
|
}
|
|
grpcLis, err = net.Listen("tcp", cfg.Server.GRPCAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("listen gRPC on %s: %w", cfg.Server.GRPCAddr, err)
|
|
}
|
|
}
|
|
|
|
// Graceful shutdown on SIGINT/SIGTERM.
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
errCh := make(chan error, 3)
|
|
|
|
// Start DNS server.
|
|
go func() {
|
|
errCh <- dnsServer.ListenAndServe(cfg.DNS.ListenAddr)
|
|
}()
|
|
|
|
// Start gRPC server.
|
|
if grpcSrv != nil {
|
|
go func() {
|
|
logger.Info("gRPC server listening", "addr", grpcLis.Addr())
|
|
errCh <- grpcSrv.Serve(grpcLis)
|
|
}()
|
|
}
|
|
|
|
// Start HTTP server.
|
|
go func() {
|
|
logger.Info("mcns starting",
|
|
"version", version,
|
|
"addr", cfg.Server.ListenAddr,
|
|
"dns_addr", cfg.DNS.ListenAddr,
|
|
)
|
|
errCh <- httpServer.ListenAndServeTLS("", "")
|
|
}()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
return fmt.Errorf("server error: %w", err)
|
|
case <-ctx.Done():
|
|
logger.Info("shutting down")
|
|
dnsServer.Shutdown()
|
|
if grpcSrv != nil {
|
|
grpcSrv.GracefulStop()
|
|
}
|
|
shutdownTimeout := 30 * time.Second
|
|
if cfg.Server.ShutdownTimeout.Duration > 0 {
|
|
shutdownTimeout = cfg.Server.ShutdownTimeout.Duration
|
|
}
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
defer cancel()
|
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
|
return fmt.Errorf("shutdown: %w", err)
|
|
}
|
|
logger.Info("mcns stopped")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func statusCmd() *cobra.Command {
|
|
var addr, caCert string
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "status",
|
|
Short: "Check MCNS health",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13}
|
|
if caCert != "" {
|
|
pemData, err := os.ReadFile(caCert)
|
|
if err != nil {
|
|
return fmt.Errorf("read CA cert: %w", err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pemData) {
|
|
return fmt.Errorf("no valid certificates in %s", caCert)
|
|
}
|
|
tlsCfg.RootCAs = pool
|
|
}
|
|
client := &http.Client{
|
|
Transport: &http.Transport{TLSClientConfig: tlsCfg},
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
resp, err := client.Get(addr + "/v1/health")
|
|
if err != nil {
|
|
return fmt.Errorf("health check: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("health check: status %d", resp.StatusCode)
|
|
}
|
|
fmt.Println("ok")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&addr, "addr", "https://localhost:8443", "server address")
|
|
cmd.Flags().StringVar(&caCert, "ca-cert", "", "CA certificate for TLS verification")
|
|
return cmd
|
|
}
|
|
|
|
func snapshotCmd() *cobra.Command {
|
|
var configPath string
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "snapshot",
|
|
Short: "Database backup via VACUUM INTO",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
cfg, err := config.Load(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load config: %w", err)
|
|
}
|
|
database, err := db.Open(cfg.Database.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("open database: %w", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
backupDir := filepath.Join(filepath.Dir(cfg.Database.Path), "backups")
|
|
snapName := fmt.Sprintf("mcns-%s.db", time.Now().Format("20060102-150405"))
|
|
snapPath := filepath.Join(backupDir, snapName)
|
|
|
|
if err := mcdsldb.Snapshot(database.DB, snapPath); err != nil {
|
|
return fmt.Errorf("snapshot: %w", err)
|
|
}
|
|
fmt.Printf("Snapshot saved to %s\n", snapPath)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&configPath, "config", "c", "mcns.toml", "path to configuration file")
|
|
return cmd
|
|
}
|
|
|
|
func parseLogLevel(s string) slog.Level {
|
|
switch s {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|