diff options
-rw-r--r-- | data/config-mode-dependencies/vyos-1x.json | 3 | ||||
-rw-r--r-- | data/templates/high-availability/keepalived.conf.j2 | 6 | ||||
-rw-r--r-- | data/templates/login/default_motd.j2 | 14 | ||||
-rw-r--r-- | data/templates/vyos-hostsd/hosts.j2 | 1 | ||||
-rw-r--r-- | data/vyos-firewall-init.conf | 2 | ||||
-rw-r--r-- | interface-definitions/high-availability.xml.in | 1 | ||||
-rw-r--r-- | python/vyos/firewall.py | 18 | ||||
-rw-r--r-- | smoketest/scripts/cli/base_vyostest_shim.py | 23 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_firewall.py | 24 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_high-availability_vrrp.py | 27 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_nat.py | 13 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_nat66.py | 13 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_policy_route.py | 11 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_system_conntrack.py | 12 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_vrf.py | 23 | ||||
-rwxr-xr-x | src/conf_mode/high-availability.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/system_conntrack.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/system_login_banner.py | 22 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 18 | ||||
-rwxr-xr-x | src/op_mode/container.py | 2 |
20 files changed, 153 insertions, 94 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index b0586e0bb..6ab36005b 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -1,6 +1,7 @@ { "system_conntrack": { - "conntrack_sync": ["service_conntrack-sync"] + "conntrack_sync": ["service_conntrack-sync"], + "vrf": ["vrf"] }, "firewall": { "conntrack": ["system_conntrack"], diff --git a/data/templates/high-availability/keepalived.conf.j2 b/data/templates/high-availability/keepalived.conf.j2 index d54f575b5..f34ce64e2 100644 --- a/data/templates/high-availability/keepalived.conf.j2 +++ b/data/templates/high-availability/keepalived.conf.j2 @@ -82,7 +82,11 @@ vrrp_instance {{ name }} { nopreempt {% endif %} {% if group_config.peer_address is vyos_defined %} - unicast_peer { {{ group_config.peer_address }} } + unicast_peer { +{% for peer_address in group_config.peer_address %} + {{ peer_address }} +{% endfor %} + } {% endif %} {% if group_config.hello_source_address is vyos_defined %} {% if group_config.peer_address is vyos_defined %} diff --git a/data/templates/login/default_motd.j2 b/data/templates/login/default_motd.j2 new file mode 100644 index 000000000..8584d261a --- /dev/null +++ b/data/templates/login/default_motd.j2 @@ -0,0 +1,14 @@ +Welcome to VyOS! + + ┌── ┐ + . VyOS {{ version_data.version }} + └ ──┘ {{ version_data.release_train }} + + * Documentation: https://docs.vyos.io/en/{{ version_data.release_train | replace('current', 'latest') }} + * Project news: https://blog.vyos.io + * Bug reports: https://vyos.dev + +You can change this banner using "set system login banner post-login" command. + +VyOS is a free software distribution that includes multiple components, +you can check individual component licenses under /usr/share/doc/*/copyright diff --git a/data/templates/vyos-hostsd/hosts.j2 b/data/templates/vyos-hostsd/hosts.j2 index 5cad983b4..71fa335da 100644 --- a/data/templates/vyos-hostsd/hosts.j2 +++ b/data/templates/vyos-hostsd/hosts.j2 @@ -4,7 +4,6 @@ # Local host 127.0.0.1 localhost -127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }} {{ host_name }}{% endif %} # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback diff --git a/data/vyos-firewall-init.conf b/data/vyos-firewall-init.conf index 5a4e03015..3929edf0b 100644 --- a/data/vyos-firewall-init.conf +++ b/data/vyos-firewall-init.conf @@ -65,11 +65,9 @@ table inet vrf_zones { # Chain for inbound traffic chain vrf_zones_ct_in { type filter hook prerouting priority raw; policy accept; - counter ct original zone set iifname map @ct_iface_map } # Chain for locally-generated traffic chain vrf_zones_ct_out { type filter hook output priority raw; policy accept; - counter ct original zone set oifname map @ct_iface_map } } diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index 59f0f1052..aef57f8ae 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -195,6 +195,7 @@ <constraint> <validator name="ip-address"/> </constraint> + <multi/> </properties> </leafNode> <leafNode name="no-preempt"> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index eee11bd2d..49e095946 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -34,6 +34,24 @@ from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import run +# Conntrack + +def conntrack_required(conf): + required_nodes = ['nat', 'nat66', 'load-balancing wan'] + + for path in required_nodes: + if conf.exists(path): + return True + + firewall = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), + no_tag_node_value_mangle=True, get_first_key=True) + + for rules, path in dict_search_recursive(firewall, 'rule'): + if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()): + return True + + return False + # Domain Resolver def fqdn_config_parse(firewall): diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index 140642806..c49d3e76c 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -102,6 +102,29 @@ class VyOSUnitTestSHIM: 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 diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index bc2848492..be5960bbd 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -22,7 +22,6 @@ from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.utils.process import cmd from vyos.utils.process import run sysfs_config = { @@ -67,28 +66,6 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter', inverse=True) - 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) - 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 @@ -808,7 +785,6 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['ct state related', 'accept'] ] - nftables_output = cmd('sudo nft list table ip vyos_filter') self.verify_nftables(nftables_search, 'ip vyos_filter') self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') diff --git a/smoketest/scripts/cli/test_high-availability_vrrp.py b/smoketest/scripts/cli/test_high-availability_vrrp.py index 98259d830..1bb35e422 100755 --- a/smoketest/scripts/cli/test_high-availability_vrrp.py +++ b/smoketest/scripts/cli/test_high-availability_vrrp.py @@ -237,5 +237,32 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): 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) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 1e6435df8..4f1c3cb4f 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -21,8 +21,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.utils.process import cmd -from vyos.utils.dict import dict_search base_path = ['nat'] src_path = base_path + ['source'] @@ -47,17 +45,6 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.assertFalse(os.path.exists(nftables_nat_config)) self.assertFalse(os.path.exists(nftables_static_nat_conf)) - 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 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 diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 0607f6616..400a895ff 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -22,8 +22,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.utils.process import cmd -from vyos.utils.dict import dict_search base_path = ['nat66'] src_path = base_path + ['source'] @@ -42,17 +40,6 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - 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 test_source_nat66(self): source_prefix = 'fc00::/64' translation_prefix = 'fc01::/64' diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index c0b7c1fe7..462fc24d0 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -68,17 +68,6 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.verify_rules(ip_rule_search, inverse=True) - def verify_nftables(self, nftables_search, table, inverse=False): - nftables_output = cmd(f'sudo nft 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_rules(self, rules_search, inverse=False): rule_output = cmd('ip rule show') diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index ce237a6e7..f00626b3d 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -21,7 +21,6 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.firewall import find_nftables_rule -from vyos.utils.process import cmd from vyos.utils.file import read_file base_path = ['system', 'conntrack'] @@ -43,17 +42,6 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - 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 test_conntrack_options(self): conntrack_config = { 'net.netfilter.nf_conntrack_expect_max' : { diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 438387f2d..c96b8e374 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -529,5 +529,28 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): 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 = '1000' + 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', 'blue', 'table', table]) + 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) + + self.cli_set(['nat']) + 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) + + self.cli_delete(['nat']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index b3b27b14e..59d49ea67 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -125,8 +125,9 @@ def verify(ha): raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!') if 'peer_address' in group_config: - if is_ipv6(group_config['peer_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') + for peer_address in group_config['peer_address']: + if is_ipv6(peer_address): + raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') if vaddrs6: tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv6'} @@ -139,8 +140,9 @@ def verify(ha): raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!') if 'peer_address' in group_config: - if is_ipv4(group_config['peer_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') + for peer_address in group_config['peer_address']: + if is_ipv4(peer_address): + raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') # Check sync groups if 'vrrp' in ha and 'sync_group' in ha['vrrp']: for sync_group, sync_config in ha['vrrp']['sync_group'].items(): diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index 7f6c71440..e075bc928 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -104,6 +104,10 @@ def get_config(config=None): if conf.exists(['service', 'conntrack-sync']): set_dependents('conntrack_sync', conf) + # If conntrack status changes, VRF zone rules need updating + if conf.exists(['vrf']): + set_dependents('vrf', conf) + return conntrack def verify(conntrack): diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py index 65fa04417..923e1bf57 100755 --- a/src/conf_mode/system_login_banner.py +++ b/src/conf_mode/system_login_banner.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# 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 @@ -18,30 +18,26 @@ from sys import exit from copy import deepcopy from vyos.config import Config +from vyos.template import render from vyos.utils.file import write_file +from vyos.version import get_version_data from vyos import ConfigError from vyos import airbag airbag.enable() -try: - with open('/usr/share/vyos/default_motd') as f: - motd = f.read() -except: - # Use an empty banner if the default banner file cannot be read - motd = "\n" - PRELOGIN_FILE = r'/etc/issue' PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' default_config_data = { 'issue': 'Welcome to VyOS - \\n \\l\n\n', - 'issue_net': '', - 'motd': motd + 'issue_net': '' } def get_config(config=None): banner = deepcopy(default_config_data) + banner['version_data'] = get_version_data() + if config: conf = config else: @@ -92,7 +88,11 @@ def generate(banner): def apply(banner): write_file(PRELOGIN_FILE, banner['issue']) write_file(PRELOGIN_NET_FILE, banner['issue_net']) - write_file(POSTLOGIN_FILE, banner['motd']) + if 'motd' in banner: + write_file(POSTLOGIN_FILE, banner['motd']) + else: + render(POSTLOGIN_FILE, 'login/default_motd.j2', banner, + permission=0o644, user='root', group='root') return None diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index a2f4956be..16908100f 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -23,6 +23,7 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_route_map +from vyos.firewall import conntrack_required from vyos.ifconfig import Interface from vyos.template import render from vyos.template import render_to_string @@ -41,6 +42,12 @@ airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' k_mod = ['vrf'] +nftables_table = 'inet vrf_zones' +nftables_rules = { + 'vrf_zones_ct_in': 'counter ct original zone set iifname map @ct_iface_map', + 'vrf_zones_ct_out': 'counter ct original zone set oifname map @ct_iface_map' +} + def has_rule(af : str, priority : int, table : str=None): """ Check if a given ip rule exists @@ -114,6 +121,9 @@ def get_config(config=None): routes = vrf_routing(conf, name) if routes: vrf['vrf_remove'][name]['route'] = routes + if 'name' in vrf: + vrf['conntrack'] = conntrack_required(conf) + # We also need the route-map information from the config # # XXX: one MUST always call this without the key_mangling() option! See @@ -294,6 +304,14 @@ def apply(vrf): nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' cmd(f'nft {nft_add_element}') + if vrf['conntrack']: + for chain, rule in nftables_rules.items(): + cmd(f'nft add rule inet vrf_zones {chain} {rule}') + + if 'name' not in vrf or not vrf['conntrack']: + for chain, rule in nftables_rules.items(): + cmd(f'nft flush chain inet vrf_zones {chain}') + # Apply FRR filters zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions diff --git a/src/op_mode/container.py b/src/op_mode/container.py index dcbb4dc55..d29af8821 100755 --- a/src/op_mode/container.py +++ b/src/op_mode/container.py @@ -75,7 +75,7 @@ def delete_image(name: str): if not name: return # replace newline with whitespace name = name.replace('\n', ' ') - rc, output = rc_cmd(f'podman image rm --force {name}') + rc, output = rc_cmd(f'podman image rm {name}') if rc != 0: raise vyos.opmode.InternalError(output) |