diff --git a/README.md b/README.md index 1df195f7788..3ab0ec06133 100644 --- a/README.md +++ b/README.md @@ -448,6 +448,7 @@ Show the nerdctl version information - :nerd_face: `--cni-netconfpath`: CNI netconf path (default: `/etc/cni/net.d`) [`$NETCONFPATH`] - :nerd_face: `--data-root`: nerdctl data root, e.g. "/var/lib/nerdctl" - :nerd_face: `--cgroup-manager=(cgroupfs|systemd)`: cgroup manager +- :nerd_face: `--insecure-registry`: skips verifying HTTPS certs, and allows falling back to plain HTTP ## Unimplemented Docker commands Container management: diff --git a/Vagrantfile b/Vagrantfile index ae1e248d668..354dd4170c7 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -9,6 +9,8 @@ Vagrant.configure("2") do |config| config.vm.provider :virtualbox do |v| v.memory = memory v.cpus = cpus + # The default CIDR conflicts with slirp4netns CIDR (10.0.2.0/24) + v.customize ["modifyvm", :id, "--natnet1", "192.168.42.0/24"] end config.vm.provider :libvirt do |v| v.memory = memory diff --git a/docs/registry.md b/docs/registry.md index 1d5d344277b..45b335d50b7 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -3,3 +3,19 @@ nerdctl uses `${DOCKER_CONFIG}/config.json` for the authentication with image registries. `$DOCKER_CONFIG` defaults to `$HOME/.docker`. + +## Using insecure registry + +If you face `http: server gave HTTP response to HTTPS client` and you cannot configure TLS for the registry, try `--insecure-registry` flag: + +e.g., +```console +$ nerdctl --insecure-registry run --rm 192.168.12.34:5000/foo +``` + +## Accessing 127.0.0.1 from rootless nerdctl + +Currently, rootless nerdctl cannot pull images from 127.0.0.1, because +the pull operation occurs in RootlessKit's network namespace. + +See https://github.com/AkihiroSuda/nerdctl/issues/86 for the discussion about workarounds. diff --git a/main.go b/main.go index 651c9235f09..c5af8d28734 100644 --- a/main.go +++ b/main.go @@ -110,6 +110,10 @@ func newApp() *cli.App { Usage: "Cgroup manager to use (\"cgroupfs\"|\"systemd\")", Value: ncdefaults.CgroupManager(), }, + &cli.BoolFlag{ + Name: "insecure-registry", + Usage: "skips verifying HTTPS certs, and allows falling back to plain HTTP", + }, } app.Before = func(clicontext *cli.Context) error { if debug { diff --git a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go index ee1791483ae..089f6b5f098 100644 --- a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go +++ b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go @@ -18,7 +18,9 @@ package dockerconfigresolver import ( + "crypto/tls" "net" + "net/http" "net/url" "github.com/containerd/containerd/remotes" @@ -29,12 +31,77 @@ import ( "github.com/sirupsen/logrus" ) +type opts struct { + plainHTTP bool + skipVerifyCerts bool +} + +// Opt for New +type Opt func(*opts) + +// WithPlainHTTP enables insecure plain HTTP +func WithPlainHTTP(b bool) Opt { + return func(o *opts) { + o.plainHTTP = b + } +} + +// WithSkipVerifyCerts skips verifying TLS certs +func WithSkipVerifyCerts(b bool) Opt { + return func(o *opts) { + o.skipVerifyCerts = b + } +} + // New instantiates a resolver using $DOCKER_CONFIG/config.json . // // $DOCKER_CONFIG defaults to "~/.docker". // // refHostname is like "docker.io". -func New(refHostname string) (remotes.Resolver, error) { +func New(refHostname string, optFuncs ...Opt) (remotes.Resolver, error) { + var o opts + for _, of := range optFuncs { + of(&o) + } + var authzOpts []docker.AuthorizerOpt + if authCreds, err := NewAuthCreds(refHostname); err != nil { + return nil, err + } else { + authzOpts = append(authzOpts, docker.WithAuthCreds(authCreds)) + } + authz := docker.NewDockerAuthorizer(authzOpts...) + plainHTTPFunc := docker.MatchLocalhost + if o.plainHTTP { + plainHTTPFunc = docker.MatchAllHosts + } + regOpts := []docker.RegistryOpt{ + docker.WithAuthorizer(authz), + docker.WithPlainHTTP(plainHTTPFunc), + } + if o.skipVerifyCerts { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{ + Transport: tr, + } + regOpts = append(regOpts, docker.WithClient(client)) + } + resovlerOpts := docker.ResolverOptions{ + Hosts: docker.ConfigureDefaultRegistries(regOpts...), + } + resolver := docker.NewResolver(resovlerOpts) + return resolver, nil +} + +// AuthCreds is for docker.WithAuthCreds +type AuthCreds func(string) (string, string, error) + +// NewAuthCreds returns AuthCreds that uses $DOCKER_CONFIG/config.json . +// AuthCreds can be nil. +func NewAuthCreds(refHostname string) (AuthCreds, error) { // Load does not raise an error on ENOENT dockerConfigFile, err := dockercliconfig.Load("") if err != nil { @@ -48,7 +115,7 @@ func New(refHostname string) (remotes.Resolver, error) { return nil, err } - var credFunc func(string) (string, string, error) + var credFunc AuthCreds authConfigHostnames := []string{refHostname} if refHostname == "docker.io" || refHostname == "registry-1.docker.io" { @@ -110,13 +177,8 @@ func New(refHostname string) (remotes.Resolver, error) { } } } - - resovlerOpts := docker.ResolverOptions{ - // FIXME: Credentials is deprecated - Credentials: credFunc, - } - resolver := docker.NewResolver(resovlerOpts) - return resolver, nil + // credsFunc can be nil here + return credFunc, nil } func isAuthConfigEmpty(ac dockercliconfigtypes.AuthConfig) bool { diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go index 6883d76b509..a12fa2e7e88 100644 --- a/pkg/imgutil/imgutil.go +++ b/pkg/imgutil/imgutil.go @@ -26,6 +26,7 @@ import ( "github.com/AkihiroSuda/nerdctl/pkg/imgutil/pull" "github.com/containerd/containerd" refdocker "github.com/containerd/containerd/reference/docker" + "github.com/containerd/containerd/remotes" "github.com/containerd/stargz-snapshotter/fs/source" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -41,7 +42,10 @@ type EnsuredImage struct { // PullMode is either one of "always", "missing", "never" type PullMode = string -func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter, rawRef string, mode PullMode) (*EnsuredImage, error) { +// EnsureImage ensures the image. +// +// When insecure is set, skips verifying certs, and also falls back to HTTP when the registry does not speak HTTPS +func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter, rawRef string, mode PullMode, insecure bool) (*EnsuredImage, error) { named, err := refdocker.ParseDockerRef(rawRef) if err != nil { return nil, err @@ -70,11 +74,50 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write return nil, errors.Errorf("image %q is not available", rawRef) } - resolver, err := dockerconfigresolver.New(refdocker.Domain(named)) + refDomain := refdocker.Domain(named) + + var dOpts []dockerconfigresolver.Opt + if insecure { + logrus.Warnf("skipping verifying HTTPS certs for %q", refDomain) + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + resolver, err := dockerconfigresolver.New(refDomain, dOpts...) if err != nil { return nil, err } + img, err := pullImage(ctx, client, stdout, snapshotter, resolver, ref) + if err != nil { + if !IsErrHTTPResponseToHTTPSClient(err) { + return nil, err + } + if insecure { + logrus.WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain) + dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) + resolver, err = dockerconfigresolver.New(refDomain, dOpts...) + if err != nil { + return nil, err + } + return pullImage(ctx, client, stdout, snapshotter, resolver, ref) + } else { + logrus.WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain) + logrus.Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") + return nil, err + } + } + return img, nil +} + +// IsErrHTTPResponseToHTTPSClient returns whether err is +// "http: server gave HTTP response to HTTPS client" +func IsErrHTTPResponseToHTTPSClient(err error) bool { + // The error string is unexposed as of Go 1.16, so we can't use `errors.Is`. + // https://github.com/golang/go/issues/44855 + const unexposed = "server gave HTTP response to HTTPS client" + return strings.Contains(err.Error(), unexposed) +} + +func pullImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter string, resolver remotes.Resolver, ref string) (*EnsuredImage, error) { ctx, done, err := client.WithLease(ctx) if err != nil { return nil, err @@ -109,6 +152,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write Remote: sgz, } return res, nil + } func isStargz(sn string) bool { diff --git a/pkg/imgutil/pull/pull.go b/pkg/imgutil/pull/pull.go index c863c0c2b56..e1c16f71039 100644 --- a/pkg/imgutil/pull/pull.go +++ b/pkg/imgutil/pull/pull.go @@ -73,6 +73,7 @@ func Pull(ctx context.Context, client *containerd.Client, ref string, config *Co } opts = append(opts, config.RemoteOpts...) + // client.Pull is for single-platform (TODO: support multi) img, err := client.Pull(pctx, ref, opts...) stopProgress() if err != nil { diff --git a/pkg/imgutil/push/push.go b/pkg/imgutil/push/push.go index fc046dd2368..3b2437d7c4a 100644 --- a/pkg/imgutil/push/push.go +++ b/pkg/imgutil/push/push.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/log" "github.com/containerd/containerd/pkg/progress" + "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -39,7 +40,8 @@ import ( "golang.org/x/sync/errgroup" ) -func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, stdout io.Writer, localRef, remoteRef string) error { +func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, stdout io.Writer, + localRef, remoteRef string, platform platforms.MatchComparer) error { img, err := client.ImageService().Get(ctx, localRef) if err != nil { return errors.Wrap(err, "unable to resolve image to manifest") @@ -66,6 +68,7 @@ func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resol return client.Push(ctx, remoteRef, desc, containerd.WithResolver(resolver), containerd.WithImageHandler(jobHandler), + containerd.WithPlatformMatcher(platform), ) }) diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 9144dcedf5d..68b17ba9ff5 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -214,4 +214,5 @@ const ( AlpineImage = "mirror.gcr.io/library/alpine:3.13" NginxAlpineImage = "mirror.gcr.io/library/nginx:1.19-alpine" NginxAlpineIndexHTMLSnippet = "