Skip to content

feat(backend): add config-only automatic IP banning for DDoS policies#279

Merged
bihius merged 2 commits into
mainfrom
feat/ddos-auto-ban
Jul 21, 2026
Merged

feat(backend): add config-only automatic IP banning for DDoS policies#279
bihius merged 2 commits into
mainfrom
feat/ddos-auto-ban

Conversation

@bihius

@bihius bihius commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Extends the per-policy DDoS protection introduced in #274 with automatic IP banning, entirely inside the generated HAProxy config (no live socket management yet).

  • Policy gains auto_ban_enabled, ban_threshold, ban_duration_seconds — only takes effect when ddos_protection_enabled is also true.
  • A dedicated st_ban_vhost_N stick-table (separate from the rate-limit table) tracks a per-source abuse counter via gpc0.
  • Each rate-limit/connection-limit violation increments the counter (sc-inc-gpc0); once it crosses ban_threshold the source is denied (429) until it stays idle for ban_duration_seconds.
  • Health checks and ACME HTTP-01 challenges are exempt, consistent with the existing DDoS rules.
  • Policy admin form gains an "Automatic IP banning" toggle nested under DDoS protection, disabled until DDoS protection itself is enabled.

Out of scope

Closes #275
Related to #176

Test plan

  • uv run pytest --cov=app (564 passed)
  • uv run mypy app/
  • uv run ruff check app/
  • pnpm run type-check
  • pnpm run lint
  • pnpm test (115 passed)
  • haproxy -c against rendered configs with auto-ban enabled (renderer + generator tests)
  • uv run alembic upgrade head applies cleanly

Extends per-policy DDoS protection with automatic IP banning: repeated
rate-limit/connection-limit violations increment a per-source abuse
counter in a dedicated stick-table, and the source is denied once it
crosses ban_threshold until it stays idle for ban_duration_seconds.

- Policy gains auto_ban_enabled, ban_threshold, ban_duration_seconds
  (only takes effect when ddos_protection_enabled is also true).
- HaproxyDdos carries the ban stick-table name and thresholds; the
  template emits track-sc1 + sc-inc-gpc0 + a gpc0-based deny per vhost,
  exempting health checks and ACME challenges like the rate/conn rules.
- Policy form gains an auto-ban toggle nested under DDoS protection.

No live ban-list read/unban yet — that remains a separate follow-up.
Copilot AI review requested due to automatic review settings July 21, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “config-only” automatic IP banning as an extension of the existing per-policy DDoS protection, spanning backend persistence/validation, HAProxy config generation, and frontend policy editing/types/tests.

Changes:

  • Added auto-ban fields to Policy (DB + Pydantic schemas + service/router wiring) and surfaced them through the frontend policy form/types.
  • Extended HAProxy render context + template + generator to emit a per-vhost ban stick-table (gpc0) and related request rules when enabled.
  • Added unit/integration/frontend tests covering round-tripping policy fields and validating rendered/generated HAProxy configs.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/frontend/src/pages/vhosts/VHostDetailPage.test.tsx Updates vhost detail mock data to include new policy auto-ban fields.
src/frontend/src/pages/policies/PolicyDetailPage.test.tsx Updates policy detail mock data to include new auto-ban fields.
src/frontend/src/pages/policies/PoliciesPage.test.tsx Updates policy list mock data to include new auto-ban fields.
src/frontend/src/features/vhosts/types.ts Extends assigned-policy TypeScript type with auto-ban fields.
src/frontend/src/features/policies/types.ts Extends policy/request TypeScript types to include auto-ban fields.
src/frontend/src/features/policies/PolicyFormModal.tsx Adds UI controls and submit wiring for auto-ban settings (nested under DDoS).
src/frontend/src/features/policies/PolicyFormModal.test.tsx Adds UI/submit tests for the new auto-ban toggle and numeric inputs.
src/backend/tests/unit/test_policy_service.py Adds unit tests asserting auto-ban fields persist/update via PolicyService.
src/backend/tests/unit/test_config_renderer_haproxy.py Adds render-context validation and template assertions for auto-ban blocks + haproxy validation.
src/backend/tests/unit/test_config_generator.py Adds generator assertions + haproxy validation for generated configs with auto-ban.
src/backend/tests/integration/test_policies_router.py Adds integration tests for create/patch and validation of auto-ban fields.
src/backend/app/templates/haproxy.cfg.j2 Adds HAProxy template blocks for ban stick-table tracking/increment/deny.
src/backend/app/services/policy_service.py Adds allowlisted fields + create parameters for auto-ban persistence.
src/backend/app/services/config_renderer.py Extends HaproxyDdos with auto-ban fields and validation; enforces unique ban table names.
src/backend/app/services/config_generator.py Wires policy auto-ban fields into the HAProxy render context and table naming.
src/backend/app/schemas/policy.py Adds Pydantic fields/constraints for create/update/response schemas.
src/backend/app/routers/policies.py Passes auto-ban fields from request body into policy creation.
src/backend/app/models/policy.py Adds DB columns + check constraints for auto-ban fields on Policy.
src/backend/alembic/versions/ea9292cbacec_add_auto_ban_fields_to_policies.py Adds Alembic migration for new columns + check constraints.

Comment on lines 65 to +77
http-request track-sc0 src table {{ route.ddos.stick_table_name }} if {{ route.vhost_acl_name }} !is_health !is_acme
http-request deny deny_status 429 if {{ route.vhost_acl_name }} !is_health !is_acme { sc_http_req_rate(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.rate_limit_requests }} }
http-request deny deny_status 429 if {{ route.vhost_acl_name }} !is_health !is_acme { sc_conn_cur(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.max_connections_per_ip }} }
{% if route.ddos.auto_ban_enabled %}
# Automatic IP banning: repeated rate/conn violations trip a ban that
# lifts after ban_duration_seconds of inactivity (track-sc1 refreshes
# the entry's expiry on every request, so a persistent attacker stays
# banned). sc-inc-gpc0 must run before the matching deny rule.
http-request track-sc1 src table {{ route.ddos.ban_stick_table_name }} if {{ route.vhost_acl_name }} !is_health !is_acme
http-request deny deny_status 429 if {{ route.vhost_acl_name }} !is_health !is_acme { sc_get_gpc0(1) gt {{ route.ddos.ban_threshold }} }
http-request sc-inc-gpc0(1) if {{ route.vhost_acl_name }} !is_health !is_acme { sc_http_req_rate(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.rate_limit_requests }} }
http-request sc-inc-gpc0(1) if {{ route.vhost_acl_name }} !is_health !is_acme { sc_conn_cur(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.max_connections_per_ip }} }
{% endif %}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — real bug. Reordered so track-sc1, the already-banned check, and both sc-inc-gpc0 rules run before the rate/conn deny rules in both fe_http and fe_https, so the abuse counter increments and the ban table's expiry refreshes on every tracked request as intended. (2335c78)

Comment on lines 135 to +143
http-request track-sc0 src table {{ route.ddos.stick_table_name }} if {{ route.vhost_acl_name }} !is_health !is_acme
http-request deny deny_status 429 if {{ route.vhost_acl_name }} !is_health !is_acme { sc_http_req_rate(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.rate_limit_requests }} }
http-request deny deny_status 429 if {{ route.vhost_acl_name }} !is_health !is_acme { sc_conn_cur(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.max_connections_per_ip }} }
{% if route.ddos.auto_ban_enabled %}
http-request track-sc1 src table {{ route.ddos.ban_stick_table_name }} if {{ route.vhost_acl_name }} !is_health !is_acme
http-request deny deny_status 429 if {{ route.vhost_acl_name }} !is_health !is_acme { sc_get_gpc0(1) gt {{ route.ddos.ban_threshold }} }
http-request sc-inc-gpc0(1) if {{ route.vhost_acl_name }} !is_health !is_acme { sc_http_req_rate(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.rate_limit_requests }} }
http-request sc-inc-gpc0(1) if {{ route.vhost_acl_name }} !is_health !is_acme { sc_conn_cur(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.max_connections_per_ip }} }
{% endif %}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the same way — track-sc1/sc-inc-gpc0 now run before the rate/conn deny rules in fe_https too. (2335c78)

http-request deny stops rule evaluation, so the rate/conn deny rules
running before sc-inc-gpc0 meant the abuse counter never incremented
and auto-ban could never trigger. Move track-sc1, the already-banned
check, and both sc-inc-gpc0 rules ahead of the rate/conn deny rules in
both fe_http and fe_https.
@bihius
bihius merged commit 517d7c4 into main Jul 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2-09a — Automatic IP banning (config-only auto-ban)

2 participants