Skip to content

Commit 2884c0f

Browse files
authored
iptables: do not ignore logging configuration (#383)
1 parent 689957d commit 2884c0f

File tree

5 files changed

+170
-18
lines changed

5 files changed

+170
-18
lines changed

pkg/iptables/iptables.go

+18-14
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,26 @@ func NewIPTables(config *cfg.BouncerConfig) (types.Backend, error) {
5656
v6Sets := make(map[string]*ipsetcmd.IPSet)
5757

5858
ipv4Ctx := &ipTablesContext{
59-
version: "v4",
60-
SetName: config.BlacklistsIpv4,
61-
SetType: config.SetType,
62-
SetSize: config.SetSize,
63-
Chains: []string{},
64-
defaultSet: defaultSet,
65-
target: target,
59+
version: "v4",
60+
SetName: config.BlacklistsIpv4,
61+
SetType: config.SetType,
62+
SetSize: config.SetSize,
63+
Chains: []string{},
64+
defaultSet: defaultSet,
65+
target: target,
66+
loggingEnabled: config.DenyLog,
67+
loggingPrefix: config.DenyLogPrefix,
6668
}
6769
ipv6Ctx := &ipTablesContext{
68-
version: "v6",
69-
SetName: config.BlacklistsIpv6,
70-
SetType: config.SetType,
71-
SetSize: config.SetSize,
72-
Chains: []string{},
73-
defaultSet: defaultSet,
74-
target: target,
70+
version: "v6",
71+
SetName: config.BlacklistsIpv6,
72+
SetType: config.SetType,
73+
SetSize: config.SetSize,
74+
Chains: []string{},
75+
defaultSet: defaultSet,
76+
target: target,
77+
loggingEnabled: config.DenyLog,
78+
loggingPrefix: config.DenyLogPrefix,
7579
}
7680

7781
ipv4Ctx.iptablesSaveBin, err = exec.LookPath("iptables-save")

pkg/iptables/iptables_context.go

+70-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
)
2020

2121
const chainName = "CROWDSEC_CHAIN"
22+
const loggingChainName = "CROWDSEC_LOG"
2223

2324
type ipTablesContext struct {
2425
version string
@@ -42,6 +43,9 @@ type ipTablesContext struct {
4243
//Store the origin of the decisions, and use the index in the slice as the name
4344
//This is not stable (ie, between two runs, the index of a set can change), but it's (probably) not an issue
4445
originSetMapping []string
46+
47+
loggingEnabled bool
48+
loggingPrefix string
4549
}
4650

4751
func (ctx *ipTablesContext) setupChain() {
@@ -69,6 +73,43 @@ func (ctx *ipTablesContext) setupChain() {
6973
continue
7074
}
7175
}
76+
77+
if ctx.loggingEnabled {
78+
// Create the logging chain
79+
cmd = []string{"-N", loggingChainName, "-t", "filter"}
80+
81+
c = exec.Command(ctx.iptablesBin, cmd...)
82+
83+
log.Infof("Creating logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))
84+
85+
if out, err := c.CombinedOutput(); err != nil {
86+
log.Errorf("error while creating logging chain : %v --> %s", err, string(out))
87+
return
88+
}
89+
90+
// Insert the logging rule
91+
cmd = []string{"-I", loggingChainName, "-j", "LOG", "--log-prefix", ctx.loggingPrefix}
92+
93+
c = exec.Command(ctx.iptablesBin, cmd...)
94+
95+
log.Infof("Adding logging rule : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))
96+
97+
if out, err := c.CombinedOutput(); err != nil {
98+
log.Errorf("error while adding logging rule : %v --> %s", err, string(out))
99+
}
100+
101+
// Add the desired target to the logging chain
102+
103+
cmd = []string{"-A", loggingChainName, "-j", ctx.target}
104+
105+
c = exec.Command(ctx.iptablesBin, cmd...)
106+
107+
log.Infof("Adding target rule to logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))
108+
109+
if out, err := c.CombinedOutput(); err != nil {
110+
log.Errorf("error while setting logging chain policy : %v --> %s", err, string(out))
111+
}
112+
}
72113
}
73114

74115
func (ctx *ipTablesContext) deleteChain() {
@@ -105,10 +146,38 @@ func (ctx *ipTablesContext) deleteChain() {
105146
if out, err := c.CombinedOutput(); err != nil {
106147
log.Errorf("error while deleting chain : %v --> %s", err, string(out))
107148
}
149+
150+
if ctx.loggingEnabled {
151+
cmd = []string{"-F", loggingChainName}
152+
153+
c = exec.Command(ctx.iptablesBin, cmd...)
154+
155+
log.Infof("Flushing logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))
156+
157+
if out, err := c.CombinedOutput(); err != nil {
158+
log.Errorf("error while flushing logging chain : %v --> %s", err, string(out))
159+
}
160+
161+
cmd = []string{"-X", loggingChainName}
162+
163+
c = exec.Command(ctx.iptablesBin, cmd...)
164+
165+
log.Infof("Deleting logging chain : %s %s", ctx.iptablesBin, strings.Join(cmd, " "))
166+
167+
if out, err := c.CombinedOutput(); err != nil {
168+
log.Errorf("error while deleting logging chain : %v --> %s", err, string(out))
169+
}
170+
}
108171
}
109172

110173
func (ctx *ipTablesContext) createRule(setName string) {
111-
cmd := []string{"-I", chainName, "-m", "set", "--match-set", setName, "src", "-j", ctx.target}
174+
target := ctx.target
175+
176+
if ctx.loggingEnabled {
177+
target = loggingChainName
178+
}
179+
180+
cmd := []string{"-I", chainName, "-m", "set", "--match-set", setName, "src", "-j", target}
112181

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
mode: iptables
2+
update_frequency: 0.1s
3+
log_mode: stdout
4+
log_dir: ./
5+
log_level: info
6+
api_url: http://127.0.0.1:8081/
7+
api_key: 1237adaf7a1724ac68a3288828820a67
8+
disable_ipv6: false
9+
deny_action: DROP
10+
deny_log: true
11+
deny_log_prefix: "blocked by crowdsec"
12+
supported_decisions_types:
13+
- ban
14+
iptables_chains:
15+
- INPUT

test/backends/iptables/test_iptables.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
from time import sleep
88

99
from test.backends.mock_lapi import MockLAPI
10-
from test.backends.utils import generate_n_decisions, run_cmd
10+
from test.backends.utils import generate_n_decisions, run_cmd, new_decision
1111

1212

1313
SCRIPT_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
1414
PROJECT_ROOT = SCRIPT_DIR.parent.parent.parent
1515
BINARY_PATH = PROJECT_ROOT.joinpath("crowdsec-firewall-bouncer")
1616
CONFIG_PATH = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer.yaml")
17+
CONFIG_PATH_LOGGING = SCRIPT_DIR.joinpath("crowdsec-firewall-bouncer-logging.yaml")
1718

1819
SET_NAME_IPV4 = "crowdsec-blacklists-0"
1920
SET_NAME_IPV6 = "crowdsec6-blacklists-0"
2021

2122
RULES_CHAIN_NAME = "CROWDSEC_CHAIN"
23+
LOGGING_CHAIN_NAME = "CROWDSEC_LOG"
2224
CHAIN_NAME = "INPUT"
2325

2426
class TestIPTables(unittest.TestCase):
@@ -175,3 +177,55 @@ def get_set_elements(set_name, with_timeout=False):
175177
to_add = member.find("elem").text
176178
elements.add(to_add)
177179
return elements
180+
181+
182+
class TestIPTablesLogging(unittest.TestCase):
183+
def setUp(self):
184+
self.fb = subprocess.Popen([BINARY_PATH, "-c", CONFIG_PATH_LOGGING])
185+
self.lapi = MockLAPI()
186+
self.lapi.start()
187+
return super().setUp()
188+
189+
def tearDown(self):
190+
self.fb.kill()
191+
self.fb.wait()
192+
self.lapi.stop()
193+
194+
def testLogging(self):
195+
#We use 1.1.1.1 because we want to see some dropped packets in the logs
196+
#We know this IP responds to ping, and the response will be dropped by the firewall
197+
d = new_decision("1.1.1.1")
198+
self.lapi.ds.insert_decisions([d])
199+
sleep(3)
200+
201+
#Check if our logging chain is in place
202+
203+
output = run_cmd("iptables", "-L", LOGGING_CHAIN_NAME)
204+
rules = [line for line in output.split("\n") if 'anywhere' in line]
205+
206+
#2 rules: one logging, one generic drop
207+
self.assertEqual(len(rules), 2)
208+
209+
#Check if the logging chain is called from the main chain
210+
output = run_cmd("iptables", "-L", CHAIN_NAME)
211+
212+
rules = [line for line in output.split("\n") if RULES_CHAIN_NAME in line]
213+
214+
self.assertEqual(len(rules), 1)
215+
216+
#Check if logging/drop chain is called from the rules chain
217+
output = run_cmd("iptables", "-L", RULES_CHAIN_NAME)
218+
219+
rules = [line for line in output.split("\n") if LOGGING_CHAIN_NAME in line]
220+
221+
self.assertEqual(len(rules), 1)
222+
223+
#Now, try to ping the IP
224+
225+
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
226+
227+
#Check if the firewall has logged the dropped response
228+
229+
output = run_cmd("dmesg | tail -n 10", shell=True)
230+
231+
assert 'blocked by crowdsec' in output

test/backends/utils.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from ipaddress import ip_address
33

44

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

@@ -34,3 +34,13 @@ def generate_n_decisions(n: int, action="ban", dup_count=0, ipv4=True, duration=
3434
decisions += decisions[: n % unique_decision_count]
3535
decisions *= n // unique_decision_count
3636
return decisions
37+
38+
def new_decision(ip: str):
39+
return {
40+
"value": ip,
41+
"scope": "ip",
42+
"type": "ban",
43+
"origin": "script",
44+
"duration": "4h",
45+
"reason": "for testing",
46+
}

0 commit comments

Comments
 (0)