Skip to content

Commit

Permalink
Add input plugin for OpenBSD/FreeBSD pf (#3405)
Browse files Browse the repository at this point in the history
  • Loading branch information
nferch authored and danielnelson committed Nov 30, 2017
1 parent 4337c98 commit d758008
Show file tree
Hide file tree
Showing 4 changed files with 504 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/openldap"
_ "github.com/influxdata/telegraf/plugins/inputs/opensmtpd"
_ "github.com/influxdata/telegraf/plugins/inputs/passenger"
_ "github.com/influxdata/telegraf/plugins/inputs/pf"
_ "github.com/influxdata/telegraf/plugins/inputs/phpfpm"
_ "github.com/influxdata/telegraf/plugins/inputs/ping"
_ "github.com/influxdata/telegraf/plugins/inputs/postfix"
Expand Down
68 changes: 68 additions & 0 deletions plugins/inputs/pf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# PF Plugin

The pf plugin gathers information from the FreeBSD/OpenBSD pf firewall. Currently it can retrive information about the state table: the number of current entries in the table, and counters for the number of searches, inserts, and removals to the table.

The pf plugin retrives this information by invoking the `pfstat` command. The `pfstat` command requires read access to the device file `/dev/pf`. You have several options to permit telegraf to run `pfctl`:

* Run telegraf as root. This is strongly discouraged.
* Change the ownership and permissions for /dev/pf such that the user telegraf runs at can read the /dev/pf device file. This is probably not that good of an idea either.
* Configure sudo to grant telegraf to run `pfctl` as root. This is the most restrictive option, but require sudo setup.

### Using sudo

You may edit your sudo configuration with the following:

```sudo
telegraf ALL=(root) NOPASSWD: /sbin/pfctl -s info
```

### Configuration:

```toml
# use sudo to run pfctl
use_sudo = false
```

### Measurements & Fields:


- pf
- entries (integer, count)
- searches (integer, count)
- inserts (integer, count)
- removals (integer, count)

### Example Output:

```
> pfctl -s info
Status: Enabled for 0 days 00:26:05 Debug: Urgent
State Table Total Rate
current entries 2
searches 11325 7.2/s
inserts 5 0.0/s
removals 3 0.0/s
Counters
match 11226 7.2/s
bad-offset 0 0.0/s
fragment 0 0.0/s
short 0 0.0/s
normalize 0 0.0/s
memory 0 0.0/s
bad-timestamp 0 0.0/s
congestion 0 0.0/s
ip-option 0 0.0/s
proto-cksum 0 0.0/s
state-mismatch 0 0.0/s
state-insert 0 0.0/s
state-limit 0 0.0/s
src-limit 0 0.0/s
synproxy 0 0.0/s
```

```
> ./telegraf --config telegraf.conf --input-filter pf --test
* Plugin: inputs.pf, Collection 1
> pf,host=columbia entries=3i,searches=2668i,inserts=12i,removals=9i 1510941775000000000
```
192 changes: 192 additions & 0 deletions plugins/inputs/pf/pf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package pf

import (
"bufio"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
)

const measurement = "pf"
const pfctlCommand = "pfctl"

type PF struct {
PfctlCommand string
PfctlArgs []string
UseSudo bool
StateTable []*Entry
infoFunc func() (string, error)
}

func (pf *PF) Description() string {
return "Gather counters from PF"
}

func (pf *PF) SampleConfig() string {
return `
## PF require root access on most systems.
## Setting 'use_sudo' to true will make use of sudo to run pfctl.
## Users must configure sudo to allow telegraf user to run pfctl with no password.
## pfctl can be restricted to only list command "pfctl -s info".
use_sudo = false
`
}

// Gather is the entrypoint for the plugin.
func (pf *PF) Gather(acc telegraf.Accumulator) error {
if pf.PfctlCommand == "" {
var err error
if pf.PfctlCommand, pf.PfctlArgs, err = pf.buildPfctlCmd(); err != nil {
acc.AddError(fmt.Errorf("Can't construct pfctl commandline: %s", err))
return nil
}
}

o, err := pf.infoFunc()
if err != nil {
acc.AddError(err)
return nil
}

if perr := pf.parsePfctlOutput(o, acc); perr != nil {
acc.AddError(perr)
}
return nil
}

var errParseHeader = fmt.Errorf("Cannot find header in %s output", pfctlCommand)

func errMissingData(tag string) error {
return fmt.Errorf("struct data for tag \"%s\" not found in %s output", tag, pfctlCommand)
}

type pfctlOutputStanza struct {
HeaderRE *regexp.Regexp
ParseFunc func([]string, telegraf.Accumulator) error
Found bool
}

var pfctlOutputStanzas = []*pfctlOutputStanza{
&pfctlOutputStanza{
HeaderRE: regexp.MustCompile("^State Table"),
ParseFunc: parseStateTable,
},
}

var anyTableHeaderRE = regexp.MustCompile("^[A-Z]")

func (pf *PF) parsePfctlOutput(pfoutput string, acc telegraf.Accumulator) error {
scanner := bufio.NewScanner(strings.NewReader(pfoutput))
for scanner.Scan() {
line := scanner.Text()
for _, s := range pfctlOutputStanzas {
if s.HeaderRE.MatchString(line) {
var stanzaLines []string
scanner.Scan()
line = scanner.Text()
for !anyTableHeaderRE.MatchString(line) {
stanzaLines = append(stanzaLines, line)
scanner.Scan()
line = scanner.Text()
}
if perr := s.ParseFunc(stanzaLines, acc); perr != nil {
return perr
}
s.Found = true
}
}
}
for _, s := range pfctlOutputStanzas {
if !s.Found {
return errParseHeader
}
}
return nil
}

type Entry struct {
Field string
PfctlTitle string
Value int64
}

var StateTable = []*Entry{
&Entry{"entries", "current entries", -1},
&Entry{"searches", "searches", -1},
&Entry{"inserts", "inserts", -1},
&Entry{"removals", "removals", -1},
}

var stateTableRE = regexp.MustCompile(`^ (.*?)\s+(\d+)`)

func parseStateTable(lines []string, acc telegraf.Accumulator) error {
for _, v := range lines {
entries := stateTableRE.FindStringSubmatch(v)
if entries != nil {
for _, f := range StateTable {
if f.PfctlTitle == entries[1] {
var err error
if f.Value, err = strconv.ParseInt(entries[2], 10, 64); err != nil {
return err
}
}
}
}
}

fields := make(map[string]interface{})
for _, v := range StateTable {
if v.Value == -1 {
return errMissingData(v.PfctlTitle)
}
fields[v.Field] = v.Value
}

acc.AddFields(measurement, fields, make(map[string]string))
return nil
}

func (pf *PF) callPfctl() (string, error) {
cmd := execCommand(pf.PfctlCommand, pf.PfctlArgs...)
out, oerr := cmd.Output()
if oerr != nil {
ee, ok := oerr.(*exec.ExitError)
if !ok {
return string(out), fmt.Errorf("error running %s: %s: (unable to get stderr)", pfctlCommand, oerr)
}
return string(out), fmt.Errorf("error running %s: %s: %s", pfctlCommand, oerr, ee.Stderr)
}
return string(out), oerr
}

var execLookPath = exec.LookPath
var execCommand = exec.Command

func (pf *PF) buildPfctlCmd() (string, []string, error) {
cmd, err := execLookPath(pfctlCommand)
if err != nil {
return "", nil, fmt.Errorf("can't locate %s: %v", pfctlCommand, err)
}
args := []string{"-s", "info"}
if pf.UseSudo {
args = append([]string{cmd}, args...)
cmd, err = execLookPath("sudo")
if err != nil {
return "", nil, fmt.Errorf("can't locate sudo: %v", err)
}
}
return cmd, args, nil
}

func init() {
inputs.Add("pf", func() telegraf.Input {
pf := new(PF)
pf.infoFunc = pf.callPfctl
return pf
})
}
Loading

0 comments on commit d758008

Please sign in to comment.