From 4e8839b6d78c7629cd2c1daee0438472c96365a4 Mon Sep 17 00:00:00 2001
From: Nicolas Fort <nicolasfort1988@gmail.com>
Date: Tue, 21 Nov 2023 17:36:20 +0000
Subject: T5419: firewall: backport firewall flowtable to Sagitta.

---
 data/templates/firewall/nftables-offload.j2        |  9 +++++
 data/templates/firewall/nftables.j2                | 13 +++++++
 interface-definitions/firewall.xml.in              | 40 +++++++++++++++++++
 .../include/firewall/action-forward.xml.i          | 45 ++++++++++++++++++++++
 .../include/firewall/ipv4-hook-forward.xml.i       |  2 +
 .../include/firewall/ipv6-hook-forward.xml.i       |  2 +
 .../include/firewall/offload-target.xml.i          | 10 +++++
 interface-definitions/interfaces-ethernet.xml.in   |  6 +++
 python/vyos/ethtool.py                             |  3 ++
 python/vyos/firewall.py                            | 31 ++++++++-------
 python/vyos/ifconfig/ethernet.py                   | 26 +++++++++++++
 smoketest/scripts/cli/test_firewall.py             | 38 ++++++++++++++++++
 src/conf_mode/firewall.py                          | 36 ++++++++++++++++-
 13 files changed, 247 insertions(+), 14 deletions(-)
 create mode 100644 data/templates/firewall/nftables-offload.j2
 create mode 100644 interface-definitions/include/firewall/action-forward.xml.i
 create mode 100644 interface-definitions/include/firewall/offload-target.xml.i

diff --git a/data/templates/firewall/nftables-offload.j2 b/data/templates/firewall/nftables-offload.j2
new file mode 100644
index 000000000..087fd141c
--- /dev/null
+++ b/data/templates/firewall/nftables-offload.j2
@@ -0,0 +1,9 @@
+{% macro flowtable(name, config) %}
+    flowtable VYOS_FLOWTABLE_{{ name }} {
+        hook ingress priority 0; devices = { {{ config.interface | join(', ') }} };
+{% if config.offload is vyos_defined('hardware') %}
+        flags offload;
+{% endif %}
+        counter
+    }
+{% endmacro %}
\ No newline at end of file
diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2
index 9fcacf677..91c4d0788 100644
--- a/data/templates/firewall/nftables.j2
+++ b/data/templates/firewall/nftables.j2
@@ -2,6 +2,7 @@
 
 {% import 'firewall/nftables-defines.j2' as group_tmpl %}
 {% import 'firewall/nftables-bridge.j2' as bridge_tmpl %}
+{% import 'firewall/nftables-offload.j2' as offload_tmpl %}
 {% import 'firewall/nftables-zone.j2' as zone_tmpl %}
 
 flush chain raw FW_CONNTRACK
@@ -38,6 +39,12 @@ delete table ip vyos_filter
 {% endif %}
 table ip vyos_filter {
 {% if ipv4 is vyos_defined %}
+{%     if flowtable is vyos_defined %}
+{%         for name, flowtable_conf in flowtable.items() %}
+{{ offload_tmpl.flowtable(name, flowtable_conf) }}
+{%         endfor %}
+{%     endif %}
+
 {%     set ns = namespace(sets=[]) %}
 {%     if ipv4.forward is vyos_defined %}
 {%         for prior, conf in ipv4.forward.items() %}
@@ -160,6 +167,12 @@ delete table ip6 vyos_filter
 {% endif %}
 table ip6 vyos_filter {
 {% if ipv6 is vyos_defined %}
+{%     if flowtable is vyos_defined %}
+{%         for name, flowtable_conf in flowtable.items() %}
+{{ offload_tmpl.flowtable(name, flowtable_conf) }}
+{%         endfor %}
+{%     endif %}
+
 {%     set ns = namespace(sets=[]) %}
 {%     if ipv6.forward is vyos_defined %}
 {%         for prior, conf in ipv6.forward.items() %}
diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index 4704b200e..0bb14a1b3 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -7,6 +7,46 @@
     </properties>
     <children>
       #include <include/firewall/global-options.xml.i>
+      <tagNode name="flowtable">
+        <properties>
+          <help>Flowtable</help>
+          <constraint>
+            <regex>[a-zA-Z0-9][\w\-\.]*</regex>
+          </constraint>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          <leafNode name="interface">
+            <properties>
+              <help>Interfaces to use this flowtable</help>
+              <completionHelp>
+                <script>${vyos_completion_dir}/list_interfaces</script>
+              </completionHelp>
+              <multi/>
+            </properties>
+          </leafNode>
+          <leafNode name="offload">
+            <properties>
+              <help>Offloading method</help>
+              <completionHelp>
+                <list>hardware software</list>
+              </completionHelp>
+              <valueHelp>
+                <format>hardware</format>
+                <description>Hardware offload</description>
+              </valueHelp>
+              <valueHelp>
+                <format>software</format>
+                <description>Software offload</description>
+              </valueHelp>
+              <constraint>
+                <regex>(hardware|software)</regex>
+              </constraint>
+            </properties>
+            <defaultValue>software</defaultValue>
+          </leafNode>
+        </children>
+      </tagNode>
       <node name="group">
         <properties>
           <help>Firewall group</help>
diff --git a/interface-definitions/include/firewall/action-forward.xml.i b/interface-definitions/include/firewall/action-forward.xml.i
new file mode 100644
index 000000000..87da72c97
--- /dev/null
+++ b/interface-definitions/include/firewall/action-forward.xml.i
@@ -0,0 +1,45 @@
+<!-- include start from firewall/action-forward.xml.i -->
+<leafNode name="action">
+  <properties>
+    <help>Rule action</help>
+    <completionHelp>
+      <list>accept continue jump reject return drop queue offload</list>
+    </completionHelp>
+    <valueHelp>
+      <format>accept</format>
+      <description>Accept matching entries</description>
+    </valueHelp>
+    <valueHelp>
+      <format>continue</format>
+      <description>Continue parsing next rule</description>
+    </valueHelp>
+    <valueHelp>
+      <format>jump</format>
+      <description>Jump to another chain</description>
+    </valueHelp>
+    <valueHelp>
+      <format>reject</format>
+      <description>Reject matching entries</description>
+    </valueHelp>
+    <valueHelp>
+      <format>return</format>
+      <description>Return from the current chain and continue at the next rule of the last chain</description>
+    </valueHelp>
+    <valueHelp>
+      <format>drop</format>
+      <description>Drop matching entries</description>
+    </valueHelp>
+    <valueHelp>
+      <format>queue</format>
+      <description>Enqueue packet to userspace</description>
+    </valueHelp>
+    <valueHelp>
+      <format>offload</format>
+      <description>Offload packet via flowtable</description>
+    </valueHelp>
+    <constraint>
+      <regex>(accept|continue|jump|reject|return|drop|queue|offload)</regex>
+    </constraint>
+  </properties>
+</leafNode>
+<!-- include end -->
\ No newline at end of file
diff --git a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i
index 9831498c9..100f1c3d9 100644
--- a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i
@@ -25,8 +25,10 @@
             <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage>
           </properties>
           <children>
+            #include <include/firewall/action-forward.xml.i>
             #include <include/firewall/common-rule-ipv4.xml.i>
             #include <include/firewall/inbound-interface.xml.i>
+            #include <include/firewall/offload-target.xml.i>
             #include <include/firewall/outbound-interface.xml.i>
           </children>
         </tagNode>
diff --git a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i
index 5c86871e5..fb38267eb 100644
--- a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i
@@ -25,8 +25,10 @@
             <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage>
           </properties>
           <children>
+            #include <include/firewall/action-forward.xml.i>
             #include <include/firewall/common-rule-ipv6.xml.i>
             #include <include/firewall/inbound-interface.xml.i>
+            #include <include/firewall/offload-target.xml.i>
             #include <include/firewall/outbound-interface.xml.i>
           </children>
         </tagNode>
diff --git a/interface-definitions/include/firewall/offload-target.xml.i b/interface-definitions/include/firewall/offload-target.xml.i
new file mode 100644
index 000000000..b1ae39100
--- /dev/null
+++ b/interface-definitions/include/firewall/offload-target.xml.i
@@ -0,0 +1,10 @@
+<!-- include start from firewall/offload-target.xml.i -->
+<leafNode name="offload-target">
+  <properties>
+    <help>Set flowtable offload target. Action offload must be defined to use this setting</help>
+    <completionHelp>
+      <path>firewall flowtable</path>
+    </completionHelp>
+  </properties>
+</leafNode>
+<!-- include end -->
\ No newline at end of file
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index 3669336fd..5aaa7095c 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -80,6 +80,12 @@
                   <valueless/>
                 </properties>
               </leafNode>
+              <leafNode name="hw-tc-offload">
+                <properties>
+                  <help>Enable Hardware Flow Offload</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
               <leafNode name="lro">
                 <properties>
                   <help>Enable Large Receive Offload</help>
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index ca3bcfc3d..f19632719 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -172,6 +172,9 @@ class Ethtool:
     def get_generic_segmentation_offload(self):
         return self._get_generic('generic-segmentation-offload')
 
+    def get_hw_tc_offload(self):
+        return self._get_generic('hw-tc-offload')
+
     def get_large_receive_offload(self):
         return self._get_generic('large-receive-offload')
 
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 8ae269fed..4fc1abb15 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -421,19 +421,24 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
     if 'action' in rule_conf:
         # Change action=return to action=action
         # #output.append(nft_action(rule_conf['action']))
-        output.append(f'{rule_conf["action"]}')
-        if 'jump' in rule_conf['action']:
-            target = rule_conf['jump_target']
-            output.append(f'NAME{def_suffix}_{target}')
-
-        if 'queue' in rule_conf['action']:
-            if 'queue' in rule_conf:
-                target = rule_conf['queue']
-                output.append(f'num {target}')
-
-            if 'queue_options' in rule_conf:
-                queue_opts = ','.join(rule_conf['queue_options'])
-                output.append(f'{queue_opts}')
+        if rule_conf['action'] == 'offload':
+            offload_target = rule_conf['offload_target']
+            output.append(f'flow add @VYOS_FLOWTABLE_{offload_target}')
+        else:
+            output.append(f'{rule_conf["action"]}')
+
+            if 'jump' in rule_conf['action']:
+                target = rule_conf['jump_target']
+                output.append(f'NAME{def_suffix}_{target}')
+
+            if 'queue' in rule_conf['action']:
+                if 'queue' in rule_conf:
+                    target = rule_conf['queue']
+                    output.append(f'num {target}')
+
+                if 'queue_options' in rule_conf:
+                    queue_opts = ','.join(rule_conf['queue_options'])
+                    output.append(f'{queue_opts}')
 
     else:
         output.append('return')
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 96e5f513b..aa1e87744 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -57,6 +57,10 @@ class EthernetIf(Interface):
             'validate': lambda v: assert_list(v, ['on', 'off']),
             'possible': lambda i, v: EthernetIf.feature(i, 'gso', v),
         },
+        'hw-tc-offload': {
+            'validate': lambda v: assert_list(v, ['on', 'off']),
+            'possible': lambda i, v: EthernetIf.feature(i, 'hw-tc-offload', v),
+        },
         'lro': {
             'validate': lambda v: assert_list(v, ['on', 'off']),
             'possible': lambda i, v: EthernetIf.feature(i, 'lro', v),
@@ -256,6 +260,25 @@ class EthernetIf(Interface):
                 print('Adapter does not support changing generic-segmentation-offload settings!')
         return False
 
+    def set_hw_tc_offload(self, state):
+        """
+        Enable hardware TC flow offload. State can be either True or False.
+        Example:
+        >>> from vyos.ifconfig import EthernetIf
+        >>> i = EthernetIf('eth0')
+        >>> i.set_hw_tc_offload(True)
+        """
+        if not isinstance(state, bool):
+            raise ValueError('Value out of range')
+
+        enabled, fixed = self.ethtool.get_hw_tc_offload()
+        if enabled != state:
+            if not fixed:
+                return self.set_interface('hw-tc-offload', 'on' if state else 'off')
+            else:
+                print('Adapter does not support changing hw-tc-offload settings!')
+        return False
+
     def set_lro(self, state):
         """
         Enable Large Receive offload. State can be either True or False.
@@ -392,6 +415,9 @@ class EthernetIf(Interface):
         # GSO (generic segmentation offload)
         self.set_gso(dict_search('offload.gso', config) != None)
 
+        # GSO (generic segmentation offload)
+        self.set_hw_tc_offload(dict_search('offload.hw-tc-offload', config) != None)
+
         # LRO (large receive offload)
         self.set_lro(dict_search('offload.lro', config) != None)
 
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index e6c928ad7..18940c04d 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -653,5 +653,43 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
                     break
             self.assertTrue(matched)
 
+    def test_flow_offload(self):
+        self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0'])
+        self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware'])
+
+        # QEMU virtual NIC does not support hw-tc-offload
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+
+        self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software'])
+
+        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'offload'])
+        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'offload-target', 'smoketest'])
+        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp'])
+        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'established'])
+        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'related'])
+
+        self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'action', 'offload'])
+        self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'offload-target', 'smoketest'])
+        self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp'])
+        self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'established'])
+        self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'related'])
+
+        self.cli_commit()
+
+        nftables_search = [
+            ['flowtable VYOS_FLOWTABLE_smoketest'],
+            ['hook ingress priority filter'],
+            ['devices = { eth0 }'],
+            ['ct state { established, related }', 'meta l4proto { tcp, udp }', 'flow add @VYOS_FLOWTABLE_smoketest'],
+        ]
+
+        self.verify_nftables(nftables_search, 'ip vyos_filter')
+        self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+        # Check conntrack
+        #self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
+        #self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index c4974d249..1cc146edd 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -26,7 +26,8 @@ from vyos.config import Config
 from vyos.configdict import node_changed
 from vyos.configdiff import get_config_diff, Diff
 from vyos.configdep import set_dependents, call_dependents
-# from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_interface_exists
+from vyos.ethtool import Ethtool
 from vyos.firewall import fqdn_config_parse
 from vyos.firewall import geoip_update
 from vyos.template import render
@@ -160,6 +161,15 @@ def verify_rule(firewall, rule_conf, ipv6):
             if target not in dict_search_args(firewall, 'ipv6', 'name'):
                 raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
 
+    if rule_conf['action'] == 'offload':
+        if 'offload_target' not in rule_conf:
+            raise ConfigError('Action set to offload, but no offload-target specified')
+
+        offload_target = rule_conf['offload_target']
+
+        if not dict_search_args(firewall, 'flowtable', offload_target):
+            raise ConfigError(f'Invalid offload-target. Flowtable "{offload_target}" does not exist on the system')
+
     if 'queue_options' in rule_conf:
         if 'queue' not in rule_conf['action']:
             raise ConfigError('queue-options defined, but action queue needed and it is not defined')
@@ -279,7 +289,31 @@ def verify_nested_group(group_name, group, groups, seen):
         if 'include' in groups[g]:
             verify_nested_group(g, groups[g], groups, seen)
 
+def verify_hardware_offload(ifname):
+    ethtool = Ethtool(ifname)
+    enabled, fixed = ethtool.get_hw_tc_offload()
+
+    if not enabled and fixed:
+        raise ConfigError(f'Interface "{ifname}" does not support hardware offload')
+
+    if not enabled:
+        raise ConfigError(f'Interface "{ifname}" requires "offload hw-tc-offload"')
+
 def verify(firewall):
+    if 'flowtable' in firewall:
+        for flowtable, flowtable_conf in firewall['flowtable'].items():
+            if 'interface' not in flowtable_conf:
+                raise ConfigError(f'Flowtable "{flowtable}" requires at least one interface')
+
+            for ifname in flowtable_conf['interface']:
+                verify_interface_exists(ifname)
+
+            if dict_search_args(flowtable_conf, 'offload') == 'hardware':
+                interfaces = flowtable_conf['interface']
+
+                for ifname in interfaces:
+                    verify_hardware_offload(ifname)
+
     if 'group' in firewall:
         for group_type in nested_group_types:
             if group_type in firewall['group']:
-- 
cgit v1.2.3