Repository: moby/moby (language: go)

Issue:
IPv6: Unable to choose source IP address for outbound connections
### Description

The `com.docker.network.host_ipv4` bridge option allows users to choose the source IP address for outbound IPv4 connections.  When that option is set, the bridge driver inserts an iptables rule that uses `SNAT` instead of the usual `MASQUERADE`.

This is a request to add the IPv6 equivalent of that feature.

For background on the existing IPv4 feature, see #30053 and moby/libnetwork#2454.


Discussion / hints:

Fix patch:
diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go
index 47ac622320a78..f438e5480c7d0 100644
--- a/libnetwork/drivers/bridge/bridge_linux.go
+++ b/libnetwork/drivers/bridge/bridge_linux.go
@@ -74,6 +74,7 @@ type networkConfiguration struct {
 	DefaultBindingIP     net.IP
 	DefaultBridge        bool
 	HostIPv4             net.IP
+	HostIPv6             net.IP
 	ContainerIfacePrefix string
 	// Internal fields set after ipam data parsing
 	AddressIPv4        *net.IPNet
@@ -270,6 +271,10 @@ func (c *networkConfiguration) fromLabels(labels map[string]string) error {
 			if c.HostIPv4 = net.ParseIP(value); c.HostIPv4 == nil {
 				return parseErr(label, value, "nil ip")
 			}
+		case netlabel.HostIPv6:
+			if c.HostIPv6 = net.ParseIP(value); c.HostIPv6 == nil {
+				return parseErr(label, value, "nil ip")
+			}
 		}
 	}
 
diff --git a/libnetwork/drivers/bridge/bridge_store.go b/libnetwork/drivers/bridge/bridge_store.go
index ad6845f72c81e..66928aba24d0e 100644
--- a/libnetwork/drivers/bridge/bridge_store.go
+++ b/libnetwork/drivers/bridge/bridge_store.go
@@ -148,6 +148,7 @@ func (ncfg *networkConfiguration) MarshalJSON() ([]byte, error) {
 	nMap["DefaultBindingIP"] = ncfg.DefaultBindingIP.String()
 	// This key is "HostIP" instead of "HostIPv4" to preserve compatibility with the on-disk format.
 	nMap["HostIP"] = ncfg.HostIPv4.String()
+	nMap["HostIPv6"] = ncfg.HostIPv6.String()
 	nMap["DefaultGatewayIPv4"] = ncfg.DefaultGatewayIPv4.String()
 	nMap["DefaultGatewayIPv6"] = ncfg.DefaultGatewayIPv6.String()
 	nMap["ContainerIfacePrefix"] = ncfg.ContainerIfacePrefix
@@ -194,6 +195,9 @@ func (ncfg *networkConfiguration) UnmarshalJSON(b []byte) error {
 	if v, ok := nMap["HostIP"]; ok {
 		ncfg.HostIPv4 = net.ParseIP(v.(string))
 	}
+	if v, ok := nMap["HostIPv6"]; ok {
+		ncfg.HostIPv6 = net.ParseIP(v.(string))
+	}
 
 	ncfg.DefaultBridge = nMap["DefaultBridge"].(bool)
 	ncfg.DefaultBindingIP = net.ParseIP(nMap["DefaultBindingIP"].(string))
diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
index 436be73153046..d7f5966dc02bb 100644
--- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go
+++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go
@@ -258,9 +258,13 @@ func setupIPTablesInternal(ipVer iptables.IPVersion, config *networkConfiguratio
 		natArgs   []string
 		hpNatArgs []string
 	)
-	// If config.HostIPv4 is set, the user wants IPv4 SNAT with the given address.
-	if config.HostIPv4 != nil && ipVer == iptables.IPv4 {
-		hostAddr := config.HostIPv4.String()
+	hostIP := config.HostIPv4
+	if ipVer == iptables.IPv6 {
+		hostIP = config.HostIPv6
+	}
+	// If hostIP is set, the user wants IPv4/IPv6 SNAT with the given address.
+	if hostIP != nil {
+		hostAddr := hostIP.String()
 		natArgs = []string{"-s", address, "!", "-o", config.BridgeName, "-j", "SNAT", "--to-source", hostAddr}
 		hpNatArgs = []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", config.BridgeName, "-j", "SNAT", "--to-source", hostAddr}
 		// Else use MASQUERADE which picks the src-ip based on NH from the route table
diff --git a/libnetwork/netlabel/labels.go b/libnetwork/netlabel/labels.go
index 0edc0f6cd291c..91232af6aae45 100644
--- a/libnetwork/netlabel/labels.go
+++ b/libnetwork/netlabel/labels.go
@@ -47,6 +47,9 @@ const (
 	// HostIPv4 is the Source-IPv4 Address used to SNAT IPv4 container traffic
 	HostIPv4 = Prefix + ".host_ipv4"
 
+	// HostIPv6 is the Source-IPv6 Address used to SNAT IPv6 container traffic
+	HostIPv6 = Prefix + ".host_ipv6"
+
 	// LocalKVClient constants represents the local kv store client
 	LocalKVClient = DriverPrivatePrefix + "localkv_client"
 )


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 2658a94dc27fb..dd5fd678b660d 100644
--- a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go
+++ b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go
@@ -212,6 +212,7 @@ func TestOutgoingNATRules(t *testing.T) {
 	maskedBrIPv4 := &net.IPNet{IP: brIPv4.IP.Mask(brIPv4.Mask), Mask: brIPv4.Mask}
 	maskedBrIPv6 := &net.IPNet{IP: brIPv6.IP.Mask(brIPv6.Mask), Mask: brIPv6.Mask}
 	hostIPv4 := net.ParseIP("192.0.2.2")
+	hostIPv6 := net.ParseIP("2001:db8:1::1")
 	for _, tc := range []struct {
 		desc               string
 		enableIPTables     bool
@@ -219,6 +220,7 @@ func TestOutgoingNATRules(t *testing.T) {
 		enableIPv6         bool
 		enableIPMasquerade bool
 		hostIPv4           net.IP
+		hostIPv6           net.IP
 		// Hairpin NAT rules are not tested here because they are orthogonal to outgoing NAT.  They
 		// exist to support the port forwarding DNAT rules: without any port forwarding there would be
 		// no need for any hairpin NAT rules, and when there is port forwarding then hairpin NAT rules
@@ -227,6 +229,7 @@ func TestOutgoingNATRules(t *testing.T) {
 		wantIPv4Masq bool
 		wantIPv4Snat bool
 		wantIPv6Masq bool
+		wantIPv6Snat bool
 	}{
 		{
 			desc: "everything disabled",
@@ -241,6 +244,7 @@ func TestOutgoingNATRules(t *testing.T) {
 			enableIPv6:         true,
 			enableIPMasquerade: true,
 			hostIPv4:           hostIPv4,
+			hostIPv6:           hostIPv6,
 		},
 		{
 			desc:            "masquerade disabled, no host IP",
@@ -254,6 +258,7 @@ func TestOutgoingNATRules(t *testing.T) {
 			enableIP6Tables: true,
 			enableIPv6:      true,
 			hostIPv4:        hostIPv4,
+			hostIPv6:        hostIPv6,
 		},
 		{
 			desc:               "IPv4 masquerade, IPv6 disabled",
@@ -277,6 +282,16 @@ func TestOutgoingNATRules(t *testing.T) {
 			wantIPv4Masq:       true,
 			wantIPv6Masq:       true,
 		},
+		{
+			desc:               "IPv4 masquerade, IPv6 SNAT",
+			enableIPTables:     true,
+			enableIP6Tables:    true,
+			enableIPv6:         true,
+			enableIPMasquerade: true,
+			hostIPv6:           hostIPv6,
+			wantIPv4Masq:       true,
+			wantIPv6Snat:       true,
+		},
 		{
 			desc:               "IPv4 SNAT, IPv6 masquerade",
 			enableIPTables:     true,
@@ -287,6 +302,17 @@ func TestOutgoingNATRules(t *testing.T) {
 			wantIPv4Snat:       true,
 			wantIPv6Masq:       true,
 		},
+		{
+			desc:               "IPv4 SNAT, IPv6 SNAT",
+			enableIPTables:     true,
+			enableIP6Tables:    true,
+			enableIPv6:         true,
+			enableIPMasquerade: true,
+			hostIPv4:           hostIPv4,
+			hostIPv6:           hostIPv6,
+			wantIPv4Snat:       true,
+			wantIPv6Snat:       true,
+		},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
 			defer netnsutils.SetupTestOSContext(t)()
@@ -308,6 +334,7 @@ func TestOutgoingNATRules(t *testing.T) {
 				EnableIPv6:         tc.enableIPv6,
 				EnableIPMasquerade: tc.enableIPMasquerade,
 				HostIPv4:           tc.hostIPv4,
+				HostIPv6:           tc.hostIPv6,
 			}
 			ipv4Data := []driverapi.IPAMData{{Pool: maskedBrIPv4, Gateway: brIPv4}}
 			ipv6Data := []driverapi.IPAMData{{Pool: maskedBrIPv6, Gateway: brIPv6}}
@@ -343,6 +370,7 @@ func TestOutgoingNATRules(t *testing.T) {
 				{tc.wantIPv4Masq, iptRule{iptables.IPv4, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv4.String(), "!", "-o", br, "-j", "MASQUERADE"}}},
 				{tc.wantIPv4Snat, iptRule{iptables.IPv4, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv4.String(), "!", "-o", br, "-j", "SNAT", "--to-source", hostIPv4.String()}}},
 				{tc.wantIPv6Masq, iptRule{iptables.IPv6, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv6.String(), "!", "-o", br, "-j", "MASQUERADE"}}},
+				{tc.wantIPv6Snat, iptRule{iptables.IPv6, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv6.String(), "!", "-o", br, "-j", "SNAT", "--to-source", hostIPv6.String()}}},
 			} {
 				assert.Equal(t, rc.rule.Exists(), rc.want)
 			}