Skip to content

Commit

Permalink
packetbeat: Enable setting promiscuous mode automatically (elastic#11366
Browse files Browse the repository at this point in the history
)

packetbeat: Enable setting promiscuous mode automatically  (elastic#11366)
  • Loading branch information
michalpristas authored Feb 5, 2020
1 parent b9791ad commit 49b0eb9
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down
15 changes: 15 additions & 0 deletions packetbeat/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion packetbeat/Makefile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
7 changes: 7 additions & 0 deletions packetbeat/_meta/beat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 12 additions & 11 deletions packetbeat/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions packetbeat/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions packetbeat/docs/packetbeat-options.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
7 changes: 7 additions & 0 deletions packetbeat/packetbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
80 changes: 77 additions & 3 deletions packetbeat/sniffer/afpacket_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,49 @@
package sniffer

import (
"fmt"
"syscall"
"time"
"unsafe"

"github.com/elastic/beats/libbeat/logp"

"github.com/tsg/gopacket"
"github.com/tsg/gopacket/afpacket"
"github.com/tsg/gopacket/layers"
)

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(
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion packetbeat/sniffer/afpacket_nonlinux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
2 changes: 1 addition & 1 deletion packetbeat/sniffer/sniffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions packetbeat/tests/system/config/packetbeat.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
84 changes: 84 additions & 0 deletions packetbeat/tests/system/test_0069_af_packet.py
Original file line number Diff line number Diff line change
@@ -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"])

0 comments on commit 49b0eb9

Please sign in to comment.