Explorar o código

Support for com.docker.network.host_ipv4 driver label

This commit allows a user to specify a Host IP via the
com.docker.network.host_ipv4 label which is used as the
Source IP during SNAT for bridge networks .

The use case is for hosts with multiple interfaces and
this label can dictate which IP will be used as Source IP
for North-South traffic

In the absence of this label, MASQUERADE is used which picks the Source IP
based on Next Hop from the Route Table

Addresses: https://github.com/moby/moby/issues/30053

Signed-off-by: Arko Dasgupta <arko.dasgupta@docker.com>
Arko Dasgupta %!s(int64=5) %!d(string=hai) anos
pai
achega
8c8a25d524

+ 5 - 0
libnetwork/drivers/bridge/bridge.go

@@ -71,6 +71,7 @@ type networkConfiguration struct {
 	Mtu                  int
 	DefaultBindingIP     net.IP
 	DefaultBridge        bool
+	HostIP               net.IP
 	ContainerIfacePrefix string
 	// Internal fields set after ipam data parsing
 	AddressIPv4        *net.IPNet
@@ -253,6 +254,10 @@ func (c *networkConfiguration) fromLabels(labels map[string]string) error {
 			}
 		case netlabel.ContainerIfacePrefix:
 			c.ContainerIfacePrefix = value
+		case netlabel.HostIP:
+			if c.HostIP = net.ParseIP(value); c.HostIP == nil {
+				return parseErr(label, value, "nil ip")
+			}
 		}
 	}
 

+ 5 - 0
libnetwork/drivers/bridge/bridge_store.go

@@ -141,6 +141,7 @@ func (ncfg *networkConfiguration) MarshalJSON() ([]byte, error) {
 	nMap["Internal"] = ncfg.Internal
 	nMap["DefaultBridge"] = ncfg.DefaultBridge
 	nMap["DefaultBindingIP"] = ncfg.DefaultBindingIP.String()
+	nMap["HostIP"] = ncfg.HostIP.String()
 	nMap["DefaultGatewayIPv4"] = ncfg.DefaultGatewayIPv4.String()
 	nMap["DefaultGatewayIPv6"] = ncfg.DefaultGatewayIPv6.String()
 	nMap["ContainerIfacePrefix"] = ncfg.ContainerIfacePrefix
@@ -183,6 +184,10 @@ func (ncfg *networkConfiguration) UnmarshalJSON(b []byte) error {
 		ncfg.ContainerIfacePrefix = v.(string)
 	}
 
+	if v, ok := nMap["HostIP"]; ok {
+		ncfg.HostIP = net.ParseIP(v.(string))
+	}
+
 	ncfg.DefaultBridge = nMap["DefaultBridge"].(bool)
 	ncfg.DefaultBindingIP = net.ParseIP(nMap["DefaultBindingIP"].(string))
 	ncfg.DefaultGatewayIPv4 = net.ParseIP(nMap["DefaultGatewayIPv4"].(string))

+ 7 - 0
libnetwork/drivers/bridge/bridge_test.go

@@ -264,6 +264,7 @@ func TestCreateFullOptionsLabels(t *testing.T) {
 	}
 
 	bndIPs := "127.0.0.1"
+	testHostIP := "1.2.3.4"
 	nwV6s := "2001:db8:2600:2700:2800::/80"
 	gwV6s := "2001:db8:2600:2700:2800::25/80"
 	nwV6, _ := types.ParseCIDR(nwV6s)
@@ -275,6 +276,7 @@ func TestCreateFullOptionsLabels(t *testing.T) {
 		EnableICC:          "true",
 		EnableIPMasquerade: "true",
 		DefaultBindingIP:   bndIPs,
+		netlabel.HostIP:    testHostIP,
 	}
 
 	netOption := make(map[string]interface{})
@@ -322,6 +324,11 @@ func TestCreateFullOptionsLabels(t *testing.T) {
 		t.Fatalf("Unexpected: %v", nw.config.DefaultBindingIP)
 	}
 
+	hostIP := net.ParseIP(testHostIP)
+	if !hostIP.Equal(nw.config.HostIP) {
+		t.Fatalf("Unexpected: %v", nw.config.HostIP)
+	}
+
 	if !types.CompareIPNet(nw.config.AddressIPv6, nwV6) {
 		t.Fatalf("Unexpected: %v", nw.config.AddressIPv6)
 	}

+ 18 - 5
libnetwork/drivers/bridge/setup_ip_tables.go

@@ -121,11 +121,11 @@ func (n *bridgeNetwork) setupIPTables(config *networkConfiguration, i *bridgeInt
 			return setupInternalNetworkRules(config.BridgeName, maskedAddrv4, config.EnableICC, false)
 		})
 	} else {
-		if err = setupIPTablesInternal(config.BridgeName, maskedAddrv4, config.EnableICC, config.EnableIPMasquerade, hairpinMode, true); err != nil {
+		if err = setupIPTablesInternal(config.HostIP, config.BridgeName, maskedAddrv4, 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.BridgeName, maskedAddrv4, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
+			return setupIPTablesInternal(config.HostIP, config.BridgeName, maskedAddrv4, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
 		})
 		natChain, filterChain, _, _, err := n.getDriverChains()
 		if err != nil {
@@ -166,15 +166,28 @@ type iptRule struct {
 	args    []string
 }
 
-func setupIPTablesInternal(bridgeIface string, addr net.Addr, icc, ipmasq, hairpin, enable bool) error {
+func setupIPTablesInternal(hostIP net.IP, bridgeIface string, addr net.Addr, icc, ipmasq, hairpin, enable bool) error {
 
 	var (
 		address   = addr.String()
-		natRule   = iptRule{table: iptables.Nat, chain: "POSTROUTING", preArgs: []string{"-t", "nat"}, args: []string{"-s", address, "!", "-o", bridgeIface, "-j", "MASQUERADE"}}
-		hpNatRule = iptRule{table: iptables.Nat, chain: "POSTROUTING", preArgs: []string{"-t", "nat"}, args: []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", bridgeIface, "-j", "MASQUERADE"}}
 		skipDNAT  = iptRule{table: iptables.Nat, chain: DockerChain, preArgs: []string{"-t", "nat"}, args: []string{"-i", bridgeIface, "-j", "RETURN"}}
 		outRule   = iptRule{table: iptables.Filter, chain: "FORWARD", args: []string{"-i", bridgeIface, "!", "-o", bridgeIface, "-j", "ACCEPT"}}
+		natArgs   []string
+		hpNatArgs []string
 	)
+	// if hostIP is set use this address as the src-ip during SNAT
+	if hostIP != nil {
+		hostAddr := hostIP.String()
+		natArgs = []string{"-s", address, "!", "-o", bridgeIface, "-j", "SNAT", "--to-source", hostAddr}
+		hpNatArgs = []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", bridgeIface, "-j", "SNAT", "--to-source", hostAddr}
+		// Else use MASQUERADE which picks the src-ip based on NH from the route table
+	} else {
+		natArgs = []string{"-s", address, "!", "-o", bridgeIface, "-j", "MASQUERADE"}
+		hpNatArgs = []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", bridgeIface, "-j", "MASQUERADE"}
+	}
+
+	natRule := iptRule{table: iptables.Nat, chain: "POSTROUTING", preArgs: []string{"-t", "nat"}, args: natArgs}
+	hpNatRule := iptRule{table: iptables.Nat, chain: "POSTROUTING", preArgs: []string{"-t", "nat"}, args: hpNatArgs}
 
 	// Set NAT.
 	if ipmasq {

+ 3 - 0
libnetwork/netlabel/labels.go

@@ -53,6 +53,9 @@ const (
 
 	// ContainerIfacePrefix can be used to override the interface prefix used inside the container
 	ContainerIfacePrefix = Prefix + ".container_iface_prefix"
+
+	// HostIP is the Source-IP Address used to SNAT container traffic
+	HostIP = Prefix + ".host_ipv4"
 )
 
 var (