Skip to content

Commit 79772a0

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]>
1 parent 3a3d5de commit 79772a0

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
@@ -121,8 +121,13 @@ func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHos
121121
hosts[len(hosts)-1].capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush
122122
}
123123

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

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

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

234251
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"
@@ -294,6 +295,30 @@ func TestLoadCertFiles(t *testing.T) {
294295
}
295296
}
296297

298+
func TestLocalhostHTTPFallback(t *testing.T) {
299+
opts := HostOptions{
300+
DefaultTLS: &tls.Config{
301+
InsecureSkipVerify: true,
302+
},
303+
}
304+
hosts := ConfigureHosts(context.TODO(), opts)
305+
306+
testHosts, err := hosts("localhost:8080")
307+
if err != nil {
308+
t.Fatal(err)
309+
}
310+
if len(testHosts) != 1 {
311+
t.Fatalf("expected a single host for localhost config, got %d hosts", len(testHosts))
312+
}
313+
if testHosts[0].Scheme != "https" {
314+
t.Fatalf("expected https scheme for localhost with tls config, got %q", testHosts[0].Scheme)
315+
}
316+
_, ok := testHosts[0].Client.Transport.(docker.HTTPFallback)
317+
if !ok {
318+
t.Fatal("expected http fallback configured for defaulted localhost endpoint")
319+
}
320+
}
321+
297322
func compareRegistryHost(j, k docker.RegistryHost) bool {
298323
if j.Scheme != k.Scheme {
299324
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)