mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-16 20:11:09 +00:00
325 lines
9.4 KiB
Go
325 lines
9.4 KiB
Go
package sniff
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/weaveworks/scope/report"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
)
|
|
|
|
// Sniffer is a packet-sniffing reporter.
|
|
type Sniffer struct {
|
|
hostID string
|
|
localNets report.Networks
|
|
reports chan chan report.Report
|
|
parser *gopacket.DecodingLayerParser
|
|
decoded []gopacket.LayerType
|
|
eth layers.Ethernet
|
|
ip4 layers.IPv4
|
|
ip6 layers.IPv6
|
|
tcp layers.TCP
|
|
udp layers.UDP
|
|
icmp4 layers.ICMPv4
|
|
icmp6 layers.ICMPv6
|
|
}
|
|
|
|
// New returns a new sniffing reporter that samples traffic by turning its
|
|
// packet capture facilities on and off. Note that the on and off durations
|
|
// represent a way to bound CPU burn. Effective sample rate needs to be
|
|
// calculated as (packets decoded / packets observed).
|
|
func New(hostID string, localNets report.Networks, src gopacket.ZeroCopyPacketDataSource, on, off time.Duration) *Sniffer {
|
|
s := &Sniffer{
|
|
hostID: hostID,
|
|
localNets: localNets,
|
|
reports: make(chan chan report.Report),
|
|
}
|
|
s.parser = gopacket.NewDecodingLayerParser(
|
|
layers.LayerTypeEthernet,
|
|
&s.eth, &s.ip4, &s.ip6, &s.tcp, &s.udp, &s.icmp4, &s.icmp6,
|
|
)
|
|
go s.loop(src, on, off)
|
|
return s
|
|
}
|
|
|
|
// Report implements the Reporter interface.
|
|
func (s *Sniffer) Report() (report.Report, error) {
|
|
c := make(chan report.Report)
|
|
s.reports <- c
|
|
return <-c, nil
|
|
}
|
|
|
|
func (s *Sniffer) loop(src gopacket.ZeroCopyPacketDataSource, on, off time.Duration) {
|
|
var (
|
|
process = uint64(1) // initially enabled
|
|
total = uint64(0) // total packets seen
|
|
count = uint64(0) // count of packets captured
|
|
packets = make(chan Packet, 1024) // decoded packets
|
|
rpt = report.MakeReport() // the report we build
|
|
turnOn = (<-chan time.Time)(nil) // signal to start capture (initially enabled)
|
|
turnOff = time.After(on) // signal to stop capture
|
|
done = make(chan struct{}) // when src is finished, we're done too
|
|
)
|
|
|
|
// As a special case, if our off duty cycle is zero, i.e. 100% sample
|
|
// rate, we simply disable the turn-off signal channel.
|
|
if off == 0 {
|
|
turnOff = nil
|
|
}
|
|
|
|
go func() {
|
|
s.read(src, packets, &process, &total, &count)
|
|
close(done)
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case p := <-packets:
|
|
s.Merge(p, &rpt)
|
|
|
|
case <-turnOn:
|
|
atomic.StoreUint64(&process, 1) // enable packet capture
|
|
turnOn = nil // disable the on switch
|
|
turnOff = time.After(on) // enable the off switch
|
|
|
|
case <-turnOff:
|
|
atomic.StoreUint64(&process, 0) // disable packet capture
|
|
turnOn = time.After(off) // enable the on switch
|
|
turnOff = nil // disable the off switch
|
|
|
|
case c := <-s.reports:
|
|
rpt.Sampling.Count = atomic.LoadUint64(&count)
|
|
rpt.Sampling.Total = atomic.LoadUint64(&total)
|
|
interpolateCounts(rpt)
|
|
c <- rpt
|
|
atomic.StoreUint64(&count, 0)
|
|
atomic.StoreUint64(&total, 0)
|
|
rpt = report.MakeReport()
|
|
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// interpolateCounts compensates for sampling by artifically inflating counts
|
|
// throughout the report. It should be run once for each report, within the
|
|
// probe, before it gets emitted into the rest of the system.
|
|
func interpolateCounts(r report.Report) {
|
|
rate := r.Sampling.Rate()
|
|
if rate >= 1.0 {
|
|
return
|
|
}
|
|
factor := 1.0 / rate
|
|
for _, topology := range r.Topologies() {
|
|
for _, nmd := range topology.Nodes {
|
|
for _, emd := range nmd.Edges {
|
|
if emd.EgressPacketCount != nil {
|
|
*emd.EgressPacketCount = uint64(float64(*emd.EgressPacketCount) * factor)
|
|
}
|
|
if emd.IngressPacketCount != nil {
|
|
*emd.IngressPacketCount = uint64(float64(*emd.IngressPacketCount) * factor)
|
|
}
|
|
if emd.EgressByteCount != nil {
|
|
*emd.EgressByteCount = uint64(float64(*emd.EgressByteCount) * factor)
|
|
}
|
|
if emd.IngressByteCount != nil {
|
|
*emd.IngressByteCount = uint64(float64(*emd.IngressByteCount) * factor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Packet is an intermediate, decoded form of a packet, with the information
|
|
// that the Scope data model cares about. Designed to decouple the packet data
|
|
// source loop, which should be as fast as possible, and the process of
|
|
// merging the packet information to a report, which may take some time and
|
|
// allocations.
|
|
type Packet struct {
|
|
SrcIP, DstIP string
|
|
SrcPort, DstPort string
|
|
Network, Transport int // byte counts
|
|
}
|
|
|
|
func (s *Sniffer) read(src gopacket.ZeroCopyPacketDataSource, dst chan Packet, process, total, count *uint64) {
|
|
var (
|
|
data []byte
|
|
err error
|
|
)
|
|
for {
|
|
data, _, err = src.ZeroCopyReadPacketData()
|
|
if err == io.EOF {
|
|
return // done
|
|
}
|
|
if err != nil {
|
|
log.Printf("sniffer: read: %v", err)
|
|
continue
|
|
}
|
|
atomic.AddUint64(total, 1)
|
|
if atomic.LoadUint64(process) == 0 {
|
|
continue
|
|
}
|
|
|
|
if err := s.parser.DecodeLayers(data, &s.decoded); err != nil {
|
|
// We'll always get an error when we encounter a layer type for
|
|
// which we haven't configured a decoder.
|
|
}
|
|
var p Packet
|
|
for _, t := range s.decoded {
|
|
switch t {
|
|
case layers.LayerTypeEthernet:
|
|
//
|
|
|
|
case layers.LayerTypeICMPv4:
|
|
p.Network += len(s.icmp4.Payload)
|
|
|
|
case layers.LayerTypeICMPv6:
|
|
p.Network += len(s.icmp6.Payload)
|
|
|
|
case layers.LayerTypeIPv4:
|
|
p.SrcIP = s.ip4.SrcIP.String()
|
|
p.DstIP = s.ip4.DstIP.String()
|
|
p.Network += len(s.ip4.Payload)
|
|
|
|
case layers.LayerTypeIPv6:
|
|
p.SrcIP = s.ip6.SrcIP.String()
|
|
p.DstIP = s.ip6.DstIP.String()
|
|
p.Network += len(s.ip6.Payload)
|
|
|
|
case layers.LayerTypeTCP:
|
|
p.SrcPort = strconv.Itoa(int(s.tcp.SrcPort))
|
|
p.DstPort = strconv.Itoa(int(s.tcp.DstPort))
|
|
p.Transport += len(s.tcp.Payload)
|
|
|
|
case layers.LayerTypeUDP:
|
|
p.SrcPort = strconv.Itoa(int(s.udp.SrcPort))
|
|
p.DstPort = strconv.Itoa(int(s.udp.DstPort))
|
|
p.Transport += len(s.udp.Payload)
|
|
}
|
|
}
|
|
select {
|
|
case dst <- p:
|
|
atomic.AddUint64(count, 1)
|
|
default:
|
|
log.Printf("sniffer dropped packet")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge puts the packet into the report.
|
|
//
|
|
// Note that, for the moment, we encode bidirectional traffic as ingress and
|
|
// egress traffic on a single edge whose src is local and dst is remote. That
|
|
// is, if we see a packet from the remote addr 9.8.7.6 to the local addr
|
|
// 1.2.3.4, we apply it as *ingress* on the edge (1.2.3.4 -> 9.8.7.6).
|
|
func (s *Sniffer) Merge(p Packet, rpt *report.Report) {
|
|
if p.SrcIP == "" || p.DstIP == "" {
|
|
return
|
|
}
|
|
|
|
// One end of the traffic has to be local. Otherwise, we don't know how to
|
|
// construct the edge.
|
|
//
|
|
// If we need to get around this limitation, we may be able to change the
|
|
// semantics of the report, and allow the src side of edges to be from
|
|
// anywhere. But that will have ramifications throughout Scope (read: it
|
|
// may violate implicit invariants) and needs to be thought through.
|
|
var (
|
|
srcLocal = s.localNets.Contains(net.ParseIP(p.SrcIP))
|
|
dstLocal = s.localNets.Contains(net.ParseIP(p.DstIP))
|
|
localIP string
|
|
remoteIP string
|
|
localPort string
|
|
remotePort string
|
|
egress bool
|
|
)
|
|
switch {
|
|
case srcLocal && !dstLocal:
|
|
localIP, localPort, remoteIP, remotePort, egress = p.SrcIP, p.SrcPort, p.DstIP, p.DstPort, true
|
|
case !srcLocal && dstLocal:
|
|
localIP, localPort, remoteIP, remotePort, egress = p.DstIP, p.DstPort, p.SrcIP, p.SrcPort, false
|
|
case srcLocal && dstLocal:
|
|
localIP, localPort, remoteIP, remotePort, egress = p.SrcIP, p.SrcPort, p.DstIP, p.DstPort, true // loopback
|
|
case !srcLocal && !dstLocal:
|
|
log.Printf("sniffer ignoring remote-to-remote (%s -> %s) traffic", p.SrcIP, p.DstIP)
|
|
return
|
|
}
|
|
|
|
addAdjacency := func(t report.Topology, srcNodeID, dstNodeID string) report.Topology {
|
|
result := t.WithNode(srcNodeID, report.MakeNode().WithAdjacent(dstNodeID))
|
|
result = result.WithNode(dstNodeID, report.MakeNode())
|
|
return result
|
|
}
|
|
|
|
// For sure, we can add to the address topology.
|
|
{
|
|
var (
|
|
srcNodeID = report.MakeAddressNodeID(s.hostID, localIP)
|
|
dstNodeID = report.MakeAddressNodeID(s.hostID, remoteIP)
|
|
)
|
|
|
|
rpt.Address = addAdjacency(rpt.Address, srcNodeID, dstNodeID)
|
|
|
|
emd := rpt.Address.Nodes[srcNodeID].Edges[dstNodeID]
|
|
if egress {
|
|
if emd.EgressPacketCount == nil {
|
|
emd.EgressPacketCount = new(uint64)
|
|
}
|
|
*emd.EgressPacketCount++
|
|
if emd.EgressByteCount == nil {
|
|
emd.EgressByteCount = new(uint64)
|
|
}
|
|
*emd.EgressByteCount += uint64(p.Network)
|
|
} else {
|
|
if emd.IngressPacketCount == nil {
|
|
emd.IngressPacketCount = new(uint64)
|
|
}
|
|
*emd.IngressPacketCount++
|
|
if emd.IngressByteCount == nil {
|
|
emd.IngressByteCount = new(uint64)
|
|
}
|
|
*emd.IngressByteCount += uint64(p.Network)
|
|
}
|
|
rpt.Address.Nodes[srcNodeID].Edges[dstNodeID] = emd
|
|
}
|
|
|
|
// If we have ports, we can add to the endpoint topology, too.
|
|
if p.SrcPort != "" && p.DstPort != "" {
|
|
var (
|
|
srcNodeID = report.MakeEndpointNodeID(s.hostID, localIP, localPort)
|
|
dstNodeID = report.MakeEndpointNodeID(s.hostID, remoteIP, remotePort)
|
|
)
|
|
|
|
rpt.Endpoint = addAdjacency(rpt.Endpoint, srcNodeID, dstNodeID)
|
|
|
|
emd := rpt.Endpoint.Nodes[srcNodeID].Edges[dstNodeID]
|
|
if egress {
|
|
if emd.EgressPacketCount == nil {
|
|
emd.EgressPacketCount = new(uint64)
|
|
}
|
|
*emd.EgressPacketCount++
|
|
if emd.EgressByteCount == nil {
|
|
emd.EgressByteCount = new(uint64)
|
|
}
|
|
*emd.EgressByteCount += uint64(p.Transport)
|
|
} else {
|
|
if emd.IngressPacketCount == nil {
|
|
emd.IngressPacketCount = new(uint64)
|
|
}
|
|
*emd.IngressPacketCount++
|
|
if emd.IngressByteCount == nil {
|
|
emd.IngressByteCount = new(uint64)
|
|
}
|
|
*emd.IngressByteCount += uint64(p.Transport)
|
|
}
|
|
rpt.Endpoint.Nodes[srcNodeID].Edges[dstNodeID] = emd
|
|
}
|
|
}
|