Permit '=' separator and '[ipv6]' in --add-host

Fixes #4648

Make it easier to specify IPv6 addresses in the '--add-host' option by
permitting 'host=ip' in addition to 'host:ip', and allowing square
brackets around the address.

For example:

    --add-host=my-hostname:127.0.0.1
    --add-host=my-hostname:::1
    --add-host=my-hostname=::1
    --add-host=my-hostname:[::1]

To avoid compatibility problems, the CLI will replace an '=' separator
with ':', and strip brackets, before sending the request to the API.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
robmry 2023-11-15 15:11:46 +00:00 committed by Rob Murray
parent af23916995
commit a682b8e655
6 changed files with 226 additions and 52 deletions

View File

@ -454,12 +454,12 @@ Specifying the `--isolation` flag without a value is the same as setting `--isol
### <a name="add-host"></a> Add entries to container hosts file (--add-host) ### <a name="add-host"></a> Add entries to container hosts file (--add-host)
You can add other hosts into a container's `/etc/hosts` file by using one or You can add other hosts into a build container's `/etc/hosts` file by using one
more `--add-host` flags. This example adds a static address for a host named or more `--add-host` flags. This example adds static addresses for hosts named
`docker`: `my-hostname` and `my_hostname_v6`:
```console ```console
$ docker build --add-host docker:10.180.0.1 . $ docker build --add-host my_hostname=8.8.8.8 --add-host my_hostname_v6=2001:4860:4860::8888 .
``` ```
If you need your build to connect to services running on the host, you can use If you need your build to connect to services running on the host, you can use
@ -467,7 +467,15 @@ the special `host-gateway` value for `--add-host`. In the following example,
build containers resolve `host.docker.internal` to the host's gateway IP. build containers resolve `host.docker.internal` to the host's gateway IP.
```console ```console
$ docker build --add-host host.docker.internal:host-gateway . $ docker build --add-host host.docker.internal=host-gateway .
```
You can wrap an IPv6 address in square brackets.
`=` and `:` are both valid separators.
Both formats in the following example are valid:
```console
$ docker build --add-host my-hostname:10.180.0.1 --add-host my-hostname_v6=[2001:4860:4860::8888] .
``` ```
### <a name="target"></a> Specifying target build stage (--target) ### <a name="target"></a> Specifying target build stage (--target)

View File

@ -746,22 +746,28 @@ section of the Docker run reference page.
You can add other hosts into a container's `/etc/hosts` file by using one or You can add other hosts into a container's `/etc/hosts` file by using one or
more `--add-host` flags. This example adds a static address for a host named more `--add-host` flags. This example adds a static address for a host named
`docker`: `my-hostname`:
```console ```console
$ docker run --add-host=docker:93.184.216.34 --rm -it alpine $ docker run --add-host=my-hostname=8.8.8.8 --rm -it alpine
/ # ping docker / # ping my-hostname
PING docker (93.184.216.34): 56 data bytes PING my-hostname (8.8.8.8): 56 data bytes
64 bytes from 93.184.216.34: seq=0 ttl=37 time=93.052 ms 64 bytes from 8.8.8.8: seq=0 ttl=37 time=93.052 ms
64 bytes from 93.184.216.34: seq=1 ttl=37 time=92.467 ms 64 bytes from 8.8.8.8: seq=1 ttl=37 time=92.467 ms
64 bytes from 93.184.216.34: seq=2 ttl=37 time=92.252 ms 64 bytes from 8.8.8.8: seq=2 ttl=37 time=92.252 ms
^C ^C
--- docker ping statistics --- --- my-hostname ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss 4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 92.209/92.495/93.052 ms round-trip min/avg/max = 92.209/92.495/93.052 ms
``` ```
You can wrap an IPv6 address in square brackets:
```console
$ docker run --add-host my-hostname=[2001:db8::33] --rm -it alpine
```
The `--add-host` flag supports a special `host-gateway` value that resolves to 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 the internal IP address of the host. This is useful when you want containers to
connect to services running on the host machine. connect to services running on the host machine.
@ -779,11 +785,17 @@ $ echo "hello from host!" > ./hello
$ python3 -m http.server 8000 $ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
$ docker run \ $ docker run \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal=host-gateway \
curlimages/curl -s host.docker.internal:8000/hello curlimages/curl -s host.docker.internal:8000/hello
hello from host! hello from host!
``` ```
The `--add-host` flag also accepts a `:` separator, for example:
```console
$ docker run --add-host=my-hostname:8.8.8.8 --rm -it alpine
```
### <a name="ulimit"></a> Set ulimits in container (--ulimit) ### <a name="ulimit"></a> Set ulimits in container (--ulimit)
Since setting `ulimit` settings in a container requires extra privileges not Since setting `ulimit` settings in a container requires extra privileges not

View File

@ -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. layers in tact, and one for the squashed version.
**--add-host** [] **--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** Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
option can be set multiple times. The **--add-host** option can be set multiple times.
**--build-arg** *variable* **--build-arg** *variable*
name and value of a **buildarg**. name and value of a **buildarg**.

View File

@ -121,10 +121,10 @@ executables expect) and pass along signals. The **-a** option can be set for
each of stdin, stdout, and stderr. each of stdin, stdout, and stderr.
**--add-host**=[] **--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** Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
option can be set multiple times. The **--add-host** option can be set multiple times.
**--annotation**=[] **--annotation**=[]
Add an annotation to the container (passed through to the OCI runtime). Add an annotation to the container (passed through to the OCI runtime).

View File

@ -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 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. // ValidateExtraHost validates that the specified string is a valid extrahost and
// ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6). // 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:
//
// my-hostname:127.0.0.1
// my-hostname:::1
// my-hostname=::1
// my-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) { func ValidateExtraHost(val string) (string, error) {
k, v, ok := strings.Cut(val, "=")
if !ok {
// allow for IPv6 addresses in extra hosts by only splitting on first ":" // allow for IPv6 addresses in extra hosts by only splitting on first ":"
k, v, ok := strings.Cut(val, ":") k, v, ok = strings.Cut(val, ":")
if !ok || k == "" { }
// 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) return "", fmt.Errorf("bad format for add-host: %q", val)
} }
// Skip IPaddr validation for "host-gateway" string // Skip IPaddr validation for "host-gateway" string
if v != hostGatewayName { 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 { if _, err := ValidateIPAddress(v); err != nil {
return "", fmt.Errorf("invalid IP address in add-host: %q", v) 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
} }

View File

@ -2,8 +2,10 @@ package opts
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
) )
func TestParseHost(t *testing.T) { func TestParseHost(t *testing.T) {
@ -146,32 +148,152 @@ func TestParseInvalidUnixAddrInvalid(t *testing.T) {
} }
func TestValidateExtraHosts(t *testing.T) { func TestValidateExtraHosts(t *testing.T) {
valid := []string{ tests := []struct {
`myhost:192.168.0.1`, doc string
`thathost:10.0.2.1`, input string
`anipv6host:2003:ab34:e::1`, expectedOut string // Expect output==input if not set.
`ipv6local:::1`, expectedErr string // Expect success if not set.
`host.docker.internal:host-gateway`, }{
{
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.invalid: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: `myhost.invalid:`,
expectedErr: `invalid IP address in add-host: ""`,
},
{
doc: "Missing address, eq sep",
input: `myhost.invalid=`,
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: ""`,
},
} }
invalid := map[string]string{ for _, tc := range tests {
`myhost:192.notanipaddress.1`: `invalid IP`, tc := tc
`thathost-nosemicolon10.0.0.1`: `bad format`, if tc.expectedOut == "" {
`anipv6host:::::1`: `invalid IP`, tc.expectedOut = tc.input
`ipv6local:::0::`: `invalid IP`,
} }
t.Run(tc.input, func(t *testing.T) {
for _, extrahost := range valid { actualOut, actualErr := ValidateExtraHost(tc.input)
if _, err := ValidateExtraHost(extrahost); err != nil { if tc.expectedErr == "" {
t.Fatalf("ValidateExtraHost(`"+extrahost+"`) should succeed: error %v", err) assert.Check(t, is.Equal(tc.expectedOut, actualOut))
} assert.NilError(t, actualErr)
} } else {
assert.Check(t, actualOut == "")
for extraHost, expectedError := range invalid { assert.Check(t, is.Error(actualErr, tc.expectedErr))
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)
} }
})
} }
} }