Browse code

Split advertised address from listen address

There are currently problems with "swarm init" and "swarm join" when an
explicit --listen-addr flag is not provided. swarmkit defaults to
finding the IP address associated with the default route, and in cloud
setups this is often the wrong choice.

Introduce a notion of "advertised address", with the client flag
--advertise-addr, and the daemon flag --swarm-default-advertise-addr to
provide a default. The default listening address is now 0.0.0.0, but a
valid advertised address must be detected or specified.

If no explicit advertised address is specified, error out if there is
more than one usable candidate IP address on the system. This requires a
user to explicitly choose instead of letting swarmkit make the wrong
choice. For the purposes of this autodetection, we ignore certain
interfaces that are unlikely to be relevant (currently docker*).

The user is also required to choose a listen address on swarm init if
they specify an explicit advertise address that is a hostname or an IP
address that's not local to the system. This is a requirement for
overlay networking.

Also support specifying interface names to --listen-addr,
--advertise-addr, and the daemon flag --swarm-default-advertise-addr.
This will fail if the interface has multiple IP addresses (unless it has
a single IPv4 address and a single IPv6 address - then we resolve the
tie in favor of IPv4).

This change also exposes the node's externally-reachable address in
docker info, as requested by #24017.

Make corresponding API and CLI docs changes.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2016/07/01 10:07:35
Showing 20 changed files
... ...
@@ -1,7 +1,9 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
6
+	"strings"
5 7
 
6 8
 	"golang.org/x/net/context"
7 9
 
... ...
@@ -21,7 +23,9 @@ const (
21 21
 
22 22
 type initOptions struct {
23 23
 	swarmOptions
24
-	listenAddr      NodeAddrOption
24
+	listenAddr NodeAddrOption
25
+	// Not a NodeAddrOption because it has no default port.
26
+	advertiseAddr   string
25 27
 	forceNewCluster bool
26 28
 }
27 29
 
... ...
@@ -40,7 +44,8 @@ func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
40 40
 	}
41 41
 
42 42
 	flags := cmd.Flags()
43
-	flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
43
+	flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|hostname|interface>[:port])")
44
+	flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|hostname|interface>[:port])")
44 45
 	flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.")
45 46
 	addSwarmFlags(flags, &opts.swarmOptions)
46 47
 	return cmd
... ...
@@ -52,12 +57,16 @@ func runInit(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts initOptions
52 52
 
53 53
 	req := swarm.InitRequest{
54 54
 		ListenAddr:      opts.listenAddr.String(),
55
+		AdvertiseAddr:   opts.advertiseAddr,
55 56
 		ForceNewCluster: opts.forceNewCluster,
56 57
 		Spec:            opts.swarmOptions.ToSpec(),
57 58
 	}
58 59
 
59 60
 	nodeID, err := client.SwarmInit(ctx, req)
60 61
 	if err != nil {
62
+		if strings.Contains(err.Error(), "could not choose an IP address to advertise") || strings.Contains(err.Error(), "could not find the system's IP address") {
63
+			return errors.New(err.Error() + " - specify one with --advertise-addr")
64
+		}
61 65
 		return err
62 66
 	}
63 67
 
... ...
@@ -14,7 +14,9 @@ import (
14 14
 type joinOptions struct {
15 15
 	remote     string
16 16
 	listenAddr NodeAddrOption
17
-	token      string
17
+	// Not a NodeAddrOption because it has no default port.
18
+	advertiseAddr string
19
+	token         string
18 20
 }
19 21
 
20 22
 func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
... ...
@@ -33,7 +35,8 @@ func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
33 33
 	}
34 34
 
35 35
 	flags := cmd.Flags()
36
-	flags.Var(&opts.listenAddr, flagListenAddr, "Listen address")
36
+	flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|hostname|interface>[:port])")
37
+	flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|hostname|interface>[:port])")
37 38
 	flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm")
38 39
 	return cmd
39 40
 }
... ...
@@ -43,9 +46,10 @@ func runJoin(dockerCli *client.DockerCli, opts joinOptions) error {
43 43
 	ctx := context.Background()
44 44
 
45 45
 	req := swarm.JoinRequest{
46
-		JoinToken:   opts.token,
47
-		ListenAddr:  opts.listenAddr.String(),
48
-		RemoteAddrs: []string{opts.remote},
46
+		JoinToken:     opts.token,
47
+		ListenAddr:    opts.listenAddr.String(),
48
+		AdvertiseAddr: opts.advertiseAddr,
49
+		RemoteAddrs:   []string{opts.remote},
49 50
 	}
50 51
 	err := client.SwarmJoin(ctx, req)
51 52
 	if err != nil {
... ...
@@ -18,6 +18,7 @@ const (
18 18
 	flagCertExpiry          = "cert-expiry"
19 19
 	flagDispatcherHeartbeat = "dispatcher-heartbeat"
20 20
 	flagListenAddr          = "listen-addr"
21
+	flagAdvertiseAddr       = "advertise-addr"
21 22
 	flagToken               = "token"
22 23
 	flagTaskHistoryLimit    = "task-history-limit"
23 24
 	flagExternalCA          = "external-ca"
... ...
@@ -86,6 +86,7 @@ func runInfo(dockerCli *client.DockerCli) error {
86 86
 			fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers)
87 87
 			fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes)
88 88
 		}
89
+		fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr)
89 90
 	}
90 91
 
91 92
 	if len(info.Runtimes) > 0 {
... ...
@@ -274,9 +274,11 @@ func (cli *DaemonCli) start() (err error) {
274 274
 	name, _ := os.Hostname()
275 275
 
276 276
 	c, err := cluster.New(cluster.Config{
277
-		Root:    cli.Config.Root,
278
-		Name:    name,
279
-		Backend: d,
277
+		Root:                   cli.Config.Root,
278
+		Name:                   name,
279
+		Backend:                d,
280
+		NetworkSubnetsProvider: d,
281
+		DefaultAdvertiseAddr:   cli.Config.SwarmDefaultAdvertiseAddr,
280 282
 	})
281 283
 	if err != nil {
282 284
 		logrus.Fatalf("Error creating cluster component: %v", err)
... ...
@@ -1839,11 +1839,17 @@ _docker_swarm_init() {
1839 1839
 			fi
1840 1840
 			return
1841 1841
 			;;
1842
+		--advertise-addr)
1843
+			if [[ $cur == *: ]] ; then
1844
+				COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) )
1845
+			fi
1846
+			return
1847
+			;;
1842 1848
 	esac
1843 1849
 
1844 1850
 	case "$cur" in
1845 1851
 		-*)
1846
-			COMPREPLY=( $( compgen -W "--force-new-cluster --help --listen-addr" -- "$cur" ) )
1852
+			COMPREPLY=( $( compgen -W "--advertise-addr --force-new-cluster --help --listen-addr" -- "$cur" ) )
1847 1853
 			;;
1848 1854
 	esac
1849 1855
 }
... ...
@@ -1873,11 +1879,17 @@ _docker_swarm_join() {
1873 1873
 			fi
1874 1874
 			return
1875 1875
 			;;
1876
+		--advertise-addr)
1877
+			if [[ $cur == *: ]] ; then
1878
+				COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) )
1879
+			fi
1880
+			return
1881
+			;;
1876 1882
 	esac
1877 1883
 
1878 1884
 	case "$cur" in
1879 1885
 		-*)
1880
-			COMPREPLY=( $( compgen -W "--help --listen-addr --token" -- "$cur" ) )
1886
+			COMPREPLY=( $( compgen -W "--adveritse-addr --help --listen-addr --token" -- "$cur" ) )
1881 1887
 			;;
1882 1888
 		*:)
1883 1889
 			COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) )
... ...
@@ -1203,6 +1203,7 @@ __docker_swarm_subcommand() {
1203 1203
         (init)
1204 1204
             _arguments $(__docker_arguments) \
1205 1205
                 $opts_help \
1206
+                "($help)--advertise-addr[Advertised address]:ip\:port: " \
1206 1207
                 "($help)*--external-ca=[Specifications of one or more certificate signing endpoints]:endpoint: " \
1207 1208
                 "($help)--force-new-cluster[Force create a new cluster from current state]" \
1208 1209
                 "($help)--listen-addr=[Listen address]:ip\:port: " && ret=0
... ...
@@ -1215,6 +1216,7 @@ __docker_swarm_subcommand() {
1215 1215
         (join)
1216 1216
             _arguments $(__docker_arguments) \
1217 1217
                 $opts_help \
1218
+                "($help)--advertise-addr[Advertised address]:ip\:port: " \
1218 1219
                 "($help)--listen-addr=[Listen address]:ip\:port: " \
1219 1220
                 "($help)--token=[Token for entry into the swarm]:secret: " \
1220 1221
                 "($help -):host\:port: " && ret=0
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"encoding/json"
5 5
 	"fmt"
6 6
 	"io/ioutil"
7
+	"net"
7 8
 	"os"
8 9
 	"path/filepath"
9 10
 	"strings"
... ...
@@ -73,14 +74,35 @@ var defaultSpec = types.Spec{
73 73
 }
74 74
 
75 75
 type state struct {
76
+	// LocalAddr is this machine's local IP or hostname, if specified.
77
+	LocalAddr string
78
+	// RemoteAddr is the address that was given to "swarm join. It is used
79
+	// to find LocalAddr if necessary.
80
+	RemoteAddr string
81
+	// ListenAddr is the address we bind to, including a port.
76 82
 	ListenAddr string
83
+	// AdvertiseAddr is the address other nodes should connect to,
84
+	// including a port.
85
+	AdvertiseAddr string
86
+}
87
+
88
+// NetworkSubnetsProvider exposes functions for retrieving the subnets
89
+// of networks managed by Docker, so they can be filtered.
90
+type NetworkSubnetsProvider interface {
91
+	V4Subnets() []net.IPNet
92
+	V6Subnets() []net.IPNet
77 93
 }
78 94
 
79 95
 // Config provides values for Cluster.
80 96
 type Config struct {
81
-	Root    string
82
-	Name    string
83
-	Backend executorpkg.Backend
97
+	Root                   string
98
+	Name                   string
99
+	Backend                executorpkg.Backend
100
+	NetworkSubnetsProvider NetworkSubnetsProvider
101
+
102
+	// DefaultAdvertiseAddr is the default host/IP or network interface to use
103
+	// if no AdvertiseAddr value is specified.
104
+	DefaultAdvertiseAddr string
84 105
 }
85 106
 
86 107
 // Cluster provides capabilities to participate in a cluster as a worker or a
... ...
@@ -88,13 +110,17 @@ type Config struct {
88 88
 type Cluster struct {
89 89
 	sync.RWMutex
90 90
 	*node
91
-	root        string
92
-	config      Config
93
-	configEvent chan struct{} // todo: make this array and goroutine safe
94
-	listenAddr  string
95
-	stop        bool
96
-	err         error
97
-	cancelDelay func()
91
+	root            string
92
+	config          Config
93
+	configEvent     chan struct{} // todo: make this array and goroutine safe
94
+	localAddr       string
95
+	actualLocalAddr string // after resolution, not persisted
96
+	remoteAddr      string
97
+	listenAddr      string
98
+	advertiseAddr   string
99
+	stop            bool
100
+	err             error
101
+	cancelDelay     func()
98 102
 }
99 103
 
100 104
 type node struct {
... ...
@@ -126,7 +152,7 @@ func New(config Config) (*Cluster, error) {
126 126
 		return nil, err
127 127
 	}
128 128
 
129
-	n, err := c.startNewNode(false, st.ListenAddr, "", "")
129
+	n, err := c.startNewNode(false, st.LocalAddr, st.RemoteAddr, st.ListenAddr, st.AdvertiseAddr, "", "")
130 130
 	if err != nil {
131 131
 		return nil, err
132 132
 	}
... ...
@@ -162,7 +188,12 @@ func (c *Cluster) loadState() (*state, error) {
162 162
 }
163 163
 
164 164
 func (c *Cluster) saveState() error {
165
-	dt, err := json.Marshal(state{ListenAddr: c.listenAddr})
165
+	dt, err := json.Marshal(state{
166
+		LocalAddr:     c.localAddr,
167
+		RemoteAddr:    c.remoteAddr,
168
+		ListenAddr:    c.listenAddr,
169
+		AdvertiseAddr: c.advertiseAddr,
170
+	})
166 171
 	if err != nil {
167 172
 		return err
168 173
 	}
... ...
@@ -195,7 +226,7 @@ func (c *Cluster) reconnectOnFailure(n *node) {
195 195
 			return
196 196
 		}
197 197
 		var err error
198
-		n, err = c.startNewNode(false, c.listenAddr, c.getRemoteAddress(), "")
198
+		n, err = c.startNewNode(false, c.localAddr, c.getRemoteAddress(), c.listenAddr, c.advertiseAddr, c.getRemoteAddress(), "")
199 199
 		if err != nil {
200 200
 			c.err = err
201 201
 			close(n.done)
... ...
@@ -204,24 +235,55 @@ func (c *Cluster) reconnectOnFailure(n *node) {
204 204
 	}
205 205
 }
206 206
 
207
-func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, joinToken string) (*node, error) {
207
+func (c *Cluster) startNewNode(forceNewCluster bool, localAddr, remoteAddr, listenAddr, advertiseAddr, joinAddr, joinToken string) (*node, error) {
208 208
 	if err := c.config.Backend.IsSwarmCompatible(); err != nil {
209 209
 		return nil, err
210 210
 	}
211
+
212
+	actualLocalAddr := localAddr
213
+	if actualLocalAddr == "" {
214
+		// If localAddr was not specified, resolve it automatically
215
+		// based on the route to joinAddr. localAddr can only be left
216
+		// empty on "join".
217
+		listenHost, _, err := net.SplitHostPort(listenAddr)
218
+		if err != nil {
219
+			return nil, fmt.Errorf("could not parse listen address: %v", err)
220
+		}
221
+
222
+		listenAddrIP := net.ParseIP(listenHost)
223
+		if listenAddrIP == nil || !listenAddrIP.IsUnspecified() {
224
+			actualLocalAddr = listenHost
225
+		} else {
226
+			if remoteAddr == "" {
227
+				// Should never happen except using swarms created by
228
+				// old versions that didn't save remoteAddr.
229
+				remoteAddr = "8.8.8.8:53"
230
+			}
231
+			conn, err := net.Dial("udp", remoteAddr)
232
+			if err != nil {
233
+				return nil, fmt.Errorf("could not find local IP address: %v", err)
234
+			}
235
+			localHostPort := conn.LocalAddr().String()
236
+			actualLocalAddr, _, _ = net.SplitHostPort(localHostPort)
237
+			conn.Close()
238
+		}
239
+	}
240
+
211 241
 	c.node = nil
212 242
 	c.cancelDelay = nil
213 243
 	c.stop = false
214 244
 	n, err := swarmagent.NewNode(&swarmagent.NodeConfig{
215
-		Hostname:         c.config.Name,
216
-		ForceNewCluster:  forceNewCluster,
217
-		ListenControlAPI: filepath.Join(c.root, controlSocket),
218
-		ListenRemoteAPI:  listenAddr,
219
-		JoinAddr:         joinAddr,
220
-		StateDir:         c.root,
221
-		JoinToken:        joinToken,
222
-		Executor:         container.NewExecutor(c.config.Backend),
223
-		HeartbeatTick:    1,
224
-		ElectionTick:     3,
245
+		Hostname:           c.config.Name,
246
+		ForceNewCluster:    forceNewCluster,
247
+		ListenControlAPI:   filepath.Join(c.root, controlSocket),
248
+		ListenRemoteAPI:    listenAddr,
249
+		AdvertiseRemoteAPI: advertiseAddr,
250
+		JoinAddr:           joinAddr,
251
+		StateDir:           c.root,
252
+		JoinToken:          joinToken,
253
+		Executor:           container.NewExecutor(c.config.Backend),
254
+		HeartbeatTick:      1,
255
+		ElectionTick:       3,
225 256
 	})
226 257
 	if err != nil {
227 258
 		return nil, err
... ...
@@ -236,8 +298,13 @@ func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, joinT
236 236
 		reconnectDelay: initialReconnectDelay,
237 237
 	}
238 238
 	c.node = node
239
+	c.localAddr = localAddr
240
+	c.actualLocalAddr = actualLocalAddr // not saved
241
+	c.remoteAddr = remoteAddr
239 242
 	c.listenAddr = listenAddr
243
+	c.advertiseAddr = advertiseAddr
240 244
 	c.saveState()
245
+
241 246
 	c.config.Backend.SetClusterProvider(c)
242 247
 	go func() {
243 248
 		err := n.Err(ctx)
... ...
@@ -301,8 +368,49 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
301 301
 		return "", err
302 302
 	}
303 303
 
304
+	listenHost, listenPort, err := resolveListenAddr(req.ListenAddr)
305
+	if err != nil {
306
+		c.Unlock()
307
+		return "", err
308
+	}
309
+
310
+	advertiseHost, advertisePort, err := c.resolveAdvertiseAddr(req.AdvertiseAddr, listenPort)
311
+	if err != nil {
312
+		c.Unlock()
313
+		return "", err
314
+	}
315
+
316
+	localAddr := listenHost
317
+
318
+	// If the advertise address is not one of the system's
319
+	// addresses, we also require a listen address.
320
+	listenAddrIP := net.ParseIP(listenHost)
321
+	if listenAddrIP != nil && listenAddrIP.IsUnspecified() {
322
+		advertiseIP := net.ParseIP(advertiseHost)
323
+		if advertiseIP == nil {
324
+			// not an IP
325
+			c.Unlock()
326
+			return "", errMustSpecifyListenAddr
327
+		}
328
+
329
+		systemIPs := listSystemIPs()
330
+
331
+		found := false
332
+		for _, systemIP := range systemIPs {
333
+			if systemIP.Equal(advertiseIP) {
334
+				found = true
335
+				break
336
+			}
337
+		}
338
+		if !found {
339
+			c.Unlock()
340
+			return "", errMustSpecifyListenAddr
341
+		}
342
+		localAddr = advertiseIP.String()
343
+	}
344
+
304 345
 	// todo: check current state existing
305
-	n, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "")
346
+	n, err := c.startNewNode(req.ForceNewCluster, localAddr, "", net.JoinHostPort(listenHost, listenPort), net.JoinHostPort(advertiseHost, advertisePort), "", "")
306 347
 	if err != nil {
307 348
 		c.Unlock()
308 349
 		return "", err
... ...
@@ -339,8 +447,23 @@ func (c *Cluster) Join(req types.JoinRequest) error {
339 339
 		c.Unlock()
340 340
 		return err
341 341
 	}
342
+
343
+	listenHost, listenPort, err := resolveListenAddr(req.ListenAddr)
344
+	if err != nil {
345
+		c.Unlock()
346
+		return err
347
+	}
348
+
349
+	var advertiseAddr string
350
+	advertiseHost, advertisePort, err := c.resolveAdvertiseAddr(req.AdvertiseAddr, listenPort)
351
+	// For joining, we don't need to provide an advertise address,
352
+	// since the remote side can detect it.
353
+	if err == nil {
354
+		advertiseAddr = net.JoinHostPort(advertiseHost, advertisePort)
355
+	}
356
+
342 357
 	// todo: check current state existing
343
-	n, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.JoinToken)
358
+	n, err := c.startNewNode(false, "", req.RemoteAddrs[0], net.JoinHostPort(listenHost, listenPort), advertiseAddr, req.RemoteAddrs[0], req.JoinToken)
344 359
 	if err != nil {
345 360
 		c.Unlock()
346 361
 		return err
... ...
@@ -530,15 +653,22 @@ func (c *Cluster) IsAgent() bool {
530 530
 	return c.node != nil && c.ready
531 531
 }
532 532
 
533
-// GetListenAddress returns the listening address for current manager's
534
-// consensus and dispatcher APIs.
535
-func (c *Cluster) GetListenAddress() string {
533
+// GetLocalAddress returns the local address.
534
+func (c *Cluster) GetLocalAddress() string {
536 535
 	c.RLock()
537 536
 	defer c.RUnlock()
538
-	if c.isActiveManager() {
539
-		return c.listenAddr
537
+	return c.actualLocalAddr
538
+}
539
+
540
+// GetAdvertiseAddress returns the remotely reachable address of this node.
541
+func (c *Cluster) GetAdvertiseAddress() string {
542
+	c.RLock()
543
+	defer c.RUnlock()
544
+	if c.advertiseAddr != "" {
545
+		advertiseHost, _, _ := net.SplitHostPort(c.advertiseAddr)
546
+		return advertiseHost
540 547
 	}
541
-	return ""
548
+	return c.actualLocalAddr
542 549
 }
543 550
 
544 551
 // GetRemoteAddress returns a known advertise address of a remote manager if
... ...
@@ -572,7 +702,10 @@ func (c *Cluster) ListenClusterEvents() <-chan struct{} {
572 572
 
573 573
 // Info returns information about the current cluster state.
574 574
 func (c *Cluster) Info() types.Info {
575
-	var info types.Info
575
+	info := types.Info{
576
+		NodeAddr: c.GetAdvertiseAddress(),
577
+	}
578
+
576 579
 	c.RLock()
577 580
 	defer c.RUnlock()
578 581
 
579 582
new file mode 100644
... ...
@@ -0,0 +1,250 @@
0
+package cluster
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"net"
6
+)
7
+
8
+var (
9
+	errNoSuchInterface       = errors.New("no such interface")
10
+	errMultipleIPs           = errors.New("could not choose an IP address to advertise since this system has multiple addresses")
11
+	errNoIP                  = errors.New("could not find the system's IP address")
12
+	errMustSpecifyListenAddr = errors.New("must specify a listening address because the address to advertise is not recognized as a system address")
13
+)
14
+
15
+func resolveListenAddr(specifiedAddr string) (string, string, error) {
16
+	specifiedHost, specifiedPort, err := net.SplitHostPort(specifiedAddr)
17
+	if err != nil {
18
+		return "", "", fmt.Errorf("could not parse listen address %s", specifiedAddr)
19
+	}
20
+
21
+	// Does the host component match any of the interface names on the
22
+	// system? If so, use the address from that interface.
23
+	interfaceAddr, err := resolveInterfaceAddr(specifiedHost)
24
+	if err == nil {
25
+		return interfaceAddr.String(), specifiedPort, nil
26
+	}
27
+	if err != errNoSuchInterface {
28
+		return "", "", err
29
+	}
30
+
31
+	return specifiedHost, specifiedPort, nil
32
+}
33
+
34
+func (c *Cluster) resolveAdvertiseAddr(advertiseAddr, listenAddrPort string) (string, string, error) {
35
+	// Approach:
36
+	// - If an advertise address is specified, use that. Resolve the
37
+	//   interface's address if an interface was specified in
38
+	//   advertiseAddr. Fill in the port from listenAddrPort if necessary.
39
+	// - If DefaultAdvertiseAddr is not empty, use that with the port from
40
+	//   listenAddrPort. Resolve the interface's address from
41
+	//   if an interface name was specified in DefaultAdvertiseAddr.
42
+	// - Otherwise, try to autodetect the system's address. Use the port in
43
+	//   listenAddrPort with this address if autodetection succeeds.
44
+
45
+	if advertiseAddr != "" {
46
+		advertiseHost, advertisePort, err := net.SplitHostPort(advertiseAddr)
47
+		if err != nil {
48
+			// Not a host:port specification
49
+			advertiseHost = advertiseAddr
50
+			advertisePort = listenAddrPort
51
+		}
52
+
53
+		// Does the host component match any of the interface names on the
54
+		// system? If so, use the address from that interface.
55
+		interfaceAddr, err := resolveInterfaceAddr(advertiseHost)
56
+		if err == nil {
57
+			return interfaceAddr.String(), advertisePort, nil
58
+		}
59
+		if err != errNoSuchInterface {
60
+			return "", "", err
61
+		}
62
+
63
+		return advertiseHost, advertisePort, nil
64
+	}
65
+
66
+	if c.config.DefaultAdvertiseAddr != "" {
67
+		// Does the default advertise address component match any of the
68
+		// interface names on the system? If so, use the address from
69
+		// that interface.
70
+		interfaceAddr, err := resolveInterfaceAddr(c.config.DefaultAdvertiseAddr)
71
+		if err == nil {
72
+			return interfaceAddr.String(), listenAddrPort, nil
73
+		}
74
+		if err != errNoSuchInterface {
75
+			return "", "", err
76
+		}
77
+
78
+		return c.config.DefaultAdvertiseAddr, listenAddrPort, nil
79
+	}
80
+
81
+	systemAddr, err := c.resolveSystemAddr()
82
+	if err != nil {
83
+		return "", "", err
84
+	}
85
+	return systemAddr.String(), listenAddrPort, nil
86
+}
87
+
88
+func resolveInterfaceAddr(specifiedInterface string) (net.IP, error) {
89
+	// Use a specific interface's IP address.
90
+	intf, err := net.InterfaceByName(specifiedInterface)
91
+	if err != nil {
92
+		return nil, errNoSuchInterface
93
+	}
94
+
95
+	addrs, err := intf.Addrs()
96
+	if err != nil {
97
+		return nil, err
98
+	}
99
+
100
+	var interfaceAddr4, interfaceAddr6 net.IP
101
+
102
+	for _, addr := range addrs {
103
+		ipAddr, ok := addr.(*net.IPNet)
104
+
105
+		if ok {
106
+			if ipAddr.IP.To4() != nil {
107
+				// IPv4
108
+				if interfaceAddr4 != nil {
109
+					return nil, fmt.Errorf("interface %s has more than one IPv4 address", specifiedInterface)
110
+				}
111
+				interfaceAddr4 = ipAddr.IP
112
+			} else {
113
+				// IPv6
114
+				if interfaceAddr6 != nil {
115
+					return nil, fmt.Errorf("interface %s has more than one IPv6 address", specifiedInterface)
116
+				}
117
+				interfaceAddr6 = ipAddr.IP
118
+			}
119
+		}
120
+	}
121
+
122
+	if interfaceAddr4 == nil && interfaceAddr6 == nil {
123
+		return nil, fmt.Errorf("interface %s has no usable IPv4 or IPv6 address", specifiedInterface)
124
+	}
125
+
126
+	// In the case that there's exactly one IPv4 address
127
+	// and exactly one IPv6 address, favor IPv4 over IPv6.
128
+	if interfaceAddr4 != nil {
129
+		return interfaceAddr4, nil
130
+	}
131
+	return interfaceAddr6, nil
132
+}
133
+
134
+func (c *Cluster) resolveSystemAddr() (net.IP, error) {
135
+	// Use the system's only IP address, or fail if there are
136
+	// multiple addresses to choose from.
137
+	interfaces, err := net.Interfaces()
138
+	if err != nil {
139
+		return nil, err
140
+	}
141
+
142
+	var systemAddr net.IP
143
+
144
+	// List Docker-managed subnets
145
+	v4Subnets := c.config.NetworkSubnetsProvider.V4Subnets()
146
+	v6Subnets := c.config.NetworkSubnetsProvider.V6Subnets()
147
+
148
+ifaceLoop:
149
+	for _, intf := range interfaces {
150
+		// Skip inactive interfaces and loopback interfaces
151
+		if (intf.Flags&net.FlagUp == 0) || (intf.Flags&net.FlagLoopback) != 0 {
152
+			continue
153
+		}
154
+
155
+		addrs, err := intf.Addrs()
156
+		if err != nil {
157
+			continue
158
+		}
159
+
160
+		var interfaceAddr4, interfaceAddr6 net.IP
161
+
162
+		for _, addr := range addrs {
163
+			ipAddr, ok := addr.(*net.IPNet)
164
+
165
+			// Skip loopback and link-local addresses
166
+			if !ok || !ipAddr.IP.IsGlobalUnicast() {
167
+				continue
168
+			}
169
+
170
+			if ipAddr.IP.To4() != nil {
171
+				// IPv4
172
+
173
+				// Ignore addresses in subnets that are managed by Docker.
174
+				for _, subnet := range v4Subnets {
175
+					if subnet.Contains(ipAddr.IP) {
176
+						continue ifaceLoop
177
+					}
178
+				}
179
+
180
+				if interfaceAddr4 != nil {
181
+					return nil, errMultipleIPs
182
+				}
183
+
184
+				interfaceAddr4 = ipAddr.IP
185
+			} else {
186
+				// IPv6
187
+
188
+				// Ignore addresses in subnets that are managed by Docker.
189
+				for _, subnet := range v6Subnets {
190
+					if subnet.Contains(ipAddr.IP) {
191
+						continue ifaceLoop
192
+					}
193
+				}
194
+
195
+				if interfaceAddr6 != nil {
196
+					return nil, errMultipleIPs
197
+				}
198
+
199
+				interfaceAddr6 = ipAddr.IP
200
+			}
201
+		}
202
+
203
+		// In the case that this interface has exactly one IPv4 address
204
+		// and exactly one IPv6 address, favor IPv4 over IPv6.
205
+		if interfaceAddr4 != nil {
206
+			if systemAddr != nil {
207
+				return nil, errMultipleIPs
208
+			}
209
+			systemAddr = interfaceAddr4
210
+		} else if interfaceAddr6 != nil {
211
+			if systemAddr != nil {
212
+				return nil, errMultipleIPs
213
+			}
214
+			systemAddr = interfaceAddr6
215
+		}
216
+	}
217
+
218
+	if systemAddr == nil {
219
+		return nil, errNoIP
220
+	}
221
+
222
+	return systemAddr, nil
223
+}
224
+
225
+func listSystemIPs() []net.IP {
226
+	interfaces, err := net.Interfaces()
227
+	if err != nil {
228
+		return nil
229
+	}
230
+
231
+	var systemAddrs []net.IP
232
+
233
+	for _, intf := range interfaces {
234
+		addrs, err := intf.Addrs()
235
+		if err != nil {
236
+			continue
237
+		}
238
+
239
+		for _, addr := range addrs {
240
+			ipAddr, ok := addr.(*net.IPNet)
241
+
242
+			if ok {
243
+				systemAddrs = append(systemAddrs, ipAddr.IP)
244
+			}
245
+		}
246
+	}
247
+
248
+	return systemAddrs
249
+}
... ...
@@ -127,6 +127,13 @@ type CommonConfig struct {
127 127
 	// Embedded structs that allow config
128 128
 	// deserialization without the full struct.
129 129
 	CommonTLSOptions
130
+
131
+	// SwarmDefaultAdvertiseAddr is the default host/IP or network interface
132
+	// to use if a wildcard address is specified in the ListenAddr value
133
+	// given to the /swarm/init endpoint and no advertise address is
134
+	// specified.
135
+	SwarmDefaultAdvertiseAddr string `json:"swarm-default-advertise-addr"`
136
+
130 137
 	LogConfig
131 138
 	bridgeConfig // bridgeConfig holds bridge network specific configuration.
132 139
 	registry.ServiceOptions
... ...
@@ -167,6 +174,8 @@ func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string)
167 167
 	cmd.IntVar(&maxConcurrentDownloads, []string{"-max-concurrent-downloads"}, defaultMaxConcurrentDownloads, usageFn("Set the max concurrent downloads for each pull"))
168 168
 	cmd.IntVar(&maxConcurrentUploads, []string{"-max-concurrent-uploads"}, defaultMaxConcurrentUploads, usageFn("Set the max concurrent uploads for each push"))
169 169
 
170
+	cmd.StringVar(&config.SwarmDefaultAdvertiseAddr, []string{"-swarm-default-advertise-addr"}, "", usageFn("Set default address or interface for swarm advertised address"))
171
+
170 172
 	config.MaxConcurrentDownloads = &maxConcurrentDownloads
171 173
 	config.MaxConcurrentUploads = &maxConcurrentUploads
172 174
 }
... ...
@@ -728,6 +728,42 @@ func (daemon *Daemon) Unmount(container *container.Container) error {
728 728
 	return nil
729 729
 }
730 730
 
731
+// V4Subnets returns the IPv4 subnets of networks that are managed by Docker.
732
+func (daemon *Daemon) V4Subnets() []net.IPNet {
733
+	var subnets []net.IPNet
734
+
735
+	managedNetworks := daemon.netController.Networks()
736
+
737
+	for _, managedNetwork := range managedNetworks {
738
+		v4Infos, _ := managedNetwork.Info().IpamInfo()
739
+		for _, v4Info := range v4Infos {
740
+			if v4Info.IPAMData.Pool != nil {
741
+				subnets = append(subnets, *v4Info.IPAMData.Pool)
742
+			}
743
+		}
744
+	}
745
+
746
+	return subnets
747
+}
748
+
749
+// V6Subnets returns the IPv6 subnets of networks that are managed by Docker.
750
+func (daemon *Daemon) V6Subnets() []net.IPNet {
751
+	var subnets []net.IPNet
752
+
753
+	managedNetworks := daemon.netController.Networks()
754
+
755
+	for _, managedNetwork := range managedNetworks {
756
+		_, v6Infos := managedNetwork.Info().IpamInfo()
757
+		for _, v6Info := range v6Infos {
758
+			if v6Info.IPAMData.Pool != nil {
759
+				subnets = append(subnets, *v6Info.IPAMData.Pool)
760
+			}
761
+		}
762
+	}
763
+
764
+	return subnets
765
+}
766
+
731 767
 func writeDistributionProgress(cancelFunc func(), outStream io.Writer, progressChan <-chan progress.Progress) {
732 768
 	progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(outStream, false)
733 769
 	operationCancelled := false
... ...
@@ -3614,8 +3614,11 @@ Initialize a new Swarm
3614 3614
 
3615 3615
 JSON Parameters:
3616 3616
 
3617
-- **ListenAddr** – Listen address used for inter-manager communication, as well as determining.
3618
-  the networking interface used for the VXLAN Tunnel Endpoint (VTEP).
3617
+- **ListenAddr** – Listen address used for inter-manager communication, as well as determining
3618
+  the networking interface used for the VXLAN Tunnel Endpoint (VTEP). This can either be an
3619
+  address/port combination in the form `192.168.1.1:4567`, or an interface followed by a port
3620
+  number, like `eth0:4567`. If the port number is omitted, the default swarm listening port is
3621
+  used.
3619 3622
 - **ForceNewCluster** – Force creating a new Swarm even if already part of one.
3620 3623
 - **Spec** – Configuration settings of the new Swarm.
3621 3624
     - **Orchestration** – Configuration settings for the orchestration aspects of the Swarm.
... ...
@@ -3615,8 +3615,11 @@ Initialize a new Swarm
3615 3615
 
3616 3616
 JSON Parameters:
3617 3617
 
3618
-- **ListenAddr** – Listen address used for inter-manager communication, as well as determining.
3619
-  the networking interface used for the VXLAN Tunnel Endpoint (VTEP).
3618
+- **ListenAddr** – Listen address used for inter-manager communication, as well as determining
3619
+  the networking interface used for the VXLAN Tunnel Endpoint (VTEP). This can either be an
3620
+  address/port combination in the form `192.168.1.1:4567`, or an interface followed by a port
3621
+  number, like `eth0:4567`. If the port number is omitted, the default swarm listening port is
3622
+  used.
3620 3623
 - **ForceNewCluster** – Force creating a new Swarm even if already part of one.
3621 3624
 - **Spec** – Configuration settings of the new Swarm.
3622 3625
     - **Orchestration** – Configuration settings for the orchestration aspects of the Swarm.
... ...
@@ -69,6 +69,7 @@ Options:
69 69
       -s, --storage-driver                   Storage driver to use
70 70
       --selinux-enabled                      Enable selinux support
71 71
       --storage-opt=[]                       Storage driver options
72
+      --swarm-default-advertise-addr         Set default address or interface for swarm advertised address
72 73
       --tls                                  Use TLS; implied by --tlsverify
73 74
       --tlscacert=~/.docker/ca.pem           Trust certs signed only by this CA
74 75
       --tlscert=~/.docker/cert.pem           Path to TLS certificate file
... ...
@@ -1042,6 +1043,7 @@ This is a full example of the allowed configuration options on Linux:
1042 1042
 	"tlscacert": "",
1043 1043
 	"tlscert": "",
1044 1044
 	"tlskey": "",
1045
+	"swarm-default-advertise-addr": "",
1045 1046
 	"api-cors-header": "",
1046 1047
 	"selinux-enabled": false,
1047 1048
 	"userns-remap": "",
... ...
@@ -1112,6 +1114,7 @@ This is a full example of the allowed configuration options on Windows:
1112 1112
     "tlscacert": "",
1113 1113
     "tlscert": "",
1114 1114
     "tlskey": "",
1115
+    "swarm-default-advertise-addr": "",
1115 1116
     "group": "",
1116 1117
     "default-ulimits": {},
1117 1118
     "bridge": "",
... ...
@@ -17,12 +17,13 @@ Usage:  docker swarm init [OPTIONS]
17 17
 Initialize a swarm
18 18
 
19 19
 Options:
20
+      --advertise-addr value            Advertised address (format: <ip|hostname|interface>[:port])
20 21
       --cert-expiry duration            Validity period for node certificates (default 2160h0m0s)
21 22
       --dispatcher-heartbeat duration   Dispatcher heartbeat period (default 5s)
22 23
       --external-ca value               Specifications of one or more certificate signing endpoints
23 24
       --force-new-cluster               Force create a new cluster from current state.
24 25
       --help                            Print usage
25
-      --listen-addr value               Listen address (default 0.0.0.0:2377)
26
+      --listen-addr value               Listen address (format: <ip|hostname|interface>[:port])
26 27
       --task-history-limit int          Task history retention limit (default 5)
27 28
 ```
28 29
 
... ...
@@ -31,7 +32,7 @@ in the newly created one node swarm cluster.
31 31
 
32 32
 
33 33
 ```bash
34
-$ docker swarm init --listen-addr 192.168.99.121:2377
34
+$ docker swarm init --advertise-addr 192.168.99.121
35 35
 Swarm initialized: current node (bvz81updecsj6wjz393c09vti) is now a manager.
36 36
 
37 37
 To add a worker to this swarm, run the following command:
... ...
@@ -70,11 +71,31 @@ The URL specifies the endpoint where signing requests should be submitted.
70 70
 
71 71
 ### `--force-new-cluster`
72 72
 
73
-This flag forces an existing node that was part of a quorum that was lost to restart as a single node Manager without losing its data
73
+This flag forces an existing node that was part of a quorum that was lost to restart as a single node Manager without losing its data.
74 74
 
75 75
 ### `--listen-addr value`
76 76
 
77
-The node listens for inbound swarm manager traffic on this IP:PORT
77
+The node listens for inbound Swarm manager traffic on this address. The default is to listen on
78
+0.0.0.0:2377. It is also possible to specify a network interface to listen on that interface's
79
+address; for example `--listen-addr eth0:2377`.
80
+
81
+Specifying a port is optional. If the value is a bare IP address, hostname, or interface
82
+name, the default port 2377 will be used.
83
+
84
+### `--advertise-addr value`
85
+
86
+This flag specifies the address that will be advertised to other members of the
87
+swarm for API access and overlay networking. If unspecified, Docker will check
88
+if the system has a single IP address, and use that IP address with with the
89
+listening port (see `--listen-addr`). If the system has multiple IP addresses,
90
+`--advertise-addr` must be specified so that the correct address is chosen for
91
+inter-manager communication and overlay networking.
92
+
93
+It is also possible to specify a network interface to advertise that interface's address;
94
+for example `--advertise-addr eth0:2377`.
95
+
96
+Specifying a port is optional. If the value is a bare IP address, hostname, or interface
97
+name, the default port 2377 will be used.
78 98
 
79 99
 ### `--task-history-limit`
80 100
 
... ...
@@ -17,9 +17,10 @@ Usage:  docker swarm join [OPTIONS] HOST:PORT
17 17
 Join a swarm as a node and/or manager
18 18
 
19 19
 Options:
20
-      --help                Print usage
21
-      --listen-addr value   Listen address (default 0.0.0.0:2377)
22
-      --token string        Token for entry into the swarm
20
+      --advertise-addr value   Advertised address (format: <ip|hostname|interface>[:port])
21
+      --help                   Print usage
22
+      --listen-addr value      Listen address
23
+      --token string           Token for entry into the swarm
23 24
 ```
24 25
 
25 26
 Join a node to a swarm. The node joins as a manager node or worker node based upon the token you
... ...
@@ -31,7 +32,7 @@ pass a worker token, the node joins as a worker.
31 31
 The example below demonstrates joining a manager node using a manager token.
32 32
 
33 33
 ```bash
34
-$ docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-7p73s1dx5in4tatdymyhg9hu2 --listen-addr 192.168.99.122:2377 192.168.99.121:2377
34
+$ docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-7p73s1dx5in4tatdymyhg9hu2 192.168.99.121:2377
35 35
 This node joined a swarm as a manager.
36 36
 $ docker node ls
37 37
 ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
... ...
@@ -48,7 +49,7 @@ should join as workers instead. Managers should be stable hosts that have static
48 48
 The example below demonstrates joining a worker node using a worker token.
49 49
 
50 50
 ```bash
51
-$ docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx --listen-addr 192.168.99.123:2377 192.168.99.121:2377
51
+$ docker swarm join --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx 192.168.99.121:2377
52 52
 This node joined a swarm as a worker.
53 53
 $ docker node ls
54 54
 ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
... ...
@@ -59,7 +60,36 @@ dvfxp4zseq4s0rih1selh0d20 *  manager1  Ready   Active        Leader
59 59
 
60 60
 ### `--listen-addr value`
61 61
 
62
-The node listens for inbound swarm manager traffic on this IP:PORT
62
+If the node is a manager, it will listen for inbound Swarm manager traffic on this
63
+address. The default is to listen on 0.0.0.0:2377. It is also possible to specify a
64
+network interface to listen on that interface's address; for example `--listen-addr eth0:2377`.
65
+
66
+Specifying a port is optional. If the value is a bare IP address, hostname, or interface
67
+name, the default port 2377 will be used.
68
+
69
+This flag is generally not necessary when joining an existing swarm.
70
+
71
+### `--advertise-addr value`
72
+
73
+This flag specifies the address that will be advertised to other members of the
74
+swarm for API access. If unspecified, Docker will check if the system has a
75
+single IP address, and use that IP address with with the listening port (see
76
+`--listen-addr`). If the system has multiple IP addresses, `--advertise-addr`
77
+must be specified so that the correct address is chosen for inter-manager
78
+communication and overlay networking.
79
+
80
+It is also possible to specify a network interface to advertise that interface's address;
81
+for example `--advertise-addr eth0:2377`.
82
+
83
+Specifying a port is optional. If the value is a bare IP address, hostname, or interface
84
+name, the default port 2377 will be used.
85
+
86
+This flag is generally not necessary when joining an existing swarm.
87
+
88
+### `--manager`
89
+
90
+Joins the node as a manager
91
+>>>>>>> 22565e1... Split advertised address from listen address
63 92
 
64 93
 ### `--token string`
65 94
 
... ...
@@ -23,14 +23,14 @@ node. For example, the tutorial uses a machine named `manager1`.
23 23
 2. Run the following command to create a new swarm:
24 24
 
25 25
     ```bash
26
-    docker swarm init --listen-addr <MANAGER-IP>:<PORT>
26
+    docker swarm init --advertise-addr <MANAGER-IP>
27 27
     ```
28 28
 
29 29
     In the tutorial, the following command creates a swarm on the `manager1`
30 30
     machine:
31 31
 
32 32
     ```bash
33
-    $ docker swarm init --listen-addr 192.168.99.100:2377
33
+    $ docker swarm init --advertise-addr 192.168.99.100
34 34
     Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager.
35 35
 
36 36
     To add a worker to this swarm, run the following command:
... ...
@@ -44,9 +44,9 @@ node. For example, the tutorial uses a machine named `manager1`.
44 44
         192.168.99.100:2377
45 45
     ```
46 46
 
47
-    The `--listen-addr` flag configures the manager node to listen on port
48
-    `2377`. The other nodes in the swarm must be able to access the manager at
49
-    the IP address.
47
+    The `--advertise-addr` flag configures the manager node to publish its
48
+    address as `192.168.99.100`. The other nodes in the swarm must be able
49
+    to access the manager at the IP address.
50 50
 
51 51
     The output incudes the commands to join new nodes to the swarm. Nodes will
52 52
     join as managers or workers depending on the value for the `--swarm-token`
... ...
@@ -211,7 +211,7 @@ func (s *DockerSwarmSuite) AddDaemon(c *check.C, joinSwarm, manager bool) *Swarm
211 211
 		port:   defaultSwarmPort + s.portIndex,
212 212
 	}
213 213
 	d.listenAddr = fmt.Sprintf("0.0.0.0:%d", d.port)
214
-	err := d.StartWithBusybox("--iptables=false") // avoid networking conflicts
214
+	err := d.StartWithBusybox("--iptables=false", "--swarm-default-advertise-addr=lo") // avoid networking conflicts
215 215
 	c.Assert(err, check.IsNil)
216 216
 
217 217
 	if joinSwarm == true {
... ...
@@ -624,11 +624,9 @@ func (s *DockerSwarmSuite) TestApiSwarmLeaveOnPendingJoin(c *check.C) {
624 624
 
625 625
 	go d2.Join(swarm.JoinRequest{
626 626
 		RemoteAddrs: []string{"nosuchhost:1234"},
627
-	}) // will block on pending state
628
-
629
-	waitAndAssert(c, defaultReconciliationTimeout, d2.checkLocalNodeState, checker.Equals, swarm.LocalNodeStatePending)
627
+	})
630 628
 
631
-	c.Assert(d2.Leave(true), checker.IsNil)
629
+	waitAndAssert(c, defaultReconciliationTimeout, d2.checkLocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
632 630
 
633 631
 	waitAndAssert(c, defaultReconciliationTimeout, d2.checkActiveContainerCount, checker.Equals, 1)
634 632
 
... ...
@@ -642,9 +640,9 @@ func (s *DockerSwarmSuite) TestApiSwarmRestoreOnPendingJoin(c *check.C) {
642 642
 	d := s.AddDaemon(c, false, false)
643 643
 	go d.Join(swarm.JoinRequest{
644 644
 		RemoteAddrs: []string{"nosuchhost:1234"},
645
-	}) // will block on pending state
645
+	})
646 646
 
647
-	waitAndAssert(c, defaultReconciliationTimeout, d.checkLocalNodeState, checker.Equals, swarm.LocalNodeStatePending)
647
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkLocalNodeState, checker.Equals, swarm.LocalNodeStateInactive)
648 648
 
649 649
 	c.Assert(d.Stop(), checker.IsNil)
650 650
 	c.Assert(d.Start(), checker.IsNil)
... ...
@@ -55,6 +55,7 @@ dockerd - Enable daemon mode
55 55
 [**-s**|**--storage-driver**[=*STORAGE-DRIVER*]]
56 56
 [**--selinux-enabled**]
57 57
 [**--storage-opt**[=*[]*]]
58
+[**--swarm-default-advertise-addr**[=*IP|HOSTNAME|INTERFACE*]]
58 59
 [**--tls**]
59 60
 [**--tlscacert**[=*~/.docker/ca.pem*]]
60 61
 [**--tlscert**[=*~/.docker/cert.pem*]]
... ...
@@ -239,6 +240,11 @@ output otherwise.
239 239
 **--storage-opt**=[]
240 240
   Set storage driver options. See STORAGE DRIVER OPTIONS.
241 241
 
242
+**--swarm-default-advertise-addr**=*IP|HOSTNAME|INTERFACE*
243
+  Set default address or interface for swarm to advertise as its externally-reachable address to other cluster
244
+  members. This can be a hostname, an IP address, or an interface such as `eth0`. A port cannot be specified with
245
+  this option.
246
+
242 247
 **--tls**=*true*|*false*
243 248
   Use TLS; implied by --tlsverify. Default is false.
244 249