diff options
-rw-r--r-- | data/templates/firewall/nftables.j2 | 25 | ||||
-rw-r--r-- | data/vyos-firewall-init.conf | 14 | ||||
-rw-r--r-- | interface-definitions/include/firewall/global-options.xml.i | 32 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 61 | ||||
-rw-r--r-- | smoketest/scripts/cli/base_interfaces_test.py | 8 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_firewall.py | 8 | ||||
-rwxr-xr-x | src/op_mode/image_manager.py | 25 |
7 files changed, 117 insertions, 56 deletions
diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 3f7906628..4851e3a05 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -8,29 +8,36 @@ flush chain raw FW_CONNTRACK flush chain ip6 raw FW_CONNTRACK +flush chain raw vyos_global_rpfilter +flush chain ip6 raw vyos_global_rpfilter + table raw { chain FW_CONNTRACK { {{ ipv4_conntrack_action }} } + + chain vyos_global_rpfilter { +{% if global_options.source_validation is vyos_defined('loose') %} + fib saddr oif 0 counter drop +{% elif global_options.source_validation is vyos_defined('strict') %} + fib saddr . iif oif 0 counter drop +{% endif %} + return + } } table ip6 raw { chain FW_CONNTRACK { {{ ipv6_conntrack_action }} } -} -{% if first_install is not vyos_defined %} -delete table inet vyos_global_rpfilter -{% endif %} -table inet vyos_global_rpfilter { - chain PREROUTING { - type filter hook prerouting priority -300; policy accept; -{% if global_options.source_validation is vyos_defined('loose') %} + chain vyos_global_rpfilter { +{% if global_options.ipv6_source_validation is vyos_defined('loose') %} fib saddr oif 0 counter drop -{% elif global_options.source_validation is vyos_defined('strict') %} +{% elif global_options.ipv6_source_validation is vyos_defined('strict') %} fib saddr . iif oif 0 counter drop {% endif %} + return } } diff --git a/data/vyos-firewall-init.conf b/data/vyos-firewall-init.conf index 41e7627f5..b0026fdf3 100644 --- a/data/vyos-firewall-init.conf +++ b/data/vyos-firewall-init.conf @@ -19,6 +19,15 @@ table raw { type filter hook forward priority -300; policy accept; } + chain vyos_global_rpfilter { + return + } + + chain vyos_rpfilter { + type filter hook prerouting priority -300; policy accept; + counter jump vyos_global_rpfilter + } + chain PREROUTING { type filter hook prerouting priority -300; policy accept; counter jump VYOS_CT_IGNORE @@ -82,8 +91,13 @@ table ip6 raw { type filter hook forward priority -300; policy accept; } + chain vyos_global_rpfilter { + return + } + chain vyos_rpfilter { type filter hook prerouting priority -300; policy accept; + counter jump vyos_global_rpfilter } chain PREROUTING { diff --git a/interface-definitions/include/firewall/global-options.xml.i b/interface-definitions/include/firewall/global-options.xml.i index 3026b54ab..415d85f05 100644 --- a/interface-definitions/include/firewall/global-options.xml.i +++ b/interface-definitions/include/firewall/global-options.xml.i @@ -145,21 +145,21 @@ </leafNode> <leafNode name="source-validation"> <properties> - <help>Policy for source validation by reversed path, as specified in RFC3704</help> + <help>Policy for IPv4 source validation by reversed path, as specified in RFC3704</help> <completionHelp> <list>strict loose disable</list> </completionHelp> <valueHelp> <format>strict</format> - <description>Enable Strict Reverse Path Forwarding as defined in RFC3704</description> + <description>Enable IPv4 Strict Reverse Path Forwarding as defined in RFC3704</description> </valueHelp> <valueHelp> <format>loose</format> - <description>Enable Loose Reverse Path Forwarding as defined in RFC3704</description> + <description>Enable IPv4 Loose Reverse Path Forwarding as defined in RFC3704</description> </valueHelp> <valueHelp> <format>disable</format> - <description>No source validation</description> + <description>No IPv4 source validation</description> </valueHelp> <constraint> <regex>(strict|loose|disable)</regex> @@ -264,6 +264,30 @@ </properties> <defaultValue>disable</defaultValue> </leafNode> + <leafNode name="ipv6-source-validation"> + <properties> + <help>Policy for IPv6 source validation by reversed path, as specified in RFC3704</help> + <completionHelp> + <list>strict loose disable</list> + </completionHelp> + <valueHelp> + <format>strict</format> + <description>Enable IPv6 Strict Reverse Path Forwarding as defined in RFC3704</description> + </valueHelp> + <valueHelp> + <format>loose</format> + <description>Enable IPv6 Loose Reverse Path Forwarding as defined in RFC3704</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>No IPv6 source validation</description> + </valueHelp> + <constraint> + <regex>(strict|loose|disable)</regex> + </constraint> + </properties> + <defaultValue>disable</defaultValue> + </leafNode> <leafNode name="ipv6-src-route"> <properties> <help>Policy for handling IPv6 packets with routing extension header</help> diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index a038ef28d..c793f6199 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -175,10 +175,6 @@ class Interface(Control): 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', }, - 'rp_filter': { - 'validate': lambda flt: assert_range(flt,0,3), - 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', - }, 'ipv6_accept_ra': { 'validate': lambda ara: assert_range(ara,0,3), 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', @@ -252,9 +248,6 @@ class Interface(Control): 'ipv4_directed_broadcast': { 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', }, - 'rp_filter': { - 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', - }, 'ipv6_accept_ra': { 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', }, @@ -755,40 +748,32 @@ class Interface(Control): return None return self.set_interface('ipv4_directed_broadcast', forwarding) - def set_ipv4_source_validation(self, value): - """ - Help prevent attacks used by Spoofing IP Addresses. Reverse path - filtering is a Kernel feature that, when enabled, is designed to ensure - packets that are not routable to be dropped. The easiest example of this - would be and IP Address of the range 10.0.0.0/8, a private IP Address, - being received on the Internet facing interface of the router. + def _cleanup_ipv4_source_validation_rules(self, ifname): + results = self._cmd(f'nft -a list chain ip raw vyos_rpfilter').split("\n") + for line in results: + if f'iifname "{ifname}"' in line: + handle_search = re.search('handle (\d+)', line) + if handle_search: + self._cmd(f'nft delete rule ip raw vyos_rpfilter handle {handle_search[1]}') - As per RFC3074. + def set_ipv4_source_validation(self, mode): """ - if value == 'strict': - value = 1 - elif value == 'loose': - value = 2 - else: - value = 0 - - all_rp_filter = int(read_file('/proc/sys/net/ipv4/conf/all/rp_filter')) - if all_rp_filter > value: - global_setting = 'disable' - if all_rp_filter == 1: global_setting = 'strict' - elif all_rp_filter == 2: global_setting = 'loose' - - from vyos.base import Warning - Warning(f'Global source-validation is set to "{global_setting}", this '\ - f'overrides per interface setting on "{self.ifname}"!') + Set IPv4 reverse path validation - tmp = self.get_interface('rp_filter') - if int(tmp) == value: - return None - return self.set_interface('rp_filter', value) + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_ipv4_source_validation('strict') + """ + self._cleanup_ipv4_source_validation_rules(self.ifname) + nft_prefix = f'nft insert rule ip raw vyos_rpfilter iifname "{self.ifname}"' + if mode in ['strict', 'loose']: + self._cmd(f"{nft_prefix} counter return") + if mode == 'strict': + self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop") + elif mode == 'loose': + self._cmd(f"{nft_prefix} fib saddr oif 0 counter drop") def _cleanup_ipv6_source_validation_rules(self, ifname): - commands = [] results = self._cmd(f'nft -a list chain ip6 raw vyos_rpfilter').split("\n") for line in results: if f'iifname "{ifname}"' in line: @@ -805,7 +790,9 @@ class Interface(Control): >>> Interface('eth0').set_ipv6_source_validation('strict') """ self._cleanup_ipv6_source_validation_rules(self.ifname) - nft_prefix = f'nft add rule ip6 raw vyos_rpfilter iifname "{self.ifname}"' + nft_prefix = f'nft insert rule ip6 raw vyos_rpfilter iifname "{self.ifname}"' + if mode in ['strict', 'loose']: + self._cmd(f"{nft_prefix} counter return") if mode == 'strict': self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop") elif mode == 'loose': diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 366d71abd..a40b762a8 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -841,8 +841,12 @@ class BasicInterfaceTest: self.assertEqual('1', tmp) if cli_defined(self._base_path + ['ip'], 'source-validation'): - tmp = read_file(f'{proc_base}/rp_filter') - self.assertEqual('2', tmp) + 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: diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index a3885158b..f74a33566 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -600,23 +600,27 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): 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(nftables_strict_search, 'inet vyos_global_rpfilter') + 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(nftables_loose_search, 'inet vyos_global_rpfilter') + 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(): diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index e75485f9f..e64a85b95 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -33,6 +33,27 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:' MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first' MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first' +def annotated_list(images_list: list[str]) -> list[str]: + """Annotate list of images with additional info + + Args: + images_list (list[str]): a list of image names + + Returns: + list[str]: a list of image names with additional info + """ + index_running: int = None + index_default: int = None + try: + index_running = images_list.index(image.get_running_image()) + index_default = images_list.index(image.get_default_image()) + except ValueError: + pass + if index_running is not None: + images_list[index_running] += ' (running)' + if index_default is not None: + images_list[index_default] += ' (default boot)' + return images_list @compat.grub_cfg_update def delete_image(image_name: Optional[str] = None, @@ -42,7 +63,7 @@ def delete_image(image_name: Optional[str] = None, Args: image_name (str): a name of image to delete """ - available_images: list[str] = grub.version_list() + available_images: list[str] = annotated_list(grub.version_list()) if image_name is None: if no_prompt: exit('An image name is required for delete action') @@ -83,7 +104,7 @@ def set_image(image_name: Optional[str] = None, Args: image_name (str): an image name """ - available_images: list[str] = grub.version_list() + available_images: list[str] = annotated_list(grub.version_list()) if image_name is None: if not prompt: exit('An image name is required for set action') |