From 5c080ec06267b03bffd9274a148e350e47659314 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Wed, 14 Sep 2016 07:04:21 +0000 Subject: [PATCH 1/6] Add pseudo-nodes for known services AWS S3 and Dynamo to start with --- render/container.go | 6 +++--- render/detailed/summary.go | 11 +++++++++++ render/filters.go | 4 ++-- render/process.go | 15 ++++++++++++--- render/theinternet.go | 27 +++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/render/container.go b/render/container.go index c4bab4fc5..3a2705b32 100644 --- a/render/container.go +++ b/render/container.go @@ -74,8 +74,8 @@ func ShortLivedConnectionJoin(r Renderer, toIPs func(report.Node) []string) Rend return report.Nodes{} } - // Propagate the internet pseudo node - if strings.HasSuffix(n.ID, TheInternetID) { + // Propagate the internet and service pseudo nodes + if strings.HasSuffix(n.ID, TheInternetID) || strings.HasPrefix(n.ID, ServiceNodeIDPrefix) { return report.Nodes{n.ID: n} } @@ -112,7 +112,7 @@ func ShortLivedConnectionJoin(r Renderer, toIPs func(report.Node) []string) Rend return report.Nodes{} } if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { - node := theInternetNode(m) + node := toInternetNode(m) return report.Nodes{node.ID: node} } diff --git a/render/detailed/summary.go b/render/detailed/summary.go index ae95a3954..3f68e2fad 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -115,6 +115,7 @@ func pseudoNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { base.Pseudo = true base.Rank = n.ID + // try rendering as an internet node if template, ok := templates[n.ID]; ok { base.Label = template.Label base.LabelMinor = template.LabelMinor @@ -122,6 +123,16 @@ func pseudoNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { return base, true } + // try rendering as a known service node + if strings.HasPrefix(n.ID, render.ServiceNodeIDPrefix) { + serviceID := n.ID[len(render.ServiceNodeIDPrefix):] + base.Label = render.KnownServicesForHumans[serviceID] + base.LabelMinor = "" + // TODO: use custom icons for known services? + base.Shape = report.Cloud + return base, true + } + // try rendering it as an uncontained node if strings.HasPrefix(n.ID, render.MakePseudoNodeID(render.UncontainedID)) { base.Label = render.UncontainedMajor diff --git a/render/filters.go b/render/filters.go index 82e2fc2a1..347ca36f3 100644 --- a/render/filters.go +++ b/render/filters.go @@ -245,9 +245,9 @@ func IsApplication(n report.Node) bool { var IsSystem = Complement(IsApplication) // IsNotPseudo returns true if the node is not a pseudo node -// or the internet nodes. +// or internet/service nodes. func IsNotPseudo(n report.Node) bool { - return n.Topology != Pseudo || strings.HasSuffix(n.ID, TheInternetID) + return n.Topology != Pseudo || strings.HasSuffix(n.ID, TheInternetID) || strings.HasPrefix(n.ID, ServiceNodeIDPrefix) } // IsNamespace checks if the node is a pod/service in the specified namespace diff --git a/render/process.go b/render/process.go index c756fa2c3..0e81261f1 100644 --- a/render/process.go +++ b/render/process.go @@ -96,8 +96,8 @@ func MapEndpoint2Pseudo(n report.Node, local report.Networks) report.Nodes { if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { // If the dstNodeAddr is not in a network local to this report, we emit an - // internet node - node = theInternetNode(n) + // external pseudoNode + node = toInternetNode(n) } else { // due to https://github.com/weaveworks/scope/issues/1323 we are dropping // all non-internet pseudo nodes for now. @@ -157,7 +157,16 @@ func MapProcess2Name(n report.Node, _ report.Networks) report.Nodes { return report.Nodes{name: node} } -func theInternetNode(m report.Node) report.Node { +func toInternetNode(m report.Node) report.Node { + // First, check if it's a known service and emit a + // a specific node if it is + hostnames, _ := m.Sets.Lookup(endpoint.ReverseDNSNames) + for _, hostname := range hostnames { + if serviceID, ok := lookupKnownService(hostname); ok { + return NewDerivedPseudoNode(ServiceNodeIDPrefix+serviceID, m) + } + } + // emit one internet node for incoming, one for outgoing if len(m.Adjacency) > 0 { return NewDerivedPseudoNode(IncomingInternetID, m) diff --git a/render/theinternet.go b/render/theinternet.go index 6a3c13739..cb24dcc55 100644 --- a/render/theinternet.go +++ b/render/theinternet.go @@ -2,11 +2,38 @@ package render import ( "net" + "regexp" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" ) +var ( + // ServiceNodeIDPrefix is how the ID all service pseudo nodes begin + ServiceNodeIDPrefix = "service-" + + // KnownServicesForHumans contains a human-readable format of the service Ids + KnownServicesForHumans = map[string]string{ + "aws-dynamo": "AWS Dynamo", + "aws-s3": "AWS S3", + } + + // Correspondence between hostnames and the service id they are part of + knownServicesMatchers = map[*regexp.Regexp]string{ + regexp.MustCompile(`dynamodb.[^.]+.amazonaws.com`): "aws-dynamo", + regexp.MustCompile(`s3-[^.]+.amazonaws.com`): "aws-s3", + } +) + +func lookupKnownService(hostname string) (string, bool) { + for re, id := range knownServicesMatchers { + if re.MatchString(hostname) { + return id, true + } + } + return "", false +} + // LocalNetworks returns a superset of the networks (think: CIDRs) that are // "local" from the perspective of each host represented in the report. It's // used to determine which nodes in the report are "remote", i.e. outside of From 0223b0b3859e296b62681fc2fdb28f1c9deac5ae Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Wed, 14 Sep 2016 18:05:37 +0000 Subject: [PATCH 2/6] Implement DNS snooper --- probe/endpoint/dns_snooper.go | 215 ++++++++++++++++++++++++++++++++++ probe/endpoint/reporter.go | 12 +- prog/probe.go | 10 +- 3 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 probe/endpoint/dns_snooper.go diff --git a/probe/endpoint/dns_snooper.go b/probe/endpoint/dns_snooper.go new file mode 100644 index 000000000..3af147fd0 --- /dev/null +++ b/probe/endpoint/dns_snooper.go @@ -0,0 +1,215 @@ +package endpoint + +import ( + "bytes" + "fmt" + "math" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/bluele/gcache" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" +) + +const ( + bufSize = 8 * 1024 * 1024 // 8MB + maxReverseDNSrecords = 10000 +) + +// DNSSnooper is a snopper of DNS queries +type DNSSnooper struct { + stop chan struct{} + pcapHandle *pcap.Handle + reverseDNSCache gcache.Cache +} + +// NewDNSSnooper creates a new snooper of DNS queries +func NewDNSSnooper() (*DNSSnooper, error) { + pcapHandle, err := newPcapHandle() + if err != nil { + return nil, err + } + reverseDNSCache := gcache.New(maxReverseDNSrecords).LRU().Build() + + s := &DNSSnooper{ + stop: make(chan struct{}), + pcapHandle: pcapHandle, + reverseDNSCache: reverseDNSCache, + } + go s.run() + return s, nil +} + +func newPcapHandle() (*pcap.Handle, error) { + // TODO: use specific interfaces instead? + inactive, err := pcap.NewInactiveHandle("any") + if err != nil { + return nil, err + } + defer inactive.CleanUp() + if err = inactive.SetPromisc(true); err != nil { + return nil, err + } + // TODO: reduce the size of packets being copied? maybe an overoptimization + // if err = inactive.SetSnapLen(snaplen); err != nil { + // return + // } + + // pcap timeout blackmagic copied from Weave Net to reduce CPU consumption + // see https://github.com/weaveworks/weave/commit/025315363d5ea8b8265f1b3ea800f24df2be51a4 + if err = inactive.SetTimeout(time.Duration(math.MaxInt64)); err != nil { + return nil, err + } + if err = inactive.SetImmediateMode(true); err != nil { + // If gopacket is compiled against an older pcap.h that + // doesn't have pcap_set_immediate_mode, it supplies a dummy + // definition that always returns PCAP_ERROR. That becomes + // "Generic error", which is not very helpful. The real + // pcap_set_immediate_mode never returns PCAP_ERROR, so this + // turns it into a more informative message. + if fmt.Sprint(err) == "Generic error" { + return nil, fmt.Errorf("compiled against an old version of libpcap; please compile against libpcap-1.5.0 or later") + } + + return nil, err + } + if err = inactive.SetBufferSize(bufSize); err != nil { + return nil, err + } + pcapHandle, err := inactive.Activate() + if err != nil { + return nil, err + } + if err := pcapHandle.SetDirection(pcap.DirectionIn); err != nil { + pcapHandle.Close() + return nil, err + } + if err := pcapHandle.SetBPFFilter("inbound and port 53"); err != nil { + pcapHandle.Close() + return nil, err + } + + return pcapHandle, nil +} + +// CachedNamesForIP obtains the domains associated to an IP, +// obtained while snooping A-record queries +func (s *DNSSnooper) CachedNamesForIP(ip string) []string { + result := []string{} + + domains, err := s.reverseDNSCache.Get(ip) + if err != nil { + return result + } + + for domain := range domains.(map[string]struct{}) { + result = append(result, domain) + } + + return result +} + +// Stop makes the snooper stop inspecting DNS communications +func (s *DNSSnooper) Stop() { + if s != nil { + close(s.stop) + } +} + +func (s *DNSSnooper) run() { + var ( + decodedLayers []gopacket.LayerType + dns layers.DNS + udp layers.UDP + tcp layers.TCP + ip4 layers.IPv4 + ip6 layers.IPv6 + eth layers.Ethernet + sll layers.LinuxSLL + ) + + // assumes that the "any" interface in being used (see https://wiki.wireshark.org/SLL) + packetParser := gopacket.NewDecodingLayerParser(layers.LayerTypeLinuxSLL, &sll, ð, &ip4, &ip6, &udp, &tcp, &dns) + + for { + select { + case <-s.stop: + s.pcapHandle.Close() + return + default: + } + + packet, _, err := s.pcapHandle.ZeroCopyReadPacketData() + if err != nil { + // TimeoutExpired is acceptable due to the Timeout black magic + // on the handle + if err != pcap.NextErrorTimeoutExpired { + log.Errorf("DNSSnooper: error reading packet data: %s", err) + } + continue + } + + if err := packetParser.DecodeLayers(packet, &decodedLayers); err != nil { + log.Errorf("DNSSnooper: error decoding packet: %s", err) + continue + } + + for _, layerType := range decodedLayers { + if layerType == layers.LayerTypeDNS { + s.processDNSMessage(&dns) + } + } + } +} + +func (s *DNSSnooper) processDNSMessage(dns *layers.DNS) { + + // Only consider responses to singleton, A-record questions + if !dns.QR || dns.ResponseCode != 0 || len(dns.Questions) != 1 { + return + } + question := dns.Questions[0] + if question.Type != layers.DNSTypeA || question.Class != layers.DNSClassIN { + return + } + + var ( + domainQueried = question.Name + records = append(dns.Answers, dns.Additionals...) + ips = map[string]struct{}{} + alias []byte + ) + + // Traverse records for a CNAME first since the DNS RFCs don't seem to guarantee it + // appearing before its A-records + for _, record := range records { + if record.Type == layers.DNSTypeCNAME && record.Class == layers.DNSClassIN && bytes.Equal(domainQueried, record.Name) { + alias = record.CNAME + break + } + } + + // Finally, get the answer + for _, record := range records { + if record.Type != layers.DNSTypeA || record.Class != layers.DNSClassIN { + continue + } + if bytes.Equal(domainQueried, record.Name) || (alias != nil && bytes.Equal(alias, record.Name)) { + ips[record.IP.String()] = struct{}{} + } + } + + log.Debugf("DNSSnooper: caught DNS lookup: %s -> %v", string(domainQueried), ips) + // Update cache + // TODO: Be smarter about the expiration of IPs with multiple reverse-domains + newDomain := string(domainQueried) + for ip := range ips { + if existingDomains, err := s.reverseDNSCache.Get(ip); err != nil { + existingDomains.(map[string]struct{})[newDomain] = struct{}{} + } else { + s.reverseDNSCache.Set(ip, map[string]struct{}{newDomain: {}}) + } + } +} diff --git a/probe/endpoint/reporter.go b/probe/endpoint/reporter.go index a9d61a6b9..58d59e50e 100644 --- a/probe/endpoint/reporter.go +++ b/probe/endpoint/reporter.go @@ -33,6 +33,7 @@ type Reporter struct { scanner procspy.ConnectionScanner natMapper natMapper reverseResolver *reverseResolver + dnsSnooper *DNSSnooper } // SpyDuration is an exported prometheus metric @@ -52,7 +53,7 @@ var SpyDuration = prometheus.NewSummaryVec( // on the host machine, at the granularity of host and port. That information // is stored in the Endpoint topology. It optionally enriches that topology // with process (PID) information. -func NewReporter(hostID, hostName string, spyProcs, useConntrack, walkProc bool, procRoot string, scanner procspy.ConnectionScanner) *Reporter { +func NewReporter(hostID, hostName string, spyProcs, useConntrack, walkProc bool, procRoot string, scanner procspy.ConnectionScanner, dnsSnooper *DNSSnooper) *Reporter { return &Reporter{ hostID: hostID, hostName: hostName, @@ -62,6 +63,7 @@ func NewReporter(hostID, hostName string, spyProcs, useConntrack, walkProc bool, natMapper: makeNATMapper(newConntrackFlowWalker(useConntrack, procRoot, "--any-nat")), reverseResolver: newReverseResolver(), scanner: scanner, + dnsSnooper: dnsSnooper, } } @@ -193,9 +195,11 @@ func (r *Reporter) makeEndpointNode(namespaceID string, addr string, port uint16 node := report.MakeNodeWith( report.MakeEndpointNodeID(r.hostID, namespaceID, addr, portStr), map[string]string{Addr: addr, Port: portStr}) - // In case we have a reverse resolution for the IP, we can use it for - // the name... - if names, err := r.reverseResolver.get(addr); err == nil { + names := r.dnsSnooper.CachedNamesForIP(addr) + if resolvedNames, err := r.reverseResolver.get(addr); err == nil { + append(names, resolvedNames) + } + if len(names) > 0 { node = node.WithSet(ReverseDNSNames, report.MakeStringSet(names...)) } if extra != nil { diff --git a/prog/probe.go b/prog/probe.go index 8562a5d8d..c3ba71496 100644 --- a/prog/probe.go +++ b/prog/probe.go @@ -145,7 +145,15 @@ func probeMain(flags probeFlags) { p.AddReporter(process.NewReporter(processCache, hostID, process.GetDeltaTotalJiffies)) } - endpointReporter := endpoint.NewReporter(hostID, hostName, flags.spyProcs, flags.useConntrack, flags.procEnabled, flags.procRoot, scanner) + // TODO: make the snooper optional + dnsSnooper, err := endpoint.NewDNSSnooper() + if err != nil { + log.Errorf("Fail to start DNS snooper: nodes for external services will be less accurate: %s", err) + } else { + defer dnsSnooper.Stop() + } + + endpointReporter := endpoint.NewReporter(hostID, hostName, flags.spyProcs, flags.useConntrack, flags.procEnabled, flags.procRoot, scanner, dnsSnooper) defer endpointReporter.Stop() p.AddReporter(endpointReporter) From 028ed32b6f6d8900e1fbdbf78d364d789130df1b Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 16 Sep 2016 13:48:37 +0000 Subject: [PATCH 3/6] Simplify service node matching --- probe/endpoint/dns_snooper.go | 4 ++-- probe/endpoint/reporter.go | 2 +- render/detailed/summary.go | 4 +--- render/process.go | 4 ++-- render/theinternet.go | 26 +++++++++++--------------- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/probe/endpoint/dns_snooper.go b/probe/endpoint/dns_snooper.go index 3af147fd0..b07cc7367 100644 --- a/probe/endpoint/dns_snooper.go +++ b/probe/endpoint/dns_snooper.go @@ -207,9 +207,9 @@ func (s *DNSSnooper) processDNSMessage(dns *layers.DNS) { newDomain := string(domainQueried) for ip := range ips { if existingDomains, err := s.reverseDNSCache.Get(ip); err != nil { - existingDomains.(map[string]struct{})[newDomain] = struct{}{} - } else { s.reverseDNSCache.Set(ip, map[string]struct{}{newDomain: {}}) + } else { + existingDomains.(map[string]struct{})[newDomain] = struct{}{} } } } diff --git a/probe/endpoint/reporter.go b/probe/endpoint/reporter.go index 58d59e50e..544311236 100644 --- a/probe/endpoint/reporter.go +++ b/probe/endpoint/reporter.go @@ -197,7 +197,7 @@ func (r *Reporter) makeEndpointNode(namespaceID string, addr string, port uint16 map[string]string{Addr: addr, Port: portStr}) names := r.dnsSnooper.CachedNamesForIP(addr) if resolvedNames, err := r.reverseResolver.get(addr); err == nil { - append(names, resolvedNames) + names = append(names, resolvedNames...) } if len(names) > 0 { node = node.WithSet(ReverseDNSNames, report.MakeStringSet(names...)) diff --git a/render/detailed/summary.go b/render/detailed/summary.go index 3f68e2fad..f88a62a07 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -125,10 +125,8 @@ func pseudoNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { // try rendering as a known service node if strings.HasPrefix(n.ID, render.ServiceNodeIDPrefix) { - serviceID := n.ID[len(render.ServiceNodeIDPrefix):] - base.Label = render.KnownServicesForHumans[serviceID] + base.Label = n.ID[len(render.ServiceNodeIDPrefix):] base.LabelMinor = "" - // TODO: use custom icons for known services? base.Shape = report.Cloud return base, true } diff --git a/render/process.go b/render/process.go index 0e81261f1..bc344eba7 100644 --- a/render/process.go +++ b/render/process.go @@ -162,8 +162,8 @@ func toInternetNode(m report.Node) report.Node { // a specific node if it is hostnames, _ := m.Sets.Lookup(endpoint.ReverseDNSNames) for _, hostname := range hostnames { - if serviceID, ok := lookupKnownService(hostname); ok { - return NewDerivedPseudoNode(ServiceNodeIDPrefix+serviceID, m) + if isKnownService(hostname) { + return NewDerivedPseudoNode(ServiceNodeIDPrefix+hostname, m) } } diff --git a/render/theinternet.go b/render/theinternet.go index cb24dcc55..d32ed6fb3 100644 --- a/render/theinternet.go +++ b/render/theinternet.go @@ -2,7 +2,7 @@ package render import ( "net" - "regexp" + "strings" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" @@ -12,26 +12,22 @@ var ( // ServiceNodeIDPrefix is how the ID all service pseudo nodes begin ServiceNodeIDPrefix = "service-" - // KnownServicesForHumans contains a human-readable format of the service Ids - KnownServicesForHumans = map[string]string{ - "aws-dynamo": "AWS Dynamo", - "aws-s3": "AWS S3", - } - // Correspondence between hostnames and the service id they are part of - knownServicesMatchers = map[*regexp.Regexp]string{ - regexp.MustCompile(`dynamodb.[^.]+.amazonaws.com`): "aws-dynamo", - regexp.MustCompile(`s3-[^.]+.amazonaws.com`): "aws-s3", + knownServicesSuffixes = []string{ + // See http://docs.aws.amazon.com/general/latest/gr/rande.html for fainer grained + // details + "amazonaws.com", + "googleapis.com", } ) -func lookupKnownService(hostname string) (string, bool) { - for re, id := range knownServicesMatchers { - if re.MatchString(hostname) { - return id, true +func isKnownService(hostname string) bool { + for _, suffix := range knownServicesSuffixes { + if strings.HasSuffix(hostname, suffix) { + return true } } - return "", false + return false } // LocalNetworks returns a superset of the networks (think: CIDRs) that are From f5ad6a8e97bbac9b3a0ab25d489700c369b2d44c Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 16 Sep 2016 13:50:53 +0000 Subject: [PATCH 4/6] Minor fixes/refinements --- probe/endpoint/dns_snooper.go | 8 +++++--- probe/endpoint/reporter_test.go | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/probe/endpoint/dns_snooper.go b/probe/endpoint/dns_snooper.go index b07cc7367..11f5ce6b8 100644 --- a/probe/endpoint/dns_snooper.go +++ b/probe/endpoint/dns_snooper.go @@ -98,7 +98,9 @@ func newPcapHandle() (*pcap.Handle, error) { // obtained while snooping A-record queries func (s *DNSSnooper) CachedNamesForIP(ip string) []string { result := []string{} - + if s == nil { + return result + } domains, err := s.reverseDNSCache.Get(ip) if err != nil { return result @@ -201,14 +203,14 @@ func (s *DNSSnooper) processDNSMessage(dns *layers.DNS) { } } - log.Debugf("DNSSnooper: caught DNS lookup: %s -> %v", string(domainQueried), ips) // Update cache - // TODO: Be smarter about the expiration of IPs with multiple reverse-domains newDomain := string(domainQueried) + log.Debugf("DNSSnooper: caught DNS lookup: %s -> %v", newDomain, ips) for ip := range ips { if existingDomains, err := s.reverseDNSCache.Get(ip); err != nil { s.reverseDNSCache.Set(ip, map[string]struct{}{newDomain: {}}) } else { + // TODO: Be smarter about the expiration of entries with pre-existing associated domains existingDomains.(map[string]struct{})[newDomain] = struct{}{} } } diff --git a/probe/endpoint/reporter_test.go b/probe/endpoint/reporter_test.go index e62307266..7a6f4e948 100644 --- a/probe/endpoint/reporter_test.go +++ b/probe/endpoint/reporter_test.go @@ -69,7 +69,7 @@ func TestSpyNoProcesses(t *testing.T) { ) scanner := procspy.FixedScanner(fixConnections) - reporter := endpoint.NewReporter(nodeID, nodeName, false, false, false, "", scanner) + reporter := endpoint.NewReporter(nodeID, nodeName, false, false, false, "", scanner, nil) r, _ := reporter.Report() //buf, _ := json.MarshalIndent(r, "", " ") //t.Logf("\n%s\n", buf) @@ -86,7 +86,7 @@ func TestSpyWithProcesses(t *testing.T) { ) scanner := procspy.FixedScanner(fixConnectionsWithProcesses) - reporter := endpoint.NewReporter(nodeID, nodeName, true, false, true, "", scanner) + reporter := endpoint.NewReporter(nodeID, nodeName, true, false, true, "", scanner, nil) r, _ := reporter.Report() // buf, _ := json.MarshalIndent(r, "", " ") ; t.Logf("\n%s\n", buf) From 193bfec50b8fb932d1e3a039e0c202f638ebff1f Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 16 Sep 2016 16:00:47 +0000 Subject: [PATCH 5/6] Exclude ec2 since it's too generic --- render/theinternet.go | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/render/theinternet.go b/render/theinternet.go index d32ed6fb3..7546f93a2 100644 --- a/render/theinternet.go +++ b/render/theinternet.go @@ -2,7 +2,7 @@ package render import ( "net" - "strings" + "regexp" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" @@ -12,22 +12,38 @@ var ( // ServiceNodeIDPrefix is how the ID all service pseudo nodes begin ServiceNodeIDPrefix = "service-" - // Correspondence between hostnames and the service id they are part of - knownServicesSuffixes = []string{ + knownServicesMatchers = []*regexp.Regexp{ // See http://docs.aws.amazon.com/general/latest/gr/rande.html for fainer grained // details - "amazonaws.com", - "googleapis.com", + regexp.MustCompile(`^.+\.amazonaws\.com$`), + regexp.MustCompile(`^.+\.googleapis\.com$`), + } + + knownServicesExcluder = []*regexp.Regexp{ + // We exclude ec2 machines because they are too generic + // and having separate nodes for them makes visualizations worse + regexp.MustCompile(`^ec2.*\.amazonaws\.com$`), } ) func isKnownService(hostname string) bool { - for _, suffix := range knownServicesSuffixes { - if strings.HasSuffix(hostname, suffix) { - return true + foundMatch := false + for _, matcher := range knownServicesMatchers { + if matcher.MatchString(hostname) { + foundMatch = true + break } } - return false + if !foundMatch { + return false + } + + for _, excluder := range knownServicesExcluder { + if excluder.MatchString(hostname) { + return false + } + } + return true } // LocalNetworks returns a superset of the networks (think: CIDRs) that are From c5ac315b383fdf47c57cebb30bb2b7edd437ec74 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Mon, 19 Sep 2016 15:47:27 +0000 Subject: [PATCH 6/6] Review feedback --- ..._snooper.go => dns_snooper_linux_amd64.go} | 11 +------- probe/endpoint/dns_snooper_others.go | 25 +++++++++++++++++++ probe/endpoint/reporter.go | 8 +++--- prog/probe.go | 3 +-- render/container.go | 2 +- render/process.go | 14 ++++++++--- render/theinternet.go | 11 ++++---- 7 files changed, 48 insertions(+), 26 deletions(-) rename probe/endpoint/{dns_snooper.go => dns_snooper_linux_amd64.go} (94%) create mode 100644 probe/endpoint/dns_snooper_others.go diff --git a/probe/endpoint/dns_snooper.go b/probe/endpoint/dns_snooper_linux_amd64.go similarity index 94% rename from probe/endpoint/dns_snooper.go rename to probe/endpoint/dns_snooper_linux_amd64.go index 11f5ce6b8..2c5b9dc76 100644 --- a/probe/endpoint/dns_snooper.go +++ b/probe/endpoint/dns_snooper_linux_amd64.go @@ -43,20 +43,11 @@ func NewDNSSnooper() (*DNSSnooper, error) { } func newPcapHandle() (*pcap.Handle, error) { - // TODO: use specific interfaces instead? inactive, err := pcap.NewInactiveHandle("any") if err != nil { return nil, err } defer inactive.CleanUp() - if err = inactive.SetPromisc(true); err != nil { - return nil, err - } - // TODO: reduce the size of packets being copied? maybe an overoptimization - // if err = inactive.SetSnapLen(snaplen); err != nil { - // return - // } - // pcap timeout blackmagic copied from Weave Net to reduce CPU consumption // see https://github.com/weaveworks/weave/commit/025315363d5ea8b8265f1b3ea800f24df2be51a4 if err = inactive.SetTimeout(time.Duration(math.MaxInt64)); err != nil { @@ -132,7 +123,7 @@ func (s *DNSSnooper) run() { sll layers.LinuxSLL ) - // assumes that the "any" interface in being used (see https://wiki.wireshark.org/SLL) + // assumes that the "any" interface is being used (see https://wiki.wireshark.org/SLL) packetParser := gopacket.NewDecodingLayerParser(layers.LayerTypeLinuxSLL, &sll, ð, &ip4, &ip6, &udp, &tcp, &dns) for { diff --git a/probe/endpoint/dns_snooper_others.go b/probe/endpoint/dns_snooper_others.go new file mode 100644 index 000000000..52b18cde4 --- /dev/null +++ b/probe/endpoint/dns_snooper_others.go @@ -0,0 +1,25 @@ +// +build darwin arm + +// Cross-compiling the snooper requires having pcap binaries, +// let's disable it for now. +// See http://stackoverflow.com/questions/31648793/go-programming-cross-compile-for-revel-framework + +package endpoint + +// DNSSnooper is a snopper of DNS queries +type DNSSnooper struct{} + +// NewDNSSnooper creates a new snooper of DNS queries +func NewDNSSnooper() (*DNSSnooper, error) { + return nil, nil +} + +// CachedNamesForIP obtains the domains associated to an IP, +// obtained while snooping A-record queries +func (s *DNSSnooper) CachedNamesForIP(ip string) []string { + return []string{} +} + +// Stop makes the snooper stop inspecting DNS communications +func (s *DNSSnooper) Stop() { +} diff --git a/probe/endpoint/reporter.go b/probe/endpoint/reporter.go index 544311236..f357e6efd 100644 --- a/probe/endpoint/reporter.go +++ b/probe/endpoint/reporter.go @@ -21,6 +21,7 @@ const ( Conntracked = "conntracked" Procspied = "procspied" ReverseDNSNames = "reverse_dns_names" + SnoopedDNSNames = "snooped_dns_names" ) // Reporter generates Reports containing the Endpoint topology. @@ -195,11 +196,10 @@ func (r *Reporter) makeEndpointNode(namespaceID string, addr string, port uint16 node := report.MakeNodeWith( report.MakeEndpointNodeID(r.hostID, namespaceID, addr, portStr), map[string]string{Addr: addr, Port: portStr}) - names := r.dnsSnooper.CachedNamesForIP(addr) - if resolvedNames, err := r.reverseResolver.get(addr); err == nil { - names = append(names, resolvedNames...) + if names := r.dnsSnooper.CachedNamesForIP(addr); len(names) > 0 { + node = node.WithSet(SnoopedDNSNames, report.MakeStringSet(names...)) } - if len(names) > 0 { + if names, err := r.reverseResolver.get(addr); err == nil && len(names) > 0 { node = node.WithSet(ReverseDNSNames, report.MakeStringSet(names...)) } if extra != nil { diff --git a/prog/probe.go b/prog/probe.go index c3ba71496..65f52ed5d 100644 --- a/prog/probe.go +++ b/prog/probe.go @@ -145,10 +145,9 @@ func probeMain(flags probeFlags) { p.AddReporter(process.NewReporter(processCache, hostID, process.GetDeltaTotalJiffies)) } - // TODO: make the snooper optional dnsSnooper, err := endpoint.NewDNSSnooper() if err != nil { - log.Errorf("Fail to start DNS snooper: nodes for external services will be less accurate: %s", err) + log.Errorf("Failed to start DNS snooper: nodes for external services will be less accurate: %s", err) } else { defer dnsSnooper.Stop() } diff --git a/render/container.go b/render/container.go index 3a2705b32..eec54e872 100644 --- a/render/container.go +++ b/render/container.go @@ -112,7 +112,7 @@ func ShortLivedConnectionJoin(r Renderer, toIPs func(report.Node) []string) Rend return report.Nodes{} } if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { - node := toInternetNode(m) + node := externalNode(m) return report.Nodes{node.ID: node} } diff --git a/render/process.go b/render/process.go index bc344eba7..7c8f14d18 100644 --- a/render/process.go +++ b/render/process.go @@ -2,6 +2,7 @@ package render import ( "net" + "sort" "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" @@ -97,7 +98,7 @@ func MapEndpoint2Pseudo(n report.Node, local report.Networks) report.Nodes { if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { // If the dstNodeAddr is not in a network local to this report, we emit an // external pseudoNode - node = toInternetNode(n) + node = externalNode(n) } else { // due to https://github.com/weaveworks/scope/issues/1323 we are dropping // all non-internet pseudo nodes for now. @@ -157,11 +158,16 @@ func MapProcess2Name(n report.Node, _ report.Networks) report.Nodes { return report.Nodes{name: node} } -func toInternetNode(m report.Node) report.Node { +func externalNode(m report.Node) report.Node { // First, check if it's a known service and emit a // a specific node if it is - hostnames, _ := m.Sets.Lookup(endpoint.ReverseDNSNames) - for _, hostname := range hostnames { + snoopedHostnames, _ := m.Sets.Lookup(endpoint.SnoopedDNSNames) + reverseHostnames, _ := m.Sets.Lookup(endpoint.ReverseDNSNames) + // Sort the names to make the lookup more deterministic + sort.StringSlice(snoopedHostnames).Sort() + sort.StringSlice(reverseHostnames).Sort() + // Intentionally prioritize snooped hostnames + for _, hostname := range append(snoopedHostnames, reverseHostnames...) { if isKnownService(hostname) { return NewDerivedPseudoNode(ServiceNodeIDPrefix+hostname, m) } diff --git a/render/theinternet.go b/render/theinternet.go index 7546f93a2..9d6c7d3f6 100644 --- a/render/theinternet.go +++ b/render/theinternet.go @@ -9,26 +9,27 @@ import ( ) var ( - // ServiceNodeIDPrefix is how the ID all service pseudo nodes begin + // ServiceNodeIDPrefix is how the ID of all service pseudo nodes begin ServiceNodeIDPrefix = "service-" - knownServicesMatchers = []*regexp.Regexp{ + knownServiceMatchers = []*regexp.Regexp{ // See http://docs.aws.amazon.com/general/latest/gr/rande.html for fainer grained // details regexp.MustCompile(`^.+\.amazonaws\.com$`), regexp.MustCompile(`^.+\.googleapis\.com$`), } - knownServicesExcluder = []*regexp.Regexp{ + knownServiceExcluders = []*regexp.Regexp{ // We exclude ec2 machines because they are too generic // and having separate nodes for them makes visualizations worse regexp.MustCompile(`^ec2.*\.amazonaws\.com$`), } ) +// TODO: Make it user-customizable https://github.com/weaveworks/scope/issues/1876 func isKnownService(hostname string) bool { foundMatch := false - for _, matcher := range knownServicesMatchers { + for _, matcher := range knownServiceMatchers { if matcher.MatchString(hostname) { foundMatch = true break @@ -38,7 +39,7 @@ func isKnownService(hostname string) bool { return false } - for _, excluder := range knownServicesExcluder { + for _, excluder := range knownServiceExcluders { if excluder.MatchString(hostname) { return false }