feat: issue-3628 Serve gRPC and HTTP from one endpoint per service (control plane)
Summary
Implements the control plane part of the single-endpoint design: the control plane now accepts every connection on its current HTTP port. A small connection classifier looks at the start of each new connection without consuming it and gives it to the gRPC server or to the HTTP server that run today, so the middleware chain, the authentication and the gRPC-Web wrapper do not change.
Which port is the single endpoint — decided with the repository owner. During implementation the original spec choice (D-002: gRPC port) was revisited and flipped with the owner: the single endpoint is the current HTTP port (server.http.addr — cp.chainloop.dev/infoz and friends become THE endpoint, serving HTTP, gRPC-Web and native gRPC), and the gRPC port becomes the deprecated legacy listener. The decision and rationale are recorded in the spec PR (comment); D-002 will be superseded through br:eng-spec --revise. The classifier is port-agnostic, so this is a wiring change, not a redesign.
pkg/httpgrpcmux: connection-level classifier. Matches every native gRPC content type, including parameters, and excludes gRPC-Web. A client that stalls waiting for the server to speak first is unblocked with an empty SETTINGS frame after a short grace period; its acknowledgement is consumed from the replay — including when it arrives after the first request (HTTP-classified connections wait bounded; gRPC-classified ones hand off immediately). Classification runs under one absolute deadline (reads and writes), a per-connection byte budget, an aggregate 256 MiB budget including payload-allocation reservations, a frame-count cap, decoded-header-list caps against HPACK amplification, and admission control (shedding beyond 8192 in-flight classifications). Recoverable accept errors retry with backoff.- The single port binds
server.http.addrand terminates TLS from the newserver.http.tls_config, offering HTTP/2 and HTTP/1.1 through ALPN, so one certificate covers all protocols (R-004). A partialtls_configis a hard startup error. - The legacy gRPC listener stays on by default behind the renamed
server.legacy_grpc_listenerflag (R-006, unreleased field), with a deprecation warning at startup; it keeps its own TLS settings for backward compatibility. - The HTTP server now also speaks plain HTTP/2 (h2c) natively, serves with a 60s
ReadHeaderTimeout(HTTP/1.x), and forwards native-gRPC requests that arrive on HTTP-owned HTTP/2 connections to the gRPC server (same middleware chain). - No Helm chart changes in this PR; existing CLI configurations and the control plane connection to the CAS keep working — the cp-api ingress only changes its backend port when the legacy port retires (R-005).
Reviews
Four model review rounds (GPT-6.1 Sol ×2, GPT-6.1 Sol Pro, Kimi K3) plus cubic ran against this PR. Sixteen correctness/security findings and four cubic findings were confirmed and fixed, each with a regression test — notably: the mux joins the application lifecycle; the injected SETTINGS' ACK is consumed from the replay in both orderings; parser bounds (decoded-header caps, byte budgets with allocation reservations, frame cap, deadlines, admission control); accept-loop recovery; dispatcher-owned listener shutdown; delayed-ACK protocol soundness for HTTP connections. Dispositions with per-finding tests: round 1, security, round 3. The Kimi K3 round verified all seven fix areas correct and flagged the shutdown race fixed in this revision, plus minor hardening items tracked below.
The security review verified explicitly: no auth/authz bypass in either HTTP→gRPC path, no cross-client request smuggling, and no cleartext h2c against a TLS-configured listener. The security-context priors for the touched files were re-confirmed (comment).
Spec notes
Spec in #3629, ticket #3628 — milestone 1, control plane only.
- D-002 superseded (see the decision above): the spec PR carries the change; this PR implements it.
- Parity coverage (R-002): server-level tests cover native gRPC, HTTP/1.1, h2c, gRPC-Web, native gRPC on an HTTP-owned connection, the legacy flag on/off (legacy = native gRPC only, not the HTTP API), and one TLS certificate negotiating h2 and h1.1. The full auth-matrix parity suite (token types, suspended org, validation errors) is follow-up work in this ticket.
- TLS identity metadata (
Request.TLS, gRPCAuthInfo) is not surfaced through the handoff yet; the security review found no consumer for it in the codebase. Follow-up in this ticket. HTTP/2 header-completion protection is likewise a follow-up (the 60s timeout covers HTTP/1.x).
Checklist
-
go build ./... -
go test ./app/controlplane/... ./pkg/...(race-enabled for the new packages) - golangci-lint clean on the changed packages
- CI green (tests, lint, CodeQL, DCO)
- Four independent model review rounds + cubic, all findings dispositioned
- Reviews approved