From 420ea18fd96839b84d36ae9d17f18db17a2acd4c Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 25 Apr 2026 18:39:30 +0200 Subject: [PATCH] Refactor backend services and utilities - Update service layer implementations - Improve configuration handling utilities - Update documentation tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Docs/Tasks.md | 23 ------------ backend/app/services/action_config_service.py | 2 -- backend/app/services/ban_service.py | 16 ++++----- backend/app/services/config_service.py | 17 +++++---- .../app/services/fail2ban_metadata_service.py | 4 +-- backend/app/services/health_service.py | 7 ++-- backend/app/services/jail_config_service.py | 2 -- backend/app/services/jail_service.py | 35 +++++++++---------- backend/app/services/log_service.py | 5 ++- backend/app/services/server_service.py | 9 +++-- backend/app/utils/config_file_utils.py | 8 ++--- backend/app/utils/constants.py | 7 ++-- 12 files changed, 52 insertions(+), 83 deletions(-) diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 1d1b83b..7020181 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,26 +1,3 @@ -### T-07 · Break cross-service import: `jail_config_service` imports `jail_service` - -**Where found:** `backend/app/services/jail_config_service.py` — `import app.services.jail_service as jail_service` - -**Why this is needed:** Services at the same layer should not depend on each other. Shared logic should move to a lower-level utility. This creates a dependency cycle risk and makes both services harder to test independently. - -**Goal:** Shared socket operations extracted to `app/utils/` or `app/utils/jail_socket.py`. No service imports a sibling service. - -**What to do:** -1. Identify which functions from `jail_service` are called by `jail_config_service`. -2. Extract shared low-level socket helpers to `app/utils/jail_socket.py` (or extend `fail2ban_client.py`). -3. Update both services to import from the utility layer. - -**Possible traps and issues:** -- Some `jail_service` functions that are called may themselves import geo, config, or other services — trace the full dependency graph before extracting. -- APScheduler tasks reference `jail_service` — ensure they still work after any reorganisation. - -**Docs changes needed:** `Docs/Architekture.md` — add rule: services must not import sibling services. - -**Doc references:** `Docs/Architekture.md` - ---- - ### T-08 · `_SOCKET_TIMEOUT` defined 6× — `constants.py` constant unused **Where found:** `backend/app/utils/constants.py:16` (defines `FAIL2BAN_SOCKET_TIMEOUT_SECONDS = 5.0` but is never imported); `ban_service.py` (5.0), `jail_service.py` (10.0), `config_service.py` (10.0), `server_service.py` (10.0), `log_service.py` (10.0), `jail_config_service.py` (10.0), `config_file_utils.py` (10.0) diff --git a/backend/app/services/action_config_service.py b/backend/app/services/action_config_service.py index 36b5bcd..fcf3e52 100644 --- a/backend/app/services/action_config_service.py +++ b/backend/app/services/action_config_service.py @@ -65,8 +65,6 @@ async def _get_active_jail_names(socket_path: str) -> set[str]: # Constants # --------------------------------------------------------------------------- -_SOCKET_TIMEOUT: float = 10.0 - # Allowlist pattern for action names used in path construction. _SAFE_ACTION_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") diff --git a/backend/app/services/ban_service.py b/backend/app/services/ban_service.py index 035f288..499e1a7 100644 --- a/backend/app/services/ban_service.py +++ b/backend/app/services/ban_service.py @@ -43,7 +43,11 @@ from app.models.ban import ( from app.repositories import fail2ban_db_repo from app.repositories import history_archive_repo as default_history_archive_repo from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service -from app.utils.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE +from app.utils.constants import ( + DEFAULT_PAGE_SIZE, + FAIL2BAN_SOCKET_TIMEOUT, + MAX_PAGE_SIZE, +) from app.utils.fail2ban_client import ( Fail2BanClient, ) @@ -73,10 +77,6 @@ async def get_fail2ban_db_path(socket_path: str) -> str: # Constants # --------------------------------------------------------------------------- -_SOCKET_TIMEOUT: float = 5.0 - - - async def ban_ip(socket_path: str, jail: str, ip: str) -> None: """Ban an IP address in the specified jail.""" @@ -85,7 +85,7 @@ async def ban_ip(socket_path: str, jail: str, ip: str) -> None: except ValueError as exc: raise ValueError(f"Invalid IP address: {ip!r}") from exc - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["set", jail, "banip", ip])) @@ -102,7 +102,7 @@ async def unban_ip(socket_path: str, ip: str, jail: str | None = None) -> None: except ValueError as exc: raise ValueError(f"Invalid IP address: {ip!r}") from exc - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) if jail is None: ok(await client.send(["unban", ip])) @@ -254,7 +254,7 @@ async def get_active_bans( cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) global_status = to_dict(ok(await client.send(["status"]))) jail_list_raw: str = str(global_status.get("Jail list", "") or "").strip() diff --git a/backend/app/services/config_service.py b/backend/app/services/config_service.py index 7a41365..0baa96c 100644 --- a/backend/app/services/config_service.py +++ b/backend/app/services/config_service.py @@ -51,6 +51,7 @@ from app.services.settings_service import ( from app.services.settings_service import ( set_map_color_thresholds as util_set_map_color_thresholds, ) +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT from app.utils.fail2ban_client import Fail2BanClient from app.utils.fail2ban_response import ( ensure_list, @@ -61,8 +62,6 @@ from app.utils.fail2ban_response import ( log: structlog.stdlib.BoundLogger = structlog.get_logger() -_SOCKET_TIMEOUT: float = 10.0 - # --------------------------------------------------------------------------- # Custom exceptions # --------------------------------------------------------------------------- @@ -134,7 +133,7 @@ async def get_jail_config(socket_path: str, name: str) -> JailConfigResponse: JailNotFoundError: If *name* is not a known jail. ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) # Verify existence. try: @@ -207,7 +206,7 @@ async def list_jail_configs(socket_path: str) -> JailConfigListResponse: Raises: ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) global_status = to_dict(ok(await client.send(["status"]))) jail_list_raw: str = str(global_status.get("Jail list", "") or "").strip() @@ -272,7 +271,7 @@ async def update_jail_config( if err: raise ConfigValidationError(f"Invalid regex in 'prefregex': {err!r} (pattern: {update.prefregex!r})") - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) # Verify existence. try: @@ -391,7 +390,7 @@ async def get_global_config(socket_path: str) -> GlobalConfigResponse: Raises: ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) ( log_level_raw, @@ -424,7 +423,7 @@ async def update_global_config(socket_path: str, update: GlobalConfigUpdate) -> ConfigOperationError: If a ``set`` command is rejected. ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) async def _set_global(key: str, value: Fail2BanToken) -> None: try: @@ -476,7 +475,7 @@ async def add_log_path( ConfigOperationError: If the command is rejected by fail2ban. ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["status", jail, "short"])) @@ -513,7 +512,7 @@ async def delete_log_path( ConfigOperationError: If the command is rejected by fail2ban. ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["status", jail, "short"])) diff --git a/backend/app/services/fail2ban_metadata_service.py b/backend/app/services/fail2ban_metadata_service.py index 61a2bf3..eeb25c8 100644 --- a/backend/app/services/fail2ban_metadata_service.py +++ b/backend/app/services/fail2ban_metadata_service.py @@ -6,6 +6,7 @@ import asyncio import structlog +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT_FAST from app.utils.fail2ban_client import ( Fail2BanClient, Fail2BanConnectionError, @@ -64,8 +65,7 @@ class Fail2BanMetadataService: async def _resolve_db_path(self, socket_path: str) -> str: """Query fail2ban for the configured database path.""" - socket_timeout: float = 5.0 - async with Fail2BanClient(socket_path, timeout=socket_timeout) as client: + async with Fail2BanClient(socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT_FAST) as client: response = await client.send(["get", "dbfile"]) if not isinstance(response, tuple) or len(response) != 2: diff --git a/backend/app/services/health_service.py b/backend/app/services/health_service.py index 8a30854..6112da3 100644 --- a/backend/app/services/health_service.py +++ b/backend/app/services/health_service.py @@ -18,6 +18,7 @@ import structlog from app import __version__ from app.models.config import ServiceStatusResponse from app.models.server import ServerStatus +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT_FAST from app.utils.fail2ban_client import ( Fail2BanClient, Fail2BanCommand, @@ -35,8 +36,6 @@ log: structlog.stdlib.BoundLogger = structlog.get_logger() # Internal helpers # --------------------------------------------------------------------------- -_SOCKET_TIMEOUT: float = 5.0 - T = TypeVar("T") @@ -93,7 +92,7 @@ async def get_service_status( if server_status.online: client = Fail2BanClient( socket_path=socket_path, - timeout=_SOCKET_TIMEOUT, + timeout=FAIL2BAN_SOCKET_TIMEOUT_FAST, ) log_level_raw, log_target_raw = await asyncio.gather( _safe_get_typed(client, ["get", "loglevel"], "INFO"), @@ -129,7 +128,7 @@ async def get_service_status( async def probe( socket_path: str, - timeout: float = _SOCKET_TIMEOUT, + timeout: float = FAIL2BAN_SOCKET_TIMEOUT_FAST, ) -> ServerStatus: """Probe the fail2ban daemon and return a :class:`~app.models.server.ServerStatus`. diff --git a/backend/app/services/jail_config_service.py b/backend/app/services/jail_config_service.py index 9e3adfe..58f0e35 100644 --- a/backend/app/services/jail_config_service.py +++ b/backend/app/services/jail_config_service.py @@ -70,8 +70,6 @@ async def _get_active_jail_names(socket_path: str) -> set[str]: # Constants # --------------------------------------------------------------------------- -_SOCKET_TIMEOUT: float = 10.0 - # Sections that are not jail definitions. _META_SECTIONS: frozenset[str] = frozenset({"INCLUDES", "DEFAULT"}) diff --git a/backend/app/services/jail_service.py b/backend/app/services/jail_service.py index e43de7d..7bcc5b9 100644 --- a/backend/app/services/jail_service.py +++ b/backend/app/services/jail_service.py @@ -31,6 +31,7 @@ from app.models.jail import ( ) from app.services import geo_service from app.utils.config_file_utils import start_daemon, wait_for_fail2ban +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT from app.utils.fail2ban_client import ( Fail2BanClient, Fail2BanCommand, @@ -73,8 +74,6 @@ class IpLookupResult(TypedDict): # Constants # --------------------------------------------------------------------------- -_SOCKET_TIMEOUT: float = 10.0 - # Capability detection for optional fail2ban transmitter commands (backend, idle). # These commands are not supported in all fail2ban versions. Caching the result # avoids sending unsupported commands every polling cycle and spamming the @@ -223,7 +222,7 @@ async def list_jails(socket_path: str) -> JailListResponse: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) # 1. Fetch global status to get jail names. global_status = to_dict(ok(await client.send(["status"]))) @@ -376,7 +375,7 @@ async def get_jail(socket_path: str, name: str) -> JailDetailResponse: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) # Verify the jail exists by sending a status command first. try: @@ -493,7 +492,7 @@ async def start_jail(socket_path: str, name: str) -> None: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["start", name])) log.info("jail_started", jail=name) @@ -518,7 +517,7 @@ async def stop_jail(socket_path: str, name: str) -> None: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["stop", name])) log.info("jail_stopped", jail=name) @@ -548,7 +547,7 @@ async def set_idle(socket_path: str, name: str, *, on: bool) -> None: cannot be reached. """ state = "on" if on else "off" - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["set", name, "idle", state])) log.info("jail_idle_toggled", jail=name, idle=on) @@ -578,7 +577,7 @@ async def reload_jail(socket_path: str, name: str) -> None: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["reload", name, [], [["start", name]]])) log.info("jail_reloaded", jail=name) @@ -608,7 +607,7 @@ async def restart(socket_path: str) -> None: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["stop"])) log.info("fail2ban_stopped_for_restart") @@ -619,7 +618,7 @@ async def restart(socket_path: str) -> None: async def restart_daemon( socket_path: str, start_cmd_parts: list[str], - max_wait_seconds: float = _SOCKET_TIMEOUT, + max_wait_seconds: float = FAIL2BAN_SOCKET_TIMEOUT, ) -> bool: """Restart the fail2ban daemon and verify it comes back online. @@ -781,7 +780,7 @@ async def get_jail_banned_ips( # Clamp page_size to the allowed maximum. page_size = min(page_size, _MAX_PAGE_SIZE) - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) # Verify the jail exists. try: @@ -896,7 +895,7 @@ async def get_ignore_list(socket_path: str, name: str) -> list[str]: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: raw = ok(await client.send(["get", name, "ignoreip"])) return ensure_list(raw) @@ -926,7 +925,7 @@ async def add_ignore_ip(socket_path: str, name: str, ip: str) -> None: except ValueError as exc: raise ValueError(f"Invalid IP address or network: {ip!r}") from exc - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["set", name, "addignoreip", ip])) log.info("ignore_ip_added", jail=name, ip=ip) @@ -950,7 +949,7 @@ async def del_ignore_ip(socket_path: str, name: str, ip: str) -> None: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["set", name, "delignoreip", ip])) log.info("ignore_ip_removed", jail=name, ip=ip) @@ -975,7 +974,7 @@ async def get_ignore_self(socket_path: str, name: str) -> bool: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: raw = ok(await client.send(["get", name, "ignoreself"])) return bool(raw) @@ -1000,7 +999,7 @@ async def set_ignore_self(socket_path: str, name: str, *, on: bool) -> None: cannot be reached. """ value = "true" if on else "false" - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: ok(await client.send(["set", name, "ignoreself", value])) log.info("ignore_self_toggled", jail=name, on=on) @@ -1047,7 +1046,7 @@ async def lookup_ip( except ValueError as exc: raise ValueError(f"Invalid IP address: {ip!r}") from exc - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) with contextlib.suppress(ValueError, Fail2BanConnectionError): # Use fail2ban's "banned " command which checks all jails. @@ -1120,7 +1119,7 @@ async def unban_all_ips(socket_path: str) -> int: ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket cannot be reached. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) count: int = int(str(ok(await client.send(["unban", "--all"])) or 0)) log.info("all_ips_unbanned", count=count) return count diff --git a/backend/app/services/log_service.py b/backend/app/services/log_service.py index 76978ca..eef197e 100644 --- a/backend/app/services/log_service.py +++ b/backend/app/services/log_service.py @@ -21,6 +21,7 @@ from app.models.config import ( RegexTestResponse, ) from app.utils.async_utils import run_blocking +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT from app.utils.fail2ban_client import ( Fail2BanClient, Fail2BanConnectionError, @@ -30,8 +31,6 @@ from app.utils.fail2ban_response import ok log: structlog.stdlib.BoundLogger = structlog.get_logger() -_SOCKET_TIMEOUT: float = 10.0 - _NON_FILE_LOG_TARGETS: frozenset[str] = frozenset( {"STDOUT", "STDERR", "SYSLOG", "SYSTEMD-JOURNAL"} ) @@ -85,7 +84,7 @@ async def read_fail2ban_log( validates that the target is a readable file, then returns the last *lines* entries optionally filtered by *filter_text*. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) log_level_raw, log_target_raw = await asyncio.gather( _safe_get_typed(client, ["get", "loglevel"], "INFO"), diff --git a/backend/app/services/server_service.py b/backend/app/services/server_service.py index a4b99b5..31f1462 100644 --- a/backend/app/services/server_service.py +++ b/backend/app/services/server_service.py @@ -16,6 +16,7 @@ import structlog from app.exceptions import ServerOperationError from app.models.server import ServerSettings, ServerSettingsResponse, ServerSettingsUpdate +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT from app.utils.fail2ban_client import Fail2BanClient, Fail2BanCommand, Fail2BanResponse from app.utils.fail2ban_response import ok @@ -28,8 +29,6 @@ type Fail2BanSettingValue = str | int | bool log: structlog.stdlib.BoundLogger = structlog.get_logger() -_SOCKET_TIMEOUT: float = 10.0 - def _to_int(value: object | None, default: int) -> int: """Convert a raw value to an int, falling back to a default. @@ -105,7 +104,7 @@ async def get_settings(socket_path: str) -> ServerSettingsResponse: """ import asyncio - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) ( log_level_raw, @@ -160,7 +159,7 @@ async def update_settings(socket_path: str, update: ServerSettingsUpdate) -> Non ServerOperationError: If any ``set`` command is rejected. ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) async def _set(key: str, value: Fail2BanSettingValue) -> None: try: @@ -197,7 +196,7 @@ async def flush_logs(socket_path: str) -> str: ServerOperationError: If the command is rejected. ~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable. """ - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: response = await client.send(["flushlogs"]) result = ok(cast("Fail2BanResponse", response)) diff --git a/backend/app/utils/config_file_utils.py b/backend/app/utils/config_file_utils.py index 74ba1e1..68802ce 100644 --- a/backend/app/utils/config_file_utils.py +++ b/backend/app/utils/config_file_utils.py @@ -22,7 +22,7 @@ from app.models.config import ( JailValidationIssue, JailValidationResult, ) -from app.utils.constants import FAIL2BAN_TRUTHY_VALUES +from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT, FAIL2BAN_TRUTHY_VALUES from app.utils.fail2ban_client import ( Fail2BanClient, Fail2BanConnectionError, @@ -32,8 +32,6 @@ from app.utils.fail2ban_response import ok, to_dict log: structlog.stdlib.BoundLogger = structlog.get_logger() -_SOCKET_TIMEOUT: float = 10.0 - # Allowlist pattern for jail names used in path construction. _SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") @@ -253,7 +251,7 @@ def _parse_jails_sync( async def _get_active_jail_names(socket_path: str) -> set[str]: """Fetch the set of currently running jail names from fail2ban.""" try: - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) status_raw = ok(await client.send(["status"])) status_dict = to_dict(status_raw) @@ -272,7 +270,7 @@ async def _get_active_jail_names(socket_path: str) -> set[str]: async def _probe_fail2ban_running(socket_path: str) -> bool: """Return ``True`` when fail2ban responds successfully to a status request.""" try: - client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) response = await client.send(["status"]) code, _ = cast("Fail2BanResponse", response) return code == 0 diff --git a/backend/app/utils/constants.py b/backend/app/utils/constants.py index ca1dce8..abd57fa 100644 --- a/backend/app/utils/constants.py +++ b/backend/app/utils/constants.py @@ -13,8 +13,11 @@ from typing import Final DEFAULT_FAIL2BAN_SOCKET: Final[str] = "/var/run/fail2ban/fail2ban.sock" """Default path to the fail2ban Unix domain socket.""" -FAIL2BAN_SOCKET_TIMEOUT_SECONDS: Final[float] = 5.0 -"""Maximum seconds to wait for a response from the fail2ban socket.""" +FAIL2BAN_SOCKET_TIMEOUT_FAST: Final[float] = 5.0 +"""Maximum seconds for fast operations (health checks, metadata probes).""" + +FAIL2BAN_SOCKET_TIMEOUT: Final[float] = 10.0 +"""Maximum seconds for command operations (config, jail management).""" FAIL2BAN_TRUTHY_VALUES: Final[frozenset[str]] = frozenset({"true", "yes", "1"}) """String values treated as boolean true by fail2ban configuration parsers."""