L4 passthrough: cache l4_routes in memory instead of querying DB on every HTTPS connection #4

Closed
opened 2026-07-22 14:57:42 +00:00 by james.oates · 1 comment
Owner

Found during the final whole-branch review of the L4 SNI Passthrough (Phase A) feature (#3-adjacent work, Obsidian plan 2026-07-14-l4-sni-passthrough-phase-a.md).

Problem

handle_connection (src/l4.rs) calls db.list_l4_routes() on every SNI-bearing HTTPS connection through the shared accept loop (main.rs's HTTPS accept loop) — including deployments with zero L4 routes registered. Since browsers always send SNI, this means every normal HTTPS connection (not just L4 passthrough traffic) now triggers a SQLite SELECT ... FROM l4_routes before falling through to normal TLS termination.

This adds a per-connection DB round-trip and connection-pool contention (shared with request logging, health checks, cert operations) to all existing HTTPS traffic.

Why it matters

At current homelab scale this is not urgent, but it's a real scalability regression on the shared, universal HTTPS path — not scoped to L4 traffic.

Suggested fix

Cache routes in an Arc<RwLock<Vec<L4Route>>> (or similar), refreshed on registration/deregistration/update and on config-file reload — mirroring the existing SniResolver pattern (src/tls.rs) which already solves the identical "hot-path-read, occasionally-mutated" problem for TLS certs. Alternatively, at minimum, a fast-path that skips the query entirely when the table is known-empty.

Context

Not a correctness bug — behavior is currently correct, this is purely a performance/scalability concern. Reviewer's verdict: does not block merge, logged as a follow-up.

Found during the final whole-branch review of the L4 SNI Passthrough (Phase A) feature (#3-adjacent work, Obsidian plan `2026-07-14-l4-sni-passthrough-phase-a.md`). ## Problem `handle_connection` (`src/l4.rs`) calls `db.list_l4_routes()` on **every** SNI-bearing HTTPS connection through the shared accept loop (`main.rs`'s HTTPS accept loop) — including deployments with zero L4 routes registered. Since browsers always send SNI, this means every normal HTTPS connection (not just L4 passthrough traffic) now triggers a SQLite `SELECT ... FROM l4_routes` before falling through to normal TLS termination. This adds a per-connection DB round-trip and connection-pool contention (shared with request logging, health checks, cert operations) to *all* existing HTTPS traffic. ## Why it matters At current homelab scale this is not urgent, but it's a real scalability regression on the shared, universal HTTPS path — not scoped to L4 traffic. ## Suggested fix Cache routes in an `Arc<RwLock<Vec<L4Route>>>` (or similar), refreshed on registration/deregistration/update and on config-file reload — mirroring the existing `SniResolver` pattern (`src/tls.rs`) which already solves the identical "hot-path-read, occasionally-mutated" problem for TLS certs. Alternatively, at minimum, a fast-path that skips the query entirely when the table is known-empty. ## Context Not a correctness bug — behavior is currently correct, this is purely a performance/scalability concern. Reviewer's verdict: does not block merge, logged as a follow-up.
Author
Owner

Resolved in b18bbf4.

Every terminating HTTPS connection was calling db.list_l4_routes() during the SNI peek to decide passthrough routing — a SQLite round-trip on the per-connection hot path. That's now backed by an in-memory L4RouteCache (RwLock<Arc<Vec<L4Route>>>, atomic Arc-swap on refresh, following the SniResolver pattern). The accept loop reads a lock-free snapshot(); no DB query per connection.

The cache is refreshed at every point routes change, so it never goes stale:

  • startup seed (after TOML L4 routes are upserted)
  • the register / update / deregister L4 route API handlers
  • the config-file watcher reload

Tests: a discriminating test registers a route in the DB but leaves the cache empty — the connection falls through to Terminate instead of tunneling, proving the hot path reads the cache and not the DB — plus an L4RouteCache refresh/snapshot unit test. Full suite green (150 passing), clippy and fmt clean.

Resolved in b18bbf4. Every terminating HTTPS connection was calling `db.list_l4_routes()` during the SNI peek to decide passthrough routing — a SQLite round-trip on the per-connection hot path. That's now backed by an in-memory `L4RouteCache` (`RwLock<Arc<Vec<L4Route>>>`, atomic Arc-swap on refresh, following the `SniResolver` pattern). The accept loop reads a lock-free `snapshot()`; no DB query per connection. The cache is refreshed at every point routes change, so it never goes stale: - startup seed (after TOML L4 routes are upserted) - the `register` / `update` / `deregister` L4 route API handlers - the config-file watcher reload Tests: a discriminating test registers a route in the DB but leaves the cache empty — the connection falls through to `Terminate` instead of tunneling, proving the hot path reads the cache and not the DB — plus an `L4RouteCache` refresh/snapshot unit test. Full suite green (150 passing), clippy and fmt clean.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#4
No description provided.