From 3f4de1390d6459cdd17dd1b6f22b1a3aec002671 Mon Sep 17 00:00:00 2001
From: Christian Breunig <christian@breunig.cc>
Date: Sat, 8 Apr 2023 22:09:04 +0200
Subject: T5150: initial implementation of new Kernel/Zebra route-map support

It is possible to install a route-map which filters the routes between routing
daemons and the OS kernel (zebra)

As of now this can be done by e.g.
* set protocols ospf route-map foo
* set protocols ospfv3 route-map foo
* set protocols bgp route-map foo

Which in turn will install the following lines into FRR
* ip protocol ospf route-map foo
* ipv6 protocol ospf6 route-map foo
* ip protocol bgp route-map foo

The current state of the VyOS CLI is incomplete as there is no way to:
* Install a filter for BGP IPv6 routes
* Install a filter for static routes
* Install a filter for connected routes

Thus the CLI should be redesigned to close match what FRR does for both the
default and any other VRF

* set system ip protocol ospf route-map foo
* set system ipv6 protocol ospfv3 route-map foo
* set system ip protocol bgp route-map foo
* set system ipv6 protocol bgp route-map foo

The configuration can be migrated accordingly. This commit does not come with
the migrator, it will be comitted later.
---
 data/templates/frr/zebra.route-map.frr.j2          | 21 ++++++++
 .../include/system-ip-protocol.xml.i               | 56 ++++++++++++++++++++++
 .../include/system-ipv6-protocol.xml.i             | 52 ++++++++++++++++++++
 interface-definitions/system-ip.xml.in             |  1 +
 interface-definitions/system-ipv6.xml.in           |  1 +
 interface-definitions/vrf.xml.in                   |  2 +
 smoketest/scripts/cli/test_system_ip.py            | 29 ++++++++++-
 smoketest/scripts/cli/test_system_ipv6.py          | 34 ++++++++++++-
 src/conf_mode/protocols_static.py                  |  7 ---
 src/conf_mode/system-ip.py                         | 38 +++++++++++++--
 src/conf_mode/system-ipv6.py                       | 38 +++++++++++++--
 11 files changed, 264 insertions(+), 15 deletions(-)
 create mode 100644 data/templates/frr/zebra.route-map.frr.j2
 create mode 100644 interface-definitions/include/system-ip-protocol.xml.i
 create mode 100644 interface-definitions/include/system-ipv6-protocol.xml.i

diff --git a/data/templates/frr/zebra.route-map.frr.j2 b/data/templates/frr/zebra.route-map.frr.j2
new file mode 100644
index 000000000..bd461d904
--- /dev/null
+++ b/data/templates/frr/zebra.route-map.frr.j2
@@ -0,0 +1,21 @@
+!
+{% if vrf is vyos_defined %}
+vrf {{ vrf }}
+{%     if protocol is vyos_defined %}
+{%         for prot, prot_config in protocol.items() %}
+ {{ afi }} protocol {{ protocol }} route-map {{ prot_config.route_map }}
+{%         endfor %}
+{%     endif %}
+ exit-vrf
+!
+{% else %}
+{%     if protocol is vyos_defined %}
+{%         for prot, prot_config in protocol.items() %}
+{%             if prot is vyos_defined('ospfv3') %}
+{%                 set prot = 'ospf6' %}
+{%             endif %}
+{{ afi }} protocol {{ prot }} route-map {{ prot_config.route_map }}
+{%         endfor %}
+{%     endif %}
+{% endif %}
+!
diff --git a/interface-definitions/include/system-ip-protocol.xml.i b/interface-definitions/include/system-ip-protocol.xml.i
new file mode 100644
index 000000000..c630eb3f7
--- /dev/null
+++ b/interface-definitions/include/system-ip-protocol.xml.i
@@ -0,0 +1,56 @@
+<!-- include start from system-ip-protocol.xml.i -->
+<tagNode name="protocol">
+  <properties>
+    <help>Filter routing info exchanged between routing protocol and zebra</help>
+    <completionHelp>
+      <list>any babel bgp connected eigrp isis kernel ospf rip static table</list>
+    </completionHelp>
+    <valueHelp>
+      <format>any</format>
+      <description>Any of the above protocols</description>
+    </valueHelp>
+    <valueHelp>
+      <format>babel</format>
+      <description>Babel routing protocol</description>
+    </valueHelp>
+    <valueHelp>
+      <format>bgp</format>
+      <description>Border Gateway Protocol</description>
+    </valueHelp>
+    <valueHelp>
+      <format>connected</format>
+      <description>Connected routes (directly attached subnet or host)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>eigrp</format>
+      <description>Enhanced Interior Gateway Routing Protocol</description>
+    </valueHelp>
+    <valueHelp>
+      <format>isis</format>
+      <description>Intermediate System to Intermediate System</description>
+    </valueHelp>
+    <valueHelp>
+      <format>kernel</format>
+      <description>Kernel routes (not installed via the zebra RIB)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>ospf</format>
+      <description>Open Shortest Path First (OSPFv2)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>rip</format>
+      <description>Routing Information Protocol</description>
+    </valueHelp>
+    <valueHelp>
+      <format>static</format>
+      <description>Statically configured routes</description>
+    </valueHelp>
+    <constraint>
+      <regex>(any|babel|bgp|connected|eigrp|isis|kernel|ospf|rip|static|table)</regex>
+    </constraint>
+  </properties>
+  <children>
+    #include <include/route-map.xml.i>
+  </children>
+</tagNode>
+<!-- include end -->
\ No newline at end of file
diff --git a/interface-definitions/include/system-ipv6-protocol.xml.i b/interface-definitions/include/system-ipv6-protocol.xml.i
new file mode 100644
index 000000000..485776a71
--- /dev/null
+++ b/interface-definitions/include/system-ipv6-protocol.xml.i
@@ -0,0 +1,52 @@
+<!-- include start from system-ipv6-protocol.xml.i -->
+<tagNode name="protocol">
+  <properties>
+    <help>Filter routing info exchanged between routing protocol and zebra</help>
+    <completionHelp>
+      <list>any babel bgp connected isis kernel ospfv3 ripng static table</list>
+    </completionHelp>
+    <valueHelp>
+      <format>any</format>
+      <description>Any of the above protocols</description>
+    </valueHelp>
+    <valueHelp>
+      <format>babel</format>
+      <description>Babel routing protocol</description>
+    </valueHelp>
+    <valueHelp>
+      <format>bgp</format>
+      <description>Border Gateway Protocol</description>
+    </valueHelp>
+    <valueHelp>
+      <format>connected</format>
+      <description>Connected routes (directly attached subnet or host)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>isis</format>
+      <description>Intermediate System to Intermediate System</description>
+    </valueHelp>
+    <valueHelp>
+      <format>kernel</format>
+      <description>Kernel routes (not installed via the zebra RIB)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>ospfv3</format>
+      <description>Open Shortest Path First (OSPFv3)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>ripng</format>
+      <description>Routing Information Protocol next-generation</description>
+    </valueHelp>
+    <valueHelp>
+      <format>static</format>
+      <description>Statically configured routes</description>
+    </valueHelp>
+    <constraint>
+      <regex>(any|babel|bgp|connected|isis|kernel|ospfv3|ripng|static|table)</regex>
+    </constraint>
+  </properties>
+  <children>
+    #include <include/route-map.xml.i>
+  </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
index e00dbf252..abdede979 100644
--- a/interface-definitions/system-ip.xml.in
+++ b/interface-definitions/system-ip.xml.in
@@ -48,6 +48,7 @@
               </leafNode>
             </children>
           </node>
+          #include <include/system-ip-protocol.xml.i>
         </children>
       </node>
     </children>
diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in
index 63260d00c..e17e1c01c 100644
--- a/interface-definitions/system-ipv6.xml.in
+++ b/interface-definitions/system-ipv6.xml.in
@@ -36,6 +36,7 @@
               #include <include/arp-ndp-table-size.xml.i>
             </children>
           </node>
+          #include <include/system-ipv6-protocol.xml.i>
           <leafNode name="strict-dad">
             <properties>
               <help>Disable IPv6 operation on interface when DAD fails on LL addr</help>
diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in
index 96c6d8be2..028b31f7b 100644
--- a/interface-definitions/vrf.xml.in
+++ b/interface-definitions/vrf.xml.in
@@ -34,6 +34,7 @@
             </properties>
             <children>
               #include <include/interface/disable-forwarding.xml.i>
+              #include <include/system-ip-protocol.xml.i>
             </children>
           </node>
           <node name="ipv6">
@@ -42,6 +43,7 @@
             </properties>
             <children>
               #include <include/interface/disable-forwarding.xml.i>
+              #include <include/system-ipv6-protocol.xml.i>
             </children>
           </node>
           <node name="protocols">
diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py
index f71ef5b3f..e7f7e3345 100755
--- a/smoketest/scripts/cli/test_system_ip.py
+++ b/smoketest/scripts/cli/test_system_ip.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -17,6 +17,7 @@
 import unittest
 from base_vyostest_shim import VyOSUnitTestSHIM
 
+from vyos.configsession import ConfigSessionError
 from vyos.util import read_file
 
 base_path = ['system', 'ip']
@@ -82,5 +83,31 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase):
             self.assertEqual(read_file(gc_thresh2), str(size // 2))
             self.assertEqual(read_file(gc_thresh1), str(size // 8))
 
+    def test_system_ip_protocol_route_map(self):
+        protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis',
+                     'kernel', 'ospf', 'rip', 'static', 'table']
+
+        for protocol in protocols:
+            self.cli_set(['policy', 'route-map', f'route-map-{protocol}', 'rule', '10', 'action', 'permit'])
+            self.cli_set(base_path + ['protocol', protocol, 'route-map', f'route-map-{protocol}'])
+
+        self.cli_commit()
+
+        # Verify route-map properly applied to FRR
+        frrconfig = self.getFRRconfig('ip protocol', end='', daemon='zebra')
+        for protocol in protocols:
+            self.assertIn(f'ip protocol {protocol} route-map route-map-{protocol}', frrconfig)
+
+    def test_system_ip_protocol_non_existing_route_map(self):
+        non_existing = 'non-existing'
+        self.cli_set(base_path + ['protocol', 'static', 'route-map', non_existing])
+
+        # VRF does yet not exist - an error must be thrown
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+        self.cli_set(['policy', 'route-map', non_existing, 'rule', '10', 'action', 'deny'])
+        # Commit again
+        self.cli_commit()
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py
index c8aea9100..e91b924fc 100755
--- a/smoketest/scripts/cli/test_system_ipv6.py
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright (C) 2021-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -18,6 +18,7 @@ import unittest
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
+from vyos.configsession import ConfigSessionError
 from vyos.template import is_ipv4
 from vyos.util import read_file
 from vyos.util import get_interface_config
@@ -88,5 +89,36 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
             self.assertEqual(read_file(gc_thresh2), str(size // 2))
             self.assertEqual(read_file(gc_thresh1), str(size // 8))
 
+    def test_system_ipv6_protocol_route_map(self):
+        protocols = ['any', 'babel', 'bgp', 'connected', 'isis',
+                     'kernel', 'ospfv3', 'ripng', 'static', 'table']
+
+        for protocol in protocols:
+            route_map = 'route-map-' + protocol.replace('ospfv3', 'ospf6')
+
+            self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
+            self.cli_set(base_path + ['protocol', protocol, 'route-map', route_map])
+
+        self.cli_commit()
+
+        # Verify route-map properly applied to FRR
+        frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon='zebra')
+        for protocol in protocols:
+            # VyOS and FRR use a different name for OSPFv3 (IPv6)
+            if protocol == 'ospfv3':
+                protocol = 'ospf6'
+            self.assertIn(f'ipv6 protocol {protocol} route-map route-map-{protocol}', frrconfig)
+
+    def test_system_ipv6_protocol_non_existing_route_map(self):
+        non_existing = 'non-existing6'
+        self.cli_set(base_path + ['protocol', 'static', 'route-map', non_existing])
+
+        # VRF does yet not exist - an error must be thrown
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+        self.cli_set(['policy', 'route-map', non_existing, 'rule', '10', 'action', 'deny'])
+        # Commit again
+        self.cli_commit()
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 3e5ebb805..3eabf24bc 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -105,17 +105,10 @@ def generate(static):
 
 def apply(static):
     static_daemon = 'staticd'
-    zebra_daemon = 'zebra'
 
     # Save original configuration prior to starting any commit actions
     frr_cfg = frr.FRRConfig()
 
-    # The route-map used for the FIB (zebra) is part of the zebra daemon
-    frr_cfg.load_configuration(zebra_daemon)
-    frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '')
-    frr_cfg.commit_configuration(zebra_daemon)
-    frr_cfg.load_configuration(static_daemon)
-
     if 'vrf' in static:
         vrf = static['vrf']
         frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True)
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 0c5063ed3..95865c690 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2022 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -18,12 +18,15 @@ from sys import exit
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_map
+from vyos.template import render_to_string
 from vyos.util import call
 from vyos.util import dict_search
 from vyos.util import sysctl_write
 from vyos.util import write_file
 from vyos.xml import defaults
 from vyos import ConfigError
+from vyos import frr
 from vyos import airbag
 airbag.enable()
 
@@ -40,13 +43,30 @@ def get_config(config=None):
     default_values = defaults(base)
     opt = dict_merge(default_values, opt)
 
+    # When working with FRR we need to know the corresponding address-family
+    opt['afi'] = 'ip'
+
+    # We also need the route-map information from the config
+    #
+    # XXX: one MUST always call this without the key_mangling() option! See
+    # vyos.configverify.verify_common_route_maps() for more information.
+    tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+                                                          get_first_key=True)}}
+    # Merge policy dict into "regular" config dict
+    opt = dict_merge(tmp, opt)
     return opt
 
 def verify(opt):
-    pass
+    if 'protocol' in opt:
+        for protocol, protocol_options in opt['protocol'].items():
+            if 'route_map' in protocol_options:
+                verify_route_map(protocol_options['route_map'], opt)
+    return
 
 def generate(opt):
-    pass
+    if 'protocol' in opt:
+        opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+    return
 
 def apply(opt):
     # Apply ARP threshold values
@@ -78,6 +98,18 @@ def apply(opt):
     value = '1' if (tmp != None) else '0'
     sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
 
+    if 'protocol' in opt:
+        zebra_daemon = 'zebra'
+        # Save original configuration prior to starting any commit actions
+        frr_cfg = frr.FRRConfig()
+
+        # The route-map used for the FIB (zebra) is part of the zebra daemon
+        frr_cfg.load_configuration(zebra_daemon)
+        frr_cfg.modify_section(r'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+        if 'frr_zebra_config' in opt:
+            frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
+        frr_cfg.commit_configuration(zebra_daemon)
+
 if __name__ == '__main__':
     try:
         c = get_config()
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index 26aacf46b..b6d3a79c3 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2022 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -19,11 +19,14 @@ import os
 from sys import exit
 from vyos.config import Config
 from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_map
+from vyos.template import render_to_string
 from vyos.util import dict_search
 from vyos.util import sysctl_write
 from vyos.util import write_file
 from vyos.xml import defaults
 from vyos import ConfigError
+from vyos import frr
 from vyos import airbag
 airbag.enable()
 
@@ -41,13 +44,30 @@ def get_config(config=None):
     default_values = defaults(base)
     opt = dict_merge(default_values, opt)
 
+    # When working with FRR we need to know the corresponding address-family
+    opt['afi'] = 'ipv6'
+
+    # We also need the route-map information from the config
+    #
+    # XXX: one MUST always call this without the key_mangling() option! See
+    # vyos.configverify.verify_common_route_maps() for more information.
+    tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+                                                          get_first_key=True)}}
+    # Merge policy dict into "regular" config dict
+    opt = dict_merge(tmp, opt)
     return opt
 
 def verify(opt):
-    pass
+    if 'protocol' in opt:
+        for protocol, protocol_options in opt['protocol'].items():
+            if 'route_map' in protocol_options:
+                verify_route_map(protocol_options['route_map'], opt)
+    return
 
 def generate(opt):
-    pass
+    if 'protocol' in opt:
+        opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+    return
 
 def apply(opt):
     # configure multipath
@@ -78,6 +98,18 @@ def apply(opt):
             if name == 'accept_dad':
                 write_file(os.path.join(root, name), value)
 
+    if 'protocol' in opt:
+        zebra_daemon = 'zebra'
+        # Save original configuration prior to starting any commit actions
+        frr_cfg = frr.FRRConfig()
+
+        # The route-map used for the FIB (zebra) is part of the zebra daemon
+        frr_cfg.load_configuration(zebra_daemon)
+        frr_cfg.modify_section(r'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+        if 'frr_zebra_config' in opt:
+            frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
+        frr_cfg.commit_configuration(zebra_daemon)
+
 if __name__ == '__main__':
     try:
         c = get_config()
-- 
cgit v1.2.3