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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user