diff --git a/integration/300_internet_edge_test.sh b/integration/300_internet_edge_test.sh new file mode 100755 index 000000000..1b3a61a5d --- /dev/null +++ b/integration/300_internet_edge_test.sh @@ -0,0 +1,26 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Test short lived connections from the Internet" + +weave_on $HOST1 launch +scope_on $HOST1 launch +docker_on $HOST1 run -d -p 80:80 --name nginx nginx + +do_connections() { + while true; do + curl -s http://$HOST1:80/ >/dev/null + sleep 1 + done +} +do_connections& + +sleep 5 # give the probe a few seconds to build a report and send it to the app + +has_container $HOST1 nginx 1 +has_connection $HOST1 "The Internet" nginx + +kill %do_connections + +scope_end_suite diff --git a/integration/config.sh b/integration/config.sh index 84769111f..d41e06ef4 100644 --- a/integration/config.sh +++ b/integration/config.sh @@ -31,12 +31,12 @@ weave_on() { DOCKER_HOST=tcp://$host:$DOCKER_PORT $WEAVE "$@" } -# this checks we have a weavescope container +# this checks we have a named container has_container() { local host=$1 local name=$2 local count=$3 - assert "curl -s http://$host:4040/api/topology/containers?system=show | jq -r '[.nodes | .[] | select(.label_major == \"$name\")] | length'" $count + assert "curl -s http://$host:4040/api/topology/containers?system=show | jq -r '[.nodes[] | select(.label_major == \"$name\")] | length'" $count } scope_end_suite() { @@ -45,3 +45,19 @@ scope_end_suite() { docker_on $host rm -f $(docker_on $host ps -a -q) 2>/dev/null 1>&2 || true done } + +container_id() { + local host="$1" + local name="$2" + echo $(curl -s http://$host:4040/api/topology/containers?system=show | jq -r ".nodes[] | select(.label_major == \"$name\") | .id") +} + +# this checks we have an edge from container 1 to container 2 +has_connection() { + local host="$1" + local from="$2" + local to="$3" + local from_id=$(container_id "$host" "$from") + local to_id=$(container_id "$host" "$to") + assert "curl -s http://$host:4040/api/topology/containers?system=show | jq -r '.nodes[\"$from_id\"].adjacency | contains([\"$to_id\"])'" true +} diff --git a/probe/docker/container.go b/probe/docker/container.go index 6bbae82d3..113a99a63 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -70,7 +70,7 @@ type Container interface { ID() string Image() string PID() int - GetNode() report.Node + GetNode([]net.IP) report.Node StartGatheringStats() error StopGatheringStats() @@ -183,7 +183,7 @@ func (c *container) StopGatheringStats() { return } -func (c *container) ports() string { +func (c *container) ports(localAddrs []net.IP) string { if c.container.NetworkSettings == nil { return "" } @@ -195,21 +195,27 @@ func (c *container) ports() string { continue } for _, b := range bindings { - ports = append(ports, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port)) + if b.HostIP == "0.0.0.0" { + for _, ip := range localAddrs { + ports = append(ports, fmt.Sprintf("%s:%s->%s", ip, b.HostPort, port)) + } + } else { + ports = append(ports, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port)) + } } } return strings.Join(ports, ", ") } -func (c *container) GetNode() report.Node { +func (c *container) GetNode(localAddrs []net.IP) report.Node { c.RLock() defer c.RUnlock() result := report.MakeNodeWith(map[string]string{ ContainerID: c.ID(), ContainerName: strings.TrimPrefix(c.container.Name, "/"), - ContainerPorts: c.ports(), + ContainerPorts: c.ports(localAddrs), ContainerCreated: c.container.Created.Format(time.RFC822), ContainerCommand: c.container.Path + " " + strings.Join(c.container.Args, " "), ImageID: c.container.Image, diff --git a/probe/docker/container_linux_test.go b/probe/docker/container_linux_test.go index a5714b6e5..a7a027acd 100644 --- a/probe/docker/container_linux_test.go +++ b/probe/docker/container_linux_test.go @@ -78,7 +78,7 @@ func TestContainer(t *testing.T) { "memory_usage": "12345", }) test.Poll(t, 100*time.Millisecond, want, func() interface{} { - node := c.GetNode() + node := c.GetNode([]net.IP{}) for k, v := range node.Metadata { if v == "0" { delete(node.Metadata, k) @@ -93,7 +93,7 @@ func TestContainer(t *testing.T) { if c.PID() != 1 { t.Errorf("%s != 1", c.PID()) } - if !reflect.DeepEqual(docker.ExtractContainerIPs(c.GetNode()), []string{"1.2.3.4"}) { - t.Errorf("%v != %v", docker.ExtractContainerIPs(c.GetNode()), []string{"1.2.3.4"}) + if !reflect.DeepEqual(docker.ExtractContainerIPs(c.GetNode([]net.IP{})), []string{"1.2.3.4"}) { + t.Errorf("%v != %v", docker.ExtractContainerIPs(c.GetNode([]net.IP{})), []string{"1.2.3.4"}) } } diff --git a/probe/docker/registry_test.go b/probe/docker/registry_test.go index 42559f2e0..dae59098c 100644 --- a/probe/docker/registry_test.go +++ b/probe/docker/registry_test.go @@ -1,6 +1,7 @@ package docker_test import ( + "net" "runtime" "sort" "sync" @@ -36,7 +37,7 @@ func (c *mockContainer) StartGatheringStats() error { func (c *mockContainer) StopGatheringStats() {} -func (c *mockContainer) GetNode() report.Node { +func (c *mockContainer) GetNode(_ []net.IP) report.Node { return report.MakeNodeWith(map[string]string{ docker.ContainerID: c.c.ID, docker.ContainerName: c.c.Name, diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index 38f64c059..5e7b0053d 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -1,6 +1,8 @@ package docker import ( + "net" + docker_client "github.com/fsouza/go-dockerclient" "github.com/weaveworks/scope/report" @@ -28,18 +30,23 @@ func NewReporter(registry Registry, hostID string) *Reporter { // Report generates a Report containing Container and ContainerImage topologies func (r *Reporter) Report() (report.Report, error) { + localAddrs, err := report.LocalAddresses() + if err != nil { + return report.MakeReport(), nil + } + result := report.MakeReport() - result.Container = result.Container.Merge(r.containerTopology()) + result.Container = result.Container.Merge(r.containerTopology(localAddrs)) result.ContainerImage = result.ContainerImage.Merge(r.containerImageTopology()) return result, nil } -func (r *Reporter) containerTopology() report.Topology { +func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { result := report.MakeTopology() r.registry.WalkContainers(func(c Container) { nodeID := report.MakeContainerNodeID(r.hostID, c.ID()) - result.AddNode(nodeID, c.GetNode()) + result.AddNode(nodeID, c.GetNode(localAddrs)) }) return result diff --git a/render/mapping.go b/render/mapping.go index fd8d33740..c5bb3e0d7 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -3,6 +3,7 @@ package render import ( "fmt" "net" + "regexp" "strconv" "strings" @@ -240,7 +241,8 @@ func MapHostIdentity(m RenderableNode, _ report.Networks) RenderableNodes { // will be joined to containers through the process topology, and we // don't want to double count edges. func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes { - _, ok := m.Metadata[process.PID] + // Don't include procspied connections, to prevent double counting + _, ok := m.Metadata[endpoint.Procspied] if ok { return RenderableNodes{} } @@ -251,9 +253,20 @@ func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes { if !local.Contains(net.ParseIP(addr)) { return RenderableNodes{TheInternetID: newDerivedPseudoNode(TheInternetID, TheInternetMajor, m)} } - return RenderableNodes{addr: NewRenderableNodeWith(addr, "", "", "", m)} + + result := RenderableNodes{addr: NewRenderableNodeWith(addr, "", "", "", m)} + // Emit addr:port nodes as well, so connections from the internet to containers + // via port mapping also works. + port, ok := m.Metadata[endpoint.Port] + if ok { + id := fmt.Sprintf("%s:%s", addr, port) + result[id] = NewRenderableNodeWith(id, "", "", "", m) + } + return result } +var portMappingMatch = regexp.MustCompile(`([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):([0-9]+)->([0-9]+)/tcp`) + // MapContainer2IP maps container nodes to their IP addresses (outputs // multiple nodes). This allows container to be joined directly with // the endpoint topology. @@ -264,10 +277,20 @@ func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes { return result } for _, addr := range strings.Fields(addrs) { - n := NewRenderableNodeWith(addr, "", "", "", m) - n.Node.Counters[containersKey] = 1 - result[addr] = n + node := NewRenderableNodeWith(addr, "", "", "", m) + node.Counters[containersKey] = 1 + result[addr] = node } + + // also output all the host:port port mappings + for _, mapping := range portMappingMatch.FindAllStringSubmatch(m.Metadata[docker.ContainerPorts], -1) { + ip, port := mapping[1], mapping[2] + id := fmt.Sprintf("%s:%s", ip, port) + node := NewRenderableNodeWith(id, "", "", "", m) + node.Counters[containersKey] = 1 + result[id] = node + } + return result } diff --git a/render/short_lived_connections_test.go b/render/short_lived_connections_test.go new file mode 100644 index 000000000..b5d6b1a93 --- /dev/null +++ b/render/short_lived_connections_test.go @@ -0,0 +1,94 @@ +package render_test + +import ( + "fmt" + "reflect" + "testing" + + "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/endpoint" + "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test" +) + +var ( + serverHostID = "host1" + serverHostNodeID = report.MakeHostNodeID(serverHostID) + + randomIP = "3.4.5.6" + randomPort = "56789" + randomEndpointNodeID = report.MakeEndpointNodeID(serverHostID, randomIP, randomPort) + + serverIP = "192.168.1.1" + serverPort = "80" + serverEndpointNodeID = report.MakeEndpointNodeID(serverHostID, serverIP, serverPort) + + containerID = "a1b2c3d4e5" + containerIP = "192.168.0.1" + containerName = "foo" + containerNodeID = report.MakeContainerNodeID(serverHostID, containerID) + + rpt = report.Report{ + Endpoint: report.Topology{ + Nodes: report.Nodes{ + randomEndpointNodeID: report.MakeNode().WithMetadata(map[string]string{ + endpoint.Addr: randomIP, + endpoint.Port: randomPort, + endpoint.Conntracked: "true", + }).WithAdjacent(serverEndpointNodeID), + + serverEndpointNodeID: report.MakeNode().WithMetadata(map[string]string{ + endpoint.Addr: serverIP, + endpoint.Port: serverPort, + endpoint.Conntracked: "true", + }), + }, + }, + Container: report.Topology{ + Nodes: report.Nodes{ + containerNodeID: report.MakeNode().WithMetadata(map[string]string{ + docker.ContainerID: containerID, + docker.ContainerName: containerName, + docker.ContainerIPs: containerIP, + docker.ContainerPorts: fmt.Sprintf("%s:%s->%s/tcp", serverIP, serverPort, serverPort), + report.HostNodeID: serverHostNodeID, + }), + }, + }, + Host: report.Topology{ + Nodes: report.Nodes{ + serverHostNodeID: report.MakeNodeWith(map[string]string{ + "local_networks": "192.168.0.0/16", + report.HostNodeID: serverHostNodeID, + }), + }, + }, + } + + want = (render.RenderableNodes{ + render.TheInternetID: { + ID: render.TheInternetID, + LabelMajor: render.TheInternetMajor, + Pseudo: true, + Node: report.MakeNode().WithAdjacent(containerID), + Origins: report.MakeIDList(randomEndpointNodeID), + }, + containerID: { + ID: containerID, + LabelMajor: containerName, + LabelMinor: serverHostID, + Rank: "", + Pseudo: false, + Origins: report.MakeIDList(containerNodeID, serverEndpointNodeID, serverHostNodeID), + Node: report.MakeNode(), + }, + }).Prune() +) + +func TestShortLivedInternetNodeConnections(t *testing.T) { + have := (render.ContainerWithImageNameRenderer.Render(rpt)).Prune() + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} diff --git a/report/networks.go b/report/networks.go index a1aa9e6e4..42efd5e4b 100644 --- a/report/networks.go +++ b/report/networks.go @@ -2,6 +2,7 @@ package report import ( "net" + "strings" ) // Networks represent a set of subnets @@ -29,6 +30,40 @@ func (n Networks) Contains(ip net.IP) bool { return false } +// LocalAddresses returns a list of the local IP addresses. +func LocalAddresses() ([]net.IP, error) { + result := []net.IP{} + + infs, err := net.Interfaces() + if err != nil { + return []net.IP{}, err + } + + for _, inf := range infs { + if strings.HasPrefix(inf.Name, "veth") || + strings.HasPrefix(inf.Name, "docker") || + strings.HasPrefix(inf.Name, "lo") { + continue + } + + addrs, err := inf.Addrs() + if err != nil { + return []net.IP{}, err + } + + for _, addr := range addrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + continue + } + + result = append(result, ipnet.IP) + } + } + + return result, nil +} + // AddLocalBridge records the subnet address associated with the bridge name // supplied, such that MakeAddressNodeID will scope addresses in this subnet // as local.