Implement MCNS v1: custom Go DNS server replacing CoreDNS
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>
This commit is contained in:
278
cmd/mcns/main.go
Normal file
278
cmd/mcns/main.go
Normal file
@@ -0,0 +1,278 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user