diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index b4714b95f13..9494d59cbc3 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -84,6 +84,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d *Packetbeat* +- Enable setting promiscuous mode automatically. {pull}11366[11366] *Winlogbeat* diff --git a/packetbeat/Dockerfile b/packetbeat/Dockerfile new file mode 100644 index 00000000000..fd609f8e9b2 --- /dev/null +++ b/packetbeat/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.13.7 + +RUN \ + apt-get update \ + && apt-get install -y --no-install-recommends \ + python-pip \ + virtualenv \ + librpm-dev \ + netcat \ + libpcap-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --upgrade pip +RUN pip install --upgrade setuptools +RUN pip install --upgrade docker-compose==1.23.2 diff --git a/packetbeat/Makefile b/packetbeat/Makefile index 1ef455d71cf..1ee3b731787 100644 --- a/packetbeat/Makefile +++ b/packetbeat/Makefile @@ -1,7 +1,7 @@ BEAT_NAME?=packetbeat BEAT_TITLE?=Packetbeat SYSTEM_TESTS?=true -TEST_ENVIRONMENT=false +TEST_ENVIRONMENT?=true ES_BEATS?=.. EXCLUDE_COMMON_UPDATE_TARGET=true diff --git a/packetbeat/_meta/beat.reference.yml b/packetbeat/_meta/beat.reference.yml index 237eac991fe..6ddd057b4c9 100644 --- a/packetbeat/_meta/beat.reference.yml +++ b/packetbeat/_meta/beat.reference.yml @@ -40,6 +40,13 @@ packetbeat.interfaces.device: {{ call .device .GOOS }} # Use this setting to override the automatically generated BPF filter. #packetbeat.interfaces.bpf_filter: +# With `auto_promisc_mode` Packetbeat puts interface in promiscuous mode automatically on startup. +# This option does not work with `any` interface device. +# The default option is false and requires manual set-up of promiscuous mode. +# Warning: under some circumstances (e.g beat crash) promiscuous mode +# can stay enabled even after beat is shut down. +#packetbeat.interfaces.auto_promisc_mode: true + #================================== Flows ===================================== packetbeat.flows: diff --git a/packetbeat/config/config.go b/packetbeat/config/config.go index f4a1cb7e9d5..45d79efd1d2 100644 --- a/packetbeat/config/config.go +++ b/packetbeat/config/config.go @@ -36,17 +36,18 @@ type Config struct { } type InterfacesConfig struct { - Device string `config:"device"` - Type string `config:"type"` - File string `config:"file"` - WithVlans bool `config:"with_vlans"` - BpfFilter string `config:"bpf_filter"` - Snaplen int `config:"snaplen"` - BufferSizeMb int `config:"buffer_size_mb"` - TopSpeed bool - Dumpfile string - OneAtATime bool - Loop int + Device string `config:"device"` + Type string `config:"type"` + File string `config:"file"` + WithVlans bool `config:"with_vlans"` + BpfFilter string `config:"bpf_filter"` + Snaplen int `config:"snaplen"` + BufferSizeMb int `config:"buffer_size_mb"` + EnableAutoPromiscMode bool `config:"auto_promisc_mode"` + TopSpeed bool + Dumpfile string + OneAtATime bool + Loop int } type Flows struct { diff --git a/packetbeat/docker-compose.yml b/packetbeat/docker-compose.yml new file mode 100644 index 00000000000..8abfad19410 --- /dev/null +++ b/packetbeat/docker-compose.yml @@ -0,0 +1,37 @@ +version: '2.3' +services: + beat: + build: ${PWD}/. + depends_on: + - proxy_dep + working_dir: /go/src/github.com/elastic/beats/packetbeat + environment: + - ES_HOST=elasticsearch + - ES_PORT=9200 + - ES_USER=beats + - ES_PASS=testing + - KIBANA_HOST=kibana + - KIBANA_PORT=5601 + volumes: + - ${PWD}/..:/go/src/github.com/elastic/beats/ + command: make + privileged: true + pid: host + + # This is a proxy used to block beats until all services are healthy. + # See: https://github.com/docker/compose/issues/4369 + proxy_dep: + image: busybox + depends_on: + elasticsearch: { condition: service_healthy } + kibana: { condition: service_healthy } + + elasticsearch: + extends: + file: ../testing/environments/${TESTING_ENVIRONMENT}.yml + service: elasticsearch + + kibana: + extends: + file: ../testing/environments/${TESTING_ENVIRONMENT}.yml + service: kibana diff --git a/packetbeat/docs/packetbeat-options.asciidoc b/packetbeat/docs/packetbeat-options.asciidoc index e8aa9882a0f..4855cbb773f 100644 --- a/packetbeat/docs/packetbeat-options.asciidoc +++ b/packetbeat/docs/packetbeat-options.asciidoc @@ -176,6 +176,26 @@ packetbeat.interfaces.type: af_packet packetbeat.interfaces.buffer_size_mb: 100 ------------------------------------------------------------------------------ +[float] +==== `auto_promisc_mode` + +With `auto_promisc_mode` Packetbeat puts interface in promiscuous mode automatically on startup. +This option does not work with `any` interface device. +The default option is false and requires manual set-up of promiscuous mode. +Warning: under some circumstances (e.g beat crash) promiscuous mode +can stay enabled even after beat is shut down. + +Example: + +[source,yaml] +------------------------------------------------------------------------------ +packetbeat.interfaces.device: eth0 +packetbeat.interfaces.type: af_packet +packetbeat.interfaces.buffer_size_mb: 100 +packetbeat.interfaces.auto_promisc_mode: true +------------------------------------------------------------------------------ + + [float] ==== `with_vlans` diff --git a/packetbeat/packetbeat.reference.yml b/packetbeat/packetbeat.reference.yml index ea639408de4..b81447896c4 100644 --- a/packetbeat/packetbeat.reference.yml +++ b/packetbeat/packetbeat.reference.yml @@ -40,6 +40,13 @@ packetbeat.interfaces.device: any # Use this setting to override the automatically generated BPF filter. #packetbeat.interfaces.bpf_filter: +# With `auto_promisc_mode` Packetbeat puts interface in promiscuous mode automatically on startup. +# This option does not work with `any` interface device. +# The default option is false and requires manual set-up of promiscuous mode. +# Warning: under some circumstances (e.g beat crash) promiscuous mode +# can stay enabled even after beat is shut down. +#packetbeat.interfaces.auto_promisc_mode: true + #================================== Flows ===================================== packetbeat.flows: diff --git a/packetbeat/sniffer/afpacket_linux.go b/packetbeat/sniffer/afpacket_linux.go index 3a8cf83ca79..005254a4ff4 100644 --- a/packetbeat/sniffer/afpacket_linux.go +++ b/packetbeat/sniffer/afpacket_linux.go @@ -20,7 +20,12 @@ package sniffer import ( + "fmt" + "syscall" "time" + "unsafe" + + "github.com/elastic/beats/libbeat/logp" "github.com/tsg/gopacket" "github.com/tsg/gopacket/afpacket" @@ -28,14 +33,36 @@ import ( ) type afpacketHandle struct { - TPacket *afpacket.TPacket + TPacket *afpacket.TPacket + promiscPreviousState bool + promiscPreviousStateDetected bool + device string } func newAfpacketHandle(device string, snaplen int, block_size int, num_blocks int, - timeout time.Duration) (*afpacketHandle, error) { + timeout time.Duration, autoPromiscMode bool) (*afpacketHandle, error) { - h := &afpacketHandle{} var err error + var promiscEnabled bool + + if autoPromiscMode { + promiscEnabled, err = isPromiscEnabled(device) + if err != nil { + logp.Err("Failed to get promiscuous mode for device '%s': %v", device, err) + } + + if !promiscEnabled { + if setPromiscErr := setPromiscMode(device, true); setPromiscErr != nil { + logp.Warn("Failed to set promiscuous mode for device '%s'. Packetbeat may be unable to see any network traffic. Please follow packetbeat FAQ to learn about mitigation: Error: %v", device, err) + } + } + } + + h := &afpacketHandle{ + promiscPreviousState: promiscEnabled, + device: device, + promiscPreviousStateDetected: autoPromiscMode && err == nil, + } if device == "any" { h.TPacket, err = afpacket.NewTPacket( @@ -69,4 +96,51 @@ func (h *afpacketHandle) LinkType() layers.LinkType { func (h *afpacketHandle) Close() { h.TPacket.Close() + // previous state detected only if auto mode was on + if h.promiscPreviousStateDetected { + if err := setPromiscMode(h.device, h.promiscPreviousState); err != nil { + logp.Warn("Failed to reset promiscuous mode for device '%s'. Your device might be in promiscuous mode.: %v", h.device, err) + } + } +} + +func isPromiscEnabled(device string) (bool, error) { + if device == "any" { + return false, nil + } + + s, e := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0) + if e != nil { + return false, e + } + + defer syscall.Close(s) + + var ifreq struct { + name [syscall.IFNAMSIZ]byte + flags uint16 + } + + copy(ifreq.name[:], []byte(device)) + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), syscall.SIOCGIFFLAGS, uintptr(unsafe.Pointer(&ifreq))) + if ep != 0 { + return false, fmt.Errorf("ioctl command SIOCGIFFLAGS failed to get device flags for %v: return code %d", device, ep) + } + + return ifreq.flags&uint16(syscall.IFF_PROMISC) != 0, nil +} + +// setPromiscMode enables promisc mode if configured. +// this makes maintenance for user simpler without any additional manual steps +// issue [700](https://github.com/elastic/beats/issues/700) +func setPromiscMode(device string, enabled bool) error { + if device == "any" { + logp.Warn("Cannot set promiscuous mode to device 'any'") + return nil + } + + // SetLsfPromisc is marked as deprecated but used to improve readability (bpf) + // and avoid Cgo (pcap) + // TODO: replace with x/net/bpf or pcap + return syscall.SetLsfPromisc(device, enabled) } diff --git a/packetbeat/sniffer/afpacket_nonlinux.go b/packetbeat/sniffer/afpacket_nonlinux.go index 2daa64520bf..7d1e423c40d 100644 --- a/packetbeat/sniffer/afpacket_nonlinux.go +++ b/packetbeat/sniffer/afpacket_nonlinux.go @@ -31,7 +31,7 @@ type afpacketHandle struct { } func newAfpacketHandle(device string, snaplen int, blockSize int, numBlocks int, - timeout time.Duration) (*afpacketHandle, error) { + timeout time.Duration, enableAutoPromiscMode bool) (*afpacketHandle, error) { return nil, fmt.Errorf("Afpacket MMAP sniffing is only available on Linux") } diff --git a/packetbeat/sniffer/sniffer.go b/packetbeat/sniffer/sniffer.go index 714d2c46d31..d38101c4b20 100644 --- a/packetbeat/sniffer/sniffer.go +++ b/packetbeat/sniffer/sniffer.go @@ -305,7 +305,7 @@ func openAFPacket(filter string, cfg *config.InterfacesConfig) (snifferHandle, e } timeout := 500 * time.Millisecond - h, err := newAfpacketHandle(cfg.Device, szFrame, szBlock, numBlocks, timeout) + h, err := newAfpacketHandle(cfg.Device, szFrame, szBlock, numBlocks, timeout, cfg.EnableAutoPromiscMode) if err != nil { return nil, err } diff --git a/packetbeat/tests/system/config/packetbeat.yml.j2 b/packetbeat/tests/system/config/packetbeat.yml.j2 index b55bfc6fcfc..b687f6f0402 100644 --- a/packetbeat/tests/system/config/packetbeat.yml.j2 +++ b/packetbeat/tests/system/config/packetbeat.yml.j2 @@ -4,6 +4,12 @@ # keyword to sniff on all connected interfaces. packetbeat.interfaces.device: {{ iface_device|default("any") }} +{% if af_packet %} +packetbeat.interfaces.type: af_packet +packetbeat.interfaces.buffer_size_mb: 100 +packetbeat.interfaces.auto_promisc_mode: true +{% endif %} + {% if bpf_filter %} packetbeat.interfaces.bpf_filter: {{ bpf_filter }} {% endif %} diff --git a/packetbeat/tests/system/test_0069_af_packet.py b/packetbeat/tests/system/test_0069_af_packet.py new file mode 100644 index 00000000000..71e7863a54f --- /dev/null +++ b/packetbeat/tests/system/test_0069_af_packet.py @@ -0,0 +1,84 @@ +import os +import subprocess +import sys +import time +import unittest +from packetbeat import BaseTest + +""" +Tests for afpacket. +""" + + +def is_root(): + if 'geteuid' not in dir(os): + return False + euid = os.geteuid() + print("euid is", euid) + return euid == 0 + + +class Test(BaseTest): + + @unittest.skipUnless( + sys.platform.startswith("linux"), + "af_packet only on Linux") + @unittest.skipUnless(is_root(), "Requires root") + def test_afpacket_promisc(self): + """ + Should switch to promisc mode and back. + """ + + # get device name, leave out loopback device + devices = [f for f in os.listdir( + "/sys/class/net") if f.startswith("lo")] + assert len(devices) > 0 + + device = devices[0] + + ip_proc = subprocess.Popen( + ["ip", "link", "show", device], stdout=subprocess.PIPE) + o, e = ip_proc.communicate() + assert e is None + + prev_promisc = "PROMISC" in o.decode("utf-8") + + # turn off promics if was on + if prev_promisc: + subprocess.call(["ip", "link", "set", device, + "promisc", "off"], stdout=subprocess.PIPE) + + self.render_config_template( + af_packet=True, + iface_device=device + ) + packetbeat = self.start_packetbeat() + + # wait for promisc to be turned on, cap(90s) + for x in range(10): + time.sleep(5) + + ip_proc = subprocess.Popen( + ["ip", "link", "show", device], stdout=subprocess.PIPE) + o, e = ip_proc.communicate() + + is_promisc = "PROMISC" in o.decode("utf-8") + if is_promisc: + break + + assert is_promisc + + # stop packetbeat and check if promisc is set to previous state + packetbeat.kill_and_wait() + + ip_proc = subprocess.Popen( + ["ip", "link", "show", device], stdout=subprocess.PIPE) + o, e = ip_proc.communicate() + assert e is None + + is_promisc = "PROMISC" in o.decode("utf-8") + assert is_promisc == False + + # reset device + if prev_promisc: + subprocess.call(["ip", "link", "set", device, "promisc", "on"])