Repository: moby/moby (language: go)

Issue:
ip6tables fails during network creation when com.docker.network.host_ipv4 bridge option is set
### Description

With IPv6 and the experimental `ip6tables` option enabled in the daemon, creating an IPv6-enabled bridge network with the `com.docker.network.host_ipv4` option set fails with:

```
Error response from daemon: Failed to Setup IP tables: Unable to enable NAT rule:  (iptables failed: ip6tables --wait -t nat -I POSTROUTING -s fd01::/64 ! -o br-f15cb6308dee -j SNAT --to-source 192.168.0.2: ip6tables v1.8.7 (legacy): Bad IP address "192.168.0.2"

Try `ip6tables -h' or 'ip6tables --help' for more information.
 (exit status 2))
```

This is because the IPv4 address is used when setting up both the IPv4 rules and the IPv6 rules:  https://github.com/moby/moby/blob/6ce5aa1cd5a46e02ffaa3fe0c794642b35601676/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L160-L165

### Reproduce

1. `dockerd --ipv6 --fixed-cidr-v6=fd00::/64 --experimental --ip6tables`
2. `docker network create -d bridge --ipv6 --subnet fd01::/64 --opt com.docker.network.host_ipv4=192.168.0.2 test-network`

### Expected behavior

Docker should create a bridge network with IPv4 SNAT (to the given IP) and IPv6 masquerade.

### docker version

```bash
Client: Docker Engine - Community
 Version:           24.0.6
 API version:       1.43
 Go version:        go1.20.7
 Git commit:        ed223bc
 Built:             Mon Sep  4 12:31:44 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.6
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.7
  Git commit:       1a79695
  Built:            Mon Sep  4 12:31:44 2023
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.6.22
  GitCommit:        8165feabfdfe38c65b599c4993d227328c231fca
 runc:
  Version:          1.1.8
  GitCommit:        v1.1.8-0-g82f18fe
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### docker info

```bash
Client: Docker Engine - Community
 Version:    24.0.6
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.11.2
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.21.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 5
  Running: 5
  Paused: 0
  Stopped: 0
 Images: 1159
 Server Version: 24.0.6
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8165feabfdfe38c65b599c4993d227328c231fca
 runc version: v1.1.8-0-g82f18fe
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.2.0-26-generic
 Operating System: Ubuntu 22.04.3 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 8
 Total Memory: 23.46GiB
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: true
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
 Default Address Pools:
   Base: 172.17.0.0/16, Size: 24
   Base: 172.18.0.0/15, Size: 23
   Base: 172.20.0.0/14, Size: 22
   Base: 172.24.0.0/13, Size: 21
   Base: fd01::/64, Size: 64
```


### Additional Info

The following change should fix it (I'll open a new pull request once I write a regression test):

```diff
diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
index c98781f3b0..5d2289014b 100644
--- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go
+++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
@@ -157,11 +157,15 @@ func (n *bridgeNetwork) setupIPTables(ipVersion iptables.IPVersion, maskedAddr *
                        return setupInternalNetworkRules(config.BridgeName, maskedAddr, config.EnableICC, false)
                })
        } else {
-               if err = setupIPTablesInternal(config.HostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, true); err != nil {
+               hostIP := config.HostIP
+               if ipVersion != iptables.IPv4 {
+                       hostIP = nil
+               }
+               if err = setupIPTablesInternal(hostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, true); err != nil {
                        return fmt.Errorf("Failed to Setup IP tables: %s", err.Error())
                }
                n.registerIptCleanFunc(func() error {
-                       return setupIPTablesInternal(config.HostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
+                       return setupIPTablesInternal(hostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
                })
                natChain, filterChain, _, _, err := n.getDriverChains(ipVersion)
                if err != nil {
```


Discussion / hints:

Fix patch:
diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
index c98781f3b0920..5d2289014bc5c 100644
--- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go
+++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
@@ -157,11 +157,15 @@ func (n *bridgeNetwork) setupIPTables(ipVersion iptables.IPVersion, maskedAddr *
 			return setupInternalNetworkRules(config.BridgeName, maskedAddr, config.EnableICC, false)
 		})
 	} else {
-		if err = setupIPTablesInternal(config.HostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, true); err != nil {
+		hostIP := config.HostIP
+		if ipVersion != iptables.IPv4 {
+			hostIP = nil
+		}
+		if err = setupIPTablesInternal(hostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, true); err != nil {
 			return fmt.Errorf("Failed to Setup IP tables: %s", err.Error())
 		}
 		n.registerIptCleanFunc(func() error {
-			return setupIPTablesInternal(config.HostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
+			return setupIPTablesInternal(hostIP, config.BridgeName, maskedAddr, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
 		})
 		natChain, filterChain, _, _, err := n.getDriverChains(ipVersion)
 		if err != nil {


Test patch:
diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go
index 8d46a73b453da..30fa1909f02e4 100644
--- a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go
+++ b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/docker/docker/internal/testutils/netnsutils"
 	"github.com/docker/docker/libnetwork/iptables"
+	"github.com/docker/docker/libnetwork/netlabel"
 	"github.com/docker/docker/libnetwork/portmapper"
 	"github.com/vishvananda/netlink"
 )
@@ -92,6 +93,11 @@ func createTestBridge(config *networkConfiguration, br *bridgeInterface, t *test
 	if err := setupBridgeIPv4(config, br); err != nil {
 		t.Fatalf("Failed to bring up the testing Bridge: %s", err.Error())
 	}
+	if config.EnableIPv6 {
+		if err := setupBridgeIPv6(config, br); err != nil {
+			t.Fatalf("Failed to bring up the testing Bridge: %s", err.Error())
+		}
+	}
 }
 
 // Assert base function which pushes iptables chain rules on insertion and removal.
@@ -123,13 +129,20 @@ func assertChainConfig(d *driver, t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	if d.config.EnableIP6Tables {
+		d.natChainV6, d.filterChainV6, d.isolationChain1V6, d.isolationChain2V6, err = setupIPChains(d.config, iptables.IPv6)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
 }
 
 // Assert function which pushes chains based on bridge config parameters.
 func assertBridgeConfig(config *networkConfiguration, br *bridgeInterface, d *driver, t *testing.T) {
 	nw := bridgeNetwork{
-		portMapper: portmapper.New(""),
-		config:     config,
+		portMapper:   portmapper.New(""),
+		portMapperV6: portmapper.New(""),
+		config:       config,
 	}
 	nw.driver = d
 
@@ -138,4 +151,37 @@ func assertBridgeConfig(config *networkConfiguration, br *bridgeInterface, d *dr
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
+	if d.config.EnableIP6Tables {
+		if err := nw.setupIP6Tables(config, br); err != nil {
+			t.Fatalf("%v", err)
+		}
+	}
+}
+
+// Regression test for https://github.com/moby/moby/issues/46445
+func TestSetupIP6TablesWithHostIP(t *testing.T) {
+	defer netnsutils.SetupTestOSContext(t)()
+	d := newDriver()
+	dc := &configuration{
+		EnableIPTables:  true,
+		EnableIP6Tables: true,
+	}
+	if err := d.configure(map[string]interface{}{netlabel.GenericData: dc}); err != nil {
+		t.Fatal(err)
+	}
+	nc := &networkConfiguration{
+		BridgeName:         DefaultBridgeName,
+		AddressIPv4:        &net.IPNet{IP: net.ParseIP(iptablesTestBridgeIP), Mask: net.CIDRMask(16, 32)},
+		EnableIPMasquerade: true,
+		EnableIPv6:         true,
+		AddressIPv6:        &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)},
+		HostIP:             net.ParseIP("192.0.2.2"),
+	}
+	nh, err := netlink.NewHandle()
+	if err != nil {
+		t.Fatal(err)
+	}
+	br := &bridgeInterface{nlh: nh}
+	createTestBridge(nc, br, t)
+	assertBridgeConfig(nc, br, d, t)
 }