Blame SOURCES/v1.0.0-0051-fix-ipset-further-reduce-cost-of-entry-overlap-detec.patch

b8221b
From 33b10b9112f2f51df049315438204efec7a5434c Mon Sep 17 00:00:00 2001
b8221b
From: Eric Garver <eric@garver.life>
b8221b
Date: Tue, 25 Jan 2022 09:29:32 -0500
b8221b
Subject: [PATCH 51/51] fix(ipset): further reduce cost of entry overlap
b8221b
 detection
b8221b
b8221b
This makes the complexity linear by sorting the networks ahead of time.
b8221b
b8221b
Fixes: #881
b8221b
Fixes: rhbz2043289
b8221b
(cherry picked from commit 36c170db265265e838a089858be4b20dbbd582eb)
b8221b
---
b8221b
 src/firewall/core/ipset.py    | 59 ++++++++++++++++++++++++++++++++---
b8221b
 src/tests/regression/gh881.at | 42 ++++++++++++++++++++++---
b8221b
 2 files changed, 92 insertions(+), 9 deletions(-)
b8221b
b8221b
diff --git a/src/firewall/core/ipset.py b/src/firewall/core/ipset.py
b8221b
index 66ea4335536d..b160d8345669 100644
b8221b
--- a/src/firewall/core/ipset.py
b8221b
+++ b/src/firewall/core/ipset.py
b8221b
@@ -327,8 +327,57 @@ def check_for_overlapping_entries(entries):
b8221b
         # at least one entry can not be parsed
b8221b
         return
b8221b
 
b8221b
-    while entries:
b8221b
-        entry = entries.pop()
b8221b
-        for itr in entries:
b8221b
-            if entry.overlaps(itr):
b8221b
-                raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps entry '{}'".format(entry, itr))
b8221b
+    # We can take advantage of some facts of IPv4Network/IPv6Network and
b8221b
+    # how Python sorts the networks to quickly detect overlaps.
b8221b
+    #
b8221b
+    # Facts:
b8221b
+    #
b8221b
+    #   1. IPv{4,6}Network are normalized to remove host bits, e.g.
b8221b
+    #     10.1.1.0/16 will become 10.1.0.0/16.
b8221b
+    #
b8221b
+    #   2. IPv{4,6}Network objects are sorted by:
b8221b
+    #     a. IP address (network bits)
b8221b
+    #   then
b8221b
+    #     b. netmask (significant bits count)
b8221b
+    #
b8221b
+    # Because of the above we have these properties:
b8221b
+    #
b8221b
+    #   1. big networks (netA) are sorted before smaller networks (netB)
b8221b
+    #      that overlap the big network (netA)
b8221b
+    #     - e.g. 10.1.128.0/17 (netA) sorts before 10.1.129.0/24 (netB)
b8221b
+    #   2. same value addresses (network bits) are grouped together even
b8221b
+    #      if the number of network bits vary. e.g. /16 vs /24
b8221b
+    #     - recall that address are normalized to remove host bits
b8221b
+    #     - e.g. 10.1.128.0/17 (netA) sorts before 10.1.128.0/24 (netC)
b8221b
+    #   3. non-overlapping networks (netD, netE) are always sorted before or
b8221b
+    #      after networks that overlap (netB, netC) the current one (netA)
b8221b
+    #     - e.g. 10.1.128.0/17 (netA) sorts before 10.2.128.0/16 (netD)
b8221b
+    #     - e.g. 10.1.128.0/17 (netA) sorts after 9.1.128.0/17 (netE)
b8221b
+    #     - e.g. 9.1.128.0/17 (netE) sorts before 10.1.129.0/24 (netB)
b8221b
+    #
b8221b
+    # With this we know the sorted list looks like:
b8221b
+    #
b8221b
+    #   list: [ netE, netA, netB, netC, netD ]
b8221b
+    #
b8221b
+    #   netE = non-overlapping network
b8221b
+    #   netA = big network
b8221b
+    #   netB = smaller network that overlaps netA (subnet)
b8221b
+    #   netC = smaller network that overlaps netA (subnet)
b8221b
+    #   netD = non-overlapping network
b8221b
+    #
b8221b
+    #   If networks netB and netC exist in the list, they overlap and are
b8221b
+    #   adjacent to netA.
b8221b
+    #
b8221b
+    # Checking for overlaps on a sorted list is thus:
b8221b
+    #
b8221b
+    #   1. compare adjacent elements in the list for overlaps
b8221b
+    #
b8221b
+    # Recall that we only need to detect a single overlap. We do not need to
b8221b
+    # detect them all.
b8221b
+    #
b8221b
+    entries.sort()
b8221b
+    prev_network = entries.pop(0)
b8221b
+    for current_network in entries:
b8221b
+        if prev_network.overlaps(current_network):
b8221b
+            raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps entry '{}'".format(prev_network, current_network))
b8221b
+        prev_network = current_network
b8221b
diff --git a/src/tests/regression/gh881.at b/src/tests/regression/gh881.at
b8221b
index c7326805b555..a5cf7e4eb912 100644
b8221b
--- a/src/tests/regression/gh881.at
b8221b
+++ b/src/tests/regression/gh881.at
b8221b
@@ -5,21 +5,55 @@ dnl build a large ipset
b8221b
 dnl
b8221b
 AT_DATA([./deny_cidr], [])
b8221b
 NS_CHECK([sh -c '
b8221b
-for I in $(seq 10); do
b8221b
+for I in $(seq 250); do
b8221b
   for J in $(seq 250); do
b8221b
     echo "10.${I}.${J}.0/24" >> ./deny_cidr
b8221b
   done
b8221b
 done
b8221b
 '])
b8221b
+NS_CHECK([echo "10.254.0.0/16" >> ./deny_cidr])
b8221b
 
b8221b
 dnl verify non-overlapping does not error
b8221b
 dnl
b8221b
 FWD_CHECK([--permanent --new-ipset=deny_set --type=hash:net --option=family=inet --option=hashsize=16384 --option=maxelem=20000], 0, [ignore])
b8221b
-NS_CHECK([time timeout 300 firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
b8221b
+NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
b8221b
+
b8221b
+dnl still no overlap
b8221b
+dnl
b8221b
+AT_DATA([./deny_cidr], [
b8221b
+9.0.0.0/8
b8221b
+11.1.0.0/16
b8221b
+])
b8221b
+NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
b8221b
 
b8221b
 dnl verify overlap detection actually detects an overlap
b8221b
 dnl
b8221b
-NS_CHECK([echo "10.1.0.0/16" >> ./deny_cidr])
b8221b
-NS_CHECK([time timeout 300 firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
b8221b
+AT_DATA([./deny_cidr], [
b8221b
+10.1.0.0/16
b8221b
+10.2.0.0/16
b8221b
+10.250.0.0/16
b8221b
+])
b8221b
+NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
b8221b
+
b8221b
+AT_DATA([./deny_cidr], [
b8221b
+10.253.0.0/16
b8221b
+10.253.128.0/17
b8221b
+])
b8221b
+NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
b8221b
+
b8221b
+AT_DATA([./deny_cidr], [
b8221b
+10.1.1.1/32
b8221b
+])
b8221b
+NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
b8221b
+
b8221b
+AT_DATA([./deny_cidr], [
b8221b
+10.0.0.0/8
b8221b
+10.0.0.0/25
b8221b
+])
b8221b
+NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
b8221b
+
b8221b
+dnl empty file, no additions, but previous ones will remain
b8221b
+AT_DATA([./deny_cidr], [])
b8221b
+FWD_CHECK([--permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
b8221b
 
b8221b
 FWD_END_TEST()
b8221b
-- 
b8221b
2.31.1
b8221b