Skip to content

Commit

Permalink
Add input plugin for OpenBSD/FreeBSD pf
Browse files Browse the repository at this point in the history
  • Loading branch information
nferch committed Oct 29, 2017
1 parent 53b13a2 commit 02d0ad2
Show file tree
Hide file tree
Showing 5 changed files with 528 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 @@ -61,6 +61,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/ntpq"
_ "github.com/influxdata/telegraf/plugins/inputs/openldap"
_ "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/postgresql"
Expand Down
65 changes: 65 additions & 0 deletions plugins/inputs/pf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# PF Plugin

The pf plugin gathers counters from the FreeBSD/OpenBSD pf firewall.

The pfstat command requires read access to the device file /dev/pf. You have several options to grant 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)

### 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=2i 1507492593000000000
```
207 changes: 207 additions & 0 deletions plugins/inputs/pf/pf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// +build freebsd

package pf

import (
"errors"
"fmt"
"os/exec"
"reflect"
"regexp"
"strconv"
"strings"

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

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

type PF struct {
UseSudo bool
StateTable StateTable
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 {
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)
}

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

func (pf *PF) parsePfctlOutput(pfoutput string, acc telegraf.Accumulator) error {
lines := strings.Split(pfoutput, "\n")
stateTableFound := false
for i, line := range lines {
if stateTableHeaderRE.MatchString(line) {
endline := len(lines)
for j, el := range lines[i+1:] {
if anyTableHeaderRE.MatchString(el) {
endline = j + i + 1
break
}
}
if perr := pf.parseStateTable(lines[i+1:endline], acc); perr != nil {
return perr
}
stateTableFound = true
}
}
if !stateTableFound {
return errParseHeader
}
return nil
}

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

func (pf *PF) parseStateTable(lines []string, acc telegraf.Accumulator) error {
st := StateTable{}
tags, err := st.getTags()
if err != nil {
return fmt.Errorf("Can't retrieve struct tags: %v", err)
}
fMap := make(map[string]bool)
for i := 0; i < len(tags); i++ {
fMap[tags[i]] = false
}

for _, v := range lines {
entries := stateTableRE.FindStringSubmatch(v)
if entries != nil {
fs, err := st.setByTag(entries[1], entries[2])
if err != nil {
return errors.New("can't set statetable field from tag")
}
if fs {
fMap[entries[1]] = true
}
}
}

for k, v := range fMap {
if !v {
return errMissingData(k)
}
}

fields := make(map[string]interface{})
fields["entries"] = st.CurrentEntries
fields["searches"] = st.Searches
fields["inserts"] = st.Inserts
fields["removals"] = st.Removals
acc.AddFields(measurement, fields, make(map[string]string))
return nil
}

func (pf *PF) callPfctl() (string, error) {
c, err := pf.buildPfctlCmd()
if err != nil {
return "", fmt.Errorf("Can't construct commandline: %s", err)
}
out, oerr := c.Output()
if oerr != nil {
return string(out), fmt.Errorf("error running %s: %s: %s", pfctlCommand, oerr, oerr.(*exec.ExitError).Stderr)
}
return string(out), oerr
}

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

func (pf *PF) buildPfctlCmd() (*exec.Cmd, 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)
}
}
c := execCommand(cmd, args...)
return c, nil
}

type StateTable struct {
CurrentEntries uint32 `pfctl:"current entries"`
Searches uint64 `pfctl:"searches"`
Inserts uint64 `pfctl:"inserts"`
Removals uint64 `pfctl:"removals"`
}

func (pf *StateTable) getTags() ([]string, error) {
tags := []string{}
structVal := reflect.ValueOf(pf).Elem()
for i := 0; i < structVal.NumField(); i++ {
tags = append(tags, structVal.Type().Field(i).Tag.Get(pfctlCommand))
}
return tags, nil
}

// setByTag sets val for a struct field given the tag. returns false if tag not found.
func (pf *StateTable) setByTag(tag string, val string) (bool, error) {
structVal := reflect.ValueOf(pf).Elem()

for i := 0; i < structVal.NumField(); i++ {
tagField := structVal.Type().Field(i).Tag.Get(pfctlCommand)
if tagField == tag {
valueField := structVal.Field(i)
switch valueField.Type().Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
iVal, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return false, fmt.Errorf("Error parsing \"%s\" into uint: %s", val, err)
}
valueField.SetUint(iVal)
return true, nil
default:
return false, fmt.Errorf("unhandled struct type %s", valueField.Type())
}
}
}
return false, nil
}

func init() {
inputs.Add("pf", func() telegraf.Input {
pf := new(PF)
pf.infoFunc = pf.callPfctl
return pf
})
}
3 changes: 3 additions & 0 deletions plugins/inputs/pf/pf_nocompile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// +build !freebsd

package pf
Loading

0 comments on commit 02d0ad2

Please sign in to comment.