Fix fail2ban runtime errors: jail not found, action locks, log noise

This commit implements fixes for three independent bugs in the fail2ban configuration and integration layer:

1. Task 1: Detect UnknownJailException and prevent silent failures
   - Added JailNotFoundError detection in jail_service.reload_all()
   - Enhanced error handling in config_file_service to catch JailNotFoundError
   - Added specific error message with logpath validation hints
   - Added rollback test for this scenario

2. Task 2: Fix iptables-allports exit code 4 (xtables lock contention)
   - Added global banaction setting in jail.conf with -w 5 lockingopt
   - Removed redundant per-jail banaction overrides from bangui-sim and blocklist-import
   - Added production compose documentation note

3. Task 3: Suppress log noise from unsupported backend/idle commands
   - Implemented capability detection to cache command support status
   - Double-check locking to minimize lock contention
   - Avoids sending unsupported get <jail> backend/idle commands
   - Returns default values without socket calls when unsupported

All changes include comprehensive tests and maintain backward compatibility.
This commit is contained in:
2026-03-15 10:57:00 +01:00
parent 1e33220f59
commit f62785aaf2
8 changed files with 446 additions and 145 deletions

View File

@@ -3110,4 +3110,68 @@ class TestActivateJailRollback:
assert result.active is False
assert result.recovered is False
async def test_activate_jail_rollback_on_jail_not_found_error(
self, tmp_path: Path
) -> None:
"""Rollback when reload_all raises JailNotFoundError (invalid config).
When fail2ban cannot create a jail due to invalid configuration
(e.g., missing logpath), it raises UnknownJailException which becomes
JailNotFoundError. This test verifies proper handling and rollback.
Expects:
- The .local file is restored to its original content.
- The response indicates recovered=True.
- The error message mentions the logpath issue.
"""
from app.models.config import ActivateJailRequest, JailValidationResult
from app.services.jail_service import JailNotFoundError
_write(tmp_path / "jail.conf", JAIL_CONF)
original_local = "[apache-auth]\nenabled = false\n"
local_path = tmp_path / "jail.d" / "apache-auth.local"
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_text(original_local)
req = ActivateJailRequest()
reload_call_count = 0
async def reload_side_effect(socket_path: str, **kwargs: object) -> None:
nonlocal reload_call_count
reload_call_count += 1
if reload_call_count == 1:
# Simulate UnknownJailException from fail2ban due to missing logpath.
raise JailNotFoundError("apache-auth")
# Recovery reload succeeds.
with (
patch(
"app.services.config_file_service._get_active_jail_names",
new=AsyncMock(return_value=set()),
),
patch("app.services.config_file_service.jail_service") as mock_js,
patch(
"app.services.config_file_service._probe_fail2ban_running",
new=AsyncMock(return_value=True),
),
patch(
"app.services.config_file_service._validate_jail_config_sync",
return_value=JailValidationResult(
jail_name="apache-auth", valid=True
),
),
):
mock_js.reload_all = AsyncMock(side_effect=reload_side_effect)
mock_js.JailNotFoundError = JailNotFoundError
result = await activate_jail(
str(tmp_path), "/fake.sock", "apache-auth", req
)
assert result.active is False
assert result.recovered is True
assert local_path.read_text() == original_local
# Verify the error message mentions logpath issues.
assert "logpath" in result.message.lower() or "check that all logpath" in result.message.lower()