Compare commits

...

20 Commits

Author SHA1 Message Date
Sebastiaan van Stijn
662f78c0b1 Merge pull request #48090 from thaJeztah/27.0_backport_48067_fix_specific_ipv6_portmap_proxy_to_ipv4
[27.0 backport] Fix incorrect validation of port mapping
2024-06-28 23:16:49 +02:00
Sebastiaan van Stijn
b86d9bdaf3 Merge pull request #48086 from thaJeztah/27.0_backport_fix_rootless_pull
[27.0 backport] daemon/graphdriver/overlay2: set TarOptions.InUserNS for native differ (fix "failed to Lchown "/dev/console")
2024-06-28 22:40:07 +02:00
Sebastiaan van Stijn
0dbc3ac59e Merge pull request #48087 from thaJeztah/27.0_backport_gofmt
[27.0 backport] fix some gofmt issues reported by goreportcard
2024-06-28 21:11:01 +02:00
Rob Murray
276a648ec3 Fix incorrect validation of port mapping
Regression introduced in 01eecb6.

A port mapping from a specific IPv6 host address can be used
by a container on an IPv4-only network, docker-proxy makes the
connection.

Signed-off-by: Rob Murray <rob.murray@docker.com>
(cherry picked from commit dfbcddb9f5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 21:07:29 +02:00
Sebastiaan van Stijn
22aa07b28f Merge pull request #48089 from robmry/backport-27.0/48069_fix_overlapping_subnets
[27.0 backport] Fix duplicate subnet allocations
2024-06-28 18:26:59 +02:00
Rob Murray
23b8b023dd Fix duplicate subnet allocations
Keep allocated subnets in-order, so that they're not mistakenly
reallocated due to a gap in the list where misplaced subnets should
have been.

Introduced in 9d288b5.

The iterator over allocated subnets was incremented too early, this
change moves it past three clauses in addrSpace.allocatePredefinedPool().
The three new unit tests correspond to a separate failure caused by
incrementing before each of them.

(cherry picked from commit 4de54ee14c)
Signed-off-by: Rob Murray <rob.murray@docker.com>
2024-06-28 16:24:47 +01:00
Sebastiaan van Stijn
bf222d635b fix some gofmt issues reported by goreportcard
https://goreportcard.com/report/github.com/docker/docker

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6ada1cff02)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 16:48:01 +02:00
Sebastiaan van Stijn
f8231b52d3 daemon/graphdriver/overlay2: set TarOptions.InUserNS for native differ
Commits b2fd67de77 (and the follow-up commit
f6b80253b8) updated doesSupportNativeDiff to
detect whether the host can support native overlay diffing with userns
enabled.

As a result, [useNaiveDiff] would now return "false" in cases where it
previously would return "true" (and thus skip). However, [overlay2],
unlike [fuse-overlay] did not take user namespaces into account, when
using the native differ, and it therefore did not set the InUserNS option
in TarOptions.

As a result pkg/archive.createTarFile would attempt tocreate [device-nodes]
through [handleTarTypeBlockCharFifo] which would fail, but the resulting
error `EPERM` would be discarded, and `createTarFile` would not return
early, therefor attempting to [os.LChown] the missing file, ultimately
resulting in an error:

    failed to Lchown "/dev/console" for UID 0, GID 0: lchown /dev/console: no such file or directory

This patch fixes the missing option in overlay.

[useNaiveDiff]: 47eebd718f/daemon/graphdriver/overlay2/overlay.go (L248-L256)
[overlay2]: 47eebd718f/daemon/graphdriver/overlay2/overlay.go (L684-L689)
[fuse-overlay]: 47eebd718f/daemon/graphdriver/fuse-overlayfs/fuseoverlayfs.go (L456-L462)
[device-nodes]: ff1e2c0de7/pkg/archive/archive.go (L713-L720)
[handleTarTypeBlockCharFifo]: 47eebd718f/pkg/archive/archive_unix.go (L110-L114)
[os.LChown]: ff1e2c0de7/pkg/archive/archive.go (L762-L773)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6521057bb2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 16:31:54 +02:00
Sebastiaan van Stijn
b951474404 pkg/archive: createTarFile: consistently use the same value for userns
createTarFile accepts a opts (TarOptions) argument to specify whether
userns is enabled; whe should consider always detecting locally, but
at least make sure we're consistently working with the same value within
this function.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 969993a729)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 16:31:53 +02:00
Sebastiaan van Stijn
c5794e23ec pkg/archive: handleTarTypeBlockCharFifo: don't discard EPERM errors
This function was discarding EPERM errors if it detected that userns was
enabled; move such checks to the caller-site, so that they can decide
how to handle the error (which, in case of userns may be to log and ignore).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 379ce56cd8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 16:31:53 +02:00
Sebastiaan van Stijn
02e24483be pkg/archive: getWhiteoutConverter: don't error with userns enabled
Since 838047a1f5, the overlayWhiteoutConverter
is supported with userns enabled, so we no longer need this check.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit af85e47343)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 16:31:53 +02:00
Sebastiaan van Stijn
b70040a8fc Merge pull request #48074 from vvoland/v27.0-48073
[27.0 backport] Dockerfile: update compose to v2.28.1, update cli to v27.0.2
2024-06-27 18:00:44 +02:00
Paweł Gronowski
838330bac3 Dockerfile: update docker CLI to v27.0.2
Update the Docker CLI used in the dev-container

full diff: https://github.com/docker/cli/compare/v26.1.0...v27.0.2

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 3928165cf7)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-27 14:55:28 +02:00
Paweł Gronowski
e419e22f29 Dockerfile: update compose to v2.28.1
Update the compose cli plugin used in the dev-container

full diff: https://github.com/docker/cli/compare/v2.27.1...v2.28.1

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 790035f754)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-27 14:55:26 +02:00
Paweł Gronowski
e953d76450 Merge pull request #48060 from thaJeztah/27.0_backport_api_deprecate_ContainerJSONBase_Node
[27.0 backport] api/types: deprecate ContainerJSONBase.Node, ContainerNode
2024-06-26 20:30:43 +02:00
Paweł Gronowski
861fde8cc9 Merge pull request #48061 from thaJeztah/27_backport_bump_golangci_lint
[27.0 backport] update golangci-lint to v1.59.1
2024-06-26 19:14:38 +02:00
Sebastiaan van Stijn
3557077867 update golangci-lint to v1.59.1
full diff: https://github.com/golangci/golangci-lint/compare/v1.55.2...v1.59.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 95fae036ae)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-26 14:09:41 +02:00
Sebastiaan van Stijn
c95b917167 pkg/archive: reformat code to make #nosec comment work again
Looks like the way it picks up #nosec comments changed, causing the
linter error to re-appear;

    pkg/archive/archive_linux.go:57:17: G305: File traversal when extracting zip/tar archive (gosec)
                    Name:       filepath.Join(hdr.Name, WhiteoutOpaqueDir),
                                ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d4160d5aa7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-26 14:09:41 +02:00
Sebastiaan van Stijn
c0ff08acbd builder/remotecontext: reformat code to make #nosec comment work again
Looks like the way it picks up #nosec comments changed, causing the
linter error to re-appear;

    builder/remotecontext/remote.go:48:17: G107: Potential HTTP request made with variable url (gosec)
        if resp, err = http.Get(address); err != nil {
                       ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 04bf0e3d69)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-26 14:09:41 +02:00
Sebastiaan van Stijn
4587688258 api/types: deprecate ContainerJSONBase.Node, ContainerNode
The `Node` field and related `ContainerNode` type were used by the classic
(standalone) Swarm API. API documentation for this field was already removed
in 234d5a78fe (API 1.41 / docker 20.10), and
as the Docker Engine didn't implement these fields for the Swarm API, it
would always have been unset / nil.

Let's do a quick deprecation, and remove it on the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1fc9236119)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-26 14:05:47 +02:00
19 changed files with 245 additions and 129 deletions

View File

@@ -8,12 +8,12 @@ ARG XX_VERSION=1.4.0
ARG VPNKIT_VERSION=0.5.0
ARG DOCKERCLI_REPOSITORY="https://github.com/docker/cli.git"
ARG DOCKERCLI_VERSION=v26.1.0
ARG DOCKERCLI_VERSION=v27.0.2
# cli version used for integration-cli tests
ARG DOCKERCLI_INTEGRATION_REPOSITORY="https://github.com/docker/cli.git"
ARG DOCKERCLI_INTEGRATION_VERSION=v17.06.2-ce
ARG BUILDX_VERSION=0.15.1
ARG COMPOSE_VERSION=v2.27.1
ARG COMPOSE_VERSION=v2.28.1
ARG SYSTEMD="false"
ARG DOCKER_STATIC=1
@@ -229,7 +229,7 @@ FROM binary-dummy AS containerd-windows
FROM containerd-${TARGETOS} AS containerd
FROM base AS golangci_lint
ARG GOLANGCI_LINT_VERSION=v1.55.2
ARG GOLANGCI_LINT_VERSION=v1.59.1
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
GOBIN=/build/ GO111MODULE=on go install "github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}" \

View File

@@ -53,7 +53,7 @@ func TestAdjustForAPIVersion(t *testing.T) {
Target: "/bar",
TmpfsOptions: &mount.TmpfsOptions{
Options: [][]string{
[]string{"exec"},
{"exec"},
},
},
},
@@ -73,7 +73,7 @@ func TestAdjustForAPIVersion(t *testing.T) {
adjustForAPIVersion("1.46", spec)
if !reflect.DeepEqual(
spec.TaskTemplate.ContainerSpec.Mounts[0].TmpfsOptions.Options,
[][]string{[]string{"exec"}},
[][]string{{"exec"}},
) {
t.Error("TmpfsOptions.Options was stripped from spec")
}

View File

@@ -245,18 +245,6 @@ type ContainerState struct {
Health *Health `json:",omitempty"`
}
// ContainerNode stores information about the node that a container
// is running on. It's only used by the Docker Swarm standalone API
type ContainerNode struct {
ID string
IPAddress string `json:"IP"`
Addr string
Name string
Cpus int
Memory int64
Labels map[string]string
}
// ContainerJSONBase contains response of Engine API:
// GET "/containers/{name:.*}/json"
type ContainerJSONBase struct {
@@ -270,7 +258,7 @@ type ContainerJSONBase struct {
HostnamePath string
HostsPath string
LogPath string
Node *ContainerNode `json:",omitempty"` // Node is only propagated by Docker Swarm standalone API
Node *ContainerNode `json:",omitempty"` // Deprecated: Node was only propagated by Docker Swarm standalone API. It sill be removed in the next release.
Name string
RestartCount int
Driver string

View File

@@ -194,3 +194,17 @@ type ImageImportSource image.ImportSource
//
// Deprecated: use [image.LoadResponse].
type ImageLoadResponse = image.LoadResponse
// ContainerNode stores information about the node that a container
// is running on. It's only used by the Docker Swarm standalone API.
//
// Deprecated: ContainerNode was used for the classic Docker Swarm standalone API. It will be removed in the next release.
type ContainerNode struct {
ID string
IPAddress string `json:"IP"`
Addr string
Name string
Cpus int
Memory int64
Labels map[string]string
}

View File

@@ -44,8 +44,8 @@ func downloadRemote(remoteURL string) (string, io.ReadCloser, error) {
// GetWithStatusError does an http.Get() and returns an error if the
// status code is 4xx or 5xx.
func GetWithStatusError(address string) (resp *http.Response, err error) {
// #nosec G107
if resp, err = http.Get(address); err != nil {
resp, err = http.Get(address) // #nosec G107 -- ignore G107: Potential HTTP request made with variable url
if err != nil {
if uerr, ok := err.(*url.Error); ok {
if derr, ok := uerr.Err.(*net.DNSError); ok && !derr.IsTimeout {
return nil, errdefs.NotFound(err)

View File

@@ -83,55 +83,3 @@ func TestContainerInspect(t *testing.T) {
t.Fatalf("expected `name`, got %s", r.Name)
}
}
// TestContainerInspectNode tests that the "Node" field is included in the "inspect"
// output. This information is only present when connected to a Swarm standalone API.
func TestContainerInspectNode(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
content, err := json.Marshal(types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "container_id",
Image: "image",
Name: "name",
Node: &types.ContainerNode{
ID: "container_node_id",
Addr: "container_node",
Labels: map[string]string{"foo": "bar"},
},
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
r, err := client.ContainerInspect(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
if r.ID != "container_id" {
t.Fatalf("expected `container_id`, got %s", r.ID)
}
if r.Image != "image" {
t.Fatalf("expected `image`, got %s", r.Image)
}
if r.Name != "name" {
t.Fatalf("expected `name`, got %s", r.Name)
}
if r.Node.ID != "container_node_id" {
t.Fatalf("expected `container_node_id`, got %s", r.Node.ID)
}
if r.Node.Addr != "container_node" {
t.Fatalf("expected `container_node`, got %s", r.Node.Addr)
}
foo, ok := r.Node.Labels["foo"]
if foo != "bar" || !ok {
t.Fatalf("expected `bar` for label `foo`")
}
}

View File

@@ -8,8 +8,8 @@ import (
func TestTmpfsOptionsToGRPC(t *testing.T) {
options := [][]string{
[]string{"noexec"},
[]string{"uid", "12345"},
{"noexec"},
{"uid", "12345"},
}
expected := `[["noexec"],["uid","12345"]]`
@@ -21,8 +21,8 @@ func TestTmpfsOptionsFromGRPC(t *testing.T) {
options := `[["noexec"],["uid","12345"]]`
expected := [][]string{
[]string{"noexec"},
[]string{"uid", "12345"},
{"noexec"},
{"uid", "12345"},
}
actual := tmpfsOptionsFromGRPC(options)

View File

@@ -167,7 +167,7 @@ func TestTmpfsConversion(t *testing.T) {
Target: "/bar",
Type: mount.TypeTmpfs,
TmpfsOptions: &mount.TmpfsOptions{
Options: [][]string{[]string{"exec"}},
Options: [][]string{{"exec"}},
},
},
},
@@ -190,7 +190,7 @@ func TestTmpfsConversion(t *testing.T) {
Target: "/bar",
Type: mount.TypeTmpfs,
TmpfsOptions: &mount.TmpfsOptions{
Options: [][]string{[]string{"noexec"}},
Options: [][]string{{"noexec"}},
},
},
},

View File

@@ -14,6 +14,7 @@ import (
"strings"
"sync"
"github.com/containerd/containerd/pkg/userns"
"github.com/containerd/continuity/fs"
"github.com/containerd/log"
"github.com/docker/docker/daemon/graphdriver"
@@ -678,7 +679,6 @@ func (d *Driver) ApplyDiff(id string, parent string, diff io.Reader) (size int64
return d.naiveDiff.ApplyDiff(id, parent, diff)
}
// never reach here if we are running in UserNS
applyDir := d.getDiffPath(id)
logger.Debugf("Applying tar in %s", applyDir)
@@ -686,6 +686,7 @@ func (d *Driver) ApplyDiff(id string, parent string, diff io.Reader) (size int64
if err := untar(diff, applyDir, &archive.TarOptions{
IDMap: d.idMap,
WhiteoutFormat: archive.OverlayWhiteoutFormat,
InUserNS: userns.RunningInUserNS(),
}); err != nil {
return 0, err
}

View File

@@ -108,10 +108,10 @@ func TestSaveOCI(t *testing.T) {
testCases := []testCase{
// Busybox by tagged name
testCase{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"},
{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"},
// Busybox by ID
testCase{image: inspectBusybox.ID},
{image: inspectBusybox.ID},
}
if testEnv.DaemonInfo.OSType != "windows" {

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"net/http"
"os/exec"
"regexp"
"runtime"
@@ -1055,3 +1056,38 @@ func TestPortMappedHairpin(t *testing.T) {
defer c.ContainerRemove(ctx, res.ContainerID, containertypes.RemoveOptions{Force: true})
assert.Check(t, is.Contains(res.Stderr.String(), "404 Not Found"))
}
// Check that a container on an IPv4-only network can have a port mapping
// from a specific IPv6 host address (using docker-proxy).
// Regression test for https://github.com/moby/moby/issues/48067 (which
// is about incorrectly reporting this as invalid config).
func TestProxy4To6(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "uses bridge network and docker-proxy")
skip.If(t, testEnv.IsRootless)
ctx := setupTest(t)
d := daemon.New(t)
d.StartWithBusybox(ctx, t)
defer d.Stop(t)
c := d.NewClientT(t)
defer c.Close()
const netName = "ipv4net"
network.CreateNoError(ctx, t, c, netName)
serverId := container.Run(ctx, t, c,
container.WithNetworkMode(netName),
container.WithExposedPorts("80"),
container.WithPortMap(nat.PortMap{"80": {{HostIP: "::1"}}}),
container.WithCmd("httpd", "-f"),
)
defer c.ContainerRemove(ctx, serverId, containertypes.RemoveOptions{Force: true})
inspect := container.Inspect(ctx, t, c, serverId)
hostPort := inspect.NetworkSettings.Ports["80/tcp"][0].HostPort
resp, err := http.Get("http://[::1]:" + hostPort)
assert.NilError(t, err)
assert.Check(t, is.Equal(resp.StatusCode, 404))
}

View File

@@ -60,9 +60,7 @@ func (n *bridgeNetwork) addPortMappings(
}
disableNAT4, disableNAT6 := n.getNATDisabled()
if err := validatePortBindings(cfg,
!disableNAT4 && len(containerIPv4) > 0,
!disableNAT6 && len(containerIPv6) > 0); err != nil {
if err := validatePortBindings(cfg, !disableNAT4, !disableNAT6, containerIPv6); err != nil {
return nil, err
}
@@ -157,27 +155,32 @@ const validationErrLimit = 6
// docker-proxy is used to forward between the default IPv6 address and the
// container's IPv4. So, simply disallowing a non-zero IPv6 default when NAT6
// is disabled for the network would be incorrect.)
func validatePortBindings(pbs []types.PortBinding, nat4, nat6 bool) error {
func validatePortBindings(pbs []types.PortBinding, nat4, nat6 bool, cIPv6 net.IP) error {
var errs []error
for i := range pbs {
pb := &pbs[i]
disallowHostPort := false
if !nat4 && len(pb.HostIP) > 0 && pb.HostIP.To4() != nil && !pb.HostIP.Equal(net.IPv4zero) {
// There's no NAT4, so don't allow a nonzero IPv4 host address in the mapping. The port will
// accessible via any host interface.
// be accessible via any host interface.
errs = append(errs,
fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use 0.0.0.0::%d to open port %d for IPv4-only",
pb, pb.Port, pb.Port))
// The mapping is IPv4-specific but there's no NAT4, so a host port would make no sense.
disallowHostPort = true
} else if !nat6 && len(pb.HostIP) > 0 && pb.HostIP.To4() == nil && !pb.HostIP.Equal(net.IPv6zero) {
// There's no NAT6, so don't allow an IPv6 host address in the mapping. The port will
// accessible via any host interface.
errs = append(errs,
fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use [::]::%d to open port %d for IPv6-only",
pb, pb.Port, pb.Port))
// The mapping is IPv6-specific but there's no NAT6, so a host port would make no sense.
disallowHostPort = true
// If the container has no IPv6 address, the userland proxy will proxy between the
// host's IPv6 address and the container's IPv4. So, even with no NAT6, it's ok for
// an IPv6 port mapping to include a specific host address or port.
if len(cIPv6) > 0 {
// There's no NAT6, so don't allow an IPv6 host address in the mapping. The port will
// accessible via any host interface.
errs = append(errs,
fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use [::]::%d to open port %d for IPv6-only",
pb, pb.Port, pb.Port))
// The mapping is IPv6-specific but there's no NAT6, so a host port would make no sense.
disallowHostPort = true
}
} else if !nat4 && !nat6 {
// There's no NAT, so it would make no sense to specify a host port.
disallowHostPort = true

View File

@@ -186,6 +186,7 @@ func TestValidatePortBindings(t *testing.T) {
name string
nat4 bool
nat6 bool
ctrIPv6 net.IP
pbs []types.PortBinding
expErrs []string
}{
@@ -196,7 +197,8 @@ func TestValidatePortBindings(t *testing.T) {
},
},
{
name: "no nat with addrs",
name: "no nat with addrs",
ctrIPv6: newIPNet(t, "fd2c:b48c:69fb::2/128").IP,
pbs: []types.PortBinding{
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, Port: 80},
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, Port: 80},
@@ -246,7 +248,16 @@ func TestValidatePortBindings(t *testing.T) {
},
},
{
name: "no nat and addrs and ports",
name: "nat4 and addrs and ports",
nat4: true,
pbs: []types.PortBinding{
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80},
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80},
},
},
{
name: "no nat and addrs and ports",
ctrIPv6: newIPNet(t, "fd2c:b48c:69fb::2/128").IP,
pbs: []types.PortBinding{
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80},
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80},
@@ -258,6 +269,17 @@ func TestValidatePortBindings(t *testing.T) {
"host port must not be specified in mapping [2001:db8::2]:8080:80/tcp because NAT is disabled",
},
},
{
name: "no nat no ctrIPv6 and addrs and ports",
pbs: []types.PortBinding{
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80},
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80},
},
expErrs: []string{
"NAT is disabled, omit host address in port mapping 233.252.0.2:8080:80/tcp, or use 0.0.0.0::80 to open port 80 for IPv4-only",
"host port must not be specified in mapping 233.252.0.2:8080:80/tcp because NAT is disabled",
},
},
{
name: "max errs reached",
pbs: []types.PortBinding{
@@ -283,7 +305,7 @@ func TestValidatePortBindings(t *testing.T) {
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := validatePortBindings(tc.pbs, tc.nat4, tc.nat6)
err := validatePortBindings(tc.pbs, tc.nat4, tc.nat6, tc.ctrIPv6)
if tc.expErrs == nil {
assert.Check(t, err)
} else {

View File

@@ -164,8 +164,6 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
pdf := aSpace.predefined[pdfID]
if allocated.Overlaps(pdf.Base) {
it.Inc()
if allocated.Bits() <= pdf.Base.Bits() {
// The current 'allocated' prefix is bigger than the 'pdf'
// network, thus the block is fully overlapped.
@@ -195,6 +193,8 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
return makeAlloc(afterPrev), nil
}
it.Inc()
if netiputil.LastAddr(allocated) == netiputil.LastAddr(pdf.Base) {
// The last address of the current 'allocated' prefix is the
// same as the last address of the 'pdf' network, it's fully

View File

@@ -1,6 +1,7 @@
package defaultipam
import (
"fmt"
"net/netip"
"testing"
@@ -387,3 +388,113 @@ func TestStaticAllocation(t *testing.T) {
netip.MustParsePrefix("192.168.3.0/24"),
}, cmpopts.EquateComparable(netip.Prefix{})))
}
// Regression test for https://github.com/moby/moby/issues/48069
func TestPoolAllocateAndRelease(t *testing.T) {
type testClosures struct {
alloc func(netname string)
release func(netname string)
}
testcases := []struct {
name string
predefined []*ipamutils.NetworkToSplit
reserved []netip.Prefix
calls []func(tcs testClosures)
tcs testClosures
}{
{
name: "allocate after reserved",
predefined: []*ipamutils.NetworkToSplit{
{Base: netip.MustParsePrefix("10.0.0.0/24"), Size: 24},
{Base: netip.MustParsePrefix("10.0.1.0/24"), Size: 24},
{Base: netip.MustParsePrefix("10.1.0.0/16"), Size: 24},
},
reserved: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/16"),
},
calls: []func(tcs testClosures){
func(tcs testClosures) { tcs.alloc("n1") },
func(tcs testClosures) { tcs.alloc("n2") },
},
},
{
name: "reallocate first subnet",
predefined: []*ipamutils.NetworkToSplit{
{Base: netip.MustParsePrefix("10.0.0.0/8"), Size: 24},
},
calls: []func(tcs testClosures){
func(tcs testClosures) { tcs.alloc("n1") },
func(tcs testClosures) { tcs.alloc("n2") },
func(tcs testClosures) { tcs.alloc("n3") },
func(tcs testClosures) { tcs.release("n1") },
func(tcs testClosures) { tcs.alloc("n4") },
func(tcs testClosures) { tcs.alloc("n5") },
},
},
{
name: "reallocate after release",
predefined: []*ipamutils.NetworkToSplit{
{Base: netip.MustParsePrefix("10.0.0.0/8"), Size: 24},
},
calls: []func(tcs testClosures){
func(tcs testClosures) { tcs.alloc("n1") },
func(tcs testClosures) { tcs.alloc("n2") },
func(tcs testClosures) { tcs.alloc("n3") },
func(tcs testClosures) { tcs.alloc("n4") },
func(tcs testClosures) { tcs.release("n2") },
func(tcs testClosures) { tcs.release("n3") },
func(tcs testClosures) { tcs.alloc("n5") },
func(tcs testClosures) { tcs.alloc("n6") },
func(tcs testClosures) { tcs.alloc("n7") },
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
as, err := newAddrSpace(tc.predefined)
assert.NilError(t, err)
subnetToNetname := map[netip.Prefix]string{}
netnameToSubnet := map[string]netip.Prefix{}
// To avoid passing as,subnetToNetname,netnameToSubnet into each of the
// functions in tc.calls (cluttering the list of testcases), create closures
// that use them and pass those.
tcs := testClosures{
// Allocate a pool for netname, check that a subnet is returned that
// isn't already allocated, and doesn't overlap with a reserved range.
alloc: func(netname string) {
subnet, err := as.allocatePredefinedPool(tc.reserved)
assert.NilError(t, err)
otherNetname, exists := subnetToNetname[subnet]
assert.Assert(t, !exists, fmt.Sprintf(
"subnet %s allocated to %s, reallocated for %s", subnet, otherNetname, netname))
for _, reserved := range tc.reserved {
assert.Assert(t, !reserved.Overlaps(subnet),
fmt.Sprintf("subnet %s allocated in reserved range %s", subnet, reserved))
}
subnetToNetname[subnet] = netname
netnameToSubnet[netname] = subnet
},
// Release a pool for a netname - the test must ensure netname currently
// has an allocated subnet.
release: func(netname string) {
subnet, ok := netnameToSubnet[netname]
assert.Assert(t, ok)
err := as.releaseSubnet(subnet, netip.Prefix{})
assert.NilError(t, err)
delete(netnameToSubnet, netname)
delete(subnetToNetname, subnet)
},
}
for _, f := range tc.calls {
f(tcs)
}
})
}
}

View File

@@ -20,7 +20,6 @@ import (
"syscall"
"time"
"github.com/containerd/containerd/pkg/userns"
"github.com/containerd/log"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/ioutils"
@@ -675,9 +674,11 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
inUserns, bestEffortXattrs bool
chownOpts *idtools.Identity
)
// TODO(thaJeztah): make opts a required argument.
if opts != nil {
Lchown = !opts.NoLchown
inUserns = opts.InUserNS
inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
chownOpts = opts.ChownOpts
bestEffortXattrs = opts.BestEffortXattrs
}
@@ -712,6 +713,7 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
case tar.TypeBlock, tar.TypeChar:
if inUserns { // cannot create devices in a userns
log.G(context.TODO()).WithFields(log.Fields{"path": path, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns")
return nil
}
// Handle this is an OS-specific way
@@ -722,6 +724,11 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
case tar.TypeFifo:
// Handle this is an OS-specific way
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
if inUserns && errors.Is(err, syscall.EPERM) {
// In most cases, cannot create a fifo if running in user namespace
log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": path, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
return nil
}
return err
}
@@ -765,7 +772,7 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
}
if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil {
msg := "failed to Lchown %q for UID %d, GID %d"
if errors.Is(err, syscall.EINVAL) && userns.RunningInUserNS() {
if inUserns && errors.Is(err, syscall.EINVAL) {
msg += " (try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)"
}
return errors.Wrapf(err, msg, path, hdr.Uid, hdr.Gid)
@@ -871,11 +878,6 @@ func NewTarballer(srcPath string, options *TarOptions) (*Tarballer, error) {
return nil, err
}
whiteoutConverter, err := getWhiteoutConverter(options.WhiteoutFormat, options.InUserNS)
if err != nil {
return nil, err
}
return &Tarballer{
// Fix the source path to work with long path names. This is a no-op
// on platforms other than Windows.
@@ -885,7 +887,7 @@ func NewTarballer(srcPath string, options *TarOptions) (*Tarballer, error) {
pipeReader: pipeReader,
pipeWriter: pipeWriter,
compressWriter: compressWriter,
whiteoutConverter: whiteoutConverter,
whiteoutConverter: getWhiteoutConverter(options.WhiteoutFormat),
}, nil
}
@@ -1080,10 +1082,7 @@ func Unpack(decompressedArchive io.Reader, dest string, options *TarOptions) err
defer pools.BufioReader32KPool.Put(trBuf)
var dirs []*tar.Header
whiteoutConverter, err := getWhiteoutConverter(options.WhiteoutFormat, options.InUserNS)
if err != nil {
return err
}
whiteoutConverter := getWhiteoutConverter(options.WhiteoutFormat)
// Iterate through the files in the archive.
loop:

View File

@@ -12,14 +12,11 @@ import (
"golang.org/x/sys/unix"
)
func getWhiteoutConverter(format WhiteoutFormat, inUserNS bool) (tarWhiteoutConverter, error) {
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
if format == OverlayWhiteoutFormat {
if inUserNS {
return nil, errors.New("specifying OverlayWhiteoutFormat is not allowed in userns")
}
return overlayWhiteoutConverter{}, nil
return overlayWhiteoutConverter{}
}
return nil, nil
return nil
}
type overlayWhiteoutConverter struct{}
@@ -54,7 +51,7 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os
wo = &tar.Header{
Typeflag: tar.TypeReg,
Mode: hdr.Mode & int64(os.ModePerm),
Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir),
Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
Size: 0,
Uid: hdr.Uid,
Uname: hdr.Uname,
@@ -62,7 +59,7 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os
Gname: hdr.Gname,
AccessTime: hdr.AccessTime,
ChangeTime: hdr.ChangeTime,
} // #nosec G305 -- An archive is being created, not extracted.
}
}
}

View File

@@ -2,6 +2,6 @@
package archive // import "github.com/docker/docker/pkg/archive"
func getWhiteoutConverter(format WhiteoutFormat, inUserNS bool) (tarWhiteoutConverter, error) {
return nil, nil
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
return nil
}

View File

@@ -11,7 +11,6 @@ import (
"strings"
"syscall"
"github.com/containerd/containerd/pkg/userns"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/system"
"golang.org/x/sys/unix"
@@ -95,7 +94,10 @@ func getFileUIDGID(stat interface{}) (idtools.Identity, error) {
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
// createTarFile to handle the following types of header: Block; Char; Fifo.
//
// Creating device nodes is not supported when running in a user namespace,
// produces a [syscall.EPERM] in most cases.
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
mode := uint32(hdr.Mode & 0o7777)
switch hdr.Typeflag {
@@ -107,12 +109,7 @@ func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
mode |= unix.S_IFIFO
}
err := system.Mknod(path, mode, int(system.Mkdev(hdr.Devmajor, hdr.Devminor)))
if errors.Is(err, syscall.EPERM) && userns.RunningInUserNS() {
// In most cases, cannot create a device if running in user namespace
err = nil
}
return err
return system.Mknod(path, mode, int(system.Mkdev(hdr.Devmajor, hdr.Devminor)))
}
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {