Blob Blame History Raw
From ddcd5e1677c3c273e259699c3de8ef3e5f69f14c Mon Sep 17 00:00:00 2001
From: Eric Garver <e@erig.me>
Date: Fri, 30 Nov 2018 09:55:30 -0500
Subject: [PATCH 25/34] ipXtables: support rich rule priorities

(cherry picked from commit 29d657527bd24492ec269fd9ab756bb7360dd3df)
---
 src/firewall/core/ipXtables.py | 214 ++++++++++++++++++++++++++++-----
 1 file changed, 186 insertions(+), 28 deletions(-)

diff --git a/src/firewall/core/ipXtables.py b/src/firewall/core/ipXtables.py
index b98ba5228e68..43ff9307a41c 100644
--- a/src/firewall/core/ipXtables.py
+++ b/src/firewall/core/ipXtables.py
@@ -20,6 +20,7 @@
 #
 
 import os.path
+import copy
 
 from firewall.core.base import SHORTCUTS, DEFAULT_ZONE_TARGET
 from firewall.core.prog import runProg
@@ -27,8 +28,9 @@ from firewall.core.logger import log
 from firewall.functions import tempFile, readfile, splitArgs, check_mac, portStr, \
                                check_single_address
 from firewall import config
-from firewall.errors import FirewallError, INVALID_PASSTHROUGH, INVALID_RULE
-from firewall.core.rich import Rich_Accept, Rich_Reject, Rich_Drop, Rich_Mark
+from firewall.errors import FirewallError, INVALID_PASSTHROUGH, INVALID_RULE, UNKNOWN_ERROR
+from firewall.core.rich import Rich_Accept, Rich_Reject, Rich_Drop, Rich_Mark, \
+                               Rich_Masquerade, Rich_ForwardPort, Rich_IcmpBlock
 import string
 
 BUILT_IN_CHAINS = {
@@ -275,6 +277,7 @@ class ip4tables(object):
         self.restore_wait_option = self._detect_restore_wait_option()
         self.fill_exists()
         self.available_tables = []
+        self.rich_rule_priority_counts = {}
 
     def fill_exists(self):
         self.command_exists = os.path.exists(self._command)
@@ -385,10 +388,91 @@ class ip4tables(object):
                     chain = args[i+1]
         return (table, chain)
 
+    def _set_rule_replace_rich_rule_priority(self, rule, rich_rule_priority_counts):
+        """
+        Change something like
+          -t filter -I public_IN %%RICH_RULE_PRIORITY%% 123
+        or
+          -t filter -A public_IN %%RICH_RULE_PRIORITY%% 321
+        into
+          -t filter -I public_IN 4
+        or
+          -t filter -I public_IN
+        """
+        try:
+            i = rule.index("%%RICH_RULE_PRIORITY%%")
+        except ValueError:
+            pass
+        else:
+            rule_add = True
+            insert = False
+            insert_add_index = -1
+            rule.pop(i)
+            priority = rule.pop(i)
+            if type(priority) != int:
+                raise FirewallError(INVALID_RULE, "rich rule priority must be followed by a number")
+
+            table = "filter"
+            for opt in [ "-t", "--table" ]:
+                try:
+                    j = rule.index(opt)
+                except ValueError:
+                    pass
+                else:
+                    if len(rule) >= j+1:
+                        table = rule[j+1]
+            for opt in [ "-A", "--append",
+                         "-I", "--insert",
+                         "-D", "--delete" ]:
+                try:
+                    insert_add_index = rule.index(opt)
+                except ValueError:
+                    pass
+                else:
+                    if len(rule) >= insert_add_index+1:
+                        chain = rule[insert_add_index+1]
+
+                    if opt in [ "-I", "--insert" ]:
+                        insert = True
+                    if opt in [ "-D", "--delete" ]:
+                        rule_add = False
+
+            chain = (table, chain)
+
+            # Add the rule to the priority counts. We don't need to store the
+            # rule, just bump the ref count for the priority value.
+            if not rule_add:
+                if chain not in rich_rule_priority_counts or \
+                   priority not in rich_rule_priority_counts[chain] or \
+                   rich_rule_priority_counts[chain][priority] <= 0:
+                    raise FirewallError(UNKNOWN_ERROR, "nonexistent or underflow of rich rule priority count")
+
+                rich_rule_priority_counts[chain][priority] -= 1
+            else:
+                if chain not in rich_rule_priority_counts:
+                    rich_rule_priority_counts[chain] = {}
+                if priority not in rich_rule_priority_counts[chain]:
+                    rich_rule_priority_counts[chain][priority] = 0
+
+                # calculate index of new rule
+                index = 1
+                for p in sorted(rich_rule_priority_counts[chain].keys()):
+                    if p == priority and insert:
+                        break
+                    index += rich_rule_priority_counts[chain][p]
+                    if p == priority:
+                        break
+
+                rich_rule_priority_counts[chain][priority] += 1
+
+                rule[insert_add_index] = "-I"
+                rule.insert(insert_add_index+2, "%d" % index)
+
     def set_rules(self, rules, log_denied):
         temp_file = tempFile()
 
         table_rules = { }
+        rich_rule_priority_counts = copy.deepcopy(self.rich_rule_priority_counts)
         for _rule in rules:
             rule = _rule[:]
 
@@ -412,6 +496,8 @@ class ip4tables(object):
                 else:
                     rule.pop(i)
 
+            self._set_rule_replace_rich_rule_priority(rule, rich_rule_priority_counts)
+
             table = "filter"
             # get table form rule
             for opt in [ "-t", "--table" ]:
@@ -473,6 +559,7 @@ class ip4tables(object):
         if status != 0:
             raise ValueError("'%s %s' failed: %s" % (self._restore_command,
                                                      " ".join(args), ret))
+        self.rich_rule_priority_counts = rich_rule_priority_counts
         return ret
 
     def set_rule(self, rule, log_denied):
@@ -496,7 +583,11 @@ class ip4tables(object):
             else:
                 rule.pop(i)
 
-        return self.__run(rule)
+        rich_rule_priority_counts = copy.deepcopy(self.rich_rule_priority_counts)
+        self._set_rule_replace_rich_rule_priority(rule, self.rich_rule_priority_counts)
+        output = self.__run(rule)
+        self.rich_rule_priority_counts = rich_rule_priority_counts
+        return output
 
     def get_available_tables(self, table=None):
         ret = []
@@ -546,6 +637,7 @@ class ip4tables(object):
         return wait_option
 
     def build_flush_rules(self):
+        self.rich_rule_priority_counts = {}
         rules = []
         for table in BUILT_IN_CHAINS.keys():
             # Flush firewall rules: -F
@@ -712,16 +804,22 @@ class ip4tables(object):
         OUR_CHAINS[table].update(set([_zone,
                                       "%s_log" % _zone,
                                       "%s_deny" % _zone,
+                                      "%s_rich_rule_pre" % _zone,
+                                      "%s_rich_rule_post" % _zone,
                                       "%s_allow" % _zone]))
 
         rules = []
         rules.append([ "-N", _zone, "-t", table ])
+        rules.append([ "-N", "%s_rich_rule_pre" % _zone, "-t", table ])
         rules.append([ "-N", "%s_log" % _zone, "-t", table ])
         rules.append([ "-N", "%s_deny" % _zone, "-t", table ])
         rules.append([ "-N", "%s_allow" % _zone, "-t", table ])
-        rules.append([ "-I", _zone, "1", "-t", table, "-j", "%s_log" % _zone ])
-        rules.append([ "-I", _zone, "2", "-t", table, "-j", "%s_deny" % _zone ])
-        rules.append([ "-I", _zone, "3", "-t", table, "-j", "%s_allow" % _zone ])
+        rules.append([ "-N", "%s_rich_rule_post" % _zone, "-t", table ])
+        rules.append([ "-I", _zone, "1", "-t", table, "-j", "%s_rich_rule_pre" % _zone ])
+        rules.append([ "-I", _zone, "2", "-t", table, "-j", "%s_log" % _zone ])
+        rules.append([ "-I", _zone, "3", "-t", table, "-j", "%s_deny" % _zone ])
+        rules.append([ "-I", _zone, "4", "-t", table, "-j", "%s_allow" % _zone ])
+        rules.append([ "-I", _zone, "5", "-t", table, "-j", "%s_rich_rule_post" % _zone ])
 
         # Handle trust, block and drop zones:
         # Add an additional rule with the zone target (accept, reject
@@ -733,17 +831,17 @@ class ip4tables(object):
         if table == "filter" and \
            target in [ "ACCEPT", "REJECT", "%%REJECT%%", "DROP" ] and \
            chain in [ "INPUT", "FORWARD_IN", "FORWARD_OUT", "OUTPUT" ]:
-            rules.append([ "-I", _zone, "4", "-t", table, "-j", target ])
+            rules.append([ "-I", _zone, "6", "-t", table, "-j", target ])
 
         if self._fw.get_log_denied() != "off":
             if table == "filter" and \
                chain in [ "INPUT", "FORWARD_IN", "FORWARD_OUT", "OUTPUT" ]:
                 if target in [ "REJECT", "%%REJECT%%" ]:
-                    rules.append([ "-I", _zone, "4", "-t", table, "%%LOGTYPE%%",
+                    rules.append([ "-I", _zone, "6", "-t", table, "%%LOGTYPE%%",
                                    "-j", "LOG", "--log-prefix",
                                    "\"%s_REJECT: \"" % _zone ])
                 if target == "DROP":
-                    rules.append([ "-I", _zone, "4", "-t", table, "%%LOGTYPE%%",
+                    rules.append([ "-I", _zone, "6", "-t", table, "%%LOGTYPE%%",
                                    "-j", "LOG", "--log-prefix",
                                    "\"%s_DROP: \"" % _zone ])
         return rules
@@ -753,13 +851,53 @@ class ip4tables(object):
             return [ "-m", "limit", "--limit", limit.value ]
         return []
 
+    def _rich_rule_chain_suffix(self, rich_rule):
+        if type(rich_rule.element) in [Rich_Masquerade, Rich_ForwardPort, Rich_IcmpBlock]:
+            # These are special and don't have an explicit action
+            pass
+        elif rich_rule.action:
+            if type(rich_rule.action) not in [Rich_Accept, Rich_Reject, Rich_Drop, Rich_Mark]:
+                raise FirewallError(INVALID_RULE, "Unknown action %s" % type(rich_rule.action))
+        else:
+            raise FirewallError(INVALID_RULE, "No rule action specified.")
+
+        if rich_rule.priority == 0:
+            if type(rich_rule.element) in [Rich_Masquerade, Rich_ForwardPort] or \
+               type(rich_rule.action) in [Rich_Accept, Rich_Mark]:
+                return "allow"
+            elif type(rich_rule.element) in [Rich_IcmpBlock] or \
+                 type(rich_rule.action) in [Rich_Reject, Rich_Drop]:
+                return "deny"
+        elif rich_rule.priority < 0:
+            return "rich_rule_pre"
+        else:
+            return "rich_rule_post"
+
+    def _rich_rule_chain_suffix_from_log(self, rich_rule):
+        if not rich_rule.log and not rich_rule.audit:
+            raise FirewallError(INVALID_RULE, "Not log or audit")
+
+        if rich_rule.priority == 0:
+            return "log"
+        elif rich_rule.priority < 0:
+            return "rich_rule_pre"
+        else:
+            return "rich_rule_post"
+
+    def _rich_rule_priority_fragment(self, rich_rule):
+        if rich_rule.priority == 0:
+            return []
+        return ["%%RICH_RULE_PRIORITY%%", rich_rule.priority]
+
     def _rich_rule_log(self, rich_rule, enable, table, target, rule_fragment):
         if not rich_rule.log:
             return []
 
         add_del = { True: "-A", False: "-D" }[enable]
 
-        rule = [ add_del, "%s_log" % (target), "-t", table]
+        chain_suffix = self._rich_rule_chain_suffix_from_log(rich_rule)
+        rule = ["-t", table, add_del, "%s_%s" % (target, chain_suffix)]
+        rule += self._rich_rule_priority_fragment(rich_rule)
         rule += rule_fragment + [ "-j", "LOG" ]
         if rich_rule.log.prefix:
             rule += [ "--log-prefix", "'%s'" % rich_rule.log.prefix ]
@@ -775,7 +913,10 @@ class ip4tables(object):
 
         add_del = { True: "-A", False: "-D" }[enable]
 
-        rule = [add_del, "%s_log" % (target), "-t", table] + rule_fragment
+        chain_suffix = self._rich_rule_chain_suffix_from_log(rich_rule)
+        rule = ["-t", table, add_del, "%s_%s" % (target, chain_suffix)]
+        rule += self._rich_rule_priority_fragment(rich_rule)
+        rule += rule_fragment
         if type(rich_rule.action) == Rich_Accept:
             _type = "accept"
         elif type(rich_rule.action) == Rich_Reject:
@@ -795,28 +936,28 @@ class ip4tables(object):
 
         add_del = { True: "-A", False: "-D" }[enable]
 
+        chain_suffix = self._rich_rule_chain_suffix(rich_rule)
+        chain = "%s_%s" % (target, chain_suffix)
         if type(rich_rule.action) == Rich_Accept:
-            chain = "%s_allow" % target
             rule_action = [ "-j", "ACCEPT" ]
         elif type(rich_rule.action) == Rich_Reject:
-            chain = "%s_deny" % target
             rule_action = [ "-j", "REJECT" ]
             if rich_rule.action.type:
                 rule_action += [ "--reject-with", rich_rule.action.type ]
         elif type(rich_rule.action) ==  Rich_Drop:
-            chain = "%s_deny" % target
             rule_action = [ "-j", "DROP" ]
         elif type(rich_rule.action) == Rich_Mark:
             target = DEFAULT_ZONE_TARGET.format(chain=SHORTCUTS["PREROUTING"],
                                                 zone=zone)
             table = "mangle"
-            chain = "%s_allow" % target
+            chain = "%s_%s" % (target, chain_suffix)
             rule_action = [ "-j", "MARK", "--set-xmark", rich_rule.action.set ]
         else:
             raise FirewallError(INVALID_RULE,
                                 "Unknown action %s" % type(rich_rule.action))
 
-        rule = [ add_del, chain, "-t", table ]
+        rule = ["-t", table, add_del, chain]
+        rule += self._rich_rule_priority_fragment(rich_rule)
         rule += rule_fragment + rule_action
         rule += self._rule_limit(rich_rule.action.limit)
 
@@ -957,11 +1098,15 @@ class ip4tables(object):
                                             zone=zone)
         rule_fragment = []
         if rich_rule:
+            chain_suffix = self._rich_rule_chain_suffix(rich_rule)
+            rule_fragment += self._rich_rule_priority_fragment(rich_rule)
             rule_fragment += self._rich_rule_destination_fragment(rich_rule.destination)
             rule_fragment += self._rich_rule_source_fragment(rich_rule.source)
+        else:
+            chain_suffix = "allow"
 
         rules = []
-        rules.append([ add_del, "%s_allow" % (target), "-t", "nat" ]
+        rules.append(["-t", "nat", add_del, "%s_%s" % (target, chain_suffix)]
                      + rule_fragment +
                      [ "!", "-o", "lo", "-j", "MASQUERADE" ])
         # FORWARD_OUT
@@ -969,10 +1114,14 @@ class ip4tables(object):
                                             zone=zone)
         rule_fragment = []
         if rich_rule:
+            chain_suffix = self._rich_rule_chain_suffix(rich_rule)
+            rule_fragment += self._rich_rule_priority_fragment(rich_rule)
             rule_fragment += self._rich_rule_destination_fragment(rich_rule.destination)
             rule_fragment += self._rich_rule_source_fragment(rich_rule.source)
+        else:
+            chain_suffix = "allow"
 
-        rules.append([ add_del, "%s_allow" % (target), "-t", "filter"]
+        rules.append(["-t", "filter", add_del, "%s_%s" % (target, chain_suffix)]
                      + rule_fragment +
                      ["-m", "conntrack", "--ctstate", "NEW,UNTRACKED", "-j", "ACCEPT" ])
 
@@ -998,28 +1147,35 @@ class ip4tables(object):
                                             zone=zone)
 
         rule_fragment = [ "-p", protocol, "--dport", portStr(port) ]
+        rich_rule_priority_fragment = []
         if rich_rule:
+            chain_suffix = self._rich_rule_chain_suffix(rich_rule)
+            rich_rule_priority_fragment = self._rich_rule_priority_fragment(rich_rule)
             rule_fragment += self._rich_rule_destination_fragment(rich_rule.destination)
             rule_fragment += self._rich_rule_source_fragment(rich_rule.source)
+        else:
+            chain_suffix = "allow"
 
         rules = []
         if rich_rule:
             rules.append(self._rich_rule_log(rich_rule, enable, "mangle", target, rule_fragment))
-        rules.append([ add_del, "%s_allow" % (target), "-t", "mangle"]
-                     + rule_fragment + 
+        rules.append(["-t", "mangle", add_del, "%s_%s" % (target, chain_suffix)]
+                     + rich_rule_priority_fragment + rule_fragment +
                      [ "-j", "MARK", "--set-mark", mark_str ])
 
         # local and remote
-        rules.append([ add_del, "%s_allow" % (target), "-t", "nat",
-                     "-p", protocol ] + mark +
+        rules.append(["-t", "nat", add_del, "%s_%s" % (target, chain_suffix)]
+                     + rich_rule_priority_fragment +
+                     ["-p", protocol ] + mark +
                      [ "-j", "DNAT", "--to-destination", to ])
 
         target = DEFAULT_ZONE_TARGET.format(chain=SHORTCUTS[filter_chain],
                                             zone=zone)
-        rules.append([ add_del, "%s_allow" % (target),
-                     "-t", "filter", "-m", "conntrack",
-                     "--ctstate", "NEW,UNTRACKED" ] +
-                     mark + [ "-j", "ACCEPT" ])
+        rules.append(["-t", "filter", add_del, "%s_%s" % (target, chain_suffix)]
+                     + rich_rule_priority_fragment +
+                     ["-m", "conntrack", "--ctstate", "NEW,UNTRACKED" ]
+                     + mark +
+                     [ "-j", "ACCEPT" ])
 
         return rules
 
@@ -1057,7 +1213,9 @@ class ip4tables(object):
                 if rich_rule.action:
                     rules.append(self._rich_rule_action(zone, rich_rule, enable, table, target, rule_fragment))
                 else:
-                    rules.append([ add_del, "%s_deny" % target, "-t", table ]
+                    chain_suffix = self._rich_rule_chain_suffix(rich_rule)
+                    rules.append(["-t", table, add_del, "%s_%s" % (target, chain_suffix)]
+                                 + self._rich_rule_priority_fragment(rich_rule)
                                  + rule_fragment +
                                  [ "-j", "%%REJECT%%" ])
             else:
@@ -1076,7 +1234,7 @@ class ip4tables(object):
         table = "filter"
         rules = []
         for chain in [ "INPUT", "FORWARD_IN" ]:
-            rule_idx = 4
+            rule_idx = 6
             _zone = DEFAULT_ZONE_TARGET.format(chain=SHORTCUTS[chain],
                                                zone=zone)
 
-- 
2.18.0