Browse code

Option to disable NAT for IPv4/IPv6 for a bridge network.

Add bridge driver options...
com.docker.network.bridge.gateway_mode_ipv4=<nat|routed>
com.docker.network.bridge.gateway_mode_ipv6=<nat|routed>

If set to "routed", no NAT or masquerade rules are set up for port
mappings.

When NAT is disabled, the mapping is shown in 'inspect' output with
no host port number. For example, for "-p 80" with NAT disabled for
IPv6 but not IPv4:

"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "32768"
},
{
"HostIp": "::",
"HostPort": ""
}

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2024/05/27 02:18:40
Showing 9 changed files
... ...
@@ -1026,11 +1026,16 @@ func getEndpointPortMapInfo(ep *libnetwork.Endpoint) (nat.PortMap, error) {
1026 1026
 
1027 1027
 	if portMapping, ok := mapData.([]networktypes.PortBinding); ok {
1028 1028
 		for _, pp := range portMapping {
1029
+			// Use an empty string for the host port if there's no port assigned.
1029 1030
 			natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port)))
1030 1031
 			if err != nil {
1031 1032
 				return pm, err
1032 1033
 			}
1033
-			natBndg := nat.PortBinding{HostIP: pp.HostIP.String(), HostPort: strconv.Itoa(int(pp.HostPort))}
1034
+			var hp string
1035
+			if pp.HostPort > 0 {
1036
+				hp = strconv.Itoa(int(pp.HostPort))
1037
+			}
1038
+			natBndg := nat.PortBinding{HostIP: pp.HostIP.String(), HostPort: hp}
1034 1039
 			pm[natPort] = append(pm[natPort], natBndg)
1035 1040
 		}
1036 1041
 	}
... ...
@@ -2,6 +2,7 @@ package container
2 2
 
3 3
 import (
4 4
 	"maps"
5
+	"slices"
5 6
 	"strings"
6 7
 
7 8
 	"github.com/docker/docker/api/types/container"
... ...
@@ -71,6 +72,16 @@ func WithExposedPorts(ports ...string) func(*TestContainerConfig) {
71 71
 	}
72 72
 }
73 73
 
74
+// WithPortMap sets/replaces port mappings.
75
+func WithPortMap(pm nat.PortMap) func(*TestContainerConfig) {
76
+	return func(c *TestContainerConfig) {
77
+		c.HostConfig.PortBindings = nat.PortMap{}
78
+		for p, b := range pm {
79
+			c.HostConfig.PortBindings[p] = slices.Clone(b)
80
+		}
81
+	}
82
+}
83
+
74 84
 // WithTty sets the TTY mode of the container
75 85
 func WithTty(tty bool) func(*TestContainerConfig) {
76 86
 	return func(c *TestContainerConfig) {
... ...
@@ -13,9 +13,11 @@ import (
13 13
 	networktypes "github.com/docker/docker/api/types/network"
14 14
 	"github.com/docker/docker/integration/internal/container"
15 15
 	"github.com/docker/docker/integration/internal/network"
16
+	"github.com/docker/docker/libnetwork/drivers/bridge"
16 17
 	"github.com/docker/docker/libnetwork/netlabel"
17 18
 	"github.com/docker/docker/testutil"
18 19
 	"github.com/docker/docker/testutil/daemon"
20
+	"github.com/docker/go-connections/nat"
19 21
 	"github.com/google/go-cmp/cmp/cmpopts"
20 22
 	"gotest.tools/v3/assert"
21 23
 	is "gotest.tools/v3/assert/cmp"
... ...
@@ -920,3 +922,84 @@ func TestSetEndpointSysctl(t *testing.T) {
920 920
 		}
921 921
 	}
922 922
 }
923
+
924
+func TestDisableNAT(t *testing.T) {
925
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "bridge driver option doesn't apply to Windows")
926
+
927
+	ctx := setupTest(t)
928
+	d := daemon.New(t)
929
+	d.StartWithBusybox(ctx, t)
930
+	defer d.Stop(t)
931
+
932
+	c := d.NewClientT(t)
933
+	defer c.Close()
934
+
935
+	testcases := []struct {
936
+		name       string
937
+		gwMode4    string
938
+		gwMode6    string
939
+		expPortMap nat.PortMap
940
+	}{
941
+		{
942
+			name: "defaults",
943
+			expPortMap: nat.PortMap{
944
+				"80/tcp": []nat.PortBinding{
945
+					{HostIP: "0.0.0.0", HostPort: "8080"},
946
+					{HostIP: "::", HostPort: "8080"},
947
+				},
948
+			},
949
+		},
950
+		{
951
+			name:    "nat4 routed6",
952
+			gwMode4: "nat",
953
+			gwMode6: "routed",
954
+			expPortMap: nat.PortMap{
955
+				"80/tcp": []nat.PortBinding{
956
+					{HostIP: "0.0.0.0", HostPort: "8080"},
957
+					{HostIP: "::", HostPort: ""},
958
+				},
959
+			},
960
+		},
961
+		{
962
+			name:    "nat6 routed4",
963
+			gwMode4: "routed",
964
+			gwMode6: "nat",
965
+			expPortMap: nat.PortMap{
966
+				"80/tcp": []nat.PortBinding{
967
+					{HostIP: "0.0.0.0", HostPort: ""},
968
+					{HostIP: "::", HostPort: "8080"},
969
+				},
970
+			},
971
+		},
972
+	}
973
+
974
+	for _, tc := range testcases {
975
+		t.Run(tc.name, func(t *testing.T) {
976
+			ctx := testutil.StartSpan(ctx, t)
977
+
978
+			const netName = "testnet"
979
+			nwOpts := []func(options *networktypes.CreateOptions){
980
+				network.WithIPv6(),
981
+				network.WithIPAM("fd2a:a2c3:4448::/64", "fd2a:a2c3:4448::1"),
982
+			}
983
+			if tc.gwMode4 != "" {
984
+				nwOpts = append(nwOpts, network.WithOption(bridge.IPv4GatewayMode, tc.gwMode4))
985
+			}
986
+			if tc.gwMode6 != "" {
987
+				nwOpts = append(nwOpts, network.WithOption(bridge.IPv6GatewayMode, tc.gwMode6))
988
+			}
989
+			network.CreateNoError(ctx, t, c, netName, nwOpts...)
990
+			defer network.RemoveNoError(ctx, t, c, netName)
991
+
992
+			id := container.Run(ctx, t, c,
993
+				container.WithNetworkMode(netName),
994
+				container.WithExposedPorts("80/tcp"),
995
+				container.WithPortMap(nat.PortMap{"80/tcp": {{HostPort: "8080"}}}),
996
+			)
997
+			defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
998
+
999
+			inspect := container.Inspect(ctx, t, c, id)
1000
+			assert.Check(t, is.DeepEqual(inspect.NetworkSettings.Ports, tc.expPortMap))
1001
+		})
1002
+	}
1003
+}
... ...
@@ -62,6 +62,8 @@ type networkConfiguration struct {
62 62
 	BridgeName           string
63 63
 	EnableIPv6           bool
64 64
 	EnableIPMasquerade   bool
65
+	GwModeIPv4           gwMode
66
+	GwModeIPv6           gwMode
65 67
 	EnableICC            bool
66 68
 	InhibitIPv4          bool
67 69
 	Mtu                  int
... ...
@@ -145,6 +147,14 @@ type driver struct {
145 145
 	sync.Mutex
146 146
 }
147 147
 
148
+type gwMode string
149
+
150
+const (
151
+	gwModeDefault gwMode = ""
152
+	gwModeNAT     gwMode = "nat"
153
+	gwModeRouted  gwMode = "routed"
154
+)
155
+
148 156
 // New constructs a new bridge driver
149 157
 func newDriver() *driver {
150 158
 	return &driver{
... ...
@@ -289,6 +299,14 @@ func (c *networkConfiguration) fromLabels(labels map[string]string) error {
289 289
 			if c.EnableIPMasquerade, err = strconv.ParseBool(value); err != nil {
290 290
 				return parseErr(label, value, err.Error())
291 291
 			}
292
+		case IPv4GatewayMode:
293
+			if c.GwModeIPv4, err = newGwMode(value); err != nil {
294
+				return parseErr(label, value, err.Error())
295
+			}
296
+		case IPv6GatewayMode:
297
+			if c.GwModeIPv6, err = newGwMode(value); err != nil {
298
+				return parseErr(label, value, err.Error())
299
+			}
292 300
 		case EnableICC:
293 301
 			if c.EnableICC, err = strconv.ParseBool(value); err != nil {
294 302
 				return parseErr(label, value, err.Error())
... ...
@@ -321,6 +339,20 @@ func (c *networkConfiguration) fromLabels(labels map[string]string) error {
321 321
 	return nil
322 322
 }
323 323
 
324
+func newGwMode(gwMode string) (gwMode, error) {
325
+	switch gwMode {
326
+	case "nat":
327
+		return gwModeNAT, nil
328
+	case "routed":
329
+		return gwModeRouted, nil
330
+	}
331
+	return gwModeDefault, fmt.Errorf("unknown gateway mode %s", gwMode)
332
+}
333
+
334
+func (m gwMode) natDisabled() bool {
335
+	return m == gwModeRouted
336
+}
337
+
324 338
 func parseErr(label, value, errString string) error {
325 339
 	return types.InvalidParameterErrorf("failed to parse %s value: %v (%s)", label, value, errString)
326 340
 }
... ...
@@ -367,6 +399,12 @@ func (n *bridgeNetwork) getNetworkBridgeName() string {
367 367
 	return config.BridgeName
368 368
 }
369 369
 
370
+func (n *bridgeNetwork) getNATDisabled() (ipv4, ipv6 bool) {
371
+	n.Lock()
372
+	defer n.Unlock()
373
+	return n.config.GwModeIPv4.natDisabled(), n.config.GwModeIPv6.natDisabled()
374
+}
375
+
370 376
 func (n *bridgeNetwork) userlandProxyPath() string {
371 377
 	n.Lock()
372 378
 	defer n.Unlock()
... ...
@@ -123,6 +123,8 @@ func (ncfg *networkConfiguration) MarshalJSON() ([]byte, error) {
123 123
 	nMap["BridgeName"] = ncfg.BridgeName
124 124
 	nMap["EnableIPv6"] = ncfg.EnableIPv6
125 125
 	nMap["EnableIPMasquerade"] = ncfg.EnableIPMasquerade
126
+	nMap["GwModeIPv4"] = ncfg.GwModeIPv4
127
+	nMap["GwModeIPv6"] = ncfg.GwModeIPv6
126 128
 	nMap["EnableICC"] = ncfg.EnableICC
127 129
 	nMap["InhibitIPv4"] = ncfg.InhibitIPv4
128 130
 	nMap["Mtu"] = ncfg.Mtu
... ...
@@ -190,6 +192,12 @@ func (ncfg *networkConfiguration) UnmarshalJSON(b []byte) error {
190 190
 	ncfg.BridgeName = nMap["BridgeName"].(string)
191 191
 	ncfg.EnableIPv6 = nMap["EnableIPv6"].(bool)
192 192
 	ncfg.EnableIPMasquerade = nMap["EnableIPMasquerade"].(bool)
193
+	if v, ok := nMap["GwModeIPv4"]; ok {
194
+		ncfg.GwModeIPv4, _ = newGwMode(v.(string))
195
+	}
196
+	if v, ok := nMap["GwModeIPv6"]; ok {
197
+		ncfg.GwModeIPv6, _ = newGwMode(v.(string))
198
+	}
193 199
 	ncfg.EnableICC = nMap["EnableICC"].(bool)
194 200
 	if v, ok := nMap["InhibitIPv4"]; ok {
195 201
 		ncfg.InhibitIPv4 = v.(bool)
... ...
@@ -7,6 +7,11 @@ const (
7 7
 	// EnableIPMasquerade label for bridge driver
8 8
 	EnableIPMasquerade = "com.docker.network.bridge.enable_ip_masquerade"
9 9
 
10
+	// IPv4GatewayMode label for bridge driver
11
+	IPv4GatewayMode = "com.docker.network.bridge.gateway_mode_ipv4"
12
+	// IPv6GatewayMode label for bridge driver
13
+	IPv6GatewayMode = "com.docker.network.bridge.gateway_mode_ipv6"
14
+
10 15
 	// EnableICC label
11 16
 	EnableICC = "com.docker.network.bridge.enable_icc"
12 17
 
... ...
@@ -20,6 +20,11 @@ type portBinding struct {
20 20
 	stopProxy func() error
21 21
 }
22 22
 
23
+type portBindingReq struct {
24
+	types.PortBinding
25
+	disableNAT bool
26
+}
27
+
23 28
 // addPortMappings takes cfg, the configuration for port mappings, selects host
24 29
 // ports when ranges are given, starts docker-proxy or its dummy to reserve
25 30
 // host ports, and sets up iptables NAT/forwarding rules as necessary. If
... ...
@@ -60,9 +65,10 @@ func (n *bridgeNetwork) addPortMappings(
60 60
 	}()
61 61
 
62 62
 	proxyPath := n.userlandProxyPath()
63
+	disableNAT4, disableNAT6 := n.getNATDisabled()
63 64
 	for _, c := range cfg {
64
-		toBind := make([]types.PortBinding, 0, 2)
65
-		if bindingIPv4, ok := configurePortBindingIPv4(c, containerIPv4, defHostIP); ok {
65
+		toBind := make([]portBindingReq, 0, 2)
66
+		if bindingIPv4, ok := configurePortBindingIPv4(disableNAT4, c, containerIPv4, defHostIP); ok {
66 67
 			toBind = append(toBind, bindingIPv4)
67 68
 		}
68 69
 
... ...
@@ -78,7 +84,7 @@ func (n *bridgeNetwork) addPortMappings(
78 78
 		if proxyPath != "" && (containerIPv6 == nil) {
79 79
 			containerIP = containerIPv4
80 80
 		}
81
-		if bindingIPv6, ok := configurePortBindingIPv6(c, containerIP, defHostIP); ok {
81
+		if bindingIPv6, ok := configurePortBindingIPv6(disableNAT6, c, containerIP, defHostIP); ok {
82 82
 			toBind = append(toBind, bindingIPv6)
83 83
 		}
84 84
 
... ...
@@ -100,19 +106,19 @@ func (n *bridgeNetwork) addPortMappings(
100 100
 
101 101
 // configurePortBindingIPv4 returns a new port binding with the HostIP field populated
102 102
 // if a binding is required, else nil.
103
-func configurePortBindingIPv4(bnd types.PortBinding, containerIPv4, defHostIP net.IP) (types.PortBinding, bool) {
103
+func configurePortBindingIPv4(disableNAT bool, bnd types.PortBinding, containerIPv4, defHostIP net.IP) (portBindingReq, bool) {
104 104
 	if len(containerIPv4) == 0 {
105
-		return types.PortBinding{}, false
105
+		return portBindingReq{}, false
106 106
 	}
107 107
 	if len(bnd.HostIP) > 0 && bnd.HostIP.To4() == nil {
108 108
 		// The mapping is explicitly IPv6.
109
-		return types.PortBinding{}, false
109
+		return portBindingReq{}, false
110 110
 	}
111 111
 	// If there's no host address, use the default.
112 112
 	if len(bnd.HostIP) == 0 {
113 113
 		if defHostIP.To4() == nil {
114 114
 			// The default binding address is IPv6.
115
-			return types.PortBinding{}, false
115
+			return portBindingReq{}, false
116 116
 		}
117 117
 		bnd.HostIP = defHostIP
118 118
 	}
... ...
@@ -123,18 +129,21 @@ func configurePortBindingIPv4(bnd types.PortBinding, containerIPv4, defHostIP ne
123 123
 	if bnd.HostPortEnd == 0 {
124 124
 		bnd.HostPortEnd = bnd.HostPort
125 125
 	}
126
-	return bnd, true
126
+	return portBindingReq{
127
+		PortBinding: bnd,
128
+		disableNAT:  disableNAT,
129
+	}, true
127 130
 }
128 131
 
129 132
 // configurePortBindingIPv6 returns a new port binding with the HostIP field populated
130 133
 // if a binding is required, else nil.
131
-func configurePortBindingIPv6(bnd types.PortBinding, containerIP, defHostIP net.IP) (types.PortBinding, bool) {
134
+func configurePortBindingIPv6(disableNAT bool, bnd types.PortBinding, containerIP, defHostIP net.IP) (portBindingReq, bool) {
132 135
 	if containerIP == nil {
133
-		return types.PortBinding{}, false
136
+		return portBindingReq{}, false
134 137
 	}
135 138
 	if len(bnd.HostIP) > 0 && bnd.HostIP.To4() != nil {
136 139
 		// The mapping is explicitly IPv4.
137
-		return types.PortBinding{}, false
140
+		return portBindingReq{}, false
138 141
 	}
139 142
 
140 143
 	// If there's no host address, use the default.
... ...
@@ -142,7 +151,7 @@ func configurePortBindingIPv6(bnd types.PortBinding, containerIP, defHostIP net.
142 142
 		if defHostIP.Equal(net.IPv4zero) {
143 143
 			if !netutils.IsV6Listenable() {
144 144
 				// No implicit binding if the host has no IPv6 support.
145
-				return types.PortBinding{}, false
145
+				return portBindingReq{}, false
146 146
 			}
147 147
 			// Implicit binding to "::", no explicit HostIP and the default is 0.0.0.0
148 148
 			bnd.HostIP = net.IPv6zero
... ...
@@ -151,7 +160,7 @@ func configurePortBindingIPv6(bnd types.PortBinding, containerIP, defHostIP net.
151 151
 			bnd.HostIP = defHostIP
152 152
 		} else {
153 153
 			// The default binding IP is an IPv4 address, nothing to do here.
154
-			return types.PortBinding{}, false
154
+			return portBindingReq{}, false
155 155
 		}
156 156
 	}
157 157
 	bnd.IP = containerIP
... ...
@@ -159,13 +168,16 @@ func configurePortBindingIPv6(bnd types.PortBinding, containerIP, defHostIP net.
159 159
 	if bnd.HostPortEnd == 0 {
160 160
 		bnd.HostPortEnd = bnd.HostPort
161 161
 	}
162
-	return bnd, true
162
+	return portBindingReq{
163
+		PortBinding: bnd,
164
+		disableNAT:  disableNAT,
165
+	}, true
163 166
 }
164 167
 
165 168
 // bindHostPorts allocates ports and starts docker-proxy for the given cfg. The
166 169
 // caller is responsible for ensuring that all entries in cfg map the same proto,
167 170
 // container port, and host port range (their host addresses must differ).
168
-func bindHostPorts(cfg []types.PortBinding, proxyPath string) ([]portBinding, error) {
171
+func bindHostPorts(cfg []portBindingReq, proxyPath string) ([]portBinding, error) {
169 172
 	if len(cfg) == 0 {
170 173
 		return nil, nil
171 174
 	}
... ...
@@ -208,45 +220,60 @@ var startProxy = portmapper.StartProxy
208 208
 // already bound the port), all resources are released and an error is returned.
209 209
 // When ports are successfully reserved, a portBinding is returned for each
210 210
 // mapping.
211
+//
212
+// If NAT is disabled for any of the bindings, no host port reservation is
213
+// needed. These bindings are included in results, as the container port itself
214
+// needs to be opened in the firewall.
211 215
 func attemptBindHostPorts(
212
-	cfg []types.PortBinding,
216
+	cfg []portBindingReq,
213 217
 	proto string,
214 218
 	hostPortStart, hostPortEnd uint16,
215 219
 	proxyPath string,
216 220
 ) (_ []portBinding, retErr error) {
221
+	var err error
222
+	var port int
223
+
217 224
 	addrs := make([]net.IP, 0, len(cfg))
218 225
 	for _, c := range cfg {
219
-		addrs = append(addrs, c.HostIP)
220
-	}
221
-
222
-	pa := portallocator.Get()
223
-	port, err := pa.RequestPortsInRange(addrs, proto, int(hostPortStart), int(hostPortEnd))
224
-	if err != nil {
225
-		return nil, err
226
-	}
227
-	defer func() {
228
-		if retErr != nil {
229
-			for _, a := range addrs {
230
-				pa.ReleasePort(a, proto, port)
231
-			}
226
+		if !c.disableNAT {
227
+			addrs = append(addrs, c.HostIP)
232 228
 		}
233
-	}()
229
+	}
234 230
 
235
-	res := make([]portBinding, 0, len(cfg))
236
-	for _, c := range cfg {
237
-		pb := portBinding{PortBinding: c.GetCopy()}
238
-		pb.stopProxy, err = startProxy(c.Proto.String(), c.HostIP, port, c.IP, int(c.Port), proxyPath)
231
+	if len(addrs) > 0 {
232
+		pa := portallocator.Get()
233
+		port, err = pa.RequestPortsInRange(addrs, proto, int(hostPortStart), int(hostPortEnd))
239 234
 		if err != nil {
240
-			return nil, fmt.Errorf("failed to bind port %s:%d/%s: %w", c.HostIP, port, c.Proto, err)
235
+			return nil, err
241 236
 		}
242 237
 		defer func() {
243 238
 			if retErr != nil {
244
-				if err := pb.stopProxy(); err != nil {
245
-					log.G(context.TODO()).Warnf("Failed to stop userland proxy for port mapping %s: %s", pb, err)
239
+				for _, a := range addrs {
240
+					pa.ReleasePort(a, proto, port)
246 241
 				}
247 242
 			}
248 243
 		}()
249
-		pb.HostPort = uint16(port)
244
+	}
245
+
246
+	res := make([]portBinding, 0, len(cfg))
247
+	for _, c := range cfg {
248
+		pb := portBinding{PortBinding: c.GetCopy()}
249
+		if c.disableNAT {
250
+			pb.HostPort = 0
251
+		} else {
252
+			pb.stopProxy, err = startProxy(c.Proto.String(), c.HostIP, port, c.IP, int(c.Port), proxyPath)
253
+			if err != nil {
254
+				return nil, fmt.Errorf("failed to bind port %s:%d/%s: %w", c.HostIP, port, c.Proto, err)
255
+			}
256
+			defer func() {
257
+				if retErr != nil {
258
+					if err := pb.stopProxy(); err != nil {
259
+						log.G(context.TODO()).Warnf("Failed to stop userland proxy for port mapping %s: %s", pb, err)
260
+					}
261
+				}
262
+			}()
263
+			pb.HostPort = uint16(port)
264
+		}
250 265
 		pb.HostPortEnd = pb.HostPort
251 266
 		res = append(res, pb)
252 267
 	}
... ...
@@ -266,15 +293,20 @@ func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
266 266
 func (n *bridgeNetwork) releasePortBindings(pbs []portBinding) error {
267 267
 	var errs []error
268 268
 	for _, pb := range pbs {
269
-		errP := pb.stopProxy()
270
-		if errP != nil {
271
-			errP = fmt.Errorf("failed to stop docker-proxy for port mapping %s: %w", pb, errP)
269
+		var errP error
270
+		if pb.stopProxy != nil {
271
+			errP = pb.stopProxy()
272
+			if errP != nil {
273
+				errP = fmt.Errorf("failed to stop docker-proxy for port mapping %s: %w", pb, errP)
274
+			}
272 275
 		}
273 276
 		errN := n.setPerPortIptables(pb, false)
274 277
 		if errN != nil {
275 278
 			errN = fmt.Errorf("failed to remove iptables rules for port mapping %s: %w", pb, errN)
276 279
 		}
277
-		portallocator.Get().ReleasePort(pb.HostIP, pb.Proto.String(), int(pb.HostPort))
280
+		if pb.HostPort > 0 {
281
+			portallocator.Get().ReleasePort(pb.HostIP, pb.Proto.String(), int(pb.HostPort))
282
+		}
278 283
 		errs = append(errs, errP, errN)
279 284
 	}
280 285
 	return errors.Join(errs...)
... ...
@@ -309,6 +341,10 @@ func (n *bridgeNetwork) setPerPortIptables(b portBinding, enable bool) error {
309 309
 }
310 310
 
311 311
 func setPerPortNAT(b portBinding, ipv iptables.IPVersion, proxyPath string, bridgeName string, enable bool) error {
312
+	if b.HostPort == 0 {
313
+		// NAT is disabled.
314
+		return nil
315
+	}
312 316
 	// iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we
313 317
 	// want "0.0.0.0/0". "0/0" is correctly interpreted as "any
314 318
 	// value" by both iptables and ip6tables.
... ...
@@ -181,18 +181,22 @@ func loopbackUp() error {
181 181
 }
182 182
 
183 183
 func TestBindHostPortsError(t *testing.T) {
184
-	cfg := []types.PortBinding{
184
+	cfg := []portBindingReq{
185 185
 		{
186
-			Proto:       types.TCP,
187
-			Port:        80,
188
-			HostPort:    8080,
189
-			HostPortEnd: 8080,
186
+			PortBinding: types.PortBinding{
187
+				Proto:       types.TCP,
188
+				Port:        80,
189
+				HostPort:    8080,
190
+				HostPortEnd: 8080,
191
+			},
190 192
 		},
191 193
 		{
192
-			Proto:       types.TCP,
193
-			Port:        80,
194
-			HostPort:    8080,
195
-			HostPortEnd: 8081,
194
+			PortBinding: types.PortBinding{
195
+				Proto:       types.TCP,
196
+				Port:        80,
197
+				HostPort:    8080,
198
+				HostPortEnd: 8081,
199
+			},
196 200
 		},
197 201
 	}
198 202
 	pbs, err := bindHostPorts(cfg, "")
... ...
@@ -218,6 +222,8 @@ func TestAddPortMappings(t *testing.T) {
218 218
 		name         string
219 219
 		epAddrV4     *net.IPNet
220 220
 		epAddrV6     *net.IPNet
221
+		gwMode4      gwMode
222
+		gwMode6      gwMode
221 223
 		cfg          []types.PortBinding
222 224
 		defHostIP    net.IP
223 225
 		proxyPath    string
... ...
@@ -258,6 +264,18 @@ func TestAddPortMappings(t *testing.T) {
258 258
 			},
259 259
 		},
260 260
 		{
261
+			name:     "nat explicitly enabled",
262
+			epAddrV4: ctrIP4,
263
+			epAddrV6: ctrIP6,
264
+			cfg:      []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
265
+			gwMode4:  gwModeNAT,
266
+			gwMode6:  gwModeNAT,
267
+			expPBs: []types.PortBinding{
268
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
269
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
270
+			},
271
+		},
272
+		{
261 273
 			name:         "specific host port in-use",
262 274
 			epAddrV4:     ctrIP4,
263 275
 			epAddrV6:     ctrIP6,
... ...
@@ -430,6 +448,55 @@ func TestAddPortMappings(t *testing.T) {
430 430
 				"failed to stop docker-proxy for port mapping tcp/172.19.0.2:22/0.0.0.0:2222: can't stop now\n" +
431 431
 				"failed to stop docker-proxy for port mapping tcp/fdf8:b88e:bb5c:3483::2:22/:::2222: can't stop now",
432 432
 		},
433
+		{
434
+			name:     "disable nat6",
435
+			epAddrV4: ctrIP4,
436
+			epAddrV6: ctrIP6,
437
+			cfg: []types.PortBinding{
438
+				{Proto: types.TCP, Port: 22},
439
+				{Proto: types.TCP, Port: 80},
440
+			},
441
+			gwMode6: gwModeRouted,
442
+			expPBs: []types.PortBinding{
443
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
444
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero},
445
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
446
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero},
447
+			},
448
+		},
449
+		{
450
+			name:     "disable nat4",
451
+			epAddrV4: ctrIP4,
452
+			epAddrV6: ctrIP6,
453
+			cfg: []types.PortBinding{
454
+				{Proto: types.TCP, Port: 22},
455
+				{Proto: types.TCP, Port: 80},
456
+			},
457
+			gwMode4: gwModeRouted,
458
+			expPBs: []types.PortBinding{
459
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero},
460
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
461
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero},
462
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
463
+			},
464
+		},
465
+		{
466
+			name:     "disable nat",
467
+			epAddrV4: ctrIP4,
468
+			epAddrV6: ctrIP6,
469
+			cfg: []types.PortBinding{
470
+				{Proto: types.TCP, Port: 22},
471
+				{Proto: types.TCP, Port: 80},
472
+			},
473
+			gwMode4: gwModeRouted,
474
+			gwMode6: gwModeRouted,
475
+			expPBs: []types.PortBinding{
476
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero},
477
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero},
478
+				{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero},
479
+				{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero},
480
+			},
481
+		},
433 482
 	}
434 483
 
435 484
 	for _, tc := range testcases {
... ...
@@ -470,6 +537,8 @@ func TestAddPortMappings(t *testing.T) {
470 470
 				config: &networkConfiguration{
471 471
 					BridgeName: "dummybridge",
472 472
 					EnableIPv6: tc.epAddrV6 != nil,
473
+					GwModeIPv4: tc.gwMode4,
474
+					GwModeIPv6: tc.gwMode6,
473 475
 				},
474 476
 				driver: newDriver(),
475 477
 			}
... ...
@@ -497,14 +566,17 @@ func TestAddPortMappings(t *testing.T) {
497 497
 
498 498
 			// Check the iptables rules.
499 499
 			for _, expPB := range tc.expPBs {
500
+				var disableNAT bool
500 501
 				var addrM, addrD, addrH string
501 502
 				var ipv iptables.IPVersion
502 503
 				if expPB.IP.To4() == nil {
504
+					disableNAT = tc.gwMode6.natDisabled()
503 505
 					ipv = iptables.IPv6
504 506
 					addrM = ctrIP6.IP.String() + "/128"
505 507
 					addrD = "[" + ctrIP6.IP.String() + "]"
506 508
 					addrH = expPB.HostIP.String() + "/128"
507 509
 				} else {
510
+					disableNAT = tc.gwMode4.natDisabled()
508 511
 					ipv = iptables.IPv4
509 512
 					addrM = ctrIP4.IP.String() + "/32"
510 513
 					addrD = ctrIP4.IP.String()
... ...
@@ -518,7 +590,11 @@ func TestAddPortMappings(t *testing.T) {
518 518
 				masqRule := fmt.Sprintf("-s %s -d %s -p %s -m %s --dport %d -j MASQUERADE",
519 519
 					addrM, addrM, expPB.Proto, expPB.Proto, expPB.Port)
520 520
 				ir := iptRule{ipv: ipv, table: iptables.Nat, chain: "POSTROUTING", args: strings.Split(masqRule, " ")}
521
-				assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
521
+				if disableNAT {
522
+					assert.Check(t, !ir.Exists(), fmt.Sprintf("unexpected rule %s", ir))
523
+				} else {
524
+					assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
525
+				}
522 526
 
523 527
 				// Check the DNAT rule.
524 528
 				dnatRule := ""
... ...
@@ -529,7 +605,11 @@ func TestAddPortMappings(t *testing.T) {
529 529
 				dnatRule += fmt.Sprintf("-d %s -p %s -m %s --dport %d -j DNAT --to-destination %s:%d",
530 530
 					addrH, expPB.Proto, expPB.Proto, expPB.HostPort, addrD, expPB.Port)
531 531
 				ir = iptRule{ipv: ipv, table: iptables.Nat, chain: "DOCKER", args: strings.Split(dnatRule, " ")}
532
-				assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
532
+				if disableNAT {
533
+					assert.Check(t, !ir.Exists(), fmt.Sprintf("unexpected rule %s", ir))
534
+				} else {
535
+					assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
536
+				}
533 537
 
534 538
 				// Check that the container's port is open.
535 539
 				filterRule := fmt.Sprintf("-d %s ! -i dummybridge -o dummybridge -p %s -m %s --dport %d -j ACCEPT",
... ...
@@ -549,6 +629,10 @@ func TestAddPortMappings(t *testing.T) {
549 549
 			// Check a docker-proxy was started and stopped for each expected port binding.
550 550
 			expProxies := map[proxyCall]bool{}
551 551
 			for _, expPB := range tc.expPBs {
552
+				is4 := expPB.HostIP.To4() != nil
553
+				if (is4 && tc.gwMode4.natDisabled()) || (!is4 && tc.gwMode6.natDisabled()) {
554
+					continue
555
+				}
552 556
 				p := newProxyCall(expPB.Proto.String(),
553 557
 					expPB.HostIP, int(expPB.HostPort),
554 558
 					expPB.IP, int(expPB.Port), tc.proxyPath)
... ...
@@ -255,8 +255,10 @@ func setupIPTablesInternal(ipVer iptables.IPVersion, config *networkConfiguratio
255 255
 		hpNatArgs []string
256 256
 	)
257 257
 	hostIP := config.HostIPv4
258
+	nat := !config.GwModeIPv4.natDisabled()
258 259
 	if ipVer == iptables.IPv6 {
259 260
 		hostIP = config.HostIPv6
261
+		nat = !config.GwModeIPv6.natDisabled()
260 262
 	}
261 263
 	// If hostIP is set, the user wants IPv4/IPv6 SNAT with the given address.
262 264
 	if hostIP != nil {
... ...
@@ -273,15 +275,14 @@ func setupIPTablesInternal(ipVer iptables.IPVersion, config *networkConfiguratio
273 273
 	hpNatRule := iptRule{ipv: ipVer, table: iptables.Nat, chain: "POSTROUTING", args: hpNatArgs}
274 274
 
275 275
 	// Set NAT.
276
-	if config.EnableIPMasquerade {
276
+	if nat && config.EnableIPMasquerade {
277 277
 		if err := programChainRule(natRule, "NAT", enable); err != nil {
278 278
 			return err
279 279
 		}
280
-	}
281
-
282
-	if config.EnableIPMasquerade && !hairpin {
283
-		if err := programChainRule(skipDNAT, "SKIP DNAT", enable); err != nil {
284
-			return err
280
+		if !hairpin {
281
+			if err := programChainRule(skipDNAT, "SKIP DNAT", enable); err != nil {
282
+				return err
283
+			}
285 284
 		}
286 285
 	}
287 286