Skip to content

Commit 781d395

Browse files
authored
Merge pull request #9188 from dmcgowan/backport-1.7-localhost-http-fallback
[release/1.7] remotes: always try to establish tls connection when tls configured
2 parents c12225c + 7779ce6 commit 781d395

4 files changed

Lines changed: 104 additions & 6 deletions

File tree

remotes/docker/config/hosts.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,13 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
122122
hosts[len(hosts)-1].capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush
123123
}
124124

125+
// explicitTLS indicates that TLS was explicitly configured and HTTP endpoints should
126+
// attempt to use the TLS configuration before falling back to HTTP
127+
var explicitTLS bool
128+
125129
var defaultTLSConfig *tls.Config
126130
if options.DefaultTLS != nil {
131+
explicitTLS = true
127132
defaultTLSConfig = options.DefaultTLS
128133
} else {
129134
defaultTLSConfig = &tls.Config{}
@@ -161,14 +166,11 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
161166

162167
rhosts := make([]docker.RegistryHost, len(hosts))
163168
for i, host := range hosts {
164-
165-
rhosts[i].Scheme = host.scheme
166-
rhosts[i].Host = host.host
167-
rhosts[i].Path = host.path
168-
rhosts[i].Capabilities = host.capabilities
169-
rhosts[i].Header = host.header
169+
// Allow setting for each host as well
170+
explicitTLS := explicitTLS
170171

171172
if host.caCerts != nil || host.clientPairs != nil || host.skipVerify != nil {
173+
explicitTLS = true
172174
tr := defaultTransport.Clone()
173175
tlsConfig := tr.TLSClientConfig
174176
if host.skipVerify != nil {
@@ -232,6 +234,21 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
232234
rhosts[i].Client = client
233235
rhosts[i].Authorizer = authorizer
234236
}
237+
238+
// When TLS has been explicitly configured for the operation or host, use the
239+
// docker.HTTPFallback roundtripper to catch TLS errors and re-attempt the request as http.
240+
// This allows preference for https when configured but also catches TLS errors early enough
241+
// in the request to avoid sending the request twice or consuming the request body.
242+
if host.scheme == "http" && explicitTLS {
243+
host.scheme = "https"
244+
rhosts[i].Client.Transport = docker.HTTPFallback{RoundTripper: rhosts[i].Client.Transport}
245+
}
246+
247+
rhosts[i].Scheme = host.scheme
248+
rhosts[i].Host = host.host
249+
rhosts[i].Path = host.path
250+
rhosts[i].Capabilities = host.capabilities
251+
rhosts[i].Header = host.header
235252
}
236253

237254
return rhosts, nil

remotes/docker/config/hosts_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package config
1919
import (
2020
"bytes"
2121
"context"
22+
"crypto/tls"
2223
"fmt"
2324
"net/http"
2425
"os"
@@ -284,6 +285,30 @@ func TestLoadCertFiles(t *testing.T) {
284285
}
285286
}
286287

288+
func TestLocalhostHTTPFallback(t *testing.T) {
289+
opts := HostOptions{
290+
DefaultTLS: &tls.Config{
291+
InsecureSkipVerify: true,
292+
},
293+
}
294+
hosts := ConfigureHosts(context.TODO(), opts)
295+
296+
testHosts, err := hosts("localhost:8080")
297+
if err != nil {
298+
t.Fatal(err)
299+
}
300+
if len(testHosts) != 1 {
301+
t.Fatalf("expected a single host for localhost config, got %d hosts", len(testHosts))
302+
}
303+
if testHosts[0].Scheme != "https" {
304+
t.Fatalf("expected https scheme for localhost with tls config, got %q", testHosts[0].Scheme)
305+
}
306+
_, ok := testHosts[0].Client.Transport.(docker.HTTPFallback)
307+
if !ok {
308+
t.Fatal("expected http fallback configured for defaulted localhost endpoint")
309+
}
310+
}
311+
287312
func compareRegistryHost(j, k docker.RegistryHost) bool {
288313
if j.Scheme != k.Scheme {
289314
return false

remotes/docker/resolver.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package docker
1818

1919
import (
2020
"context"
21+
"crypto/tls"
2122
"errors"
2223
"fmt"
2324
"io"
@@ -707,3 +708,27 @@ func IsLocalhost(host string) bool {
707708
ip := net.ParseIP(host)
708709
return ip.IsLoopback()
709710
}
711+
712+
// HTTPFallback is an http.RoundTripper which allows fallback from https to http
713+
// for registry endpoints with configurations for both http and TLS, such as
714+
// defaulted localhost endpoints.
715+
type HTTPFallback struct {
716+
http.RoundTripper
717+
}
718+
719+
func (f HTTPFallback) RoundTrip(r *http.Request) (*http.Response, error) {
720+
resp, err := f.RoundTripper.RoundTrip(r)
721+
var tlsErr tls.RecordHeaderError
722+
if errors.As(err, &tlsErr) && string(tlsErr.RecordHeader[:]) == "HTTP/" {
723+
// server gave HTTP response to HTTPS client
724+
plainHTTPUrl := *r.URL
725+
plainHTTPUrl.Scheme = "http"
726+
727+
plainHTTPRequest := *r
728+
plainHTTPRequest.URL = &plainHTTPUrl
729+
730+
return f.RoundTripper.RoundTrip(&plainHTTPRequest)
731+
}
732+
733+
return resp, err
734+
}

remotes/docker/resolver_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"io"
2727
"net/http"
2828
"net/http/httptest"
29+
"net/url"
2930
"strconv"
3031
"strings"
3132
"testing"
@@ -332,6 +333,36 @@ func TestHostTLSFailureFallbackResolver(t *testing.T) {
332333
runBasicTest(t, "testname", sf)
333334
}
334335

336+
func TestHTTPFallbackResolver(t *testing.T) {
337+
sf := func(h http.Handler) (string, ResolverOptions, func()) {
338+
s := httptest.NewServer(h)
339+
u, err := url.Parse(s.URL)
340+
if err != nil {
341+
t.Fatal(err)
342+
}
343+
344+
client := &http.Client{
345+
Transport: HTTPFallback{http.DefaultTransport},
346+
}
347+
options := ResolverOptions{
348+
Hosts: func(host string) ([]RegistryHost, error) {
349+
return []RegistryHost{
350+
{
351+
Client: client,
352+
Host: u.Host,
353+
Scheme: "https",
354+
Path: "/v2",
355+
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
356+
},
357+
}, nil
358+
},
359+
}
360+
return u.Host, options, s.Close
361+
}
362+
363+
runBasicTest(t, "testname", sf)
364+
}
365+
335366
func TestResolveProxy(t *testing.T) {
336367
var (
337368
ctx = context.Background()

0 commit comments

Comments
 (0)