The h2c-only transport (http2.Transport) fails against backends like
Gitea that only speak HTTP/1.1. Switch to standard http.Transport for
non-TLS backends, which handles HTTP/1.1 natively and can upgrade to
h2c if the backend supports it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When backend_tls=true, the h2 transport was verifying the backend's
TLS certificate. This fails when the backend address is an IP (no
IP SANs) or uses a self-signed cert. Backend connections are to
trusted internal services — skip verification. Also change rift
metrics port to 9091 to avoid conflict with exod on 9090.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instrument mc-proxy with prometheus/client_golang. New internal/metrics/
package defines counters, gauges, and histograms for connection totals,
active connections, firewall blocks by reason, backend dial latency,
bytes transferred, L7 HTTP status codes, and L7 policy blocks. Optional
[metrics] config section starts a scrape endpoint. Firewall gains
BlockedWithReason() to report block cause. L7 handler wraps
ResponseWriter to record status codes per hostname.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-route HTTP-level blocking policies for L7 routes. Two rule types:
block_user_agent (substring match against User-Agent, returns 403)
and require_header (named header must be present, returns 403).
Config: L7Policy struct with type/value fields, added as L7Policies
slice on Route. Validated in config (type enum, non-empty value,
warning if set on L4 routes).
DB: Migration 4 creates l7_policies table with route_id FK (cascade
delete), type CHECK constraint, UNIQUE(route_id, type, value). New
l7policies.go with ListL7Policies, CreateL7Policy, DeleteL7Policy,
GetRouteID. Seed updated to persist policies from config.
L7 middleware: PolicyMiddleware in internal/l7/policy.go evaluates
rules in order, returns 403 on first match, no-op if empty. Composed
into the handler chain between context injection and reverse proxy.
Server: L7PolicyRule type on RouteInfo with AddL7Policy/RemoveL7Policy
mutation methods on ListenerState. handleL7 threads policies into
l7.RouteConfig. Startup loads policies per L7 route from DB.
Proto: L7Policy message, repeated l7_policies on Route. Three new
RPCs: ListL7Policies, AddL7Policy, RemoveL7Policy. All follow the
write-through pattern.
Client: L7Policy type, ListL7Policies/AddL7Policy/RemoveL7Policy
methods. CLI: mcproxyctl policies list/add/remove subcommands.
Tests: 6 PolicyMiddleware unit tests (no policies, UA match/no-match,
header present/absent, multiple rules). 4 DB tests (CRUD, cascade,
duplicate, GetRouteID). 3 gRPC tests (add+list, remove, validation).
2 end-to-end L7 tests (UA block, required header with allow/deny).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. DB migration: add CHECK(mode IN ('l4', 'l7')) constraint on the
routes.mode column. ARCHITECTURE.md documented this constraint but
migration v2 omitted it. Enforces mode validity at the database
level in addition to application-level validation.
2. L7 reverse proxy: distinguish timeout errors from connection errors
in the ErrorHandler. Backend timeouts now return HTTP 504 Gateway
Timeout instead of 502. Uses errors.Is(context.DeadlineExceeded)
and net.Error.Timeout() detection. Added isTimeoutError unit tests.
3. Config validation: warn when L4 routes have tls_cert or tls_key set
(they are silently ignored). ARCHITECTURE.md documented this warning
but config.validate() did not emit it. Uses slog.Warn.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New internal/l7 package implements TLS termination and HTTP/2 reverse
proxying for L7 routes. The proxy terminates the client TLS connection
using per-route certificates, then forwards HTTP/2 traffic to backends
over h2c (plaintext HTTP/2) or h2 (re-encrypted TLS).
PrefixConn replays the peeked ClientHello bytes into crypto/tls.Server
so the TLS handshake sees the complete ClientHello despite SNI
extraction having already read it.
Serve() is the L7 entry point: TLS handshake with route certificate,
ALPN negotiation (h2 preferred, HTTP/1.1 fallback), then HTTP reverse
proxy via httputil.ReverseProxy. Backend transport uses h2c by default
(AllowHTTP + plain TCP dial) or h2-over-TLS when backend_tls is set.
Forwarding headers (X-Forwarded-For, X-Forwarded-Proto, X-Real-IP)
are injected from the real client IP in the Rewrite function. PROXY
protocol v2 is sent to backends when send_proxy_protocol is enabled,
using the request context to carry the client address through the
HTTP/2 transport's dial function.
Server integration: handleConn dispatches to handleL7 when route.Mode
is "l7". The L7 handler converts RouteInfo to l7.RouteConfig and
delegates to l7.Serve.
L7 package tests: PrefixConn (4 tests), h2c backend round-trip,
forwarding header injection, backend unreachable (502), multiple
HTTP/2 requests over one connection.
Server integration tests: L7 route through full server pipeline with
TLS client, mixed L4+L7 routes on the same listener verifying both
paths work independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>