Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions docs/reference/commandline/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,20 +454,28 @@ 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)

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
`docker`:
You can add other hosts into a build container's `/etc/hosts` file by using one
or more `--add-host` flags. This example adds static addresses for hosts named
`my-hostname` and `my_hostname_v6`:

```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
the special `host-gateway` value for `--add-host`. In the following example,
build containers resolve `host.docker.internal` to the host's gateway IP.

```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)
Expand Down
30 changes: 21 additions & 9 deletions docs/reference/commandline/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
more `--add-host` flags. This example adds a static address for a host named
`docker`:
`my-hostname`:

```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 docker (93.184.216.34): 56 data bytes
64 bytes from 93.184.216.34: 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 93.184.216.34: seq=2 ttl=37 time=92.252 ms
/ # ping my-hostname
PING my-hostname (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=37 time=93.052 ms
64 bytes from 8.8.8.8: seq=1 ttl=37 time=92.467 ms
64 bytes from 8.8.8.8: seq=2 ttl=37 time=92.252 ms
^C
--- docker ping statistics ---
--- my-hostname ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
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 internal IP address of the host. This is useful when you want containers to
connect to services running on the host machine.
Expand All @@ -779,11 +785,17 @@ $ echo "hello from host!" > ./hello
$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
$ docker run \
--add-host host.docker.internal:host-gateway \
--add-host host.docker.internal=host-gateway \
curlimages/curl -s host.docker.internal:8000/hello
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)

Since setting `ulimit` settings in a container requires extra privileges not
Expand Down
6 changes: 3 additions & 3 deletions man/docker-build.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
6 changes: 3 additions & 3 deletions man/docker-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
46 changes: 39 additions & 7 deletions opts/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
// 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) {
// 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 {
Comment on lines +201 to 205
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but still considering using the normalised variant; that would potentially avoid duplicates;

docker run --add-host=my-hostname:0:0:0:0:0:0:0:1 --add-host=my-hostname:::1 --rm alpine sh -c 'cat /etc/hosts | grep my-hostname'
0:0:0:0:0:0:0:1	my-hostname
::1	my-hostname

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I hadn't thought about duplicates - was more worried about second-guessing the user. I think only the last entry will be used if there are duplicates. I suppose we could use the canonical form to check for duplicates internally, and preserve the user's address in the file.

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
}
172 changes: 147 additions & 25 deletions opts/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -146,32 +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`,
}

for _, extrahost := range valid {
if _, err := ValidateExtraHost(extrahost); err != nil {
t.Fatalf("ValidateExtraHost(`"+extrahost+"`) should succeed: error %v", err)
}
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.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: ""`,
},
}

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)
for _, tc := range tests {
tc := tc
if tc.expectedOut == "" {
tc.expectedOut = tc.input
}
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))
}
})
}
}
Loading