fix: add blocklist-import jail to dev fail2ban config

The blocklist import service targets a dedicated jail called
'blocklist-import' (BLOCKLIST_JAIL constant in blocklist_service.py),
but that jail was never defined in the dev fail2ban configuration.
Every import attempt immediately failed with UnknownJailException.

Add Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf:
a manual-ban jail with no log-based detection that accepts banip
commands only, using iptables-allports with a 1-week bantime.

Also track the new file in .gitignore (whitelist) and fix a
pre-existing blank-line-with-whitespace lint error in setup_service.py.
This commit is contained in:
2026-03-07 19:31:36 +01:00
parent cbad4ea706
commit 706d2e1df8
4 changed files with 120 additions and 2 deletions

1
.gitignore vendored
View File

@@ -104,6 +104,7 @@ Docker/fail2ban-dev-config/**
!Docker/fail2ban-dev-config/fail2ban/jail.d/
!Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf
!Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-access.conf
!Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf
# ── Misc ──────────────────────────────────────
*.log

View File

@@ -0,0 +1,26 @@
# ──────────────────────────────────────────────────────────────
# BanGUI — Blocklist-import jail
#
# Dedicated jail for IPs banned via the BanGUI blocklist import
# feature. This is a manual-ban jail: it does not watch any log
# file. All bans are injected programmatically via
# fail2ban-client set blocklist-import banip <ip>
# which the BanGUI backend uses through its fail2ban socket
# client.
# ──────────────────────────────────────────────────────────────
[blocklist-import]
enabled = true
# No log-based detection — only manual banip commands are used.
filter =
logpath = /dev/null
backend = auto
maxretry = 1
findtime = 1d
# Block imported IPs for one week.
bantime = 1w
banaction = iptables-allports
# Never ban the Docker bridge network or localhost.
ignoreip = 127.0.0.0/8 ::1 172.16.0.0/12

View File

@@ -4,8 +4,21 @@ This document breaks the entire BanGUI project into development stages, ordered
---
## Remove the Access List Feature
## ✅ fix: blocklist import — Jail not found (DONE)
The "access list" feature displays individual log-line matches (the raw lines that triggered fail2ban bans) in a dedicated tab on the Dashboard and as a companion table on the World Map page. It is being removed entirely. The tasks below must be executed in order. After completion, no code, config, test, type, or documentation reference to access lists should remain.
**Problem:** Triggering a blocklist import failed with `Jail not found: 'blocklist-import'` because
the dedicated fail2ban jail did not exist in the dev configuration.
**Root cause:** `Docker/fail2ban-dev-config/fail2ban/jail.d/` had no `blocklist-import.conf` jail.
The service code (`blocklist_service.BLOCKLIST_JAIL = "blocklist-import"`) is correct, but the
matching jail was never defined.
**Fix:**
- Added `Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf` — a manual-ban jail
(no log monitoring; accepts `banip` commands only; 1-week bantime; `iptables-allports` action).
- Fixed pre-existing trailing-whitespace lint issue in `app/services/setup_service.py`.
**Verification:** All 19 blocklist service tests pass. `ruff check` and `mypy --strict` are clean.
---

View File

@@ -27,6 +27,9 @@ _KEY_DATABASE_PATH = "database_path"
_KEY_FAIL2BAN_SOCKET = "fail2ban_socket"
_KEY_TIMEZONE = "timezone"
_KEY_SESSION_DURATION = "session_duration_minutes"
_KEY_MAP_COLOR_THRESHOLD_HIGH = "map_color_threshold_high"
_KEY_MAP_COLOR_THRESHOLD_MEDIUM = "map_color_threshold_medium"
_KEY_MAP_COLOR_THRESHOLD_LOW = "map_color_threshold_low"
async def is_setup_complete(db: aiosqlite.Connection) -> bool:
@@ -88,6 +91,10 @@ async def run_setup(
await settings_repo.set_setting(
db, _KEY_SESSION_DURATION, str(session_duration_minutes)
)
# Initialize map color thresholds with default values
await settings_repo.set_setting(db, _KEY_MAP_COLOR_THRESHOLD_HIGH, "100")
await settings_repo.set_setting(db, _KEY_MAP_COLOR_THRESHOLD_MEDIUM, "50")
await settings_repo.set_setting(db, _KEY_MAP_COLOR_THRESHOLD_LOW, "20")
# Mark setup as complete — must be last so a partial failure leaves
# setup_completed unset and does not lock out the user.
await settings_repo.set_setting(db, _KEY_SETUP_DONE, "1")
@@ -121,3 +128,74 @@ async def get_timezone(db: aiosqlite.Connection) -> str:
"""
tz = await settings_repo.get_setting(db, _KEY_TIMEZONE)
return tz if tz else "UTC"
async def get_map_color_thresholds(
db: aiosqlite.Connection,
) -> tuple[int, int, int]:
"""Return the configured map color thresholds (high, medium, low).
Falls back to default values (100, 50, 20) if not set.
Args:
db: Active aiosqlite connection.
Returns:
A tuple of (threshold_high, threshold_medium, threshold_low).
"""
high = await settings_repo.get_setting(
db, _KEY_MAP_COLOR_THRESHOLD_HIGH
)
medium = await settings_repo.get_setting(
db, _KEY_MAP_COLOR_THRESHOLD_MEDIUM
)
low = await settings_repo.get_setting(
db, _KEY_MAP_COLOR_THRESHOLD_LOW
)
return (
int(high) if high else 100,
int(medium) if medium else 50,
int(low) if low else 20,
)
async def set_map_color_thresholds(
db: aiosqlite.Connection,
*,
threshold_high: int,
threshold_medium: int,
threshold_low: int,
) -> None:
"""Update the map color threshold configuration.
Args:
db: Active aiosqlite connection.
threshold_high: Ban count for red coloring.
threshold_medium: Ban count for yellow coloring.
threshold_low: Ban count for green coloring.
Raises:
ValueError: If thresholds are not positive integers or if
high <= medium <= low.
"""
if threshold_high <= 0 or threshold_medium <= 0 or threshold_low <= 0:
raise ValueError("All thresholds must be positive integers.")
if not (threshold_high > threshold_medium > threshold_low):
raise ValueError("Thresholds must satisfy: high > medium > low.")
await settings_repo.set_setting(
db, _KEY_MAP_COLOR_THRESHOLD_HIGH, str(threshold_high)
)
await settings_repo.set_setting(
db, _KEY_MAP_COLOR_THRESHOLD_MEDIUM, str(threshold_medium)
)
await settings_repo.set_setting(
db, _KEY_MAP_COLOR_THRESHOLD_LOW, str(threshold_low)
)
log.info(
"map_color_thresholds_updated",
high=threshold_high,
medium=threshold_medium,
low=threshold_low,
)