package detailed import ( "fmt" "sort" "strconv" "github.com/weaveworks/scope/probe/endpoint" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) const ( portKey = "port" portLabel = "Port" countKey = "count" countLabel = "Count" remoteKey = "remote" remoteLabel = "Remote" number = "number" ) // Exported for testing var ( NormalColumns = []Column{ {ID: portKey, Label: portLabel, Datatype: report.Number}, {ID: countKey, Label: countLabel, Datatype: report.Number, DefaultSort: true}, } InternetColumns = []Column{ {ID: remoteKey, Label: remoteLabel}, {ID: portKey, Label: portLabel, Datatype: report.Number}, {ID: countKey, Label: countLabel, Datatype: report.Number, DefaultSort: true}, } ) // ConnectionsSummary is the table of connection to/form a node type ConnectionsSummary struct { ID string `json:"id"` TopologyID string `json:"topologyId"` Label string `json:"label"` Columns []Column `json:"columns"` Connections []Connection `json:"connections"` } // Connection is a row in the connections table. type Connection struct { ID string `json:"id"` // ID of this element in the UI. Must be unique for a given ConnectionsSummary. NodeID string `json:"nodeId"` // ID of a node in the topology. Optional, must be set if linkable is true. Label string `json:"label"` LabelMinor string `json:"labelMinor,omitempty"` Metadata []report.MetadataRow `json:"metadata,omitempty"` } type connectionsByID []Connection func (s connectionsByID) Len() int { return len(s) } func (s connectionsByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s connectionsByID) Less(i, j int) bool { return s[i].ID < s[j].ID } // Intermediate type used as a key to dedupe rows type connection struct { remoteNodeID string remoteAddr, localAddr string // for internet nodes only port string // destination port } type connectionCounters struct { counted map[string]struct{} counts map[connection]int } func newConnectionCounters() *connectionCounters { return &connectionCounters{counted: map[string]struct{}{}, counts: map[connection]int{}} } func (c *connectionCounters) add(dns report.DNSRecords, outgoing bool, localNode, remoteNode, localEndpoint, remoteEndpoint report.Node) { // We identify connections by their source endpoint, pre-NAT, to // ensure we only count them once. srcEndpoint, dstEndpoint := remoteEndpoint, localEndpoint if outgoing { srcEndpoint, dstEndpoint = localEndpoint, remoteEndpoint } connectionID := srcEndpoint.ID if copySrcEndpointID, _, ok := srcEndpoint.Latest.LookupEntry(endpoint.CopyOf); ok { connectionID = copySrcEndpointID } if _, ok := c.counted[connectionID]; ok { return } conn := connection{remoteNodeID: remoteNode.ID} var ok bool if _, _, conn.port, ok = report.ParseEndpointNodeID(dstEndpoint.ID); !ok { return } // For internet nodes we break out individual addresses if conn.remoteAddr, ok = internetAddr(dns, remoteNode, remoteEndpoint); !ok { return } if conn.localAddr, ok = internetAddr(dns, localNode, localEndpoint); !ok { return } count := 1 if countStr, _, ok := srcEndpoint.Latest.LookupEntry(report.ConnectionCount); ok { if i, err := strconv.Atoi(countStr); err == nil { count = i } } c.counted[connectionID] = struct{}{} c.counts[conn] += count } func internetAddr(dns report.DNSRecords, node report.Node, ep report.Node) (string, bool) { if !render.IsInternetNode(node) { return "", true } _, addr, _, ok := report.ParseEndpointNodeID(ep.ID) if !ok { return "", false } if name, found := dns.FirstMatch(ep.ID, func(string) bool { return true }); found { // we show the "most important" name only, since we don't have // space for more addr = fmt.Sprintf("%s (%s)", name, addr) } return addr, true } func (c *connectionCounters) rows(r report.Report, ns report.Nodes, includeLocal bool) []Connection { output := []Connection{} for row, count := range c.counts { // Use MakeBasicNodeSummary to render the id and label of this node summary, _ := MakeBasicNodeSummary(r, ns[row.remoteNodeID]) connection := Connection{ ID: fmt.Sprintf("%s-%s-%s-%s", row.remoteNodeID, row.remoteAddr, row.localAddr, row.port), NodeID: summary.ID, Label: summary.Label, LabelMinor: summary.LabelMinor, } if row.remoteAddr != "" { connection.Label = row.remoteAddr connection.LabelMinor = "" } if includeLocal { connection.Metadata = append(connection.Metadata, report.MetadataRow{ ID: remoteKey, Value: row.localAddr, }) } connection.Metadata = append(connection.Metadata, report.MetadataRow{ ID: portKey, Value: row.port, }, report.MetadataRow{ ID: countKey, Value: strconv.Itoa(count), }, ) output = append(output, connection) } sort.Sort(connectionsByID(output)) return output } func incomingConnectionsSummary(topologyID string, r report.Report, n report.Node, ns report.Nodes) ConnectionsSummary { localEndpointIDs, localEndpointIDCopies := endpointChildIDsAndCopyMapOf(n) counts := newConnectionCounters() // For each node which has an edge TO me for _, node := range ns { if !node.Adjacency.Contains(n.ID) { continue } for _, remoteEndpoint := range endpointChildrenOf(node) { for _, localEndpointID := range remoteEndpoint.Adjacency.Intersection(localEndpointIDs) { localEndpointID = canonicalEndpointID(localEndpointIDCopies, localEndpointID) counts.add(r.DNS, false, n, node, r.Endpoint.Nodes[localEndpointID], remoteEndpoint) } } } columnHeaders := NormalColumns if render.IsInternetNode(n) { columnHeaders = InternetColumns } return ConnectionsSummary{ ID: "incoming-connections", TopologyID: topologyID, Label: "Inbound", Columns: columnHeaders, Connections: counts.rows(r, ns, render.IsInternetNode(n)), } } func outgoingConnectionsSummary(topologyID string, r report.Report, n report.Node, ns report.Nodes) ConnectionsSummary { localEndpoints := endpointChildrenOf(n) counts := newConnectionCounters() // For each node which has an edge FROM me for _, id := range n.Adjacency { node, ok := ns[id] if !ok { continue } remoteEndpointIDs, remoteEndpointIDCopies := endpointChildIDsAndCopyMapOf(node) for _, localEndpoint := range localEndpoints { for _, remoteEndpointID := range localEndpoint.Adjacency.Intersection(remoteEndpointIDs) { remoteEndpointID = canonicalEndpointID(remoteEndpointIDCopies, remoteEndpointID) counts.add(r.DNS, true, n, node, localEndpoint, r.Endpoint.Nodes[remoteEndpointID]) } } } columnHeaders := NormalColumns if render.IsInternetNode(n) { columnHeaders = InternetColumns } return ConnectionsSummary{ ID: "outgoing-connections", TopologyID: topologyID, Label: "Outbound", Columns: columnHeaders, Connections: counts.rows(r, ns, render.IsInternetNode(n)), } } func endpointChildrenOf(n report.Node) []report.Node { result := []report.Node{} n.Children.ForEach(func(child report.Node) { if child.Topology == report.Endpoint { result = append(result, child) } }) return result } func endpointChildIDsAndCopyMapOf(n report.Node) (report.IDList, map[string]string) { ids := report.MakeIDList() copies := map[string]string{} n.Children.ForEach(func(child report.Node) { if child.Topology == report.Endpoint { ids = ids.Add(child.ID) if copyID, _, ok := child.Latest.LookupEntry(endpoint.CopyOf); ok { copies[child.ID] = copyID } } }) return ids, copies } // canonicalEndpointID returns the original endpoint ID of which id is // a "copy_of" (due to NATing), or, if the id is not a copy, the id // itself. // // This is used for determining a unique destination endpoint ID for a // connection, removing any arbitrariness in the destination port we // are associating with the connection when it is encountered multiple // times in the topology (with different destination endpoints, due to // DNATing). func canonicalEndpointID(copies map[string]string, id string) string { if original, ok := copies[id]; ok { return original } return id }