diff --git a/mesh/connection.go b/mesh/connection.go index 7edb9253f8..ae03017df2 100644 --- a/mesh/connection.go +++ b/mesh/connection.go @@ -40,17 +40,19 @@ type RemoteConnection struct { type LocalConnection struct { sync.RWMutex RemoteConnection - TCPConn *net.TCPConn - version byte - tcpSender TCPSender - SessionKey *[32]byte - heartbeatTCP *time.Ticker - Router *Router - uid uint64 - actionChan chan<- ConnectionAction - errorChan chan<- error - finished <-chan struct{} // closed to signal that actorLoop has finished - OverlayConn OverlayConnection + TCPConn *net.TCPConn + TrustRemote bool // is remote on a trusted subnet? + TrustedByRemote bool // does remote trust us? + version byte + tcpSender TCPSender + SessionKey *[32]byte + heartbeatTCP *time.Ticker + Router *Router + uid uint64 + actionChan chan<- ConnectionAction + errorChan chan<- error + finished <-chan struct{} // closed to signal that actorLoop has finished + OverlayConn OverlayConnection } type ConnectionAction func() error @@ -94,6 +96,7 @@ func StartLocalConnection(connRemote *RemoteConnection, tcpConn *net.TCPConn, ro RemoteConnection: *connRemote, // NB, we're taking a copy of connRemote here. Router: router, TCPConn: tcpConn, + TrustRemote: router.Trusts(connRemote), uid: randUint64(), actionChan: actionChan, errorChan: errorChan, @@ -189,13 +192,19 @@ func (conn *LocalConnection) run(actionChan <-chan ConnectionAction, errorChan < conn.Log("connection ready; using protocol version", conn.version) + // only use negotiated session key for untrusted connections + var sessionKey *[32]byte + if conn.Untrusted() { + sessionKey = conn.SessionKey + } + params := OverlayConnectionParams{ RemotePeer: conn.remote, LocalAddr: conn.TCPConn.LocalAddr().(*net.TCPAddr), RemoteAddr: conn.TCPConn.RemoteAddr().(*net.TCPAddr), Outbound: conn.outbound, ConnUID: conn.uid, - SessionKey: conn.SessionKey, + SessionKey: sessionKey, SendControlMessage: conn.sendOverlayControlMessage, Features: intro.Features, } @@ -253,6 +262,7 @@ func (conn *LocalConnection) makeFeatures() map[string]string { "ShortID": fmt.Sprint(conn.local.ShortID), "UID": fmt.Sprint(conn.local.UID), "ConnID": fmt.Sprint(conn.uid), + "Trusted": fmt.Sprint(conn.TrustRemote), } conn.Router.Overlay.AddFeaturesTo(features) return features @@ -300,6 +310,15 @@ func (conn *LocalConnection) parseFeatures(features features) (*Peer, error) { } } + var trusted bool + if trustedStr, present := features["Trusted"]; present { + trusted, err = strconv.ParseBool(trustedStr) + if err != nil { + return nil, err + } + } + conn.TrustedByRemote = trusted + uid, err := ParsePeerUID(features.Get("UID")) if err != nil { return nil, err @@ -435,3 +454,7 @@ func (conn *LocalConnection) handleProtocolMsg(tag ProtocolTag, payload []byte) func (conn *LocalConnection) extendReadDeadline() { conn.TCPConn.SetReadDeadline(time.Now().Add(TCPHeartbeat * 2)) } + +func (conn *LocalConnection) Untrusted() bool { + return !conn.TrustRemote || !conn.TrustedByRemote +} diff --git a/mesh/router.go b/mesh/router.go index 5f51cc8799..598e485ddc 100644 --- a/mesh/router.go +++ b/mesh/router.go @@ -37,6 +37,7 @@ type Config struct { Password []byte ConnLimit int PeerDiscovery bool + TrustedSubnets []*net.IPNet } type Router struct { @@ -181,3 +182,17 @@ func (router *Router) applyTopologyUpdate(update []byte) (PeerNameSet, PeerNameS } return origUpdate, newUpdate, nil } + +func (router *Router) Trusts(remote *RemoteConnection) bool { + if tcpAddr, err := net.ResolveTCPAddr("tcp4", remote.remoteTCPAddr); err == nil { + for _, trustedSubnet := range router.TrustedSubnets { + if trustedSubnet.Contains(tcpAddr.IP) { + return true + } + } + } else { + // Should not happen as remoteTCPAddr was obtained from TCPConn + log.Errorf("Unable to parse remote TCP addr: %s", err) + } + return false +} diff --git a/mesh/status.go b/mesh/status.go index 0dce08bbf3..559dae6617 100644 --- a/mesh/status.go +++ b/mesh/status.go @@ -2,6 +2,7 @@ package mesh import ( "fmt" + "net" ) type Status struct { @@ -19,6 +20,7 @@ type Status struct { Connections []LocalConnectionStatus Targets []string OverlayDiagnostics interface{} + TrustedSubnets []string } type PeerStatus struct { @@ -69,7 +71,8 @@ func NewStatus(router *Router) *Status { NewBroadcastRouteStatusSlice(router.Routes), NewLocalConnectionStatusSlice(router.ConnectionMaker), NewTargetSlice(router.ConnectionMaker), - router.Overlay.Diagnostics()} + router.Overlay.Diagnostics(), + NewTrustedSubnetsSlice(router.TrustedSubnets)} } func NewPeerStatusSlice(peers *Peers) []PeerStatus { @@ -147,6 +150,13 @@ func NewLocalConnectionStatusSlice(cm *ConnectionMaker) []LocalConnectionStatus } lc, _ := conn.(*LocalConnection) info := fmt.Sprintf("%-6v %v", lc.OverlayConn.DisplayName(), conn.Remote()) + if lc.Router.UsingPassword() { + if lc.Untrusted() { + info = fmt.Sprintf("%-11v %v", "encrypted", info) + } else { + info = fmt.Sprintf("%-11v %v", "unencrypted", info) + } + } slice = append(slice, LocalConnectionStatus{conn.RemoteTCPAddr(), conn.Outbound(), state, info}) } for address, target := range cm.targets { @@ -191,3 +201,11 @@ func NewTargetSlice(cm *ConnectionMaker) []string { } return <-resultChan } + +func NewTrustedSubnetsSlice(trustedSubnets []*net.IPNet) []string { + trustedSubnetStrs := []string{} + for _, trustedSubnet := range trustedSubnets { + trustedSubnetStrs = append(trustedSubnetStrs, trustedSubnet.String()) + } + return trustedSubnetStrs +} diff --git a/prog/weaver/http.go b/prog/weaver/http.go index 1dc849256b..a0cca5c88d 100644 --- a/prog/weaver/http.go +++ b/prog/weaver/http.go @@ -27,11 +27,11 @@ var rootTemplate = template.New("root").Funcs(map[string]interface{}{ } return count }, - "upstreamServers": func(servers []string) string { - if len(servers) == 0 { + "printList": func(list []string) string { + if len(list) == 0 { return "none" } - return strings.Join(servers, ", ") + return strings.Join(list, ", ") }, "printIPAMRanges": func(router weave.NetworkRouterStatus, status ipam.Status) string { var buffer bytes.Buffer @@ -138,47 +138,48 @@ func defTemplate(name string, text string) *template.Template { } var statusTemplate = defTemplate("status", `\ - Version: {{.Version}} + Version: {{.Version}} - Service: router - Protocol: {{.Router.Protocol}} \ + Service: router + Protocol: {{.Router.Protocol}} \ {{if eq .Router.ProtocolMinVersion .Router.ProtocolMaxVersion}}\ {{.Router.ProtocolMaxVersion}}\ {{else}}\ {{.Router.ProtocolMinVersion}}..{{.Router.ProtocolMaxVersion}}\ {{end}} - Name: {{.Router.Name}}({{.Router.NickName}}) - Encryption: {{printState .Router.Encryption}} - PeerDiscovery: {{printState .Router.PeerDiscovery}} - Targets: {{len .Router.Targets}} - Connections: {{len .Router.Connections}}{{with printConnectionCounts .Router.Connections}} ({{.}}){{end}} - Peers: {{len .Router.Peers}}{{with printPeerConnectionCounts .Router.Peers}} (with {{.}} connections){{end}} + Name: {{.Router.Name}}({{.Router.NickName}}) + Encryption: {{printState .Router.Encryption}} + PeerDiscovery: {{printState .Router.PeerDiscovery}} + Targets: {{len .Router.Targets}} + Connections: {{len .Router.Connections}}{{with printConnectionCounts .Router.Connections}} ({{.}}){{end}} + Peers: {{len .Router.Peers}}{{with printPeerConnectionCounts .Router.Peers}} (with {{.}} connections){{end}} + TrustedSubnets: {{printList .Router.TrustedSubnets}} {{if .IPAM}}\ - Service: ipam + Service: ipam {{if .IPAM.Entries}}\ {{if allIPAMOwnersUnreachable .IPAM}}\ - Status: all IP ranges owned by unreachable peers - use 'rmpeer' if they are dead + Status: all IP ranges owned by unreachable peers - use 'rmpeer' if they are dead {{else if len .IPAM.PendingAllocates}}\ - Status: waiting for IP range grant from peers + Status: waiting for IP range grant from peers {{else}}\ - Status: ready + Status: ready {{end}}\ {{else if .IPAM.Paxos}}\ - Status: awaiting consensus (quorum: {{.IPAM.Paxos.Quorum}}, known: {{.IPAM.Paxos.KnownNodes}}) + Status: awaiting consensus (quorum: {{.IPAM.Paxos.Quorum}}, known: {{.IPAM.Paxos.KnownNodes}}) {{else}}\ - Status: idle + Status: idle {{end}}\ - Range: {{.IPAM.Range}} - DefaultSubnet: {{.IPAM.DefaultSubnet}} + Range: {{.IPAM.Range}} + DefaultSubnet: {{.IPAM.DefaultSubnet}} {{end}}\ {{if .DNS}}\ - Service: dns - Domain: {{.DNS.Domain}} - Upstream: {{upstreamServers .DNS.Upstream}} - TTL: {{.DNS.TTL}} - Entries: {{countDNSEntries .DNS.Entries}} + Service: dns + Domain: {{.DNS.Domain}} + Upstream: {{printList .DNS.Upstream}} + TTL: {{.DNS.TTL}} + Entries: {{countDNSEntries .DNS.Entries}} {{end}}\ `) diff --git a/prog/weaver/main.go b/prog/weaver/main.go index d5fb2553e0..51acd0c33f 100644 --- a/prog/weaver/main.go +++ b/prog/weaver/main.go @@ -69,6 +69,7 @@ func main() { dnsEffectiveListenAddress string iface *net.Interface datapathName string + trustedSubnetStr string ) mflag.BoolVar(&justVersion, []string{"#version", "-version"}, false, "print version and exit") @@ -101,6 +102,8 @@ func main() { mflag.StringVar(&dnsEffectiveListenAddress, []string{"-dns-effective-listen-address"}, "", "address DNS will actually be listening, after Docker port mapping") mflag.StringVar(&datapathName, []string{"-datapath"}, "", "ODP datapath name") + mflag.StringVar(&trustedSubnetStr, []string{"-trusted-subnets"}, "", "Command separated list of trusted subnets in CIDR notation") + // crude way of detecting that we probably have been started in a // container, with `weave launch` --> suppress misleading paths in // mflags error messages. @@ -193,10 +196,7 @@ func main() { Log.Println("Communication between peers is unencrypted.") } else { config.Password = []byte(password) - Log.Println("Communication between peers is encrypted.") - - // fastdp doesn't support encryption - fastDPOverlay = nil + Log.Println("Communication between peers via untrusted networks is encrypted.") } overlays := weave.NewOverlaySwitch() @@ -237,6 +237,10 @@ func main() { networkConfig.PacketLogging = nopPacketLogging{} } + if config.TrustedSubnets, err = parseTrustedSubnets(trustedSubnetStr); err != nil { + Log.Fatal("Unable to parse trusted subnets: ", err) + } + router := weave.NewNetworkRouter(config, networkConfig, name, nickName, overlays) Log.Println("Our name is", router.Ourself) @@ -405,6 +409,24 @@ func determineQuorum(initPeerCountFlag int, peers []string) uint { return quorum } +func parseTrustedSubnets(trustedSubnetStr string) ([]*net.IPNet, error) { + trustedSubnets := []*net.IPNet{} + + if trustedSubnetStr == "" { + return trustedSubnets, nil + } + + for _, subnetStr := range strings.Split(trustedSubnetStr, ",") { + _, subnet, err := net.ParseCIDR(subnetStr) + if err != nil { + return nil, err + } + trustedSubnets = append(trustedSubnets, subnet) + } + + return trustedSubnets, nil +} + func listenAndServeHTTP(httpAddr string, muxRouter *mux.Router) { protocol := "tcp" if strings.HasPrefix(httpAddr, "/") { diff --git a/router/fastdp.go b/router/fastdp.go index 571fb10cad..0fee520533 100644 --- a/router/fastdp.go +++ b/router/fastdp.go @@ -515,9 +515,8 @@ type fastDatapathForwarder struct { func (fastdp fastDatapathOverlay) PrepareConnection(params mesh.OverlayConnectionParams) (mesh.OverlayConnection, error) { if params.SessionKey != nil { - // No encryption suport in fastdp. The weaver main.go - // is responsible for ensuring this doesn't happen. - log.Fatal("Attempt to use FastDatapath with encryption") + // No encryption support in fastdp + return nil, fmt.Errorf("encryption not supported") } vxlanVportID := fastdp.mainVxlanVportID diff --git a/router/overlay_switch.go b/router/overlay_switch.go index 9e6efd009b..da4bab7838 100644 --- a/router/overlay_switch.go +++ b/router/overlay_switch.go @@ -204,8 +204,16 @@ func (osw *OverlaySwitch) PrepareConnection(params mesh.OverlayConnectionParams) subConn, err := overlay.PrepareConnection(params) if err != nil { - fwd.stopFrom(0) - return nil, err + log.Infof("Unable to use %s for connection to %s(%s): %s", + overlay.name, + params.RemotePeer.Name, + params.RemotePeer.NickName, + err) + // failed to start subforwarder - record overlay name and continue + fwd.forwarders[i] = subForwarder{ + overlayName: overlay.name, + } + continue } subFwd := subConn.(OverlayForwarder) diff --git a/site/features.md b/site/features.md index 4dbbb7ab26..e7e949a121 100644 --- a/site/features.md +++ b/site/features.md @@ -63,7 +63,9 @@ Weave automatically chooses the fastest available method to transport data between peers. The most performant of these ('fastdp') offers near-native throughput and latency but does not support encryption; consequently supplying a password will cause the router to fall back -to a slower mode ('sleeve') that does. +to a slower mode ('sleeve') that does, for connections that traverse +untrusted networks (see the [security](#security) section for more +details). Even when encryption is not in use, certain adverse network conditions will cause this fallback to occur dynamically; in these circumstances, @@ -321,10 +323,16 @@ way to generate a random password which satsifies this requirement is < /dev/urandom tr -dc A-Za-z0-9 | head -c9 ; echo -The same password must be specified for all weave peers. Note that -supplying a password will [cause weave to fall back to a slower -method](#fast-data-path) for transporting data between -peers. +The same password must be specified for all weave peers; by default +both control and data plane traffic will then use authenticated +encryption. If some of your peers are colocated in a trusted network +(for example within the boundary of your own datacentre) you can use +the `--trusted-subnets` argument to `weave launch` to selectively +disable data plane encryption as an optimisation. Both peers must +consider the other to be in a trusted subnet for this to take place - +if they do not, weave will [fall back to a slower +method](#fast-data-path) for transporting data between peers as fast +datapath does not support encryption. Be aware that: diff --git a/site/troubleshooting.md b/site/troubleshooting.md index bc5bbb2b1a..22b81758e0 100644 --- a/site/troubleshooting.md +++ b/site/troubleshooting.md @@ -68,29 +68,30 @@ A status summary can be obtained with `weave status`: ```` $ weave status - Version: 1.1.0 - - Service: router - Protocol: weave 1..2 - Name: 4a:0f:f6:ec:1c:93(host1) - Encryption: disabled - PeerDiscovery: enabled - Targets: [192.168.48.14 192.168.48.15] - Connections: 5 (1 established, 1 pending, 1 retrying, 1 failed, 1 connecting) - Peers: 3 (with 5 established, 1 pending connections) - - Service: ipam - Consensus: achieved - Range: 10.32.0.0-10.47.255.255 - DefaultSubnet: 10.32.0.0/12 - - Service: dns - Domain: weave.local. - TTL: 1 - Entries: 9 - - Service: proxy - Address: tcp://127.0.0.1:12375 + Version: 1.1.0 + + Service: router + Protocol: weave 1..2 + Name: 4a:0f:f6:ec:1c:93(host1) + Encryption: disabled + PeerDiscovery: enabled + Targets: [192.168.48.14 192.168.48.15] + Connections: 5 (1 established, 1 pending, 1 retrying, 1 failed, 1 connecting) + Peers: 3 (with 5 established, 1 pending connections) + TrustedSubnets: none + + Service: ipam + Consensus: achieved + Range: 10.32.0.0-10.47.255.255 + DefaultSubnet: 10.32.0.0/12 + + Service: dns + Domain: weave.local. + TTL: 1 + Entries: 9 + + Service: proxy + Address: tcp://127.0.0.1:12375 ```` @@ -129,6 +130,9 @@ state. Further details are available with number of connections peers have to other peers. Further details are available with [`weave status peers`](#weave-status-peers). +'TrustedSubnets' shows subnets which the router trusts as specified by +the `--trusted-subnets` option to `weave launch`. + There are further sections for the [IP address allocator](ipam.html#troubleshooting), [weaveDNS](weavedns.html#troubleshooting), and [Weave Docker API @@ -141,8 +145,8 @@ obtained with `weave status connections`: ```` $ weave status connections -<- 192.168.48.12:33866 established fastdp 7e:21:4a:70:2f:45(host2) -<- 192.168.48.13:60773 pending fastdp 7e:ae:cd:d5:23:8d(host3) +<- 192.168.48.12:33866 established unencrypted fastdp 7e:21:4a:70:2f:45(host2) +<- 192.168.48.13:60773 pending encrypted fastdp 7e:ae:cd:d5:23:8d(host3) -> 192.168.48.14:6783 retrying dial tcp4 192.168.48.14:6783: no route to host -> 192.168.48.15:6783 failed dial tcp4 192.168.48.15:6783: no route to host, retry: 2015-08-06 18:55:38.246910357 +0000 UTC -> 192.168.48.16:6783 connecting @@ -162,8 +166,8 @@ The columns are as follows: heartbeat * `established` - TCP connection and corresponding UDP path are up * Info - the failure reason for failed and retrying connections, or - the data transport method, remote peer name and nickname for - pending and established connections + the encryption mode, data transport method, remote peer name and + nickname for pending and established connections ### List peers diff --git a/test/110_encryption_2_test.sh b/test/110_encryption_2_test.sh index 5f46a463e2..205a992803 100755 --- a/test/110_encryption_2_test.sh +++ b/test/110_encryption_2_test.sh @@ -14,4 +14,7 @@ start_container $HOST1 $C1/24 --name=c1 start_container $HOST2 $C2/24 --name=c2 assert_raises "exec_on $HOST1 c1 $PING $C2" +assert_raises "weave_on $HOST1 status connections | grep encrypted" +assert_raises "weave_on $HOST2 status connections | grep encrypted" + end_suite diff --git a/test/115_optional_encryption_2_test.sh b/test/115_optional_encryption_2_test.sh new file mode 100755 index 0000000000..a84f2e6590 --- /dev/null +++ b/test/115_optional_encryption_2_test.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +. ./config.sh + +start_suite "Optional encryption via trusted subnets" + +# Determine subnet for hosts given either an IP or name. We need to resolve +# these entries on the remote hosts to make sure we get the private +# IP addresses in the Circle/GCE context +HOST1_IP=$($SSH $HOST1 "getent hosts $HOST1" | grep $HOST1 | cut -d ' ' -f 1) +HOST2_IP=$($SSH $HOST2 "getent hosts $HOST2" | grep $HOST2 | cut -d ' ' -f 1) +HOST1_CIDR=$($SSH $HOST1 "ip addr show" | grep -oP $HOST1_IP/[0-9]+) +HOST2_CIDR=$($SSH $HOST2 "ip addr show" | grep -oP $HOST2_IP/[0-9]+) + +# Check asymmetric trust - connections should be encrypted +weave_on $HOST1 launch --password wfvAwt7sj --trusted-subnets $HOST2_CIDR +weave_on $HOST2 launch --password wfvAwt7sj $HOST1 +assert_raises "weave_on $HOST1 status connections | grep encrypted" +assert_raises "weave_on $HOST2 status connections | grep encrypted" + +weave_on $HOST1 stop +weave_on $HOST2 stop + +# Check symmetric trust - overlay in plaintext +weave_on $HOST1 launch --password wfvAwt7sj --trusted-subnets $HOST2_CIDR +weave_on $HOST2 launch --password wfvAwt7sj --trusted-subnets $HOST1_CIDR $HOST1 +assert_raises "weave_on $HOST1 status connections | grep unencrypted" +assert_raises "weave_on $HOST2 status connections | grep unencrypted" + +end_suite diff --git a/weave b/weave index c9e7399d59..ac3e894c0d 100755 --- a/weave +++ b/weave @@ -42,7 +42,8 @@ weave setup weave version weave launch [--password ] [--nickname ] [--ipalloc-range [--ipalloc-default-subnet ]] - [--no-discovery] [--init-peer-count ] ... + [--no-discovery] [--init-peer-count ] + [--trusted-subnets ,...] ... weave launch-router [--password ] [--nickname ] [--ipalloc-range [--ipalloc-default-subnet ]] [--no-discovery] [--init-peer-count ] ... @@ -321,8 +322,6 @@ DATAPATH=datapath BRIDGE_IFNAME=link-${BRIDGE} DATAPATH_IFNAME=${DATAPATH}-link CONTAINER_IFNAME=ethwe -# ROUTER_HOSTNETNS_IFNAME is only used for fastdp with encryption -ROUTER_HOSTNETNS_IFNAME=veth-weave PORT=${WEAVE_PORT:-6783} HTTP_PORT=6784 PROXY_PORT=12375 @@ -765,27 +764,7 @@ ask_version() { } router_opts_fastdp() { - if [ -z "$WEAVE_PASSWORD" ] ; then - echo "--datapath $DATAPATH" - else - # When using encryption, we still do bridging on the ODP - # datapath, because you can 'weave launch' without encryption - # and then later restart the router with encryption, or vice - # versa. Encryption disables the use of the fastdp Overlay, - # but the router could still use the fastdp Bridge to receive - # packets. However, pcap has better performance when sniffing - # every packet. So we pass --iface to use the pcap Bridge. - # - # Why don't we simply pass "--iface $BRIDGE". We could, - # except for the fact that NetworkManager likes to down the - # odp $BRIDGE netdev (at least under ubuntu), and you can only - # use pcap on an interface that is up. We avoid that by use - # pcap via a veth pair (NetworkManager leaves them alone). - # Having a netdev in the host netns called "ethwe" might - # surprise people, so it is called $ROUTER_HOSTNETNS_IFNAME - # instead. - echo "--datapath $DATAPATH --iface $ROUTER_HOSTNETNS_IFNAME" - fi + echo "--datapath $DATAPATH" } router_opts_bridge() { @@ -801,15 +780,7 @@ router_opts_bridged_fastdp() { ###################################################################### setup_router_iface_fastdp() { - if [ -n "$WEAVE_PASSWORD" ] ; then - # See router_opts_fastdp - # No-op if already attached - if ip link show $LOCAL_IFNAME >/dev/null 2>&1 ; then - return 0 - fi - connect_container_to_bridge $ROUTER_HOSTNETNS_IFNAME && - ip link set $ROUTER_HOSTNETNS_IFNAME up - fi + true } setup_router_iface_bridge() { @@ -1606,8 +1577,6 @@ launch_router() { if [ "$BRIDGE_TYPE" != bridge ] ; then NETHOST_OPT="--net=host" HTTP_IP=127.0.0.1 - # In case there is a lingering veth-weave netdev - ip link del $ROUTER_HOSTNETNS_IFNAME >/dev/null 2>&1 || true fi # Set WEAVE_DOCKER_ARGS in the environment in order to supply @@ -1660,10 +1629,6 @@ attach_router() { stop_router() { stop $CONTAINER_NAME "Weave" conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true - # Remove the veth-weave netdev in a fastdp context - if detect_bridge_type && [ "$BRIDGE_TYPE" != bridge ] ; then - ip link del $ROUTER_HOSTNETNS_IFNAME >/dev/null 2>&1 || true - fi } launch_proxy() { @@ -1917,13 +1882,13 @@ EOF fi if [ -z "$SUB_STATUS" ] && check_running $PROXY_CONTAINER_NAME 2>/dev/null && PROXY_ADDRS=$(proxy_addrs) ; then echo - echo " Service: proxy" - echo " Address: $PROXY_ADDRS" + echo " Service: proxy" + echo " Address: $PROXY_ADDRS" fi if [ -z "$SUB_STATUS" ] && check_running $PLUGIN_CONTAINER_NAME 2>/dev/null ; then echo - echo " Service: plugin" - echo " DriverName: weave" + echo " Service: plugin" + echo " DriverName: weave" fi [ -n "$SUB_STATUS" ] || echo [ $res -eq 0 ]