Skip to content

Commit a682b8e

Browse files
committed
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 <[email protected]>
1 parent af23916 commit a682b8e

6 files changed

Lines changed: 226 additions & 52 deletions

File tree

docs/reference/commandline/build.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,20 +454,28 @@ Specifying the `--isolation` flag without a value is the same as setting `--isol
454454

455455
### <a name="add-host"></a> Add entries to container hosts file (--add-host)
456456

457-
You can add other hosts into a container's `/etc/hosts` file by using one or
458-
more `--add-host` flags. This example adds a static address for a host named
459-
`docker`:
457+
You can add other hosts into a build container's `/etc/hosts` file by using one
458+
or more `--add-host` flags. This example adds static addresses for hosts named
459+
`my-hostname` and `my_hostname_v6`:
460460

461461
```console
462-
$ docker build --add-host docker:10.180.0.1 .
462+
$ docker build --add-host my_hostname=8.8.8.8 --add-host my_hostname_v6=2001:4860:4860::8888 .
463463
```
464464

465465
If you need your build to connect to services running on the host, you can use
466466
the special `host-gateway` value for `--add-host`. In the following example,
467467
build containers resolve `host.docker.internal` to the host's gateway IP.
468468

469469
```console
470-
$ docker build --add-host host.docker.internal:host-gateway .
470+
$ docker build --add-host host.docker.internal=host-gateway .
471+
```
472+
473+
You can wrap an IPv6 address in square brackets.
474+
`=` and `:` are both valid separators.
475+
Both formats in the following example are valid:
476+
477+
```console
478+
$ docker build --add-host my-hostname:10.180.0.1 --add-host my-hostname_v6=[2001:4860:4860::8888] .
471479
```
472480

473481
### <a name="target"></a> Specifying target build stage (--target)

docs/reference/commandline/run.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -746,22 +746,28 @@ section of the Docker run reference page.
746746

747747
You can add other hosts into a container's `/etc/hosts` file by using one or
748748
more `--add-host` flags. This example adds a static address for a host named
749-
`docker`:
749+
`my-hostname`:
750750

751751
```console
752-
$ docker run --add-host=docker:93.184.216.34 --rm -it alpine
752+
$ docker run --add-host=my-hostname=8.8.8.8 --rm -it alpine
753753

754-
/ # ping docker
755-
PING docker (93.184.216.34): 56 data bytes
756-
64 bytes from 93.184.216.34: seq=0 ttl=37 time=93.052 ms
757-
64 bytes from 93.184.216.34: seq=1 ttl=37 time=92.467 ms
758-
64 bytes from 93.184.216.34: seq=2 ttl=37 time=92.252 ms
754+
/ # ping my-hostname
755+
PING my-hostname (8.8.8.8): 56 data bytes
756+
64 bytes from 8.8.8.8: seq=0 ttl=37 time=93.052 ms
757+
64 bytes from 8.8.8.8: seq=1 ttl=37 time=92.467 ms
758+
64 bytes from 8.8.8.8: seq=2 ttl=37 time=92.252 ms
759759
^C
760-
--- docker ping statistics ---
760+
--- my-hostname ping statistics ---
761761
4 packets transmitted, 4 packets received, 0% packet loss
762762
round-trip min/avg/max = 92.209/92.495/93.052 ms
763763
```
764764

765+
You can wrap an IPv6 address in square brackets:
766+
767+
```console
768+
$ docker run --add-host my-hostname=[2001:db8::33] --rm -it alpine
769+
```
770+
765771
The `--add-host` flag supports a special `host-gateway` value that resolves to
766772
the internal IP address of the host. This is useful when you want containers to
767773
connect to services running on the host machine.
@@ -779,11 +785,17 @@ $ echo "hello from host!" > ./hello
779785
$ python3 -m http.server 8000
780786
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
781787
$ docker run \
782-
--add-host host.docker.internal:host-gateway \
788+
--add-host host.docker.internal=host-gateway \
783789
curlimages/curl -s host.docker.internal:8000/hello
784790
hello from host!
785791
```
786792

793+
The `--add-host` flag also accepts a `:` separator, for example:
794+
795+
```console
796+
$ docker run --add-host=my-hostname:8.8.8.8 --rm -it alpine
797+
```
798+
787799
### <a name="ulimit"></a> Set ulimits in container (--ulimit)
788800

789801
Since setting `ulimit` settings in a container requires extra privileges not

man/docker-build.1.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
7878
layers in tact, and one for the squashed version.
7979

8080
**--add-host** []
81-
Add a custom host-to-IP mapping (host:ip)
81+
Add a custom host-to-IP mapping (host=ip, or host:ip)
8282

83-
Add a line to /etc/hosts. The format is hostname:ip. The **--add-host**
84-
option can be set multiple times.
83+
Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
84+
The **--add-host** option can be set multiple times.
8585

8686
**--build-arg** *variable*
8787
name and value of a **buildarg**.

man/docker-run.1.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ executables expect) and pass along signals. The **-a** option can be set for
121121
each of stdin, stdout, and stderr.
122122

123123
**--add-host**=[]
124-
Add a custom host-to-IP mapping (host:ip)
124+
Add a custom host-to-IP mapping (host=ip, or host:ip)
125125

126-
Add a line to /etc/hosts. The format is hostname:ip. The **--add-host**
127-
option can be set multiple times.
126+
Add a line to /etc/hosts. The format is hostname=ip, or hostname:ip.
127+
The **--add-host** option can be set multiple times.
128128

129129
**--annotation**=[]
130130
Add an annotation to the container (passed through to the OCI runtime).

opts/hosts.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,53 @@ func ParseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
161161
return fmt.Sprintf("tcp://%s%s", net.JoinHostPort(host, port), u.Path), nil
162162
}
163163

164-
// ValidateExtraHost validates that the specified string is a valid extrahost and returns it.
165-
// ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6).
164+
// ValidateExtraHost validates that the specified string is a valid extrahost and
165+
// returns it. ExtraHost is in the form of name:ip or name=ip, where the ip has
166+
// to be a valid ip (IPv4 or IPv6). The address may be enclosed in square
167+
// brackets.
166168
//
167-
// TODO(thaJeztah): remove client-side validation, and delegate to the API server.
169+
// For example:
170+
//
171+
// my-hostname:127.0.0.1
172+
// my-hostname:::1
173+
// my-hostname=::1
174+
// my-hostname:[::1]
175+
//
176+
// For compatibility with the API server, this function normalises the given
177+
// argument to use the ':' separator and strip square brackets enclosing the
178+
// address.
168179
func ValidateExtraHost(val string) (string, error) {
169-
// allow for IPv6 addresses in extra hosts by only splitting on first ":"
170-
k, v, ok := strings.Cut(val, ":")
171-
if !ok || k == "" {
180+
k, v, ok := strings.Cut(val, "=")
181+
if !ok {
182+
// allow for IPv6 addresses in extra hosts by only splitting on first ":"
183+
k, v, ok = strings.Cut(val, ":")
184+
}
185+
// Check that a hostname was given, and that it doesn't contain a ":". (Colon
186+
// isn't allowed in a hostname, along with many other characters. It's
187+
// special-cased here because the API server doesn't know about '=' separators in
188+
// '--add-host'. So, it'll split at the first colon and generate a strange error
189+
// message.)
190+
if !ok || k == "" || strings.Contains(k, ":") {
172191
return "", fmt.Errorf("bad format for add-host: %q", val)
173192
}
174193
// Skip IPaddr validation for "host-gateway" string
175194
if v != hostGatewayName {
195+
// If the address is enclosed in square brackets, extract it (for IPv6, but
196+
// permit it for IPv4 as well; we don't know the address family here, but it's
197+
// unambiguous).
198+
if len(v) > 2 && v[0] == '[' && v[len(v)-1] == ']' {
199+
v = v[1 : len(v)-1]
200+
}
201+
// ValidateIPAddress returns the address in canonical form (for example,
202+
// 0:0:0:0:0:0:0:1 -> ::1). But, stick with the original form, to avoid
203+
// surprising a user who's expecting to see the address they supplied in the
204+
// output of 'docker inspect' or '/etc/hosts'.
176205
if _, err := ValidateIPAddress(v); err != nil {
177206
return "", fmt.Errorf("invalid IP address in add-host: %q", v)
178207
}
179208
}
180-
return val, nil
209+
// This result is passed directly to the API, the daemon doesn't accept the '='
210+
// separator or an address enclosed in brackets. So, construct something it can
211+
// understand.
212+
return k + ":" + v, nil
181213
}

opts/hosts_test.go

Lines changed: 147 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package opts
22

33
import (
44
"fmt"
5-
"strings"
65
"testing"
6+
7+
"gotest.tools/v3/assert"
8+
is "gotest.tools/v3/assert/cmp"
79
)
810

911
func TestParseHost(t *testing.T) {
@@ -146,32 +148,152 @@ func TestParseInvalidUnixAddrInvalid(t *testing.T) {
146148
}
147149

148150
func TestValidateExtraHosts(t *testing.T) {
149-
valid := []string{
150-
`myhost:192.168.0.1`,
151-
`thathost:10.0.2.1`,
152-
`anipv6host:2003:ab34:e::1`,
153-
`ipv6local:::1`,
154-
`host.docker.internal:host-gateway`,
155-
}
156-
157-
invalid := map[string]string{
158-
`myhost:192.notanipaddress.1`: `invalid IP`,
159-
`thathost-nosemicolon10.0.0.1`: `bad format`,
160-
`anipv6host:::::1`: `invalid IP`,
161-
`ipv6local:::0::`: `invalid IP`,
162-
}
163-
164-
for _, extrahost := range valid {
165-
if _, err := ValidateExtraHost(extrahost); err != nil {
166-
t.Fatalf("ValidateExtraHost(`"+extrahost+"`) should succeed: error %v", err)
167-
}
151+
tests := []struct {
152+
doc string
153+
input string
154+
expectedOut string // Expect output==input if not set.
155+
expectedErr string // Expect success if not set.
156+
}{
157+
{
158+
doc: "IPv4, colon sep",
159+
input: `myhost:192.168.0.1`,
160+
},
161+
{
162+
doc: "IPv4, eq sep",
163+
input: `myhost=192.168.0.1`,
164+
expectedOut: `myhost:192.168.0.1`,
165+
},
166+
{
167+
doc: "Weird but permitted, IPv4 with brackets",
168+
input: `myhost=[192.168.0.1]`,
169+
expectedOut: `myhost:192.168.0.1`,
170+
},
171+
{
172+
doc: "Host and domain",
173+
input: `host.and.domain.invalid:10.0.2.1`,
174+
},
175+
{
176+
doc: "IPv6, colon sep",
177+
input: `anipv6host:2003:ab34:e::1`,
178+
},
179+
{
180+
doc: "IPv6, colon sep, brackets",
181+
input: `anipv6host:[2003:ab34:e::1]`,
182+
expectedOut: `anipv6host:2003:ab34:e::1`,
183+
},
184+
{
185+
doc: "IPv6, eq sep, brackets",
186+
input: `anipv6host=[2003:ab34:e::1]`,
187+
expectedOut: `anipv6host:2003:ab34:e::1`,
188+
},
189+
{
190+
doc: "IPv6 localhost, colon sep",
191+
input: `ipv6local:::1`,
192+
},
193+
{
194+
doc: "IPv6 localhost, eq sep",
195+
input: `ipv6local=::1`,
196+
expectedOut: `ipv6local:::1`,
197+
},
198+
{
199+
doc: "IPv6 localhost, eq sep, brackets",
200+
input: `ipv6local=[::1]`,
201+
expectedOut: `ipv6local:::1`,
202+
},
203+
{
204+
doc: "IPv6 localhost, non-canonical, colon sep",
205+
input: `ipv6local:0:0:0:0:0:0:0:1`,
206+
},
207+
{
208+
doc: "IPv6 localhost, non-canonical, eq sep",
209+
input: `ipv6local=0:0:0:0:0:0:0:1`,
210+
expectedOut: `ipv6local:0:0:0:0:0:0:0:1`,
211+
},
212+
{
213+
doc: "IPv6 localhost, non-canonical, eq sep, brackets",
214+
input: `ipv6local=[0:0:0:0:0:0:0:1]`,
215+
expectedOut: `ipv6local:0:0:0:0:0:0:0:1`,
216+
},
217+
{
218+
doc: "host-gateway special case, colon sep",
219+
input: `host.docker.internal:host-gateway`,
220+
},
221+
{
222+
doc: "host-gateway special case, eq sep",
223+
input: `host.docker.internal=host-gateway`,
224+
expectedOut: `host.docker.internal:host-gateway`,
225+
},
226+
{
227+
doc: "Bad address, colon sep",
228+
input: `myhost:192.notanipaddress.1`,
229+
expectedErr: `invalid IP address in add-host: "192.notanipaddress.1"`,
230+
},
231+
{
232+
doc: "Bad address, eq sep",
233+
input: `myhost=192.notanipaddress.1`,
234+
expectedErr: `invalid IP address in add-host: "192.notanipaddress.1"`,
235+
},
236+
{
237+
doc: "No sep",
238+
input: `thathost-nosemicolon10.0.0.1`,
239+
expectedErr: `bad format for add-host: "thathost-nosemicolon10.0.0.1"`,
240+
},
241+
{
242+
doc: "Bad IPv6",
243+
input: `anipv6host:::::1`,
244+
expectedErr: `invalid IP address in add-host: "::::1"`,
245+
},
246+
{
247+
doc: "Bad IPv6, trailing colons",
248+
input: `ipv6local:::0::`,
249+
expectedErr: `invalid IP address in add-host: "::0::"`,
250+
},
251+
{
252+
doc: "Bad IPv6, missing close bracket",
253+
input: `ipv6addr=[::1`,
254+
expectedErr: `invalid IP address in add-host: "[::1"`,
255+
},
256+
{
257+
doc: "Bad IPv6, missing open bracket",
258+
input: `ipv6addr=::1]`,
259+
expectedErr: `invalid IP address in add-host: "::1]"`,
260+
},
261+
{
262+
doc: "Missing address, colon sep",
263+
input: `myhost.invalid:`,
264+
expectedErr: `invalid IP address in add-host: ""`,
265+
},
266+
{
267+
doc: "Missing address, eq sep",
268+
input: `myhost.invalid=`,
269+
expectedErr: `invalid IP address in add-host: ""`,
270+
},
271+
{
272+
doc: "IPv6 localhost, bad name",
273+
input: `:=::1`,
274+
expectedErr: `bad format for add-host: ":=::1"`,
275+
},
276+
{
277+
doc: "No input",
278+
input: ``,
279+
expectedErr: `bad format for add-host: ""`,
280+
},
168281
}
169282

170-
for extraHost, expectedError := range invalid {
171-
if _, err := ValidateExtraHost(extraHost); err == nil {
172-
t.Fatalf("ValidateExtraHost(`%q`) should have failed validation", extraHost)
173-
} else if !strings.Contains(err.Error(), expectedError) {
174-
t.Fatalf("ValidateExtraHost(`%q`) error should contain %q", extraHost, expectedError)
283+
for _, tc := range tests {
284+
tc := tc
285+
if tc.expectedOut == "" {
286+
tc.expectedOut = tc.input
175287
}
288+
t.Run(tc.input, func(t *testing.T) {
289+
actualOut, actualErr := ValidateExtraHost(tc.input)
290+
if tc.expectedErr == "" {
291+
assert.Check(t, is.Equal(tc.expectedOut, actualOut))
292+
assert.NilError(t, actualErr)
293+
} else {
294+
assert.Check(t, actualOut == "")
295+
assert.Check(t, is.Error(actualErr, tc.expectedErr))
296+
}
297+
})
176298
}
177299
}

0 commit comments

Comments
 (0)