Skip to content

Commit

Permalink
Merge pull request #37 from sbz/add_pf_support
Browse files Browse the repository at this point in the history
Add pf support
  • Loading branch information
registergoofy authored Apr 15, 2021
2 parents 4e43a6b + e218300 commit 58744a1
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 1 deletion.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Supported firewalls:
- iptables (IPv4 :heavy_check_mark: / IPv6 :heavy_check_mark: )
- nftables (IPv4 :heavy_check_mark: / IPv6 :heavy_check_mark: )
- ipset only (IPv4 :heavy_check_mark: / IPv6 :heavy_check_mark: )
- pf (IPV4 :heavy_check_mark: / IPV6 :heavy_check_mark: )

## Installation

Expand Down Expand Up @@ -87,7 +88,7 @@ iptables_chains:
- FORWARD
```
- `mode` can be set to `iptables`, `nftables` or `ipset`
- `mode` can be set to `iptables`, `nftables` , `ipset` or `pf`
- `update_frequency` controls how often the bouncer is going to query the local API
- `api_url` and `api_key` control local API parameters.
- `iptables_chains` allows (in _iptables_ mode) to control in which chain rules are going to be inserted. (if empty, bouncer will only maintain ipset lists)
Expand All @@ -111,3 +112,12 @@ logs can be found in `/var/log/cs-firewall-bouncer.log`
- mode `nftables` relies on github.com/google/nftables to create table, chain and set.
- mode `iptables` relies on `iptables` and `ipset` commands to insert `match-set` directives and maintain associated ipsets
- mode `ipset` relies on `ipset` and only manage contents of the sets (they need to exist at startup and will be flushed rather than created)
- mode `pf` relies on `pfctl` command to alter the tables. You are required to create the following tables on your `pf.conf` configuration:
```
# create crowdsec ipv4 table
table <crowdsec-blacklists> persist
# create crowdsec ipv6 table
table <crowdsec6-blacklists> persist
```
32 changes: 32 additions & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"runtime"

"github.com/crowdsecurity/crowdsec/pkg/models"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -48,6 +49,19 @@ func (b *backendCTX) Delete(decision *models.Decision) error {
return nil
}

func isPFSupported(runtimeOS string) bool {
var supported bool

switch runtimeOS {
case "openbsd", "freebsd":
supported = true
default:
supported = false
}

return supported
}

func newBackend(config *bouncerConfig) (*backendCTX, error) {
var ok bool

Expand All @@ -58,6 +72,9 @@ func newBackend(config *bouncerConfig) (*backendCTX, error) {
}
switch config.Mode {
case "iptables", "ipset":
if runtime.GOOS != "linux" {
return nil, fmt.Errorf("iptables and ipset is linux only")
}
tmpCtx, err := newIPTables(config)
if err != nil {
return nil, err
Expand All @@ -67,6 +84,9 @@ func newBackend(config *bouncerConfig) (*backendCTX, error) {
return nil, fmt.Errorf("unexpected type '%T' for iptables context", tmpCtx)
}
case "nftables":
if runtime.GOOS != "linux" {
return nil, fmt.Errorf("nftables is linux only")
}
tmpCtx, err := newNFTables(config)
if err != nil {
return nil, err
Expand All @@ -75,6 +95,18 @@ func newBackend(config *bouncerConfig) (*backendCTX, error) {
if !ok {
return nil, fmt.Errorf("unexpected type '%T' for nftables context", tmpCtx)
}
case "pf":
if !isPFSupported(runtime.GOOS) {
return nil, fmt.Errorf("pf mode is supported only for openbsd and freebsd")
}
tmpCtx, err := newPF(config)
if err != nil {
return nil, err
}
b.firewall, ok = tmpCtx.(backend)
if !ok {
return nil, fmt.Errorf("unexpected type '%T' for pf context", tmpCtx)
}
default:
return b, fmt.Errorf("firewall '%s' is not supported", config.Mode)
}
Expand Down
2 changes: 2 additions & 0 deletions iptables.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build linux

package main

import (
Expand Down
2 changes: 2 additions & 0 deletions iptables_context.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build linux

package main

import (
Expand Down
7 changes: 7 additions & 0 deletions iptables_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !linux

package main

func newIPTables(config *bouncerConfig) (interface{}, error) {
return nil, nil
}
2 changes: 2 additions & 0 deletions nftables.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build linux

package main

import (
Expand Down
7 changes: 7 additions & 0 deletions nftables_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !linux

package main

func newNFTables(config *bouncerConfig) (interface{}, error) {
return nil, nil
}
211 changes: 211 additions & 0 deletions pf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// +build openbsd freebsd

package main

import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"

"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/pkg/errors"

log "github.com/sirupsen/logrus"
)

type pfContext struct {
proto string
table string
version string
}

type pf struct {
inet *pfContext
inet6 *pfContext
}

const (
backendName = "pf"

pfinetTable = "crowdsec-blacklists"
pfinet6Table = "crowdsec6-blacklists"

pfctlCmd = "/sbin/pfctl"
pfDevice = "/dev/pf"

addBanFormat = "%s: add ban on %s for %s sec (%s)"
delBanFormat = "%s: del ban on %s for %s sec (%s)"
)

func newPF(config *bouncerConfig) (interface{}, error) {
ret := &pf{}

inetCtx := &pfContext{
table: pfinetTable,
proto: "inet",
version: "ipv4",
}

inet6Ctx := &pfContext{
table: pfinet6Table,
proto: "inet6",
version: "ipv6",
}

ret.inet = inetCtx

if config.DisableIPV6 {
return ret, nil
}

ret.inet6 = inet6Ctx

return ret, nil
}

func (ctx *pfContext) checkTable() error {
log.Infof("Checking pf table: %s", ctx.table)

cmd := exec.Command(pfctlCmd, "-s", "Tables")
out, err := cmd.CombinedOutput()

if err != nil {
return errors.Wrapf(err, "pfctl error : %v - %s", err, string(out))
} else if !strings.Contains(string(out), ctx.table) {
return errors.Errorf("table %s doesn't exist", ctx.table)
}

return nil
}

func (ctx *pfContext) shutDown() error {
cmd := exec.Command(pfctlCmd, "-t", ctx.table, "-T", "flush")
log.Infof("pf table clean-up : %s", cmd.String())
if out, err := cmd.CombinedOutput(); err != nil {
log.Errorf("Error while flushing table (%s): %v - %s", err, string(out))
}

return nil
}

func (ctx *pfContext) Add(decision *models.Decision) error {
banDuration, err := time.ParseDuration(*decision.Duration)
if err != nil {
return err
}
log.Debugf(addBanFormat, backendName, *decision.Value, strconv.Itoa(int(banDuration.Seconds())), *decision.Scenario)
cmd := exec.Command(pfctlCmd, "-t", ctx.table, "-T", "add", *decision.Value)
log.Debugf("pfctl add : %s", cmd.String())
if out, err := cmd.CombinedOutput(); err != nil {
log.Infof("Error while adding to table (%s): %v --> %s", cmd.String(), err, string(out))
}

return nil
}

func (ctx *pfContext) Delete(decision *models.Decision) error {
banDuration, err := time.ParseDuration(*decision.Duration)
if err != nil {
return err
}
log.Debugf(delBanFormat, backendName, *decision.Value, strconv.Itoa(int(banDuration.Seconds())), *decision.Scenario)
cmd := exec.Command(pfctlCmd, "-t", ctx.table, "-T", "delete", *decision.Value)
log.Debugf("pfctl del : %s", cmd.String())
if out, err := cmd.CombinedOutput(); err != nil {
log.Infof("Error while deleting from table (%s): %v --> %s", cmd.String(), err, string(out))
}
return nil
}

func initPF(ctx *pfContext) error {

if err := ctx.shutDown(); err != nil {
return fmt.Errorf("pf table flush failed: %s", err.Error())
}
if err := ctx.checkTable(); err != nil {
return fmt.Errorf("pf init failed: %s", err.Error())
}
log.Infof("%s initiated for %s", backendName, ctx.version)

return nil
}

func (pf *pf) Init() error {

if _, err := os.Stat(pfDevice); err != nil {
return fmt.Errorf("%s device not found: %s", pfDevice, err.Error())
}

if _, err := exec.LookPath(pfctlCmd); err != nil {
return fmt.Errorf("%s command not found: %s", pfctlCmd, err.Error())
}

if err := initPF(pf.inet); err != nil {
return err
}

if pf.inet6 != nil {
if err := initPF(pf.inet6); err != nil {
return err
}
}

return nil
}

func (pf *pf) Add(decision *models.Decision) error {
if strings.Contains(*decision.Value, ":") && pf.inet6 != nil { // inet6
if pf.inet6 != nil {
if err := pf.inet6.Add(decision); err != nil {
return fmt.Errorf("failed to add ban ip '%s' to inet6 table", *decision.Value)
}
} else {
log.Debugf("not adding '%s' because ipv6 is disabled", *decision.Value)
return nil
}
} else { // inet
if err := pf.inet.Add(decision); err != nil {
return fmt.Errorf("failed adding ban ip '%s' to inet table", *decision.Value)
}
}

return nil
}

func (pf *pf) Delete(decision *models.Decision) error {
if strings.Contains(*decision.Value, ":") { // ipv6
if pf.inet6 != nil {
if err := pf.inet6.Delete(decision); err != nil {
return fmt.Errorf("failed to remove ban ip '%s' from inet6 table", *decision.Value)
}
} else {
log.Debugf("not removing '%s' because ipv6 is disabled", *decision.Value)
return nil
}
} else { // ipv4
if err := pf.inet.Delete(decision); err != nil {
return fmt.Errorf("failed to remove ban ip '%s' from inet6 table", *decision.Value)
}
}

return nil
}

func (pf *pf) ShutDown() error {
log.Infof("flushing 'crowdsec' table(s)")

if err := pf.inet.shutDown(); err != nil {
return fmt.Errorf("unable to flush %s table (%s): ", pf.inet.version, pf.inet.table)
}

if pf.inet6 != nil {
if err := pf.inet6.shutDown(); err != nil {
return fmt.Errorf("unable to flush %s table (%s): ", pf.inet6.version, pf.inet6.table)
}
}

return nil
}
7 changes: 7 additions & 0 deletions pf_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build !openbsd,!freebsd

package main

func newPF(config *bouncerConfig) (interface{}, error) {
return nil, nil
}

0 comments on commit 58744a1

Please sign in to comment.