Skip to content
Merged
2 changes: 1 addition & 1 deletion haproxy-ddos-protection-configurator/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def _on_config_changed(self, event):
import logging
from collections.abc import MutableMapping
Comment thread
Thanhphan1147 marked this conversation as resolved.
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
Expand All @@ -101,6 +101,8 @@ def _on_config_changed(self, event):
BaseModel,
ConfigDict,
Field,
IPvAnyAddress,
IPvAnyNetwork,
ValidationError,
field_validator,
model_validator,
Expand All @@ -115,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"
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion haproxy-ddos-protection-configurator/src/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
]
22 changes: 12 additions & 10 deletions haproxy-operator/lib/charms/haproxy/v0/ddos_protection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -101,6 +101,8 @@ def _on_config_changed(self, event):
BaseModel,
ConfigDict,
Field,
IPvAnyAddress,
IPvAnyNetwork,
ValidationError,
field_validator,
model_validator,
Expand All @@ -115,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"
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions haproxy-operator/tests/unit/test_ddos_protection_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading