Skip to content

Commit 6b59122

Browse files
committed
remotes: always try to establish tls connection when tls configured
When a endpoint is configured for http and has a tls configuration, always try to the tls connection and fallback to http when the tls connections fails from receiving an http response. This fixes an issue with default localhost endpoints which get defaulted to http with insecure tls also configured but are using tls. Signed-off-by: Derek McGowan <[email protected]> (cherry picked from commit 79772a0) Signed-off-by: Derek McGowan <[email protected]>
1 parent 4483cbe commit 6b59122

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"
@@ -691,3 +692,27 @@ func IsLocalhost(host string) bool {
691692
ip := net.ParseIP(host)
692693
return ip.IsLoopback()
693694
}
695+
696+
// HTTPFallback is an http.RoundTripper which allows fallback from https to http
697+
// for registry endpoints with configurations for both http and TLS, such as
698+
// defaulted localhost endpoints.
699+
type HTTPFallback struct {
700+
http.RoundTripper
701+
}
702+
703+
func (f HTTPFallback) RoundTrip(r *http.Request) (*http.Response, error) {
704+
resp, err := f.RoundTripper.RoundTrip(r)
705+
var tlsErr tls.RecordHeaderError
706+
if errors.As(err, &tlsErr) && string(tlsErr.RecordHeader[:]) == "HTTP/" {
707+
// server gave HTTP response to HTTPS client
708+
plainHTTPUrl := *r.URL
709+
plainHTTPUrl.Scheme = "http"
710+
711+
plainHTTPRequest := *r
712+
plainHTTPRequest.URL = &plainHTTPUrl
713+
714+
return f.RoundTripper.RoundTrip(&plainHTTPRequest)
715+
}
716+
717+
return resp, err
718+
}

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"
@@ -328,6 +329,36 @@ func TestHostTLSFailureFallbackResolver(t *testing.T) {
328329
runBasicTest(t, "testname", sf)
329330
}
330331

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

0 commit comments

Comments
 (0)