mirror of
https://github.com/moby/buildkit.git
synced 2026-06-24 08:47:57 +00:00
proxyprovider: fix exec proxy HTTPS regressions
Commit 91cc422d5 split exec proxying from exec network mode and
started cloning the proxy transport per egress namespace with a custom
DialContext.
Without ForceAttemptHTTP2, the cloned transport could advertise h2 via
ALPN without a registered HTTP/2 RoundTripper, causing Alpine apk update
requests to fail with proxy 502 responses.
Restoring HTTP/2 upstream also exposes unknown-length HTTP/2 responses.
When those responses are rewritten for the MITM HTTP/1.1 client, close
the client-facing connection so clients can delimit the response body.
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -81,10 +81,7 @@ func New(opt Opt) (network.ProxyProvider, error) {
|
||||
lru: list.New(),
|
||||
egressProviders: maps.Clone(opt.EgressProviders),
|
||||
ownedEgressProviders: slices.Clone(opt.OwnedEgressProviders),
|
||||
transport: &http.Transport{
|
||||
Proxy: nil,
|
||||
DisableCompression: true,
|
||||
},
|
||||
transport: newProxyTransport(),
|
||||
}
|
||||
p.pool = netpool.New(netpool.Opt[*proxyNS]{
|
||||
Name: "proxy network namespace",
|
||||
@@ -98,6 +95,14 @@ func New(opt Opt) (network.ProxyProvider, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func newProxyTransport() *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: nil,
|
||||
DisableCompression: true,
|
||||
ForceAttemptHTTP2: true,
|
||||
}
|
||||
}
|
||||
|
||||
type provider struct {
|
||||
root string
|
||||
next atomic.Uint32
|
||||
@@ -586,13 +591,7 @@ func (h *proxyHandler) handleConnect(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
h.recordRequest(req, resp.StatusCode, finalURL(req, resp))
|
||||
// Response.Write uses resp.Proto for the status line. In the MITM
|
||||
// path, resp describes the upstream fetch, so align it to the
|
||||
// client-facing request we intercepted.
|
||||
resp.Proto = req.Proto
|
||||
resp.ProtoMajor = req.ProtoMajor
|
||||
resp.ProtoMinor = req.ProtoMinor
|
||||
resp.Close = resp.Close || req.Close || !req.ProtoAtLeast(1, 1)
|
||||
prepareMITMResponse(req, resp)
|
||||
tracker := newProxyBodyTracker(resp.Body)
|
||||
resp.Body = tracker
|
||||
if err := resp.Write(tlsConn); err != nil {
|
||||
@@ -608,6 +607,23 @@ func (h *proxyHandler) handleConnect(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMITMResponse(req *http.Request, resp *http.Response) {
|
||||
// Response.Write uses resp.Proto for the status line. In the MITM
|
||||
// path, resp describes the upstream fetch, so align it to the
|
||||
// client-facing request we intercepted.
|
||||
resp.Proto = req.Proto
|
||||
resp.ProtoMajor = req.ProtoMajor
|
||||
resp.ProtoMinor = req.ProtoMinor
|
||||
resp.Close = resp.Close || req.Close || !req.ProtoAtLeast(1, 1)
|
||||
if resp.ContentLength < 0 && len(resp.TransferEncoding) == 0 {
|
||||
// Response.Write marks its internal response clone as close-delimited
|
||||
// for this case, but it does not update resp.Close. Mirror that here
|
||||
// so handleConnect closes the client-facing TLS connection after the
|
||||
// upstream body is copied.
|
||||
resp.Close = true
|
||||
}
|
||||
}
|
||||
|
||||
type proxyBodyTracker struct {
|
||||
body io.ReadCloser
|
||||
hash hash.Hash
|
||||
|
||||
@@ -98,6 +98,36 @@ func TestProxyHandlerRoundTripIgnoresClientContextCancel(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestNewProxyTransportAttemptsHTTP2(t *testing.T) {
|
||||
tr := newProxyTransport()
|
||||
t.Cleanup(tr.CloseIdleConnections)
|
||||
|
||||
// The provider transport is cloned per namespace and given a custom
|
||||
// DialContext. Without ForceAttemptHTTP2, the clone can advertise h2 via
|
||||
// ALPN without a registered HTTP/2 RoundTripper, causing apk update to fail
|
||||
// against Alpine HTTPS repositories with proxy 502 responses.
|
||||
require.True(t, tr.ForceAttemptHTTP2)
|
||||
}
|
||||
|
||||
func TestPrepareMITMResponseClosesUnknownLength(t *testing.T) {
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "https://www.example.com/", nil)
|
||||
resp := &http.Response{
|
||||
Proto: "HTTP/2.0",
|
||||
ProtoMajor: 2,
|
||||
ProtoMinor: 0,
|
||||
StatusCode: http.StatusOK,
|
||||
Status: "200 OK",
|
||||
ContentLength: -1,
|
||||
}
|
||||
|
||||
prepareMITMResponse(req, resp)
|
||||
|
||||
require.Equal(t, req.Proto, resp.Proto)
|
||||
require.Equal(t, req.ProtoMajor, resp.ProtoMajor)
|
||||
require.Equal(t, req.ProtoMinor, resp.ProtoMinor)
|
||||
require.True(t, resp.Close)
|
||||
}
|
||||
|
||||
func TestProxyHandlerMarksPostIncomplete(t *testing.T) {
|
||||
methodCh := make(chan string, 1)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user