diff --git a/aikido_zen/helpers/ip_matcher/__init__.py b/aikido_zen/helpers/ip_matcher/__init__.py index 98ca78a97..a9221cc3d 100644 --- a/aikido_zen/helpers/ip_matcher/__init__.py +++ b/aikido_zen/helpers/ip_matcher/__init__.py @@ -1,64 +1,67 @@ import ipaddress try: - import pytricia + from ipset_c import IPSet - PYTRICIA_AVAILABLE = True + IPSET_C_AVAILABLE = True except ImportError: - PYTRICIA_AVAILABLE = False + IPSET_C_AVAILABLE = False from aikido_zen.helpers.logging import logger logger.warning( - "pytricia is not available. This happens on windows devices where pytricia is not supported yet." + "ipset_c is not available on this platform/architecture." "Using fallback, this may result in slower performance." - "You can try to install pytricia for better performance: pip install pytricia" + "You can try to install ipset_c for better performance: pip install ipset_c" ) -def preparse(network: str) -> str: - # Remove the brackets around IPv6 addresses if they are there. +IPV4_MAPPED_IPV6_BASE = ipaddress.ip_network("::ffff:0:0/96") + + +def preparse(network: str): + """ + Strips the brackets around IPv6 addresses if they are there and parses the + network into an ipaddress network object. IPv4-mapped IPv6 networks (e.g. + ::ffff:127.0.0.1) are converted to their plain IPv4 equivalent. + Returns None if the network is invalid. + """ network = network.strip("[]") try: - ip = ipaddress.IPv6Address(network) - if ip.ipv4_mapped: - return str(ip.ipv4_mapped) + net = ipaddress.ip_network(network, strict=False) except ValueError: - pass - return network + return None + if net.version == 6 and net.subnet_of(IPV4_MAPPED_IPV6_BASE): + ipv4_addr = net.network_address.ipv4_mapped + return ipaddress.ip_network(f"{ipv4_addr}/{net.prefixlen - 96}", strict=False) + return net -if PYTRICIA_AVAILABLE: +if IPSET_C_AVAILABLE: class IPMatcher: def __init__(self, networks=None): - self.trie = pytricia.PyTricia(128) + v4_cidrs = [] + v6_cidrs = [] if networks is not None: for s in networks: - self._add(s) - # We freeze in constructor ensuring that after initialization the IPMatcher is always frozen. - self.trie.freeze() + net = preparse(s) + if net is None: + continue + (v4_cidrs if net.version == 4 else v6_cidrs).append(str(net)) + self.v4 = IPSet(v4_cidrs) + self.v6 = IPSet(v6_cidrs) def has(self, network): - try: - return self.trie.get(preparse(network)) is not None - except ValueError: + net = preparse(network) + if net is None: return False - - def _add(self, network): - try: - self.trie[preparse(network)] = True - except ValueError: - pass - except SystemError: - # SystemError's have been known to occur in the PyTricia library (see issue #34 e.g.), - # best to play it safe and catch these errors. - pass - return self + ipset = self.v4 if net.version == 4 else self.v6 + return ipset.isContainsCidr(str(net)) def is_empty(self): - return len(self.trie) == 0 + return self.v4.size == 0 and self.v6.size == 0 else: - # Fallback to pure Python implementation - this happens on windows machines since pytricia is not - # fully supported there. + # Fallback to pure Python implementation - this happens when ipset_c is not + # available for the current platform/architecture. from aikido_zen.helpers.ip_matcher_fallback import IPMatcher # noqa: F401 diff --git a/aikido_zen/helpers/ip_matcher/init_test.py b/aikido_zen/helpers/ip_matcher/init_test.py index e06a3ca4d..8dc681ef0 100644 --- a/aikido_zen/helpers/ip_matcher/init_test.py +++ b/aikido_zen/helpers/ip_matcher/init_test.py @@ -144,7 +144,7 @@ def test_strange_ips(): matcher = IPMatcher(input_list) assert matcher.has("::ffff:0.0.0.0") == True assert matcher.has("::ffff:127.0.0.1") == True - assert matcher.has("::ffff:123") == True + assert matcher.has("::ffff:123") == False assert matcher.has("2001:db8::1") == False assert matcher.has("[::ffff:0.0.0.0]") == True assert matcher.has("::ffff:0:0:0:0") == True @@ -201,3 +201,11 @@ def test_edge_cases(): matcher1 = IPMatcher(["224.0.0.0/4"]) assert matcher1.has("224.0.0.1") == True assert matcher1.has("240.0.0.0") == False + + +def test_adjacent_ranges_at_end_of_address_space(): + matcher = IPMatcher(["224.0.0.0/4", "240.0.0.0/4"]) + assert matcher.has("224.0.0.1") == True + assert matcher.has("240.0.0.1") == True + assert matcher.has("255.255.255.255") == True + assert matcher.has("223.255.255.255") == False diff --git a/aikido_zen/helpers/ip_matcher_fallback/init_test.py b/aikido_zen/helpers/ip_matcher_fallback/init_test.py index c0b78e625..eabaf1e70 100644 --- a/aikido_zen/helpers/ip_matcher_fallback/init_test.py +++ b/aikido_zen/helpers/ip_matcher_fallback/init_test.py @@ -202,3 +202,11 @@ def test_edge_cases(): matcher1 = IPMatcher(["224.0.0.0/4"]) assert matcher1.has("224.0.0.1") == True assert matcher1.has("240.0.0.0") == False + + +def test_adjacent_ranges_at_end_of_address_space(): + matcher = IPMatcher(["224.0.0.0/4", "240.0.0.0/4"]) + assert matcher.has("224.0.0.1") == True + assert matcher.has("240.0.0.1") == True + assert matcher.has("255.255.255.255") == True + assert matcher.has("223.255.255.255") == False diff --git a/aikido_zen/helpers/ip_matcher_fallback/network.py b/aikido_zen/helpers/ip_matcher_fallback/network.py index dd1e9f689..4a3c0246d 100644 --- a/aikido_zen/helpers/ip_matcher_fallback/network.py +++ b/aikido_zen/helpers/ip_matcher_fallback/network.py @@ -84,6 +84,9 @@ def contains(self, network): other_next = network.duplicate().next() if not next_network.is_valid(): return True + # Handle edge case where the other network's next address overflows + if not other_next.is_valid(): + return False if next_network.addr.compare(other_next.addr) == BEFORE: return False return True diff --git a/poetry.lock b/poetry.lock index 155981067..c64149f14 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1354,6 +1354,43 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipset-c" +version = "0.2.1" +description = "IPSet written in C" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "ipset_c-0.2.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:7baf78062ff39e30011b6cc3e3d30651a66fc9d7f1a5c6207302f55665824492"}, + {file = "ipset_c-0.2.1-cp310-cp310-manylinux_2_39_x86_64.whl", hash = "sha256:53c6271b0b3fdd9e5e2dc36dbdefa4fe3624ef8bc912be6ea3cdb1d36699a357"}, + {file = "ipset_c-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:469ea1af51ba8f98472e56a4ff8b3f2a8bb45546aab522e8646695e48d906b5e"}, + {file = "ipset_c-0.2.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:e2de2623979a3b95c523ff301bef59859414d14435ba9f9d4d425f4d4444c84f"}, + {file = "ipset_c-0.2.1-cp311-cp311-manylinux_2_39_x86_64.whl", hash = "sha256:4cd0ee4d7f773942bb67298b47cd8ba51d979e1bcb945a6e3035c75b0bbecc08"}, + {file = "ipset_c-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:024c0ca72ed171b566632a2c3906dc812c0c6119c1bc74049f64182eb8339973"}, + {file = "ipset_c-0.2.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:46785cd80681103fa966e3d4af185fbc0c894732469c8699745249631aa26b70"}, + {file = "ipset_c-0.2.1-cp312-cp312-manylinux_2_39_x86_64.whl", hash = "sha256:4f9db71f772fbb84d233adf5449f38a81dabd0c667e1594ef29cadc5fc90c6f7"}, + {file = "ipset_c-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:13bc25cfc281a24e451073b6de7011efd7f2de36c42d660c1b8c9e4f59350b5b"}, + {file = "ipset_c-0.2.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:63764a4e3b129d3fd4370759555bc2c501bcdb85ded7017ea0a66e0278721e05"}, + {file = "ipset_c-0.2.1-cp313-cp313-manylinux_2_39_x86_64.whl", hash = "sha256:d04c98b404ea1a2694affd881df3d256d552ee9c4512c5005838d398e7620f44"}, + {file = "ipset_c-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:f457c45e33caca82f7a53f23212b6da07b0eecd417d5a19c842e7ea501902119"}, + {file = "ipset_c-0.2.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:5a3a5f0fd5949dee92705c387b79a0180e2ebc62bf5ad6d474341c60e1eb7e8c"}, + {file = "ipset_c-0.2.1-cp314-cp314-manylinux_2_39_x86_64.whl", hash = "sha256:5655e2e938817e9743c3133dcbd584e50a994085c9c0852c5c8d49a9a1fc9b3b"}, + {file = "ipset_c-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:5cd8f94a2713315ef781fb53a30b0631147d14642de2ea31d11edd7ab323bd34"}, + {file = "ipset_c-0.2.1-cp314-cp314t-macosx_15_0_arm64.whl", hash = "sha256:2608b1462fdb04832b06ba0ab49f1d99857a9859d1ddfa747b815d23331df299"}, + {file = "ipset_c-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:699eb0dd249f33b59927d01a309204fbbb81f531daa97d093ddaeb8fb724c0c4"}, + {file = "ipset_c-0.2.1-cp38-cp38-macosx_15_0_arm64.whl", hash = "sha256:dc6c71efe247f2b25a8e35c03bbdc6475fb2edbf7292ae1ce7a1dc91a5e52265"}, + {file = "ipset_c-0.2.1-cp38-cp38-manylinux_2_39_x86_64.whl", hash = "sha256:785fd7174d9f3f236a15f53701736929456f9e7d273931f01407961250635c7f"}, + {file = "ipset_c-0.2.1-cp39-cp39-macosx_15_0_arm64.whl", hash = "sha256:1ab504fa0fd90b635debdfed071d998df76696dc3a2c18865bc5c54dcf065953"}, + {file = "ipset_c-0.2.1-cp39-cp39-manylinux_2_39_x86_64.whl", hash = "sha256:9d65c5d83e37e90df66835e4cd38da204158413d91c2418750f940e6e0b47257"}, + {file = "ipset_c-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:d8867b363ceb35ea028b450ca73ee8816abd9225465658a65a85194adaae0b2a"}, + {file = "ipset_c-0.2.1.tar.gz", hash = "sha256:18e3ba2cbd675afbbf9ae17129fe8f52ee02b23ff7d6ccce75d41e3f51846bf9"}, +] + +[package.dependencies] +setuptools = "*" +typing_extensions = {version = "*", markers = "python_version < \"3.11\""} + [[package]] name = "isort" version = "5.13.2" @@ -3045,18 +3082,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "pytricia" -version = "1.3.0" -description = "An efficient IP address storage and lookup module for Python." -optional = false -python-versions = "*" -groups = ["main"] -markers = "sys_platform != \"win32\"" -files = [ - {file = "pytricia-1.3.0.tar.gz", hash = "sha256:1c3a3d6909e10d4c9c2f0fe4542a2481e109d29aab99cc027ca7fe93f8c8853f"}, -] - [[package]] name = "pytz" version = "2025.2" @@ -3440,6 +3465,72 @@ botocore = ">=1.37.4,<2.0a0" [package.extras] crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] +[[package]] +name = "setuptools" +version = "75.3.4" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.8\"" +files = [ + {file = "setuptools-75.3.4-py3-none-any.whl", hash = "sha256:2dd50a7f42dddfa1d02a36f275dbe716f38ed250224f609d35fb60a09593d93e"}, + {file = "setuptools-75.3.4.tar.gz", hash = "sha256:b4ea3f76e1633c4d2d422a5d68ab35fd35402ad71e6acaa5d7e5956eb47e8887"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] +core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "ruff (<=0.7.1)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.12.*)", "pytest-mypy"] + +[[package]] +name = "setuptools" +version = "82.0.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] + +[[package]] +name = "setuptools" +version = "84.0.0" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670"}, + {file = "setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "six" version = "1.17.0" @@ -3757,7 +3848,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] markers = "python_version == \"3.8\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, @@ -3770,12 +3861,12 @@ version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version >= \"3.9\"" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] +markers = {main = "python_version < \"3.11\" and python_version >= \"3.9\"", dev = "python_version >= \"3.9\""} [[package]] name = "typing-inspection" @@ -4106,4 +4197,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = ">=3.8,<3.15" -content-hash = "b0a2f0b885ebf712bda6b5cc09b705144a21352a70584f35671d2f8bfdade61a" +content-hash = "237e84c8244c4bd34552cf4f32265cdeac014cccd9f41672327ec4f62dc03c13" diff --git a/pyproject.toml b/pyproject.toml index 5a4318222..5bdad8cf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ regex = [ ] packaging = "^24.1" wrapt = "^1.17.2" -pytricia = { version = "^1.3.0", markers = "sys_platform != 'win32'" } +ipset_c = "0.2.1" [tool.poetry.group.dev.dependencies] black = "^24.4.2"