diff --git a/probe/endpoint/connection_tracker.go b/probe/endpoint/connection_tracker.go index 1ab979c489..58d7852714 100644 --- a/probe/endpoint/connection_tracker.go +++ b/probe/endpoint/connection_tracker.go @@ -219,23 +219,33 @@ func (t *connectionTracker) addConnection(rpt *report.Report, incoming bool, ft ) rpt.Endpoint.AddNode(fromNode.WithAdjacent(toNode.ID)) rpt.Endpoint.AddNode(toNode) + t.addDNS(rpt, ft.fromAddr) + t.addDNS(rpt, ft.toAddr) } func (t *connectionTracker) makeEndpointNode(namespaceID string, addr string, port uint16, extra map[string]string) report.Node { portStr := strconv.Itoa(int(port)) node := report.MakeNodeWith(report.MakeEndpointNodeID(t.conf.HostID, namespaceID, addr, portStr), nil) - if names := t.conf.DNSSnooper.CachedNamesForIP(addr); len(names) > 0 { - node = node.WithSet(SnoopedDNSNames, report.MakeStringSet(names...)) - } - if names, err := t.reverseResolver.get(addr); err == nil && len(names) > 0 { - node = node.WithSet(ReverseDNSNames, report.MakeStringSet(names...)) - } if extra != nil { node = node.WithLatests(extra) } return node } +// Add DNS record for address to report, if not already there +func (t *connectionTracker) addDNS(rpt *report.Report, addr string) { + if _, found := rpt.DNS[addr]; !found { + forward := t.conf.DNSSnooper.CachedNamesForIP(addr) + record := report.DNSRecord{ + Forward: report.MakeStringSet(forward...), + } + if names, err := t.reverseResolver.get(addr); err == nil && len(names) > 0 { + record.Reverse = report.MakeStringSet(names...) + } + rpt.DNS[addr] = record + } +} + func (t *connectionTracker) Stop() error { if t.ebpfTracker != nil { t.ebpfTracker.stop() diff --git a/probe/endpoint/resolver.go b/probe/endpoint/resolver.go index 0f816d86e3..cbd533d1c3 100644 --- a/probe/endpoint/resolver.go +++ b/probe/endpoint/resolver.go @@ -41,8 +41,7 @@ func newReverseResolver() *reverseResolver { } // get the reverse resolution for an IP address if already in the cache, a -// gcache.NotFoundKeyError error otherwise. Note: it returns one of the -// possible names that can be obtained for that IP. +// gcache.NotFoundKeyError error otherwise. func (r *reverseResolver) get(address string) ([]string, error) { val, err := r.cache.Get(address) if hostnames, ok := val.([]string); err == nil && ok { diff --git a/render/detailed/connections.go b/render/detailed/connections.go index e41dbd4013..e7ac88fb5e 100644 --- a/render/detailed/connections.go +++ b/render/detailed/connections.go @@ -73,7 +73,7 @@ func newConnectionCounters() *connectionCounters { return &connectionCounters{counted: map[string]struct{}{}, counts: map[connection]int{}} } -func (c *connectionCounters) add(outgoing bool, localNode, remoteNode, localEndpoint, remoteEndpoint report.Node) { +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 @@ -94,10 +94,10 @@ func (c *connectionCounters) add(outgoing bool, localNode, remoteNode, localEndp return } // For internet nodes we break out individual addresses - if conn.remoteAddr, ok = internetAddr(remoteNode, remoteEndpoint); !ok { + if conn.remoteAddr, ok = internetAddr(dns, remoteNode, remoteEndpoint); !ok { return } - if conn.localAddr, ok = internetAddr(localNode, localEndpoint); !ok { + if conn.localAddr, ok = internetAddr(dns, localNode, localEndpoint); !ok { return } @@ -105,7 +105,7 @@ func (c *connectionCounters) add(outgoing bool, localNode, remoteNode, localEndp c.counts[conn]++ } -func internetAddr(node report.Node, ep report.Node) (string, bool) { +func internetAddr(dns report.DNSRecords, node report.Node, ep report.Node) (string, bool) { if !render.IsInternetNode(node) { return "", true } @@ -113,7 +113,7 @@ func internetAddr(node report.Node, ep report.Node) (string, bool) { if !ok { return "", false } - if name, found := render.DNSFirstMatch(ep, func(string) bool { return true }); found { + 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) @@ -171,7 +171,7 @@ func incomingConnectionsSummary(topologyID string, r report.Report, n report.Nod for _, remoteEndpoint := range endpointChildrenOf(node) { for _, localEndpointID := range remoteEndpoint.Adjacency.Intersection(localEndpointIDs) { localEndpointID = canonicalEndpointID(localEndpointIDCopies, localEndpointID) - counts.add(false, n, node, r.Endpoint.Nodes[localEndpointID], remoteEndpoint) + counts.add(r.DNS, false, n, node, r.Endpoint.Nodes[localEndpointID], remoteEndpoint) } } } @@ -203,7 +203,7 @@ func outgoingConnectionsSummary(topologyID string, r report.Report, n report.Nod for _, localEndpoint := range localEndpoints { for _, remoteEndpointID := range localEndpoint.Adjacency.Intersection(remoteEndpointIDs) { remoteEndpointID = canonicalEndpointID(remoteEndpointIDCopies, remoteEndpointID) - counts.add(true, n, node, localEndpoint, r.Endpoint.Nodes[remoteEndpointID]) + counts.add(r.DNS, true, n, node, localEndpoint, r.Endpoint.Nodes[remoteEndpointID]) } } } diff --git a/render/endpoint.go b/render/endpoint.go index 113ab7eaaf..2e610b1f83 100644 --- a/render/endpoint.go +++ b/render/endpoint.go @@ -36,7 +36,7 @@ func (e mapEndpoints) Render(rpt report.Report) Nodes { // Nodes without a hostid are mapped to pseudo nodes, if // possible. if _, ok := n.Latest.Lookup(report.HostNodeID); !ok { - if id, ok := pseudoNodeID(n, local); ok { + if id, ok := pseudoNodeID(rpt, n, local); ok { ret.addChild(n, id, Pseudo) continue } diff --git a/render/id.go b/render/id.go index 3b8eda0a69..72ca6feb92 100644 --- a/render/id.go +++ b/render/id.go @@ -3,7 +3,6 @@ package render import ( "strings" - "github.com/weaveworks/scope/probe/endpoint" "github.com/weaveworks/scope/report" ) @@ -62,13 +61,13 @@ func NewDerivedPseudoNode(id string, node report.Node) report.Node { return output } -func pseudoNodeID(n report.Node, local report.Networks) (string, bool) { +func pseudoNodeID(rpt report.Report, n report.Node, local report.Networks) (string, bool) { _, addr, _, ok := report.ParseEndpointNodeID(n.ID) if !ok { return "", false } - if id, ok := externalNodeID(n, addr, local); ok { + if id, ok := externalNodeID(rpt, n, addr, local); ok { return id, ok } @@ -78,11 +77,11 @@ func pseudoNodeID(n report.Node, local report.Networks) (string, bool) { } // figure out if a node should be considered external and returns an ID which can be used to create a pseudo node -func externalNodeID(n report.Node, addr string, local report.Networks) (string, bool) { +func externalNodeID(rpt report.Report, n report.Node, addr string, local report.Networks) (string, bool) { // First, check if it's a known service and emit a a specific node if it // is. This needs to be done before checking IPs since known services can // live in the same network, see https://github.com/weaveworks/scope/issues/2163 - if hostname, found := DNSFirstMatch(n, isKnownService); found { + if hostname, found := rpt.DNS.FirstMatch(n.ID, isKnownService); found { return ServiceNodeIDPrefix + hostname, true } @@ -101,25 +100,3 @@ func externalNodeID(n report.Node, addr string, local report.Networks) (string, // The node is not external return "", false } - -// DNSFirstMatch returns the first DNS name where match() returns -// true, from a prioritized list of snooped and reverse-resolved DNS -// names associated with node n. -func DNSFirstMatch(n report.Node, match func(name string) bool) (string, bool) { - // we rely on Sets being sorted, to make selection for display more - // deterministic - // prioritize snooped names - snoopedNames, _ := n.Sets.Lookup(endpoint.SnoopedDNSNames) - for _, hostname := range snoopedNames { - if match(hostname) { - return hostname, true - } - } - reverseNames, _ := n.Sets.Lookup(endpoint.ReverseDNSNames) - for _, hostname := range reverseNames { - if match(hostname) { - return hostname, true - } - } - return "", false -} diff --git a/report/dns.go b/report/dns.go new file mode 100644 index 0000000000..a09ed0b449 --- /dev/null +++ b/report/dns.go @@ -0,0 +1,60 @@ +package report + +// DNSRecord contains names that an IP address maps to +type DNSRecord struct { + Forward StringSet `json:"forward,omitempty"` + Reverse StringSet `json:"reverse,omitempty"` +} + +// DNSRecords contains all address->name mappings for a report +type DNSRecords map[string]DNSRecord + +// Copy makes a copy of the DNSRecords +func (r DNSRecords) Copy() DNSRecords { + cp := make(DNSRecords, len(r)) + for k, v := range r { + cp[k] = v + } + return cp +} + +// Merge merges the other object into this one, and returns the result object. +// The original is not modified. +func (r DNSRecords) Merge(other DNSRecords) DNSRecords { + if len(other) > len(r) { + r, other = other, r + } + cp := r.Copy() + for k, v := range other { + if v2, ok := cp[k]; ok { + cp[k] = DNSRecord{ + Forward: v.Forward.Merge(v2.Forward), + Reverse: v.Reverse.Merge(v2.Reverse), + } + } else { + cp[k] = v + } + } + return cp +} + +// FirstMatch returns the first DNS name where match() returns true +func (r DNSRecords) FirstMatch(id string, match func(name string) bool) (string, bool) { + _, addr, _, ok := ParseEndpointNodeID(id) + if !ok { + return "", false + } + // we rely on StringSets being sorted, to make selection deterministic + // prioritize forward names + for _, hostname := range r[addr].Forward { + if match(hostname) { + return hostname, true + } + } + for _, hostname := range r[addr].Reverse { + if match(hostname) { + return hostname, true + } + } + return "", false +} diff --git a/report/report.go b/report/report.go index 2cac43de1d..759ad2b477 100644 --- a/report/report.go +++ b/report/report.go @@ -151,6 +151,8 @@ type Report struct { // their status endpoints. Edges are present. Overlay Topology + DNS DNSRecords + // Sampling data for this report. Sampling Sampling @@ -242,6 +244,8 @@ func MakeReport() Report { WithShape(Heptagon). WithLabel("service", "services"), + DNS: DNSRecords{}, + Sampling: Sampling{}, Window: 0, Plugins: xfer.MakePluginSpecs(), @@ -252,6 +256,7 @@ func MakeReport() Report { // Copy returns a value copy of the report. func (r Report) Copy() Report { newReport := Report{ + DNS: r.DNS.Copy(), Sampling: r.Sampling, Window: r.Window, Plugins: r.Plugins.Copy(), @@ -267,6 +272,7 @@ func (r Report) Copy() Report { // original is not modified. func (r Report) Merge(other Report) Report { newReport := r.Copy() + newReport.DNS = newReport.DNS.Merge(other.DNS) newReport.Sampling = newReport.Sampling.Merge(other.Sampling) newReport.Window = newReport.Window + other.Window newReport.Plugins = newReport.Plugins.Merge(other.Plugins) @@ -371,7 +377,7 @@ func (r Report) Validate() error { // // This for now creates node's LatestControls from Controls. func (r Report) Upgrade() Report { - return r.upgradeLatestControls().upgradePodNodes().upgradeNamespaces() + return r.upgradeLatestControls().upgradePodNodes().upgradeNamespaces().upgradeDNSRecords() } func (r Report) upgradeLatestControls() Report { @@ -469,6 +475,33 @@ func (r Report) upgradeNamespaces() Report { return r } +func (r Report) upgradeDNSRecords() Report { + if len(r.DNS) > 0 { + return r + } + dns := make(DNSRecords) + for endpointID, endpoint := range r.Endpoint.Nodes { + _, addr, _, ok := ParseEndpointNodeID(endpointID) + snoopedNames, foundS := endpoint.Sets.Lookup(SnoopedDNSNames) + reverseNames, foundR := endpoint.Sets.Lookup(ReverseDNSNames) + if ok && (foundS || foundR) { + // Add address and names to report-level map + if existing, found := dns[addr]; found { + // Optimise the expected case that they are equal + if existing.Forward.Equal(snoopedNames) && existing.Reverse.Equal(reverseNames) { + continue + } + // Not equal - merge this node's data into existing data, + snoopedNames = snoopedNames.Merge(existing.Forward) + reverseNames = reverseNames.Merge(existing.Reverse) + } + dns[addr] = DNSRecord{Forward: snoopedNames, Reverse: reverseNames} + } + } + r.DNS = dns + return r +} + // BackwardCompatible returns a new backward-compatible report. // // This for now creates node's Controls from LatestControls. diff --git a/report/string_set.go b/report/string_set.go index d3ba3645b9..c20014e314 100644 --- a/report/string_set.go +++ b/report/string_set.go @@ -50,6 +50,19 @@ func (s StringSet) Intersection(b StringSet) StringSet { return result } +// Equal returns true if a and b have the same contents +func (s StringSet) Equal(b StringSet) bool { + if len(s) != len(b) { + return false + } + for i := range s { + if s[i] != b[i] { + return false + } + } + return true +} + // Add adds the strings to the StringSet. Add is the only valid way to grow a // StringSet. Add returns the StringSet to enable chaining. func (s StringSet) Add(strs ...string) StringSet {