Repository navigation
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
esnible
force-pushed
the
feat/forward-proxy-absolute-form
branch
2 times, most recently
from
November 2, 2026 15:22
ec1aa0b to
889e71d
Compare
esnible
force-pushed
the
feat/forward-proxy-absolute-form
branch
from
November 2, 2026 19:24
889e71d to
b028e77
Compare
araujof
self-requested a review
November 2, 2026 19:44
esnible
force-pushed
the
feat/forward-proxy-absolute-form
branch
7 times, most recently
from
November 6, 2026 21:49
58da69d to
c6ba7f8
Compare
Fork 0.11.3 carries the CONNECT tunnel support the forward-proxy runtime needs: 2xx responses to CONNECT switch the session to tunnel mode (praxis-proxy/pingora#7), and a refused CONNECT closes the connection so early tunnel bytes are never parsed as a pipelined request (praxis-proxy/pingora#8). The fork has capped h2 at >=0.4.16, <0.4.20 since 0.11.1, so the workspace h2 requirement moves from 0.4.20 to 0.4.19; cargo cannot resolve the two otherwise. h2 0.4.20 began rejecting conflicting duplicate Content-Length itself, and the fork's tests expect Pingora's own check to do it. That defense is unchanged: the fork and Praxis inbound admission both reject conflicting values. The other 0.4.20 fixes are forgone until the fork lifts the cap, as the Cargo.toml comment records. No RustSec advisory affects h2 0.4.19. Signed-off-by: Ed Snible <snible@us.ibm.com>
Listeners with an enabled forward_proxy block now forward-proxy: CONNECT requests pass the same inbound admission as every other request, run the filter pipeline, then the destination allowlist and the private-IP guard decide the dial, and the tunnel relays raw bytes under the configured idle timeout and optional max duration, with structured tunnel_established / tunnel_close events. HTTP/2 CONNECT is answered with 501; every non-CONNECT request keeps the reverse-proxy pipeline. Admission runs before the pipeline so a client cannot hand trusted context to policy filters: reserved x-praxis-* headers are refused with 400, as on any other request. A target without a port is refused with 400, since authority-form requires one (RFC 9112 Section 3.2.3). A filter that answers the CONNECT with a 2xx gets a 502 instead, without its body: to CONNECT a 2xx means the tunnel is open, and none was. The pump splits the destination stream so a pending destination write is its own select! arm: a destination that stops reading stalls only the client-to-destination direction, while destination bytes keep reaching the client and both deadlines keep firing. Client writes share the session with client reads, so each runs under the idle timeout and the whole relay under the hard deadline; a peer that stops reading cannot hold a tunnel open past either limit. Byte counts include only what the receiving side accepted. The tunnel 200 goes through the response filters like any response, before the pump starts, and the tunnel ends with a bodyless end-of-stream body pass, so access_log emits its own record at close with the final byte counts. The logging hook then neither re-runs the response filters without a response nor falls back to a fixed-shape record. The destination host is resolved as an absolute name (with a trailing dot), so the system resolver cannot append its search domains and dial a host the destination rules never approved, such as api.internal becoming api.internal.corp.example. IP literals are dialed as given. The runtime is behind the forward-proxy cargo feature (protocol and server), which also turns on the config key in core; without it the connect module is not compiled and CONNECT stays answered with 405, even when the config key is on through the core feature alone. The feature selects the Pingora backend, so the hyper suite lanes leave it out, as they do tls-health-check. Relies on the pingora fork 0.11.3 2xx-to-CONNECT tunnel mode (praxis-proxy/pingora#7). Signed-off-by: Ed Snible <snible@us.ibm.com>
An HTTP/1 absolute-form plain request (GET http://host/path, what HTTP_PROXY clients send for http:// URLs) on a forward_proxy listener runs the same pipeline and destination rules as CONNECT and is then forwarded to its origin, TLS-originated with SNI for https. The destination is parsed from the verbatim wire target (the fork's H1 reader keeps scheme and authority off the Uri), and the upstream leg is rewritten to origin-form. Origin-form requests keep the reverse-proxy pipeline when it selects an upstream, so one port can reverse- and forward-proxy; when it selects none they are answered with 400 rather than failing with a 500 for want of an upstream. A private or reserved IP-literal destination is refused with 502 unless allow_private_upstreams is set. The HTTP upstream resolver checks only names it resolves and trusts literals, which come from validated config in reverse-proxy mode but from the client here; the CONNECT dialer already checks literals. The destination is dialed as an absolute name (with a trailing dot), as CONNECT is, so the system resolver cannot append its search domains to a relative name and reach a host the destination rules never approved. The Host header and TLS SNI keep the name the client sent. An IPv6 literal destination (http://[::1]/) is stored without its brackets. http::Uri keeps them, which made the dial address [[::1]]:8080 and kept the private-address check from parsing the host as an IP, so a private IPv6 literal was not refused. Signed-off-by: Ed Snible <snible@us.ibm.com>
esnible
force-pushed
the
feat/forward-proxy-absolute-form
branch
from
November 7, 2026 03:24
c6ba7f8 to
6edd06d
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
On a forward-proxy listener, an HTTP/1 absolute-form plain request
(
GET http://host/path— whatHTTP_PROXYclients send forhttp://URLs) now runs the same pipeline and the same
allow_destinationsrulesas CONNECT, and is then forwarded to its origin: TLS-originated with SNI
for
httpsdestinations, plain TCP forhttp.Details:
raw_path()),because the HTTP/1 reader keeps scheme and authority off the
Uri.CONNECT gate.
router/load_balanceron the listener), so one port can reverse- and forward-proxy. When it selects none, the request is answered with 400 instead of failing later with a 500 for want of an upstream — that applies on HTTP/2 too. Pipeline rejections and terminal responses (e.g. anip_acl403) still answer first.GET http://169.254.169.254/, loopback, RFC 1918) is refused with 502 unlessinsecure_options.allow_private_upstreamsis set. The HTTP upstream resolver checks only names it resolves and trusts literals — fine for reverse proxying, where literals come from validated config, but here the client picks them. The CONNECT dialer already checked literals.Hostheader and TLS SNI keep the name the client sent.http://[::1]/) is stored without its brackets.http::Urikeeps them, which made the dial address[[::1]]:8080and kept the private-address check from parsing the host as an IP, so private IPv6 literals were not refused.Why
HTTP_PROXYclients send absolute-form targets for plain-httpURLs andCONNECT tunnels for
https. Without this, a forward-proxy listeneranswers every plain request with 500 (no upstream selected), so the
forward proxy from #781 only half-works.
Stacking and dependencies
review the last commit only. Once feat(protocol): add HTTP CONNECT forward-proxy runtime #1479 merges, this PR's diff shrinks
to the absolute-form commit alone.
Fixes part of #781.
🤖 Generated with Claude Code