summaryrefslogtreecommitdiff
path: root/smoketest/scripts/cli
diff options
context:
space:
mode:
Diffstat (limited to 'smoketest/scripts/cli')
-rw-r--r--smoketest/scripts/cli/base_accel_ppp_test.py648
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py1320
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py162
-rw-r--r--smoketest/scripts/cli/test_backslash_escape.py68
-rw-r--r--smoketest/scripts/cli/test_cgnat.py138
-rw-r--r--smoketest/scripts/cli/test_config_dependency.py130
-rw-r--r--smoketest/scripts/cli/test_configd_init.py39
-rw-r--r--smoketest/scripts/cli/test_container.py268
-rw-r--r--smoketest/scripts/cli/test_firewall.py1167
-rw-r--r--smoketest/scripts/cli/test_high-availability_virtual-server.py149
-rw-r--r--smoketest/scripts/cli/test_high-availability_vrrp.py320
-rw-r--r--smoketest/scripts/cli/test_interfaces_bonding.py313
-rw-r--r--smoketest/scripts/cli/test_interfaces_bridge.py498
-rw-r--r--smoketest/scripts/cli/test_interfaces_dummy.py30
-rw-r--r--smoketest/scripts/cli/test_interfaces_ethernet.py226
-rw-r--r--smoketest/scripts/cli/test_interfaces_geneve.py84
-rw-r--r--smoketest/scripts/cli/test_interfaces_input.py51
-rw-r--r--smoketest/scripts/cli/test_interfaces_l2tpv3.py66
-rw-r--r--smoketest/scripts/cli/test_interfaces_loopback.py59
-rw-r--r--smoketest/scripts/cli/test_interfaces_macsec.py272
-rw-r--r--smoketest/scripts/cli/test_interfaces_openvpn.py868
-rw-r--r--smoketest/scripts/cli/test_interfaces_pppoe.py259
-rw-r--r--smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py46
-rw-r--r--smoketest/scripts/cli/test_interfaces_tunnel.py413
-rw-r--r--smoketest/scripts/cli/test_interfaces_virtual-ethernet.py61
-rw-r--r--smoketest/scripts/cli/test_interfaces_vti.py49
-rw-r--r--smoketest/scripts/cli/test_interfaces_vxlan.py366
-rw-r--r--smoketest/scripts/cli/test_interfaces_wireguard.py204
-rw-r--r--smoketest/scripts/cli/test_interfaces_wireless.py635
-rw-r--r--smoketest/scripts/cli/test_load-balancing_reverse-proxy.py502
-rw-r--r--smoketest/scripts/cli/test_load-balancing_wan.py254
-rw-r--r--smoketest/scripts/cli/test_nat.py308
-rw-r--r--smoketest/scripts/cli/test_nat64.py98
-rw-r--r--smoketest/scripts/cli/test_nat66.py241
-rw-r--r--smoketest/scripts/cli/test_netns.py79
-rw-r--r--smoketest/scripts/cli/test_op-mode_show.py46
-rw-r--r--smoketest/scripts/cli/test_pki.py254
-rw-r--r--smoketest/scripts/cli/test_policy.py1994
-rw-r--r--smoketest/scripts/cli/test_policy_route.py321
-rw-r--r--smoketest/scripts/cli/test_protocols_bfd.py236
-rw-r--r--smoketest/scripts/cli/test_protocols_bgp.py1411
-rw-r--r--smoketest/scripts/cli/test_protocols_igmp-proxy.py96
-rw-r--r--smoketest/scripts/cli/test_protocols_isis.py416
-rw-r--r--smoketest/scripts/cli/test_protocols_mpls.py120
-rw-r--r--smoketest/scripts/cli/test_protocols_nhrp.py121
-rw-r--r--smoketest/scripts/cli/test_protocols_openfabric.py186
-rw-r--r--smoketest/scripts/cli/test_protocols_ospf.py565
-rw-r--r--smoketest/scripts/cli/test_protocols_ospfv3.py339
-rw-r--r--smoketest/scripts/cli/test_protocols_pim.py192
-rw-r--r--smoketest/scripts/cli/test_protocols_pim6.py146
-rw-r--r--smoketest/scripts/cli/test_protocols_rip.py183
-rw-r--r--smoketest/scripts/cli/test_protocols_ripng.py160
-rw-r--r--smoketest/scripts/cli/test_protocols_rpki.py247
-rw-r--r--smoketest/scripts/cli/test_protocols_segment-routing.py110
-rw-r--r--smoketest/scripts/cli/test_protocols_static.py482
-rw-r--r--smoketest/scripts/cli/test_protocols_static_arp.py88
-rw-r--r--smoketest/scripts/cli/test_protocols_static_multicast.py49
-rw-r--r--smoketest/scripts/cli/test_qos.py859
-rw-r--r--smoketest/scripts/cli/test_service_broadcast-relay.py68
-rw-r--r--smoketest/scripts/cli/test_service_dhcp-relay.py124
-rw-r--r--smoketest/scripts/cli/test_service_dhcp-server.py830
-rw-r--r--smoketest/scripts/cli/test_service_dhcpv6-relay.py110
-rw-r--r--smoketest/scripts/cli/test_service_dhcpv6-server.py288
-rw-r--r--smoketest/scripts/cli/test_service_dns_dynamic.py352
-rw-r--r--smoketest/scripts/cli/test_service_dns_forwarding.py344
-rw-r--r--smoketest/scripts/cli/test_service_https.py502
-rw-r--r--smoketest/scripts/cli/test_service_ids_ddos-protection.py116
-rw-r--r--smoketest/scripts/cli/test_service_ipoe-server.py272
-rw-r--r--smoketest/scripts/cli/test_service_lldp.py138
-rw-r--r--smoketest/scripts/cli/test_service_mdns_repeater.py134
-rw-r--r--smoketest/scripts/cli/test_service_monitoring_telegraf.py96
-rw-r--r--smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py87
-rw-r--r--smoketest/scripts/cli/test_service_ndp-proxy.py69
-rw-r--r--smoketest/scripts/cli/test_service_ntp.py264
-rw-r--r--smoketest/scripts/cli/test_service_pppoe-server.py216
-rw-r--r--smoketest/scripts/cli/test_service_router-advert.py257
-rw-r--r--smoketest/scripts/cli/test_service_salt-minion.py105
-rw-r--r--smoketest/scripts/cli/test_service_snmp.py264
-rw-r--r--smoketest/scripts/cli/test_service_ssh.py325
-rw-r--r--smoketest/scripts/cli/test_service_stunnel.py624
-rw-r--r--smoketest/scripts/cli/test_service_tftp-server.py151
-rw-r--r--smoketest/scripts/cli/test_service_webproxy.py302
-rw-r--r--smoketest/scripts/cli/test_system_acceleration_qat.py43
-rw-r--r--smoketest/scripts/cli/test_system_conntrack.py318
-rw-r--r--smoketest/scripts/cli/test_system_flow-accounting.py295
-rw-r--r--smoketest/scripts/cli/test_system_frr.py162
-rw-r--r--smoketest/scripts/cli/test_system_ip.py126
-rw-r--r--smoketest/scripts/cli/test_system_ipv6.py145
-rw-r--r--smoketest/scripts/cli/test_system_lcd.py51
-rw-r--r--smoketest/scripts/cli/test_system_login.py348
-rw-r--r--smoketest/scripts/cli/test_system_logs.py117
-rw-r--r--smoketest/scripts/cli/test_system_option.py99
-rw-r--r--smoketest/scripts/cli/test_system_resolvconf.py112
-rw-r--r--smoketest/scripts/cli/test_system_sflow.py122
-rw-r--r--smoketest/scripts/cli/test_system_syslog.py106
-rw-r--r--smoketest/scripts/cli/test_vpn_ipsec.py1359
-rw-r--r--smoketest/scripts/cli/test_vpn_l2tp.py123
-rw-r--r--smoketest/scripts/cli/test_vpn_openconnect.py268
-rw-r--r--smoketest/scripts/cli/test_vpn_pptp.py39
-rw-r--r--smoketest/scripts/cli/test_vpn_sstp.py90
-rw-r--r--smoketest/scripts/cli/test_vrf.py599
101 files changed, 29572 insertions, 0 deletions
diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py
new file mode 100644
index 0000000..c6f6cb8
--- /dev/null
+++ b/smoketest/scripts/cli/base_accel_ppp_test.py
@@ -0,0 +1,648 @@
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from configparser import ConfigParser
+
+from vyos.configsession import ConfigSessionError
+from vyos.template import is_ipv4
+from vyos.utils.cpu import get_core_count
+from vyos.utils.process import process_named_running
+from vyos.utils.process import cmd
+
+class BasicAccelPPPTest:
+ class TestCase(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._process_name = "accel-pppd"
+
+ super(BasicAccelPPPTest.TestCase, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, cls._base_path)
+
+ def setUp(self):
+ self._gateway = "192.0.2.1"
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ self.cli_delete(self._base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(self._process_name))
+
+ self.cli_delete(self._base_path)
+ self.cli_commit()
+
+ # Check for running process
+ self.assertFalse(process_named_running(self._process_name))
+
+ def set(self, path):
+ self.cli_set(self._base_path + path)
+
+ def delete(self, path):
+ self.cli_delete(self._base_path + path)
+
+ def basic_protocol_specific_config(self):
+ """
+ An astract method.
+ Initialize protocol scpecific configureations.
+ """
+ self.assertFalse(True, msg="Function must be defined")
+
+ def initial_auth_config(self):
+ """
+ Initialization of default authentication for all protocols
+ """
+ self.set(
+ [
+ "authentication",
+ "local-users",
+ "username",
+ "vyos",
+ "password",
+ "vyos",
+ ]
+ )
+ self.set(["authentication", "mode", "local"])
+
+ def initial_gateway_config(self):
+ """
+ Initialization of default gateway
+ """
+ self.set(["gateway-address", self._gateway])
+
+ def initial_pool_config(self):
+ """
+ Initialization of default client ip pool
+ """
+ first_pool = "SIMPLE-POOL"
+ self.set(["client-ip-pool", first_pool, "range", "192.0.2.0/24"])
+ self.set(["default-pool", first_pool])
+
+ def basic_config(self, is_auth=True, is_gateway=True, is_client_pool=True):
+ """
+ Initialization of basic configuration
+ :param is_auth: authentication initialization
+ :type is_auth: bool
+ :param is_gateway: gateway initialization
+ :type is_gateway: bool
+ :param is_client_pool: client ip pool initialization
+ :type is_client_pool: bool
+ """
+ self.basic_protocol_specific_config()
+ if is_auth:
+ self.initial_auth_config()
+ if is_gateway:
+ self.initial_gateway_config()
+ if is_client_pool:
+ self.initial_pool_config()
+
+ def getConfig(self, start, end="cli"):
+ """
+ Return part of configuration from line
+ where the first injection of start keyword to the line
+ where the first injection of end keyowrd
+ :param start: start keyword
+ :type start: str
+ :param end: end keyword
+ :type end: str
+ :return: part of config
+ :rtype: str
+ """
+ command = f'cat {self._config_file} | sed -n "/^\[{start}/,/^\[{end}/p"'
+ out = cmd(command)
+ return out
+
+ def verify(self, conf):
+ self.assertEqual(conf["core"]["thread-count"], str(get_core_count()))
+
+ def test_accel_name_servers(self):
+ # Verify proper Name-Server configuration for IPv4 and IPv6
+ self.basic_config()
+
+ nameserver = ["192.0.2.1", "192.0.2.2", "2001:db8::1"]
+ for ns in nameserver:
+ self.set(["name-server", ns])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+
+ # IPv4 and IPv6 nameservers must be checked individually
+ for ns in nameserver:
+ if is_ipv4(ns):
+ self.assertIn(ns, [conf["dns"]["dns1"], conf["dns"]["dns2"]])
+ else:
+ self.assertEqual(conf["ipv6-dns"][ns], None)
+
+ def test_accel_local_authentication(self):
+ # Test configuration of local authentication
+ self.basic_config()
+
+ # upload / download limit
+ user = "test"
+ password = "test2"
+ static_ip = "100.100.100.101"
+ upload = "5000"
+ download = "10000"
+ self.set(
+ [
+ "authentication",
+ "local-users",
+ "username",
+ user,
+ "password",
+ password,
+ ]
+ )
+ self.set(
+ [
+ "authentication",
+ "local-users",
+ "username",
+ user,
+ "static-ip",
+ static_ip,
+ ]
+ )
+ self.set(
+ [
+ "authentication",
+ "local-users",
+ "username",
+ user,
+ "rate-limit",
+ "upload",
+ upload,
+ ]
+ )
+
+ # upload rate-limit requires also download rate-limit
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.set(
+ [
+ "authentication",
+ "local-users",
+ "username",
+ user,
+ "rate-limit",
+ "download",
+ download,
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+
+ # check proper path to chap-secrets file
+ self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets)
+
+ # basic verification
+ self.verify(conf)
+
+ # check local users
+ tmp = cmd(f"sudo cat {self._chap_secrets}")
+ regex = f"{user}\s+\*\s+{password}\s+{static_ip}\s+{download}/{upload}"
+ tmp = re.findall(regex, tmp)
+ self.assertTrue(tmp)
+
+ # Check local-users default value(s)
+ self.delete(
+ ["authentication", "local-users", "username", user, "static-ip"]
+ )
+ # commit changes
+ self.cli_commit()
+
+ # check local users
+ tmp = cmd(f"sudo cat {self._chap_secrets}")
+ regex = f"{user}\s+\*\s+{password}\s+\*\s+{download}/{upload}"
+ tmp = re.findall(regex, tmp)
+ self.assertTrue(tmp)
+
+ def test_accel_radius_authentication(self):
+ # Test configuration of RADIUS authentication for PPPoE server
+ self.basic_config()
+
+ radius_server = "192.0.2.22"
+ radius_key = "secretVyOS"
+ radius_port = "2000"
+ radius_port_acc = "3000"
+ acct_interim_jitter = '10'
+ acct_interim_interval = '10'
+ acct_timeout = '30'
+
+ self.set(["authentication", "mode", "radius"])
+ self.set(
+ ["authentication", "radius", "server", radius_server, "key", radius_key]
+ )
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "server",
+ radius_server,
+ "port",
+ radius_port,
+ ]
+ )
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "server",
+ radius_server,
+ "acct-port",
+ radius_port_acc,
+ ]
+ )
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "acct-interim-jitter",
+ acct_interim_jitter,
+ ]
+ )
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "accounting-interim-interval",
+ acct_interim_interval,
+ ]
+ )
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "acct-timeout",
+ acct_timeout,
+ ]
+ )
+
+ coa_server = "4.4.4.4"
+ coa_key = "testCoA"
+ self.set(
+ ["authentication", "radius", "dynamic-author", "server", coa_server]
+ )
+ self.set(["authentication", "radius", "dynamic-author", "key", coa_key])
+
+ nas_id = "VyOS-PPPoE"
+ nas_ip = "7.7.7.7"
+ self.set(["authentication", "radius", "nas-identifier", nas_id])
+ self.set(["authentication", "radius", "nas-ip-address", nas_ip])
+
+ source_address = "1.2.3.4"
+ self.set(["authentication", "radius", "source-address", source_address])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+
+ # basic verification
+ self.verify(conf)
+
+ # check auth
+ self.assertTrue(conf["radius"].getboolean("verbose"))
+ self.assertEqual(conf["radius"]["acct-timeout"], acct_timeout)
+ self.assertEqual(conf["radius"]["acct-interim-interval"], acct_interim_interval)
+ self.assertEqual(conf["radius"]["acct-interim-jitter"], acct_interim_jitter)
+ self.assertEqual(conf["radius"]["timeout"], "3")
+ self.assertEqual(conf["radius"]["max-try"], "3")
+
+ self.assertEqual(
+ conf["radius"]["dae-server"], f"{coa_server}:1700,{coa_key}"
+ )
+ self.assertEqual(conf["radius"]["nas-identifier"], nas_id)
+ self.assertEqual(conf["radius"]["nas-ip-address"], nas_ip)
+ self.assertEqual(conf["radius"]["bind"], source_address)
+
+ server = conf["radius"]["server"].split(",")
+ self.assertEqual(radius_server, server[0])
+ self.assertEqual(radius_key, server[1])
+ self.assertEqual(f"auth-port={radius_port}", server[2])
+ self.assertEqual(f"acct-port={radius_port_acc}", server[3])
+ self.assertEqual(f"req-limit=0", server[4])
+ self.assertEqual(f"fail-time=0", server[5])
+
+ #
+ # Disable Radius Accounting
+ #
+ self.delete(
+ ["authentication", "radius", "server", radius_server, "acct-port"]
+ )
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "server",
+ radius_server,
+ "disable-accounting",
+ ]
+ )
+
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "server",
+ radius_server,
+ "backup",
+ ]
+ )
+
+ self.set(
+ [
+ "authentication",
+ "radius",
+ "server",
+ radius_server,
+ "priority",
+ "10",
+ ]
+ )
+
+ # commit changes
+ self.cli_commit()
+
+ conf.read(self._config_file)
+
+ server = conf["radius"]["server"].split(",")
+ self.assertEqual(radius_server, server[0])
+ self.assertEqual(radius_key, server[1])
+ self.assertEqual(f"auth-port={radius_port}", server[2])
+ self.assertEqual(f"acct-port=0", server[3])
+ self.assertEqual(f"req-limit=0", server[4])
+ self.assertEqual(f"fail-time=0", server[5])
+ self.assertIn('weight=10', server)
+ self.assertIn('backup', server)
+
+ def test_accel_ipv4_pool(self):
+ self.basic_config(is_gateway=False, is_client_pool=False)
+ gateway = "192.0.2.1"
+ subnet = "172.16.0.0/24"
+ first_pool = "POOL1"
+ second_pool = "POOL2"
+ range = "192.0.2.10-192.0.2.20"
+ range_config = "192.0.2.10-20"
+
+ self.set(["gateway-address", gateway])
+ self.set(["client-ip-pool", first_pool, "range", subnet])
+ self.set(["client-ip-pool", first_pool, "next-pool", second_pool])
+ self.set(["client-ip-pool", second_pool, "range", range])
+ self.set(["default-pool", first_pool])
+ # commit changes
+
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+
+ self.assertEqual(
+ f"{first_pool},next={second_pool}", conf["ip-pool"][f"{subnet},name"]
+ )
+ self.assertEqual(second_pool, conf["ip-pool"][f"{range_config},name"])
+ self.assertEqual(gateway, conf["ip-pool"]["gw-ip-address"])
+ self.assertEqual(first_pool, conf[self._protocol_section]["ip-pool"])
+
+ def test_accel_next_pool(self):
+ # T5099 required specific order
+ self.basic_config(is_gateway=False, is_client_pool=False)
+
+ gateway = "192.0.2.1"
+ first_pool = "VyOS-pool1"
+ first_subnet = "192.0.2.0/25"
+ second_pool = "Vyos-pool2"
+ second_subnet = "203.0.113.0/25"
+ third_pool = "Vyos-pool3"
+ third_subnet = "198.51.100.0/24"
+
+ self.set(["gateway-address", gateway])
+ self.set(["client-ip-pool", first_pool, "range", first_subnet])
+ self.set(["client-ip-pool", first_pool, "next-pool", second_pool])
+ self.set(["client-ip-pool", second_pool, "range", second_subnet])
+ self.set(["client-ip-pool", second_pool, "next-pool", third_pool])
+ self.set(["client-ip-pool", third_pool, "range", third_subnet])
+
+ # commit changes
+ self.cli_commit()
+
+ config = self.getConfig("ip-pool")
+
+ pool_config = f"""gw-ip-address={gateway}
+{third_subnet},name={third_pool}
+{second_subnet},name={second_pool},next={third_pool}
+{first_subnet},name={first_pool},next={second_pool}"""
+ self.assertIn(pool_config, config)
+
+ def test_accel_ipv6_pool(self):
+ # Test configuration of IPv6 client pools
+ self.basic_config(is_gateway=False, is_client_pool=False)
+
+ # Enable IPv6
+ allow_ipv6 = 'allow'
+ self.set(['ppp-options', 'ipv6', allow_ipv6])
+
+ pool_name = 'ipv6_test_pool'
+ prefix_1 = '2001:db8:fffe::/56'
+ prefix_mask = '64'
+ prefix_2 = '2001:db8:ffff::/56'
+ client_prefix_1 = f'{prefix_1},{prefix_mask}'
+ client_prefix_2 = f'{prefix_2},{prefix_mask}'
+ self.set(
+ ['client-ipv6-pool', pool_name, 'prefix', prefix_1, 'mask',
+ prefix_mask])
+ self.set(
+ ['client-ipv6-pool', pool_name, 'prefix', prefix_2, 'mask',
+ prefix_mask])
+
+ delegate_1_prefix = '2001:db8:fff1::/56'
+ delegate_2_prefix = '2001:db8:fff2::/56'
+ delegate_mask = '64'
+ self.set(
+ ['client-ipv6-pool', pool_name, 'delegate', delegate_1_prefix,
+ 'delegation-prefix', delegate_mask])
+ self.set(
+ ['client-ipv6-pool', pool_name, 'delegate', delegate_2_prefix,
+ 'delegation-prefix', delegate_mask])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=',
+ strict=False)
+ conf.read(self._config_file)
+
+ for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']:
+ self.assertEqual(conf['modules'][tmp], None)
+
+ self.assertEqual(conf['ppp']['ipv6'], allow_ipv6)
+
+ config = self.getConfig("ipv6-pool")
+ pool_config = f"""{client_prefix_1},name={pool_name}
+{client_prefix_2},name={pool_name}
+delegate={delegate_1_prefix},{delegate_mask},name={pool_name}
+delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
+ self.assertIn(pool_config, config)
+
+ def test_accel_ppp_options(self):
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+
+ # other settings
+ mppe = 'require'
+ self.set(['ppp-options', 'disable-ccp'])
+ self.set(['ppp-options', 'mppe', mppe])
+
+ # min-mtu
+ min_mtu = '1400'
+ self.set(['ppp-options', 'min-mtu', min_mtu])
+
+ # mru
+ mru = '9000'
+ self.set(['ppp-options', 'mru', mru])
+
+ # interface-cache
+ interface_cache = '128000'
+ self.set(['ppp-options', 'interface-cache', interface_cache])
+
+ # ipv6
+ allow_ipv6 = 'allow'
+ allow_ipv4 = 'require'
+ random = 'random'
+ lcp_failure = '4'
+ lcp_interval = '40'
+ lcp_timeout = '100'
+ self.set(['ppp-options', 'ipv4', allow_ipv4])
+ self.set(['ppp-options', 'ipv6', allow_ipv6])
+ self.set(['ppp-options', 'ipv6-interface-id', random])
+ self.set(['ppp-options', 'ipv6-accept-peer-interface-id'])
+ self.set(['ppp-options', 'ipv6-peer-interface-id', random])
+ self.set(['ppp-options', 'lcp-echo-failure', lcp_failure])
+ self.set(['ppp-options', 'lcp-echo-interval', lcp_interval])
+ self.set(['ppp-options', 'lcp-echo-timeout', lcp_timeout])
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+
+ self.assertEqual(conf['chap-secrets']['gw-ip-address'], self._gateway)
+
+ # check ppp
+ self.assertEqual(conf['ppp']['mppe'], mppe)
+ self.assertEqual(conf['ppp']['min-mtu'], min_mtu)
+ self.assertEqual(conf['ppp']['mru'], mru)
+
+ self.assertEqual(conf['ppp']['ccp'],'0')
+
+ # check interface-cache
+ self.assertEqual(conf['ppp']['unit-cache'], interface_cache)
+
+ #check ipv6
+ for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']:
+ self.assertEqual(conf['modules'][tmp], None)
+
+ self.assertEqual(conf['ppp']['ipv6'], allow_ipv6)
+ self.assertEqual(conf['ppp']['ipv6-intf-id'], random)
+ self.assertEqual(conf['ppp']['ipv6-peer-intf-id'], random)
+ self.assertTrue(conf['ppp'].getboolean('ipv6-accept-peer-intf-id'))
+ self.assertEqual(conf['ppp']['lcp-echo-failure'], lcp_failure)
+ self.assertEqual(conf['ppp']['lcp-echo-interval'], lcp_interval)
+ self.assertEqual(conf['ppp']['lcp-echo-timeout'], lcp_timeout)
+
+
+ def test_accel_wins_server(self):
+ self.basic_config()
+ winsservers = ["192.0.2.1", "192.0.2.2"]
+ for wins in winsservers:
+ self.set(["wins-server", wins])
+ self.cli_commit()
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+ for ws in winsservers:
+ self.assertIn(ws, [conf["wins"]["wins1"], conf["wins"]["wins2"]])
+
+ def test_accel_snmp(self):
+ self.basic_config()
+ self.set(['snmp', 'master-agent'])
+ self.cli_commit()
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+ self.assertEqual(conf['modules']['net-snmp'], None)
+ self.assertEqual(conf['snmp']['master'],'1')
+
+ def test_accel_shaper(self):
+ self.basic_config()
+ fwmark = '2'
+ self.set(['shaper', 'fwmark', fwmark])
+ self.cli_commit()
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+ self.assertEqual(conf['modules']['shaper'], None)
+ self.assertEqual(conf['shaper']['verbose'], '1')
+ self.assertEqual(conf['shaper']['down-limiter'], 'tbf')
+ self.assertEqual(conf['shaper']['fwmark'], fwmark)
+
+ def test_accel_limits(self):
+ self.basic_config()
+ burst = '100'
+ timeout = '20'
+ limits = '1/min'
+ self.set(['limits', 'connection-limit', limits])
+ self.set(['limits', 'timeout', timeout])
+ self.set(['limits', 'burst', burst])
+ self.cli_commit()
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+ self.assertEqual(conf['modules']['connlimit'], None)
+ self.assertEqual(conf['connlimit']['limit'], limits)
+ self.assertEqual(conf['connlimit']['burst'], burst)
+ self.assertEqual(conf['connlimit']['timeout'], timeout)
+
+ def test_accel_log_level(self):
+ self.basic_config()
+ self.cli_commit()
+
+ # check default value
+ conf = ConfigParser(allow_no_value=True)
+ conf.read(self._config_file)
+ self.assertEqual(conf['log']['level'], '3')
+
+ for log_level in range(0, 5):
+ self.set(['log', 'level', str(log_level)])
+ self.cli_commit()
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True)
+ conf.read(self._config_file)
+
+ self.assertEqual(conf['log']['level'], str(log_level))
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
new file mode 100644
index 0000000..593b4b4
--- /dev/null
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -0,0 +1,1320 @@
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+
+from netifaces import AF_INET
+from netifaces import AF_INET6
+from netifaces import ifaddresses
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.defaults import directories
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
+from vyos.pki import CERT_BEGIN
+from vyos.utils.file import read_file
+from vyos.utils.dict import dict_search
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_interface_vrf
+from vyos.utils.network import get_vrf_tableid
+from vyos.utils.network import interface_exists
+from vyos.utils.network import is_intf_addr_assigned
+from vyos.utils.network import is_ipv6_link_local
+from vyos.utils.network import get_nft_vrf_zone_mapping
+from vyos.xml_ref import cli_defined
+
+dhclient_base_dir = directories['isc_dhclient_dir']
+dhclient_process_name = 'dhclient'
+dhcp6c_base_dir = directories['dhcp6_client_dir']
+dhcp6c_process_name = 'dhcp6c'
+
+server_ca_root_cert_data = """
+MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
+Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj
+ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK
+BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW
+a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ==
+"""
+
+server_ca_intermediate_cert_data = """
+MIIBmTCCAT+gAwIBAgIUNzrtHzLmi3QpPK57tUgCnJZhXXQwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
+Fw0zMjAyMTUxOTQxMjFaMCYxJDAiBgNVBAMMG1Z5T1Mgc2VydmVyIGludGVybWVk
+aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEl2nJ1CzoqPV6hWII2m
+eGN/uieU6wDMECTk/LgG8CCCSYb488dibUiFN/1UFsmoLIdIhkx/6MUCYh62m8U2
+WNujUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMV3YwH88I5gFsFUibbQ
+kMR0ECPsMB8GA1UdIwQYMBaAFHAL9hqJ4wSeOFhrsrcWXD+jRIW7MAoGCCqGSM49
+BAMCA0gAMEUCIQC/ahujD9dp5pMMCd3SZddqGC9cXtOwMN0JR3e5CxP13AIgIMQm
+jMYrinFoInxmX64HfshYqnUY8608nK9D2BNPOHo=
+"""
+
+client_ca_root_cert_data = """
+MIIBcDCCARagAwIBAgIUZmoW2xVdwkZSvglnkCq0AHKa6zIwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
+Fw0zMjAyMTUxOTQxMjFaMB4xHDAaBgNVBAMME1Z5T1MgY2xpZW50IHJvb3QgQ0Ew
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATUpKXzQk2NOVKDN4VULk2yw4mOKPvn
+mg947+VY7lbpfOfAUD0QRg95qZWCw899eKnXp/U4TkAVrmEKhUb6OJTFozIwMDAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTXu6xGWUl25X3sBtrhm3BJSICIATAK
+BggqhkjOPQQDAgNIADBFAiEAnTzEwuTI9bz2Oae3LZbjP6f/f50KFJtjLZFDbQz7
+DpYCIDNRHV8zBUibC+zg5PqMpQBKd/oPfNU76nEv6xkp/ijO
+"""
+
+client_ca_intermediate_cert_data = """
+MIIBmDCCAT+gAwIBAgIUJEMdotgqA7wU4XXJvEzDulUAGqgwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjJa
+Fw0zMjAyMTUxOTQxMjJaMCYxJDAiBgNVBAMMG1Z5T1MgY2xpZW50IGludGVybWVk
+aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGyIVIi217s9j3O+WQ2b
+6R65/Z0ZjQpELxPjBRc0CA0GFCo+pI5EvwI+jNFArvTAJ5+ZdEWUJ1DQhBKDDQdI
+avCjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOUS8oNJjChB1Rb9Blcl
+ETvziHJ9MB8GA1UdIwQYMBaAFNe7rEZZSXblfewG2uGbcElIgIgBMAoGCCqGSM49
+BAMCA0cAMEQCIArhaxWgRsAUbEeNHD/ULtstLHxw/P97qPUSROLQld53AiBjgiiz
+9pDfISmpekZYz6bIDWRIR0cXUToZEMFNzNMrQg==
+"""
+
+client_cert_data = """
+MIIBmTCCAUCgAwIBAgIUV5T77XdE/tV82Tk4Vzhp5BIFFm0wCgYIKoZIzj0EAwIw
+JjEkMCIGA1UEAwwbVnlPUyBjbGllbnQgaW50ZXJtZWRpYXRlIENBMB4XDTIyMDIx
+NzE5NDEyMloXDTMyMDIxNTE5NDEyMlowIjEgMB4GA1UEAwwXVnlPUyBjbGllbnQg
+Y2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuyynqfc/qJj5e
+KJ03oOH8X4Z8spDeAPO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAh
+CIhytmJao1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTIFKrxZ+PqOhYSUqnl
+TGCUmM7wTjAfBgNVHSMEGDAWgBTlEvKDSYwoQdUW/QZXJRE784hyfTAKBggqhkjO
+PQQDAgNHADBEAiAvO8/jvz05xqmP3OXD53XhfxDLMIxzN4KPoCkFqvjlhQIgIHq2
+/geVx3rAOtSps56q/jiDouN/aw01TdpmGKVAa9U=
+"""
+
+client_key_data = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxaxAQsJwjoOCByQE
++qSYKtKtJzbdbOnTsKNSrfgkFH6hRANCAARuyynqfc/qJj5eKJ03oOH8X4Z8spDe
+APO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAhCIhytmJa
+"""
+
+def get_wpa_supplicant_value(interface, key):
+ tmp = read_file(f'/run/wpa_supplicant/{interface}.conf')
+ tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp)
+ return tmp[0]
+
+def get_certificate_count(interface, cert_type):
+ tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem')
+ return tmp.count(CERT_BEGIN)
+
+def is_mirrored_to(interface, mirror_if, qdisc):
+ """
+ Ask TC if we are mirroring traffic to a discrete interface.
+
+ interface: source interface
+ mirror_if: destination where we mirror our data to
+ qdisc: must be ffff or 1 for ingress/egress
+ """
+ if qdisc not in ['ffff', '1']:
+ raise ValueError()
+
+ ret_val = False
+ tmp = cmd(f'tc -s -p filter ls dev {interface} parent {qdisc}: | grep mirred')
+ tmp = tmp.lower()
+ if mirror_if in tmp:
+ ret_val = True
+ return ret_val
+class BasicInterfaceTest:
+ class TestCase(VyOSUnitTestSHIM.TestCase):
+ _test_dhcp = False
+ _test_eapol = False
+ _test_ip = False
+ _test_mtu = False
+ _test_vlan = False
+ _test_qinq = False
+ _test_ipv6 = False
+ _test_ipv6_pd = False
+ _test_ipv6_dhcpc6 = False
+ _test_mirror = False
+ _test_vrf = False
+ _base_path = []
+
+ _options = {}
+ _interfaces = []
+ _qinq_range = ['10', '20', '30']
+ _vlan_range = ['100', '200', '300', '2000']
+ _test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32',
+ '2001:db8:1::ffff/64', '2001:db8:101::1/112']
+
+ _mirror_interfaces = []
+ # choose IPv6 minimum MTU value for tests - this must always work
+ _mtu = '1280'
+
+ @classmethod
+ def setUpClass(cls):
+ super(BasicInterfaceTest.TestCase, cls).setUpClass()
+
+ # XXX the case of test_vif_8021q_mtu_limits, below, shows that
+ # we should extend cli_defined to support more complex queries
+ cls._test_vlan = cli_defined(cls._base_path, 'vif')
+ cls._test_qinq = cli_defined(cls._base_path, 'vif-s')
+ cls._test_dhcp = cli_defined(cls._base_path, 'dhcp-options')
+ cls._test_eapol = cli_defined(cls._base_path, 'eapol')
+ cls._test_ip = cli_defined(cls._base_path, 'ip')
+ cls._test_ipv6 = cli_defined(cls._base_path, 'ipv6')
+ cls._test_ipv6_dhcpc6 = cli_defined(cls._base_path, 'dhcpv6-options')
+ cls._test_ipv6_pd = cli_defined(cls._base_path + ['dhcpv6-options'], 'pd')
+ cls._test_mtu = cli_defined(cls._base_path, 'mtu')
+ cls._test_vrf = cli_defined(cls._base_path, 'vrf')
+
+ # Setup mirror interfaces for SPAN (Switch Port Analyzer)
+ for span in cls._mirror_interfaces:
+ section = Section.section(span)
+ cls.cli_set(cls, ['interfaces', section, span])
+
+ @classmethod
+ def tearDownClass(cls):
+ # Tear down mirror interfaces for SPAN (Switch Port Analyzer)
+ for span in cls._mirror_interfaces:
+ section = Section.section(span)
+ cls.cli_delete(cls, ['interfaces', section, span])
+
+ super(BasicInterfaceTest.TestCase, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(self._base_path)
+ self.cli_commit()
+
+ # Verify that no previously interface remained on the system
+ ct_map = get_nft_vrf_zone_mapping()
+ for intf in self._interfaces:
+ self.assertFalse(interface_exists(intf))
+ for map_entry in ct_map:
+ self.assertNotEqual(intf, map_entry['interface'])
+
+ # No daemon started during tests should remain running
+ for daemon in ['dhcp6c', 'dhclient']:
+ # if _interface list is populated do a more fine grained search
+ # by also checking the cmd arguments passed to the daemon
+ if self._interfaces:
+ for tmp in self._interfaces:
+ self.assertFalse(process_named_running(daemon, tmp))
+ else:
+ self.assertFalse(process_named_running(daemon))
+
+ def test_dhcp_disable_interface(self):
+ if not self._test_dhcp:
+ self.skipTest('not supported')
+
+ # When interface is configured as admin down, it must be admin down
+ # even when dhcpc starts on the given interface
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'disable'])
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ # Also enable DHCP (ISC DHCP always places interface in admin up
+ # state so we check that we do not start DHCP client.
+ # https://vyos.dev/T2767
+ self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ flags = read_file(f'/sys/class/net/{interface}/flags')
+ self.assertEqual(int(flags, 16) & 1, 0)
+
+ def test_dhcp_client_options(self):
+ if not self._test_dhcp or not self._test_vrf:
+ self.skipTest('not supported')
+
+ client_id = 'VyOS-router'
+ distance = '100'
+ hostname = 'vyos'
+ vendor_class_id = 'vyos-vendor'
+ user_class = 'vyos'
+
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
+ self.cli_set(self._base_path + [interface, 'dhcp-options', 'client-id', client_id])
+ self.cli_set(self._base_path + [interface, 'dhcp-options', 'default-route-distance', distance])
+ self.cli_set(self._base_path + [interface, 'dhcp-options', 'host-name', hostname])
+ self.cli_set(self._base_path + [interface, 'dhcp-options', 'vendor-class-id', vendor_class_id])
+ self.cli_set(self._base_path + [interface, 'dhcp-options', 'user-class', user_class])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Check if dhclient process runs
+ dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10)
+ self.assertTrue(dhclient_pid)
+
+ dhclient_config = read_file(f'{dhclient_base_dir}/dhclient_{interface}.conf')
+ self.assertIn(f'request subnet-mask, broadcast-address, routers, domain-name-servers', dhclient_config)
+ self.assertIn(f'require subnet-mask;', dhclient_config)
+ self.assertIn(f'send host-name "{hostname}";', dhclient_config)
+ self.assertIn(f'send dhcp-client-identifier "{client_id}";', dhclient_config)
+ self.assertIn(f'send vendor-class-identifier "{vendor_class_id}";', dhclient_config)
+ self.assertIn(f'send user-class "{user_class}";', dhclient_config)
+
+ # and the commandline has the appropriate options
+ cmdline = read_file(f'/proc/{dhclient_pid}/cmdline')
+ self.assertIn(f'-e\x00IF_METRIC={distance}', cmdline)
+
+ def test_dhcp_vrf(self):
+ if not self._test_dhcp or not self._test_vrf:
+ self.skipTest('not supported')
+
+ vrf_name = 'purple4'
+ self.cli_set(['vrf', 'name', vrf_name, 'table', '65000'])
+
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
+ self.cli_set(self._base_path + [interface, 'vrf', vrf_name])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf_name)
+
+ # Check if dhclient process runs
+ dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10)
+ self.assertTrue(dhclient_pid)
+ # .. inside the appropriate VRF instance
+ vrf_pids = cmd(f'ip vrf pids {vrf_name}')
+ self.assertIn(str(dhclient_pid), vrf_pids)
+ # and the commandline has the appropriate options
+ cmdline = read_file(f'/proc/{dhclient_pid}/cmdline')
+ self.assertIn('-e\x00IF_METRIC=210', cmdline) # 210 is the default value
+
+ self.cli_delete(['vrf', 'name', vrf_name])
+
+ def test_dhcpv6_vrf(self):
+ if not self._test_ipv6_dhcpc6 or not self._test_vrf:
+ self.skipTest('not supported')
+
+ vrf_name = 'purple6'
+ self.cli_set(['vrf', 'name', vrf_name, 'table', '65001'])
+
+ # When interface is configured as admin down, it must be admin down
+ # even when dhcpc starts on the given interface
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'address', 'dhcpv6'])
+ self.cli_set(self._base_path + [interface, 'vrf', vrf_name])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf_name)
+
+ # Check if dhclient process runs
+ tmp = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)
+ self.assertTrue(tmp)
+ # .. inside the appropriate VRF instance
+ vrf_pids = cmd(f'ip vrf pids {vrf_name}')
+ self.assertIn(str(tmp), vrf_pids)
+
+ self.cli_delete(['vrf', 'name', vrf_name])
+
+ def test_move_interface_between_vrf_instances(self):
+ if not self._test_vrf:
+ self.skipTest('not supported')
+
+ vrf1_name = 'smoketest_mgmt1'
+ vrf1_table = '5424'
+ vrf2_name = 'smoketest_mgmt2'
+ vrf2_table = '7412'
+
+ self.cli_set(['vrf', 'name', vrf1_name, 'table', vrf1_table])
+ self.cli_set(['vrf', 'name', vrf2_name, 'table', vrf2_table])
+
+ # move interface into first VRF
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+ self.cli_set(self._base_path + [interface, 'vrf', vrf1_name])
+
+ self.cli_commit()
+
+ # check that interface belongs to proper VRF
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf1_name)
+
+ tmp = get_interface_config(vrf1_name)
+ self.assertEqual(int(vrf1_table), get_vrf_tableid(interface))
+
+ # move interface into second VRF
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'vrf', vrf2_name])
+
+ self.cli_commit()
+
+ # check that interface belongs to proper VRF
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf2_name)
+
+ tmp = get_interface_config(vrf2_name)
+ self.assertEqual(int(vrf2_table), get_vrf_tableid(interface))
+
+ self.cli_delete(['vrf', 'name', vrf1_name])
+ self.cli_delete(['vrf', 'name', vrf2_name])
+
+ def test_add_to_invalid_vrf(self):
+ if not self._test_vrf:
+ self.skipTest('not supported')
+
+ # move interface into first VRF
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+ self.cli_set(self._base_path + [interface, 'vrf', 'invalid'])
+
+ # check validate() - can not use a non-existing VRF
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'vrf', 'invalid'])
+ self.cli_set(self._base_path + [interface, 'description', 'test_add_to_invalid_vrf'])
+
+ def test_span_mirror(self):
+ if not self._mirror_interfaces:
+ self.skipTest('not supported')
+
+ # Check the two-way mirror rules of ingress and egress
+ for mirror in self._mirror_interfaces:
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'mirror', 'ingress', mirror])
+ self.cli_set(self._base_path + [interface, 'mirror', 'egress', mirror])
+
+ self.cli_commit()
+
+ # Verify config
+ for mirror in self._mirror_interfaces:
+ for interface in self._interfaces:
+ self.assertTrue(is_mirrored_to(interface, mirror, 'ffff'))
+ self.assertTrue(is_mirrored_to(interface, mirror, '1'))
+
+ def test_interface_disable(self):
+ # Check if description can be added to interface and
+ # can be read back
+ for intf in self._interfaces:
+ self.cli_set(self._base_path + [intf, 'disable'])
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_commit()
+
+ # Validate interface description
+ for intf in self._interfaces:
+ self.assertEqual(Interface(intf).get_admin_state(), 'down')
+
+ def test_interface_description(self):
+ # Check if description can be added to interface and
+ # can be read back
+ for intf in self._interfaces:
+ test_string=f'Description-Test-{intf}'
+ self.cli_set(self._base_path + [intf, 'description', test_string])
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_commit()
+
+ # Validate interface description
+ for intf in self._interfaces:
+ test_string=f'Description-Test-{intf}'
+ tmp = read_file(f'/sys/class/net/{intf}/ifalias')
+ self.assertEqual(tmp, test_string)
+ self.assertEqual(Interface(intf).get_alias(), test_string)
+ self.cli_delete(self._base_path + [intf, 'description'])
+
+ self.cli_commit()
+
+ # Validate remove interface description "empty"
+ for intf in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{intf}/ifalias')
+ self.assertEqual(tmp, str())
+ self.assertEqual(Interface(intf).get_alias(), str())
+
+ # Test maximum interface description lengt (255 characters)
+ test_string='abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789___'
+ for intf in self._interfaces:
+
+ self.cli_set(self._base_path + [intf, 'description', test_string])
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_commit()
+
+ # Validate interface description
+ for intf in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{intf}/ifalias')
+ self.assertEqual(tmp, test_string)
+ self.assertEqual(Interface(intf).get_alias(), test_string)
+
+ def test_add_single_ip_address(self):
+ addr = '192.0.2.0/31'
+ for intf in self._interfaces:
+ self.cli_set(self._base_path + [intf, 'address', addr])
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_commit()
+
+ for intf in self._interfaces:
+ self.assertTrue(is_intf_addr_assigned(intf, addr))
+ self.assertEqual(Interface(intf).get_admin_state(), 'up')
+
+ def test_add_multiple_ip_addresses(self):
+ # Add address
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+ for addr in self._test_addr:
+ self.cli_set(self._base_path + [intf, 'address', addr])
+
+ self.cli_commit()
+
+ # Validate address
+ for intf in self._interfaces:
+ for af in AF_INET, AF_INET6:
+ for addr in ifaddresses(intf)[af]:
+ # checking link local addresses makes no sense
+ if is_ipv6_link_local(addr['addr']):
+ continue
+
+ self.assertTrue(is_intf_addr_assigned(intf, addr['addr']))
+
+ def test_ipv6_link_local_address(self):
+ # Common function for IPv6 link-local address assignemnts
+ if not self._test_ipv6:
+ self.skipTest('not supported')
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ # just set the interface base without any option - some interfaces
+ # (VTI) do not require any option to be brought up
+ self.cli_set(base)
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+
+ # after commit we must have an IPv6 link-local address
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ self.assertIn(AF_INET6, ifaddresses(interface))
+ for addr in ifaddresses(interface)[AF_INET6]:
+ self.assertTrue(is_ipv6_link_local(addr['addr']))
+
+ # disable IPv6 link-local address assignment
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['ipv6', 'address', 'no-default-link-local'])
+
+ # after commit we must have no IPv6 link-local address
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ self.assertNotIn(AF_INET6, ifaddresses(interface))
+
+ def test_interface_mtu(self):
+ if not self._test_mtu:
+ self.skipTest('not supported')
+
+ for intf in self._interfaces:
+ base = self._base_path + [intf]
+ self.cli_set(base + ['mtu', self._mtu])
+ for option in self._options.get(intf, []):
+ self.cli_set(base + option.split())
+
+ # commit interface changes
+ self.cli_commit()
+
+ # verify changed MTU
+ for intf in self._interfaces:
+ tmp = get_interface_config(intf)
+ self.assertEqual(tmp['mtu'], int(self._mtu))
+
+ def test_mtu_1200_no_ipv6_interface(self):
+ # Testcase if MTU can be changed to 1200 on non IPv6
+ # enabled interfaces
+ if not self._test_mtu:
+ self.skipTest('not supported')
+
+ old_mtu = self._mtu
+ self._mtu = '1200'
+
+ for intf in self._interfaces:
+ base = self._base_path + [intf]
+ for option in self._options.get(intf, []):
+ self.cli_set(base + option.split())
+ self.cli_set(base + ['mtu', self._mtu])
+
+ # check validate() - can not set low MTU if 'no-default-link-local'
+ # is not set on CLI
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for intf in self._interfaces:
+ base = self._base_path + [intf]
+ self.cli_set(base + ['ipv6', 'address', 'no-default-link-local'])
+
+ # commit interface changes
+ self.cli_commit()
+
+ # verify changed MTU
+ for intf in self._interfaces:
+ tmp = get_interface_config(intf)
+ self.assertEqual(tmp['mtu'], int(self._mtu))
+
+ self._mtu = old_mtu
+
+ def test_vif_8021q_interfaces(self):
+ # XXX: This testcase is not allowed to run as first testcase, reason
+ # is the Wireless test will first load the wifi kernel hwsim module
+ # which creates a wlan0 and wlan1 interface which will fail the
+ # tearDown() test in the end that no interface is allowed to survive!
+ if not self._test_vlan:
+ self.skipTest('not supported')
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+
+ for vlan in self._vlan_range:
+ base = self._base_path + [interface, 'vif', vlan]
+ for address in self._test_addr:
+ self.cli_set(base + ['address', address])
+
+ self.cli_commit()
+
+ for intf in self._interfaces:
+ for vlan in self._vlan_range:
+ vif = f'{intf}.{vlan}'
+ for address in self._test_addr:
+ self.assertTrue(is_intf_addr_assigned(vif, address))
+
+ self.assertEqual(Interface(vif).get_admin_state(), 'up')
+
+ # T4064: Delete interface addresses, keep VLAN interface
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for vlan in self._vlan_range:
+ base = self._base_path + [interface, 'vif', vlan]
+ self.cli_delete(base + ['address'])
+
+ self.cli_commit()
+
+ # Verify no IP address is assigned
+ for interface in self._interfaces:
+ for vlan in self._vlan_range:
+ vif = f'{intf}.{vlan}'
+ for address in self._test_addr:
+ self.assertFalse(is_intf_addr_assigned(vif, address))
+
+
+ def test_vif_8021q_mtu_limits(self):
+ # XXX: This testcase is not allowed to run as first testcase, reason
+ # is the Wireless test will first load the wifi kernel hwsim module
+ # which creates a wlan0 and wlan1 interface which will fail the
+ # tearDown() test in the end that no interface is allowed to survive!
+ if not self._test_vlan or not self._test_mtu:
+ self.skipTest('not supported')
+
+ mtu_1500 = '1500'
+ mtu_9000 = '9000'
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['mtu', mtu_1500])
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+ if 'source-interface' in option:
+ iface = option.split()[-1]
+ iface_type = Section.section(iface)
+ self.cli_set(['interfaces', iface_type, iface, 'mtu', mtu_9000])
+
+ for vlan in self._vlan_range:
+ base = self._base_path + [interface, 'vif', vlan]
+ self.cli_set(base + ['mtu', mtu_9000])
+
+ # check validate() - Interface MTU "9000" too high, parent interface MTU is "1500"!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # Change MTU on base interface to be the same as on the VIF interface
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['mtu', mtu_9000])
+
+ self.cli_commit()
+
+ # Verify MTU on base and VIF interfaces
+ for interface in self._interfaces:
+ tmp = get_interface_config(interface)
+ self.assertEqual(tmp['mtu'], int(mtu_9000))
+
+ for vlan in self._vlan_range:
+ tmp = get_interface_config(f'{interface}.{vlan}')
+ self.assertEqual(tmp['mtu'], int(mtu_9000))
+
+
+ def test_vif_8021q_qos_change(self):
+ # XXX: This testcase is not allowed to run as first testcase, reason
+ # is the Wireless test will first load the wifi kernel hwsim module
+ # which creates a wlan0 and wlan1 interface which will fail the
+ # tearDown() test in the end that no interface is allowed to survive!
+ if not self._test_vlan:
+ self.skipTest('not supported')
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+
+ for vlan in self._vlan_range:
+ base = self._base_path + [interface, 'vif', vlan]
+ self.cli_set(base + ['ingress-qos', '0:1'])
+ self.cli_set(base + ['egress-qos', '1:6'])
+
+ self.cli_commit()
+
+ for intf in self._interfaces:
+ for vlan in self._vlan_range:
+ vif = f'{intf}.{vlan}'
+ tmp = get_interface_config(f'{vif}')
+
+ tmp2 = dict_search('linkinfo.info_data.ingress_qos', tmp)
+ for item in tmp2 if tmp2 else []:
+ from_key = item['from']
+ to_key = item['to']
+ self.assertEqual(from_key, 0)
+ self.assertEqual(to_key, 1)
+
+ tmp2 = dict_search('linkinfo.info_data.egress_qos', tmp)
+ for item in tmp2 if tmp2 else []:
+ from_key = item['from']
+ to_key = item['to']
+ self.assertEqual(from_key, 1)
+ self.assertEqual(to_key, 6)
+
+ self.assertEqual(Interface(vif).get_admin_state(), 'up')
+
+ new_ingress_qos_from = 1
+ new_ingress_qos_to = 6
+ new_egress_qos_from = 2
+ new_egress_qos_to = 7
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for vlan in self._vlan_range:
+ base = self._base_path + [interface, 'vif', vlan]
+ self.cli_set(base + ['ingress-qos', f'{new_ingress_qos_from}:{new_ingress_qos_to}'])
+ self.cli_set(base + ['egress-qos', f'{new_egress_qos_from}:{new_egress_qos_to}'])
+
+ self.cli_commit()
+
+ for intf in self._interfaces:
+ for vlan in self._vlan_range:
+ vif = f'{intf}.{vlan}'
+ tmp = get_interface_config(f'{vif}')
+
+ tmp2 = dict_search('linkinfo.info_data.ingress_qos', tmp)
+ if tmp2:
+ from_key = tmp2[0]['from']
+ to_key = tmp2[0]['to']
+ self.assertEqual(from_key, new_ingress_qos_from)
+ self.assertEqual(to_key, new_ingress_qos_to)
+
+ tmp2 = dict_search('linkinfo.info_data.egress_qos', tmp)
+ if tmp2:
+ from_key = tmp2[0]['from']
+ to_key = tmp2[0]['to']
+ self.assertEqual(from_key, new_egress_qos_from)
+ self.assertEqual(to_key, new_egress_qos_to)
+
+ def test_vif_8021q_lower_up_down(self):
+ # Testcase for https://vyos.dev/T3349
+ if not self._test_vlan:
+ self.skipTest('not supported')
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+
+ # disable the lower interface
+ self.cli_set(base + ['disable'])
+
+ for vlan in self._vlan_range:
+ vlan_base = self._base_path + [interface, 'vif', vlan]
+ # disable the vlan interface
+ self.cli_set(vlan_base + ['disable'])
+
+ self.cli_commit()
+
+ # re-enable all lower interfaces
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_delete(base + ['disable'])
+
+ self.cli_commit()
+
+ # verify that the lower interfaces are admin up and the vlan
+ # interfaces are all admin down
+ for interface in self._interfaces:
+ self.assertEqual(Interface(interface).get_admin_state(), 'up')
+
+ for vlan in self._vlan_range:
+ ifname = f'{interface}.{vlan}'
+ self.assertEqual(Interface(ifname).get_admin_state(), 'down')
+
+
+ def test_vif_s_8021ad_vlan_interfaces(self):
+ # XXX: This testcase is not allowed to run as first testcase, reason
+ # is the Wireless test will first load the wifi kernel hwsim module
+ # which creates a wlan0 and wlan1 interface which will fail the
+ # tearDown() test in the end that no interface is allowed to survive!
+ if not self._test_qinq:
+ self.skipTest('not supported')
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+
+ for vif_s in self._qinq_range:
+ for vif_c in self._vlan_range:
+ base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c]
+ self.cli_set(base + ['mtu', self._mtu])
+ for address in self._test_addr:
+ self.cli_set(base + ['address', address])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ for vif_s in self._qinq_range:
+ tmp = get_interface_config(f'{interface}.{vif_s}')
+ self.assertEqual(dict_search('linkinfo.info_data.protocol', tmp), '802.1ad')
+
+ for vif_c in self._vlan_range:
+ vif = f'{interface}.{vif_s}.{vif_c}'
+ # For an unknown reason this regularely fails on the QEMU builds,
+ # thus the test for reading back IP addresses is temporary
+ # disabled. There is no big deal here, as this uses the same
+ # methods on 802.1q and here it works and is verified.
+# for address in self._test_addr:
+# self.assertTrue(is_intf_addr_assigned(vif, address))
+
+ tmp = get_interface_config(vif)
+ self.assertEqual(tmp['mtu'], int(self._mtu))
+
+
+ # T4064: Delete interface addresses, keep VLAN interface
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for vif_s in self._qinq_range:
+ for vif_c in self._vlan_range:
+ self.cli_delete(self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c, 'address'])
+
+ self.cli_commit()
+ # Verify no IP address is assigned
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for vif_s in self._qinq_range:
+ for vif_c in self._vlan_range:
+ vif = f'{interface}.{vif_s}.{vif_c}'
+ for address in self._test_addr:
+ self.assertFalse(is_intf_addr_assigned(vif, address))
+
+ # T3972: remove vif-c interfaces from vif-s
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for vif_s in self._qinq_range:
+ base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c']
+ self.cli_delete(base)
+
+ self.cli_commit()
+
+
+ def test_vif_s_protocol_change(self):
+ # XXX: This testcase is not allowed to run as first testcase, reason
+ # is the Wireless test will first load the wifi kernel hwsim module
+ # which creates a wlan0 and wlan1 interface which will fail the
+ # tearDown() test in the end that no interface is allowed to survive!
+ if not self._test_qinq:
+ self.skipTest('not supported')
+
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(base + option.split())
+
+ for vif_s in self._qinq_range:
+ for vif_c in self._vlan_range:
+ base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c]
+ for address in self._test_addr:
+ self.cli_set(base + ['address', address])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ for vif_s in self._qinq_range:
+ tmp = get_interface_config(f'{interface}.{vif_s}')
+ # check for the default value
+ self.assertEqual(tmp['linkinfo']['info_data']['protocol'], '802.1ad')
+
+ # T3532: now change ethertype
+ new_protocol = '802.1q'
+ for interface in self._interfaces:
+ for vif_s in self._qinq_range:
+ base = self._base_path + [interface, 'vif-s', vif_s]
+ self.cli_set(base + ['protocol', new_protocol])
+
+ self.cli_commit()
+
+ # Verify new ethertype configuration
+ for interface in self._interfaces:
+ for vif_s in self._qinq_range:
+ tmp = get_interface_config(f'{interface}.{vif_s}')
+ self.assertEqual(tmp['linkinfo']['info_data']['protocol'], new_protocol.upper())
+
+ def test_interface_ip_options(self):
+ if not self._test_ip:
+ self.skipTest('not supported')
+
+ arp_tmo = '300'
+ mss = '1420'
+
+ for interface in self._interfaces:
+ path = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(path + option.split())
+
+ # Options
+ if cli_defined(self._base_path + ['ip'], 'adjust-mss'):
+ self.cli_set(path + ['ip', 'adjust-mss', mss])
+
+ if cli_defined(self._base_path + ['ip'], 'arp-cache-timeout'):
+ self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo])
+
+ if cli_defined(self._base_path + ['ip'], 'disable-arp-filter'):
+ self.cli_set(path + ['ip', 'disable-arp-filter'])
+
+ if cli_defined(self._base_path + ['ip'], 'disable-forwarding'):
+ self.cli_set(path + ['ip', 'disable-forwarding'])
+
+ if cli_defined(self._base_path + ['ip'], 'enable-directed-broadcast'):
+ self.cli_set(path + ['ip', 'enable-directed-broadcast'])
+
+ if cli_defined(self._base_path + ['ip'], 'enable-arp-accept'):
+ self.cli_set(path + ['ip', 'enable-arp-accept'])
+
+ if cli_defined(self._base_path + ['ip'], 'enable-arp-announce'):
+ self.cli_set(path + ['ip', 'enable-arp-announce'])
+
+ if cli_defined(self._base_path + ['ip'], 'enable-arp-ignore'):
+ self.cli_set(path + ['ip', 'enable-arp-ignore'])
+
+ if cli_defined(self._base_path + ['ip'], 'enable-proxy-arp'):
+ self.cli_set(path + ['ip', 'enable-proxy-arp'])
+
+ if cli_defined(self._base_path + ['ip'], 'proxy-arp-pvlan'):
+ self.cli_set(path + ['ip', 'proxy-arp-pvlan'])
+
+ if cli_defined(self._base_path + ['ip'], 'source-validation'):
+ self.cli_set(path + ['ip', 'source-validation', 'loose'])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ if cli_defined(self._base_path + ['ip'], 'adjust-mss'):
+ base_options = f'oifname "{interface}"'
+ out = cmd('sudo nft list chain raw VYOS_TCP_MSS')
+ for line in out.splitlines():
+ if line.startswith(base_options):
+ self.assertIn(f'tcp option maxseg size set {mss}', line)
+
+ if cli_defined(self._base_path + ['ip'], 'arp-cache-timeout'):
+ tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms')
+ self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds
+
+ proc_base = f'/proc/sys/net/ipv4/conf/{interface}'
+
+ if cli_defined(self._base_path + ['ip'], 'disable-arp-filter'):
+ tmp = read_file(f'{proc_base}/arp_filter')
+ self.assertEqual('0', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'enable-arp-accept'):
+ tmp = read_file(f'{proc_base}/arp_accept')
+ self.assertEqual('1', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'enable-arp-announce'):
+ tmp = read_file(f'{proc_base}/arp_announce')
+ self.assertEqual('1', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'enable-arp-ignore'):
+ tmp = read_file(f'{proc_base}/arp_ignore')
+ self.assertEqual('1', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'disable-forwarding'):
+ tmp = read_file(f'{proc_base}/forwarding')
+ self.assertEqual('0', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'enable-directed-broadcast'):
+ tmp = read_file(f'{proc_base}/bc_forwarding')
+ self.assertEqual('1', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'enable-proxy-arp'):
+ tmp = read_file(f'{proc_base}/proxy_arp')
+ self.assertEqual('1', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'proxy-arp-pvlan'):
+ tmp = read_file(f'{proc_base}/proxy_arp_pvlan')
+ self.assertEqual('1', tmp)
+
+ if cli_defined(self._base_path + ['ip'], 'source-validation'):
+ base_options = f'iifname "{interface}"'
+ out = cmd('sudo nft list chain ip raw vyos_rpfilter')
+ for line in out.splitlines():
+ if line.startswith(base_options):
+ self.assertIn('fib saddr oif 0', line)
+ self.assertIn('drop', line)
+
+ def test_interface_ipv6_options(self):
+ if not self._test_ipv6:
+ self.skipTest('not supported')
+
+ mss = '1400'
+ dad_transmits = '10'
+ accept_dad = '0'
+ source_validation = 'strict'
+
+ for interface in self._interfaces:
+ path = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(path + option.split())
+
+ # Options
+ if cli_defined(self._base_path + ['ipv6'], 'adjust-mss'):
+ self.cli_set(path + ['ipv6', 'adjust-mss', mss])
+
+ if cli_defined(self._base_path + ['ipv6'], 'accept-dad'):
+ self.cli_set(path + ['ipv6', 'accept-dad', accept_dad])
+
+ if cli_defined(self._base_path + ['ipv6'], 'dup-addr-detect-transmits'):
+ self.cli_set(path + ['ipv6', 'dup-addr-detect-transmits', dad_transmits])
+
+ if cli_defined(self._base_path + ['ipv6'], 'disable-forwarding'):
+ self.cli_set(path + ['ipv6', 'disable-forwarding'])
+
+ if cli_defined(self._base_path + ['ipv6'], 'source-validation'):
+ self.cli_set(path + ['ipv6', 'source-validation', source_validation])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ proc_base = f'/proc/sys/net/ipv6/conf/{interface}'
+ if cli_defined(self._base_path + ['ipv6'], 'adjust-mss'):
+ base_options = f'oifname "{interface}"'
+ out = cmd('sudo nft list chain ip6 raw VYOS_TCP_MSS')
+ for line in out.splitlines():
+ if line.startswith(base_options):
+ self.assertIn(f'tcp option maxseg size set {mss}', line)
+
+ if cli_defined(self._base_path + ['ipv6'], 'accept-dad'):
+ tmp = read_file(f'{proc_base}/accept_dad')
+ self.assertEqual(accept_dad, tmp)
+
+ if cli_defined(self._base_path + ['ipv6'], 'dup-addr-detect-transmits'):
+ tmp = read_file(f'{proc_base}/dad_transmits')
+ self.assertEqual(dad_transmits, tmp)
+
+ if cli_defined(self._base_path + ['ipv6'], 'disable-forwarding'):
+ tmp = read_file(f'{proc_base}/forwarding')
+ self.assertEqual('0', tmp)
+
+ if cli_defined(self._base_path + ['ipv6'], 'source-validation'):
+ base_options = f'iifname "{interface}"'
+ out = cmd('sudo nft list chain ip6 raw vyos_rpfilter')
+ for line in out.splitlines():
+ if line.startswith(base_options):
+ self.assertIn('fib saddr . iif oif 0', line)
+ self.assertIn('drop', line)
+
+ def test_dhcpv6_client_options(self):
+ if not self._test_ipv6_dhcpc6:
+ self.skipTest('not supported')
+
+ duid_base = 10
+ for interface in self._interfaces:
+ duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
+ path = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(path + option.split())
+
+ # Enable DHCPv6 client
+ self.cli_set(path + ['address', 'dhcpv6'])
+ self.cli_set(path + ['dhcpv6-options', 'no-release'])
+ self.cli_set(path + ['dhcpv6-options', 'rapid-commit'])
+ self.cli_set(path + ['dhcpv6-options', 'parameters-only'])
+ self.cli_set(path + ['dhcpv6-options', 'duid', duid])
+ duid_base += 1
+
+ self.cli_commit()
+
+ duid_base = 10
+ for interface in self._interfaces:
+ duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
+ dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf')
+ self.assertIn(f'interface {interface} ' + '{', dhcpc6_config)
+ self.assertIn(f' request domain-name-servers;', dhcpc6_config)
+ self.assertIn(f' request domain-name;', dhcpc6_config)
+ self.assertIn(f' information-only;', dhcpc6_config)
+ self.assertIn(f' send ia-na 0;', dhcpc6_config)
+ self.assertIn(f' send rapid-commit;', dhcpc6_config)
+ self.assertIn(f' send client-id {duid};', dhcpc6_config)
+ self.assertIn('};', dhcpc6_config)
+ duid_base += 1
+
+ # Better ask the process about it's commandline in the future
+ pid = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)
+ self.assertTrue(pid)
+
+ dhcp6c_options = read_file(f'/proc/{pid}/cmdline')
+ self.assertIn('-n', dhcp6c_options)
+
+ def test_dhcpv6pd_auto_sla_id(self):
+ if not self._test_ipv6_pd:
+ self.skipTest('not supported')
+
+ prefix_len = '56'
+ sla_len = str(64 - int(prefix_len))
+
+ # Create delegatee interfaces first to avoid any confusion by dhcpc6
+ # this is mainly an "issue" with virtual-ethernet interfaces
+ delegatees = ['dum2340', 'dum2341', 'dum2342', 'dum2343', 'dum2344']
+ for delegatee in delegatees:
+ section = Section.section(delegatee)
+ self.cli_set(['interfaces', section, delegatee])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ path = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(path + option.split())
+
+ address = '1'
+ # prefix delegation stuff
+ pd_base = path + ['dhcpv6-options', 'pd', '0']
+ self.cli_set(pd_base + ['length', prefix_len])
+
+ for delegatee in delegatees:
+ self.cli_set(pd_base + ['interface', delegatee, 'address', address])
+ # increment interface address
+ address = str(int(address) + 1)
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf')
+
+ # verify DHCPv6 prefix delegation
+ self.assertIn(f'prefix ::/{prefix_len} infinity;', dhcpc6_config)
+
+ address = '1'
+ sla_id = '0'
+ for delegatee in delegatees:
+ self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config)
+ self.assertIn(f'ifid {address};', dhcpc6_config)
+ self.assertIn(f'sla-id {sla_id};', dhcpc6_config)
+ self.assertIn(f'sla-len {sla_len};', dhcpc6_config)
+
+ # increment sla-id
+ sla_id = str(int(sla_id) + 1)
+ # increment interface address
+ address = str(int(address) + 1)
+
+ # Check for running process
+ self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10))
+
+ for delegatee in delegatees:
+ # we can already cleanup the test delegatee interface here
+ # as until commit() is called, nothing happens
+ section = Section.section(delegatee)
+ self.cli_delete(['interfaces', section, delegatee])
+
+ def test_dhcpv6pd_manual_sla_id(self):
+ if not self._test_ipv6_pd:
+ self.skipTest('not supported')
+
+ prefix_len = '56'
+ sla_len = str(64 - int(prefix_len))
+
+ # Create delegatee interfaces first to avoid any confusion by dhcpc6
+ # this is mainly an "issue" with virtual-ethernet interfaces
+ delegatees = ['dum3340', 'dum3341', 'dum3342', 'dum3343', 'dum3344']
+ for delegatee in delegatees:
+ section = Section.section(delegatee)
+ self.cli_set(['interfaces', section, delegatee])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ path = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(path + option.split())
+
+ # prefix delegation stuff
+ address = '1'
+ sla_id = '1'
+ pd_base = path + ['dhcpv6-options', 'pd', '0']
+ self.cli_set(pd_base + ['length', prefix_len])
+
+ for delegatee in delegatees:
+ self.cli_set(pd_base + ['interface', delegatee, 'address', address])
+ self.cli_set(pd_base + ['interface', delegatee, 'sla-id', sla_id])
+
+ # increment interface address
+ address = str(int(address) + 1)
+ sla_id = str(int(sla_id) + 1)
+
+ self.cli_commit()
+
+ # Verify dhcpc6 client configuration
+ for interface in self._interfaces:
+ address = '1'
+ sla_id = '1'
+ dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf')
+
+ # verify DHCPv6 prefix delegation
+ self.assertIn(f'prefix ::/{prefix_len} infinity;', dhcpc6_config)
+
+ for delegatee in delegatees:
+ self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config)
+ self.assertIn(f'ifid {address};', dhcpc6_config)
+ self.assertIn(f'sla-id {sla_id};', dhcpc6_config)
+ self.assertIn(f'sla-len {sla_len};', dhcpc6_config)
+
+ # increment sla-id
+ sla_id = str(int(sla_id) + 1)
+ # increment interface address
+ address = str(int(address) + 1)
+
+ # Check for running process
+ self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10))
+
+ for delegatee in delegatees:
+ # we can already cleanup the test delegatee interface here
+ # as until commit() is called, nothing happens
+ section = Section.section(delegatee)
+ self.cli_delete(['interfaces', section, delegatee])
+
+ def test_eapol(self):
+ if not self._test_eapol:
+ self.skipTest('not supported')
+
+ cfg_dir = '/run/wpa_supplicant'
+
+ ca_certs = {
+ 'eapol-server-ca-root': server_ca_root_cert_data,
+ 'eapol-server-ca-intermediate': server_ca_intermediate_cert_data,
+ 'eapol-client-ca-root': client_ca_root_cert_data,
+ 'eapol-client-ca-intermediate': client_ca_intermediate_cert_data,
+ }
+ cert_name = 'eapol-client'
+
+ for name, data in ca_certs.items():
+ self.cli_set(['pki', 'ca', name, 'certificate', data.replace('\n','')])
+
+ self.cli_set(['pki', 'certificate', cert_name, 'certificate', client_cert_data.replace('\n','')])
+ self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', client_key_data.replace('\n','')])
+
+ for interface in self._interfaces:
+ path = self._base_path + [interface]
+ for option in self._options.get(interface, []):
+ self.cli_set(path + option.split())
+
+ # Enable EAPoL
+ self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-server-ca-intermediate'])
+ self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate'])
+ self.cli_set(self._base_path + [interface, 'eapol', 'certificate', cert_name])
+
+ self.cli_commit()
+
+ # Test multiple CA chains
+ self.assertEqual(get_certificate_count(interface, 'ca'), 4)
+
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate'])
+
+ self.cli_commit()
+
+ # Validate interface config
+ for interface in self._interfaces:
+ tmp = get_wpa_supplicant_value(interface, 'key_mgmt')
+ self.assertEqual('IEEE8021X', tmp)
+
+ tmp = get_wpa_supplicant_value(interface, 'eap')
+ self.assertEqual('TLS', tmp)
+
+ tmp = get_wpa_supplicant_value(interface, 'eapol_flags')
+ self.assertEqual('0', tmp)
+
+ tmp = get_wpa_supplicant_value(interface, 'ca_cert')
+ self.assertEqual(f'"{cfg_dir}/{interface}_ca.pem"', tmp)
+
+ tmp = get_wpa_supplicant_value(interface, 'client_cert')
+ self.assertEqual(f'"{cfg_dir}/{interface}_cert.pem"', tmp)
+
+ tmp = get_wpa_supplicant_value(interface, 'private_key')
+ self.assertEqual(f'"{cfg_dir}/{interface}_cert.key"', tmp)
+
+ mac = read_file(f'/sys/class/net/{interface}/address')
+ tmp = get_wpa_supplicant_value(interface, 'identity')
+ self.assertEqual(f'"{mac}"', tmp)
+
+ # Check certificate files have the full chain
+ self.assertEqual(get_certificate_count(interface, 'ca'), 2)
+ self.assertEqual(get_certificate_count(interface, 'cert'), 3)
+
+ # Check for running process
+ self.assertTrue(process_named_running('wpa_supplicant', cmdline=f'-i{interface}'))
+
+ # Remove EAPoL configuration
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'eapol'])
+
+ # Commit and check that process is no longer running
+ self.cli_commit()
+ self.assertFalse(process_named_running('wpa_supplicant'))
+
+ for name in ca_certs:
+ self.cli_delete(['pki', 'ca', name])
+ self.cli_delete(['pki', 'certificate', cert_name])
diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
new file mode 100644
index 0000000..940306a
--- /dev/null
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -0,0 +1,162 @@
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+import paramiko
+import pprint
+
+from time import sleep
+from typing import Type
+
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+from vyos import ConfigError
+from vyos.defaults import commit_lock
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+
+save_config = '/tmp/vyos-smoketest-save'
+
+# This class acts as shim between individual Smoketests developed for VyOS and
+# the Python UnitTest framework. Before every test is loaded, we dump the current
+# system configuration and reload it after the test - despite the test results.
+#
+# Using this approach we can not render a live system useless while running any
+# kind of smoketest. In addition it adds debug capabilities like printing the
+# command used to execute the test.
+class VyOSUnitTestSHIM:
+ class TestCase(unittest.TestCase):
+ # if enabled in derived class, print out each and every set/del command
+ # on the CLI. This is usefull to grap all the commands required to
+ # trigger the certain failure condition.
+ # Use "self.debug = True" in derived classes setUp() method
+ debug = False
+
+ @classmethod
+ def setUpClass(cls):
+ cls._session = ConfigSession(os.getpid())
+ cls._session.save_config(save_config)
+ if os.path.exists('/tmp/vyos.smoketest.debug'):
+ cls.debug = True
+ pass
+
+ @classmethod
+ def tearDownClass(cls):
+ # discard any pending changes which might caused a messed up config
+ cls._session.discard()
+ # ... and restore the initial state
+ cls._session.migrate_and_load_config(save_config)
+
+ try:
+ cls._session.commit()
+ except (ConfigError, ConfigSessionError):
+ cls._session.discard()
+ cls.fail(cls)
+
+ def cli_set(self, config):
+ if self.debug:
+ print('set ' + ' '.join(config))
+ self._session.set(config)
+
+ def cli_delete(self, config):
+ if self.debug:
+ print('del ' + ' '.join(config))
+ self._session.delete(config)
+
+ def cli_discard(self):
+ if self.debug:
+ print('DISCARD')
+ self._session.discard()
+
+ def cli_commit(self):
+ if self.debug:
+ print('commit')
+ self._session.commit()
+ # during a commit there is a process opening commit_lock, and run() returns 0
+ while run(f'sudo lsof -nP {commit_lock}') == 0:
+ sleep(0.250)
+
+ def op_mode(self, path : list) -> None:
+ """
+ Execute OP-mode command and return stdout
+ """
+ if self.debug:
+ print('commit')
+ path = ' '.join(path)
+ out = cmd(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {path}')
+ if self.debug:
+ print(f'\n\ncommand "{path}" returned:\n')
+ pprint.pprint(out)
+ return out
+
+ def getFRRconfig(self, string=None, end='$', endsection='^!', daemon=''):
+ """ Retrieve current "running configuration" from FRR """
+ command = f'vtysh -c "show run {daemon} no-header"'
+ if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"'
+ out = cmd(command)
+ if self.debug:
+ print(f'\n\ncommand "{command}" returned:\n')
+ pprint.pprint(out)
+ return out
+
+ @staticmethod
+ def ssh_send_cmd(command, username, password, hostname='localhost'):
+ """ SSH command execution helper """
+ # Try to login via SSH
+ ssh_client = paramiko.SSHClient()
+ ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh_client.connect(hostname=hostname, username=username, password=password)
+ _, stdout, stderr = ssh_client.exec_command(command)
+ output = stdout.read().decode().strip()
+ error = stderr.read().decode().strip()
+ ssh_client.close()
+ return output, error
+
+ # Verify nftables output
+ def verify_nftables(self, nftables_search, table, inverse=False, args=''):
+ nftables_output = cmd(f'sudo nft {args} list table {table}')
+
+ for search in nftables_search:
+ matched = False
+ for line in nftables_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(not matched if inverse else matched, msg=search)
+
+ def verify_nftables_chain(self, nftables_search, table, chain, inverse=False, args=''):
+ nftables_output = cmd(f'sudo nft {args} list chain {table} {chain}')
+
+ for search in nftables_search:
+ matched = False
+ for line in nftables_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(not matched if inverse else matched, msg=search)
+
+# standard construction; typing suggestion: https://stackoverflow.com/a/70292317
+def ignore_warning(warning: Type[Warning]):
+ import warnings
+ from functools import wraps
+
+ def inner(f):
+ @wraps(f)
+ def wrapped(*args, **kwargs):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=warning)
+ return f(*args, **kwargs)
+ return wrapped
+ return inner
diff --git a/smoketest/scripts/cli/test_backslash_escape.py b/smoketest/scripts/cli/test_backslash_escape.py
new file mode 100644
index 0000000..e94e9ab
--- /dev/null
+++ b/smoketest/scripts/cli/test_backslash_escape.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configtree import ConfigTree
+
+base_path = ['interfaces', 'ethernet', 'eth0', 'description']
+
+cases_word = [r'fo\o', r'fo\\o', r'foço\o', r'foço\\o']
+# legacy CLI output quotes only if whitespace present; this is a notable
+# difference that confounds the translation legacy -> modern, hence
+# determines the regex used in function replace_backslash
+cases_phrase = [r'some fo\o', r'some fo\\o', r'some foço\o', r'some foço\\o']
+
+case_save_config = '/tmp/smoketest-case-save'
+
+class TestBackslashEscape(VyOSUnitTestSHIM.TestCase):
+ def test_backslash_escape_word(self):
+ for case in cases_word:
+ self.cli_set(base_path + [case])
+ self.cli_commit()
+ # save_config tests translation though subsystems:
+ # legacy output -> config -> configtree -> file
+ self._session.save_config(case_save_config)
+ # reload to configtree and confirm:
+ with open(case_save_config) as f:
+ config_string = f.read()
+ ct = ConfigTree(config_string)
+ res = ct.return_value(base_path)
+ self.assertEqual(case, res, msg=res)
+ print(f'description: {res}')
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_backslash_escape_phrase(self):
+ for case in cases_phrase:
+ self.cli_set(base_path + [case])
+ self.cli_commit()
+ # save_config tests translation though subsystems:
+ # legacy output -> config -> configtree -> file
+ self._session.save_config(case_save_config)
+ # reload to configtree and confirm:
+ with open(case_save_config) as f:
+ config_string = f.read()
+ ct = ConfigTree(config_string)
+ res = ct.return_value(base_path)
+ self.assertEqual(case, res, msg=res)
+ print(f'description: {res}')
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_cgnat.py b/smoketest/scripts/cli/test_cgnat.py
new file mode 100644
index 0000000..02dad3d
--- /dev/null
+++ b/smoketest/scripts/cli/test_cgnat.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+
+
+base_path = ['nat', 'cgnat']
+nftables_cgnat_config = '/run/nftables-cgnat.nft'
+
+
+class TestCGNAT(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestCGNAT, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+ self.assertFalse(os.path.exists(nftables_cgnat_config))
+
+ def test_cgnat(self):
+ internal_name = 'vyos-int-01'
+ external_name = 'vyos-ext-01'
+ internal_net = '100.64.0.0/29'
+ external_net = '192.0.2.1-192.0.2.2'
+ external_ports = '40000-60000'
+ ports_per_subscriber = '5000'
+ rule = '100'
+
+ nftables_search = [
+ ['map tcp_nat_map'],
+ ['map udp_nat_map'],
+ ['map icmp_nat_map'],
+ ['map other_nat_map'],
+ ['100.64.0.0 : 192.0.2.1 . 40000-44999'],
+ ['100.64.0.1 : 192.0.2.1 . 45000-49999'],
+ ['100.64.0.2 : 192.0.2.1 . 50000-54999'],
+ ['100.64.0.3 : 192.0.2.1 . 55000-59999'],
+ ['100.64.0.4 : 192.0.2.2 . 40000-44999'],
+ ['100.64.0.5 : 192.0.2.2 . 45000-49999'],
+ ['100.64.0.6 : 192.0.2.2 . 50000-54999'],
+ ['100.64.0.7 : 192.0.2.2 . 55000-59999'],
+ ['chain POSTROUTING'],
+ ['type nat hook postrouting priority srcnat'],
+ ['ip protocol tcp counter snat ip to ip saddr map @tcp_nat_map'],
+ ['ip protocol udp counter snat ip to ip saddr map @udp_nat_map'],
+ ['ip protocol icmp counter snat ip to ip saddr map @icmp_nat_map'],
+ ['counter snat ip to ip saddr map @other_nat_map'],
+ ]
+
+ self.cli_set(base_path + ['pool', 'external', external_name, 'external-port-range', external_ports])
+ self.cli_set(base_path + ['pool', 'external', external_name, 'range', external_net])
+
+ # allocation out of the available ports
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(base_path + ['pool', 'external', external_name, 'per-user-limit', 'port', '8000'])
+ self.cli_commit()
+ self.cli_set(base_path + ['pool', 'external', external_name, 'per-user-limit', 'port', ports_per_subscriber])
+
+ # internal pool not set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['pool', 'internal', internal_name, 'range', internal_net])
+
+ self.cli_set(base_path + ['rule', rule, 'source', 'pool', internal_name])
+ # non-exist translation pool
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(base_path + ['rule', rule, 'translation', 'pool', 'fake-pool'])
+ self.cli_commit()
+
+ self.cli_set(base_path + ['rule', rule, 'translation', 'pool', external_name])
+ self.cli_commit()
+
+ self.verify_nftables(nftables_search, 'ip cgnat', inverse=False, args='-s')
+
+
+ def test_cgnat_sequence(self):
+ internal_name = 'earth'
+ external_name = 'milky_way'
+ internal_net = '100.64.0.0/28'
+
+ ext_addr_alpha_proxima = '192.0.2.121/32'
+ ext_addr_beta_cygni = '198.51.100.23/32'
+ ext_addr_gamma_leonis = '203.0.113.102/32'
+
+ ext_seq_beta_cygni = '3'
+ ext_seq_gamma_leonis = '10'
+
+ external_ports = '1024-65535'
+ ports_per_subscriber = '10000'
+ rule = '100'
+
+ nftables_search = [
+ ['100.64.0.0 : 198.51.100.23 . 1024-11023, 100.64.0.1 : 198.51.100.23 . 11024-21023'],
+ ['100.64.0.4 : 198.51.100.23 . 41024-51023, 100.64.0.5 : 198.51.100.23 . 51024-61023'],
+ ['100.64.0.6 : 203.0.113.102 . 1024-11023, 100.64.0.7 : 203.0.113.102 . 11024-21023'],
+ ['100.64.0.8 : 203.0.113.102 . 21024-31023, 100.64.0.9 : 203.0.113.102 . 31024-41023'],
+ ['100.64.0.10 : 203.0.113.102 . 41024-51023, 100.64.0.11 : 203.0.113.102 . 51024-61023'],
+ ['100.64.0.12 : 192.0.2.121 . 1024-11023, 100.64.0.13 : 192.0.2.121 . 11024-21023'],
+ ['100.64.0.14 : 192.0.2.121 . 21024-31023, 100.64.0.15 : 192.0.2.121 . 31024-41023'],
+ ]
+
+ self.cli_set(base_path + ['pool', 'external', external_name, 'external-port-range', external_ports])
+ self.cli_set(base_path + ['pool', 'external', external_name, 'per-user-limit', 'port', ports_per_subscriber])
+ self.cli_set(base_path + ['pool', 'external', external_name, 'range', ext_addr_alpha_proxima])
+ self.cli_set(base_path + ['pool', 'external', external_name, 'range', ext_addr_beta_cygni, 'seq', ext_seq_beta_cygni])
+ self.cli_set(base_path + ['pool', 'external', external_name, 'range', ext_addr_gamma_leonis, 'seq', ext_seq_gamma_leonis])
+ self.cli_set(base_path + ['pool', 'internal', internal_name, 'range', internal_net])
+ self.cli_set(base_path + ['rule', rule, 'source', 'pool', internal_name])
+ self.cli_set(base_path + ['rule', rule, 'translation', 'pool', external_name])
+ self.cli_commit()
+
+ self.verify_nftables(nftables_search, 'ip cgnat', inverse=False, args='-s')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_config_dependency.py b/smoketest/scripts/cli/test_config_dependency.py
new file mode 100644
index 0000000..99e807a
--- /dev/null
+++ b/smoketest/scripts/cli/test_config_dependency.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+import unittest
+from time import sleep
+
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.process import cmd
+from vyos.configsession import ConfigSessionError
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+
+class TestConfigDep(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # smoketests are run without configd in 1.4; with configd in 1.5
+ # the tests below check behavior under configd:
+ # test_configdep_error checks for regression under configd (T6559)
+ # test_configdep_prio_queue checks resolution under configd (T6671)
+ cls.running_state = is_systemd_service_running('vyos-configd.service')
+
+ if not cls.running_state:
+ cmd('sudo systemctl start vyos-configd.service')
+ # allow time for init
+ sleep(1)
+
+ super(TestConfigDep, cls).setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestConfigDep, cls).tearDownClass()
+
+ # return to running_state
+ if not cls.running_state:
+ cmd('sudo systemctl stop vyos-configd.service')
+
+ def test_configdep_error(self):
+ address_group = 'AG'
+ address = '192.168.137.5'
+ nat_base = ['nat', 'source', 'rule', '10']
+ interface = 'eth1'
+
+ self.cli_set(['firewall', 'group', 'address-group', address_group,
+ 'address', address])
+ self.cli_set(nat_base + ['outbound-interface', 'name', interface])
+ self.cli_set(nat_base + ['source', 'group', 'address-group', address_group])
+ self.cli_set(nat_base + ['translation', 'address', 'masquerade'])
+ self.cli_commit()
+
+ self.cli_delete(['firewall'])
+ # check error in call to dependent script (nat)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # clean up remaining
+ self.cli_delete(['nat'])
+ self.cli_commit()
+
+ def test_configdep_prio_queue(self):
+ # confirm that that a dependency (in this case, conntrack ->
+ # conntrack-sync) is not immediately called if the target is
+ # scheduled in the priority queue, indicating that it may require an
+ # intermediate activitation (bond0)
+ bonding_base = ['interfaces', 'bonding']
+ bond_interface = 'bond0'
+ bond_address = '192.0.2.1/24'
+ vrrp_group_base = ['high-availability', 'vrrp', 'group']
+ vrrp_sync_group_base = ['high-availability', 'vrrp', 'sync-group']
+ vrrp_group = 'ETH2'
+ vrrp_sync_group = 'GROUP'
+ conntrack_sync_base = ['service', 'conntrack-sync']
+ conntrack_peer = '192.0.2.77'
+
+ # simple set to trigger in-session conntrack -> conntrack-sync
+ # dependency; note that this is triggered on boot in 1.4 due to
+ # default 'system conntrack modules'
+ self.cli_set(['system', 'conntrack', 'table-size', '524288'])
+
+ self.cli_set(['interfaces', 'ethernet', 'eth2', 'address',
+ '198.51.100.2/24'])
+
+ self.cli_set(bonding_base + [bond_interface, 'address',
+ bond_address])
+ self.cli_set(bonding_base + [bond_interface, 'member', 'interface',
+ 'eth3'])
+
+ self.cli_set(vrrp_group_base + [vrrp_group, 'address',
+ '198.51.100.200/24'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'hello-source-address',
+ '198.51.100.2'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'interface', 'eth2'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'priority', '200'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'vrid', '22'])
+ self.cli_set(vrrp_sync_group_base + [vrrp_sync_group, 'member',
+ vrrp_group])
+
+ self.cli_set(conntrack_sync_base + ['failover-mechanism', 'vrrp',
+ 'sync-group', vrrp_sync_group])
+
+ self.cli_set(conntrack_sync_base + ['interface', bond_interface,
+ 'peer', conntrack_peer])
+
+ self.cli_commit()
+
+ # clean up
+ self.cli_delete(bonding_base)
+ self.cli_delete(vrrp_group_base)
+ self.cli_delete(vrrp_sync_group_base)
+ self.cli_delete(conntrack_sync_base)
+ self.cli_delete(['interfaces', 'ethernet', 'eth2', 'address'])
+ self.cli_delete(['system', 'conntrack', 'table-size'])
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_configd_init.py b/smoketest/scripts/cli/test_configd_init.py
new file mode 100644
index 0000000..245c038
--- /dev/null
+++ b/smoketest/scripts/cli/test_configd_init.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from time import sleep
+
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.process import cmd
+
+class TestConfigdInit(unittest.TestCase):
+ def setUp(self):
+ self.running_state = is_systemd_service_running('vyos-configd.service')
+
+ def test_configd_init(self):
+ if not self.running_state:
+ cmd('sudo systemctl start vyos-configd.service')
+ # allow time for init to succeed/fail
+ sleep(2)
+ self.assertTrue(is_systemd_service_running('vyos-configd.service'))
+
+ def tearDown(self):
+ if not self.running_state:
+ cmd('sudo systemctl stop vyos-configd.service')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py
new file mode 100644
index 0000000..c03b9eb
--- /dev/null
+++ b/smoketest/scripts/cli/test_container.py
@@ -0,0 +1,268 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import glob
+import json
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from ipaddress import ip_interface
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+
+base_path = ['container']
+cont_image = 'busybox:stable' # busybox is included in vyos-build
+PROCESS_NAME = 'conmon'
+PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid'
+
+busybox_image_path = '/usr/share/vyos/busybox-stable.tar'
+
+def cmd_to_json(command):
+ c = cmd(command + ' --format=json')
+ data = json.loads(c)[0]
+ return data
+
+class TestContainer(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestContainer, cls).setUpClass()
+
+ # Load image for smoketest provided in vyos-build
+ try:
+ cmd(f'cat {busybox_image_path} | sudo podman load')
+ except:
+ cls.skipTest(cls, reason='busybox image not available')
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestContainer, cls).tearDownClass()
+
+ # Cleanup podman image
+ cmd(f'sudo podman image rm -f {cont_image}')
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Ensure no container process remains
+ self.assertIsNone(process_named_running(PROCESS_NAME))
+
+ # Ensure systemd units are removed
+ units = glob.glob('/run/systemd/system/vyos-container-*')
+ self.assertEqual(units, [])
+
+ def test_basic(self):
+ cont_name = 'c1'
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '10.0.2.15/24'])
+ self.cli_set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2'])
+ self.cli_set(['system', 'name-server', '1.1.1.1'])
+ self.cli_set(['system', 'name-server', '8.8.8.8'])
+
+ self.cli_set(base_path + ['name', cont_name, 'image', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
+ self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096'])
+
+ # commit changes
+ self.cli_commit()
+
+ pid = 0
+ with open(PROCESS_PIDFILE.format(cont_name), 'r') as f:
+ pid = int(f.read())
+
+ # Check for running process
+ self.assertEqual(process_named_running(PROCESS_NAME), pid)
+
+ # verify
+ tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax')
+ self.assertEqual(tmp, 'kernel.msgmax = 4096')
+
+ def test_cpu_limit(self):
+ cont_name = 'c2'
+
+ self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
+ self.cli_set(base_path + ['name', cont_name, 'image', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'cpu-quota', '1.25'])
+
+ self.cli_commit()
+
+ pid = 0
+ with open(PROCESS_PIDFILE.format(cont_name), 'r') as f:
+ pid = int(f.read())
+
+ # Check for running process
+ self.assertEqual(process_named_running(PROCESS_NAME), pid)
+
+ def test_ipv4_network(self):
+ prefix = '192.0.2.0/24'
+ base_name = 'ipv4'
+ net_name = 'NET01'
+
+ self.cli_set(base_path + ['network', net_name, 'prefix', prefix])
+
+ for ii in range(1, 6):
+ name = f'{base_name}-{ii}'
+ self.cli_set(base_path + ['name', name, 'image', cont_image])
+ self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)])
+
+ # verify() - first IP address of a prefix can not be used by a container
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ tmp = f'{base_name}-1'
+ self.cli_delete(base_path + ['name', tmp])
+ self.cli_commit()
+
+ n = cmd_to_json(f'sudo podman network inspect {net_name}')
+ self.assertEqual(n['subnets'][0]['subnet'], prefix)
+
+ # skipt first container, it was never created
+ for ii in range(2, 6):
+ name = f'{base_name}-{ii}'
+ c = cmd_to_json(f'sudo podman container inspect {name}')
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix).ip + 1))
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'], str(ip_interface(prefix).ip + ii))
+
+ def test_ipv6_network(self):
+ prefix = '2001:db8::/64'
+ base_name = 'ipv6'
+ net_name = 'NET02'
+
+ self.cli_set(base_path + ['network', net_name, 'prefix', prefix])
+
+ for ii in range(1, 6):
+ name = f'{base_name}-{ii}'
+ self.cli_set(base_path + ['name', name, 'image', cont_image])
+ self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)])
+
+ # verify() - first IP address of a prefix can not be used by a container
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ tmp = f'{base_name}-1'
+ self.cli_delete(base_path + ['name', tmp])
+ self.cli_commit()
+
+ n = cmd_to_json(f'sudo podman network inspect {net_name}')
+ self.assertEqual(n['subnets'][0]['subnet'], prefix)
+
+ # skipt first container, it was never created
+ for ii in range(2, 6):
+ name = f'{base_name}-{ii}'
+ c = cmd_to_json(f'sudo podman container inspect {name}')
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix).ip + 1))
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix).ip + ii))
+
+ def test_dual_stack_network(self):
+ prefix4 = '192.0.2.0/24'
+ prefix6 = '2001:db8::/64'
+ base_name = 'dual-stack'
+ net_name = 'net-4-6'
+
+ self.cli_set(base_path + ['network', net_name, 'prefix', prefix4])
+ self.cli_set(base_path + ['network', net_name, 'prefix', prefix6])
+
+ for ii in range(1, 6):
+ name = f'{base_name}-{ii}'
+ self.cli_set(base_path + ['name', name, 'image', cont_image])
+ self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix4).ip + ii)])
+ self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix6).ip + ii)])
+
+ # verify() - first IP address of a prefix can not be used by a container
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ tmp = f'{base_name}-1'
+ self.cli_delete(base_path + ['name', tmp])
+ self.cli_commit()
+
+ n = cmd_to_json(f'sudo podman network inspect {net_name}')
+ self.assertEqual(n['subnets'][0]['subnet'], prefix4)
+ self.assertEqual(n['subnets'][1]['subnet'], prefix6)
+
+ # skipt first container, it was never created
+ for ii in range(2, 6):
+ name = f'{base_name}-{ii}'
+ c = cmd_to_json(f'sudo podman container inspect {name}')
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix6).ip + 1))
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix6).ip + ii))
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix4).ip + 1))
+ self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'] , str(ip_interface(prefix4).ip + ii))
+
+ def test_no_name_server(self):
+ prefix = '192.0.2.0/24'
+ base_name = 'ipv4'
+ net_name = 'NET01'
+
+ self.cli_set(base_path + ['network', net_name, 'prefix', prefix])
+ self.cli_set(base_path + ['network', net_name, 'no-name-server'])
+
+ name = f'{base_name}-2'
+ self.cli_set(base_path + ['name', name, 'image', cont_image])
+ self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)])
+ self.cli_commit()
+
+ n = cmd_to_json(f'sudo podman network inspect {net_name}')
+ self.assertEqual(n['dns_enabled'], False)
+
+ def test_uid_gid(self):
+ cont_name = 'uid-test'
+ gid = '100'
+ uid = '1001'
+
+ self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
+ self.cli_set(base_path + ['name', cont_name, 'image', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'gid', gid])
+
+ # verify() - GID can only be set if UID is set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['name', cont_name, 'uid', uid])
+
+ self.cli_commit()
+
+ # verify
+ tmp = cmd(f'sudo podman exec -it {cont_name} id -u')
+ self.assertEqual(tmp, uid)
+ tmp = cmd(f'sudo podman exec -it {cont_name} id -g')
+ self.assertEqual(tmp, gid)
+
+ def test_api_socket(self):
+ base_name = 'api-test'
+ container_list = range(1, 5)
+
+ for ii in container_list:
+ name = f'{base_name}-{ii}'
+ self.cli_set(base_path + ['name', name, 'image', cont_image])
+ self.cli_set(base_path + ['name', name, 'allow-host-networks'])
+
+ self.cli_commit()
+
+ # Query API about running containers
+ tmp = cmd("sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json")
+ tmp = json.loads(tmp)
+
+ # We expect the same amount of containers from the API that we started above
+ self.assertEqual(len(container_list), len(tmp))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
new file mode 100644
index 0000000..3e9ec29
--- /dev/null
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -0,0 +1,1167 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from glob import glob
+from time import sleep
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import run
+from vyos.utils.file import read_file
+
+sysfs_config = {
+ 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'},
+ 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'},
+ 'directed_broadcast': {'sysfs': '/proc/sys/net/ipv4/conf/all/bc_forwarding', 'default': '1', 'test_value': 'disable'},
+ 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'},
+ 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'},
+ 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'},
+ 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'},
+ 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'},
+ 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'},
+ 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'},
+ 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'}
+}
+
+def get_sysctl(parameter):
+ tmp = parameter.replace(r'.', r'/')
+ return read_file(f'/proc/sys/{tmp}')
+
+class TestFirewall(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestFirewall, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, ['firewall'])
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestFirewall, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(['firewall'])
+ self.cli_commit()
+
+ # Verify chains/sets are cleaned up from nftables
+ nftables_search = [
+ ['set M_smoketest_mac'],
+ ['set N_smoketest_network'],
+ ['set P_smoketest_port'],
+ ['set D_smoketest_domain'],
+ ['set RECENT_smoketest_4'],
+ ['chain NAME_smoketest']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter', inverse=True)
+
+ def wait_for_domain_resolver(self, table, set_name, element, max_wait=10):
+ # Resolver no longer blocks commit, need to wait for daemon to populate set
+ count = 0
+ while count < max_wait:
+ code = run(f'sudo nft get element {table} {set_name} {{ {element} }}')
+ if code == 0:
+ return True
+ count += 1
+ sleep(1)
+ return False
+
+ def test_geoip(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['ip saddr @GEOIP_CC_name_smoketest_1', 'drop'],
+ ['ip saddr != @GEOIP_CC_name_smoketest_2', 'accept']
+ ]
+
+ # -t prevents 1000+ GeoIP elements being returned
+ self.verify_nftables(nftables_search, 'ip vyos_filter', args='-t')
+
+ def test_groups(self):
+ hostmap_path = ['system', 'static-host-mapping', 'host-name']
+ example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11']
+
+ self.cli_set(hostmap_path + ['example.com', 'inet', '192.0.2.5'])
+ for ips in example_org:
+ self.cli_set(hostmap_path + ['example.org', 'inet', ips])
+
+ self.cli_commit()
+
+ self.cli_set(['firewall', 'group', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05'])
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
+ self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53'])
+ self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123'])
+ self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com'])
+ self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org'])
+ self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0'])
+
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'address', '172.16.10.10'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface'])
+
+ self.cli_commit()
+
+ self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5')
+
+ nftables_search = [
+ ['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'accept'],
+ ['elements = { 172.16.99.0/24 }'],
+ ['elements = { 53, 123 }'],
+ ['ether saddr @M_smoketest_mac', 'accept'],
+ ['elements = { 00:01:02:03:04:05 }'],
+ ['set D_smoketest_domain'],
+ ['elements = { 192.0.2.5, 192.0.2.8,'],
+ ['192.0.2.10, 192.0.2.11 }'],
+ ['ip saddr @D_smoketest_domain', 'accept'],
+ ['oifname != @I_smoketest_interface', 'accept']
+ ]
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ self.cli_delete(['system', 'static-host-mapping'])
+ self.cli_commit()
+
+ def test_nested_groups(self):
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24'])
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network'])
+ self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53'])
+ self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'port', '123'])
+ self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'include', 'smoketest_port'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network1'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port1'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp'])
+
+ self.cli_commit()
+
+ # Test circular includes
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1'])
+
+ nftables_search = [
+ ['ip saddr @N_smoketest_network1', 'th dport @P_smoketest_port1', 'accept'],
+ ['elements = { 172.16.99.0/24, 172.16.101.0/24 }'],
+ ['elements = { 53, 123 }']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ def test_ipv4_basic_rules(self):
+ name = 'smoketest'
+ interface = 'eth0'
+ interface_inv = '!eth0'
+ interface_wc = 'l2tp*'
+ mss_range = '501-1460'
+ conn_mark = '555'
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-log'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'source', 'address', '172.16.20.10'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '172.16.10.10'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log-options', 'level', 'debug'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'ttl', 'eq', '15'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'destination', 'port', '8888'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log-options', 'level', 'err'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'syn'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'ttl', 'gt', '102'])
+
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-log'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'destination', 'port', '22'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'limit', 'rate', '5/minute'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'log'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'destination', 'port', '22'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'count', '10'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'time', 'minute'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'packet-type', 'host'])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'flags', 'syn'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'mss', mss_range])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'packet-type', 'broadcast'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'name', interface_wc])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'action', 'return'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'connection-mark', conn_mark])
+
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-log'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'name', interface_inv])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'action', 'return'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'protocol', 'icmp'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'connection-mark', conn_mark])
+
+ self.cli_set(['firewall', 'ipv4', 'output', 'raw', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'raw', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'raw', 'rule', '1', 'protocol', 'udp'])
+
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'notrack'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23'])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(int(conn_mark))
+
+ nftables_search = [
+ ['chain VYOS_FORWARD_filter'],
+ ['type filter hook forward priority filter; policy accept;'],
+ ['tcp dport 22', 'limit rate 5/minute', 'accept'],
+ ['tcp dport 22', 'add @RECENT_FWD_filter_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'meta pkttype host', 'drop'],
+ ['log prefix "[ipv4-FWD-filter-default-D]"','FWD-filter default-action drop', 'drop'],
+ ['chain VYOS_INPUT_filter'],
+ ['type filter hook input priority filter; policy accept;'],
+ ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface_wc}"', 'meta pkttype broadcast', 'accept'],
+ ['meta l4proto gre', f'ct mark {mark_hex}', 'return'],
+ ['INP-filter default-action accept', 'accept'],
+ ['chain VYOS_OUTPUT_filter'],
+ ['type filter hook output priority filter; policy accept;'],
+ ['meta l4proto gre', f'oifname != "{interface}"', 'drop'],
+ ['meta l4proto icmp', f'ct mark {mark_hex}', 'return'],
+ ['log prefix "[ipv4-OUT-filter-default-D]"','OUT-filter default-action drop', 'drop'],
+ ['chain VYOS_OUTPUT_raw'],
+ ['type filter hook output priority raw; policy accept;'],
+ ['udp', 'accept'],
+ ['OUT-raw default-action drop', 'drop'],
+ ['chain VYOS_PREROUTING_raw'],
+ ['type filter hook prerouting priority raw; policy accept;'],
+ ['tcp dport 23', 'notrack'],
+ ['PRE-raw default-action accept', 'accept'],
+ ['chain NAME_smoketest'],
+ ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'],
+ ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[ipv4-NAM-smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'],
+ ['log prefix "[ipv4-NAM-smoketest-default-D]"','smoketest default-action', 'drop']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ def test_ipv4_advanced(self):
+ name = 'smoketest-adv'
+ name2 = 'smoketest-adv2'
+ interface = 'eth0'
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-log'])
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '64'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '512'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '1024'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '17'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '52'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'group', '66'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'snapshot-length', '6666'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'queue-threshold','32000'])
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length', '1-30000'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length-exclude', '60000-65535'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp', '3-11'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp-exclude', '21-25'])
+
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'address', '198.51.100.1-198.51.100.50'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'mark', '1010'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', name])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'mark', '!98765'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'action', 'queue'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'queue', '3'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'protocol', 'udp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'action', 'queue'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'fanout'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'bypass'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue', '0-15'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain VYOS_FORWARD_filter'],
+ ['type filter hook forward priority filter; policy accept;'],
+ ['ip saddr 198.51.100.1-198.51.100.50', 'meta mark 0x000003f2', f'jump NAME_{name}'],
+ ['FWD-filter default-action drop', 'drop'],
+ ['chain VYOS_INPUT_filter'],
+ ['type filter hook input priority filter; policy accept;'],
+ ['meta mark != 0x000181cd', 'meta l4proto tcp','queue to 3'],
+ ['meta l4proto udp','queue flags bypass,fanout to 0-15'],
+ ['INP-filter default-action accept', 'accept'],
+ [f'chain NAME_{name}'],
+ ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[ipv4-NAM-{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'],
+ ['ip length 1-30000', 'ip length != 60000-65535', 'ip dscp 0x03-0x0b', 'ip dscp != 0x15-0x19', 'accept'],
+ [f'log prefix "[ipv4-NAM-{name}-default-D]"', 'drop']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ def test_ipv4_synproxy(self):
+ tcp_mss = '1460'
+ tcp_wscale = '7'
+ dport = '22'
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'port', dport])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'synproxy', 'tcp', 'mss', tcp_mss])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'synproxy', 'tcp', 'window-scale', tcp_wscale])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'synproxy'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'tcp dport {dport} ct state invalid,untracked', f'synproxy mss {tcp_mss} wscale {tcp_wscale} timestamp sack-perm']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+
+ def test_ipv4_mask(self):
+ name = 'smoketest-mask'
+ interface = 'eth0'
+
+ self.cli_set(['firewall', 'group', 'address-group', 'mask_group', 'address', '1.1.1.1'])
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-log'])
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '0.0.1.2'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address-mask', '0.0.255.255'])
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address', '!0.0.3.4'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address-mask', '0.0.255.255'])
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'address-mask', '0.0.255.255'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'daddr & 0.0.255.255 == 0.0.1.2'],
+ [f'saddr & 0.0.255.255 != 0.0.3.4'],
+ [f'saddr & 0.0.255.255 == @A_mask_group']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ def test_ipv4_dynamic_groups(self):
+ group01 = 'knock01'
+ group02 = 'allowed'
+
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group01])
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group02])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s'])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m'])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'destination', 'port', '22'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'DA_{group01}'],
+ [f'DA_{group02}'],
+ ['type ipv4_addr'],
+ ['flags dynamic,timeout'],
+ ['chain VYOS_INPUT_filter {'],
+ ['type filter hook input priority filter', 'policy accept'],
+ ['tcp dport 5151', f'update @DA_{group01}', '{ ip saddr timeout 30s }', 'drop'],
+ ['tcp dport 7272', f'ip saddr @DA_{group01}', f'update @DA_{group02}', '{ ip saddr timeout 5m }', 'drop'],
+ ['tcp dport 22', f'ip saddr @DA_{group02}', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ def test_ipv6_basic_rules(self):
+ name = 'v6-smoketest'
+ interface = 'eth0'
+
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'source', 'address', '2002::1-2002::10'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '2002::1:1'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log-options', 'level', 'crit'])
+
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'default-action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'default-log'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'protocol', 'tcp_udp'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'destination', 'port', '8888'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'inbound-interface', 'name', interface])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'udp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'source', 'address', '2002::1:2'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'inbound-interface', 'name', interface])
+
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-log'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'action', 'return'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'outbound-interface', 'name', interface])
+
+ self.cli_set(['firewall', 'ipv6', 'output', 'raw', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'raw', 'rule', '1', 'action', 'notrack'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'raw', 'rule', '1', 'protocol', 'udp'])
+
+ self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'prerouting', 'raw', 'rule', '1', 'destination', 'port', '23'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain VYOS_IPV6_FORWARD_filter'],
+ ['type filter hook forward priority filter; policy accept;'],
+ ['meta l4proto { tcp, udp }', 'th dport 8888', f'iifname "{interface}"', 'reject'],
+ ['log prefix "[ipv6-FWD-filter-default-A]"','FWD-filter default-action accept', 'accept'],
+ ['chain VYOS_IPV6_INPUT_filter'],
+ ['type filter hook input priority filter; policy accept;'],
+ ['meta l4proto udp', 'ip6 saddr 2002::1:2', f'iifname "{interface}"', 'accept'],
+ ['INP-filter default-action accept', 'accept'],
+ ['chain VYOS_IPV6_OUTPUT_filter'],
+ ['type filter hook output priority filter; policy accept;'],
+ ['meta l4proto gre', f'oifname "{interface}"', 'return'],
+ ['log prefix "[ipv6-OUT-filter-default-D]"','OUT-filter default-action drop', 'drop'],
+ ['chain VYOS_IPV6_OUTPUT_raw'],
+ ['type filter hook output priority raw; policy accept;'],
+ ['udp', 'notrack'],
+ ['OUT-raw default-action drop', 'drop'],
+ ['chain VYOS_IPV6_PREROUTING_raw'],
+ ['type filter hook prerouting priority raw; policy accept;'],
+ ['tcp dport 23', 'drop'],
+ ['PRE-raw default-action accept', 'accept'],
+ [f'chain NAME6_{name}'],
+ ['saddr 2002::1-2002::10', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'],
+ [f'"NAM-{name} default-action drop"', f'log prefix "[ipv6-NAM-{name}-default-D]"', 'drop'],
+ ['jump VYOS_STATE_POLICY6'],
+ ['chain VYOS_STATE_POLICY6'],
+ ['ct state established', 'accept'],
+ ['ct state invalid', 'drop'],
+ ['ct state related', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ def test_ipv6_advanced(self):
+ name = 'v6-smoke-adv'
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '65'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '513'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '1025'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '18'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '53'])
+
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length', '1-1999'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length-exclude', '60000-65535'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp', '4-14'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp-exclude', '31-35'])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'default-action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'source', 'address', '2001:db8::/64'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'mark', '!6655-7766'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'jump-target', name])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain VYOS_IPV6_FORWARD_filter'],
+ ['type filter hook forward priority filter; policy accept;'],
+ ['ip6 length 1-1999', 'ip6 length != 60000-65535', 'ip6 dscp 0x04-0x0e', 'ip6 dscp != 0x1f-0x23', 'accept'],
+ ['chain VYOS_IPV6_INPUT_filter'],
+ ['type filter hook input priority filter; policy accept;'],
+ ['ip6 saddr 2001:db8::/64', 'meta mark != 0x000019ff-0x00001e56', f'jump NAME6_{name}'],
+ [f'chain NAME6_{name}'],
+ ['ip6 length { 65, 513, 1025 }', 'ip6 dscp { af21, 0x35 }', 'accept'],
+ [f'log prefix "[ipv6-NAM-{name}-default-D]"', 'drop']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ def test_ipv6_mask(self):
+ name = 'v6-smoketest-mask'
+ interface = 'eth0'
+
+ self.cli_set(['firewall', 'group', 'ipv6-address-group', 'mask_group', 'address', '::beef'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'default-log'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '::1111:2222:3333:4444'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address-mask', '::ffff:ffff:ffff:ffff'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address', '!::aaaa:bbbb:cccc:dddd'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address-mask', '::ffff:ffff:ffff:ffff'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group'])
+ self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'address-mask', '::ffff:ffff:ffff:ffff'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['daddr & ::ffff:ffff:ffff:ffff == ::1111:2222:3333:4444'],
+ ['saddr & ::ffff:ffff:ffff:ffff != ::aaaa:bbbb:cccc:dddd'],
+ ['saddr & ::ffff:ffff:ffff:ffff == @A6_mask_group']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ def test_ipv6_dynamic_groups(self):
+ group01 = 'knock01'
+ group02 = 'allowed'
+
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group01])
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group02])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s'])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m'])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'destination', 'port', '22'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'DA6_{group01}'],
+ [f'DA6_{group02}'],
+ ['type ipv6_addr'],
+ ['flags dynamic,timeout'],
+ ['chain VYOS_IPV6_INPUT_filter {'],
+ ['type filter hook input priority filter', 'policy accept'],
+ ['tcp dport 5151', f'update @DA6_{group01}', '{ ip6 saddr timeout 30s }', 'drop'],
+ ['tcp dport 7272', f'ip6 saddr @DA6_{group01}', f'update @DA6_{group02}', '{ ip6 saddr timeout 5m }', 'drop'],
+ ['tcp dport 22', f'ip6 saddr @DA6_{group02}', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ def test_ipv4_global_state(self):
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['jump VYOS_STATE_POLICY'],
+ ['chain VYOS_STATE_POLICY'],
+ ['ct state established', 'accept'],
+ ['ct state invalid', 'drop'],
+ ['ct state related', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ # Check conntrack is enabled from state-policy
+ self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
+ self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
+
+ def test_ipv4_state_and_status_rules(self):
+ name = 'smoketest-state'
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'related'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'state', 'invalid'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'state', 'new'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'connection-status', 'nat', 'destination'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'new'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'established'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'connection-status', 'nat', 'source'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'state', 'related'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'conntrack-helper', 'ftp'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'conntrack-helper', 'pptp'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['ct state { established, related }', 'accept'],
+ ['ct state invalid', 'reject'],
+ ['ct state new', 'ct status dnat', 'accept'],
+ ['ct state { established, new }', 'ct status snat', 'accept'],
+ ['ct state related', 'ct helper { "ftp", "pptp" }', 'accept'],
+ ['drop', f'comment "NAM-{name} default-action drop"']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ # Check conntrack
+ self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
+ self.verify_nftables_chain([['return']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
+
+ def test_bridge_firewall(self):
+ name = 'smoketest'
+ interface_in = 'eth0'
+ mac_address = '00:53:00:00:00:01'
+ vlan_id = '12'
+ vlan_prior = '3'
+
+ # Check bridge-nf-call-iptables default value: 0
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-iptables'), '0')
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-ip6tables'), '0')
+
+ self.cli_set(['firewall', 'group', 'ipv6-address-group', 'AGV6', 'address', '2001:db1::1'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'ipv4'])
+ self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'invalid-connections'])
+
+ self.cli_set(['firewall', 'bridge', 'name', name, 'default-action', 'accept'])
+ self.cli_set(['firewall', 'bridge', 'name', name, 'default-log'])
+ self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'source', 'mac-address', mac_address])
+ self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'inbound-interface', 'name', interface_in])
+ self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log'])
+ self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log-options', 'level', 'crit'])
+
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-log'])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'id', vlan_id])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'ethernet-type', 'ipv4'])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'action', 'jump'])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name])
+ self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior])
+
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'inbound-interface', 'name', interface_in])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'source', 'address', '192.0.2.2'])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'state', 'new'])
+
+ self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '1', 'action', 'notrack'])
+ self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '1', 'destination', 'group', 'ipv6-address-group', 'AGV6'])
+ self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '2', 'ethernet-type', 'arp'])
+ self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '2', 'action', 'accept'])
+
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['set A6_AGV6'],
+ ['type ipv6_addr'],
+ ['elements', '2001:db1::1'],
+ ['chain VYOS_FORWARD_filter'],
+ ['type filter hook forward priority filter; policy accept;'],
+ ['jump VYOS_STATE_POLICY'],
+ [f'vlan id {vlan_id}', 'vlan type ip', 'accept'],
+ [f'vlan pcp {vlan_prior}', f'jump NAME_{name}'],
+ ['log prefix "[bri-FWD-filter-default-D]"', 'drop', 'FWD-filter default-action drop'],
+ [f'chain NAME_{name}'],
+ [f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'],
+ ['accept', f'{name} default-action accept'],
+ ['chain VYOS_INPUT_filter'],
+ ['type filter hook input priority filter; policy accept;'],
+ ['ct state new', 'ip saddr 192.0.2.2', f'iifname "{interface_in}"', 'accept'],
+ ['chain VYOS_OUTPUT_filter'],
+ ['type filter hook output priority filter; policy accept;'],
+ ['ct state invalid', 'udp sport 67', 'udp dport 68', 'accept'],
+ ['ct state invalid', 'ether type arp', 'accept'],
+ ['chain VYOS_PREROUTING_filter'],
+ ['type filter hook prerouting priority filter; policy accept;'],
+ ['ip6 daddr @A6_AGV6', 'notrack'],
+ ['ether type arp', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'bridge vyos_filter')
+ ## Check bridge-nf-call-iptables is set to 1, and for ipv6 remains on default 0
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-iptables'), '1')
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-ip6tables'), '0')
+
+ def test_source_validation(self):
+ # Strict
+ self.cli_set(['firewall', 'global-options', 'source-validation', 'strict'])
+ self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'strict'])
+ self.cli_commit()
+
+ nftables_strict_search = [
+ ['fib saddr . iif oif 0', 'drop']
+ ]
+
+ self.verify_nftables_chain(nftables_strict_search, 'ip raw', 'vyos_global_rpfilter')
+ self.verify_nftables_chain(nftables_strict_search, 'ip6 raw', 'vyos_global_rpfilter')
+
+ # Loose
+ self.cli_set(['firewall', 'global-options', 'source-validation', 'loose'])
+ self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'loose'])
+ self.cli_commit()
+
+ nftables_loose_search = [
+ ['fib saddr oif 0', 'drop']
+ ]
+
+ self.verify_nftables_chain(nftables_loose_search, 'ip raw', 'vyos_global_rpfilter')
+ self.verify_nftables_chain(nftables_loose_search, 'ip6 raw', 'vyos_global_rpfilter')
+
+ def test_sysfs(self):
+ for name, conf in sysfs_config.items():
+ paths = glob(conf['sysfs'])
+ for path in paths:
+ with open(path, 'r') as f:
+ self.assertEqual(f.read().strip(), conf['default'], msg=path)
+
+ self.cli_set(['firewall', 'global-options', name.replace("_", "-"), conf['test_value']])
+
+ self.cli_commit()
+
+ for name, conf in sysfs_config.items():
+ paths = glob(conf['sysfs'])
+ for path in paths:
+ with open(path, 'r') as f:
+ self.assertNotEqual(f.read().strip(), conf['default'], msg=path)
+
+ def test_timeout_sysctl(self):
+ timeout_config = {
+ 'net.netfilter.nf_conntrack_icmp_timeout' :{
+ 'cli' : ['global-options', 'timeout', 'icmp'],
+ 'test_value' : '180',
+ 'default_value' : '30',
+ },
+ 'net.netfilter.nf_conntrack_generic_timeout' :{
+ 'cli' : ['global-options', 'timeout', 'other'],
+ 'test_value' : '1200',
+ 'default_value' : '600',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_close_wait' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'close-wait'],
+ 'test_value' : '30',
+ 'default_value' : '60',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_close' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'close'],
+ 'test_value' : '20',
+ 'default_value' : '10',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_established' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'established'],
+ 'test_value' : '1000',
+ 'default_value' : '432000',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_fin_wait' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'fin-wait'],
+ 'test_value' : '240',
+ 'default_value' : '120',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_last_ack' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'last-ack'],
+ 'test_value' : '300',
+ 'default_value' : '30',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_syn_recv' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'syn-recv'],
+ 'test_value' : '100',
+ 'default_value' : '60',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_syn_sent' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'syn-sent'],
+ 'test_value' : '300',
+ 'default_value' : '120',
+ },
+ 'net.netfilter.nf_conntrack_tcp_timeout_time_wait' :{
+ 'cli' : ['global-options', 'timeout', 'tcp', 'time-wait'],
+ 'test_value' : '303',
+ 'default_value' : '120',
+ },
+ 'net.netfilter.nf_conntrack_udp_timeout' :{
+ 'cli' : ['global-options', 'timeout', 'udp', 'other'],
+ 'test_value' : '90',
+ 'default_value' : '30',
+ },
+ 'net.netfilter.nf_conntrack_udp_timeout_stream' :{
+ 'cli' : ['global-options', 'timeout', 'udp', 'stream'],
+ 'test_value' : '200',
+ 'default_value' : '180',
+ },
+ }
+
+ for parameter, parameter_config in timeout_config.items():
+ self.cli_set(['firewall'] + parameter_config['cli'] + [parameter_config['test_value']])
+
+ # commit changes
+ self.cli_commit()
+
+ # validate configuration
+ for parameter, parameter_config in timeout_config.items():
+ tmp = parameter_config['test_value']
+ self.assertEqual(get_sysctl(f'{parameter}'), tmp)
+
+ # delete all configuration options and revert back to defaults
+ self.cli_delete(['firewall', 'global-options', 'timeout'])
+ self.cli_commit()
+
+ # validate configuration
+ for parameter, parameter_config in timeout_config.items():
+ self.assertEqual(get_sysctl(f'{parameter}'), parameter_config['default_value'])
+
+### Zone
+ def test_zone_basic(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketestv6', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest'])
+ self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'intra-zone-filtering', 'firewall', 'ipv6-name', 'smoketestv6'])
+ self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone'])
+ self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'log'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain VYOS_ZONE_FORWARD'],
+ ['type filter hook forward priority filter + 1'],
+ ['chain VYOS_ZONE_OUTPUT'],
+ ['type filter hook output priority filter + 1'],
+ ['chain VYOS_ZONE_LOCAL'],
+ ['type filter hook input priority filter + 1'],
+ ['chain VZONE_smoketest-eth0'],
+ ['chain VZONE_smoketest-local_IN'],
+ ['chain VZONE_smoketest-local_OUT'],
+ ['oifname "eth0"', 'jump VZONE_smoketest-eth0'],
+ ['jump VZONE_smoketest-local_IN'],
+ ['jump VZONE_smoketest-local_OUT'],
+ ['iifname "eth0"', 'jump NAME_smoketest'],
+ ['oifname "eth0"', 'jump NAME_smoketest'],
+ ['jump VYOS_STATE_POLICY'],
+ ['chain VYOS_STATE_POLICY'],
+ ['ct state established', 'log prefix "[STATE-POLICY-EST-A]"', 'accept'],
+ ['ct state invalid', 'drop'],
+ ['ct state related', 'accept']
+ ]
+
+ nftables_search_v6 = [
+ ['chain VYOS_ZONE_FORWARD'],
+ ['type filter hook forward priority filter + 1'],
+ ['chain VYOS_ZONE_OUTPUT'],
+ ['type filter hook output priority filter + 1'],
+ ['chain VYOS_ZONE_LOCAL'],
+ ['type filter hook input priority filter + 1'],
+ ['chain VZONE_smoketest-eth0'],
+ ['chain VZONE_smoketest-local_IN'],
+ ['chain VZONE_smoketest-local_OUT'],
+ ['oifname "eth0"', 'jump VZONE_smoketest-eth0'],
+ ['jump VZONE_smoketest-local_IN'],
+ ['jump VZONE_smoketest-local_OUT'],
+ ['iifname "eth0"', 'jump NAME6_smoketestv6'],
+ ['jump VYOS_STATE_POLICY6'],
+ ['chain VYOS_STATE_POLICY6'],
+ ['ct state established', 'log prefix "[STATE-POLICY-EST-A]"', 'accept'],
+ ['ct state invalid', 'drop'],
+ ['ct state related', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+ self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
+
+ def test_flow_offload(self):
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '10'])
+ self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0.10'])
+ 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.10 }'],
+ ['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')
+
+ def test_zone_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', 'name', 'smoketest', 'rule', '1', 'action', 'offload'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'offload-target', 'smoketest'])
+
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest', 'rule', '1', 'action', 'offload'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest', 'rule', '1', 'offload-target', 'smoketest'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain NAME_smoketest'],
+ ['flow add @VYOS_FLOWTABLE_smoketest']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ nftables_search = [
+ ['chain NAME6_smoketest'],
+ ['flow add @VYOS_FLOWTABLE_smoketest']
+ ]
+
+ 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')
+
+ def test_ipsec_metadata_match(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'ipsec', 'match-ipsec-in'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'ipsec', 'match-none-in'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'action', 'continue'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'ipsec', 'match-ipsec-out'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'ipsec', 'match-none-out'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'ipsec', 'match-ipsec-in'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'ipsec', 'match-none-in'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'action', 'continue'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'ipsec', 'match-ipsec-out'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'ipsec', 'match-none-out'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['meta ipsec exists', 'accept comment'],
+ ['meta ipsec missing', 'drop comment'],
+ ['rt ipsec exists', 'continue comment'],
+ ['rt ipsec missing', 'reject comment'],
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
+
+ # All valid directional usage of ipsec matches
+ self.cli_commit()
+
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in-indirect'])
+
+ # nft does not support ANY usage of 'meta ipsec' under an output hook, it will fail to load cfg
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_cyclic_jump_validation(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'jump-target', 'smoketest-cycle-2'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'jump-target', 'smoketest-cycle-3'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'log'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
+
+ # Multi-level jumps are unwise but allowed
+ self.cli_commit()
+
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
+
+ # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
+ # It should be caught by conf validation.
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_gre_match(self):
+ name = 'smoketest-gre'
+
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'return'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'gre', 'flags', 'key'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'gre', 'flags', 'checksum', 'unset'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'gre', 'key', '1234'])
+ self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log'])
+
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'action', 'continue'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'gre', 'inner-proto', '0x6558'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'log'])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'gre', 'flags', 'checksum'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'gre', 'key', '4321'])
+
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'protocol', 'gre'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'gre', 'version', 'pptp'])
+
+ self.cli_commit()
+
+ nftables_search_v4 = [
+ ['gre protocol 0x6558', 'continue comment'],
+ ['gre flags & 5 == 4 @th,32,32 0x4d2', 'accept comment'],
+ ]
+
+ nftables_search_v6 = [
+ ['gre flags & 5 == 5 @th,64,32 0x10e1', 'drop comment'],
+ ['gre version 1', 'reject comment'],
+ ]
+
+ self.verify_nftables(nftables_search_v4, 'ip vyos_filter')
+ self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')
+
+ # GRE match will only work with protocol GRE
+ self.cli_delete(['firewall', 'ipv4', 'name', name, 'rule', '1', 'protocol', 'gre'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_discard()
+
+ # GREv1 (PPTP) does not include a key field, match not available
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'gre', 'flags', 'checksum', 'unset'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '4', 'gre', 'key', '1234'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_high-availability_virtual-server.py b/smoketest/scripts/cli/test_high-availability_virtual-server.py
new file mode 100644
index 0000000..2dbf4a5
--- /dev/null
+++ b/smoketest/scripts/cli/test_high-availability_virtual-server.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.ifconfig.vrrp import VRRP
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'keepalived'
+KEEPALIVED_CONF = VRRP.location['config']
+base_path = ['high-availability']
+vrrp_interface = 'eth1'
+
+class TestHAVirtualServer(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(['interfaces', 'ethernet', vrrp_interface, 'address'])
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Process must be terminated after deleting the config
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_ha_virtual_server(self):
+ algo = 'least-connection'
+ delay = '10'
+ method = 'nat'
+ persistence_timeout = '600'
+ vs = 'serv-one'
+ vip = '203.0.113.111'
+ vport = '2222'
+ rservers = ['192.0.2.21', '192.0.2.22', '192.0.2.23']
+ rport = '22'
+ proto = 'tcp'
+ connection_timeout = '30'
+
+ vserver_base = base_path + ['virtual-server']
+
+ self.cli_set(vserver_base + [vs, 'address', vip])
+ self.cli_set(vserver_base + [vs, 'algorithm', algo])
+ self.cli_set(vserver_base + [vs, 'delay-loop', delay])
+ self.cli_set(vserver_base + [vs, 'forward-method', method])
+ self.cli_set(vserver_base + [vs, 'persistence-timeout', persistence_timeout])
+ self.cli_set(vserver_base + [vs, 'port', vport])
+ self.cli_set(vserver_base + [vs, 'protocol', proto])
+ for rs in rservers:
+ self.cli_set(vserver_base + [vs, 'real-server', rs, 'connection-timeout', connection_timeout])
+ self.cli_set(vserver_base + [vs, 'real-server', rs, 'port', rport])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEEPALIVED_CONF)
+
+ self.assertIn(f'virtual_server {vip} {vport}', config)
+ self.assertIn(f'delay_loop {delay}', config)
+ self.assertIn(f'lb_algo lc', config)
+ self.assertIn(f'lb_kind {method.upper()}', config)
+ self.assertIn(f'persistence_timeout {persistence_timeout}', config)
+ self.assertIn(f'protocol {proto.upper()}', config)
+ for rs in rservers:
+ self.assertIn(f'real_server {rs} {rport}', config)
+ self.assertIn(f'{proto.upper()}_CHECK', config)
+ self.assertIn(f'connect_timeout {connection_timeout}', config)
+
+ def test_02_ha_virtual_server_and_vrrp(self):
+ algo = 'least-connection'
+ delay = '15'
+ method = 'nat'
+ persistence_timeout = '300'
+ vs = 'serv-two'
+ vip = '203.0.113.222'
+ vport = '22322'
+ rservers = ['192.0.2.11', '192.0.2.12']
+ rport = '222'
+ proto = 'tcp'
+ connection_timeout = '23'
+ group = 'VyOS'
+ vrid = '99'
+
+ vrrp_base = base_path + ['vrrp', 'group']
+ vserver_base = base_path + ['virtual-server']
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'address', '203.0.113.10/24'])
+
+ # VRRP config
+ self.cli_set(vrrp_base + [group, 'description', group])
+ self.cli_set(vrrp_base + [group, 'interface', vrrp_interface])
+ self.cli_set(vrrp_base + [group, 'address', vip + '/24'])
+ self.cli_set(vrrp_base + [group, 'vrid', vrid])
+
+ # Virtual-server config
+ self.cli_set(vserver_base + [vs, 'address', vip])
+ self.cli_set(vserver_base + [vs, 'algorithm', algo])
+ self.cli_set(vserver_base + [vs, 'delay-loop', delay])
+ self.cli_set(vserver_base + [vs, 'forward-method', method])
+ self.cli_set(vserver_base + [vs, 'persistence-timeout', persistence_timeout])
+ self.cli_set(vserver_base + [vs, 'port', vport])
+ self.cli_set(vserver_base + [vs, 'protocol', proto])
+ for rs in rservers:
+ self.cli_set(vserver_base + [vs, 'real-server', rs, 'connection-timeout', connection_timeout])
+ self.cli_set(vserver_base + [vs, 'real-server', rs, 'port', rport])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEEPALIVED_CONF)
+
+ # Keepalived vrrp
+ self.assertIn(f'# {group}', config)
+ self.assertIn(f'interface {vrrp_interface}', config)
+ self.assertIn(f'virtual_router_id {vrid}', config)
+ self.assertIn(f'priority 100', config) # default value
+ self.assertIn(f'advert_int 1', config) # default value
+ self.assertIn(f'preempt_delay 0', config) # default value
+
+ # Keepalived virtual-server
+ self.assertIn(f'virtual_server {vip} {vport}', config)
+ self.assertIn(f'delay_loop {delay}', config)
+ self.assertIn(f'lb_algo lc', config)
+ self.assertIn(f'lb_kind {method.upper()}', config)
+ self.assertIn(f'persistence_timeout {persistence_timeout}', config)
+ self.assertIn(f'protocol {proto.upper()}', config)
+ for rs in rservers:
+ self.assertIn(f'real_server {rs} {rport}', config)
+ self.assertIn(f'{proto.upper()}_CHECK', config)
+ self.assertIn(f'connect_timeout {connection_timeout}', config)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_high-availability_vrrp.py b/smoketest/scripts/cli/test_high-availability_vrrp.py
new file mode 100644
index 0000000..aa9fa43
--- /dev/null
+++ b/smoketest/scripts/cli/test_high-availability_vrrp.py
@@ -0,0 +1,320 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig.vrrp import VRRP
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.template import inc_ip
+
+PROCESS_NAME = 'keepalived'
+KEEPALIVED_CONF = VRRP.location['config']
+base_path = ['high-availability']
+
+vrrp_interface = 'eth1'
+groups = ['VLAN77', 'VLAN78', 'VLAN201']
+
+def getConfig(string, end='}'):
+ command = f'cat {KEEPALIVED_CONF} | sed -n "/^{string}/,/^{end}/p"'
+ out = cmd(command)
+ return out
+
+class TestVRRP(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ self.cli_delete(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id])
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Process must be terminated after deleting the config
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_default_values(self):
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+ group_base = base_path + ['vrrp', 'group', group]
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]])
+
+ self.cli_set(group_base + ['description', group])
+ self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}'])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['vrid', vlan_id])
+
+ # commit changes
+ self.cli_commit()
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+
+ config = getConfig(f'vrrp_instance {group}')
+
+ self.assertIn(f'# {group}', config)
+ self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config)
+ self.assertIn(f'virtual_router_id {vlan_id}', config)
+ self.assertIn(f'priority 100', config) # default value
+ self.assertIn(f'advert_int 1', config) # default value
+ self.assertIn(f'preempt_delay 0', config) # default value
+ self.assertNotIn(f'use_vmac', config)
+ self.assertIn(f' {vip}', config)
+
+ def test_02_simple_options(self):
+ advertise_interval = '77'
+ priority = '123'
+ preempt_delay = '400'
+ startup_delay = '120'
+ garp_master_delay = '2'
+ garp_master_repeat = '3'
+ garp_master_refresh = '4'
+ garp_master_refresh_repeat = '5'
+ garp_interval = '1.5'
+ group_garp_master_delay = '12'
+ group_garp_master_repeat = '13'
+ group_garp_master_refresh = '14'
+ vrrp_version = '3'
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+ group_base = base_path + ['vrrp', 'group', group]
+ global_param_base = base_path + ['vrrp', 'global-parameters']
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]])
+
+ self.cli_set(group_base + ['description', group])
+ self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}'])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['vrid', vlan_id])
+
+ self.cli_set(group_base + ['advertise-interval', advertise_interval])
+ self.cli_set(group_base + ['priority', priority])
+ self.cli_set(group_base + ['preempt-delay', preempt_delay])
+
+ self.cli_set(group_base + ['rfc3768-compatibility'])
+
+ # Authentication
+ self.cli_set(group_base + ['authentication', 'type', 'plaintext-password'])
+ self.cli_set(group_base + ['authentication', 'password', f'{group}'])
+
+ # GARP
+ self.cli_set(group_base + ['garp', 'master-delay', group_garp_master_delay])
+ self.cli_set(group_base + ['garp', 'master-repeat', group_garp_master_repeat])
+ self.cli_set(group_base + ['garp', 'master-refresh', group_garp_master_refresh])
+
+ # Global parameters
+ #config = getConfig(f'global_defs')
+ self.cli_set(global_param_base + ['startup-delay', f'{startup_delay}'])
+ self.cli_set(global_param_base + ['garp', 'interval', f'{garp_interval}'])
+ self.cli_set(global_param_base + ['garp', 'master-delay', f'{garp_master_delay}'])
+ self.cli_set(global_param_base + ['garp', 'master-repeat', f'{garp_master_repeat}'])
+ self.cli_set(global_param_base + ['garp', 'master-refresh', f'{garp_master_refresh}'])
+ self.cli_set(global_param_base + ['garp', 'master-refresh-repeat', f'{garp_master_refresh_repeat}'])
+ self.cli_set(global_param_base + ['version', vrrp_version])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check Global parameters
+ config = getConfig(f'global_defs')
+ self.assertIn(f'vrrp_startup_delay {startup_delay}', config)
+ self.assertIn(f'vrrp_garp_interval {garp_interval}', config)
+ self.assertIn(f'vrrp_garp_master_delay {garp_master_delay}', config)
+ self.assertIn(f'vrrp_garp_master_repeat {garp_master_repeat}', config)
+ self.assertIn(f'vrrp_garp_master_refresh {garp_master_refresh}', config)
+ self.assertIn(f'vrrp_garp_master_refresh_repeat {garp_master_refresh_repeat}', config)
+ self.assertIn(f'vrrp_version {vrrp_version}', config)
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+
+ config = getConfig(f'vrrp_instance {group}')
+ self.assertIn(f'# {group}', config)
+ self.assertIn(f'state BACKUP', config)
+ self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config)
+ self.assertIn(f'virtual_router_id {vlan_id}', config)
+ self.assertIn(f'priority {priority}', config)
+ self.assertIn(f'advert_int {advertise_interval}', config)
+ self.assertIn(f'preempt_delay {preempt_delay}', config)
+ self.assertIn(f'use_vmac {vrrp_interface}.{vlan_id}v{vlan_id}', config)
+ self.assertIn(f' {vip}', config)
+
+ # Authentication
+ self.assertIn(f'auth_pass "{group}"', config)
+ self.assertIn(f'auth_type PASS', config)
+
+ #GARP
+ self.assertIn(f'garp_master_delay {group_garp_master_delay}', config)
+ self.assertIn(f'garp_master_refresh {group_garp_master_refresh}', config)
+ self.assertIn(f'garp_master_repeat {group_garp_master_repeat}', config)
+
+ def test_03_sync_group(self):
+ sync_group = 'VyOS'
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+ group_base = base_path + ['vrrp', 'group', group]
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]])
+
+ self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}'])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['vrid', vlan_id])
+
+ self.cli_set(base_path + ['vrrp', 'sync-group', sync_group, 'member', group])
+
+ # commit changes
+ self.cli_commit()
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+ config = getConfig(f'vrrp_instance {group}')
+
+ self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config)
+ self.assertIn(f'virtual_router_id {vlan_id}', config)
+ self.assertNotIn(f'use_vmac', config)
+ self.assertIn(f' {vip}', config)
+
+ config = getConfig(f'vrrp_sync_group {sync_group}')
+ self.assertIn(r'group {', config)
+ for group in groups:
+ self.assertIn(f'{group}', config)
+
+ def test_04_exclude_vrrp_interface(self):
+ group = 'VyOS-WAN'
+ none_vrrp_interface = 'eth2'
+ vlan_id = '24'
+ vip = '100.64.24.1/24'
+ vip_dev = '192.0.2.2/24'
+ vrid = '150'
+ group_base = base_path + ['vrrp', 'group', group]
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', '100.64.24.11/24'])
+ self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}'])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['address', vip_dev, 'interface', none_vrrp_interface])
+ self.cli_set(group_base + ['track', 'exclude-vrrp-interface'])
+ self.cli_set(group_base + ['track', 'interface', none_vrrp_interface])
+ self.cli_set(group_base + ['vrid', vrid])
+
+ # commit changes
+ self.cli_commit()
+
+ config = getConfig(f'vrrp_instance {group}')
+
+ self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config)
+ self.assertIn(f'virtual_router_id {vrid}', config)
+ self.assertIn(f'dont_track_primary', config)
+ self.assertIn(f' {vip}', config)
+ self.assertIn(f' {vip_dev} dev {none_vrrp_interface}', config)
+ self.assertIn(f'track_interface', config)
+ self.assertIn(f' {none_vrrp_interface}', config)
+
+ def test_05_set_multiple_peer_address(self):
+ group = 'VyOS-WAN'
+ vlan_id = '24'
+ vip = '100.64.24.1/24'
+ peer_address_1 = '192.0.2.1'
+ peer_address_2 = '192.0.2.2'
+ vrid = '150'
+ group_base = base_path + ['vrrp', 'group', group]
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', '100.64.24.11/24'])
+ self.cli_set(group_base + ['interface', vrrp_interface])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['peer-address', peer_address_1])
+ self.cli_set(group_base + ['peer-address', peer_address_2])
+ self.cli_set(group_base + ['vrid', vrid])
+
+ # commit changes
+ self.cli_commit()
+
+ config = getConfig(f'vrrp_instance {group}')
+
+ self.assertIn(f'interface {vrrp_interface}', config)
+ self.assertIn(f'virtual_router_id {vrid}', config)
+ self.assertIn(f'unicast_peer', config)
+ self.assertIn(f' {peer_address_1}', config)
+ self.assertIn(f' {peer_address_2}', config)
+
+ def test_check_health_script(self):
+ sync_group = 'VyOS'
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+ group_base = base_path + ['vrrp', 'group', group]
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]])
+
+ self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}'])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['vrid', vlan_id])
+
+ self.cli_set(group_base + ['health-check', 'ping', '127.0.0.1'])
+
+ # commit changes
+ self.cli_commit()
+
+ for group in groups:
+ config = getConfig(f'vrrp_instance {group}')
+ self.assertIn(f'track_script', config)
+
+ self.cli_set(base_path + ['vrrp', 'sync-group', sync_group, 'member', groups[0]])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['vrrp', 'group', groups[0], 'health-check'])
+ self.cli_commit()
+
+ for group in groups[1:]:
+ config = getConfig(f'vrrp_instance {group}')
+ self.assertIn(f'track_script', config)
+
+ config = getConfig(f'vrrp_instance {groups[0]}')
+ self.assertNotIn(f'track_script', config)
+
+ config = getConfig(f'vrrp_sync_group {sync_group}')
+ self.assertNotIn(f'track_script', config)
+
+ self.cli_set(base_path + ['vrrp', 'sync-group', sync_group, 'health-check', 'ping', '127.0.0.1'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = getConfig(f'vrrp_instance {groups[0]}')
+ self.assertNotIn(f'track_script', config)
+
+ config = getConfig(f'vrrp_sync_group {sync_group}')
+ self.assertIn(f'track_script', config)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
new file mode 100644
index 0000000..f436424
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+
+from vyos.ifconfig import Section
+from vyos.ifconfig.interface import Interface
+from vyos.configsession import ConfigSessionError
+from vyos.utils.network import get_interface_config
+from vyos.utils.file import read_file
+
+class BondingInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'bonding']
+ cls._mirror_interfaces = ['dum21354']
+ cls._members = []
+
+ # we need to filter out VLAN interfaces identified by a dot (.)
+ # in their name - just in case!
+ if 'TEST_ETH' in os.environ:
+ cls._members = os.environ['TEST_ETH'].split()
+ else:
+ for tmp in Section.interfaces('ethernet', vlan=False):
+ cls._members.append(tmp)
+
+ cls._options = {'bond0' : []}
+ for member in cls._members:
+ cls._options['bond0'].append(f'member interface {member}')
+ cls._interfaces = list(cls._options)
+
+ # call base-classes classmethod
+ super(BondingInterfaceTest, cls).setUpClass()
+
+ def test_add_single_ip_address(self):
+ super().test_add_single_ip_address()
+
+ for interface in self._interfaces:
+ slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split()
+ self.assertListEqual(slaves, self._members)
+
+ def test_vif_8021q_interfaces(self):
+ super().test_vif_8021q_interfaces()
+
+ for interface in self._interfaces:
+ slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split()
+ self.assertListEqual(slaves, self._members)
+
+ def test_bonding_remove_member(self):
+ # T2515: when removing a bond member the previously enslaved/member
+ # interface must be in its former admin-up/down state. Here we ensure
+ # that it is admin-up as it was admin-up before.
+
+ # configure member interfaces
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_commit()
+
+ # remove single bond member port
+ for interface in self._interfaces:
+ remove_member = self._members[0]
+ self.cli_delete(self._base_path + [interface, 'member', 'interface', remove_member])
+
+ self.cli_commit()
+
+ # removed member port must be admin-up
+ for interface in self._interfaces:
+ remove_member = self._members[0]
+ state = Interface(remove_member).get_admin_state()
+ self.assertEqual('up', state)
+
+ def test_bonding_min_links(self):
+ # configure member interfaces
+ min_links = len(self._interfaces)
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'min-links', str(min_links)])
+
+ self.cli_commit()
+
+ # verify config
+ for interface in self._interfaces:
+ tmp = get_interface_config(interface)
+
+ self.assertEqual(min_links, tmp['linkinfo']['info_data']['min_links'])
+ # check LACP default rate
+ self.assertEqual('slow', tmp['linkinfo']['info_data']['ad_lacp_rate'])
+
+ def test_bonding_lacp_rate(self):
+ # configure member interfaces
+ lacp_rate = 'fast'
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'lacp-rate', lacp_rate])
+
+ self.cli_commit()
+
+ # verify config
+ for interface in self._interfaces:
+ tmp = get_interface_config(interface)
+
+ # check LACP minimum links (default value)
+ self.assertEqual(0, tmp['linkinfo']['info_data']['min_links'])
+ self.assertEqual(lacp_rate, tmp['linkinfo']['info_data']['ad_lacp_rate'])
+
+ def test_bonding_hash_policy(self):
+ # Define available bonding hash policies
+ hash_policies = ['layer2', 'layer2+3', 'encap2+3', 'encap3+4']
+ for hash_policy in hash_policies:
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'hash-policy', hash_policy])
+
+ self.cli_commit()
+
+ # verify config
+ for interface in self._interfaces:
+ defined_policy = read_file(f'/sys/class/net/{interface}/bonding/xmit_hash_policy').split()
+ self.assertEqual(defined_policy[0], hash_policy)
+
+ def test_bonding_mii_monitoring_interval(self):
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_commit()
+
+ # verify default
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split()
+ self.assertIn('100', tmp)
+
+ mii_mon = '250'
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'mii-mon-interval', mii_mon])
+
+ self.cli_commit()
+
+ # verify new CLI value
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split()
+ self.assertIn(mii_mon, tmp)
+
+ def test_bonding_multi_use_member(self):
+ # Define available bonding hash policies
+ for interface in ['bond10', 'bond20']:
+ for member in self._members:
+ self.cli_set(self._base_path + [interface, 'member', 'interface', member])
+
+ # check validate() - can not use the same member interfaces multiple times
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(self._base_path + ['bond20'])
+
+ self.cli_commit()
+
+ def test_bonding_source_interface(self):
+ # Re-use member interface that is already a source-interface
+ bond = 'bond99'
+ pppoe = 'pppoe98756'
+ member = next(iter(self._members))
+
+ self.cli_set(self._base_path + [bond, 'member', 'interface', member])
+ self.cli_set(['interfaces', 'pppoe', pppoe, 'source-interface', member])
+
+ # check validate() - can not add interface to bond, it is the source-interface of ...
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'pppoe', pppoe])
+ self.cli_commit()
+
+ # verify config
+ slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ self.assertIn(member, slaves)
+
+ def test_bonding_source_bridge_interface(self):
+ # Re-use member interface that is already a source-interface
+ bond = 'bond1097'
+ bridge = 'br6327'
+ member = next(iter(self._members))
+
+ self.cli_set(self._base_path + [bond, 'member', 'interface', member])
+ self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', member])
+
+ # check validate() - can not add interface to bond, it is a member of bridge ...
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'bridge', bridge])
+ self.cli_commit()
+
+ # verify config
+ slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ self.assertIn(member, slaves)
+
+ def test_bonding_uniq_member_description(self):
+ ethernet_path = ['interfaces', 'ethernet']
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_commit()
+
+ # Add any changes on bonding members
+ # For example add description on separate ethX interfaces
+ for interface in self._interfaces:
+ for member in self._members:
+ self.cli_set(ethernet_path + [member, 'description', member + '_interface'])
+
+ self.cli_commit()
+
+ # verify config
+ for interface in self._interfaces:
+ slaves = read_file(f'/sys/class/net/{interface}/bonding/slaves').split()
+ for member in self._members:
+ self.assertIn(member, slaves)
+
+ def test_bonding_system_mac(self):
+ # configure member interfaces and system-mac
+ default_system_mac = '00:00:00:00:00:00' # default MAC is all zeroes
+ system_mac = '00:50:ab:cd:ef:11'
+
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'system-mac', system_mac])
+
+ self.cli_commit()
+
+ # verify config
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/ad_actor_system')
+ self.assertIn(tmp, system_mac)
+
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'system-mac'])
+
+ self.cli_commit()
+
+ # verify default value
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bonding/ad_actor_system')
+ self.assertIn(tmp, default_system_mac)
+
+ def test_bonding_evpn_multihoming(self):
+ id = '5'
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ self.cli_set(self._base_path + [interface, 'evpn', 'es-id', id])
+ self.cli_set(self._base_path + [interface, 'evpn', 'es-df-pref', id])
+ self.cli_set(self._base_path + [interface, 'evpn', 'es-sys-mac', f'00:12:34:56:78:0{id}'])
+ self.cli_set(self._base_path + [interface, 'evpn', 'uplink'])
+
+ id = int(id) + 1
+
+ self.cli_commit()
+
+ id = '5'
+ for interface in self._interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra')
+
+ self.assertIn(f' evpn mh es-id {id}', frrconfig)
+ self.assertIn(f' evpn mh es-df-pref {id}', frrconfig)
+ self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig)
+ self.assertIn(f' evpn mh uplink', frrconfig)
+
+ id = int(id) + 1
+
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'evpn', 'es-id'])
+ self.cli_delete(self._base_path + [interface, 'evpn', 'es-df-pref'])
+
+ self.cli_commit()
+
+ id = '5'
+ for interface in self._interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra')
+ self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig)
+ self.assertIn(f' evpn mh uplink', frrconfig)
+
+ id = int(id) + 1
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
new file mode 100644
index 0000000..54c981a
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -0,0 +1,498 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import json
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+from copy import deepcopy
+from glob import glob
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.template import ip_from_cidr
+from vyos.utils.process import cmd
+from vyos.utils.file import read_file
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import interface_exists
+
+class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'bridge']
+ cls._mirror_interfaces = ['dum21354']
+ cls._members = []
+
+ # we need to filter out VLAN interfaces identified by a dot (.)
+ # in their name - just in case!
+ if 'TEST_ETH' in os.environ:
+ cls._members = os.environ['TEST_ETH'].split()
+ else:
+ for tmp in Section.interfaces('ethernet', vlan=False):
+ cls._members.append(tmp)
+
+ cls._options['br0'] = []
+ for member in cls._members:
+ cls._options['br0'].append(f'member interface {member}')
+ cls._interfaces = list(cls._options)
+
+ # call base-classes classmethod
+ super(BridgeInterfaceTest, cls).setUpClass()
+
+ def tearDown(self):
+ for intf in self._interfaces:
+ self.cli_delete(self._base_path + [intf])
+
+ super().tearDown()
+
+ def test_isolated_interfaces(self):
+ # Add member interfaces to bridge and set STP cost/priority
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['stp'])
+
+ # assign members to bridge interface
+ for member in self._members:
+ base_member = base + ['member', 'interface', member]
+ self.cli_set(base_member + ['isolated'])
+
+ # commit config
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ tmp = get_interface_config(interface)
+ # STP must be enabled as configured above
+ self.assertEqual(1, tmp['linkinfo']['info_data']['stp_state'])
+
+ # validate member interface configuration
+ for member in self._members:
+ tmp = get_interface_config(member)
+ # verify member is assigned to the bridge
+ self.assertEqual(interface, tmp['master'])
+ # Isolated must be enabled as configured above
+ self.assertTrue(tmp['linkinfo']['info_slave_data']['isolated'])
+
+ def test_igmp_querier_snooping(self):
+ # Add member interfaces to bridge
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+
+ # assign members to bridge interface
+ for member in self._members:
+ base_member = base + ['member', 'interface', member]
+ self.cli_set(base_member)
+
+ # commit config
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Verify IGMP default configuration
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping')
+ self.assertEqual(tmp, '0')
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier')
+ self.assertEqual(tmp, '0')
+
+ # Enable IGMP snooping
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['igmp', 'snooping'])
+
+ # commit config
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Verify IGMP snooping configuration
+ # Verify IGMP default configuration
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping')
+ self.assertEqual(tmp, '1')
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier')
+ self.assertEqual(tmp, '0')
+
+ # Enable IGMP querieer
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['igmp', 'querier'])
+
+ # commit config
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Verify IGMP snooping & querier configuration
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping')
+ self.assertEqual(tmp, '1')
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier')
+ self.assertEqual(tmp, '1')
+
+ # Disable IGMP
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_delete(base + ['igmp'])
+
+ # commit config
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Verify IGMP snooping & querier configuration
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping')
+ self.assertEqual(tmp, '0')
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier')
+ self.assertEqual(tmp, '0')
+
+ # validate member interface configuration
+ for member in self._members:
+ tmp = get_interface_config(member)
+ # verify member is assigned to the bridge
+ self.assertEqual(interface, tmp['master'])
+
+
+ def test_add_remove_bridge_member(self):
+ # Add member interfaces to bridge and set STP cost/priority
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['stp'])
+ self.cli_set(base + ['address', '192.0.2.1/24'])
+
+ cost = 1000
+ priority = 10
+ # assign members to bridge interface
+ for member in self._members:
+ base_member = base + ['member', 'interface', member]
+ self.cli_set(base_member + ['cost', str(cost)])
+ self.cli_set(base_member + ['priority', str(priority)])
+ cost += 1
+ priority += 1
+
+ # commit config
+ self.cli_commit()
+
+ # Add member interfaces to bridge and set STP cost/priority
+ for interface in self._interfaces:
+ cost = 1000
+ priority = 10
+
+ tmp = get_interface_config(interface)
+ self.assertEqual('802.1Q', tmp['linkinfo']['info_data']['vlan_protocol']) # default VLAN protocol
+
+ for member in self._members:
+ tmp = get_interface_config(member)
+ self.assertEqual(interface, tmp['master'])
+ self.assertFalse( tmp['linkinfo']['info_slave_data']['isolated'])
+ self.assertEqual(cost, tmp['linkinfo']['info_slave_data']['cost'])
+ self.assertEqual(priority, tmp['linkinfo']['info_slave_data']['priority'])
+
+ cost += 1
+ priority += 1
+
+
+ def test_vif_8021q_interfaces(self):
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['enable-vlan'])
+ super().test_vif_8021q_interfaces()
+
+ def test_vif_8021q_lower_up_down(self):
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['enable-vlan'])
+ super().test_vif_8021q_lower_up_down()
+
+ def test_vif_8021q_qos_change(self):
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['enable-vlan'])
+ super().test_vif_8021q_qos_change()
+
+ def test_vif_8021q_mtu_limits(self):
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['enable-vlan'])
+ super().test_vif_8021q_mtu_limits()
+
+ def test_bridge_vlan_filter(self):
+ vifs = ['10', '20', '30', '40']
+ native_vlan = '20'
+
+ # Add member interface to bridge and set VLAN filter
+ for interface in self._interfaces:
+ base = self._base_path + [interface]
+ self.cli_set(base + ['enable-vlan'])
+ self.cli_set(base + ['address', '192.0.2.1/24'])
+
+ for vif in vifs:
+ self.cli_set(base + ['vif', vif, 'address', f'192.0.{vif}.1/24'])
+ self.cli_set(base + ['vif', vif, 'mtu', self._mtu])
+
+ for member in self._members:
+ base_member = base + ['member', 'interface', member]
+ self.cli_set(base_member + ['native-vlan', native_vlan])
+ for vif in vifs:
+ self.cli_set(base_member + ['allowed-vlan', vif])
+
+ # commit config
+ self.cli_commit()
+
+ def _verify_members(interface, members) -> None:
+ # check member interfaces are added on the bridge
+ bridge_members = []
+ for tmp in glob(f'/sys/class/net/{interface}/lower_*'):
+ bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+
+ self.assertListEqual(sorted(members), sorted(bridge_members))
+
+ def _check_vlan_filter(interface, vifs) -> None:
+ configured_vlan_ids = []
+
+ bridge_json = cmd(f'bridge -j vlan show dev {interface}')
+ bridge_json = json.loads(bridge_json)
+ self.assertIsNotNone(bridge_json)
+
+ for tmp in bridge_json:
+ self.assertIn('vlans', tmp)
+
+ for vlan in tmp['vlans']:
+ self.assertIn('vlan', vlan)
+ configured_vlan_ids.append(str(vlan['vlan']))
+
+ # Verify native VLAN ID has 'PVID' flag set on individual member ports
+ if not interface.startswith('br') and str(vlan['vlan']) == native_vlan:
+ self.assertIn('flags', vlan)
+ self.assertIn('PVID', vlan['flags'])
+
+ self.assertListEqual(sorted(configured_vlan_ids), sorted(vifs))
+
+ # Verify correct setting of VLAN filter function
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/bridge/vlan_filtering')
+ self.assertEqual(tmp, '1')
+
+ # Obtain status information and verify proper VLAN filter setup.
+ # First check if all members are present, second check if all VLANs
+ # are assigned on the parend bridge interface, third verify all the
+ # VLANs are properly setup on the downstream "member" ports
+ for interface in self._interfaces:
+ # check member interfaces are added on the bridge
+ _verify_members(interface, self._members)
+
+ # Check if all VLAN ids are properly set up. Bridge interface always
+ # has native VLAN 1
+ tmp = deepcopy(vifs)
+ tmp.append('1')
+ _check_vlan_filter(interface, tmp)
+
+ for member in self._members:
+ _check_vlan_filter(member, vifs)
+
+ # change member interface description to trigger config update,
+ # VLANs must still exist (T4565)
+ for interface in self._interfaces:
+ for member in self._members:
+ self.cli_set(['interfaces', Section.section(member), member, 'description', f'foo {member}'])
+
+ # commit config
+ self.cli_commit()
+
+ # Obtain status information and verify proper VLAN filter setup.
+ # First check if all members are present, second check if all VLANs
+ # are assigned on the parend bridge interface, third verify all the
+ # VLANs are properly setup on the downstream "member" ports
+ for interface in self._interfaces:
+ # check member interfaces are added on the bridge
+ _verify_members(interface, self._members)
+
+ # Check if all VLAN ids are properly set up. Bridge interface always
+ # has native VLAN 1
+ tmp = deepcopy(vifs)
+ tmp.append('1')
+ _check_vlan_filter(interface, tmp)
+
+ for member in self._members:
+ _check_vlan_filter(member, vifs)
+
+ # delete all members
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'member'])
+
+ # commit config
+ self.cli_commit()
+
+ # verify member interfaces are no longer assigned on the bridge
+ for interface in self._interfaces:
+ bridge_members = []
+ for tmp in glob(f'/sys/class/net/{interface}/lower_*'):
+ bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+
+ self.assertNotEqual(len(self._members), len(bridge_members))
+ for member in self._members:
+ self.assertNotIn(member, bridge_members)
+
+ def test_bridge_vif_members(self):
+ # T2945: ensure that VIFs are not dropped from bridge
+ vifs = ['300', '400']
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif in vifs:
+ self.cli_set(['interfaces', 'ethernet', member, 'vif', vif])
+ self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}'])
+
+ self.cli_commit()
+
+ # Verify config
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif in vifs:
+ # member interface must be assigned to the bridge
+ self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif}'))
+
+ # delete all members
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif in vifs:
+ self.cli_delete(['interfaces', 'ethernet', member, 'vif', vif])
+ self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}'])
+
+ def test_bridge_vif_s_vif_c_members(self):
+ # T2945: ensure that VIFs are not dropped from bridge
+ vifs = ['300', '400']
+ vifc = ['301', '401']
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ for vif_c in vifc:
+ self.cli_set(['interfaces', 'ethernet', member, 'vif-s', vif_s, 'vif-c', vif_c])
+ self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}'])
+
+ self.cli_commit()
+
+ # Verify config
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ for vif_c in vifc:
+ # member interface must be assigned to the bridge
+ self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif_s}.{vif_c}'))
+
+ # delete all members
+ for interface in self._interfaces:
+ for member in self._members:
+ for vif_s in vifs:
+ self.cli_delete(['interfaces', 'ethernet', member, 'vif-s', vif_s])
+ for vif_c in vifc:
+ self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}'])
+
+ def test_bridge_tunnel_vxlan_multicast(self):
+ # Testcase for T6043 running VXLAN over gretap
+ br_if = 'br0'
+ tunnel_if = 'tun0'
+ eth_if = 'eth1'
+ vxlan_if = 'vxlan0'
+ multicast_group = '239.0.0.241'
+ vni = '123'
+ eth0_addr = '192.0.2.2/30'
+
+ self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', eth_if])
+ self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vxlan_if])
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr])
+
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'address', '10.0.0.2/24'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'enable-multicast'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'encapsulation', 'gretap'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'mtu', '1500'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'ignore-df'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'key', '1'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'no-pmtu-discovery'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'ttl', '0'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'remote', '203.0.113.2'])
+ self.cli_set(['interfaces', 'tunnel', tunnel_if, 'source-address', ip_from_cidr(eth0_addr)])
+
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'group', multicast_group])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'mtu', '1426'])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'source-interface', tunnel_if])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'vni', vni])
+
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(eth_if))
+ self.assertTrue(interface_exists(vxlan_if))
+ self.assertTrue(interface_exists(tunnel_if))
+
+ tmp = get_interface_config(vxlan_if)
+ self.assertEqual(tmp['ifname'], vxlan_if)
+ self.assertEqual(tmp['linkinfo']['info_data']['link'], tunnel_if)
+ self.assertEqual(tmp['linkinfo']['info_data']['group'], multicast_group)
+ self.assertEqual(tmp['linkinfo']['info_data']['id'], int(vni))
+
+ bridge_members = []
+ for tmp in glob(f'/sys/class/net/{br_if}/lower_*'):
+ bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+ self.assertIn(eth_if, bridge_members)
+ self.assertIn(vxlan_if, bridge_members)
+
+ self.cli_delete(['interfaces', 'bridge', br_if])
+ self.cli_delete(['interfaces', 'vxlan', vxlan_if])
+ self.cli_delete(['interfaces', 'tunnel', tunnel_if])
+ self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr])
+
+ def test_bridge_vlan_protocol(self):
+ protocol = '802.1ad'
+
+ # Add member interface to bridge and set VLAN filter
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'protocol', protocol])
+
+ # commit config
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ tmp = get_interface_config(interface)
+ self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol'])
+
+ def test_bridge_delete_with_vxlan_heighbor_suppress(self):
+ vxlan_if = 'vxlan0'
+ vni = '123'
+ br_if = 'br0'
+ eth0_addr = '192.0.2.2/30'
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress'])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'mtu', '1426'])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'source-address', ip_from_cidr(eth0_addr)])
+ self.cli_set(['interfaces', 'vxlan', vxlan_if, 'vni', vni])
+
+ self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vxlan_if])
+
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(vxlan_if))
+ self.assertTrue(interface_exists(br_if))
+
+ # cannot delete bridge interface if "neighbor-suppress" parameter is configured for VXLAN interface
+ self.cli_delete(['interfaces', 'bridge', br_if])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(['interfaces', 'vxlan', vxlan_if, 'parameters', 'neighbor-suppress'])
+
+ self.cli_commit()
+
+ self.assertFalse(interface_exists(br_if))
+
+ self.cli_delete(['interfaces', 'vxlan', vxlan_if])
+ self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr])
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py
new file mode 100644
index 0000000..d96ec2c
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_dummy.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2021 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+
+class DummyInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'dummy']
+ cls._interfaces = ['dum435', 'dum8677', 'dum0931', 'dum089']
+ # call base-classes classmethod
+ super(DummyInterfaceTest, cls).setUpClass()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
new file mode 100644
index 0000000..3d12364
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from glob import glob
+from json import loads
+
+from netifaces import AF_INET
+from netifaces import AF_INET6
+from netifaces import ifaddresses
+
+from base_interfaces_test import BasicInterfaceTest
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import cmd
+from vyos.utils.process import popen
+from vyos.utils.file import read_file
+from vyos.utils.network import is_ipv6_link_local
+
+class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'ethernet']
+ cls._mirror_interfaces = ['dum21354']
+
+ # We only test on physical interfaces and not VLAN (sub-)interfaces
+ if 'TEST_ETH' in os.environ:
+ tmp = os.environ['TEST_ETH'].split()
+ cls._interfaces = tmp
+ else:
+ for tmp in Section.interfaces('ethernet', vlan=False):
+ cls._interfaces.append(tmp)
+
+ cls._macs = {}
+ for interface in cls._interfaces:
+ cls._macs[interface] = read_file(f'/sys/class/net/{interface}/address')
+
+ # call base-classes classmethod
+ super(EthernetInterfaceTest, cls).setUpClass()
+
+ def tearDown(self):
+ for interface in self._interfaces:
+ # when using a dedicated interface to test via TEST_ETH environment
+ # variable only this one will be cleared in the end - usable to test
+ # ethernet interfaces via SSH
+ self.cli_delete(self._base_path + [interface])
+ self.cli_set(self._base_path + [interface, 'duplex', 'auto'])
+ self.cli_set(self._base_path + [interface, 'speed', 'auto'])
+ self.cli_set(self._base_path + [interface, 'hw-id', self._macs[interface]])
+
+ self.cli_commit()
+
+ # Verify that no address remains on the system as this is an eternal
+ # interface.
+ for interface in self._interfaces:
+ self.assertNotIn(AF_INET, ifaddresses(interface))
+ # required for IPv6 link-local address
+ self.assertIn(AF_INET6, ifaddresses(interface))
+ for addr in ifaddresses(interface)[AF_INET6]:
+ # checking link local addresses makes no sense
+ if is_ipv6_link_local(addr['addr']):
+ continue
+ self.assertFalse(is_intf_addr_assigned(interface, addr['addr']))
+ # Ensure no VLAN interfaces are left behind
+ tmp = [x for x in Section.interfaces('ethernet') if x.startswith(f'{interface}.')]
+ self.assertListEqual(tmp, [])
+
+ def test_offloading_rps(self):
+ # enable RPS on all available CPUs, RPS works with a CPU bitmask,
+ # where each bit represents a CPU (core/thread). The formula below
+ # expands to rps_cpus = 255 for a 8 core system
+ rps_cpus = (1 << os.cpu_count()) -1
+
+ # XXX: we should probably reserve one core when the system is under
+ # high preasure so we can still have a core left for housekeeping.
+ # This is done by masking out the lowst bit so CPU0 is spared from
+ # receive packet steering.
+ rps_cpus &= ~1
+
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'offload', 'rps'])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ cpus = read_file(f'/sys/class/net/{interface}/queues/rx-0/rps_cpus')
+ # remove the nasty ',' separation on larger strings
+ cpus = cpus.replace(',','')
+ cpus = int(cpus, 16)
+
+ self.assertEqual(f'{cpus:x}', f'{rps_cpus:x}')
+
+ def test_offloading_rfs(self):
+ global_rfs_flow = 32768
+ rfs_flow = global_rfs_flow
+
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'offload', 'rfs'])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*'))
+ rfs_flow = int(global_rfs_flow/queues)
+ for i in range(0, queues):
+ tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt')
+ self.assertEqual(int(tmp), rfs_flow)
+
+ tmp = read_file(f'/proc/sys/net/core/rps_sock_flow_entries')
+ self.assertEqual(int(tmp), global_rfs_flow)
+
+ # delete configuration of RFS and check all values returned to default "0"
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'offload', 'rfs'])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ queues = len(glob(f'/sys/class/net/{interface}/queues/rx-*'))
+ rfs_flow = int(global_rfs_flow/queues)
+ for i in range(0, queues):
+ tmp = read_file(f'/sys/class/net/{interface}/queues/rx-{i}/rps_flow_cnt')
+ self.assertEqual(int(tmp), 0)
+
+
+ def test_non_existing_interface(self):
+ unknonw_interface = self._base_path + ['eth667']
+ self.cli_set(unknonw_interface)
+
+ # check validate() - interface does not exist
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # we need to remove this wrong interface from the configuration
+ # manually, else tearDown() will have problem in commit()
+ self.cli_delete(unknonw_interface)
+
+ def test_speed_duplex_verify(self):
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'speed', '1000'])
+
+ # check validate() - if either speed or duplex is not auto, the
+ # other one must be manually configured, too
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'speed', 'auto'])
+ self.cli_commit()
+
+ def test_ethtool_ring_buffer(self):
+ for interface in self._interfaces:
+ # We do not use vyos.ethtool here to not have any chance
+ # for invalid testcases. Re-gain data by hand
+ tmp = cmd(f'sudo ethtool --json --show-ring {interface}')
+ tmp = loads(tmp)
+ max_rx = str(tmp[0]['rx-max'])
+ max_tx = str(tmp[0]['tx-max'])
+
+ self.cli_set(self._base_path + [interface, 'ring-buffer', 'rx', max_rx])
+ self.cli_set(self._base_path + [interface, 'ring-buffer', 'tx', max_tx])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ tmp = cmd(f'sudo ethtool --json --show-ring {interface}')
+ tmp = loads(tmp)
+ max_rx = str(tmp[0]['rx-max'])
+ max_tx = str(tmp[0]['tx-max'])
+ rx = str(tmp[0]['rx'])
+ tx = str(tmp[0]['tx'])
+
+ # validate if the above change was carried out properly and the
+ # ring-buffer size got increased
+ self.assertEqual(max_rx, rx)
+ self.assertEqual(max_tx, tx)
+
+ def test_ethtool_flow_control(self):
+ for interface in self._interfaces:
+ # Disable flow-control
+ self.cli_set(self._base_path + [interface, 'disable-flow-control'])
+ # Check current flow-control state on ethernet interface
+ out, err = popen(f'sudo ethtool --json --show-pause {interface}')
+ # Flow-control not supported - test if it bails out with a proper
+ # this is a dynamic path where err = 1 on VMware, but err = 0 on
+ # a physical box.
+ if bool(err):
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ else:
+ out = loads(out)
+ # Flow control is on
+ self.assertTrue(out[0]['autonegotiate'])
+
+ # commit change on CLI to disable-flow-control and re-test
+ self.cli_commit()
+
+ out, err = popen(f'sudo ethtool --json --show-pause {interface}')
+ out = loads(out)
+ self.assertFalse(out[0]['autonegotiate'])
+
+ def test_ethtool_evpn_uplink_tarcking(self):
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'evpn', 'uplink'])
+
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra')
+ self.assertIn(f' evpn mh uplink', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py
new file mode 100644
index 0000000..5f8fae9
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_geneve.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from vyos.ifconfig import Interface
+from vyos.utils.network import get_interface_config
+
+from base_interfaces_test import BasicInterfaceTest
+
+class GeneveInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'geneve']
+ cls._options = {
+ 'gnv0': ['vni 10', 'remote 127.0.1.1'],
+ 'gnv1': ['vni 20', 'remote 127.0.1.2'],
+ 'gnv1': ['vni 30', 'remote 2001:db8::1', 'parameters ipv6 flowlabel 0x1000'],
+ }
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(GeneveInterfaceTest, cls).setUpClass()
+
+ def test_geneve_parameters(self):
+ tos = '40'
+ ttl = 20
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'df', 'set'])
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'tos', tos])
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'innerproto'])
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'ttl', str(ttl)])
+ ttl += 10
+
+ self.cli_commit()
+
+ ttl = 20
+ for interface in self._interfaces:
+ options = get_interface_config(interface)
+
+ vni = options['linkinfo']['info_data']['id']
+ self.assertIn(f'vni {vni}', self._options[interface])
+
+ if any('remote' in s for s in self._options[interface]):
+ key = 'remote'
+ if 'remote6' in options['linkinfo']['info_data']:
+ key = 'remote6'
+
+ remote = options['linkinfo']['info_data'][key]
+ self.assertIn(f'remote {remote}', self._options[interface])
+
+ if any('flowlabel' in s for s in self._options[interface]):
+ label = options['linkinfo']['info_data']['label']
+ self.assertIn(f'parameters ipv6 flowlabel {label}', self._options[interface])
+
+ if any('innerproto' in s for s in self._options[interface]):
+ inner = options['linkinfo']['info_data']['innerproto']
+ self.assertIn(f'parameters ip {inner}', self._options[interface])
+
+
+ self.assertEqual('geneve', options['linkinfo']['info_kind'])
+ self.assertEqual('set', options['linkinfo']['info_data']['df'])
+ self.assertEqual(f'0x{tos}', options['linkinfo']['info_data']['tos'])
+ self.assertEqual(ttl, options['linkinfo']['info_data']['ttl'])
+ self.assertEqual(Interface(interface).get_admin_state(), 'up')
+ ttl += 10
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_input.py b/smoketest/scripts/cli/test_interfaces_input.py
new file mode 100644
index 0000000..3ddf860
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_input.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from vyos.utils.file import read_file
+from vyos.ifconfig import Interface
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+base_path = ['interfaces', 'input']
+
+# add a classmethod to setup a temporaray PPPoE server for "proper" validation
+class InputInterfaceTest(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(InputInterfaceTest, cls).setUpClass()
+ cls._interfaces = ['ifb10', 'ifb20', 'ifb30']
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_01_description(self):
+ # Check if PPPoE dialer can be configured and runs
+ for interface in self._interfaces:
+ self.cli_set(base_path + [interface, 'description', f'foo-{interface}'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate remove interface description "empty"
+ for interface in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{interface}/ifalias')
+ self.assertEqual(tmp, f'foo-{interface}')
+ self.assertEqual(Interface(interface).get_alias(), f'foo-{interface}')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py
new file mode 100644
index 0000000..2816573
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+from vyos.utils.process import cmd
+from vyos.utils.kernel import unload_kmod
+class L2TPv3InterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'l2tpv3']
+ cls._options = {
+ 'l2tpeth10': ['source-address 127.0.0.1', 'remote 127.10.10.10',
+ 'tunnel-id 100', 'peer-tunnel-id 10',
+ 'session-id 100', 'peer-session-id 10',
+ 'source-port 1010', 'destination-port 10101'],
+ 'l2tpeth20': ['source-address 127.0.0.1', 'peer-session-id 20',
+ 'peer-tunnel-id 200', 'remote 127.20.20.20',
+ 'session-id 20', 'tunnel-id 200',
+ 'source-port 2020', 'destination-port 20202'],
+ }
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(L2TPv3InterfaceTest, cls).setUpClass()
+
+ def test_add_single_ip_address(self):
+ super().test_add_single_ip_address()
+
+ command = 'sudo ip -j l2tp show session'
+ json_out = json.loads(cmd(command))
+ for interface in self._options:
+ for config in json_out:
+ if config['interface'] == interface:
+ # convert list with configuration items into a dict
+ dict = {}
+ for opt in self._options[interface]:
+ dict.update({opt.split()[0].replace('-','_'): opt.split()[1]})
+
+ for key in ['peer_session_id', 'peer_tunnel_id',
+ 'session_id', 'tunnel_id']:
+ self.assertEqual(str(config[key]), dict[key])
+
+
+if __name__ == '__main__':
+ # when re-running this test, cleanup loaded modules first so they are
+ # reloaded on demand - not needed but test more and more features
+ for module in ['l2tp_ip6', 'l2tp_ip', 'l2tp_eth', 'l2tp_eth',
+ 'l2tp_netlink', 'l2tp_core']:
+ unload_kmod(module)
+
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py
new file mode 100644
index 0000000..0454dc6
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_loopback.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+from netifaces import interfaces
+
+from vyos.utils.network import is_intf_addr_assigned
+
+loopbacks = ['127.0.0.1', '::1']
+
+class LoopbackInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'loopback']
+ cls._interfaces = ['lo']
+ # call base-classes classmethod
+ super(LoopbackInterfaceTest, cls).setUpClass()
+
+ # we need to override tearDown() as loopback interfaces are ephemeral and
+ # will always be present on the system - the base class check will fail as
+ # the loopback interface will still exist.
+ def tearDown(self):
+ self.cli_delete(self._base_path)
+ self.cli_commit()
+
+ # loopback interface must persist!
+ for intf in self._interfaces:
+ self.assertIn(intf, interfaces())
+
+ def test_add_single_ip_address(self):
+ super().test_add_single_ip_address()
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned('lo', addr))
+
+ def test_add_multiple_ip_addresses(self):
+ super().test_add_multiple_ip_addresses()
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned('lo', addr))
+
+ def test_interface_disable(self):
+ self.skipTest('not supported')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py
new file mode 100644
index 0000000..d73895b
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_macsec.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.file import read_file
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import interface_exists
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'wpa_supplicant'
+
+def get_config_value(interface, key):
+ tmp = read_file(f'/run/wpa_supplicant/{interface}.conf')
+ tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp)
+ return tmp[0]
+
+class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'macsec']
+ cls._options = { 'macsec0': ['source-interface eth0', 'security cipher gcm-aes-128'] }
+
+ # if we have a physical eth1 interface, add a second macsec instance
+ if 'eth1' in Section.interfaces('ethernet'):
+ macsec = { 'macsec1': [f'source-interface eth1', 'security cipher gcm-aes-128'] }
+ cls._options.update(macsec)
+
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(MACsecInterfaceTest, cls).setUpClass()
+
+ def tearDown(self):
+ super().tearDown()
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_macsec_encryption(self):
+ # MACsec can be operating in authentication and encryption mode - both
+ # using different mandatory settings, lets test encryption as the basic
+ # authentication test has been performed using the base class tests
+
+ mak_cak = '232e44b7fda6f8e2d88a07bf78a7aff4'
+ mak_ckn = '40916f4b23e3d548ad27eedd2d10c6f98c2d21684699647d63d41b500dfe8836'
+ replay_window = '64'
+
+ for interface, option_value in self._options.items():
+ for option in option_value:
+ if option.split()[0] == 'source-interface':
+ src_interface = option.split()[1]
+
+ self.cli_set(self._base_path + [interface] + option.split())
+
+ # Encrypt link
+ self.cli_set(self._base_path + [interface, 'security', 'encrypt'])
+
+ # check validate() - Physical source interface MTU must be higher then our MTU
+ self.cli_set(self._base_path + [interface, 'mtu', '1500'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'mtu'])
+
+ # check validate() - MACsec security keys mandartory when encryption is enabled
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'mka', 'cak', mak_cak])
+
+ # check validate() - MACsec security keys mandartory when encryption is enabled
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'mka', 'ckn', mak_ckn])
+
+ self.cli_set(self._base_path + [interface, 'security', 'replay-window', replay_window])
+
+ # final commit of settings
+ self.cli_commit()
+
+ tmp = get_config_value(src_interface, 'macsec_integ_only')
+ self.assertIn("0", tmp)
+
+ tmp = get_config_value(src_interface, 'mka_cak')
+ self.assertIn(mak_cak, tmp)
+
+ tmp = get_config_value(src_interface, 'mka_ckn')
+ self.assertIn(mak_ckn, tmp)
+
+ # check that the default priority of 255 is programmed
+ tmp = get_config_value(src_interface, 'mka_priority')
+ self.assertIn("255", tmp)
+
+ tmp = get_config_value(src_interface, 'macsec_replay_window')
+ self.assertIn(replay_window, tmp)
+
+ tmp = read_file(f'/sys/class/net/{interface}/mtu')
+ self.assertEqual(tmp, '1460')
+
+ # Encryption enabled?
+ tmp = get_interface_config(interface)
+ self.assertTrue(tmp['linkinfo']['info_data']['encrypt'])
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_macsec_gcm_aes_128(self):
+ src_interface = 'eth0'
+ interface = 'macsec1'
+ cipher = 'gcm-aes-128'
+ self.cli_set(self._base_path + [interface])
+
+ # check validate() - source interface is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'source-interface', src_interface])
+
+ # check validate() - cipher is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher])
+
+ # final commit and verify
+ self.cli_commit()
+ self.assertTrue(interface_exists(interface))
+
+ # Verify proper cipher suite (T4537)
+ tmp = get_interface_config(interface)
+ self.assertEqual(cipher, tmp['linkinfo']['info_data']['cipher_suite'].lower())
+
+ def test_macsec_gcm_aes_256(self):
+ src_interface = 'eth0'
+ interface = 'macsec4'
+ cipher = 'gcm-aes-256'
+ self.cli_set(self._base_path + [interface])
+
+ # check validate() - source interface is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'source-interface', src_interface])
+
+ # check validate() - cipher is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher])
+
+ # final commit and verify
+ self.cli_commit()
+ self.assertTrue(interface_exists(interface))
+
+ # Verify proper cipher suite (T4537)
+ tmp = get_interface_config(interface)
+ self.assertEqual(cipher, tmp['linkinfo']['info_data']['cipher_suite'].lower())
+
+ def test_macsec_source_interface(self):
+ # Ensure source-interface can bot be part of any other bond or bridge
+ base_bridge = ['interfaces', 'bridge', 'br200']
+ base_bond = ['interfaces', 'bonding', 'bond200']
+
+ for interface, option_value in self._options.items():
+ for option in option_value:
+ self.cli_set(self._base_path + [interface] + option.split())
+ if option.split()[0] == 'source-interface':
+ src_interface = option.split()[1]
+
+ self.cli_set(base_bridge + ['member', 'interface', src_interface])
+ # check validate() - Source interface must not already be a member of a bridge
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_bridge)
+
+ self.cli_set(base_bond + ['member', 'interface', src_interface])
+ # check validate() - Source interface must not already be a member of a bridge
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_bond)
+
+ # final commit and verify
+ self.cli_commit()
+ self.assertTrue(interface_exists(interface))
+
+ def test_macsec_static_keys(self):
+ src_interface = 'eth0'
+ interface = 'macsec5'
+ cipher1 = 'gcm-aes-128'
+ cipher2 = 'gcm-aes-256'
+ tx_key_1 = '71a82a48eddfa12c08a19792ca20c4bb'
+ tx_key_2 = 'dd487b2958e855ea35a5d43a5ecb3dcfbe7889ffcb877770252feb13b734478d'
+ rx_key_1 = '0022d00f57e75241a230cdf7118dfcc5'
+ rx_key_2 = 'b7d6d7ad075e02323fdeb845217b884d3f93ff36b2cdaf6b07eeb189b877245f'
+ peer_mac = '00:11:22:33:44:55'
+ self.cli_set(self._base_path + [interface])
+
+ # Encrypt link
+ self.cli_set(self._base_path + [interface, 'security', 'encrypt'])
+
+ # check validate() - source interface is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'source-interface', src_interface])
+
+ # check validate() - cipher is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher1])
+
+ # check validate() - only static or mka config is allowed
+ self.cli_set(self._base_path + [interface, 'security', 'static'])
+ self.cli_set(self._base_path + [interface, 'security', 'mka', 'cak', tx_key_1])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'security', 'mka'])
+
+ # check validate() - key required
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # check validate() - key length must match cipher
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_2])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_1])
+
+ # check validate() - at least one peer must be defined
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # check validate() - enabled peer must have both key and MAC defined
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac', peer_mac])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac'])
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'key', rx_key_1])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac', peer_mac])
+
+ # check validate() - peer key length must match cipher
+ self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher2])
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_2])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'key', rx_key_2])
+
+ # final commit and verify
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(interface))
+ tmp = get_interface_config(interface)
+ self.assertEqual(cipher2, tmp['linkinfo']['info_data']['cipher_suite'].lower())
+ # Encryption enabled?
+ self.assertTrue(tmp['linkinfo']['info_data']['encrypt'])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
new file mode 100644
index 0000000..e087b87
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -0,0 +1,868 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from glob import glob
+from ipaddress import IPv4Network
+from netifaces import interfaces
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.template import address_from_cidr
+from vyos.template import inc_ip
+from vyos.template import last_host_address
+from vyos.template import netmask_from_cidr
+
+PROCESS_NAME = 'openvpn'
+
+base_path = ['interfaces', 'openvpn']
+
+cert_data = """
+MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw
+WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx
+MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV
+BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP
+UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
+01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3
+QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
+BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu
++JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz
+ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93
++dm/LDnp7C0=
+"""
+
+key_data = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
+2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7
+u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
+"""
+
+dh_data = """
+MIIBCAKCAQEApzGAPcQlLJiOyfGZgl1qxNgufXkdpjG7lMaOrO4TGr1giFe3jIFO
+FxJNC/G9Dn+KSukaWssVVR+Jwr/JesZFPawihS03wC7cZsccykNRIjiteqJDwYJZ
+UHieOxyCuCeY4pqOUCl1uswRGjLvIFtwynpnXKKuz2YtjNifma90PEgv/vVWKix+
+Q0TAbdbzJzO5xp8UVn9DuYfSr10k3LbDqDM7w5ezHZxFk24S5pN/yoOpdbxB8TS6
+7q3IYXxR3F+RseKu4J3AvkxXSP1j7COXddPpLnvbJT/SW8NrjuC/n0eKGvmeyqNv
+108Y89jnT79MxMMRQk66iwlsd1m4pa/OYwIBAg==
+"""
+
+ovpn_key_data = """
+443f2a710ac411c36894b2531e62c4550b079b8f3f08997f4be57c64abfdaaa4
+31d2396b01ecec3a2c0618959e8186d99f489742d25673ffb3268841ebb2e704
+2a2daabe584e79d51d2b1d7409bf8840f7e42efa3e660a521719b04ee88b9043
+e6315ae12da7c9abd55f67eeed71a9ee8c6e163b5d2661fc332cf90cb45658b4
+adf892f79537d37d3a3d90da283ce885adf325ffd2b5be92067cdf0345c7712c
+9d36b642c170351b6d9ce9f6230c7a2617b0c181121bce7d5373404fb68e6521
+0b36e6d40ef2769cf8990503859f6f2db3c85ba74420430a6250d6a74ca51ece
+4b85124bfdfec0c8a530cefa7350378d81a4539f74bed832a902ae4798142e4a
+"""
+
+remote_port = '1194'
+protocol = 'udp'
+path = []
+interface = ''
+remote_host = ''
+vrf_name = 'orange'
+dummy_if = 'dum1301'
+
+def get_vrf(interface):
+ for upper in glob(f'/sys/class/net/{interface}/upper*'):
+ # an upper interface could be named: upper_bond0.1000.1100, thus
+ # we need top drop the upper_ prefix
+ tmp = os.path.basename(upper)
+ tmp = tmp.replace('upper_', '')
+ return tmp
+
+class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestInterfacesOpenVPN, cls).setUpClass()
+
+ cls.cli_set(cls, ['interfaces', 'dummy', dummy_if, 'address', '192.0.2.1/32'])
+ cls.cli_set(cls, ['vrf', 'name', vrf_name, 'table', '12345'])
+
+ cls.cli_set(cls, ['pki', 'ca', 'ovpn_test', 'certificate', cert_data.replace('\n','')])
+ cls.cli_set(cls, ['pki', 'certificate', 'ovpn_test', 'certificate', cert_data.replace('\n','')])
+ cls.cli_set(cls, ['pki', 'certificate', 'ovpn_test', 'private', 'key', key_data.replace('\n','')])
+ cls.cli_set(cls, ['pki', 'dh', 'ovpn_test', 'parameters', dh_data.replace('\n','')])
+ cls.cli_set(cls, ['pki', 'openvpn', 'shared-secret', 'ovpn_test', 'key', ovpn_key_data.replace('\n','')])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if])
+ cls.cli_delete(cls, ['vrf'])
+
+ super(TestInterfacesOpenVPN, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_openvpn_client_verify(self):
+ # Create OpenVPN client interface and test verify() steps.
+ interface = 'vtun2000'
+ path = base_path + [interface]
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm'])
+
+ # check validate() - cannot specify local-port in client mode
+ self.cli_set(path + ['local-port', '5000'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['local-port'])
+
+ # check validate() - cannot specify local-host in client mode
+ self.cli_set(path + ['local-host', '127.0.0.1'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['local-host'])
+
+ # check validate() - cannot specify protocol tcp-passive in client mode
+ self.cli_set(path + ['protocol', 'tcp-passive'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['protocol'])
+
+ # check validate() - remote-host must be set in client mode
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['remote-host', '192.0.9.9'])
+
+ # check validate() - cannot specify "tls dh-params" in client mode
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['tls'])
+
+ # check validate() - must specify one of "shared-secret-key" and "tls"
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+
+ # check validate() - must specify one of "shared-secret-key" and "tls"
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['shared-secret-key', 'ovpn_test'])
+
+ # check validate() - cannot specify "encryption cipher" in client mode
+ self.cli_set(path + ['encryption', 'cipher', 'aes192gcm'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['encryption', 'cipher'])
+
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+
+ # check validate() - can not have auth username without a password
+ self.cli_set(path + ['authentication', 'username', 'vyos'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['authentication', 'password', 'vyos'])
+
+ # client commit must pass
+ self.cli_commit()
+
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertIn(interface, interfaces())
+
+
+ def test_openvpn_client_interfaces(self):
+ # Create OpenVPN client interfaces connecting to different
+ # server IP addresses. Validate configuration afterwards.
+ num_range = range(10, 15)
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ remote_host = f'192.0.2.{ii}'
+ path = base_path + [interface]
+ auth_hash = 'sha1'
+
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['persistent-tunnel'])
+ self.cli_set(path + ['protocol', protocol])
+ self.cli_set(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-port', remote_port])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['vrf', vrf_name])
+ self.cli_set(path + ['authentication', 'username', interface+'user'])
+ self.cli_set(path + ['authentication', 'password', interface+'secretpw'])
+
+ self.cli_commit()
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ remote_host = f'192.0.2.{ii}'
+ config_file = f'/run/openvpn/{interface}.conf'
+ pw_file = f'/run/openvpn/{interface}.pw'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto {protocol}', config)
+ self.assertIn(f'rport {remote_port}', config)
+ self.assertIn(f'remote {remote_host}', config)
+ self.assertIn(f'persist-tun', config)
+ self.assertIn(f'auth {auth_hash}', config)
+ self.assertIn(f'data-ciphers AES-256-CBC', config)
+
+ # TLS options
+ self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config)
+ self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config)
+ self.assertIn(f'key /run/openvpn/{interface}_cert.key', config)
+
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertEqual(get_vrf(interface), vrf_name)
+ self.assertIn(interface, interfaces())
+
+ pw = cmd(f'sudo cat {pw_file}')
+ self.assertIn(f'{interface}user', pw)
+ self.assertIn(f'{interface}secretpw', pw)
+
+ # check that no interface remained after deleting them
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ self.assertNotIn(interface, interfaces())
+
+ def test_openvpn_client_ip_version(self):
+ # Test the client mode behavior combined with different IP protocol versions
+
+ interface = 'vtun10'
+ remote_host = '192.0.2.10'
+ remote_host_v6 = 'fd00::2:10'
+ path = base_path + [interface]
+ auth_hash = 'sha1'
+
+ # Default behavior: client uses uspecified protocol version (udp)
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['persistent-tunnel'])
+ self.cli_set(path + ['protocol', 'udp'])
+ self.cli_set(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-port', remote_port])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['vrf', vrf_name])
+ self.cli_set(path + ['authentication', 'username', interface+'user'])
+ self.cli_set(path + ['authentication', 'password', interface+'secretpw'])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev vtun10', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config)
+ self.assertIn(f'rport {remote_port}', config)
+ self.assertIn(f'remote {remote_host}', config)
+ self.assertIn(f'persist-tun', config)
+
+ # IPv4 only: client usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: client uses udp6 protocol
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_delete(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-host', remote_host_v6])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+
+ # IPv6 dual-stack: not allowed in client mode
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_openvpn_server_verify(self):
+ # Create one OpenVPN server interface and check required verify() stages
+ interface = 'vtun5000'
+ path = base_path + [interface]
+
+ # check validate() - must speciy operating mode
+ self.cli_set(path)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['mode', 'server'])
+
+ # check validate() - cannot specify protocol tcp-active in server mode
+ self.cli_set(path + ['protocol', 'tcp-active'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['protocol'])
+
+ # check validate() - cannot specify local-port in client mode
+ self.cli_set(path + ['remote-port', '5000'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['remote-port'])
+
+ # check validate() - cannot specify local-host in client mode
+ self.cli_set(path + ['remote-host', '127.0.0.1'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['remote-host'])
+
+ # check validate() - must specify "tls dh-params" when not using EC keys
+ # in server mode
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+
+ # check validate() - must specify "server subnet" or add interface to
+ # bridge in server mode
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # check validate() - server client-ip-pool is too large
+ # [100.64.0.4 -> 100.127.255.251 = 4194295], maximum is 65536 addresses.
+ self.cli_set(path + ['server', 'subnet', '100.64.0.0/10'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # check validate() - cannot specify more than 1 IPv4 and 1 IPv6 server subnet
+ self.cli_set(path + ['server', 'subnet', '100.64.0.0/20'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['server', 'subnet', '100.64.0.0/10'])
+
+ # check validate() - must specify "tls ca-certificate"
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+
+ # check validate() - must specify "tls certificate"
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+
+ # check validate() - cannot specify "tls role" in client-server mode'
+ self.cli_set(path + ['tls', 'role', 'active'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # check validate() - cannot specify "tls role" in client-server mode'
+ self.cli_set(path + ['tls', 'auth-key', 'ovpn_test'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # check validate() - cannot specify "tcp-passive" when "tls role" is "active"
+ self.cli_set(path + ['protocol', 'tcp-passive'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['protocol'])
+
+ # check validate() - cannot specify "tls dh-params" when "tls role" is "active"
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['tls', 'dh-params'])
+
+ # check validate() - cannot specify "encryption cipher" in server mode
+ self.cli_set(path + ['encryption', 'cipher', 'aes256'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['encryption', 'cipher'])
+
+ # Now test the other path with tls role passive
+ self.cli_set(path + ['tls', 'role', 'passive'])
+ # check validate() - cannot specify "tcp-active" when "tls role" is "passive"
+ self.cli_set(path + ['protocol', 'tcp-active'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['protocol'])
+
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+
+ self.cli_commit()
+
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertIn(interface, interfaces())
+
+ def test_openvpn_server_subnet_topology(self):
+ # Create OpenVPN server interfaces using different client subnets.
+ # Validate configuration afterwards.
+
+ auth_hash = 'sha256'
+ num_range = range(20, 25)
+ port = ''
+ client1_routes = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ subnet = f'192.0.{ii}.0/24'
+ client_ip = inc_ip(subnet, '5')
+ path = base_path + [interface]
+ port = str(2000 + ii)
+
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'server'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['server', 'mfa', 'totp'])
+ self.cli_set(path + ['server', 'subnet', subnet])
+ self.cli_set(path + ['server', 'topology', 'subnet'])
+ self.cli_set(path + ['keep-alive', 'failure-count', '5'])
+ self.cli_set(path + ['keep-alive', 'interval', '5'])
+
+ # clients
+ self.cli_set(path + ['server', 'client', 'client1', 'ip', client_ip])
+ for route in client1_routes:
+ self.cli_set(path + ['server', 'client', 'client1', 'subnet', route])
+
+ self.cli_set(path + ['replace-default-route'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+ self.cli_set(path + ['vrf', vrf_name])
+
+ self.cli_commit()
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ plugin = f'plugin "/usr/lib/openvpn/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{interface}-otp-secrets otp_slop=180 totp_t0=0 totp_step=30 totp_digits=6 password_is_cr=1"'
+ subnet = f'192.0.{ii}.0/24'
+
+ start_addr = inc_ip(subnet, '2')
+ stop_addr = last_host_address(subnet)
+
+ client_ip = inc_ip(subnet, '5')
+ client_netmask = netmask_from_cidr(subnet)
+
+ port = str(2000 + ii)
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ client_config_file = f'/run/openvpn/ccd/{interface}/client1'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config) # default protocol
+ self.assertIn(f'auth {auth_hash}', config)
+ self.assertIn(f'data-ciphers AES-192-CBC', config)
+ self.assertIn(f'topology subnet', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'push "redirect-gateway def1"', config)
+ self.assertIn(f'{plugin}', config)
+ self.assertIn(f'keepalive 5 25', config)
+
+ # TLS options
+ self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config)
+ self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config)
+ self.assertIn(f'key /run/openvpn/{interface}_cert.key', config)
+ self.assertIn(f'dh /run/openvpn/{interface}_dh.pem', config)
+
+ # IP pool configuration
+ netmask = IPv4Network(subnet).netmask
+ network = IPv4Network(subnet).network_address
+ self.assertIn(f'server {network} {netmask}', config)
+
+ # Verify client
+ client_config = read_file(client_config_file)
+
+ self.assertIn(f'ifconfig-push {client_ip} {client_netmask}', client_config)
+ for route in client1_routes:
+ self.assertIn('iroute {} {}'.format(address_from_cidr(route), netmask_from_cidr(route)), client_config)
+
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertEqual(get_vrf(interface), vrf_name)
+ self.assertIn(interface, interfaces())
+
+ # check that no interface remained after deleting them
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ self.assertNotIn(interface, interfaces())
+
+ def test_openvpn_server_ip_version(self):
+ # Test the server mode behavior combined with each IP protocol version
+
+ auth_hash = 'sha256'
+ port = '2000'
+
+ interface = 'vtun20'
+ subnet = '192.0.20.0/24'
+ path = base_path + [interface]
+
+ # Default behavior: client uses uspecified protocol version (udp)
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'server'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['server', 'subnet', subnet])
+ self.cli_set(path + ['server', 'topology', 'subnet'])
+
+ self.cli_set(path + ['replace-default-route'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+
+ self.cli_commit()
+
+ start_addr = inc_ip(subnet, '2')
+ stop_addr = last_host_address(subnet)
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config) # default protocol
+ self.assertIn(f'auth {auth_hash}', config)
+ self.assertIn(f'data-ciphers AES-192-CBC', config)
+ self.assertIn(f'topology subnet', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'push "redirect-gateway def1"', config)
+
+ # IPv4 only: server usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: server uses udp6 protocol + bind ipv6only
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertIn(f'bind ipv6only', config)
+
+ # IPv6 dual-stack: server uses udp6 protocol without bind ipv6only
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertNotIn(f'bind ipv6only', config)
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_openvpn_site2site_verify(self):
+ # Create one OpenVPN site2site interface and check required
+ # verify() stages
+
+ interface = 'vtun5000'
+ path = base_path + [interface]
+
+ self.cli_set(path + ['mode', 'site-to-site'])
+
+ # check validate() - cipher negotiation cannot be enabled in site-to-site mode
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['encryption'])
+
+ # check validate() - must specify "local-address" or add interface to bridge
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['local-address', '10.0.0.1'])
+ self.cli_set(path + ['local-address', '2001:db8:1::1'])
+
+ # check validate() - cannot specify more than 1 IPv4 local-address
+ self.cli_set(path + ['local-address', '10.0.0.2'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['local-address', '10.0.0.2'])
+
+ # check validate() - cannot specify more than 1 IPv6 local-address
+ self.cli_set(path + ['local-address', '2001:db8:1::2'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['local-address', '2001:db8:1::2'])
+
+ # check validate() - IPv4 "local-address" requires IPv4 "remote-address"
+ # or IPv4 "local-address subnet"
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['remote-address', '192.168.0.1'])
+ self.cli_set(path + ['remote-address', '2001:db8:ffff::1'])
+
+ # check validate() - Cannot specify more than 1 IPv4 "remote-address"
+ self.cli_set(path + ['remote-address', '192.168.0.2'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['remote-address', '192.168.0.2'])
+
+ # check validate() - Cannot specify more than 1 IPv6 "remote-address"
+ self.cli_set(path + ['remote-address', '2001:db8:ffff::2'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['remote-address', '2001:db8:ffff::2'])
+
+ # check validate() - Must specify one of "shared-secret-key" and "tls"
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+
+ self.cli_commit()
+
+ def test_openvpn_options(self):
+ # Ensure OpenVPN process restart on openvpn-option CLI node change
+
+ interface = 'vtun5001'
+ path = base_path + [interface]
+ encryption_cipher = 'aes256'
+
+ self.cli_set(path + ['mode', 'site-to-site'])
+ self.cli_set(path + ['local-address', '10.0.0.2'])
+ self.cli_set(path + ['remote-address', '192.168.0.3'])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
+
+ self.cli_commit()
+
+ # Now verify the OpenVPN "raw" option passing. Once an openvpn-option is
+ # added, modified or deleted from the CLI, OpenVPN daemon must be restarted
+ cur_pid = process_named_running('openvpn')
+ self.cli_set(path + ['openvpn-option', '--persist-tun'])
+ self.cli_commit()
+
+ # PID must be different as OpenVPN Must be restarted
+ new_pid = process_named_running('openvpn')
+ self.assertNotEqual(cur_pid, new_pid)
+ cur_pid = new_pid
+
+ self.cli_set(path + ['openvpn-option', '--persist-key'])
+ self.cli_commit()
+
+ # PID must be different as OpenVPN Must be restarted
+ new_pid = process_named_running('openvpn')
+ self.assertNotEqual(cur_pid, new_pid)
+ cur_pid = new_pid
+
+ self.cli_delete(path + ['openvpn-option'])
+ self.cli_commit()
+
+ # PID must be different as OpenVPN Must be restarted
+ new_pid = process_named_running('openvpn')
+ self.assertNotEqual(cur_pid, new_pid)
+ cur_pid = new_pid
+
+ def test_openvpn_site2site_interfaces_tun(self):
+ # Create two OpenVPN site-to-site interfaces
+
+ num_range = range(30, 35)
+ port = ''
+ local_address = ''
+ remote_address = ''
+ encryption_cipher = 'aes256'
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ local_address = f'192.0.{ii}.1'
+ local_address_subnet = '255.255.255.252'
+ remote_address = f'172.16.{ii}.1'
+ path = base_path + [interface]
+ port = str(3000 + ii)
+
+ self.cli_set(path + ['local-address', local_address])
+
+ # even numbers use tun type, odd numbers use tap type
+ if ii % 2 == 0:
+ self.cli_set(path + ['device-type', 'tun'])
+ else:
+ self.cli_set(path + ['device-type', 'tap'])
+ self.cli_set(path + ['local-address', local_address, 'subnet-mask', local_address_subnet])
+
+ self.cli_set(path + ['mode', 'site-to-site'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['remote-port', port])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+ self.cli_set(path + ['remote-address', remote_address])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
+ self.cli_set(path + ['vrf', vrf_name])
+
+ self.cli_commit()
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ local_address = f'192.0.{ii}.1'
+ remote_address = f'172.16.{ii}.1'
+ port = str(3000 + ii)
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ # even numbers use tun type, odd numbers use tap type
+ if ii % 2 == 0:
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'ifconfig {local_address} {remote_address}', config)
+ else:
+ self.assertIn(f'dev-type tap', config)
+ self.assertIn(f'ifconfig {local_address} {local_address_subnet}', config)
+
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'rport {port}', config)
+
+
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertEqual(get_vrf(interface), vrf_name)
+ self.assertIn(interface, interfaces())
+
+
+ # check that no interface remained after deleting them
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ for ii in num_range:
+ interface = f'vtun{ii}'
+ self.assertNotIn(interface, interfaces())
+
+
+ def test_openvpn_site2site_ip_version(self):
+ # Test the site-to-site mode behavior combined with each IP protocol version
+
+ encryption_cipher = 'aes256'
+
+ interface = 'vtun30'
+ local_address = '192.0.30.1'
+ local_address_subnet = '255.255.255.252'
+ remote_address = '172.16.30.1'
+ path = base_path + [interface]
+ port = '3030'
+
+ self.cli_set(path + ['local-address', local_address])
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['mode', 'site-to-site'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['remote-port', port])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+ self.cli_set(path + ['remote-address', remote_address])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'ifconfig {local_address} {remote_address}', config)
+ self.assertIn(f'proto udp', config)
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'rport {port}', config)
+
+ # IPv4 only: server usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: server uses udp6 protocol + bind ipv6only
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertIn(f'bind ipv6only', config)
+
+ # IPv6 dual-stack: not allowed in site-to-site mode
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_openvpn_server_server_bridge(self):
+ # Create OpenVPN server interface using bridge.
+ # Validate configuration afterwards.
+ br_if = 'br0'
+ vtun_if = 'vtun5010'
+ auth_hash = 'sha256'
+ path = base_path + [vtun_if]
+ start_subnet = "192.168.0.100"
+ stop_subnet = "192.168.0.200"
+ mask_subnet = "255.255.255.0"
+ gw_subnet = "192.168.0.1"
+
+ self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vtun_if])
+ self.cli_set(path + ['device-type', 'tap'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'server'])
+ self.cli_set(path + ['server', 'bridge', 'gateway', gw_subnet])
+ self.cli_set(path + ['server', 'bridge', 'start', start_subnet])
+ self.cli_set(path + ['server', 'bridge', 'stop', stop_subnet])
+ self.cli_set(path + ['server', 'bridge', 'subnet-mask', mask_subnet])
+ self.cli_set(path + ['keep-alive', 'failure-count', '5'])
+ self.cli_set(path + ['keep-alive', 'interval', '5'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{vtun_if}.conf'
+ config = read_file(config_file)
+ self.assertIn(f'dev {vtun_if}', config)
+ self.assertIn(f'dev-type tap', config)
+ self.assertIn(f'proto udp', config) # default protocol
+ self.assertIn(f'auth {auth_hash}', config)
+ self.assertIn(f'data-ciphers AES-192-CBC', config)
+ self.assertIn(f'mode server', config)
+ self.assertIn(f'server-bridge {gw_subnet} {mask_subnet} {start_subnet} {stop_subnet}', config)
+ self.assertIn(f'keepalive 5 25', config)
+
+ # TLS options
+ self.assertIn(f'ca /run/openvpn/{vtun_if}_ca.pem', config)
+ self.assertIn(f'cert /run/openvpn/{vtun_if}_cert.pem', config)
+ self.assertIn(f'key /run/openvpn/{vtun_if}_cert.key', config)
+ self.assertIn(f'dh /run/openvpn/{vtun_if}_dh.pem', config)
+
+ # check that no interface remained after deleting them
+ self.cli_delete(['interfaces', 'bridge', br_if, 'member', 'interface', vtun_if])
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
new file mode 100644
index 0000000..2683a31
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from psutil import process_iter
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.xml_ref import default_value
+
+config_file = '/etc/ppp/peers/{}'
+base_path = ['interfaces', 'pppoe']
+
+def get_config_value(interface, key):
+ with open(config_file.format(interface), 'r') as f:
+ for line in f:
+ if line.startswith(key):
+ return list(line.split())
+ return []
+
+# add a classmethod to setup a temporaray PPPoE server for "proper" validation
+class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(PPPoEInterfaceTest, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls._interfaces = ['pppoe10', 'pppoe20', 'pppoe30']
+ cls._source_interface = 'eth0'
+
+ def tearDown(self):
+ # Validate PPPoE client process
+ for interface in self._interfaces:
+ running = False
+ for proc in process_iter():
+ if interface in proc.cmdline():
+ running = True
+ break
+ self.assertTrue(running)
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_pppoe_client(self):
+ # Check if PPPoE dialer can be configured and runs
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+ mtu = '1400'
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+ self.cli_set(base_path + [interface, 'mtu', mtu])
+ self.cli_set(base_path + [interface, 'no-peer-dns'])
+
+ # check validate() - a source-interface is required
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration file(s)
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+
+ tmp = get_config_value(interface, 'mtu')[1]
+ self.assertEqual(tmp, mtu)
+ # MRU must default to MTU if not specified on CLI
+ tmp = get_config_value(interface, 'mru')[1]
+ self.assertEqual(tmp, mtu)
+ tmp = get_config_value(interface, 'user')[1].replace('"', '')
+ self.assertEqual(tmp, user)
+ tmp = get_config_value(interface, 'password')[1].replace('"', '')
+ self.assertEqual(tmp, passwd)
+ tmp = get_config_value(interface, 'ifname')[1]
+ self.assertEqual(tmp, interface)
+
+ def test_pppoe_client_disabled_interface(self):
+ # Check if PPPoE Client can be disabled
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+ self.cli_set(base_path + [interface, 'disable'])
+
+ self.cli_commit()
+
+ # Validate PPPoE client process - must not run as interfaces are disabled
+ for interface in self._interfaces:
+ running = False
+ for proc in process_iter():
+ if interface in proc.cmdline():
+ running = True
+ break
+ self.assertFalse(running)
+
+ # enable PPPoE interfaces
+ for interface in self._interfaces:
+ self.cli_delete(base_path + [interface, 'disable'])
+
+ self.cli_commit()
+
+
+ def test_pppoe_authentication(self):
+ # When username or password is set - so must be the other
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+ self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf'])
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ # check validate() - if user is set, so must be the password
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+
+ self.cli_commit()
+
+ def test_pppoe_dhcpv6pd(self):
+ # Check if PPPoE dialer can be configured with DHCPv6-PD
+ address = '1'
+ sla_id = '0'
+ sla_len = '8'
+
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+ self.cli_set(base_path + [interface, 'no-default-route'])
+ self.cli_set(base_path + [interface, 'no-peer-dns'])
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+ self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf'])
+
+ # prefix delegation stuff
+ dhcpv6_pd_base = base_path + [interface, 'dhcpv6-options', 'pd', '0']
+ self.cli_set(dhcpv6_pd_base + ['length', '56'])
+ self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address])
+ self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id])
+
+ # commit changes
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+ mtu_default = default_value(base_path + [interface, 'mtu'])
+
+ tmp = get_config_value(interface, 'mtu')[1]
+ self.assertEqual(tmp, mtu_default)
+ tmp = get_config_value(interface, 'user')[1].replace('"', '')
+ self.assertEqual(tmp, user)
+ tmp = get_config_value(interface, 'password')[1].replace('"', '')
+ self.assertEqual(tmp, passwd)
+ tmp = get_config_value(interface, '+ipv6 ipv6cp-use-ipaddr')
+ self.assertListEqual(tmp, ['+ipv6', 'ipv6cp-use-ipaddr'])
+
+ def test_pppoe_options(self):
+ # Check if PPPoE dialer can be configured with DHCPv6-PD
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+ ac_name = f'AC{interface}'
+ service_name = f'SRV{interface}'
+ host_uniq = 'cafebeefBABE123456'
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+
+ self.cli_set(base_path + [interface, 'access-concentrator', ac_name])
+ self.cli_set(base_path + [interface, 'service-name', service_name])
+ self.cli_set(base_path + [interface, 'host-uniq', host_uniq])
+
+ # commit changes
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ ac_name = f'AC{interface}'
+ service_name = f'SRV{interface}'
+ host_uniq = 'cafebeefBABE123456'
+
+ tmp = get_config_value(interface, 'pppoe-ac')[1]
+ self.assertEqual(tmp, f'"{ac_name}"')
+ tmp = get_config_value(interface, 'pppoe-service')[1]
+ self.assertEqual(tmp, f'"{service_name}"')
+ tmp = get_config_value(interface, 'pppoe-host-uniq')[1]
+ self.assertEqual(tmp, f'"{host_uniq}"')
+
+ def test_pppoe_mtu_mru(self):
+ # Check if PPPoE dialer can be configured and runs
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+ mtu = '1400'
+ mru = '1300'
+
+ self.cli_set(base_path + [interface, 'authentication', 'username', user])
+ self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
+ self.cli_set(base_path + [interface, 'mtu', mtu])
+ self.cli_set(base_path + [interface, 'mru', '9000'])
+
+ # check validate() - a source-interface is required
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
+
+ # check validate() - MRU needs to be less or equal then MTU
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + [interface, 'mru', mru])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration file(s)
+ for interface in self._interfaces:
+ user = f'VyOS-user-{interface}'
+ passwd = f'VyOS-passwd-{interface}'
+
+ tmp = get_config_value(interface, 'mtu')[1]
+ self.assertEqual(tmp, mtu)
+ tmp = get_config_value(interface, 'mru')[1]
+ self.assertEqual(tmp, mru)
+ tmp = get_config_value(interface, 'user')[1].replace('"', '')
+ self.assertEqual(tmp, user)
+ tmp = get_config_value(interface, 'password')[1].replace('"', '')
+ self.assertEqual(tmp, passwd)
+ tmp = get_config_value(interface, 'ifname')[1]
+ self.assertEqual(tmp, interface)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py
new file mode 100644
index 0000000..0d6f5bc
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_pseudo-ethernet.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from vyos.ifconfig import Section
+from base_interfaces_test import BasicInterfaceTest
+
+class PEthInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'pseudo-ethernet']
+
+ cls._options = {}
+ # we need to filter out VLAN interfaces identified by a dot (.)
+ # in their name - just in case!
+ if 'TEST_ETH' in os.environ:
+ for tmp in os.environ['TEST_ETH'].split():
+ cls._options.update({f'p{tmp}' : [f'source-interface {tmp}']})
+
+ else:
+ for tmp in Section.interfaces('ethernet'):
+ if '.' in tmp:
+ continue
+ cls._options.update({f'p{tmp}' : [f'source-interface {tmp}']})
+
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(PEthInterfaceTest, cls).setUpClass()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py
new file mode 100644
index 0000000..dd9f1d2
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_tunnel.py
@@ -0,0 +1,413 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.network import get_interface_config
+from vyos.template import inc_ip
+
+remote_ip4 = '192.0.2.100'
+remote_ip6 = '2001:db8::ffff'
+source_if = 'dum2222'
+mtu = 1476
+
+class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'tunnel']
+ cls.local_v4 = '192.0.2.1'
+ cls.local_v6 = '2001:db8::1'
+ cls._options = {
+ 'tun10': ['encapsulation ipip', 'remote 192.0.2.10', 'source-address ' + cls.local_v4],
+ 'tun20': ['encapsulation gre', 'remote 192.0.2.20', 'source-address ' + cls.local_v4],
+ }
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(TunnelInterfaceTest, cls).setUpClass()
+
+ # create some test interfaces
+ cls.cli_set(cls, ['interfaces', 'dummy', source_if, 'address', cls.local_v4 + '/32'])
+ cls.cli_set(cls, ['interfaces', 'dummy', source_if, 'address', cls.local_v6 + '/128'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', source_if])
+ super().tearDownClass()
+
+ def test_ipv4_encapsulations(self):
+ # When running tests ensure that for certain encapsulation types the
+ # local and remote IP address is actually an IPv4 address
+
+ interface = f'tun1000'
+ local_if_addr = f'10.10.200.1/24'
+ for encapsulation in ['ipip', 'sit', 'gre', 'gretap']:
+ self.cli_set(self._base_path + [interface, 'address', local_if_addr])
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v6])
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip6])
+
+ # Encapsulation mode requires IPv4 source-address
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v4])
+
+ # Encapsulation mode requires IPv4 remote
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip4])
+ self.cli_set(self._base_path + [interface, 'source-interface', source_if])
+
+ # Source interface can not be used with sit and gretap
+ if encapsulation in ['sit', 'gretap']:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'source-interface'])
+
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ if encapsulation not in ['sit', 'gretap']:
+ self.assertEqual(source_if, conf['link'])
+
+ self.assertEqual(interface, conf['ifname'])
+ self.assertEqual(mtu, conf['mtu'])
+ self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
+ self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local'])
+ self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote'])
+ self.assertTrue(conf['linkinfo']['info_data']['pmtudisc'])
+
+ # cleanup this instance
+ self.cli_delete(self._base_path + [interface])
+ self.cli_commit()
+
+ def test_ipv6_encapsulations(self):
+ # When running tests ensure that for certain encapsulation types the
+ # local and remote IP address is actually an IPv6 address
+
+ interface = f'tun1010'
+ local_if_addr = f'10.10.200.1/24'
+ for encapsulation in ['ipip6', 'ip6ip6', 'ip6gre', 'ip6gretap']:
+ self.cli_set(self._base_path + [interface, 'address', local_if_addr])
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v4])
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip4])
+
+ # Encapsulation mode requires IPv6 source-address
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v6])
+
+ # Encapsulation mode requires IPv6 remote
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip6])
+
+ # Configure Tunnel Source interface
+ self.cli_set(self._base_path + [interface, 'source-interface', source_if])
+ # Source interface can not be used with ip6gretap
+ if encapsulation in ['ip6gretap']:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'source-interface'])
+
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ if encapsulation not in ['ip6gretap']:
+ self.assertEqual(source_if, conf['link'])
+
+ self.assertEqual(interface, conf['ifname'])
+ self.assertEqual(mtu, conf['mtu'])
+
+ # Not applicable for ip6gre
+ if 'proto' in conf['linkinfo']['info_data']:
+ self.assertEqual(encapsulation, conf['linkinfo']['info_data']['proto'])
+
+ # remap encapsulation protocol(s) only for ipip6, ip6ip6
+ if encapsulation in ['ipip6', 'ip6ip6']:
+ encapsulation = 'ip6tnl'
+
+ self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
+ self.assertEqual(self.local_v6, conf['linkinfo']['info_data']['local'])
+ self.assertEqual(remote_ip6, conf['linkinfo']['info_data']['remote'])
+
+ # cleanup this instance
+ self.cli_delete(self._base_path + [interface])
+ self.cli_commit()
+
+ def test_tunnel_parameters_gre(self):
+ interface = f'tun1030'
+ gre_key = '10'
+ encapsulation = 'gre'
+ tos = '20'
+
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v4])
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip4])
+
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'no-pmtu-discovery'])
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'key', gre_key])
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'tos', tos])
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'ttl', '0'])
+
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(mtu, conf['mtu'])
+ self.assertEqual(interface, conf['ifname'])
+ self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
+ self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local'])
+ self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote'])
+ self.assertEqual(0, conf['linkinfo']['info_data']['ttl'])
+ self.assertFalse( conf['linkinfo']['info_data']['pmtudisc'])
+
+ def test_gretap_parameters_change(self):
+ interface = f'tun1040'
+ gre_key = '10'
+ encapsulation = 'gretap'
+ tos = '20'
+
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v4])
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip4])
+
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(mtu, conf['mtu'])
+ self.assertEqual(interface, conf['ifname'])
+ self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
+ self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local'])
+ self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote'])
+ self.assertEqual(64, conf['linkinfo']['info_data']['ttl'])
+
+ # Change remote ip address (inc host by 2
+ new_remote = inc_ip(remote_ip4, 2)
+ self.cli_set(self._base_path + [interface, 'remote', new_remote])
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(new_remote, conf['linkinfo']['info_data']['remote'])
+
+ def test_erspan_v1(self):
+ interface = f'tun1070'
+ encapsulation = 'erspan'
+ ip_key = '77'
+ idx = '20'
+
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v4])
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip4])
+
+ self.cli_set(self._base_path + [interface, 'parameters', 'erspan', 'index', idx])
+
+ # ERSPAN requires ip key parameter
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'key', ip_key])
+
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(mtu, conf['mtu'])
+ self.assertEqual(interface, conf['ifname'])
+ self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
+ self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local'])
+ self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote'])
+ self.assertEqual(64, conf['linkinfo']['info_data']['ttl'])
+ self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['ikey'])
+ self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['okey'])
+ self.assertEqual(int(idx), conf['linkinfo']['info_data']['erspan_index'])
+ # version defaults to 1
+ self.assertEqual(1, conf['linkinfo']['info_data']['erspan_ver'])
+ self.assertTrue( conf['linkinfo']['info_data']['iseq'])
+ self.assertTrue( conf['linkinfo']['info_data']['oseq'])
+
+ # Change remote ip address (inc host by 2
+ new_remote = inc_ip(remote_ip4, 2)
+ self.cli_set(self._base_path + [interface, 'remote', new_remote])
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(new_remote, conf['linkinfo']['info_data']['remote'])
+
+ def test_ip6erspan_v2(self):
+ interface = f'tun1070'
+ encapsulation = 'ip6erspan'
+ ip_key = '77'
+ erspan_ver = 2
+ direction = 'ingress'
+
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', self.local_v6])
+ self.cli_set(self._base_path + [interface, 'remote', remote_ip6])
+
+ # ERSPAN requires ip key parameter
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'key', ip_key])
+
+ self.cli_set(self._base_path + [interface, 'parameters', 'erspan', 'version', str(erspan_ver)])
+
+ # ERSPAN index is not valid on version 2
+ self.cli_set(self._base_path + [interface, 'parameters', 'erspan', 'index', '10'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'parameters', 'erspan', 'index'])
+
+ # ERSPAN requires direction to be set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'parameters', 'erspan', 'direction', direction])
+
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(mtu, conf['mtu'])
+ self.assertEqual(interface, conf['ifname'])
+ self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
+ self.assertEqual(self.local_v6, conf['linkinfo']['info_data']['local'])
+ self.assertEqual(remote_ip6, conf['linkinfo']['info_data']['remote'])
+ self.assertEqual(64, conf['linkinfo']['info_data']['ttl'])
+ self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['ikey'])
+ self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['okey'])
+ self.assertEqual(erspan_ver, conf['linkinfo']['info_data']['erspan_ver'])
+ self.assertEqual(direction, conf['linkinfo']['info_data']['erspan_dir'])
+ self.assertTrue( conf['linkinfo']['info_data']['iseq'])
+ self.assertTrue( conf['linkinfo']['info_data']['oseq'])
+
+ # Change remote ip address (inc host by 2
+ new_remote = inc_ip(remote_ip6, 2)
+ self.cli_set(self._base_path + [interface, 'remote', new_remote])
+ # Check if commit is ok
+ self.cli_commit()
+
+ conf = get_interface_config(interface)
+ self.assertEqual(new_remote, conf['linkinfo']['info_data']['remote'])
+
+ def test_tunnel_src_any_gre_key(self):
+ interface = f'tun1280'
+ encapsulation = 'gre'
+ src_addr = '0.0.0.0'
+ key = '127'
+
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'source-address', src_addr])
+ # GRE key must be supplied with a 0.0.0.0 source address
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'key', key])
+
+ self.cli_commit()
+
+ def test_multiple_gre_tunnel_same_remote(self):
+ tunnels = {
+ 'tun10' : {
+ 'encapsulation' : 'gre',
+ 'source_interface' : source_if,
+ 'remote' : '1.2.3.4',
+ },
+ 'tun20' : {
+ 'encapsulation' : 'gre',
+ 'source_interface' : source_if,
+ 'remote' : '1.2.3.4',
+ },
+ }
+
+ for tunnel, tunnel_config in tunnels.items():
+ self.cli_set(self._base_path + [tunnel, 'encapsulation', tunnel_config['encapsulation']])
+ if 'source_interface' in tunnel_config:
+ self.cli_set(self._base_path + [tunnel, 'source-interface', tunnel_config['source_interface']])
+ if 'remote' in tunnel_config:
+ self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']])
+
+ # GRE key must be supplied when two or more tunnels are formed to the same desitnation
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for tunnel, tunnel_config in tunnels.items():
+ self.cli_set(self._base_path + [tunnel, 'parameters', 'ip', 'key', tunnel.lstrip('tun')])
+
+ self.cli_commit()
+
+ for tunnel, tunnel_config in tunnels.items():
+ conf = get_interface_config(tunnel)
+ ip_key = tunnel.lstrip('tun')
+
+ self.assertEqual(tunnel_config['source_interface'], conf['link'])
+ self.assertEqual(tunnel_config['encapsulation'], conf['linkinfo']['info_kind'])
+ self.assertEqual(tunnel_config['remote'], conf['linkinfo']['info_data']['remote'])
+ self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['ikey'])
+ self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['okey'])
+
+ def test_multiple_gre_tunnel_different_remote(self):
+ tunnels = {
+ 'tun10' : {
+ 'encapsulation' : 'gre',
+ 'source_interface' : source_if,
+ 'remote' : '1.2.3.4',
+ },
+ 'tun20' : {
+ 'encapsulation' : 'gre',
+ 'source_interface' : source_if,
+ 'remote' : '1.2.3.5',
+ },
+ }
+
+ for tunnel, tunnel_config in tunnels.items():
+ self.cli_set(self._base_path + [tunnel, 'encapsulation', tunnel_config['encapsulation']])
+ if 'source_interface' in tunnel_config:
+ self.cli_set(self._base_path + [tunnel, 'source-interface', tunnel_config['source_interface']])
+ if 'remote' in tunnel_config:
+ self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']])
+
+ self.cli_commit()
+
+ for tunnel, tunnel_config in tunnels.items():
+ conf = get_interface_config(tunnel)
+
+ self.assertEqual(tunnel_config['source_interface'], conf['link'])
+ self.assertEqual(tunnel_config['encapsulation'], conf['linkinfo']['info_kind'])
+ self.assertEqual(tunnel_config['remote'], conf['linkinfo']['info_data']['remote'])
+
+ def test_tunnel_invalid_source_interface(self):
+ encapsulation = 'gre'
+ remote = '192.0.2.1'
+ interface = 'tun7543'
+
+ self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+ self.cli_set(self._base_path + [interface, 'remote', remote])
+
+ for dynamic_interface in ['l2tp0', 'ppp4220', 'sstpc0', 'ipoe654']:
+ self.cli_set(self._base_path + [interface, 'source-interface', dynamic_interface])
+ # verify() - we can not source from dynamic interfaces
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'source-interface', 'eth0'])
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
new file mode 100644
index 0000000..c6a4613
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from netifaces import interfaces
+
+from vyos.utils.process import process_named_running
+from base_interfaces_test import BasicInterfaceTest
+
+class VEthInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'virtual-ethernet']
+ cls._options = {
+ 'veth0': ['peer-name veth1'],
+ 'veth1': ['peer-name veth0'],
+ }
+
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(VEthInterfaceTest, cls).setUpClass()
+
+ def test_vif_8021q_mtu_limits(self):
+ self.skipTest('not supported')
+
+ # As we always need a pair of veth interfaces, we can not rely on the base
+ # class check to determine if there is a dhcp6c or dhclient instance running.
+ # This test will always fail as there is an instance running on the peer
+ # interface.
+ def tearDown(self):
+ self.cli_delete(self._base_path)
+ self.cli_commit()
+
+ # Verify that no previously interface remained on the system
+ for intf in self._interfaces:
+ self.assertNotIn(intf, interfaces())
+
+ @classmethod
+ def tearDownClass(cls):
+ # No daemon started during tests should remain running
+ for daemon in ['dhcp6c', 'dhclient']:
+ cls.assertFalse(cls, process_named_running(daemon))
+
+ super(VEthInterfaceTest, cls).tearDownClass()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py
new file mode 100644
index 0000000..8d90ca5
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_vti.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+
+from vyos.ifconfig import Interface
+from vyos.utils.network import is_intf_addr_assigned
+
+class VTIInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'vti']
+ cls._interfaces = ['vti10', 'vti20', 'vti30']
+
+ # call base-classes classmethod
+ super(VTIInterfaceTest, cls).setUpClass()
+
+ def test_add_single_ip_address(self):
+ addr = '192.0.2.0/31'
+ for intf in self._interfaces:
+ self.cli_set(self._base_path + [intf, 'address', addr])
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_commit()
+
+ # VTI interfaces are default down and only brought up when an
+ # IPSec connection is configured to use them
+ for intf in self._interfaces:
+ self.assertTrue(is_intf_addr_assigned(intf, addr))
+ self.assertEqual(Interface(intf).get_admin_state(), 'down')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
new file mode 100644
index 0000000..b2076b4
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -0,0 +1,366 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
+from vyos.utils.network import get_bridge_fdb
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import interface_exists
+from vyos.utils.network import get_vxlan_vlan_tunnels
+from vyos.utils.network import get_vxlan_vni_filter
+from vyos.template import is_ipv6
+from base_interfaces_test import BasicInterfaceTest
+
+def convert_to_list(ranges_to_convert):
+ result_list = []
+ for r in ranges_to_convert:
+ ranges = r.split('-')
+ result_list.extend([str(i) for i in range(int(ranges[0]), int(ranges[1]) + 1)])
+ return result_list
+
+class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'vxlan']
+ cls._options = {
+ 'vxlan10': ['vni 10', 'remote 127.0.0.2'],
+ 'vxlan20': ['vni 20', 'group 239.1.1.1', 'source-interface eth0', 'mtu 1450'],
+ 'vxlan30': ['vni 30', 'remote 2001:db8:2000::1', 'source-address 2001:db8:1000::1', 'parameters ipv6 flowlabel 0x1000'],
+ 'vxlan40': ['vni 40', 'remote 127.0.0.2', 'remote 127.0.0.3'],
+ 'vxlan50': ['vni 50', 'remote 2001:db8:2000::1', 'remote 2001:db8:2000::2', 'parameters ipv6 flowlabel 0x1000'],
+ }
+ cls._interfaces = list(cls._options)
+ cls._mtu = '1450'
+ # call base-classes classmethod
+ super(VXLANInterfaceTest, cls).setUpClass()
+
+ def test_vxlan_parameters(self):
+ tos = '40'
+ ttl = 20
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'df', 'set'])
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'tos', tos])
+ self.cli_set(self._base_path + [intf, 'parameters', 'ip', 'ttl', str(ttl)])
+ ttl += 10
+
+ self.cli_commit()
+
+ ttl = 20
+ for interface in self._interfaces:
+ options = get_interface_config(interface)
+ bridge = get_bridge_fdb(interface)
+
+ vni = options['linkinfo']['info_data']['id']
+ self.assertIn(f'vni {vni}', self._options[interface])
+
+ if any('source-interface' in s for s in self._options[interface]):
+ link = options['linkinfo']['info_data']['link']
+ self.assertIn(f'source-interface {link}', self._options[interface])
+
+ # Verify source-address setting was properly configured on the Kernel
+ if any('source-address' in s for s in self._options[interface]):
+ for s in self._options[interface]:
+ if 'source-address' in s:
+ address = s.split()[-1]
+ if is_ipv6(address):
+ tmp = options['linkinfo']['info_data']['local6']
+ else:
+ tmp = options['linkinfo']['info_data']['local']
+ self.assertIn(f'source-address {tmp}', self._options[interface])
+
+ # Verify remote setting was properly configured on the Kernel
+ if any('remote' in s for s in self._options[interface]):
+ for s in self._options[interface]:
+ if 'remote' in s:
+ for fdb in bridge:
+ if 'mac' in fdb and fdb['mac'] == '00:00:00:00:00:00':
+ remote = fdb['dst']
+ self.assertIn(f'remote {remote}', self._options[interface])
+
+ if any('group' in s for s in self._options[interface]):
+ group = options['linkinfo']['info_data']['group']
+ self.assertIn(f'group {group}', self._options[interface])
+
+ if any('flowlabel' in s for s in self._options[interface]):
+ label = options['linkinfo']['info_data']['label']
+ self.assertIn(f'parameters ipv6 flowlabel {label}', self._options[interface])
+
+ if any('external' in s for s in self._options[interface]):
+ self.assertTrue(options['linkinfo']['info_data']['external'])
+
+ self.assertEqual('vxlan', options['linkinfo']['info_kind'])
+ self.assertEqual('set', options['linkinfo']['info_data']['df'])
+ self.assertEqual(f'0x{tos}', options['linkinfo']['info_data']['tos'])
+ self.assertEqual(ttl, options['linkinfo']['info_data']['ttl'])
+ self.assertEqual(Interface(interface).get_admin_state(), 'up')
+ ttl += 10
+
+ def test_vxlan_external(self):
+ interface = 'vxlan0'
+ source_address = '192.0.2.1'
+ self.cli_set(self._base_path + [interface, 'parameters', 'external'])
+ self.cli_set(self._base_path + [interface, 'source-address', source_address])
+
+ # Both 'VNI' and 'external' can not be specified at the same time.
+ self.cli_set(self._base_path + [interface, 'vni', '111'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'vni'])
+
+ # Now add some more interfaces - this must fail and a CLI error needs
+ # to be generated as Linux can only handle one VXLAN tunnel when using
+ # external mode.
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # Remove those test interfaces again
+ for intf in self._interfaces:
+ self.cli_delete(self._base_path + [intf])
+
+ self.cli_commit()
+
+ options = get_interface_config(interface)
+ self.assertTrue(options['linkinfo']['info_data']['external'])
+ self.assertEqual('vxlan', options['linkinfo']['info_kind'])
+
+ def test_vxlan_vlan_vni_mapping(self):
+ bridge = 'br0'
+ interface = 'vxlan0'
+ source_address = '192.0.2.99'
+
+ vlan_to_vni = {
+ '10': '10010',
+ '11': '10011',
+ '12': '10012',
+ '13': '10013',
+ '20': '10020',
+ '30': '10030',
+ '31': '10031',
+ }
+
+ vlan_to_vni_ranges = {
+ '40-43': '10040-10043',
+ '45-47': '10045-10047'
+ }
+
+ self.cli_set(self._base_path + [interface, 'parameters', 'external'])
+ self.cli_set(self._base_path + [interface, 'source-address', source_address])
+
+ for vlan, vni in vlan_to_vni.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+
+ # This must fail as this VXLAN interface is not associated with any bridge
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', interface])
+
+ # It is not allowed to use duplicate VNIs
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', '11', 'vni', vlan_to_vni['10']])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ # restore VLAN - VNI mappings
+ for vlan, vni in vlan_to_vni.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+
+ # commit configuration
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(bridge))
+ self.assertTrue(interface_exists(interface))
+
+ tmp = get_interface_config(interface)
+ self.assertEqual(tmp['master'], bridge)
+ self.assertFalse(tmp['linkinfo']['info_slave_data']['neigh_suppress'])
+
+ tmp = get_vxlan_vlan_tunnels('vxlan0')
+ self.assertEqual(tmp, list(vlan_to_vni))
+
+ # add ranged VLAN - VNI mapping
+ for vlan, vni in vlan_to_vni_ranges.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+ self.cli_commit()
+
+ tmp = get_vxlan_vlan_tunnels('vxlan0')
+ vlans_list = convert_to_list(vlan_to_vni_ranges.keys())
+ self.assertEqual(tmp, list(vlan_to_vni) + vlans_list)
+
+ # check validate() - cannot map VNI range to a single VLAN id
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', '100', 'vni', '100-102'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'vlan-to-vni', '100'])
+
+ # check validate() - cannot map VLAN to VNI with different ranges
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', '100-102', 'vni', '100-105'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['interfaces', 'bridge', bridge])
+
+ def test_vxlan_neighbor_suppress(self):
+ bridge = 'br555'
+ interface = 'vxlan555'
+ source_interface = 'dum0'
+
+ self.cli_set(['interfaces', Section.section(source_interface), source_interface, 'mtu', '9000'])
+
+ self.cli_set(self._base_path + [interface, 'parameters', 'external'])
+ self.cli_set(self._base_path + [interface, 'source-interface', source_interface])
+ self.cli_set(self._base_path + [interface, 'parameters', 'neighbor-suppress'])
+
+ # This must fail as this VXLAN interface is not associated with any bridge
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', interface])
+
+ # commit configuration
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(bridge))
+ self.assertTrue(interface_exists(interface))
+
+ tmp = get_interface_config(interface)
+ self.assertEqual(tmp['master'], bridge)
+ self.assertTrue(tmp['linkinfo']['info_slave_data']['neigh_suppress'])
+ self.assertFalse(tmp['linkinfo']['info_slave_data']['learning'])
+
+ # Remove neighbor suppress configuration and re-test
+ self.cli_delete(self._base_path + [interface, 'parameters', 'neighbor-suppress'])
+ # commit configuration
+ self.cli_commit()
+
+ tmp = get_interface_config(interface)
+ self.assertEqual(tmp['master'], bridge)
+ self.assertFalse(tmp['linkinfo']['info_slave_data']['neigh_suppress'])
+ self.assertTrue(tmp['linkinfo']['info_slave_data']['learning'])
+
+ self.cli_delete(['interfaces', 'bridge', bridge])
+ self.cli_delete(['interfaces', Section.section(source_interface), source_interface])
+
+ def test_vxlan_vni_filter(self):
+ interfaces = ['vxlan987', 'vxlan986', 'vxlan985']
+ source_address = '192.0.2.77'
+
+ for interface in interfaces:
+ self.cli_set(self._base_path + [interface, 'parameters', 'external'])
+ self.cli_set(self._base_path + [interface, 'source-address', source_address])
+
+ # This must fail as there can only be one "external" VXLAN device unless "vni-filter" is defined
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # Enable "vni-filter" on the first VXLAN interface
+ self.cli_set(self._base_path + [interfaces[0], 'parameters', 'vni-filter'])
+
+ # This must fail as if it's enabled on one VXLAN interface, it must be enabled on all
+ # VXLAN interfaces
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(self._base_path + [interface, 'parameters', 'vni-filter'])
+
+ # commit configuration
+ self.cli_commit()
+
+ for interface in interfaces:
+ self.assertTrue(interface_exists(interface))
+
+ tmp = get_interface_config(interface)
+ self.assertTrue(tmp['linkinfo']['info_data']['vnifilter'])
+
+ def test_vxlan_vni_filter_add_remove(self):
+ interface = 'vxlan987'
+ source_address = '192.0.2.66'
+ bridge = 'br0'
+
+ self.cli_set(self._base_path + [interface, 'parameters', 'external'])
+ self.cli_set(self._base_path + [interface, 'source-address', source_address])
+ self.cli_set(self._base_path + [interface, 'parameters', 'vni-filter'])
+
+ # commit configuration
+ self.cli_commit()
+
+ # Check if VXLAN interface got created
+ self.assertTrue(interface_exists(interface))
+
+ # VNI filter configured?
+ tmp = get_interface_config(interface)
+ self.assertTrue(tmp['linkinfo']['info_data']['vnifilter'])
+
+ # Now create some VLAN mappings and VNI filter
+ vlan_to_vni = {
+ '50': '10050',
+ '51': '10051',
+ '52': '10052',
+ '53': '10053',
+ '54': '10054',
+ '60': '10060',
+ '69': '10069',
+ }
+
+ vlan_to_vni_ranges = {
+ '70-73': '10070-10073',
+ '75-77': '10075-10077'
+ }
+
+ for vlan, vni in vlan_to_vni.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+ # we need a bridge ...
+ self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', interface])
+ # commit configuration
+ self.cli_commit()
+
+ # All VNIs configured?
+ tmp = get_vxlan_vni_filter(interface)
+ self.assertListEqual(list(vlan_to_vni.values()), tmp)
+
+ #
+ # Delete a VLAN mappings and check if all VNIs are properly set up
+ #
+ vlan_to_vni.popitem()
+ self.cli_delete(self._base_path + [interface, 'vlan-to-vni'])
+ for vlan, vni in vlan_to_vni.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+
+ # commit configuration
+ self.cli_commit()
+
+ # All VNIs configured?
+ tmp = get_vxlan_vni_filter(interface)
+ self.assertListEqual(list(vlan_to_vni.values()), tmp)
+
+ # add ranged VLAN - VNI mapping
+ for vlan, vni in vlan_to_vni_ranges.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+ self.cli_commit()
+
+ tmp = get_vxlan_vni_filter(interface)
+ vnis_list = convert_to_list(vlan_to_vni_ranges.values())
+ self.assertListEqual(list(vlan_to_vni.values()) + vnis_list, tmp)
+
+ self.cli_delete(['interfaces', 'bridge', bridge])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py
new file mode 100644
index 0000000..4b994a6
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_wireguard.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+from vyos.utils.process import cmd
+
+base_path = ['interfaces', 'wireguard']
+
+class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(WireGuardInterfaceTest, cls).setUpClass()
+
+ cls._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32',
+ '2001:db8:1::ffff/64', '2001:db8:101::1/112']
+ cls._interfaces = ['wg0', 'wg1']
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_01_wireguard_peer(self):
+ # Create WireGuard interfaces with associated peers
+ for intf in self._interfaces:
+ peer = 'foo-' + intf
+ privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc='
+ psk = 'u2xdA70hkz0S1CG0dZlOh0aq2orwFXRIVrKo4DCvHgM='
+ pubkey = 'n6ZZL7ph/QJUJSUUTyu19c77my1dRCDHkMzFQUO9Z3A='
+
+ for addr in self._test_addr:
+ self.cli_set(base_path + [intf, 'address', addr])
+
+ self.cli_set(base_path + [intf, 'private-key', privkey])
+
+ self.cli_set(base_path + [intf, 'peer', peer, 'address', '127.0.0.1'])
+ self.cli_set(base_path + [intf, 'peer', peer, 'port', '1337'])
+
+ # Allow different prefixes to traverse the tunnel
+ allowed_ips = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
+ for ip in allowed_ips:
+ self.cli_set(base_path + [intf, 'peer', peer, 'allowed-ips', ip])
+
+ self.cli_set(base_path + [intf, 'peer', peer, 'preshared-key', psk])
+ self.cli_set(base_path + [intf, 'peer', peer, 'public-key', pubkey])
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}'))
+
+ def test_02_wireguard_add_remove_peer(self):
+ # T2939: Create WireGuard interfaces with associated peers.
+ # Remove one of the configured peers.
+ # T4774: Test prevention of duplicate peer public keys
+ interface = 'wg0'
+ port = '12345'
+ privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc='
+ pubkey_1 = 'n1CUsmR0M2LUUsyicBd6blZICwUqqWWHbu4ifZ2/9gk='
+ pubkey_2 = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
+
+ self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
+ self.cli_set(base_path + [interface, 'private-key', privkey])
+
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_1])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
+
+ self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_1])
+ self.cli_set(base_path + [interface, 'peer', 'PEER02', 'port', port])
+ self.cli_set(base_path + [interface, 'peer', 'PEER02', 'allowed-ips', '10.205.212.11/32'])
+ self.cli_set(base_path + [interface, 'peer', 'PEER02', 'address', '192.0.2.2'])
+
+ # Duplicate pubkey_1
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_2])
+
+ # Commit peers
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}'))
+
+ # Delete second peer
+ self.cli_delete(base_path + [interface, 'peer', 'PEER01'])
+ self.cli_commit()
+
+ def test_03_wireguard_same_public_key(self):
+ # T5413: Test prevention of equality interface public key and peer's
+ # public key
+ interface = 'wg0'
+ port = '12345'
+ privkey = 'OOjcXGfgQlAuM6q8Z9aAYduCua7pxf7UKYvIqoUPoGQ='
+ pubkey_fail = 'eiVeYKq66mqKLbrZLzlckSP9voaw8jSFyVNiNTdZDjU='
+ pubkey_ok = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
+
+ self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
+ self.cli_set(base_path + [interface, 'private-key', privkey])
+
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_fail])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
+
+ # The same pubkey as the interface wg0
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_ok])
+
+ # Commit peers
+ self.cli_commit()
+
+ self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}'))
+
+ def test_04_wireguard_threaded(self):
+ # T5409: Test adding threaded option on interface.
+ # Test prevention for adding threaded
+ # if no enabled peer is configured.
+ interface = 'wg0'
+ port = '12345'
+ privkey = 'OOjcXGfgQlAuM6q8Z9aAYduCua7pxf7UKYvIqoUPoGQ='
+ pubkey = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
+
+ self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
+ self.cli_set(base_path + [interface, 'private-key', privkey])
+
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
+ self.cli_set(base_path + [interface, 'per-client-thread'])
+
+ # Commit peers
+ self.cli_commit()
+ tmp = read_file(f'/sys/class/net/{interface}/threaded')
+ self.assertTrue(tmp, "1")
+
+ def test_05_wireguard_peer_pubkey_change(self):
+ # T5707 changing WireGuard CLI public key of a peer - it's not removed
+
+ def get_peers(interface) -> list:
+ tmp = cmd(f'sudo wg show {interface} dump')
+ first_line = True
+ peers = []
+ for line in tmp.split('\n'):
+ if not line:
+ continue # Skip empty lines and last line
+ items = line.split('\t')
+ if first_line:
+ self.assertEqual(privkey, items[0])
+ first_line = False
+ else:
+ peers.append(items[0])
+ return peers
+
+
+ interface = 'wg1337'
+ port = '1337'
+ privkey = 'iJi4lb2HhkLx2KSAGOjji2alKkYsJjSPkHkrcpxgEVU='
+ pubkey_1 = 'srQ8VF6z/LDjKCzpxBzFpmaNUOeuHYzIfc2dcmoc/h4='
+ pubkey_2 = '8pbMHiQ7NECVP7F65Mb2W8+4ldGG2oaGvDSpSEsOBn8='
+
+ self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
+ self.cli_set(base_path + [interface, 'port', port])
+ self.cli_set(base_path + [interface, 'private-key', privkey])
+
+ self.cli_set(base_path + [interface, 'peer', 'VyOS', 'public-key', pubkey_1])
+ self.cli_set(base_path + [interface, 'peer', 'VyOS', 'allowed-ips', '10.205.212.10/32'])
+
+ self.cli_commit()
+
+ peers = get_peers(interface)
+ self.assertIn(pubkey_1, peers)
+ self.assertNotIn(pubkey_2, peers)
+
+ # Now change the public key of our peer
+ self.cli_set(base_path + [interface, 'peer', 'VyOS', 'public-key', pubkey_2])
+ self.cli_commit()
+
+ # Verify config
+ peers = get_peers(interface)
+ self.assertNotIn(pubkey_1, peers)
+ self.assertIn(pubkey_2, peers)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py
new file mode 100644
index 0000000..b8b18f3
--- /dev/null
+++ b/smoketest/scripts/cli/test_interfaces_wireless.py
@@ -0,0 +1,635 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import unittest
+
+from base_interfaces_test import BasicInterfaceTest
+from glob import glob
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import interface_exists
+from vyos.utils.process import process_named_running
+from vyos.utils.process import call
+from vyos.xml_ref import default_value
+
+def get_config_value(interface, key):
+ tmp = read_file(f'/run/hostapd/{interface}.conf')
+ tmp = re.findall(f'{key}=+(.*)', tmp)
+ return tmp[0]
+
+wifi_cc_path = ['system', 'wireless', 'country-code']
+country = 'se'
+class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['interfaces', 'wireless']
+ cls._options = {
+ 'wlan0': ['physical-device phy0',
+ 'ssid VyOS-WIFI-0',
+ 'type station',
+ 'address 192.0.2.1/30'],
+ 'wlan1': ['physical-device phy0',
+ 'ssid VyOS-WIFI-1',
+ 'type access-point',
+ 'address 192.0.2.5/30',
+ 'channel 0'],
+ 'wlan10': ['physical-device phy1',
+ 'ssid VyOS-WIFI-2',
+ 'type station',
+ 'address 192.0.2.9/30'],
+ 'wlan11': ['physical-device phy1',
+ 'ssid VyOS-WIFI-3',
+ 'type access-point',
+ 'address 192.0.2.13/30',
+ 'channel 0'],
+ }
+ cls._interfaces = list(cls._options)
+ # call base-classes classmethod
+ super(WirelessInterfaceTest, cls).setUpClass()
+
+ # T5245 - currently testcases are disabled
+ cls._test_ipv6 = False
+ cls._test_vlan = False
+
+ cls.cli_set(cls, wifi_cc_path + [country])
+
+
+ def test_wireless_add_single_ip_address(self):
+ # derived method to check if member interfaces are enslaved properly
+ super().test_add_single_ip_address()
+
+ for option, option_value in self._options.items():
+ if 'type access-point' in option_value:
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+ elif 'type station' in option_value:
+ # Check for running process
+ self.assertTrue(process_named_running('wpa_supplicant'))
+ else:
+ self.assertTrue(False)
+
+ def test_wireless_hostapd_config(self):
+ # Only set the hostapd (access-point) options
+ interface = self._interfaces[1] # wlan1
+ ssid = 'ssid'
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+
+ # auto-powersave is special
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'auto-powersave'])
+
+ ht_opt = {
+ # VyOS CLI option hostapd - ht_capab setting
+ '40mhz-incapable' : '[40-INTOLERANT]',
+ 'delayed-block-ack' : '[DELAYED-BA]',
+ 'greenfield' : '[GF]',
+ 'ldpc' : '[LDPC]',
+ 'lsig-protection' : '[LSIG-TXOP-PROT]',
+ 'channel-set-width ht40+' : '[HT40+]',
+ 'stbc tx' : '[TX-STBC]',
+ 'stbc rx 123' : '[RX-STBC-123]',
+ 'max-amsdu 7935' : '[MAX-AMSDU-7935]',
+ 'smps static' : '[SMPS-STATIC]',
+ }
+ for key in ht_opt:
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht'] + key.split())
+
+ vht_opt = {
+ # VyOS CLI option hostapd - ht_capab setting
+ 'channel-set-width 3' : '[VHT160-80PLUS80]',
+ 'stbc tx' : '[TX-STBC-2BY1]',
+ 'stbc rx 12' : '[RX-STBC-12]',
+ 'ldpc' : '[RXLDPC]',
+ 'tx-powersave' : '[VHT-TXOP-PS]',
+ 'vht-cf' : '[HTC-VHT]',
+ 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]',
+ 'max-mpdu 11454' : '[MAX-MPDU-11454]',
+ 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2]',
+ 'link-adaptation both' : '[VHT-LINK-ADAPT3]',
+ 'short-gi 80' : '[SHORT-GI-80]',
+ 'short-gi 160' : '[SHORT-GI-160]',
+ }
+ for key in vht_opt:
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht'] + key.split())
+
+ self.cli_commit()
+
+ #
+ # Validate Config
+ #
+ tmp = get_config_value(interface, 'interface')
+ self.assertEqual(interface, tmp)
+
+ # ssid
+ tmp = get_config_value(interface, 'ssid')
+ self.assertEqual(ssid, tmp)
+
+ # channel
+ tmp = get_config_value(interface, 'channel')
+ cli_default = default_value(self._base_path + [interface, 'channel'])
+ self.assertEqual(cli_default, tmp)
+
+ # auto-powersave is special
+ tmp = get_config_value(interface, 'uapsd_advertisement_enabled')
+ self.assertEqual('1', tmp)
+
+ tmp = get_config_value(interface, 'ht_capab')
+ for key, value in ht_opt.items():
+ self.assertIn(value, tmp)
+
+ tmp = get_config_value(interface, 'vht_capab')
+ for key, value in vht_opt.items():
+ self.assertIn(value, tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+
+ def test_wireless_hostapd_vht_mu_beamformer_config(self):
+ # Multi-User-Beamformer
+ interface = self._interfaces[1] # wlan1
+ ssid = 'vht_mu-beamformer'
+ antennas = '3'
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'channel', '36'])
+
+ ht_opt = {
+ # VyOS CLI option hostapd - ht_capab setting
+ 'channel-set-width ht20' : '[HT20]',
+ 'channel-set-width ht40-' : '[HT40-]',
+ 'channel-set-width ht40+' : '[HT40+]',
+ 'dsss-cck-40' : '[DSSS_CCK-40]',
+ 'short-gi 20' : '[SHORT-GI-20]',
+ 'short-gi 40' : '[SHORT-GI-40]',
+ 'max-amsdu 7935' : '[MAX-AMSDU-7935]',
+ }
+ for key in ht_opt:
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht'] + key.split())
+
+ vht_opt = {
+ # VyOS CLI option hostapd - ht_capab setting
+ 'max-mpdu 11454' : '[MAX-MPDU-11454]',
+ 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2]',
+ 'stbc tx' : '[TX-STBC-2BY1]',
+ 'stbc rx 12' : '[RX-STBC-12]',
+ 'ldpc' : '[RXLDPC]',
+ 'tx-powersave' : '[VHT-TXOP-PS]',
+ 'vht-cf' : '[HTC-VHT]',
+ 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]',
+ 'link-adaptation both' : '[VHT-LINK-ADAPT3]',
+ 'short-gi 80' : '[SHORT-GI-80]',
+ 'short-gi 160' : '[SHORT-GI-160]',
+ 'beamform multi-user-beamformer' : '[MU-BEAMFORMER][BF-ANTENNA-3][SOUNDING-DIMENSION-3]',
+ }
+
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'channel-set-width', '1'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'center-channel-freq', 'freq-1', '42'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'antenna-count', antennas])
+ for key in vht_opt:
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht'] + key.split())
+
+ self.cli_commit()
+
+ #
+ # Validate Config
+ #
+ tmp = get_config_value(interface, 'interface')
+ self.assertEqual(interface, tmp)
+
+ # ssid
+ tmp = get_config_value(interface, 'ssid')
+ self.assertEqual(ssid, tmp)
+
+ # channel
+ tmp = get_config_value(interface, 'channel')
+ self.assertEqual('36', tmp)
+
+ tmp = get_config_value(interface, 'ht_capab')
+ for key, value in ht_opt.items():
+ self.assertIn(value, tmp)
+
+ tmp = get_config_value(interface, 'vht_capab')
+ for key, value in vht_opt.items():
+ self.assertIn(value, tmp)
+
+ def test_wireless_hostapd_vht_su_beamformer_config(self):
+ # Single-User-Beamformer
+ interface = self._interfaces[1] # wlan1
+ ssid = 'vht_su-beamformer'
+ antennas = '3'
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'channel', '36'])
+
+ ht_opt = {
+ # VyOS CLI option hostapd - ht_capab setting
+ 'channel-set-width ht20' : '[HT20]',
+ 'channel-set-width ht40-' : '[HT40-]',
+ 'channel-set-width ht40+' : '[HT40+]',
+ 'dsss-cck-40' : '[DSSS_CCK-40]',
+ 'short-gi 20' : '[SHORT-GI-20]',
+ 'short-gi 40' : '[SHORT-GI-40]',
+ 'max-amsdu 7935' : '[MAX-AMSDU-7935]',
+ }
+ for key in ht_opt:
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht'] + key.split())
+
+ vht_opt = {
+ # VyOS CLI option hostapd - ht_capab setting
+ 'max-mpdu 11454' : '[MAX-MPDU-11454]',
+ 'max-mpdu-exp 2' : '[MAX-A-MPDU-LEN-EXP-2]',
+ 'stbc tx' : '[TX-STBC-2BY1]',
+ 'stbc rx 12' : '[RX-STBC-12]',
+ 'ldpc' : '[RXLDPC]',
+ 'tx-powersave' : '[VHT-TXOP-PS]',
+ 'vht-cf' : '[HTC-VHT]',
+ 'antenna-pattern-fixed' : '[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN]',
+ 'link-adaptation both' : '[VHT-LINK-ADAPT3]',
+ 'short-gi 80' : '[SHORT-GI-80]',
+ 'short-gi 160' : '[SHORT-GI-160]',
+ 'beamform single-user-beamformer' : '[SU-BEAMFORMER][BF-ANTENNA-2][SOUNDING-DIMENSION-2]',
+ }
+
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'channel-set-width', '1'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'center-channel-freq', 'freq-1', '42'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht', 'antenna-count', antennas])
+ for key in vht_opt:
+ self.cli_set(self._base_path + [interface, 'capabilities', 'vht'] + key.split())
+
+ self.cli_commit()
+
+ #
+ # Validate Config
+ #
+ tmp = get_config_value(interface, 'interface')
+ self.assertEqual(interface, tmp)
+
+ # ssid
+ tmp = get_config_value(interface, 'ssid')
+ self.assertEqual(ssid, tmp)
+
+ # channel
+ tmp = get_config_value(interface, 'channel')
+ self.assertEqual('36', tmp)
+
+ tmp = get_config_value(interface, 'ht_capab')
+ for key, value in ht_opt.items():
+ self.assertIn(value, tmp)
+
+ tmp = get_config_value(interface, 'vht_capab')
+ for key, value in vht_opt.items():
+ self.assertIn(value, tmp)
+
+ def test_wireless_hostapd_he_2ghz_config(self):
+ # Only set the hostapd (access-point) options - HE mode for 802.11ax at 2.4GHz
+ interface = self._interfaces[1] # wlan1
+ ssid = 'ssid'
+ channel = '1'
+ sae_pw = 'VyOSVyOSVyOS'
+ bss_color = '13'
+ channel_set_width = '81'
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'channel', channel])
+ self.cli_set(self._base_path + [interface, 'mode', 'ax'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'mode', 'wpa2'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'passphrase', sae_pw])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'cipher', 'CCMP'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'cipher', 'GCMP'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', '40mhz-incapable'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'channel-set-width', 'ht20'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'channel-set-width', 'ht40+'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'channel-set-width', 'ht40-'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'short-gi', '20'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'ht', 'short-gi', '40'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'bss-color', bss_color])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'channel-set-width', channel_set_width])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'multi-user-beamformer'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'single-user-beamformer'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'single-user-beamformee'])
+
+ self.cli_commit()
+
+ #
+ # Validate Config
+ #
+ tmp = get_config_value(interface, 'interface')
+ self.assertEqual(interface, tmp)
+
+ # ssid
+ tmp = get_config_value(interface, 'ssid')
+ self.assertEqual(ssid, tmp)
+
+ # mode of operation resulting from [interface, 'mode', 'ax']
+ tmp = get_config_value(interface, 'hw_mode')
+ self.assertEqual('g', tmp)
+ tmp = get_config_value(interface, 'ieee80211h')
+ self.assertEqual('1', tmp)
+ tmp = get_config_value(interface, 'ieee80211ax')
+ self.assertEqual('1', tmp)
+
+ # channel and channel width
+ tmp = get_config_value(interface, 'channel')
+ self.assertEqual(channel, tmp)
+ tmp = get_config_value(interface, 'op_class')
+ self.assertEqual(channel_set_width, tmp)
+
+ # BSS coloring
+ tmp = get_config_value(interface, 'he_bss_color')
+ self.assertEqual(bss_color, tmp)
+
+ # sae_password
+ tmp = get_config_value(interface, 'wpa_passphrase')
+ self.assertEqual(sae_pw, tmp)
+
+ # WPA3 and dependencies
+ tmp = get_config_value(interface, 'wpa')
+ self.assertEqual('2', tmp)
+ tmp = get_config_value(interface, 'rsn_pairwise')
+ self.assertEqual('CCMP GCMP', tmp)
+ tmp = get_config_value(interface, 'wpa_key_mgmt')
+ self.assertEqual('WPA-PSK WPA-PSK-SHA256', tmp)
+
+ # beamforming
+ tmp = get_config_value(interface, 'he_mu_beamformer')
+ self.assertEqual('1', tmp)
+ tmp = get_config_value(interface, 'he_su_beamformee')
+ self.assertEqual('1', tmp)
+ tmp = get_config_value(interface, 'he_mu_beamformer')
+ self.assertEqual('1', tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+
+ def test_wireless_hostapd_he_6ghz_config(self):
+ # Only set the hostapd (access-point) options - HE mode for 802.11ax at 6GHz
+ interface = self._interfaces[1] # wlan1
+ ssid = 'ssid'
+ channel = '1'
+ sae_pw = 'VyOSVyOSVyOS'
+ bss_color = '37'
+ channel_set_width = '134'
+ center_channel_freq_1 = '15'
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'channel', channel])
+ self.cli_set(self._base_path + [interface, 'mode', 'ax'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'mode', 'wpa3'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'passphrase', sae_pw])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'cipher', 'CCMP'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'cipher', 'GCMP'])
+ self.cli_set(self._base_path + [interface, 'enable-bf-protection'])
+ self.cli_set(self._base_path + [interface, 'mgmt-frame-protection', 'required'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'bss-color', bss_color])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'channel-set-width', channel_set_width])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'center-channel-freq', 'freq-1', center_channel_freq_1])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'antenna-pattern-fixed'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'multi-user-beamformer'])
+ self.cli_set(self._base_path + [interface, 'capabilities', 'he', 'beamform', 'single-user-beamformer'])
+
+ self.cli_commit()
+
+ #
+ # Validate Config
+ #
+ tmp = get_config_value(interface, 'interface')
+ self.assertEqual(interface, tmp)
+
+ # ssid
+ tmp = get_config_value(interface, 'ssid')
+ self.assertEqual(ssid, tmp)
+
+ # mode of operation resulting from [interface, 'mode', 'ax']
+ tmp = get_config_value(interface, 'hw_mode')
+ self.assertEqual('a', tmp)
+ tmp = get_config_value(interface, 'ieee80211h')
+ self.assertEqual('1', tmp)
+ tmp = get_config_value(interface, 'ieee80211ax')
+ self.assertEqual('1', tmp)
+
+ # channel and channel width
+ tmp = get_config_value(interface, 'channel')
+ self.assertEqual(channel, tmp)
+ tmp = get_config_value(interface, 'op_class')
+ self.assertEqual(channel_set_width, tmp)
+ tmp = get_config_value(interface, 'he_oper_centr_freq_seg0_idx')
+ self.assertEqual(center_channel_freq_1, tmp)
+
+ # BSS coloring
+ tmp = get_config_value(interface, 'he_bss_color')
+ self.assertEqual(bss_color, tmp)
+
+ # sae_password
+ tmp = get_config_value(interface, 'sae_password')
+ self.assertEqual(sae_pw, tmp)
+
+ # WPA3 and dependencies
+ tmp = get_config_value(interface, 'wpa')
+ self.assertEqual('2', tmp)
+ tmp = get_config_value(interface, 'rsn_pairwise')
+ self.assertEqual('CCMP GCMP', tmp)
+ tmp = get_config_value(interface, 'wpa_key_mgmt')
+ self.assertEqual('SAE', tmp)
+
+ # antenna pattern
+ tmp = get_config_value(interface, 'he_6ghz_rx_ant_pat')
+ self.assertEqual('1', tmp)
+
+ # beamforming
+ tmp = get_config_value(interface, 'he_mu_beamformer')
+ self.assertEqual('1', tmp)
+ tmp = get_config_value(interface, 'he_su_beamformee')
+ self.assertEqual('0', tmp)
+ tmp = get_config_value(interface, 'he_mu_beamformer')
+ self.assertEqual('1', tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+
+ def test_wireless_hostapd_wpa_config(self):
+ # Only set the hostapd (access-point) options
+ interface = self._interfaces[1] # wlan1
+ ssid = 'VyOS-SMOKETEST'
+ channel = '1'
+ wpa_key = 'VyOSVyOSVyOS'
+ mode = 'n'
+
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'mode', mode])
+
+ # SSID and country-code are already configured in self.setUpClass()
+ # Therefore, we must delete those here to check if commit will fail without it.
+ self.cli_delete(wifi_cc_path)
+ self.cli_delete(self._base_path + [interface, 'ssid'])
+
+ # Country-Code must be set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(wifi_cc_path + [country])
+
+ # SSID must be set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+
+ # Channel must be set (defaults to channel 0)
+ self.cli_set(self._base_path + [interface, 'channel', channel])
+
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'mode', 'wpa2'])
+ self.cli_set(self._base_path + [interface, 'security', 'wpa', 'passphrase', wpa_key])
+
+ self.cli_commit()
+
+ #
+ # Validate Config
+ #
+ tmp = get_config_value(interface, 'interface')
+ self.assertEqual(interface, tmp)
+
+ tmp = get_config_value(interface, 'hw_mode')
+ # rewrite special mode
+ if mode == 'n': mode = 'g'
+ self.assertEqual(mode, tmp)
+
+ # WPA key
+ tmp = get_config_value(interface, 'wpa')
+ self.assertEqual('2', tmp)
+ tmp = get_config_value(interface, 'wpa_passphrase')
+ self.assertEqual(wpa_key, tmp)
+
+ # SSID
+ tmp = get_config_value(interface, 'ssid')
+ self.assertEqual(ssid, tmp)
+
+ # channel
+ tmp = get_config_value(interface, 'channel')
+ self.assertEqual(channel, tmp)
+
+ # Country code
+ tmp = get_config_value(interface, 'country_code')
+ self.assertEqual(country.upper(), tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+
+ def test_wireless_access_point_bridge(self):
+ interface = self._interfaces[1] # wlan1
+ ssid = 'VyOS-Test'
+ bridge = 'br42477'
+
+ # We need a bridge where we can hook our access-point interface to
+ bridge_path = ['interfaces', 'bridge', bridge]
+ self.cli_set(bridge_path + ['member', 'interface', interface])
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'channel', '1'])
+
+ self.cli_commit()
+
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+
+ bridge_members = []
+ for tmp in glob(f'/sys/class/net/{bridge}/lower_*'):
+ bridge_members.append(os.path.basename(tmp).replace('lower_', ''))
+
+ self.assertIn(interface, bridge_members)
+
+ # Now generate a VLAN on the bridge
+ self.cli_set(bridge_path + ['enable-vlan'])
+ self.cli_set(bridge_path + ['vif', '20', 'address', '10.0.0.1/24'])
+
+ self.cli_commit()
+
+ tmp = get_config_value(interface, 'bridge')
+ self.assertEqual(tmp, bridge)
+ tmp = get_config_value(interface, 'wds_sta')
+ self.assertEqual(tmp, '1')
+
+ self.cli_delete(bridge_path)
+
+ def test_wireless_security_station_address(self):
+ interface = self._interfaces[1] # wlan1
+ ssid = 'VyOS-ACL'
+
+ hostapd_accept_station_conf = f'/run/hostapd/{interface}_station_accept.conf'
+ hostapd_deny_station_conf = f'/run/hostapd/{interface}_station_deny.conf'
+
+ accept_mac = ['00:00:00:00:ac:01', '00:00:00:00:ac:02', '00:00:00:00:ac:03', '00:00:00:00:ac:04']
+ deny_mac = ['00:00:00:00:de:01', '00:00:00:00:de:02', '00:00:00:00:de:03', '00:00:00:00:de:04']
+
+ self.cli_set(self._base_path + [interface, 'ssid', ssid])
+ self.cli_set(self._base_path + [interface, 'type', 'access-point'])
+ self.cli_set(self._base_path + [interface, 'security', 'station-address', 'mode', 'accept'])
+
+ for mac in accept_mac:
+ self.cli_set(self._base_path + [interface, 'security', 'station-address', 'accept', 'mac', mac])
+ for mac in deny_mac:
+ self.cli_set(self._base_path + [interface, 'security', 'station-address', 'deny', 'mac', mac])
+
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(interface))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf'))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf'))
+
+ self.assertTrue(process_named_running('hostapd'))
+
+ # in accept mode all addresses are allowed unless specified in the deny list
+ tmp = get_config_value(interface, 'macaddr_acl')
+ self.assertEqual(tmp, '0')
+
+ accept_list = read_file(hostapd_accept_station_conf)
+ for mac in accept_mac:
+ self.assertIn(mac, accept_list)
+
+ deny_list = read_file(hostapd_deny_station_conf)
+ for mac in deny_mac:
+ self.assertIn(mac, deny_list)
+
+ # Switch mode accept -> deny
+ self.cli_set(self._base_path + [interface, 'security', 'station-address', 'mode', 'deny'])
+ self.cli_commit()
+
+ self.assertTrue(interface_exists(interface))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf'))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf'))
+
+ # In deny mode all addresses are denied unless specified in the allow list
+ tmp = get_config_value(interface, 'macaddr_acl')
+ self.assertEqual(tmp, '1')
+
+ # Check for running process
+ self.assertTrue(process_named_running('hostapd'))
+
+if __name__ == '__main__':
+ check_kmod('mac80211_hwsim')
+ # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1)
+ # remove them to have a clean test start
+ for interface in ['wlan0', 'wlan1']:
+ if interface_exists(interface):
+ call(f'sudo iw dev {interface} del')
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
new file mode 100644
index 0000000..34f77b9
--- /dev/null
+++ b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
@@ -0,0 +1,502 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'haproxy'
+HAPROXY_CONF = '/run/haproxy/haproxy.cfg'
+base_path = ['load-balancing', 'reverse-proxy']
+proxy_interface = 'eth1'
+
+valid_ca_cert = """
+MIIDnTCCAoWgAwIBAgIUewSDtLiZbhg1YEslMnqRl1shoPcwDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
+NDA0MDEwNTQ3MzJaFw0yOTAzMzEwNTQ3MzJaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC/D6W27rfpdPIf16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+c
+nf7oExp9zi/4HJ/KRbcc1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jj
+klHFSxjEoT/0YvJQ1IV/3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJo
+O3e7Ew9HFkamvuL6Z6c4uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKg
+SbOiQaFk3blOky/e3ifNjZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2
+rZyxRdZTC9kh+dShR1s/qcPnDw7lAgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w
+DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd
+BgNVHQ4EFgQU/HE2UPn8JQB/9EL52GquPxZqr5MwDQYJKoZIhvcNAQELBQADggEB
+AIkMmqyoMqidTa3lvUPJNl4H+Ef/yPQkTkrsOd3WL8DQysyUdMLdQozr3K1bH5XB
+wRxoXX211nu4WhN18LsFJRCuHBSxmaNkBGFyl+JNvhPUSI6j0somNMCS75KJ0ZDx
+2HZsXmmJFF902VQxCR7vCIrFDrKDYq1e7GQbFS8t46FlpqivQMQWNPt18Bthj/1Y
+lO2GKRWFCX8VlOW7FtDQ6B3oC1oAGHBBGogAx7/0gh9DnYBKT14V/kuWW3RNABZJ
+ewHO1C6icQdnjtaREDyTP4oyL+uyAfXrFfbpti2hc00f8oYPQZYxj1yxl4UAdNij
+mS6YqH/WRioGMe3tBVeSdoo=
+"""
+
+valid_ca_private_key = """
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/D6W27rfpdPIf
+16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+cnf7oExp9zi/4HJ/KRbcc
+1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jjklHFSxjEoT/0YvJQ1IV/
+3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJoO3e7Ew9HFkamvuL6Z6c4
+uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKgSbOiQaFk3blOky/e3ifN
+jZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2rZyxRdZTC9kh+dShR1s/
+qcPnDw7lAgMBAAECggEAGm+j0kf9koPn7Jf9kEZD6CwlgEraLXiNvBqmDOhcDS9Z
+VPTA3XdGWHQ3uofx+VKLW9TntkDfqzEyQP83v6h8W7a0opDKzvUPkMQi/Dh1ttAY
+SdfGrozhUINiRbq9LbtSVgKpwrreJGkDf8mK3GE1Gd9xuHEnmahDvwlyE7HLF3Eh
+2xJDSAPx3OxcjR5hW7vbojhVCyCfuYTlZB86f0Sb8SqxZMt/y2zKmbzoTqpUBWbg
+lBnE7GJoNR07DWjxvEP8r6kQMh670I01SUR42CSK8X8asHhhZHUcggsNno+BBc6K
+sy4HzDIYIay6oy0atcVzKsGrlNCveeAiSEcw7x2yAQKBgQDsXz2FbhXYV5Vbt4wU
+5EWOa7if/+FG+TcVezOF3xlNBgykjXHQaYTYHrJq0qsEFrNT3ZGm9ezY4LdF3BTt
+5z/+i8QlCCw/nr3N7JZx6U5+OJl1j3NLFoFx3+DXo31pgJJEQCHHwdCkF5IuOcZ/
+b3nXkRZ80BVv7XD6F9bMHEwLYQKBgQDO7THcRDbsE6/+7VsTDf0P/JENba3DBBu1
+gjb1ItL5FHJwMgnkUadRZRo0QKye848ugribed39qSoJfNaBJrAT5T8S/9q+lXft
+vXUckcBO1CKNaP9gqF5fPIdNHf64GbmCiiHjOTE3rwJjkxJPpzLXyvgBO4aLeesK
+ThBdW+iWBQKBgD3crz08knsMcQqP/xl4pLuhdbBqR4tLrh7xH4rp2LVP3/8xBZiG
+BT6Kyicq+5cWWdiZJIWN127rYQvnjZK18wmriqomeW4tHX/Ha5hkdyaRqZga8xGz
+0iz7at0E7M2v2JgEMNMW5oQLpzZx6IFxq3G/hyMjUnj4q5jIpG7G+SABAoGBAKgT
+8Ika+4WcpDssrup2VVTT8Tp4GUkroBo6D8vkInvhiObrLi+/x2mM9tD0q4JdEbNU
+yQC454EwFA4q0c2MED/I2QfkvNhLbmO0nVi8ZvlgxEQawjzP5f/zmW8haxI9Cvsm
+mkoH3Zt+UzFwd9ItXFX97p6JrErEmA8Bw7chfXXFAoGACWR/c+s7hnX6gzyah3N1
+Db0xAaS6M9fzogcg2OM1i/6OCOcp4Sh1fmPG7tN45CCnFkhgVoRkSSA5MJAe2I/r
+xFm72VX7567T+4qIFua2iDxIBA/Z4zmj+RYfhHGPYZjdSjprKJxY6QOv5aoluBvE
+mlLy1Hmcry+ukWZtWezZfGY=
+"""
+
+valid_cert = """
+MIIDsTCCApmgAwIBAgIUDKOfYIwwtjww0vAMvJnXnGLhL+0wDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
+NDA0MDEwNTQ5NTdaFw0yNTA0MDEwNTQ5NTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQCHtW25Umt6rqm2gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASii
+W5PToC7N8StMwFl2YoIof+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E0
+96xVobb2KY4lMZ2rVwmpB7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgn
+PgTtJcdVIU75XhQWqBmAUsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM
+64paIKZooFm78IsxJ26jHpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt
+49uOsy82VmUcHPyoZ8DKYkBFHfSpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD
+VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTeTcgM
+pRxAMjVBirjzo2QUu5H5fzAfBgNVHSMEGDAWgBT8cTZQ+fwlAH/0QvnYaq4/Fmqv
+kzANBgkqhkiG9w0BAQsFAAOCAQEAi4dBcH7TIYwWRW6bWRubMA7ztonV4EYb15Zf
+9yNafMWAEEBOii/DFo+j/ky9oInl7ZHw7gTIyXfLEarX/bM6fHOgiyj4zp3u6RnH
+5qlBypu/YCnyPjE/GvV05m2rrXnxZ4rCtcoO4u/HyGbV+jGnCmjShKICKyu1FdMd
+eeZRrLKPO/yghadGH34WVQnrbaorwlbi+NjB6fxmZQx5HE/SyK/9sb6WCpLMGHoy
+MpdQo3lV1ewtL3ElIWDq6mO030Mo5pwpjIU+8yHHNBVzg6mlGVgQPAp0gbUei9aP
+CJ8SLmMEi3NDk0E/sPgVC17e6bf2bx2nRuXROZekG2dd90Iu8g==
+"""
+
+valid_cert_private_key = """
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCHtW25Umt6rqm2
+gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASiiW5PToC7N8StMwFl2YoIo
+f+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E096xVobb2KY4lMZ2rVwmp
+B7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgnPgTtJcdVIU75XhQWqBmA
+UsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM64paIKZooFm78IsxJ26j
+HpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt49uOsy82VmUcHPyoZ8DK
+YkBFHfSpAgMBAAECggEABofhw0W/ACEMcAjmpNTFkFCUXPGQXWDVD7EzuIZSNdOv
+yOm4Rbys6H6/B7wwO6KVagoBf1Cw5Xh1YtFPuoZxsZ+liMD6eLc+SB/j/RTYAhPO
+0bvsyK3gSF8w4nGKWLce9M74ZRwThkG6qGijmlDdPyP3r2kn8GoTQzVOWYZbavk/
+H3uE6PsZSWjOY+Mnm3vEmeItPYKGZ5+IP+YiTqZ4NCggBwH7csnR3/kbwY5Ns7jl
+3Av+EAdIeUwDNeMfLTzN7GphJR7gL6YQIhGKxE+W0GHXL2FubnnrFx8G75HFh1ay
+GkJXEqY5Lbd+7VPS0KcQdwhMSSoJsY5GUORUqrU80QKBgQC/0wJSu+Gfe7dONIby
+mnGRppSRIQVRjCjbVIN+Y2h1Kp3aK0qDpV7KFLCiUUtz9rWHR/NB4cDaIW543T55
+/jXUMD2j3EqtbtlsVQfDLQV7DyDrMmBAs4REHmyZmWTzHjCDUO79ahdOlZs34Alz
+wfpX3L3WVYGIAJKZtsUZ8FbrGQKBgQC1HFgVZ1PqP9/pW50RMh06BbQrhWPGiWgH
+Rn5bFthLkp3uqr9bReBq9tu3sqJuAhFudH68wup+Z+fTcHAcNg2Rs+Q+IKnULdB/
+UQHYoPjeWOvHAuOmgn9iD9OD7GCIv8fZmLit09vAsOWq+NKNBKCknGM70CDrvAlQ
+lOAUa34YEQKBgQC5i8GThWiYe3Kzktt1jy6LVDYgq3AZkRl0Diui9UT1EGPfxEAv
+VqZ5kcnJOBlj8h9k25PRBi0k0XGqN1dXaS1oMcFt3ofdenuU7iqz/7htcBTHa9Lu
+wrYNreAeMuISyADlBEQnm5cvzEZ3pZ1++wLMOhjmWY8Rnnwvczrz/CYXAQKBgH+t
+vcNJFvWblkUzWuWWiNgw0TWlUhPTJs2KOuYIku+kK0bohQLZnj6KTZeRjcU0HAnc
+gsScPShkJCEBsWeSC7reMVhDOrbknYpEF6MayJgn5ABm3wqyEQ+WzKzCZcPCQCf8
+7KVPKCsOCrufsv/LdVzXC3ZNYggOhhqS+e4rYbehAoGBAIsq252o3vgrunzS5FZx
+IONA2FvYrxVbDn5aF8WfNSdKFy3CAlt0P+Fm8gYbrKylIfMXpL8Oqc9RJou5onZP
+ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx
+7i7xFz2WEiQeSCPaKYOiqM3t
+"""
+
+
+class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(['interfaces', 'ethernet', proxy_interface, 'address'])
+ self.cli_delete(base_path)
+ self.cli_delete(['pki'])
+ self.cli_commit()
+
+ # Process must be terminated after deleting the config
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def base_config(self):
+ self.cli_set(base_path + ['service', 'https_front', 'mode', 'http'])
+ self.cli_set(base_path + ['service', 'https_front', 'port', '4433'])
+ self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01'])
+
+ self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy'])
+
+ self.cli_set(base_path + ['global-parameters', 'max-connections', '1000'])
+
+ def configure_pki(self):
+
+ # Valid CA
+ self.cli_set(['pki', 'ca', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(['pki', 'ca', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
+
+ # Valid cert
+ self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')])
+ self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')])
+
+ def test_01_lb_reverse_proxy_domain(self):
+ domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com']
+ domain_bk_second = 'n5.example.com'
+ frontend = 'https_front'
+ front_port = '4433'
+ bk_server_first = '192.0.2.11'
+ bk_server_second = '192.0.2.12'
+ bk_first_name = 'bk-01'
+ bk_second_name = 'bk-02'
+ bk_server_port = '9090'
+ mode = 'http'
+ rule_ten = '10'
+ rule_twenty = '20'
+ rule_thirty = '30'
+ send_proxy = 'send-proxy'
+ max_connections = '1000'
+
+ back_base = base_path + ['backend']
+
+ self.cli_set(base_path + ['service', frontend, 'mode', mode])
+ self.cli_set(base_path + ['service', frontend, 'port', front_port])
+ for domain in domains_bk_first:
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'domain-name', domain])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'set', 'backend', bk_first_name])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'domain-name', domain_bk_second])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'url-path', 'end', '/test'])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_second_name])
+
+ self.cli_set(back_base + [bk_first_name, 'mode', mode])
+ self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first])
+ self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'port', bk_server_port])
+ self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, send_proxy])
+
+ self.cli_set(back_base + [bk_second_name, 'mode', mode])
+ self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'address', bk_server_second])
+ self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'port', bk_server_port])
+ self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'backup'])
+
+ self.cli_set(base_path + ['global-parameters', 'max-connections', max_connections])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(HAPROXY_CONF)
+
+ # Global
+ self.assertIn(f'maxconn {max_connections}', config)
+
+ # Frontend
+ self.assertIn(f'frontend {frontend}', config)
+ self.assertIn(f'bind [::]:{front_port} v4v6', config)
+ self.assertIn(f'mode {mode}', config)
+ for domain in domains_bk_first:
+ self.assertIn(f'acl {rule_ten} hdr(host) -i {domain}', config)
+ self.assertIn(f'use_backend {bk_first_name} if {rule_ten}', config)
+ self.assertIn(f'acl {rule_twenty} hdr(host) -i {domain_bk_second}', config)
+ self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config)
+ self.assertIn(f'acl {rule_thirty} path -i -m end /test', config)
+ self.assertIn(f'use_backend {bk_second_name} if {rule_thirty}', config)
+
+ # Backend
+ self.assertIn(f'backend {bk_first_name}', config)
+ self.assertIn(f'balance roundrobin', config)
+ self.assertIn(f'option forwardfor', config)
+ self.assertIn('http-request add-header X-Forwarded-Proto https if { ssl_fc }', config)
+ self.assertIn(f'mode {mode}', config)
+ self.assertIn(f'server {bk_first_name} {bk_server_first}:{bk_server_port} send-proxy', config)
+
+ self.assertIn(f'backend {bk_second_name}', config)
+ self.assertIn(f'mode {mode}', config)
+ self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config)
+ self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config)
+
+ def test_02_lb_reverse_proxy_cert_not_exists(self):
+ self.base_config()
+ self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+ # self.assertIn('\nCertificates does not exist in PKI\n', str(e.exception))
+
+ self.cli_delete(base_path)
+ self.configure_pki()
+
+ self.base_config()
+ self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+ # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception))
+
+ self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest'])
+ self.cli_commit()
+
+ def test_03_lb_reverse_proxy_ca_not_exists(self):
+ self.base_config()
+ self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+ # self.assertIn('\nCA certificates does not exist in PKI\n', str(e.exception))
+
+ self.cli_delete(base_path)
+ self.configure_pki()
+
+ self.base_config()
+ self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+ # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception))
+
+ self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+ self.cli_commit()
+
+ def test_04_lb_reverse_proxy_backend_ssl_no_verify(self):
+ # Setup base
+ self.configure_pki()
+ self.base_config()
+
+ # Set no-verify option
+ self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify'])
+ self.cli_commit()
+
+ # Test no-verify option
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config)
+
+ # Test setting ca-certificate alongside no-verify option fails, to test config validation
+ self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+
+ def test_05_lb_reverse_proxy_backend_http_check(self):
+ # Setup base
+ self.base_config()
+
+ # Set http-check
+ self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get'])
+ self.cli_commit()
+
+ # Test http-check
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('option httpchk', config)
+ self.assertIn('http-check send meth GET', config)
+
+ # Set http-check with uri and status
+ self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
+ self.cli_commit()
+
+ # Test http-check with uri and status
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('option httpchk', config)
+ self.assertIn('http-check send meth GET uri /health', config)
+ self.assertIn('http-check expect status 200', config)
+
+ # Set http-check with string
+ self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success'])
+ self.cli_commit()
+
+ # Test http-check with string
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('option httpchk', config)
+ self.assertIn('http-check send meth GET uri /health', config)
+ self.assertIn('http-check expect string success', config)
+
+ # Test configuring both http-check & health-check fails validation script
+ self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap'])
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+
+ def test_06_lb_reverse_proxy_tcp_mode(self):
+ frontend = 'tcp_8443'
+ mode = 'tcp'
+ front_port = '8433'
+ tcp_request_delay = "5000"
+ rule_thirty = '30'
+ domain_bk = 'n6.example.com'
+ ssl_opt = "req-ssl-sni"
+ bk_name = 'bk-03'
+ bk_server = '192.0.2.11'
+ bk_server_port = '9090'
+
+ back_base = base_path + ['backend']
+
+ self.cli_set(base_path + ['service', frontend, 'mode', mode])
+ self.cli_set(base_path + ['service', frontend, 'port', front_port])
+ self.cli_set(base_path + ['service', frontend, 'tcp-request', 'inspect-delay', tcp_request_delay])
+
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'domain-name', domain_bk])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'ssl', ssl_opt])
+ self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_name])
+
+ self.cli_set(back_base + [bk_name, 'mode', mode])
+ self.cli_set(back_base + [bk_name, 'server', bk_name, 'address', bk_server])
+ self.cli_set(back_base + [bk_name, 'server', bk_name, 'port', bk_server_port])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(HAPROXY_CONF)
+
+ # Frontend
+ self.assertIn(f'frontend {frontend}', config)
+ self.assertIn(f'bind [::]:{front_port} v4v6', config)
+ self.assertIn(f'mode {mode}', config)
+
+ self.assertIn(f'tcp-request inspect-delay {tcp_request_delay}', config)
+ self.assertIn(f"tcp-request content accept if {{ req_ssl_hello_type 1 }}", config)
+ self.assertIn(f'acl {rule_thirty} req_ssl_sni -i {domain_bk}', config)
+ self.assertIn(f'use_backend {bk_name} if {rule_thirty}', config)
+
+ # Backend
+ self.assertIn(f'backend {bk_name}', config)
+ self.assertIn(f'balance roundrobin', config)
+ self.assertIn(f'mode {mode}', config)
+ self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config)
+
+ def test_07_lb_reverse_proxy_http_response_headers(self):
+ # Setup base
+ self.configure_pki()
+ self.base_config()
+
+ # Set example headers in both frontend and backend
+ self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
+ self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01'])
+ self.cli_commit()
+
+ # Test headers are present in generated configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config)
+ self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config)
+
+ # Test setting alongside modes other than http is blocked by validation conditions
+ self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp'])
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+
+ def test_08_lb_reverse_proxy_tcp_health_checks(self):
+ # Setup PKI
+ self.configure_pki()
+
+ # Define variables
+ frontend = 'fe_ldaps'
+ mode = 'tcp'
+ health_check = 'ldap'
+ front_port = '636'
+ bk_name = 'bk_ldap'
+ bk_servers = ['192.0.2.11', '192.0.2.12']
+ bk_server_port = '389'
+
+ # Configure frontend
+ self.cli_set(base_path + ['service', frontend, 'mode', mode])
+ self.cli_set(base_path + ['service', frontend, 'port', front_port])
+ self.cli_set(base_path + ['service', frontend, 'ssl', 'certificate', 'smoketest'])
+
+ # Configure backend
+ self.cli_set(base_path + ['backend', bk_name, 'mode', mode])
+ self.cli_set(base_path + ['backend', bk_name, 'health-check', health_check])
+ for index, bk_server in enumerate(bk_servers):
+ self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'address', bk_server])
+ self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'port', bk_server_port])
+
+ # Commit & read config
+ self.cli_commit()
+ config = read_file(HAPROXY_CONF)
+
+ # Validate Frontend
+ self.assertIn(f'frontend {frontend}', config)
+ self.assertIn(f'bind [::]:{front_port} v4v6 ssl crt /run/haproxy/smoketest.pem', config)
+ self.assertIn(f'mode {mode}', config)
+ self.assertIn(f'backend {bk_name}', config)
+
+ # Validate Backend
+ self.assertIn(f'backend {bk_name}', config)
+ self.assertIn(f'option {health_check}-check', config)
+ self.assertIn(f'mode {mode}', config)
+ for index, bk_server in enumerate(bk_servers):
+ self.assertIn(f'server srv-{index} {bk_server}:{bk_server_port}', config)
+
+ # Validate SMTP option renders correctly
+ self.cli_set(base_path + ['backend', bk_name, 'health-check', 'smtp'])
+ self.cli_commit()
+ config = read_file(HAPROXY_CONF)
+ self.assertIn(f'option smtpchk', config)
+
+ def test_09_lb_reverse_proxy_logging(self):
+ # Setup base
+ self.base_config()
+ self.cli_commit()
+
+ # Ensure default logging configuration is present
+ config = read_file(HAPROXY_CONF)
+
+ # Test global-parameters logging options
+ self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local1', 'level', 'err'])
+ self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local2', 'level', 'warning'])
+ self.cli_commit()
+
+ # Test global logging parameters are generated in configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('log /dev/log local1 err', config)
+ self.assertIn('log /dev/log local2 warning', config)
+
+ # Test backend logging options
+ backend_path = base_path + ['backend', 'bk-01']
+ self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug'])
+ self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info'])
+ self.cli_commit()
+
+ # Test backend logging parameters are generated in configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('log /dev/log local3 debug', config)
+ self.assertIn('log /dev/log local4 info', config)
+
+ # Test service logging options
+ service_path = base_path + ['service', 'https_front']
+ self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice'])
+ self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit'])
+ self.cli_commit()
+
+ # Test service logging parameters are generated in configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('log /dev/log local5 notice', config)
+ self.assertIn('log /dev/log local6 crit', config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_load-balancing_wan.py b/smoketest/scripts/cli/test_load-balancing_wan.py
new file mode 100644
index 0000000..92b4000
--- /dev/null
+++ b/smoketest/scripts/cli/test_load-balancing_wan.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import time
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+
+base_path = ['load-balancing']
+
+def create_netns(name):
+ return call(f'sudo ip netns add {name}')
+
+def create_veth_pair(local='veth0', peer='ceth0'):
+ return call(f'sudo ip link add {local} type veth peer name {peer}')
+
+def move_interface_to_netns(iface, netns_name):
+ return call(f'sudo ip link set {iface} netns {netns_name}')
+
+def rename_interface(iface, new_name):
+ return call(f'sudo ip link set {iface} name {new_name}')
+
+def cmd_in_netns(netns, cmd):
+ return call(f'sudo ip netns exec {netns} {cmd}')
+
+def delete_netns(name):
+ return call(f'sudo ip netns del {name}')
+
+class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestLoadBalancingWan, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_table_routes(self):
+ ns1 = 'ns201'
+ ns2 = 'ns202'
+ ns3 = 'ns203'
+ iface1 = 'eth201'
+ iface2 = 'eth202'
+ iface3 = 'eth203'
+ container_iface1 = 'ceth0'
+ container_iface2 = 'ceth1'
+ container_iface3 = 'ceth2'
+
+ # Create network namespeces
+ create_netns(ns1)
+ create_netns(ns2)
+ create_netns(ns3)
+ create_veth_pair(iface1, container_iface1)
+ create_veth_pair(iface2, container_iface2)
+ create_veth_pair(iface3, container_iface3)
+
+ move_interface_to_netns(container_iface1, ns1)
+ move_interface_to_netns(container_iface2, ns2)
+ move_interface_to_netns(container_iface3, ns3)
+ call(f'sudo ip address add 203.0.113.10/24 dev {iface1}')
+ call(f'sudo ip address add 192.0.2.10/24 dev {iface2}')
+ call(f'sudo ip address add 198.51.100.10/24 dev {iface3}')
+ call(f'sudo ip link set dev {iface1} up')
+ call(f'sudo ip link set dev {iface2} up')
+ call(f'sudo ip link set dev {iface3} up')
+ cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0')
+ cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0')
+ cmd_in_netns(ns3, f'ip link set {container_iface3} name eth0')
+ cmd_in_netns(ns1, 'ip address add 203.0.113.1/24 dev eth0')
+ cmd_in_netns(ns2, 'ip address add 192.0.2.1/24 dev eth0')
+ cmd_in_netns(ns3, 'ip address add 198.51.100.1/24 dev eth0')
+ cmd_in_netns(ns1, 'ip link set dev eth0 up')
+ cmd_in_netns(ns2, 'ip link set dev eth0 up')
+ cmd_in_netns(ns3, 'ip link set dev eth0 up')
+
+ # Set load-balancing configuration
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'nexthop', '192.0.2.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'success-count', '1'])
+
+ self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
+
+
+ # commit changes
+ self.cli_commit()
+
+ time.sleep(5)
+ # Check default routes in tables 201, 202
+ # Expected values
+ original = 'default via 203.0.113.1 dev eth201'
+ tmp = cmd('sudo ip route show table 201')
+ self.assertEqual(tmp, original)
+
+ original = 'default via 192.0.2.1 dev eth202'
+ tmp = cmd('sudo ip route show table 202')
+ self.assertEqual(tmp, original)
+
+ # Delete veth interfaces and netns
+ for iface in [iface1, iface2, iface3]:
+ call(f'sudo ip link del dev {iface}')
+
+ delete_netns(ns1)
+ delete_netns(ns2)
+ delete_netns(ns3)
+
+ def test_check_chains(self):
+
+ ns1 = 'nsA'
+ ns2 = 'nsB'
+ ns3 = 'nsC'
+ iface1 = 'veth1'
+ iface2 = 'veth2'
+ iface3 = 'veth3'
+ container_iface1 = 'ceth0'
+ container_iface2 = 'ceth1'
+ container_iface3 = 'ceth2'
+ mangle_isp1 = """table ip mangle {
+ chain ISP_veth1 {
+ counter ct mark set 0xc9
+ counter meta mark set 0xc9
+ counter accept
+ }
+}"""
+ mangle_isp2 = """table ip mangle {
+ chain ISP_veth2 {
+ counter ct mark set 0xca
+ counter meta mark set 0xca
+ counter accept
+ }
+}"""
+ mangle_prerouting = """table ip mangle {
+ chain PREROUTING {
+ type filter hook prerouting priority mangle; policy accept;
+ counter jump WANLOADBALANCE_PRE
+ }
+}"""
+ mangle_wanloadbalance_pre = """table ip mangle {
+ chain WANLOADBALANCE_PRE {
+ iifname "veth3" ip saddr 198.51.100.0/24 ct state new meta random & 2147483647 < 1073741824 counter jump ISP_veth1
+ iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2
+ iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark
+ }
+}"""
+ nat_wanloadbalance = """table ip nat {
+ chain WANLOADBALANCE {
+ ct mark 0xc9 counter snat to 203.0.113.10
+ ct mark 0xca counter snat to 192.0.2.10
+ }
+}"""
+ nat_vyos_pre_snat_hook = """table ip nat {
+ chain VYOS_PRE_SNAT_HOOK {
+ type nat hook postrouting priority srcnat - 1; policy accept;
+ counter jump WANLOADBALANCE
+ }
+}"""
+
+ # Create network namespeces
+ create_netns(ns1)
+ create_netns(ns2)
+ create_netns(ns3)
+ create_veth_pair(iface1, container_iface1)
+ create_veth_pair(iface2, container_iface2)
+ create_veth_pair(iface3, container_iface3)
+ move_interface_to_netns(container_iface1, ns1)
+ move_interface_to_netns(container_iface2, ns2)
+ move_interface_to_netns(container_iface3, ns3)
+ call(f'sudo ip address add 203.0.113.10/24 dev {iface1}')
+ call(f'sudo ip address add 192.0.2.10/24 dev {iface2}')
+ call(f'sudo ip address add 198.51.100.10/24 dev {iface3}')
+
+ for iface in [iface1, iface2, iface3]:
+ call(f'sudo ip link set dev {iface} up')
+
+ cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0')
+ cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0')
+ cmd_in_netns(ns3, f'ip link set {container_iface3} name eth0')
+ cmd_in_netns(ns1, 'ip address add 203.0.113.1/24 dev eth0')
+ cmd_in_netns(ns2, 'ip address add 192.0.2.1/24 dev eth0')
+ cmd_in_netns(ns3, 'ip address add 198.51.100.1/24 dev eth0')
+ cmd_in_netns(ns1, 'ip link set dev eth0 up')
+ cmd_in_netns(ns2, 'ip link set dev eth0 up')
+ cmd_in_netns(ns3, 'ip link set dev eth0 up')
+
+ # Set load-balancing configuration
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'failure-count', '2'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'nexthop', '192.0.2.1'])
+ self.cli_set(base_path + ['wan', 'interface-health', iface2, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2])
+
+ # commit changes
+ self.cli_commit()
+
+ time.sleep(5)
+
+ # Check mangle chains
+ tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}')
+ self.assertEqual(tmp, mangle_isp1)
+
+ tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}')
+ self.assertEqual(tmp, mangle_isp2)
+
+ tmp = cmd(f'sudo nft -s list chain mangle PREROUTING')
+ self.assertEqual(tmp, mangle_prerouting)
+
+ tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE')
+ self.assertEqual(tmp, mangle_wanloadbalance_pre)
+
+ # Check nat chains
+ tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE')
+ self.assertEqual(tmp, nat_wanloadbalance)
+
+ tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK')
+ self.assertEqual(tmp, nat_vyos_pre_snat_hook)
+
+ # Delete veth interfaces and netns
+ for iface in [iface1, iface2, iface3]:
+ call(f'sudo ip link del dev {iface}')
+
+ delete_netns(ns1)
+ delete_netns(ns2)
+ delete_netns(ns3)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py
new file mode 100644
index 0000000..5161e47
--- /dev/null
+++ b/smoketest/scripts/cli/test_nat.py
@@ -0,0 +1,308 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+
+base_path = ['nat']
+src_path = base_path + ['source']
+dst_path = base_path + ['destination']
+static_path = base_path + ['static']
+
+nftables_nat_config = '/run/nftables_nat.conf'
+nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
+
+class TestNAT(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestNAT, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+ self.assertFalse(os.path.exists(nftables_nat_config))
+ self.assertFalse(os.path.exists(nftables_static_nat_conf))
+
+ def wait_for_domain_resolver(self, table, set_name, element, max_wait=10):
+ # Resolver no longer blocks commit, need to wait for daemon to populate set
+ count = 0
+ while count < max_wait:
+ code = run(f'sudo nft get element {table} {set_name} {{ {element} }}')
+ if code == 0:
+ return True
+ count += 1
+ sleep(1)
+ return False
+
+ def test_snat(self):
+ rules = ['100', '110', '120', '130', '200', '210', '220', '230']
+ outbound_iface_100 = 'eth0'
+ outbound_iface_200 = 'eth1'
+
+ nftables_search = ['jump VYOS_PRE_SNAT_HOOK']
+
+ for rule in rules:
+ network = f'192.168.{rule}.0/24'
+ # depending of rule order we check either for source address for NAT
+ # or configured destination address for NAT
+ if int(rule) < 200:
+ self.cli_set(src_path + ['rule', rule, 'source', 'address', network])
+ self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'name', outbound_iface_100])
+ self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])
+ nftables_search.append([f'saddr {network}', f'oifname "{outbound_iface_100}"', 'masquerade'])
+ else:
+ self.cli_set(src_path + ['rule', rule, 'destination', 'address', network])
+ self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'name', outbound_iface_200])
+ self.cli_set(src_path + ['rule', rule, 'exclude'])
+ nftables_search.append([f'daddr {network}', f'oifname "{outbound_iface_200}"', 'return'])
+
+ self.cli_commit()
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+ def test_snat_groups(self):
+ address_group = 'smoketest_addr'
+ address_group_member = '192.0.2.1'
+ interface_group = 'smoketest_ifaces'
+ interface_group_member = 'bond.99'
+
+ self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member])
+ self.cli_set(['firewall', 'group', 'interface-group', interface_group, 'interface', interface_group_member])
+
+ self.cli_set(src_path + ['rule', '100', 'source', 'group', 'address-group', address_group])
+ self.cli_set(src_path + ['rule', '100', 'outbound-interface', 'group', interface_group])
+ self.cli_set(src_path + ['rule', '100', 'translation', 'address', 'masquerade'])
+
+ self.cli_set(src_path + ['rule', '110', 'source', 'group', 'address-group', address_group])
+ self.cli_set(src_path + ['rule', '110', 'translation', 'address', '203.0.113.1'])
+
+ self.cli_set(src_path + ['rule', '120', 'source', 'group', 'address-group', address_group])
+ self.cli_set(src_path + ['rule', '120', 'translation', 'address', '203.0.113.111/32'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'set A_{address_group}'],
+ [f'elements = {{ {address_group_member} }}'],
+ [f'ip saddr @A_{address_group}', f'oifname @I_{interface_group}', 'masquerade'],
+ [f'ip saddr @A_{address_group}', 'snat to 203.0.113.1'],
+ [f'ip saddr @A_{address_group}', 'snat prefix to 203.0.113.111/32']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+ self.cli_delete(['firewall'])
+
+ def test_dnat(self):
+ rules = ['100', '110', '120', '130', '200', '210', '220', '230']
+ inbound_iface_100 = 'eth0'
+ inbound_iface_200 = 'eth1'
+ inbound_proto_100 = 'udp'
+ inbound_proto_200 = 'tcp'
+
+ nftables_search = ['jump VYOS_PRE_DNAT_HOOK']
+
+ for rule in rules:
+ port = f'10{rule}'
+ self.cli_set(dst_path + ['rule', rule, 'source', 'port', port])
+ self.cli_set(dst_path + ['rule', rule, 'translation', 'address', '192.0.2.1'])
+ self.cli_set(dst_path + ['rule', rule, 'translation', 'port', port])
+ rule_search = [f'dnat to 192.0.2.1:{port}']
+ if int(rule) < 200:
+ self.cli_set(dst_path + ['rule', rule, 'protocol', inbound_proto_100])
+ self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'name', inbound_iface_100])
+ rule_search.append(f'{inbound_proto_100} sport {port}')
+ rule_search.append(f'iifname "{inbound_iface_100}"')
+ else:
+ self.cli_set(dst_path + ['rule', rule, 'protocol', inbound_proto_200])
+ self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'name', inbound_iface_200])
+ rule_search.append(f'iifname "{inbound_iface_200}"')
+
+ nftables_search.append(rule_search)
+
+ self.cli_commit()
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+ def test_snat_required_translation_address(self):
+ # T2813: Ensure translation address is specified
+ rule = '5'
+ self.cli_set(src_path + ['rule', rule, 'source', 'address', '192.0.2.0/24'])
+
+ # check validate() - translation address not specified
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])
+ self.cli_commit()
+
+ def test_dnat_negated_addresses(self):
+ # T3186: negated addresses are not accepted by nftables
+ rule = '1000'
+ self.cli_set(dst_path + ['rule', rule, 'destination', 'address', '!192.0.2.1'])
+ self.cli_set(dst_path + ['rule', rule, 'destination', 'port', '53'])
+ self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'name', 'eth0'])
+ self.cli_set(dst_path + ['rule', rule, 'protocol', 'tcp_udp'])
+ self.cli_set(dst_path + ['rule', rule, 'source', 'address', '!192.0.2.1'])
+ self.cli_set(dst_path + ['rule', rule, 'translation', 'address', '192.0.2.1'])
+ self.cli_set(dst_path + ['rule', rule, 'translation', 'port', '53'])
+ self.cli_commit()
+
+ def test_nat_no_rules(self):
+ # T3206: deleting all rules but keep the direction 'destination' or
+ # 'source' resulteds in KeyError: 'rule'.
+ #
+ # Test that both 'nat destination' and 'nat source' nodes can exist
+ # without any rule
+ self.cli_set(src_path)
+ self.cli_set(dst_path)
+ self.cli_set(static_path)
+ self.cli_commit()
+
+ def test_dnat_without_translation_address(self):
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '443'])
+ self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp'])
+ self.cli_set(dst_path + ['rule', '1', 'packet-type', 'host'])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'port', '443'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['iifname "eth1"', 'tcp dport 443', 'pkttype host', 'dnat to :443']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+ def test_static_nat(self):
+ dst_addr_1 = '10.0.1.1'
+ translate_addr_1 = '192.168.1.1'
+ dst_addr_2 = '203.0.113.0/24'
+ translate_addr_2 = '192.0.2.0/24'
+ ifname = 'eth0'
+
+ self.cli_set(static_path + ['rule', '10', 'destination', 'address', dst_addr_1])
+ self.cli_set(static_path + ['rule', '10', 'inbound-interface', ifname])
+ self.cli_set(static_path + ['rule', '10', 'translation', 'address', translate_addr_1])
+
+ self.cli_set(static_path + ['rule', '20', 'destination', 'address', dst_addr_2])
+ self.cli_set(static_path + ['rule', '20', 'inbound-interface', ifname])
+ self.cli_set(static_path + ['rule', '20', 'translation', 'address', translate_addr_2])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'dnat to {translate_addr_1}'],
+ [f'oifname "{ifname}"', f'ip saddr {translate_addr_1}', f'snat to {dst_addr_1}'],
+ [f'iifname "{ifname}"', f'dnat ip prefix to ip daddr map {{ {dst_addr_2} : {translate_addr_2} }}'],
+ [f'oifname "{ifname}"', f'snat ip prefix to ip saddr map {{ {translate_addr_2} : {dst_addr_2} }}']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_static_nat')
+
+ def test_dnat_redirect(self):
+ dst_addr_1 = '10.0.1.1'
+ dest_port = '5122'
+ protocol = 'tcp'
+ redirected_port = '22'
+ ifname = 'eth0'
+
+ self.cli_set(dst_path + ['rule', '10', 'destination', 'address', dst_addr_1])
+ self.cli_set(dst_path + ['rule', '10', 'destination', 'port', dest_port])
+ self.cli_set(dst_path + ['rule', '10', 'protocol', protocol])
+ self.cli_set(dst_path + ['rule', '10', 'inbound-interface', 'name', ifname])
+ self.cli_set(dst_path + ['rule', '10', 'translation', 'redirect', 'port', redirected_port])
+
+ self.cli_set(dst_path + ['rule', '20', 'destination', 'address', dst_addr_1])
+ self.cli_set(dst_path + ['rule', '20', 'destination', 'port', dest_port])
+ self.cli_set(dst_path + ['rule', '20', 'protocol', protocol])
+ self.cli_set(dst_path + ['rule', '20', 'inbound-interface', 'name', ifname])
+ self.cli_set(dst_path + ['rule', '20', 'translation', 'redirect'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'{protocol} dport {dest_port}', f'redirect to :{redirected_port}'],
+ [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'{protocol} dport {dest_port}', f'redirect']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+ def test_nat_balance(self):
+ ifname = 'eth0'
+ member_1 = '198.51.100.1'
+ weight_1 = '10'
+ member_2 = '198.51.100.2'
+ weight_2 = '90'
+ member_3 = '192.0.2.1'
+ weight_3 = '35'
+ member_4 = '192.0.2.2'
+ weight_4 = '65'
+ dst_port = '443'
+
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', ifname])
+ self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dst_port])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'source-address'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'source-port'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'destination-address'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'destination-port'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_1, 'weight', weight_1])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_2, 'weight', weight_2])
+
+ self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', ifname])
+ self.cli_set(src_path + ['rule', '1', 'load-balance', 'hash', 'random'])
+ self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_3, 'weight', weight_3])
+ self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_4, 'weight', weight_4])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'iifname "{ifname}"', f'tcp dport {dst_port}', f'dnat to jhash ip saddr . tcp sport . ip daddr . tcp dport mod 100 map', f'0-9 : {member_1}, 10-99 : {member_2}'],
+ [f'oifname "{ifname}"', f'snat to numgen random mod 100 map', f'0-34 : {member_3}, 35-99 : {member_4}']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+ def test_snat_net_port_map(self):
+ self.cli_set(src_path + ['rule', '10', 'protocol', 'tcp_udp'])
+ self.cli_set(src_path + ['rule', '10', 'source', 'address', '100.64.0.0/25'])
+ self.cli_set(src_path + ['rule', '10', 'translation', 'address', '203.0.113.0/25'])
+ self.cli_set(src_path + ['rule', '10', 'translation', 'port', '1025-3072'])
+
+ self.cli_set(src_path + ['rule', '20', 'protocol', 'tcp_udp'])
+ self.cli_set(src_path + ['rule', '20', 'source', 'address', '100.64.0.128/25'])
+ self.cli_set(src_path + ['rule', '20', 'translation', 'address', '203.0.113.128/25'])
+ self.cli_set(src_path + ['rule', '20', 'translation', 'port', '1025-3072'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['meta l4proto { tcp, udp }', 'snat ip prefix to ip saddr map { 100.64.0.0/25 : 203.0.113.0/25 . 1025-3072 }', 'comment "SRC-NAT-10"'],
+ ['meta l4proto { tcp, udp }', 'snat ip prefix to ip saddr map { 100.64.0.128/25 : 203.0.113.128/25 . 1025-3072 }', 'comment "SRC-NAT-20"']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_nat')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_nat64.py b/smoketest/scripts/cli/test_nat64.py
new file mode 100644
index 0000000..5c907f6
--- /dev/null
+++ b/smoketest/scripts/cli/test_nat64.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+base_path = ['nat64']
+src_path = base_path + ['source']
+
+jool_nat64_config = '/run/jool/instance-100.json'
+
+class TestNAT64(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestNAT64, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+ self.assertFalse(os.path.exists(jool_nat64_config))
+
+ def test_snat64(self):
+ rule = '100'
+ translation_rule = '10'
+ prefix_v6 = '64:ff9b::/96'
+ pool = '192.0.2.10'
+ pool_port = '1-65535'
+
+ self.cli_set(src_path + ['rule', rule, 'source', 'prefix', prefix_v6])
+ self.cli_set(
+ src_path
+ + ['rule', rule, 'translation', 'pool', translation_rule, 'address', pool]
+ )
+ self.cli_set(
+ src_path
+ + ['rule', rule, 'translation', 'pool', translation_rule, 'port', pool_port]
+ )
+ self.cli_commit()
+
+ # Load the JSON file
+ with open(f'/run/jool/instance-{rule}.json', 'r') as json_file:
+ config_data = json.load(json_file)
+
+ # Assertions based on the content of the JSON file
+ self.assertEqual(config_data['instance'], f'instance-{rule}')
+ self.assertEqual(config_data['framework'], 'netfilter')
+ self.assertEqual(config_data['global']['pool6'], prefix_v6)
+ self.assertTrue(config_data['global']['manually-enabled'])
+
+ # Check the pool4 entries
+ pool4_entries = config_data.get('pool4', [])
+ self.assertIsInstance(pool4_entries, list)
+ self.assertGreater(len(pool4_entries), 0)
+
+ for entry in pool4_entries:
+ self.assertIn('protocol', entry)
+ self.assertIn('prefix', entry)
+ self.assertIn('port range', entry)
+
+ protocol = entry['protocol']
+ prefix = entry['prefix']
+ port_range = entry['port range']
+
+ if protocol == 'ICMP':
+ self.assertEqual(prefix, pool)
+ self.assertEqual(port_range, pool_port)
+ elif protocol == 'UDP':
+ self.assertEqual(prefix, pool)
+ self.assertEqual(port_range, pool_port)
+ elif protocol == 'TCP':
+ self.assertEqual(prefix, pool)
+ self.assertEqual(port_range, pool_port)
+ else:
+ self.fail(f'Unexpected protocol: {protocol}')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py
new file mode 100644
index 0000000..52ad8e3
--- /dev/null
+++ b/smoketest/scripts/cli/test_nat66.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+
+base_path = ['nat66']
+src_path = base_path + ['source']
+dst_path = base_path + ['destination']
+
+class TestNAT66(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestNAT66, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_source_nat66(self):
+ source_prefix = 'fc00::/64'
+ translation_prefix = 'fc01::/64'
+ self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth1'])
+ self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix])
+ self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix])
+
+ self.cli_set(src_path + ['rule', '2', 'outbound-interface', 'name', 'eth1'])
+ self.cli_set(src_path + ['rule', '2', 'source', 'prefix', source_prefix])
+ self.cli_set(src_path + ['rule', '2', 'translation', 'address', 'masquerade'])
+
+ self.cli_set(src_path + ['rule', '3', 'outbound-interface', 'name', 'eth1'])
+ self.cli_set(src_path + ['rule', '3', 'source', 'prefix', source_prefix])
+ self.cli_set(src_path + ['rule', '3', 'exclude'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', f'snat prefix to {translation_prefix}'],
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', 'masquerade'],
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', 'return']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_source_nat66_address(self):
+ source_prefix = 'fc00::/64'
+ translation_address = 'fc00::1'
+ self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth1'])
+ self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix])
+ self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_address])
+
+ # check validate() - outbound-interface must be defined
+ self.cli_commit()
+
+ nftables_search = [
+ ['oifname "eth1"', f'ip6 saddr {source_prefix}', f'snat to {translation_address}']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_destination_nat66(self):
+ destination_address = 'fc00::1'
+ translation_address = 'fc01::1'
+ source_address = 'fc02::1'
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'address', destination_address])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_address])
+
+ self.cli_set(dst_path + ['rule', '2', 'inbound-interface', 'name', 'eth1'])
+ self.cli_set(dst_path + ['rule', '2', 'destination', 'address', destination_address])
+ self.cli_set(dst_path + ['rule', '2', 'source', 'address', source_address])
+ self.cli_set(dst_path + ['rule', '2', 'exclude'])
+
+ # check validate() - outbound-interface must be defined
+ self.cli_commit()
+
+ nftables_search = [
+ ['iifname "eth1"', 'ip6 daddr fc00::1', 'dnat to fc01::1'],
+ ['iifname "eth1"', 'ip6 saddr fc02::1', 'ip6 daddr fc00::1', 'return']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_destination_nat66_protocol(self):
+ translation_address = '2001:db8:1111::1'
+ source_prefix = '2001:db8:2222::/64'
+ dport = '4545'
+ sport = '8080'
+ tport = '5555'
+ proto = 'tcp'
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dport])
+ self.cli_set(dst_path + ['rule', '1', 'source', 'address', source_prefix])
+ self.cli_set(dst_path + ['rule', '1', 'source', 'port', sport])
+ self.cli_set(dst_path + ['rule', '1', 'protocol', proto])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_address])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'port', tport])
+
+ # check validate() - outbound-interface must be defined
+ self.cli_commit()
+
+ nftables_search = [
+ ['iifname "eth1"', 'tcp dport 4545', 'ip6 saddr 2001:db8:2222::/64', 'tcp sport 8080', 'dnat to [2001:db8:1111::1]:5555']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_destination_nat66_prefix(self):
+ destination_prefix = 'fc00::/64'
+ translation_prefix = 'fc01::/64'
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'address', destination_prefix])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_prefix])
+
+ # check validate() - outbound-interface must be defined
+ self.cli_commit()
+
+ nftables_search = [
+ ['iifname "eth1"', f'ip6 daddr {destination_prefix}', f'dnat prefix to {translation_prefix}']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_destination_nat66_network_group(self):
+ address_group = 'smoketest_addr'
+ address_group_member = 'fc00::1'
+ network_group = 'smoketest_net'
+ network_group_member = 'fc00::/64'
+ translation_prefix = 'fc01::/64'
+
+ self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member])
+ self.cli_set(['firewall', 'group', 'ipv6-network-group', network_group, 'network', network_group_member])
+
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'group', 'address-group', address_group])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_prefix])
+
+ self.cli_set(dst_path + ['rule', '2', 'destination', 'group', 'network-group', network_group])
+ self.cli_set(dst_path + ['rule', '2', 'translation', 'address', translation_prefix])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'set A6_{address_group}'],
+ [f'elements = {{ {address_group_member} }}'],
+ [f'set N6_{network_group}'],
+ [f'elements = {{ {network_group_member} }}'],
+ ['ip6 daddr', f'@A6_{address_group}', 'dnat prefix to fc01::/64'],
+ ['ip6 daddr', f'@N6_{network_group}', 'dnat prefix to fc01::/64']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+
+ def test_destination_nat66_without_translation_address(self):
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '443'])
+ self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp'])
+ self.cli_set(dst_path + ['rule', '1', 'translation', 'port', '443'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['iifname "eth1"', 'tcp dport 443', 'dnat to :443']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_source_nat66_required_translation_prefix(self):
+ # T2813: Ensure translation address is specified
+ rule = '5'
+ source_prefix = 'fc00::/64'
+ self.cli_set(src_path + ['rule', rule, 'source', 'prefix', source_prefix])
+
+ # check validate() - outbound-interface must be defined
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'name', 'eth0'])
+
+ # check validate() - translation address not specified
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])
+ self.cli_commit()
+
+ def test_source_nat66_protocol(self):
+ translation_address = '2001:db8:1111::1'
+ source_prefix = '2001:db8:2222::/64'
+ dport = '9999'
+ sport = '8080'
+ tport = '80'
+ proto = 'tcp'
+ self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth1'])
+ self.cli_set(src_path + ['rule', '1', 'destination', 'port', dport])
+ self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix])
+ self.cli_set(src_path + ['rule', '1', 'source', 'port', sport])
+ self.cli_set(src_path + ['rule', '1', 'protocol', proto])
+ self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_address])
+ self.cli_set(src_path + ['rule', '1', 'translation', 'port', tport])
+
+ # check validate() - outbound-interface must be defined
+ self.cli_commit()
+
+ nftables_search = [
+ ['oifname "eth1"', 'ip6 saddr 2001:db8:2222::/64', 'tcp dport 9999', 'tcp sport 8080', 'snat to [2001:db8:1111::1]:80']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
+ def test_nat66_no_rules(self):
+ # T3206: deleting all rules but keep the direction 'destination' or
+ # 'source' resulteds in KeyError: 'rule'.
+ #
+ # Test that both 'nat destination' and 'nat source' nodes can exist
+ # without any rule
+ self.cli_set(src_path)
+ self.cli_set(dst_path)
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_netns.py b/smoketest/scripts/cli/test_netns.py
new file mode 100644
index 0000000..2ac603a
--- /dev/null
+++ b/smoketest/scripts/cli/test_netns.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.process import cmd
+from vyos.utils.network import is_netns_interface
+from vyos.utils.network import get_netns_all
+
+base_path = ['netns']
+interfaces = ['dum10', 'dum12', 'dum50']
+
+class NetNSTest(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ # commit changes
+ self.cli_commit()
+
+ # There should be no network namespace remaining
+ tmp = cmd('ip netns ls')
+ self.assertFalse(tmp)
+
+ super(NetNSTest, self).tearDown()
+
+ def test_netns_create(self):
+ namespaces = ['mgmt', 'front', 'back']
+ for netns in namespaces:
+ self.cli_set(base_path + ['name', netns])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify NETNS configuration
+ for netns in namespaces:
+ self.assertIn(netns, get_netns_all())
+
+ def test_netns_interface(self):
+ netns = 'foo'
+ self.cli_set(base_path + ['name', netns])
+
+ # Set
+ for iface in interfaces:
+ self.cli_set(['interfaces', 'dummy', iface, 'netns', netns])
+
+ # commit changes
+ self.cli_commit()
+
+ for interface in interfaces:
+ self.assertTrue(is_netns_interface(interface, netns))
+
+ # Delete
+ for interface in interfaces:
+ self.cli_delete(['interfaces', 'dummy', interface])
+
+ # commit changes
+ self.cli_commit()
+
+ netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show')
+
+ for interface in interfaces:
+ self.assertFalse(is_netns_interface(interface, netns))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_op-mode_show.py b/smoketest/scripts/cli/test_op-mode_show.py
new file mode 100644
index 0000000..62f8e88
--- /dev/null
+++ b/smoketest/scripts/cli/test_op-mode_show.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.process import cmd
+from vyos.version import get_version
+
+base_path = ['show']
+
+class TestOPModeShow(VyOSUnitTestSHIM.TestCase):
+ def test_op_mode_show_version(self):
+ # Retrieve output of "show version" OP-mode command
+ tmp = self.op_mode(base_path + ['version'])
+ # Validate
+ version = get_version()
+ self.assertIn(f'Version: VyOS {version}', tmp)
+
+ def test_op_mode_show_version_kernel(self):
+ # Retrieve output of "show version" OP-mode command
+ tmp = self.op_mode(base_path + ['version', 'kernel'])
+ self.assertEqual(cmd('uname -r'), tmp)
+
+ def test_op_mode_show_vrf(self):
+ # Retrieve output of "show version" OP-mode command
+ tmp = self.op_mode(base_path + ['vrf'])
+ # Validate
+ self.assertIn('VRF is not configured', tmp)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py
new file mode 100644
index 0000000..02beafb
--- /dev/null
+++ b/smoketest/scripts/cli/test_pki.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+
+from vyos.utils.file import read_file
+
+base_path = ['pki']
+
+valid_ca_cert = """
+MIIDgTCCAmmgAwIBAgIUeM0mATGs+sKF7ViBM6DEf9fQ19swDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHVnlPUyBDQTAeFw0y
+MTA2MjgxMzE2NDZaFw0yNjA2MjcxMzE2NDZaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB1Z5T1MgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDK98WwZIqgC6teHPSsyKLLRtboy55aisJN0D3iHJ8WGKkDmIrdCR2LI4J5
+C82ErfPOzl4Ck4vTmqh8wnuK/dhUxxzNdFJBMPHAe/E+UawYrubtJj5g8iHYowZJ
+T5HQKnZbcqlPvl6EizA+etO48WGljKhpimj9/LVTp81+BtFNP4tJ/vOl+iqyJ0+P
+xiqQNDJgAF18meQRKaT9CcXycsciG9snMlB1tdOR7KDbi8lJ86lOi5ukPJaiMgWE
+u4UlyFVyHJ/68NvtwRhYerMoQquqDs21OXkOd8spZL6qEsxMeK8InedA7abPaxgx
+ORpHguPQV4Ib5HBH9Chdb9zBMheZAgMBAAGjRTBDMA8GA1UdEwEB/wQFMAMBAf8w
+IAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIB
+hjANBgkqhkiG9w0BAQsFAAOCAQEAbwJZifMEDbrKPQfGLp7ZA1muM728o4EYmmE7
+9eWwH22wGMSZI7T2xr5zRlFLs+Jha917yQK4b5xBMjQRAJlHKjzNLJ+3XaGlnWja
+TBJ2SC5YktrmXRAIS7PxTRk/r1bHs/D00+sEWewbFYr8Js4a1Cv4TksTNyjHx8pv
+phA+KIx/4qdojTslz+oH/cakUz0M9fh2B2xsO4bab5vX+LGLCK7jjeAL4Zyjf1hD
+yx+Ri79L5N8h4Q69fER4cIkW7KVKUOyjEg3N4ST56urdycmyq9bXFz5pRxuZLInA
+6RRToJrL8i0aPLJ6SyMujfREfjqOxdW5vyNF5/RkY+5Nz8JMgQ==
+"""
+
+valid_ca_private_key = """
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK98WwZIqgC6te
+HPSsyKLLRtboy55aisJN0D3iHJ8WGKkDmIrdCR2LI4J5C82ErfPOzl4Ck4vTmqh8
+wnuK/dhUxxzNdFJBMPHAe/E+UawYrubtJj5g8iHYowZJT5HQKnZbcqlPvl6EizA+
+etO48WGljKhpimj9/LVTp81+BtFNP4tJ/vOl+iqyJ0+PxiqQNDJgAF18meQRKaT9
+CcXycsciG9snMlB1tdOR7KDbi8lJ86lOi5ukPJaiMgWEu4UlyFVyHJ/68NvtwRhY
+erMoQquqDs21OXkOd8spZL6qEsxMeK8InedA7abPaxgxORpHguPQV4Ib5HBH9Chd
+b9zBMheZAgMBAAECggEAa/CK5L0DcAvkrd9OS9lDokFhJ1qqM1KZ9NHrJyW7gP/K
+Wow0RUqEuKtAxuj8+jOcdn4PRuV6tiUIt5iiJQ/MjYF6ktTqrZq+5nPDnzXGBTZ2
+vuXYxKvgThqczD4RuJfsa8O1wR/nmit/k6q0kCVmnakJI1+laHWNZRjXUs+DXcWb
+rUN5D4/5kyjvFilH1c8arfrO2O4DcwfX1zNbxicgYrGmjE5m6WCZKWdcgpBcIQSh
+ZfNATfXIEZ16WmDIFZnuOEUtFAzweR2ataLQNoyaTUeEe6g+ZDtUQIGKR/f0+Z4T
+/JMJfPX/vRn0l3nRJWWC7Okpa2xb0hVdBmS/op+TNQKBgQDvNGAkS4uUx8xw724k
+zCKQJRnzR80AQ6b2FoqRbAevWm+i0ntsCMyvCItAQS8Bw+9fgITvsmd9SdYPncMQ
+Z1oQYPk5yso/SPUyuNPXtygDxUP1xS1yja5MObqyrq2O2EzcxiVxEHGlZMLTNxNA
+1tE8nF4c0nQpV/EfLtkQFnnUSwKBgQDZOA2hiLaiDlPj03S4UXDu6aUD2o07782C
+UKl6A331ZhH/8zGEiUvBKg8IG/2FyCHQDC0C6rbfoarAhrRGbDHKkDTKNmThTj+I
+YBkLt/5OATvqkEw8eL0nB+PY5JKH04/jE0F/YM/StUsgxvMCVhtp0u/d2Hq4V9sk
+xah6oFbtKwKBgGEvs3wroWtyffLIpMSYl9Ze7Js2aekYk4ZahDQvYzPwl3jc8b5k
+GN1oqEMT+MhL1j7EFb7ZikiSLkGsBGvuwd3zuG6toNxzhQP1qkRzqvNVO5ZoZV2s
+iMt5jQw6AlQON7RfYSj92F6tgKaWMuFeJibtFSO6se12SIY134U0zIzfAoGAQWF7
+yNkrj4+cdICbKzdoNKEiyAwqYpYFV2oL+OvAJ/L3DAEZMHla0eNk7t3t6yyX8NUZ
+Xz1imeFBUf25mVDLk9rf6NWCe8ZfnR6/qyVQaA47CJkyOSlmVa8sR4ZVDIkDUCfl
+mP98zkE/QbhgQJ3GVo3lIPMdzQq0rVbJJU/Jmk0CgYEAtHRNaoKBsxKfb7N7ewla
+MzwcULIORODjWM8MUXM+R50F/2uYMiTvpz6eIUVfXoFyQoioYI8kcDZ8NamiQIS7
+uZsHfKpgMDJkV3kOoZQusoDhasGQ0SOnxbz/y0XmNUtAePipH0jPY1SYUvWbvm2y
+a4aWVhBFly9hi2ZeHiVxVhk=
+"""
+
+valid_cert = """
+MIIB9zCCAZygAwIBAgIUQ5G1nyASL/YsKGyLNGhRPPQyo4kwCgYIKoZIzj0EAwIw
+XjELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEXMBUGA1UEAwwOVnlPUyBUZXN0IENlcnQw
+HhcNMjEwNjI4MTMyNjIyWhcNMjIwNjI4MTMyNjIyWjBeMQswCQYDVQQGEwJHQjET
+MBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJU29tZS1DaXR5MQ0wCwYDVQQK
+DARWeU9TMRcwFQYDVQQDDA5WeU9TIFRlc3QgQ2VydDBZMBMGByqGSM49AgEGCCqG
+SM49AwEHA0IABBsebIt+8rr2UysTpL8NnYUtmt47e3sC3H9IO8iI/N4uFrmGVgTL
+E2G+RDGzZgG/r7LviJSTuE9HX7wHLcIr0SmjODA2MAwGA1UdEwEB/wQCMAAwFgYD
+VR0lAQH/BAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMC
+A0kAMEYCIQD5xK5kdC3TJ7SZrBGvzIM7E7Cil/KZJUyQDR9eFNNZVQIhALg8DTfr
+wAawf8L+Ncjn/l2gd5cB0nGij0D7uYnm3zf/
+"""
+
+valid_dh_params = """
+MIIBCAKCAQEAnNldZCrJk5MxhFoUlvvaYmUO+TmtL0uL62H2RIHJ+O0R+8vzdGPh
+6zDAzo46EJK735haUgu8+A1RTsXDOXcwBqDlVe0hYj9KaPHz1HpfNKntpoPCJAYJ
+wiH8dd5zVMH+iBwEKlrfteV9vWHn0HUxgLJFSLp5o6y0qpKPREJu6k0XguGScrPa
+Iw6RUwsoDy3unHfk+YeC0o040R18F75V1mXWTjQlEgM7ZO2JZkLGkhW30jB0vSHr
+krFqOvtPUiyG7r3+j18IUYLTN0s+5FOCfCjvSVKibNlB1vUz5y/9Ve8roctpkRM/
+5R5FA0mtbl7U/yMSX4FRIQ/A9BlHiu4bowIBAg==
+"""
+valid_public_ec_key = """
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAoInInwjlu/3+wDqvRa/Eyg3EMvB
+pPyq2v4jqEtEh2n4lOCi7ZgNjr+1sQSvrn8mccpALYl3/RKOougC5oQzCg==
+"""
+
+valid_private_rsa_key = """
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDoAVyJPpcLBFs
+2NdS1qMOSj7mwKBKVZiBN3nqbLiOvEHbVe22UMNvUFU3sGs2Ta2zXwhPF3d6vPPs
+GlYTkO3XAffMSNXhjCsvWHiIOR4JrWf598Bpt+txBsxsa12kM3/HM7RDf3zdN2gT
+twzrcWzu+zOTXlqJ2OSq/BRRZO9IMbQLQ1/h42GJHEr4THnY4zDqUjmMmIuiBXn4
+xoE4KFLH1+xPTVleeKvPPeJ1wsshoUjlXYOgcsrXasDUt5gtkkXsVQwR9Lvbh+Rc
+BhT+tJmrX9Cwq4YAd3tLSNJARS9HanRZ8uV0RTyZsImdw1Fr5ySpG2oEp/Z5mbL6
+QYqDmQ+DAgMBAAECggEAGu7qMQf0TEJo98J3CtmwQ2Rnep+ksfdM8uVvbJ4hXs1+
+h7Mx8jr2XVoDEZLBgA17z8lSvIjvkz92mdgaZ8E5bbPAqSiSAeapf3A/0AmFIDH2
+scyxehyvVrVn6blygAvzGLr+o5hm2ZIqSySVq8jHBbQiKrT/5CCvgvcH2Rj7dMXd
+T5lL73tCRJZsgvFNlxyj4Omj9Lh7SjL+tIwEQaLFbvANXrZ/BPyw4OlK8daBNg9b
+5GvJSDitAVMgDEEApGYu1iNwMM4UJSQAC27eJdr+qJO6DDqktWOyWcyXrxJ9mDVK
+FNbb9QNQZDj7bFfm6rCuSdH9yYe3vly+SNJqtyCiwQKBgQDvemt/57KiwQffmoKR
+65NAZsQvmA4PtELYOV8NPeYH1BZN/EPmCc74iELJdQPFDYy903aRJEPGt7jfqprd
+PexLwt73P/XiUjPrsbqgJqfF/EMiczxAktyW3xBt2lIWU1MUUmO1ps+ZZEg8Ks4e
+K/3+FWqbwZ8drDBUT9BthUA0oQKBgQDRHxU6bu938PGweFJcIG6U21nsYaWiwCiT
+LXA5vWZ+UEqz81BUye6tIcCDgeku3HvC/0ycvrBM9F4AZCjnnEvrAJHKl6e4j+C4
+IpghGQvRvQ9ihDs9JIHnaoUC1i8dE3ISbbp1r7CN+J/HnAC2OeECMJuffXdnkVWa
+xRdxU+9towKBgCwFVeNyJO00DI126o+GPVA2U9Pn4JXUbgEvMqDNgw5nVx5Iw/Zy
+USBwc85yexnq7rcqOv5dKzRJK2u6AbOvoVMf5DqRAFL1B2RJDGRKFscXIwQfKLE6
+DeCR6oQ3AKXn9TqkFn4axsiMnZapy6/SKGNfbnRpOCWNNGkbLtYjC3VhAoGAN0kO
+ZapaaM0sOEk3DOAOHBB5j4KpNYOztmU23Cz0YcR8W2KiBCh2jxLzQFEiAp+LoJu5
+9156YX3hNB1GqySo9XHrGTJKxwJSmJucuHNUqphe7t6igqGaLkH89CkHv5oaeEDG
+IMLX3FC0fSMDFSnsEJYlLl8PKDRF+2rLrcxQ6h0CgYAZllNu8a7tE6cM6QsCILQn
+NjuLuZRX8/KYWRqBJxatwZXCcMe2jti1HKTVVVCyYffOFa1QcAjCPknAmAz80l3e
+g6a75NnEXo0J6YLAOOxd8fD2/HidhbceCmTF+3msidIzCsBidBkgn6V5TXx2IyMS
+xGsJxVHfSKeooUQn6q76sg==
+"""
+
+valid_update_cert = """
+MIICJTCCAcugAwIBAgIUZJqjNmPfVQwePjNFBtB6WI31ThMwCgYIKoZIzj0EAwIw
+VzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yMjA1
+MzExNTE3NDlaFw0yMzA1MzExNTE3NDlaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQI
+DApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1Mx
+EDAOBgNVBAMMB3Z5b3MuaW8wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQMe0h/
+3CdD8mEgy+klk55QfJ8R3ZycefxCn4abWjzTXz/TuCIxqb4wpRT8DZtIn4NRimFT
+mODYdEDOYxFtZm37o3UwczAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAT
+BgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUqH7KSZpzArpMFuxLXqI8e1QD
+fBkwHwYDVR0jBBgwFoAUqH7KSZpzArpMFuxLXqI8e1QDfBkwCgYIKoZIzj0EAwID
+SAAwRQIhAKofUgRtcUljmbubPF6sqHtn/3TRvuafl8VfPbk3s2bJAiBp3Q1AnU/O
+i7t5FGhCgnv5m8DW2F3LZPCJdW4ELQ3d9A==
+"""
+
+valid_update_private_key = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvyODf22w/p7Zgfz9
+dyLIT09LqLOrUN6zbAecfukiiiyhRANCAAQMe0h/3CdD8mEgy+klk55QfJ8R3Zyc
+efxCn4abWjzTXz/TuCIxqb4wpRT8DZtIn4NRimFTmODYdEDOYxFtZm37
+"""
+
+class TestPKI(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestPKI, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['service', 'https'])
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_valid_pki(self):
+ # Valid CA
+ self.cli_set(base_path + ['ca', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['ca', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
+
+ # Valid cert
+ self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')])
+
+ # Valid DH
+ self.cli_set(base_path + ['dh', 'smoketest', 'parameters', valid_dh_params.replace('\n','')])
+
+ # Valid public key
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'public', 'key', valid_public_ec_key.replace('\n','')])
+
+ # Valid private key
+ self.cli_set(base_path + ['key-pair', 'smoketest1', 'private', 'key', valid_private_rsa_key.replace('\n','')])
+ self.cli_commit()
+
+ def test_invalid_ca_valid_certificate(self):
+ self.cli_set(base_path + ['ca', 'invalid-ca', 'certificate', valid_cert.replace('\n','')])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_certificate_in_use(self):
+ cert_name = 'smoketest'
+
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')])
+ self.cli_commit()
+
+ self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name])
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['certificate', cert_name])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(['service', 'https', 'certificates', 'certificate'])
+
+ def test_certificate_https_update(self):
+ cert_name = 'smoke-test_foo'
+ cert_path = f'/run/nginx/certs/{cert_name}_cert.pem'
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')])
+ self.cli_commit()
+
+ self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name])
+ self.cli_commit()
+
+ cert_data = None
+
+ cert_data = read_file(cert_path)
+
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')])
+ self.cli_commit()
+
+ self.assertNotEqual(cert_data, read_file(cert_path))
+
+ self.cli_delete(['service', 'https', 'certificates', 'certificate'])
+
+ def test_certificate_eapol_update(self):
+ cert_name = 'eapol'
+ interface = 'eth1'
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')])
+ self.cli_commit()
+
+ self.cli_set(['interfaces', 'ethernet', interface, 'eapol', 'certificate', cert_name])
+ self.cli_commit()
+
+ cert_data = None
+
+ with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f:
+ cert_data = f.read()
+
+ self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')])
+ self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')])
+ self.cli_commit()
+
+ with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f:
+ self.assertNotEqual(cert_data, f.read())
+
+ self.cli_delete(['interfaces', 'ethernet', interface, 'eapol'])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py
new file mode 100644
index 0000000..a0c6ab0
--- /dev/null
+++ b/smoketest/scripts/cli/test_policy.py
@@ -0,0 +1,1994 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+
+base_path = ['policy']
+
+class TestPolicy(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_access_list(self):
+ acls = {
+ '50' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'source' : { 'any' : '' },
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'source' : { 'host' : '1.2.3.4' },
+ },
+ },
+ },
+ '150' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'source' : { 'any' : '' },
+ 'destination' : { 'host' : '2.2.2.2' },
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'source' : { 'any' : '' },
+ 'destination' : { 'any' : '' },
+ },
+ },
+ },
+ '2000' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'destination' : { 'any' : '' },
+ 'source' : { 'network' : '10.0.0.0', 'inverse-mask' : '0.255.255.255' },
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'destination' : { 'any' : '' },
+ 'source' : { 'network' : '172.16.0.0', 'inverse-mask' : '0.15.255.255' },
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'destination' : { 'any' : '' },
+ 'source' : { 'network' : '192.168.0.0', 'inverse-mask' : '0.0.255.255' },
+ },
+ '20' : {
+ 'action' : 'permit',
+ 'destination' : { 'network' : '172.16.0.0', 'inverse-mask' : '0.15.255.255' },
+ 'source' : { 'network' : '10.0.0.0', 'inverse-mask' : '0.255.255.255' },
+ },
+ '25' : {
+ 'action' : 'deny',
+ 'destination' : { 'network' : '192.168.0.0', 'inverse-mask' : '0.0.255.255' },
+ 'source' : { 'network' : '172.16.0.0', 'inverse-mask' : '0.15.255.255' },
+ },
+ '30' : {
+ 'action' : 'deny',
+ 'destination' : { 'any' : '' },
+ 'source' : { 'any' : '' },
+ },
+ },
+ },
+ }
+
+ for acl, acl_config in acls.items():
+ path = base_path + ['access-list', acl]
+ self.cli_set(path + ['description', f'VyOS-ACL-{acl}'])
+ if 'rule' not in acl_config:
+ continue
+
+ for rule, rule_config in acl_config['rule'].items():
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ for direction in ['source', 'destination']:
+ if direction in rule_config:
+ if 'any' in rule_config[direction]:
+ self.cli_set(path + ['rule', rule, direction, 'any'])
+ if 'host' in rule_config[direction]:
+ self.cli_set(path + ['rule', rule, direction, 'host', rule_config[direction]['host']])
+ if 'network' in rule_config[direction]:
+ self.cli_set(path + ['rule', rule, direction, 'network', rule_config[direction]['network']])
+ self.cli_set(path + ['rule', rule, direction, 'inverse-mask', rule_config[direction]['inverse-mask']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('access-list', end='')
+ for acl, acl_config in acls.items():
+ for rule, rule_config in acl_config['rule'].items():
+ tmp = f'access-list {acl} seq {rule}'
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ if {'source', 'destination'} <= set(rule_config):
+ tmp += ' ip'
+
+ for direction in ['source', 'destination']:
+ if direction in rule_config:
+ if 'any' in rule_config[direction]:
+ tmp += ' any'
+ if 'host' in rule_config[direction]:
+ # XXX: Some weird side rule from the old vyatta days
+ # possible to clean this up after the vyos-1x migration
+ if int(acl) in range(100, 200) or int(acl) in range(2000, 2700):
+ tmp += ' host'
+
+ tmp += ' ' + rule_config[direction]['host']
+ if 'network' in rule_config[direction]:
+ tmp += ' ' + rule_config[direction]['network'] + ' ' + rule_config[direction]['inverse-mask']
+
+ self.assertIn(tmp, config)
+
+ def test_access_list6(self):
+ acls = {
+ '50' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'source' : { 'any' : '' },
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'source' : { 'network' : '2001:db8:10::/48', 'exact-match' : '' },
+ },
+ '15' : {
+ 'action' : 'deny',
+ 'source' : { 'network' : '2001:db8:20::/48' },
+ },
+ },
+ },
+ '100' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'deny',
+ 'source' : { 'network' : '2001:db8:10::/64', 'exact-match' : '' },
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'source' : { 'network' : '2001:db8:20::/64', },
+ },
+ '15' : {
+ 'action' : 'deny',
+ 'source' : { 'network' : '2001:db8:30::/64', 'exact-match' : '' },
+ },
+ '20' : {
+ 'action' : 'deny',
+ 'source' : { 'network' : '2001:db8:40::/64', 'exact-match' : '' },
+ },
+ '25' : {
+ 'action' : 'deny',
+ 'source' : { 'any' : '' },
+ },
+ },
+ },
+ }
+
+ for acl, acl_config in acls.items():
+ path = base_path + ['access-list6', acl]
+ self.cli_set(path + ['description', f'VyOS-ACL-{acl}'])
+ if 'rule' not in acl_config:
+ continue
+
+ for rule, rule_config in acl_config['rule'].items():
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ for direction in ['source', 'destination']:
+ if direction in rule_config:
+ if 'any' in rule_config[direction]:
+ self.cli_set(path + ['rule', rule, direction, 'any'])
+ if 'network' in rule_config[direction]:
+ self.cli_set(path + ['rule', rule, direction, 'network', rule_config[direction]['network']])
+ if 'exact-match' in rule_config[direction]:
+ self.cli_set(path + ['rule', rule, direction, 'exact-match'])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('ipv6 access-list', end='')
+ for acl, acl_config in acls.items():
+ for rule, rule_config in acl_config['rule'].items():
+ tmp = f'ipv6 access-list {acl} seq {rule}'
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ if {'source', 'destination'} <= set(rule_config):
+ tmp += ' ip'
+
+ for direction in ['source', 'destination']:
+ if direction in rule_config:
+ if 'any' in rule_config[direction]:
+ tmp += ' any'
+ if 'network' in rule_config[direction]:
+ tmp += ' ' + rule_config[direction]['network']
+ if 'exact-match' in rule_config[direction]:
+ tmp += ' exact-match'
+
+ self.assertIn(tmp, config)
+
+
+ def test_as_path_list(self):
+ test_data = {
+ 'VyOS' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '^44501 64502$',
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'regex' : '44501|44502|44503',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'regex' : '^44501_([0-9]+_)+',
+ },
+ },
+ },
+ 'Customers' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '_10_',
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'regex' : '_20_',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'regex' : '_30_',
+ },
+ '20' : {
+ 'action' : 'deny',
+ 'regex' : '_40_',
+ },
+ },
+ },
+ 'bogons' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '_0_',
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'regex' : '_23456_',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'regex' : '_6449[6-9]_|_65[0-4][0-9][0-9]_|_655[0-4][0-9]_|_6555[0-1]_',
+ },
+ '20' : {
+ 'action' : 'permit',
+ 'regex' : '_6555[2-9]_|_655[6-9][0-9]_|_65[6-9][0-9][0-9]_|_6[6-9][0-9][0-9][0-]_|_[7-9][0-9][0-9][0-9][0-9]_|_1[0-2][0-9][0-9][0-9][0-9]_|_130[0-9][0-9][0-9]_|_1310[0-6][0-9]_|_13107[01]_',
+ },
+ },
+ },
+ }
+
+ for as_path, as_path_config in test_data.items():
+ path = base_path + ['as-path-list', as_path]
+ self.cli_set(path + ['description', f'VyOS-ASPATH-{as_path}'])
+ if 'rule' not in as_path_config:
+ continue
+
+ for rule, rule_config in as_path_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'regex' in rule_config:
+ self.cli_set(path + ['rule', rule, 'regex', rule_config['regex']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('bgp as-path access-list', end='')
+ for as_path, as_path_config in test_data.items():
+ if 'rule' not in as_path_config:
+ continue
+
+ for rule, rule_config in as_path_config['rule'].items():
+ tmp = f'bgp as-path access-list {as_path} seq {rule}'
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ tmp += ' ' + rule_config['regex']
+
+ self.assertIn(tmp, config)
+
+ def test_community_list(self):
+ test_data = {
+ '100' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '.*',
+ },
+ },
+ },
+ '200' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'deny',
+ 'regex' : '^1:201$',
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'regex' : '1:101$',
+ },
+ '15' : {
+ 'action' : 'deny',
+ 'regex' : '^1:100$',
+ },
+ },
+ },
+ }
+
+ for comm_list, comm_list_config in test_data.items():
+ path = base_path + ['community-list', comm_list]
+ self.cli_set(path + ['description', f'VyOS-COMM-{comm_list}'])
+ if 'rule' not in comm_list_config:
+ continue
+
+ for rule, rule_config in comm_list_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'regex' in rule_config:
+ self.cli_set(path + ['rule', rule, 'regex', rule_config['regex']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('bgp community-list', end='')
+ for comm_list, comm_list_config in test_data.items():
+ if 'rule' not in comm_list_config:
+ continue
+
+ for rule, rule_config in comm_list_config['rule'].items():
+ tmp = f'bgp community-list {comm_list} seq {rule}'
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ tmp += ' ' + rule_config['regex']
+
+ self.assertIn(tmp, config)
+
+ def test_extended_community_list(self):
+ test_data = {
+ 'foo' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '.*',
+ },
+ },
+ },
+ '200' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'deny',
+ 'regex' : '^1:201$',
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'regex' : '1:101$',
+ },
+ '15' : {
+ 'action' : 'deny',
+ 'regex' : '^1:100$',
+ },
+ },
+ },
+ }
+
+ for comm_list, comm_list_config in test_data.items():
+ path = base_path + ['extcommunity-list', comm_list]
+ self.cli_set(path + ['description', f'VyOS-EXTCOMM-{comm_list}'])
+ if 'rule' not in comm_list_config:
+ continue
+
+ for rule, rule_config in comm_list_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'regex' in rule_config:
+ self.cli_set(path + ['rule', rule, 'regex', rule_config['regex']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('bgp extcommunity-list', end='')
+ for comm_list, comm_list_config in test_data.items():
+ if 'rule' not in comm_list_config:
+ continue
+
+ for rule, rule_config in comm_list_config['rule'].items():
+ # if the community is not a number but a name, the expanded
+ # keyword is used
+ expanded = ''
+ if not comm_list.isnumeric():
+ expanded = ' expanded'
+ tmp = f'bgp extcommunity-list{expanded} {comm_list} seq {rule}'
+
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ tmp += ' ' + rule_config['regex']
+
+ self.assertIn(tmp, config)
+
+
+ def test_large_community_list(self):
+ test_data = {
+ 'foo' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '667:123:100',
+ },
+ },
+ },
+ 'bar' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'regex' : '65000:120:10',
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'regex' : '65000:120:20',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'regex' : '65000:120:30',
+ },
+ },
+ },
+ }
+
+ for comm_list, comm_list_config in test_data.items():
+ path = base_path + ['large-community-list', comm_list]
+ self.cli_set(path + ['description', f'VyOS-LARGECOMM-{comm_list}'])
+ if 'rule' not in comm_list_config:
+ continue
+
+ for rule, rule_config in comm_list_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'regex' in rule_config:
+ self.cli_set(path + ['rule', rule, 'regex', rule_config['regex']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('bgp large-community-list', end='')
+ for comm_list, comm_list_config in test_data.items():
+ if 'rule' not in comm_list_config:
+ continue
+
+ for rule, rule_config in comm_list_config['rule'].items():
+ tmp = f'bgp large-community-list expanded {comm_list} seq {rule}'
+
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ tmp += ' ' + rule_config['regex']
+
+ self.assertIn(tmp, config)
+
+
+ def test_prefix_list(self):
+ test_data = {
+ 'foo' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'prefix' : '10.0.0.0/8',
+ 'ge' : '16',
+ 'le' : '24',
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'prefix' : '172.16.0.0/12',
+ 'ge' : '16',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'prefix' : '192.168.0.0/16',
+ },
+ },
+ },
+ 'bar' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'prefix' : '10.0.10.0/24',
+ 'ge' : '25',
+ 'le' : '26',
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'prefix' : '10.0.20.0/24',
+ 'le' : '25',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'prefix' : '10.0.25.0/24',
+ },
+ },
+ },
+ }
+
+ for prefix_list, prefix_list_config in test_data.items():
+ path = base_path + ['prefix-list', prefix_list]
+ self.cli_set(path + ['description', f'VyOS-PFX-LIST-{prefix_list}'])
+ if 'rule' not in prefix_list_config:
+ continue
+
+ for rule, rule_config in prefix_list_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'prefix' in rule_config:
+ self.cli_set(path + ['rule', rule, 'prefix', rule_config['prefix']])
+ if 'ge' in rule_config:
+ self.cli_set(path + ['rule', rule, 'ge', rule_config['ge']])
+ if 'le' in rule_config:
+ self.cli_set(path + ['rule', rule, 'le', rule_config['le']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('ip prefix-list', end='')
+ for prefix_list, prefix_list_config in test_data.items():
+ if 'rule' not in prefix_list_config:
+ continue
+
+ for rule, rule_config in prefix_list_config['rule'].items():
+ tmp = f'ip prefix-list {prefix_list} seq {rule}'
+
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ tmp += ' ' + rule_config['prefix']
+
+ if 'ge' in rule_config:
+ tmp += ' ge ' + rule_config['ge']
+ if 'le' in rule_config:
+ tmp += ' le ' + rule_config['le']
+
+ self.assertIn(tmp, config)
+
+
+ def test_prefix_list6(self):
+ test_data = {
+ 'foo' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'prefix' : '2001:db8::/32',
+ 'ge' : '40',
+ 'le' : '48',
+ },
+ '10' : {
+ 'action' : 'deny',
+ 'prefix' : '2001:db8::/32',
+ 'ge' : '48',
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'prefix' : '2001:db8:1000::/64',
+ },
+ },
+ },
+ 'bar' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'prefix' : '2001:db8:100::/40',
+ 'ge' : '48',
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'prefix' : '2001:db8:200::/40',
+ 'ge' : '48',
+ },
+ '15' : {
+ 'action' : 'deny',
+ 'prefix' : '2001:db8:300::/40',
+ 'le' : '64',
+ },
+ },
+ },
+ }
+
+ for prefix_list, prefix_list_config in test_data.items():
+ path = base_path + ['prefix-list6', prefix_list]
+ self.cli_set(path + ['description', f'VyOS-PFX-LIST-{prefix_list}'])
+ if 'rule' not in prefix_list_config:
+ continue
+
+ for rule, rule_config in prefix_list_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'prefix' in rule_config:
+ self.cli_set(path + ['rule', rule, 'prefix', rule_config['prefix']])
+ if 'ge' in rule_config:
+ self.cli_set(path + ['rule', rule, 'ge', rule_config['ge']])
+ if 'le' in rule_config:
+ self.cli_set(path + ['rule', rule, 'le', rule_config['le']])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('ipv6 prefix-list', end='')
+ for prefix_list, prefix_list_config in test_data.items():
+ if 'rule' not in prefix_list_config:
+ continue
+
+ for rule, rule_config in prefix_list_config['rule'].items():
+ tmp = f'ipv6 prefix-list {prefix_list} seq {rule}'
+
+ if rule_config['action'] == 'permit':
+ tmp += ' permit'
+ else:
+ tmp += ' deny'
+
+ tmp += ' ' + rule_config['prefix']
+
+ if 'ge' in rule_config:
+ tmp += ' ge ' + rule_config['ge']
+ if 'le' in rule_config:
+ tmp += ' le ' + rule_config['le']
+
+ self.assertIn(tmp, config)
+
+ def test_prefix_list_duplicates(self):
+ # FRR does not allow to specify the same profix list rule multiple times
+ #
+ # vyos(config)# ip prefix-list foo seq 10 permit 192.0.2.0/24
+ # vyos(config)# ip prefix-list foo seq 20 permit 192.0.2.0/24
+ # % Configuration failed.
+ # Error type: validation
+ # Error description: duplicated prefix list value: 192.0.2.0/24
+
+ # There is also a VyOS verify() function to test this
+
+ prefix = '100.64.0.0/10'
+ prefix_list = 'duplicates'
+ test_range = range(20, 25)
+ path = base_path + ['prefix-list', prefix_list]
+
+ for rule in test_range:
+ self.cli_set(path + ['rule', str(rule), 'action', 'permit'])
+ self.cli_set(path + ['rule', str(rule), 'prefix', prefix])
+
+ # Duplicate prefixes
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for rule in test_range:
+ self.cli_set(path + ['rule', str(rule), 'le', str(rule)])
+
+ self.cli_commit()
+
+ config = self.getFRRconfig('ip prefix-list', end='')
+ for rule in test_range:
+ tmp = f'ip prefix-list {prefix_list} seq {rule} permit {prefix} le {rule}'
+ self.assertIn(tmp, config)
+ def test_route_map_community_set(self):
+ test_data = {
+ "community-configuration": {
+ "rule": {
+ "10": {
+ "action": "permit",
+ "set": {
+ "community": {
+ "replace": [
+ "65000:10",
+ "65001:11"
+ ]
+ },
+ "extcommunity": {
+ "bandwidth": "200",
+ "rt": [
+ "65000:10",
+ "192.168.0.1:11"
+ ],
+ "soo": [
+ "192.168.0.1:11",
+ "65000:10"
+ ]
+ },
+ "large-community": {
+ "replace": [
+ "65000:65000:10",
+ "65000:65000:11"
+ ]
+ }
+ }
+ },
+ "20": {
+ "action": "permit",
+ "set": {
+ "community": {
+ "add": [
+ "65000:10",
+ "65001:11"
+ ]
+ },
+ "extcommunity": {
+ "bandwidth": "200",
+ "bandwidth-non-transitive": {}
+ },
+ "large-community": {
+ "add": [
+ "65000:65000:10",
+ "65000:65000:11"
+ ]
+ }
+ }
+ },
+ "30": {
+ "action": "permit",
+ "set": {
+ "community": {
+ "none": {}
+ },
+ "extcommunity": {
+ "none": {}
+ },
+ "large-community": {
+ "none": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ for route_map, route_map_config in test_data.items():
+ path = base_path + ['route-map', route_map]
+ self.cli_set(path + ['description', f'VyOS ROUTE-MAP {route_map}'])
+ if 'rule' not in route_map_config:
+ continue
+
+ for rule, rule_config in route_map_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+ if 'set' in rule_config:
+
+ #Add community in configuration
+ if 'community' in rule_config['set']:
+ if 'none' in rule_config['set']['community']:
+ self.cli_set(path + ['rule', rule, 'set', 'community', 'none'])
+ else:
+ community_path = path + ['rule', rule, 'set', 'community']
+ if 'add' in rule_config['set']['community']:
+ for community_unit in rule_config['set']['community']['add']:
+ self.cli_set(community_path + ['add', community_unit])
+ if 'replace' in rule_config['set']['community']:
+ for community_unit in rule_config['set']['community']['replace']:
+ self.cli_set(community_path + ['replace', community_unit])
+
+ #Add large-community in configuration
+ if 'large-community' in rule_config['set']:
+ if 'none' in rule_config['set']['large-community']:
+ self.cli_set(path + ['rule', rule, 'set', 'large-community', 'none'])
+ else:
+ community_path = path + ['rule', rule, 'set', 'large-community']
+ if 'add' in rule_config['set']['large-community']:
+ for community_unit in rule_config['set']['large-community']['add']:
+ self.cli_set(community_path + ['add', community_unit])
+ if 'replace' in rule_config['set']['large-community']:
+ for community_unit in rule_config['set']['large-community']['replace']:
+ self.cli_set(community_path + ['replace', community_unit])
+
+ #Add extcommunity in configuration
+ if 'extcommunity' in rule_config['set']:
+ if 'none' in rule_config['set']['extcommunity']:
+ self.cli_set(path + ['rule', rule, 'set', 'extcommunity', 'none'])
+ else:
+ if 'bandwidth' in rule_config['set']['extcommunity']:
+ self.cli_set(path + ['rule', rule, 'set', 'extcommunity', 'bandwidth', rule_config['set']['extcommunity']['bandwidth']])
+ if 'bandwidth-non-transitive' in rule_config['set']['extcommunity']:
+ self.cli_set(path + ['rule', rule, 'set','extcommunity', 'bandwidth-non-transitive'])
+ if 'rt' in rule_config['set']['extcommunity']:
+ for community_unit in rule_config['set']['extcommunity']['rt']:
+ self.cli_set(path + ['rule', rule, 'set', 'extcommunity','rt',community_unit])
+ if 'soo' in rule_config['set']['extcommunity']:
+ for community_unit in rule_config['set']['extcommunity']['soo']:
+ self.cli_set(path + ['rule', rule, 'set', 'extcommunity','soo',community_unit])
+ self.cli_commit()
+
+ for route_map, route_map_config in test_data.items():
+ if 'rule' not in route_map_config:
+ continue
+ for rule, rule_config in route_map_config['rule'].items():
+ name = f'route-map {route_map} {rule_config["action"]} {rule}'
+ config = self.getFRRconfig(name)
+ self.assertIn(name, config)
+
+ if 'set' in rule_config:
+ #Check community
+ if 'community' in rule_config['set']:
+ if 'none' in rule_config['set']['community']:
+ tmp = f'set community none'
+ self.assertIn(tmp, config)
+ if 'replace' in rule_config['set']['community']:
+ values = ' '.join(rule_config['set']['community']['replace'])
+ tmp = f'set community {values}'
+ self.assertIn(tmp, config)
+ if 'add' in rule_config['set']['community']:
+ values = ' '.join(rule_config['set']['community']['add'])
+ tmp = f'set community {values} additive'
+ self.assertIn(tmp, config)
+ #Check large-community
+ if 'large-community' in rule_config['set']:
+ if 'none' in rule_config['set']['large-community']:
+ tmp = f'set large-community none'
+ self.assertIn(tmp, config)
+ if 'replace' in rule_config['set']['large-community']:
+ values = ' '.join(rule_config['set']['large-community']['replace'])
+ tmp = f'set large-community {values}'
+ self.assertIn(tmp, config)
+ if 'add' in rule_config['set']['large-community']:
+ values = ' '.join(rule_config['set']['large-community']['add'])
+ tmp = f'set large-community {values} additive'
+ self.assertIn(tmp, config)
+ #Check extcommunity
+ if 'extcommunity' in rule_config['set']:
+ if 'none' in rule_config['set']['extcommunity']:
+ tmp = 'set extcommunity none'
+ self.assertIn(tmp, config)
+ if 'bandwidth' in rule_config['set']['extcommunity']:
+ values = rule_config['set']['extcommunity']['bandwidth']
+ tmp = f'set extcommunity bandwidth {values}'
+ if 'bandwidth-non-transitive' in rule_config['set']['extcommunity']:
+ tmp = tmp + ' non-transitive'
+ self.assertIn(tmp, config)
+ if 'rt' in rule_config['set']['extcommunity']:
+ values = ' '.join(rule_config['set']['extcommunity']['rt'])
+ tmp = f'set extcommunity rt {values}'
+ self.assertIn(tmp, config)
+ if 'soo' in rule_config['set']['extcommunity']:
+ values = ' '.join(rule_config['set']['extcommunity']['soo'])
+ tmp = f'set extcommunity soo {values}'
+ self.assertIn(tmp, config)
+
+ def test_route_map(self):
+ access_list = '50'
+ as_path_list = '100'
+ test_interface = 'eth0'
+ community_list = 'BGP-comm-0815'
+
+ # ext community name only allows alphanumeric characters and no hyphen :/
+ # maybe change this if possible in vyos-1x rewrite
+ extcommunity_list = 'BGPextcomm123'
+
+ large_community_list = 'bgp-large-community-123456'
+ prefix_list = 'foo-pfx-list'
+ ipv6_nexthop_address = 'fe80::1'
+ local_pref = '300'
+ metric = '50'
+ peer = '2.3.4.5'
+ peerv6 = '2001:db8::1'
+ tag = '6542'
+ goto = '25'
+
+ ipv4_nexthop_address= '192.0.2.2'
+ ipv4_prefix_len= '18'
+ ipv6_prefix_len= '122'
+ ipv4_nexthop_type= 'blackhole'
+ ipv6_nexthop_type= 'blackhole'
+
+ test_data = {
+ 'foo-map-bar' : {
+ 'rule' : {
+ '5' : {
+ 'action' : 'permit',
+ 'continue' : '20',
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'call' : 'complicated-configuration',
+ },
+ },
+ },
+ 'a-matching-rule-0815': {
+ 'rule' : {
+ '5' : {
+ 'action' : 'deny',
+ 'match' : {
+ 'as-path' : as_path_list,
+ 'rpki-invalid': '',
+ 'tag': tag,
+ },
+ },
+ '10' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'community' : community_list,
+ 'interface' : test_interface,
+ 'rpki-not-found': '',
+ },
+ },
+ '15' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'extcommunity' : extcommunity_list,
+ 'rpki-valid': '',
+ },
+ 'on-match' : {
+ 'next' : '',
+ },
+ },
+ '20' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ip-address-acl': access_list,
+ 'ip-nexthop-acl': access_list,
+ 'ip-route-source-acl': access_list,
+ 'ipv6-address-acl': access_list,
+ 'origin-incomplete' : '',
+ },
+ 'on-match' : {
+ 'goto' : goto,
+ },
+ },
+ '25' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ip-address-pfx': prefix_list,
+ 'ip-nexthop-pfx': prefix_list,
+ 'ip-route-source-pfx': prefix_list,
+ 'ipv6-address-pfx': prefix_list,
+ 'origin-igp': '',
+ },
+ },
+ '30' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ipv6-nexthop-address' : ipv6_nexthop_address,
+ 'ipv6-nexthop-access-list' : access_list,
+ 'ipv6-nexthop-prefix-list' : prefix_list,
+ 'ipv6-nexthop-type' : ipv6_nexthop_type,
+ 'ipv6-address-pfx-len' : ipv6_prefix_len,
+ 'large-community' : large_community_list,
+ 'local-pref' : local_pref,
+ 'metric': metric,
+ 'origin-egp': '',
+ 'peer' : peer,
+ },
+ },
+
+ '31' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'peer' : peerv6,
+ },
+ },
+
+ '40' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ip-nexthop-addr' : ipv4_nexthop_address,
+ 'ip-address-pfx-len' : ipv4_prefix_len,
+ },
+ },
+ '42' : {
+ 'action' : 'deny',
+ 'match' : {
+ 'ip-nexthop-plen' : ipv4_prefix_len,
+ },
+ },
+ '44' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ip-nexthop-type' : ipv4_nexthop_type,
+ },
+ },
+ },
+ },
+ 'complicated-configuration' : {
+ 'rule' : {
+ '10' : {
+ 'action' : 'deny',
+ 'set' : {
+ 'aggregator-as' : '1234567890',
+ 'aggregator-ip' : '10.255.255.0',
+ 'as-path-exclude' : '1234',
+ 'as-path-prepend' : '1234567890 987654321',
+ 'as-path-prepend-last-as' : '5',
+ 'atomic-aggregate' : '',
+ 'distance' : '110',
+ 'ipv6-next-hop-global' : '2001::1',
+ 'ipv6-next-hop-local' : 'fe80::1',
+ 'ip-next-hop' : '192.168.1.1',
+ 'local-preference' : '500',
+ 'metric' : '150',
+ 'metric-type' : 'type-1',
+ 'origin' : 'incomplete',
+ 'l3vpn' : '',
+ 'originator-id' : '172.16.10.1',
+ 'src' : '100.0.0.1',
+ 'tag' : '65530',
+ 'weight' : '2',
+ },
+ },
+ },
+ },
+ 'bandwidth-configuration' : {
+ 'rule' : {
+ '10' : {
+ 'action' : 'deny',
+ 'set' : {
+ 'as-path-prepend' : '100 100',
+ 'distance' : '200',
+ 'extcommunity-bw' : 'num-multipaths',
+ },
+ },
+ },
+ },
+ 'evpn-configuration' : {
+ 'rule' : {
+ '10' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'evpn-default-route' : '',
+ 'evpn-rd' : '100:300',
+ 'evpn-route-type' : 'prefix',
+ 'evpn-vni' : '1234',
+ },
+ },
+ '20' : {
+ 'action' : 'permit',
+ 'set' : {
+ 'as-path-exclude' : 'all',
+ 'evpn-gateway-ipv4' : '192.0.2.99',
+ 'evpn-gateway-ipv6' : '2001:db8:f00::1',
+ },
+ },
+ },
+ },
+ 'match-protocol' : {
+ 'rule' : {
+ '10' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'protocol' : 'static',
+ },
+ },
+ '20' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'protocol' : 'bgp',
+ },
+ },
+ },
+ },
+ 'relative-metric' : {
+ 'rule' : {
+ '10' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ip-nexthop-addr' : ipv4_nexthop_address,
+ },
+ 'set' : {
+ 'metric' : '+10',
+ },
+ },
+ '20' : {
+ 'action' : 'permit',
+ 'match' : {
+ 'ip-nexthop-addr' : ipv4_nexthop_address,
+ },
+ 'set' : {
+ 'metric' : '-20',
+ },
+ },
+ '30': {
+ 'action': 'permit',
+ 'match': {
+ 'ip-nexthop-addr': ipv4_nexthop_address,
+ },
+ 'set': {
+ 'metric': 'rtt',
+ },
+ },
+ '40': {
+ 'action': 'permit',
+ 'match': {
+ 'ip-nexthop-addr': ipv4_nexthop_address,
+ },
+ 'set': {
+ 'metric': '+rtt',
+ },
+ },
+ '50': {
+ 'action': 'permit',
+ 'match': {
+ 'ip-nexthop-addr': ipv4_nexthop_address,
+ },
+ 'set': {
+ 'metric': '-rtt',
+ },
+ },
+ },
+ },
+ }
+
+ self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'action', 'permit'])
+ self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'source', 'host', '1.1.1.1'])
+ self.cli_set(['policy', 'access-list6', access_list, 'rule', '10', 'action', 'permit'])
+ self.cli_set(['policy', 'access-list6', access_list, 'rule', '10', 'source', 'network', '2001:db8::/32'])
+
+ self.cli_set(['policy', 'as-path-list', as_path_list, 'rule', '10', 'action', 'permit'])
+ self.cli_set(['policy', 'as-path-list', as_path_list, 'rule', '10', 'regex', '64501 64502'])
+ self.cli_set(['policy', 'community-list', community_list, 'rule', '10', 'action', 'deny'])
+ self.cli_set(['policy', 'community-list', community_list, 'rule', '10', 'regex', '65432'])
+ self.cli_set(['policy', 'extcommunity-list', extcommunity_list, 'rule', '10', 'action', 'deny'])
+ self.cli_set(['policy', 'extcommunity-list', extcommunity_list, 'rule', '10', 'regex', '65000'])
+ self.cli_set(['policy', 'large-community-list', large_community_list, 'rule', '10', 'action', 'permit'])
+ self.cli_set(['policy', 'large-community-list', large_community_list, 'rule', '10', 'regex', '100:200:300'])
+
+ self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '10', 'action', 'permit'])
+ self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '10', 'prefix', '192.0.2.0/24'])
+ self.cli_set(['policy', 'prefix-list6', prefix_list, 'rule', '10', 'action', 'permit'])
+ self.cli_set(['policy', 'prefix-list6', prefix_list, 'rule', '10', 'prefix', '2001:db8::/32'])
+
+ for route_map, route_map_config in test_data.items():
+ path = base_path + ['route-map', route_map]
+ self.cli_set(path + ['description', f'VyOS ROUTE-MAP {route_map}'])
+ if 'rule' not in route_map_config:
+ continue
+
+ for rule, rule_config in route_map_config['rule'].items():
+ if 'action' in rule_config:
+ self.cli_set(path + ['rule', rule, 'action', rule_config['action']])
+
+ if 'call' in rule_config:
+ self.cli_set(path + ['rule', rule, 'call', rule_config['call']])
+
+ if 'continue' in rule_config:
+ self.cli_set(path + ['rule', rule, 'continue', rule_config['continue']])
+
+ if 'match' in rule_config:
+ if 'as-path' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'as-path', rule_config['match']['as-path']])
+ if 'community' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'community', 'community-list', rule_config['match']['community']])
+ self.cli_set(path + ['rule', rule, 'match', 'community', 'exact-match'])
+ if 'evpn-default-route' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'evpn', 'default-route'])
+ if 'evpn-rd' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'evpn', 'rd', rule_config['match']['evpn-rd']])
+ if 'evpn-route-type' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'evpn', 'route-type', rule_config['match']['evpn-route-type']])
+ if 'evpn-vni' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'evpn', 'vni', rule_config['match']['evpn-vni']])
+ if 'extcommunity' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'extcommunity', rule_config['match']['extcommunity']])
+ if 'interface' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'interface', rule_config['match']['interface']])
+ if 'ip-address-acl' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'address', 'access-list', rule_config['match']['ip-address-acl']])
+ if 'ip-address-pfx' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'address', 'prefix-list', rule_config['match']['ip-address-pfx']])
+ if 'ip-address-pfx-len' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'address', 'prefix-len', rule_config['match']['ip-address-pfx-len']])
+ if 'ip-nexthop-acl' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'access-list', rule_config['match']['ip-nexthop-acl']])
+ if 'ip-nexthop-pfx' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'prefix-list', rule_config['match']['ip-nexthop-pfx']])
+ if 'ip-nexthop-addr' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'address', rule_config['match']['ip-nexthop-addr']])
+ if 'ip-nexthop-plen' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'prefix-len', rule_config['match']['ip-nexthop-plen']])
+ if 'ip-nexthop-type' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'type', rule_config['match']['ip-nexthop-type']])
+ if 'ip-route-source-acl' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'route-source', 'access-list', rule_config['match']['ip-route-source-acl']])
+ if 'ip-route-source-pfx' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ip', 'route-source', 'prefix-list', rule_config['match']['ip-route-source-pfx']])
+ if 'ipv6-address-acl' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'address', 'access-list', rule_config['match']['ipv6-address-acl']])
+ if 'ipv6-address-pfx' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'address', 'prefix-list', rule_config['match']['ipv6-address-pfx']])
+ if 'ipv6-address-pfx-len' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'address', 'prefix-len', rule_config['match']['ipv6-address-pfx-len']])
+ if 'ipv6-nexthop-address' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'address', rule_config['match']['ipv6-nexthop-address']])
+ if 'ipv6-nexthop-access-list' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'access-list', rule_config['match']['ipv6-nexthop-access-list']])
+ if 'ipv6-nexthop-prefix-list' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'prefix-list', rule_config['match']['ipv6-nexthop-prefix-list']])
+ if 'ipv6-nexthop-type' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'type', rule_config['match']['ipv6-nexthop-type']])
+ if 'large-community' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'large-community', 'large-community-list', rule_config['match']['large-community']])
+ if 'local-pref' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'local-preference', rule_config['match']['local-pref']])
+ if 'metric' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'metric', rule_config['match']['metric']])
+ if 'origin-igp' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'origin', 'igp'])
+ if 'origin-egp' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'origin', 'egp'])
+ if 'origin-incomplete' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'origin', 'incomplete'])
+ if 'peer' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'peer', rule_config['match']['peer']])
+ if 'rpki-invalid' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'rpki', 'invalid'])
+ if 'rpki-not-found' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'rpki', 'notfound'])
+ if 'rpki-valid' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'rpki', 'valid'])
+ if 'protocol' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'protocol', rule_config['match']['protocol']])
+ if 'tag' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'tag', rule_config['match']['tag']])
+
+ if 'on-match' in rule_config:
+ if 'goto' in rule_config['on-match']:
+ self.cli_set(path + ['rule', rule, 'on-match', 'goto', rule_config['on-match']['goto']])
+ if 'next' in rule_config['on-match']:
+ self.cli_set(path + ['rule', rule, 'on-match', 'next'])
+
+ if 'set' in rule_config:
+ if 'aggregator-as' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'aggregator', 'as', rule_config['set']['aggregator-as']])
+ if 'aggregator-ip' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'aggregator', 'ip', rule_config['set']['aggregator-ip']])
+ if 'as-path-exclude' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'as-path', 'exclude', rule_config['set']['as-path-exclude']])
+ if 'as-path-prepend' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'as-path', 'prepend', rule_config['set']['as-path-prepend']])
+ if 'atomic-aggregate' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'atomic-aggregate'])
+ if 'distance' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'distance', rule_config['set']['distance']])
+ if 'ipv6-next-hop-global' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'ipv6-next-hop', 'global', rule_config['set']['ipv6-next-hop-global']])
+ if 'ipv6-next-hop-local' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'ipv6-next-hop', 'local', rule_config['set']['ipv6-next-hop-local']])
+ if 'ip-next-hop' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'ip-next-hop', rule_config['set']['ip-next-hop']])
+ if 'l3vpn' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'l3vpn-nexthop', 'encapsulation', 'gre'])
+ if 'local-preference' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'local-preference', rule_config['set']['local-preference']])
+ if 'metric' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'metric', rule_config['set']['metric']])
+ if 'metric-type' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'metric-type', rule_config['set']['metric-type']])
+ if 'origin' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'origin', rule_config['set']['origin']])
+ if 'originator-id' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'originator-id', rule_config['set']['originator-id']])
+ if 'src' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'src', rule_config['set']['src']])
+ if 'tag' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'tag', rule_config['set']['tag']])
+ if 'weight' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'weight', rule_config['set']['weight']])
+ if 'evpn-gateway-ipv4' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'evpn', 'gateway', 'ipv4', rule_config['set']['evpn-gateway-ipv4']])
+ if 'evpn-gateway-ipv6' in rule_config['set']:
+ self.cli_set(path + ['rule', rule, 'set', 'evpn', 'gateway', 'ipv6', rule_config['set']['evpn-gateway-ipv6']])
+
+ self.cli_commit()
+
+ for route_map, route_map_config in test_data.items():
+ if 'rule' not in route_map_config:
+ continue
+ for rule, rule_config in route_map_config['rule'].items():
+ name = f'route-map {route_map} {rule_config["action"]} {rule}'
+ config = self.getFRRconfig(name)
+ self.assertIn(name, config)
+
+ if 'call' in rule_config:
+ tmp = 'call ' + rule_config['call']
+ self.assertIn(tmp, config)
+
+ if 'continue' in rule_config:
+ tmp = 'on-match goto ' + rule_config['continue']
+ self.assertIn(tmp, config)
+
+ if 'match' in rule_config:
+ if 'as-path' in rule_config['match']:
+ tmp = 'match as-path ' + rule_config['match']['as-path']
+ self.assertIn(tmp, config)
+ if 'community' in rule_config['match']:
+ tmp = f'match community {rule_config["match"]["community"]} exact-match'
+ self.assertIn(tmp, config)
+ if 'evpn-default-route' in rule_config['match']:
+ tmp = f'match evpn default-route'
+ self.assertIn(tmp, config)
+ if 'evpn-rd' in rule_config['match']:
+ tmp = f'match evpn rd {rule_config["match"]["evpn-rd"]}'
+ self.assertIn(tmp, config)
+ if 'evpn-route-type' in rule_config['match']:
+ tmp = f'match evpn route-type {rule_config["match"]["evpn-route-type"]}'
+ self.assertIn(tmp, config)
+ if 'evpn-vni' in rule_config['match']:
+ tmp = f'match evpn vni {rule_config["match"]["evpn-vni"]}'
+ self.assertIn(tmp, config)
+ if 'extcommunity' in rule_config['match']:
+ tmp = f'match extcommunity {rule_config["match"]["extcommunity"]}'
+ self.assertIn(tmp, config)
+ if 'interface' in rule_config['match']:
+ tmp = f'match interface {rule_config["match"]["interface"]}'
+ self.assertIn(tmp, config)
+ if 'ip-address-acl' in rule_config['match']:
+ tmp = f'match ip address {rule_config["match"]["ip-address-acl"]}'
+ self.assertIn(tmp, config)
+ if 'ip-address-pfx' in rule_config['match']:
+ tmp = f'match ip address prefix-list {rule_config["match"]["ip-address-pfx"]}'
+ self.assertIn(tmp, config)
+ if 'ip-address-pfx-len' in rule_config['match']:
+ tmp = f'match ip address prefix-len {rule_config["match"]["ip-address-pfx-len"]}'
+ self.assertIn(tmp, config)
+ if 'ip-nexthop-acl' in rule_config['match']:
+ tmp = f'match ip next-hop {rule_config["match"]["ip-nexthop-acl"]}'
+ self.assertIn(tmp, config)
+ if 'ip-nexthop-pfx' in rule_config['match']:
+ tmp = f'match ip next-hop prefix-list {rule_config["match"]["ip-nexthop-pfx"]}'
+ self.assertIn(tmp, config)
+ if 'ip-nexthop-addr' in rule_config['match']:
+ tmp = f'match ip next-hop address {rule_config["match"]["ip-nexthop-addr"]}'
+ self.assertIn(tmp, config)
+ if 'ip-nexthop-plen' in rule_config['match']:
+ tmp = f'match ip next-hop prefix-len {rule_config["match"]["ip-nexthop-plen"]}'
+ self.assertIn(tmp, config)
+ if 'ip-nexthop-type' in rule_config['match']:
+ tmp = f'match ip next-hop type {rule_config["match"]["ip-nexthop-type"]}'
+ self.assertIn(tmp, config)
+ if 'ip-route-source-acl' in rule_config['match']:
+ tmp = f'match ip route-source {rule_config["match"]["ip-route-source-acl"]}'
+ self.assertIn(tmp, config)
+ if 'ip-route-source-pfx' in rule_config['match']:
+ tmp = f'match ip route-source prefix-list {rule_config["match"]["ip-route-source-pfx"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-address-acl' in rule_config['match']:
+ tmp = f'match ipv6 address {rule_config["match"]["ipv6-address-acl"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-address-pfx' in rule_config['match']:
+ tmp = f'match ipv6 address prefix-list {rule_config["match"]["ipv6-address-pfx"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-address-pfx-len' in rule_config['match']:
+ tmp = f'match ipv6 address prefix-len {rule_config["match"]["ipv6-address-pfx-len"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-nexthop-address' in rule_config['match']:
+ tmp = f'match ipv6 next-hop address {rule_config["match"]["ipv6-nexthop-address"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-nexthop-access-list' in rule_config['match']:
+ tmp = f'match ipv6 next-hop {rule_config["match"]["ipv6-nexthop-access-list"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-nexthop-prefix-list' in rule_config['match']:
+ tmp = f'match ipv6 next-hop prefix-list {rule_config["match"]["ipv6-nexthop-prefix-list"]}'
+ self.assertIn(tmp, config)
+ if 'ipv6-nexthop-type' in rule_config['match']:
+ tmp = f'match ipv6 next-hop type {rule_config["match"]["ipv6-nexthop-type"]}'
+ self.assertIn(tmp, config)
+ if 'large-community' in rule_config['match']:
+ tmp = f'match large-community {rule_config["match"]["large-community"]}'
+ self.assertIn(tmp, config)
+ if 'local-pref' in rule_config['match']:
+ tmp = f'match local-preference {rule_config["match"]["local-pref"]}'
+ self.assertIn(tmp, config)
+ if 'metric' in rule_config['match']:
+ tmp = f'match metric {rule_config["match"]["metric"]}'
+ self.assertIn(tmp, config)
+ if 'origin-igp' in rule_config['match']:
+ tmp = f'match origin igp'
+ self.assertIn(tmp, config)
+ if 'origin-egp' in rule_config['match']:
+ tmp = f'match origin egp'
+ self.assertIn(tmp, config)
+ if 'origin-incomplete' in rule_config['match']:
+ tmp = f'match origin incomplete'
+ self.assertIn(tmp, config)
+ if 'peer' in rule_config['match']:
+ tmp = f'match peer {rule_config["match"]["peer"]}'
+ self.assertIn(tmp, config)
+ if 'protocol' in rule_config['match']:
+ tmp = f'match source-protocol {rule_config["match"]["protocol"]}'
+ self.assertIn(tmp, config)
+ if 'rpki-invalid' in rule_config['match']:
+ tmp = f'match rpki invalid'
+ self.assertIn(tmp, config)
+ if 'rpki-not-found' in rule_config['match']:
+ tmp = f'match rpki notfound'
+ self.assertIn(tmp, config)
+ if 'rpki-valid' in rule_config['match']:
+ tmp = f'match rpki valid'
+ self.assertIn(tmp, config)
+ if 'tag' in rule_config['match']:
+ tmp = f'match tag {rule_config["match"]["tag"]}'
+ self.assertIn(tmp, config)
+
+ if 'on-match' in rule_config:
+ if 'goto' in rule_config['on-match']:
+ tmp = f'on-match goto {rule_config["on-match"]["goto"]}'
+ self.assertIn(tmp, config)
+ if 'next' in rule_config['on-match']:
+ tmp = f'on-match next'
+ self.assertIn(tmp, config)
+
+ if 'set' in rule_config:
+ tmp = ' set '
+ if 'aggregator-as' in rule_config['set']:
+ tmp += 'aggregator as ' + rule_config['set']['aggregator-as']
+ elif 'aggregator-ip' in rule_config['set']:
+ tmp += ' ' + rule_config['set']['aggregator-ip']
+ elif 'as-path-exclude' in rule_config['set']:
+ tmp += 'as-path exclude ' + rule_config['set']['as-path-exclude']
+ elif 'as-path-prepend' in rule_config['set']:
+ tmp += 'as-path prepend ' + rule_config['set']['as-path-prepend']
+ elif 'as-path-prepend-last-as' in rule_config['set']:
+ tmp += 'as-path prepend last-as' + rule_config['set']['as-path-prepend-last-as']
+ elif 'atomic-aggregate' in rule_config['set']:
+ tmp += 'atomic-aggregate'
+ elif 'distance' in rule_config['set']:
+ tmp += 'distance ' + rule_config['set']['distance']
+ elif 'ip-next-hop' in rule_config['set']:
+ tmp += 'ip next-hop ' + rule_config['set']['ip-next-hop']
+ elif 'ipv6-next-hop-global' in rule_config['set']:
+ tmp += 'ipv6 next-hop global ' + rule_config['set']['ipv6-next-hop-global']
+ elif 'ipv6-next-hop-local' in rule_config['set']:
+ tmp += 'ipv6 next-hop local ' + rule_config['set']['ipv6-next-hop-local']
+ elif 'l3vpn' in rule_config['set']:
+ tmp += 'l3vpn next-hop encapsulation gre'
+ elif 'local-preference' in rule_config['set']:
+ tmp += 'local-preference ' + rule_config['set']['local-preference']
+ elif 'metric' in rule_config['set']:
+ tmp += 'metric ' + rule_config['set']['metric']
+ elif 'metric-type' in rule_config['set']:
+ tmp += 'metric-type ' + rule_config['set']['metric-type']
+ elif 'origin' in rule_config['set']:
+ tmp += 'origin ' + rule_config['set']['origin']
+ elif 'originator-id' in rule_config['set']:
+ tmp += 'originator-id ' + rule_config['set']['originator-id']
+ elif 'src' in rule_config['set']:
+ tmp += 'src ' + rule_config['set']['src']
+ elif 'tag' in rule_config['set']:
+ tmp += 'tag ' + rule_config['set']['tag']
+ elif 'weight' in rule_config['set']:
+ tmp += 'weight ' + rule_config['set']['weight']
+ elif 'vpn-gateway-ipv4' in rule_config['set']:
+ tmp += 'evpn gateway ipv4 ' + rule_config['set']['vpn-gateway-ipv4']
+ elif 'vpn-gateway-ipv6' in rule_config['set']:
+ tmp += 'evpn gateway ipv6 ' + rule_config['set']['vpn-gateway-ipv6']
+
+ self.assertIn(tmp, config)
+
+
+ # Test set table for some sources
+ def test_table_id(self):
+ path = base_path + ['local-route']
+
+ sources = ['203.0.113.1', '203.0.113.2']
+ rule = '50'
+ table = '23'
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+
+ self.cli_commit()
+
+ original = """
+ 50: from 203.0.113.1 lookup 23
+ 50: from 203.0.113.2 lookup 23
+ """
+ tmp = cmd('ip rule show prio 50')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for fwmark
+ def test_fwmark_table_id(self):
+ path = base_path + ['local-route']
+
+ fwmk = '24'
+ rule = '101'
+ table = '154'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 101: from all fwmark 0x18 lookup 154
+ """
+ tmp = cmd('ip rule show prio 101')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for destination
+ def test_destination_table_id(self):
+ path = base_path + ['local-route']
+
+ dst = '203.0.113.1'
+ rule = '102'
+ table = '154'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+
+ self.cli_commit()
+
+ original = """
+ 102: from all to 203.0.113.1 lookup 154
+ """
+ tmp = cmd('ip rule show prio 102')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for destination and protocol
+ def test_protocol_destination_table_id(self):
+ path = base_path + ['local-route']
+
+ dst = '203.0.113.12'
+ rule = '85'
+ table = '104'
+ proto = 'tcp'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path + ['rule', rule, 'protocol', proto])
+
+ self.cli_commit()
+
+ original = """
+ 85: from all to 203.0.113.12 ipproto tcp lookup 104
+ """
+ tmp = cmd('ip rule show prio 85')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for destination, source, protocol, fwmark and port
+ def test_protocol_port_address_fwmark_table_id(self):
+ path = base_path + ['local-route']
+
+ dst = '203.0.113.5'
+ src_list = ['203.0.113.1', '203.0.113.2']
+ rule = '23'
+ fwmark = '123456'
+ table = '123'
+ new_table = '111'
+ proto = 'udp'
+ new_proto = 'tcp'
+ src_port = '5555'
+ dst_port = '8888'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path + ['rule', rule, 'source', 'port', src_port])
+ self.cli_set(path + ['rule', rule, 'protocol', proto])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmark])
+ self.cli_set(path + ['rule', rule, 'destination', 'port', dst_port])
+ for src in src_list:
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+
+ self.cli_commit()
+
+ original = """
+ 23: from 203.0.113.1 to 203.0.113.5 fwmark 0x1e240 ipproto udp sport 5555 dport 8888 lookup 123
+ 23: from 203.0.113.2 to 203.0.113.5 fwmark 0x1e240 ipproto udp sport 5555 dport 8888 lookup 123
+ """
+ tmp = cmd(f'ip rule show prio {rule}')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Change table and protocol, delete fwmark and source port
+ self.cli_delete(path + ['rule', rule, 'fwmark'])
+ self.cli_delete(path + ['rule', rule, 'source', 'port'])
+ self.cli_set(path + ['rule', rule, 'set', 'table', new_table])
+ self.cli_set(path + ['rule', rule, 'protocol', new_proto])
+
+ self.cli_commit()
+
+ original = """
+ 23: from 203.0.113.1 to 203.0.113.5 ipproto tcp dport 8888 lookup 111
+ 23: from 203.0.113.2 to 203.0.113.5 ipproto tcp dport 8888 lookup 111
+ """
+ tmp = cmd(f'ip rule show prio {rule}')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for sources with fwmark
+ def test_fwmark_sources_table_id(self):
+ path = base_path + ['local-route']
+
+ sources = ['203.0.113.11', '203.0.113.12']
+ fwmk = '23'
+ rule = '100'
+ table = '150'
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 100: from 203.0.113.11 fwmark 0x17 lookup 150
+ 100: from 203.0.113.12 fwmark 0x17 lookup 150
+ """
+ tmp = cmd('ip rule show prio 100')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for sources with iif
+ def test_iif_sources_table_id(self):
+ path = base_path + ['local-route']
+
+ sources = ['203.0.113.11', '203.0.113.12']
+ iif = 'lo'
+ rule = '100'
+ table = '150'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'inbound-interface', iif])
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+
+ self.cli_commit()
+
+ # Check generated configuration
+ # Expected values
+ original = """
+ 100: from 203.0.113.11 iif lo lookup 150
+ 100: from 203.0.113.12 iif lo lookup 150
+ """
+ tmp = cmd('ip rule show prio 100')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for sources and destinations with fwmark
+ def test_fwmark_sources_destination_table_id(self):
+ path = base_path + ['local-route']
+
+ sources = ['203.0.113.11', '203.0.113.12']
+ destinations = ['203.0.113.13', '203.0.113.15']
+ fwmk = '23'
+ rule = '103'
+ table = '150'
+ for src in sources:
+ for dst in destinations:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 103: from 203.0.113.11 to 203.0.113.13 fwmark 0x17 lookup 150
+ 103: from 203.0.113.11 to 203.0.113.15 fwmark 0x17 lookup 150
+ 103: from 203.0.113.12 to 203.0.113.13 fwmark 0x17 lookup 150
+ 103: from 203.0.113.12 to 203.0.113.15 fwmark 0x17 lookup 150
+ """
+ tmp = cmd('ip rule show prio 103')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table ipv6 for some sources ipv6
+ def test_ipv6_table_id(self):
+ path = base_path + ['local-route6']
+
+ sources = ['2001:db8:123::/48', '2001:db8:126::/48']
+ rule = '50'
+ table = '23'
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+
+ self.cli_commit()
+
+ original = """
+ 50: from 2001:db8:123::/48 lookup 23
+ 50: from 2001:db8:126::/48 lookup 23
+ """
+ tmp = cmd('ip -6 rule show prio 50')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for fwmark ipv6
+ def test_fwmark_ipv6_table_id(self):
+ path = base_path + ['local-route6']
+
+ fwmk = '24'
+ rule = '100'
+ table = '154'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 100: from all fwmark 0x18 lookup 154
+ """
+ tmp = cmd('ip -6 rule show prio 100')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for destination ipv6
+ def test_destination_ipv6_table_id(self):
+ path = base_path + ['local-route6']
+
+ dst = '2001:db8:1337::/126'
+ rule = '101'
+ table = '154'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+
+ self.cli_commit()
+
+ original = """
+ 101: from all to 2001:db8:1337::/126 lookup 154
+ """
+ tmp = cmd('ip -6 rule show prio 101')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for sources with fwmark ipv6
+ def test_fwmark_sources_ipv6_table_id(self):
+ path = base_path + ['local-route6']
+
+ sources = ['2001:db8:1338::/126', '2001:db8:1339::/126']
+ fwmk = '23'
+ rule = '102'
+ table = '150'
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 102: from 2001:db8:1338::/126 fwmark 0x17 lookup 150
+ 102: from 2001:db8:1339::/126 fwmark 0x17 lookup 150
+ """
+ tmp = cmd('ip -6 rule show prio 102')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for sources with iif ipv6
+ def test_iif_sources_ipv6_table_id(self):
+ path = base_path + ['local-route6']
+
+ sources = ['2001:db8:1338::/126', '2001:db8:1339::/126']
+ iif = 'lo'
+ rule = '102'
+ table = '150'
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path + ['rule', rule, 'inbound-interface', iif])
+
+ self.cli_commit()
+
+ # Check generated configuration
+ # Expected values
+ original = """
+ 102: from 2001:db8:1338::/126 iif lo lookup 150
+ 102: from 2001:db8:1339::/126 iif lo lookup 150
+ """
+ tmp = cmd('ip -6 rule show prio 102')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test set table for sources and destinations with fwmark ipv6
+ def test_fwmark_sources_destination_ipv6_table_id(self):
+ path = base_path + ['local-route6']
+
+ sources = ['2001:db8:1338::/126', '2001:db8:1339::/56']
+ destinations = ['2001:db8:13::/48', '2001:db8:16::/48']
+ fwmk = '23'
+ rule = '103'
+ table = '150'
+ for src in sources:
+ for dst in destinations:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 103: from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150
+ 103: from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150
+ 103: from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150
+ 103: from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150
+ """
+ tmp = cmd('ip -6 rule show prio 103')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Test delete table for sources and destination with fwmark ipv4/ipv6
+ def test_delete_ipv4_ipv6_table_id(self):
+ path = base_path + ['local-route']
+ path_v6 = base_path + ['local-route6']
+
+ sources = ['203.0.113.0/24', '203.0.114.5']
+ destinations = ['203.0.112.0/24', '203.0.116.5']
+ sources_v6 = ['2001:db8:1338::/126', '2001:db8:1339::/56']
+ destinations_v6 = ['2001:db8:13::/48', '2001:db8:16::/48']
+ fwmk = '23'
+ rule = '103'
+ table = '150'
+ for src in sources:
+ for dst in destinations:
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmk])
+
+ for src in sources_v6:
+ for dst in destinations_v6:
+ self.cli_set(path_v6 + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path_v6 + ['rule', rule, 'source', 'address', src])
+ self.cli_set(path_v6 + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path_v6 + ['rule', rule, 'fwmark', fwmk])
+
+ self.cli_commit()
+
+ original = """
+ 103: from 203.0.113.0/24 to 203.0.116.5 fwmark 0x17 lookup 150
+ 103: from 203.0.114.5 to 203.0.112.0/24 fwmark 0x17 lookup 150
+ 103: from 203.0.114.5 to 203.0.116.5 fwmark 0x17 lookup 150
+ 103: from 203.0.113.0/24 to 203.0.112.0/24 fwmark 0x17 lookup 150
+ """
+ original_v6 = """
+ 103: from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150
+ 103: from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150
+ 103: from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150
+ 103: from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150
+ """
+ tmp = cmd('ip rule show prio 103')
+ tmp_v6 = cmd('ip -6 rule show prio 103')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+ self.assertEqual(sort_ip(tmp_v6), sort_ip(original_v6))
+
+ self.cli_delete(path)
+ self.cli_delete(path_v6)
+ self.cli_commit()
+
+ tmp = cmd('ip rule show prio 103')
+ tmp_v6 = cmd('ip -6 rule show prio 103')
+
+ self.assertEqual(sort_ip(tmp), [])
+ self.assertEqual(sort_ip(tmp_v6), [])
+
+ # Test multiple commits ipv4
+ def test_multiple_commit_ipv4_table_id(self):
+ path = base_path + ['local-route']
+
+ sources = ['192.0.2.1', '192.0.2.2']
+ destination = '203.0.113.25'
+ rule = '105'
+ table = '151'
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ for src in sources:
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+
+ self.cli_commit()
+
+ original_first = """
+ 105: from 192.0.2.1 lookup 151
+ 105: from 192.0.2.2 lookup 151
+ """
+ tmp = cmd('ip rule show prio 105')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original_first))
+
+ # Create second commit with added destination
+ self.cli_set(path + ['rule', rule, 'destination', 'address', destination])
+ self.cli_commit()
+
+ original_second = """
+ 105: from 192.0.2.1 to 203.0.113.25 lookup 151
+ 105: from 192.0.2.2 to 203.0.113.25 lookup 151
+ """
+ tmp = cmd('ip rule show prio 105')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original_second))
+
+ def test_frr_individual_remove_T6283_T6250(self):
+ path = base_path + ['route-map']
+ route_maps = ['RMAP-1', 'RMAP_2']
+ seq = '10'
+ base_local_preference = 300
+ base_table = 50
+
+ # T6250
+ local_preference = base_local_preference
+ table = base_table
+ for route_map in route_maps:
+ self.cli_set(path + [route_map, 'rule', seq, 'action', 'permit'])
+ self.cli_set(path + [route_map, 'rule', seq, 'set', 'table', str(table)])
+ self.cli_set(path + [route_map, 'rule', seq, 'set', 'local-preference', str(local_preference)])
+ local_preference += 20
+ table += 5
+
+ self.cli_commit()
+
+ local_preference = base_local_preference
+ table = base_table
+ for route_map in route_maps:
+ config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='')
+ self.assertIn(f' set local-preference {local_preference}', config)
+ self.assertIn(f' set table {table}', config)
+ local_preference += 20
+ table += 5
+
+ for route_map in route_maps:
+ self.cli_delete(path + [route_map, 'rule', '10', 'set', 'table'])
+ # we explicitly commit multiple times to be as vandal as possible to the system
+ self.cli_commit()
+
+ local_preference = base_local_preference
+ for route_map in route_maps:
+ config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='')
+ self.assertIn(f' set local-preference {local_preference}', config)
+ local_preference += 20
+
+ # T6283
+ seq = '20'
+ prepend = '100 100 100'
+ for route_map in route_maps:
+ self.cli_set(path + [route_map, 'rule', seq, 'action', 'permit'])
+ self.cli_set(path + [route_map, 'rule', seq, 'set', 'as-path', 'prepend', prepend])
+
+ self.cli_commit()
+
+ for route_map in route_maps:
+ config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='')
+ self.assertIn(f' set as-path prepend {prepend}', config)
+
+ for route_map in route_maps:
+ self.cli_delete(path + [route_map, 'rule', seq, 'set'])
+ # we explicitly commit multiple times to be as vandal as possible to the system
+ self.cli_commit()
+
+ for route_map in route_maps:
+ config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='')
+ self.assertNotIn(f' set', config)
+
+def sort_ip(output):
+ o = '\n'.join([' '.join(line.strip().split()) for line in output.strip().splitlines()])
+ o = o.splitlines()
+ o.sort()
+ return o
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
new file mode 100644
index 0000000..797ab97
--- /dev/null
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.process import cmd
+
+mark = '100'
+conn_mark = '555'
+conn_mark_set = '111'
+table_mark_offset = 0x7fffffff
+table_id = '101'
+vrf = 'PBRVRF'
+vrf_table_id = '102'
+interface = 'eth0'
+interface_wc = 'ppp*'
+interface_ip = '172.16.10.1/24'
+
+class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestPolicyRoute, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, ['policy', 'route'])
+ cls.cli_delete(cls, ['policy', 'route6'])
+
+ cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
+ cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface])
+
+ cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
+ cls.cli_delete(cls, ['protocols', 'static', 'table', table_id])
+ cls.cli_delete(cls, ['vrf', 'name', vrf])
+
+ super(TestPolicyRoute, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(['policy', 'route'])
+ self.cli_delete(['policy', 'route6'])
+ self.cli_commit()
+
+ # Verify nftables cleanup
+ nftables_search = [
+ ['set N_smoketest_network'],
+ ['set N_smoketest_network1'],
+ ['chain VYOS_PBR_smoketest']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle', inverse=True)
+
+ # Verify ip rule cleanup
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id]
+ ]
+
+ self.verify_rules(ip_rule_search, inverse=True)
+
+ def verify_rules(self, rules_search, inverse=False):
+ rule_output = cmd('ip rule show')
+
+ for search in rules_search:
+ matched = False
+ for line in rule_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(not matched if inverse else matched, msg=search)
+
+ def test_pbr_group(self):
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24'])
+ self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network'])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'group', 'network-group', 'smoketest_network1'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark])
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'iifname "{interface}"','jump VYOS_PBR_UD_smoketest'],
+ ['ip daddr @N_smoketest_network1', 'ip saddr @N_smoketest_network'],
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ self.cli_delete(['firewall'])
+
+ def test_pbr_mark(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark])
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(int(mark))
+
+ nftables_search = [
+ [f'iifname "{interface}"','jump VYOS_PBR_UD_smoketest'],
+ ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'meta mark set ' + mark_hex],
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ def test_pbr_mark_connection(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'connection-mark', conn_mark])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'connection-mark', conn_mark_set])
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(int(conn_mark))
+ mark_hex_set = "{0:#010x}".format(int(conn_mark_set))
+
+ nftables_search = [
+ [f'iifname "{interface}"','jump VYOS_PBR_UD_smoketest'],
+ ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'ct mark ' + mark_hex, 'ct mark set ' + mark_hex_set],
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ def test_pbr_table(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(table_id))
+
+ # IPv4
+
+ nftables_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR_UD_smoketest'],
+ ['tcp flags syn / syn,ack', 'tcp dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ # IPv6
+
+ nftables6_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR6_UD_smoketest'],
+ ['meta l4proto { tcp, udp }', 'th dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+
+ # IP rule fwmark -> table
+
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id]
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
+ def test_pbr_vrf(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'vrf', vrf])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'vrf', vrf])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(vrf_table_id))
+
+ # IPv4
+
+ nftables_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR_UD_smoketest'],
+ ['tcp flags syn / syn,ack', 'tcp dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ # IPv6
+
+ nftables6_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR6_UD_smoketest'],
+ ['meta l4proto { tcp, udp }', 'th dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+
+ # IP rule fwmark -> table
+
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(vrf_table_id)), 'lookup ' + vrf]
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
+ def test_pbr_matching_criteria(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'default-log'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'udp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'action', 'drop'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'mark', '2020'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'mark', '2-3000'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'source', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'destination', 'port', '22'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'state', 'new'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'ttl', 'gt', '2'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'mark', '!456'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'protocol', 'icmp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'icmp', 'type-name', 'echo-request'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'packet-length', '128'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'packet-length', '1024-2048'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'packet-type', 'other'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'log'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '5', 'dscp', '41'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '5', 'dscp', '57-59'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '5', 'mark', '!456-500'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '5', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'route6', 'smoketest6', 'default-log'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'udp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'action', 'drop'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'source', 'address', '2001:db8::0/64'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'destination', 'port', '22'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'state', 'new'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'hop-limit', 'gt', '2'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'protocol', 'icmpv6'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'icmpv6', 'type', 'echo-request'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'packet-length-exclude', '128'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'packet-length-exclude', '1024-2048'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'packet-type', 'multicast'])
+
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'log'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'set', 'table', table_id])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'dscp-exclude', '61'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'dscp-exclude', '14-19'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface_wc])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface_wc])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(table_id))
+
+ # IPv4
+ nftables_search = [
+ ['iifname { "' + interface + '", "' + interface_wc + '" }', 'jump VYOS_PBR_UD_smoketest'],
+ ['meta l4proto udp', 'meta mark 0x000007e4', 'drop'],
+ ['tcp flags syn / syn,ack', 'meta mark 0x00000002-0x00000bb8', 'meta mark set ' + mark_hex],
+ ['ct state new', 'tcp dport 22', 'ip saddr 198.51.100.0/24', 'ip ttl > 2', 'meta mark != 0x000001c8', 'meta mark set ' + mark_hex],
+ ['log prefix "[ipv4-route-smoketest-4-A]"', 'icmp type echo-request', 'ip length { 128, 1024-2048 }', 'meta pkttype other', 'meta mark set ' + mark_hex],
+ ['ip dscp { 0x29, 0x39-0x3b }', 'meta mark != 0x000001c8-0x000001f4', 'meta mark set ' + mark_hex],
+ ['log prefix "[ipv4-smoketest-default]"']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ # IPv6
+ nftables6_search = [
+ [f'iifname "{interface_wc}"', 'jump VYOS_PBR6_UD_smoketest'],
+ ['meta l4proto udp', 'drop'],
+ ['tcp flags syn / syn,ack', 'meta mark set ' + mark_hex],
+ ['ct state new', 'tcp dport 22', 'ip6 saddr 2001:db8::/64', 'ip6 hoplimit > 2', 'meta mark set ' + mark_hex],
+ ['log prefix "[ipv6-route6-smoketest6-4-A]"', 'icmpv6 type echo-request', 'ip6 length != { 128, 1024-2048 }', 'meta pkttype multicast', 'meta mark set ' + mark_hex],
+ ['ip6 dscp != { 0x0e-0x13, 0x3d }', 'meta mark set ' + mark_hex],
+ ['log prefix "[ipv6-smoketest6-default]"']
+ ]
+
+ self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py
new file mode 100644
index 0000000..716d0a8
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_bfd.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'bfdd'
+base_path = ['protocols', 'bfd']
+
+dum_if = 'dum1001'
+vrf_name = 'red'
+peers = {
+ '192.0.2.10' : {
+ 'intv_rx' : '500',
+ 'intv_tx' : '600',
+ 'multihop' : '',
+ 'source_addr': '192.0.2.254',
+ 'profile' : 'foo-bar-baz',
+ 'minimum_ttl': '20',
+ },
+ '192.0.2.20' : {
+ 'echo_mode' : '',
+ 'intv_echo' : '100',
+ 'intv_mult' : '100',
+ 'intv_rx' : '222',
+ 'intv_tx' : '333',
+ 'passive' : '',
+ 'shutdown' : '',
+ 'profile' : 'foo',
+ 'source_intf': dum_if,
+ },
+ '2001:db8::1000:1' : {
+ 'source_addr': '2001:db8::1',
+ 'vrf' : vrf_name,
+ },
+ '2001:db8::2000:1' : {
+ 'source_addr': '2001:db8::1',
+ 'multihop' : '',
+ 'profile' : 'baz_foo',
+ },
+}
+
+profiles = {
+ 'foo' : {
+ 'echo_mode' : '',
+ 'intv_echo' : '100',
+ 'intv_mult' : '101',
+ 'intv_rx' : '222',
+ 'intv_tx' : '333',
+ 'shutdown' : '',
+ 'minimum_ttl': '40',
+ },
+ 'foo-bar-baz' : {
+ 'intv_mult' : '4',
+ 'intv_rx' : '400',
+ 'intv_tx' : '400',
+ },
+ 'baz_foo' : {
+ 'intv_mult' : '102',
+ 'intv_rx' : '444',
+ 'passive' : '',
+ },
+}
+
+class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsBFD, cls).setUpClass()
+
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_bfd_peer(self):
+ self.cli_set(['vrf', 'name', vrf_name, 'table', '1000'])
+
+ for peer, peer_config in peers.items():
+ if 'echo_mode' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'echo-mode'])
+ if 'intv_echo' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'interval', 'echo-interval', peer_config["intv_echo"]])
+ if 'intv_mult' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'interval', 'multiplier', peer_config["intv_mult"]])
+ if 'intv_rx' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'interval', 'receive', peer_config["intv_rx"]])
+ if 'intv_tx' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'interval', 'transmit', peer_config["intv_tx"]])
+ if 'minimum_ttl' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'minimum-ttl', peer_config["minimum_ttl"]])
+ if 'multihop' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'multihop'])
+ if 'passive' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'passive'])
+ if 'shutdown' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'shutdown'])
+ if 'source_addr' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'source', 'address', peer_config["source_addr"]])
+ if 'source_intf' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'source', 'interface', peer_config["source_intf"]])
+ if 'vrf' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'vrf', peer_config["vrf"]])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig('bfd', daemon=PROCESS_NAME)
+ for peer, peer_config in peers.items():
+ tmp = f'peer {peer}'
+ if 'multihop' in peer_config:
+ tmp += f' multihop'
+ if 'source_addr' in peer_config:
+ tmp += f' local-address {peer_config["source_addr"]}'
+ if 'source_intf' in peer_config:
+ tmp += f' interface {peer_config["source_intf"]}'
+ if 'vrf' in peer_config:
+ tmp += f' vrf {peer_config["vrf"]}'
+
+ self.assertIn(tmp, frrconfig)
+ peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME)
+
+ if 'echo_mode' in peer_config:
+ self.assertIn(f'echo-mode', peerconfig)
+ if 'intv_echo' in peer_config:
+ self.assertIn(f'echo receive-interval {peer_config["intv_echo"]}', peerconfig)
+ self.assertIn(f'echo transmit-interval {peer_config["intv_echo"]}', peerconfig)
+ if 'intv_mult' in peer_config:
+ self.assertIn(f'detect-multiplier {peer_config["intv_mult"]}', peerconfig)
+ if 'intv_rx' in peer_config:
+ self.assertIn(f'receive-interval {peer_config["intv_rx"]}', peerconfig)
+ if 'intv_tx' in peer_config:
+ self.assertIn(f'transmit-interval {peer_config["intv_tx"]}', peerconfig)
+ if 'minimum_ttl' in peer_config:
+ self.assertIn(f'minimum-ttl {peer_config["minimum_ttl"]}', peerconfig)
+ if 'passive' in peer_config:
+ self.assertIn(f'passive-mode', peerconfig)
+ if 'shutdown' in peer_config:
+ self.assertIn(f'shutdown', peerconfig)
+ else:
+ self.assertNotIn(f'shutdown', peerconfig)
+
+ self.cli_delete(['vrf', 'name', vrf_name])
+
+ def test_bfd_profile(self):
+ for profile, profile_config in profiles.items():
+ if 'echo_mode' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'echo-mode'])
+ if 'intv_echo' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'interval', 'echo-interval', profile_config["intv_echo"]])
+ if 'intv_mult' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'interval', 'multiplier', profile_config["intv_mult"]])
+ if 'intv_rx' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'interval', 'receive', profile_config["intv_rx"]])
+ if 'intv_tx' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'interval', 'transmit', profile_config["intv_tx"]])
+ if 'minimum_ttl' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'minimum-ttl', profile_config["minimum_ttl"]])
+ if 'passive' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'passive'])
+ if 'shutdown' in profile_config:
+ self.cli_set(base_path + ['profile', profile, 'shutdown'])
+
+ for peer, peer_config in peers.items():
+ if 'profile' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'profile', peer_config["profile"] + 'wrong'])
+ if 'source_addr' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'source', 'address', peer_config["source_addr"]])
+ if 'source_intf' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'source', 'interface', peer_config["source_intf"]])
+
+ # BFD profile does not exist!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for peer, peer_config in peers.items():
+ if 'profile' in peer_config:
+ self.cli_set(base_path + ['peer', peer, 'profile', peer_config["profile"]])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ for profile, profile_config in profiles.items():
+ config = self.getFRRconfig(f' profile {profile}', endsection='^ !')
+ if 'echo_mode' in profile_config:
+ self.assertIn(f' echo-mode', config)
+ if 'intv_echo' in profile_config:
+ self.assertIn(f' echo receive-interval {profile_config["intv_echo"]}', config)
+ self.assertIn(f' echo transmit-interval {profile_config["intv_echo"]}', config)
+ if 'intv_mult' in profile_config:
+ self.assertIn(f' detect-multiplier {profile_config["intv_mult"]}', config)
+ if 'intv_rx' in profile_config:
+ self.assertIn(f' receive-interval {profile_config["intv_rx"]}', config)
+ if 'intv_tx' in profile_config:
+ self.assertIn(f' transmit-interval {profile_config["intv_tx"]}', config)
+ if 'minimum_ttl' in profile_config:
+ self.assertIn(f' minimum-ttl {profile_config["minimum_ttl"]}', config)
+ if 'passive' in profile_config:
+ self.assertIn(f' passive-mode', config)
+ if 'shutdown' in profile_config:
+ self.assertIn(f' shutdown', config)
+ else:
+ self.assertNotIn(f'shutdown', config)
+
+ for peer, peer_config in peers.items():
+ peerconfig = self.getFRRconfig(f' peer {peer}', end='', daemon=PROCESS_NAME)
+ if 'profile' in peer_config:
+ self.assertIn(f' profile {peer_config["profile"]}', peerconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
new file mode 100644
index 0000000..ea2f561
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -0,0 +1,1411 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from time import sleep
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.ifconfig import Section
+from vyos.configsession import ConfigSessionError
+from vyos.template import is_ipv6
+from vyos.utils.process import process_named_running
+from vyos.utils.process import cmd
+
+PROCESS_NAME = 'bgpd'
+ASN = '64512'
+base_path = ['protocols', 'bgp']
+
+route_map_in = 'foo-map-in'
+route_map_out = 'foo-map-out'
+prefix_list_in = 'pfx-foo-in'
+prefix_list_out = 'pfx-foo-out'
+prefix_list_in6 = 'pfx-foo-in6'
+prefix_list_out6 = 'pfx-foo-out6'
+bfd_profile = 'foo-bar-baz'
+
+import_afi = 'ipv4-unicast'
+import_vrf = 'red'
+import_rd = ASN + ':100'
+import_vrf_base = ['vrf', 'name']
+neighbor_config = {
+ '192.0.2.1' : {
+ 'bfd' : '',
+ 'cap_dynamic' : '',
+ 'cap_ext_next' : '',
+ 'cap_ext_sver' : '',
+ 'remote_as' : '100',
+ 'adv_interv' : '400',
+ 'passive' : '',
+ 'password' : 'VyOS-Secure123',
+ 'shutdown' : '',
+ 'cap_over' : '',
+ 'ttl_security' : '5',
+ 'system_as' : '300',
+ 'route_map_in' : route_map_in,
+ 'route_map_out' : route_map_out,
+ 'no_send_comm_ext' : '',
+ 'addpath_all' : '',
+ 'p_attr_discard' : ['10', '20', '30', '40', '50'],
+ },
+ '192.0.2.2' : {
+ 'bfd_profile' : bfd_profile,
+ 'remote_as' : '200',
+ 'shutdown' : '',
+ 'no_cap_nego' : '',
+ 'port' : '667',
+ 'cap_strict' : '',
+ 'advertise_map' : route_map_in,
+ 'non_exist_map' : route_map_out,
+ 'pfx_list_in' : prefix_list_in,
+ 'pfx_list_out' : prefix_list_out,
+ 'no_send_comm_std' : '',
+ 'local_role' : 'rs-client',
+ 'p_attr_taw' : '200',
+ },
+ '192.0.2.3' : {
+ 'advertise_map' : route_map_in,
+ 'description' : 'foo bar baz',
+ 'remote_as' : '200',
+ 'passive' : '',
+ 'multi_hop' : '5',
+ 'update_src' : 'lo',
+ 'peer_group' : 'foo',
+ 'graceful_rst' : '',
+ },
+ '2001:db8::1' : {
+ 'advertise_map' : route_map_in,
+ 'exist_map' : route_map_out,
+ 'cap_dynamic' : '',
+ 'cap_ext_next' : '',
+ 'cap_ext_sver' : '',
+ 'remote_as' : '123',
+ 'adv_interv' : '400',
+ 'passive' : '',
+ 'password' : 'VyOS-Secure123',
+ 'shutdown' : '',
+ 'cap_over' : '',
+ 'ttl_security' : '5',
+ 'system_as' : '300',
+ 'solo' : '',
+ 'route_map_in' : route_map_in,
+ 'route_map_out' : route_map_out,
+ 'no_send_comm_std' : '',
+ 'addpath_per_as' : '',
+ 'peer_group' : 'foo-bar',
+ 'local_role' : 'customer',
+ 'local_role_strict': '',
+ },
+ '2001:db8::2' : {
+ 'remote_as' : '456',
+ 'shutdown' : '',
+ 'no_cap_nego' : '',
+ 'port' : '667',
+ 'cap_strict' : '',
+ 'pfx_list_in' : prefix_list_in6,
+ 'pfx_list_out' : prefix_list_out6,
+ 'no_send_comm_ext' : '',
+ 'peer_group' : 'foo-bar_baz',
+ 'graceful_rst_hlp' : '',
+ 'disable_conn_chk' : '',
+ },
+}
+
+peer_group_config = {
+ 'foo' : {
+ 'advertise_map' : route_map_in,
+ 'exist_map' : route_map_out,
+ 'bfd' : '',
+ 'remote_as' : '100',
+ 'passive' : '',
+ 'password' : 'VyOS-Secure123',
+ 'shutdown' : '',
+ 'cap_over' : '',
+ 'ttl_security' : '5',
+ 'disable_conn_chk' : '',
+ 'p_attr_discard' : ['100', '150', '200'],
+ },
+ 'bar' : {
+ 'remote_as' : '111',
+ 'graceful_rst_no' : '',
+ 'port' : '667',
+ 'p_attr_taw' : '126',
+ },
+ 'foo-bar' : {
+ 'advertise_map' : route_map_in,
+ 'description' : 'foo peer bar group',
+ 'remote_as' : '200',
+ 'shutdown' : '',
+ 'no_cap_nego' : '',
+ 'system_as' : '300',
+ 'pfx_list_in' : prefix_list_in,
+ 'pfx_list_out' : prefix_list_out,
+ 'no_send_comm_ext' : '',
+ },
+ 'foo-bar_baz' : {
+ 'advertise_map' : route_map_in,
+ 'non_exist_map' : route_map_out,
+ 'bfd_profile' : bfd_profile,
+ 'cap_dynamic' : '',
+ 'cap_ext_next' : '',
+ 'remote_as' : '200',
+ 'passive' : '',
+ 'multi_hop' : '5',
+ 'update_src' : 'lo',
+ 'route_map_in' : route_map_in,
+ 'route_map_out' : route_map_out,
+ 'local_role' : 'peer',
+ 'local_role_strict': '',
+ },
+}
+class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsBGP, cls).setUpClass()
+
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['policy', 'route-map'])
+ cls.cli_delete(cls, ['policy', 'prefix-list'])
+ cls.cli_delete(cls, ['policy', 'prefix-list6'])
+ cls.cli_delete(cls, ['vrf'])
+
+ cls.cli_set(cls, ['policy', 'route-map', route_map_in, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'route-map', route_map_out, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '10', 'prefix', '192.0.2.0/25'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '10', 'prefix', '192.0.2.128/25'])
+
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in6, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in6, 'rule', '10', 'prefix', '2001:db8:1000::/64'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'action', 'deny'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out6, 'rule', '10', 'prefix', '2001:db8:2000::/64'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['policy', 'route-map'])
+ cls.cli_delete(cls, ['policy', 'prefix-list'])
+ cls.cli_delete(cls, ['policy', 'prefix-list6'])
+
+ def setUp(self):
+ self.cli_set(base_path + ['system-as', ASN])
+
+ def tearDown(self):
+ # cleanup any possible VRF mess
+ self.cli_delete(['vrf'])
+ # always destrox the entire bgpd configuration to make the processes
+ # life as hard as possible
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def create_bgp_instances_for_import_test(self):
+ table = '1000'
+ self.cli_set(import_vrf_base + [import_vrf, 'table', table])
+ self.cli_set(import_vrf_base + [import_vrf, 'protocols', 'bgp', 'system-as', ASN])
+
+ def verify_frr_config(self, peer, peer_config, frrconfig):
+ # recurring patterns to verify for both a simple neighbor and a peer-group
+ if 'bfd' in peer_config:
+ self.assertIn(f' neighbor {peer} bfd', frrconfig)
+ if 'bfd_profile' in peer_config:
+ self.assertIn(f' neighbor {peer} bfd profile {peer_config["bfd_profile"]}', frrconfig)
+ self.assertIn(f' neighbor {peer} bfd check-control-plane-failure', frrconfig)
+ if 'cap_dynamic' in peer_config:
+ self.assertIn(f' neighbor {peer} capability dynamic', frrconfig)
+ if 'cap_ext_next' in peer_config:
+ self.assertIn(f' neighbor {peer} capability extended-nexthop', frrconfig)
+ if 'cap_ext_sver' in peer_config:
+ self.assertIn(f' neighbor {peer} capability software-version', frrconfig)
+ if 'description' in peer_config:
+ self.assertIn(f' neighbor {peer} description {peer_config["description"]}', frrconfig)
+ if 'no_cap_nego' in peer_config:
+ self.assertIn(f' neighbor {peer} dont-capability-negotiate', frrconfig)
+ if 'multi_hop' in peer_config:
+ self.assertIn(f' neighbor {peer} ebgp-multihop {peer_config["multi_hop"]}', frrconfig)
+ if 'local_as' in peer_config:
+ self.assertIn(f' neighbor {peer} local-as {peer_config["local_as"]} no-prepend replace-as', frrconfig)
+ if 'local_role' in peer_config:
+ tmp = f' neighbor {peer} local-role {peer_config["local_role"]}'
+ if 'local_role_strict' in peer_config:
+ tmp += ' strict'
+ self.assertIn(tmp, frrconfig)
+ if 'cap_over' in peer_config:
+ self.assertIn(f' neighbor {peer} override-capability', frrconfig)
+ if 'passive' in peer_config:
+ self.assertIn(f' neighbor {peer} passive', frrconfig)
+ if 'password' in peer_config:
+ self.assertIn(f' neighbor {peer} password {peer_config["password"]}', frrconfig)
+ if 'port' in peer_config:
+ self.assertIn(f' neighbor {peer} port {peer_config["port"]}', frrconfig)
+ if 'remote_as' in peer_config:
+ self.assertIn(f' neighbor {peer} remote-as {peer_config["remote_as"]}', frrconfig)
+ if 'solo' in peer_config:
+ self.assertIn(f' neighbor {peer} solo', frrconfig)
+ if 'shutdown' in peer_config:
+ self.assertIn(f' neighbor {peer} shutdown', frrconfig)
+ if 'ttl_security' in peer_config:
+ self.assertIn(f' neighbor {peer} ttl-security hops {peer_config["ttl_security"]}', frrconfig)
+ if 'update_src' in peer_config:
+ self.assertIn(f' neighbor {peer} update-source {peer_config["update_src"]}', frrconfig)
+ if 'route_map_in' in peer_config:
+ self.assertIn(f' neighbor {peer} route-map {peer_config["route_map_in"]} in', frrconfig)
+ if 'route_map_out' in peer_config:
+ self.assertIn(f' neighbor {peer} route-map {peer_config["route_map_out"]} out', frrconfig)
+ if 'pfx_list_in' in peer_config:
+ self.assertIn(f' neighbor {peer} prefix-list {peer_config["pfx_list_in"]} in', frrconfig)
+ if 'pfx_list_out' in peer_config:
+ self.assertIn(f' neighbor {peer} prefix-list {peer_config["pfx_list_out"]} out', frrconfig)
+ if 'no_send_comm_std' in peer_config:
+ self.assertIn(f' no neighbor {peer} send-community', frrconfig)
+ if 'no_send_comm_ext' in peer_config:
+ self.assertIn(f' no neighbor {peer} send-community extended', frrconfig)
+ if 'addpath_all' in peer_config:
+ self.assertIn(f' neighbor {peer} addpath-tx-all-paths', frrconfig)
+ if 'p_attr_discard' in peer_config:
+ tmp = ' '.join(peer_config["p_attr_discard"])
+ self.assertIn(f' neighbor {peer} path-attribute discard {tmp}', frrconfig)
+ if 'p_attr_taw' in peer_config:
+ self.assertIn(f' neighbor {peer} path-attribute treat-as-withdraw {peer_config["p_attr_taw"]}', frrconfig)
+ if 'addpath_per_as' in peer_config:
+ self.assertIn(f' neighbor {peer} addpath-tx-bestpath-per-AS', frrconfig)
+ if 'advertise_map' in peer_config:
+ base = f' neighbor {peer} advertise-map {peer_config["advertise_map"]}'
+ if 'exist_map' in peer_config:
+ base = f'{base} exist-map {peer_config["exist_map"]}'
+ if 'non_exist_map' in peer_config:
+ base = f'{base} non-exist-map {peer_config["non_exist_map"]}'
+ self.assertIn(base, frrconfig)
+ if 'graceful_rst' in peer_config:
+ self.assertIn(f' neighbor {peer} graceful-restart', frrconfig)
+ if 'graceful_rst_no' in peer_config:
+ self.assertIn(f' neighbor {peer} graceful-restart-disable', frrconfig)
+ if 'graceful_rst_hlp' in peer_config:
+ self.assertIn(f' neighbor {peer} graceful-restart-helper', frrconfig)
+ if 'disable_conn_chk' in peer_config:
+ self.assertIn(f' neighbor {peer} disable-connected-check', frrconfig)
+
+ def test_bgp_01_simple(self):
+ router_id = '127.0.0.1'
+ local_pref = '500'
+ stalepath_time = '60'
+ max_path_v4 = '2'
+ max_path_v4ibgp = '4'
+ max_path_v6 = '8'
+ max_path_v6ibgp = '16'
+ cond_adv_timer = '30'
+ min_hold_time = '2'
+ tcp_keepalive_idle = '66'
+ tcp_keepalive_interval = '77'
+ tcp_keepalive_probes = '22'
+
+ self.cli_set(base_path + ['parameters', 'allow-martian-nexthop'])
+ self.cli_set(base_path + ['parameters', 'disable-ebgp-connected-route-check'])
+ self.cli_set(base_path + ['parameters', 'no-hard-administrative-reset'])
+ self.cli_set(base_path + ['parameters', 'log-neighbor-changes'])
+ self.cli_set(base_path + ['parameters', 'labeled-unicast', 'explicit-null'])
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+
+ # System AS number MUST be defined - as this is set in setUp() we remove
+ # this once for testing of the proper error
+ self.cli_delete(base_path + ['system-as'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['system-as', ASN])
+
+ # Default local preference (higher = more preferred, default value is 100)
+ self.cli_set(base_path + ['parameters', 'default', 'local-pref', local_pref])
+ self.cli_set(base_path + ['parameters', 'graceful-restart', 'stalepath-time', stalepath_time])
+ self.cli_set(base_path + ['parameters', 'graceful-shutdown'])
+ self.cli_set(base_path + ['parameters', 'ebgp-requires-policy'])
+
+ self.cli_set(base_path + ['parameters', 'bestpath', 'as-path', 'multipath-relax'])
+ self.cli_set(base_path + ['parameters', 'bestpath', 'bandwidth', 'default-weight-for-missing'])
+ self.cli_set(base_path + ['parameters', 'bestpath', 'compare-routerid'])
+ self.cli_set(base_path + ['parameters', 'bestpath', 'peer-type', 'multipath-relax'])
+
+ self.cli_set(base_path + ['parameters', 'conditional-advertisement', 'timer', cond_adv_timer])
+ self.cli_set(base_path + ['parameters', 'fast-convergence'])
+ self.cli_set(base_path + ['parameters', 'minimum-holdtime', min_hold_time])
+ self.cli_set(base_path + ['parameters', 'no-suppress-duplicates'])
+ self.cli_set(base_path + ['parameters', 'reject-as-sets'])
+ self.cli_set(base_path + ['parameters', 'route-reflector-allow-outbound-policy'])
+ self.cli_set(base_path + ['parameters', 'shutdown'])
+ self.cli_set(base_path + ['parameters', 'suppress-fib-pending'])
+ self.cli_set(base_path + ['parameters', 'tcp-keepalive', 'idle', tcp_keepalive_idle])
+ self.cli_set(base_path + ['parameters', 'tcp-keepalive', 'interval', tcp_keepalive_interval])
+ self.cli_set(base_path + ['parameters', 'tcp-keepalive', 'probes', tcp_keepalive_probes])
+
+ # AFI maximum path support
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ebgp', max_path_v4])
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp])
+ self.cli_set(base_path + ['address-family', 'ipv4-labeled-unicast', 'maximum-paths', 'ebgp', max_path_v4])
+ self.cli_set(base_path + ['address-family', 'ipv4-labeled-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp])
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ebgp', max_path_v6])
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ibgp', max_path_v6ibgp])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' bgp router-id {router_id}', frrconfig)
+ self.assertIn(f' bgp allow-martian-nexthop', frrconfig)
+ self.assertIn(f' bgp disable-ebgp-connected-route-check', frrconfig)
+ self.assertIn(f' bgp log-neighbor-changes', frrconfig)
+ self.assertIn(f' bgp default local-preference {local_pref}', frrconfig)
+ self.assertIn(f' bgp conditional-advertisement timer {cond_adv_timer}', frrconfig)
+ self.assertIn(f' bgp fast-convergence', frrconfig)
+ self.assertIn(f' bgp graceful-restart stalepath-time {stalepath_time}', frrconfig)
+ self.assertIn(f' bgp graceful-shutdown', frrconfig)
+ self.assertIn(f' no bgp hard-administrative-reset', frrconfig)
+ self.assertIn(f' bgp labeled-unicast explicit-null', frrconfig)
+ self.assertIn(f' bgp bestpath as-path multipath-relax', frrconfig)
+ self.assertIn(f' bgp bestpath bandwidth default-weight-for-missing', frrconfig)
+ self.assertIn(f' bgp bestpath compare-routerid', frrconfig)
+ self.assertIn(f' bgp bestpath peer-type multipath-relax', frrconfig)
+ self.assertIn(f' bgp minimum-holdtime {min_hold_time}', frrconfig)
+ self.assertIn(f' bgp reject-as-sets', frrconfig)
+ self.assertIn(f' bgp route-reflector allow-outbound-policy', frrconfig)
+ self.assertIn(f' bgp shutdown', frrconfig)
+ self.assertIn(f' bgp suppress-fib-pending', frrconfig)
+ self.assertIn(f' bgp tcp-keepalive {tcp_keepalive_idle} {tcp_keepalive_interval} {tcp_keepalive_probes}', frrconfig)
+ self.assertNotIn(f'bgp ebgp-requires-policy', frrconfig)
+ self.assertIn(f' no bgp suppress-duplicates', frrconfig)
+
+ afiv4_config = self.getFRRconfig(' address-family ipv4 unicast')
+ self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config)
+ self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config)
+
+ afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast')
+ self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config)
+ self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config)
+
+ afiv6_config = self.getFRRconfig(' address-family ipv6 unicast')
+ self.assertIn(f' maximum-paths {max_path_v6}', afiv6_config)
+ self.assertIn(f' maximum-paths ibgp {max_path_v6ibgp}', afiv6_config)
+
+ def test_bgp_02_neighbors(self):
+ # Test out individual neighbor configuration items, not all of them are
+ # also available to a peer-group!
+ self.cli_set(base_path + ['parameters', 'deterministic-med'])
+
+ for peer, peer_config in neighbor_config.items():
+ afi = 'ipv4-unicast'
+ if is_ipv6(peer):
+ afi = 'ipv6-unicast'
+
+ if 'adv_interv' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'advertisement-interval', peer_config["adv_interv"]])
+ if 'bfd' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'bfd'])
+ if 'bfd_profile' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'bfd', 'profile', peer_config["bfd_profile"]])
+ self.cli_set(base_path + ['neighbor', peer, 'bfd', 'check-control-plane-failure'])
+ if 'cap_dynamic' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'capability', 'dynamic'])
+ if 'cap_ext_next' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'capability', 'extended-nexthop'])
+ if 'cap_ext_sver' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'capability', 'software-version'])
+ if 'description' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'description', peer_config["description"]])
+ if 'no_cap_nego' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'disable-capability-negotiation'])
+ if 'multi_hop' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'ebgp-multihop', peer_config["multi_hop"]])
+ if 'local_as' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'local-as', peer_config["local_as"], 'no-prepend', 'replace-as'])
+ if 'local_role' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'local-role', peer_config["local_role"]])
+ if 'local_role_strict' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'local-role', peer_config["local_role"], 'strict'])
+ if 'cap_over' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'override-capability'])
+ if 'passive' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'passive'])
+ if 'password' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'password', peer_config["password"]])
+ if 'port' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'port', peer_config["port"]])
+ if 'remote_as' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'remote-as', peer_config["remote_as"]])
+ if 'cap_strict' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'strict-capability-match'])
+ if 'shutdown' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'shutdown'])
+ if 'solo' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'solo'])
+ if 'ttl_security' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'ttl-security', 'hops', peer_config["ttl_security"]])
+ if 'update_src' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'update-source', peer_config["update_src"]])
+ if 'p_attr_discard' in peer_config:
+ for attribute in peer_config['p_attr_discard']:
+ self.cli_set(base_path + ['neighbor', peer, 'path-attribute', 'discard', attribute])
+ if 'p_attr_taw' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'path-attribute', 'treat-as-withdraw', peer_config["p_attr_taw"]])
+ if 'route_map_in' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'route-map', 'import', peer_config["route_map_in"]])
+ if 'route_map_out' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'route-map', 'export', peer_config["route_map_out"]])
+ if 'pfx_list_in' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'prefix-list', 'import', peer_config["pfx_list_in"]])
+ if 'pfx_list_out' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'prefix-list', 'export', peer_config["pfx_list_out"]])
+ if 'no_send_comm_std' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'disable-send-community', 'standard'])
+ if 'no_send_comm_ext' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'disable-send-community', 'extended'])
+ if 'addpath_all' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'addpath-tx-all'])
+ if 'addpath_per_as' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'addpath-tx-per-as'])
+ if 'graceful_rst' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'enable'])
+ if 'graceful_rst_no' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'disable'])
+ if 'graceful_rst_hlp' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'restart-helper'])
+ if 'disable_conn_chk' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'disable-connected-check'])
+
+ # Conditional advertisement
+ if 'advertise_map' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'advertise-map', peer_config["advertise_map"]])
+ # Either exist-map or non-exist-map needs to be specified
+ if 'exist_map' not in peer_config and 'non_exist_map' not in peer_config:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'exist-map', route_map_in])
+
+ if 'exist_map' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'exist-map', peer_config["exist_map"]])
+ if 'non_exist_map' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'address-family', afi, 'conditionally-advertise', 'non-exist-map', peer_config["non_exist_map"]])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+
+ for peer, peer_config in neighbor_config.items():
+ if 'adv_interv' in peer_config:
+ self.assertIn(f' neighbor {peer} advertisement-interval {peer_config["adv_interv"]}', frrconfig)
+ if 'cap_strict' in peer_config:
+ self.assertIn(f' neighbor {peer} strict-capability-match', frrconfig)
+
+ self.verify_frr_config(peer, peer_config, frrconfig)
+
+ def test_bgp_03_peer_groups(self):
+ # Test out individual peer-group configuration items
+ for peer_group, config in peer_group_config.items():
+ if 'bfd' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'bfd'])
+ if 'bfd_profile' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'bfd', 'profile', config["bfd_profile"]])
+ self.cli_set(base_path + ['peer-group', peer_group, 'bfd', 'check-control-plane-failure'])
+ if 'cap_dynamic' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'capability', 'dynamic'])
+ if 'cap_ext_next' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'capability', 'extended-nexthop'])
+ if 'cap_ext_sver' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'capability', 'software-version'])
+ if 'description' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'description', config["description"]])
+ if 'no_cap_nego' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'disable-capability-negotiation'])
+ if 'multi_hop' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'ebgp-multihop', config["multi_hop"]])
+ if 'local_as' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'local-as', config["local_as"], 'no-prepend', 'replace-as'])
+ if 'local_role' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'local-role', config["local_role"]])
+ if 'local_role_strict' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'local-role', config["local_role"], 'strict'])
+ if 'cap_over' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'override-capability'])
+ if 'passive' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'passive'])
+ if 'password' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'password', config["password"]])
+ if 'port' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'port', config["port"]])
+ if 'remote_as' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'remote-as', config["remote_as"]])
+ if 'shutdown' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'shutdown'])
+ if 'ttl_security' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'ttl-security', 'hops', config["ttl_security"]])
+ if 'update_src' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'update-source', config["update_src"]])
+ if 'route_map_in' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'route-map', 'import', config["route_map_in"]])
+ if 'route_map_out' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'route-map', 'export', config["route_map_out"]])
+ if 'pfx_list_in' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'prefix-list', 'import', config["pfx_list_in"]])
+ if 'pfx_list_out' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'prefix-list', 'export', config["pfx_list_out"]])
+ if 'no_send_comm_std' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'disable-send-community', 'standard'])
+ if 'no_send_comm_ext' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'disable-send-community', 'extended'])
+ if 'addpath_all' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'addpath-tx-all'])
+ if 'addpath_per_as' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'addpath-tx-per-as'])
+ if 'graceful_rst' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'enable'])
+ if 'graceful_rst_no' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'disable'])
+ if 'graceful_rst_hlp' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'restart-helper'])
+ if 'disable_conn_chk' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'disable-connected-check'])
+ if 'p_attr_discard' in config:
+ for attribute in config['p_attr_discard']:
+ self.cli_set(base_path + ['peer-group', peer_group, 'path-attribute', 'discard', attribute])
+ if 'p_attr_taw' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'path-attribute', 'treat-as-withdraw', config["p_attr_taw"]])
+
+ # Conditional advertisement
+ if 'advertise_map' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'advertise-map', config["advertise_map"]])
+ # Either exist-map or non-exist-map needs to be specified
+ if 'exist_map' not in config and 'non_exist_map' not in config:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'exist-map', route_map_in])
+
+ if 'exist_map' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'exist-map', config["exist_map"]])
+ if 'non_exist_map' in config:
+ self.cli_set(base_path + ['peer-group', peer_group, 'address-family', 'ipv4-unicast', 'conditionally-advertise', 'non-exist-map', config["non_exist_map"]])
+
+ for peer, peer_config in neighbor_config.items():
+ if 'peer_group' in peer_config:
+ self.cli_set(base_path + ['neighbor', peer, 'peer-group', peer_config['peer_group']])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+
+ for peer, peer_config in peer_group_config.items():
+ self.assertIn(f' neighbor {peer_group} peer-group', frrconfig)
+ self.verify_frr_config(peer, peer_config, frrconfig)
+
+ for peer, peer_config in neighbor_config.items():
+ if 'peer_group' in peer_config:
+ self.assertIn(f' neighbor {peer} peer-group {peer_config["peer_group"]}', frrconfig)
+
+ def test_bgp_04_afi_ipv4(self):
+ networks = {
+ '10.0.0.0/8' : {
+ 'as_set' : '',
+ 'summary_only' : '',
+ 'route_map' : route_map_in,
+ },
+ '100.64.0.0/10' : {
+ 'as_set' : '',
+ },
+ '192.168.0.0/16' : {
+ 'summary_only' : '',
+ },
+ }
+
+ # We want to redistribute ...
+ redistributes = ['connected', 'isis', 'kernel', 'ospf', 'rip', 'static']
+ for redistribute in redistributes:
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast',
+ 'redistribute', redistribute])
+
+ for network, network_config in networks.items():
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast',
+ 'network', network])
+ if 'as_set' in network_config:
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast',
+ 'aggregate-address', network, 'as-set'])
+ if 'summary_only' in network_config:
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast',
+ 'aggregate-address', network, 'summary-only'])
+ if 'route_map' in network_config:
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast',
+ 'aggregate-address', network, 'route-map', network_config['route_map']])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family ipv4 unicast', frrconfig)
+
+ for redistribute in redistributes:
+ self.assertIn(f' redistribute {redistribute}', frrconfig)
+
+ for network, network_config in networks.items():
+ self.assertIn(f' network {network}', frrconfig)
+ command = f'aggregate-address {network}'
+ if 'as_set' in network_config:
+ command = f'{command} as-set'
+ if 'summary_only' in network_config:
+ command = f'{command} summary-only'
+ if 'route_map' in network_config:
+ command = f'{command} route-map {network_config["route_map"]}'
+ self.assertIn(command, frrconfig)
+
+ def test_bgp_05_afi_ipv6(self):
+ networks = {
+ '2001:db8:100::/48' : {
+ },
+ '2001:db8:200::/48' : {
+ },
+ '2001:db8:300::/48' : {
+ 'summary_only' : '',
+ },
+ }
+
+ # We want to redistribute ...
+ redistributes = ['connected', 'kernel', 'ospfv3', 'ripng', 'static']
+ for redistribute in redistributes:
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast',
+ 'redistribute', redistribute])
+
+ for network, network_config in networks.items():
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast',
+ 'network', network])
+ if 'summary_only' in network_config:
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast',
+ 'aggregate-address', network, 'summary-only'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family ipv6 unicast', frrconfig)
+ # T2100: By default ebgp-requires-policy is disabled to keep VyOS
+ # 1.3 and 1.2 backwards compatibility
+ self.assertIn(f' no bgp ebgp-requires-policy', frrconfig)
+
+ for redistribute in redistributes:
+ # FRR calls this OSPF6
+ if redistribute == 'ospfv3':
+ redistribute = 'ospf6'
+ self.assertIn(f' redistribute {redistribute}', frrconfig)
+
+ for network, network_config in networks.items():
+ self.assertIn(f' network {network}', frrconfig)
+ if 'as_set' in network_config:
+ self.assertIn(f' aggregate-address {network} summary-only', frrconfig)
+
+ def test_bgp_06_listen_range(self):
+ # Implemented via T1875
+ limit = '64'
+ listen_ranges = ['192.0.2.0/25', '192.0.2.128/25']
+ peer_group = 'listenfoobar'
+
+ self.cli_set(base_path + ['listen', 'limit', limit])
+
+ for prefix in listen_ranges:
+ self.cli_set(base_path + ['listen', 'range', prefix])
+ # check validate() - peer-group must be defined for range/prefix
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['listen', 'range', prefix, 'peer-group', peer_group])
+
+ # check validate() - peer-group does yet not exist!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['peer-group', peer_group, 'remote-as', ASN])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {peer_group} peer-group', frrconfig)
+ self.assertIn(f' neighbor {peer_group} remote-as {ASN}', frrconfig)
+ self.assertIn(f' bgp listen limit {limit}', frrconfig)
+ for prefix in listen_ranges:
+ self.assertIn(f' bgp listen range {prefix} peer-group {peer_group}', frrconfig)
+
+ def test_bgp_07_l2vpn_evpn(self):
+ vnis = ['10010', '10020', '10030']
+ soo = '1.2.3.4:10000'
+ evi_limit = '1000'
+ route_targets = ['1.1.1.1:100', '1.1.1.1:200', '1.1.1.1:300']
+
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'advertise-all-vni'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'advertise-default-gw'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'advertise-svi-ip'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'flooding', 'disable'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'default-originate', 'ipv4'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'default-originate', 'ipv6'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'disable-ead-evi-rx'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'disable-ead-evi-tx'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'mac-vrf', 'soo', soo])
+ for vni in vnis:
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'vni', vni, 'advertise-default-gw'])
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'vni', vni, 'advertise-svi-ip'])
+
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'ead-es-frag', 'evi-limit', evi_limit])
+ for route_target in route_targets:
+ self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'ead-es-route-target', 'export', route_target])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family l2vpn evpn', frrconfig)
+ self.assertIn(f' advertise-all-vni', frrconfig)
+ self.assertIn(f' advertise-default-gw', frrconfig)
+ self.assertIn(f' advertise-svi-ip', frrconfig)
+ self.assertIn(f' default-originate ipv4', frrconfig)
+ self.assertIn(f' default-originate ipv6', frrconfig)
+ self.assertIn(f' disable-ead-evi-rx', frrconfig)
+ self.assertIn(f' disable-ead-evi-tx', frrconfig)
+ self.assertIn(f' flooding disable', frrconfig)
+ self.assertIn(f' mac-vrf soo {soo}', frrconfig)
+ for vni in vnis:
+ vniconfig = self.getFRRconfig(f' vni {vni}')
+ self.assertIn(f'vni {vni}', vniconfig)
+ self.assertIn(f' advertise-default-gw', vniconfig)
+ self.assertIn(f' advertise-svi-ip', vniconfig)
+ self.assertIn(f' ead-es-frag evi-limit {evi_limit}', frrconfig)
+ for route_target in route_targets:
+ self.assertIn(f' ead-es-route-target export {route_target}', frrconfig)
+
+
+ def test_bgp_09_distance_and_flowspec(self):
+ distance_external = '25'
+ distance_internal = '30'
+ distance_local = '35'
+ distance_v4_prefix = '169.254.0.0/32'
+ distance_v6_prefix = '2001::/128'
+ distance_prefix_value = '110'
+ distance_families = ['ipv4-unicast', 'ipv6-unicast','ipv4-multicast', 'ipv6-multicast']
+ verify_families = ['ipv4 unicast', 'ipv6 unicast','ipv4 multicast', 'ipv6 multicast']
+ flowspec_families = ['address-family ipv4 flowspec', 'address-family ipv6 flowspec']
+ flowspec_int = 'lo'
+
+ # Per family distance support
+ for family in distance_families:
+ self.cli_set(base_path + ['address-family', family, 'distance', 'external', distance_external])
+ self.cli_set(base_path + ['address-family', family, 'distance', 'internal', distance_internal])
+ self.cli_set(base_path + ['address-family', family, 'distance', 'local', distance_local])
+ if 'ipv4' in family:
+ self.cli_set(base_path + ['address-family', family, 'distance',
+ 'prefix', distance_v4_prefix, 'distance', distance_prefix_value])
+ if 'ipv6' in family:
+ self.cli_set(base_path + ['address-family', family, 'distance',
+ 'prefix', distance_v6_prefix, 'distance', distance_prefix_value])
+
+ # IPv4 flowspec interface check
+ self.cli_set(base_path + ['address-family', 'ipv4-flowspec', 'local-install', 'interface', flowspec_int])
+
+ # IPv6 flowspec interface check
+ self.cli_set(base_path + ['address-family', 'ipv6-flowspec', 'local-install', 'interface', flowspec_int])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Verify FRR distances configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ for family in verify_families:
+ self.assertIn(f'address-family {family}', frrconfig)
+ self.assertIn(f'distance bgp {distance_external} {distance_internal} {distance_local}', frrconfig)
+ if 'ipv4' in family:
+ self.assertIn(f'distance {distance_prefix_value} {distance_v4_prefix}', frrconfig)
+ if 'ipv6' in family:
+ self.assertIn(f'distance {distance_prefix_value} {distance_v6_prefix}', frrconfig)
+
+ # Verify FRR flowspec configuration
+ for family in flowspec_families:
+ self.assertIn(f'{family}', frrconfig)
+ self.assertIn(f'local-install {flowspec_int}', frrconfig)
+
+ def test_bgp_10_vrf_simple(self):
+ router_id = '127.0.0.3'
+ vrfs = ['red', 'green', 'blue']
+
+ # It is safe to assume that when the basic VRF test works, all
+ # other BGP related features work, as we entirely inherit the CLI
+ # templates and Jinja2 FRR template.
+ table = '1000'
+
+ # testing only one AFI is sufficient as it's generic code
+ for vrf in vrfs:
+ vrf_base = ['vrf', 'name', vrf]
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN])
+ self.cli_set(vrf_base + ['protocols', 'bgp', 'parameters', 'router-id', router_id])
+ table = str(int(table) + 1000)
+
+ # import VRF routes do main RIB
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'import', 'vrf', vrf])
+
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family ipv6 unicast', frrconfig)
+
+ for vrf in vrfs:
+ self.assertIn(f' import vrf {vrf}', frrconfig)
+
+ # Verify FRR bgpd configuration
+ frr_vrf_config = self.getFRRconfig(f'router bgp {ASN} vrf {vrf}')
+ self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config)
+ self.assertIn(f' bgp router-id {router_id}', frr_vrf_config)
+
+ def test_bgp_11_confederation(self):
+ router_id = '127.10.10.2'
+ confed_id = str(int(ASN) + 1)
+ confed_asns = '10 20 30 40'
+
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+ self.cli_set(base_path + ['parameters', 'confederation', 'identifier', confed_id])
+ for asn in confed_asns.split():
+ self.cli_set(base_path + ['parameters', 'confederation', 'peers', asn])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' bgp router-id {router_id}', frrconfig)
+ self.assertIn(f' bgp confederation identifier {confed_id}', frrconfig)
+ self.assertIn(f' bgp confederation peers {confed_asns}', frrconfig)
+
+ def test_bgp_12_v6_link_local(self):
+ remote_asn = str(int(ASN) + 10)
+ interface = 'eth0'
+
+ self.cli_set(base_path + ['neighbor', interface, 'address-family', 'ipv6-unicast'])
+ self.cli_set(base_path + ['neighbor', interface, 'interface', 'v6only', 'remote-as', remote_asn])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {interface} interface v6only remote-as {remote_asn}', frrconfig)
+ self.assertIn(f' address-family ipv6 unicast', frrconfig)
+ self.assertIn(f' neighbor {interface} activate', frrconfig)
+ self.assertIn(f' exit-address-family', frrconfig)
+
+ def test_bgp_13_vpn(self):
+ remote_asn = str(int(ASN) + 150)
+ neighbor = '192.0.2.55'
+ vrf_name = 'red'
+ label = 'auto'
+ rd = f'{neighbor}:{ASN}'
+ rt_export = f'{neighbor}:1002 1.2.3.4:567'
+ rt_import = f'{neighbor}:1003 500:100'
+
+ # testing only one AFI is sufficient as it's generic code
+ for afi in ['ipv4-unicast', 'ipv6-unicast']:
+ self.cli_set(base_path + ['address-family', afi, 'export', 'vpn'])
+ self.cli_set(base_path + ['address-family', afi, 'import', 'vpn'])
+ self.cli_set(base_path + ['address-family', afi, 'label', 'vpn', 'export', label])
+ self.cli_set(base_path + ['address-family', afi, 'label', 'vpn', 'allocation-mode', 'per-nexthop'])
+ self.cli_set(base_path + ['address-family', afi, 'rd', 'vpn', 'export', rd])
+ self.cli_set(base_path + ['address-family', afi, 'route-map', 'vpn', 'export', route_map_out])
+ self.cli_set(base_path + ['address-family', afi, 'route-map', 'vpn', 'import', route_map_in])
+ self.cli_set(base_path + ['address-family', afi, 'route-target', 'vpn', 'export', rt_export])
+ self.cli_set(base_path + ['address-family', afi, 'route-target', 'vpn', 'import', rt_import])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+
+ for afi in ['ipv4', 'ipv6']:
+ afi_config = self.getFRRconfig(f' address-family {afi} unicast', endsection='exit-address-family', daemon='bgpd')
+ self.assertIn(f'address-family {afi} unicast', afi_config)
+ self.assertIn(f' export vpn', afi_config)
+ self.assertIn(f' import vpn', afi_config)
+ self.assertIn(f' label vpn export {label}', afi_config)
+ self.assertIn(f' label vpn export allocation-mode per-nexthop', afi_config)
+ self.assertIn(f' rd vpn export {rd}', afi_config)
+ self.assertIn(f' route-map vpn export {route_map_out}', afi_config)
+ self.assertIn(f' route-map vpn import {route_map_in}', afi_config)
+ self.assertIn(f' rt vpn export {rt_export}', afi_config)
+ self.assertIn(f' rt vpn import {rt_import}', afi_config)
+ self.assertIn(f' exit-address-family', afi_config)
+
+ def test_bgp_14_remote_as_peer_group_override(self):
+ # Peer-group member cannot override remote-as of peer-group
+ remote_asn = str(int(ASN) + 150)
+ neighbor = '192.0.2.1'
+ peer_group = 'bar'
+ interface = 'eth0'
+
+ self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn])
+ self.cli_set(base_path + ['neighbor', neighbor, 'peer-group', peer_group])
+ self.cli_set(base_path + ['peer-group', peer_group, 'remote-as', remote_asn])
+
+ # Peer-group member cannot override remote-as of peer-group
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['neighbor', neighbor, 'remote-as'])
+
+ # re-test with interface based peer-group
+ self.cli_set(base_path + ['neighbor', interface, 'interface', 'peer-group', peer_group])
+ self.cli_set(base_path + ['neighbor', interface, 'interface', 'remote-as', 'external'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['neighbor', interface, 'interface', 'remote-as'])
+
+ # re-test with interface based v6only peer-group
+ self.cli_set(base_path + ['neighbor', interface, 'interface', 'v6only', 'peer-group', peer_group])
+ self.cli_set(base_path + ['neighbor', interface, 'interface', 'v6only', 'remote-as', 'external'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['neighbor', interface, 'interface', 'v6only', 'remote-as'])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {neighbor} peer-group {peer_group}', frrconfig)
+ self.assertIn(f' neighbor {peer_group} peer-group', frrconfig)
+ self.assertIn(f' neighbor {peer_group} remote-as {remote_asn}', frrconfig)
+
+ def test_bgp_15_local_as_ebgp(self):
+ # https://vyos.dev/T4560
+ # local-as allowed only for ebgp peers
+
+ neighbor = '192.0.2.99'
+ remote_asn = '500'
+ local_asn = '400'
+
+ self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', ASN])
+ self.cli_set(base_path + ['neighbor', neighbor, 'local-as', local_asn])
+
+ # check validate() - local-as allowed only for ebgp peers
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig)
+ self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig)
+
+ def test_bgp_16_import_rd_rt_compatibility(self):
+ # Verify if import vrf and rd vpn export
+ # exist in the same address family
+ self.create_bgp_instances_for_import_test()
+ self.cli_set(
+ base_path + ['address-family', import_afi, 'import', 'vrf',
+ import_vrf])
+ self.cli_set(
+ base_path + ['address-family', import_afi, 'rd', 'vpn', 'export',
+ import_rd])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_bgp_17_import_rd_rt_compatibility(self):
+ # Verify if vrf that is in import vrf list contains rd vpn export
+ self.create_bgp_instances_for_import_test()
+ self.cli_set(
+ base_path + ['address-family', import_afi, 'import', 'vrf',
+ import_vrf])
+ self.cli_commit()
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f'address-family ipv4 unicast', frrconfig)
+ self.assertIn(f' import vrf {import_vrf}', frrconfig)
+ self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf)
+
+ self.cli_set(
+ import_vrf_base + [import_vrf] + base_path + ['address-family',
+ import_afi, 'rd',
+ 'vpn', 'export',
+ import_rd])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_bgp_18_deleting_import_vrf(self):
+ # Verify deleting vrf that is in import vrf list
+ self.create_bgp_instances_for_import_test()
+ self.cli_set(
+ base_path + ['address-family', import_afi, 'import', 'vrf',
+ import_vrf])
+ self.cli_commit()
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f'address-family ipv4 unicast', frrconfig)
+ self.assertIn(f' import vrf {import_vrf}', frrconfig)
+ self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf)
+ self.cli_delete(import_vrf_base + [import_vrf])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_bgp_19_deleting_default_vrf(self):
+ # Verify deleting existent vrf default if other vrfs were created
+ self.create_bgp_instances_for_import_test()
+ self.cli_commit()
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf)
+ self.cli_delete(base_path)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_bgp_20_import_rd_rt_compatibility(self):
+ # Verify if vrf that has rd vpn export is in import vrf of other vrfs
+ self.create_bgp_instances_for_import_test()
+ self.cli_set(
+ import_vrf_base + [import_vrf] + base_path + ['address-family',
+ import_afi, 'rd',
+ 'vpn', 'export',
+ import_rd])
+ self.cli_commit()
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf)
+ self.assertIn(f'address-family ipv4 unicast', frrconfig_vrf)
+ self.assertIn(f' rd vpn export {import_rd}', frrconfig_vrf)
+
+ self.cli_set(
+ base_path + ['address-family', import_afi, 'import', 'vrf',
+ import_vrf])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_bgp_21_import_unspecified_vrf(self):
+ # Verify if vrf that is in import is unspecified
+ self.create_bgp_instances_for_import_test()
+ self.cli_set(
+ base_path + ['address-family', import_afi, 'import', 'vrf',
+ 'test'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_bgp_22_interface_mpls_forwarding(self):
+ interfaces = Section.interfaces('ethernet', vlan=False)
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mpls', 'forwarding'])
+
+ self.cli_commit()
+
+ for interface in interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}')
+ self.assertIn(f'interface {interface}', frrconfig)
+ self.assertIn(f' mpls bgp forwarding', frrconfig)
+
+ def test_bgp_23_vrf_interface_mpls_forwarding(self):
+ self.create_bgp_instances_for_import_test()
+ interfaces = Section.interfaces('ethernet', vlan=False)
+ for interface in interfaces:
+ self.cli_set(['interfaces', 'ethernet', interface, 'vrf', import_vrf])
+ self.cli_set(import_vrf_base + [import_vrf] + base_path + ['interface', interface, 'mpls', 'forwarding'])
+
+ self.cli_commit()
+
+ for interface in interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}')
+ self.assertIn(f'interface {interface}', frrconfig)
+ self.assertIn(f' mpls bgp forwarding', frrconfig)
+ self.cli_delete(['interfaces', 'ethernet', interface, 'vrf'])
+
+ def test_bgp_24_srv6_sid(self):
+ locator_name = 'VyOS_foo'
+ sid = 'auto'
+ nexthop_ipv4 = '192.0.0.1'
+ nexthop_ipv6 = '2001:db8:100:200::2'
+
+ self.cli_set(base_path + ['srv6', 'locator', locator_name])
+ self.cli_set(base_path + ['sid', 'vpn', 'per-vrf', 'export', sid])
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'sid', 'vpn', 'export', sid])
+ # verify() - SID per VRF and SID per address-family are mutually exclusive!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['address-family', 'ipv4-unicast', 'sid'])
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' segment-routing srv6', frrconfig)
+ self.assertIn(f' locator {locator_name}', frrconfig)
+ self.assertIn(f' sid vpn per-vrf export {sid}', frrconfig)
+
+ # Now test AFI SID
+ self.cli_delete(base_path + ['sid'])
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'sid', 'vpn', 'export', sid])
+ self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'nexthop', 'vpn', 'export', nexthop_ipv4])
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'sid', 'vpn', 'export', sid])
+ self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'nexthop', 'vpn', 'export', nexthop_ipv6])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' segment-routing srv6', frrconfig)
+ self.assertIn(f' locator {locator_name}', frrconfig)
+
+ afiv4_config = self.getFRRconfig(' address-family ipv4 unicast')
+ self.assertIn(f' sid vpn export {sid}', afiv4_config)
+ self.assertIn(f' nexthop vpn export {nexthop_ipv4}', afiv4_config)
+ afiv6_config = self.getFRRconfig(' address-family ipv6 unicast')
+ self.assertIn(f' sid vpn export {sid}', afiv6_config)
+ self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv4_config)
+
+ def test_bgp_25_ipv4_labeled_unicast_peer_group(self):
+ pg_ipv4 = 'foo4'
+ ipv4_max_prefix = '20'
+ ipv4_prefix = '192.0.2.0/24'
+
+ self.cli_set(base_path + ['listen', 'range', ipv4_prefix, 'peer-group', pg_ipv4])
+ self.cli_set(base_path + ['parameters', 'labeled-unicast', 'ipv4-explicit-null'])
+ self.cli_set(base_path + ['peer-group', pg_ipv4, 'address-family', 'ipv4-labeled-unicast', 'maximum-prefix', ipv4_max_prefix])
+ self.cli_set(base_path + ['peer-group', pg_ipv4, 'remote-as', 'external'])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {pg_ipv4} peer-group', frrconfig)
+ self.assertIn(f' neighbor {pg_ipv4} remote-as external', frrconfig)
+ self.assertIn(f' bgp listen range {ipv4_prefix} peer-group {pg_ipv4}', frrconfig)
+ self.assertIn(f' bgp labeled-unicast ipv4-explicit-null', frrconfig)
+
+ afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast')
+ self.assertIn(f' neighbor {pg_ipv4} activate', afiv4_config)
+ self.assertIn(f' neighbor {pg_ipv4} maximum-prefix {ipv4_max_prefix}', afiv4_config)
+
+ def test_bgp_26_ipv6_labeled_unicast_peer_group(self):
+ pg_ipv6 = 'foo6'
+ ipv6_max_prefix = '200'
+ ipv6_prefix = '2001:db8:1000::/64'
+
+ self.cli_set(base_path + ['listen', 'range', ipv6_prefix, 'peer-group', pg_ipv6])
+ self.cli_set(base_path + ['parameters', 'labeled-unicast', 'ipv6-explicit-null'])
+
+ self.cli_set(base_path + ['peer-group', pg_ipv6, 'address-family', 'ipv6-labeled-unicast', 'maximum-prefix', ipv6_max_prefix])
+ self.cli_set(base_path + ['peer-group', pg_ipv6, 'remote-as', 'external'])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig)
+ self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig)
+ self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig)
+ self.assertIn(f' bgp labeled-unicast ipv6-explicit-null', frrconfig)
+
+ afiv6_config = self.getFRRconfig(' address-family ipv6 labeled-unicast')
+ self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config)
+ self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config)
+
+ def test_bgp_27_route_reflector_client(self):
+ self.cli_set(base_path + ['peer-group', 'peer1', 'address-family', 'l2vpn-evpn', 'route-reflector-client'])
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+
+ self.cli_set(base_path + ['peer-group', 'peer1', 'remote-as', 'internal'])
+ self.cli_commit()
+
+ conf = self.getFRRconfig(' address-family l2vpn evpn')
+
+ self.assertIn('neighbor peer1 route-reflector-client', conf)
+
+ def test_bgp_28_peer_group_member_all_internal_or_external(self):
+ def _common_config_check(conf, include_ras=True):
+ if include_ras:
+ self.assertIn(f'neighbor {int_neighbors[0]} remote-as {ASN}', conf)
+ self.assertIn(f'neighbor {int_neighbors[1]} remote-as {ASN}', conf)
+ self.assertIn(f'neighbor {ext_neighbors[0]} remote-as {int(ASN) + 1}',conf)
+
+ self.assertIn(f'neighbor {int_neighbors[0]} peer-group {int_pg_name}', conf)
+ self.assertIn(f'neighbor {int_neighbors[1]} peer-group {int_pg_name}', conf)
+ self.assertIn(f'neighbor {ext_neighbors[0]} peer-group {ext_pg_name}', conf)
+
+ int_neighbors = ['192.0.2.2', '192.0.2.3']
+ ext_neighbors = ['192.122.2.2', '192.122.2.3']
+ int_pg_name, ext_pg_name = 'SMOKETESTINT', 'SMOKETESTEXT'
+
+ self.cli_set(base_path + ['neighbor', int_neighbors[0], 'peer-group', int_pg_name])
+ self.cli_set(base_path + ['neighbor', int_neighbors[0], 'remote-as', ASN])
+ self.cli_set(base_path + ['peer-group', int_pg_name, 'address-family', 'ipv4-unicast'])
+ self.cli_set(base_path + ['neighbor', ext_neighbors[0], 'peer-group', ext_pg_name])
+ self.cli_set(base_path + ['neighbor', ext_neighbors[0], 'remote-as', f'{int(ASN) + 1}'])
+ self.cli_set(base_path + ['peer-group', ext_pg_name, 'address-family', 'ipv4-unicast'])
+ self.cli_commit()
+
+ # test add external remote-as to internal group
+ self.cli_set(base_path + ['neighbor', int_neighbors[1], 'peer-group', int_pg_name])
+ self.cli_set(base_path + ['neighbor', int_neighbors[1], 'remote-as', f'{int(ASN) + 1}'])
+
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+ # self.assertIn('\nPeer-group members must be all internal or all external\n', str(e.exception))
+
+ # test add internal remote-as to internal group
+ self.cli_set(base_path + ['neighbor', int_neighbors[1], 'remote-as', ASN])
+ self.cli_commit()
+
+ conf = self.getFRRconfig(f'router bgp {ASN}')
+ _common_config_check(conf)
+
+ # test add internal remote-as to external group
+ self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'peer-group', ext_pg_name])
+ self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', ASN])
+
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+ # self.assertIn('\nPeer-group members must be all internal or all external\n', str(e.exception))
+
+ # test add external remote-as to external group
+ self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', f'{int(ASN) + 2}'])
+ self.cli_commit()
+
+ conf = self.getFRRconfig(f'router bgp {ASN}')
+ _common_config_check(conf)
+ self.assertIn(f'neighbor {ext_neighbors[1]} remote-as {int(ASN) + 2}', conf)
+ self.assertIn(f'neighbor {ext_neighbors[1]} peer-group {ext_pg_name}', conf)
+
+ # test named remote-as
+ self.cli_set(base_path + ['neighbor', int_neighbors[0], 'remote-as', 'internal'])
+ self.cli_set(base_path + ['neighbor', int_neighbors[1], 'remote-as', 'internal'])
+ self.cli_set(base_path + ['neighbor', ext_neighbors[0], 'remote-as', 'external'])
+ self.cli_set(base_path + ['neighbor', ext_neighbors[1], 'remote-as', 'external'])
+ self.cli_commit()
+
+ conf = self.getFRRconfig(f'router bgp {ASN}')
+ _common_config_check(conf, include_ras=False)
+
+ self.assertIn(f'neighbor {int_neighbors[0]} remote-as internal', conf)
+ self.assertIn(f'neighbor {int_neighbors[1]} remote-as internal', conf)
+ self.assertIn(f'neighbor {ext_neighbors[0]} remote-as external', conf)
+ self.assertIn(f'neighbor {ext_neighbors[1]} remote-as external', conf)
+ self.assertIn(f'neighbor {ext_neighbors[1]} peer-group {ext_pg_name}', conf)
+
+ def test_bgp_29_peer_group_remote_as_equal_local_as(self):
+ self.cli_set(base_path + ['system-as', ASN])
+ self.cli_set(base_path + ['peer-group', 'OVERLAY', 'local-as', f'{int(ASN) + 1}'])
+ self.cli_set(base_path + ['peer-group', 'OVERLAY', 'remote-as', f'{int(ASN) + 1}'])
+ self.cli_set(base_path + ['peer-group', 'OVERLAY', 'address-family', 'l2vpn-evpn'])
+
+ self.cli_set(base_path + ['peer-group', 'UNDERLAY', 'address-family', 'ipv4-unicast'])
+
+ self.cli_set(base_path + ['neighbor', '10.177.70.62', 'peer-group', 'UNDERLAY'])
+ self.cli_set(base_path + ['neighbor', '10.177.70.62', 'remote-as', 'external'])
+
+ self.cli_set(base_path + ['neighbor', '10.177.75.1', 'peer-group', 'OVERLAY'])
+ self.cli_set(base_path + ['neighbor', '10.177.75.2', 'peer-group', 'OVERLAY'])
+
+ self.cli_commit()
+
+ conf = self.getFRRconfig(f'router bgp {ASN}')
+
+ self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf)
+ self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf)
+
+ def test_bgp_99_bmp(self):
+ target_name = 'instance-bmp'
+ target_address = '127.0.0.1'
+ target_port = '5000'
+ min_retry = '1024'
+ max_retry = '2048'
+ monitor_ipv4 = 'pre-policy'
+ monitor_ipv6 = 'pre-policy'
+ mirror_buffer = '32000000'
+ bmp_path = base_path + ['bmp']
+ target_path = bmp_path + ['target', target_name]
+
+ # by default the 'bmp' module not loaded for the bgpd expect Error
+ self.cli_set(bmp_path)
+ if not process_named_running('bgpd', 'bmp'):
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # add required 'bmp' module to bgpd and restart bgpd
+ self.cli_delete(bmp_path)
+ self.cli_set(['system', 'frr', 'bmp'])
+ self.cli_commit()
+
+ # restart bgpd to apply "-M bmp" and update PID
+ cmd(f'sudo kill -9 {self.daemon_pid}')
+ # let the bgpd process recover
+ sleep(10)
+ # update daemon PID - this was a planned daemon restart
+ self.daemon_pid = process_named_running(PROCESS_NAME)
+
+ # set bmp config but not set address
+ self.cli_set(target_path + ['port', target_port])
+ # address is not set, expect Error
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # config other bmp options
+ self.cli_set(target_path + ['address', target_address])
+ self.cli_set(bmp_path + ['mirror-buffer-limit', mirror_buffer])
+ self.cli_set(target_path + ['port', target_port])
+ self.cli_set(target_path + ['min-retry', min_retry])
+ self.cli_set(target_path + ['max-retry', max_retry])
+ self.cli_set(target_path + ['mirror'])
+ self.cli_set(target_path + ['monitor', 'ipv4-unicast', monitor_ipv4])
+ self.cli_set(target_path + ['monitor', 'ipv6-unicast', monitor_ipv6])
+ self.cli_commit()
+
+ # Verify bgpd bmp configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'bmp mirror buffer-limit {mirror_buffer}', frrconfig)
+ self.assertIn(f'bmp targets {target_name}', frrconfig)
+ self.assertIn(f'bmp mirror', frrconfig)
+ self.assertIn(f'bmp monitor ipv4 unicast {monitor_ipv4}', frrconfig)
+ self.assertIn(f'bmp monitor ipv6 unicast {monitor_ipv6}', frrconfig)
+ self.assertIn(f'bmp connect {target_address} port {target_port} min-retry {min_retry} max-retry {max_retry}', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_igmp-proxy.py b/smoketest/scripts/cli/test_protocols_igmp-proxy.py
new file mode 100644
index 0000000..df10442
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_igmp-proxy.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'igmpproxy'
+IGMP_PROXY_CONF = '/etc/igmpproxy.conf'
+base_path = ['protocols', 'igmp-proxy']
+upstream_if = 'eth1'
+downstream_if = 'eth2'
+
+class TestProtocolsIGMPProxy(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsIGMPProxy, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_set(cls, ['interfaces', 'ethernet', upstream_if, 'address', '172.16.1.1/24'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'ethernet', upstream_if, 'address'])
+
+ # call base-classes classmethod
+ super(TestProtocolsIGMPProxy, cls).tearDownClass()
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for no longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_igmpproxy(self):
+ threshold = '20'
+ altnet = '192.0.2.0/24'
+ whitelist = '10.0.0.0/8'
+
+ self.cli_set(base_path + ['disable-quickleave'])
+ self.cli_set(base_path + ['interface', upstream_if, 'threshold', threshold])
+ self.cli_set(base_path + ['interface', upstream_if, 'alt-subnet', altnet])
+ self.cli_set(base_path + ['interface', upstream_if, 'whitelist', whitelist])
+
+ # Must define an upstream and at least 1 downstream interface!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['interface', upstream_if, 'role', 'upstream'])
+
+ # Interface does not exist
+ self.cli_set(base_path + ['interface', 'eth20', 'role', 'upstream'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', 'eth20'])
+
+ # Only 1 upstream interface allowed
+ self.cli_set(base_path + ['interface', downstream_if, 'role', 'upstream'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['interface', downstream_if, 'role', 'downstream'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check generated configuration
+ config = read_file(IGMP_PROXY_CONF)
+ self.assertIn(f'phyint {upstream_if} upstream ratelimit 0 threshold {threshold}', config)
+ self.assertIn(f'altnet {altnet}', config)
+ self.assertIn(f'whitelist {whitelist}', config)
+ self.assertIn(f'phyint {downstream_if} downstream ratelimit 0 threshold 1', config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py
new file mode 100644
index 0000000..769f3dd
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_isis.py
@@ -0,0 +1,416 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'isisd'
+base_path = ['protocols', 'isis']
+
+domain = 'VyOS'
+net = '49.0001.1921.6800.1002.00'
+
+class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interfaces = Section.interfaces('ethernet')
+ # call base-classes classmethod
+ super(TestProtocolsISIS, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['vrf'])
+
+ def tearDown(self):
+ # cleanup any possible VRF mess
+ self.cli_delete(['vrf'])
+ # always destrox the entire isisd configuration to make the processes
+ # life as hard as possible
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def isis_base_config(self):
+ self.cli_set(base_path + ['net', net])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ def test_isis_01_redistribute(self):
+ prefix_list = 'EXPORT-ISIS'
+ route_map = 'EXPORT-ISIS'
+ rule = '10'
+ metric_style = 'transition'
+
+ self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit'])
+ self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24'])
+ self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit'])
+ self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'match', 'ip', 'address', 'prefix-list', prefix_list])
+
+ self.cli_set(base_path)
+
+ # verify() - net id and interface are mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.isis_base_config()
+
+ self.cli_set(base_path + ['redistribute', 'ipv4', 'connected'])
+ # verify() - Redistribute level-1 or level-2 should be specified
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map])
+ self.cli_set(base_path + ['metric-style', metric_style])
+ self.cli_set(base_path + ['log-adjacency-changes'])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' metric-style {metric_style}', tmp)
+ self.assertIn(f' log-adjacency-changes', tmp)
+ self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp)
+
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' ip router isis {domain}', tmp)
+ self.assertIn(f' ipv6 router isis {domain}', tmp)
+
+ self.cli_delete(['policy', 'route-map', route_map])
+ self.cli_delete(['policy', 'prefix-list', prefix_list])
+
+ def test_isis_02_vrfs(self):
+ vrfs = ['red', 'green', 'blue']
+ # It is safe to assume that when the basic VRF test works, all other
+ # IS-IS related features work, as we entirely inherit the CLI templates
+ # and Jinja2 FRR template.
+ table = '1000'
+ vrf = 'red'
+ vrf_base = ['vrf', 'name', vrf]
+ vrf_iface = 'eth1'
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'isis', 'net', net])
+ self.cli_set(vrf_base + ['protocols', 'isis', 'interface', vrf_iface])
+ self.cli_set(vrf_base + ['protocols', 'isis', 'advertise-high-metrics'])
+ self.cli_set(vrf_base + ['protocols', 'isis', 'advertise-passive-only'])
+ self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf])
+
+ # Also set a default VRF IS-IS config
+ self.cli_set(base_path + ['net', net])
+ self.cli_set(base_path + ['interface', 'eth0'])
+ self.cli_commit()
+
+ # Verify FRR isisd configuration
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f'router isis {domain}', tmp)
+ self.assertIn(f' net {net}', tmp)
+
+ tmp = self.getFRRconfig(f'router isis {domain} vrf {vrf}', daemon='isisd')
+ self.assertIn(f'router isis {domain} vrf {vrf}', tmp)
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' advertise-high-metrics', tmp)
+ self.assertIn(f' advertise-passive-only', tmp)
+
+ self.cli_delete(['vrf', 'name', vrf])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+
+ def test_isis_04_default_information(self):
+ metric = '50'
+ route_map = 'default-foo-'
+
+ self.isis_base_config()
+ for afi in ['ipv4', 'ipv6']:
+ for level in ['level-1', 'level-2']:
+ self.cli_set(base_path + ['default-information', 'originate', afi, level, 'always'])
+ self.cli_set(base_path + ['default-information', 'originate', afi, level, 'metric', metric])
+ self.cli_set(base_path + ['default-information', 'originate', afi, level, 'route-map', route_map + level + afi])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+
+ for afi in ['ipv4', 'ipv6']:
+ for level in ['level-1', 'level-2']:
+ route_map_name = route_map + level + afi
+ self.assertIn(f' default-information originate {afi} {level} always route-map {route_map_name} metric {metric}', tmp)
+
+
+ def test_isis_05_password(self):
+ password = 'foo'
+
+ self.isis_base_config()
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'password', 'plaintext-password', f'{password}-{interface}'])
+
+ self.cli_set(base_path + ['area-password', 'plaintext-password', password])
+ self.cli_set(base_path + ['area-password', 'md5', password])
+ self.cli_set(base_path + ['domain-password', 'plaintext-password', password])
+ self.cli_set(base_path + ['domain-password', 'md5', password])
+
+ # verify() - can not use both md5 and plaintext-password for area-password
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['area-password', 'md5', password])
+
+ # verify() - can not use both md5 and plaintext-password for domain-password
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['domain-password', 'md5', password])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' domain-password clear {password}', tmp)
+ self.assertIn(f' area-password clear {password}', tmp)
+
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' isis password clear {password}-{interface}', tmp)
+
+ def test_isis_06_spf_delay_bfd(self):
+ network = 'point-to-point'
+ holddown = '10'
+ init_delay = '50'
+ long_delay = '200'
+ short_delay = '100'
+ time_to_learn = '75'
+ bfd_profile = 'isis-bfd'
+
+ self.cli_set(base_path + ['net', net])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'network', network])
+ self.cli_set(base_path + ['interface', interface, 'bfd', 'profile', bfd_profile])
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'holddown', holddown])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'init-delay', init_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'long-delay', long_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'short-delay', short_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['spf-delay-ietf', 'time-to-learn', time_to_learn])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' spf-delay-ietf init-delay {init_delay} short-delay {short_delay} long-delay {long_delay} holddown {holddown} time-to-learn {time_to_learn}', tmp)
+
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' ip router isis {domain}', tmp)
+ self.assertIn(f' ipv6 router isis {domain}', tmp)
+ self.assertIn(f' isis network {network}', tmp)
+ self.assertIn(f' isis bfd', tmp)
+ self.assertIn(f' isis bfd profile {bfd_profile}', tmp)
+
+ def test_isis_07_segment_routing_configuration(self):
+ global_block_low = "300"
+ global_block_high = "399"
+ local_block_low = "400"
+ local_block_high = "499"
+ interface = 'lo'
+ maximum_stack_size = '5'
+ prefix_one = '192.168.0.1/32'
+ prefix_two = '192.168.0.2/32'
+ prefix_three = '192.168.0.3/32'
+ prefix_four = '192.168.0.4/32'
+ prefix_one_value = '1'
+ prefix_two_value = '2'
+ prefix_three_value = '60000'
+ prefix_four_value = '65000'
+
+ self.cli_set(base_path + ['net', net])
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size])
+ self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low])
+ self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high])
+ self.cli_set(base_path + ['segment-routing', 'local-block', 'low-label-value', local_block_low])
+ self.cli_set(base_path + ['segment-routing', 'local-block', 'high-label-value', local_block_high])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_one, 'index', 'value', prefix_one_value])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_one, 'index', 'explicit-null'])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_two, 'index', 'value', prefix_two_value])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_two, 'index', 'no-php-flag'])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_three, 'absolute', 'value', prefix_three_value])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_three, 'absolute', 'explicit-null'])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_four, 'absolute', 'value', prefix_four_value])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_four, 'absolute', 'no-php-flag'])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' segment-routing on', tmp)
+ self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', tmp)
+ self.assertIn(f' segment-routing node-msd {maximum_stack_size}', tmp)
+ self.assertIn(f' segment-routing prefix {prefix_one} index {prefix_one_value} explicit-null', tmp)
+ self.assertIn(f' segment-routing prefix {prefix_two} index {prefix_two_value} no-php-flag', tmp)
+ self.assertIn(f' segment-routing prefix {prefix_three} absolute {prefix_three_value} explicit-null', tmp)
+ self.assertIn(f' segment-routing prefix {prefix_four} absolute {prefix_four_value} no-php-flag', tmp)
+
+ def test_isis_08_ldp_sync(self):
+ holddown = "500"
+ interface = 'lo'
+
+ self.cli_set(base_path + ['net', net])
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['ldp-sync', 'holddown', holddown])
+
+ # Commit main ISIS changes
+ self.cli_commit()
+
+ # Verify main ISIS changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' mpls ldp-sync', tmp)
+ self.assertIn(f' mpls ldp-sync holddown {holddown}', tmp)
+
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'ldp-sync', 'holddown', holddown])
+
+ # Commit interface changes for holddown
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Verify interface changes for holddown
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f'interface {interface}', tmp)
+ self.assertIn(f' ip router isis {domain}', tmp)
+ self.assertIn(f' ipv6 router isis {domain}', tmp)
+ self.assertIn(f' isis mpls ldp-sync holddown {holddown}', tmp)
+
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'ldp-sync', 'disable'])
+
+ # Commit interface changes for disable
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ # Verify interface changes for disable
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f'interface {interface}', tmp)
+ self.assertIn(f' ip router isis {domain}', tmp)
+ self.assertIn(f' ipv6 router isis {domain}', tmp)
+ self.assertIn(f' no isis mpls ldp-sync', tmp)
+
+ def test_isis_09_lfa(self):
+ prefix_list = 'lfa-prefix-list-test-1'
+ prefix_list_address = '192.168.255.255/32'
+ interface = 'lo'
+
+ self.cli_set(base_path + ['net', net])
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '1', 'action', 'permit'])
+ self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '1', 'prefix', prefix_list_address])
+
+ # Commit main ISIS changes
+ self.cli_commit()
+
+ # Add remote portion of LFA with prefix list with validation
+ for level in ['level-1', 'level-2']:
+ self.cli_set(base_path + ['fast-reroute', 'lfa', 'remote', 'prefix-list', prefix_list, level])
+ self.cli_commit()
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' fast-reroute remote-lfa prefix-list {prefix_list} {level}', tmp)
+ self.cli_delete(base_path + ['fast-reroute'])
+ self.cli_commit()
+
+ # Add local portion of LFA load-sharing portion with validation
+ for level in ['level-1', 'level-2']:
+ self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'load-sharing', 'disable', level])
+ self.cli_commit()
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' fast-reroute load-sharing disable {level}', tmp)
+ self.cli_delete(base_path + ['fast-reroute'])
+ self.cli_commit()
+
+ # Add local portion of LFA priority-limit portion with validation
+ for priority in ['critical', 'high', 'medium']:
+ for level in ['level-1', 'level-2']:
+ self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'priority-limit', priority, level])
+ self.cli_commit()
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' fast-reroute priority-limit {priority} {level}', tmp)
+ self.cli_delete(base_path + ['fast-reroute'])
+ self.cli_commit()
+
+ # Add local portion of LFA tiebreaker portion with validation
+ index = '100'
+ for tiebreaker in ['downstream','lowest-backup-metric','node-protecting']:
+ for level in ['level-1', 'level-2']:
+ self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'tiebreaker', tiebreaker, 'index', index, level])
+ self.cli_commit()
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' fast-reroute lfa tiebreaker {tiebreaker} index {index} {level}', tmp)
+ self.cli_delete(base_path + ['fast-reroute'])
+ self.cli_commit()
+
+ # Clean up and remove prefix list
+ self.cli_delete(['policy', 'prefix-list', prefix_list])
+ self.cli_commit()
+
+ def test_isis_10_topology(self):
+ topologies = ['ipv4-multicast', 'ipv4-mgmt', 'ipv6-unicast', 'ipv6-multicast', 'ipv6-mgmt']
+ interface = 'lo'
+
+ # Set a basic IS-IS config
+ self.cli_set(base_path + ['net', net])
+ self.cli_set(base_path + ['interface', interface])
+ for topology in topologies:
+ self.cli_set(base_path + ['topology', topology])
+ self.cli_commit()
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' topology {topology}', tmp)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py
new file mode 100644
index 0000000..0c1599f
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_mpls.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ldpd'
+base_path = ['protocols', 'mpls', 'ldp']
+
+peers = {
+ '192.0.2.10' : {
+ 'intv_rx' : '500',
+ 'intv_tx' : '600',
+ 'multihop' : '',
+ 'source_addr': '192.0.2.254',
+ },
+ '192.0.2.20' : {
+ 'echo_mode' : '',
+ 'intv_echo' : '100',
+ 'intv_mult' : '100',
+ 'intv_rx' : '222',
+ 'intv_tx' : '333',
+ 'passive' : '',
+ 'shutdown' : '',
+ },
+ '2001:db8::a' : {
+ 'source_addr': '2001:db8::1',
+ },
+ '2001:db8::b' : {
+ 'source_addr': '2001:db8::1',
+ 'multihop' : '',
+ },
+}
+
+profiles = {
+ 'foo' : {
+ 'echo_mode' : '',
+ 'intv_echo' : '100',
+ 'intv_mult' : '101',
+ 'intv_rx' : '222',
+ 'intv_tx' : '333',
+ 'shutdown' : '',
+ },
+ 'bar' : {
+ 'intv_mult' : '102',
+ 'intv_rx' : '444',
+ 'passive' : '',
+ },
+}
+
+class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsMPLS, cls).setUpClass()
+
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_mpls_basic(self):
+ router_id = '1.2.3.4'
+ transport_ipv4_addr = '5.6.7.8'
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['router-id', router_id])
+
+ # At least one LDP interface must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ # LDP transport address missing
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate configuration
+ frrconfig = self.getFRRconfig('mpls ldp', daemon=PROCESS_NAME)
+ self.assertIn(f'mpls ldp', frrconfig)
+ self.assertIn(f' router-id {router_id}', frrconfig)
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig(' address-family ipv4', daemon=PROCESS_NAME)
+ self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config)
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv4_config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py
new file mode 100644
index 0000000..43ae4ab
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_nhrp.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.firewall import find_nftables_rule
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+tunnel_path = ['interfaces', 'tunnel']
+nhrp_path = ['protocols', 'nhrp']
+vpn_path = ['vpn', 'ipsec']
+
+class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsNHRP, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, nhrp_path)
+ cls.cli_delete(cls, tunnel_path)
+
+ def tearDown(self):
+ self.cli_delete(nhrp_path)
+ self.cli_delete(tunnel_path)
+ self.cli_commit()
+
+ def test_config(self):
+ tunnel_if = "tun100"
+ tunnel_source = "192.0.2.1"
+ tunnel_encapsulation = "gre"
+ esp_group = "ESP-HUB"
+ ike_group = "IKE-HUB"
+ nhrp_secret = "vyos123"
+ nhrp_profile = "NHRPVPN"
+ ipsec_secret = "secret"
+
+ # Tunnel
+ self.cli_set(tunnel_path + [tunnel_if, "address", "172.16.253.134/29"])
+ self.cli_set(tunnel_path + [tunnel_if, "encapsulation", tunnel_encapsulation])
+ self.cli_set(tunnel_path + [tunnel_if, "source-address", tunnel_source])
+ self.cli_set(tunnel_path + [tunnel_if, "enable-multicast"])
+ self.cli_set(tunnel_path + [tunnel_if, "parameters", "ip", "key", "1"])
+
+ # NHRP
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "cisco-authentication", nhrp_secret])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "holding-time", "300"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "multicast", "dynamic"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "redirect"])
+ self.cli_set(nhrp_path + ["tunnel", tunnel_if, "shortcut"])
+
+ # IKE/ESP Groups
+ self.cli_set(vpn_path + ["esp-group", esp_group, "lifetime", "1800"])
+ self.cli_set(vpn_path + ["esp-group", esp_group, "mode", "transport"])
+ self.cli_set(vpn_path + ["esp-group", esp_group, "pfs", "dh-group2"])
+ self.cli_set(vpn_path + ["esp-group", esp_group, "proposal", "1", "encryption", "aes256"])
+ self.cli_set(vpn_path + ["esp-group", esp_group, "proposal", "1", "hash", "sha1"])
+ self.cli_set(vpn_path + ["esp-group", esp_group, "proposal", "2", "encryption", "3des"])
+ self.cli_set(vpn_path + ["esp-group", esp_group, "proposal", "2", "hash", "md5"])
+
+ self.cli_set(vpn_path + ["ike-group", ike_group, "key-exchange", "ikev1"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "lifetime", "3600"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "proposal", "1", "dh-group", "2"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "proposal", "1", "encryption", "aes256"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "proposal", "1", "hash", "sha1"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "proposal", "2", "dh-group", "2"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "proposal", "2", "encryption", "aes128"])
+ self.cli_set(vpn_path + ["ike-group", ike_group, "proposal", "2", "hash", "sha1"])
+
+ # Profile - Not doing full DMVPN checks here, just want to verify the profile name in the output
+ self.cli_set(vpn_path + ["interface", "eth0"])
+ self.cli_set(vpn_path + ["profile", nhrp_profile, "authentication", "mode", "pre-shared-secret"])
+ self.cli_set(vpn_path + ["profile", nhrp_profile, "authentication", "pre-shared-secret", ipsec_secret])
+ self.cli_set(vpn_path + ["profile", nhrp_profile, "bind", "tunnel", tunnel_if])
+ self.cli_set(vpn_path + ["profile", nhrp_profile, "esp-group", esp_group])
+ self.cli_set(vpn_path + ["profile", nhrp_profile, "ike-group", ike_group])
+
+ self.cli_commit()
+
+ opennhrp_lines = [
+ f'interface {tunnel_if} #hub {nhrp_profile}',
+ f'cisco-authentication {nhrp_secret}',
+ f'holding-time 300',
+ f'shortcut',
+ f'multicast dynamic',
+ f'redirect'
+ ]
+
+ tmp_opennhrp_conf = read_file('/run/opennhrp/opennhrp.conf')
+
+ for line in opennhrp_lines:
+ self.assertIn(line, tmp_opennhrp_conf)
+
+ firewall_matches = [
+ f'ip protocol {tunnel_encapsulation}',
+ f'ip saddr {tunnel_source}',
+ f'ip daddr 224.0.0.0/4',
+ f'comment "VYOS_NHRP_{tunnel_if}"'
+ ]
+
+ self.assertTrue(find_nftables_rule('ip vyos_nhrp_filter', 'VYOS_NHRP_OUTPUT', firewall_matches) is not None)
+ self.assertTrue(process_named_running('opennhrp'))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_openfabric.py b/smoketest/scripts/cli/test_protocols_openfabric.py
new file mode 100644
index 0000000..e37aed4
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_openfabric.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'fabricd'
+base_path = ['protocols', 'openfabric']
+
+domain = 'VyOS'
+net = '49.0001.1111.1111.1111.00'
+dummy_if = 'dum1234'
+address_families = ['ipv4', 'ipv6']
+
+path = base_path + ['domain', domain]
+
+class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsOpenFabric, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def openfabric_base_config(self):
+ self.cli_set(['interfaces', 'dummy', dummy_if])
+ self.cli_set(base_path + ['net', net])
+ for family in address_families:
+ self.cli_set(path + ['interface', dummy_if, 'address-family', family])
+
+ def test_openfabric_01_router_params(self):
+ fabric_tier = '5'
+ lsp_gen_interval = '20'
+
+ self.cli_set(base_path)
+
+ # verify() - net id and domain name are mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.openfabric_base_config()
+
+ self.cli_set(path + ['log-adjacency-changes'])
+ self.cli_set(path + ['set-overload-bit'])
+ self.cli_set(path + ['fabric-tier', fabric_tier])
+ self.cli_set(path + ['lsp-gen-interval', lsp_gen_interval])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' log-adjacency-changes', tmp)
+ self.assertIn(f' set-overload-bit', tmp)
+ self.assertIn(f' fabric-tier {fabric_tier}', tmp)
+ self.assertIn(f' lsp-gen-interval {lsp_gen_interval}', tmp)
+
+ tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd')
+ self.assertIn(f' ip router openfabric {domain}', tmp)
+ self.assertIn(f' ipv6 router openfabric {domain}', tmp)
+
+ def test_openfabric_02_loopback_interface(self):
+ interface = 'lo'
+ hello_interval = '100'
+ metric = '24478'
+
+ self.openfabric_base_config()
+ self.cli_set(path + ['interface', interface, 'address-family', 'ipv4'])
+
+ self.cli_set(path + ['interface', interface, 'hello-interval', hello_interval])
+ self.cli_set(path + ['interface', interface, 'metric', metric])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify FRR openfabric configuration
+ tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ self.assertIn(f'router openfabric {domain}', tmp)
+ self.assertIn(f' net {net}', tmp)
+
+ # Verify interface configuration
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='fabricd')
+ self.assertIn(f' ip router openfabric {domain}', tmp)
+ # for lo interface 'openfabric passive' is implied
+ self.assertIn(f' openfabric passive', tmp)
+ self.assertIn(f' openfabric metric {metric}', tmp)
+
+ def test_openfabric_03_password(self):
+ password = 'foo'
+
+ self.openfabric_base_config()
+
+ self.cli_set(path + ['interface', dummy_if, 'password', 'plaintext-password', f'{password}-{dummy_if}'])
+ self.cli_set(path + ['interface', dummy_if, 'password', 'md5', f'{password}-{dummy_if}'])
+
+ # verify() - can not use both md5 and plaintext-password for password for the interface
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['interface', dummy_if, 'password', 'md5'])
+
+ self.cli_set(path + ['domain-password', 'plaintext-password', password])
+ self.cli_set(path + ['domain-password', 'md5', password])
+
+ # verify() - can not use both md5 and plaintext-password for domain-password
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['domain-password', 'md5'])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' domain-password clear {password}', tmp)
+
+ tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd')
+ self.assertIn(f' openfabric password clear {password}-{dummy_if}', tmp)
+
+ def test_openfabric_multiple_domains(self):
+ domain_2 = 'VyOS_2'
+ interface = 'dum5678'
+ new_path = base_path + ['domain', domain_2]
+
+ self.openfabric_base_config()
+
+ # set same interface for 2 OpenFabric domains
+ self.cli_set(['interfaces', 'dummy', interface])
+ self.cli_set(new_path + ['interface', interface, 'address-family', 'ipv4'])
+ self.cli_set(path + ['interface', interface, 'address-family', 'ipv4'])
+
+ # verify() - same interface can be used only for one OpenFabric instance
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(path + ['interface', interface])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify FRR openfabric configuration
+ tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd')
+ self.assertIn(f'router openfabric {domain}', tmp)
+ self.assertIn(f' net {net}', tmp)
+
+ tmp = self.getFRRconfig(f'router openfabric {domain_2}', daemon='fabricd')
+ self.assertIn(f'router openfabric {domain_2}', tmp)
+ self.assertIn(f' net {net}', tmp)
+
+ # Verify interface configuration
+ tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd')
+ self.assertIn(f' ip router openfabric {domain}', tmp)
+ self.assertIn(f' ipv6 router openfabric {domain}', tmp)
+
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='fabricd')
+ self.assertIn(f' ip router openfabric {domain_2}', tmp)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
new file mode 100644
index 0000000..905eaf2
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -0,0 +1,565 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ospfd'
+base_path = ['protocols', 'ospf']
+
+route_map = 'foo-bar-baz10'
+dummy_if = 'dum3562'
+
+class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsOSPF, cls).setUpClass()
+
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+
+ cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit'])
+ cls.cli_set(cls, ['interfaces', 'dummy', dummy_if])
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['policy', 'route-map', route_map])
+ cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if])
+ super(TestProtocolsOSPF, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_ospf_01_defaults(self):
+ # commit changes
+ self.cli_set(base_path)
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+
+ def test_ospf_02_simple(self):
+ router_id = '127.0.0.1'
+ abr_type = 'ibm'
+ bandwidth = '1000'
+ metric = '123'
+
+ self.cli_set(base_path + ['auto-cost', 'reference-bandwidth', bandwidth])
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+ self.cli_set(base_path + ['parameters', 'abr-type', abr_type])
+ self.cli_set(base_path + ['parameters', 'opaque-lsa'])
+ self.cli_set(base_path + ['parameters', 'rfc1583-compatibility'])
+ self.cli_set(base_path + ['log-adjacency-changes', 'detail'])
+ self.cli_set(base_path + ['default-metric', metric])
+ self.cli_set(base_path + ['passive-interface', 'default'])
+ self.cli_set(base_path + ['area', '10', 'area-type', 'stub'])
+ self.cli_set(base_path + ['area', '10', 'network', '10.0.0.0/16'])
+ self.cli_set(base_path + ['area', '10', 'range', '10.0.1.0/24'])
+ self.cli_set(base_path + ['area', '10', 'range', '10.0.2.0/24', 'not-advertise'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' compatible rfc1583', frrconfig)
+ self.assertIn(f' auto-cost reference-bandwidth {bandwidth}', frrconfig)
+ self.assertIn(f' ospf router-id {router_id}', frrconfig)
+ self.assertIn(f' ospf abr-type {abr_type}', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+ self.assertIn(f' capability opaque', frrconfig)
+ self.assertIn(f' default-metric {metric}', frrconfig)
+ self.assertIn(f' passive-interface default', frrconfig)
+ self.assertIn(f' area 10 stub', frrconfig)
+ self.assertIn(f' network 10.0.0.0/16 area 10', frrconfig)
+ self.assertIn(f' area 10 range 10.0.1.0/24', frrconfig)
+ self.assertNotIn(f' area 10 range 10.0.1.0/24 not-advertise', frrconfig)
+ self.assertIn(f' area 10 range 10.0.2.0/24 not-advertise', frrconfig)
+
+
+ def test_ospf_03_access_list(self):
+ acl = '100'
+ seq = '10'
+ protocols = ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static']
+
+ self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'action', 'permit'])
+ self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'source', 'any'])
+ self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'destination', 'any'])
+ for ptotocol in protocols:
+ self.cli_set(base_path + ['access-list', acl, 'export', ptotocol])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+ for ptotocol in protocols:
+ self.assertIn(f' distribute-list {acl} out {ptotocol}', frrconfig) # defaults
+ self.cli_delete(['policy', 'access-list', acl])
+
+
+ def test_ospf_04_default_originate(self):
+ seq = '100'
+ metric = '50'
+ metric_type = '1'
+
+ self.cli_set(base_path + ['default-information', 'originate', 'metric', metric])
+ self.cli_set(base_path + ['default-information', 'originate', 'metric-type', metric_type])
+ self.cli_set(base_path + ['default-information', 'originate', 'route-map', route_map])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+ self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
+
+ # Now set 'always'
+ self.cli_set(base_path + ['default-information', 'originate', 'always'])
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
+
+
+ def test_ospf_05_options(self):
+ global_distance = '128'
+ intra_area = '100'
+ inter_area = '110'
+ external = '120'
+ on_startup = '30'
+ on_shutdown = '60'
+ refresh = '50'
+ aggregation_timer = '100'
+ summary_nets = {
+ '10.0.1.0/24' : {},
+ '10.0.2.0/24' : {'tag' : '50'},
+ '10.0.3.0/24' : {'no_advertise' : {}},
+ }
+
+ self.cli_set(base_path + ['distance', 'global', global_distance])
+ self.cli_set(base_path + ['distance', 'ospf', 'external', external])
+ self.cli_set(base_path + ['distance', 'ospf', 'intra-area', intra_area])
+
+ self.cli_set(base_path + ['max-metric', 'router-lsa', 'on-startup', on_startup])
+ self.cli_set(base_path + ['max-metric', 'router-lsa', 'on-shutdown', on_shutdown])
+
+ self.cli_set(base_path + ['mpls-te', 'enable'])
+ self.cli_set(base_path + ['refresh', 'timers', refresh])
+
+ self.cli_set(base_path + ['aggregation', 'timer', aggregation_timer])
+
+ for summary, summary_options in summary_nets.items():
+ self.cli_set(base_path + ['summary-address', summary])
+ if 'tag' in summary_options:
+ self.cli_set(base_path + ['summary-address', summary, 'tag', summary_options['tag']])
+ if 'no_advertise' in summary_options:
+ self.cli_set(base_path + ['summary-address', summary, 'no-advertise'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' mpls-te on', frrconfig)
+ self.assertIn(f' mpls-te router-address 0.0.0.0', frrconfig) # default
+ self.assertIn(f' distance {global_distance}', frrconfig)
+ self.assertIn(f' distance ospf intra-area {intra_area} external {external}', frrconfig)
+ self.assertIn(f' max-metric router-lsa on-startup {on_startup}', frrconfig)
+ self.assertIn(f' max-metric router-lsa on-shutdown {on_shutdown}', frrconfig)
+ self.assertIn(f' refresh timer {refresh}', frrconfig)
+
+ self.assertIn(f' aggregation timer {aggregation_timer}', frrconfig)
+ for summary, summary_options in summary_nets.items():
+ self.assertIn(f' summary-address {summary}', frrconfig)
+ if 'tag' in summary_options:
+ tag = summary_options['tag']
+ self.assertIn(f' summary-address {summary} tag {tag}', frrconfig)
+ if 'no_advertise' in summary_options:
+ self.assertIn(f' summary-address {summary} no-advertise', frrconfig)
+
+ # enable inter-area
+ self.cli_set(base_path + ['distance', 'ospf', 'inter-area', inter_area])
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f' distance ospf intra-area {intra_area} inter-area {inter_area} external {external}', frrconfig)
+
+
+ def test_ospf_06_neighbor(self):
+ priority = '10'
+ poll_interval = '20'
+ neighbors = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
+ for neighbor in neighbors:
+ self.cli_set(base_path + ['neighbor', neighbor, 'priority', priority])
+ self.cli_set(base_path + ['neighbor', neighbor, 'poll-interval', poll_interval])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ for neighbor in neighbors:
+ self.assertIn(f' neighbor {neighbor} priority {priority} poll-interval {poll_interval}', frrconfig) # default
+
+ def test_ospf_07_redistribute(self):
+ metric = '15'
+ metric_type = '1'
+ redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']
+
+ for protocol in redistribute:
+ self.cli_set(base_path + ['redistribute', protocol, 'metric', metric])
+ self.cli_set(base_path + ['redistribute', protocol, 'route-map', route_map])
+ self.cli_set(base_path + ['redistribute', protocol, 'metric-type', metric_type])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ for protocol in redistribute:
+ self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
+
+ def test_ospf_08_virtual_link(self):
+ networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
+ area = '10'
+ shortcut = 'enable'
+ virtual_link = '192.0.2.1'
+ hello = '6'
+ retransmit = '5'
+ transmit = '5'
+ dead = '40'
+
+ self.cli_set(base_path + ['area', area, 'shortcut', shortcut])
+ self.cli_set(base_path + ['area', area, 'virtual-link', virtual_link, 'hello-interval', hello])
+ self.cli_set(base_path + ['area', area, 'virtual-link', virtual_link, 'retransmit-interval', retransmit])
+ self.cli_set(base_path + ['area', area, 'virtual-link', virtual_link, 'transmit-delay', transmit])
+ self.cli_set(base_path + ['area', area, 'virtual-link', virtual_link, 'dead-interval', dead])
+ for network in networks:
+ self.cli_set(base_path + ['area', area, 'network', network])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' area {area} shortcut {shortcut}', frrconfig)
+ self.assertIn(f' area {area} virtual-link {virtual_link} hello-interval {hello} retransmit-interval {retransmit} transmit-delay {transmit} dead-interval {dead}', frrconfig)
+ for network in networks:
+ self.assertIn(f' network {network} area {area}', frrconfig)
+
+
+ def test_ospf_09_interface_configuration(self):
+ interfaces = Section.interfaces('ethernet')
+ password = 'vyos1234'
+ bandwidth = '10000'
+ cost = '150'
+ network = 'point-to-point'
+ priority = '200'
+ bfd_profile = 'vyos-test'
+
+ self.cli_set(base_path + ['passive-interface', 'default'])
+ for interface in interfaces:
+ base_interface = base_path + ['interface', interface]
+ self.cli_set(base_interface + ['authentication', 'plaintext-password', password])
+ self.cli_set(base_interface + ['bandwidth', bandwidth])
+ self.cli_set(base_interface + ['bfd', 'profile', bfd_profile])
+ self.cli_set(base_interface + ['cost', cost])
+ self.cli_set(base_interface + ['mtu-ignore'])
+ self.cli_set(base_interface + ['network', network])
+ self.cli_set(base_interface + ['priority', priority])
+ self.cli_set(base_interface + ['passive', 'disable'])
+
+ # commit changes
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' passive-interface default', frrconfig)
+
+ for interface in interfaces:
+ # Can not use daemon for getFRRconfig() as bandwidth parameter belongs to zebra process
+ config = self.getFRRconfig(f'interface {interface}')
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ip ospf authentication-key {password}', config)
+ self.assertIn(f' ip ospf bfd', config)
+ self.assertIn(f' ip ospf bfd profile {bfd_profile}', config)
+ self.assertIn(f' ip ospf cost {cost}', config)
+ self.assertIn(f' ip ospf mtu-ignore', config)
+ self.assertIn(f' ip ospf network {network}', config)
+ self.assertIn(f' ip ospf priority {priority}', config)
+ self.assertIn(f' no ip ospf passive', config)
+ self.assertIn(f' bandwidth {bandwidth}', config)
+
+ # T5467: Remove interface from OSPF process and VRF
+ self.cli_delete(base_path + ['interface'])
+ self.cli_commit()
+
+ for interface in interfaces:
+ # T5467: It must also be removed from FRR config
+ frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertNotIn(f'interface {interface}', frrconfig)
+ # There should be no OSPF related command at all under the interface
+ self.assertNotIn(f' ip ospf', frrconfig)
+
+ def test_ospf_11_interface_area(self):
+ area = '0'
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['area', area, 'network', '10.0.0.0/8'])
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'area', area])
+
+ # we can not have bot area network and interface area set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['area', area, 'network'])
+
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+
+ for interface in interfaces:
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ip ospf area {area}', config)
+
+ def test_ospf_12_vrfs(self):
+ # It is safe to assume that when the basic VRF test works, all
+ # other OSPF related features work, as we entirely inherit the CLI
+ # templates and Jinja2 FRR template.
+ table = '1000'
+ vrf = 'blue'
+ vrf_base = ['vrf', 'name', vrf]
+ vrf_iface = 'eth1'
+ area = '1'
+
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'ospf', 'interface', vrf_iface, 'area', area])
+ self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf])
+
+ # Also set a default VRF OSPF config
+ self.cli_set(base_path)
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+
+ frrconfig = self.getFRRconfig(f'router ospf vrf {vrf}', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf vrf {vrf}', frrconfig)
+ self.assertIn(f' auto-cost reference-bandwidth 100', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults
+
+ frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {vrf_iface}', frrconfig)
+ self.assertIn(f' ip ospf area {area}', frrconfig)
+
+ # T5467: Remove interface from OSPF process and VRF
+ self.cli_delete(vrf_base + ['protocols', 'ospf', 'interface'])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+ self.cli_commit()
+
+ # T5467: It must also be removed from FRR config
+ frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME)
+ self.assertNotIn(f'interface {vrf_iface}', frrconfig)
+ # There should be no OSPF related command at all under the interface
+ self.assertNotIn(f' ip ospf', frrconfig)
+
+ # cleanup
+ self.cli_delete(['vrf', 'name', vrf])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+
+ def test_ospf_13_export_list(self):
+ # Verify explort-list works on ospf-area
+ acl = '100'
+ seq = '10'
+ area = '0.0.0.10'
+ network = '10.0.0.0/8'
+
+ self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'action', 'permit'])
+ self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'source', 'any'])
+ self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'destination', 'any'])
+ self.cli_set(base_path + ['area', area, 'network', network])
+ self.cli_set(base_path + ['area', area, 'export-list', acl])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # default
+ self.assertIn(f' network {network} area {area}', frrconfig)
+ self.assertIn(f' area {area} export-list {acl}', frrconfig)
+
+
+ def test_ospf_14_segment_routing_configuration(self):
+ global_block_low = "300"
+ global_block_high = "399"
+ local_block_low = "400"
+ local_block_high = "499"
+ maximum_stack_size = '5'
+ prefix_one = '192.168.0.1/32'
+ prefix_two = '192.168.0.2/32'
+ prefix_one_value = '1'
+ prefix_two_value = '2'
+
+ self.cli_set(base_path + ['interface', dummy_if])
+ self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size])
+ self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low])
+ self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high])
+ self.cli_set(base_path + ['segment-routing', 'local-block', 'low-label-value', local_block_low])
+ self.cli_set(base_path + ['segment-routing', 'local-block', 'high-label-value', local_block_high])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_one, 'index', 'value', prefix_one_value])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_one, 'index', 'explicit-null'])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_two, 'index', 'value', prefix_two_value])
+ self.cli_set(base_path + ['segment-routing', 'prefix', prefix_two, 'index', 'no-php-flag'])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f' segment-routing on', frrconfig)
+ self.assertIn(f' segment-routing global-block {global_block_low} {global_block_high} local-block {local_block_low} {local_block_high}', frrconfig)
+ self.assertIn(f' segment-routing node-msd {maximum_stack_size}', frrconfig)
+ self.assertIn(f' segment-routing prefix {prefix_one} index {prefix_one_value} explicit-null', frrconfig)
+ self.assertIn(f' segment-routing prefix {prefix_two} index {prefix_two_value} no-php-flag', frrconfig)
+
+ def test_ospf_15_ldp_sync(self):
+ holddown = "500"
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['interface', dummy_if])
+ self.cli_set(base_path + ['ldp-sync', 'holddown', holddown])
+
+ # Commit main OSPF changes
+ self.cli_commit()
+
+ # Verify main OSPF changes
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig)
+ self.assertIn(f' mpls ldp-sync holddown {holddown}', frrconfig)
+
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'ldp-sync', 'holddown', holddown])
+
+ # Commit interface changes for holddown
+ self.cli_commit()
+
+ for interface in interfaces:
+ # Verify interface changes for holddown
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ip ospf dead-interval 40', config)
+ self.assertIn(f' ip ospf mpls ldp-sync', config)
+ self.assertIn(f' ip ospf mpls ldp-sync holddown {holddown}', config)
+
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'ldp-sync', 'disable'])
+
+ # Commit interface changes for disable
+ self.cli_commit()
+
+ for interface in interfaces:
+ # Verify interface changes for disable
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ip ospf dead-interval 40', config)
+ self.assertNotIn(f' ip ospf mpls ldp-sync', config)
+
+ def test_ospf_16_graceful_restart(self):
+ period = '300'
+ supported_grace_time = '400'
+ router_ids = ['192.0.2.1', '192.0.2.2']
+
+ self.cli_set(base_path + ['capability', 'opaque'])
+ self.cli_set(base_path + ['graceful-restart', 'grace-period', period])
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'planned-only'])
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'no-strict-lsa-checking'])
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'supported-grace-time', supported_grace_time])
+ for router_id in router_ids:
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'enable', 'router-id', router_id])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' capability opaque', frrconfig)
+ self.assertIn(f' graceful-restart grace-period {period}', frrconfig)
+ self.assertIn(f' graceful-restart helper planned-only', frrconfig)
+ self.assertIn(f' no graceful-restart helper strict-lsa-checking', frrconfig)
+ self.assertIn(f' graceful-restart helper supported-grace-time {supported_grace_time}', frrconfig)
+ for router_id in router_ids:
+ self.assertIn(f' graceful-restart helper enable {router_id}', frrconfig)
+
+ def test_ospf_17_duplicate_area_network(self):
+ area0 = '0'
+ area1 = '1'
+ network = '10.0.0.0/8'
+
+ self.cli_set(base_path + ['area', area0, 'network', network])
+
+ # we can not have the same network defined on two areas
+ self.cli_set(base_path + ['area', area1, 'network', network])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['area', area0])
+
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf', frrconfig)
+ self.assertIn(f' network {network} area {area1}', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py
new file mode 100644
index 0000000..989e155
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_ospfv3.py
@@ -0,0 +1,339 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ospf6d'
+base_path = ['protocols', 'ospfv3']
+
+route_map = 'foo-bar-baz-0815'
+
+router_id = '192.0.2.1'
+default_area = '0'
+
+class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsOSPFv3, cls).setUpClass()
+
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+
+ cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit'])
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['policy', 'route-map', route_map])
+ super(TestProtocolsOSPFv3, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_ospfv3_01_basic(self):
+ seq = '10'
+ prefix = '2001:db8::/32'
+ acl_name = 'foo-acl-100'
+
+ self.cli_set(['policy', 'access-list6', acl_name, 'rule', seq, 'action', 'permit'])
+ self.cli_set(['policy', 'access-list6', acl_name, 'rule', seq, 'source', 'any'])
+
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+ self.cli_set(base_path + ['area', default_area, 'range', prefix, 'advertise'])
+ self.cli_set(base_path + ['area', default_area, 'export-list', acl_name])
+ self.cli_set(base_path + ['area', default_area, 'import-list', acl_name])
+
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'area', default_area])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' area {default_area} range {prefix}', frrconfig)
+ self.assertIn(f' ospf6 router-id {router_id}', frrconfig)
+ self.assertIn(f' area {default_area} import-list {acl_name}', frrconfig)
+ self.assertIn(f' area {default_area} export-list {acl_name}', frrconfig)
+
+ for interface in interfaces:
+ if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'ipv6 ospf6 area {default_area}', if_config)
+
+ self.cli_delete(['policy', 'access-list6', acl_name])
+
+
+ def test_ospfv3_02_distance(self):
+ dist_global = '200'
+ dist_external = '110'
+ dist_inter_area = '120'
+ dist_intra_area = '130'
+
+ self.cli_set(base_path + ['distance', 'global', dist_global])
+ self.cli_set(base_path + ['distance', 'ospfv3', 'external', dist_external])
+ self.cli_set(base_path + ['distance', 'ospfv3', 'inter-area', dist_inter_area])
+ self.cli_set(base_path + ['distance', 'ospfv3', 'intra-area', dist_intra_area])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' distance {dist_global}', frrconfig)
+ self.assertIn(f' distance ospf6 intra-area {dist_intra_area} inter-area {dist_inter_area} external {dist_external}', frrconfig)
+
+
+ def test_ospfv3_03_redistribute(self):
+ metric = '15'
+ metric_type = '1'
+ route_map = 'foo-bar'
+ route_map_seq = '10'
+ redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']
+
+ self.cli_set(['policy', 'route-map', route_map, 'rule', route_map_seq, 'action', 'permit'])
+
+ for protocol in redistribute:
+ self.cli_set(base_path + ['redistribute', protocol, 'metric', metric])
+ self.cli_set(base_path + ['redistribute', protocol, 'route-map', route_map])
+ self.cli_set(base_path + ['redistribute', protocol, 'metric-type', metric_type])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ for protocol in redistribute:
+ self.assertIn(f' redistribute {protocol} metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
+
+
+ def test_ospfv3_04_interfaces(self):
+ bfd_profile = 'vyos-ipv6'
+
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+ self.cli_set(base_path + ['area', default_area])
+
+ cost = '100'
+ priority = '10'
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ if_base = base_path + ['interface', interface]
+ self.cli_set(if_base + ['bfd', 'profile', bfd_profile])
+ self.cli_set(if_base + ['cost', cost])
+ self.cli_set(if_base + ['instance-id', '0'])
+ self.cli_set(if_base + ['mtu-ignore'])
+ self.cli_set(if_base + ['network', 'point-to-point'])
+ self.cli_set(if_base + ['passive'])
+ self.cli_set(if_base + ['priority', priority])
+ cost = str(int(cost) + 10)
+ priority = str(int(priority) + 5)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+
+ cost = '100'
+ priority = '10'
+ for interface in interfaces:
+ if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', if_config)
+ self.assertIn(f' ipv6 ospf6 bfd', if_config)
+ self.assertIn(f' ipv6 ospf6 bfd profile {bfd_profile}', if_config)
+ self.assertIn(f' ipv6 ospf6 cost {cost}', if_config)
+ self.assertIn(f' ipv6 ospf6 mtu-ignore', if_config)
+ self.assertIn(f' ipv6 ospf6 network point-to-point', if_config)
+ self.assertIn(f' ipv6 ospf6 passive', if_config)
+ self.assertIn(f' ipv6 ospf6 priority {priority}', if_config)
+ cost = str(int(cost) + 10)
+ priority = str(int(priority) + 5)
+
+ # Cleanup interfaces
+ self.cli_delete(base_path + ['interface'])
+ self.cli_commit()
+
+ for interface in interfaces:
+ if_config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ # There should be no OSPF6 configuration at all after interface removal
+ self.assertNotIn(f' ipv6 ospf6', if_config)
+
+
+ def test_ospfv3_05_area_stub(self):
+ area_stub = '23'
+ area_stub_nosum = '26'
+
+ self.cli_set(base_path + ['area', area_stub, 'area-type', 'stub'])
+ self.cli_set(base_path + ['area', area_stub_nosum, 'area-type', 'stub', 'no-summary'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' area {area_stub} stub', frrconfig)
+ self.assertIn(f' area {area_stub_nosum} stub no-summary', frrconfig)
+
+
+ def test_ospfv3_06_area_nssa(self):
+ area_nssa = '1.1.1.1'
+ area_nssa_nosum = '2.2.2.2'
+ area_nssa_default = '3.3.3.3'
+
+ self.cli_set(base_path + ['area', area_nssa, 'area-type', 'nssa'])
+ self.cli_set(base_path + ['area', area_nssa, 'area-type', 'stub'])
+ # can only set one area-type per OSPFv3 area
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['area', area_nssa, 'area-type', 'stub'])
+
+ self.cli_set(base_path + ['area', area_nssa_nosum, 'area-type', 'nssa', 'no-summary'])
+ self.cli_set(base_path + ['area', area_nssa_nosum, 'area-type', 'nssa', 'default-information-originate'])
+ self.cli_set(base_path + ['area', area_nssa_default, 'area-type', 'nssa', 'default-information-originate'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' area {area_nssa} nssa', frrconfig)
+ self.assertIn(f' area {area_nssa_nosum} nssa default-information-originate no-summary', frrconfig)
+ self.assertIn(f' area {area_nssa_default} nssa default-information-originate', frrconfig)
+
+
+ def test_ospfv3_07_default_originate(self):
+ seq = '100'
+ metric = '50'
+ metric_type = '1'
+
+ self.cli_set(base_path + ['default-information', 'originate', 'metric', metric])
+ self.cli_set(base_path + ['default-information', 'originate', 'metric-type', metric_type])
+ self.cli_set(base_path + ['default-information', 'originate', 'route-map', route_map])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' default-information originate metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
+
+ # Now set 'always'
+ self.cli_set(base_path + ['default-information', 'originate', 'always'])
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f' default-information originate always metric {metric} metric-type {metric_type} route-map {route_map}', frrconfig)
+
+
+ def test_ospfv3_08_vrfs(self):
+ # It is safe to assume that when the basic VRF test works, all
+ # other OSPF related features work, as we entirely inherit the CLI
+ # templates and Jinja2 FRR template.
+ table = '1000'
+ vrf = 'blue'
+ vrf_base = ['vrf', 'name', vrf]
+ vrf_iface = 'eth1'
+ router_id = '1.2.3.4'
+ router_id_vrf = '1.2.3.5'
+
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'ospfv3', 'interface', vrf_iface, 'bfd'])
+ self.cli_set(vrf_base + ['protocols', 'ospfv3', 'parameters', 'router-id', router_id_vrf])
+
+ self.cli_set(['interfaces', 'ethernet', vrf_iface, 'vrf', vrf])
+
+ # Also set a default VRF OSPF config
+ self.cli_set(base_path + ['parameters', 'router-id', router_id])
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' ospf6 router-id {router_id}', frrconfig)
+
+ frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {vrf_iface}', frrconfig)
+ self.assertIn(f' ipv6 ospf6 bfd', frrconfig)
+
+ frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6 vrf {vrf}', frrconfig)
+ self.assertIn(f' ospf6 router-id {router_id_vrf}', frrconfig)
+
+ # T5467: Remove interface from OSPF process and VRF
+ self.cli_delete(vrf_base + ['protocols', 'ospfv3', 'interface'])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+ self.cli_commit()
+
+ # T5467: It must also be removed from FRR config
+ frrconfig = self.getFRRconfig(f'interface {vrf_iface}', daemon=PROCESS_NAME)
+ self.assertNotIn(f'interface {vrf_iface}', frrconfig)
+ # There should be no OSPF related command at all under the interface
+ self.assertNotIn(f' ipv6 ospf6', frrconfig)
+
+ # cleanup
+ self.cli_delete(['vrf', 'name', vrf])
+ self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
+
+
+ def test_ospfv3_09_graceful_restart(self):
+ period = '300'
+ supported_grace_time = '400'
+ router_ids = ['192.0.2.1', '192.0.2.2']
+
+ self.cli_set(base_path + ['graceful-restart', 'grace-period', period])
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'planned-only'])
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'lsa-check-disable'])
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'supported-grace-time', supported_grace_time])
+ for router_id in router_ids:
+ self.cli_set(base_path + ['graceful-restart', 'helper', 'enable', 'router-id', router_id])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ospf6', daemon=PROCESS_NAME)
+ self.assertIn(f'router ospf6', frrconfig)
+ self.assertIn(f' graceful-restart grace-period {period}', frrconfig)
+ self.assertIn(f' graceful-restart helper planned-only', frrconfig)
+ self.assertIn(f' graceful-restart helper lsa-check-disable', frrconfig)
+ self.assertIn(f' graceful-restart helper supported-grace-time {supported_grace_time}', frrconfig)
+ for router_id in router_ids:
+ self.assertIn(f' graceful-restart helper enable {router_id}', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_pim.py b/smoketest/scripts/cli/test_protocols_pim.py
new file mode 100644
index 0000000..ccfced1
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_pim.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'pimd'
+base_path = ['protocols', 'pim']
+
+class TestProtocolsPIM(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # pimd process must be running
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # pimd process must be stopped by now
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_pim_basic(self):
+ rp = '127.0.0.1'
+ group = '224.0.0.0/4'
+ hello = '100'
+ dr_priority = '64'
+
+ self.cli_set(base_path + ['rp', 'address', rp, 'group', group])
+
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface , 'bfd'])
+ self.cli_set(base_path + ['interface', interface , 'dr-priority', dr_priority])
+ self.cli_set(base_path + ['interface', interface , 'hello', hello])
+ self.cli_set(base_path + ['interface', interface , 'no-bsm'])
+ self.cli_set(base_path + ['interface', interface , 'no-unicast-bsm'])
+ self.cli_set(base_path + ['interface', interface , 'passive'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR pimd configuration
+ frrconfig = self.getFRRconfig(daemon=PROCESS_NAME)
+ self.assertIn(f'ip pim rp {rp} {group}', frrconfig)
+
+ for interface in interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', frrconfig)
+ self.assertIn(f' ip pim', frrconfig)
+ self.assertIn(f' ip pim bfd', frrconfig)
+ self.assertIn(f' ip pim drpriority {dr_priority}', frrconfig)
+ self.assertIn(f' ip pim hello {hello}', frrconfig)
+ self.assertIn(f' no ip pim bsm', frrconfig)
+ self.assertIn(f' no ip pim unicast-bsm', frrconfig)
+ self.assertIn(f' ip pim passive', frrconfig)
+
+ self.cli_commit()
+
+ def test_02_pim_advanced(self):
+ rp = '127.0.0.2'
+ group = '224.0.0.0/4'
+ join_prune_interval = '123'
+ rp_keep_alive_timer = '190'
+ keep_alive_timer = '180'
+ packets = '10'
+ prefix_list = 'pim-test'
+ register_suppress_time = '300'
+
+ self.cli_set(base_path + ['rp', 'address', rp, 'group', group])
+ self.cli_set(base_path + ['rp', 'keep-alive-timer', rp_keep_alive_timer])
+
+ self.cli_set(base_path + ['ecmp', 'rebalance'])
+ self.cli_set(base_path + ['join-prune-interval', join_prune_interval])
+ self.cli_set(base_path + ['keep-alive-timer', keep_alive_timer])
+ self.cli_set(base_path + ['packets', packets])
+ self.cli_set(base_path + ['register-accept-list', 'prefix-list', prefix_list])
+ self.cli_set(base_path + ['register-suppress-time', register_suppress_time])
+ self.cli_set(base_path + ['no-v6-secondary'])
+ self.cli_set(base_path + ['spt-switchover', 'infinity-and-beyond', 'prefix-list', prefix_list])
+ self.cli_set(base_path + ['ssm', 'prefix-list', prefix_list])
+
+ # check validate() - PIM require defined interfaces!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR pimd configuration
+ frrconfig = self.getFRRconfig(daemon=PROCESS_NAME)
+ self.assertIn(f'ip pim rp {rp} {group}', frrconfig)
+ self.assertIn(f'ip pim rp keep-alive-timer {rp_keep_alive_timer}', frrconfig)
+ self.assertIn(f'ip pim ecmp rebalance', frrconfig)
+ self.assertIn(f'ip pim join-prune-interval {join_prune_interval}', frrconfig)
+ self.assertIn(f'ip pim keep-alive-timer {keep_alive_timer}', frrconfig)
+ self.assertIn(f'ip pim packets {packets}', frrconfig)
+ self.assertIn(f'ip pim register-accept-list {prefix_list}', frrconfig)
+ self.assertIn(f'ip pim register-suppress-time {register_suppress_time}', frrconfig)
+ self.assertIn(f'no ip pim send-v6-secondary', frrconfig)
+ self.assertIn(f'ip pim spt-switchover infinity-and-beyond prefix-list {prefix_list}', frrconfig)
+ self.assertIn(f'ip pim ssm prefix-list {prefix_list}', frrconfig)
+
+ def test_03_pim_igmp_proxy(self):
+ igmp_proxy = ['protocols', 'igmp-proxy']
+ rp = '127.0.0.1'
+ group = '224.0.0.0/4'
+
+ self.cli_set(base_path)
+ self.cli_set(igmp_proxy)
+
+ # check validate() - can not set both IGMP proxy and PIM
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(igmp_proxy)
+
+ self.cli_set(base_path + ['rp', 'address', rp, 'group', group])
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface , 'bfd'])
+
+ # commit changes
+ self.cli_commit()
+
+ def test_04_igmp(self):
+ watermark_warning = '2000'
+ query_interval = '1000'
+ query_max_response_time = '200'
+ version = '2'
+
+ igmp_join = {
+ '224.1.1.1' : { 'source' : ['1.1.1.1', '2.2.2.2', '3.3.3.3'] },
+ '224.1.2.2' : { 'source' : [] },
+ '224.1.3.3' : {},
+ }
+
+ self.cli_set(base_path + ['igmp', 'watermark-warning', watermark_warning])
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface , 'igmp', 'version', version])
+ self.cli_set(base_path + ['interface', interface , 'igmp', 'query-interval', query_interval])
+ self.cli_set(base_path + ['interface', interface , 'igmp', 'query-max-response-time', query_max_response_time])
+
+ for join, join_config in igmp_join.items():
+ self.cli_set(base_path + ['interface', interface , 'igmp', 'join', join])
+ if 'source' in join_config:
+ for source in join_config['source']:
+ self.cli_set(base_path + ['interface', interface , 'igmp', 'join', join, 'source-address', source])
+
+ self.cli_commit()
+
+ frrconfig = self.getFRRconfig(daemon=PROCESS_NAME)
+ self.assertIn(f'ip igmp watermark-warn {watermark_warning}', frrconfig)
+
+ for interface in interfaces:
+ frrconfig = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', frrconfig)
+ self.assertIn(f' ip igmp', frrconfig)
+ self.assertIn(f' ip igmp version {version}', frrconfig)
+ self.assertIn(f' ip igmp query-interval {query_interval}', frrconfig)
+ self.assertIn(f' ip igmp query-max-response-time {query_max_response_time}', frrconfig)
+
+ for join, join_config in igmp_join.items():
+ if 'source' in join_config:
+ for source in join_config['source']:
+ self.assertIn(f' ip igmp join {join} {source}', frrconfig)
+ else:
+ self.assertIn(f' ip igmp join {join}', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py
new file mode 100644
index 0000000..ba24edc
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_pim6.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'pim6d'
+base_path = ['protocols', 'pim6']
+
+class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsPIMv6, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_pim6_01_mld_simple(self):
+ # commit changes
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ self.cli_commit()
+
+ # Verify FRR pim6d configuration
+ for interface in interfaces:
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ipv6 mld', config)
+ self.assertNotIn(f' ipv6 mld version 1', config)
+
+ # Change to MLD version 1
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mld', 'version', '1'])
+
+ self.cli_commit()
+
+ # Verify FRR pim6d configuration
+ for interface in interfaces:
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ipv6 mld', config)
+ self.assertIn(f' ipv6 mld version 1', config)
+
+ def test_pim6_02_mld_join(self):
+ interfaces = Section.interfaces('ethernet')
+ # Use an invalid multicast group address
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'fd00::1234'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface'])
+
+ # Use a valid multicast group address
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'ff18::1234'])
+
+ self.cli_commit()
+
+ # Verify FRR pim6d configuration
+ for interface in interfaces:
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ipv6 mld join ff18::1234', config)
+
+ # Join a source-specific multicast group
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mld', 'join', 'ff38::5678', 'source', '2001:db8::5678'])
+
+ self.cli_commit()
+
+ # Verify FRR pim6d configuration
+ for interface in interfaces:
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f'interface {interface}', config)
+ self.assertIn(f' ipv6 mld join ff38::5678 2001:db8::5678', config)
+
+ def test_pim6_03_basic(self):
+ interfaces = Section.interfaces('ethernet')
+ join_prune_interval = '123'
+ keep_alive_timer = '77'
+ packets = '5'
+ register_suppress_time = '99'
+ dr_priority = '100'
+ hello = '50'
+
+ self.cli_set(base_path + ['join-prune-interval', join_prune_interval])
+ self.cli_set(base_path + ['keep-alive-timer', keep_alive_timer])
+ self.cli_set(base_path + ['packets', packets])
+ self.cli_set(base_path + ['register-suppress-time', register_suppress_time])
+
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'dr-priority', dr_priority])
+ self.cli_set(base_path + ['interface', interface, 'hello', hello])
+ self.cli_set(base_path + ['interface', interface, 'no-bsm'])
+ self.cli_set(base_path + ['interface', interface, 'no-unicast-bsm'])
+ self.cli_set(base_path + ['interface', interface, 'passive'])
+
+ self.cli_commit()
+
+ # Verify FRR pim6d configuration
+ config = self.getFRRconfig(daemon=PROCESS_NAME)
+ self.assertIn(f'ipv6 pim join-prune-interval {join_prune_interval}', config)
+ self.assertIn(f'ipv6 pim keep-alive-timer {keep_alive_timer}', config)
+ self.assertIn(f'ipv6 pim packets {packets}', config)
+ self.assertIn(f'ipv6 pim register-suppress-time {register_suppress_time}', config)
+
+ for interface in interfaces:
+ config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
+ self.assertIn(f' ipv6 pim drpriority {dr_priority}', config)
+ self.assertIn(f' ipv6 pim hello {hello}', config)
+ self.assertIn(f' no ipv6 pim bsm', config)
+ self.assertIn(f' no ipv6 pim unicast-bsm', config)
+ self.assertIn(f' ipv6 pim passive', config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py
new file mode 100644
index 0000000..bfc327f
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_rip.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ripd'
+acl_in = '198'
+acl_out = '199'
+prefix_list_in = 'foo-prefix'
+prefix_list_out = 'bar-prefix'
+route_map = 'FooBar123'
+
+base_path = ['protocols', 'rip']
+
+class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsRIP, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'source', 'any'])
+ cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'destination', 'any'])
+ cls.cli_set(cls, ['policy', 'access-list', acl_out, 'rule', '20', 'action', 'deny'])
+ cls.cli_set(cls, ['policy', 'access-list', acl_out, 'rule', '20', 'source', 'any'])
+ cls.cli_set(cls, ['policy', 'access-list', acl_out, 'rule', '20', 'destination', 'any'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '100', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '100', 'prefix', '192.0.2.0/24'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '200', 'action', 'deny'])
+ cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '200', 'prefix', '192.0.2.0/24'])
+ cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['policy', 'access-list', acl_in])
+ cls.cli_delete(cls, ['policy', 'access-list', acl_out])
+ cls.cli_delete(cls, ['policy', 'prefix-list', prefix_list_in])
+ cls.cli_delete(cls, ['policy', 'prefix-list', prefix_list_out])
+ cls.cli_delete(cls, ['policy', 'route-map', route_map])
+
+ super(TestProtocolsRIP, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_rip_01_parameters(self):
+ distance = '40'
+ network_distance = '66'
+ metric = '8'
+ interfaces = Section.interfaces('ethernet')
+ neighbors = ['1.2.3.4', '1.2.3.5', '1.2.3.6']
+ networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
+ redistribute = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'static']
+ timer_garbage = '888'
+ timer_timeout = '1000'
+ timer_update = '90'
+
+ self.cli_set(base_path + ['default-distance', distance])
+ self.cli_set(base_path + ['default-information', 'originate'])
+ self.cli_set(base_path + ['default-metric', metric])
+ self.cli_set(base_path + ['distribute-list', 'access-list', 'in', acl_in])
+ self.cli_set(base_path + ['distribute-list', 'access-list', 'out', acl_out])
+ self.cli_set(base_path + ['distribute-list', 'prefix-list', 'in', prefix_list_in])
+ self.cli_set(base_path + ['distribute-list', 'prefix-list', 'out', prefix_list_out])
+ self.cli_set(base_path + ['passive-interface', 'default'])
+ self.cli_set(base_path + ['timers', 'garbage-collection', timer_garbage])
+ self.cli_set(base_path + ['timers', 'timeout', timer_timeout])
+ self.cli_set(base_path + ['timers', 'update', timer_update])
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'access-list', 'in', acl_in])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'access-list', 'out', acl_out])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'prefix-list', 'in', prefix_list_in])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'prefix-list', 'out', prefix_list_out])
+ for neighbor in neighbors:
+ self.cli_set(base_path + ['neighbor', neighbor])
+ for network in networks:
+ self.cli_set(base_path + ['network', network])
+ self.cli_set(base_path + ['network-distance', network, 'distance', network_distance])
+ self.cli_set(base_path + ['route', network])
+ for proto in redistribute:
+ self.cli_set(base_path + ['redistribute', proto, 'metric', metric])
+ self.cli_set(base_path + ['redistribute', proto, 'route-map', route_map])
+
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ripd configuration
+ frrconfig = self.getFRRconfig('router rip')
+ self.assertIn(f'router rip', frrconfig)
+ self.assertIn(f' distance {distance}', frrconfig)
+ self.assertIn(f' default-information originate', frrconfig)
+ self.assertIn(f' default-metric {metric}', frrconfig)
+ self.assertIn(f' distribute-list {acl_in} in', frrconfig)
+ self.assertIn(f' distribute-list {acl_out} out', frrconfig)
+ self.assertIn(f' distribute-list prefix {prefix_list_in} in', frrconfig)
+ self.assertIn(f' distribute-list prefix {prefix_list_out} out', frrconfig)
+ self.assertIn(f' passive-interface default', frrconfig)
+ self.assertIn(f' timers basic {timer_update} {timer_timeout} {timer_garbage}', frrconfig)
+ for interface in interfaces:
+ self.assertIn(f' network {interface}', frrconfig)
+ self.assertIn(f' distribute-list {acl_in} in {interface}', frrconfig)
+ self.assertIn(f' distribute-list {acl_out} out {interface}', frrconfig)
+ self.assertIn(f' distribute-list prefix {prefix_list_in} in {interface}', frrconfig)
+ self.assertIn(f' distribute-list prefix {prefix_list_out} out {interface}', frrconfig)
+ for neighbor in neighbors:
+ self.assertIn(f' neighbor {neighbor}', frrconfig)
+ for network in networks:
+ self.assertIn(f' network {network}', frrconfig)
+ self.assertIn(f' distance {network_distance} {network}', frrconfig)
+ self.assertIn(f' route {network}', frrconfig)
+ for proto in redistribute:
+ self.assertIn(f' redistribute {proto} metric {metric} route-map {route_map}', frrconfig)
+
+ def test_rip_02_zebra_route_map(self):
+ # Implemented because of T3328
+ self.cli_set(base_path + ['route-map', route_map])
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ zebra_route_map = f'ip protocol rip route-map {route_map}'
+ frrconfig = self.getFRRconfig(zebra_route_map)
+ self.assertIn(zebra_route_map, frrconfig)
+
+ # Remove the route-map again
+ self.cli_delete(base_path + ['route-map'])
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig(zebra_route_map)
+ self.assertNotIn(zebra_route_map, frrconfig)
+
+ def test_rip_03_version(self):
+ rx_version = '1'
+ tx_version = '2'
+ interface = 'eth0'
+
+ self.cli_set(base_path + ['version', tx_version])
+ self.cli_set(base_path + ['interface', interface, 'send', 'version', tx_version])
+ self.cli_set(base_path + ['interface', interface, 'receive', 'version', rx_version])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig('router rip')
+ self.assertIn(f'version {tx_version}', frrconfig)
+
+ frrconfig = self.getFRRconfig(f'interface {interface}')
+ self.assertIn(f' ip rip receive version {rx_version}', frrconfig)
+ self.assertIn(f' ip rip send version {tx_version}', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py
new file mode 100644
index 0000000..0cfb065
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_ripng.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ripngd'
+acl_in = '198'
+acl_out = '199'
+prefix_list_in = 'foo-prefix'
+prefix_list_out = 'bar-prefix'
+route_map = 'FooBar123'
+
+base_path = ['protocols', 'ripng']
+
+class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsRIPng, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'source', 'any'])
+ cls.cli_set(cls, ['policy', 'access-list6', acl_out, 'rule', '20', 'action', 'deny'])
+ cls.cli_set(cls, ['policy', 'access-list6', acl_out, 'rule', '20', 'source', 'any'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in, 'rule', '100', 'action', 'permit'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in, 'rule', '100', 'prefix', '2001:db8::/32'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out, 'rule', '200', 'action', 'deny'])
+ cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out, 'rule', '200', 'prefix', '2001:db8::/32'])
+ cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
+
+ @classmethod
+ def tearDownClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsRIPng, cls).tearDownClass()
+
+ cls.cli_delete(cls, ['policy', 'access-list6', acl_in])
+ cls.cli_delete(cls, ['policy', 'access-list6', acl_out])
+ cls.cli_delete(cls, ['policy', 'prefix-list6', prefix_list_in])
+ cls.cli_delete(cls, ['policy', 'prefix-list6', prefix_list_out])
+ cls.cli_delete(cls, ['policy', 'route-map', route_map])
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_ripng_01_parameters(self):
+ metric = '8'
+ interfaces = Section.interfaces('ethernet')
+ aggregates = ['2001:db8:1000::/48', '2001:db8:2000::/48', '2001:db8:3000::/48']
+ networks = ['2001:db8:1000::/64', '2001:db8:1001::/64', '2001:db8:2000::/64', '2001:db8:2001::/64']
+ redistribute = ['bgp', 'connected', 'kernel', 'ospfv3', 'static']
+ timer_garbage = '888'
+ timer_timeout = '1000'
+ timer_update = '90'
+
+ self.cli_set(base_path + ['default-information', 'originate'])
+ self.cli_set(base_path + ['default-metric', metric])
+ self.cli_set(base_path + ['distribute-list', 'access-list', 'in', acl_in])
+ self.cli_set(base_path + ['distribute-list', 'access-list', 'out', acl_out])
+ self.cli_set(base_path + ['distribute-list', 'prefix-list', 'in', prefix_list_in])
+ self.cli_set(base_path + ['distribute-list', 'prefix-list', 'out', prefix_list_out])
+ self.cli_set(base_path + ['passive-interface', 'default'])
+ self.cli_set(base_path + ['timers', 'garbage-collection', timer_garbage])
+ self.cli_set(base_path + ['timers', 'timeout', timer_timeout])
+ self.cli_set(base_path + ['timers', 'update', timer_update])
+ for aggregate in aggregates:
+ self.cli_set(base_path + ['aggregate-address', aggregate])
+
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'access-list', 'in', acl_in])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'access-list', 'out', acl_out])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'prefix-list', 'in', prefix_list_in])
+ self.cli_set(base_path + ['distribute-list', 'interface', interface, 'prefix-list', 'out', prefix_list_out])
+ for network in networks:
+ self.cli_set(base_path + ['network', network])
+ self.cli_set(base_path + ['route', network])
+ for proto in redistribute:
+ self.cli_set(base_path + ['redistribute', proto, 'metric', metric])
+ self.cli_set(base_path + ['redistribute', proto, 'route-map', route_map])
+
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR ospfd configuration
+ frrconfig = self.getFRRconfig('router ripng')
+ self.assertIn(f'router ripng', frrconfig)
+ self.assertIn(f' default-information originate', frrconfig)
+ self.assertIn(f' default-metric {metric}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list {acl_in} in', frrconfig)
+ self.assertIn(f' ipv6 distribute-list {acl_out} out', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in} in', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out} out', frrconfig)
+ self.assertIn(f' passive-interface default', frrconfig)
+ self.assertIn(f' timers basic {timer_update} {timer_timeout} {timer_garbage}', frrconfig)
+ for aggregate in aggregates:
+ self.assertIn(f' aggregate-address {aggregate}', frrconfig)
+ for interface in interfaces:
+ self.assertIn(f' network {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list {acl_in} in {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list {acl_out} out {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_in} in {interface}', frrconfig)
+ self.assertIn(f' ipv6 distribute-list prefix {prefix_list_out} out {interface}', frrconfig)
+ for network in networks:
+ self.assertIn(f' network {network}', frrconfig)
+ self.assertIn(f' route {network}', frrconfig)
+ for proto in redistribute:
+ if proto == 'ospfv3':
+ proto = 'ospf6'
+ self.assertIn(f' redistribute {proto} metric {metric} route-map {route_map}', frrconfig)
+
+ def test_ripng_02_zebra_route_map(self):
+ # Implemented because of T3328
+ self.cli_set(base_path + ['route-map', route_map])
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ zebra_route_map = f'ipv6 protocol ripng route-map {route_map}'
+ frrconfig = self.getFRRconfig(zebra_route_map)
+ self.assertIn(zebra_route_map, frrconfig)
+
+ # Remove the route-map again
+ self.cli_delete(base_path + ['route-map'])
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig(zebra_route_map)
+ self.assertNotIn(zebra_route_map, frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py
new file mode 100644
index 0000000..29f03a2
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_rpki.py
@@ -0,0 +1,247 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+from vyos.utils.process import process_named_running
+
+base_path = ['protocols', 'rpki']
+PROCESS_NAME = 'bgpd'
+
+rpki_key_name = 'rpki-smoketest'
+rpki_key_type = 'ssh-rsa'
+
+rpki_ssh_key = """
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAQEAweDyflDFR4qyEwETbJkZ2ZZc+sJNiDTvYpwGsWIkju49lJSxHe1x
+Kf8FhwfyMu40Snt1yDlRmmmz4CsbLgbuZGMPvXG11e34+C0pSVUvpF6aqRTeLl1pDRK7Rn
+jgm3su+I8SRLQR4qbLG6VXWOFuVpwiqbExLaU0hFYTPNP+dArNpsWEEKsohk6pTXdhg3Vz
+Wp3vCMjl2JTshDa3lD7p2xISSAReEY0fnfEAmQzH4Z6DIwwGdFuMWoQIg+oFBM9ARrO2/F
+IjRsz6AecR/WeU72JEw4aJic1/cAJQA6PiQBHwkuo3Wll1tbpxeRZoB2NQG22ETyJLvhfT
+aooNLT9HpQAAA8joU5dM6FOXTAAAAAdzc2gtcnNhAAABAQDB4PJ+UMVHirITARNsmRnZll
+z6wk2INO9inAaxYiSO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV
+7fj4LSlJVS+kXpqpFN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVh
+M80/50Cs2mxYQQqyiGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfh
+noMjDAZ0W4xahAiD6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS6j
+daWXW1unF5FmgHY1AbbYRPIku+F9Nqig0tP0elAAAAAwEAAQAAAQACkDlUjzfUhtJs6uY5
+WNrdJB5NmHUS+HQzzxFNlhkapK6+wKqI1UNaRUtq6iF7J+gcFf7MK2nXS098BsXguWm8fQ
+zPuemoDvHsQhiaJhyvpSqRUrvPTB/f8t/0AhQiKiJIWgfpTaIw53inAGwjujNNxNm2eafH
+TThhCYxOkRT7rsT6bnSio6yeqPy5QHg7IKFztp5FXDUyiOS3aX3SvzQcDUkMXALdvzX50t
+1XIk+X48Rgkq72dL4VpV2oMNDu3hM6FqBUplf9Mv3s51FNSma/cibCQoVufrIfoqYjkNTj
+IpYFUcq4zZ0/KvgXgzSsy9VN/4TtbalrOuu7X/SHJbvhAAAAgGPFsXgONYQvXxCnK1dIue
+ozgaZg1I/n522E2ZCOXBW4dYJVyNpppwRreDzuFzTDEe061MpNHfScjVBJCCulivFYWscL
+6oaGsryDbFxO3QmB4I98UBqrds2yan9/JGc6EYe299yvaHy7Y64+NC0+fN8H2RAZ61T4w1
+0JrCaJRyvzAAAAgQDvBfuV1U7o9k/fbU+U7W2UYnWblpOZAMfi1XQP6IJJeyWs90PdTdXh
++l0eIQrCawIiRJytNfxMmbD4huwTf77fWiyCcPznmALQ7ex/yJ+W5Z0V4dPGF3h7o1uiS2
+36JhQ7mfcliCkhp/1PIklBIMPcCp0zl+s9wMv2hX7w1Pah9QAAAIEAz6YgU9Xute+J+dBw
+oWxEQ+igR6KE55Um7O9AvSrqnCm9r7lSFsXC2ErYOxoDSJ3yIBEV0b4XAGn6tbbVIs3jS8
+BnLHxclAHQecOx1PGn7PKbnPW0oJRq/X9QCIEelKYvlykpayn7uZooTXqcDaPZxfPpmPdy
+e8chVJvdygi7kPEAAAAMY3BvQExSMS53dWUzAQIDBAUGBw==
+"""
+
+rpki_ssh_pub = """
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDB4PJ+UMVHirITARNsmRnZllz6wk2INO9inAaxYi
+SO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV7fj4LSlJVS+kXpqp
+FN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVhM80/50Cs2mxYQQqy
+iGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfhnoMjDAZ0W4xahAiD
+6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS6jdaWXW1unF5FmgHY1
+AbbYRPIku+F9Nqig0tP0el
+"""
+
+rpki_ssh_key_replacement = """
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAQEAtLPMwiGR3o6puPDbus9Yqoah9/7rv7i6ykykPmcEZ6ERnA0N6bl7
+LkQxnCuX270ukTTZOhROvQnvQYIZohCMz27Q16z7r+I755QXL0x8x4Gqhg/hQUY7UtX6ts
+db8+pO7G1PL4r9zT6/KJAF/wv86DezJ3I6TMaA7MCikXfQWJisBvhgAXF1+7V9CWaroGgV
+/hHzQJu1yd4cfsYoHyeDaZ+lwFw4egNItIy63fIGDxrnXaonJ1ODGQh7zWlpl/cwQR/KyJ
+P8vvOZ9olQ6syZV+DAcAo4Fe59wW2Zj4bl8bdGcdiDn0grkafxwTcg9ynr9kwQ8b66oXY4
+hwB4vlPFPwAAA8jkGyX45Bsl+AAAAAdzc2gtcnNhAAABAQC0s8zCIZHejqm48Nu6z1iqhq
+H3/uu/uLrKTKQ+ZwRnoRGcDQ3puXsuRDGcK5fbvS6RNNk6FE69Ce9BghmiEIzPbtDXrPuv
+4jvnlBcvTHzHgaqGD+FBRjtS1fq2x1vz6k7sbU8viv3NPr8okAX/C/zoN7MncjpMxoDswK
+KRd9BYmKwG+GABcXX7tX0JZqugaBX+EfNAm7XJ3hx+xigfJ4Npn6XAXDh6A0i0jLrd8gYP
+GuddqicnU4MZCHvNaWmX9zBBH8rIk/y+85n2iVDqzJlX4MBwCjgV7n3BbZmPhuXxt0Zx2I
+OfSCuRp/HBNyD3Kev2TBDxvrqhdjiHAHi+U8U/AAAAAwEAAQAAAQA99gkX5/rknXaE+9Hc
+VIzKrC+NodOkgetKwszuuNRB1HD9WVyT8A3U5307V5dSuaPmFoEF8UCugWGQzNONRq+B0T
+W7Po1u2dxAo/7vMQL4RfX60icjAroExWqakfFtycIWP8UPQFGWtxVFC12C/tFRrwe3Vuu2
+t7otdEBKMRM3zU0Hj88/5FIk/MDhththDCKTMe4+iwNKo30dyqSCckpTd2k5de9JYz8Aom
+87jtQcyDdynaELSo9CsA8KRPlozZ4VSWTVLH+Cv2TZWPL7hy79YvvIfuF/Sd6PGkNwG1Vj
+TAbq2Wx4uq+HmpNiz7W0LnbZtQJ7dzLA3FZlvQMC8fVBAAAAgQDWvImVZCyVWpoG+LnKY3
+joegjKRYKdgKRPCqGoIHiYsqCRxqSRW3jsuQCCvk4YO3/ZmqORiGktK+5r8R1QEtwg5qbi
+N7GZD34m7USNuqG2G/4puEly8syMmR6VRRvEURFQrpv2wniXNSefvsDc+WDqTfXGUxr+FT
+478wkzjwc/fAAAAIEA9uP0Ym3OC3cZ5FOvmu51lxo5lqPlUeE78axg2I4u/9Il8nOvSVuq
+B9X5wAUyGAGcUjT3EZmRAtL2sQxc5T0Vw3bnxCjzukEbFM+DRtYy1hXSOoGTTwKoMWBpho
+R3X5uRLUQL/22C4rd7tSJpjqnZXIH0B5z2fFh4vzu8/SrgCrUAAACBALtep4BcGJfjfhfF
+ODzQe7Rk7tsaX8pfNv6bQu0sR5C9pDURFRf0fRC0oqgeTuzq/vHPyNLsUUgTCpKWiLFmvU
+G9pelLT3XPPgzA+g0gycM0unuX8kkP3T5VQAM/7u0+h1CaJ8A6cCkzvDJxYdfio3WR60OP
+ulHg7HCcyomFLaSjAAAADGNwb0BMUjEud3VlMwECAwQFBg==
+"""
+
+rpki_ssh_pub_replacement = """
+AAAAB3NzaC1yc2EAAAADAQABAAABAQC0s8zCIZHejqm48Nu6z1iqhqH3/uu/uLrKTKQ+Zw
+RnoRGcDQ3puXsuRDGcK5fbvS6RNNk6FE69Ce9BghmiEIzPbtDXrPuv4jvnlBcvTHzHgaqG
+D+FBRjtS1fq2x1vz6k7sbU8viv3NPr8okAX/C/zoN7MncjpMxoDswKKRd9BYmKwG+GABcX
+X7tX0JZqugaBX+EfNAm7XJ3hx+xigfJ4Npn6XAXDh6A0i0jLrd8gYPGuddqicnU4MZCHvN
+aWmX9zBBH8rIk/y+85n2iVDqzJlX4MBwCjgV7n3BbZmPhuXxt0Zx2IOfSCuRp/HBNyD3Ke
+v2TBDxvrqhdjiHAHi+U8U/
+"""
+
+class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsRPKI, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_rpki(self):
+ expire_interval = '3600'
+ polling_period = '600'
+ retry_interval = '300'
+ cache = {
+ '192.0.2.1' : {
+ 'port' : '8080',
+ 'preference' : '10'
+ },
+ '2001:db8::1' : {
+ 'port' : '1234',
+ 'preference' : '30'
+ },
+ 'rpki.vyos.net' : {
+ 'port' : '5678',
+ 'preference' : '40'
+ },
+ }
+
+ self.cli_set(base_path + ['expire-interval', expire_interval])
+ self.cli_set(base_path + ['polling-period', polling_period])
+ self.cli_set(base_path + ['retry-interval', retry_interval])
+
+ for peer, peer_config in cache.items():
+ self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']])
+ self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig('rpki')
+ self.assertIn(f'rpki expire_interval {expire_interval}', frrconfig)
+ self.assertIn(f'rpki polling_period {polling_period}', frrconfig)
+ self.assertIn(f'rpki retry_interval {retry_interval}', frrconfig)
+
+ for peer, peer_config in cache.items():
+ port = peer_config['port']
+ preference = peer_config['preference']
+ self.assertIn(f'rpki cache {peer} {port} preference {preference}', frrconfig)
+
+ def test_rpki_ssh(self):
+ polling = '7200'
+ cache = {
+ '192.0.2.3' : {
+ 'port' : '1234',
+ 'username' : 'foo',
+ 'preference' : '10'
+ },
+ '192.0.2.4' : {
+ 'port' : '5678',
+ 'username' : 'bar',
+ 'preference' : '20'
+ },
+ }
+
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n','')])
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n','')])
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type])
+
+ for cache_name, cache_config in cache.items():
+ self.cli_set(base_path + ['cache', cache_name, 'port', cache_config['port']])
+ self.cli_set(base_path + ['cache', cache_name, 'preference', cache_config['preference']])
+ self.cli_set(base_path + ['cache', cache_name, 'ssh', 'username', cache_config['username']])
+ self.cli_set(base_path + ['cache', cache_name, 'ssh', 'key', rpki_key_name])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig('rpki')
+ for cache_name, cache_config in cache.items():
+ port = cache_config['port']
+ preference = cache_config['preference']
+ username = cache_config['username']
+ self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig)
+
+ # Verify content of SSH keys
+ tmp = read_file(f'/run/frr/id_rpki_{cache_name}')
+ self.assertIn(rpki_ssh_key.replace('\n',''), tmp)
+ tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub')
+ self.assertIn(rpki_ssh_pub.replace('\n',''), tmp)
+
+ # Change OpenSSH key and verify it was properly written to filesystem
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key_replacement.replace('\n','')])
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub_replacement.replace('\n','')])
+ # commit changes
+ self.cli_commit()
+
+ for cache_name, cache_config in cache.items():
+ port = cache_config['port']
+ preference = cache_config['preference']
+ username = cache_config['username']
+ self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig)
+
+ # Verify content of SSH keys
+ tmp = read_file(f'/run/frr/id_rpki_{cache_name}')
+ self.assertIn(rpki_ssh_key_replacement.replace('\n',''), tmp)
+ tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub')
+ self.assertIn(rpki_ssh_pub_replacement.replace('\n',''), tmp)
+
+ self.cli_delete(['pki', 'openssh'])
+
+ def test_rpki_verify_preference(self):
+ cache = {
+ '192.0.2.1' : {
+ 'port' : '8080',
+ 'preference' : '1'
+ },
+ '192.0.2.2' : {
+ 'port' : '9090',
+ 'preference' : '1'
+ },
+ }
+
+ for peer, peer_config in cache.items():
+ self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']])
+ self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']])
+
+ # check validate() - preferences must be unique
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_segment-routing.py b/smoketest/scripts/cli/test_protocols_segment-routing.py
new file mode 100644
index 0000000..daa7f08
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_segment-routing.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+from vyos.utils.system import sysctl_read
+
+base_path = ['protocols', 'segment-routing']
+PROCESS_NAME = 'zebra'
+
+class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestProtocolsSegmentRouting, cls).setUpClass()
+ # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same
+ cls.daemon_pid = process_named_running(PROCESS_NAME)
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # check process health and continuity
+ self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))
+
+ def test_srv6(self):
+ interfaces = Section.interfaces('ethernet', vlan=False)
+ locators = {
+ 'foo' : { 'prefix' : '2001:a::/64' },
+ 'foo' : { 'prefix' : '2001:b::/64', 'usid' : {} },
+ }
+
+ for locator, locator_config in locators.items():
+ self.cli_set(base_path + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']])
+ if 'usid' in locator_config:
+ self.cli_set(base_path + ['srv6', 'locator', locator, 'behavior-usid'])
+
+ # verify() - SRv6 should be enabled on at least one interface!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'srv6'])
+
+ self.cli_commit()
+
+ for interface in interfaces:
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1')
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '0') # default
+
+ frrconfig = self.getFRRconfig(f'segment-routing', daemon='zebra')
+ self.assertIn(f'segment-routing', frrconfig)
+ self.assertIn(f' srv6', frrconfig)
+ self.assertIn(f' locators', frrconfig)
+ for locator, locator_config in locators.items():
+ self.assertIn(f' locator {locator}', frrconfig)
+ self.assertIn(f' prefix {locator_config["prefix"]} block-len 40 node-len 24 func-bits 16', frrconfig)
+
+ def test_srv6_sysctl(self):
+ interfaces = Section.interfaces('ethernet', vlan=False)
+
+ # HMAC accept
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'srv6'])
+ self.cli_set(base_path + ['interface', interface, 'srv6', 'hmac', 'ignore'])
+ self.cli_commit()
+
+ for interface in interfaces:
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1')
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '-1') # ignore
+
+ # HMAC drop
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'srv6'])
+ self.cli_set(base_path + ['interface', interface, 'srv6', 'hmac', 'drop'])
+ self.cli_commit()
+
+ for interface in interfaces:
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_enabled'), '1')
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{interface}.seg6_require_hmac'), '1') # drop
+
+ # Disable SRv6 on first interface
+ first_if = interfaces[-1]
+ self.cli_delete(base_path + ['interface', first_if])
+ self.cli_commit()
+
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{first_if}.seg6_enabled'), '0')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py
new file mode 100644
index 0000000..f676e2a
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_static.py
@@ -0,0 +1,482 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.template import is_ipv6
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
+
+base_path = ['protocols', 'static']
+vrf_path = ['protocols', 'vrf']
+
+routes = {
+ '10.0.0.0/8' : {
+ 'next_hop' : {
+ '192.0.2.100' : { 'distance' : '100' },
+ '192.0.2.110' : { 'distance' : '110', 'interface' : 'eth0' },
+ '192.0.2.120' : { 'distance' : '120', 'disable' : '' },
+ '192.0.2.130' : { 'bfd' : '' },
+ '192.0.2.140' : { 'bfd_source' : '192.0.2.10' },
+ },
+ 'interface' : {
+ 'eth0' : { 'distance' : '130' },
+ 'eth1' : { 'distance' : '140' },
+ },
+ 'blackhole' : { 'distance' : '250', 'tag' : '500' },
+ },
+ '172.16.0.0/12' : {
+ 'interface' : {
+ 'eth0' : { 'distance' : '50', 'vrf' : 'black' },
+ 'eth1' : { 'distance' : '60', 'vrf' : 'black' },
+ },
+ 'blackhole' : { 'distance' : '90' },
+ },
+ '192.0.2.0/24' : {
+ 'interface' : {
+ 'eth0' : { 'distance' : '50', 'vrf' : 'black' },
+ 'eth1' : { 'disable' : '' },
+ },
+ 'blackhole' : { 'distance' : '90' },
+ },
+ '100.64.0.0/16' : {
+ 'blackhole' : {},
+ },
+ '100.65.0.0/16' : {
+ 'reject' : { 'distance' : '10', 'tag' : '200' },
+ },
+ '100.66.0.0/16' : {
+ 'blackhole' : {},
+ 'reject' : { 'distance' : '10', 'tag' : '200' },
+ },
+ '2001:db8:100::/40' : {
+ 'next_hop' : {
+ '2001:db8::1' : { 'distance' : '10' },
+ '2001:db8::2' : { 'distance' : '20', 'interface' : 'eth0' },
+ '2001:db8::3' : { 'distance' : '30', 'disable' : '' },
+ '2001:db8::4' : { 'bfd' : '' },
+ '2001:db8::5' : { 'bfd_source' : '2001:db8::ffff' },
+ },
+ 'interface' : {
+ 'eth0' : { 'distance' : '40', 'vrf' : 'black' },
+ 'eth1' : { 'distance' : '50', 'disable' : '' },
+ },
+ 'blackhole' : { 'distance' : '250', 'tag' : '500' },
+ },
+ '2001:db8:200::/40' : {
+ 'interface' : {
+ 'eth0' : { 'distance' : '40' },
+ 'eth1' : { 'distance' : '50', 'disable' : '' },
+ },
+ 'blackhole' : { 'distance' : '250', 'tag' : '500' },
+ },
+ '2001:db8:300::/40' : {
+ 'reject' : { 'distance' : '250', 'tag' : '500' },
+ },
+ '2001:db8:400::/40' : {
+ 'next_hop' : {
+ '2001:db8::400' : { 'segments' : '2001:db8:aaaa::400/2002::400/2003::400/2004::400' },
+ },
+ },
+ '2001:db8:500::/40' : {
+ 'next_hop' : {
+ '2001:db8::500' : { 'segments' : '2001:db8:aaaa::500/2002::500/2003::500/2004::500' },
+ },
+ },
+ '2001:db8:600::/40' : {
+ 'interface' : {
+ 'eth0' : { 'segments' : '2001:db8:aaaa::600/2002::600' },
+ },
+ },
+ '2001:db8:700::/40' : {
+ 'interface' : {
+ 'eth1' : { 'segments' : '2001:db8:aaaa::700' },
+ },
+ },
+ '2001:db8::/32' : {
+ 'blackhole' : { 'distance' : '200', 'tag' : '600' }
+ },
+}
+
+tables = ['80', '81', '82']
+
+class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestProtocolsStatic, cls).setUpClass()
+ cls.cli_delete(cls, ['vrf'])
+ cls.cli_set(cls, ['vrf', 'name', 'black', 'table', '43210'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['vrf'])
+ super(TestProtocolsStatic, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ v4route = self.getFRRconfig('ip route', end='')
+ self.assertFalse(v4route)
+ v6route = self.getFRRconfig('ipv6 route', end='')
+ self.assertFalse(v6route)
+
+ def test_01_static(self):
+ bfd_profile = 'vyos-test'
+ for route, route_config in routes.items():
+ route_type = 'route'
+ if is_ipv6(route):
+ route_type = 'route6'
+ base = base_path + [route_type, route]
+ if 'next_hop' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ self.cli_set(base + ['next-hop', next_hop])
+ if 'disable' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'disable'])
+ if 'distance' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'distance', next_hop_config['distance']])
+ if 'interface' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'interface', next_hop_config['interface']])
+ if 'vrf' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']])
+ if 'bfd' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', bfd_profile ])
+ if 'bfd_source' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source', next_hop_config['bfd_source'], 'profile', bfd_profile])
+ if 'segments' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'segments', next_hop_config['segments']])
+
+ if 'interface' in route_config:
+ for interface, interface_config in route_config['interface'].items():
+ self.cli_set(base + ['interface', interface])
+ if 'disable' in interface_config:
+ self.cli_set(base + ['interface', interface, 'disable'])
+ if 'distance' in interface_config:
+ self.cli_set(base + ['interface', interface, 'distance', interface_config['distance']])
+ if 'vrf' in interface_config:
+ self.cli_set(base + ['interface', interface, 'vrf', interface_config['vrf']])
+ if 'segments' in interface_config:
+ self.cli_set(base + ['interface', interface, 'segments', interface_config['segments']])
+
+ if 'blackhole' in route_config:
+ self.cli_set(base + ['blackhole'])
+ if 'distance' in route_config['blackhole']:
+ self.cli_set(base + ['blackhole', 'distance', route_config['blackhole']['distance']])
+ if 'tag' in route_config['blackhole']:
+ self.cli_set(base + ['blackhole', 'tag', route_config['blackhole']['tag']])
+
+ if 'reject' in route_config:
+ self.cli_set(base + ['reject'])
+ if 'distance' in route_config['reject']:
+ self.cli_set(base + ['reject', 'distance', route_config['reject']['distance']])
+ if 'tag' in route_config['reject']:
+ self.cli_set(base + ['reject', 'tag', route_config['reject']['tag']])
+
+ if {'blackhole', 'reject'} <= set(route_config):
+ # Can not use blackhole and reject at the same time
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base + ['blackhole'])
+ self.cli_delete(base + ['reject'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig('ip route', end='')
+
+ # Verify routes
+ for route, route_config in routes.items():
+ ip_ipv6 = 'ip'
+ if is_ipv6(route):
+ ip_ipv6 = 'ipv6'
+
+ if 'next_hop' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ tmp = f'{ip_ipv6} route {route} {next_hop}'
+ if 'interface' in next_hop_config:
+ tmp += ' ' + next_hop_config['interface']
+ if 'distance' in next_hop_config:
+ tmp += ' ' + next_hop_config['distance']
+ if 'vrf' in next_hop_config:
+ tmp += ' nexthop-vrf ' + next_hop_config['vrf']
+ if 'bfd' in next_hop_config:
+ tmp += ' bfd profile ' + bfd_profile
+ if 'bfd_source' in next_hop_config:
+ tmp += ' bfd multi-hop source ' + next_hop_config['bfd_source'] + ' profile ' + bfd_profile
+ if 'segments' in next_hop_config:
+ tmp += ' segments ' + next_hop_config['segments']
+
+ if 'disable' in next_hop_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if 'interface' in route_config:
+ for interface, interface_config in route_config['interface'].items():
+ tmp = f'{ip_ipv6} route {route} {interface}'
+ if 'interface' in interface_config:
+ tmp += ' ' + interface_config['interface']
+ if 'distance' in interface_config:
+ tmp += ' ' + interface_config['distance']
+ if 'vrf' in interface_config:
+ tmp += ' nexthop-vrf ' + interface_config['vrf']
+ if 'segments' in interface_config:
+ tmp += ' segments ' + interface_config['segments']
+
+ if 'disable' in interface_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if {'blackhole', 'reject'} <= set(route_config):
+ # Can not use blackhole and reject at the same time
+ # Config error validated above - skip this route
+ continue
+
+ if 'blackhole' in route_config:
+ tmp = f'{ip_ipv6} route {route} blackhole'
+ if 'tag' in route_config['blackhole']:
+ tmp += ' tag ' + route_config['blackhole']['tag']
+ if 'distance' in route_config['blackhole']:
+ tmp += ' ' + route_config['blackhole']['distance']
+
+ self.assertIn(tmp, frrconfig)
+
+ if 'reject' in route_config:
+ tmp = f'{ip_ipv6} route {route} reject'
+ if 'tag' in route_config['reject']:
+ tmp += ' tag ' + route_config['reject']['tag']
+ if 'distance' in route_config['reject']:
+ tmp += ' ' + route_config['reject']['distance']
+
+ self.assertIn(tmp, frrconfig)
+
+ def test_02_static_table(self):
+ for table in tables:
+ for route, route_config in routes.items():
+ route_type = 'route'
+ if is_ipv6(route):
+ route_type = 'route6'
+ base = base_path + ['table', table, route_type, route]
+
+ if 'next_hop' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ self.cli_set(base + ['next-hop', next_hop])
+ if 'disable' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'disable'])
+ if 'distance' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'distance', next_hop_config['distance']])
+ if 'interface' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'interface', next_hop_config['interface']])
+ if 'vrf' in next_hop_config:
+ self.cli_set(base + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']])
+
+
+ if 'interface' in route_config:
+ for interface, interface_config in route_config['interface'].items():
+ self.cli_set(base + ['interface', interface])
+ if 'disable' in interface_config:
+ self.cli_set(base + ['interface', interface, 'disable'])
+ if 'distance' in interface_config:
+ self.cli_set(base + ['interface', interface, 'distance', interface_config['distance']])
+ if 'vrf' in interface_config:
+ self.cli_set(base + ['interface', interface, 'vrf', interface_config['vrf']])
+
+ if 'blackhole' in route_config:
+ self.cli_set(base + ['blackhole'])
+ if 'distance' in route_config['blackhole']:
+ self.cli_set(base + ['blackhole', 'distance', route_config['blackhole']['distance']])
+ if 'tag' in route_config['blackhole']:
+ self.cli_set(base + ['blackhole', 'tag', route_config['blackhole']['tag']])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig('ip route', end='')
+
+ for table in tables:
+ # Verify routes
+ for route, route_config in routes.items():
+ ip_ipv6 = 'ip'
+ if is_ipv6(route):
+ ip_ipv6 = 'ipv6'
+
+ if 'next_hop' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ tmp = f'{ip_ipv6} route {route} {next_hop}'
+ if 'interface' in next_hop_config:
+ tmp += ' ' + next_hop_config['interface']
+ if 'distance' in next_hop_config:
+ tmp += ' ' + next_hop_config['distance']
+ if 'vrf' in next_hop_config:
+ tmp += ' nexthop-vrf ' + next_hop_config['vrf']
+
+ tmp += ' table ' + table
+ if 'disable' in next_hop_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if 'interface' in route_config:
+ for interface, interface_config in route_config['interface'].items():
+ tmp = f'{ip_ipv6} route {route} {interface}'
+ if 'interface' in interface_config:
+ tmp += ' ' + interface_config['interface']
+ if 'distance' in interface_config:
+ tmp += ' ' + interface_config['distance']
+ if 'vrf' in interface_config:
+ tmp += ' nexthop-vrf ' + interface_config['vrf']
+
+ tmp += ' table ' + table
+ if 'disable' in interface_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if 'blackhole' in route_config:
+ tmp = f'{ip_ipv6} route {route} blackhole'
+ if 'tag' in route_config['blackhole']:
+ tmp += ' tag ' + route_config['blackhole']['tag']
+ if 'distance' in route_config['blackhole']:
+ tmp += ' ' + route_config['blackhole']['distance']
+
+ tmp += ' table ' + table
+ self.assertIn(tmp, frrconfig)
+
+
+ def test_03_static_vrf(self):
+ # Create VRF instances and apply the static routes from above to FRR.
+ # Re-read the configured routes and match them if they are programmed
+ # properly. This also includes VRF leaking
+ vrfs = {
+ 'red' : { 'table' : '1000' },
+ 'green' : { 'table' : '2000' },
+ 'blue' : { 'table' : '3000' },
+ }
+
+ for vrf, vrf_config in vrfs.items():
+ vrf_base_path = ['vrf', 'name', vrf]
+ self.cli_set(vrf_base_path + ['table', vrf_config['table']])
+
+ for route, route_config in routes.items():
+ route_type = 'route'
+ if is_ipv6(route):
+ route_type = 'route6'
+ route_base_path = vrf_base_path + ['protocols', 'static', route_type, route]
+
+ if 'next_hop' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ self.cli_set(route_base_path + ['next-hop', next_hop])
+ if 'disable' in next_hop_config:
+ self.cli_set(route_base_path + ['next-hop', next_hop, 'disable'])
+ if 'distance' in next_hop_config:
+ self.cli_set(route_base_path + ['next-hop', next_hop, 'distance', next_hop_config['distance']])
+ if 'interface' in next_hop_config:
+ self.cli_set(route_base_path + ['next-hop', next_hop, 'interface', next_hop_config['interface']])
+ if 'vrf' in next_hop_config:
+ self.cli_set(route_base_path + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']])
+ if 'segments' in next_hop_config:
+ self.cli_set(route_base_path + ['next-hop', next_hop, 'segments', next_hop_config['segments']])
+
+ if 'interface' in route_config:
+ for interface, interface_config in route_config['interface'].items():
+ self.cli_set(route_base_path + ['interface', interface])
+ if 'disable' in interface_config:
+ self.cli_set(route_base_path + ['interface', interface, 'disable'])
+ if 'distance' in interface_config:
+ self.cli_set(route_base_path + ['interface', interface, 'distance', interface_config['distance']])
+ if 'vrf' in interface_config:
+ self.cli_set(route_base_path + ['interface', interface, 'vrf', interface_config['vrf']])
+ if 'segments' in interface_config:
+ self.cli_set(route_base_path + ['interface', interface, 'segments', interface_config['segments']])
+
+ if 'blackhole' in route_config:
+ self.cli_set(route_base_path + ['blackhole'])
+ if 'distance' in route_config['blackhole']:
+ self.cli_set(route_base_path + ['blackhole', 'distance', route_config['blackhole']['distance']])
+ if 'tag' in route_config['blackhole']:
+ self.cli_set(route_base_path + ['blackhole', 'tag', route_config['blackhole']['tag']])
+
+ # commit changes
+ self.cli_commit()
+
+ for vrf, vrf_config in vrfs.items():
+ tmp = get_interface_config(vrf)
+
+ # Compare VRF table ID
+ self.assertEqual(get_vrf_tableid(vrf), int(vrf_config['table']))
+ self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf')
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f'vrf {vrf}', frrconfig)
+
+ # Verify routes
+ for route, route_config in routes.items():
+ ip_ipv6 = 'ip'
+ if is_ipv6(route):
+ ip_ipv6 = 'ipv6'
+
+ if 'next_hop' in route_config:
+ for next_hop, next_hop_config in route_config['next_hop'].items():
+ tmp = f'{ip_ipv6} route {route} {next_hop}'
+ if 'interface' in next_hop_config:
+ tmp += ' ' + next_hop_config['interface']
+ if 'distance' in next_hop_config:
+ tmp += ' ' + next_hop_config['distance']
+ if 'vrf' in next_hop_config:
+ tmp += ' nexthop-vrf ' + next_hop_config['vrf']
+ if 'segments' in next_hop_config:
+ tmp += ' segments ' + next_hop_config['segments']
+
+ if 'disable' in next_hop_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if 'interface' in route_config:
+ for interface, interface_config in route_config['interface'].items():
+ tmp = f'{ip_ipv6} route {route} {interface}'
+ if 'interface' in interface_config:
+ tmp += ' ' + interface_config['interface']
+ if 'distance' in interface_config:
+ tmp += ' ' + interface_config['distance']
+ if 'vrf' in interface_config:
+ tmp += ' nexthop-vrf ' + interface_config['vrf']
+ if 'segments' in interface_config:
+ tmp += ' segments ' + interface_config['segments']
+
+ if 'disable' in interface_config:
+ self.assertNotIn(tmp, frrconfig)
+ else:
+ self.assertIn(tmp, frrconfig)
+
+ if 'blackhole' in route_config:
+ tmp = f'{ip_ipv6} route {route} blackhole'
+ if 'tag' in route_config['blackhole']:
+ tmp += ' tag ' + route_config['blackhole']['tag']
+ if 'distance' in route_config['blackhole']:
+ tmp += ' ' + route_config['blackhole']['distance']
+
+ self.assertIn(tmp, frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=True)
diff --git a/smoketest/scripts/cli/test_protocols_static_arp.py b/smoketest/scripts/cli/test_protocols_static_arp.py
new file mode 100644
index 0000000..7f80472
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_static_arp.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.process import cmd
+
+base_path = ['protocols', 'static', 'arp']
+interface = 'eth0'
+address = '192.0.2.1/24'
+
+class TestARP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestARP, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ # we need a L2 interface with a L3 address to properly configure ARP entries
+ cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', address])
+
+ @classmethod
+ def tearDownClass(cls):
+ # cleanuop L2 interface
+ cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', address])
+ cls.cli_commit(cls)
+
+ super(TestARP, cls).tearDownClass()
+
+ def tearDown(self):
+ # delete test config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_static_arp(self):
+ test_data = {
+ '192.0.2.10' : { 'mac' : '00:01:02:03:04:0a' },
+ '192.0.2.11' : { 'mac' : '00:01:02:03:04:0b' },
+ '192.0.2.12' : { 'mac' : '00:01:02:03:04:0c' },
+ '192.0.2.13' : { 'mac' : '00:01:02:03:04:0d' },
+ '192.0.2.14' : { 'mac' : '00:01:02:03:04:0e' },
+ '192.0.2.15' : { 'mac' : '00:01:02:03:04:0f' },
+ }
+
+ for host, host_config in test_data.items():
+ self.cli_set(base_path + ['interface', interface, 'address', host, 'mac', host_config['mac']])
+
+ self.cli_commit()
+
+ arp_table = json.loads(cmd('ip -j -4 neigh show'))
+ for host, host_config in test_data.items():
+ # As we search within a list of hosts we need to mark if it was
+ # found or not. This ensures all hosts from test_data are processed
+ found = False
+ for entry in arp_table:
+ # Other ARP entry - not related to this testcase
+ if entry['dst'] not in list(test_data):
+ continue
+
+ if entry['dst'] == host:
+ self.assertEqual(entry['lladdr'], host_config['mac'])
+ self.assertEqual(entry['dev'], interface)
+ found = True
+
+ if found == False:
+ print(entry)
+ self.assertTrue(found)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_static_multicast.py b/smoketest/scripts/cli/test_protocols_static_multicast.py
new file mode 100644
index 0000000..9fdda23
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_static_multicast.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+
+base_path = ['protocols', 'static', 'multicast']
+
+
+class TestProtocolsStaticMulticast(VyOSUnitTestSHIM.TestCase):
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ mroute = self.getFRRconfig('ip mroute', end='')
+ self.assertFalse(mroute)
+
+ def test_01_static_multicast(self):
+
+ self.cli_set(base_path + ['route', '224.202.0.0/24', 'next-hop', '224.203.0.1'])
+ self.cli_set(base_path + ['interface-route', '224.203.0.0/24', 'next-hop-interface', 'eth0'])
+
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig('ip mroute', end='')
+
+ self.assertIn('ip mroute 224.202.0.0/24 224.203.0.1', frrconfig)
+ self.assertIn('ip mroute 224.203.0.0/24 eth0', frrconfig)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py
new file mode 100644
index 0000000..b98c0e9
--- /dev/null
+++ b/smoketest/scripts/cli/test_qos.py
@@ -0,0 +1,859 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from json import loads
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import cmd
+
+base_path = ['qos']
+
+def get_tc_qdisc_json(interface) -> dict:
+ tmp = cmd(f'tc -detail -json qdisc show dev {interface}')
+ tmp = loads(tmp)
+ return next(iter(tmp))
+
+def get_tc_filter_json(interface, direction) -> list:
+ if direction not in ['ingress', 'egress']:
+ raise ValueError()
+ tmp = cmd(f'tc -detail -json filter show dev {interface} {direction}')
+ tmp = loads(tmp)
+ return tmp
+
+def get_tc_filter_details(interface, direction) -> list:
+ # json doesn't contain all params, such as mtu
+ if direction not in ['ingress', 'egress']:
+ raise ValueError()
+ tmp = cmd(f'tc -details filter show dev {interface} {direction}')
+ return tmp
+
+class TestQoS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestQoS, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ # We only test on physical interfaces and not VLAN (sub-)interfaces
+ cls._interfaces = []
+ if 'TEST_ETH' in os.environ:
+ tmp = os.environ['TEST_ETH'].split()
+ cls._interfaces = tmp
+ else:
+ for tmp in Section.interfaces('ethernet', vlan=False):
+ cls._interfaces.append(tmp)
+
+ def tearDown(self):
+ # delete testing SSH config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_01_cake(self):
+ bandwidth = 1000000
+ rtt = 200
+
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)])
+ self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)])
+ self.cli_set(base_path + ['policy', 'cake', policy_name, 'flow-isolation', 'dual-src-host'])
+
+ bandwidth += 1000000
+ rtt += 20
+
+ # commit changes
+ self.cli_commit()
+
+ bandwidth = 1000000
+ rtt = 200
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('cake', tmp['kind'])
+ # TC store rates as a 32-bit unsigned integer in bps (Bytes per second)
+ self.assertEqual(int(bandwidth *125), tmp['options']['bandwidth'])
+ # RTT internally is in us
+ self.assertEqual(int(rtt *1000), tmp['options']['rtt'])
+ self.assertEqual('dual-srchost', tmp['options']['flowmode'])
+ self.assertFalse(tmp['options']['ingress'])
+ self.assertFalse(tmp['options']['nat'])
+ self.assertTrue(tmp['options']['raw'])
+
+ bandwidth += 1000000
+ rtt += 20
+
+ def test_02_drop_tail(self):
+ queue_limit = 50
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'drop-tail', policy_name, 'queue-limit', str(queue_limit)])
+
+ queue_limit += 10
+
+ # commit changes
+ self.cli_commit()
+
+ queue_limit = 50
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('pfifo', tmp['kind'])
+ self.assertEqual(queue_limit, tmp['options']['limit'])
+
+ queue_limit += 10
+
+ def test_03_fair_queue(self):
+ hash_interval = 10
+ queue_limit = 5
+ policy_type = 'fair-queue'
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'hash-interval', str(hash_interval)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)])
+
+ hash_interval += 1
+ queue_limit += 1
+
+ # commit changes
+ self.cli_commit()
+
+ hash_interval = 10
+ queue_limit = 5
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('sfq', tmp['kind'])
+ self.assertEqual(hash_interval, tmp['options']['perturb'])
+ self.assertEqual(queue_limit, tmp['options']['limit'])
+
+ hash_interval += 1
+ queue_limit += 1
+
+ def test_04_fq_codel(self):
+ policy_type = 'fq-codel'
+ codel_quantum = 1500
+ flows = 512
+ interval = 100
+ queue_limit = 2048
+ target = 5
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'codel-quantum', str(codel_quantum)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'flows', str(flows)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'interval', str(interval)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'target', str(target)])
+
+ codel_quantum += 10
+ flows += 2
+ interval += 10
+ queue_limit += 512
+ target += 1
+
+ # commit changes
+ self.cli_commit()
+
+ codel_quantum = 1500
+ flows = 512
+ interval = 100
+ queue_limit = 2048
+ target = 5
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('fq_codel', tmp['kind'])
+ self.assertEqual(codel_quantum, tmp['options']['quantum'])
+ self.assertEqual(flows, tmp['options']['flows'])
+ self.assertEqual(queue_limit, tmp['options']['limit'])
+
+ # due to internal rounding we need to substract 1 from interval and target after converting to milliseconds
+ # configuration of:
+ # tc qdisc add dev eth0 root fq_codel quantum 1500 flows 512 interval 100ms limit 2048 target 5ms noecn
+ # results in: tc -j qdisc show dev eth0
+ # [{"kind":"fq_codel","handle":"8046:","root":true,"refcnt":3,"options":{"limit":2048,"flows":512,
+ # "quantum":1500,"target":4999,"interval":99999,"memory_limit":33554432,"drop_batch":64}}]
+ self.assertAlmostEqual(tmp['options']['interval'], interval *1000, delta=1)
+ self.assertAlmostEqual(tmp['options']['target'], target *1000 -1, delta=1)
+
+ codel_quantum += 10
+ flows += 2
+ interval += 10
+ queue_limit += 512
+ target += 1
+
+ def test_05_limiter(self):
+ qos_config = {
+ '1' : {
+ 'bandwidth' : '3000000',
+ 'exceed' : 'pipe',
+ 'burst' : '100Kb',
+ 'mtu' : '1600',
+ 'not-exceed' : 'continue',
+ 'priority': '15',
+ 'match4' : {
+ 'ssh' : { 'dport' : '22', },
+ },
+ },
+ '2' : {
+ 'bandwidth' : '1000000',
+ 'match6' : {
+ 'ssh' : { 'dport' : '22', },
+ },
+ },
+ }
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'egress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # set default bandwidth parameter for all remaining connections
+ self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'bandwidth', '500000'])
+ self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'burst', '200kb'])
+ self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'exceed', 'drop'])
+ self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'mtu', '3000'])
+ self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'not-exceed', 'ok'])
+
+ for qos_class, qos_class_config in qos_config.items():
+ qos_class_base = base_path + ['policy', 'limiter', policy_name, 'class', qos_class]
+
+ if 'match4' in qos_class_config:
+ for match, match_config in qos_class_config['match4'].items():
+ if 'dport' in match_config:
+ self.cli_set(qos_class_base + ['match', match, 'ip', 'destination', 'port', match_config['dport']])
+
+ if 'match6' in qos_class_config:
+ for match, match_config in qos_class_config['match6'].items():
+ if 'dport' in match_config:
+ self.cli_set(qos_class_base + ['match', match, 'ipv6', 'destination', 'port', match_config['dport']])
+
+ if 'bandwidth' in qos_class_config:
+ self.cli_set(qos_class_base + ['bandwidth', qos_class_config['bandwidth']])
+
+ if 'exceed' in qos_class_config:
+ self.cli_set(qos_class_base + ['exceed', qos_class_config['exceed']])
+
+ if 'not-exceed' in qos_class_config:
+ self.cli_set(qos_class_base + ['not-exceed', qos_class_config['not-exceed']])
+
+ if 'burst' in qos_class_config:
+ self.cli_set(qos_class_base + ['burst', qos_class_config['burst']])
+
+ if 'mtu' in qos_class_config:
+ self.cli_set(qos_class_base + ['mtu', qos_class_config['mtu']])
+
+ if 'priority' in qos_class_config:
+ self.cli_set(qos_class_base + ['priority', qos_class_config['priority']])
+
+
+ # commit changes
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ for filter in get_tc_filter_json(interface, 'ingress'):
+ # bail out early if filter has no attached action
+ if 'options' not in filter or 'actions' not in filter['options']:
+ continue
+
+ for qos_class, qos_class_config in qos_config.items():
+ # Every flowid starts with ffff and we encopde the class number after the colon
+ if 'flowid' not in filter['options'] or filter['options']['flowid'] != f'ffff:{qos_class}':
+ continue
+
+ ip_hdr_offset = 20
+ if 'match6' in qos_class_config:
+ ip_hdr_offset = 40
+
+ self.assertEqual(ip_hdr_offset, filter['options']['match']['off'])
+ if 'dport' in match_config:
+ dport = int(match_config['dport'])
+ self.assertEqual(f'{dport:x}', filter['options']['match']['value'])
+
+ tc_details = get_tc_filter_details(interface, 'ingress')
+ self.assertTrue('filter parent ffff: protocol all pref 20 u32 chain 0' in tc_details)
+ self.assertTrue('rate 1Gbit burst 15125b mtu 2Kb action drop overhead 0b linklayer ethernet' in tc_details)
+ self.assertTrue('filter parent ffff: protocol all pref 15 u32 chain 0' in tc_details)
+ self.assertTrue('rate 3Gbit burst 102000b mtu 1600b action pipe/continue overhead 0b linklayer ethernet' in tc_details)
+ self.assertTrue('rate 500Mbit burst 204687b mtu 3000b action drop overhead 0b linklayer ethernet' in tc_details)
+ self.assertTrue('filter parent ffff: protocol all pref 255 basic chain 0' in tc_details)
+
+ def test_06_network_emulator(self):
+ policy_type = 'network-emulator'
+
+ bandwidth = 1000000
+ corruption = 1
+ delay = 2
+ duplicate = 3
+ loss = 4
+ queue_limit = 5
+ reordering = 6
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'bandwidth', str(bandwidth)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'corruption', str(corruption)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'delay', str(delay)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'duplicate', str(duplicate)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'loss', str(loss)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)])
+ self.cli_set(base_path + ['policy', policy_type, policy_name, 'reordering', str(reordering)])
+
+ bandwidth += 1000000
+ corruption += 1
+ delay += 1
+ duplicate +=1
+ loss += 1
+ queue_limit += 1
+ reordering += 1
+
+ # commit changes
+ self.cli_commit()
+
+ bandwidth = 1000000
+ corruption = 1
+ delay = 2
+ duplicate = 3
+ loss = 4
+ queue_limit = 5
+ reordering = 6
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+ self.assertEqual('netem', tmp['kind'])
+
+ self.assertEqual(int(bandwidth *125), tmp['options']['rate']['rate'])
+ # values are in %
+ self.assertEqual(corruption/100, tmp['options']['corrupt']['corrupt'])
+ self.assertEqual(duplicate/100, tmp['options']['duplicate']['duplicate'])
+ self.assertEqual(loss/100, tmp['options']['loss-random']['loss'])
+ self.assertEqual(reordering/100, tmp['options']['reorder']['reorder'])
+ self.assertEqual(delay/1000, tmp['options']['delay']['delay'])
+
+ self.assertEqual(queue_limit, tmp['options']['limit'])
+
+ bandwidth += 1000000
+ corruption += 1
+ delay += 1
+ duplicate += 1
+ loss += 1
+ queue_limit += 1
+ reordering += 1
+
+ def test_07_priority_queue(self):
+ priorities = ['1', '2', '3', '4', '5']
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'priority-queue', policy_name, 'default', 'queue-limit', '10'])
+
+ for priority in priorities:
+ prio_base = base_path + ['policy', 'priority-queue', policy_name, 'class', priority]
+ self.cli_set(prio_base + ['match', f'prio-{priority}', 'ip', 'destination', 'port', str(1000 + int(priority))])
+
+ # commit changes
+ self.cli_commit()
+
+ def test_08_random_detect(self):
+ bandwidth = 5000
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'random-detect', policy_name, 'bandwidth', str(bandwidth)])
+
+ bandwidth += 1000
+
+ # commit changes
+ self.cli_commit()
+
+ bandwidth = 5000
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+ self.assertTrue('gred' in tmp.get('kind'))
+ self.assertEqual(8, len(tmp.get('options', {}).get('vqs')))
+ self.assertEqual(8, tmp.get('options', {}).get('dp_cnt'))
+ self.assertEqual(0, tmp.get('options', {}).get('dp_default'))
+ self.assertTrue(tmp.get('options', {}).get('grio'))
+
+ def test_09_rate_control(self):
+ bandwidth = 5000
+ burst = 20
+ latency = 5
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+ self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'bandwidth', str(bandwidth)])
+ self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'burst', str(burst)])
+ self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'latency', str(latency)])
+
+ bandwidth += 1000
+ burst += 5
+ latency += 1
+ # commit changes
+ self.cli_commit()
+
+ bandwidth = 5000
+ burst = 20
+ latency = 5
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+
+ self.assertEqual('tbf', tmp['kind'])
+ self.assertEqual(0, tmp['options']['mpu'])
+ # TC store rates as a 32-bit unsigned integer in bps (Bytes per second)
+ self.assertEqual(int(bandwidth * 125), tmp['options']['rate'])
+
+ bandwidth += 1000
+ burst += 5
+ latency += 1
+
+ def test_10_round_robin(self):
+ qos_config = {
+ '1' : {
+ 'match4' : {
+ 'ssh' : { 'dport' : '22', },
+ },
+ },
+ '2' : {
+ 'match6' : {
+ 'ssh' : { 'dport' : '22', },
+ },
+ },
+ }
+
+ first = True
+ for interface in self._interfaces:
+ policy_name = f'qos-policy-{interface}'
+
+ if first:
+ self.cli_set(base_path + ['interface', interface, 'ingress', policy_name])
+ # verify() - selected QoS policy on interface only supports egress
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name])
+ first = False
+
+ self.cli_set(base_path + ['interface', interface, 'egress', policy_name])
+
+ for qos_class, qos_class_config in qos_config.items():
+ qos_class_base = base_path + ['policy', 'round-robin', policy_name, 'class', qos_class]
+
+ if 'match4' in qos_class_config:
+ for match, match_config in qos_class_config['match4'].items():
+ if 'dport' in match_config:
+ self.cli_set(qos_class_base + ['match', match, 'ip', 'destination', 'port', match_config['dport']])
+
+ if 'match6' in qos_class_config:
+ for match, match_config in qos_class_config['match6'].items():
+ if 'dport' in match_config:
+ self.cli_set(qos_class_base + ['match', match, 'ipv6', 'destination', 'port', match_config['dport']])
+
+
+ # commit changes
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ tmp = get_tc_qdisc_json(interface)
+ self.assertEqual('drr', tmp['kind'])
+
+ for filter in get_tc_filter_json(interface, 'ingress'):
+ # bail out early if filter has no attached action
+ if 'options' not in filter or 'actions' not in filter['options']:
+ continue
+
+ for qos_class, qos_class_config in qos_config.items():
+ # Every flowid starts with ffff and we encopde the class number after the colon
+ if 'flowid' not in filter['options'] or filter['options']['flowid'] != f'ffff:{qos_class}':
+ continue
+
+ ip_hdr_offset = 20
+ if 'match6' in qos_class_config:
+ ip_hdr_offset = 40
+
+ self.assertEqual(ip_hdr_offset, filter['options']['match']['off'])
+ if 'dport' in match_config:
+ dport = int(match_config['dport'])
+ self.assertEqual(f'{dport:x}', filter['options']['match']['value'])
+
+ def test_11_shaper(self):
+ bandwidth = 250
+ default_bandwidth = 20
+ default_ceil = 30
+ class_bandwidth = 50
+ class_ceil = 80
+ dst_address = '192.0.2.8/32'
+
+ for interface in self._interfaces:
+ shaper_name = f'qos-shaper-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'{bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'ceiling', f'{default_ceil}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-type', 'fair-queue'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'bandwidth', f'{class_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'ceiling', f'{class_ceil}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ip', 'destination', 'address', dst_address])
+
+ bandwidth += 1
+ default_bandwidth += 1
+ default_ceil += 1
+ class_bandwidth += 1
+ class_ceil += 1
+
+ # commit changes
+ self.cli_commit()
+
+ bandwidth = 250
+ default_bandwidth = 20
+ default_ceil = 30
+ class_bandwidth = 50
+ class_ceil = 80
+
+ for interface in self._interfaces:
+ config_entries = (
+ f'root rate {bandwidth}Mbit ceil {bandwidth}Mbit',
+ f'prio 0 rate {class_bandwidth}Mbit ceil {class_ceil}Mbit',
+ f'prio 7 rate {default_bandwidth}Mbit ceil {default_ceil}Mbit'
+ )
+
+ output = cmd(f'tc class show dev {interface}')
+
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ bandwidth += 1
+ default_bandwidth += 1
+ default_ceil += 1
+ class_bandwidth += 1
+ class_ceil += 1
+
+ def test_12_shaper_with_red_queue(self):
+ bandwidth = 100
+ default_bandwidth = 100
+ default_burst = 100
+ interface = self._interfaces[0]
+ class_bandwidth = 50
+ dst_address = '192.0.2.8/32'
+
+ shaper_name = f'qos-shaper-{interface}'
+ self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'{bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}%'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'burst', f'{default_burst}'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-type', 'random-detect'])
+
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'bandwidth', f'{class_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'match', '10', 'ip', 'destination', 'address', dst_address])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'queue-type', 'random-detect'])
+
+ # commit changes
+ self.cli_commit()
+
+ # check root htb config
+ output = cmd(f'tc class show dev {interface}')
+
+ config_entries = (
+ f'prio 0 rate {class_bandwidth}Mbit ceil 50Mbit burst 15Kb', # specified class
+ f'prio 7 rate {default_bandwidth}Mbit ceil 100Mbit burst {default_burst}b', # default class
+ )
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ output = cmd(f'tc -d qdisc show dev {interface}')
+ config_entries = (
+ 'qdisc red', # use random detect
+ 'limit 72Kb min 9Kb max 18Kb ewma 3 probability 0.1', # default config for random detect
+ )
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ # test random detect queue params
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-limit', '1024'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'average-packet', '1024'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'maximum-threshold', '32'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'minimum-threshold', '16'])
+
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'queue-limit', '1024'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'average-packet', '512'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'maximum-threshold', '32'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'minimum-threshold', '16'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '2', 'mark-probability', '20'])
+
+ self.cli_commit()
+
+ output = cmd(f'tc -d qdisc show dev {interface}')
+ config_entries = (
+ 'qdisc red', # use random detect
+ 'limit 1Mb min 16Kb max 32Kb ewma 3 probability 0.1', # default config for random detect
+ 'limit 512Kb min 8Kb max 16Kb ewma 3 probability 0.05', # class config for random detect
+ )
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ def test_13_shaper_delete_only_rule(self):
+ default_bandwidth = 100
+ default_burst = 100
+ interface = self._interfaces[0]
+ class_bandwidth = 50
+ class_ceiling = 5
+ src_address = '10.1.1.0/24'
+
+ shaper_name = f'qos-shaper-{interface}'
+ self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'10mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'burst', f'{default_burst}'])
+
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'bandwidth', f'{class_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'ceiling', f'{class_ceiling}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'match', 'ADDRESS30', 'ip', 'source', 'address', src_address])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'match', 'ADDRESS30', 'description', 'smoketest'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'priority', '5'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'queue-type', 'fair-queue'])
+
+ # commit changes
+ self.cli_commit()
+ # check root htb config
+ output = cmd(f'tc class show dev {interface}')
+
+ config_entries = (
+ f'prio 5 rate {class_bandwidth}Mbit ceil {class_ceiling}Mbit burst 15Kb', # specified class
+ f'prio 7 rate {default_bandwidth}Mbit ceil 100Mbit burst {default_burst}b', # default class
+ )
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ self.assertTrue('' != cmd(f'tc filter show dev {interface}'))
+ # self.cli_delete(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'match', 'ADDRESS30'])
+ self.cli_delete(base_path + ['policy', 'shaper', shaper_name, 'class', '30', 'match', 'ADDRESS30', 'ip', 'source', 'address', src_address])
+ self.cli_commit()
+ self.assertEqual('', cmd(f'tc filter show dev {interface}'))
+
+ def test_14_policy_limiter_marked_traffic(self):
+ policy_name = 'smoke_test'
+ base_policy_path = ['qos', 'policy', 'limiter', policy_name]
+
+ self.cli_set(['qos', 'interface', self._interfaces[0], 'ingress', policy_name])
+ self.cli_set(base_policy_path + ['class', '100', 'bandwidth', '20gbit'])
+ self.cli_set(base_policy_path + ['class', '100', 'burst', '3760k'])
+ self.cli_set(base_policy_path + ['class', '100', 'match', 'INTERNAL', 'mark', '100'])
+ self.cli_set(base_policy_path + ['class', '100', 'priority', '20'])
+ self.cli_set(base_policy_path + ['default', 'bandwidth', '1gbit'])
+ self.cli_set(base_policy_path + ['default', 'burst', '125000000b'])
+ self.cli_commit()
+
+ tc_filters = cmd(f'tc filter show dev {self._interfaces[0]} ingress')
+ # class 100
+ self.assertIn('filter parent ffff: protocol all pref 20 fw chain 0', tc_filters)
+ self.assertIn('action order 1: police 0x1 rate 20Gbit burst 3847500b mtu 2Kb action drop overhead 0b', tc_filters)
+ # default
+ self.assertIn('filter parent ffff: protocol all pref 255 basic chain 0', tc_filters)
+ self.assertIn('action order 1: police 0x2 rate 1Gbit burst 125000000b mtu 2Kb action drop overhead 0b', tc_filters)
+
+ def test_15_traffic_match_group(self):
+ interface = self._interfaces[0]
+ self.cli_set(['qos', 'interface', interface, 'egress', 'VyOS-HTB'])
+ base_policy_path = ['qos', 'policy', 'shaper', 'VyOS-HTB']
+
+ #old syntax
+ self.cli_set(base_policy_path + ['bandwidth', '100mbit'])
+ self.cli_set(base_policy_path + ['class', '10', 'bandwidth', '40%'])
+ self.cli_set(base_policy_path + ['class', '10', 'match', 'AF11', 'ip', 'dscp', 'AF11'])
+ self.cli_set(base_policy_path + ['class', '10', 'match', 'AF41', 'ip', 'dscp', 'AF41'])
+ self.cli_set(base_policy_path + ['class', '10', 'match', 'AF43', 'ip', 'dscp', 'AF43'])
+ self.cli_set(base_policy_path + ['class', '10', 'match', 'CS4', 'ip', 'dscp', 'CS4'])
+ self.cli_set(base_policy_path + ['class', '10', 'priority', '1'])
+ self.cli_set(base_policy_path + ['class', '10', 'queue-type', 'fair-queue'])
+ self.cli_set(base_policy_path + ['class', '20', 'bandwidth', '30%'])
+ self.cli_set(base_policy_path + ['class', '20', 'match', 'EF', 'ip', 'dscp', 'EF'])
+ self.cli_set(base_policy_path + ['class', '20', 'match', 'CS5', 'ip', 'dscp', 'CS5'])
+ self.cli_set(base_policy_path + ['class', '20', 'priority', '2'])
+ self.cli_set(base_policy_path + ['class', '20', 'queue-type', 'fair-queue'])
+ self.cli_set(base_policy_path + ['default', 'bandwidth', '20%'])
+ self.cli_set(base_policy_path + ['default', 'queue-type', 'fair-queue'])
+ self.cli_commit()
+
+ tc_filters_old = cmd(f'tc -details filter show dev {interface}')
+ self.assertIn('match 00280000/00ff0000', tc_filters_old)
+ self.assertIn('match 00880000/00ff0000', tc_filters_old)
+ self.assertIn('match 00980000/00ff0000', tc_filters_old)
+ self.assertIn('match 00800000/00ff0000', tc_filters_old)
+ self.assertIn('match 00a00000/00ff0000', tc_filters_old)
+ self.assertIn('match 00b80000/00ff0000', tc_filters_old)
+ # delete config by old syntax
+ self.cli_delete(base_policy_path)
+ self.cli_delete(['qos', 'interface', interface, 'egress', 'VyOS-HTB'])
+ self.cli_commit()
+ self.assertEqual('', cmd(f'tc -s filter show dev {interface}'))
+
+ self.cli_set(['qos', 'interface', interface, 'egress', 'VyOS-HTB'])
+ # prepare traffic match group
+ self.cli_set(['qos', 'traffic-match-group', 'VOICE', 'description', 'voice shaper'])
+ self.cli_set(['qos', 'traffic-match-group', 'VOICE', 'match', 'EF', 'ip', 'dscp', 'EF'])
+ self.cli_set(['qos', 'traffic-match-group', 'VOICE', 'match', 'CS5', 'ip', 'dscp', 'CS5'])
+
+ self.cli_set(['qos', 'traffic-match-group', 'REAL_TIME_COMMON', 'description', 'real time common filters'])
+ self.cli_set(['qos', 'traffic-match-group', 'REAL_TIME_COMMON', 'match', 'AF43', 'ip', 'dscp', 'AF43'])
+ self.cli_set(['qos', 'traffic-match-group', 'REAL_TIME_COMMON', 'match', 'CS4', 'ip', 'dscp', 'CS4'])
+
+ self.cli_set(['qos', 'traffic-match-group', 'REAL_TIME', 'description', 'real time shaper'])
+ self.cli_set(['qos', 'traffic-match-group', 'REAL_TIME', 'match', 'AF41', 'ip', 'dscp', 'AF41'])
+ self.cli_set(['qos', 'traffic-match-group', 'REAL_TIME', 'match-group', 'REAL_TIME_COMMON'])
+
+ # new syntax
+ self.cli_set(base_policy_path + ['bandwidth', '100mbit'])
+ self.cli_set(base_policy_path + ['class', '10', 'bandwidth', '40%'])
+ self.cli_set(base_policy_path + ['class', '10', 'match', 'AF11', 'ip', 'dscp', 'AF11'])
+ self.cli_set(base_policy_path + ['class', '10', 'match-group', 'REAL_TIME'])
+ self.cli_set(base_policy_path + ['class', '10', 'priority', '1'])
+ self.cli_set(base_policy_path + ['class', '10', 'queue-type', 'fair-queue'])
+ self.cli_set(base_policy_path + ['class', '20', 'bandwidth', '30%'])
+ self.cli_set(base_policy_path + ['class', '20', 'match-group', 'VOICE'])
+ self.cli_set(base_policy_path + ['class', '20', 'priority', '2'])
+ self.cli_set(base_policy_path + ['class', '20', 'queue-type', 'fair-queue'])
+ self.cli_set(base_policy_path + ['default', 'bandwidth', '20%'])
+ self.cli_set(base_policy_path + ['default', 'queue-type', 'fair-queue'])
+ self.cli_commit()
+
+ self.assertEqual(tc_filters_old, cmd(f'tc -details filter show dev {interface}'))
+
+ def test_16_wrong_traffic_match_group(self):
+ interface = self._interfaces[0]
+ self.cli_set(['qos', 'interface', interface])
+
+ # Can not use both IPv6 and IPv4 in one match
+ self.cli_set(['qos', 'traffic-match-group', '1', 'match', 'one', 'ip', 'dscp', 'EF'])
+ self.cli_set(['qos', 'traffic-match-group', '1', 'match', 'one', 'ipv6', 'dscp', 'EF'])
+ with self.assertRaises(ConfigSessionError) as e:
+ self.cli_commit()
+
+ # check contain itself, should commit success
+ self.cli_delete(['qos', 'traffic-match-group', '1', 'match', 'one', 'ipv6'])
+ self.cli_set(['qos', 'traffic-match-group', '1', 'match-group', '1'])
+ self.cli_commit()
+
+ # check cycle dependency, should commit success
+ self.cli_set(['qos', 'traffic-match-group', '1', 'match-group', '3'])
+ self.cli_set(['qos', 'traffic-match-group', '2', 'match', 'one', 'ip', 'dscp', 'CS4'])
+ self.cli_set(['qos', 'traffic-match-group', '2', 'match-group', '1'])
+
+ self.cli_set(['qos', 'traffic-match-group', '3', 'match', 'one', 'ipv6', 'dscp', 'CS4'])
+ self.cli_set(['qos', 'traffic-match-group', '3', 'match-group', '2'])
+ self.cli_commit()
+
+ # inherit from non exist group, should commit success with warning
+ self.cli_set(['qos', 'traffic-match-group', '3', 'match-group', 'unexpected'])
+ self.cli_commit()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_broadcast-relay.py b/smoketest/scripts/cli/test_service_broadcast-relay.py
new file mode 100644
index 0000000..8790186
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_broadcast-relay.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from psutil import process_iter
+from vyos.configsession import ConfigSessionError
+
+base_path = ['service', 'broadcast-relay']
+
+class TestServiceBroadcastRelay(VyOSUnitTestSHIM.TestCase):
+ _address1 = '192.0.2.1/24'
+ _address2 = '192.0.2.1/24'
+
+ def setUp(self):
+ self.cli_set(['interfaces', 'dummy', 'dum1001', 'address', self._address1])
+ self.cli_set(['interfaces', 'dummy', 'dum1002', 'address', self._address2])
+
+ def tearDown(self):
+ self.cli_delete(['interfaces', 'dummy', 'dum1001'])
+ self.cli_delete(['interfaces', 'dummy', 'dum1002'])
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_broadcast_relay_service(self):
+ ids = range(1, 5)
+ for id in ids:
+ base = base_path + ['id', str(id)]
+ self.cli_set(base + ['description', 'vyos'])
+ self.cli_set(base + ['port', str(10000 + id)])
+
+ # check validate() - two interfaces must be present
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base + ['interface', 'dum1001'])
+ self.cli_set(base + ['interface', 'dum1002'])
+ self.cli_set(base + ['address', self._address1.split('/')[0]])
+
+ self.cli_commit()
+
+ for id in ids:
+ # check if process is running
+ running = False
+ for p in process_iter():
+ if "udp-broadcast-relay" in p.name():
+ if p.cmdline()[3] == str(id):
+ running = True
+ break
+ self.assertTrue(running)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dhcp-relay.py b/smoketest/scripts/cli/test_service_dhcp-relay.py
new file mode 100644
index 0000000..59c4b59
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_dhcp-relay.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'dhcrelay'
+RELAY_CONF = '/run/dhcp-relay/dhcrelay.conf'
+base_path = ['service', 'dhcp-relay']
+
+class TestServiceDHCPRelay(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_relay_default(self):
+ max_size = '800'
+ hop_count = '20'
+ agents_packets = 'append'
+ servers = ['192.0.2.1', '192.0.2.2']
+
+ self.cli_set(base_path + ['interface', 'lo'])
+ # check validate() - DHCP relay does not support the loopback interface
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', 'lo'])
+
+ # activate DHCP relay on all ethernet interfaces
+ for tmp in Section.interfaces("ethernet"):
+ self.cli_set(base_path + ['interface', tmp])
+
+ # check validate() - No DHCP relay server(s) configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ self.cli_set(base_path + ['relay-options', 'max-size', max_size])
+ self.cli_set(base_path + ['relay-options', 'hop-count', hop_count])
+ self.cli_set(base_path + ['relay-options', 'relay-agents-packets', agents_packets])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ config = read_file(RELAY_CONF)
+
+ # Test configured relay interfaces
+ for tmp in Section.interfaces("ethernet"):
+ self.assertIn(f'-i {tmp}', config)
+
+ # Test relay servers
+ for server in servers:
+ self.assertIn(f' {server}', config)
+
+ # Test max-size
+ self.assertIn(f'-A {max_size}', config)
+ # Hop count
+ self.assertIn(f'-c {hop_count}', config)
+ # relay-agents-packets
+ self.assertIn(f'-a -m {agents_packets}', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_relay_interfaces(self):
+ max_size = '800'
+ hop_count = '20'
+ agents_packets = 'append'
+ servers = ['192.0.2.1', '192.0.2.2']
+ listen_iface = 'eth0'
+ up_iface = 'eth1'
+
+ self.cli_set(base_path + ['interface', up_iface])
+ self.cli_set(base_path + ['listen-interface', listen_iface])
+ # check validate() - backward interface plus listen_interface
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface'])
+
+ self.cli_set(base_path + ['upstream-interface', up_iface])
+
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ config = read_file(RELAY_CONF)
+
+ # Test configured relay interfaces
+ self.assertIn(f'-id {listen_iface}', config)
+ self.assertIn(f'-iu {up_iface}', config)
+
+ # Test relay servers
+ for server in servers:
+ self.assertIn(f' {server}', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
+
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
new file mode 100644
index 0000000..46c4e25
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -0,0 +1,830 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from json import loads
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.template import inc_ip
+from vyos.template import dec_ip
+
+PROCESS_NAME = 'kea-dhcp4'
+CTRL_PROCESS_NAME = 'kea-ctrl-agent'
+KEA4_CONF = '/run/kea/kea-dhcp4.conf'
+KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket'
+base_path = ['service', 'dhcp-server']
+interface = 'dum8765'
+subnet = '192.0.2.0/25'
+router = inc_ip(subnet, 1)
+dns_1 = inc_ip(subnet, 2)
+dns_2 = inc_ip(subnet, 3)
+domain_name = 'vyos.net'
+
+class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceDHCPServer, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, base_path)
+
+ cidr_mask = subnet.split('/')[-1]
+ cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', interface])
+ super(TestServiceDHCPServer, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def walk_path(self, obj, path):
+ current = obj
+
+ for i, key in enumerate(path):
+ if isinstance(key, str):
+ self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}')
+ self.assertTrue(key in current, msg=f'Failed path: {path}')
+ elif isinstance(key, int):
+ self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
+ self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
+ else:
+ assert False, "Invalid type"
+
+ current = current[key]
+
+ return current
+
+ def verify_config_object(self, obj, path, value):
+ base_obj = self.walk_path(obj, path)
+ self.assertTrue(isinstance(base_obj, list))
+ self.assertTrue(any(True for v in base_obj if v == value))
+
+ def verify_config_value(self, obj, path, key, value):
+ base_obj = self.walk_path(obj, path)
+ if isinstance(base_obj, list):
+ self.assertTrue(any(True for v in base_obj if key in v and v[key] == value))
+ elif isinstance(base_obj, dict):
+ self.assertTrue(key in base_obj)
+ self.assertEqual(base_obj[key], value)
+
+ def test_dhcp_single_pool_range(self):
+ shared_net_name = 'SMOKE-1'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+ range_1_start = inc_ip(subnet, 40)
+ range_1_stop = inc_ip(subnet, 50)
+
+ self.cli_set(base_path + ['listen-interface', interface])
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['ignore-client-id'])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+
+ # check validate() - No DHCP address range or active static-mapping set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+ self.cli_set(pool + ['range', '1', 'start', range_1_start])
+ self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface])
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'match-client-id', False)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_1_start} - {range_1_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_single_pool_options(self):
+ shared_net_name = 'SMOKE-0815'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+ smtp_server = '1.2.3.4'
+ time_server = '4.3.2.1'
+ tftp_server = 'tftp.vyos.io'
+ search_domains = ['foo.vyos.net', 'bar.vyos.net']
+ bootfile_name = 'vyos'
+ bootfile_server = '192.0.2.1'
+ wpad = 'http://wpad.vyos.io/foo/bar'
+ server_identifier = bootfile_server
+ ipv6_only_preferred = '300'
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+ self.cli_set(pool + ['option', 'ip-forwarding'])
+ self.cli_set(pool + ['option', 'smtp-server', smtp_server])
+ self.cli_set(pool + ['option', 'pop-server', smtp_server])
+ self.cli_set(pool + ['option', 'time-server', time_server])
+ self.cli_set(pool + ['option', 'tftp-server-name', tftp_server])
+ for search in search_domains:
+ self.cli_set(pool + ['option', 'domain-search', search])
+ self.cli_set(pool + ['option', 'bootfile-name', bootfile_name])
+ self.cli_set(pool + ['option', 'bootfile-server', bootfile_server])
+ self.cli_set(pool + ['option', 'wpad-url', wpad])
+ self.cli_set(pool + ['option', 'server-identifier', server_identifier])
+
+ self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
+ self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred])
+ self.cli_set(pool + ['option', 'time-zone', 'Europe/London'])
+
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-search', 'data': ', '.join(search_domains)})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'pop-server', 'data': smtp_server})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'smtp-server', 'data': smtp_server})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'time-servers', 'data': time_server})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'dhcp-server-identifier', 'data': server_identifier})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'tftp-server-name', 'data': tftp_server})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'wpad-url', 'data': wpad})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'v6-only-preferred', 'data': ipv6_only_preferred})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'ip-forwarding', 'data': "true"})
+
+ # Time zone
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'tcode', 'data': 'Europe/London'})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_single_pool_options_scoped(self):
+ shared_net_name = 'SMOKE-2'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ range_router = inc_ip(subnet, 5)
+ range_dns_1 = inc_ip(subnet, 6)
+ range_dns_2 = inc_ip(subnet, 7)
+
+ shared_network = base_path + ['shared-network-name', shared_net_name]
+ pool = shared_network + ['subnet', subnet]
+
+ self.cli_set(pool + ['subnet-id', '1'])
+
+ # we use the first subnet IP address as default gateway
+ self.cli_set(shared_network + ['option', 'default-router', router])
+ self.cli_set(shared_network + ['option', 'name-server', dns_1])
+ self.cli_set(shared_network + ['option', 'name-server', dns_2])
+ self.cli_set(shared_network + ['option', 'domain-name', domain_name])
+
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+ self.cli_set(pool + ['range', '0', 'option', 'default-router', range_router])
+ self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_1])
+ self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_2])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+
+ # Verify shared-network options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ # Verify range options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
+ {'name': 'routers', 'data': range_router})
+
+ # Verify pool
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}')
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_single_pool_static_mapping(self):
+ shared_net_name = 'SMOKE-2'
+ domain_name = 'private'
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+
+ # check validate() - No DHCP address range or active static-mapping set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ client_base = 10
+ for client in ['client1', 'client2', 'client3']:
+ mac = '00:50:00:00:00:{}'.format(client_base)
+ self.cli_set(pool + ['static-mapping', client, 'mac', mac])
+ self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
+ client_base += 1
+
+ # cannot have both mac-address and duid set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11'])
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'client1', 'duid'])
+
+ # cannot have mappings with duplicate IP addresses
+ self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:fe:ff'])
+ self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ # Should allow disabled duplicate
+ self.cli_set(pool + ['static-mapping', 'dupe1', 'disable'])
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'dupe1'])
+
+ # cannot have mappings with duplicate MAC addresses
+ self.cli_set(pool + ['static-mapping', 'dupe2', 'mac', '00:50:00:00:00:10'])
+ self.cli_set(pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'dupe2'])
+
+
+ # cannot have mappings with duplicate MAC addresses
+ self.cli_set(pool + ['static-mapping', 'dupe3', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01'])
+ self.cli_set(pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)])
+ self.cli_set(pool + ['static-mapping', 'dupe4', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01'])
+ self.cli_set(pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'dupe3'])
+ self.cli_delete(pool + ['static-mapping', 'dupe4'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ client_base = 10
+ for client in ['client1', 'client2', 'client3']:
+ mac = '00:50:00:00:00:{}'.format(client_base)
+ ip = inc_ip(subnet, client_base)
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip})
+
+ client_base += 1
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_multiple_pools(self):
+ lease_time = '14400'
+
+ for network in ['0', '1', '2', '3']:
+ shared_net_name = f'VyOS-SMOKETEST-{network}'
+ subnet = f'192.0.{network}.0/24'
+ router = inc_ip(subnet, 1)
+ dns_1 = inc_ip(subnet, 2)
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+ range_1_start = inc_ip(subnet, 30)
+ range_1_stop = inc_ip(subnet, 40)
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', str(int(network) + 1)])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+ self.cli_set(pool + ['lease', lease_time])
+
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+ self.cli_set(pool + ['range', '1', 'start', range_1_start])
+ self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
+
+ client_base = 60
+ for client in ['client1', 'client2', 'client3', 'client4']:
+ mac = '02:50:00:00:00:{}'.format(client_base)
+ self.cli_set(pool + ['static-mapping', client, 'mac', mac])
+ self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
+ client_base += 1
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ for network in ['0', '1', '2', '3']:
+ shared_net_name = f'VyOS-SMOKETEST-{network}'
+ subnet = f'192.0.{network}.0/24'
+ router = inc_ip(subnet, 1)
+ dns_1 = inc_ip(subnet, 2)
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+ range_1_start = inc_ip(subnet, 30)
+ range_1_stop = inc_ip(subnet, 40)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'id', int(network) + 1)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time))
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time))
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': dns_1})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
+ {'pool': f'{range_1_start} - {range_1_stop}'})
+
+ client_base = 60
+ for client in ['client1', 'client2', 'client3', 'client4']:
+ mac = '02:50:00:00:00:{}'.format(client_base)
+ ip = inc_ip(subnet, client_base)
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'],
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip})
+
+ client_base += 1
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_exclude_not_in_range(self):
+ # T3180: verify else path when slicing DHCP ranges and exclude address
+ # is not part of the DHCP range
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['exclude', router])
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_exclude_in_range(self):
+ # T3180: verify else path when slicing DHCP ranges and exclude address
+ # is not part of the DHCP range
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 100)
+
+ # the DHCP exclude addresse is blanked out of the range which is done
+ # by slicing one range into two ranges
+ exclude_addr = inc_ip(range_0_start, 20)
+ range_0_stop_excl = dec_ip(exclude_addr, 1)
+ range_0_start_excl = inc_ip(exclude_addr, 1)
+
+ pool = base_path + ['shared-network-name', 'EXCLUDE-TEST-2', 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['exclude', exclude_addr])
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop_excl}'})
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start_excl} - {range_0_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_relay_server(self):
+ # Listen on specific address and return DHCP leases from a non
+ # directly connected pool
+ self.cli_set(base_path + ['listen-address', router])
+
+ relay_subnet = '10.0.0.0/16'
+ relay_router = inc_ip(relay_subnet, 1)
+
+ range_0_start = '10.0.1.0'
+ range_0_stop = '10.0.250.255'
+
+ pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['option', 'default-router', relay_router])
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}'])
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': relay_router})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_dhcp_high_availability(self):
+ shared_net_name = 'FAILOVER'
+ failover_name = 'VyOS-Failover'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['option', 'default-router', router])
+
+ # check validate() - No DHCP address range or active static-mapping set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ # failover
+ failover_local = router
+ failover_remote = inc_ip(router, 1)
+
+ self.cli_set(base_path + ['high-availability', 'source-address', failover_local])
+ self.cli_set(base_path + ['high-availability', 'name', failover_name])
+ self.cli_set(base_path + ['high-availability', 'remote', failover_remote])
+ self.cli_set(base_path + ['high-availability', 'status', 'primary'])
+ ## No mode defined -> its active-active mode by default
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ # Verify failover
+ self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
+ {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True})
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
+ {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'secondary', 'auto-failover': True})
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
+
+ def test_dhcp_high_availability_standby(self):
+ shared_net_name = 'FAILOVER'
+ failover_name = 'VyOS-Failover'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ # failover
+ failover_local = router
+ failover_remote = inc_ip(router, 1)
+
+ self.cli_set(base_path + ['high-availability', 'source-address', failover_local])
+ self.cli_set(base_path + ['high-availability', 'name', failover_name])
+ self.cli_set(base_path + ['high-availability', 'remote', failover_remote])
+ self.cli_set(base_path + ['high-availability', 'status', 'secondary'])
+ self.cli_set(base_path + ['high-availability', 'mode', 'active-passive'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ # Verify failover
+ self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
+ {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'standby', 'auto-failover': True})
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
+ {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'primary', 'auto-failover': True})
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
+
+ def test_dhcp_on_interface_with_vrf(self):
+ self.cli_set(['interfaces', 'ethernet', 'eth1', 'address', '10.1.1.1/30'])
+ self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
+ self.cli_set(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf', 'SMOKE-DHCP'])
+ self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'protocols', 'static', 'route', '10.1.10.0/24', 'next-hop', '10.1.1.2'])
+ self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'table', '1000'])
+ self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'subnet-id', '1'])
+ self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'default-router', '10.1.10.1'])
+ self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'name-server', '1.1.1.1'])
+ self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'start', '10.1.10.10'])
+ self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'stop', '10.1.10.20'])
+ self.cli_set(base_path + ['listen-address', '10.1.1.1'])
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1'])
+
+ self.cli_delete(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
+ self.cli_delete(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf'])
+ self.cli_delete(['vrf', 'name', 'SMOKE-DHCP'])
+ self.cli_commit()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dhcpv6-relay.py b/smoketest/scripts/cli/test_service_dhcpv6-relay.py
new file mode 100644
index 0000000..e634a01
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_dhcpv6-relay.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'dhcrelay'
+RELAY_CONF = '/run/dhcp-relay/dhcrelay6.conf'
+base_path = ['service', 'dhcpv6-relay']
+
+upstream_if = 'eth0'
+upstream_if_addr = '2001:db8::1/64'
+listen_addr = '2001:db8:ffff::1/64'
+interfaces = []
+
+class TestServiceDHCPv6Relay(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceDHCPv6Relay, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ for tmp in Section.interfaces('ethernet', vlan=False):
+ interfaces.append(tmp)
+ listen = listen_addr
+ if tmp == upstream_if:
+ listen = upstream_if_addr
+ cls.cli_set(cls, ['interfaces', 'ethernet', tmp, 'address', listen])
+
+ @classmethod
+ def tearDownClass(cls):
+ for tmp in interfaces:
+ listen = listen_addr
+ if tmp == upstream_if:
+ listen = upstream_if_addr
+ cls.cli_delete(cls, ['interfaces', 'ethernet', tmp, 'address', listen])
+
+ super(TestServiceDHCPv6Relay, cls).tearDownClass()
+
+ def test_relay_default(self):
+ dhcpv6_server = '2001:db8::ffff'
+ hop_count = '20'
+
+ self.cli_set(base_path + ['use-interface-id-option'])
+ self.cli_set(base_path + ['max-hop-count', hop_count])
+
+ # check validate() - Must set at least one listen and upstream
+ # interface addresses.
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['upstream-interface', upstream_if, 'address', dhcpv6_server])
+
+ # check validate() - Must set at least one listen and upstream
+ # interface addresses.
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # add listener on all ethernet interfaces except the upstream interface
+ for tmp in interfaces:
+ if tmp == upstream_if:
+ continue
+ self.cli_set(base_path + ['listen-interface', tmp, 'address', listen_addr.split('/')[0]])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ config = read_file(RELAY_CONF)
+
+ # Test configured upstream interfaces
+ self.assertIn(f'-u {dhcpv6_server}%{upstream_if}', config)
+
+ # Check listener on all ethernet interfaces
+ for tmp in interfaces:
+ if tmp == upstream_if:
+ continue
+ addr = listen_addr.split('/')[0]
+ self.assertIn(f'-l {addr}%{tmp}', config)
+
+ # Check hop count
+ self.assertIn(f'-c {hop_count}', config)
+ # Check Interface ID option
+ self.assertIn('-I', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py
new file mode 100644
index 0000000..6ecf6c1
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from json import loads
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.template import inc_ip
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'kea-dhcp6'
+KEA6_CONF = '/run/kea/kea-dhcp6.conf'
+base_path = ['service', 'dhcpv6-server']
+
+subnet = '2001:db8:f00::/64'
+dns_1 = '2001:db8::1'
+dns_2 = '2001:db8::2'
+domain = 'vyos.net'
+nis_servers = ['2001:db8:ffff::1', '2001:db8:ffff::2']
+interface = 'eth0'
+interface_addr = inc_ip(subnet, 1) + '/64'
+
+class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceDHCPv6Server, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr])
+ cls.cli_commit(cls)
+
+ super(TestServiceDHCPv6Server, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def walk_path(self, obj, path):
+ current = obj
+
+ for i, key in enumerate(path):
+ if isinstance(key, str):
+ self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}')
+ self.assertTrue(key in current, msg=f'Failed path: {path}')
+ elif isinstance(key, int):
+ self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
+ self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
+ else:
+ assert False, "Invalid type"
+
+ current = current[key]
+
+ return current
+
+ def verify_config_object(self, obj, path, value):
+ base_obj = self.walk_path(obj, path)
+ self.assertTrue(isinstance(base_obj, list))
+ self.assertTrue(any(True for v in base_obj if v == value))
+
+ def verify_config_value(self, obj, path, key, value):
+ base_obj = self.walk_path(obj, path)
+ if isinstance(base_obj, list):
+ self.assertTrue(any(True for v in base_obj if key in v and v[key] == value))
+ elif isinstance(base_obj, dict):
+ self.assertTrue(key in base_obj)
+ self.assertEqual(base_obj[key], value)
+
+ def test_single_pool(self):
+ shared_net_name = 'SMOKE-1'
+ search_domains = ['foo.vyos.net', 'bar.vyos.net']
+ lease_time = '1200'
+ max_lease_time = '72000'
+ min_lease_time = '600'
+ preference = '10'
+ sip_server = 'sip.vyos.net'
+ sntp_server = inc_ip(subnet, 100)
+ range_start = inc_ip(subnet, 256) # ::100
+ range_stop = inc_ip(subnet, 65535) # ::ffff
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+
+ self.cli_set(base_path + ['preference', preference])
+ self.cli_set(pool + ['interface', interface])
+ self.cli_set(pool + ['subnet-id', '1'])
+ # we use the first subnet IP address as default gateway
+ self.cli_set(pool + ['lease-time', 'default', lease_time])
+ self.cli_set(pool + ['lease-time', 'maximum', max_lease_time])
+ self.cli_set(pool + ['lease-time', 'minimum', min_lease_time])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'nis-domain', domain])
+ self.cli_set(pool + ['option', 'nisplus-domain', domain])
+ self.cli_set(pool + ['option', 'sip-server', sip_server])
+ self.cli_set(pool + ['option', 'sntp-server', sntp_server])
+ self.cli_set(pool + ['range', '1', 'start', range_start])
+ self.cli_set(pool + ['range', '1', 'stop', range_stop])
+
+ for server in nis_servers:
+ self.cli_set(pool + ['option', 'nis-server', server])
+ self.cli_set(pool + ['option', 'nisplus-server', server])
+
+ for search in search_domains:
+ self.cli_set(pool + ['option', 'domain-search', search])
+
+ client_base = 1
+ for client in ['client1', 'client2', 'client3']:
+ duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}'
+ self.cli_set(pool + ['static-mapping', client, 'duid', duid])
+ self.cli_set(pool + ['static-mapping', client, 'ipv6-address', inc_ip(subnet, client_base)])
+ self.cli_set(pool + ['static-mapping', client, 'ipv6-prefix', inc_ip(subnet, client_base << 64) + '/64'])
+ client_base += 1
+
+ # cannot have both mac-address and duid set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(pool + ['static-mapping', 'client1', 'mac', '00:50:00:00:00:11'])
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'client1', 'mac'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA6_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'interface', interface)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time))
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time))
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'max-valid-lifetime', int(max_lease_time))
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'domain-search', 'data': ", ".join(search_domains)})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'nis-domain-name', 'data': domain})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'nis-servers', 'data': ", ".join(nis_servers)})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'nisp-domain-name', 'data': domain})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'nisp-servers', 'data': ", ".join(nis_servers)})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'sntp-servers', 'data': sntp_server})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'sip-server-dns', 'data': sip_server})
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'],
+ {'pool': f'{range_start} - {range_stop}'})
+
+ client_base = 1
+ for client in ['client1', 'client2', 'client3']:
+ duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}'
+ ip = inc_ip(subnet, client_base)
+ prefix = inc_ip(subnet, client_base << 64) + '/64'
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'],
+ {'hostname': client, 'duid': duid, 'ip-addresses': [ip], 'prefixes': [prefix]})
+
+ client_base += 1
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+
+ def test_prefix_delegation(self):
+ shared_net_name = 'SMOKE-2'
+ range_start = inc_ip(subnet, 256) # ::100
+ range_stop = inc_ip(subnet, 65535) # ::ffff
+ delegate_start = '2001:db8:ee::'
+ delegate_len = '64'
+ prefix_len = '56'
+ exclude_len = '66'
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['range', '1', 'start', range_start])
+ self.cli_set(pool + ['range', '1', 'stop', range_stop])
+ self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'delegated-length', delegate_len])
+ self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'prefix-length', prefix_len])
+ self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'excluded-prefix', delegate_start])
+ self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'excluded-prefix-length', exclude_len])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA6_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'],
+ {'pool': f'{range_start} - {range_stop}'})
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pd-pools'],
+ {
+ 'prefix': delegate_start,
+ 'prefix-len': int(prefix_len),
+ 'delegated-len': int(delegate_len),
+ 'excluded-prefix': delegate_start,
+ 'excluded-prefix-len': int(exclude_len)
+ })
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_global_nameserver(self):
+ shared_net_name = 'SMOKE-3'
+ ns_global_1 = '2001:db8::1111'
+ ns_global_2 = '2001:db8::2222'
+
+ self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_1])
+ self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_2])
+ self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'subnet-id', '1'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA6_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1)
+
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'option-data'],
+ {'name': 'dns-servers', "code": 23, "space": "dhcp6", "csv-format": True, 'data': f'{ns_global_1}, {ns_global_2}'})
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
new file mode 100644
index 0000000..c39d446
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+import tempfile
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.process import process_running
+
+DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf'
+DDCLIENT_CONF = '/run/ddclient/ddclient.conf'
+DDCLIENT_PID = '/run/ddclient/ddclient.pid'
+DDCLIENT_PNAME = 'ddclient'
+
+base_path = ['service', 'dns', 'dynamic']
+name_path = base_path + ['name']
+server = 'ddns.vyos.io'
+hostname = 'test.ddns.vyos.io'
+zone = 'vyos.io'
+username = 'vyos_user'
+password = 'paSS_@4ord'
+ttl = '300'
+interface = 'eth0'
+
+class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
+ def setUp(self):
+ # Always start with a clean CLI instance
+ self.cli_delete(base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_running(DDCLIENT_PID))
+
+ # Delete DDNS configuration
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # PID file must no londer exist after process exited
+ self.assertFalse(os.path.exists(DDCLIENT_PID))
+
+ # IPv4 standard DDNS service configuration
+ def test_01_dyndns_service_standard(self):
+ services = {'cloudflare': {'protocol': 'cloudflare'},
+ 'freedns': {'protocol': 'freedns', 'username': username},
+ 'zoneedit': {'protocol': 'zoneedit1', 'username': username}}
+
+ for svc, details in services.items():
+ self.cli_set(name_path + [svc, 'address', 'interface', interface])
+ self.cli_set(name_path + [svc, 'host-name', hostname])
+ self.cli_set(name_path + [svc, 'password', password])
+ for opt, value in details.items():
+ self.cli_set(name_path + [svc, opt, value])
+
+ # 'zone' option is supported by 'cloudfare' and 'zoneedit1', but not 'freedns'
+ self.cli_set(name_path + [svc, 'zone', zone])
+ if details['protocol'] in ['cloudflare', 'zoneedit1']:
+ pass
+ else:
+ # exception is raised for unsupported ones
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(name_path + [svc, 'zone'])
+
+ # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit'
+ self.cli_set(name_path + [svc, 'ttl', ttl])
+ if details['protocol'] == 'cloudflare':
+ pass
+ else:
+ # exception is raised for unsupported ones
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(name_path + [svc, 'ttl'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check the generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ # default value 300 seconds
+ self.assertIn(f'daemon=300', ddclient_conf)
+ self.assertIn(f'usev4=ifv4', ddclient_conf)
+ self.assertIn(f'ifv4={interface}', ddclient_conf)
+ self.assertIn(f'password=\'{password}\'', ddclient_conf)
+
+ for opt in details.keys():
+ if opt == 'username':
+ login = details[opt]
+ self.assertIn(f'login={login}', ddclient_conf)
+ else:
+ tmp = details[opt]
+ self.assertIn(f'{opt}={tmp}', ddclient_conf)
+
+ # IPv6 only DDNS service configuration
+ def test_02_dyndns_service_ipv6(self):
+ interval = '60'
+ svc_path = name_path + ['dynv6']
+ proto = 'dyndns2'
+ ip_version = 'ipv6'
+ wait_time = '600'
+ expiry_time_good = '3600'
+ expiry_time_bad = '360'
+
+ self.cli_set(base_path + ['interval', interval])
+ self.cli_set(svc_path + ['address', 'interface', interface])
+ self.cli_set(svc_path + ['ip-version', ip_version])
+ self.cli_set(svc_path + ['protocol', proto])
+ self.cli_set(svc_path + ['server', server])
+ self.cli_set(svc_path + ['username', username])
+ self.cli_set(svc_path + ['password', password])
+ self.cli_set(svc_path + ['host-name', hostname])
+ self.cli_set(svc_path + ['wait-time', wait_time])
+
+ # expiry-time must be greater than wait-time, exception is raised otherwise
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(svc_path + ['expiry-time', expiry_time_bad])
+ self.cli_commit()
+ self.cli_set(svc_path + ['expiry-time', expiry_time_good])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check the generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ self.assertIn(f'daemon={interval}', ddclient_conf)
+ self.assertIn(f'usev6=ifv6', ddclient_conf)
+ self.assertIn(f'ifv6={interface}', ddclient_conf)
+ self.assertIn(f'protocol={proto}', ddclient_conf)
+ self.assertIn(f'server={server}', ddclient_conf)
+ self.assertIn(f'login={username}', ddclient_conf)
+ self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f'min-interval={wait_time}', ddclient_conf)
+ self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf)
+
+ # IPv4+IPv6 dual DDNS service configuration
+ def test_03_dyndns_service_dual_stack(self):
+ services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone},
+ 'freedns': {'protocol': 'freedns', 'username': username},
+ 'google': {'protocol': 'googledomains', 'username': username}}
+ ip_version = 'both'
+
+ for name, details in services.items():
+ self.cli_set(name_path + [name, 'address', 'interface', interface])
+ self.cli_set(name_path + [name, 'host-name', hostname])
+ self.cli_set(name_path + [name, 'password', password])
+ for opt, value in details.items():
+ self.cli_set(name_path + [name, opt, value])
+
+ # Dual stack is supported by 'cloudfare' and 'freedns' but not 'googledomains'
+ # exception is raised for unsupported ones
+ self.cli_set(name_path + [name, 'ip-version', ip_version])
+ if details['protocol'] not in ['cloudflare', 'freedns']:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(name_path + [name, 'ip-version'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check the generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ if details['protocol'] not in ['cloudflare', 'freedns']:
+ self.assertIn(f'usev4=ifv4', ddclient_conf)
+ self.assertIn(f'ifv4={interface}', ddclient_conf)
+ else:
+ self.assertIn(f'usev4=ifv4', ddclient_conf)
+ self.assertIn(f'usev6=ifv6', ddclient_conf)
+ self.assertIn(f'ifv4={interface}', ddclient_conf)
+ self.assertIn(f'ifv6={interface}', ddclient_conf)
+ self.assertIn(f'password=\'{password}\'', ddclient_conf)
+
+ for opt in details.keys():
+ if opt == 'username':
+ login = details[opt]
+ self.assertIn(f'login={login}', ddclient_conf)
+ else:
+ tmp = details[opt]
+ self.assertIn(f'{opt}={tmp}', ddclient_conf)
+
+ def test_04_dyndns_rfc2136(self):
+ # Check if DDNS service can be configured and runs
+ svc_path = name_path + ['vyos']
+ proto = 'nsupdate'
+
+ with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file:
+ key_file.write(b'S3cretKey')
+
+ self.cli_set(svc_path + ['address', 'interface', interface])
+ self.cli_set(svc_path + ['protocol', proto])
+ self.cli_set(svc_path + ['server', server])
+ self.cli_set(svc_path + ['zone', zone])
+ self.cli_set(svc_path + ['key', key_file.name])
+ self.cli_set(svc_path + ['ttl', ttl])
+ self.cli_set(svc_path + ['host-name', hostname])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check some generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ self.assertIn(f'use=if', ddclient_conf)
+ self.assertIn(f'if={interface}', ddclient_conf)
+ self.assertIn(f'protocol={proto}', ddclient_conf)
+ self.assertIn(f'server={server}', ddclient_conf)
+ self.assertIn(f'zone={zone}', ddclient_conf)
+ self.assertIn(f'password=\'{key_file.name}\'', ddclient_conf)
+ self.assertIn(f'ttl={ttl}', ddclient_conf)
+
+ def test_05_dyndns_hostname(self):
+ # Check if DDNS service can be configured and runs
+ svc_path = name_path + ['namecheap']
+ proto = 'namecheap'
+ hostnames = ['@', 'www', hostname, f'@.{hostname}']
+
+ for name in hostnames:
+ self.cli_set(svc_path + ['address', 'interface', interface])
+ self.cli_set(svc_path + ['protocol', proto])
+ self.cli_set(svc_path + ['server', server])
+ self.cli_set(svc_path + ['username', username])
+ self.cli_set(svc_path + ['password', password])
+ self.cli_set(svc_path + ['host-name', name])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check the generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ self.assertIn(f'protocol={proto}', ddclient_conf)
+ self.assertIn(f'server={server}', ddclient_conf)
+ self.assertIn(f'login={username}', ddclient_conf)
+ self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f'{name}', ddclient_conf)
+
+ def test_06_dyndns_web_options(self):
+ # Check if DDNS service can be configured and runs
+ svc_path = name_path + ['cloudflare']
+ proto = 'cloudflare'
+ web_url = 'https://ifconfig.me/ip'
+ web_skip = 'Current IP Address:'
+
+ self.cli_set(svc_path + ['protocol', proto])
+ self.cli_set(svc_path + ['zone', zone])
+ self.cli_set(svc_path + ['password', password])
+ self.cli_set(svc_path + ['host-name', hostname])
+
+ # not specifying either 'interface' or 'web' will raise an exception
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(svc_path + ['address', 'web'])
+
+ # specifying both 'interface' and 'web' will raise an exception as well
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(svc_path + ['address', 'interface', interface])
+ self.cli_commit()
+ self.cli_delete(svc_path + ['address', 'interface'])
+ self.cli_commit()
+
+ # web option 'skip' is useless without the option 'url'
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(svc_path + ['address', 'web', 'skip', web_skip])
+ self.cli_commit()
+ self.cli_set(svc_path + ['address', 'web', 'url', web_url])
+ self.cli_commit()
+
+ # Check the generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ self.assertIn(f'usev4=webv4', ddclient_conf)
+ self.assertIn(f'webv4={web_url}', ddclient_conf)
+ self.assertIn(f'webv4-skip=\'{web_skip}\'', ddclient_conf)
+ self.assertIn(f'protocol={proto}', ddclient_conf)
+ self.assertIn(f'zone={zone}', ddclient_conf)
+ self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f'{hostname}', ddclient_conf)
+
+ def test_07_dyndns_dynamic_interface(self):
+ # Check if DDNS service can be configured and runs
+ svc_path = name_path + ['namecheap']
+ proto = 'namecheap'
+ dyn_interface = 'pppoe587'
+
+ self.cli_set(svc_path + ['address', 'interface', dyn_interface])
+ self.cli_set(svc_path + ['protocol', proto])
+ self.cli_set(svc_path + ['server', server])
+ self.cli_set(svc_path + ['username', username])
+ self.cli_set(svc_path + ['password', password])
+ self.cli_set(svc_path + ['host-name', hostname])
+
+ # Dynamic interface will raise a warning but still go through
+ # XXX: We should have idiomatic class "ConfigSessionWarning" wrapping
+ # "Warning" similar to "ConfigSessionError".
+ # with self.assertWarns(Warning):
+ # self.cli_commit()
+ self.cli_commit()
+
+ # Check the generating config parameters
+ ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
+ self.assertIn(f'ifv4={dyn_interface}', ddclient_conf)
+ self.assertIn(f'protocol={proto}', ddclient_conf)
+ self.assertIn(f'server={server}', ddclient_conf)
+ self.assertIn(f'login={username}', ddclient_conf)
+ self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f'{hostname}', ddclient_conf)
+
+ def test_08_dyndns_vrf(self):
+ # Table number randomized, but should be within range 100-65535
+ vrf_table = '58710'
+ vrf_name = f'vyos-test-{vrf_table}'
+ svc_path = name_path + ['cloudflare']
+ proto = 'cloudflare'
+
+ self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table])
+ self.cli_set(base_path + ['vrf', vrf_name])
+
+ self.cli_set(svc_path + ['address', 'interface', interface])
+ self.cli_set(svc_path + ['protocol', proto])
+ self.cli_set(svc_path + ['host-name', hostname])
+ self.cli_set(svc_path + ['zone', zone])
+ self.cli_set(svc_path + ['password', password])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check for process in VRF
+ systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}')
+ self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient -file {DDCLIENT_CONF}',
+ systemd_override)
+
+ # Check for process in VRF
+ proc = cmd(f'ip vrf pids {vrf_name}')
+ self.assertIn(DDCLIENT_PNAME, proc)
+
+ # Cleanup VRF
+ self.cli_delete(['vrf', 'name', vrf_name])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py
new file mode 100644
index 0000000..9a3f493
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_dns_forwarding.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.template import bracketize_ipv6
+from vyos.utils.file import read_file
+from vyos.utils.process import process_named_running
+
+PDNS_REC_RUN_DIR = '/run/pdns-recursor'
+CONFIG_FILE = f'{PDNS_REC_RUN_DIR}/recursor.conf'
+PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.conf.lua'
+FORWARD_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf'
+HOSTSD_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua'
+PROCESS_NAME= 'pdns_recursor'
+
+base_path = ['service', 'dns', 'forwarding']
+
+allow_from = ['192.0.2.0/24', '2001:db8::/32']
+listen_adress = ['127.0.0.1', '::1']
+
+def get_config_value(key, file=CONFIG_FILE):
+ tmp = read_file(file)
+ tmp = re.findall(r'\n{}=+(.*)'.format(key), tmp)
+ return tmp[0]
+
+class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServicePowerDNS, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # Delete DNS forwarding configuration
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def setUp(self):
+ # forward to base class
+ super().setUp()
+ for network in allow_from:
+ self.cli_set(base_path + ['allow-from', network])
+ for address in listen_adress:
+ self.cli_set(base_path + ['listen-address', address])
+
+ def test_basic_forwarding(self):
+ # Check basic DNS forwarding settings
+ cache_size = '20'
+ negative_ttl = '120'
+
+ # remove code from setUp() as in this test-case we validate the proper
+ # handling of assertions when specific CLI nodes are missing
+ self.cli_delete(base_path)
+
+ self.cli_set(base_path + ['cache-size', cache_size])
+ self.cli_set(base_path + ['negative-ttl', negative_ttl])
+
+ # check validate() - allow from must be defined
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for network in allow_from:
+ self.cli_set(base_path + ['allow-from', network])
+
+ # check validate() - listen-address must be defined
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for address in listen_adress:
+ self.cli_set(base_path + ['listen-address', address])
+
+ # configure DNSSEC
+ self.cli_set(base_path + ['dnssec', 'validate'])
+
+ # Do not use local /etc/hosts file in name resolution
+ self.cli_set(base_path + ['ignore-hosts-file'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured cache-size
+ tmp = get_config_value('max-cache-entries')
+ self.assertEqual(tmp, cache_size)
+
+ # Networks allowed to query this server
+ tmp = get_config_value('allow-from')
+ self.assertEqual(tmp, ','.join(allow_from))
+
+ # Addresses to listen for DNS queries
+ tmp = get_config_value('local-address')
+ self.assertEqual(tmp, ','.join(listen_adress))
+
+ # Maximum amount of time negative entries are cached
+ tmp = get_config_value('max-negative-ttl')
+ self.assertEqual(tmp, negative_ttl)
+
+ # Do not use local /etc/hosts file in name resolution
+ tmp = get_config_value('export-etc-hosts')
+ self.assertEqual(tmp, 'no')
+
+ # RFC1918 addresses are looked up by default
+ tmp = get_config_value('serve-rfc1918')
+ self.assertEqual(tmp, 'yes')
+
+ # verify default port configuration
+ tmp = get_config_value('local-port')
+ self.assertEqual(tmp, '53')
+
+ def test_dnssec(self):
+ # DNSSEC option testing
+ options = ['off', 'process-no-validate', 'process', 'log-fail', 'validate']
+ for option in options:
+ self.cli_set(base_path + ['dnssec', option])
+
+ # commit changes
+ self.cli_commit()
+
+ tmp = get_config_value('dnssec')
+ self.assertEqual(tmp, option)
+
+ def test_external_nameserver(self):
+ # Externe Domain Name Servers (DNS) addresses
+ nameservers = {'192.0.2.1': {}, '192.0.2.2': {'port': '53'}, '2001:db8::1': {'port': '853'}}
+ for h,p in nameservers.items():
+ if 'port' in p:
+ self.cli_set(base_path + ['name-server', h, 'port', p['port']])
+ else:
+ self.cli_set(base_path + ['name-server', h])
+
+ # commit changes
+ self.cli_commit()
+
+ tmp = get_config_value(r'\+.', file=FORWARD_FILE)
+ canonical_entries = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port'] if 'port' in p else 53}")(h, p)
+ for (h, p) in nameservers.items()]
+ self.assertEqual(tmp, ', '.join(canonical_entries))
+
+ # Do not use local /etc/hosts file in name resolution
+ # default: yes
+ tmp = get_config_value('export-etc-hosts')
+ self.assertEqual(tmp, 'yes')
+
+ def test_domain_forwarding(self):
+ domains = ['vyos.io', 'vyos.net', 'vyos.com']
+ nameservers = {'192.0.2.1': {}, '192.0.2.2': {'port': '53'}, '2001:db8::1': {'port': '853'}}
+ for domain in domains:
+ for h,p in nameservers.items():
+ if 'port' in p:
+ self.cli_set(base_path + ['domain', domain, 'name-server', h, 'port', p['port']])
+ else:
+ self.cli_set(base_path + ['domain', domain, 'name-server', h])
+
+ # Test 'recursion-desired' flag for only one domain
+ if domain == domains[0]:
+ self.cli_set(base_path + ['domain', domain, 'recursion-desired'])
+
+ # Test 'negative trust anchor' flag for the second domain only
+ if domain == domains[1]:
+ self.cli_set(base_path + ['domain', domain, 'addnta'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Test configured name-servers
+ hosts_conf = read_file(HOSTSD_FILE)
+ for domain in domains:
+ # Test 'recursion-desired' flag for the first domain only
+ if domain == domains[0]: key =f'\+{domain}'
+ else: key =f'{domain}'
+ tmp = get_config_value(key, file=FORWARD_FILE)
+ canonical_entries = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port'] if 'port' in p else 53}")(h, p)
+ for (h, p) in nameservers.items()]
+ self.assertEqual(tmp, ', '.join(canonical_entries))
+
+ # Test 'negative trust anchor' flag for the second domain only
+ if domain == domains[1]:
+ self.assertIn(f'addNTA("{domain}", "static")', hosts_conf)
+
+ def test_no_rfc1918_forwarding(self):
+ self.cli_set(base_path + ['no-serve-rfc1918'])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ tmp = get_config_value('serve-rfc1918')
+ self.assertEqual(tmp, 'no')
+
+ def test_dns64(self):
+ dns_prefix = '64:ff9b::/96'
+ # Check dns64-prefix - must be prefix /96
+ self.cli_set(base_path + ['dns64-prefix', '2001:db8:aabb::/64'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['dns64-prefix', dns_prefix])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify dns64-prefix configuration
+ tmp = get_config_value('dns64-prefix')
+ self.assertEqual(tmp, dns_prefix)
+
+ def test_exclude_throttle_adress(self):
+ exclude_throttle_adress_examples = [
+ '192.168.128.255',
+ '10.0.0.0/25',
+ '2001:db8:85a3:8d3:1319:8a2e:370:7348',
+ '64:ff9b::/96'
+ ]
+ for exclude_throttle_adress in exclude_throttle_adress_examples:
+ self.cli_set(base_path + ['exclude-throttle-address', exclude_throttle_adress])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify dont-throttle-netmasks configuration
+ tmp = get_config_value('dont-throttle-netmasks')
+ self.assertEqual(tmp, ','.join(exclude_throttle_adress_examples))
+
+ def test_serve_stale_extension(self):
+ server_stale = '20'
+ self.cli_set(base_path + ['serve-stale-extension', server_stale])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ tmp = get_config_value('serve-stale-extensions')
+ self.assertEqual(tmp, server_stale)
+
+ def test_listening_port(self):
+ # We can listen on a different port compared to '53' but only one at a time
+ for port in ['10053', '10054']:
+ self.cli_set(base_path + ['port', port])
+ # commit changes
+ self.cli_commit()
+ # verify local-port configuration
+ tmp = get_config_value('local-port')
+ self.assertEqual(tmp, port)
+
+ def test_ecs_add_for(self):
+ options = ['0.0.0.0/0', '!10.0.0.0/8', 'fc00::/7', '!fe80::/10']
+ for param in options:
+ self.cli_set(base_path + ['options', 'ecs-add-for', param])
+
+ # commit changes
+ self.cli_commit()
+ # verify ecs_add_for configuration
+ tmp = get_config_value('ecs-add-for')
+ self.assertEqual(tmp, ','.join(options))
+
+ def test_ecs_ipv4_bits(self):
+ option_value = '24'
+ self.cli_set(base_path + ['options', 'ecs-ipv4-bits', option_value])
+ # commit changes
+ self.cli_commit()
+ # verify ecs_ipv4_bits configuration
+ tmp = get_config_value('ecs-ipv4-bits')
+ self.assertEqual(tmp, option_value)
+
+ def test_edns_subnet_allow_list(self):
+ options = ['192.0.2.1/32', 'example.com', 'fe80::/10']
+ for param in options:
+ self.cli_set(base_path + ['options', 'edns-subnet-allow-list', param])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify edns_subnet_allow_list configuration
+ tmp = get_config_value('edns-subnet-allow-list')
+ self.assertEqual(tmp, ','.join(options))
+
+ def test_multiple_ns_records(self):
+ test_zone = 'example.com'
+ self.cli_set(base_path + ['authoritative-domain', test_zone, 'records', 'ns', 'test', 'target', f'ns1.{test_zone}'])
+ self.cli_set(base_path + ['authoritative-domain', test_zone, 'records', 'ns', 'test', 'target', f'ns2.{test_zone}'])
+ self.cli_commit()
+ zone_config = read_file(f'{PDNS_REC_RUN_DIR}/zone.{test_zone}.conf')
+ self.assertRegex(zone_config, fr'test\s+\d+\s+NS\s+ns1\.{test_zone}\.')
+ self.assertRegex(zone_config, fr'test\s+\d+\s+NS\s+ns2\.{test_zone}\.')
+
+ def test_zone_cache_url(self):
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'url', 'https://www.internic.net/domain/root.zone'])
+ self.cli_commit()
+
+ lua_config = read_file(PDNS_REC_LUA_CONF_FILE)
+ self.assertIn('zoneToCache("smoketest", "url", "https://www.internic.net/domain/root.zone", { dnssec = "validate", zonemd = "validate", maxReceivedMBytes = 0, retryOnErrorPeriod = 60, refreshPeriod = 86400, timeout = 20 })', lua_config)
+
+ def test_zone_cache_axfr(self):
+
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'axfr', '127.0.0.1'])
+ self.cli_commit()
+
+ lua_config = read_file(PDNS_REC_LUA_CONF_FILE)
+ self.assertIn('zoneToCache("smoketest", "axfr", "127.0.0.1", { dnssec = "validate", zonemd = "validate", maxReceivedMBytes = 0, retryOnErrorPeriod = 60, refreshPeriod = 86400, timeout = 20 })', lua_config)
+
+ def test_zone_cache_options(self):
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'url', 'https://www.internic.net/domain/root.zone'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'dnssec', 'ignore'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'max-zone-size', '100'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'refresh', 'interval', '10'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'retry-interval', '90'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'timeout', '50'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'options', 'zonemd', 'require'])
+ self.cli_commit()
+
+ lua_config = read_file(PDNS_REC_LUA_CONF_FILE)
+ self.assertIn('zoneToCache("smoketest", "url", "https://www.internic.net/domain/root.zone", { dnssec = "ignore", maxReceivedMBytes = 100, refreshPeriod = 10, retryOnErrorPeriod = 90, timeout = 50, zonemd = "require" })', lua_config)
+
+ def test_zone_cache_wrong_source(self):
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'url', 'https://www.internic.net/domain/root.zone'])
+ self.cli_set(base_path + ['zone-cache', 'smoketest', 'source', 'axfr', '127.0.0.1'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ # correct config to correct finish the test
+ self.cli_delete(base_path + ['zone-cache', 'smoketest', 'source', 'axfr'])
+ self.cli_commit()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
new file mode 100644
index 0000000..8a6386e
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -0,0 +1,502 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import json
+
+from requests import request
+from urllib3.exceptions import InsecureRequestWarning
+from time import sleep
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from base_vyostest_shim import ignore_warning
+from vyos.utils.file import read_file
+from vyos.utils.file import write_file
+from vyos.utils.process import call
+from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
+
+from vyos.configsession import ConfigSessionError
+
+base_path = ['service', 'https']
+pki_base = ['pki']
+
+cert_data = """
+MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw
+WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx
+MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV
+BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP
+UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
+01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3
+QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
+BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu
++JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz
+ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93
++dm/LDnp7C0=
+"""
+
+key_data = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
+2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7
+u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
+"""
+
+dh_1024 = """
+MIGHAoGBAM3nvMkHGi/xmRs8cYg4pcl5sAanxel9EM+1XobVhUViXw8JvlmSEVOj
+n2aXUifc4SEs3WDzVPRC8O8qQWjvErpTq/HOgt3aqBCabMgvflmt706XP0KiqnpW
+EyvNiI27J3wBUzEXLIS110MxPAX5Tcug974PecFcOxn1RWrbWcx/AgEC
+"""
+
+dh_2048 = """
+MIIBCAKCAQEA1mld/V7WnxxRinkOlhx/BoZkRELtIUQFYxyARBqYk4C5G3YnZNNu
+zjaGyPnfIKHu8SIUH85OecM+5/co9nYlcUJuph2tbR6qNgPw7LOKIhf27u7WhvJk
+iVsJhwZiWmvvMV4jTParNEI2svoooMyhHXzeweYsg6YtgLVmwiwKj3XP3gRH2i3B
+Mq8CDS7X6xaKvjfeMPZBFqOM5nb6HhsbaAUyiZxrfipLvXxtnbzd/eJUQVfVdxM3
+pn0i+QrO2tuNAzX7GoPc9pefrbb5xJmGS50G0uqsR59+7LhYmyZSBASA0lxTEW9t
+kv/0LPvaYTY57WL7hBeqqHy/WPZHPzDI3wIBAg==
+"""
+# to test load config via HTTP URL
+nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest'
+nginx_conf_smoketest = """
+server {
+ listen 8000;
+ server_name localhost;
+
+ root /tmp;
+
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ =404;
+ autoindex on;
+ }
+}
+"""
+
+PROCESS_NAME = 'nginx'
+
+class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestHTTPSService, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, pki_base)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestHTTPSService, cls).tearDownClass()
+ call(f'sudo rm -f {nginx_tmp_site}')
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_delete(pki_base)
+ self.cli_commit()
+
+ # Check for stopped process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_certificate(self):
+ cert_name = 'test_https'
+ dh_name = 'dh-test'
+
+ self.cli_set(base_path + ['certificates', 'certificate', cert_name])
+ # verify() - certificates do not exist (yet)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')])
+ self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')])
+
+ self.cli_set(base_path + ['certificates', 'dh-params', dh_name])
+ # verify() - dh-params do not exist (yet)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')])
+ # verify() - dh-param minimum length is 2048 bit
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')])
+
+ self.cli_commit()
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.debug = False
+
+ def test_api_missing_keys(self):
+ self.cli_set(base_path + ['api'])
+ self.assertRaises(ConfigSessionError, self.cli_commit)
+
+ def test_api_incomplete_key(self):
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01'])
+ self.assertRaises(ConfigSessionError, self.cli_commit)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_auth(self):
+ address = '127.0.0.1'
+ port = default_value(base_path + ['port'])
+
+ key = 'MySuperSecretVyOS'
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+
+ self.cli_set(base_path + ['listen-address', address])
+
+ self.cli_commit()
+
+ nginx_config = read_file('/etc/nginx/sites-enabled/default')
+ self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
+ self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default
+
+ url = f'https://{address}/retrieve'
+ payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
+ headers = {}
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ # Must get HTTP code 200 on success
+ self.assertEqual(r.status_code, 200)
+
+ payload_invalid = {'data': '{"op": "showConfig", "path": []}', 'key': 'invalid'}
+ r = request('POST', url, verify=False, headers=headers, data=payload_invalid)
+ # Must get HTTP code 401 on invalid key (Unauthorized)
+ self.assertEqual(r.status_code, 401)
+
+ payload_no_key = {'data': '{"op": "showConfig", "path": []}'}
+ r = request('POST', url, verify=False, headers=headers, data=payload_no_key)
+ # Must get HTTP code 401 on missing key (Unauthorized)
+ self.assertEqual(r.status_code, 401)
+
+ # Check path config
+ payload = {'data': '{"op": "showConfig", "path": ["system", "login"]}', 'key': f'{key}'}
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ response = r.json()
+ vyos_user_exists = 'vyos' in response.get('data', {}).get('user', {})
+ self.assertTrue(vyos_user_exists, "The 'vyos' user does not exist in the response.")
+
+ # GraphQL auth test: a missing key will return status code 400, as
+ # 'key' is a non-nullable field in the schema; an incorrect key is
+ # caught by the resolver, and returns success 'False', so one must
+ # check the return value.
+
+ self.cli_set(base_path + ['api', 'graphql'])
+ self.cli_commit()
+
+ graphql_url = f'https://{address}/graphql'
+
+ query_valid_key = f"""
+ {{
+ SystemStatus (data: {{key: "{key}"}}) {{
+ success
+ errors
+ data {{
+ result
+ }}
+ }}
+ }}
+ """
+
+ r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key})
+ success = r.json()['data']['SystemStatus']['success']
+ self.assertTrue(success)
+
+ query_invalid_key = """
+ {
+ SystemStatus (data: {key: "invalid"}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+ }
+ """
+
+ r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key})
+ success = r.json()['data']['SystemStatus']['success']
+ self.assertFalse(success)
+
+ query_no_key = """
+ {
+ SystemStatus (data: {}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+ }
+ """
+
+ r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key})
+ success = r.json()['data']['SystemStatus']['success']
+ self.assertFalse(success)
+
+ # GraphQL token authentication test: request token; pass in header
+ # of query.
+
+ self.cli_set(base_path + ['api', 'graphql', 'authentication', 'type', 'token'])
+ self.cli_commit()
+
+ mutation = """
+ mutation {
+ AuthToken (data: {username: "vyos", password: "vyos"}) {
+ success
+ errors
+ data {
+ result
+ }
+ }
+ }
+ """
+ r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation})
+
+ token = r.json()['data']['AuthToken']['data']['result']['token']
+
+ headers = {'Authorization': f'Bearer {token}'}
+
+ query = """
+ {
+ ShowVersion (data: {}) {
+ success
+ errors
+ op_mode_error {
+ name
+ message
+ vyos_code
+ }
+ data {
+ result
+ }
+ }
+ }
+ """
+
+ r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query})
+ success = r.json()['data']['ShowVersion']['success']
+ self.assertTrue(success)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_add_delete(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/retrieve'
+ payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
+ headers = {}
+
+ self.cli_set(base_path)
+ self.cli_commit()
+
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ # api not configured; expect 503
+ self.assertEqual(r.status_code, 503)
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+ sleep(2)
+
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ # api configured; expect 200
+ self.assertEqual(r.status_code, 200)
+
+ self.cli_delete(base_path + ['api'])
+ self.cli_commit()
+
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ # api deleted; expect 503
+ self.assertEqual(r.status_code, 503)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_show(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/show'
+ headers = {}
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ payload = {
+ 'data': '{"op": "show", "path": ["system", "image"]}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_generate(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/generate'
+ headers = {}
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ payload = {
+ 'data': '{"op": "generate", "path": ["macsec", "mka", "cak", "gcm-aes-256"]}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_configure(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/configure'
+ headers = {}
+ conf_interface = 'dum0'
+ conf_address = '192.0.2.44/32'
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ payload_path = [
+ "interfaces",
+ "dummy",
+ f"{conf_interface}",
+ "address",
+ f"{conf_address}",
+ ]
+
+ payload = {'data': json.dumps({"op": "set", "path": payload_path}), 'key': key}
+
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_config_file(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/config-file'
+ headers = {}
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ payload = {
+ 'data': '{"op": "save"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_reset(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/reset'
+ headers = {}
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ payload = {
+ 'data': '{"op": "reset", "path": ["ip", "arp", "table"]}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_image(self):
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/image'
+ headers = {}
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ payload = {
+ 'data': '{"op": "add"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 400)
+ self.assertIn('Missing required field "url"', r.json().get('error'))
+
+ payload = {
+ 'data': '{"op": "delete"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 400)
+ self.assertIn('Missing required field "name"', r.json().get('error'))
+
+ payload = {
+ 'data': '{"op": "set_default"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 400)
+ self.assertIn('Missing required field "name"', r.json().get('error'))
+
+ payload = {
+ 'data': '{"op": "show"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ @ignore_warning(InsecureRequestWarning)
+ def test_api_config_file_load_http(self):
+ # Test load config from HTTP URL
+ address = '127.0.0.1'
+ key = 'VyOS-key'
+ url = f'https://{address}/config-file'
+ url_config = f'https://{address}/configure'
+ headers = {}
+
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ self.cli_commit()
+
+ # load config via HTTP requires nginx config
+ call(f'sudo touch {nginx_tmp_site}')
+ call(f'sudo chmod 666 {nginx_tmp_site}')
+ write_file(nginx_tmp_site, nginx_conf_smoketest)
+ call('sudo systemctl reload nginx')
+
+ # save config
+ payload = {
+ 'data': '{"op": "save", "file": "/tmp/tmp-config.boot"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ # change config
+ payload = {
+ 'data': '{"op": "set", "path": ["interfaces", "dummy", "dum1", "address", "192.0.2.31/32"]}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url_config, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ # load config from URL
+ payload = {
+ 'data': '{"op": "load", "file": "http://localhost:8000/tmp-config.boot"}',
+ 'key': f'{key}',
+ }
+ r = request('POST', url, verify=False, headers=headers, data=payload)
+ self.assertEqual(r.status_code, 200)
+
+ # cleanup tmp nginx conf
+ call(f'sudo rm -f {nginx_tmp_site}')
+ call('sudo systemctl reload nginx')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=5)
diff --git a/smoketest/scripts/cli/test_service_ids_ddos-protection.py b/smoketest/scripts/cli/test_service_ids_ddos-protection.py
new file mode 100644
index 0000000..91b056e
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_ids_ddos-protection.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'fastnetmon'
+FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf'
+NETWORKS_CONF = '/run/fastnetmon/networks_list'
+EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list'
+base_path = ['service', 'ids', 'ddos-protection']
+
+class TestServiceIDS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceIDS, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete test config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.assertFalse(os.path.exists(FASTNETMON_CONF))
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_fastnetmon(self):
+ networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64']
+ excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128']
+ interfaces = ['eth0', 'eth1']
+ fps = '3500'
+ mbps = '300'
+ pps = '60000'
+
+ self.cli_set(base_path + ['mode', 'mirror'])
+ # Required network!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for tmp in networks:
+ self.cli_set(base_path + ['network', tmp])
+
+ # optional excluded-network!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for tmp in excluded_networks:
+ self.cli_set(base_path + ['excluded-network', tmp])
+
+ # Required interface(s)!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for tmp in interfaces:
+ self.cli_set(base_path + ['listen-interface', tmp])
+
+ self.cli_set(base_path + ['direction', 'in'])
+ self.cli_set(base_path + ['threshold', 'general', 'fps', fps])
+ self.cli_set(base_path + ['threshold', 'general', 'pps', pps])
+ self.cli_set(base_path + ['threshold', 'general', 'mbps', mbps])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ config = read_file(FASTNETMON_CONF)
+ self.assertIn(f'mirror_afpacket = on', config)
+ self.assertIn(f'process_incoming_traffic = on', config)
+ self.assertIn(f'process_outgoing_traffic = off', config)
+ self.assertIn(f'ban_for_flows = on', config)
+ self.assertIn(f'threshold_flows = {fps}', config)
+ self.assertIn(f'ban_for_bandwidth = on', config)
+ self.assertIn(f'threshold_mbps = {mbps}', config)
+ self.assertIn(f'ban_for_pps = on', config)
+ self.assertIn(f'threshold_pps = {pps}', config)
+ # default
+ self.assertIn(f'enable_ban = on', config)
+ self.assertIn(f'enable_ban_ipv6 = on', config)
+ self.assertIn(f'ban_time = 1900', config)
+
+ tmp = ','.join(interfaces)
+ self.assertIn(f'interfaces = {tmp}', config)
+
+
+ network_config = read_file(NETWORKS_CONF)
+ for tmp in networks:
+ self.assertIn(f'{tmp}', network_config)
+
+ excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF)
+ for tmp in excluded_networks:
+ self.assertIn(f'{tmp}', excluded_network_config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py
new file mode 100644
index 0000000..be03179
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_ipoe-server.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from collections import OrderedDict
+from base_accel_ppp_test import BasicAccelPPPTest
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.template import range_to_regex
+from configparser import ConfigParser
+from configparser import RawConfigParser
+
+ac_name = "ACN"
+interface = "eth0"
+
+
+class MultiOrderedDict(OrderedDict):
+ # Accel-ppp has duplicate keys in config file (gw-ip-address)
+ # This class is used to define dictionary which can contain multiple values
+ # in one key.
+ def __setitem__(self, key, value):
+ if isinstance(value, list) and key in self:
+ self[key].extend(value)
+ else:
+ super(OrderedDict, self).__setitem__(key, value)
+
+
+class TestServiceIPoEServer(BasicAccelPPPTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ["service", "ipoe-server"]
+ cls._config_file = "/run/accel-pppd/ipoe.conf"
+ cls._chap_secrets = "/run/accel-pppd/ipoe.chap-secrets"
+ cls._protocol_section = "ipoe"
+
+ # call base-classes classmethod
+ super(TestServiceIPoEServer, cls).setUpClass()
+
+ def verify(self, conf):
+ super().verify(conf)
+
+ # Validate configuration values
+ accel_modules = list(conf["modules"].keys())
+ self.assertIn("log_syslog", accel_modules)
+ self.assertIn("ipoe", accel_modules)
+ self.assertIn("shaper", accel_modules)
+ self.assertIn("ipv6pool", accel_modules)
+ self.assertIn("ipv6_nd", accel_modules)
+ self.assertIn("ipv6_dhcp", accel_modules)
+ self.assertIn("ippool", accel_modules)
+
+ def initial_gateway_config(self):
+ self._gateway = "192.0.2.1/24"
+ super().initial_gateway_config()
+
+ def initial_auth_config(self):
+ self.set(["authentication", "mode", "noauth"])
+
+ def basic_protocol_specific_config(self):
+ self.set(["interface", interface, "client-subnet", "192.168.0.0/24"])
+
+ def test_accel_local_authentication(self):
+ mac_address = "08:00:27:2f:d8:06"
+ self.set(["authentication", "interface", interface, "mac", mac_address])
+ self.set(["authentication", "mode", "local"])
+
+ # No IPoE interface configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+ # Rewrite authentication from basic_config
+ self.set(["authentication", "interface", interface, "mac", mac_address])
+ self.set(["authentication", "mode", "local"])
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
+ conf.read(self._config_file)
+
+ # check proper path to chap-secrets file
+ self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets)
+
+ accel_modules = list(conf["modules"].keys())
+ self.assertIn("chap-secrets", accel_modules)
+
+ # basic verification
+ self.verify(conf)
+
+ # check local users
+ tmp = cmd(f"sudo cat {self._chap_secrets}")
+ regex = f"{interface}\s+\*\s+{mac_address}\s+\*"
+ tmp = re.findall(regex, tmp)
+ self.assertTrue(tmp)
+
+ def test_accel_ipv4_pool(self):
+ self.basic_config(is_gateway=False, is_client_pool=False)
+
+ gateway = ["172.16.0.1/25", "192.0.2.1/24"]
+ subnet = "172.16.0.0/24"
+ first_pool = "POOL1"
+ second_pool = "POOL2"
+ range = "192.0.2.10-192.0.2.20"
+ range_config = "192.0.2.10-20"
+
+ for gw in gateway:
+ self.set(["gateway-address", gw])
+
+ self.set(["client-ip-pool", first_pool, "range", subnet])
+ self.set(["client-ip-pool", first_pool, "next-pool", second_pool])
+ self.set(["client-ip-pool", second_pool, "range", range])
+ self.set(["default-pool", first_pool])
+ # commit changes
+
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = RawConfigParser(
+ allow_no_value=True,
+ delimiters="=",
+ strict=False,
+ dict_type=MultiOrderedDict,
+ )
+ conf.read(self._config_file)
+
+ self.assertIn(
+ f"{first_pool},next={second_pool}", conf["ip-pool"][f"{subnet},name"]
+ )
+ self.assertIn(second_pool, conf["ip-pool"][f"{range_config},name"])
+
+ gw_pool_config_list = conf.get("ip-pool", "gw-ip-address")
+ gw_ipoe_config_list = conf.get(self._protocol_section, "gw-ip-address")
+ for gw in gateway:
+ self.assertIn(gw.split("/")[0], gw_pool_config_list)
+ self.assertIn(gw, gw_ipoe_config_list)
+
+ self.assertIn(first_pool, conf[self._protocol_section]["ip-pool"])
+
+ def test_accel_next_pool(self):
+ self.basic_config(is_gateway=False, is_client_pool=False)
+
+ first_pool = "VyOS-pool1"
+ first_subnet = "192.0.2.0/25"
+ first_gateway = "192.0.2.1/24"
+ second_pool = "Vyos-pool2"
+ second_subnet = "203.0.113.0/25"
+ second_gateway = "203.0.113.1/24"
+ third_pool = "Vyos-pool3"
+ third_subnet = "198.51.100.0/24"
+ third_gateway = "198.51.100.1/24"
+
+ self.set(["gateway-address", f"{first_gateway}"])
+ self.set(["gateway-address", f"{second_gateway}"])
+ self.set(["gateway-address", f"{third_gateway}"])
+
+ self.set(["client-ip-pool", first_pool, "range", first_subnet])
+ self.set(["client-ip-pool", first_pool, "next-pool", second_pool])
+ self.set(["client-ip-pool", second_pool, "range", second_subnet])
+ self.set(["client-ip-pool", second_pool, "next-pool", third_pool])
+ self.set(["client-ip-pool", third_pool, "range", third_subnet])
+
+ # commit changes
+ self.cli_commit()
+
+ config = self.getConfig("ip-pool")
+ # T5099 required specific order
+ pool_config = f"""gw-ip-address={first_gateway.split('/')[0]}
+gw-ip-address={second_gateway.split('/')[0]}
+gw-ip-address={third_gateway.split('/')[0]}
+{third_subnet},name={third_pool}
+{second_subnet},name={second_pool},next={third_pool}
+{first_subnet},name={first_pool},next={second_pool}"""
+ self.assertIn(pool_config, config)
+
+ def test_accel_ipv6_pool(self):
+ # Test configuration of IPv6 client pools
+ self.basic_config(is_gateway=False, is_client_pool=False)
+
+ pool_name = 'ipv6_test_pool'
+ prefix_1 = '2001:db8:fffe::/56'
+ prefix_mask = '64'
+ prefix_2 = '2001:db8:ffff::/56'
+ client_prefix_1 = f'{prefix_1},{prefix_mask}'
+ client_prefix_2 = f'{prefix_2},{prefix_mask}'
+ self.set(['client-ipv6-pool', pool_name, 'prefix', prefix_1, 'mask',
+ prefix_mask])
+ self.set(['client-ipv6-pool', pool_name, 'prefix', prefix_2, 'mask',
+ prefix_mask])
+
+ delegate_1_prefix = '2001:db8:fff1::/56'
+ delegate_2_prefix = '2001:db8:fff2::/56'
+ delegate_mask = '64'
+ self.set(['client-ipv6-pool', pool_name, 'delegate', delegate_1_prefix,
+ 'delegation-prefix', delegate_mask])
+ self.set(['client-ipv6-pool', pool_name, 'delegate', delegate_2_prefix,
+ 'delegation-prefix', delegate_mask])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+
+ for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']:
+ self.assertEqual(conf['modules'][tmp], None)
+
+ config = self.getConfig("ipv6-pool")
+ pool_config = f"""{client_prefix_1},name={pool_name}
+{client_prefix_2},name={pool_name}
+delegate={delegate_1_prefix},{delegate_mask},name={pool_name}
+delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
+ self.assertIn(pool_config, config)
+
+ def test_ipoe_server_vlan(self):
+ vlans = ['100', '200', '300-310']
+
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+ # cannot use "client-subnet" option with "vlan" option
+ # have to delete it
+ self.delete(['interface', interface, 'client-subnet'])
+ self.cli_commit()
+
+ self.set(['interface', interface, 'vlan-mon'])
+
+ # cannot use option "vlan-mon" if no "vlan" set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for vlan in vlans:
+ self.set(['interface', interface, 'vlan', vlan])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+ tmp = range_to_regex(vlans)
+ self.assertIn(f're:^{interface}\.{tmp}$', conf['ipoe']['interface'])
+
+ tmp = ','.join(vlans)
+ self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon'])
+
+ @unittest.skip("PPP is not a part of IPoE")
+ def test_accel_ppp_options(self):
+ pass
+
+ @unittest.skip("WINS server is not used in IPoE")
+ def test_accel_wins_server(self):
+ pass
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py
new file mode 100644
index 0000000..9d72ef7
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_lldp.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.version import get_version_data
+
+PROCESS_NAME = 'lldpd'
+LLDPD_CONF = '/etc/lldpd.d/01-vyos.conf'
+base_path = ['service', 'lldp']
+mgmt_if = 'dum83513'
+mgmt_addr = ['1.2.3.4', '1.2.3.5']
+
+class TestServiceLLDP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestServiceLLDP, cls).setUpClass()
+
+ # create a test interfaces
+ for addr in mgmt_addr:
+ cls.cli_set(cls, ['interfaces', 'dummy', mgmt_if, 'address', addr + '/32'])
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', mgmt_if])
+ super(TestServiceLLDP, cls).tearDownClass()
+
+ def tearDown(self):
+ # service must be running after it was configured
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete/stop LLDP service
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # service is no longer allowed to run after it was removed
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_lldp_basic(self):
+ self.cli_set(base_path)
+ self.cli_commit()
+
+ config = read_file(LLDPD_CONF)
+ version_data = get_version_data()
+ version = version_data['version']
+ self.assertIn(f'configure system platform VyOS', config)
+ self.assertIn(f'configure system description "VyOS {version}"', config)
+
+ def test_02_lldp_mgmt_address(self):
+ for addr in mgmt_addr:
+ self.cli_set(base_path + ['management-address', addr])
+ self.cli_commit()
+
+ config = read_file(LLDPD_CONF)
+ self.assertIn(f'configure system ip management pattern {",".join(mgmt_addr)}', config)
+
+ def test_03_lldp_interfaces(self):
+ for interface in Section.interfaces('ethernet'):
+ if not '.' in interface:
+ self.cli_set(base_path + ['interface', interface])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+
+ interface_list = []
+ for interface in Section.interfaces('ethernet'):
+ if not '.' in interface:
+ interface_list.append(interface)
+ tmp = ','.join(interface_list)
+ self.assertIn(f'configure system interface pattern "{tmp}"', config)
+
+ def test_04_lldp_all_interfaces(self):
+ self.cli_set(base_path + ['interface', 'all'])
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ self.assertIn(f'configure system interface pattern "*"', config)
+
+ def test_05_lldp_location(self):
+ interface = 'eth0'
+ elin = '1234567890'
+ self.cli_set(base_path + ['interface', interface, 'location', 'elin', elin])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+
+ self.assertIn(f'configure ports {interface} med location elin "{elin}"', config)
+ self.assertIn(f'configure system interface pattern "{interface}"', config)
+
+ def test_06_lldp_snmp(self):
+ self.cli_set(base_path + ['snmp'])
+
+ # verify - can not start lldp snmp without snmp beeing configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(['service', 'snmp'])
+ self.cli_commit()
+
+ # SNMP required process to be started with -x option
+ tmp = read_file('/etc/default/lldpd')
+ self.assertIn('-x', tmp)
+
+ self.cli_delete(['service', 'snmp'])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_mdns_repeater.py b/smoketest/scripts/cli/test_service_mdns_repeater.py
new file mode 100644
index 0000000..f2fb3b5
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_mdns_repeater.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from configparser import ConfigParser
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+
+base_path = ['service', 'mdns', 'repeater']
+intf_base = ['interfaces', 'dummy']
+config_file = '/run/avahi-daemon/avahi-daemon.conf'
+
+
+class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase):
+ def setUp(self):
+ # Start with a clean CLI instance
+ self.cli_delete(base_path)
+
+ # Service required a configured IP address on the interface
+ self.cli_set(intf_base + ['dum10', 'address', '192.0.2.1/30'])
+ self.cli_set(intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local'])
+ self.cli_set(intf_base + ['dum20', 'address', '192.0.2.5/30'])
+ self.cli_set(intf_base + ['dum20', 'address', '2001:db8:0:2::5/64'])
+ self.cli_set(intf_base + ['dum30', 'address', '192.0.2.9/30'])
+ self.cli_set(intf_base + ['dum30', 'address', '2001:db8:0:2::9/64'])
+ self.cli_set(intf_base + ['dum40', 'address', '2001:db8:0:2::11/64'])
+ self.cli_commit()
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running('avahi-daemon'))
+
+ self.cli_delete(base_path)
+ self.cli_delete(intf_base + ['dum10'])
+ self.cli_delete(intf_base + ['dum20'])
+ self.cli_delete(intf_base + ['dum30'])
+ self.cli_delete(intf_base + ['dum40'])
+ self.cli_commit()
+
+ # Check that there is no longer a running process
+ self.assertFalse(process_named_running('avahi-daemon'))
+
+ def test_service_dual_stack(self):
+ # mDNS browsing domains in addition to the default one (local)
+ domains = ['dom1.home.arpa', 'dom2.home.arpa']
+
+ # mDNS services to be repeated
+ services = ['_ipp._tcp', '_smb._tcp', '_ssh._tcp']
+
+ self.cli_set(base_path + ['ip-version', 'both'])
+ self.cli_set(base_path + ['interface', 'dum20'])
+ self.cli_set(base_path + ['interface', 'dum30'])
+
+ for domain in domains:
+ self.cli_set(base_path + ['browse-domain', domain])
+
+ for service in services:
+ self.cli_set(base_path + ['allow-service', service])
+
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(delimiters='=')
+ conf.read(config_file)
+
+ self.assertEqual(conf['server']['use-ipv4'], 'yes')
+ self.assertEqual(conf['server']['use-ipv6'], 'yes')
+ self.assertEqual(conf['server']['allow-interfaces'], 'dum20, dum30')
+ self.assertEqual(conf['server']['browse-domains'], ', '.join(domains))
+ self.assertEqual(conf['reflector']['enable-reflector'], 'yes')
+ self.assertEqual(conf['reflector']['reflect-filters'], ', '.join(services))
+
+ def test_service_ipv4(self):
+ # partcipating interfaces should have IPv4 addresses
+ self.cli_set(base_path + ['ip-version', 'ipv4'])
+ self.cli_set(base_path + ['interface', 'dum10'])
+ self.cli_set(base_path + ['interface', 'dum40'])
+
+ # exception is raised if partcipating interfaces do not have IPv4 address
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', 'dum40'])
+ self.cli_set(base_path + ['interface', 'dum20'])
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(delimiters='=')
+ conf.read(config_file)
+
+ self.assertEqual(conf['server']['use-ipv4'], 'yes')
+ self.assertEqual(conf['server']['use-ipv6'], 'no')
+ self.assertEqual(conf['server']['allow-interfaces'], 'dum10, dum20')
+ self.assertEqual(conf['reflector']['enable-reflector'], 'yes')
+
+ def test_service_ipv6(self):
+ # partcipating interfaces should have IPv6 addresses
+ self.cli_set(base_path + ['ip-version', 'ipv6'])
+ self.cli_set(base_path + ['interface', 'dum10'])
+ self.cli_set(base_path + ['interface', 'dum30'])
+
+ # exception is raised if partcipating interfaces do not have IPv4 address
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['interface', 'dum10'])
+ self.cli_set(base_path + ['interface', 'dum40'])
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(delimiters='=')
+ conf.read(config_file)
+
+ self.assertEqual(conf['server']['use-ipv4'], 'no')
+ self.assertEqual(conf['server']['use-ipv6'], 'yes')
+ self.assertEqual(conf['server']['allow-interfaces'], 'dum30, dum40')
+ self.assertEqual(conf['reflector']['enable-reflector'], 'yes')
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py
new file mode 100644
index 0000000..886b886
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'telegraf'
+TELEGRAF_CONF = '/run/telegraf/telegraf.conf'
+base_path = ['service', 'monitoring', 'telegraf']
+org = 'log@in.local'
+token = 'GuRJc12tIzfjnYdKRAIYbxdWd2aTpOT9PVYNddzDnFV4HkAcD7u7-kndTFXjGuXzJN6TTxmrvPODB4mnFcseDV=='
+port = '8888'
+url = 'https://foo.local'
+bucket = 'main'
+inputs = ['cpu', 'disk', 'mem', 'net', 'system', 'kernel', 'interrupts', 'syslog']
+
+class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for not longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_basic_config(self):
+ self.cli_set(base_path + ['influxdb', 'authentication', 'organization', org])
+ self.cli_set(base_path + ['influxdb', 'authentication', 'token', token])
+ self.cli_set(base_path + ['influxdb', 'port', port])
+ self.cli_set(base_path + ['influxdb', 'url', url])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(TELEGRAF_CONF)
+
+ # Check telegraf config
+ self.assertIn(f'organization = "{org}"', config)
+ self.assertIn(f' token = "$INFLUX_TOKEN"', config)
+ self.assertIn(f'urls = ["{url}:{port}"]', config)
+ self.assertIn(f'bucket = "{bucket}"', config)
+ self.assertIn(f'[[inputs.exec]]', config)
+
+ for input in inputs:
+ self.assertIn(input, config)
+
+ def test_02_loki(self):
+ label = 'r123'
+ loki_url = 'http://localhost'
+ port = '3100'
+ loki_username = 'VyOS'
+ loki_password = 'PassW0Rd_VyOS'
+
+ self.cli_set(base_path + ['loki', 'url', loki_url])
+ self.cli_set(base_path + ['loki', 'port', port])
+ self.cli_set(base_path + ['loki', 'metric-name-label', label])
+
+ self.cli_set(base_path + ['loki', 'authentication', 'username', loki_username])
+ # password not set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['loki', 'authentication', 'password', loki_password])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(TELEGRAF_CONF)
+ self.assertIn(f'[[outputs.loki]]', config)
+ self.assertIn(f'domain = "{loki_url}:{port}"', config)
+ self.assertIn(f'metric_name_label = "{label}"', config)
+ self.assertIn(f'username = "{loki_username}"', config)
+ self.assertIn(f'password = "{loki_password}"', config)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py
new file mode 100644
index 0000000..a60dae0
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+
+PROCESS_NAME = 'zabbix_agent2'
+ZABBIX_AGENT_CONF = '/run/zabbix/zabbix-agent2.conf'
+base_path = ['service', 'monitoring', 'zabbix-agent']
+
+
+class TestZabbixAgent(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Process must be terminated after deleting the config
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_zabbix_agent(self):
+ directory = '/tmp'
+ buffer_send = '8'
+ buffer_size = '120'
+ log_level = {'warning': '3'}
+ log_size = '1'
+ servers = ['192.0.2.1', '2001:db8::1']
+ servers_active = {'192.0.2.5': {'port': '10051'}, '2001:db8::123': {'port': '10052'}}
+ port = '10050'
+ timeout = '5'
+ listen_ip = '0.0.0.0'
+ hostname = 'r-vyos'
+
+ self.cli_set(base_path + ['directory', directory])
+ self.cli_set(base_path + ['limits', 'buffer-flush-interval', buffer_send])
+ self.cli_set(base_path + ['limits', 'buffer-size', buffer_size])
+ self.cli_set(base_path + ['log', 'debug-level', next(iter(log_level))])
+ self.cli_set(base_path + ['log', 'size', log_size])
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+ for server_active, server_config in servers_active.items():
+ self.cli_set(base_path + ['server-active', server_active, 'port', server_config['port']])
+ self.cli_set(base_path + ['timeout', timeout])
+ self.cli_set(base_path + ['host-name', hostname])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(ZABBIX_AGENT_CONF)
+
+ self.assertIn(f'LogFileSize={log_size}', config)
+ self.assertIn(f'DebugLevel={log_level.get("warning")}', config)
+
+ self.assertIn(f'Server={",".join(sorted(servers))}', config)
+ tmp = 'ServerActive=192.0.2.5:10051,[2001:db8::123]:10052'
+ self.assertIn(tmp, config)
+
+ self.assertIn(f'ListenPort={port}', config)
+ self.assertIn(f'ListenIP={listen_ip}', config)
+ self.assertIn(f'BufferSend={buffer_send}', config)
+ self.assertIn(f'BufferSize={buffer_size}', config)
+ self.assertIn(f'Include={directory}/*.conf', config)
+ self.assertIn(f'Timeout={timeout}', config)
+ self.assertIn(f'Hostname={hostname}', config)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ndp-proxy.py b/smoketest/scripts/cli/test_service_ndp-proxy.py
new file mode 100644
index 0000000..dfdb3f6
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_ndp-proxy.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.ifconfig import Section
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ndppd'
+NDPPD_CONF = '/run/ndppd/ndppd.conf'
+base_path = ['service', 'ndp-proxy']
+
+def getConfigSection(string=None, end=' {', endsection='^}'):
+ tmp = f'cat {NDPPD_CONF} | sed -n "/^{string}{end}/,/{endsection}/p"'
+ out = cmd(tmp)
+ return out
+
+class TestServiceNDPProxy(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceNDPProxy, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SSH config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_basic(self):
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', interface, 'enable-router-bit'])
+
+ self.cli_commit()
+
+ for interface in interfaces:
+ config = getConfigSection(f'proxy {interface}')
+ self.assertIn(f'proxy {interface}', config)
+ self.assertIn(f'router yes', config)
+ self.assertIn(f'timeout 500', config) # default value
+ self.assertIn(f'ttl 30000', config) # default value
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ntp.py b/smoketest/scripts/cli/test_service_ntp.py
new file mode 100644
index 0000000..07af4f5
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_ntp.py
@@ -0,0 +1,264 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.xml_ref import default_value
+
+PROCESS_NAME = 'chronyd'
+NTP_CONF = '/run/chrony/chrony.conf'
+base_path = ['service', 'ntp']
+
+class TestSystemNTP(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemNTP, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_base_options(self):
+ # Test basic NTP support with multiple servers and their options
+ servers = ['192.0.2.1', '192.0.2.2']
+ options = ['nts', 'noselect', 'prefer']
+ pools = ['pool.vyos.io']
+
+ for server in servers:
+ for option in options:
+ self.cli_set(base_path + ['server', server, option])
+
+ # Test NTP pool
+ for pool in pools:
+ self.cli_set(base_path + ['server', pool, 'pool'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check generated configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ self.assertIn('driftfile /run/chrony/drift', config)
+ self.assertIn('dumpdir /run/chrony', config)
+ self.assertIn('ntsdumpdir /run/chrony', config)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst ' + ' '.join(options), config)
+
+ for pool in pools:
+ self.assertIn(f'pool {pool} iburst', config)
+
+ def test_clients(self):
+ # Test the allowed-networks statement
+ listen_address = ['127.0.0.1', '::1']
+ for listen in listen_address:
+ self.cli_set(base_path + ['listen-address', listen])
+
+ networks = ['192.0.2.0/24', '2001:db8:1000::/64', '100.64.0.0', '2001:db8::ffff']
+ for network in networks:
+ self.cli_set(base_path + ['allow-client', 'address', network])
+
+ # Verify "NTP server not configured" verify() statement
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ servers = ['192.0.2.1', '192.0.2.2']
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ self.cli_commit()
+
+ # Check generated client address configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ for network in networks:
+ self.assertIn(f'allow {network}', config)
+
+ # Check listen address
+ for listen in listen_address:
+ self.assertIn(f'bindaddress {listen}', config)
+
+ def test_interface(self):
+ interfaces = ['eth0']
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+
+ servers = ['time1.vyos.net', 'time2.vyos.net']
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ self.cli_commit()
+
+ # Check generated client address configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ for interface in interfaces:
+ self.assertIn(f'binddevice {interface}', config)
+
+ def test_vrf(self):
+ vrf_name = 'vyos-mgmt'
+
+ self.cli_set(['vrf', 'name', vrf_name, 'table', '12345'])
+ self.cli_set(base_path + ['vrf', vrf_name])
+
+ servers = ['time1.vyos.net', 'time2.vyos.net']
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ self.cli_commit()
+
+ # Check for process in VRF
+ tmp = cmd(f'ip vrf pids {vrf_name}')
+ self.assertIn(PROCESS_NAME, tmp)
+
+ self.cli_delete(['vrf', 'name', vrf_name])
+
+ def test_leap_seconds(self):
+ servers = ['time1.vyos.net', 'time2.vyos.net']
+ for server in servers:
+ self.cli_set(base_path + ['server', server])
+
+ self.cli_commit()
+
+ # Check generated client address configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ self.assertIn('leapsectz right/UTC', config) # CLI default
+
+ for mode in ['ignore', 'system', 'smear']:
+ self.cli_set(base_path + ['leap-second', mode])
+ self.cli_commit()
+ config = cmd(f'sudo cat {NTP_CONF}')
+ if mode != 'smear':
+ self.assertIn(f'leapsecmode {mode}', config)
+ else:
+ self.assertIn(f'leapsecmode slew', config)
+ self.assertIn(f'maxslewrate 1000', config)
+ self.assertIn(f'smoothtime 400 0.001024 leaponly', config)
+
+ def test_interleave_option(self):
+ # "interleave" option differs from some others in that the
+ # name is not a 1:1 mapping from VyOS config
+ servers = ['192.0.2.1', '192.0.2.2']
+ options = ['prefer']
+
+ for server in servers:
+ for option in options:
+ self.cli_set(base_path + ['server', server, option])
+ self.cli_set(base_path + ['server', server, 'interleave'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check generated configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ self.assertIn('driftfile /run/chrony/drift', config)
+ self.assertIn('dumpdir /run/chrony', config)
+ self.assertIn('ntsdumpdir /run/chrony', config)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst ' + ' '.join(options) + ' xleave', config)
+
+ def test_offload_timestamp_default(self):
+ # Test offloading of NIC timestamp
+ servers = ['192.0.2.1', '192.0.2.2']
+ ptp_port = '8319'
+
+ for server in servers:
+ self.cli_set(base_path + ['server', server, 'ptp'])
+
+ self.cli_set(base_path + ['ptp', 'port', ptp_port])
+ self.cli_set(base_path + ['ptp', 'timestamp', 'interface', 'all'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check generated configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ self.assertIn('driftfile /run/chrony/drift', config)
+ self.assertIn('dumpdir /run/chrony', config)
+ self.assertIn('ntsdumpdir /run/chrony', config)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst port {ptp_port}', config)
+
+ self.assertIn('hwtimestamp *', config)
+
+ def test_ptp_transport(self):
+ # Test offloading of NIC timestamp
+ servers = ['192.0.2.1', '192.0.2.2']
+ options = ['prefer']
+
+ default_ptp_port = default_value(base_path + ['ptp', 'port'])
+
+ for server in servers:
+ for option in options:
+ self.cli_set(base_path + ['server', server, option])
+ self.cli_set(base_path + ['server', server, 'ptp'])
+
+ # commit changes (expected to fail)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # add the required top-level option and commit
+ self.cli_set(base_path + ['ptp'])
+ self.cli_commit()
+
+ # Check generated configuration
+ # this file must be read with higher permissions
+ config = cmd(f'sudo cat {NTP_CONF}')
+ self.assertIn('driftfile /run/chrony/drift', config)
+ self.assertIn('dumpdir /run/chrony', config)
+ self.assertIn('ntsdumpdir /run/chrony', config)
+ self.assertIn('clientloglimit 1048576', config)
+ self.assertIn('rtcsync', config)
+ self.assertIn('makestep 1.0 3', config)
+ self.assertIn('leapsectz right/UTC', config)
+
+ for server in servers:
+ self.assertIn(f'server {server} iburst ' + ' '.join(options) + f' port {default_ptp_port}', config)
+
+ self.assertIn(f'ptpport {default_ptp_port}', config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py
new file mode 100644
index 0000000..8cd87e0
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_pppoe-server.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_accel_ppp_test import BasicAccelPPPTest
+
+from configparser import ConfigParser
+from vyos.utils.file import read_file
+from vyos.template import range_to_regex
+from vyos.configsession import ConfigSessionError
+
+local_if = ['interfaces', 'dummy', 'dum667']
+ac_name = 'ACN'
+interface = 'eth0'
+
+class TestServicePPPoEServer(BasicAccelPPPTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['service', 'pppoe-server']
+ cls._config_file = '/run/accel-pppd/pppoe.conf'
+ cls._chap_secrets = '/run/accel-pppd/pppoe.chap-secrets'
+ cls._protocol_section = 'pppoe'
+ # call base-classes classmethod
+ super(TestServicePPPoEServer, cls).setUpClass()
+
+ def tearDown(self):
+ self.cli_delete(local_if)
+ super().tearDown()
+
+ def verify(self, conf):
+ mtu = '1492'
+
+ # validate some common values in the configuration
+ for tmp in ['log_syslog', 'pppoe', 'ippool',
+ 'auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5',
+ 'auth_pap', 'shaper']:
+ # Settings without values provide None
+ self.assertEqual(conf['modules'][tmp], None)
+
+ # check Access Concentrator setting
+ self.assertTrue(conf['pppoe']['ac-name'] == ac_name)
+ self.assertTrue(conf['pppoe'].getboolean('verbose'))
+ self.assertTrue(conf['pppoe']['interface'], interface)
+
+ # check ppp
+ self.assertTrue(conf['ppp'].getboolean('verbose'))
+ self.assertTrue(conf['ppp'].getboolean('check-ip'))
+ self.assertEqual(conf['ppp']['mtu'], mtu)
+
+ super().verify(conf)
+
+ def basic_protocol_specific_config(self):
+ self.cli_set(local_if + ['address', '192.0.2.1/32'])
+ self.set(['access-concentrator', ac_name])
+ self.set(['interface', interface])
+
+ def test_pppoe_limits(self):
+ self.basic_config()
+ self.set(['limits', 'connection-limit', '20/min'])
+ self.cli_commit()
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+ self.assertEqual(conf['connlimit']['limit'], '20/min')
+
+ def test_pppoe_server_authentication_protocols(self):
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+
+ # explicitly test mschap-v2 - no special reason
+ self.set( ['authentication', 'protocols', 'mschap-v2'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True)
+ conf.read(self._config_file)
+
+ self.assertEqual(conf['modules']['auth_mschap_v2'], None)
+
+ def test_pppoe_server_shaper(self):
+ fwmark = '223'
+ limiter = 'tbf'
+ self.basic_config()
+
+ self.set(['shaper', 'fwmark', fwmark])
+ # commit changes
+
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+
+ # basic verification
+ self.verify(conf)
+
+ self.assertEqual(conf['shaper']['fwmark'], fwmark)
+ self.assertEqual(conf['shaper']['down-limiter'], limiter)
+
+ def test_accel_radius_authentication(self):
+ radius_called_sid = 'ifname:mac'
+
+ self.set(['authentication', 'radius', 'called-sid-format', radius_called_sid])
+
+ # run common tests
+ super().test_accel_radius_authentication()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+
+ # Validate configuration
+ self.assertEqual(conf['pppoe']['called-sid'], radius_called_sid)
+
+ def test_pppoe_server_vlan(self):
+
+ vlans = ['100', '200', '300-310']
+
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+
+ self.set(['interface', interface, 'vlan-mon'])
+
+ # cannot use option "vlan-mon" if no "vlan" set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for vlan in vlans:
+ self.set(['interface', interface, 'vlan', vlan])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ config = read_file(self._config_file)
+ for vlan in vlans:
+ tmp = range_to_regex(vlan)
+ self.assertIn(f'interface=re:^{interface}\.{tmp}$', config)
+
+ tmp = ','.join(vlans)
+ self.assertIn(f'vlan-mon={interface},{tmp}', config)
+
+ def test_pppoe_server_pado_delay(self):
+ delay_without_sessions = '10'
+ delays = {'20': '200', '30': '300'}
+
+ self.basic_config()
+
+ self.set(['pado-delay', delay_without_sessions])
+ self.cli_commit()
+
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+ self.assertEqual(conf['pppoe']['pado-delay'], delay_without_sessions)
+
+ for delay, sessions in delays.items():
+ self.set(['pado-delay', delay, 'sessions', sessions])
+ self.cli_commit()
+
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+
+ self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,30:300')
+
+ self.set(['pado-delay', 'disable', 'sessions', '400'])
+ self.cli_commit()
+
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+ self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,30:300,-1:400')
+
+ def test_pppoe_server_any_login(self):
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+
+ self.set(['authentication', 'any-login'])
+ self.cli_commit()
+
+ # Validate configuration values
+ config = read_file(self._config_file)
+ self.assertIn('any-login=1', config)
+
+ def test_pppoe_server_accept_service(self):
+ services = ['user1-service', 'user2-service']
+ self.basic_config()
+
+ for service in services:
+ self.set(['service-name', service])
+ self.set(['accept-any-service'])
+ self.set(['accept-blank-service'])
+ self.cli_commit()
+
+ # Validate configuration values
+ config = read_file(self._config_file)
+ self.assertIn(f'service-name={",".join(services)}', config)
+ self.assertIn('accept-any-service=1', config)
+ self.assertIn('accept-blank-service=1', config)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
new file mode 100644
index 0000000..6dbb6ad
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_router-advert.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from vyos.configsession import ConfigSessionError
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.file import read_file
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'radvd'
+RADVD_CONF = '/run/radvd/radvd.conf'
+
+interface = 'eth1'
+base_path = ['service', 'router-advert', 'interface', interface]
+address_base = ['interfaces', 'ethernet', interface, 'address']
+prefix = '::/64'
+
+def get_config_value(key):
+ tmp = read_file(RADVD_CONF)
+ tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp)
+ return tmp[0].split()[0].replace(';','')
+
+class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceRADVD, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, ['service', 'router-advert'])
+
+ cls.cli_set(cls, address_base + ['2001:db8::1/64'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, address_base)
+ super(TestServiceRADVD, cls).tearDownClass()
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for no longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_common(self):
+ self.cli_set(base_path + ['prefix', prefix, 'no-on-link-flag'])
+ self.cli_set(base_path + ['prefix', prefix, 'no-autonomous-flag'])
+ self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['other-config-flag'])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify values
+ tmp = get_config_value('interface')
+ self.assertEqual(tmp, interface)
+
+ tmp = get_config_value('prefix')
+ self.assertEqual(tmp, prefix)
+
+ tmp = get_config_value('AdvOtherConfigFlag')
+ self.assertEqual(tmp, 'on')
+
+ # this is a default value
+ tmp = get_config_value('AdvRetransTimer')
+ self.assertEqual(tmp, '0')
+
+ # this is a default value
+ tmp = get_config_value('AdvCurHopLimit')
+ self.assertEqual(tmp, '64')
+
+ # this is a default value
+ tmp = get_config_value('AdvDefaultPreference')
+ self.assertEqual(tmp, 'medium')
+
+ tmp = get_config_value('AdvAutonomous')
+ self.assertEqual(tmp, 'off')
+
+ # this is a default value
+ tmp = get_config_value('AdvValidLifetime')
+ self.assertEqual(tmp, 'infinity')
+
+ # this is a default value
+ tmp = get_config_value('AdvPreferredLifetime')
+ self.assertEqual(tmp, '14400')
+
+ tmp = get_config_value('AdvOnLink')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('DeprecatePrefix')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('DecrementLifetimes')
+ self.assertEqual(tmp, 'off')
+
+ def test_dns(self):
+ nameserver = ['2001:db8::1', '2001:db8::2']
+ dnssl = ['vyos.net', 'vyos.io']
+ ns_lifetime = '599'
+
+ self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['other-config-flag'])
+
+ for ns in nameserver:
+ self.cli_set(base_path + ['name-server', ns])
+ for sl in dnssl:
+ self.cli_set(base_path + ['dnssl', sl])
+
+ self.cli_set(base_path + ['name-server-lifetime', ns_lifetime])
+ # The value, if not 0, must be at least interval max (defaults to 600).
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ ns_lifetime = '600'
+ self.cli_set(base_path + ['name-server-lifetime', ns_lifetime])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(RADVD_CONF)
+
+ tmp = 'RDNSS ' + ' '.join(nameserver) + ' {'
+ self.assertIn(tmp, config)
+
+ tmp = f'AdvRDNSSLifetime {ns_lifetime};'
+ self.assertIn(tmp, config)
+
+ tmp = 'DNSSL ' + ' '.join(dnssl) + ' {'
+ self.assertIn(tmp, config)
+
+ def test_deprecate_prefix(self):
+ self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['prefix', prefix, 'deprecate-prefix'])
+ self.cli_set(base_path + ['prefix', prefix, 'decrement-lifetime'])
+
+ # commit changes
+ self.cli_commit()
+
+ tmp = get_config_value('DeprecatePrefix')
+ self.assertEqual(tmp, 'on')
+
+ tmp = get_config_value('DecrementLifetimes')
+ self.assertEqual(tmp, 'on')
+
+ def test_route(self):
+ route = '2001:db8:1000::/64'
+
+ self.cli_set(base_path + ['prefix', prefix])
+ self.cli_set(base_path + ['route', route])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(RADVD_CONF)
+
+ tmp = f'route {route}' + ' {'
+ self.assertIn(tmp, config)
+
+ self.assertIn('AdvRouteLifetime 1800;', config)
+ self.assertIn('AdvRoutePreference medium;', config)
+ self.assertIn('RemoveRoute on;', config)
+
+ def test_rasrcaddress(self):
+ ra_src = ['fe80::1', 'fe80::2']
+
+ self.cli_set(base_path + ['prefix', prefix])
+ for src in ra_src:
+ self.cli_set(base_path + ['source-address', src])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(RADVD_CONF)
+ self.assertIn('AdvRASrcAddress {', config)
+ for src in ra_src:
+ self.assertIn(f' {src};', config)
+
+ def test_nat64prefix(self):
+ nat64prefix = '64:ff9b::/96'
+ nat64prefix_invalid = '64:ff9b::/44'
+
+ self.cli_set(base_path + ['nat64prefix', nat64prefix])
+
+ # and another invalid prefix
+ # Invalid NAT64 prefix length for "2001:db8::/34", can only be one of:
+ # /32, /40, /48, /56, /64, /96
+ self.cli_set(base_path + ['nat64prefix', nat64prefix_invalid])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['nat64prefix', nat64prefix_invalid])
+
+ # NAT64 valid-lifetime must not be smaller then "interval max"
+ self.cli_set(base_path + ['nat64prefix', nat64prefix, 'valid-lifetime', '500'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['nat64prefix', nat64prefix, 'valid-lifetime'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(RADVD_CONF)
+
+ tmp = f'nat64prefix {nat64prefix}' + ' {'
+ self.assertIn(tmp, config)
+ self.assertIn('AdvValidLifetime 65528;', config) # default
+
+ def test_advsendadvert_advintervalopt(self):
+ ra_src = ['fe80::1', 'fe80::2']
+
+ self.cli_set(base_path + ['prefix', prefix])
+ self.cli_set(base_path + ['no-send-advert'])
+ # commit changes
+ self.cli_commit()
+
+ # Verify generated configuration
+ config = read_file(RADVD_CONF)
+ tmp = get_config_value('AdvSendAdvert')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('AdvIntervalOpt')
+ self.assertEqual(tmp, 'on')
+
+ self.cli_set(base_path + ['no-send-interval'])
+ # commit changes
+ self.cli_commit()
+
+ # Verify generated configuration
+ config = read_file(RADVD_CONF)
+ tmp = get_config_value('AdvSendAdvert')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('AdvIntervalOpt')
+ self.assertEqual(tmp, 'off')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_salt-minion.py b/smoketest/scripts/cli/test_service_salt-minion.py
new file mode 100644
index 0000000..48a588b
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_salt-minion.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022-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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from socket import gethostname
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.utils.process import cmd
+
+PROCESS_NAME = 'salt-minion'
+SALT_CONF = '/etc/salt/minion'
+base_path = ['service', 'salt-minion']
+
+interface = 'dum4456'
+
+class TestServiceSALT(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceSALT, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', '100.64.0.1/16'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', interface])
+ super(TestServiceSALT, cls).tearDownClass()
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SALT config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # For an unknown reason on QEMU systems (e.g. where smoketests are executed
+ # from the CI) salt-minion process is not killed by systemd. Apparently
+ # no issue on VMWare.
+ if cmd('systemd-detect-virt') != 'kvm':
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_default(self):
+ servers = ['192.0.2.1', '192.0.2.2']
+
+ for server in servers:
+ self.cli_set(base_path + ['master', server])
+
+ self.cli_commit()
+
+ # commiconf = read_file() Check configured port
+ conf = read_file(SALT_CONF)
+ self.assertIn(f' - {server}', conf)
+
+ # defaults
+ hostname = gethostname()
+ self.assertIn(f'hash_type: sha256', conf)
+ self.assertIn(f'id: {hostname}', conf)
+ self.assertIn(f'mine_interval: 60', conf)
+
+ def test_options(self):
+ server = '192.0.2.3'
+ hash = 'sha1'
+ id = 'foo'
+ interval = '120'
+
+ self.cli_set(base_path + ['master', server])
+ self.cli_set(base_path + ['hash', hash])
+ self.cli_set(base_path + ['id', id])
+ self.cli_set(base_path + ['interval', interval])
+ self.cli_set(base_path + ['source-interface', interface])
+
+ self.cli_commit()
+
+ # commiconf = read_file() Check configured port
+ conf = read_file(SALT_CONF)
+ self.assertIn(f'- {server}', conf)
+
+ # defaults
+ self.assertIn(f'hash_type: {hash}', conf)
+ self.assertIn(f'id: {id}', conf)
+ self.assertIn(f'mine_interval: {interval}', conf)
+ self.assertIn(f'source_interface_name: {interface}', conf)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py
new file mode 100644
index 0000000..7d5eaa4
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_snmp.py
@@ -0,0 +1,264 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2021 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.template import is_ipv4
+from vyos.template import address_from_cidr
+from vyos.utils.process import call
+from vyos.utils.process import DEVNULL
+from vyos.utils.file import read_file
+from vyos.utils.process import process_named_running
+from vyos.version import get_version_data
+
+PROCESS_NAME = 'snmpd'
+SNMPD_CONF = '/etc/snmp/snmpd.conf'
+
+base_path = ['service', 'snmp']
+
+snmpv3_group = 'default_group'
+snmpv3_view = 'default_view'
+snmpv3_view_oid = '1'
+snmpv3_user = 'vyos'
+snmpv3_auth_pw = 'vyos12345678'
+snmpv3_priv_pw = 'vyos87654321'
+snmpv3_engine_id = '000000000000000000000002'
+
+def get_config_value(key):
+ tmp = read_file(SNMPD_CONF)
+ tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp)
+ return tmp[0]
+
+class TestSNMPService(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSNMPService, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SNMP config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_snmp_basic(self):
+ dummy_if = 'dum7312'
+ dummy_addr = '100.64.0.1/32'
+ contact = 'maintainers@vyos.io'
+ location = 'QEMU'
+
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr])
+
+ # Check if SNMP can be configured and service runs
+ clients = ['192.0.2.1', '2001:db8::1']
+ networks = ['192.0.2.128/25', '2001:db8:babe::/48']
+ listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)]
+ port = '5000'
+
+ for auth in ['ro', 'rw']:
+ community = 'VyOS' + auth
+ self.cli_set(base_path + ['community', community, 'authorization', auth])
+ for client in clients:
+ self.cli_set(base_path + ['community', community, 'client', client])
+ for network in networks:
+ self.cli_set(base_path + ['community', community, 'network', network])
+
+ for addr in listen:
+ self.cli_set(base_path + ['listen-address', addr, 'port', port])
+
+ self.cli_set(base_path + ['contact', contact])
+ self.cli_set(base_path + ['location', location])
+
+ self.cli_commit()
+
+ # verify listen address, it will be returned as
+ # ['unix:/run/snmpd.socket,udp:127.0.0.1:161,udp6:[::1]:161']
+ # thus we need to transfor this into a proper list
+ config = get_config_value('agentaddress')
+ expected = 'unix:/run/snmpd.socket'
+ self.assertIn(expected, config)
+ for addr in listen:
+ if is_ipv4(addr):
+ expected = f'udp:{addr}:{port}'
+ else:
+ expected = f'udp6:[{addr}]:{port}'
+ self.assertIn(expected, config)
+
+ config = get_config_value('sysDescr')
+ version_data = get_version_data()
+ self.assertEqual('VyOS ' + version_data['version'], config)
+
+ config = get_config_value('SysContact')
+ self.assertEqual(contact, config)
+
+ config = get_config_value('SysLocation')
+ self.assertEqual(location, config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
+
+ ## Check communities and default view RESTRICTED
+ for auth in ['ro', 'rw']:
+ community = 'VyOS' + auth
+ for addr in clients:
+ if is_ipv4(addr):
+ entry = auth + 'community ' + community + ' ' + addr + ' -V'
+ else:
+ entry = auth + 'community6 ' + community + ' ' + addr + ' -V'
+ config = get_config_value(entry)
+ expected = 'RESTRICTED'
+ self.assertIn(expected, config)
+ for addr in networks:
+ if is_ipv4(addr):
+ entry = auth + 'community ' + community + ' ' + addr + ' -V'
+ else:
+ entry = auth + 'community6 ' + community + ' ' + addr + ' -V'
+ config = get_config_value(entry)
+ expected = 'RESTRICTED'
+ self.assertIn(expected, config)
+ # And finally check global entry for RESTRICTED view
+ config = get_config_value('view RESTRICTED included .1')
+ self.assertIn('80', config)
+
+ def test_snmpv3_sha(self):
+ # Check if SNMPv3 can be configured with SHA authentication
+ # and service runs
+ self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id])
+ self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro'])
+ # check validate() - a view must be created before this can be committed
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['v3', 'view', 'default', 'oid', '1'])
+ self.cli_set(base_path + ['v3', 'group', 'default', 'view', 'default'])
+
+ # create user
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'plaintext-password', snmpv3_auth_pw])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'type', 'sha'])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'plaintext-password', snmpv3_priv_pw])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'type', 'aes'])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'group', 'default'])
+
+ self.cli_commit()
+
+ # commit will alter the CLI values - check if they have been updated:
+ hashed_password = '4e52fe55fd011c9c51ae2c65f4b78ca93dcafdfe'
+ tmp = self._session.show_config(base_path + ['v3', 'user', snmpv3_user, 'auth', 'encrypted-password']).split()[1]
+ self.assertEqual(tmp, hashed_password)
+
+ hashed_password = '54705c8de9e81fdf61ad7ac044fa8fe611ddff6b'
+ tmp = self._session.show_config(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'encrypted-password']).split()[1]
+ self.assertEqual(tmp, hashed_password)
+
+ # TODO: read in config file and check values
+
+ # Try SNMPv3 connection
+ tmp = call(f'snmpwalk -v 3 -u {snmpv3_user} -a SHA -A {snmpv3_auth_pw} -x AES -X {snmpv3_priv_pw} -l authPriv 127.0.0.1', stdout=DEVNULL)
+ self.assertEqual(tmp, 0)
+
+ def test_snmpv3_md5(self):
+ # Check if SNMPv3 can be configured with MD5 authentication
+ # and service runs
+ self.cli_set(base_path + ['v3', 'engineid', snmpv3_engine_id])
+
+ # create user
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'plaintext-password', snmpv3_auth_pw])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'type', 'md5'])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'plaintext-password', snmpv3_priv_pw])
+ self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'type', 'des'])
+
+ # check validate() - user requires a group to be created
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', snmpv3_group])
+
+ self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'mode', 'ro'])
+ # check validate() - a view must be created before this can be comitted
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid])
+ self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view])
+
+ self.cli_commit()
+
+ # commit will alter the CLI values - check if they have been updated:
+ hashed_password = '4c67690d45d3dfcd33d0d7e308e370ad'
+ tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1]
+ self.assertEqual(tmp, hashed_password)
+
+ hashed_password = 'e11c83f2c510540a3c4de84ee66de440'
+ tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1]
+ self.assertEqual(tmp, hashed_password)
+
+ tmp = read_file(SNMPD_CONF)
+ # views
+ self.assertIn(f'view {snmpv3_view} included .{snmpv3_view_oid}', tmp)
+ # group
+ self.assertIn(f'group {snmpv3_group} usm {snmpv3_user}', tmp)
+ # access
+ self.assertIn(f'access {snmpv3_group} "" usm auth exact {snmpv3_view} none none', tmp)
+
+ # Try SNMPv3 connection
+ tmp = call(f'snmpwalk -v 3 -u {snmpv3_user} -a MD5 -A {snmpv3_auth_pw} -x DES -X {snmpv3_priv_pw} -l authPriv 127.0.0.1', stdout=DEVNULL)
+ self.assertEqual(tmp, 0)
+
+ def test_snmpv3_view_exclude(self):
+ snmpv3_view_oid_exclude = ['1.3.6.1.2.1.4.21', '1.3.6.1.2.1.4.24']
+
+ self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view])
+ self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid])
+
+ for excluded in snmpv3_view_oid_exclude:
+ self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid, 'exclude', excluded])
+
+ self.cli_commit()
+
+ tmp = read_file(SNMPD_CONF)
+ # views
+ self.assertIn(f'view {snmpv3_view} included .{snmpv3_view_oid}', tmp)
+ for excluded in snmpv3_view_oid_exclude:
+ self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp)
+
+ def test_snmp_script_extensions(self):
+ extensions = {
+ 'default': 'snmp_smoketest_extension_script.sh',
+ 'external': '/run/external_snmp_smoketest_extension_script.sh'
+ }
+
+ for key, val in extensions.items():
+ self.cli_set(base_path + ['script-extensions', 'extension-name', key, 'script', val])
+ self.cli_commit()
+
+ self.assertEqual(get_config_value('extend default'), f'/config/user-data/{extensions["default"]}')
+ self.assertEqual(get_config_value('extend external'), extensions["external"])
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
new file mode 100644
index 0000000..d8e325e
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import paramiko
+import re
+import unittest
+
+from pwd import getpwall
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.xml_ref import default_value
+
+PROCESS_NAME = 'sshd'
+SSHD_CONF = '/run/sshd/sshd_config'
+base_path = ['service', 'ssh']
+
+key_rsa = '/etc/ssh/ssh_host_rsa_key'
+key_dsa = '/etc/ssh/ssh_host_dsa_key'
+key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
+
+def get_config_value(key):
+ tmp = read_file(SSHD_CONF)
+ tmp = re.findall(f'\n?{key}\s+(.*)', tmp)
+ return tmp
+
+class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceSSH, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['vrf'])
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SSH config
+ self.cli_delete(base_path)
+ self.cli_delete(['vrf'])
+ self.cli_commit()
+
+ self.assertTrue(os.path.isfile(key_rsa))
+ self.assertTrue(os.path.isfile(key_dsa))
+ self.assertTrue(os.path.isfile(key_ed25519))
+
+ # Established SSH connections remains running after service is stopped.
+ # We can not use process_named_running here - we rather need to check
+ # that the systemd service is no longer running
+ self.assertFalse(is_systemd_service_running(PROCESS_NAME))
+
+ def test_ssh_default(self):
+ # Check if SSH service runs with default settings - used for checking
+ # behavior of <defaultValue> in XML definition
+ self.cli_set(base_path)
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port agains CLI default value
+ port = get_config_value('Port')
+ cli_default = default_value(base_path + ['port'])
+ self.assertEqual(port, cli_default)
+
+ def test_ssh_single_listen_address(self):
+ # Check if SSH service can be configured and runs
+ self.cli_set(base_path + ['port', '1234'])
+ self.cli_set(base_path + ['disable-host-validation'])
+ self.cli_set(base_path + ['disable-password-authentication'])
+ self.cli_set(base_path + ['loglevel', 'verbose'])
+ self.cli_set(base_path + ['client-keepalive-interval', '100'])
+ self.cli_set(base_path + ['listen-address', '127.0.0.1'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ port = get_config_value('Port')[0]
+ self.assertTrue("1234" in port)
+
+ # Check DNS usage
+ dns = get_config_value('UseDNS')[0]
+ self.assertTrue("no" in dns)
+
+ # Check PasswordAuthentication
+ pwd = get_config_value('PasswordAuthentication')[0]
+ self.assertTrue("no" in pwd)
+
+ # Check loglevel
+ loglevel = get_config_value('LogLevel')[0]
+ self.assertTrue("VERBOSE" in loglevel)
+
+ # Check listen address
+ address = get_config_value('ListenAddress')[0]
+ self.assertTrue("127.0.0.1" in address)
+
+ # Check keepalive
+ keepalive = get_config_value('ClientAliveInterval')[0]
+ self.assertTrue("100" in keepalive)
+
+ def test_ssh_multiple_listen_addresses(self):
+ # Check if SSH service can be configured and runs with multiple
+ # listen ports and listen-addresses
+ ports = ['22', '2222', '2223', '2224']
+ for port in ports:
+ self.cli_set(base_path + ['port', port])
+
+ addresses = ['127.0.0.1', '::1']
+ for address in addresses:
+ self.cli_set(base_path + ['listen-address', address])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ tmp = get_config_value('Port')
+ for port in ports:
+ self.assertIn(port, tmp)
+
+ # Check listen address
+ tmp = get_config_value('ListenAddress')
+ for address in addresses:
+ self.assertIn(address, tmp)
+
+ def test_ssh_vrf_single(self):
+ vrf = 'mgmt'
+ # Check if SSH service can be bound to given VRF
+ self.cli_set(base_path + ['vrf', vrf])
+
+ # VRF does yet not exist - an error must be thrown
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(['vrf', 'name', vrf, 'table', '1338'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check for process in VRF
+ tmp = cmd(f'ip vrf pids {vrf}')
+ self.assertIn(PROCESS_NAME, tmp)
+
+ def test_ssh_vrf_multi(self):
+ # Check if SSH service can be bound to multiple VRFs
+ vrfs = ['red', 'blue', 'green']
+ for vrf in vrfs:
+ self.cli_set(base_path + ['vrf', vrf])
+
+ # VRF does yet not exist - an error must be thrown
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ table = 12345
+ for vrf in vrfs:
+ self.cli_set(['vrf', 'name', vrf, 'table', str(table)])
+ table += 1
+
+ # commit changes
+ self.cli_commit()
+
+ # Check for process in VRF
+ for vrf in vrfs:
+ tmp = cmd(f'ip vrf pids {vrf}')
+ self.assertIn(PROCESS_NAME, tmp)
+
+ def test_ssh_login(self):
+ # Perform SSH login and command execution with a predefined user. The
+ # result (output of uname -a) must match the output if the command is
+ # run natively.
+ #
+ # We also try to login as an invalid user - this is not allowed to work.
+
+ test_user = 'ssh_test'
+ test_pass = 'v2i57DZs8idUwMN3VC92'
+ test_command = 'uname -a'
+
+ self.cli_set(base_path)
+ self.cli_set(['system', 'login', 'user', test_user, 'authentication', 'plaintext-password', test_pass])
+
+ # commit changes
+ self.cli_commit()
+
+ # Login with proper credentials
+ output, error = self.ssh_send_cmd(test_command, test_user, test_pass)
+ # verify login
+ self.assertFalse(error)
+ self.assertEqual(output, cmd(test_command))
+
+ # Login with invalid credentials
+ with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
+ output, error = self.ssh_send_cmd(test_command, 'invalid_user', 'invalid_password')
+
+ self.cli_delete(['system', 'login', 'user', test_user])
+ self.cli_commit()
+
+ # After deletion the test user is not allowed to remain in /etc/passwd
+ usernames = [x[0] for x in getpwall()]
+ self.assertNotIn(test_user, usernames)
+
+ def test_ssh_dynamic_protection(self):
+ # check sshguard service
+
+ SSHGUARD_CONFIG = '/etc/sshguard/sshguard.conf'
+ SSHGUARD_WHITELIST = '/etc/sshguard/whitelist'
+ SSHGUARD_PROCESS = 'sshguard'
+ block_time = '123'
+ detect_time = '1804'
+ port = '22'
+ threshold = '10'
+ allow_list = ['192.0.2.0/24', '2001:db8::/48']
+
+ self.cli_set(base_path + ['dynamic-protection', 'block-time', block_time])
+ self.cli_set(base_path + ['dynamic-protection', 'detect-time', detect_time])
+ self.cli_set(base_path + ['dynamic-protection', 'threshold', threshold])
+ for allow in allow_list:
+ self.cli_set(base_path + ['dynamic-protection', 'allow-from', allow])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check configured port
+ tmp = get_config_value('Port')
+ self.assertIn(port, tmp)
+
+ # Check sshgurad service
+ self.assertTrue(process_named_running(SSHGUARD_PROCESS))
+
+ sshguard_lines = [
+ f'THRESHOLD={threshold}',
+ f'BLOCK_TIME={block_time}',
+ f'DETECTION_TIME={detect_time}'
+ ]
+
+ tmp_sshguard_conf = read_file(SSHGUARD_CONFIG)
+ for line in sshguard_lines:
+ self.assertIn(line, tmp_sshguard_conf)
+
+ tmp_whitelist_conf = read_file(SSHGUARD_WHITELIST)
+ for allow in allow_list:
+ self.assertIn(allow, tmp_whitelist_conf)
+
+ # Delete service ssh dynamic-protection
+ # but not service ssh itself
+ self.cli_delete(base_path + ['dynamic-protection'])
+ self.cli_commit()
+
+ self.assertFalse(process_named_running(SSHGUARD_PROCESS))
+
+
+ # Network Device Collaborative Protection Profile
+ def test_ssh_ndcpp(self):
+ ciphers = ['aes128-cbc', 'aes128-ctr', 'aes256-cbc', 'aes256-ctr']
+ host_key_algs = ['sk-ssh-ed25519@openssh.com', 'ssh-rsa', 'ssh-ed25519']
+ kexes = ['diffie-hellman-group14-sha1', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521']
+ macs = ['hmac-sha1', 'hmac-sha2-256', 'hmac-sha2-512']
+ rekey_time = '60'
+ rekey_data = '1024'
+
+ for cipher in ciphers:
+ self.cli_set(base_path + ['ciphers', cipher])
+ for host_key in host_key_algs:
+ self.cli_set(base_path + ['hostkey-algorithm', host_key])
+ for kex in kexes:
+ self.cli_set(base_path + ['key-exchange', kex])
+ for mac in macs:
+ self.cli_set(base_path + ['mac', mac])
+ # Optional rekey parameters
+ self.cli_set(base_path + ['rekey', 'data', rekey_data])
+ self.cli_set(base_path + ['rekey', 'time', rekey_time])
+
+ # commit changes
+ self.cli_commit()
+
+ ssh_lines = ['Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr',
+ 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519',
+ 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512',
+ 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521',
+ 'RekeyLimit 1024M 60M'
+ ]
+ tmp_sshd_conf = read_file(SSHD_CONF)
+
+ for line in ssh_lines:
+ self.assertIn(line, tmp_sshd_conf)
+
+ def test_ssh_pubkey_accepted_algorithm(self):
+ algs = ['ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384',
+ 'ecdsa-sha2-nistp521', 'ssh-dss', 'ssh-rsa', 'rsa-sha2-256',
+ 'rsa-sha2-512'
+ ]
+
+ expected = 'PubkeyAcceptedAlgorithms '
+ for alg in algs:
+ self.cli_set(base_path + ['pubkey-accepted-algorithm', alg])
+ expected = f'{expected}{alg},'
+ expected = expected[:-1]
+
+ self.cli_commit()
+ tmp_sshd_conf = read_file(SSHD_CONF)
+ self.assertIn(expected, tmp_sshd_conf)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_stunnel.py b/smoketest/scripts/cli/test_service_stunnel.py
new file mode 100644
index 0000000..3aeffd0
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_stunnel.py
@@ -0,0 +1,624 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+
+PROCESS_NAME = 'stunnel'
+STUNNEL_CONF = '/run/stunnel/stunnel.conf'
+base_path = ['service', 'stunnel']
+
+ca_certificate = """
+MIIDnTCCAoWgAwIBAgIUcSMo/zT/GUAyH3uM3Hr3qjCDmMUwDQYJKoZIhvcNAQELBQAwVzELMAkGA1U
+EBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVn
+lPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yNDA2MDQwNjU1MDFaFw0yOTA2MDMwNjU1MDFaMFcxCzAJB
+gNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoM
+BFZ5T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzN7B
+Zw0OBBgeGL7KCKdDIUfBEhh08+3V8Nm7K23mU/pYd3bR5WXt9VWkW5YWUw1hr1N3qEQ2AZX8TrIDj37
+zzy1jyDCvJHGWnKTOOAboNIInP+PvUQrSH8SDAw/+/KjKKgM069NFhGq9TTHg4BAYC0GsZL+JE3Ptee
+cIVmekf5Dw+vnD0Mlwx5Ouaf/9OwRcGhfwEkIORQLXDuMayOI/JdFbaDVlA6Z/d8GLp3Xlc8/l5XFtg
+fvMNQSB9B69Cs4qwU/yey8tPWeDBiW6Cx2XOnKqiNBaCY1BzvSH+hmHcos1DOLHgEZ3d2zaNn2mrhmB
+Ry7/5Ww7O5PoF00OB9WHFAgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB
+0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAdBgNVHQ4EFgQU4zgMpOMOweRZUbeNewJnh5xZL
+XwwDQYJKoZIhvcNAQELBQADggEBAAEK+jXvCKuC/n8qu9XFcLYfO3kUKPlXD30V61KRZilHyYGYu0MY
+sSNeX8+K7CpeAo06HHocrrDfCKltoLFuix7qblr2DEub+v3V21pllMfThkz9FsXWFGfmOyI7sXNXUg9
+cVQHzj2SvMj+IfnJoCIuYnigmlKVTuxV31iYv2RpML/PBw29xI0G/AsmXZK4wOQ0eA9gU+ggURE98hG
+8f4DRpGVnlyP1d+P2Va0bsl3Yek9QfrotnmE1EzwZzPZyCL9rv8oDjfJ98O3YqoNSRNvD+Glke2ZlTj
+WFw+uCj0GTki5+V40E9X9Rwcje+s/5zWDBfu0akufcI1nsu++rZz/s=
+"""
+
+ca_private_key = """
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCzN7BZw0OBBgeGL7KCKdDIUfBEhh0
+8+3V8Nm7K23mU/pYd3bR5WXt9VWkW5YWUw1hr1N3qEQ2AZX8TrIDj37zzy1jyDCvJHGWnKTOOAboNII
+nP+PvUQrSH8SDAw/+/KjKKgM069NFhGq9TTHg4BAYC0GsZL+JE3PteecIVmekf5Dw+vnD0Mlwx5Ouaf
+/9OwRcGhfwEkIORQLXDuMayOI/JdFbaDVlA6Z/d8GLp3Xlc8/l5XFtgfvMNQSB9B69Cs4qwU/yey8tP
+WeDBiW6Cx2XOnKqiNBaCY1BzvSH+hmHcos1DOLHgEZ3d2zaNn2mrhmBRy7/5Ww7O5PoF00OB9WHFAgM
+BAAECggEAHFC/pacCutdrh+lwUD1wFb5QclsoMnLeYJYvEhD0GDTTHfvh4ExhhO9iL7Jq1RK6HStgNn
+OkSPWASuj14kr+zRwDPRbsMhWw/+S0FwsxzJIoA/poO2SgplvUG3C8LwVpP9XS1y5ICIoRSl1qHxuPo
+ZExYqTcoJmzg31ES2pqWVXPx14DdpE6yvSL2XwFS4mb291OkydnvKSBcK0MwgEWLQHouzMjihJ1MCXx
+7NXsOxFX76OpmywMW7EtTLEngxL9b61hCYwWeNxmx9pN8qRzmvayKl40VLyqAlVcElZ7aEK0+O/Qpsu
+QhsFRjA4HcXUqlHbvh92OqX+QmBU2RIZ27wKBgQDnJ8E8cJOlJ9ZvFBfw8az49IX9E72oxb2yaXm0Eo
+OQ2Jz88+b2jzWqf3wdGvigNO25DbdYYodR7iJJo4OYPuyAnkJMWdPQ91ADo7ABwJiBqtUHC+Gvq5Rmm
+I2T3T4+Vqu5Sa8lVfHWfv7Pnb++I5/7bH+VuGspyf+0NcpPh9UfIwKBgQDGet1wh0+2378HnnQNb10w
+wxDiMC2hP+3RGPB6bKHLJ60LE+Ed2KFY+j8Q1+jQk9eMe6A75bwB/q6rMO1evpauCHTJoA863HxXtuL
+P9ozVpDk9k4TbiSOsD0s8TXL3XG1ANshk4VfuLboKj4MEwiuxfGt6QGpsgLfHcmlkFIM99wKBgQDeea
+C97wvrVOBJoGk6eSAlrBKZdTqBCXB+Go4MBhWifxj5TDXq8AKSyohF6wOIDekOxmjEJHBhJnTRsxKgo
+U82qxrcKUh4Qs878Xsg9KDTi/vkAEeCr/zwkbsRqUqS7Q/yET0FDibobuoIIKe+9MKxVcel7g0V91in
+tW22BeHVSQKBgQCKctwSiaCWTP8A/ouvb3ZO9FLLpJW/vEtUpxPgIfS+NH/lkUlfu2PZID5rrmAtVmN
+uEDJWdcsujQwkSC3cABA1d5qXpnnZMkHeIamXLUFSKYrwI/3x8XibpdNyTgga+jMPLuecTwA6GVWD1l
+WrNRKrbMG/9j0GUMdhbbKMaC6gQwKBgQCC9EUZBqCXS167OZNPQN4oKx6nJ9uTKUVyPigr12cMpPL6t
+JwZAVVwSnyg+cVznNrMhAnG547c0NnoOe+nd9zczLJOuQHHLSMZUH08c2ZWtwpwbHDWI55hfZL4te8e
+dEcxanXNYAfSMMtOoA+LmcCtfvqld/EucAN4mKTPGmWPQg==
+"""
+
+srv_certificate = """
+MIIDsTCCApmgAwIBAgIUIvMZU3zc3iYl6JzbDLSvr8NOK5swDQYJKoZIhvcNAQELBQAwVzELMAkGA1U
+EBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVn
+lPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yNDA2MDQwNjU1NDVaFw0yNTA2MDQwNjU1NDVaMFcxCzAJB
+gNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoM
+BFZ5T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtani
+zx0h1fEH0pMRBB7V7nUAnSOAiCRUNpeTz6RoUqH0y/UaxM+kqitUm+MSAWxEJAXW4ZlxNzU+tC6DOwP
+d+7/rZsT6fKeCbMIs8Es9VaXd2sZzb7DajEygeyIy1b3JGXIiNJ9KcOxzhmu5VHe+6qLCO3FDt4iFIr
+HXJxwQKm8qL6zgn7f9kboQYBHKOhY8x+ghkhLYAwMlvIHGwjF+I/p65J1LOBhAsmOLcX0/CygKXz5qe
+wyG16zNft6OWPIOBTs56NnNlW6EdqomxBM5SWr888qEjUy0ruUpAH4Ug8SloL+AeDW+TqUUcfoOiTi9
+3ZJ/t9YRj0+wQw4vakpUTAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1
+UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTCubAbczcJE76YabOv+2oVV1zNSzAfBgNVHSMEGDAWg
+BTjOAyk4w7B5FlRt417AmeHnFktfDANBgkqhkiG9w0BAQsFAAOCAQEAjW9ovWDgEoUi8DWwNVtudKiT
+6ylJTSMqY7C+qJlRHpnZ64TNZFXI0BldYZr0QXGsZ257m9m9BiUcZr6UR0hywy4SiyxuteufniKIp9E
+vqv0aJhdTXO+l5msaGWu7YvWYqXW0m3rA9oiNYyBcNSFzlwiyvztYUmFFPrvhFHVSt+DuxZSltdf78G
+exS4YRMCTI+cuCfBt65Vkss4bNJH7kyWVc5aSQ/vKitMxB10gzsUa7psgS6LsBWxnehd3HKBPaHiWG9
+ssHKhHJWfjifgz0K1Y0/vi33USPJ1cBhWWx/dolXWmSmpfqXpD3Q84YjVWIRnFpQzwbT650v/H+fwB1
+zw==
+"""
+
+srv_private_key = """
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtanizx0h1fEH0pMRBB7V7nUAnSOA
+iCRUNpeTz6RoUqH0y/UaxM+kqitUm+MSAWxEJAXW4ZlxNzU+tC6DOwPd+7/rZsT6fKeCbMIs8Es9VaX
+d2sZzb7DajEygeyIy1b3JGXIiNJ9KcOxzhmu5VHe+6qLCO3FDt4iFIrHXJxwQKm8qL6zgn7f9kboQYB
+HKOhY8x+ghkhLYAwMlvIHGwjF+I/p65J1LOBhAsmOLcX0/CygKXz5qewyG16zNft6OWPIOBTs56NnNl
+W6EdqomxBM5SWr888qEjUy0ruUpAH4Ug8SloL+AeDW+TqUUcfoOiTi93ZJ/t9YRj0+wQw4vakpUTAgM
+BAAECggEAEa54SyBSb4QxYg/bYM636Y2G3oU229GK6il+4YMOy99tZeG0L6+IInR7DO5ddBbqSD2esq
+QL3PTw9EcUvi/9AYjXeL3H5vOeo+7Rq4OMIfx5wp+Ty6AB5s5hD1kfG7AWzzzHwYNiHS2Gdtb/XldfO
+5bP6xO5/rSenynSbWCTir8yakfoDenT12CXWzU+T10MKhoTXb/Uao+bMjziKEviK6OWq0vsLlDqyOAE
+Va685s7T0vHTfSs+yK9pqVypHXbkH1nJCoi9P4pcJ4Sslc3meStv3bqg8T62Ufv8QkQLTfJyKZlR1aV
+9ZjWT84YoH1XRnnkAZ+BMC267sHeBJbu6EQKBgQDbIUjQh/iPlkK77tFa//gSMD5ouJtuwtdS1MJ44p
+C81A140vjpkSCdU8zWRifi+akR1k6fXCp6VFUFvTCXkGlpbD4TNjCCRJjS4SoQ89jEnePQ2iS59jkn3
+V3OPNitOzk0jEm/x3R5wNdPlSX6+pLiUZAtMgcmCMv205VOkeqx8QKBgQDKmB3FtEfkKRrGkOJgaEkY
+iXp9b0Uy8peBoTcdqMMnXSlm9+CfIdhSwbQDiAhEcUeCE/S6TDqaMS+ekKFfs6kDlaJMStGsy81lwr5
+W/oZOldajDCu1CDInc+czkep10lsHdQwr71zXntiK3Fwq8Mr3ROBSpaH+DWIjILiQIOMzQwKBgQC7Mt
+UUqIQUjkZWbG/XcMLJLwOxzLukRLlUXsQAJ3WEixczN/BDAKM/JB7ikq5yfdwMi+tAwqjbNn4n1/bSF
+CGpWToyiWGpd9aimI6qStbNKSE9A47KeulbAAaqMFreqrB1Dr/WIRuFA9QsfXsjzLp8szcbFRj8ShmM
+tDZiF8/K0QKBgQCYbb0wzESu8RJZRhddC/m7QWzsxXReMdI2UTLj2N8EVf7ZnzTc5h0Znu4vHgGCZWy
+0/QjLxqDs9Ibsmcsg807+CG51UnHRvgFLSCvnzlcE943nXTfhXEpIDtdsoKO0hFHDGZjP0aeb/8LTL5
+sVH9jGFIdnB4ILYMxuu6bBokzvewKBgBWbjPppjrM46bZ0rwEYCcG0F/k6TKkw4pjyrDR4B0XsrqTjK
+0yz0ga7FHe10saeS2cXMqygdkjhWLZ6Zhrp0LAEzhEvdiBYeRH37J9Bvwo2YIHakox4hJCSXNnELs/A
+GhUb5YIISNnZnZZeUD/Z0IJXJryjk9eUbhDCgEZRVzeT
+"""
+
+
+def get_config_value(key):
+ tmp = read_file(STUNNEL_CONF)
+ tmp = re.findall(f'\n?{key}\s+(.*)', tmp)
+ return tmp
+
+
+def read_config():
+ config = {'global': {},
+ 'services': {}
+ }
+ service_pattern = re.compile(r'\[(.+?)\]')
+ key_value_pattern = re.compile(r'(\S+)\s*=\s*(.+)')
+ service = None
+
+ for line in read_file(STUNNEL_CONF).split('\n'):
+ line.strip()
+ if not line or line.startswith(';'):
+ continue
+ if service_pattern.match(line):
+ service = line.strip('[]')
+ config['services'][service] = {}
+ key_value_match = key_value_pattern.match(line)
+ if key_value_match:
+ key, value = key_value_match.group(1), key_value_match.group(2)
+ if service:
+ apply_value(config['services'][service], key, value)
+ else:
+ apply_value(config['global'], key, value)
+
+ return config
+
+
+def apply_value(service_config, key, value):
+ if service_config.get(key) is None:
+ service_config[key] = value
+ else:
+ if not isinstance(service_config[key], list):
+ service_config[key] = [
+ service_config[key]]
+ else:
+ service_config[key].append(value)
+
+
+class TestServiceStunnel(VyOSUnitTestSHIM.TestCase):
+ maxDiff = None
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceStunnel, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.is_valid_conf = True
+
+ def tearDown(self):
+ # Check for running process
+ if self.is_valid_conf:
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ self.is_valid_conf = True
+ # delete testing Stunnel config
+ self.cli_delete(base_path)
+ self.cli_delete(['pki'])
+ self.cli_commit()
+
+ # Check for stopped process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def set_pki(self):
+ self.cli_set(['pki', 'ca', 'ca-1', 'certificate', ca_certificate.replace('\n','')])
+ self.cli_set(['pki', 'ca', 'ca-1', 'private', 'key', ca_private_key.replace('\n','')])
+ self.cli_set(['pki', 'certificate', 'srv-1', 'certificate', srv_certificate.replace('\n','')])
+ self.cli_set(['pki', 'certificate', 'srv-1', 'private', 'key', srv_private_key.replace('\n','')])
+ self.cli_commit()
+
+ def test_01_stunnel_simple_client(self):
+ service = 'app1'
+ self.cli_set(base_path + ['client', service, 'connect', 'port', '9001'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['client', service, 'listen', 'port', '8001'])
+
+ self.cli_commit()
+ config = read_config()
+
+ self.assertEqual('/run/stunnel/stunnel.pid', config['global']['pid'])
+ self.assertEqual('notice', config['global']['debug'])
+ self.assertListEqual([service], list(config['services']))
+ self.assertEqual('8001', config['services'][service]['accept'])
+ self.assertEqual('9001', config['services'][service]['connect'])
+ self.assertEqual('yes', config['services'][service]['client'])
+
+ def test_02_stunnel_simple_server(self):
+ service = 'ser1'
+ self.set_pki()
+ self.cli_set(base_path + ['server', service, 'connect', 'port', '8080'])
+ self.cli_set(base_path + ['server', service, 'ssl', 'certificate', 'srv-1'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['server', service, 'listen', 'port', '9001'])
+
+ self.cli_commit()
+ config = read_config()
+
+ self.assertEqual('/run/stunnel/stunnel.pid', config['global']['pid'])
+ self.assertEqual('notice', config['global']['debug'])
+ self.assertListEqual([service], list(config['services']))
+ self.assertEqual('9001', config['services'][service]['accept'])
+ self.assertEqual('8080', config['services'][service]['connect'])
+ self.assertIsNone(config['services'][service].get('client'))
+ self.assertEqual('/run/stunnel/server-ser1-srv-1.pem', config['services'][service]['cert'])
+ self.assertEqual('/run/stunnel/server-ser1-srv-1.pem.key', config['services'][service]['key'])
+
+ def test_03_multy_services(self):
+ self.set_pki()
+ clients = ['app1', 'app2', 'app3']
+ servers = ['serv1', 'serv2', 'serv3']
+ port = 80
+ for service in clients:
+ port += 1
+ self.cli_set(base_path + ['client', service, 'listen', 'port', f'{port}'])
+ port += 1
+ self.cli_set(base_path + ['client', service, 'connect', 'port', f'{port}'])
+ if service == 'app2':
+ self.cli_set(base_path + ['client', service, 'connect', 'address', f'192.168.0.10'])
+ self.cli_set(base_path + ['client', service, 'listen', 'address', '127.0.0.1'])
+ self.cli_set(base_path + ['client', service, 'protocol', 'connect'])
+ self.cli_set(base_path + ['client', service, 'options', 'authentication', 'basic'])
+ self.cli_set(base_path + ['client', service, 'options', 'domain', 'basic.com'])
+ self.cli_set(base_path + ['client', service, 'options', 'host', 'address', '127.0.0.1'])
+ self.cli_set(base_path + ['client', service, 'options', 'host', 'port', '5000'])
+ self.cli_set(base_path + ['client', service, 'options', 'password', 'test_pass'])
+ self.cli_set(base_path + ['client', service, 'options', 'username', 'test'])
+ if service == 'app3':
+ self.cli_set(base_path + ['client', service, 'ssl', 'ca-certificate', 'ca-1'])
+ self.cli_set(base_path + ['client', service, 'ssl', 'certificate', 'srv-1'])
+
+ for service in servers:
+ port += 1
+ self.cli_set(base_path + ['server', service, 'listen', 'port', f'{port}'])
+ port += 1
+ self.cli_set(base_path + ['server', service, 'connect', 'port', f'{port}'])
+ self.cli_set(base_path + ['server', service, 'ssl', 'certificate', 'srv-1'])
+ if service == 'serv2':
+ self.cli_set(base_path + ['server', service, 'ssl', 'ca-certificate', 'ca-1'])
+ self.cli_set(base_path + ['server', service, 'connect', 'address', f'google.com'])
+ self.cli_set(base_path + ['server', service, 'listen', 'address', f'127.0.0.1'])
+ if service == 'serv3':
+ self.cli_set(base_path + ['server', service, 'connect', 'address', f'10.18.105.10'])
+ self.cli_set(base_path + ['server', service, 'protocol', 'imap'])
+
+ self.cli_commit()
+ config = read_config()
+
+ self.assertEqual('/run/stunnel/stunnel.pid', config['global']['pid'])
+ self.assertListEqual(clients + servers, list(config['services']))
+ self.assertDictEqual(config['services'], {
+ 'app1': {
+ 'client': 'yes',
+ 'accept': '81',
+ 'connect': '82'
+ },
+ 'app2': {
+ 'client': 'yes',
+ 'accept': '127.0.0.1:83',
+ 'connect': '192.168.0.10:84',
+ 'protocol': 'connect',
+ 'protocolAuthentication': 'basic',
+ 'protocolDomain': 'basic.com',
+ 'protocolHost': '127.0.0.1:5000',
+ 'protocolPassword': 'test_pass',
+ 'protocolUsername': 'test'
+ },
+ 'app3': {
+ 'client': 'yes',
+ 'accept': '85',
+ 'connect': '86',
+ 'CApath': '/run/stunnel/ca',
+ 'CAfile': 'client-app3-ca.pem',
+ 'cert': '/run/stunnel/client-app3-srv-1.pem',
+ 'key': '/run/stunnel/client-app3-srv-1.pem.key'
+ },
+ 'serv1': {
+ 'accept': '87',
+ 'connect': '88',
+ 'cert': '/run/stunnel/server-serv1-srv-1.pem',
+ 'key': '/run/stunnel/server-serv1-srv-1.pem.key'
+ },
+ 'serv2': {
+ 'accept': '127.0.0.1:89',
+ 'connect': 'google.com:90',
+ 'CApath': '/run/stunnel/ca',
+ 'CAfile': 'server-serv2-ca.pem',
+ 'cert': '/run/stunnel/server-serv2-srv-1.pem',
+ 'key': '/run/stunnel/server-serv2-srv-1.pem.key'
+ },
+ 'serv3': {
+ 'accept': '91',
+ 'connect': '10.18.105.10:92',
+ 'protocol': 'imap',
+ 'cert': '/run/stunnel/server-serv3-srv-1.pem',
+ 'key': '/run/stunnel/server-serv3-srv-1.pem.key'
+ }
+ })
+
+ def test_04_cert_problems(self):
+ service = 'app1'
+ self.cli_set(base_path + ['client', service, 'connect', 'port', '9001'])
+ self.cli_set(base_path + ['client', service, 'listen', 'port', '8001'])
+ self.cli_set(base_path + ['client', service, 'ssl', 'ca-certificate', 'ca-2'])
+
+ # ca not exist in pki
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['client', service, 'ssl', 'ca-certificate', 'ca-2'])
+ self.cli_set(base_path + ['client', service, 'ssl', 'certificate', 'srv-2'])
+
+ # cert not exist in pki
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path)
+
+ self.cli_set(base_path + ['server', service, 'connect', 'port', '8080'])
+ self.cli_set(base_path + ['server', service, 'listen', 'port', '9001'])
+
+ # Create server without any cert
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['server', service, 'ssl', 'ca-certificate', 'ca-2'])
+ # ca not exist in pki
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['server', service, 'ssl', 'ca-certificate', 'ca-2'])
+ self.cli_set(base_path + ['server', service, 'ssl', 'certificate', 'srv-2'])
+ # cert not exist in pki
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.is_valid_conf = False
+
+ def test_05_psk_auth(self):
+ modes = ['client', 'server']
+ psk_id_1 = 'psk_id_1'
+ psk_secret_1 = '1234567890ABCDEF1234567890ABCDEF'
+ psk_id_2 = 'psk_id_2'
+ psk_secret_2 = '1234567890ABCDEF1234567890ABCDEA'
+ expected_config = {
+ 'global': {'pid': '/run/stunnel/stunnel.pid',
+ 'debug': 'notice'},
+ 'services': {}}
+ port = 8000
+ for mode in modes:
+ service = f'{mode}-one'
+ psk_secrets = f'/run/stunnel/psk/{mode}_{service}.txt'
+ expected_config['services'][service] = {
+ 'PSKsecrets': psk_secrets,
+ }
+ port += 1
+ expected_config['services'][service]['accept'] = f'{port}'
+ self.cli_set(base_path + [mode, service, 'listen', 'port', f'{port}'])
+ port += 1
+ expected_config['services'][service]['connect'] = f'{port}'
+ self.cli_set(base_path + [mode, service, 'connect', 'port', f'{port}'])
+ self.cli_set(base_path + [mode, service, 'psk', 'smoketest1', 'id', psk_id_1])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(base_path + [mode, service, 'psk', 'smoketest1', 'secret', '123'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(base_path + [mode, service, 'psk', 'smoketest1', 'secret', '1234567890ABCDEF1234567890ABCDEZ'])
+ self.cli_set(base_path + [mode, service, 'psk', 'smoketest1', 'secret', psk_secret_1])
+ self.cli_set(base_path + [mode, service, 'psk', 'smoketest2', 'id', psk_id_2])
+ self.cli_set(base_path + [mode, service, 'psk', 'smoketest2', 'secret', psk_secret_2])
+ if mode != 'server':
+ expected_config['services'][service]['client'] = 'yes'
+
+ self.cli_commit()
+ config = read_config()
+
+ self.assertDictEqual(expected_config, config)
+
+ self.assertListEqual([f'{psk_id_1}:{psk_secret_1}',
+ f'{psk_id_2}:{psk_secret_2}'],
+ [line for line in read_file(psk_secrets).split('\n')])
+
+ def test_06_socks_proxy(self):
+ server_port = '9001'
+ client_port = '9000'
+ srv_name = 'srv-one'
+ cli_name = 'cli-one'
+ expected_config = {
+ 'global': {'pid': '/run/stunnel/stunnel.pid',
+ 'debug': 'notice'},
+ 'services': {
+ 'cli-one': {
+ 'PSKsecrets': f'/run/stunnel/psk/client_{cli_name}.txt',
+ 'client': 'yes',
+ 'accept': client_port,
+ 'connect': server_port
+ },
+ 'srv-one': {
+ 'PSKsecrets': f'/run/stunnel/psk/server_{srv_name}.txt',
+ 'accept': server_port,
+ 'protocol': 'socks'
+ }
+ }}
+
+ self.cli_set(base_path + ['server', srv_name, 'listen', 'port', server_port])
+ self.cli_set(base_path + ['server', srv_name, 'connect', 'port', '9005'])
+ self.cli_set(base_path + ['server', srv_name, 'protocol', 'socks'])
+ self.cli_set(base_path + ['server', srv_name, 'psk', 'sock_proxy', 'id', cli_name])
+ self.cli_set(base_path + ['server', srv_name, 'psk', 'sock_proxy', 'secret', '1234567890ABCDEF1234567890ABCDEF'])
+
+ self.cli_set(base_path + ['client', cli_name, 'listen', 'port', client_port])
+ self.cli_set(base_path + ['client', cli_name, 'connect', 'port', server_port])
+ self.cli_set(base_path + ['client', cli_name, 'psk', 'sock_proxy', 'id', cli_name])
+ self.cli_set(base_path + ['client', cli_name, 'psk', 'sock_proxy', 'secret', '1234567890ABCDEF1234567890ABCDEF'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['server', srv_name, 'connect'])
+ self.cli_commit()
+ config = read_config()
+
+ self.assertDictEqual(expected_config, config)
+
+ def test_07_available_port(self):
+ expected_config = {
+ 'global': {'pid': '/run/stunnel/stunnel.pid',
+ 'debug': 'notice'},
+ 'services': {
+ 'app1': {
+ 'client': 'yes',
+ 'accept': '8001',
+ 'connect': '9001'
+ },
+ 'srv1': {
+ 'PSKsecrets': f'/run/stunnel/psk/server_srv1.txt',
+ 'accept': '127.0.0.1:8002',
+ 'connect': '9001'
+ }
+ }}
+ self.cli_set(base_path + ['client', 'app1', 'connect', 'port', '9001'])
+ self.cli_set(base_path + ['client', 'app1', 'listen', 'port', '8001'])
+
+ self.cli_set(base_path + ['server', 'srv1', 'connect', 'port', '9001'])
+ self.cli_set(base_path + ['server', 'srv1', 'listen', 'address',
+ '127.0.0.1'])
+ self.cli_set(base_path + ['server', 'srv1', 'listen', 'port', '8001'])
+ self.cli_set(base_path + ['server', 'srv1', 'psk', 'smoketest1',
+ 'id', 'foo'])
+ self.cli_set(base_path + ['server', 'srv1', 'psk', 'smoketest1',
+ 'secret', '1234567890ABCDEF1234567890ABCDEF'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['server', 'srv1', 'listen', 'port', '8002'])
+ self.cli_commit()
+
+ config = read_config()
+ self.assertDictEqual(expected_config, config)
+
+ def test_08_two_endpoints(self):
+ expected_config = {
+ 'global': {'pid': '/run/stunnel/stunnel.pid',
+ 'debug': 'notice'},
+ 'services': {
+ 'app1': {
+ 'client': 'yes',
+ 'accept': '8001',
+ 'connect': '9001'
+ }
+ }}
+
+ self.cli_set(base_path + ['client', 'app1', 'listen', 'port', '8001'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['client', 'app1', 'connect', 'port', '9001'])
+ self.cli_commit()
+
+ config = read_config()
+ self.assertDictEqual(expected_config, config)
+
+ def test_09_pki_still_used(self):
+ service = 'ser1'
+ self.set_pki()
+ self.cli_set(base_path + ['server', service, 'connect', 'port', '8080'])
+ self.cli_set(base_path + ['server', service, 'listen', 'port', '9001'])
+ self.cli_set(base_path + ['server', service, 'ssl', 'certificate', 'srv-1'])
+ self.cli_commit()
+
+ self.cli_delete(['pki'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.is_valid_conf = False
+
+ def test_99_protocols(self):
+ self.set_pki()
+ service = 'one'
+ proto_address = 'google.com'
+ proto_port = '80'
+ modes = ['client', 'server']
+ protocols = ['cifs', 'connect', 'imap', 'nntp', 'pgsql', 'pop3',
+ 'proxy', 'smtp', 'socks']
+ options = ['authentication', 'domain', 'host', 'password', 'username']
+
+ for protocol in protocols:
+ for mode in modes:
+ expected_config = {
+ 'global': {'pid': '/run/stunnel/stunnel.pid',
+ 'debug': 'notice'},
+ 'services': {'one': {
+ 'accept': '8001',
+ 'protocol': protocol,
+ }}}
+ if not(mode == 'server' and protocol == 'socks'):
+ self.cli_set(base_path + [mode, service, 'connect', 'port', '9001'])
+ expected_config['services']['one']['connect'] = '9001'
+ self.cli_set(base_path + [mode, service, 'listen', 'port', '8001'])
+
+ if mode == 'server':
+ expected_config['services'][service]['cert'] = '/run/stunnel/server-one-srv-1.pem'
+ expected_config['services'][service]['key'] = '/run/stunnel/server-one-srv-1.pem.key'
+ self.cli_set(base_path + [mode, service, 'ssl',
+ 'certificate', 'srv-1'])
+ else:
+ expected_config['services'][service]['client'] = 'yes'
+
+ # protocols connect and nntp is only supported in client mode.
+ if mode == 'server' and protocol in ['connect', 'nntp']:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(base_path + [mode, service, 'protocol', protocol])
+ # self.cli_commit()
+ else:
+ self.cli_set(base_path + [mode, service, 'protocol', protocol])
+ self.cli_commit()
+ config = read_config()
+
+ self.assertDictEqual(expected_config, config)
+
+ expected_config['services'][service]['protocolDomain'] = 'valdomain'
+ expected_config['services'][service]['protocolPassword'] = 'valpassword'
+ expected_config['services'][service]['protocolUsername'] = 'valusername'
+
+ for option in options:
+ if option == 'authentication':
+ expected_config['services'][service]['protocolAuthentication'] = \
+ 'basic' if protocol == 'connect' else 'plain'
+ continue
+
+ if option == 'host' and mode != 'server':
+ expected_config['services'][service]['protocolHost'] = \
+ f'{proto_address}:{proto_port}'
+ self.cli_set(base_path + [mode, service, 'options',
+ option, 'address', f'{proto_address}'])
+ self.cli_set(base_path + [mode, service, 'options',
+ option, 'port', f'{proto_port}'])
+ continue
+ if mode == 'server':
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(
+ base_path + [mode, service, 'options', option, f'val{option}'])
+ else:
+ self.cli_set(
+ base_path + [mode, service, 'options', option, f'val{option}'])
+ # Additional option is only supported in the 'connect' and 'smtp' protocols.
+ if mode != 'server':
+ if protocol not in ['connect', 'smtp']:
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ else:
+ if protocol == 'smtp':
+ # Protocol smtp does not support options domain and host
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(
+ base_path + [mode, service, 'options', 'domain'])
+ self.cli_delete(
+ base_path + [mode, service, 'options', 'host'])
+ del expected_config['services'][service]['protocolDomain']
+ del expected_config['services'][service]['protocolHost']
+
+ self.cli_commit()
+ config = read_config()
+
+ self.assertDictEqual(expected_config, config)
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.is_valid_conf = False
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py
new file mode 100644
index 0000000..d607949
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_tftp-server.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from psutil import process_iter
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.file import read_file
+from vyos.utils.process import process_named_running
+from vyos.template import is_ipv6
+
+PROCESS_NAME = 'in.tftpd'
+base_path = ['service', 'tftp-server']
+dummy_if_path = ['interfaces', 'dummy', 'dum69']
+address_ipv4 = '192.0.2.1'
+address_ipv6 = '2001:db8::1'
+vrf = 'mgmt'
+
+class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceTFTPD, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, dummy_if_path + ['address', address_ipv4 + '/32'])
+ cls.cli_set(cls, dummy_if_path + ['address', address_ipv6 + '/128'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, dummy_if_path)
+ super(TestServiceTFTPD, cls).tearDownClass()
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for no longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_01_tftpd_single(self):
+ directory = '/tmp'
+ port = '69' # default port
+
+ self.cli_set(base_path + ['allow-upload'])
+ self.cli_set(base_path + ['directory', directory])
+ self.cli_set(base_path + ['listen-address', address_ipv4])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file('/etc/default/tftpd0')
+ # verify listen IP address
+ self.assertIn(f'{address_ipv4}:{port} -4', config)
+ # verify directory
+ self.assertIn(directory, config)
+ # verify upload
+ self.assertIn('--create --umask 000', config)
+
+ def test_02_tftpd_multi(self):
+ directory = '/tmp'
+ address = [address_ipv4, address_ipv6]
+ port = '70'
+
+ self.cli_set(base_path + ['directory', directory])
+ for addr in address:
+ self.cli_set(base_path + ['listen-address', addr])
+ self.cli_set(base_path + ['port', port])
+
+ # commit changes
+ self.cli_commit()
+
+ for idx in range(0, len(address)):
+ config = read_file(f'/etc/default/tftpd{idx}')
+ addr = address[idx]
+
+ # verify listen IP address
+ if is_ipv6(addr):
+ addr = f'[{addr}]'
+ self.assertIn(f'{addr}:{port} -6', config)
+ else:
+ self.assertIn(f'{addr}:{port} -4', config)
+
+ # verify directory
+ self.assertIn(directory, config)
+
+ # Check for running processes - one process is spawned per listen
+ # IP address, wheter it's IPv4 or IPv6
+ count = 0
+ for p in process_iter():
+ if PROCESS_NAME in p.name():
+ count += 1
+ self.assertEqual(count, len(address))
+
+ def test_03_tftpd_vrf(self):
+ directory = '/tmp'
+ port = '69' # default port
+
+ self.cli_set(base_path + ['allow-upload'])
+ self.cli_set(base_path + ['directory', directory])
+ self.cli_set(base_path + ['listen-address', address_ipv4, 'vrf', vrf])
+
+ # VRF does yet not exist - an error must be thrown
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(['vrf', 'name', vrf, 'table', '1338'])
+ self.cli_set(dummy_if_path + ['vrf', vrf])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file('/etc/default/tftpd0')
+ # verify listen IP address
+ self.assertIn(f'{address_ipv4}:{port} -4', config)
+ # verify directory
+ self.assertIn(directory, config)
+ # verify upload
+ self.assertIn('--create --umask 000', config)
+
+ # Check for process in VRF
+ tmp = cmd(f'ip vrf pids {vrf}')
+ self.assertIn(PROCESS_NAME, tmp)
+
+ # delete VRF
+ self.cli_delete(dummy_if_path + ['vrf'])
+ self.cli_delete(['vrf', 'name', vrf])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py
new file mode 100644
index 0000000..2b3f6d2
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_webproxy.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2022 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'squid'
+PROXY_CONF = '/etc/squid/squid.conf'
+base_path = ['service', 'webproxy']
+listen_if = 'dum3632'
+listen_ip = '192.0.2.1'
+
+class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # call base-classes classmethod
+ super(TestServiceWebProxy, cls).setUpClass()
+ # create a test interfaces
+ cls.cli_set(cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['interfaces', 'dummy', listen_if])
+ super(TestServiceWebProxy, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_01_basic_proxy(self):
+ default_cache = '100'
+ self.cli_set(base_path + ['listen-address', listen_ip])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(PROXY_CONF)
+ self.assertIn(f'http_port {listen_ip}:3128 intercept', config)
+ self.assertIn(f'cache_dir ufs /var/spool/squid {default_cache} 16 256', config)
+ self.assertIn(f'access_log /var/log/squid/access.log squid', config)
+
+ # ACL verification
+ self.assertIn(f'acl net src all', config)
+ self.assertIn(f'acl SSL_ports port 443', config)
+
+ safe_ports = ['80', '21', '443', '873', '70', '210', '1025-65535', '280',
+ '488', '591', '777']
+ for port in safe_ports:
+ self.assertIn(f'acl Safe_ports port {port}', config)
+ self.assertIn(f'acl CONNECT method CONNECT', config)
+
+ self.assertIn(f'http_access allow manager localhost', config)
+ self.assertIn(f'http_access deny manager', config)
+ self.assertIn(f'http_access deny !Safe_ports', config)
+ self.assertIn(f'http_access deny CONNECT !SSL_ports', config)
+ self.assertIn(f'http_access allow localhost', config)
+ self.assertIn(f'http_access allow net', config)
+ self.assertIn(f'http_access deny all', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_02_advanced_proxy(self):
+ domain = '.vyos.io'
+ cache_size = '900'
+ port = '8080'
+ min_obj_size = '128'
+ max_obj_size = '8192'
+ block_mine = ['application/pdf', 'application/x-sh']
+ body_max_size = '4096'
+ safe_port = '88'
+ ssl_safe_port = '8443'
+
+ self.cli_set(base_path + ['listen-address', listen_ip])
+ self.cli_set(base_path + ['append-domain', domain])
+ self.cli_set(base_path + ['default-port', port])
+ self.cli_set(base_path + ['cache-size', cache_size])
+ self.cli_set(base_path + ['disable-access-log'])
+
+ self.cli_set(base_path + ['minimum-object-size', min_obj_size])
+ self.cli_set(base_path + ['maximum-object-size', max_obj_size])
+
+ self.cli_set(base_path + ['outgoing-address', listen_ip])
+
+ for mime in block_mine:
+ self.cli_set(base_path + ['reply-block-mime', mime])
+
+ self.cli_set(base_path + ['reply-body-max-size', body_max_size])
+
+ self.cli_set(base_path + ['safe-ports', safe_port])
+ self.cli_set(base_path + ['ssl-safe-ports', ssl_safe_port])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(PROXY_CONF)
+ self.assertIn(f'http_port {listen_ip}:{port} intercept', config)
+ self.assertIn(f'append_domain {domain}', config)
+ self.assertIn(f'cache_dir ufs /var/spool/squid {cache_size} 16 256', config)
+ self.assertIn(f'access_log none', config)
+ self.assertIn(f'minimum_object_size {min_obj_size} KB', config)
+ self.assertIn(f'maximum_object_size {max_obj_size} KB', config)
+ self.assertIn(f'tcp_outgoing_address {listen_ip}', config)
+
+ for mime in block_mine:
+ self.assertIn(f'acl BLOCK_MIME rep_mime_type {mime}', config)
+ self.assertIn(f'http_reply_access deny BLOCK_MIME', config)
+
+ self.assertIn(f'reply_body_max_size {body_max_size} KB', config)
+
+ self.assertIn(f'acl Safe_ports port {safe_port}', config)
+ self.assertIn(f'acl SSL_ports port {ssl_safe_port}', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_03_ldap_proxy_auth(self):
+ auth_children = '20'
+ cred_ttl = '120'
+ realm = 'VyOS Webproxy'
+ ldap_base_dn = 'DC=vyos,DC=net'
+ ldap_server = 'ldap.vyos.net'
+ ldap_bind_dn = f'CN=proxyuser,CN=Users,{ldap_base_dn}'
+ ldap_password = 'VyOS12345'
+ ldap_attr = 'cn'
+ ldap_filter = '(cn=%s)'
+
+ self.cli_set(base_path + ['listen-address', listen_ip, 'disable-transparent'])
+ self.cli_set(base_path + ['authentication', 'children', auth_children])
+ self.cli_set(base_path + ['authentication', 'credentials-ttl', cred_ttl])
+
+ self.cli_set(base_path + ['authentication', 'realm', realm])
+ self.cli_set(base_path + ['authentication', 'method', 'ldap'])
+ # check validate() - LDAP authentication is enabled, but server not set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['authentication', 'ldap', 'server', ldap_server])
+
+ # check validate() - LDAP password can not be set when bind-dn is not define
+ self.cli_set(base_path + ['authentication', 'ldap', 'password', ldap_password])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['authentication', 'ldap', 'bind-dn', ldap_bind_dn])
+
+ # check validate() - LDAP base-dn must be set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['authentication', 'ldap', 'base-dn', ldap_base_dn])
+
+ self.cli_set(base_path + ['authentication', 'ldap', 'username-attribute', ldap_attr])
+ self.cli_set(base_path + ['authentication', 'ldap', 'filter-expression', ldap_filter])
+ self.cli_set(base_path + ['authentication', 'ldap', 'use-ssl'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(PROXY_CONF)
+ self.assertIn(f'http_port {listen_ip}:3128', config) # disable-transparent
+
+ # Now verify LDAP settings
+ self.assertIn(f'auth_param basic children {auth_children}', config)
+ self.assertIn(f'auth_param basic credentialsttl {cred_ttl} minute', config)
+ self.assertIn(f'auth_param basic realm "{realm}"', config)
+ self.assertIn(f'auth_param basic program /usr/lib/squid/basic_ldap_auth -v 3 -b "{ldap_base_dn}" -D "{ldap_bind_dn}" -w "{ldap_password}" -f "{ldap_filter}" -u "{ldap_attr}" -p 389 -ZZ -R -h "{ldap_server}"', config)
+ self.assertIn(f'acl auth proxy_auth REQUIRED', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_04_cache_peer(self):
+ self.cli_set(base_path + ['listen-address', listen_ip])
+
+ cache_peers = {
+ 'foo' : '192.0.2.1',
+ 'bar' : '192.0.2.2',
+ 'baz' : '192.0.2.3',
+ }
+ for peer in cache_peers:
+ self.cli_set(base_path + ['cache-peer', peer, 'address', cache_peers[peer]])
+ if peer == 'baz':
+ self.cli_set(base_path + ['cache-peer', peer, 'type', 'sibling'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(PROXY_CONF)
+ self.assertIn('never_direct allow all', config)
+
+ for peer in cache_peers:
+ address = cache_peers[peer]
+ if peer == 'baz':
+ self.assertIn(f'cache_peer {address} sibling 3128 0 no-query default', config)
+ else:
+ self.assertIn(f'cache_peer {address} parent 3128 0 no-query default', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_05_basic_squidguard(self):
+ # Create very basic local SquidGuard blacklist and verify its contents
+ sg_db_dir = '/opt/vyatta/etc/config/url-filtering/squidguard/db'
+
+ default_cache = '100'
+ local_block = ['192.0.0.1', '10.0.0.1', 'block.vyos.net']
+ local_block_url = ['foo.com/bar.html', 'bar.com/foo.htm']
+ local_block_pattern = ['porn', 'cisco', 'juniper']
+ local_ok = ['10.0.0.0', 'vyos.net']
+ local_ok_url = ['vyos.net', 'vyos.io']
+
+ self.cli_set(base_path + ['listen-address', listen_ip])
+ self.cli_set(base_path + ['url-filtering', 'squidguard', 'log', 'all'])
+
+ for block in local_block:
+ self.cli_set(base_path + ['url-filtering', 'squidguard', 'local-block', block])
+ for ok in local_ok:
+ self.cli_set(base_path + ['url-filtering', 'squidguard', 'local-ok', ok])
+ for url in local_block_url:
+ self.cli_set(base_path + ['url-filtering', 'squidguard', 'local-block-url', url])
+ for url in local_ok_url:
+ self.cli_set(base_path + ['url-filtering', 'squidguard', 'local-ok-url', url])
+ for pattern in local_block_pattern:
+ self.cli_set(base_path + ['url-filtering', 'squidguard', 'local-block-keyword', pattern])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check regular Squid config
+ config = read_file(PROXY_CONF)
+ self.assertIn(f'http_port {listen_ip}:3128 intercept', config)
+
+ self.assertIn(f'url_rewrite_program /usr/bin/squidGuard -c /etc/squidguard/squidGuard.conf', config)
+ self.assertIn(f'url_rewrite_children 8', config)
+
+ # Check SquidGuard config
+ sg_config = read_file('/etc/squidguard/squidGuard.conf')
+ self.assertIn(f'log blacklist.log', sg_config)
+
+ # The following are rewrite strings to force safe/strict search for
+ # several popular search engines.
+ self.assertIn(r's@(.*\.google\..*/(custom|search|images|groups|news)?.*q=.*)@\1\&safe=active@i', sg_config)
+ self.assertIn(r's@(.*\..*/yandsearch?.*text=.*)@\1\&fyandex=1@i', sg_config)
+ self.assertIn(r's@(.*\.yahoo\..*/search.*p=.*)@\1\&vm=r@i', sg_config)
+ self.assertIn(r's@(.*\.live\..*/.*q=.*)@\1\&adlt=strict@i', sg_config)
+ self.assertIn(r's@(.*\.msn\..*/.*q=.*)@\1\&adlt=strict@i', sg_config)
+ self.assertIn(r's@(.*\.bing\..*/search.*q=.*)@\1\&adlt=strict@i', sg_config)
+
+ # URL lists
+ self.assertIn(r'dest local-ok-default {', sg_config)
+ self.assertIn(f'domainlist local-ok-default/domains', sg_config)
+ self.assertIn(r'dest local-ok-url-default {', sg_config)
+ self.assertIn(f'urllist local-ok-url-default/urls', sg_config)
+
+ # Redirect - default value
+ self.assertIn(f'redirect 302:http://block.vyos.net', sg_config)
+
+ # local-block database
+ tmp = cmd(f'sudo cat {sg_db_dir}/local-block-default/domains')
+ for block in local_block:
+ self.assertIn(f'{block}', tmp)
+
+ tmp = cmd(f'sudo cat {sg_db_dir}/local-block-url-default/urls')
+ for url in local_block_url:
+ self.assertIn(f'{url}', tmp)
+
+ tmp = cmd(f'sudo cat {sg_db_dir}/local-block-keyword-default/expressions')
+ for pattern in local_block_pattern:
+ self.assertIn(f'{pattern}', tmp)
+
+ # local-ok database
+ tmp = cmd(f'sudo cat {sg_db_dir}/local-ok-default/domains')
+ for ok in local_ok:
+ self.assertIn(f'{ok}', tmp)
+
+ tmp = cmd(f'sudo cat {sg_db_dir}/local-ok-url-default/urls')
+ for url in local_ok_url:
+ self.assertIn(f'{url}', tmp)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_acceleration_qat.py b/smoketest/scripts/cli/test_system_acceleration_qat.py
new file mode 100644
index 0000000..9e60bb2
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_acceleration_qat.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2021 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+
+base_path = ['system', 'acceleration', 'qat']
+
+class TestIntelQAT(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_simple_unsupported(self):
+ # Check if configuration script is in place and that the config script
+ # throws an error as QAT device is not present in Qemu. This *must* be
+ # extended with QAT autodetection once run on a QAT enabled device
+
+ # configure some system display
+ self.cli_set(base_path)
+
+ # An error must be thrown if QAT device could not be found
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py
new file mode 100644
index 0000000..72deb75
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_conntrack.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.firewall import find_nftables_rule
+from vyos.utils.file import read_file, read_json
+
+base_path = ['system', 'conntrack']
+
+def get_sysctl(parameter):
+ tmp = parameter.replace(r'.', r'/')
+ return read_file(f'/proc/sys/{tmp}')
+
+def get_logger_config():
+ return read_json('/run/vyos-conntrack-logger.conf')
+
+class TestSystemConntrack(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemConntrack, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_conntrack_options(self):
+ conntrack_config = {
+ 'net.netfilter.nf_conntrack_expect_max' : {
+ 'cli' : ['expect-table-size'],
+ 'test_value' : '8192',
+ 'default_value' : '2048',
+ },
+ 'net.nf_conntrack_max' :{
+ 'cli' : ['table-size'],
+ 'test_value' : '500000',
+ 'default_value' : '262144',
+ },
+ 'net.ipv4.tcp_max_syn_backlog' :{
+ 'cli' : ['tcp', 'half-open-connections'],
+ 'test_value' : '2048',
+ 'default_value' : '512',
+ },
+ 'net.netfilter.nf_conntrack_tcp_loose' :{
+ 'cli' : ['tcp', 'loose'],
+ 'test_value' : 'disable',
+ 'default_value' : '1',
+ },
+ 'net.netfilter.nf_conntrack_tcp_max_retrans' :{
+ 'cli' : ['tcp', 'max-retrans'],
+ 'test_value' : '128',
+ 'default_value' : '3',
+ },
+ }
+
+ for parameter, parameter_config in conntrack_config.items():
+ self.cli_set(base_path + parameter_config['cli'] + [parameter_config['test_value']])
+
+ # commit changes
+ self.cli_commit()
+
+ # validate configuration
+ for parameter, parameter_config in conntrack_config.items():
+ tmp = parameter_config['test_value']
+ # net.netfilter.nf_conntrack_tcp_loose has a fancy "disable" value,
+ # make this work
+ if tmp == 'disable':
+ tmp = '0'
+ self.assertEqual(get_sysctl(f'{parameter}'), tmp)
+
+ # delete all configuration options and revert back to defaults
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # validate configuration
+ for parameter, parameter_config in conntrack_config.items():
+ self.assertEqual(get_sysctl(f'{parameter}'), parameter_config['default_value'])
+
+
+ def test_conntrack_module_enable(self):
+ # conntrack helper modules are disabled by default
+ modules = {
+ 'ftp': {
+ 'driver': ['nf_nat_ftp', 'nf_conntrack_ftp'],
+ 'nftables': ['ct helper set "ftp_tcp"']
+ },
+ 'h323': {
+ 'driver': ['nf_nat_h323', 'nf_conntrack_h323'],
+ 'nftables': ['ct helper set "ras_udp"',
+ 'ct helper set "q931_tcp"']
+ },
+ 'nfs': {
+ 'nftables': ['ct helper set "rpc_tcp"',
+ 'ct helper set "rpc_udp"']
+ },
+ 'pptp': {
+ 'driver': ['nf_nat_pptp', 'nf_conntrack_pptp'],
+ 'nftables': ['ct helper set "pptp_tcp"']
+ },
+ 'rtsp': {
+ 'driver': ['nf_nat_rtsp', 'nf_conntrack_rtsp'],
+ 'nftables': ['ct helper set "rtsp_tcp"']
+ },
+ 'sip': {
+ 'driver': ['nf_nat_sip', 'nf_conntrack_sip'],
+ 'nftables': ['ct helper set "sip_tcp"',
+ 'ct helper set "sip_udp"']
+ },
+ 'sqlnet': {
+ 'nftables': ['ct helper set "tns_tcp"']
+ },
+ 'tftp': {
+ 'driver': ['nf_nat_tftp', 'nf_conntrack_tftp'],
+ 'nftables': ['ct helper set "tftp_udp"']
+ },
+ }
+
+ # load modules
+ for module in modules:
+ self.cli_set(base_path + ['modules', module])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify modules are loaded on the system
+ for module, module_options in modules.items():
+ if 'driver' in module_options:
+ for driver in module_options['driver']:
+ self.assertTrue(os.path.isdir(f'/sys/module/{driver}'))
+ if 'nftables' in module_options:
+ for rule in module_options['nftables']:
+ self.assertTrue(find_nftables_rule('ip vyos_conntrack', 'VYOS_CT_HELPER', [rule]) != None)
+
+ # unload modules
+ for module in modules:
+ self.cli_delete(base_path + ['modules', module])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify modules are not loaded on the system
+ for module, module_options in modules.items():
+ if 'driver' in module_options:
+ for driver in module_options['driver']:
+ self.assertFalse(os.path.isdir(f'/sys/module/{driver}'))
+ if 'nftables' in module_options:
+ for rule in module_options['nftables']:
+ self.assertTrue(find_nftables_rule('ip vyos_conntrack', 'VYOS_CT_HELPER', [rule]) == None)
+
+ def test_conntrack_hash_size(self):
+ hash_size = '65536'
+ hash_size_default = '32768'
+
+ self.cli_set(base_path + ['hash-size', hash_size])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify new configuration - only effective after reboot, but
+ # a valid config file is sufficient
+ tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf')
+ self.assertIn(hash_size, tmp)
+
+ # Test default value by deleting the configuration
+ self.cli_delete(base_path + ['hash-size'])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify new configuration - only effective after reboot, but
+ # a valid config file is sufficient
+ tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf')
+ self.assertIn(hash_size_default, tmp)
+
+ def test_conntrack_ignore(self):
+ address_group = 'conntracktest'
+ address_group_member = '192.168.0.1'
+ ipv6_address_group = 'conntracktest6'
+ ipv6_address_group_member = 'dead:beef::1'
+
+ self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member])
+ self.cli_set(['firewall', 'group', 'ipv6-address-group', ipv6_address_group, 'address', ipv6_address_group_member])
+
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'source', 'address', '192.0.2.1'])
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'destination', 'address', '192.0.2.2'])
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'destination', 'port', '22'])
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'tcp', 'flags', 'syn'])
+
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'source', 'address', '192.0.2.1'])
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'destination', 'group', 'address-group', address_group])
+ self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'protocol', 'all'])
+
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'source', 'address', 'fe80::1'])
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'address', 'fe80::2'])
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'port', '22'])
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'protocol', 'tcp'])
+
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '12', 'source', 'address', 'fe80::1'])
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '12', 'destination', 'group', 'address-group', ipv6_address_group])
+
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '13', 'source', 'address', 'fe80::1'])
+ self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '13', 'destination', 'address', '!fe80::3'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'tcp flags & syn == syn', 'notrack'],
+ ['ip saddr 192.0.2.1', 'ip daddr @A_conntracktest', 'notrack']
+ ]
+
+ nftables6_search = [
+ ['ip6 saddr fe80::1', 'ip6 daddr fe80::2', 'tcp dport 22', 'notrack'],
+ ['ip6 saddr fe80::1', 'ip6 daddr @A6_conntracktest6', 'notrack'],
+ ['ip6 saddr fe80::1', 'ip6 daddr != fe80::3', 'notrack']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_conntrack')
+ self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack')
+
+ self.cli_delete(['firewall'])
+
+ def test_conntrack_timeout_custom(self):
+
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'source', 'address', '192.0.2.1'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'destination', 'address', '192.0.2.2'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'destination', 'port', '22'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'protocol', 'tcp', 'syn-sent', '77'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'protocol', 'tcp', 'close', '88'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'protocol', 'tcp', 'established', '99'])
+
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'inbound-interface', 'eth1'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'source', 'address', '198.51.100.1'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'protocol', 'udp', 'unreplied', '55'])
+
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'source', 'address', '2001:db8::1'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'inbound-interface', 'eth2'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'protocol', 'tcp', 'time-wait', '22'])
+ self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'protocol', 'tcp', 'last-ack', '33'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['ct timeout ct-timeout-1 {'],
+ ['protocol tcp'],
+ ['policy = { syn_sent : 1m17s, established : 1m39s, close : 1m28s }'],
+ ['ct timeout ct-timeout-2 {'],
+ ['protocol udp'],
+ ['policy = { unreplied : 55s }'],
+ ['chain VYOS_CT_TIMEOUT {'],
+ ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'ct timeout set "ct-timeout-1"'],
+ ['iifname "eth1"', 'meta l4proto udp', 'ip saddr 198.51.100.1', 'ct timeout set "ct-timeout-2"']
+ ]
+
+ nftables6_search = [
+ ['ct timeout ct-timeout-1 {'],
+ ['protocol tcp'],
+ ['policy = { last_ack : 33s, time_wait : 22s }'],
+ ['chain VYOS_CT_TIMEOUT {'],
+ ['iifname "eth2"', 'meta l4proto tcp', 'ip6 saddr 2001:db8::1', 'ct timeout set "ct-timeout-1"']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_conntrack')
+ self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack')
+
+ self.cli_delete(['firewall'])
+
+ def test_conntrack_log(self):
+ expected_config = {
+ 'event': {
+ 'destroy': {},
+ 'new': {},
+ 'update': {},
+ },
+ 'queue_size': '10000'
+ }
+ self.cli_set(base_path + ['log', 'event', 'destroy'])
+ self.cli_set(base_path + ['log', 'event', 'new'])
+ self.cli_set(base_path + ['log', 'event', 'update'])
+ self.cli_set(base_path + ['log', 'queue-size', '10000'])
+ self.cli_commit()
+ self.assertEqual(expected_config, get_logger_config())
+ self.assertEqual('0', get_sysctl('net.netfilter.nf_conntrack_timestamp'))
+
+ for event in ['destroy', 'new', 'update']:
+ for proto in ['icmp', 'other', 'tcp', 'udp']:
+ self.cli_set(base_path + ['log', 'event', event, proto])
+ expected_config['event'][event][proto] = {}
+ self.cli_set(base_path + ['log', 'timestamp'])
+ expected_config['timestamp'] = {}
+ self.cli_commit()
+
+ self.assertEqual(expected_config, get_logger_config())
+ self.assertEqual('1', get_sysctl('net.netfilter.nf_conntrack_timestamp'))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py
new file mode 100644
index 0000000..5151342
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_flow-accounting.py
@@ -0,0 +1,295 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.template import bracketize_ipv6
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'uacctd'
+base_path = ['system', 'flow-accounting']
+
+uacctd_conf = '/run/pmacct/uacctd.conf'
+
+class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemFlowAccounting, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # after service removal process must no longer run
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # after service removal process must no longer run
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_basic(self):
+ buffer_size = '5' # MiB
+ syslog = 'all'
+
+ self.cli_set(base_path + ['buffer-size', buffer_size])
+ self.cli_set(base_path + ['syslog-facility', syslog])
+
+ # You need to configure at least one interface for flow-accounting
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ nftables_output = cmd('sudo nft list chain raw VYOS_PREROUTING_HOOK').splitlines()
+ for interface in Section.interfaces('ethernet'):
+ rule_found = False
+ ifname_search = f'iifname "{interface}"'
+
+ for nftables_line in nftables_output:
+ if 'FLOW_ACCOUNTING_RULE' in nftables_line and ifname_search in nftables_line:
+ self.assertIn('group 2', nftables_line)
+ self.assertIn('snaplen 128', nftables_line)
+ self.assertIn('queue-threshold 100', nftables_line)
+ rule_found = True
+ break
+
+ self.assertTrue(rule_found)
+
+ uacctd = read_file(uacctd_conf)
+ # circular queue size - buffer_size
+ tmp = int(buffer_size) *1024 *1024
+ self.assertIn(f'plugin_pipe_size: {tmp}', uacctd)
+ # transfer buffer size - recommended value from pmacct developers 1/1000 of pipe size
+ tmp = int(buffer_size) *1024 *1024
+ # do an integer division
+ tmp //= 1000
+ self.assertIn(f'plugin_buffer_size: {tmp}', uacctd)
+
+ # when 'disable-imt' is not configured on the CLI it must be present
+ self.assertIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
+ self.assertIn(f'imt_mem_pools_number: 169', uacctd)
+ self.assertIn(f'syslog: {syslog}', uacctd)
+ self.assertIn(f'plugins: memory', uacctd)
+
+ def test_sflow(self):
+ sampling_rate = '4000'
+ source_address = '192.0.2.1'
+ dummy_if = 'dum3841'
+ agent_address = '192.0.2.2'
+
+ sflow_server = {
+ '1.2.3.4' : { },
+ '5.6.7.8' : { 'port' : '6000' },
+ }
+
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32'])
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32'])
+ self.cli_set(base_path + ['disable-imt'])
+
+ # You need to configure at least one interface for flow-accounting
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+
+ # You need to configure at least one sFlow or NetFlow protocol, or not
+ # set "disable-imt" for flow-accounting
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['sflow', 'agent-address', agent_address])
+ self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate])
+ self.cli_set(base_path + ['sflow', 'source-address', source_address])
+ for server, server_config in sflow_server.items():
+ self.cli_set(base_path + ['sflow', 'server', server])
+ if 'port' in server_config:
+ self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']])
+
+ # commit changes
+ self.cli_commit()
+
+ uacctd = read_file(uacctd_conf)
+
+ # when 'disable-imt' is not configured on the CLI it must be present
+ self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
+ self.assertNotIn(f'imt_mem_pools_number: 169', uacctd)
+ self.assertNotIn(f'plugins: memory', uacctd)
+
+ for server, server_config in sflow_server.items():
+ plugin_name = server.replace('.', '-')
+ if 'port' in server_config:
+ self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}', uacctd)
+ else:
+ self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}:6343', uacctd)
+
+ self.assertIn(f'sfprobe_agentip[sf_{plugin_name}]: {agent_address}', uacctd)
+ self.assertIn(f'sampling_rate[sf_{plugin_name}]: {sampling_rate}', uacctd)
+ self.assertIn(f'sfprobe_source_ip[sf_{plugin_name}]: {source_address}', uacctd)
+
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
+
+ def test_sflow_ipv6(self):
+ sampling_rate = '100'
+ sflow_server = {
+ '2001:db8::1' : { },
+ '2001:db8::2' : { 'port' : '6000' },
+ }
+
+ self.cli_set(base_path + ['disable-imt'])
+
+ # You need to configure at least one interface for flow-accounting
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+
+ # You need to configure at least one sFlow or NetFlow protocol, or not
+ # set "disable-imt" for flow-accounting
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate])
+ for server, server_config in sflow_server.items():
+ self.cli_set(base_path + ['sflow', 'server', server])
+ if 'port' in server_config:
+ self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']])
+
+ # commit changes
+ self.cli_commit()
+
+ uacctd = read_file(uacctd_conf)
+
+ # when 'disable-imt' is not configured on the CLI it must be present
+ self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd)
+ self.assertNotIn(f'imt_mem_pools_number: 169', uacctd)
+ self.assertNotIn(f'plugins: memory', uacctd)
+
+ for server, server_config in sflow_server.items():
+ tmp_srv = server
+ tmp_srv = tmp_srv.replace(':', '-')
+
+ if 'port' in server_config:
+ self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd)
+ else:
+ self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}:6343', uacctd)
+ self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd)
+
+ def test_netflow(self):
+ engine_id = '33'
+ max_flows = '667'
+ sampling_rate = '100'
+ source_address = '192.0.2.1'
+ dummy_if = 'dum3842'
+ agent_address = '192.0.2.10'
+ version = '10'
+ tmo_expiry = '120'
+ tmo_flow = '1200'
+ tmo_icmp = '60'
+ tmo_max = '50000'
+ tmo_tcp_fin = '100'
+ tmo_tcp_generic = '120'
+ tmo_tcp_rst = '99'
+ tmo_udp = '10'
+
+ netflow_server = {
+ '11.22.33.44' : { },
+ '55.66.77.88' : { 'port' : '6000' },
+ '2001:db8::1' : { },
+ }
+
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32'])
+ self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32'])
+
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+ self.cli_set(base_path + ['netflow', 'engine-id', engine_id])
+ self.cli_set(base_path + ['netflow', 'max-flows', max_flows])
+ self.cli_set(base_path + ['netflow', 'sampling-rate', sampling_rate])
+ self.cli_set(base_path + ['netflow', 'source-address', source_address])
+ self.cli_set(base_path + ['netflow', 'version', version])
+
+ # timeouts
+ self.cli_set(base_path + ['netflow', 'timeout', 'expiry-interval', tmo_expiry])
+ self.cli_set(base_path + ['netflow', 'timeout', 'flow-generic', tmo_flow])
+ self.cli_set(base_path + ['netflow', 'timeout', 'icmp', tmo_icmp])
+ self.cli_set(base_path + ['netflow', 'timeout', 'max-active-life', tmo_max])
+ self.cli_set(base_path + ['netflow', 'timeout', 'tcp-fin', tmo_tcp_fin])
+ self.cli_set(base_path + ['netflow', 'timeout', 'tcp-generic', tmo_tcp_generic])
+ self.cli_set(base_path + ['netflow', 'timeout', 'tcp-rst', tmo_tcp_rst])
+ self.cli_set(base_path + ['netflow', 'timeout', 'udp', tmo_udp])
+
+ # You need to configure at least one netflow server
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for server, server_config in netflow_server.items():
+ self.cli_set(base_path + ['netflow', 'server', server])
+ if 'port' in server_config:
+ self.cli_set(base_path + ['netflow', 'server', server, 'port', server_config['port']])
+
+ # commit changes
+ self.cli_commit()
+
+ uacctd = read_file(uacctd_conf)
+
+ tmp = []
+ for server, server_config in netflow_server.items():
+ tmp_srv = server
+ tmp_srv = tmp_srv.replace('.', '-')
+ tmp_srv = tmp_srv.replace(':', '-')
+ tmp.append(f'nfprobe[nf_{tmp_srv}]')
+ tmp.append('memory')
+ self.assertIn('plugins: ' + ','.join(tmp), uacctd)
+
+ for server, server_config in netflow_server.items():
+ tmp_srv = server
+ tmp_srv = tmp_srv.replace('.', '-')
+ tmp_srv = tmp_srv.replace(':', '-')
+
+ self.assertIn(f'nfprobe_engine[nf_{tmp_srv}]: {engine_id}', uacctd)
+ self.assertIn(f'nfprobe_maxflows[nf_{tmp_srv}]: {max_flows}', uacctd)
+ self.assertIn(f'sampling_rate[nf_{tmp_srv}]: {sampling_rate}', uacctd)
+ self.assertIn(f'nfprobe_source_ip[nf_{tmp_srv}]: {source_address}', uacctd)
+ self.assertIn(f'nfprobe_version[nf_{tmp_srv}]: {version}', uacctd)
+
+ if 'port' in server_config:
+ self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd)
+ else:
+ self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}:2055', uacctd)
+
+ self.assertIn(f'nfprobe_timeouts[nf_{tmp_srv}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd)
+
+
+ self.cli_delete(['interfaces', 'dummy', dummy_if])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_frr.py b/smoketest/scripts/cli/test_system_frr.py
new file mode 100644
index 0000000..a2ce58b
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_frr.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import read_file
+
+config_file = '/etc/frr/daemons'
+base_path = ['system', 'frr']
+
+def daemons_config_parse(daemons_config):
+ # create regex for parsing daemons options
+ regex_daemon_config = re.compile(
+ r'^(?P<daemon_name>\w+)_options="(?P<daemon_options>.*)"$', re.M)
+ # create empty dict for config
+ daemons_config_dict = {}
+ # fill dictionary with actual config
+ for daemon in regex_daemon_config.finditer(daemons_config):
+ daemon_name = daemon.group('daemon_name')
+ daemon_options = daemon.group('daemon_options')
+ daemons_config_dict[daemon_name] = daemon_options.lstrip()
+
+ # return daemons config
+ return (daemons_config_dict)
+
+
+class TestSystemFRR(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemFRR, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_frr_snmp_multipledaemons(self):
+ # test SNMP integration for multiple daemons
+ test_daemon_names = ['ospfd', 'bgpd']
+ for test_daemon_name in test_daemon_names:
+ self.cli_set(base_path + ['snmp', test_daemon_name])
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ daemons_config_dict = daemons_config_parse(daemons_config)
+ # prepare regex for matching SNMP integration
+ regex_snmp = re.compile(r'^.* -M snmp.*$')
+ for (daemon_name, daemon_options) in daemons_config_dict.items():
+ snmp_enabled = regex_snmp.match(daemon_options)
+ if daemon_name in test_daemon_names:
+ self.assertTrue(snmp_enabled)
+ else:
+ self.assertFalse(snmp_enabled)
+
+ def test_frr_snmp_add_remove(self):
+ # test enabling and disabling of SNMP integration
+ test_daemon_names = ['ospfd', 'bgpd']
+ for test_daemon_name in test_daemon_names:
+ self.cli_set(base_path + ['snmp', test_daemon_name])
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ daemons_config_dict = daemons_config_parse(daemons_config)
+ # prepare regex for matching SNMP integration
+ regex_snmp = re.compile(r'^.* -M snmp.*$')
+ for test_daemon_name in test_daemon_names:
+ snmp_enabled = regex_snmp.match(
+ daemons_config_dict[test_daemon_name])
+ self.assertFalse(snmp_enabled)
+
+ def test_frr_snmp_empty(self):
+ # test empty config section
+ self.cli_set(base_path + ['snmp'])
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ daemons_config_dict = daemons_config_parse(daemons_config)
+ # prepare regex for matching SNMP integration
+ regex_snmp = re.compile(r'^.* -M snmp.*$')
+ for daemon_options in daemons_config_dict.values():
+ snmp_enabled = regex_snmp.match(daemon_options)
+ self.assertFalse(snmp_enabled)
+
+ def test_frr_bmp(self):
+ # test BMP
+ self.cli_set(base_path + ['bmp'])
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ daemons_config_dict = daemons_config_parse(daemons_config)
+ # prepare regex
+ regex_bmp = re.compile(r'^.* -M bmp.*$')
+ bmp_enabled = regex_bmp.match(daemons_config_dict['bgpd'])
+ self.assertTrue(bmp_enabled)
+
+ def test_frr_irdp(self):
+ # test IRDP
+ self.cli_set(base_path + ['irdp'])
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ daemons_config_dict = daemons_config_parse(daemons_config)
+ # prepare regex
+ regex_irdp = re.compile(r'^.* -M irdp.*$')
+ irdp_enabled = regex_irdp.match(daemons_config_dict['zebra'])
+ self.assertTrue(irdp_enabled)
+
+ def test_frr_bmp_and_snmp(self):
+ # test empty config section
+ self.cli_set(base_path + ['bmp'])
+ self.cli_set(base_path + ['snmp', 'bgpd'])
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ daemons_config_dict = daemons_config_parse(daemons_config)
+ # prepare regex
+ regex_snmp = re.compile(r'^.* -M bmp.*$')
+ regex_snmp = re.compile(r'^.* -M snmp.*$')
+ bmp_enabled = regex_snmp.match(daemons_config_dict['bgpd'])
+ snmp_enabled = regex_snmp.match(daemons_config_dict['bgpd'])
+ self.assertTrue(bmp_enabled)
+ self.assertTrue(snmp_enabled)
+
+ def test_frr_file_descriptors(self):
+ file_descriptors = '4096'
+
+ self.cli_set(base_path + ['descriptors', file_descriptors])
+ self.cli_commit()
+
+ # read the config file and check content
+ daemons_config = read_file(config_file)
+ self.assertIn(f'MAX_FDS={file_descriptors}', daemons_config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py
new file mode 100644
index 0000000..5b00902
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_ip.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+
+base_path = ['system', 'ip']
+
+class TestSystemIP(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_system_ip_forwarding(self):
+ # Test if IPv4 forwarding can be disabled globally, default is '1'
+ # which means forwarding enabled
+ all_forwarding = '/proc/sys/net/ipv4/conf/all/forwarding'
+ self.assertEqual(read_file(all_forwarding), '1')
+
+ self.cli_set(base_path + ['disable-forwarding'])
+ self.cli_commit()
+
+ self.assertEqual(read_file(all_forwarding), '0')
+
+ def test_system_ip_multipath(self):
+ # Test IPv4 multipathing options, options default to off -> '0'
+ use_neigh = '/proc/sys/net/ipv4/fib_multipath_use_neigh'
+ hash_policy = '/proc/sys/net/ipv4/fib_multipath_hash_policy'
+
+ self.assertEqual(read_file(use_neigh), '0')
+ self.assertEqual(read_file(hash_policy), '0')
+
+ self.cli_set(base_path + ['multipath', 'ignore-unreachable-nexthops'])
+ self.cli_set(base_path + ['multipath', 'layer4-hashing'])
+ self.cli_commit()
+
+ self.assertEqual(read_file(use_neigh), '1')
+ self.assertEqual(read_file(hash_policy), '1')
+
+ def test_system_ip_arp_table_size(self):
+ # Maximum number of entries to keep in the ARP cache, the
+ # default is 8k
+
+ gc_thresh3 = '/proc/sys/net/ipv4/neigh/default/gc_thresh3'
+ gc_thresh2 = '/proc/sys/net/ipv4/neigh/default/gc_thresh2'
+ gc_thresh1 = '/proc/sys/net/ipv4/neigh/default/gc_thresh1'
+ self.assertEqual(read_file(gc_thresh3), '8192')
+ self.assertEqual(read_file(gc_thresh2), '4096')
+ self.assertEqual(read_file(gc_thresh1), '1024')
+
+ for size in [1024, 2048, 4096, 8192, 16384, 32768]:
+ self.cli_set(base_path + ['arp', 'table-size', str(size)])
+ self.cli_commit()
+
+ self.assertEqual(read_file(gc_thresh3), str(size))
+ 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)
+
+ # Delete route-maps
+ self.cli_delete(['policy', 'route-map'])
+ self.cli_delete(base_path + ['protocol'])
+
+ self.cli_commit()
+
+ # Verify route-map properly applied to FRR
+ frrconfig = self.getFRRconfig('ip protocol', end='', daemon='zebra')
+ self.assertNotIn(f'ip 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()
+
+ def test_system_ip_nht(self):
+ self.cli_set(base_path + ['nht', 'no-resolve-via-default'])
+ self.cli_commit()
+ # Verify CLI config applied to FRR
+ frrconfig = self.getFRRconfig('', end='', daemon='zebra')
+ self.assertIn(f'no ip nht resolve-via-default', frrconfig)
+
+ self.cli_delete(base_path + ['nht', 'no-resolve-via-default'])
+ self.cli_commit()
+ # Verify CLI config removed to FRR
+ frrconfig = self.getFRRconfig('', end='', daemon='zebra')
+ self.assertNotIn(f'no ip nht resolve-via-default', frrconfig)
+
+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
new file mode 100644
index 0000000..0c77c1d
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.file import read_file
+
+base_path = ['system', 'ipv6']
+
+file_forwarding = '/proc/sys/net/ipv6/conf/all/forwarding'
+file_disable = '/proc/sys/net/ipv6/conf/all/disable_ipv6'
+file_dad = '/proc/sys/net/ipv6/conf/all/accept_dad'
+file_multipath = '/proc/sys/net/ipv6/fib_multipath_hash_policy'
+
+class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_system_ipv6_forwarding(self):
+ # Test if IPv6 forwarding can be disabled globally, default is '1'
+ # which means forwearding enabled
+ self.assertEqual(read_file(file_forwarding), '1')
+
+ self.cli_set(base_path + ['disable-forwarding'])
+ self.cli_commit()
+
+ self.assertEqual(read_file(file_forwarding), '0')
+
+ def test_system_ipv6_strict_dad(self):
+ # This defaults to 1
+ self.assertEqual(read_file(file_dad), '1')
+
+ # Do not assign any IPv6 address on interfaces, this requires a reboot
+ # which can not be tested, but we can read the config file :)
+ self.cli_set(base_path + ['strict-dad'])
+ self.cli_commit()
+
+ # Verify configuration file
+ self.assertEqual(read_file(file_dad), '2')
+
+ def test_system_ipv6_multipath(self):
+ # This defaults to 0
+ self.assertEqual(read_file(file_multipath), '0')
+
+ # Do not assign any IPv6 address on interfaces, this requires a reboot
+ # which can not be tested, but we can read the config file :)
+ self.cli_set(base_path + ['multipath', 'layer4-hashing'])
+ self.cli_commit()
+
+ # Verify configuration file
+ self.assertEqual(read_file(file_multipath), '1')
+
+ def test_system_ipv6_neighbor_table_size(self):
+ # Maximum number of entries to keep in the ARP cache, the
+ # default is 8192
+
+ gc_thresh3 = '/proc/sys/net/ipv6/neigh/default/gc_thresh3'
+ gc_thresh2 = '/proc/sys/net/ipv6/neigh/default/gc_thresh2'
+ gc_thresh1 = '/proc/sys/net/ipv6/neigh/default/gc_thresh1'
+ self.assertEqual(read_file(gc_thresh3), '8192')
+ self.assertEqual(read_file(gc_thresh2), '4096')
+ self.assertEqual(read_file(gc_thresh1), '1024')
+
+ for size in [1024, 2048, 4096, 8192, 16384, 32768]:
+ self.cli_set(base_path + ['neighbor', 'table-size', str(size)])
+ self.cli_commit()
+
+ self.assertEqual(read_file(gc_thresh3), str(size))
+ 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)
+
+ # Delete route-maps
+ self.cli_delete(['policy', 'route-map'])
+ self.cli_delete(base_path + ['protocol'])
+
+ self.cli_commit()
+
+ # Verify route-map properly applied to FRR
+ frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon='zebra')
+ self.assertNotIn(f'ipv6 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()
+
+ def test_system_ipv6_nht(self):
+ self.cli_set(base_path + ['nht', 'no-resolve-via-default'])
+ self.cli_commit()
+ # Verify CLI config applied to FRR
+ frrconfig = self.getFRRconfig('', end='', daemon='zebra')
+ self.assertIn(f'no ipv6 nht resolve-via-default', frrconfig)
+
+ self.cli_delete(base_path + ['nht', 'no-resolve-via-default'])
+ self.cli_commit()
+ # Verify CLI config removed to FRR
+ frrconfig = self.getFRRconfig('', end='', daemon='zebra')
+ self.assertNotIn(f'no ipv6 nht resolve-via-default', frrconfig)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py
new file mode 100644
index 0000000..fc440ca
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_lcd.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 Francois Mertz fireboxled@gmail.com
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from configparser import ConfigParser
+
+from vyos.utils.process import process_named_running
+
+config_file = '/run/LCDd/LCDd.conf'
+base_path = ['system', 'lcd']
+
+class TestSystemLCD(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_system_display(self):
+ # configure some system display
+ self.cli_set(base_path + ['device', 'ttyS1'])
+ self.cli_set(base_path + ['model', 'cfa-533'])
+
+ # commit changes
+ self.cli_commit()
+
+ # load up ini-styled LCDd.conf
+ conf = ConfigParser()
+ conf.read(config_file)
+
+ self.assertEqual(conf['CFontzPacket']['Model'], '533')
+ self.assertEqual(conf['CFontzPacket']['Device'], '/dev/ttyS1')
+
+ # Check for running process
+ self.assertTrue(process_named_running('LCDd'))
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
new file mode 100644
index 0000000..28abba0
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -0,0 +1,348 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from gzip import GzipFile
+from subprocess import Popen, PIPE
+from pwd import getpwall
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.auth import get_current_user
+from vyos.utils.process import cmd
+from vyos.utils.file import read_file
+from vyos.template import inc_ip
+
+base_path = ['system', 'login']
+users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']
+
+ssh_pubkey = """
+AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF
+TlXYfVt6Ls2NZZsUSwHbnhK4BKDrPvVZMW/LycjQPzWW6TGtk6UbZP1WqdviQ9hP
+jsEeKJSTKciMSvQpjBWyEQQPXSKYQC7ryQQilZDqnJgzqwzejKEe+nhhOdBvjuZc
+uukxjT69E0UmWAwLxzvfiurwiQaC7tG+PwqvtfHOPL3i6yRO2C5ORpFarx8PeGDS
+IfIXJCr3LoUbLHeuE7T2KaOKQcX0UsWJ4CoCapRLpTVYPDB32BYfgq7cW1Sal1re
+EGH2PzuXBklinTBgCHA87lHjpwDIAqdmvMj7SXIW9LxazLtP+e37sexE7xEs0cpN
+l68txdDbY2P2Kbz5mqGFfCvBYKv9V2clM5vyWNy/Xp5TsCis89nn83KJmgFS7sMx
+pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr
+TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk=
+"""
+
+class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemLogin, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration which will break this test
+ cls.cli_delete(cls, base_path + ['radius'])
+ cls.cli_delete(cls, base_path + ['tacacs'])
+
+ def tearDown(self):
+ # Delete individual users from configuration
+ for user in users:
+ self.cli_delete(base_path + ['user', user])
+
+ self.cli_delete(base_path + ['radius'])
+ self.cli_delete(base_path + ['tacacs'])
+
+ self.cli_commit()
+
+ # After deletion, a user is not allowed to remain in /etc/passwd
+ usernames = [x[0] for x in getpwall()]
+ for user in users:
+ self.assertNotIn(user, usernames)
+
+ def test_add_linux_system_user(self):
+ # We are not allowed to re-use a username already taken by the Linux
+ # base system
+ system_user = 'backup'
+ self.cli_set(base_path + ['user', system_user, 'authentication', 'plaintext-password', system_user])
+
+ # check validate() - can not add username which exists on the Debian
+ # base system (UID < 1000)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['user', system_user])
+
+ def test_system_login_user(self):
+ # Check if user can be created and we can SSH to localhost
+ self.cli_set(['service', 'ssh', 'port', '22'])
+
+ for user in users:
+ name = "VyOS Roxx " + user
+ home_dir = "/tmp/" + user
+
+ self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user])
+ self.cli_set(base_path + ['user', user, 'full-name', 'VyOS Roxx'])
+ self.cli_set(base_path + ['user', user, 'home-directory', home_dir])
+
+ self.cli_commit()
+
+ for user in users:
+ tmp = ['su','-', user]
+ proc = Popen(tmp, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ tmp = "{}\nuname -a".format(user)
+ proc.stdin.write(tmp.encode())
+ proc.stdin.flush()
+ (stdout, stderr) = proc.communicate()
+
+ # stdout is something like this:
+ # b'Linux LR1.wue3 5.10.61-amd64-vyos #1 SMP Fri Aug 27 08:55:46 UTC 2021 x86_64 GNU/Linux\n'
+ self.assertTrue(len(stdout) > 40)
+
+ locked_user = users[0]
+ # disable the first user in list
+ self.cli_set(base_path + ['user', locked_user, 'disable'])
+ self.cli_commit()
+ # check if account is locked
+ tmp = cmd(f'sudo passwd -S {locked_user}')
+ self.assertIn(f'{locked_user} L ', tmp)
+
+ # unlock account
+ self.cli_delete(base_path + ['user', locked_user, 'disable'])
+ self.cli_commit()
+ # check if account is unlocked
+ tmp = cmd(f'sudo passwd -S {locked_user}')
+ self.assertIn(f'{locked_user} P ', tmp)
+
+
+ def test_system_login_otp(self):
+ otp_user = 'otp-test_user'
+ otp_password = 'SuperTestPassword'
+ otp_key = '76A3ZS6HFHBTOK2H4NDHTIVFPQ'
+
+ self.cli_set(base_path + ['user', otp_user, 'authentication', 'plaintext-password', otp_password])
+ self.cli_set(base_path + ['user', otp_user, 'authentication', 'otp', 'key', otp_key])
+
+ self.cli_commit()
+
+ # Check if OTP key was written properly
+ tmp = cmd(f'sudo head -1 /home/{otp_user}/.google_authenticator')
+ self.assertIn(otp_key, tmp)
+
+ self.cli_delete(base_path + ['user', otp_user])
+
+ def test_system_user_ssh_key(self):
+ ssh_user = 'ssh-test_user'
+ public_keys = 'vyos_test@domain-foo.com'
+ type = 'ssh-rsa'
+
+ self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'key', ssh_pubkey.replace('\n','')])
+
+ # check validate() - missing type for public-key
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'type', type])
+
+ self.cli_commit()
+
+ # Check that SSH key was written properly
+ tmp = cmd(f'sudo cat /home/{ssh_user}/.ssh/authorized_keys')
+ key = f'{type} ' + ssh_pubkey.replace('\n','')
+ self.assertIn(key, tmp)
+
+ self.cli_delete(base_path + ['user', ssh_user])
+
+ def test_radius_kernel_features(self):
+ # T2886: RADIUS requires some Kernel options to be present
+ kernel_config = GzipFile('/proc/config.gz').read().decode('UTF-8')
+
+ # T2886 - RADIUS authentication - check for statically compiled options
+ options = ['CONFIG_AUDIT', 'CONFIG_AUDITSYSCALL', 'CONFIG_AUDIT_ARCH']
+
+ for option in options:
+ self.assertIn(f'{option}=y', kernel_config)
+
+ def test_system_login_radius_ipv4(self):
+ # Verify generated RADIUS configuration files
+
+ radius_key = 'VyOSsecretVyOS'
+ radius_server = '172.16.100.10'
+ radius_source = '127.0.0.1'
+ radius_port = '2000'
+ radius_timeout = '1'
+
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
+ self.cli_set(base_path + ['radius', 'source-address', radius_source])
+ self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ # check validate() - Only one IPv4 source-address supported
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ self.cli_commit()
+
+ # this file must be read with higher permissions
+ pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
+ tmp = re.findall(r'\n?{}:{}\s+{}\s+{}\s+{}'.format(radius_server,
+ radius_port, radius_key, radius_timeout,
+ radius_source), pam_radius_auth_conf)
+ self.assertTrue(tmp)
+
+ # required, static options
+ self.assertIn('priv-lvl 15', pam_radius_auth_conf)
+ self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf)
+
+ # PAM
+ pam_common_account = read_file('/etc/pam.d/common-account')
+ self.assertIn('pam_radius_auth.so', pam_common_account)
+
+ pam_common_auth = read_file('/etc/pam.d/common-auth')
+ self.assertIn('pam_radius_auth.so', pam_common_auth)
+
+ pam_common_session = read_file('/etc/pam.d/common-session')
+ self.assertIn('pam_radius_auth.so', pam_common_session)
+
+ pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive')
+ self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive)
+
+ # NSS
+ nsswitch_conf = read_file('/etc/nsswitch.conf')
+ tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ def test_system_login_radius_ipv6(self):
+ # Verify generated RADIUS configuration files
+
+ radius_key = 'VyOS-VyOS'
+ radius_server = '2001:db8::1'
+ radius_source = '::1'
+ radius_port = '4000'
+ radius_timeout = '4'
+
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
+ self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
+ self.cli_set(base_path + ['radius', 'source-address', radius_source])
+ self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ # check validate() - Only one IPv4 source-address supported
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ self.cli_commit()
+
+ # this file must be read with higher permissions
+ pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
+ tmp = re.findall(r'\n?\[{}\]:{}\s+{}\s+{}\s+\[{}\]'.format(radius_server,
+ radius_port, radius_key, radius_timeout,
+ radius_source), pam_radius_auth_conf)
+ self.assertTrue(tmp)
+
+ # required, static options
+ self.assertIn('priv-lvl 15', pam_radius_auth_conf)
+ self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf)
+
+ # PAM
+ pam_common_account = read_file('/etc/pam.d/common-account')
+ self.assertIn('pam_radius_auth.so', pam_common_account)
+
+ pam_common_auth = read_file('/etc/pam.d/common-auth')
+ self.assertIn('pam_radius_auth.so', pam_common_auth)
+
+ pam_common_session = read_file('/etc/pam.d/common-session')
+ self.assertIn('pam_radius_auth.so', pam_common_session)
+
+ pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive')
+ self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive)
+
+ # NSS
+ nsswitch_conf = read_file('/etc/nsswitch.conf')
+ tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ def test_system_login_max_login_session(self):
+ max_logins = '2'
+ timeout = '600'
+
+ self.cli_set(base_path + ['max-login-session', max_logins])
+
+ # 'max-login-session' must be only with 'timeout' option
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['timeout', timeout])
+
+ self.cli_commit()
+
+ security_limits = read_file('/etc/security/limits.d/10-vyos.conf')
+ self.assertIn(f'* - maxsyslogins {max_logins}', security_limits)
+
+ self.cli_delete(base_path + ['timeout'])
+ self.cli_delete(base_path + ['max-login-session'])
+
+ def test_system_login_tacacs(self):
+ tacacs_secret = 'tac_plus_key'
+ tacacs_servers = ['100.64.0.11', '100.64.0.12']
+
+ # Enable TACACS
+ for server in tacacs_servers:
+ self.cli_set(base_path + ['tacacs', 'server', server, 'key', tacacs_secret])
+
+ self.cli_commit()
+
+ # NSS
+ nsswitch_conf = read_file('/etc/nsswitch.conf')
+ tmp = re.findall(r'passwd:\s+tacplus\s+files', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ tmp = re.findall(r'group:\s+tacplus\s+files', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ # PAM TACACS configuration
+ pam_tacacs_conf = read_file('/etc/tacplus_servers')
+ # NSS TACACS configuration
+ nss_tacacs_conf = read_file('/etc/tacplus_nss.conf')
+ # Users have individual home directories
+ self.assertIn('user_homedir=1', pam_tacacs_conf)
+
+ # specify services
+ self.assertIn('service=shell', pam_tacacs_conf)
+ self.assertIn('protocol=ssh', pam_tacacs_conf)
+
+ for server in tacacs_servers:
+ self.assertIn(f'secret={tacacs_secret}', pam_tacacs_conf)
+ self.assertIn(f'server={server}', pam_tacacs_conf)
+
+ self.assertIn(f'secret={tacacs_secret}', nss_tacacs_conf)
+ self.assertIn(f'server={server}', nss_tacacs_conf)
+
+ def test_delete_current_user(self):
+ current_user = get_current_user()
+
+ # We are not allowed to delete the current user
+ self.cli_delete(base_path + ['user', current_user])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_logs.py b/smoketest/scripts/cli/test_system_logs.py
new file mode 100644
index 0000000..17cce5c
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_logs.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import read_file
+
+# path to logrotate configs
+logrotate_atop_file = '/etc/logrotate.d/vyos-atop'
+logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog'
+# default values
+default_atop_maxsize = '10M'
+default_atop_rotate = '10'
+default_rsyslog_size = '1M'
+default_rsyslog_rotate = '10'
+
+base_path = ['system', 'logs']
+
+
+def logrotate_config_parse(file_path):
+ # read the file
+ logrotate_config = read_file(file_path)
+ # create regex for parsing options
+ regex_options = re.compile(
+ r'(^\s+(?P<option_name_script>postrotate|prerotate|firstaction|lastaction|preremove)\n(?P<option_value_script>((?!endscript).)*)\n\s+endscript\n)|(^\s+(?P<option_name>[\S]+)([ \t]+(?P<option_value>\S+))*$)',
+ re.M | re.S)
+ # create empty dict for config
+ logrotate_config_dict = {}
+ # fill dictionary with actual config
+ for option in regex_options.finditer(logrotate_config):
+ option_name = option.group('option_name')
+ option_value = option.group('option_value')
+ option_name_script = option.group('option_name_script')
+ option_value_script = option.group('option_value_script')
+ if option_name:
+ logrotate_config_dict[option_name] = option_value
+ if option_name_script:
+ logrotate_config_dict[option_name_script] = option_value_script
+
+ # return config dictionary
+ return (logrotate_config_dict)
+
+
+class TestSystemLogs(VyOSUnitTestSHIM.TestCase):
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_logs_defaults(self):
+ # test with empty section for default values
+ self.cli_set(base_path)
+ self.cli_commit()
+
+ # read the config file and check content
+ logrotate_config_atop = logrotate_config_parse(logrotate_atop_file)
+ logrotate_config_rsyslog = logrotate_config_parse(
+ logrotate_rsyslog_file)
+ self.assertEqual(logrotate_config_atop['maxsize'], default_atop_maxsize)
+ self.assertEqual(logrotate_config_atop['rotate'], default_atop_rotate)
+ self.assertEqual(logrotate_config_rsyslog['size'], default_rsyslog_size)
+ self.assertEqual(logrotate_config_rsyslog['rotate'],
+ default_rsyslog_rotate)
+
+ def test_logs_atop_maxsize(self):
+ # test for maxsize option
+ self.cli_set(base_path + ['logrotate', 'atop', 'max-size', '50'])
+ self.cli_commit()
+
+ # read the config file and check content
+ logrotate_config = logrotate_config_parse(logrotate_atop_file)
+ self.assertEqual(logrotate_config['maxsize'], '50M')
+
+ def test_logs_atop_rotate(self):
+ # test for rotate option
+ self.cli_set(base_path + ['logrotate', 'atop', 'rotate', '50'])
+ self.cli_commit()
+
+ # read the config file and check content
+ logrotate_config = logrotate_config_parse(logrotate_atop_file)
+ self.assertEqual(logrotate_config['rotate'], '50')
+
+ def test_logs_rsyslog_size(self):
+ # test for size option
+ self.cli_set(base_path + ['logrotate', 'messages', 'max-size', '50'])
+ self.cli_commit()
+
+ # read the config file and check content
+ logrotate_config = logrotate_config_parse(logrotate_rsyslog_file)
+ self.assertEqual(logrotate_config['size'], '50M')
+
+ def test_logs_rsyslog_rotate(self):
+ # test for rotate option
+ self.cli_set(base_path + ['logrotate', 'messages', 'rotate', '50'])
+ self.cli_commit()
+
+ # read the config file and check content
+ logrotate_config = logrotate_config_parse(logrotate_rsyslog_file)
+ self.assertEqual(logrotate_config['rotate'], '50')
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py
new file mode 100644
index 0000000..ffb1d76
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_option.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import read_file
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.system import sysctl_read
+
+base_path = ['system', 'option']
+
+class TestSystemOption(VyOSUnitTestSHIM.TestCase):
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_ctrl_alt_delete(self):
+ self.cli_set(base_path + ['ctrl-alt-delete', 'reboot'])
+ self.cli_commit()
+
+ tmp = os.readlink('/lib/systemd/system/ctrl-alt-del.target')
+ self.assertEqual(tmp, '/lib/systemd/system/reboot.target')
+
+ self.cli_set(base_path + ['ctrl-alt-delete', 'poweroff'])
+ self.cli_commit()
+
+ tmp = os.readlink('/lib/systemd/system/ctrl-alt-del.target')
+ self.assertEqual(tmp, '/lib/systemd/system/poweroff.target')
+
+ self.cli_delete(base_path + ['ctrl-alt-delete', 'poweroff'])
+ self.cli_commit()
+ self.assertFalse(os.path.exists('/lib/systemd/system/ctrl-alt-del.target'))
+
+ def test_reboot_on_panic(self):
+ panic_file = '/proc/sys/kernel/panic'
+
+ tmp = read_file(panic_file)
+ self.assertEqual(tmp, '0')
+
+ self.cli_set(base_path + ['reboot-on-panic'])
+ self.cli_commit()
+
+ tmp = read_file(panic_file)
+ self.assertEqual(tmp, '60')
+
+ def test_performance(self):
+ tuned_service = 'tuned.service'
+
+ self.assertFalse(is_systemd_service_active(tuned_service))
+
+ # T3204 sysctl options must not be overwritten by tuned
+ gc_thresh1 = '131072'
+ gc_thresh2 = '262000'
+ gc_thresh3 = '524000'
+
+ self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh1', 'value', gc_thresh1])
+ self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh2', 'value', gc_thresh2])
+ self.cli_set(['system', 'sysctl', 'parameter', 'net.ipv4.neigh.default.gc_thresh3', 'value', gc_thresh3])
+
+ self.cli_set(base_path + ['performance', 'throughput'])
+ self.cli_commit()
+
+ self.assertTrue(is_systemd_service_active(tuned_service))
+
+ self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh1'), gc_thresh1)
+ self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh2'), gc_thresh2)
+ self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh3'), gc_thresh3)
+
+ def test_ssh_client_options(self):
+ loopback = 'lo'
+ ssh_client_opt_file = '/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf'
+
+ self.cli_set(['system', 'option', 'ssh-client', 'source-interface', loopback])
+ self.cli_commit()
+
+ tmp = read_file(ssh_client_opt_file)
+ self.assertEqual(tmp, f'BindInterface {loopback}')
+
+ self.cli_delete(['system', 'option'])
+ self.cli_commit()
+ self.assertFalse(os.path.exists(ssh_client_opt_file))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, failfast=True)
diff --git a/smoketest/scripts/cli/test_system_resolvconf.py b/smoketest/scripts/cli/test_system_resolvconf.py
new file mode 100644
index 0000000..d8726a3
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_resolvconf.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.file import read_file
+
+RESOLV_CONF = '/etc/resolv.conf'
+
+name_servers = ['192.0.2.10', '2001:db8:1::100']
+domain_name = 'vyos.net'
+domain_search = ['vyos.net', 'vyos.io']
+
+base_path_nameserver = ['system', 'name-server']
+base_path_domainname = ['system', 'domain-name']
+base_path_domainsearch = ['system', 'domain-search']
+
+def get_name_servers():
+ resolv_conf = read_file(RESOLV_CONF)
+ return re.findall(r'\n?nameserver\s+(.*)', resolv_conf)
+
+def get_domain_name():
+ resolv_conf = read_file(RESOLV_CONF)
+ res = re.findall(r'\n?domain\s+(.*)', resolv_conf)
+ return res[0] if res else None
+
+def get_domain_searches():
+ resolv_conf = read_file(RESOLV_CONF)
+ res = re.findall(r'\n?search\s+(.*)', resolv_conf)
+ return res[0].split() if res else []
+
+class TestSystemResolvConf(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemResolvConf, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, base_path_nameserver)
+ cls.cli_delete(cls, base_path_domainname)
+ cls.cli_delete(cls, base_path_domainsearch)
+
+ def tearDown(self):
+ # Delete test entries servers
+ self.cli_delete(base_path_nameserver)
+ self.cli_delete(base_path_domainname)
+ self.cli_delete(base_path_domainsearch)
+ self.cli_commit()
+
+ def test_nameserver(self):
+ # Check if server is added to resolv.conf
+ for s in name_servers:
+ self.cli_set(base_path_nameserver + [s])
+ self.cli_commit()
+
+ for s in get_name_servers():
+ self.assertTrue(s in name_servers)
+
+ # Test if a deleted server disappears from resolv.conf
+ for s in name_servers:
+ self.cli_delete(base_path_nameserver + [s])
+ self.cli_commit()
+
+ for s in get_name_servers():
+ self.assertTrue(s not in name_servers)
+
+ def test_domainname(self):
+ # Check if domain-name is added to resolv.conf
+ self.cli_set(base_path_domainname + [domain_name])
+ self.cli_commit()
+
+ self.assertEqual(get_domain_name(), domain_name)
+
+ # Test if domain-name disappears from resolv.conf
+ self.cli_delete(base_path_domainname + [domain_name])
+ self.cli_commit()
+
+ self.assertTrue(get_domain_name() is None)
+
+ def test_domainsearch(self):
+ # Check if domain-search is added to resolv.conf
+ for s in domain_search:
+ self.cli_set(base_path_domainsearch + [s])
+ self.cli_commit()
+
+ for s in get_domain_searches():
+ self.assertTrue(s in domain_search)
+
+ # Test if domain-search disappears from resolv.conf
+ for s in domain_search:
+ self.cli_delete(base_path_domainsearch + [s])
+ self.cli_commit()
+
+ for s in get_domain_searches():
+ self.assertTrue(s not in domain_search)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_sflow.py b/smoketest/scripts/cli/test_system_sflow.py
new file mode 100644
index 0000000..74c0654
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_sflow.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'hsflowd'
+base_path = ['system', 'sflow']
+vrf = 'mgmt'
+
+hsflowd_conf = '/run/sflow/hsflowd.conf'
+
+class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestSystemFlowAccounting, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # after service removal process must no longer run
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_delete(['vrf', 'name', vrf])
+ self.cli_commit()
+
+ # after service removal process must no longer run
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_sflow(self):
+ agent_address = '192.0.2.5'
+ agent_interface = 'eth0'
+ polling = '24'
+ sampling_rate = '128'
+ server = '192.0.2.254'
+ local_server = '127.0.0.1'
+ port = '8192'
+ default_port = '6343'
+ mon_limit = '50'
+
+ self.cli_set(
+ ['interfaces', 'dummy', 'dum0', 'address', f'{agent_address}/24'])
+ self.cli_set(base_path + ['agent-address', agent_address])
+ self.cli_set(base_path + ['agent-interface', agent_interface])
+
+ # You need to configure at least one interface for sflow
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in Section.interfaces('ethernet'):
+ self.cli_set(base_path + ['interface', interface])
+
+ self.cli_set(base_path + ['polling', polling])
+ self.cli_set(base_path + ['sampling-rate', sampling_rate])
+ self.cli_set(base_path + ['server', server, 'port', port])
+ self.cli_set(base_path + ['server', local_server])
+ self.cli_set(base_path + ['drop-monitor-limit', mon_limit])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ hsflowd = read_file(hsflowd_conf)
+
+ self.assertIn(f'polling={polling}', hsflowd)
+ self.assertIn(f'sampling={sampling_rate}', hsflowd)
+ self.assertIn(f'agentIP={agent_address}', hsflowd)
+ self.assertIn(f'agent={agent_interface}', hsflowd)
+ self.assertIn(f'collector {{ ip = {server} udpport = {port} }}', hsflowd)
+ self.assertIn(f'collector {{ ip = {local_server} udpport = {default_port} }}', hsflowd)
+ self.assertIn(f'dropmon {{ limit={mon_limit} start=on sw=on hw=off }}', hsflowd)
+ self.assertIn('dbus { }', hsflowd)
+
+ for interface in Section.interfaces('ethernet'):
+ self.assertIn(f'pcap {{ dev={interface} }}', hsflowd)
+
+ def test_vrf(self):
+ interface = 'eth0'
+ server = '192.0.2.1'
+
+ # Check if sFlow service can be bound to given VRF
+ self.cli_set(['vrf', 'name', vrf, 'table', '10100'])
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['server', server])
+ self.cli_set(base_path + ['vrf', vrf])
+
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ hsflowd = read_file(hsflowd_conf)
+ self.assertIn(f'collector {{ ip = {server} udpport = 6343 }}', hsflowd) # default port
+ self.assertIn(f'pcap {{ dev=eth0 }}', hsflowd)
+
+ # Check for process in VRF
+ tmp = cmd(f'ip vrf pids {vrf}')
+ self.assertIn(PROCESS_NAME, tmp)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py
new file mode 100644
index 0000000..c802cee
--- /dev/null
+++ b/smoketest/scripts/cli/test_system_syslog.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.utils.file import read_file
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'rsyslogd'
+RSYSLOG_CONF = '/etc/rsyslog.d/00-vyos.conf'
+
+base_path = ['system', 'syslog']
+
+def get_config_value(key):
+ tmp = read_file(RSYSLOG_CONF)
+ tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp)
+ return tmp[0]
+
+class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestRSYSLOGService, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SYSLOG config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ # Check for running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_syslog_basic(self):
+ host1 = '127.0.0.10'
+ host2 = '127.0.0.20'
+
+ self.cli_set(base_path + ['host', host1, 'port', '999'])
+ self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all'])
+ self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err'])
+ self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning'])
+
+ self.cli_commit()
+ # verify log level and facilities in config file
+ # *.warning /dev/console
+ # *.* @198.51.100.1:999
+ # kern.err @192.0.2.1:514
+ config = [
+ get_config_value('\*.\*'),
+ get_config_value('kern.err'),
+ get_config_value('\*.warning'),
+ ]
+ expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console']
+
+ for i in range(0, 3):
+ self.assertIn(expected[i], config[i])
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_syslog_global(self):
+ self.cli_set(['system', 'host-name', 'vyos'])
+ self.cli_set(['system', 'domain-name', 'example.local'])
+ self.cli_set(base_path + ['global', 'marker', 'interval', '600'])
+ self.cli_set(base_path + ['global', 'preserve-fqdn'])
+ self.cli_set(base_path + ['global', 'facility', 'kern', 'level', 'err'])
+
+ self.cli_commit()
+
+ config = cmd(f'sudo cat {RSYSLOG_CONF}')
+ expected = [
+ '$MarkMessagePeriod 600',
+ '$PreserveFQDN on',
+ 'kern.err',
+ '$LocalHostName vyos.example.local',
+ ]
+
+ for e in expected:
+ self.assertIn(e, config)
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
new file mode 100644
index 0000000..3b8687b
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -0,0 +1,1359 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Interface
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+ethernet_path = ['interfaces', 'ethernet']
+tunnel_path = ['interfaces', 'tunnel']
+vti_path = ['interfaces', 'vti']
+nhrp_path = ['protocols', 'nhrp']
+base_path = ['vpn', 'ipsec']
+
+charon_file = '/etc/strongswan.d/charon.conf'
+dhcp_interfaces_file = '/tmp/ipsec_dhcp_interfaces'
+swanctl_file = '/etc/swanctl/swanctl.conf'
+
+peer_ip = '203.0.113.45'
+connection_name = 'main-branch'
+local_id = 'left'
+remote_id = 'right'
+interface = 'eth1'
+vif = '100'
+esp_group = 'MyESPGroup'
+ike_group = 'MyIKEGroup'
+secret = 'MYSECRETKEY'
+PROCESS_NAME = 'charon-systemd'
+regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}'
+
+ca_name = 'MyVyOS-CA'
+ca_pem = """
+MIICMDCCAdegAwIBAgIUBCzIjYvD7SPbx5oU18IYg7NVxQ0wCgYIKoZIzj0EAwIw
+ZzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEgMB4GA1UEAwwXSVBTZWMgU21va2V0ZXN0
+IFJvb3QgQ0EwHhcNMjMwOTI0MTIwMzQxWhcNMzMwOTIxMTIwMzQxWjBnMQswCQYD
+VQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJU29tZS1DaXR5
+MQ0wCwYDVQQKDARWeU9TMSAwHgYDVQQDDBdJUFNlYyBTbW9rZXRlc3QgUm9vdCBD
+QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEh8/yU572B3zmFxrGgHk+H7grYt
+EHUJodY3gXNWMHz0gySrbGhsGtECDfP/G+T4Suk7cuVzB1wnLocSafD8TcqjYTBf
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsG
+AQUFBwMCBggrBgEFBQcDATAdBgNVHQ4EFgQUTYoQJNlk7X87/gRegHnCnPef39Aw
+CgYIKoZIzj0EAwIDRwAwRAIgX1spXjrUc10r3g/Zm4O31LU5O08J2vVqFo94zHE5
+0VgCIG4JK9Zg5O/yn4mYksZux7efiHRUzL2y2TXQ9IqrqM8W
+"""
+
+int_ca_name = 'MyVyOS-IntCA'
+int_ca_pem = """
+MIICYDCCAgWgAwIBAgIUcFx2BVYErHI+SneyPYHijxXt1cgwCgYIKoZIzj0EAwIw
+ZzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEgMB4GA1UEAwwXSVBTZWMgU21va2V0ZXN0
+IFJvb3QgQ0EwHhcNMjMwOTI0MTIwNTE5WhcNMzMwOTIwMTIwNTE5WjBvMQswCQYD
+VQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJU29tZS1DaXR5
+MQ0wCwYDVQQKDARWeU9TMSgwJgYDVQQDDB9JUFNlYyBTbW9rZXRlc3QgSW50ZXJt
+ZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIHw2G5dq3c715AcA
+tzR++dYu1fLRFmHzRGTZOT7hLrh2Fg4hnKFPLOeUA5Qi50xCvjJ9JnonTyy2RfRH
+axYizKOBhjCBgzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAd
+BgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwHQYDVR0OBBYEFC9KrFYtA+hO
+l7vdMbWxTMAyLB7BMB8GA1UdIwQYMBaAFE2KECTZZO1/O/4EXoB5wpz3n9/QMAoG
+CCqGSM49BAMCA0kAMEYCIQCnqWbElgOL9dGO3iLxasFNq/hM7vM/DzaiHi4BowxW
+0gIhAMohefNj+QgLfPhvyODHIPE9LMyfp7lJEaCC2K8PCSFD
+"""
+
+peer_name = 'peer1'
+peer_cert = """
+MIICSTCCAfCgAwIBAgIUPxYleUgCo/glVVePze3QmAFgi6MwCgYIKoZIzj0EAwIw
+bzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEoMCYGA1UEAwwfSVBTZWMgU21va2V0ZXN0
+IEludGVybWVkaWF0ZSBDQTAeFw0yMzA5MjQxMjA2NDJaFw0yODA5MjIxMjA2NDJa
+MGQxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlT
+b21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxHTAbBgNVBAMMFElQU2VjIFNtb2tldGVz
+dCBQZWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZJtuTDu84uy++GMwRNLl
+10JAXZxXQSDl+CdTWwjbQZURcdY+ia7BoaoYX/0VKPel3Se64rIUQQLQoY/9MJb9
+UKN1MHMwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI
+KwYBBQUHAwEwHQYDVR0OBBYEFNJCdnkm3cAmf04UwOKL7IqMJ6OXMB8GA1UdIwQY
+MBaAFC9KrFYtA+hOl7vdMbWxTMAyLB7BMAoGCCqGSM49BAMCA0cAMEQCIGVnDRUy
+UJ0U/deDvrBo1+AakZndkNAMN/XNo5a5GzhEAiBCY7E/3b0BIO8FiIbVB3iDcaxg
+g7ET2RgWxvhEoN3ZRw==
+"""
+
+peer_key = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVDEZDK7q/T+tiJUV
+WLKS3ZYDfZ4lZv0C1gJpYq0gWP2hRANCAARkm25MO7zi7L74YzBE0uXXQkBdnFdB
+IOX4J1NbCNtBlRFx1j6JrsGhqhhf/RUo96XdJ7rishRBAtChj/0wlv1Q
+"""
+
+swanctl_dir = '/etc/swanctl'
+CERT_PATH = f'{swanctl_dir}/x509/'
+CA_PATH = f'{swanctl_dir}/x509ca/'
+
+class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
+ skip_process_check = False
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestVPNIPsec, cls).setUpClass()
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+ cls.cli_delete(cls, ['pki'])
+
+ cls.cli_set(cls, base_path + ['interface', f'{interface}.{vif}'])
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestVPNIPsec, cls).tearDownClass()
+ cls.cli_delete(cls, base_path + ['interface', f'{interface}.{vif}'])
+
+ def setUp(self):
+ # Set IKE/ESP Groups
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha1'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha1'])
+
+ def tearDown(self):
+ # Check for running process
+ if not self.skip_process_check:
+ self.assertTrue(process_named_running(PROCESS_NAME))
+ else:
+ self.skip_process_check = False # Reset
+
+ self.cli_delete(base_path)
+ self.cli_delete(tunnel_path)
+ self.cli_delete(vti_path)
+ self.cli_commit()
+
+ # Check for no longer running process
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def setupPKI(self):
+ self.cli_set(['pki', 'ca', ca_name, 'certificate', ca_pem.replace('\n','')])
+ self.cli_set(['pki', 'ca', int_ca_name, 'certificate', int_ca_pem.replace('\n','')])
+ self.cli_set(['pki', 'certificate', peer_name, 'certificate', peer_cert.replace('\n','')])
+ self.cli_set(['pki', 'certificate', peer_name, 'private', 'key', peer_key.replace('\n','')])
+
+ def tearDownPKI(self):
+ self.cli_delete(['pki'])
+
+ def test_dhcp_fail_handling(self):
+ # Skip process check - connection is not created for this test
+ self.skip_process_check = True
+
+ # Interface for dhcp-interface
+ self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server
+
+ # vpn ipsec auth psk <tag> id <x.x.x.x>
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
+
+ # Site to site
+ peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_base_path + ['dhcp-interface', f'{interface}.{vif}'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'gre'])
+
+ self.cli_commit()
+
+ self.assertTrue(os.path.exists(dhcp_interfaces_file))
+
+ dhcp_interfaces = read_file(dhcp_interfaces_file)
+ self.assertIn(f'{interface}.{vif}', dhcp_interfaces) # Ensure dhcp interface was added for dhclient hook
+
+ self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address'])
+
+ def test_site_to_site(self):
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+
+ local_address = '192.0.2.10'
+ priority = '20'
+ life_bytes = '100000'
+ life_packets = '2000000'
+
+ # vpn ipsec auth psk <tag> id <x.x.x.x>
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
+
+ # Site to site
+ peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+
+ self.cli_set(base_path + ['esp-group', esp_group, 'life-bytes', life_bytes])
+ self.cli_set(base_path + ['esp-group', esp_group, 'life-packets', life_packets])
+
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_base_path + ['local-address', local_address])
+ self.cli_set(peer_base_path + ['remote-address', peer_ip])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'tcp'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.10.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.11.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'port', '443'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.10.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.11.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'port', '443'])
+
+ self.cli_set(peer_base_path + ['tunnel', '2', 'local', 'prefix', '10.1.0.0/16'])
+ self.cli_set(peer_base_path + ['tunnel', '2', 'remote', 'prefix', '10.2.0.0/16'])
+ self.cli_set(peer_base_path + ['tunnel', '2', 'priority', priority])
+
+ self.cli_commit()
+
+ # Verify strongSwan configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_conf_lines = [
+ f'version = 2',
+ f'auth = psk',
+ f'life_bytes = {life_bytes}',
+ f'life_packets = {life_packets}',
+ f'rekey_time = 28800s', # default value
+ f'proposals = aes128-sha1-modp1024',
+ f'esp_proposals = aes128-sha1-modp1024',
+ f'life_time = 3600s', # default value
+ f'local_addrs = {local_address} # dhcp:no',
+ f'remote_addrs = {peer_ip}',
+ f'mode = tunnel',
+ f'{connection_name}-tunnel-1',
+ f'local_ts = 172.16.10.0/24[tcp/443],172.16.11.0/24[tcp/443]',
+ f'remote_ts = 172.17.10.0/24[tcp/443],172.17.11.0/24[tcp/443]',
+ f'mode = tunnel',
+ f'{connection_name}-tunnel-2',
+ f'local_ts = 10.1.0.0/16',
+ f'remote_ts = 10.2.0.0/16',
+ f'priority = {priority}',
+ f'mode = tunnel',
+ f'replay_window = 32',
+ ]
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # if dpd is not specified it should not be enabled (see T6599)
+ swanctl_unexpected_lines = [
+ f'dpd_timeout'
+ f'dpd_delay'
+ ]
+
+ for unexpected_line in swanctl_unexpected_lines:
+ self.assertNotIn(unexpected_line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'id-{regex_uuid4} = "{local_id}"',
+ f'id-{regex_uuid4} = "{remote_id}"',
+ f'id-{regex_uuid4} = "{local_address}"',
+ f'id-{regex_uuid4} = "{peer_ip}"',
+ f'secret = "{secret}"'
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertRegex(swanctl_conf, fr'{line}')
+
+
+ def test_site_to_site_vti(self):
+ local_address = '192.0.2.10'
+ vti = 'vti10'
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'disable-mobike'])
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'compression'])
+ # VTI interface
+ self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24'])
+
+ # vpn ipsec auth psk <tag> id <x.x.x.x>
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
+
+ # Site to site
+ peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_base_path + ['connection-type', 'none'])
+ self.cli_set(peer_base_path + ['force-udp-encapsulation'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_base_path + ['local-address', local_address])
+ self.cli_set(peer_base_path + ['remote-address', peer_ip])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.10.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'local', 'prefix', '172.16.11.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.10.0/24'])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'remote', 'prefix', '172.17.11.0/24'])
+ self.cli_set(peer_base_path + ['vti', 'bind', vti])
+ self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group])
+
+ self.cli_commit()
+
+ swanctl_conf = read_file(swanctl_file)
+ if_id = vti.lstrip('vti')
+ # The key defaults to 0 and will match any policies which similarly do
+ # not have a lookup key configuration - thus we shift the key by one
+ # to also support a vti0 interface
+ if_id = str(int(if_id) +1)
+ swanctl_conf_lines = [
+ f'version = 2',
+ f'auth = psk',
+ f'proposals = aes128-sha1-modp1024',
+ f'esp_proposals = aes128-sha1-modp1024',
+ f'local_addrs = {local_address} # dhcp:no',
+ f'mobike = no',
+ f'remote_addrs = {peer_ip}',
+ f'mode = tunnel',
+ f'local_ts = 172.16.10.0/24,172.16.11.0/24',
+ f'remote_ts = 172.17.10.0/24,172.17.11.0/24',
+ f'ipcomp = yes',
+ f'start_action = none',
+ f'replay_window = 32',
+ f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one
+ f'if_id_out = {if_id}',
+ f'updown = "/etc/ipsec.d/vti-up-down {vti}"'
+ ]
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'id-{regex_uuid4} = "{local_id}"',
+ f'id-{regex_uuid4} = "{remote_id}"',
+ f'secret = "{secret}"'
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertRegex(swanctl_conf, fr'{line}')
+
+ # Site-to-site interfaces should start out as 'down'
+ self.assertEqual(Interface(vti).get_admin_state(), 'down')
+
+ # Disable PKI
+ self.tearDownPKI()
+
+
+ def test_dmvpn(self):
+ tunnel_if = 'tun100'
+ nhrp_secret = 'secret'
+ ike_lifetime = '3600'
+ esp_lifetime = '1800'
+
+ # Tunnel
+ self.cli_set(tunnel_path + [tunnel_if, 'address', '172.16.253.134/29'])
+ self.cli_set(tunnel_path + [tunnel_if, 'encapsulation', 'gre'])
+ self.cli_set(tunnel_path + [tunnel_if, 'source-address', '192.0.2.1'])
+ self.cli_set(tunnel_path + [tunnel_if, 'enable-multicast'])
+ self.cli_set(tunnel_path + [tunnel_if, 'parameters', 'ip', 'key', '1'])
+
+ # NHRP
+ self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'cisco-authentication', nhrp_secret])
+ self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'holding-time', '300'])
+ self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'multicast', 'dynamic'])
+ self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'redirect'])
+ self.cli_set(nhrp_path + ['tunnel', tunnel_if, 'shortcut'])
+
+ # IKE/ESP Groups
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', esp_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'mode', 'transport'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'dh-group2'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', '3des'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'md5'])
+
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev1'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha1'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'prf', 'prfsha1'])
+
+ # Profile
+ self.cli_set(base_path + ['profile', 'NHRPVPN', 'authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(base_path + ['profile', 'NHRPVPN', 'authentication', 'pre-shared-secret', nhrp_secret])
+ self.cli_set(base_path + ['profile', 'NHRPVPN', 'bind', 'tunnel', tunnel_if])
+ self.cli_set(base_path + ['profile', 'NHRPVPN', 'esp-group', esp_group])
+ self.cli_set(base_path + ['profile', 'NHRPVPN', 'ike-group', ike_group])
+
+ self.cli_commit()
+
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'proposals = aes128-sha1-modp1024,aes256-sha1-prfsha1-modp1024',
+ f'version = 1',
+ f'rekey_time = {ike_lifetime}s',
+ f'rekey_time = {esp_lifetime}s',
+ f'esp_proposals = aes128-sha1-modp1024,aes256-sha1-modp1024,3des-md5-modp1024',
+ f'local_ts = dynamic[gre]',
+ f'remote_ts = dynamic[gre]',
+ f'mode = transport',
+ f'secret = {nhrp_secret}'
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # There is only one NHRP test so no need to delete this globally in tearDown()
+ self.cli_delete(nhrp_path)
+
+ def test_site_to_site_x509(self):
+ # Enable PKI
+ self.setupPKI()
+
+ vti = 'vti20'
+ self.cli_set(vti_path + [vti, 'address', '192.168.0.1/31'])
+
+ peer_ip = '172.18.254.202'
+ connection_name = 'office'
+ local_address = '172.18.254.201'
+ peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+
+ self.cli_set(peer_base_path + ['authentication', 'local-id', peer_name])
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'x509'])
+ self.cli_set(peer_base_path + ['authentication', 'remote-id', 'peer2'])
+ self.cli_set(peer_base_path + ['authentication', 'x509', 'ca-certificate', ca_name])
+ self.cli_set(peer_base_path + ['authentication', 'x509', 'ca-certificate', int_ca_name])
+ self.cli_set(peer_base_path + ['authentication', 'x509', 'certificate', peer_name])
+ self.cli_set(peer_base_path + ['connection-type', 'initiate'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['ikev2-reauth', 'inherit'])
+ self.cli_set(peer_base_path + ['local-address', local_address])
+ self.cli_set(peer_base_path + ['remote-address', peer_ip])
+ self.cli_set(peer_base_path + ['vti', 'bind', vti])
+ self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group])
+
+ self.cli_commit()
+
+ swanctl_conf = read_file(swanctl_file)
+ tmp = peer_ip.replace('.', '-')
+ if_id = vti.lstrip('vti')
+ # The key defaults to 0 and will match any policies which similarly do
+ # not have a lookup key configuration - thus we shift the key by one
+ # to also support a vti0 interface
+ if_id = str(int(if_id) +1)
+ swanctl_lines = [
+ f'{connection_name}',
+ f'version = 0', # key-exchange not set - defaulting to 0 for ikev1 and ikev2
+ f'send_cert = always',
+ f'mobike = yes',
+ f'keyingtries = 0',
+ f'id = "{peer_name}"',
+ f'auth = pubkey',
+ f'certs = {peer_name}.pem',
+ f'proposals = aes128-sha1-modp1024',
+ f'esp_proposals = aes128-sha1-modp1024',
+ f'local_addrs = {local_address} # dhcp:no',
+ f'remote_addrs = {peer_ip}',
+ f'local_ts = 0.0.0.0/0,::/0',
+ f'remote_ts = 0.0.0.0/0,::/0',
+ f'updown = "/etc/ipsec.d/vti-up-down {vti}"',
+ f'if_id_in = {if_id}', # will be 11 for vti10
+ f'if_id_out = {if_id}',
+ f'ipcomp = no',
+ f'mode = tunnel',
+ f'start_action = start',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'{connection_name}',
+ f'file = {peer_name}.pem',
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ # Disable PKI
+ self.tearDownPKI()
+
+
+ def test_flex_vpn_vips(self):
+ local_address = '192.0.2.5'
+ local_id = 'vyos-r1'
+ remote_id = 'vyos-r2'
+ peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+
+ self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre'])
+ self.cli_set(tunnel_path + ['tun1', 'source-address', local_address])
+
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['options', 'flexvpn'])
+ self.cli_set(base_path + ['options', 'interface', 'tun1'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+
+ # vpn ipsec auth psk <tag> id <x.x.x.x>
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
+
+ self.cli_set(peer_base_path + ['authentication', 'local-id', local_id])
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_base_path + ['authentication', 'remote-id', remote_id])
+ self.cli_set(peer_base_path + ['connection-type', 'initiate'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_base_path + ['local-address', local_address])
+ self.cli_set(peer_base_path + ['remote-address', peer_ip])
+ self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'gre'])
+
+ self.cli_set(peer_base_path + ['virtual-address', '203.0.113.55'])
+ self.cli_set(peer_base_path + ['virtual-address', '203.0.113.56'])
+
+ self.cli_commit()
+
+ # Verify strongSwan configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_conf_lines = [
+ f'version = 2',
+ f'vips = 203.0.113.55, 203.0.113.56',
+ f'life_time = 3600s', # default value
+ f'local_addrs = {local_address} # dhcp:no',
+ f'remote_addrs = {peer_ip}',
+ f'{connection_name}-tunnel-1',
+ f'mode = tunnel',
+ ]
+
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'id-{regex_uuid4} = "{local_id}"',
+ f'id-{regex_uuid4} = "{remote_id}"',
+ f'id-{regex_uuid4} = "{peer_ip}"',
+ f'id-{regex_uuid4} = "{local_address}"',
+ f'secret = "{secret}"',
+ ]
+
+ for line in swanctl_secrets_lines:
+ self.assertRegex(swanctl_conf, fr'{line}')
+
+ # Verify charon configuration
+ charon_conf = read_file(charon_file)
+ charon_conf_lines = [
+ f'# Cisco FlexVPN',
+ f'cisco_flexvpn = yes',
+ f'install_virtual_ip = yes',
+ f'install_virtual_ip_on = tun1',
+ ]
+
+ for line in charon_conf_lines:
+ self.assertIn(line, charon_conf)
+
+
+ def test_remote_access(self):
+ # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'auth = eap-mschapv2',
+ f'eap_id = %any',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'replay_window = 32',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'eap-{conn_name}-{username}',
+ f'secret = "{password}"',
+ f'id-{conn_name}-{username} = "{username}"',
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {prefix}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
+ def test_remote_access_eap_tls(self):
+ # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ # Use EAP-TLS auth instead of default EAP-MSCHAPv2
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'eap-tls'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'cacerts = MyVyOS-CA.pem',
+ f'auth = eap-tls',
+ f'eap_id = %any',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {prefix}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ # Test setting of custom EAP ID
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'eap-id', 'eap-user@vyos.net'])
+ self.cli_commit()
+ self.assertIn(r'eap_id = eap-user@vyos.net', read_file(swanctl_file))
+
+ self.tearDownPKI()
+
+ def test_remote_access_x509(self):
+ # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ # Use client-mode x509 instead of default EAP-MSCHAPv2
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', int_ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_unexpected_lines = [
+ f'auth = eap-',
+ f'eap_id'
+ ]
+ for unexpected_line in swanctl_unexpected_lines:
+ self.assertNotIn(unexpected_line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {prefix}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
+ def test_remote_access_dhcp_fail_handling(self):
+ # Skip process check - connection is not created for this test
+ self.skip_process_check = True
+
+ # Interface for dhcp-interface
+ self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server
+
+ # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_server = '172.16.254.100'
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'dhcp-interface', f'{interface}.{vif}'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', name_server])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ self.assertTrue(os.path.exists(dhcp_interfaces_file))
+
+ dhcp_interfaces = read_file(dhcp_interfaces_file)
+ self.assertIn(f'{interface}.{vif}', dhcp_interfaces) # Ensure dhcp interface was added for dhclient hook
+
+ self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address'])
+
+ self.tearDownPKI()
+
+ def test_remote_access_no_rekey(self):
+ # In some RA secnarios, disabling server-initiated rekey of IKE and CHILD SA is desired
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', '0'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'disable-rekey'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ # Use client-mode x509 instead of default EAP-MSCHAPv2
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', int_ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = 0s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'rekey_time = 0s',
+ f'dpd_action = clear',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {prefix}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
+ def test_remote_access_pool_range(self):
+ # Same as test_remote_access but using an IP pool range instead of prefix
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ range_start = '172.16.250.2'
+ range_stop = '172.16.250.254'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'start', range_start])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'stop', range_stop])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'auth = eap-mschapv2',
+ f'eap_id = %any',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'replay_window = 32',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'eap-{conn_name}-{username}',
+ f'secret = "{password}"',
+ f'id-{conn_name}-{username} = "{username}"',
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {range_start}-{range_stop}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
+ def test_remote_access_vti(self):
+ # Set up and use a VTI interface for the remote access VPN
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ vti = 'vti10'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['10.1.1.1']
+ range_start = '10.1.1.10'
+ range_stop = '10.1.1.254'
+
+ # VTI interface
+ self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24'])
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'bind', vti])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'start', range_start])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'stop', range_stop])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+
+ if_id = vti.lstrip('vti')
+ # The key defaults to 0 and will match any policies which similarly do
+ # not have a lookup key configuration - thus we shift the key by one
+ # to also support a vti0 interface
+ if_id = str(int(if_id) +1)
+
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'auth = eap-mschapv2',
+ f'eap_id = %any',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'replay_window = 32',
+ f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one
+ f'if_id_out = {if_id}',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'eap-{conn_name}-{username}',
+ f'secret = "{password}"',
+ f'id-{conn_name}-{username} = "{username}"',
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {range_start}-{range_stop}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ # Remote access interfaces should be set to 'up' during configure
+ self.assertEqual(Interface(vti).get_admin_state(), 'up')
+
+ # Delete the connection to verify the VTI interfaces is taken down
+ self.cli_delete(base_path + ['remote-access', 'connection', conn_name])
+ self.cli_commit()
+ self.assertEqual(Interface(vti).get_admin_state(), 'down')
+
+ self.tearDownPKI()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py
new file mode 100644
index 0000000..07a7e29
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpn_l2tp.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_accel_ppp_test import BasicAccelPPPTest
+from configparser import ConfigParser
+from vyos.utils.process import cmd
+
+
+class TestVPNL2TPServer(BasicAccelPPPTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['vpn', 'l2tp', 'remote-access']
+ cls._config_file = '/run/accel-pppd/l2tp.conf'
+ cls._chap_secrets = '/run/accel-pppd/l2tp.chap-secrets'
+ cls._protocol_section = 'l2tp'
+ # call base-classes classmethod
+ super(TestVPNL2TPServer, cls).setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestVPNL2TPServer, cls).tearDownClass()
+
+ def basic_protocol_specific_config(self):
+ pass
+
+ def test_l2tp_server_authentication_protocols(self):
+ # Test configuration of local authentication protocols
+ self.basic_config()
+
+ # explicitly test mschap-v2 - no special reason
+ self.set( ['authentication', 'protocols', 'mschap-v2'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True)
+ conf.read(self._config_file)
+
+ self.assertEqual(conf['modules']['auth_mschap_v2'], None)
+
+ def test_vpn_l2tp_dependence_ipsec_swanctl(self):
+ # Test config vpn for tasks T3843 and T5926
+
+ base_path = ['vpn', 'l2tp', 'remote-access']
+ # make precondition
+ self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '203.0.113.1/32'])
+ self.cli_set(['vpn', 'ipsec', 'interface', 'dum0'])
+
+ self.cli_commit()
+ # check ipsec apply to swanctl
+ self.assertEqual('', cmd('echo vyos | sudo -S swanctl -L '))
+
+ self.cli_set(base_path + ['authentication', 'local-users', 'username', 'foo', 'password', 'bar'])
+ self.cli_set(base_path + ['authentication', 'mode', 'local'])
+ self.cli_set(base_path + ['authentication', 'protocols', 'chap'])
+ self.cli_set(base_path + ['client-ip-pool', 'first', 'range', '10.200.100.100-10.200.100.110'])
+ self.cli_set(base_path + ['description', 'VPN - REMOTE'])
+ self.cli_set(base_path + ['name-server', '1.1.1.1'])
+ self.cli_set(base_path + ['ipsec-settings', 'authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(base_path + ['ipsec-settings', 'authentication', 'pre-shared-secret', 'SeCret'])
+ self.cli_set(base_path + ['ipsec-settings', 'ike-lifetime', '8600'])
+ self.cli_set(base_path + ['ipsec-settings', 'lifetime', '3600'])
+ self.cli_set(base_path + ['outside-address', '203.0.113.1'])
+ self.cli_set(base_path + ['gateway-address', '203.0.113.1'])
+
+ self.cli_commit()
+
+ # check l2tp apply to swanctl
+ self.assertTrue('l2tp_remote_access:' in cmd('echo vyos | sudo -S swanctl -L '))
+
+ self.cli_delete(['vpn', 'l2tp'])
+ self.cli_commit()
+
+ # check l2tp apply to swanctl after delete config
+ self.assertEqual('', cmd('echo vyos | sudo -S swanctl -L '))
+
+ # need to correct tearDown test
+ self.basic_config()
+ self.cli_set(base_path + ['authentication', 'protocols', 'chap'])
+ self.cli_commit()
+
+ def test_l2tp_radius_server(self):
+ base_path = ['vpn', 'l2tp', 'remote-access']
+ radius_server = "192.0.2.22"
+ radius_key = "secretVyOS"
+
+ self.cli_set(base_path + ['authentication', 'mode', 'radius'])
+ self.cli_set(base_path + ['gateway-address', '192.0.2.1'])
+ self.cli_set(base_path + ['client-ip-pool', 'SIMPLE-POOL', 'range', '192.0.2.0/24'])
+ self.cli_set(base_path + ['default-pool', 'SIMPLE-POOL'])
+ self.cli_set(base_path + ['authentication', 'radius', 'server', radius_server, 'key', radius_key])
+ self.cli_set(base_path + ['authentication', 'radius', 'server', radius_server, 'priority', '10'])
+ self.cli_set(base_path + ['authentication', 'radius', 'server', radius_server, 'backup'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True)
+ conf.read(self._config_file)
+ server = conf["radius"]["server"].split(",")
+ self.assertIn('weight=10', server)
+ self.assertIn('backup', server)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py
new file mode 100644
index 0000000..dcce229
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpn_openconnect.py
@@ -0,0 +1,268 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.template import ip_from_cidr
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+OCSERV_CONF = '/run/ocserv/ocserv.conf'
+base_path = ['vpn', 'openconnect']
+
+pki_path = ['pki']
+
+cert_name = 'OCServ'
+cert_data = """
+MIIDsTCCApmgAwIBAgIURNQMaYmRIP/d+/OPWPWmuwkYHbswDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
+NDA0MDIxNjQxMTRaFw0yNTA0MDIxNjQxMTRaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDFeexWVV70fBLOxGofWYlcNxJ9JyLviAZZDXrBIYfQnSrYp51yMKRPTH1e
+Sjr7gIxVArAqLoYFgo7frRDkCKg8/izTopxtBTV2XJkLqDGA7DOrtBhgj0zjmF0A
+WWIWi83WHc+sTHSvIqNLCDAZgnnzf1ch3W/na10hBTnFX4Yv6CJ4I7doSIyWzaQr
+RvUXfaNYnvege+RrG5LzkVGxD2EhHyBqfQ2mxvlgqICqKSZkL56a3c/MHAm+7MKl
+2KbSGxwNDs+SpHrCgWVIsl9w0bN2NSAu6GzyfW7V+V1dkiCggLlxXGhGncPMiQ7T
+M7GKQULnQl5o/15GkW72Tg6wUdDpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD
+VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTtil1X
+c6dXA6kxZtZCgjx9QPzeLDAfBgNVHSMEGDAWgBTKMZvYAW1thn/uxX1fpcbP5vKq
+dzANBgkqhkiG9w0BAQsFAAOCAQEARjS+QYJDz+XTdwK/lMF1GhSdacGnOIWRsbRx
+N7odsyBV7Ud5W+Py79n+/PRirw2+jAaGXFmmgdxrcjlM+dZnlO3X0QCIuNdODggD
+0J/u1ICPdm9TcJ2lEdbIE2vm2Q9P5RdQ7En7zg8Wu+rcNPlIxd3pHFOMX79vOcgi
+RkWWII6tyeeT9COYgXUbg37wf2LkVv4b5PcShrfkWZVFWKDKr1maJ+iMwcIlosOe
+Gj3SKe7gKBuPbMRwtocqKAYbW1GH12tA49DNkvxVKxVqnP4nHkwgfOJdpcZAjlyb
+gLkzVKInZwg5EvJ7qtSJirDap9jyuLTfr5TmxbcdEhmAqeS41A==
+"""
+
+cert_key_data = """
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFeexWVV70fBLO
+xGofWYlcNxJ9JyLviAZZDXrBIYfQnSrYp51yMKRPTH1eSjr7gIxVArAqLoYFgo7f
+rRDkCKg8/izTopxtBTV2XJkLqDGA7DOrtBhgj0zjmF0AWWIWi83WHc+sTHSvIqNL
+CDAZgnnzf1ch3W/na10hBTnFX4Yv6CJ4I7doSIyWzaQrRvUXfaNYnvege+RrG5Lz
+kVGxD2EhHyBqfQ2mxvlgqICqKSZkL56a3c/MHAm+7MKl2KbSGxwNDs+SpHrCgWVI
+sl9w0bN2NSAu6GzyfW7V+V1dkiCggLlxXGhGncPMiQ7TM7GKQULnQl5o/15GkW72
+Tg6wUdDpAgMBAAECggEACbR8bHZv9GT/9EshNLQ3n3a8wQuCLd0fWWi5A90sKbun
+pj5/6uOVbP5DL7Xx4HgIrYmJyIZBI5aEg11Oi15vjOZ9o9MF4V0UVmJQ9TU0EEl2
+H/X5uA54MWaaCiaFFGWU3UqEG8wldJFSZCFyt7Y6scBW3b0JFF7+6dyyDPoCWWqh
+cNR41Hv0T0eqfXGOXX1JcBlLbqy0QXXeFoLlxV3ouIgWgkKJk7u3vDWCVM/ofP0m
+/GyZYWCEA2JljEQZaVgtk1afFoamrjM4doMiirk+Tix4yGno94HLJdDUynqdLNAd
+ZdKunFVAJau17b1VVPyfgIvIaPRvSGQVQoXH6TuB2QKBgQD5LRYTxsd8WsOwlB2R
+SBYdzDff7c3VuNSAYTp7O2MqWrsoXm2MxLzEJLJUen+jQphL6ti/ObdrSOnKF2So
+SizYeJ1Irx4M4BPSdy/Yt3T/+e+Y4K7iQ7Pdvdc/dlZ5XuNHYzuA/F7Ft/9rhUy9
+jSdQYANX+7h8vL7YrEjvhMMMZQKBgQDK4mG4D7XowLlBWv1fK4n/ErWvYSxH/X+A
+VVnLv4z4aZHyRS2nTfQnb8PKbHJ/65x9yZs8a+6HqE4CAH+0LfZuOI8qn9OksxPZ
+7GuQk/FiVyGXtu18hzlfhzmb0ZTjAalZ5b68DOIhyZIHVketebhljXaB5bfwdIgt
+7vTOfotANQKBgQCWiA5WVDgfgBXIjzJtmkcCKWV3+onnG4oFJLfXysDVzYpTkPhN
+mm0PcbvqHTcOwiSPeIkIvS15usrCM++zW1xMSlF6n5Bf5t8Svr5BBlPAcJW2ncYJ
+Gy2GQDHRPQRwvko/zkscWVpHyCieJCGAQc4GWHqspH2Hnd8Ntsc5K9NJoQKBgFR1
+5/5rM+yghr7pdT9wbbNtg4tuZbPWmYTAg3Bp3vLvaB22pOnYbwMX6SdU/Fm6qVxI
+WMLPn+6Dp2337TICTGvYSemRvdb74hC/9ouquzuYUFjLg5Rq6vyU2+u9VUEnyOuu
+1DePGXi9ZHh/d7mFSbmlKaesDWYh7StKJknsrmXdAoGBAOm+FnzryKkhIq/ELyT9
+8v4wr0lxCcAP3nNb/P5ocv3m7hRLIkf4S9k/gAL+gE/OtdesomQKjOz7noLO+I2H
+rj6ZfC/lhPIRJ4XK5BqgqqH53Zcl/HDoaUjbpmyMvZVoQfUHLut8Y912R6mfm65z
+qXl1L7EdHTY+SdoThNJTpmWb
+"""
+
+ca_name = 'VyOS-CA'
+ca_data = """
+MIIDnTCCAoWgAwIBAgIUFVRURZXSbQ7F0DiSZYfqY0gQORMwDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
+NDA0MDIxNjQxMDFaFw0yOTA0MDExNjQxMDFaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQCg7Mjl6+rs8Bdkjqgl2QDuHfrH2mTDCeB7WuNTnIz0BPDtlmwIdqhU7LdC
+B/zUSABAa6LBe/Z/bKWCRKyq8fU2/4uWECe975IMXOfFdYT6KA78DROvOi32JZml
+n0LAXV+538eb+g19xNtoBhPO8igiNevfkV+nJehRK/41ATj+assTOv87vaSX7Wqy
+aP/ZqkIdQD9Kc3cqB4JsYjkWcniHL9yk4oY3cjKK8PJ1pi4FqgFHt2hA+Ic+NvbA
+hc47K9otP8FM4jkSii3MZfHA6Czb43BtbR+YEiWPzBhzE2bCuIgeRUumMF1Z+CAT
+6U7Cpx3XPh+Ac2RnDa8wKeQ1eqE1AgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w
+DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd
+BgNVHQ4EFgQUyjGb2AFtbYZ/7sV9X6XGz+byqncwDQYJKoZIhvcNAQELBQADggEB
+AArGXCq92vtaUZt528lC34ENPL9bQ7nRAS/ojplAzM9reW3o56sfYWf1M8iwRsJT
+LbAwSnVB929RLlDolNpLwpzd1XaMt61Zcx4MFQmQCd+40dfuvMhluZaxt+F9bC1Z
+cA7uwe/2HrAIULq3sga9LzSph6dNuyd1rGchr4xHCJ7u4WcF0kqi0Hjcn9S/ppEc
+ba2L3rRqZmCbe6Yngx+MS06jonGw0z8F6e8LMkcvJUlNMEC76P+5Byjp4xZGP+y3
+DtIfsfijpb+t1OUe75YmWflTFnHR9GlybNYTxGAl49mFw6LlS1kefXyPtfuReLmv
+n+vZdJAWTq76zAPT3n9FClo=
+"""
+
+ca_key_data = """
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCg7Mjl6+rs8Bdk
+jqgl2QDuHfrH2mTDCeB7WuNTnIz0BPDtlmwIdqhU7LdCB/zUSABAa6LBe/Z/bKWC
+RKyq8fU2/4uWECe975IMXOfFdYT6KA78DROvOi32JZmln0LAXV+538eb+g19xNto
+BhPO8igiNevfkV+nJehRK/41ATj+assTOv87vaSX7WqyaP/ZqkIdQD9Kc3cqB4Js
+YjkWcniHL9yk4oY3cjKK8PJ1pi4FqgFHt2hA+Ic+NvbAhc47K9otP8FM4jkSii3M
+ZfHA6Czb43BtbR+YEiWPzBhzE2bCuIgeRUumMF1Z+CAT6U7Cpx3XPh+Ac2RnDa8w
+KeQ1eqE1AgMBAAECggEAEDDaoqVqmMWsONoQiWRMr2h1RZvPxP7OpuKVWiF3XgrM
+Ob9HZc+Ybpj1dC+NDMekvNaHhMuF2Lqz6UgjDjzzVMH/x4yfDwFWUqebSxbglvGm
+Vk4zg48JNkmArLT6GJQccD1XXjZZmqSOhagM4KalCpIdxfvgoZbTCa2xMSCLHS+1
+HCDcmpCoeXM6ZBPTn0NbjRDAqIzCwcq2veG7RSz040obk8h7nrdv7jhxRGmtPmPF
+zKgGLNn6GnL7AwYVMiidjj/ntvM4B1OMs9MwUYbtpg98TWcWyu+ZRakUrnVf9z2a
+IHCKyuJvke/PNqMgw+L8KV4/478XxWhXfl7K1F3nMQKBgQDRBUDYNFH0wC4MMWsA
++RGwyz7RlzACChDJCMtA/agbW06gUoE9UYf8KtLQQQYljlLJHxHGD72QnuM+sowG
+GXnbD4BabA9TQiQUG5c6boznTy1uU1gt8T0Zl0mmC7vIMoMBVd5bb0qrZvuR123k
+DGYn6crug9uvMIYSSlhGmBGTJQKBgQDFGC3vfkCyXzLoYy+RIs/rXgyBF1PUYQty
+DgL0N811L0H7a8JhFnt4FvodUbxv2ob+1kIc9e3yXT6FsGyO7IDOnqgeQKy74bYq
+VPZZuf1FOFb9fuxf00pn1FmhAF4OuSWkhVhrKkyrZwdD8ArjLK253J94dogjdKAY
+fN1csaOA0QKBgD0zUZI8d4a3QoRVb+RACTr/t6v8nZTrR5DlX0XvP2qLKJFutuKy
+XaOrEkDh2R/j9T9oNncMos+WhikUdEVQ7koC1u0i2LXjFtdAYN4+Akmz+DRmeNoy
+2VYF4w2YP+pVR+B7OPkCtBVNuPkx3743Fy42mTGPMCKyjX8Lf59j5Tl1AoGBAI3s
+k2dZqozHMIlWovIH92CtIKP0gFD2cJ94p3fklvZDSWgaeKYg4lffc8uZB/AjlAH9
+ly3ziZx0uIjcOc/RTg96/+SI/dls9xgUhjCmVVJ692ki9GMsau/JYaEl+pTvjcOi
+ocDJfNwQHJM3Tx+3FII59DtyXyXo3T/E6kHNSMeBAoGAR9M48DTspv9OH1S7X6yR
+6MtMY5ltsBmB3gPhQFxiDKBvARkIkAPqObQ9TG/VuOz2Purq0Oz7SHsY2jiFDd2K
+EGo6JfG61NDdIhiQC99ztSgt7NtvSCnX22SfVDWoFxSK+tek7tvDVXAXCNy4ZESM
+EUGJ6NDHImb80aF+xZ3wYKw=
+"""
+
+PROCESS_NAME = 'ocserv-main'
+config_file = '/run/ocserv/ocserv.conf'
+auth_file = '/run/ocserv/ocpasswd'
+otp_file = '/run/ocserv/users.oath'
+
+listen_if = 'dum116'
+listen_address = '100.64.0.1/32'
+
+class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestVPNOpenConnect, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ cls.cli_set(cls, ['interfaces', 'dummy', listen_if, 'address', listen_address])
+
+ cls.cli_set(cls, pki_path + ['ca', cert_name, 'certificate', ca_data.replace('\n','')])
+ cls.cli_set(cls, pki_path + ['ca', cert_name, 'private', 'key', ca_key_data.replace('\n','')])
+ cls.cli_set(cls, pki_path + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')])
+ cls.cli_set(cls, pki_path + ['certificate', cert_name, 'private', 'key', cert_key_data.replace('\n','')])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, pki_path)
+ cls.cli_delete(cls, ['interfaces', 'dummy', listen_if])
+ super(TestVPNOpenConnect, cls).tearDownClass()
+
+ def tearDown(self):
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_ocserv(self):
+ user = 'vyos_user'
+ password = 'vyos_pass'
+ otp = '37500000026900000000200000000000'
+ v4_subnet = '192.0.2.0/24'
+ v6_prefix = '2001:db8:1000::/64'
+ v6_len = '126'
+ name_server = ['1.2.3.4', '1.2.3.5', '2001:db8::1']
+ split_dns = ['vyos.net', 'vyos.io']
+
+ self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'password', password])
+ self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'otp', 'key', otp])
+ self.cli_set(base_path + ['authentication', 'mode', 'local', 'password-otp'])
+
+ self.cli_set(base_path + ['network-settings', 'client-ip-settings', 'subnet', v4_subnet])
+ self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'prefix', v6_prefix])
+ self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'mask', v6_len])
+
+ for ns in name_server:
+ self.cli_set(base_path + ['network-settings', 'name-server', ns])
+ for domain in split_dns:
+ self.cli_set(base_path + ['network-settings', 'split-dns', domain])
+
+ # SSL certificates are mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['ssl', 'ca-certificate', cert_name])
+ self.cli_set(base_path + ['ssl', 'certificate', cert_name])
+
+ listen_ip_no_cidr = ip_from_cidr(listen_address)
+ self.cli_set(base_path + ['listen-address', listen_ip_no_cidr])
+
+ self.cli_commit()
+
+ # Verify configuration
+ daemon_config = read_file(config_file)
+
+ # Verify TLS string (with default setting)
+ self.assertIn('tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128:-VERS-TLS1.0:-VERS-TLS1.1"', daemon_config)
+
+ # authentication mode local password-otp
+ self.assertIn(f'auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]"', daemon_config)
+ self.assertIn(f'listen-host = {listen_ip_no_cidr}', daemon_config)
+ self.assertIn(f'ipv4-network = {v4_subnet}', daemon_config)
+ self.assertIn(f'ipv6-network = {v6_prefix}', daemon_config)
+ self.assertIn(f'ipv6-subnet-prefix = {v6_len}', daemon_config)
+
+ # defaults
+ self.assertIn(f'tcp-port = 443', daemon_config)
+ self.assertIn(f'udp-port = 443', daemon_config)
+
+ for ns in name_server:
+ self.assertIn(f'dns = {ns}', daemon_config)
+ for domain in split_dns:
+ self.assertIn(f'split-dns = {domain}', daemon_config)
+
+ auth_config = read_file(auth_file)
+ self.assertIn(f'{user}:*:$', auth_config)
+
+ otp_config = read_file(otp_file)
+ self.assertIn(f'HOTP/T30/6 {user} - {otp}', otp_config)
+
+
+ # Verify HTTP security headers
+ self.cli_set(base_path + ['http-security-headers'])
+ self.cli_commit()
+
+ daemon_config = read_file(config_file)
+
+ self.assertIn('included-http-headers = Strict-Transport-Security: max-age=31536000 ; includeSubDomains', daemon_config)
+ self.assertIn('included-http-headers = X-Frame-Options: deny', daemon_config)
+ self.assertIn('included-http-headers = X-Content-Type-Options: nosniff', daemon_config)
+ self.assertIn('included-http-headers = Content-Security-Policy: default-src "none"', daemon_config)
+ self.assertIn('included-http-headers = X-Permitted-Cross-Domain-Policies: none', daemon_config)
+ self.assertIn('included-http-headers = Referrer-Policy: no-referrer', daemon_config)
+ self.assertIn('included-http-headers = Clear-Site-Data: "cache","cookies","storage"', daemon_config)
+ self.assertIn('included-http-headers = Cross-Origin-Embedder-Policy: require-corp', daemon_config)
+ self.assertIn('included-http-headers = Cross-Origin-Opener-Policy: same-origin', daemon_config)
+ self.assertIn('included-http-headers = Cross-Origin-Resource-Policy: same-origin', daemon_config)
+ self.assertIn('included-http-headers = X-XSS-Protection: 0', daemon_config)
+ self.assertIn('included-http-headers = Pragma: no-cache', daemon_config)
+ self.assertIn('included-http-headers = Cache-control: no-store, no-cache', daemon_config)
+
+ # Set TLS version to the highest security (v1.3 min)
+ self.cli_set(base_path + ['tls-version-min', '1.3'])
+ self.cli_commit()
+
+ # Verify TLS string
+ daemon_config = read_file(config_file)
+ self.assertIn('tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2"', daemon_config)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_pptp.py b/smoketest/scripts/cli/test_vpn_pptp.py
new file mode 100644
index 0000000..25d9a47
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpn_pptp.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_accel_ppp_test import BasicAccelPPPTest
+
+class TestVPNPPTPServer(BasicAccelPPPTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['vpn', 'pptp', 'remote-access']
+ cls._config_file = '/run/accel-pppd/pptp.conf'
+ cls._chap_secrets = '/run/accel-pppd/pptp.chap-secrets'
+ cls._protocol_section = 'pptp'
+ # call base-classes classmethod
+ super(TestVPNPPTPServer, cls).setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestVPNPPTPServer, cls).tearDownClass()
+
+ def basic_protocol_specific_config(self):
+ pass
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py
new file mode 100644
index 0000000..1a3e1df
--- /dev/null
+++ b/smoketest/scripts/cli/test_vpn_sstp.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_accel_ppp_test import BasicAccelPPPTest
+from vyos.utils.file import read_file
+
+pki_path = ['pki']
+
+cert_data = """
+MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw
+WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
+bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx
+MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV
+BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP
+UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
+01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3
+QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
+BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu
++JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz
+ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93
++dm/LDnp7C0="""
+
+key_data = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
+2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7
+u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
+"""
+
+class TestVPNSSTPServer(BasicAccelPPPTest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls._base_path = ['vpn', 'sstp']
+ cls._config_file = '/run/accel-pppd/sstp.conf'
+ cls._chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
+ cls._protocol_section = 'sstp'
+ # call base-classes classmethod
+ super(TestVPNSSTPServer, cls).setUpClass()
+
+ cls.cli_set(cls, pki_path + ['ca', 'sstp', 'certificate', cert_data.replace('\n','')])
+ cls.cli_set(cls, pki_path + ['certificate', 'sstp', 'certificate', cert_data.replace('\n','')])
+ cls.cli_set(cls, pki_path + ['certificate', 'sstp', 'private', 'key', key_data.replace('\n','')])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, pki_path)
+ super(TestVPNSSTPServer, cls).tearDownClass()
+
+ def basic_protocol_specific_config(self):
+ self.set(['ssl', 'ca-certificate', 'sstp'])
+ self.set(['ssl', 'certificate', 'sstp'])
+
+ def test_accel_local_authentication(self):
+ # Change default port
+ port = '8443'
+ self.set(['port', port])
+
+ self.basic_config()
+ super().test_accel_local_authentication()
+
+ config = read_file(self._config_file)
+ self.assertIn(f'port={port}', config)
+
+ def test_sstp_host_name(self):
+ host_name = 'test.vyos.io'
+ self.set(['host-name', host_name])
+
+ self.basic_config()
+ self.cli_commit()
+
+ config = read_file(self._config_file)
+ self.assertIn(f'host-name={host_name}', config)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
new file mode 100644
index 0000000..2bb6c91
--- /dev/null
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -0,0 +1,599 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from json import loads
+from jmespath import search
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
+from vyos.utils.file import read_file
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
+from vyos.utils.network import is_intf_addr_assigned
+from vyos.utils.network import interface_exists
+from vyos.utils.process import cmd
+from vyos.utils.system import sysctl_read
+
+base_path = ['vrf']
+vrfs = ['red', 'green', 'blue', 'foo-bar', 'baz_foo']
+v4_protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis', 'kernel', 'ospf', 'rip', 'static', 'table']
+v6_protocols = ['any', 'babel', 'bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static', 'table']
+
+class VRFTest(VyOSUnitTestSHIM.TestCase):
+ _interfaces = []
+
+ @classmethod
+ def setUpClass(cls):
+ # we need to filter out VLAN interfaces identified by a dot (.)
+ # in their name - just in case!
+ if 'TEST_ETH' in os.environ:
+ tmp = os.environ['TEST_ETH'].split()
+ cls._interfaces = tmp
+ else:
+ for tmp in Section.interfaces('ethernet', vlan=False):
+ cls._interfaces.append(tmp)
+ # call base-classes classmethod
+ super(VRFTest, cls).setUpClass()
+
+ def setUp(self):
+ # VRF strict_most ist always enabled
+ tmp = read_file('/proc/sys/net/vrf/strict_mode')
+ self.assertEqual(tmp, '1')
+
+ def tearDown(self):
+ # delete all VRFs
+ self.cli_delete(base_path)
+ self.cli_commit()
+ for vrf in vrfs:
+ self.assertFalse(interface_exists(vrf))
+
+ def test_vrf_vni_and_table_id(self):
+ base_table = '1000'
+ table = base_table
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ description = f'VyOS-VRF-{vrf}'
+ self.cli_set(base + ['description', description])
+
+ # check validate() - a table ID is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base + ['table', table])
+ self.cli_set(base + ['vni', table])
+ if vrf == 'green':
+ self.cli_set(base + ['disable'])
+
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = base_table
+ iproute2_config = read_file('/etc/iproute2/rt_tables.d/vyos-vrf.conf')
+ for vrf in vrfs:
+ description = f'VyOS-VRF-{vrf}'
+ self.assertTrue(interface_exists(vrf))
+ vrf_if = Interface(vrf)
+ # validate proper interface description
+ self.assertEqual(vrf_if.get_alias(), description)
+ # validate admin up/down state of VRF
+ state = 'up'
+ if vrf == 'green':
+ state = 'down'
+ self.assertEqual(vrf_if.get_admin_state(), state)
+
+ # Test the iproute2 lookup file, syntax is as follows:
+ #
+ # # id vrf name comment
+ # 1000 red # VyOS-VRF-red
+ # 1001 green # VyOS-VRF-green
+ # ...
+ regex = f'{table}\s+{vrf}\s+#\s+{description}'
+ self.assertTrue(re.findall(regex, iproute2_config))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+
+ self.assertEqual(int(table), get_vrf_tableid(vrf))
+
+ # Increment table ID for the next run
+ table = str(int(table) + 1)
+
+ def test_vrf_loopbacks_ips(self):
+ table = '2000'
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', str(table)])
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ loopbacks = ['127.0.0.1', '::1']
+ for vrf in vrfs:
+ # Ensure VRF was created
+ self.assertTrue(interface_exists(vrf))
+ # Verify IP forwarding is 1 (enabled)
+ self.assertEqual(sysctl_read(f'net.ipv4.conf.{vrf}.forwarding'), '1')
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{vrf}.forwarding'), '1')
+
+ # Test for proper loopback IP assignment
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned(vrf, addr))
+
+ def test_vrf_bind_all(self):
+ table = '2000'
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', str(table)])
+ table = str(int(table) + 1)
+
+ self.cli_set(base_path + ['bind-to-all'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ self.assertEqual(sysctl_read('net.ipv4.tcp_l3mdev_accept'), '1')
+ self.assertEqual(sysctl_read('net.ipv4.udp_l3mdev_accept'), '1')
+
+ # If there is any VRF defined, strict_mode should be on
+ self.assertEqual(sysctl_read('net.vrf.strict_mode'), '1')
+
+ def test_vrf_table_id_is_unalterable(self):
+ # Linux Kernel prohibits the change of a VRF table on the fly.
+ # VRF must be deleted and recreated!
+ table = '1000'
+ vrf = vrfs[0]
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+
+ # commit changes
+ self.cli_commit()
+
+ # Check if VRF has been created
+ self.assertTrue(interface_exists(vrf))
+
+ table = str(int(table) + 1)
+ self.cli_set(base + ['table', table])
+ # check validate() - table ID can not be altered!
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_vrf_assign_interface(self):
+ vrf = vrfs[0]
+ table = '5000'
+ self.cli_set(['vrf', 'name', vrf, 'table', table])
+
+ for interface in self._interfaces:
+ section = Section.section(interface)
+ self.cli_set(['interfaces', section, interface, 'vrf', vrf])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF assignmant
+ for interface in self._interfaces:
+ tmp = get_interface_config(interface)
+ self.assertEqual(vrf, tmp['master'])
+
+ # cleanup
+ section = Section.section(interface)
+ self.cli_delete(['interfaces', section, interface, 'vrf'])
+
+ def test_vrf_static_route(self):
+ base_table = '100'
+ table = base_table
+ for vrf in vrfs:
+ next_hop = f'192.0.{table}.1'
+ prefix = f'10.0.{table}.0/24'
+ base = base_path + ['name', vrf]
+
+ self.cli_set(base + ['vni', table])
+
+ # check validate() - a table ID is mandatory
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base + ['table', table])
+ self.cli_set(base + ['protocols', 'static', 'route', prefix, 'next-hop', next_hop])
+
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = base_table
+ for vrf in vrfs:
+ next_hop = f'192.0.{table}.1'
+ prefix = f'10.0.{table}.0/24'
+
+ self.assertTrue(interface_exists(vrf))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+ self.assertIn(f' ip route {prefix} {next_hop}', frrconfig)
+
+ # Increment table ID for the next run
+ table = str(int(table) + 1)
+
+ def test_vrf_link_local_ip_addresses(self):
+ # Testcase for issue T4331
+ table = '100'
+ vrf = 'orange'
+ interface = 'dum9998'
+ addresses = ['192.0.2.1/26', '2001:db8:9998::1/64', 'fe80::1/64']
+
+ for address in addresses:
+ self.cli_set(['interfaces', 'dummy', interface, 'address', address])
+
+ # Create dummy interfaces
+ self.cli_commit()
+
+ # ... and verify IP addresses got assigned
+ for address in addresses:
+ self.assertTrue(is_intf_addr_assigned(interface, address))
+
+ # Move interface to VRF
+ self.cli_set(base_path + ['name', vrf, 'table', table])
+ self.cli_set(['interfaces', 'dummy', interface, 'vrf', vrf])
+
+ # Apply VRF config
+ self.cli_commit()
+ # Ensure VRF got created
+ self.assertTrue(interface_exists(vrf))
+ # ... and IP addresses are still assigned
+ for address in addresses:
+ self.assertTrue(is_intf_addr_assigned(interface, address))
+ # Verify VRF table ID
+ self.assertEqual(int(table), get_vrf_tableid(vrf))
+
+ # Verify interface is assigned to VRF
+ tmp = get_interface_config(interface)
+ self.assertEqual(vrf, tmp['master'])
+
+ # Delete Interface
+ self.cli_delete(['interfaces', 'dummy', interface])
+ self.cli_commit()
+
+ def test_vrf_disable_forwarding(self):
+ table = '2000'
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+ self.cli_set(base + ['ip', 'disable-forwarding'])
+ self.cli_set(base + ['ipv6', 'disable-forwarding'])
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ loopbacks = ['127.0.0.1', '::1']
+ for vrf in vrfs:
+ # Ensure VRF was created
+ self.assertTrue(interface_exists(vrf))
+ # Verify IP forwarding is 0 (disabled)
+ self.assertEqual(sysctl_read(f'net.ipv4.conf.{vrf}.forwarding'), '0')
+ self.assertEqual(sysctl_read(f'net.ipv6.conf.{vrf}.forwarding'), '0')
+
+ def test_vrf_ip_protocol_route_map(self):
+ table = '6000'
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+
+ for protocol in v4_protocols:
+ self.cli_set(['policy', 'route-map', f'route-map-{vrf}-{protocol}', 'rule', '10', 'action', 'permit'])
+ self.cli_set(base + ['ip', 'protocol', protocol, 'route-map', f'route-map-{vrf}-{protocol}'])
+
+ table = str(int(table) + 1)
+
+ self.cli_commit()
+
+ # Verify route-map properly applied to FRR
+ for vrf in vrfs:
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ self.assertIn(f'vrf {vrf}', frrconfig)
+ for protocol in v4_protocols:
+ self.assertIn(f' ip protocol {protocol} route-map route-map-{vrf}-{protocol}', frrconfig)
+
+ # Delete route-maps
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_delete(['policy', 'route-map', f'route-map-{vrf}-{protocol}'])
+ self.cli_delete(base + ['ip', 'protocol'])
+
+ self.cli_commit()
+
+ # Verify route-map properly is removed from FRR
+ for vrf in vrfs:
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ self.assertNotIn(f'vrf {vrf}', frrconfig)
+
+ def test_vrf_ip_ipv6_protocol_non_existing_route_map(self):
+ table = '6100'
+ non_existing = 'non-existing'
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+ for protocol in v4_protocols:
+ self.cli_set(base + ['ip', 'protocol', protocol, 'route-map', f'v4-{non_existing}'])
+ for protocol in v6_protocols:
+ self.cli_set(base + ['ipv6', 'protocol', protocol, 'route-map', f'v6-{non_existing}'])
+
+ table = str(int(table) + 1)
+
+ # Both v4 and v6 route-maps do not exist yet
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(['policy', 'route-map', f'v4-{non_existing}', 'rule', '10', 'action', 'deny'])
+
+ # v6 route-map does not exist yet
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(['policy', 'route-map', f'v6-{non_existing}', 'rule', '10', 'action', 'deny'])
+
+ # Commit again
+ self.cli_commit()
+
+ def test_vrf_ipv6_protocol_route_map(self):
+ table = '6200'
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+
+ for protocol in v6_protocols:
+ route_map = f'route-map-{vrf}-{protocol.replace("ospfv3", "ospf6")}'
+ self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
+ self.cli_set(base + ['ipv6', 'protocol', protocol, 'route-map', route_map])
+
+ table = str(int(table) + 1)
+
+ self.cli_commit()
+
+ # Verify route-map properly applied to FRR
+ for vrf in vrfs:
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ self.assertIn(f'vrf {vrf}', frrconfig)
+ for protocol in v6_protocols:
+ # VyOS and FRR use a different name for OSPFv3 (IPv6)
+ if protocol == 'ospfv3':
+ protocol = 'ospf6'
+ route_map = f'route-map-{vrf}-{protocol}'
+ self.assertIn(f' ipv6 protocol {protocol} route-map {route_map}', frrconfig)
+
+ # Delete route-maps
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_delete(['policy', 'route-map', f'route-map-{vrf}-{protocol}'])
+ self.cli_delete(base + ['ipv6', 'protocol'])
+
+ self.cli_commit()
+
+ # Verify route-map properly is removed from FRR
+ for vrf in vrfs:
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ self.assertNotIn(f'vrf {vrf}', frrconfig)
+
+ def test_vrf_vni_duplicates(self):
+ base_table = '6300'
+ table = base_table
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', str(table)])
+ self.cli_set(base + ['vni', '100'])
+ table = str(int(table) + 1)
+
+ # L3VNIs can only be used once
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ table = base_table
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['vni', str(table)])
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = base_table
+ for vrf in vrfs:
+ self.assertTrue(interface_exists(vrf))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+ # Increment table ID for the next run
+ table = str(int(table) + 1)
+
+ def test_vrf_vni_add_change_remove(self):
+ base_table = '6300'
+ table = base_table
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', str(table)])
+ self.cli_set(base + ['vni', str(table)])
+ table = str(int(table) + 1)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = base_table
+ for vrf in vrfs:
+ self.assertTrue(interface_exists(vrf))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+ # Increment table ID for the next run
+ table = str(int(table) + 1)
+
+ # Now change all L3VNIs (increment 2)
+ # We must also change the base_table number as we probably could get
+ # duplicate VNI's during the test as VNIs are applied 1:1 to FRR
+ base_table = '5000'
+ table = base_table
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['vni', str(table)])
+ table = str(int(table) + 2)
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = base_table
+ for vrf in vrfs:
+ self.assertTrue(interface_exists(vrf))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+ # Increment table ID for the next run
+ table = str(int(table) + 2)
+
+
+ # add a new VRF with VNI - this must not delete any existing VRF/VNI
+ purple = 'purple'
+ table = str(int(table) + 10)
+ self.cli_set(base_path + ['name', purple, 'table', table])
+ self.cli_set(base_path + ['name', purple, 'vni', table])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = base_table
+ for vrf in vrfs:
+ self.assertTrue(interface_exists(vrf))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertIn(f' vni {table}', frrconfig)
+ # Increment table ID for the next run
+ table = str(int(table) + 2)
+
+ # Verify purple VRF/VNI
+ self.assertTrue(interface_exists(purple))
+ table = str(int(table) + 10)
+ frrconfig = self.getFRRconfig(f'vrf {purple}')
+ self.assertIn(f' vni {table}', frrconfig)
+
+ # Now delete all the VNIs
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_delete(base + ['vni'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify no VNI is defined
+ for vrf in vrfs:
+ self.assertTrue(interface_exists(vrf))
+
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ self.assertNotIn('vni', frrconfig)
+
+ # Verify purple VNI remains
+ self.assertTrue(interface_exists(purple))
+ frrconfig = self.getFRRconfig(f'vrf {purple}')
+ self.assertIn(f' vni {table}', frrconfig)
+
+ def test_vrf_ip_ipv6_nht(self):
+ table = '6910'
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+ self.cli_set(base + ['ip', 'nht', 'no-resolve-via-default'])
+ self.cli_set(base + ['ipv6', 'nht', 'no-resolve-via-default'])
+
+ table = str(int(table) + 1)
+
+ self.cli_commit()
+
+ # Verify route-map properly applied to FRR
+ for vrf in vrfs:
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ self.assertIn(f'vrf {vrf}', frrconfig)
+ self.assertIn(f' no ip nht resolve-via-default', frrconfig)
+ self.assertIn(f' no ipv6 nht resolve-via-default', frrconfig)
+
+ # Delete route-maps
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_delete(base + ['ip'])
+ self.cli_delete(base + ['ipv6'])
+
+ self.cli_commit()
+
+ # Verify route-map properly is removed from FRR
+ for vrf in vrfs:
+ frrconfig = self.getFRRconfig(f'vrf {vrf}', daemon='zebra')
+ self.assertNotIn(f' no ip nht resolve-via-default', frrconfig)
+ self.assertNotIn(f' no ipv6 nht resolve-via-default', frrconfig)
+
+ def test_vrf_conntrack(self):
+ table = '8710'
+ nftables_rules = {
+ 'vrf_zones_ct_in': ['ct original zone set iifname map @ct_iface_map'],
+ 'vrf_zones_ct_out': ['ct original zone set oifname map @ct_iface_map']
+ }
+
+ self.cli_set(base_path + ['name', 'randomVRF', 'table', '1000'])
+ self.cli_commit()
+
+ # Conntrack rules should not be present
+ for chain, rule in nftables_rules.items():
+ self.verify_nftables_chain(rule, 'inet vrf_zones', chain, inverse=True)
+
+ # conntrack is only enabled once NAT, NAT66 or firewalling is enabled
+ self.cli_set(['nat'])
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+ table = str(int(table) + 1)
+ # We need the commit inside the loop to trigger the bug in T6603
+ self.cli_commit()
+
+ # Conntrack rules should now be present
+ for chain, rule in nftables_rules.items():
+ self.verify_nftables_chain(rule, 'inet vrf_zones', chain, inverse=False)
+
+ # T6603: there should be only ONE entry for the iifname/oifname in the chains
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ # ['vrf_zones_ct_in', 'vrf_zones_ct_out']
+ self.assertEqual(num_rules, 2)
+
+ self.cli_delete(['nat'])
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)