diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index 837fc0d0085e..ef345c0c41de 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -210,7 +210,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.SetAnnotation("cgroupns", "version", []string{"1.41"}) // Network and port publishing flag - flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") + flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip, or host=ip)") flags.Var(&copts.dns, "dns", "Set custom DNS servers") // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. // This is to be consistent with service create/update diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 732f093e3a4f..d7ec421a4f3b 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -139,7 +139,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") flags.SetAnnotation("network", "version", []string{"1.25"}) - flags.Var(&options.extraHosts, "add-host", `Add a custom host-to-IP mapping ("host:ip")`) + flags.Var(&options.extraHosts, "add-host", `Add a custom host-to-IP mapping ("host:ip" or "host=ip")`) flags.StringVar(&options.target, "target", "", "Set the target build stage to build.") flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md index 2f8be4f7f97e..b162229182df 100644 --- a/docs/reference/commandline/build.md +++ b/docs/reference/commandline/build.md @@ -11,7 +11,7 @@ Build an image from a Dockerfile | Name | Type | Default | Description | |:------------------------------------|:--------------|:----------|:------------------------------------------------------------------| -| [`--add-host`](#add-host) | `list` | | Add a custom host-to-IP mapping (`host:ip`) | +| [`--add-host`](#add-host) | `list` | | Add a custom host-to-IP mapping (`host:ip` or `host=ip`) | | [`--build-arg`](#build-arg) | `list` | | Set build-time variables | | [`--cache-from`](#cache-from) | `stringSlice` | | Images to consider as cache sources | | [`--cgroup-parent`](#cgroup-parent) | `string` | | Set the parent cgroup for the `RUN` instructions during build | @@ -462,6 +462,12 @@ more `--add-host` flags. This example adds a static address for a host named $ docker build --add-host docker:10.180.0.1 . ``` +For IPv6 addresses, the equivalent `host=ip` form may be clearer, for example: + +```console +$ docker build --add-host thishost=::1 . +``` + If you need your build to connect to services running on the host, you can use the special `host-gateway` value for `--add-host`. In the following example, build containers resolve `host.docker.internal` to the host's gateway IP. diff --git a/docs/reference/commandline/image_build.md b/docs/reference/commandline/image_build.md index e27c85964ec3..8ca37802fdfd 100644 --- a/docs/reference/commandline/image_build.md +++ b/docs/reference/commandline/image_build.md @@ -11,7 +11,7 @@ Build an image from a Dockerfile | Name | Type | Default | Description | |:--------------------------|:--------------|:----------|:------------------------------------------------------------------| -| `--add-host` | `list` | | Add a custom host-to-IP mapping (`host:ip`) | +| `--add-host` | `list` | | Add a custom host-to-IP mapping (`host:ip` or `host=ip`) | | `--build-arg` | `list` | | Set build-time variables | | `--cache-from` | `stringSlice` | | Images to consider as cache sources | | `--cgroup-parent` | `string` | | Set the parent cgroup for the `RUN` instructions during build | diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index 247e60a17eff..abd2c56c45d5 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -11,7 +11,7 @@ Create and run a new container from an image | Name | Type | Default | Description | |:----------------------------------------------|:--------------|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`--add-host`](#add-host) | `list` | | Add a custom host-to-IP mapping (host:ip) | +| [`--add-host`](#add-host) | `list` | | Add a custom host-to-IP mapping (host:ip, or host=ip) | | `--annotation` | `map` | `map[]` | Add an annotation to the container (passed through to the OCI runtime) | | [`-a`](#attach), [`--attach`](#attach) | `list` | | Attach to STDIN, STDOUT or STDERR | | `--blkio-weight` | `uint16` | `0` | Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0) | @@ -762,6 +762,12 @@ PING docker (93.184.216.34): 56 data bytes round-trip min/avg/max = 92.209/92.495/93.052 ms ``` +For IPv6 addresses, the equivalent `host=ip` form may be clearer. For example: + +```console +$ docker run --add-host=thishost=::1 --rm -it alpine +``` + The `--add-host` flag supports a special `host-gateway` value that resolves to the internal IP address of the host. This is useful when you want containers to connect to services running on the host machine. diff --git a/man/docker-build.1.md b/man/docker-build.1.md index 341a3db3a122..4411a7b832b7 100644 --- a/man/docker-build.1.md +++ b/man/docker-build.1.md @@ -78,10 +78,10 @@ set as the **URL**, the repository is cloned locally and then sent as the contex layers in tact, and one for the squashed version. **--add-host** [] - Add a custom host-to-IP mapping (host:ip) + Add a custom host-to-IP mapping (host:ip, or host=ip) - Add a line to /etc/hosts. The format is hostname:ip. The **--add-host** -option can be set multiple times. + Add a line to /etc/hosts. The format is hostname:ip, or hostname=ip. The +**--add-host** option can be set multiple times. **--build-arg** *variable* name and value of a **buildarg**. diff --git a/man/docker-run.1.md b/man/docker-run.1.md index 7b6384c66885..17922db97a33 100644 --- a/man/docker-run.1.md +++ b/man/docker-run.1.md @@ -121,10 +121,10 @@ executables expect) and pass along signals. The **-a** option can be set for each of stdin, stdout, and stderr. **--add-host**=[] - Add a custom host-to-IP mapping (host:ip) + Add a custom host-to-IP mapping (host:ip, or host=ip) - Add a line to /etc/hosts. The format is hostname:ip. The **--add-host** -option can be set multiple times. + Add a line to /etc/hosts. The format is hostname:ip, or hostname=ip. The +**--add-host** option can be set multiple times. **--annotation**=[] Add an annotation to the container (passed through to the OCI runtime). diff --git a/opts/hosts.go b/opts/hosts.go index 7cdd1218f7c6..17c02043a246 100644 --- a/opts/hosts.go +++ b/opts/hosts.go @@ -161,21 +161,53 @@ func ParseTCPAddr(tryAddr string, defaultAddr string) (string, error) { return fmt.Sprintf("tcp://%s%s", net.JoinHostPort(host, port), u.Path), nil } -// ValidateExtraHost validates that the specified string is a valid extrahost and returns it. -// ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6). +// ValidateExtraHost validates that the specified string is a valid extrahost and +// returns it. ExtraHost is in the form of name:ip or name=ip, where the ip has +// to be a valid ip (IPv4 or IPv6). The address may be enclosed in square +// brackets. // -// TODO(thaJeztah): remove client-side validation, and delegate to the API server. +// For example: +// +// hostname:127.0.0.1 +// hostname:::1 +// hostname=::1 +// hostname:[::1] +// +// For compatibility with the API server, this function normalises the given +// argument to use the ':' separator and strip square brackets enclosing the +// address. func ValidateExtraHost(val string) (string, error) { - // allow for IPv6 addresses in extra hosts by only splitting on first ":" - k, v, ok := strings.Cut(val, ":") - if !ok || k == "" { + k, v, ok := strings.Cut(val, "=") + if !ok { + // allow for IPv6 addresses in extra hosts by only splitting on first ":" + k, v, ok = strings.Cut(val, ":") + } + // Check that a hostname was given, and that it doesn't contain a ":". (Colon + // isn't allowed in a hostname, along with many other characters. It's + // special-cased here because the API server doesn't know about '=' separators in + // '--add-host'. So, it'll split at the first colon and generate a strange error + // message.) + if !ok || k == "" || strings.Contains(k, ":") { return "", fmt.Errorf("bad format for add-host: %q", val) } // Skip IPaddr validation for "host-gateway" string if v != hostGatewayName { + // If the address is enclosed in square brackets, extract it (for IPv6, but + // permit it for IPv4 as well; we don't know the address family here, but it's + // unambiguous). + if len(v) > 2 && v[0] == '[' && v[len(v)-1] == ']' { + v = v[1 : len(v)-1] + } + // ValidateIPAddress returns the address in canonical form (for example, + // 0:0:0:0:0:0:0:1 -> ::1). But, stick with the original form, to avoid + // surprising a user who's expecting to see the address they supplied in the + // output of 'docker inspect' or '/etc/hosts'. if _, err := ValidateIPAddress(v); err != nil { return "", fmt.Errorf("invalid IP address in add-host: %q", v) } } - return val, nil + // This result is passed directly to the API, the daemon doesn't accept the '=' + // separator or an address enclosed in brackets. So, construct something it can + // understand. + return k + ":" + v, nil } diff --git a/opts/hosts_test.go b/opts/hosts_test.go index 9d38d164be72..33e45d2256df 100644 --- a/opts/hosts_test.go +++ b/opts/hosts_test.go @@ -2,8 +2,10 @@ package opts import ( "fmt" - "strings" "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" ) func TestParseHost(t *testing.T) { @@ -146,34 +148,152 @@ func TestParseInvalidUnixAddrInvalid(t *testing.T) { } func TestValidateExtraHosts(t *testing.T) { - valid := []string{ - `myhost:192.168.0.1`, - `thathost:10.0.2.1`, - `anipv6host:2003:ab34:e::1`, - `ipv6local:::1`, - `host.docker.internal:host-gateway`, - } - - invalid := map[string]string{ - `myhost:192.notanipaddress.1`: `invalid IP`, - `thathost-nosemicolon10.0.0.1`: `bad format`, - `anipv6host:::::1`: `invalid IP`, - `ipv6local:::0::`: `invalid IP`, + tests := []struct { + doc string + input string + expectedOut string // Expect output==input if not set. + expectedErr string // Expect success if not set. + }{ + { + doc: "IPv4, colon sep", + input: `myhost:192.168.0.1`, + }, + { + doc: "IPv4, eq sep", + input: `myhost=192.168.0.1`, + expectedOut: `myhost:192.168.0.1`, + }, + { + doc: "Weird but permitted, IPv4 with brackets", + input: `myhost=[192.168.0.1]`, + expectedOut: `myhost:192.168.0.1`, + }, + { + doc: "Host and domain", + input: `host.and.domain:10.0.2.1`, + }, + { + doc: "IPv6, colon sep", + input: `anipv6host:2003:ab34:e::1`, + }, + { + doc: "IPv6, colon sep, brackets", + input: `anipv6host:[2003:ab34:e::1]`, + expectedOut: `anipv6host:2003:ab34:e::1`, + }, + { + doc: "IPv6, eq sep, brackets", + input: `anipv6host=[2003:ab34:e::1]`, + expectedOut: `anipv6host:2003:ab34:e::1`, + }, + { + doc: "IPv6 localhost, colon sep", + input: `ipv6local:::1`, + }, + { + doc: "IPv6 localhost, eq sep", + input: `ipv6local=::1`, + expectedOut: `ipv6local:::1`, + }, + { + doc: "IPv6 localhost, eq sep, brackets", + input: `ipv6local=[::1]`, + expectedOut: `ipv6local:::1`, + }, + { + doc: "IPv6 localhost, non-canonical, colon sep", + input: `ipv6local:0:0:0:0:0:0:0:1`, + }, + { + doc: "IPv6 localhost, non-canonical, eq sep", + input: `ipv6local=0:0:0:0:0:0:0:1`, + expectedOut: `ipv6local:0:0:0:0:0:0:0:1`, + }, + { + doc: "IPv6 localhost, non-canonical, eq sep, brackets", + input: `ipv6local=[0:0:0:0:0:0:0:1]`, + expectedOut: `ipv6local:0:0:0:0:0:0:0:1`, + }, + { + doc: "host-gateway special case, colon sep", + input: `host.docker.internal:host-gateway`, + }, + { + doc: "host-gateway special case, eq sep", + input: `host.docker.internal=host-gateway`, + expectedOut: `host.docker.internal:host-gateway`, + }, + { + doc: "Bad address, colon sep", + input: `myhost:192.notanipaddress.1`, + expectedErr: `invalid IP address in add-host: "192.notanipaddress.1"`, + }, + { + doc: "Bad address, eq sep", + input: `myhost=192.notanipaddress.1`, + expectedErr: `invalid IP address in add-host: "192.notanipaddress.1"`, + }, + { + doc: "No sep", + input: `thathost-nosemicolon10.0.0.1`, + expectedErr: `bad format for add-host: "thathost-nosemicolon10.0.0.1"`, + }, + { + doc: "Bad IPv6", + input: `anipv6host:::::1`, + expectedErr: `invalid IP address in add-host: "::::1"`, + }, + { + doc: "Bad IPv6, trailing colons", + input: `ipv6local:::0::`, + expectedErr: `invalid IP address in add-host: "::0::"`, + }, + { + doc: "Bad IPv6, missing close bracket", + input: `ipv6addr=[::1`, + expectedErr: `invalid IP address in add-host: "[::1"`, + }, + { + doc: "Bad IPv6, missing open bracket", + input: `ipv6addr=::1]`, + expectedErr: `invalid IP address in add-host: "::1]"`, + }, + { + doc: "Missing address, colon sep", + input: `missing.address.colon:`, + expectedErr: `invalid IP address in add-host: ""`, + }, + { + doc: "Missing address, eq sep", + input: `missing.address.eq=`, + expectedErr: `invalid IP address in add-host: ""`, + }, + { + doc: "IPv6 localhost, bad name", + input: `:=::1`, + expectedErr: `bad format for add-host: ":=::1"`, + }, + { + doc: "No input", + input: ``, + expectedErr: `bad format for add-host: ""`, + }, } - for _, extrahost := range valid { - if _, err := ValidateExtraHost(extrahost); err != nil { - t.Fatalf("ValidateExtraHost(`"+extrahost+"`) should succeed: error %v", err) + for _, tc := range tests { + tc := tc + if tc.expectedOut == "" { + tc.expectedOut = tc.input } - } - - for extraHost, expectedError := range invalid { - if _, err := ValidateExtraHost(extraHost); err == nil { - t.Fatalf("ValidateExtraHost(`%q`) should have failed validation", extraHost) - } else { - if !strings.Contains(err.Error(), expectedError) { - t.Fatalf("ValidateExtraHost(`%q`) error should contain %q", extraHost, expectedError) + t.Run(tc.input, func(t *testing.T) { + actualOut, actualErr := ValidateExtraHost(tc.input) + if tc.expectedErr == "" { + assert.Check(t, is.Equal(tc.expectedOut, actualOut)) + assert.NilError(t, actualErr) + } else { + assert.Check(t, actualOut == "") + assert.Check(t, is.Error(actualErr, tc.expectedErr)) } - } + }) } }