Skip to content

Add h2c origin support#3

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

Add h2c origin support#3
mannie-exe wants to merge 5 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 HTTP/2 cleartext (h2c) origins, alongside comprehensive validation that prevents incompatible h2cOrigin + http2Origin or h2cOrigin + TLS-scheme combinations from reaching the transport layer. Infrastructure is also updated: CI is refreshed to derive the Go version from go.mod, a Docker/binary release pipeline is introduced for h2c-tagged releases, and several unrelated correctness fixes land (QUIC trailer log-once deduplication, file-handle leak fixes, test improvements).

  • h2c transport: newHTTPTransport returns an *http2.Transport with AllowHTTP: true when H2cOrigin is set; scheme validation (http://, ws:// only) and h2cOrigin+http2Origin conflict detection are enforced at both ingress-parse time (validateIngress) and service-start time.
  • Config plumbing: H2cOrigin *bool is added to config.OriginRequestConfig (raw/YAML layer) and H2cOrigin bool to ingress.OriginRequestConfig (merged layer), with setH2cOrigin, originRequestFromConfig, originRequestFromSingleRule, and ConvertToRawOriginConfig all updated consistently.
  • Release workflow: docker-publish.yml builds and publishes amd64/arm64 binaries and a multi-arch container image for v*-h2c* tags, with Trivy library and OS scans gating publication.

Confidence Score: 5/5

Safe to merge; the h2c transport, validation, and config plumbing are all consistent with existing patterns and well-covered by tests.

The core h2c changes are correct: validation fires at both parse time and service-start time, the http2.Transport is configured with AllowHTTP: true and a plain-TCP DialTLSContext, and the *http.Transport type guard in SetOriginServerName prevents a panic on the new transport type. Config merging follows the established Http2Origin pattern exactly. The only finding is a GitHub Actions expression-injection pattern in the new release workflow, which requires write access to the repository to exploit.

.github/workflows/docker-publish.yml — the three vars steps each use ${{ inputs.release_tag }} inline in bash and should use an env var intermediary instead.

Security Review

  • GitHub Actions expression injection (.github/workflows/docker-publish.yml): ${{ inputs.release_tag }} is interpolated directly into three run: bash scripts. Because Actions substitutes the expression before the shell parses the script, a value with ", $(...), or backticks can escape the quoted assignment and execute arbitrary commands prior to the v*-h2c* guard. Exploitation requires workflow_dispatch write access to the repository, limiting the practical attack surface, but the pattern should be fixed by routing the input through an env: variable.
  • No credential leakage, secrets exposure, or injection issues were found in the Go transport code; the h2c DialTLSContext correctly ignores the unused *tls.Config argument and always dials plain TCP.

Important Files Changed

Filename Overview
ingress/origin_service.go Core h2c implementation: newHTTPTransport now returns http.RoundTripper and branches on cfg.H2cOrigin to produce an *http2.Transport; validateHTTPOriginConfig/validateHTTPOriginScheme cover all service types; helloWorld.start pre-validates before delegating to httpService.start; dialer extraction is correctly shared between h2c and plain paths.
ingress/ingress.go Adds validateHTTPOriginConfig(service, cfg) call inside validateIngress loop, catching h2c conflicts at YAML-parse time before any service is started.
ingress/origin_proxy.go Adds a *http.Transport type assertion guard in SetOriginServerName so h2c's *http2.Transport is not subject to TLS SNI mutation.
ingress/config.go Adds H2cOriginFlag, H2cOrigin bool to OriginRequestConfig, and wires setH2cOrigin / originRequestFromSingleRule / ConvertToRawOriginConfig consistently with the existing Http2Origin pattern.
config/configuration.go Adds H2cOrigin *bool to raw OriginRequestConfig; also fixes os.Openos.Stat in FileExists, defers file close correctly, and replaces s.Duration.Seconds() with s.Seconds() in JSON marshaling.
connection/quic_connection.go Adds a process-wide quicTrailerDropWarningLogged atomic.Bool so the trailer-drop warning is emitted once, preventing log flooding for gRPC origins; logger is now threaded into httpResponseAdapter.
.github/workflows/docker-publish.yml New release workflow for h2c-tagged builds; uses ${{ inputs.release_tag }} directly in bash run: blocks — a GitHub Actions expression-injection pattern that should use an env var intermediary instead.
.github/workflows/check.yaml Refreshed CI: checkout now precedes setup-go (required for go-version-file: go.mod), Go version is derived from go.mod, unprivileged ICMP sysctl added for Linux runners.
datagramsession/manager_test.go Test refactored to propagate errors via channels rather than calling require in goroutines, eliminating potential goroutine-leak on assertion failures; TestManagerCtxDoneCloseSessions renamed and tightened to assert closedByRemote: true.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Ingress Rule parsed] --> B{validateHTTPOriginConfig}
    B -->|httpService| C{scheme?}
    B -->|helloWorld| D[scheme = 'https']
    B -->|unixSocketPath| E[scheme = service.scheme]
    B -->|other| F[return nil]

    C --> G[validateHTTPOriginScheme]
    D --> G
    E --> G

    G --> H{H2cOrigin AND Http2Origin?}
    H -->|yes| I[conflict error]
    H -->|no| J{H2cOrigin AND scheme is https/wss?}
    J -->|yes| K[scheme error]
    J -->|no| L[valid config]

    L --> M[newHTTPTransport]
    M --> N{cfg.H2cOrigin?}
    N -->|yes| O[return http2.Transport AllowHTTP: true ReadIdleTimeout: cfg.KeepAliveTimeout DialTLSContext: plain TCP dial]
    N -->|no| P[return http.Transport ForceAttemptHTTP2: cfg.Http2Origin 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:37-57
GitHub Actions expression injection: `${{ inputs.release_tag }}` is substituted directly into the shell script text before the runner executes it. A value containing `"`, `$(...)`, or backticks can escape the double-quoted assignment and execute arbitrary commands before the `v*-h2c*` guard runs. The safe pattern is to expose the input as an environment variable and reference it via `$VAR` inside the script — the shell then receives it as a literal value regardless of content. The same pattern appears in all three `vars` steps (`build-binaries`, `build-and-push`, `publish-release`).

```suggestion
        env:
          INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
        run: |
          if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
            raw_tag="${INPUT_RELEASE_TAG}"
          else
            raw_tag="${GITHUB_REF_NAME}"
          fi

          if [[ -z "${raw_tag}" || "${raw_tag}" != v*-h2c* ]]; then
            echo "Invalid h2c release tag: ${raw_tag}" >&2
            exit 1
          fi

          echo "raw_tag=${raw_tag}" >> "${GITHUB_OUTPUT}"
          echo "release_version=${raw_tag#v}" >> "${GITHUB_OUTPUT}"

      - name: Checkout
        uses: actions/checkout@v6.0.2
        with:
          ref: ${{ steps.vars.outputs.raw_tag }}

      - name: Install Go
```

Reviews (6): 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