Add h2c origin support#3
Conversation
Greptile SummaryThis PR adds
Confidence Score: 5/5Safe to merge; h2c logic is well-validated and tested across all service types. All three validation layers (parse-time in validateIngress, start-time in each service's start(), and schema/flag mutual-exclusion in validateHTTPOriginScheme) are consistent and covered by focused unit tests. The helloWorld bypass flagged in an earlier review is resolved via an explicit case *helloWorld: branch. The QUIC trailer once-log, transport type guard, and test refactoring are all straightforward. The Docker workflow pushes before Trivy scans, which is a process trade-off rather than a code defect. No files require special attention; the Docker workflow ordering in .github/workflows/docker-publish.yml is worth a second look if image hygiene is a priority. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ParseIngress / validateIngress] --> B{service type?}
B -->|unix:| C[unixSocketPath scheme=http]
B -->|unix+tls:| D[unixSocketPath scheme=https]
B -->|hello_world| E[helloWorld]
B -->|http/https URL| F[httpService]
C & D & E & F --> G[validateHTTPOriginConfig]
G -->|*unixSocketPath| H[validateHTTPOriginScheme with socket scheme]
G -->|*helloWorld| I[validateHTTPOriginScheme 'https']
G -->|*httpService| J[validateHTTPOriginScheme with url.Scheme]
H & I & J --> K{h2cOrigin AND http2Origin?}
K -->|yes| ERR1[error: cannot both be enabled]
K -->|no| L{h2cOrigin AND scheme == https or wss?}
L -->|yes| ERR2[error: h2cOrigin with TLS scheme]
L -->|no| OK[validation passes]
OK --> M[service.start called at tunnel startup]
M --> N[newHTTPTransport]
N --> P{cfg.H2cOrigin?}
P -->|yes| Q[http2.Transport AllowHTTP=true DialTLSContext=plain TCP ReadIdleTimeout=cfg.KeepAliveTimeout]
P -->|no| R[http.Transport with optional ForceAttemptHTTP2 TLS cert pool loaded]
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
.github/workflows/docker-publish.yml:172-205
**Trivy scan runs after image is already publicly pushed**
`docker/build-push-action` (line 172) with `push: true` publishes the image to GHCR before the Trivy scans execute (lines 186–205). If either scan exits non-zero, the workflow job fails and the release is blocked — but the image is already live in the registry and remains there. A consumer who pulls `latest-h2c` in the window between the push and the failed scan gets an unvetted image. Consider setting `push: false` initially, running Trivy against the locally-loaded image (via `load: true` or `cache-only` export), and only re-running with `push: true` after the scan passes — or at a minimum accept that the current order is a known trade-off for release speed.
Reviews (5): Last reviewed commit: "ci: add fork h2c release workflows" | Re-trigger Greptile |
| func (o *httpService) SetOriginServerName(req *http.Request) { | ||
| o.transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| conn, err := o.transport.DialContext(ctx, network, addr) | ||
| t, ok := o.transport.(*http.Transport) | ||
| if !ok { | ||
| // h2c transport doesn't use TLS, so SNI matching is not applicable | ||
| return | ||
| } | ||
| t.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { | ||
| conn, err := t.DialContext(ctx, network, addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return tls.Client(conn, &tls.Config{ | ||
| RootCAs: o.transport.TLSClientConfig.RootCAs, | ||
| InsecureSkipVerify: o.transport.TLSClientConfig.InsecureSkipVerify, // nolint: gosec | ||
| RootCAs: t.TLSClientConfig.RootCAs, | ||
| InsecureSkipVerify: t.TLSClientConfig.InsecureSkipVerify, // nolint: gosec | ||
| ServerName: req.Host, | ||
| }), nil | ||
| } |
There was a problem hiding this comment.
SetOriginServerName mutates a shared transport per-request — pre-existing data race
This code (touched in this PR to add the *http.Transport type guard) assigns to t.DialTLSContext on every request when matchSNIToHost is enabled. Because o.transport is a shared *http.Transport connection pool, concurrent requests will race writing different closures to the same field. This was present before the PR; the type-assertion guard added here is a good safety improvement, but the underlying race is still there for the *http.Transport case. Worth tracking for a follow-up fix.
Prompt To Fix With AI
This is a comment left during a code review.
Path: ingress/origin_proxy.go
Line: 61-77
Comment:
**`SetOriginServerName` mutates a shared transport per-request — pre-existing data race**
This code (touched in this PR to add the `*http.Transport` type guard) assigns to `t.DialTLSContext` on every request when `matchSNIToHost` is enabled. Because `o.transport` is a shared `*http.Transport` connection pool, concurrent requests will race writing different closures to the same field. This was present before the PR; the type-assertion guard added here is a good safety improvement, but the underlying race is still there for the `*http.Transport` case. Worth tracking for a follow-up fix.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Acknowledged. This shared *http.Transport mutation predates h2c; this PR only adds the type guard so h2c's *http2.Transport does not enter TLS SNI mutation. Keeping the SetOriginServerName race as separate follow-up scope.
|
Addressed the h2c-specific Greptile findings in 39a80e0:
The |
| if cfg.H2cOrigin && scheme == "https" { | ||
| return fmt.Errorf("h2cOrigin is enabled but the origin uses https://; " + | ||
| "h2c is HTTP/2 over cleartext; use http:// or disable h2cOrigin") | ||
| } |
There was a problem hiding this comment.
validateHTTPOriginScheme checks for scheme == "https" but not "wss". A user who configures wss://localhost:8080 as an origin with h2cOrigin: true gets no validation error at parse or start time. At runtime, httpService.RoundTrip rewrites wss → https, so the http2.Transport passes "https" to its own DialTLSContext. Since our DialTLSContext always dials plain TCP (no TLS), the h2 preface is sent to a TLS-expecting server, which closes the connection immediately with a parse error — no useful diagnostic about the misconfiguration reaches the user.
| if cfg.H2cOrigin && scheme == "https" { | |
| return fmt.Errorf("h2cOrigin is enabled but the origin uses https://; " + | |
| "h2c is HTTP/2 over cleartext; use http:// or disable h2cOrigin") | |
| } | |
| if cfg.H2cOrigin && (scheme == "https" || scheme == "wss") { | |
| return fmt.Errorf("h2cOrigin is enabled but the origin uses %s://; " + | |
| "h2c is HTTP/2 over cleartext; use http:// (or ws://) or disable h2cOrigin", scheme) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: ingress/origin_service.go
Line: 131-134
Comment:
`validateHTTPOriginScheme` checks for `scheme == "https"` but not `"wss"`. A user who configures `wss://localhost:8080` as an origin with `h2cOrigin: true` gets no validation error at parse or start time. At runtime, `httpService.RoundTrip` rewrites `wss` → `https`, so the `http2.Transport` passes `"https"` to its own `DialTLSContext`. Since our `DialTLSContext` always dials plain TCP (no TLS), the h2 preface is sent to a TLS-expecting server, which closes the connection immediately with a parse error — no useful diagnostic about the misconfiguration reaches the user.
```suggestion
if cfg.H2cOrigin && (scheme == "https" || scheme == "wss") {
return fmt.Errorf("h2cOrigin is enabled but the origin uses %s://; " +
"h2c is HTTP/2 over cleartext; use http:// (or ws://) or disable h2cOrigin", scheme)
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in 2ef591a.
validateHTTPOriginScheme now rejects both https and wss, interpolates the actual %s:// scheme, and keeps ws:// allowed as cleartext. Added coverage in TestH2cOriginTransport and TestParseIngressRejectsInvalidH2cOriginConfig.
Validation run on 2ef591a9:
git diff --checkgo test -mod=vendor ./cryptogo test -mod=vendor ./config ./ingress ./connection- focused h2c CLI/ingress test run
- temp ingress smoke config confirms
h2cOrigin + wss://fails withwss://in the error HOME="$(mktemp -d)" make test lint
|
Follow-up on the Greptile summary proxy item: not implementable as written against this vendored API. |
f976168 to
85333a1
Compare
85333a1 to
cbaf898
Compare
Summary
h2cOrigin/--h2c-originfor cleartext HTTP/2 originsh2cOrigin+http2Origincombinations at config/runtime boundariescontrib/h2c-origin-upstream-20260516Test plan
git diff --checkgo test -mod=vendor ./cryptogo test -mod=vendor ./config ./ingress ./connectiongo test -mod=vendor ./cmd/cloudflared/tunnel ./ingress -run 'TestTunnelH2cOriginFlagRegistered|TestH2cOriginFromCLI|TestH2cOriginTransport|TestUnixSocketH2cOriginConflict|TestParseIngressRejectsInvalidH2cOriginConfig' -count=1go run -mod=vendor ./cmd/cloudflared tunnel --help | rg "h2c-origin"go run -mod=vendor ./cmd/cloudflared tunnel --h2c-origin --helpcloudflared tunnel --config /tmp/h2c-valid.yml ingress validateequivalent viago runpassescloudflared tunnel --config /tmp/h2c-conflict.yml ingress validateequivalent viago runfails as expectedHOME="$(mktemp -d)" make test lint