Browse code

Smarter push/pull TLS fallback

With the --insecure-registry daemon option (or talking to a registry on
a local IP), the daemon will first try TLS, and then try plaintext if
something goes wrong with the push or pull. It doesn't make sense to try
plaintext if a HTTP request went through while using TLS. This commit
changes the logic to keep track of host/port combinations where a TLS
attempt managed to do at least one HTTP request (whether the response
code indicated success or not). If the host/port responded to a HTTP
using TLS, we won't try to make plaintext HTTP requests to it.

This will result in better error messages, which sometimes ended up
showing the result of the plaintext attempt, like this:

Error response from daemon: Get
http://myregistrydomain.com:5000/v2/: malformed HTTP response
"\x15\x03\x01\x00\x02\x02"

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2016/02/12 08:45:29
Showing 6 changed files
... ...
@@ -31,6 +31,10 @@ type fallbackError struct {
31 31
 	// supports the v2 protocol. This is used to limit fallbacks to the v1
32 32
 	// protocol.
33 33
 	confirmedV2 bool
34
+	// transportOK is set to true if we managed to speak HTTP with the
35
+	// registry. This confirms that we're using appropriate TLS settings
36
+	// (or lack of TLS).
37
+	transportOK bool
34 38
 }
35 39
 
36 40
 // Error renders the FallbackError as a string.
... ...
@@ -2,6 +2,7 @@ package distribution
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"net/url"
5 6
 
6 7
 	"github.com/Sirupsen/logrus"
7 8
 	"github.com/docker/docker/api"
... ...
@@ -109,12 +110,31 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
109 109
 		// confirm that it was talking to a v2 registry. This will
110 110
 		// prevent fallback to the v1 protocol.
111 111
 		confirmedV2 bool
112
+
113
+		// confirmedTLSRegistries is a map indicating which registries
114
+		// are known to be using TLS. There should never be a plaintext
115
+		// retry for any of these.
116
+		confirmedTLSRegistries = make(map[string]struct{})
112 117
 	)
113 118
 	for _, endpoint := range endpoints {
114 119
 		if confirmedV2 && endpoint.Version == registry.APIVersion1 {
115 120
 			logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
116 121
 			continue
117 122
 		}
123
+
124
+		parsedURL, urlErr := url.Parse(endpoint.URL)
125
+		if urlErr != nil {
126
+			logrus.Errorf("Failed to parse endpoint URL %s", endpoint.URL)
127
+			continue
128
+		}
129
+
130
+		if parsedURL.Scheme != "https" {
131
+			if _, confirmedTLS := confirmedTLSRegistries[parsedURL.Host]; confirmedTLS {
132
+				logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
133
+				continue
134
+			}
135
+		}
136
+
118 137
 		logrus.Debugf("Trying to pull %s from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version)
119 138
 
120 139
 		puller, err := newPuller(endpoint, repoInfo, imagePullConfig)
... ...
@@ -132,6 +152,9 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
132 132
 				if fallbackErr, ok := err.(fallbackError); ok {
133 133
 					fallback = true
134 134
 					confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
135
+					if fallbackErr.transportOK && parsedURL.Scheme == "https" {
136
+						confirmedTLSRegistries[parsedURL.Host] = struct{}{}
137
+					}
135 138
 					err = fallbackErr.err
136 139
 				}
137 140
 			}
... ...
@@ -62,7 +62,7 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
62 62
 	p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")
63 63
 	if err != nil {
64 64
 		logrus.Warnf("Error getting v2 registry: %v", err)
65
-		return fallbackError{err: err, confirmedV2: p.confirmedV2}
65
+		return err
66 66
 	}
67 67
 
68 68
 	if err = p.pullV2Repository(ctx, ref); err != nil {
... ...
@@ -71,7 +71,11 @@ func (p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
71 71
 		}
72 72
 		if continueOnError(err) {
73 73
 			logrus.Errorf("Error trying v2 registry: %v", err)
74
-			return fallbackError{err: err, confirmedV2: p.confirmedV2}
74
+			return fallbackError{
75
+				err:         err,
76
+				confirmedV2: p.confirmedV2,
77
+				transportOK: true,
78
+			}
75 79
 		}
76 80
 	}
77 81
 	return err
... ...
@@ -716,12 +720,20 @@ func allowV1Fallback(err error) error {
716 716
 	case errcode.Errors:
717 717
 		if len(v) != 0 {
718 718
 			if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) {
719
-				return fallbackError{err: err, confirmedV2: false}
719
+				return fallbackError{
720
+					err:         err,
721
+					confirmedV2: false,
722
+					transportOK: true,
723
+				}
720 724
 			}
721 725
 		}
722 726
 	case errcode.Error:
723 727
 		if shouldV2Fallback(v) {
724
-			return fallbackError{err: err, confirmedV2: false}
728
+			return fallbackError{
729
+				err:         err,
730
+				confirmedV2: false,
731
+				transportOK: true,
732
+			}
725 733
 		}
726 734
 	case *url.Error:
727 735
 		if v.Err == auth.ErrNoBasicAuthCredentials {
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"compress/gzip"
6 6
 	"fmt"
7 7
 	"io"
8
+	"net/url"
8 9
 
9 10
 	"github.com/Sirupsen/logrus"
10 11
 	"github.com/docker/docker/distribution/metadata"
... ...
@@ -119,6 +120,11 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
119 119
 		// confirm that it was talking to a v2 registry. This will
120 120
 		// prevent fallback to the v1 protocol.
121 121
 		confirmedV2 bool
122
+
123
+		// confirmedTLSRegistries is a map indicating which registries
124
+		// are known to be using TLS. There should never be a plaintext
125
+		// retry for any of these.
126
+		confirmedTLSRegistries = make(map[string]struct{})
122 127
 	)
123 128
 
124 129
 	for _, endpoint := range endpoints {
... ...
@@ -127,6 +133,19 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
127 127
 			continue
128 128
 		}
129 129
 
130
+		parsedURL, urlErr := url.Parse(endpoint.URL)
131
+		if urlErr != nil {
132
+			logrus.Errorf("Failed to parse endpoint URL %s", endpoint.URL)
133
+			continue
134
+		}
135
+
136
+		if parsedURL.Scheme != "https" {
137
+			if _, confirmedTLS := confirmedTLSRegistries[parsedURL.Host]; confirmedTLS {
138
+				logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
139
+				continue
140
+			}
141
+		}
142
+
130 143
 		logrus.Debugf("Trying to push %s to %s %s", repoInfo.FullName(), endpoint.URL, endpoint.Version)
131 144
 
132 145
 		pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig)
... ...
@@ -142,6 +161,9 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
142 142
 			default:
143 143
 				if fallbackErr, ok := err.(fallbackError); ok {
144 144
 					confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
145
+					if fallbackErr.transportOK && parsedURL.Scheme == "https" {
146
+						confirmedTLSRegistries[parsedURL.Host] = struct{}{}
147
+					}
145 148
 					err = fallbackErr.err
146 149
 					lastErr = err
147 150
 					logrus.Errorf("Attempting next endpoint for push after error: %v", err)
... ...
@@ -64,12 +64,16 @@ func (p *v2Pusher) Push(ctx context.Context) (err error) {
64 64
 	p.repo, p.pushState.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull")
65 65
 	if err != nil {
66 66
 		logrus.Debugf("Error getting v2 registry: %v", err)
67
-		return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
67
+		return err
68 68
 	}
69 69
 
70 70
 	if err = p.pushV2Repository(ctx); err != nil {
71 71
 		if continueOnError(err) {
72
-			return fallbackError{err: err, confirmedV2: p.pushState.confirmedV2}
72
+			return fallbackError{
73
+				err:         err,
74
+				confirmedV2: p.pushState.confirmedV2,
75
+				transportOK: true,
76
+			}
73 77
 		}
74 78
 	}
75 79
 	return err
... ...
@@ -60,14 +60,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
60 60
 	endpointStr := strings.TrimRight(endpoint.URL, "/") + "/v2/"
61 61
 	req, err := http.NewRequest("GET", endpointStr, nil)
62 62
 	if err != nil {
63
-		return nil, false, err
63
+		return nil, false, fallbackError{err: err}
64 64
 	}
65 65
 	resp, err := pingClient.Do(req)
66 66
 	if err != nil {
67
-		return nil, false, err
67
+		return nil, false, fallbackError{err: err}
68 68
 	}
69 69
 	defer resp.Body.Close()
70 70
 
71
+	// We got a HTTP request through, so we're using the right TLS settings.
72
+	// From this point forward, set transportOK to true in any fallbackError
73
+	// we return.
74
+
71 75
 	v2Version := auth.APIVersion{
72 76
 		Type:    "registry",
73 77
 		Version: "2.0",
... ...
@@ -87,7 +91,11 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
87 87
 
88 88
 	challengeManager := auth.NewSimpleChallengeManager()
89 89
 	if err := challengeManager.AddResponse(resp); err != nil {
90
-		return nil, foundVersion, err
90
+		return nil, foundVersion, fallbackError{
91
+			err:         err,
92
+			confirmedV2: foundVersion,
93
+			transportOK: true,
94
+		}
91 95
 	}
92 96
 
93 97
 	if authConfig.RegistryToken != "" {
... ...
@@ -103,11 +111,22 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
103 103
 
104 104
 	repoNameRef, err := distreference.ParseNamed(repoName)
105 105
 	if err != nil {
106
-		return nil, foundVersion, err
106
+		return nil, foundVersion, fallbackError{
107
+			err:         err,
108
+			confirmedV2: foundVersion,
109
+			transportOK: true,
110
+		}
107 111
 	}
108 112
 
109 113
 	repo, err = client.NewRepository(ctx, repoNameRef, endpoint.URL, tr)
110
-	return repo, foundVersion, err
114
+	if err != nil {
115
+		err = fallbackError{
116
+			err:         err,
117
+			confirmedV2: foundVersion,
118
+			transportOK: true,
119
+		}
120
+	}
121
+	return
111 122
 }
112 123
 
113 124
 type existingTokenHandler struct {