Skip to content

Commit

Permalink
iptables: do not ignore logging configuration (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
blotus authored Sep 23, 2024
1 parent 689957d commit 2884c0f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 18 deletions.
32 changes: 18 additions & 14 deletions pkg/iptables/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,26 @@ func NewIPTables(config *cfg.BouncerConfig) (types.Backend, error) {
v6Sets := make(map[string]*ipsetcmd.IPSet)

ipv4Ctx := &ipTablesContext{
version: "v4",
SetName: config.BlacklistsIpv4,
SetType: config.SetType,
SetSize: config.SetSize,
Chains: []string{},
defaultSet: defaultSet,
target: target,
version: "v4",
SetName: config.BlacklistsIpv4,
SetType: config.SetType,
SetSize: config.SetSize,
Chains: []string{},
defaultSet: defaultSet,
target: target,
loggingEnabled: config.DenyLog,
loggingPrefix: config.DenyLogPrefix,
}
ipv6Ctx := &ipTablesContext{
version: "v6",
SetName: config.BlacklistsIpv6,
SetType: config.SetType,
SetSize: config.SetSize,
Chains: []string{},
defaultSet: defaultSet,
target: target,
version: "v6",
SetName: config.BlacklistsIpv6,
SetType: config.SetType,
SetSize: config.SetSize,
Chains: []string{},
defaultSet: defaultSet,
target: target,
loggingEnabled: config.DenyLog,
loggingPrefix: config.DenyLogPrefix,
}

ipv4Ctx.iptablesSaveBin, err = exec.LookPath("iptables-save")
Expand Down
71 changes: 70 additions & 1 deletion pkg/iptables/iptables_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
)

const chainName = "CROWDSEC_CHAIN"
const loggingChainName = "CROWDSEC_LOG"

type ipTablesContext struct {
version string
Expand All @@ -42,6 +43,9 @@ type ipTablesContext struct {
//Store the origin of the decisions, and use the index in the slice as the name
//This is not stable (ie, between two runs, the index of a set can change), but it's (probably) not an issue
originSetMapping []string

loggingEnabled bool
loggingPrefix string
}

func (ctx *ipTablesContext) setupChain() {
Expand Down Expand Up @@ -69,6 +73,43 @@ func (ctx *ipTablesContext) setupChain() {
continue
}
}

if ctx.loggingEnabled {
// Create the logging chain
cmd = []string{"-N", loggingChainName, "-t", "filter"}

c = exec.Command(ctx.iptablesBin, cmd...)

log.Infof("Creating logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

if out, err := c.CombinedOutput(); err != nil {
log.Errorf("error while creating logging chain : %v --> %s", err, string(out))
return
}

// Insert the logging rule
cmd = []string{"-I", loggingChainName, "-j", "LOG", "--log-prefix", ctx.loggingPrefix}

c = exec.Command(ctx.iptablesBin, cmd...)

log.Infof("Adding logging rule : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

if out, err := c.CombinedOutput(); err != nil {
log.Errorf("error while adding logging rule : %v --> %s", err, string(out))
}

// Add the desired target to the logging chain

cmd = []string{"-A", loggingChainName, "-j", ctx.target}

c = exec.Command(ctx.iptablesBin, cmd...)

log.Infof("Adding target rule to logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

if out, err := c.CombinedOutput(); err != nil {
log.Errorf("error while setting logging chain policy : %v --> %s", err, string(out))
}
}
}

func (ctx *ipTablesContext) deleteChain() {
Expand Down Expand Up @@ -105,10 +146,38 @@ func (ctx *ipTablesContext) deleteChain() {
if out, err := c.CombinedOutput(); err != nil {
log.Errorf("error while deleting chain : %v --> %s", err, string(out))
}

if ctx.loggingEnabled {
cmd = []string{"-F", loggingChainName}

c = exec.Command(ctx.iptablesBin, cmd...)

log.Infof("Flushing logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

if out, err := c.CombinedOutput(); err != nil {
log.Errorf("error while flushing logging chain : %v --> %s", err, string(out))
}

cmd = []string{"-X", loggingChainName}

c = exec.Command(ctx.iptablesBin, cmd...)

log.Infof("Deleting logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))

if out, err := c.CombinedOutput(); err != nil {
log.Errorf("error while deleting logging chain : %v --> %s", err, string(out))
}
}
}

func (ctx *ipTablesContext) createRule(setName string) {
cmd := []string{"-I", chainName, "-m", "set", "--match-set", setName, "src", "-j", ctx.target}
target := ctx.target

if ctx.loggingEnabled {
target = loggingChainName
}

cmd := []string{"-I", chainName, "-m", "set", "--match-set", setName, "src", "-j", target}

c := exec.Command(ctx.iptablesBin, cmd...)

Expand Down
15 changes: 15 additions & 0 deletions test/backends/iptables/crowdsec-firewall-bouncer-logging.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
mode: iptables
update_frequency: 0.1s
log_mode: stdout
log_dir: ./
log_level: info
api_url: http://127.0.0.1:8081/
api_key: 1237adaf7a1724ac68a3288828820a67
disable_ipv6: false
deny_action: DROP
deny_log: true
deny_log_prefix: "blocked by crowdsec"
supported_decisions_types:
- ban
iptables_chains:
- INPUT
56 changes: 55 additions & 1 deletion test/backends/iptables/test_iptables.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@
from time import sleep

from test.backends.mock_lapi import MockLAPI
from test.backends.utils import generate_n_decisions, run_cmd
from test.backends.utils import generate_n_decisions, run_cmd, new_decision


SCRIPT_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
PROJECT_ROOT = SCRIPT_DIR.parent.parent.parent
BINARY_PATH = PROJECT_ROOT.joinpath("crowdsec-firewall-bouncer")
CONFIG_PATH = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer.yaml")
CONFIG_PATH_LOGGING = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer-logging.yaml")

SET_NAME_IPV4 = "crowdsec-blacklists-0"
SET_NAME_IPV6 = "crowdsec6-blacklists-0"

RULES_CHAIN_NAME = "CROWDSEC_CHAIN"
LOGGING_CHAIN_NAME = "CROWDSEC_LOG"
CHAIN_NAME = "INPUT"

class TestIPTables(unittest.TestCase):
Expand Down Expand Up @@ -175,3 +177,55 @@ def get_set_elements(set_name, with_timeout=False):
to_add = member.find("elem").text
elements.add(to_add)
return elements


class TestIPTablesLogging(unittest.TestCase):
def setUp(self):
self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH_LOGGING])
self.lapi = MockLAPI()
self.lapi.start()
return super().setUp()

def tearDown(self):
self.fb.kill()
self.fb.wait()
self.lapi.stop()

def testLogging(self):
#We use 1.1.1.1 because we want to see some dropped packets in the logs
#We know this IP responds to ping, and the response will be dropped by the firewall
d = new_decision("1.1.1.1")
self.lapi.ds.insert_decisions([d])
sleep(3)

#Check if our logging chain is in place

output = run_cmd("iptables", "-L", LOGGING_CHAIN_NAME)
rules = [line for line in output.split("\n") if 'anywhere' in line]

#2 rules: one logging, one generic drop
self.assertEqual(len(rules), 2)

#Check if the logging chain is called from the main chain
output = run_cmd("iptables", "-L", CHAIN_NAME)

rules = [line for line in output.split("\n") if RULES_CHAIN_NAME in line]

self.assertEqual(len(rules), 1)

#Check if logging/drop chain is called from the rules chain
output = run_cmd("iptables", "-L", RULES_CHAIN_NAME)

rules = [line for line in output.split("\n") if LOGGING_CHAIN_NAME in line]

self.assertEqual(len(rules), 1)

#Now, try to ping the IP

output = run_cmd("curl", "--connect-timeout", "1", "1.1.1.1", ignore_error=True) #We don't care about the output, we just want to trigger the rule

#Check if the firewall has logged the dropped response

output = run_cmd("dmesg | tail -n 10", shell=True)

assert 'blocked by crowdsec' in output
14 changes: 12 additions & 2 deletions test/backends/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from ipaddress import ip_address


def run_cmd(*cmd, ignore_error=False):
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
def run_cmd(*cmd, ignore_error=False, shell=False):
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, shell=shell)
if not ignore_error and p.returncode:
raise SystemExit(f"{cmd} exited with non-zero code with following logs:\n {p.stdout}")

Expand Down Expand Up @@ -34,3 +34,13 @@ def generate_n_decisions(n: int, action="ban", dup_count=0, ipv4=True, duration=
decisions += decisions[: n % unique_decision_count]
decisions *= n // unique_decision_count
return decisions

def new_decision(ip: str):
return {
"value": ip,
"scope": "ip",
"type": "ban",
"origin": "script",
"duration": "4h",
"reason": "for testing",
}

0 comments on commit 2884c0f

Please sign in to comment.