Add SQLite persistence and write-through gRPC mutations

Database (internal/db) stores listeners, routes, and firewall rules with
WAL mode, foreign keys, and idempotent migrations. First run seeds from
TOML config; subsequent runs load from DB as source of truth.

gRPC admin API now writes to the database before updating in-memory state
(write-through cache pattern). Adds snapshot command for VACUUM INTO
backups. Refactors firewall.New to accept raw rule slices instead of
config struct for flexibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 03:07:30 -07:00
parent d63859c28f
commit 9cba3241e8
20 changed files with 1148 additions and 135 deletions

View File

@@ -19,6 +19,7 @@ import (
// ListenerState holds the mutable state for a single proxy listener.
type ListenerState struct {
ID int64 // database primary key
Addr string
routes map[string]string // lowercase hostname → backend addr
mu sync.RWMutex
@@ -75,6 +76,13 @@ func (ls *ListenerState) lookupRoute(hostname string) (string, bool) {
return backend, ok
}
// ListenerData holds the data needed to construct a ListenerState.
type ListenerData struct {
ID int64
Addr string
Routes map[string]string // lowercase hostname → backend
}
// Server is the mc-proxy server. It manages listeners, firewall evaluation,
// SNI-based routing, and bidirectional proxying.
type Server struct {
@@ -82,27 +90,19 @@ type Server struct {
fw *firewall.Firewall
listeners []*ListenerState
logger *slog.Logger
wg sync.WaitGroup // tracks active connections
wg sync.WaitGroup
startedAt time.Time
version string
}
// New creates a Server from the given configuration.
func New(cfg *config.Config, logger *slog.Logger, version string) (*Server, error) {
fw, err := firewall.New(cfg.Firewall)
if err != nil {
return nil, fmt.Errorf("initializing firewall: %w", err)
}
// New creates a Server from pre-loaded data.
func New(cfg *config.Config, fw *firewall.Firewall, listenerData []ListenerData, logger *slog.Logger, version string) *Server {
var listeners []*ListenerState
for _, lcfg := range cfg.Listeners {
routes := make(map[string]string, len(lcfg.Routes))
for _, r := range lcfg.Routes {
routes[strings.ToLower(r.Hostname)] = r.Backend
}
for _, ld := range listenerData {
listeners = append(listeners, &ListenerState{
Addr: lcfg.Addr,
routes: routes,
ID: ld.ID,
Addr: ld.Addr,
routes: ld.Routes,
})
}
@@ -112,7 +112,7 @@ func New(cfg *config.Config, logger *slog.Logger, version string) (*Server, erro
listeners: listeners,
logger: logger,
version: version,
}, nil
}
}
// Firewall returns the server's firewall for use by the gRPC admin API.
@@ -162,23 +162,19 @@ func (s *Server) Run(ctx context.Context) error {
netListeners = append(netListeners, ln)
}
// Start accept loops.
for i, ln := range netListeners {
ln := ln
ls := s.listeners[i]
go s.serve(ctx, ln, ls)
}
// Block until shutdown signal.
<-ctx.Done()
s.logger.Info("shutting down")
// Stop accepting new connections.
for _, ln := range netListeners {
ln.Close()
}
// Wait for in-flight connections with a timeout.
done := make(chan struct{})
go func() {
s.wg.Wait()