From ecd74f9062161def39e83e306f3497c59caab79a Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 12:58:38 +0200 Subject: [PATCH 01/14] add back missing logging rules --- pkg/iptables/iptables.go | 32 +++++++------- pkg/iptables/iptables_context.go | 71 +++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/pkg/iptables/iptables.go b/pkg/iptables/iptables.go index febba9d9..9d75a7be 100644 --- a/pkg/iptables/iptables.go +++ b/pkg/iptables/iptables.go @@ -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") diff --git a/pkg/iptables/iptables_context.go b/pkg/iptables/iptables_context.go index c1a704b0..984ddb7a 100644 --- a/pkg/iptables/iptables_context.go +++ b/pkg/iptables/iptables_context.go @@ -19,6 +19,7 @@ import ( ) const chainName = "CROWDSEC_CHAIN" +const loggingChainName = "CROWDSEC_LOG" type ipTablesContext struct { version string @@ -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() { @@ -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() { @@ -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...) From 5569514232037dff02f8991b4ad5830d27eb93c2 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:25:35 +0200 Subject: [PATCH 02/14] add test for iptables logging --- .../crowdsec-firewall-bouncer-logging.yaml | 15 ++++++ test/backends/iptables/test_iptables.py | 50 ++++++++++++++++++- test/backends/utils.py | 10 ++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/backends/iptables/crowdsec-firewall-bouncer-logging.yaml diff --git a/test/backends/iptables/crowdsec-firewall-bouncer-logging.yaml b/test/backends/iptables/crowdsec-firewall-bouncer-logging.yaml new file mode 100644 index 00000000..4d75774a --- /dev/null +++ b/test/backends/iptables/crowdsec-firewall-bouncer-logging.yaml @@ -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 diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 9edda8d2..f6a8bbc8 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -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): @@ -175,3 +177,49 @@ 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]) + 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 LOGGING_CHAIN_NAME 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 LOGGING_CHAIN_NAME in line] + + self.assertEqual(len(rules), 1) + + #Now, try to ping the IP + + run_cmd("ping", "-c", "1", "1.1.1.1") #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") + + assert 'blocked by crowdsec' in output \ No newline at end of file diff --git a/test/backends/utils.py b/test/backends/utils.py index d27b08e4..7af40256 100644 --- a/test/backends/utils.py +++ b/test/backends/utils.py @@ -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", + } \ No newline at end of file From 271e34e556cb18685f792bb63ad8cb34a5457206 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:26:12 +0200 Subject: [PATCH 03/14] fix config file path --- test/backends/iptables/test_iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index f6a8bbc8..800a92b3 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -181,7 +181,7 @@ def get_set_elements(set_name, with_timeout=False): class TestIPTablesLogging(unittest.TestCase): def setUp(self): - self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH]) + self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH_LOGGING]) self.lapi = MockLAPI() self.lapi.start() return super().setUp() From c208a18d0d6b27c43c7202aacd4bd23a0870f467 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:31:44 +0200 Subject: [PATCH 04/14] up --- test/backends/iptables/test_iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 800a92b3..4263933e 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -201,7 +201,7 @@ def testLogging(self): #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 LOGGING_CHAIN_NAME in line] + rules = [line for line in output.split("\n")] #2 rules: one logging, one generic drop self.assertEqual(len(rules), 2) From 2e5dd520d99b5276555818259ace8a2ccc46d5dd Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:38:20 +0200 Subject: [PATCH 05/14] up --- test/backends/iptables/test_iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 4263933e..1009b016 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -201,7 +201,7 @@ def testLogging(self): #Check if our logging chain is in place output = run_cmd("iptables", "-L", LOGGING_CHAIN_NAME) - rules = [line for line in output.split("\n")] + rules = [line for line in output.split("\n") if 'anywhere' in line] #2 rules: one logging, one generic drop self.assertEqual(len(rules), 2) From b441e1ab0e5d052b1d47f6b3413dd13dc80f7792 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:43:06 +0200 Subject: [PATCH 06/14] debug --- test/backends/iptables/test_iptables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 1009b016..3595acea 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -201,6 +201,7 @@ def testLogging(self): #Check if our logging chain is in place output = run_cmd("iptables", "-L", LOGGING_CHAIN_NAME) + print(output) rules = [line for line in output.split("\n") if 'anywhere' in line] #2 rules: one logging, one generic drop From 94febb5d6f952e41c6be50b24f4db796261b4a4c Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:43:54 +0200 Subject: [PATCH 07/14] debug --- test/backends/iptables/test_iptables.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 3595acea..a04cfb5f 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -201,7 +201,6 @@ def testLogging(self): #Check if our logging chain is in place output = run_cmd("iptables", "-L", LOGGING_CHAIN_NAME) - print(output) rules = [line for line in output.split("\n") if 'anywhere' in line] #2 rules: one logging, one generic drop @@ -210,7 +209,7 @@ def testLogging(self): #Check if the logging chain is called from the main chain output = run_cmd("iptables", "-L", CHAIN_NAME) - + print(output) rules = [line for line in output.split("\n") if LOGGING_CHAIN_NAME in line] self.assertEqual(len(rules), 1) From 053d00390442f15365a10320fb6e1d0da908080b Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:49:22 +0200 Subject: [PATCH 08/14] up --- test/backends/iptables/test_iptables.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index a04cfb5f..ec1c65a7 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -207,9 +207,15 @@ def testLogging(self): self.assertEqual(len(rules), 2) #Check if the logging chain is called from the main chain - output = run_cmd("iptables", "-L", CHAIN_NAME) - print(output) + + 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) From 5d9cdedc52c4a834d629c04881c4de5a10fe85a7 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:53:14 +0200 Subject: [PATCH 09/14] up --- test/backends/iptables/test_iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index ec1c65a7..8a334a42 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -222,7 +222,7 @@ def testLogging(self): #Now, try to ping the IP - run_cmd("ping", "-c", "1", "1.1.1.1") #We don't care about the output, we just want to trigger the rule + run_cmd("ping", "-c", "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 From b511c5e647c33c5fb67011ab1a15780d9d9cd90d Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 15:57:29 +0200 Subject: [PATCH 10/14] up --- test/backends/iptables/test_iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 8a334a42..295165ca 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -226,6 +226,6 @@ def testLogging(self): #Check if the firewall has logged the dropped response - output = run_cmd("dmesg") + output = run_cmd("dmesg | tail -n 10", shell=True) assert 'blocked by crowdsec' in output \ No newline at end of file From ef6463a697e584e050c73bd64a3925d9e0aabc32 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 16:00:23 +0200 Subject: [PATCH 11/14] up --- test/backends/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/backends/utils.py b/test/backends/utils.py index 7af40256..5335f9c5 100644 --- a/test/backends/utils.py +++ b/test/backends/utils.py @@ -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}") From 9fd1cc4d72953be3de0acb6db7d5ba897388e88c Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 16:09:32 +0200 Subject: [PATCH 12/14] up --- test/backends/iptables/test_iptables.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index 295165ca..c0d88695 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -194,7 +194,7 @@ def tearDown(self): 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") + d = new_decision("1.1.1.42") self.lapi.ds.insert_decisions([d]) sleep(3) @@ -222,7 +222,8 @@ def testLogging(self): #Now, try to ping the IP - run_cmd("ping", "-c", "1", "1.1.1.1", ignore_error=True) #We don't care about the output, we just want to trigger the rule + output = run_cmd("ping", "-c", "3", "1.1.1.1", ignore_error=True) #We don't care about the output, we just want to trigger the rule + print(output) #Check if the firewall has logged the dropped response From 83cc10a951e3c1594608c02e331e9b6bcc88569c Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 16:35:42 +0200 Subject: [PATCH 13/14] up --- test/backends/iptables/test_iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index c0d88695..fbdc73e6 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -222,7 +222,7 @@ def testLogging(self): #Now, try to ping the IP - output = run_cmd("ping", "-c", "3", "1.1.1.1", ignore_error=True) #We don't care about the output, we just want to trigger the rule + output = run_cmd("curl", "1.1.1.1", ignore_error=True) #We don't care about the output, we just want to trigger the rule print(output) #Check if the firewall has logged the dropped response From e3f030ec07a28ff06efc13a2f69ae43fb3c0d1b2 Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Mon, 23 Sep 2024 18:09:19 +0200 Subject: [PATCH 14/14] up --- test/backends/iptables/test_iptables.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/backends/iptables/test_iptables.py b/test/backends/iptables/test_iptables.py index fbdc73e6..079ce8e0 100644 --- a/test/backends/iptables/test_iptables.py +++ b/test/backends/iptables/test_iptables.py @@ -194,7 +194,7 @@ def tearDown(self): 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.42") + d = new_decision("1.1.1.1") self.lapi.ds.insert_decisions([d]) sleep(3) @@ -222,8 +222,7 @@ def testLogging(self): #Now, try to ping the IP - output = run_cmd("curl", "1.1.1.1", ignore_error=True) #We don't care about the output, we just want to trigger the rule - print(output) + 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