Skip to content

Add h2c origin support#3

Open
mannie-exe wants to merge 4 commits into
masterfrom
feat/h2c-origin
Open

Add h2c origin support#3
mannie-exe wants to merge 4 commits into
masterfrom
feat/h2c-origin

Conversation

@mannie-exe
Copy link
Copy Markdown

Summary

  • add h2cOrigin / --h2c-origin for cleartext HTTP/2 origins
  • validate incompatible h2cOrigin + http2Origin combinations at config/runtime boundaries
  • keep fork CI baseline while preserving upstream-clean h2c commits on contrib/h2c-origin-upstream-20260516
  • align TLS curve negotiation test with Go 1.26 crypto/tls behavior

Test plan

  • git diff --check
  • go test -mod=vendor ./crypto
  • go test -mod=vendor ./config ./ingress ./connection
  • go test -mod=vendor ./cmd/cloudflared/tunnel ./ingress -run 'TestTunnelH2cOriginFlagRegistered|TestH2cOriginFromCLI|TestH2cOriginTransport|TestUnixSocketH2cOriginConflict|TestParseIngressRejectsInvalidH2cOriginConfig' -count=1
  • go run -mod=vendor ./cmd/cloudflared tunnel --help | rg "h2c-origin"
  • go run -mod=vendor ./cmd/cloudflared tunnel --h2c-origin --help
  • cloudflared tunnel --config /tmp/h2c-valid.yml ingress validate equivalent via go run passes
  • cloudflared tunnel --config /tmp/h2c-conflict.yml ingress validate equivalent via go run fails as expected
  • HOME="$(mktemp -d)" make test lint

@mannie-exe mannie-exe mentioned this pull request May 16, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 16, 2026

Greptile Summary

This PR adds h2cOrigin / --h2c-origin support for cleartext HTTP/2 origins, with parse-time and start-time validation that rejects incompatible h2cOrigin + http2Origin combinations and TLS origin schemes. It also addresses a previous per-request WARN log flooding issue with a process-level once-log via atomic.Bool, adds a type guard in SetOriginServerName for *http2.Transport, and updates CI tooling.

  • h2c transport (ingress/origin_service.go): new http2.Transport branch in newHTTPTransport, validated against scheme conflicts and http2Origin mutual exclusion at both parse time (validateIngress) and start time; *helloWorld origins are correctly blocked via case *helloWorld: in validateHTTPOriginConfig.
  • QUIC trailer fix (connection/quic_connection.go): AddTrailer now logs exactly once per process lifetime using quicTrailerDropWarningLogged atomic.Bool, replacing the silent no-op.
  • CI / build (.github/workflows/, Dockerfile*, Makefile): checkout order corrected for go-version-file, actions pinned to newer versions, new docker-publish.yml for fork release workflow, gotest.log moved to local dir.

Confidence Score: 5/5

Safe 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

Filename Overview
ingress/origin_service.go Core h2c implementation: adds http2.Transport branch in newHTTPTransport, validateHTTPOriginConfig/validateHTTPOriginScheme helpers, and explicit helloWorld validation. All service types (*httpService, *helloWorld, *unixSocketPath) are covered with correct scheme checks.
ingress/ingress.go Adds parse-time validateHTTPOriginConfig call in validateIngress for all service types including *helloWorld; correctly placed after service construction.
connection/quic_connection.go Fixes per-request WARN flood: AddTrailer now uses a package-level quicTrailerDropWarningLogged atomic.Bool to emit the warning exactly once per process; logger passed through newHTTPResponseAdapter.
ingress/origin_proxy.go Adds *http.Transport type guard in SetOriginServerName so h2c's *http2.Transport skips the TLS SNI mutation path.
ingress/config.go Adds H2cOriginFlag, H2cOrigin bool field, CLI parsing, config resolution (setH2cOrigin), and ConvertToRawOriginConfig mapping — all consistent with the existing Http2Origin pattern.
.github/workflows/docker-publish.yml New release workflow for h2c fork. Trivy vulnerability scans run after docker/build-push-action already publishes the image, so a scan failure leaves a potentially vulnerable image in GHCR.
.github/workflows/check.yaml Checkout step correctly moved before setup-go to support go-version-file: go.mod; unprivileged ICMP granted for Linux runners; action versions updated.
datagramsession/manager_test.go Tests refactored to capture errors via channels rather than goroutine-local require calls, eliminating racy assertions; TestManagerCtxDoneCloseSessions renamed and updated to verify byRemote: true shutdown semantics.

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]
Loading
Prompt To Fix All With AI
Fix 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

Comment thread ingress/origin_service.go
Comment thread ingress/origin_proxy.go
Comment on lines 61 to 77
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mannie-exe
Copy link
Copy Markdown
Author

mannie-exe commented May 16, 2026

Addressed the h2c-specific Greptile findings in 39a80e0:

  • http2.Transport now wires ReadIdleTimeout from OriginRequestConfig.KeepAliveTimeout.
  • hello_world rejects h2cOrigin during ingress validation and service startup, with coverage for both paths.

The SetOriginServerName shared-transport mutation is pre-existing and separate from h2c; keeping that as follow-up scope.

Comment thread ingress/origin_service.go Outdated
Comment on lines +131 to +134
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")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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 wsshttps, 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.

Suggested change
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --check
  • go test -mod=vendor ./crypto
  • go test -mod=vendor ./config ./ingress ./connection
  • focused h2c CLI/ingress test run
  • temp ingress smoke config confirms h2cOrigin + wss:// fails with wss:// in the error
  • HOME="$(mktemp -d)" make test lint

@mannie-exe
Copy link
Copy Markdown
Author

Follow-up on the Greptile summary proxy item: not implementable as written against this vendored API. vendor/golang.org/x/net/http2/transport_common.go defines http2.Transport without a Proxy field, so Proxy: http.ProxyFromEnvironment would not compile here. Manual HTTP proxy support for h2c would need a separate design/follow-up.

Comment thread connection/quic_connection.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant