From b26bea6f3625851bfb5a800214da52c75531d715 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:26:46 +0000 Subject: [PATCH 1/3] Initial plan From d72032ee5701509fb0a4fd042b9064311e9a5d8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:31:20 +0000 Subject: [PATCH 2/3] Support IPv6 DDoS allow lists Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com> --- .../charmcraft.yaml | 2 +- .../lib/charms/haproxy/v0/ddos_protection.py | 20 ++++++++------- .../src/state.py | 2 +- .../tests/unit/test_ddos_protection_lib.py | 25 +++++++++++++++++++ .../lib/charms/haproxy/v0/ddos_protection.py | 20 ++++++++------- .../tests/unit/test_ddos_protection_lib.py | 8 +++--- 6 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 haproxy-ddos-protection-configurator/tests/unit/test_ddos_protection_lib.py diff --git a/haproxy-ddos-protection-configurator/charmcraft.yaml b/haproxy-ddos-protection-configurator/charmcraft.yaml index 805e76b71..727099d19 100644 --- a/haproxy-ddos-protection-configurator/charmcraft.yaml +++ b/haproxy-ddos-protection-configurator/charmcraft.yaml @@ -64,7 +64,7 @@ config: ip-allow-list: type: string description: | - Comma-separated list of IPv4 addresses or CIDR blocks to be allowed on both TCP and + Comma-separated list of IP addresses or CIDR blocks to be allowed on both TCP and HTTP levels. http-request-timeout: type: int diff --git a/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py b/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py index 139d5ebd0..332e58bee 100644 --- a/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py +++ b/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py @@ -92,7 +92,7 @@ def _on_config_changed(self, event): import logging from collections.abc import MutableMapping from enum import Enum -from ipaddress import IPv4Address, IPv4Network +from ipaddress import ip_address, ip_network from typing import Optional, cast from ops import CharmBase @@ -101,6 +101,8 @@ def _on_config_changed(self, event): BaseModel, ConfigDict, Field, + IPvAnyAddress, + IPvAnyNetwork, ValidationError, field_validator, model_validator, @@ -229,7 +231,7 @@ class DDoSProtectionProviderAppData(_DatabagModel): limit_policy_http: Policy to be applied when HTTP-level limits are exceeded. limit_policy_tcp: Policy to be applied when TCP-level limits are exceeded. policy_status_code: HTTP status code for deny policy (only set when limit_policy_http is deny). - ip_allow_list: List of IPv4 addresses or CIDR blocks to be allowed. + ip_allow_list: List of IP addresses or CIDR blocks to be allowed. http_request_timeout: Timeout for HTTP requests in seconds. http_keepalive_timeout: Timeout for HTTP keep-alive connections in seconds. client_timeout: Timeout for client connections in seconds. @@ -243,7 +245,7 @@ class DDoSProtectionProviderAppData(_DatabagModel): limit_policy_http: Optional[HttpRateLimitPolicy] = Field(default=None) limit_policy_tcp: Optional[TcpRateLimitPolicy] = Field(default=None) policy_status_code: Optional[int] = Field(default=None, ge=100, le=599) - ip_allow_list: Optional[list[IPv4Network | IPv4Address]] = Field(default=None) + ip_allow_list: Optional[list[IPvAnyAddress | IPvAnyNetwork]] = Field(default=None) http_request_timeout: Optional[int] = Field(default=None, gt=0) http_keepalive_timeout: Optional[int] = Field(default=None, gt=0) client_timeout: Optional[int] = Field(default=None, gt=0) @@ -253,21 +255,21 @@ class DDoSProtectionProviderAppData(_DatabagModel): @classmethod def validate_ip_allow_list( cls, v: Optional[list[str]] - ) -> Optional[list[IPv4Network | IPv4Address]]: + ) -> Optional[list[IPvAnyAddress | IPvAnyNetwork]]: """Validate and convert IP allow list. - Converts each string to either IPv4Address (for single IPs) or IPv4Network (for CIDR blocks). + Converts each string to either an IP address or a network. Args: v: The list of IP addresses or CIDR blocks as strings. Returns: - The list of converted IPv4Address or IPv4Network objects. + The list of converted IP address or network objects. """ if v is None: return None - return [IPv4Network(ip_str) if "/" in ip_str else IPv4Address(ip_str) for ip_str in v] + return [ip_network(ip_str) if "/" in ip_str else ip_address(ip_str) for ip_str in v] @field_validator("deny_paths", mode="after") @classmethod @@ -450,7 +452,7 @@ def set_config( error_rate: Number of errors per minute per entry to trigger the limit policy. limit_policy_http: Policy to be applied when HTTP-level limits are exceeded. limit_policy_tcp: Policy to be applied when TCP-level limits are exceeded. - ip_allow_list: List of IPv4 addresses or CIDR blocks to be allowed. + ip_allow_list: List of IP addresses or CIDR blocks to be allowed. http_request_timeout: Timeout for HTTP requests in seconds. http_keepalive_timeout: Timeout for HTTP keep-alive connections in seconds. client_timeout: Timeout for client connections in seconds. @@ -467,7 +469,7 @@ def set_config( error_rate=error_rate, limit_policy_http=cast(Optional[HttpRateLimitPolicy], limit_policy_http), limit_policy_tcp=cast(Optional[TcpRateLimitPolicy], limit_policy_tcp), - ip_allow_list=cast(Optional[list[IPv4Network | IPv4Address]], ip_allow_list), + ip_allow_list=cast(Optional[list[IPvAnyAddress | IPvAnyNetwork]], ip_allow_list), http_request_timeout=http_request_timeout, http_keepalive_timeout=http_keepalive_timeout, client_timeout=client_timeout, diff --git a/haproxy-ddos-protection-configurator/src/state.py b/haproxy-ddos-protection-configurator/src/state.py index 303d609b5..e79af1ab2 100644 --- a/haproxy-ddos-protection-configurator/src/state.py +++ b/haproxy-ddos-protection-configurator/src/state.py @@ -29,7 +29,7 @@ class CharmState: error_rate_per_minute: Number of errors per minute per entry to trigger the limit policy. limit_policy_http: Policy to be applied when HTTP-level limits are exceeded. limit_policy_tcp: Policy to be applied when TCP-level limits are exceeded. - ip_allow_list: Comma-separated list of IPv4 addresses or CIDR blocks to be allowed. + ip_allow_list: Comma-separated list of IP addresses or CIDR blocks to be allowed. http_request_timeout: Timeout for HTTP requests in seconds. http_keepalive_timeout: Timeout for HTTP keep-alive connections in seconds. client_timeout: Timeout for client connections in seconds. diff --git a/haproxy-ddos-protection-configurator/tests/unit/test_ddos_protection_lib.py b/haproxy-ddos-protection-configurator/tests/unit/test_ddos_protection_lib.py new file mode 100644 index 000000000..b1f92a3e8 --- /dev/null +++ b/haproxy-ddos-protection-configurator/tests/unit/test_ddos_protection_lib.py @@ -0,0 +1,25 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Unit tests for DDoS protection interface library.""" + +from charms.haproxy.v0.ddos_protection import DDoSProtectionProviderAppData + + +def test_ddos_protection_provider_app_data_ip_allow_list(): + """ + arrange: Create DDoS protection data with IPv4 and IPv6 addresses and CIDR blocks. + act: Validate the model. + assert: All entries are correctly converted. + """ + data = DDoSProtectionProviderAppData( + rate_limit_requests_per_minute=100, + ip_allow_list=["192.168.0.0/16", "10.0.0.1", "2001:db8::1", "2001:db8:abcd::/48"], + ) + + assert [str(ip) for ip in data.ip_allow_list] == [ + "192.168.0.0/16", + "10.0.0.1", + "2001:db8::1", + "2001:db8:abcd::/48", + ] diff --git a/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py b/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py index 139d5ebd0..332e58bee 100644 --- a/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py +++ b/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py @@ -92,7 +92,7 @@ def _on_config_changed(self, event): import logging from collections.abc import MutableMapping from enum import Enum -from ipaddress import IPv4Address, IPv4Network +from ipaddress import ip_address, ip_network from typing import Optional, cast from ops import CharmBase @@ -101,6 +101,8 @@ def _on_config_changed(self, event): BaseModel, ConfigDict, Field, + IPvAnyAddress, + IPvAnyNetwork, ValidationError, field_validator, model_validator, @@ -229,7 +231,7 @@ class DDoSProtectionProviderAppData(_DatabagModel): limit_policy_http: Policy to be applied when HTTP-level limits are exceeded. limit_policy_tcp: Policy to be applied when TCP-level limits are exceeded. policy_status_code: HTTP status code for deny policy (only set when limit_policy_http is deny). - ip_allow_list: List of IPv4 addresses or CIDR blocks to be allowed. + ip_allow_list: List of IP addresses or CIDR blocks to be allowed. http_request_timeout: Timeout for HTTP requests in seconds. http_keepalive_timeout: Timeout for HTTP keep-alive connections in seconds. client_timeout: Timeout for client connections in seconds. @@ -243,7 +245,7 @@ class DDoSProtectionProviderAppData(_DatabagModel): limit_policy_http: Optional[HttpRateLimitPolicy] = Field(default=None) limit_policy_tcp: Optional[TcpRateLimitPolicy] = Field(default=None) policy_status_code: Optional[int] = Field(default=None, ge=100, le=599) - ip_allow_list: Optional[list[IPv4Network | IPv4Address]] = Field(default=None) + ip_allow_list: Optional[list[IPvAnyAddress | IPvAnyNetwork]] = Field(default=None) http_request_timeout: Optional[int] = Field(default=None, gt=0) http_keepalive_timeout: Optional[int] = Field(default=None, gt=0) client_timeout: Optional[int] = Field(default=None, gt=0) @@ -253,21 +255,21 @@ class DDoSProtectionProviderAppData(_DatabagModel): @classmethod def validate_ip_allow_list( cls, v: Optional[list[str]] - ) -> Optional[list[IPv4Network | IPv4Address]]: + ) -> Optional[list[IPvAnyAddress | IPvAnyNetwork]]: """Validate and convert IP allow list. - Converts each string to either IPv4Address (for single IPs) or IPv4Network (for CIDR blocks). + Converts each string to either an IP address or a network. Args: v: The list of IP addresses or CIDR blocks as strings. Returns: - The list of converted IPv4Address or IPv4Network objects. + The list of converted IP address or network objects. """ if v is None: return None - return [IPv4Network(ip_str) if "/" in ip_str else IPv4Address(ip_str) for ip_str in v] + return [ip_network(ip_str) if "/" in ip_str else ip_address(ip_str) for ip_str in v] @field_validator("deny_paths", mode="after") @classmethod @@ -450,7 +452,7 @@ def set_config( error_rate: Number of errors per minute per entry to trigger the limit policy. limit_policy_http: Policy to be applied when HTTP-level limits are exceeded. limit_policy_tcp: Policy to be applied when TCP-level limits are exceeded. - ip_allow_list: List of IPv4 addresses or CIDR blocks to be allowed. + ip_allow_list: List of IP addresses or CIDR blocks to be allowed. http_request_timeout: Timeout for HTTP requests in seconds. http_keepalive_timeout: Timeout for HTTP keep-alive connections in seconds. client_timeout: Timeout for client connections in seconds. @@ -467,7 +469,7 @@ def set_config( error_rate=error_rate, limit_policy_http=cast(Optional[HttpRateLimitPolicy], limit_policy_http), limit_policy_tcp=cast(Optional[TcpRateLimitPolicy], limit_policy_tcp), - ip_allow_list=cast(Optional[list[IPv4Network | IPv4Address]], ip_allow_list), + ip_allow_list=cast(Optional[list[IPvAnyAddress | IPvAnyNetwork]], ip_allow_list), http_request_timeout=http_request_timeout, http_keepalive_timeout=http_keepalive_timeout, client_timeout=client_timeout, diff --git a/haproxy-operator/tests/unit/test_ddos_protection_lib.py b/haproxy-operator/tests/unit/test_ddos_protection_lib.py index 339871168..97534091c 100644 --- a/haproxy-operator/tests/unit/test_ddos_protection_lib.py +++ b/haproxy-operator/tests/unit/test_ddos_protection_lib.py @@ -150,19 +150,21 @@ def test_ddos_protection_provider_app_data_with_invalid_tcp_policy( def test_ddos_protection_provider_app_data_ip_allow_list(): """ - arrange: Create a DDoSProtectionProviderAppData model with a mix of IPv4 addresses + arrange: Create a DDoSProtectionProviderAppData model with IPv4 and IPv6 addresses and CIDR blocks. act: Validate the model. assert: IP addresses are correctly converted and have expected values. """ data = DDoSProtectionProviderAppData( rate_limit_requests_per_minute=100, - ip_allow_list=["192.168.0.0/16", "10.0.0.0/8"], + ip_allow_list=["192.168.0.0/16", "10.0.0.0/8", "2001:db8::1", "2001:db8:abcd::/48"], ) - assert len(data.ip_allow_list) == 2 + assert len(data.ip_allow_list) == 4 assert str(data.ip_allow_list[0]) == "192.168.0.0/16" assert str(data.ip_allow_list[1]) == "10.0.0.0/8" + assert str(data.ip_allow_list[2]) == "2001:db8::1" + assert str(data.ip_allow_list[3]) == "2001:db8:abcd::/48" def test_ddos_protection_provider_app_data_invalid_ip(): From 8df640371121e359bd5ddbbf4366240768866430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:49:39 +0000 Subject: [PATCH 3/3] Bump DDoS library patch version Co-authored-by: Thanhphan1147 <42444001+Thanhphan1147@users.noreply.github.com> --- .../lib/charms/haproxy/v0/ddos_protection.py | 2 +- haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py b/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py index 332e58bee..b8d56c460 100644 --- a/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py +++ b/haproxy-ddos-protection-configurator/lib/charms/haproxy/v0/ddos_protection.py @@ -117,7 +117,7 @@ def _on_config_changed(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 4 +LIBPATCH = 5 logger = logging.getLogger(__name__) DDOS_PROTECTION_RELATION_NAME = "ddos-protection" diff --git a/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py b/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py index 332e58bee..b8d56c460 100644 --- a/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py +++ b/haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py @@ -117,7 +117,7 @@ def _on_config_changed(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 4 +LIBPATCH = 5 logger = logging.getLogger(__name__) DDOS_PROTECTION_RELATION_NAME = "ddos-protection"