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>
| ... | ... |
@@ -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 |
|