Config page tasks 1-4: dropdowns, key props, inactive jail full GUI, banaction fix
Task 1: Backend/LogEncoding/DatePattern dropdowns in JailConfigDetail
- Added BACKENDS, LOG_ENCODINGS, DATE_PATTERN_PRESETS constants
- Backend and Log Encoding: <Input readOnly> → <Select> (editable, auto-saves)
- Date Pattern: <Input> → <Combobox freeform> with presets
- Extended JailConfigUpdate model (backend, log_encoding) and service
- Added readOnly prop to JailConfigDetail (all fields, toggles, buttons)
- Extended RegexList with readOnly prop
Task 2: Fix raw action/filter config always blank
- Added key={selectedAction.name} to ActionDetail in ActionsTab
- Added key={selectedFilter.name} to FilterDetail in FiltersTab
Task 3: Inactive jail full GUI same as active jails
- Extended InactiveJail Pydantic model with all config fields
- Added _parse_time_to_seconds helper to config_file_service
- Updated _build_inactive_jail to populate all extended fields
- Extended InactiveJail TypeScript type to match
- Rewrote InactiveJailDetail to reuse JailConfigDetail (readOnly=true)
Task 4: Fix banaction interpolation error when activating jails
- _write_local_override_sync now includes banaction=iptables-multiport
and banaction_allports=iptables-allports in every .local file
This commit is contained in:
@@ -118,6 +118,8 @@ class JailConfigUpdate(BaseModel):
|
||||
prefregex: str | None = Field(default=None, description="Prefix regex; None = skip, '' = clear, non-empty = set.")
|
||||
date_pattern: str | None = Field(default=None)
|
||||
dns_mode: str | None = Field(default=None, description="DNS lookup mode: yes | warn | no | raw.")
|
||||
backend: str | None = Field(default=None, description="Log monitoring backend.")
|
||||
log_encoding: str | None = Field(default=None, description="Log file encoding.")
|
||||
enabled: bool | None = Field(default=None)
|
||||
bantime_escalation: BantimeEscalationUpdate | None = Field(
|
||||
default=None,
|
||||
@@ -751,6 +753,47 @@ class InactiveJail(BaseModel):
|
||||
default=None,
|
||||
description="Number of failures before a ban is issued.",
|
||||
)
|
||||
# ---- Extended fields for full GUI display ----
|
||||
ban_time_seconds: int = Field(
|
||||
default=600,
|
||||
description="Ban duration in seconds, parsed from bantime string.",
|
||||
)
|
||||
find_time_seconds: int = Field(
|
||||
default=600,
|
||||
description="Failure-counting window in seconds, parsed from findtime string.",
|
||||
)
|
||||
log_encoding: str = Field(
|
||||
default="auto",
|
||||
description="Log encoding, e.g. ``utf-8`` or ``auto``.",
|
||||
)
|
||||
backend: str = Field(
|
||||
default="auto",
|
||||
description="Log-monitoring backend, e.g. ``auto``, ``pyinotify``, ``polling``.",
|
||||
)
|
||||
date_pattern: str | None = Field(
|
||||
default=None,
|
||||
description="Date pattern for log parsing, or None for auto-detect.",
|
||||
)
|
||||
use_dns: str = Field(
|
||||
default="warn",
|
||||
description="DNS resolution mode: ``yes``, ``warn``, ``no``, or ``raw``.",
|
||||
)
|
||||
prefregex: str = Field(
|
||||
default="",
|
||||
description="Prefix regex prepended to every failregex.",
|
||||
)
|
||||
fail_regex: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of failure regex patterns.",
|
||||
)
|
||||
ignore_regex: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of ignore regex patterns.",
|
||||
)
|
||||
bantime_escalation: BantimeEscalation | None = Field(
|
||||
default=None,
|
||||
description="Ban-time escalation configuration, if enabled.",
|
||||
)
|
||||
source_file: str = Field(
|
||||
...,
|
||||
description="Absolute path to the config file where this jail is defined.",
|
||||
|
||||
@@ -41,6 +41,7 @@ from app.models.config import (
|
||||
ActivateJailRequest,
|
||||
AssignActionRequest,
|
||||
AssignFilterRequest,
|
||||
BantimeEscalation,
|
||||
FilterConfig,
|
||||
FilterConfigUpdate,
|
||||
FilterCreateRequest,
|
||||
@@ -290,6 +291,44 @@ def _parse_int_safe(value: str) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_time_to_seconds(value: str | None, default: int) -> int:
|
||||
"""Convert a fail2ban time string (e.g. ``1h``, ``10m``, ``3600``) to seconds.
|
||||
|
||||
Supports the suffixes ``s`` (seconds), ``m`` (minutes), ``h`` (hours),
|
||||
``d`` (days), ``w`` (weeks), and plain integers (already seconds).
|
||||
``-1`` is treated as a permanent ban and returned as-is.
|
||||
|
||||
Args:
|
||||
value: Raw time string from config, or ``None``.
|
||||
default: Value to return when ``value`` is absent or unparseable.
|
||||
|
||||
Returns:
|
||||
Duration in seconds, or ``-1`` for permanent, or ``default`` on failure.
|
||||
"""
|
||||
if not value:
|
||||
return default
|
||||
stripped = value.strip()
|
||||
if stripped == "-1":
|
||||
return -1
|
||||
multipliers: dict[str, int] = {
|
||||
"w": 604800,
|
||||
"d": 86400,
|
||||
"h": 3600,
|
||||
"m": 60,
|
||||
"s": 1,
|
||||
}
|
||||
for suffix, factor in multipliers.items():
|
||||
if stripped.endswith(suffix) and len(stripped) > 1:
|
||||
try:
|
||||
return int(stripped[:-1]) * factor
|
||||
except ValueError:
|
||||
return default
|
||||
try:
|
||||
return int(stripped)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def _parse_multiline(raw: str) -> list[str]:
|
||||
"""Split a multi-line INI value into individual non-blank lines.
|
||||
|
||||
@@ -413,6 +452,42 @@ def _build_inactive_jail(
|
||||
maxretry_raw = settings.get("maxretry", "")
|
||||
maxretry = _parse_int_safe(maxretry_raw)
|
||||
|
||||
# Extended fields for full GUI display
|
||||
ban_time_seconds = _parse_time_to_seconds(settings.get("bantime"), 600)
|
||||
find_time_seconds = _parse_time_to_seconds(settings.get("findtime"), 600)
|
||||
log_encoding = settings.get("logencoding") or "auto"
|
||||
backend = settings.get("backend") or "auto"
|
||||
date_pattern = settings.get("datepattern") or None
|
||||
use_dns = settings.get("usedns") or "warn"
|
||||
prefregex = settings.get("prefregex") or ""
|
||||
fail_regex = _parse_multiline(settings.get("failregex", ""))
|
||||
ignore_regex = _parse_multiline(settings.get("ignoreregex", ""))
|
||||
|
||||
# Ban-time escalation
|
||||
esc_increment = _is_truthy(settings.get("bantime.increment", "false"))
|
||||
esc_factor_raw = settings.get("bantime.factor")
|
||||
esc_factor = float(esc_factor_raw) if esc_factor_raw else None
|
||||
esc_formula = settings.get("bantime.formula") or None
|
||||
esc_multipliers = settings.get("bantime.multipliers") or None
|
||||
esc_max_raw = settings.get("bantime.maxtime")
|
||||
esc_max_time = _parse_time_to_seconds(esc_max_raw, 0) if esc_max_raw else None
|
||||
esc_rnd_raw = settings.get("bantime.rndtime")
|
||||
esc_rnd_time = _parse_time_to_seconds(esc_rnd_raw, 0) if esc_rnd_raw else None
|
||||
esc_overall = _is_truthy(settings.get("bantime.overalljails", "false"))
|
||||
bantime_escalation = (
|
||||
BantimeEscalation(
|
||||
increment=esc_increment,
|
||||
factor=esc_factor,
|
||||
formula=esc_formula,
|
||||
multipliers=esc_multipliers,
|
||||
max_time=esc_max_time,
|
||||
rnd_time=esc_rnd_time,
|
||||
overall_jails=esc_overall,
|
||||
)
|
||||
if esc_increment
|
||||
else None
|
||||
)
|
||||
|
||||
return InactiveJail(
|
||||
name=name,
|
||||
filter=filter_name,
|
||||
@@ -422,6 +497,16 @@ def _build_inactive_jail(
|
||||
bantime=settings.get("bantime") or None,
|
||||
findtime=settings.get("findtime") or None,
|
||||
maxretry=maxretry,
|
||||
ban_time_seconds=ban_time_seconds,
|
||||
find_time_seconds=find_time_seconds,
|
||||
log_encoding=log_encoding,
|
||||
backend=backend,
|
||||
date_pattern=date_pattern,
|
||||
use_dns=use_dns,
|
||||
prefregex=prefregex,
|
||||
fail_regex=fail_regex,
|
||||
ignore_regex=ignore_regex,
|
||||
bantime_escalation=bantime_escalation,
|
||||
source_file=source_file,
|
||||
enabled=enabled,
|
||||
)
|
||||
@@ -513,6 +598,10 @@ def _write_local_override_sync(
|
||||
f"[{jail_name}]",
|
||||
"",
|
||||
f"enabled = {'true' if enabled else 'false'}",
|
||||
# Provide explicit banaction defaults so fail2ban can resolve the
|
||||
# %(banaction)s interpolation used in the built-in action_ chain.
|
||||
"banaction = iptables-multiport",
|
||||
"banaction_allports = iptables-allports",
|
||||
]
|
||||
|
||||
if overrides.get("bantime") is not None:
|
||||
|
||||
@@ -366,6 +366,10 @@ async def update_jail_config(
|
||||
await _set("datepattern", update.date_pattern)
|
||||
if update.dns_mode is not None:
|
||||
await _set("usedns", update.dns_mode)
|
||||
if update.backend is not None:
|
||||
await _set("backend", update.backend)
|
||||
if update.log_encoding is not None:
|
||||
await _set("logencoding", update.log_encoding)
|
||||
if update.prefregex is not None:
|
||||
await _set("prefregex", update.prefregex)
|
||||
if update.enabled is not None:
|
||||
|
||||
Reference in New Issue
Block a user