Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add back missing logging rules for iptables #383

Merged
merged 14 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
}
Loading