Add L7/PROXY protocol data model, config, and architecture docs
Extend the config, database schema, and server internals to support per-route L4/L7 mode selection and PROXY protocol fields. This is the foundation for L7 HTTP/2 reverse proxying and multi-hop PROXY protocol support described in the updated ARCHITECTURE.md. Config: Listener gains ProxyProtocol; Route gains Mode, TLSCert, TLSKey, BackendTLS, SendProxyProtocol. L7 routes validated at load time (cert/key pair must exist and parse). Mode defaults to "l4". DB: Migration v2 adds columns to listeners and routes tables. CRUD and seeding updated to persist all new fields. Server: RouteInfo replaces bare backend string in route lookup. handleConn dispatches on route.Mode (L7 path stubbed with error). ListenerState and ListenerData carry ProxyProtocol flag. All existing L4 tests pass unchanged. New tests cover migration v2, L7 field persistence, config validation for mode/cert/key, and proxy_protocol flag round-tripping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ func TestIsEmpty(t *testing.T) {
|
||||
t.Fatal("expected empty database")
|
||||
}
|
||||
|
||||
if _, err := store.CreateListener(":443"); err != nil {
|
||||
if _, err := store.CreateListener(":443", false); err != nil {
|
||||
t.Fatalf("create listener: %v", err)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestIsEmpty(t *testing.T) {
|
||||
func TestListenerCRUD(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
id, err := store.CreateListener(":443")
|
||||
id, err := store.CreateListener(":443", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
@@ -75,6 +75,9 @@ func TestListenerCRUD(t *testing.T) {
|
||||
if listeners[0].Addr != ":443" {
|
||||
t.Fatalf("got addr %q, want %q", listeners[0].Addr, ":443")
|
||||
}
|
||||
if listeners[0].ProxyProtocol {
|
||||
t.Fatal("expected proxy_protocol = false")
|
||||
}
|
||||
|
||||
l, err := store.GetListenerByAddr(":443")
|
||||
if err != nil {
|
||||
@@ -97,13 +100,33 @@ func TestListenerCRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerProxyProtocol(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
id, err := store.CreateListener(":443", true)
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
l, err := store.GetListenerByAddr(":443")
|
||||
if err != nil {
|
||||
t.Fatalf("get by addr: %v", err)
|
||||
}
|
||||
if l.ID != id {
|
||||
t.Fatalf("got ID %d, want %d", l.ID, id)
|
||||
}
|
||||
if !l.ProxyProtocol {
|
||||
t.Fatal("expected proxy_protocol = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerDuplicateAddr(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
if _, err := store.CreateListener(":443"); err != nil {
|
||||
if _, err := store.CreateListener(":443", false); err != nil {
|
||||
t.Fatalf("first create: %v", err)
|
||||
}
|
||||
if _, err := store.CreateListener(":443"); err == nil {
|
||||
if _, err := store.CreateListener(":443", false); err == nil {
|
||||
t.Fatal("expected error for duplicate addr")
|
||||
}
|
||||
}
|
||||
@@ -111,12 +134,12 @@ func TestListenerDuplicateAddr(t *testing.T) {
|
||||
func TestRouteCRUD(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
listenerID, err := store.CreateListener(":443")
|
||||
listenerID, err := store.CreateListener(":443", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create listener: %v", err)
|
||||
}
|
||||
|
||||
routeID, err := store.CreateRoute(listenerID, "example.com", "127.0.0.1:8443")
|
||||
routeID, err := store.CreateRoute(listenerID, "example.com", "127.0.0.1:8443", "l4", "", "", false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("create route: %v", err)
|
||||
}
|
||||
@@ -134,6 +157,9 @@ func TestRouteCRUD(t *testing.T) {
|
||||
if routes[0].Hostname != "example.com" {
|
||||
t.Fatalf("got hostname %q, want %q", routes[0].Hostname, "example.com")
|
||||
}
|
||||
if routes[0].Mode != "l4" {
|
||||
t.Fatalf("got mode %q, want %q", routes[0].Mode, "l4")
|
||||
}
|
||||
|
||||
if err := store.DeleteRoute(listenerID, "example.com"); err != nil {
|
||||
t.Fatalf("delete route: %v", err)
|
||||
@@ -148,14 +174,51 @@ func TestRouteCRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteL7Fields(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
listenerID, _ := store.CreateListener(":443", false)
|
||||
|
||||
_, err := store.CreateRoute(listenerID, "api.example.com", "127.0.0.1:8080", "l7",
|
||||
"/certs/api.crt", "/certs/api.key", false, true)
|
||||
if err != nil {
|
||||
t.Fatalf("create L7 route: %v", err)
|
||||
}
|
||||
|
||||
routes, err := store.ListRoutes(listenerID)
|
||||
if err != nil {
|
||||
t.Fatalf("list routes: %v", err)
|
||||
}
|
||||
if len(routes) != 1 {
|
||||
t.Fatalf("got %d routes, want 1", len(routes))
|
||||
}
|
||||
|
||||
r := routes[0]
|
||||
if r.Mode != "l7" {
|
||||
t.Fatalf("mode = %q, want %q", r.Mode, "l7")
|
||||
}
|
||||
if r.TLSCert != "/certs/api.crt" {
|
||||
t.Fatalf("tls_cert = %q, want %q", r.TLSCert, "/certs/api.crt")
|
||||
}
|
||||
if r.TLSKey != "/certs/api.key" {
|
||||
t.Fatalf("tls_key = %q, want %q", r.TLSKey, "/certs/api.key")
|
||||
}
|
||||
if r.BackendTLS {
|
||||
t.Fatal("expected backend_tls = false")
|
||||
}
|
||||
if !r.SendProxyProtocol {
|
||||
t.Fatal("expected send_proxy_protocol = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteDuplicateHostname(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
listenerID, _ := store.CreateListener(":443")
|
||||
if _, err := store.CreateRoute(listenerID, "example.com", "127.0.0.1:8443"); err != nil {
|
||||
listenerID, _ := store.CreateListener(":443", false)
|
||||
if _, err := store.CreateRoute(listenerID, "example.com", "127.0.0.1:8443", "l4", "", "", false, false); err != nil {
|
||||
t.Fatalf("first create: %v", err)
|
||||
}
|
||||
if _, err := store.CreateRoute(listenerID, "example.com", "127.0.0.1:9443"); err == nil {
|
||||
if _, err := store.CreateRoute(listenerID, "example.com", "127.0.0.1:9443", "l4", "", "", false, false); err == nil {
|
||||
t.Fatal("expected error for duplicate hostname on same listener")
|
||||
}
|
||||
}
|
||||
@@ -163,9 +226,9 @@ func TestRouteDuplicateHostname(t *testing.T) {
|
||||
func TestRouteCascadeDelete(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
listenerID, _ := store.CreateListener(":443")
|
||||
store.CreateRoute(listenerID, "a.example.com", "127.0.0.1:8443")
|
||||
store.CreateRoute(listenerID, "b.example.com", "127.0.0.1:9443")
|
||||
listenerID, _ := store.CreateListener(":443", false)
|
||||
store.CreateRoute(listenerID, "a.example.com", "127.0.0.1:8443", "l4", "", "", false, false)
|
||||
store.CreateRoute(listenerID, "b.example.com", "127.0.0.1:9443", "l4", "", "", false, false)
|
||||
|
||||
if err := store.DeleteListener(listenerID); err != nil {
|
||||
t.Fatalf("delete listener: %v", err)
|
||||
@@ -237,14 +300,15 @@ func TestSeed(t *testing.T) {
|
||||
{
|
||||
Addr: ":443",
|
||||
Routes: []config.Route{
|
||||
{Hostname: "a.example.com", Backend: "127.0.0.1:8443"},
|
||||
{Hostname: "a.example.com", Backend: "127.0.0.1:8443", Mode: "l4"},
|
||||
{Hostname: "b.example.com", Backend: "127.0.0.1:9443"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Addr: ":8443",
|
||||
Addr: ":8443",
|
||||
ProxyProtocol: true,
|
||||
Routes: []config.Route{
|
||||
{Hostname: "c.example.com", Backend: "127.0.0.1:18443"},
|
||||
{Hostname: "c.example.com", Backend: "127.0.0.1:18443", Mode: "l4", SendProxyProtocol: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -266,6 +330,9 @@ func TestSeed(t *testing.T) {
|
||||
if len(dbListeners) != 2 {
|
||||
t.Fatalf("got %d listeners, want 2", len(dbListeners))
|
||||
}
|
||||
if !dbListeners[1].ProxyProtocol {
|
||||
t.Fatal("expected listener 2 proxy_protocol = true")
|
||||
}
|
||||
|
||||
routes, err := store.ListRoutes(dbListeners[0].ID)
|
||||
if err != nil {
|
||||
@@ -275,6 +342,25 @@ func TestSeed(t *testing.T) {
|
||||
t.Fatalf("got %d routes for listener 0, want 2", len(routes))
|
||||
}
|
||||
|
||||
// Verify mode defaults to "l4" even when empty in config.
|
||||
for _, r := range routes {
|
||||
if r.Mode != "l4" {
|
||||
t.Fatalf("route %q mode = %q, want %q", r.Hostname, r.Mode, "l4")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify send_proxy_protocol on listener 2's route.
|
||||
routes2, err := store.ListRoutes(dbListeners[1].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("list routes listener 2: %v", err)
|
||||
}
|
||||
if len(routes2) != 1 {
|
||||
t.Fatalf("got %d routes for listener 1, want 1", len(routes2))
|
||||
}
|
||||
if !routes2[0].SendProxyProtocol {
|
||||
t.Fatal("expected send_proxy_protocol = true on listener 2 route")
|
||||
}
|
||||
|
||||
rules, err := store.ListFirewallRules()
|
||||
if err != nil {
|
||||
t.Fatalf("list firewall rules: %v", err)
|
||||
@@ -287,7 +373,7 @@ func TestSeed(t *testing.T) {
|
||||
func TestSnapshot(t *testing.T) {
|
||||
store := openTestDB(t)
|
||||
|
||||
store.CreateListener(":443")
|
||||
store.CreateListener(":443", false)
|
||||
|
||||
dest := filepath.Join(t.TempDir(), "backup.db")
|
||||
if err := store.Snapshot(dest); err != nil {
|
||||
@@ -329,3 +415,57 @@ func TestDeleteNonexistent(t *testing.T) {
|
||||
t.Fatal("expected error deleting nonexistent firewall rule")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrationV2Upgrade verifies that migration v2 adds new columns
|
||||
// to an existing v1 database without data loss.
|
||||
func TestMigrationV2Upgrade(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := Open(filepath.Join(dir, "test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { store.Close() })
|
||||
|
||||
// Run full migrations (v1 + v2).
|
||||
if err := store.Migrate(); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
// Insert a listener and route with defaults to verify new columns work.
|
||||
lid, err := store.CreateListener(":443", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create listener: %v", err)
|
||||
}
|
||||
|
||||
_, err = store.CreateRoute(lid, "test.example.com", "127.0.0.1:8443", "l4", "", "", false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("create route: %v", err)
|
||||
}
|
||||
|
||||
// Read back and verify defaults.
|
||||
routes, err := store.ListRoutes(lid)
|
||||
if err != nil {
|
||||
t.Fatalf("list routes: %v", err)
|
||||
}
|
||||
if len(routes) != 1 {
|
||||
t.Fatalf("got %d routes, want 1", len(routes))
|
||||
}
|
||||
r := routes[0]
|
||||
if r.Mode != "l4" {
|
||||
t.Fatalf("mode = %q, want %q", r.Mode, "l4")
|
||||
}
|
||||
if r.TLSCert != "" || r.TLSKey != "" {
|
||||
t.Fatalf("expected empty cert/key, got cert=%q key=%q", r.TLSCert, r.TLSKey)
|
||||
}
|
||||
if r.BackendTLS || r.SendProxyProtocol {
|
||||
t.Fatal("expected false for backend_tls and send_proxy_protocol")
|
||||
}
|
||||
|
||||
listeners, err := store.ListListeners()
|
||||
if err != nil {
|
||||
t.Fatalf("list listeners: %v", err)
|
||||
}
|
||||
if listeners[0].ProxyProtocol {
|
||||
t.Fatal("expected proxy_protocol = false")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user