Merge pull request #47775 from vvoland/v26.1-47771

[26.1 backport] Option to avoid deleting the kernel_ll address from bridges.
diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go
index 9188669..cb2abfa 100644
--- a/integration/networking/bridge_test.go
+++ b/integration/networking/bridge_test.go
@@ -753,3 +753,78 @@
 	stdout := runRes.Stdout.String()
 	assert.Check(t, is.Contains(stdout, scName))
 }
+
+// With a read-only "/proc/sys/net" filesystem (simulated using env var
+// DOCKER_TEST_RO_DISABLE_IPV6), check that if IPv6 can't be disabled on a
+// container interface, container creation fails - unless the error is ignored by
+// setting env var DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE=1.
+// Regression test for https://github.com/moby/moby/issues/47751
+func TestReadOnlySlashProc(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
+
+	ctx := setupTest(t)
+
+	testcases := []struct {
+		name      string
+		daemonEnv []string
+		expErr    string
+	}{
+		{
+			name: "Normality",
+		},
+		{
+			name: "Read only no workaround",
+			daemonEnv: []string{
+				"DOCKER_TEST_RO_DISABLE_IPV6=1",
+			},
+			expErr: "failed to disable IPv6 on container's interface eth0, set env var DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE=1 to ignore this error",
+		},
+		{
+			name: "Read only with workaround",
+			daemonEnv: []string{
+				"DOCKER_TEST_RO_DISABLE_IPV6=1",
+				"DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE=1",
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := testutil.StartSpan(ctx, t)
+
+			d := daemon.New(t, daemon.WithEnvVars(tc.daemonEnv...))
+			d.StartWithBusybox(ctx, t)
+			defer d.Stop(t)
+			c := d.NewClientT(t)
+
+			const net4Name = "testnet4"
+			network.CreateNoError(ctx, t, c, net4Name)
+			defer network.RemoveNoError(ctx, t, c, net4Name)
+			id4 := container.Create(ctx, t, c,
+				container.WithNetworkMode(net4Name),
+				container.WithCmd("ls"),
+			)
+			defer c.ContainerRemove(ctx, id4, containertypes.RemoveOptions{Force: true})
+			err := c.ContainerStart(ctx, id4, containertypes.StartOptions{})
+			if tc.expErr == "" {
+				assert.Check(t, err)
+			} else {
+				assert.Check(t, is.ErrorContains(err, tc.expErr))
+			}
+
+			// It should always be possible to create a container on an IPv6 network (IPv6
+			// doesn't need to be disabled on the interface).
+			const net6Name = "testnet6"
+			network.CreateNoError(ctx, t, c, net6Name,
+				network.WithIPv6(),
+				network.WithIPAM("fd5c:15e3:0b62:5395::/64", "fd5c:15e3:0b62:5395::1"),
+			)
+			defer network.RemoveNoError(ctx, t, c, net6Name)
+			id6 := container.Run(ctx, t, c,
+				container.WithNetworkMode(net6Name),
+				container.WithCmd("ls"),
+			)
+			defer c.ContainerRemove(ctx, id6, containertypes.RemoveOptions{Force: true})
+		})
+	}
+}
diff --git a/libnetwork/osl/namespace_linux.go b/libnetwork/osl/namespace_linux.go
index 8a41f6a..70dbc9d 100644
--- a/libnetwork/osl/namespace_linux.go
+++ b/libnetwork/osl/namespace_linux.go
@@ -646,17 +646,47 @@
 			value = '0'
 		}
 
-		if _, err := os.Stat(path); err != nil {
+		if curVal, err := os.ReadFile(path); err != nil {
 			if os.IsNotExist(err) {
-				log.G(context.TODO()).WithError(err).Warn("Cannot configure IPv6 forwarding on container interface. Has IPv6 been disabled in this node's kernel?")
+				if enable {
+					log.G(context.TODO()).WithError(err).Warn("Cannot enable IPv6 on container interface. Has IPv6 been disabled in this node's kernel?")
+				} else {
+					log.G(context.TODO()).WithError(err).Debug("Not disabling IPv6 on container interface. Has IPv6 been disabled in this node's kernel?")
+				}
 				return
 			}
 			errCh <- err
 			return
+		} else if len(curVal) > 0 && curVal[0] == value {
+			// Nothing to do, the setting is already correct.
+			return
 		}
 
-		if err = os.WriteFile(path, []byte{value, '\n'}, 0o644); err != nil {
-			errCh <- fmt.Errorf("failed to %s IPv6 forwarding for container's interface %s: %w", action, iface, err)
+		if err = os.WriteFile(path, []byte{value, '\n'}, 0o644); err != nil || os.Getenv("DOCKER_TEST_RO_DISABLE_IPV6") != "" {
+			logger := log.G(context.TODO()).WithFields(log.Fields{
+				"error":     err,
+				"interface": iface,
+			})
+			if enable {
+				// The user asked for IPv6 on the interface, and we can't give it to them.
+				// But, in line with the IsNotExist case above, just log.
+				logger.Warn("Cannot enable IPv6 on container interface, continuing.")
+			} else if os.Getenv("DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE") == "1" {
+				// TODO(robmry) - remove this escape hatch for https://github.com/moby/moby/issues/47751
+				//   If the "/proc" file exists but isn't writable, we can't disable IPv6, which is
+				//   https://github.com/moby/moby/security/advisories/GHSA-x84c-p2g9-rqv9 ... so,
+				//   the user is required to override the error (or configure IPv6, or disable IPv6
+				//   by default in the OS, or make the "/proc" file writable). Once it's possible
+				//   to enable IPv6 without having to configure IPAM etc, the env var should be
+				//   removed. Then the user will have to explicitly enable IPv6 if it can't be
+				//   disabled on the interface.
+				logger.Info("Cannot disable IPv6 on container interface but DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE=1, continuing.")
+			} else {
+				logger.Error("Cannot disable IPv6 on container interface. Set env var DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE=1 to ignore.")
+				errCh <- fmt.Errorf(
+					"failed to %s IPv6 on container's interface %s, set env var DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE=1 to ignore this error",
+					action, iface)
+			}
 			return
 		}
 	}()