Skip to content

Commit 85987be

Browse files
committed
Switch IP matcher implementation
1 parent ad84753 commit 85987be

6 files changed

Lines changed: 165 additions & 52 deletions

File tree

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,67 @@
11
import ipaddress
22

33
try:
4-
import pytricia
4+
from ipset_c import IPSet
55

6-
PYTRICIA_AVAILABLE = True
6+
IPSET_C_AVAILABLE = True
77
except ImportError:
8-
PYTRICIA_AVAILABLE = False
8+
IPSET_C_AVAILABLE = False
99
from aikido_zen.helpers.logging import logger
1010

1111
logger.warning(
12-
"pytricia is not available. This happens on windows devices where pytricia is not supported yet."
12+
"ipset_c is not available on this platform/architecture."
1313
"Using fallback, this may result in slower performance."
14-
"You can try to install pytricia for better performance: pip install pytricia"
14+
"You can try to install ipset_c for better performance: pip install ipset_c"
1515
)
1616

1717

18-
def preparse(network: str) -> str:
19-
# Remove the brackets around IPv6 addresses if they are there.
18+
IPV4_MAPPED_IPV6_BASE = ipaddress.ip_network("::ffff:0:0/96")
19+
20+
21+
def preparse(network: str):
22+
"""
23+
Strips the brackets around IPv6 addresses if they are there and parses the
24+
network into an ipaddress network object. IPv4-mapped IPv6 networks (e.g.
25+
::ffff:127.0.0.1) are converted to their plain IPv4 equivalent.
26+
Returns None if the network is invalid.
27+
"""
2028
network = network.strip("[]")
2129
try:
22-
ip = ipaddress.IPv6Address(network)
23-
if ip.ipv4_mapped:
24-
return str(ip.ipv4_mapped)
30+
net = ipaddress.ip_network(network, strict=False)
2531
except ValueError:
26-
pass
27-
return network
32+
return None
33+
if net.version == 6 and net.subnet_of(IPV4_MAPPED_IPV6_BASE):
34+
ipv4_addr = net.network_address.ipv4_mapped
35+
return ipaddress.ip_network(f"{ipv4_addr}/{net.prefixlen - 96}", strict=False)
36+
return net
2837

2938

30-
if PYTRICIA_AVAILABLE:
39+
if IPSET_C_AVAILABLE:
3140

3241
class IPMatcher:
3342
def __init__(self, networks=None):
34-
self.trie = pytricia.PyTricia(128)
43+
v4_cidrs = []
44+
v6_cidrs = []
3545
if networks is not None:
3646
for s in networks:
37-
self._add(s)
38-
# We freeze in constructor ensuring that after initialization the IPMatcher is always frozen.
39-
self.trie.freeze()
47+
net = preparse(s)
48+
if net is None:
49+
continue
50+
(v4_cidrs if net.version == 4 else v6_cidrs).append(str(net))
51+
self.v4 = IPSet(v4_cidrs)
52+
self.v6 = IPSet(v6_cidrs)
4053

4154
def has(self, network):
42-
try:
43-
return self.trie.get(preparse(network)) is not None
44-
except ValueError:
55+
net = preparse(network)
56+
if net is None:
4557
return False
46-
47-
def _add(self, network):
48-
try:
49-
self.trie[preparse(network)] = True
50-
except ValueError:
51-
pass
52-
except SystemError:
53-
# SystemError's have been known to occur in the PyTricia library (see issue #34 e.g.),
54-
# best to play it safe and catch these errors.
55-
pass
56-
return self
58+
ipset = self.v4 if net.version == 4 else self.v6
59+
return ipset.isContainsCidr(str(net))
5760

5861
def is_empty(self):
59-
return len(self.trie) == 0
62+
return self.v4.size == 0 and self.v6.size == 0
6063

6164
else:
62-
# Fallback to pure Python implementation - this happens on windows machines since pytricia is not
63-
# fully supported there.
65+
# Fallback to pure Python implementation - this happens when ipset_c is not
66+
# available for the current platform/architecture.
6467
from aikido_zen.helpers.ip_matcher_fallback import IPMatcher # noqa: F401

aikido_zen/helpers/ip_matcher/init_test.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def test_strange_ips():
144144
matcher = IPMatcher(input_list)
145145
assert matcher.has("::ffff:0.0.0.0") == True
146146
assert matcher.has("::ffff:127.0.0.1") == True
147-
assert matcher.has("::ffff:123") == True
147+
assert matcher.has("::ffff:123") == False
148148
assert matcher.has("2001:db8::1") == False
149149
assert matcher.has("[::ffff:0.0.0.0]") == True
150150
assert matcher.has("::ffff:0:0:0:0") == True
@@ -201,3 +201,11 @@ def test_edge_cases():
201201
matcher1 = IPMatcher(["224.0.0.0/4"])
202202
assert matcher1.has("224.0.0.1") == True
203203
assert matcher1.has("240.0.0.0") == False
204+
205+
206+
def test_adjacent_ranges_at_end_of_address_space():
207+
matcher = IPMatcher(["224.0.0.0/4", "240.0.0.0/4"])
208+
assert matcher.has("224.0.0.1") == True
209+
assert matcher.has("240.0.0.1") == True
210+
assert matcher.has("255.255.255.255") == True
211+
assert matcher.has("223.255.255.255") == False

aikido_zen/helpers/ip_matcher_fallback/init_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,11 @@ def test_edge_cases():
202202
matcher1 = IPMatcher(["224.0.0.0/4"])
203203
assert matcher1.has("224.0.0.1") == True
204204
assert matcher1.has("240.0.0.0") == False
205+
206+
207+
def test_adjacent_ranges_at_end_of_address_space():
208+
matcher = IPMatcher(["224.0.0.0/4", "240.0.0.0/4"])
209+
assert matcher.has("224.0.0.1") == True
210+
assert matcher.has("240.0.0.1") == True
211+
assert matcher.has("255.255.255.255") == True
212+
assert matcher.has("223.255.255.255") == False

aikido_zen/helpers/ip_matcher_fallback/network.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def contains(self, network):
8484
other_next = network.duplicate().next()
8585
if not next_network.is_valid():
8686
return True
87+
# Handle edge case where the other network's next address overflows
88+
if not other_next.is_valid():
89+
return False
8790
if next_network.addr.compare(other_next.addr) == BEFORE:
8891
return False
8992
return True

poetry.lock

Lines changed: 107 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ regex = [
6868
]
6969
packaging = "^24.1"
7070
wrapt = "^1.17.2"
71-
pytricia = { version = "^1.3.0", markers = "sys_platform != 'win32'" }
71+
ipset_c = "0.2.1"
7272

7373
[tool.poetry.group.dev.dependencies]
7474
black = "^24.4.2"

0 commit comments

Comments
 (0)