330 lines
7.6 KiB
Go
330 lines
7.6 KiB
Go
|
// Licensed to Elasticsearch B.V. under one or more contributor
|
||
|
// license agreements. See the NOTICE file distributed with
|
||
|
// this work for additional information regarding copyright
|
||
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
||
|
// the Apache License, Version 2.0 (the "License"); you may
|
||
|
// not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing,
|
||
|
// software distributed under the License is distributed on an
|
||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||
|
// KIND, either express or implied. See the License for the
|
||
|
// specific language governing permissions and limitations
|
||
|
// under the License.
|
||
|
|
||
|
package sniffer
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"runtime"
|
||
|
"syscall"
|
||
|
"time"
|
||
|
|
||
|
"github.com/tsg/gopacket"
|
||
|
"github.com/tsg/gopacket/layers"
|
||
|
"github.com/tsg/gopacket/pcap"
|
||
|
|
||
|
"github.com/elastic/beats/libbeat/common/atomic"
|
||
|
"github.com/elastic/beats/libbeat/logp"
|
||
|
|
||
|
"github.com/elastic/beats/packetbeat/config"
|
||
|
)
|
||
|
|
||
|
// Sniffer provides packet sniffing capabilities, forwarding packets read
|
||
|
// to a Worker.
|
||
|
type Sniffer struct {
|
||
|
config config.InterfacesConfig
|
||
|
dumper *pcap.Dumper
|
||
|
|
||
|
state atomic.Int32 // store snifferState
|
||
|
|
||
|
// bpf filter
|
||
|
filter string
|
||
|
|
||
|
factory WorkerFactory
|
||
|
}
|
||
|
|
||
|
// WorkerFactory constructs a new worker instance for use with a Sniffer.
|
||
|
type WorkerFactory func(layers.LinkType) (Worker, error)
|
||
|
|
||
|
// Worker defines the callback interfaces a Sniffer instance will use
|
||
|
// to forward packets.
|
||
|
type Worker interface {
|
||
|
OnPacket(data []byte, ci *gopacket.CaptureInfo)
|
||
|
}
|
||
|
|
||
|
type snifferHandle interface {
|
||
|
gopacket.PacketDataSource
|
||
|
|
||
|
LinkType() layers.LinkType
|
||
|
Close()
|
||
|
}
|
||
|
|
||
|
// sniffer state values
|
||
|
const (
|
||
|
snifferInactive = 0
|
||
|
snifferClosing = 1
|
||
|
snifferActive = 2
|
||
|
)
|
||
|
|
||
|
// New create a new Sniffer instance. Settings are validated in a best effort
|
||
|
// only, but no device is opened yet. Accessing and configuring the actual device
|
||
|
// is done by the Run method.
|
||
|
func New(
|
||
|
testMode bool,
|
||
|
filter string,
|
||
|
factory WorkerFactory,
|
||
|
interfaces config.InterfacesConfig,
|
||
|
) (*Sniffer, error) {
|
||
|
s := &Sniffer{
|
||
|
filter: filter,
|
||
|
config: interfaces,
|
||
|
factory: factory,
|
||
|
state: atomic.MakeInt32(snifferInactive),
|
||
|
}
|
||
|
|
||
|
logp.Debug("sniffer", "BPF filter: '%s'", filter)
|
||
|
|
||
|
// pre-check and normalize configuration:
|
||
|
// - resolve potential device name
|
||
|
// - check for file output
|
||
|
// - set some defaults
|
||
|
if s.config.File != "" {
|
||
|
logp.Debug("sniffer", "Reading from file: %s", s.config.File)
|
||
|
|
||
|
if s.config.BpfFilter != "" {
|
||
|
logp.Warn("Packet filters are not applied to pcap files.")
|
||
|
}
|
||
|
|
||
|
// we read file with the pcap provider
|
||
|
s.config.Type = "pcap"
|
||
|
s.config.Device = ""
|
||
|
} else {
|
||
|
// try to resolve device name (ignore error if testMode is enabled)
|
||
|
if name, err := resolveDeviceName(s.config.Device); err != nil {
|
||
|
if !testMode {
|
||
|
return nil, err
|
||
|
}
|
||
|
} else {
|
||
|
s.config.Device = name
|
||
|
if name == "any" && !deviceAnySupported {
|
||
|
return nil, fmt.Errorf("any interface is not supported on %s", runtime.GOOS)
|
||
|
}
|
||
|
|
||
|
if s.config.Snaplen == 0 {
|
||
|
s.config.Snaplen = 65535
|
||
|
}
|
||
|
if s.config.BufferSizeMb <= 0 {
|
||
|
s.config.BufferSizeMb = 24
|
||
|
}
|
||
|
|
||
|
if t := s.config.Type; t == "autodetect" || t == "" {
|
||
|
s.config.Type = "pcap"
|
||
|
}
|
||
|
logp.Debug("sniffer", "Sniffer type: %s device: %s", s.config.Type, s.config.Device)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
err := validateConfig(filter, &s.config)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return s, nil
|
||
|
}
|
||
|
|
||
|
// Run opens the sniffing device and processes packets being read from that device.
|
||
|
// Worker instances are instantiated as needed.
|
||
|
func (s *Sniffer) Run() error {
|
||
|
var (
|
||
|
counter = 0
|
||
|
dumper *pcap.Dumper
|
||
|
)
|
||
|
|
||
|
handle, err := s.open()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error starting sniffer: %s", err)
|
||
|
}
|
||
|
defer handle.Close()
|
||
|
|
||
|
if s.config.Dumpfile != "" {
|
||
|
dumper, err = openDumper(s.config.Dumpfile, handle.LinkType())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
defer dumper.Close()
|
||
|
}
|
||
|
|
||
|
worker, err := s.factory(handle.LinkType())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Mark inactive sniffer as active. In case of the sniffer/packetbeat closing
|
||
|
// before/while Run is executed, the state will be snifferClosing.
|
||
|
// => return if state is already snifferClosing.
|
||
|
if !s.state.CAS(snifferInactive, snifferActive) {
|
||
|
return nil
|
||
|
}
|
||
|
defer s.state.Store(snifferInactive)
|
||
|
|
||
|
for s.state.Load() == snifferActive {
|
||
|
if s.config.OneAtATime {
|
||
|
fmt.Println("Press enter to read packet")
|
||
|
fmt.Scanln()
|
||
|
}
|
||
|
|
||
|
data, ci, err := handle.ReadPacketData()
|
||
|
if err == pcap.NextErrorTimeoutExpired || err == syscall.EINTR {
|
||
|
logp.Debug("sniffer", "Interrupted")
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
// ignore EOF, if sniffer was driven from file
|
||
|
if err == io.EOF && s.config.File != "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
s.state.Store(snifferInactive)
|
||
|
return fmt.Errorf("Sniffing error: %s", err)
|
||
|
}
|
||
|
|
||
|
if len(data) == 0 {
|
||
|
// Empty packet, probably timeout from afpacket
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if dumper != nil {
|
||
|
dumper.WritePacketData(data, ci)
|
||
|
}
|
||
|
|
||
|
counter++
|
||
|
logp.Debug("sniffer", "Packet number: %d", counter)
|
||
|
worker.OnPacket(data, &ci)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *Sniffer) open() (snifferHandle, error) {
|
||
|
if s.config.File != "" {
|
||
|
return newFileHandler(s.config.File, s.config.TopSpeed, s.config.Loop)
|
||
|
}
|
||
|
|
||
|
switch s.config.Type {
|
||
|
case "pcap":
|
||
|
return openPcap(s.filter, &s.config)
|
||
|
case "af_packet":
|
||
|
return openAFPacket(s.filter, &s.config)
|
||
|
default:
|
||
|
return nil, fmt.Errorf("Unknown sniffer type: %s", s.config.Type)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Stop marks a sniffer as stopped. The Run method will return once the stop
|
||
|
// signal has been given.
|
||
|
func (s *Sniffer) Stop() error {
|
||
|
s.state.Store(snifferClosing)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func validateConfig(filter string, cfg *config.InterfacesConfig) error {
|
||
|
if cfg.File == "" {
|
||
|
if err := validatePcapFilter(filter); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch cfg.Type {
|
||
|
case "pcap":
|
||
|
return validatePcapConfig(cfg)
|
||
|
case "af_packet":
|
||
|
return validateAfPacketConfig(cfg)
|
||
|
default:
|
||
|
return fmt.Errorf("Unknown sniffer type: %s", cfg.Type)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func validatePcapConfig(cfg *config.InterfacesConfig) error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func validateAfPacketConfig(cfg *config.InterfacesConfig) error {
|
||
|
_, _, _, err := afpacketComputeSize(cfg.BufferSizeMb, cfg.Snaplen, os.Getpagesize())
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func validatePcapFilter(expr string) error {
|
||
|
if expr == "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Open a dummy pcap handle to compile the filter
|
||
|
p, err := pcap.OpenDead(layers.LinkTypeEthernet, 65535)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("OpenDead: %s", err)
|
||
|
}
|
||
|
|
||
|
defer p.Close()
|
||
|
|
||
|
_, err = p.NewBPF(expr)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("invalid filter '%s': %v", expr, err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func openPcap(filter string, cfg *config.InterfacesConfig) (snifferHandle, error) {
|
||
|
snaplen := int32(cfg.Snaplen)
|
||
|
timeout := 500 * time.Millisecond
|
||
|
h, err := pcap.OpenLive(cfg.Device, snaplen, true, timeout)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
err = h.SetBPFFilter(filter)
|
||
|
if err != nil {
|
||
|
h.Close()
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return h, nil
|
||
|
}
|
||
|
|
||
|
func openAFPacket(filter string, cfg *config.InterfacesConfig) (snifferHandle, error) {
|
||
|
szFrame, szBlock, numBlocks, err := afpacketComputeSize(cfg.BufferSizeMb, cfg.Snaplen, os.Getpagesize())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
timeout := 500 * time.Millisecond
|
||
|
h, err := newAfpacketHandle(cfg.Device, szFrame, szBlock, numBlocks, timeout)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
err = h.SetBPFFilter(filter)
|
||
|
if err != nil {
|
||
|
h.Close()
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return h, nil
|
||
|
}
|
||
|
|
||
|
func openDumper(file string, linkType layers.LinkType) (*pcap.Dumper, error) {
|
||
|
p, err := pcap.OpenDead(linkType, 65535)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return p.NewDumper(file)
|
||
|
}
|