Prevent push and pull to v1 registries by filtering the available endpoints.

Add a daemon flag to control this behaviour.  Add a warning message when pulling
an image from a v1 registry.  The default order of pull is slightly altered
with this changset.

Previously it was:
https v2, https v1, http v2, http v1

now it is:
https v2, http v2, https v1, http v1

Prevent login to v1 registries by explicitly setting the version before ping to
prevent fallback to v1.

Add unit tests for v2 only mode.  Create a mock server that can register
handlers for various endpoints.  Assert no v1 endpoints are hit with legacy
registries disabled for the following commands:  pull, push, build, run and
login.  Assert the opposite when legacy registries are not disabled.

Signed-off-by: Richard Scothern <richard.scothern@gmail.com>
This commit is contained in:
Richard Scothern 2015-09-16 10:42:17 -07:00
parent 02eed8d666
commit 39f2f15a35
11 changed files with 385 additions and 103 deletions

View file

@ -60,6 +60,9 @@ func (p *v1Puller) Pull(tag string) (fallback bool, err error) {
// TODO(dmcgowan): Check if should fallback // TODO(dmcgowan): Check if should fallback
return false, err return false, err
} }
out := p.config.OutStream
out.Write(p.sf.FormatStatus("", "%s: this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.", p.repoInfo.CanonicalName))
return false, nil return false, nil
} }

View file

@ -32,10 +32,13 @@ func init() {
type DockerRegistrySuite struct { type DockerRegistrySuite struct {
ds *DockerSuite ds *DockerSuite
reg *testRegistryV2 reg *testRegistryV2
d *Daemon
} }
func (s *DockerRegistrySuite) SetUpTest(c *check.C) { func (s *DockerRegistrySuite) SetUpTest(c *check.C) {
testRequires(c, DaemonIsLinux)
s.reg = setupRegistry(c) s.reg = setupRegistry(c)
s.d = NewDaemon(c)
} }
func (s *DockerRegistrySuite) TearDownTest(c *check.C) { func (s *DockerRegistrySuite) TearDownTest(c *check.C) {
@ -45,6 +48,7 @@ func (s *DockerRegistrySuite) TearDownTest(c *check.C) {
if s.ds != nil { if s.ds != nil {
s.ds.TearDownTest(c) s.ds.TearDownTest(c)
} }
s.d.Stop()
} }
func init() { func init() {

View file

@ -0,0 +1,147 @@
package main
import (
"fmt"
"github.com/go-check/check"
"io/ioutil"
"net/http"
"os"
)
func makefile(contents string) (string, func(), error) {
cleanup := func() {
}
f, err := ioutil.TempFile(".", "tmp")
if err != nil {
return "", cleanup, err
}
err = ioutil.WriteFile(f.Name(), []byte(contents), os.ModePerm)
if err != nil {
return "", cleanup, err
}
cleanup = func() {
err := os.Remove(f.Name())
if err != nil {
fmt.Println("Error removing tmpfile")
}
}
return f.Name(), cleanup, nil
}
// TestV2Only ensures that a daemon in v2-only mode does not
// attempt to contact any v1 registry endpoints.
func (s *DockerRegistrySuite) TestV2Only(c *check.C) {
reg, err := newTestRegistry(c)
if err != nil {
c.Fatal(err.Error())
}
reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
})
reg.registerHandler("/v1/.*", func(w http.ResponseWriter, r *http.Request) {
c.Fatal("V1 registry contacted")
})
repoName := fmt.Sprintf("%s/busybox", reg.hostport)
err = s.d.Start("--insecure-registry", reg.hostport, "--no-legacy-registry=true")
if err != nil {
c.Fatalf("Error starting daemon: %s", err.Error())
}
dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport))
if err != nil {
c.Fatalf("Unable to create test dockerfile")
}
defer cleanup()
s.d.Cmd("build", "--file", dockerfileName, ".")
s.d.Cmd("run", repoName)
s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport)
s.d.Cmd("tag", "busybox", repoName)
s.d.Cmd("push", repoName)
s.d.Cmd("pull", repoName)
}
// TestV1 starts a daemon in 'normal' mode
// and ensure v1 endpoints are hit for the following operations:
// login, push, pull, build & run
func (s *DockerRegistrySuite) TestV1(c *check.C) {
reg, err := newTestRegistry(c)
if err != nil {
c.Fatal(err.Error())
}
v2Pings := 0
reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) {
v2Pings++
// V2 ping 404 causes fallback to v1
w.WriteHeader(404)
})
v1Pings := 0
reg.registerHandler("/v1/_ping", func(w http.ResponseWriter, r *http.Request) {
v1Pings++
})
v1Logins := 0
reg.registerHandler("/v1/users/", func(w http.ResponseWriter, r *http.Request) {
v1Logins++
})
v1Repo := 0
reg.registerHandler("/v1/repositories/busybox/", func(w http.ResponseWriter, r *http.Request) {
v1Repo++
})
reg.registerHandler("/v1/repositories/busybox/images", func(w http.ResponseWriter, r *http.Request) {
v1Repo++
})
err = s.d.Start("--insecure-registry", reg.hostport, "--no-legacy-registry=false")
if err != nil {
c.Fatalf("Error starting daemon: %s", err.Error())
}
dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport))
if err != nil {
c.Fatalf("Unable to create test dockerfile")
}
defer cleanup()
s.d.Cmd("build", "--file", dockerfileName, ".")
if v1Repo == 0 {
c.Errorf("Expected v1 repository access after build")
}
repoName := fmt.Sprintf("%s/busybox", reg.hostport)
s.d.Cmd("run", repoName)
if v1Repo == 1 {
c.Errorf("Expected v1 repository access after run")
}
s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport)
if v1Logins == 0 {
c.Errorf("Expected v1 login attempt")
}
s.d.Cmd("tag", "busybox", repoName)
s.d.Cmd("push", repoName)
if v1Repo != 2 || v1Pings != 1 {
c.Error("Not all endpoints contacted after push", v1Repo, v1Pings)
}
s.d.Cmd("pull", repoName)
if v1Repo != 3 {
c.Errorf("Expected v1 repository access after pull")
}
}

View file

@ -0,0 +1,56 @@
package main
import (
"net/http"
"net/http/httptest"
"regexp"
"strings"
"sync"
"github.com/go-check/check"
)
type handlerFunc func(w http.ResponseWriter, r *http.Request)
type testRegistry struct {
server *httptest.Server
hostport string
handlers map[string]handlerFunc
mu sync.Mutex
}
func (tr *testRegistry) registerHandler(path string, h handlerFunc) {
tr.mu.Lock()
defer tr.mu.Unlock()
tr.handlers[path] = h
}
func newTestRegistry(c *check.C) (*testRegistry, error) {
testReg := &testRegistry{handlers: make(map[string]handlerFunc)}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url := r.URL.String()
var matched bool
var err error
for re, function := range testReg.handlers {
matched, err = regexp.MatchString(re, url)
if err != nil {
c.Fatalf("Error with handler regexp")
return
}
if matched {
function(w, r)
break
}
}
if !matched {
c.Fatal("Unable to match", url, "with regexp")
}
}))
testReg.server = ts
testReg.hostport = strings.Replace(ts.URL, "http://", "", 1)
return testReg, nil
}

View file

@ -44,6 +44,10 @@ var (
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
emptyServiceConfig = NewServiceConfig(nil) emptyServiceConfig = NewServiceConfig(nil)
// V2Only controls access to legacy registries. If it is set to true via the
// command line flag the daemon will not attempt to contact v1 legacy registries
V2Only = false
) )
// InstallFlags adds command-line options to the top-level flag parser for // InstallFlags adds command-line options to the top-level flag parser for
@ -53,6 +57,7 @@ func (options *Options) InstallFlags(cmd *flag.FlagSet, usageFn func(string) str
cmd.Var(&options.Mirrors, []string{"-registry-mirror"}, usageFn("Preferred Docker registry mirror")) cmd.Var(&options.Mirrors, []string{"-registry-mirror"}, usageFn("Preferred Docker registry mirror"))
options.InsecureRegistries = opts.NewListOpts(ValidateIndexName) options.InsecureRegistries = opts.NewListOpts(ValidateIndexName)
cmd.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, usageFn("Enable insecure registry communication")) cmd.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, usageFn("Enable insecure registry communication"))
cmd.BoolVar(&V2Only, []string{"-no-legacy-registry"}, false, "Do not contact legacy registries")
} }
type netIPNet net.IPNet type netIPNet net.IPNet

View file

@ -42,8 +42,9 @@ func scanForAPIVersion(address string) (string, APIVersion) {
return address, APIVersionUnknown return address, APIVersionUnknown
} }
// NewEndpoint parses the given address to return a registry endpoint. // NewEndpoint parses the given address to return a registry endpoint. v can be used to
func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) { // specify a specific endpoint version
func NewEndpoint(index *IndexInfo, metaHeaders http.Header, v APIVersion) (*Endpoint, error) {
tlsConfig, err := newTLSConfig(index.Name, index.Secure) tlsConfig, err := newTLSConfig(index.Name, index.Secure)
if err != nil { if err != nil {
return nil, err return nil, err
@ -52,6 +53,9 @@ func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if v != APIVersionUnknown {
endpoint.Version = v
}
if err := validateEndpoint(endpoint); err != nil { if err := validateEndpoint(endpoint); err != nil {
return nil, err return nil, err
} }
@ -111,11 +115,6 @@ func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header)
return endpoint, nil return endpoint, nil
} }
// GetEndpoint returns a new endpoint with the specified headers
func (repoInfo *RepositoryInfo) GetEndpoint(metaHeaders http.Header) (*Endpoint, error) {
return NewEndpoint(repoInfo.Index, metaHeaders)
}
// Endpoint stores basic information about a registry endpoint. // Endpoint stores basic information about a registry endpoint.
type Endpoint struct { type Endpoint struct {
client *http.Client client *http.Client

View file

@ -49,6 +49,10 @@ func init() {
httpVersion = append(httpVersion, useragent.VersionInfo{"arch", runtime.GOARCH}) httpVersion = append(httpVersion, useragent.VersionInfo{"arch", runtime.GOARCH})
dockerUserAgent = useragent.AppendVersions("", httpVersion...) dockerUserAgent = useragent.AppendVersions("", httpVersion...)
if runtime.GOOS != "linux" {
V2Only = true
}
} }
func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {

View file

@ -23,7 +23,7 @@ const (
func spawnTestRegistrySession(t *testing.T) *Session { func spawnTestRegistrySession(t *testing.T) *Session {
authConfig := &cliconfig.AuthConfig{} authConfig := &cliconfig.AuthConfig{}
endpoint, err := NewEndpoint(makeIndex("/v1/"), nil) endpoint, err := NewEndpoint(makeIndex("/v1/"), nil, APIVersionUnknown)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -50,7 +50,7 @@ func spawnTestRegistrySession(t *testing.T) *Session {
func TestPingRegistryEndpoint(t *testing.T) { func TestPingRegistryEndpoint(t *testing.T) {
testPing := func(index *IndexInfo, expectedStandalone bool, assertMessage string) { testPing := func(index *IndexInfo, expectedStandalone bool, assertMessage string) {
ep, err := NewEndpoint(index, nil) ep, err := NewEndpoint(index, nil, APIVersionUnknown)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -70,7 +70,7 @@ func TestPingRegistryEndpoint(t *testing.T) {
func TestEndpoint(t *testing.T) { func TestEndpoint(t *testing.T) {
// Simple wrapper to fail test if err != nil // Simple wrapper to fail test if err != nil
expandEndpoint := func(index *IndexInfo) *Endpoint { expandEndpoint := func(index *IndexInfo) *Endpoint {
endpoint, err := NewEndpoint(index, nil) endpoint, err := NewEndpoint(index, nil, APIVersionUnknown)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -79,7 +79,7 @@ func TestEndpoint(t *testing.T) {
assertInsecureIndex := func(index *IndexInfo) { assertInsecureIndex := func(index *IndexInfo) {
index.Secure = true index.Secure = true
_, err := NewEndpoint(index, nil) _, err := NewEndpoint(index, nil, APIVersionUnknown)
assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index") assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index")
assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index") assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index")
index.Secure = false index.Secure = false
@ -87,7 +87,7 @@ func TestEndpoint(t *testing.T) {
assertSecureIndex := func(index *IndexInfo) { assertSecureIndex := func(index *IndexInfo) {
index.Secure = true index.Secure = true
_, err := NewEndpoint(index, nil) _, err := NewEndpoint(index, nil, APIVersionUnknown)
assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index") assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index")
assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index") assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index")
index.Secure = false index.Secure = false
@ -153,7 +153,7 @@ func TestEndpoint(t *testing.T) {
} }
for _, address := range badEndpoints { for _, address := range badEndpoints {
index.Name = address index.Name = address
_, err := NewEndpoint(index, nil) _, err := NewEndpoint(index, nil, APIVersionUnknown)
checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
} }
} }

View file

@ -2,15 +2,11 @@ package registry
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"runtime"
"strings"
"github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth"
"github.com/docker/docker/cliconfig" "github.com/docker/docker/cliconfig"
"github.com/docker/docker/pkg/tlsconfig"
) )
// Service is a registry service. It tracks configuration data such as a list // Service is a registry service. It tracks configuration data such as a list
@ -40,7 +36,14 @@ func (s *Service) Auth(authConfig *cliconfig.AuthConfig) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
endpoint, err := NewEndpoint(index, nil)
endpointVersion := APIVersion(APIVersionUnknown)
if V2Only {
// Override the endpoint to only attempt a v2 ping
endpointVersion = APIVersion2
}
endpoint, err := NewEndpoint(index, nil, endpointVersion)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -57,10 +60,11 @@ func (s *Service) Search(term string, authConfig *cliconfig.AuthConfig, headers
} }
// *TODO: Search multiple indexes. // *TODO: Search multiple indexes.
endpoint, err := repoInfo.GetEndpoint(http.Header(headers)) endpoint, err := NewEndpoint(repoInfo.Index, http.Header(headers), APIVersionUnknown)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r, err := NewSession(endpoint.client, authConfig, endpoint) r, err := NewSession(endpoint.client, authConfig, endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
@ -132,97 +136,20 @@ func (s *Service) LookupPushEndpoints(repoName string) (endpoints []APIEndpoint,
} }
func (s *Service) lookupEndpoints(repoName string) (endpoints []APIEndpoint, err error) { func (s *Service) lookupEndpoints(repoName string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault endpoints, err = s.lookupV2Endpoints(repoName)
tlsConfig := &cfg
if strings.HasPrefix(repoName, DefaultNamespace+"/") {
// v2 mirrors
for _, mirror := range s.Config.Mirrors {
mirrorTLSConfig, err := s.tlsConfigForMirror(mirror)
if err != nil {
return nil, err
}
endpoints = append(endpoints, APIEndpoint{
URL: mirror,
// guess mirrors are v2
Version: APIVersion2,
Mirror: true,
TrimHostname: true,
TLSConfig: mirrorTLSConfig,
})
}
// v2 registry
endpoints = append(endpoints, APIEndpoint{
URL: DefaultV2Registry,
Version: APIVersion2,
Official: true,
TrimHostname: true,
TLSConfig: tlsConfig,
})
if runtime.GOOS == "linux" { // do not inherit legacy API for OSes supported in the future
// v1 registry
endpoints = append(endpoints, APIEndpoint{
URL: DefaultV1Registry,
Version: APIVersion1,
Official: true,
TrimHostname: true,
TLSConfig: tlsConfig,
})
}
return endpoints, nil
}
slashIndex := strings.IndexRune(repoName, '/')
if slashIndex <= 0 {
return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName)
}
hostname := repoName[:slashIndex]
tlsConfig, err = s.TLSConfig(hostname)
if err != nil { if err != nil {
return nil, err return nil, err
} }
isSecure := !tlsConfig.InsecureSkipVerify
v2Versions := []auth.APIVersion{ if V2Only {
{ return endpoints, nil
Type: "registry",
Version: "2.0",
},
}
endpoints = []APIEndpoint{
{
URL: "https://" + hostname,
Version: APIVersion2,
TrimHostname: true,
TLSConfig: tlsConfig,
VersionHeader: DefaultRegistryVersionHeader,
Versions: v2Versions,
},
{
URL: "https://" + hostname,
Version: APIVersion1,
TrimHostname: true,
TLSConfig: tlsConfig,
},
} }
if !isSecure { legacyEndpoints, err := s.lookupV1Endpoints(repoName)
endpoints = append(endpoints, APIEndpoint{ if err != nil {
URL: "http://" + hostname, return nil, err
Version: APIVersion2,
TrimHostname: true,
// used to check if supposed to be secure via InsecureSkipVerify
TLSConfig: tlsConfig,
VersionHeader: DefaultRegistryVersionHeader,
Versions: v2Versions,
}, APIEndpoint{
URL: "http://" + hostname,
Version: APIVersion1,
TrimHostname: true,
// used to check if supposed to be secure via InsecureSkipVerify
TLSConfig: tlsConfig,
})
} }
endpoints = append(endpoints, legacyEndpoints...)
return endpoints, nil return endpoints, nil
} }

54
registry/service_v1.go Normal file
View file

@ -0,0 +1,54 @@
package registry
import (
"fmt"
"strings"
"github.com/docker/docker/pkg/tlsconfig"
)
func (s *Service) lookupV1Endpoints(repoName string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg
if strings.HasPrefix(repoName, DefaultNamespace+"/") {
endpoints = append(endpoints, APIEndpoint{
URL: DefaultV1Registry,
Version: APIVersion1,
Official: true,
TrimHostname: true,
TLSConfig: tlsConfig,
})
return endpoints, nil
}
slashIndex := strings.IndexRune(repoName, '/')
if slashIndex <= 0 {
return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName)
}
hostname := repoName[:slashIndex]
tlsConfig, err = s.TLSConfig(hostname)
if err != nil {
return nil, err
}
endpoints = []APIEndpoint{
{
URL: "https://" + hostname,
Version: APIVersion1,
TrimHostname: true,
TLSConfig: tlsConfig,
},
}
if tlsConfig.InsecureSkipVerify {
endpoints = append(endpoints, APIEndpoint{ // or this
URL: "http://" + hostname,
Version: APIVersion1,
TrimHostname: true,
// used to check if supposed to be secure via InsecureSkipVerify
TLSConfig: tlsConfig,
})
}
return endpoints, nil
}

83
registry/service_v2.go Normal file
View file

@ -0,0 +1,83 @@
package registry
import (
"fmt"
"strings"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/docker/pkg/tlsconfig"
)
func (s *Service) lookupV2Endpoints(repoName string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg
if strings.HasPrefix(repoName, DefaultNamespace+"/") {
// v2 mirrors
for _, mirror := range s.Config.Mirrors {
mirrorTLSConfig, err := s.tlsConfigForMirror(mirror)
if err != nil {
return nil, err
}
endpoints = append(endpoints, APIEndpoint{
URL: mirror,
// guess mirrors are v2
Version: APIVersion2,
Mirror: true,
TrimHostname: true,
TLSConfig: mirrorTLSConfig,
})
}
// v2 registry
endpoints = append(endpoints, APIEndpoint{
URL: DefaultV2Registry,
Version: APIVersion2,
Official: true,
TrimHostname: true,
TLSConfig: tlsConfig,
})
return endpoints, nil
}
slashIndex := strings.IndexRune(repoName, '/')
if slashIndex <= 0 {
return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName)
}
hostname := repoName[:slashIndex]
tlsConfig, err = s.TLSConfig(hostname)
if err != nil {
return nil, err
}
v2Versions := []auth.APIVersion{
{
Type: "registry",
Version: "2.0",
},
}
endpoints = []APIEndpoint{
{
URL: "https://" + hostname,
Version: APIVersion2,
TrimHostname: true,
TLSConfig: tlsConfig,
VersionHeader: DefaultRegistryVersionHeader,
Versions: v2Versions,
},
}
if tlsConfig.InsecureSkipVerify {
endpoints = append(endpoints, APIEndpoint{
URL: "http://" + hostname,
Version: APIVersion2,
TrimHostname: true,
// used to check if supposed to be secure via InsecureSkipVerify
TLSConfig: tlsConfig,
VersionHeader: DefaultRegistryVersionHeader,
Versions: v2Versions,
})
}
return endpoints, nil
}