Browse code

Support host.docker.internal in dockerd on Linux

Docker Desktop (on MAC and Windows hosts) allows containers
running inside a Linux VM to connect to the host using
the host.docker.internal DNS name, which is implemented by
VPNkit (DNS proxy on the host)

This PR allows containers to connect to Linux hosts
by appending a special string "host-gateway" to --add-host
e.g. "--add-host=host.docker.internal:host-gateway" which adds
host.docker.internal DNS entry in /etc/hosts and maps it to host-gateway-ip

This PR also add a daemon flag call host-gateway-ip which defaults to
the default bridge IP
Docker Desktop will need to set this field to the Host Proxy IP
so DNS requests for host.docker.internal can be routed to VPNkit

Addresses: https://github.com/docker/for-linux/issues/264

Signed-off-by: Arko Dasgupta <arko.dasgupta@docker.com>

Arko Dasgupta authored on 2019/11/02 09:09:40
Showing 8 changed files
... ...
@@ -64,6 +64,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
64 64
 	flags.Var(opts.NewListOptsRef(&conf.DNS, opts.ValidateIPAddress), "dns", "DNS server to use")
65 65
 	flags.Var(opts.NewNamedListOptsRef("dns-opts", &conf.DNSOptions, nil), "dns-opt", "DNS options to use")
66 66
 	flags.Var(opts.NewListOptsRef(&conf.DNSSearch, opts.ValidateDNSSearch), "dns-search", "DNS search domains to use")
67
+	flags.Var(opts.NewIPOpt(&conf.HostGatewayIP, ""), "host-gateway-ip", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the default bridge")
67 68
 	flags.Var(opts.NewNamedListOptsRef("labels", &conf.Labels, opts.ValidateLabel), "label", "Set key=value labels to the daemon")
68 69
 	flags.StringVar(&conf.LogConfig.Type, "log-driver", "json-file", "Default driver for container logs")
69 70
 	flags.Var(opts.NewNamedMapOpts("log-opts", conf.LogConfig.Config, nil), "log-opt", "Default log driver options for containers")
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"fmt"
7 7
 	"io"
8 8
 	"io/ioutil"
9
+	"net"
9 10
 	"os"
10 11
 	"reflect"
11 12
 	"strings"
... ...
@@ -115,9 +116,10 @@ type CommonTLSOptions struct {
115 115
 
116 116
 // DNSConfig defines the DNS configurations.
117 117
 type DNSConfig struct {
118
-	DNS        []string `json:"dns,omitempty"`
119
-	DNSOptions []string `json:"dns-opts,omitempty"`
120
-	DNSSearch  []string `json:"dns-search,omitempty"`
118
+	DNS           []string `json:"dns,omitempty"`
119
+	DNSOptions    []string `json:"dns-opts,omitempty"`
120
+	DNSSearch     []string `json:"dns-search,omitempty"`
121
+	HostGatewayIP net.IP   `json:"host-gateway-ip,omitempty"`
121 122
 }
122 123
 
123 124
 // CommonConfig defines the configuration of a docker daemon which is
... ...
@@ -115,6 +115,16 @@ func (daemon *Daemon) buildSandboxOptions(container *container.Container) ([]lib
115 115
 			return nil, err
116 116
 		}
117 117
 		parts := strings.SplitN(extraHost, ":", 2)
118
+		// If the IP Address is a string called "host-gateway", replace this
119
+		// value with the IP address stored in the daemon level HostGatewayIP
120
+		// config variable
121
+		if parts[1] == network.HostGatewayName {
122
+			gateway := daemon.configStore.HostGatewayIP.String()
123
+			if gateway == "" {
124
+				return nil, fmt.Errorf("unable to derive the IP value for host-gateway")
125
+			}
126
+			parts[1] = gateway
127
+		}
118 128
 		sboxOptions = append(sboxOptions, libnetwork.OptionExtraHost(parts[0], parts[1]))
119 129
 	}
120 130
 
... ...
@@ -925,6 +925,19 @@ func (daemon *Daemon) initNetworkController(config *config.Config, activeSandbox
925 925
 		removeDefaultBridgeInterface()
926 926
 	}
927 927
 
928
+	// Set HostGatewayIP to the default bridge's IP  if it is empty
929
+	if daemon.configStore.HostGatewayIP == nil && controller != nil {
930
+		if n, err := controller.NetworkByName("bridge"); err == nil {
931
+			v4Info, v6Info := n.Info().IpamInfo()
932
+			var gateway net.IP
933
+			if len(v4Info) > 0 {
934
+				gateway = v4Info[0].Gateway.IP
935
+			} else if len(v6Info) > 0 {
936
+				gateway = v6Info[0].Gateway.IP
937
+			}
938
+			daemon.configStore.HostGatewayIP = gateway
939
+		}
940
+	}
928 941
 	return controller, nil
929 942
 }
930 943
 
931 944
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+package network
1
+
2
+const (
3
+	// HostGatewayName is the string value that can be passed
4
+	// to the IPAddr section in --add-host that is replaced by
5
+	// the value of HostGatewayIP daemon config value
6
+	HostGatewayName = "host-gateway"
7
+)
... ...
@@ -120,3 +120,47 @@ func TestDaemonRestartIpcMode(t *testing.T) {
120 120
 	assert.NilError(t, err)
121 121
 	assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "shareable"))
122 122
 }
123
+
124
+// TestDaemonHostGatewayIP verifies that when a magic string "host-gateway" is passed
125
+// to ExtraHosts (--add-host) instead of an IP address, its value is set to
126
+// 1. Daemon config flag value specified by host-gateway-ip or
127
+// 2. IP of the default bridge network
128
+// and is added to the /etc/hosts file
129
+func TestDaemonHostGatewayIP(t *testing.T) {
130
+	skip.If(t, testEnv.IsRemoteDaemon)
131
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
132
+	t.Parallel()
133
+
134
+	// Verify the IP in /etc/hosts is same as host-gateway-ip
135
+	d := daemon.New(t)
136
+	// Verify the IP in /etc/hosts is same as the default bridge's IP
137
+	d.StartWithBusybox(t)
138
+	c := d.NewClientT(t)
139
+	ctx := context.Background()
140
+	cID := container.Run(ctx, t, c,
141
+		container.WithExtraHost("host.docker.internal:host-gateway"),
142
+	)
143
+	res, err := container.Exec(ctx, c, cID, []string{"cat", "/etc/hosts"})
144
+	assert.NilError(t, err)
145
+	assert.Assert(t, is.Len(res.Stderr(), 0))
146
+	assert.Equal(t, 0, res.ExitCode)
147
+	inspect, err := c.NetworkInspect(ctx, "bridge", types.NetworkInspectOptions{})
148
+	assert.NilError(t, err)
149
+	assert.Check(t, is.Contains(res.Stdout(), inspect.IPAM.Config[0].Gateway))
150
+	c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
151
+	d.Stop(t)
152
+
153
+	// Verify the IP in /etc/hosts is same as host-gateway-ip
154
+	d.StartWithBusybox(t, "--host-gateway-ip=6.7.8.9")
155
+	cID = container.Run(ctx, t, c,
156
+		container.WithExtraHost("host.docker.internal:host-gateway"),
157
+	)
158
+	res, err = container.Exec(ctx, c, cID, []string{"cat", "/etc/hosts"})
159
+	assert.NilError(t, err)
160
+	assert.Assert(t, is.Len(res.Stderr(), 0))
161
+	assert.Equal(t, 0, res.ExitCode)
162
+	assert.Check(t, is.Contains(res.Stdout(), "6.7.8.9"))
163
+	c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
164
+	d.Stop(t)
165
+
166
+}
... ...
@@ -180,3 +180,11 @@ func WithCgroupnsMode(mode string) func(*TestContainerConfig) {
180 180
 		c.HostConfig.CgroupnsMode = containertypes.CgroupnsMode(mode)
181 181
 	}
182 182
 }
183
+
184
+// WithExtraHost sets the user defined IP:Host mappings in the container's
185
+// /etc/hosts file
186
+func WithExtraHost(extraHost string) func(*TestContainerConfig) {
187
+	return func(c *TestContainerConfig) {
188
+		c.HostConfig.ExtraHosts = append(c.HostConfig.ExtraHosts, extraHost)
189
+	}
190
+}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"strconv"
9 9
 	"strings"
10 10
 
11
+	"github.com/docker/docker/daemon/network"
11 12
 	"github.com/docker/docker/pkg/homedir"
12 13
 )
13 14
 
... ...
@@ -169,8 +170,11 @@ func ValidateExtraHost(val string) (string, error) {
169 169
 	if len(arr) != 2 || len(arr[0]) == 0 {
170 170
 		return "", fmt.Errorf("bad format for add-host: %q", val)
171 171
 	}
172
-	if _, err := ValidateIPAddress(arr[1]); err != nil {
173
-		return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1])
172
+	// Skip IPaddr validation for special "host-gateway" string
173
+	if arr[1] != network.HostGatewayName {
174
+		if _, err := ValidateIPAddress(arr[1]); err != nil {
175
+			return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1])
176
+		}
174 177
 	}
175 178
 	return val, nil
176 179
 }