Files
weave-scope/probe/sniff/sniffer.go
2015-09-03 16:18:54 +00:00

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
}
}