diff options
19 files changed, 303 insertions, 195 deletions
| diff --git a/data/templates/dhcp-server/10-override.conf.j2 b/data/templates/dhcp-server/10-override.conf.j2 index dd5730b90..1504b6808 100644 --- a/data/templates/dhcp-server/10-override.conf.j2 +++ b/data/templates/dhcp-server/10-override.conf.j2 @@ -1,5 +1,5 @@  ### Autogenerated by dhcp_server.py ### -{% set lease_file = '/run/dhcp-server/dhcpd.leases' %} +{% set lease_file = '/config/dhcpd.leases' %}  [Unit]  Description=ISC DHCP IPv4 server  Documentation=man:dhcpd(8) diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index 0fbddfaa9..a82a5537b 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -5,29 +5,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/src/etc/systemd/system/keepalived.service.d/override.conf b/data/templates/high-availability/10-override.conf.j2 index d91a824b9..d1cb25581 100644 --- a/src/etc/systemd/system/keepalived.service.d/override.conf +++ b/data/templates/high-availability/10-override.conf.j2 @@ -1,3 +1,5 @@ +### Autogenerated by ${vyos_conf_scripts_dir}/high-availability.py ### +{% set snmp = '' if vrrp.disable_snmp is vyos_defined else '--snmp' %}  [Unit]  After=vyos-router.service  # Only start if there is our configuration file - remove Debian default @@ -10,5 +12,5 @@ KillMode=process  Type=simple  # Read configuration variable file if it is present  ExecStart= -ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp +ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork {{ snmp }}  PIDFile=/run/keepalived/keepalived.pid diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index f8e1587f8..0a40e1ecf 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -150,13 +150,13 @@ backend {{ back }}  {%             endfor %}  {%         endif %}  {%         if back_config.timeout.check is vyos_defined %} -    timeout check {{ back_config.timeout.check }} +    timeout check {{ back_config.timeout.check }}s  {%         endif %}  {%         if back_config.timeout.connect is vyos_defined %} -    timeout connect {{ back_config.timeout.connect }} +    timeout connect {{ back_config.timeout.connect }}s  {%         endif %}  {%         if back_config.timeout.server is vyos_defined %} -    timeout server {{ back_config.timeout.server }} +    timeout server {{ back_config.timeout.server }}s  {%         endif %}  {%     endfor %} 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/high-availability.xml.in b/interface-definitions/high-availability.xml.in index 4f55916fa..47a772d04 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -12,6 +12,12 @@            <help>Virtual Router Redundancy Protocol settings</help>          </properties>          <children> +          <leafNode name="disable-snmp"> +            <properties> +              <valueless/> +              <help>Disable SNMP</help> +            </properties> +          </leafNode>            <node name="global-parameters">              <properties>                <help>VRRP global parameters</help> diff --git a/interface-definitions/include/firewall/global-options.xml.i b/interface-definitions/include/firewall/global-options.xml.i index a63874cb0..e655cd6ac 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> @@ -227,6 +227,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/interface-definitions/interfaces-virtual-ethernet.xml.in b/interface-definitions/interfaces-virtual-ethernet.xml.in index 1daa764d4..5f205f354 100644 --- a/interface-definitions/interfaces-virtual-ethernet.xml.in +++ b/interface-definitions/interfaces-virtual-ethernet.xml.in @@ -21,6 +21,7 @@            #include <include/interface/dhcp-options.xml.i>            #include <include/interface/dhcpv6-options.xml.i>            #include <include/interface/disable.xml.i> +          #include <include/interface/netns.xml.i>            #include <include/interface/vif-s.xml.i>            #include <include/interface/vif.xml.i>            #include <include/interface/vrf.xml.i> diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index 0fc72e660..9caf6da2b 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -31,6 +31,7 @@ from vyos.configtree import ConfigTree, ConfigTreeError, show_diff  from vyos.defaults import directories  from vyos.version import get_full_version_data  from vyos.utils.io import ask_yes_no +from vyos.utils.boot import boot_configuration_complete  from vyos.utils.process import is_systemd_service_active  from vyos.utils.process import rc_cmd @@ -200,9 +201,9 @@ Proceed ?'''              raise ConfigMgmtError(out)          entry = self._read_tmp_log_entry() -        self._add_log_entry(**entry)          if self._archive_active_config(): +            self._add_log_entry(**entry)              self._update_archive()          msg = 'Reboot timer stopped' @@ -334,10 +335,10 @@ Proceed ?'''              user = self._get_user()              via = 'init'              comment = '' -            self._add_log_entry(user, via, comment)              # add empty init config before boot-config load for revision              # and diff consistency              if self._archive_active_config(): +                self._add_log_entry(user, via, comment)                  self._update_archive()          os.umask(mask) @@ -352,9 +353,8 @@ Proceed ?'''              self._new_log_entry(tmp_file=tmp_log_entry)              return -        self._add_log_entry() -          if self._archive_active_config(): +            self._add_log_entry()              self._update_archive()      def commit_archive(self): @@ -475,22 +475,26 @@ Proceed ?'''          conf_file.chmod(0o644)      def _archive_active_config(self) -> bool: +        use_tmp = (boot_configuration_complete() or not +                   os.path.isfile(archive_config_file))          mask = os.umask(0o113) -        ext = os.getpid() -        tmp_save = f'/tmp/config.boot.{ext}' -        save_config(tmp_save) +        if use_tmp: +            ext = os.getpid() +            cmp_saved = f'/tmp/config.boot.{ext}' +            save_config(cmp_saved) +        else: +            cmp_saved = config_file          try: -            if cmp(tmp_save, archive_config_file, shallow=False): -                # this will be the case on boot, as well as certain -                # re-initialiation instances after delete/set -                os.unlink(tmp_save) +            if cmp(cmp_saved, archive_config_file, shallow=False): +                if use_tmp: os.unlink(cmp_saved) +                os.umask(mask)                  return False          except FileNotFoundError:              pass -        rc, out = rc_cmd(f'sudo mv {tmp_save} {archive_config_file}') +        rc, out = rc_cmd(f'sudo mv {cmp_saved} {archive_config_file}')          os.umask(mask)          if rc != 0: @@ -522,9 +526,8 @@ Proceed ?'''          return len(l)      def _check_revision_number(self, rev: int) -> bool: -        # exclude init revision:          maxrev = self._get_number_of_revisions() -        if not 0 <= rev < maxrev - 1: +        if not 0 <= rev < maxrev:              return False          return True diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index c8366cb58..7402da55a 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2023 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 @@ -49,6 +49,18 @@ class Control(Section):          return popen(command, self.debug)      def _cmd(self, command): +        import re +        if 'netns' in self.config: +            # This command must be executed from default netns 'ip link set dev X netns X' +            # exclude set netns cmd from netns to avoid: +            # failed to run command: ip netns exec ns01 ip link set dev veth20 netns ns01 +            pattern = r'ip link set dev (\S+) netns (\S+)' +            matches = re.search(pattern, command) +            if matches and matches.group(2) == self.config['netns']: +                # Command already includes netns and matches desired namespace: +                command = command +            else: +                command = f'ip netns exec {self.config["netns"]} {command}'          return cmd(command, self.debug)      def _get_command(self, config, name): diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 41ce352ad..050095364 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -38,7 +38,9 @@ from vyos.utils.dict import dict_search  from vyos.utils.file import read_file  from vyos.utils.network import get_interface_config  from vyos.utils.network import get_interface_namespace +from vyos.utils.network import is_netns_interface  from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import run  from vyos.template import is_ipv4  from vyos.template import is_ipv6  from vyos.utils.network import is_intf_addr_assigned @@ -138,9 +140,6 @@ class Interface(Control):              'validate': assert_mtu,              'shellcmd': 'ip link set dev {ifname} mtu {value}',          }, -        'netns': { -            'shellcmd': 'ip link set dev {ifname} netns {value}', -        },          'vrf': {              'convert': lambda v: f'master {v}' if v else 'nomaster',              'shellcmd': 'ip link set dev {ifname} {value}', @@ -175,10 +174,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 +247,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',          }, @@ -286,8 +278,11 @@ class Interface(Control):      }      @classmethod -    def exists(cls, ifname): -        return os.path.exists(f'/sys/class/net/{ifname}') +    def exists(cls, ifname: str, netns: str=None) -> bool: +        cmd = f'ip link show dev {ifname}' +        if netns: +           cmd = f'ip netns exec {netns} {cmd}' +        return run(cmd) == 0      @classmethod      def get_config(cls): @@ -355,7 +350,13 @@ class Interface(Control):          self.vrrp = VRRP(ifname)      def _create(self): +        # Do not create interface that already exist or exists in netns +        netns = self.config.get('netns', None) +        if self.exists(f'{self.ifname}', netns=netns): +            return +          cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) +        if 'netns' in self.config: cmd = f'ip netns exec {netns} {cmd}'          self._cmd(cmd)      def remove(self): @@ -390,6 +391,9 @@ class Interface(Control):          # after interface removal no other commands should be allowed          # to be called and instead should raise an Exception:          cmd = 'ip link del dev {ifname}'.format(**self.config) +        # for delete we can't get data from self.config{'netns'} +        netns = get_interface_namespace(self.ifname) +        if netns: cmd = f'ip netns exec {netns} {cmd}'          return self._cmd(cmd)      def _set_vrf_ct_zone(self, vrf): @@ -397,6 +401,10 @@ class Interface(Control):          Add/Remove rules in nftables to associate traffic in VRF to an          individual conntack zone          """ +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          if vrf:              # Get routing table ID for VRF              vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get( @@ -540,36 +548,30 @@ class Interface(Control):          if prev_state == 'up':              self.set_admin_state('up') -    def del_netns(self, netns): -        """ -        Remove interface from given NETNS. -        """ - -        # If NETNS does not exist then there is nothing to delete +    def del_netns(self, netns: str) -> bool: +        """ Remove interface from given network namespace """ +        # If network namespace does not exist then there is nothing to delete          if not os.path.exists(f'/run/netns/{netns}'): -            return None - -        # As a PoC we only allow 'dummy' interfaces -        if 'dum' not in self.ifname: -            return None +            return False -        # Check if interface realy exists in namespace -        if get_interface_namespace(self.ifname) != None: -            self._cmd(f'ip netns exec {get_interface_namespace(self.ifname)} ip link del dev {self.ifname}') -            return +        # Check if interface exists in network namespace +        if is_netns_interface(self.ifname, netns): +            self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}') +            return True +        return False -    def set_netns(self, netns): +    def set_netns(self, netns: str) -> bool:          """ -        Add interface from given NETNS. +        Add interface from given network namespace          Example:          >>> from vyos.ifconfig import Interface          >>> Interface('dum0').set_netns('foo')          """ +        self._cmd(f'ip link set dev {self.ifname} netns {netns}') +        return True -        self.set_interface('netns', netns) - -    def set_vrf(self, vrf): +    def set_vrf(self, vrf: str) -> bool:          """          Add/Remove interface from given VRF instance. @@ -581,10 +583,11 @@ class Interface(Control):          tmp = self.get_interface('vrf')          if tmp == vrf: -            return None +            return False          self.set_interface('vrf', vrf)          self._set_vrf_ct_zone(vrf) +        return True      def set_arp_cache_tmo(self, tmo):          """ @@ -621,6 +624,10 @@ class Interface(Control):          >>> from vyos.ifconfig import Interface          >>> Interface('eth0').set_tcp_ipv4_mss(1340)          """ +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          self._cleanup_mss_rules('raw', self.ifname)          nft_prefix = 'nft add rule raw VYOS_TCP_MSS'          base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn' @@ -641,6 +648,10 @@ class Interface(Control):          >>> from vyos.ifconfig import Interface          >>> Interface('eth0').set_tcp_mss(1320)          """ +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          self._cleanup_mss_rules('ip6 raw', self.ifname)          nft_prefix = 'nft add rule ip6 raw VYOS_TCP_MSS'          base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn' @@ -745,40 +756,36 @@ 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: +        Example: +        >>> from vyos.ifconfig import Interface +        >>> Interface('eth0').set_ipv4_source_validation('strict') +        """ +        # Don't allow for netns yet +        if 'netns' in self.config:              return None -        return self.set_interface('rp_filter', value) + +        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: @@ -794,8 +801,14 @@ class Interface(Control):          >>> from vyos.ifconfig import Interface          >>> Interface('eth0').set_ipv6_source_validation('strict')          """ +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          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': @@ -1143,13 +1156,17 @@ class Interface(Control):          if addr in self._addr:              return False +        # get interface network namespace if specified +        netns = self.config.get('netns', None) +          # add to interface          if addr == 'dhcp':              self.set_dhcp(True)          elif addr == 'dhcpv6':              self.set_dhcpv6(True) -        elif not is_intf_addr_assigned(self.ifname, addr): -            tmp = f'ip addr add {addr} dev {self.ifname}' +        elif not is_intf_addr_assigned(self.ifname, addr, netns=netns): +            netns_cmd  = f'ip netns exec {netns}' if netns else '' +            tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}'              # Add broadcast address for IPv4              if is_ipv4(addr): tmp += ' brd +' @@ -1189,13 +1206,17 @@ class Interface(Control):          if not addr:              raise ValueError() +        # get interface network namespace if specified +        netns = self.config.get('netns', None) +          # remove from interface          if addr == 'dhcp':              self.set_dhcp(False)          elif addr == 'dhcpv6':              self.set_dhcpv6(False) -        elif is_intf_addr_assigned(self.ifname, addr): -            self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') +        elif is_intf_addr_assigned(self.ifname, addr, netns=netns): +            netns_cmd  = f'ip netns exec {netns}' if netns else '' +            self._cmd(f'{netns_cmd} ip addr del {addr} dev {self.ifname}')          else:              return False @@ -1215,8 +1236,11 @@ class Interface(Control):          self.set_dhcp(False)          self.set_dhcpv6(False) +        netns = get_interface_namespace(self.ifname) +        netns_cmd = f'ip netns exec {netns}' if netns else '' +        cmd = f'{netns_cmd} ip addr flush dev {self.ifname}'          # flush all addresses -        self._cmd(f'ip addr flush dev "{self.ifname}"') +        self._cmd(cmd)      def add_to_bridge(self, bridge_dict):          """ @@ -1371,6 +1395,11 @@ class Interface(Control):          #   - https://man7.org/linux/man-pages/man8/tc-mirred.8.html          # Depening if we are the source or the target interface of the port          # mirror we need to setup some variables. + +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          source_if = self.config['ifname']          mirror_config = None @@ -1471,8 +1500,8 @@ class Interface(Control):          # Since the interface is pushed onto a separate logical stack          # Configure NETNS          if dict_search('netns', config) != None: -            self.set_netns(config.get('netns', '')) -            return +            if not is_netns_interface(self.ifname, self.config['netns']): +                self.set_netns(config.get('netns', ''))          else:              self.del_netns(config.get('netns', '')) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index bc6899e45..55ff29f0c 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -40,13 +40,19 @@ def interface_exists(interface) -> bool:      import os      return os.path.exists(f'/sys/class/net/{interface}') -def interface_exists_in_netns(interface_name, netns): +def is_netns_interface(interface, netns):      from vyos.utils.process import rc_cmd -    rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') +    rc, out = rc_cmd(f'sudo ip netns exec {netns} ip link show dev {interface}')      if rc == 0:          return True      return False +def get_netns_all() -> list: +    from json import loads +    from vyos.utils.process import cmd +    tmp = loads(cmd('ip --json netns ls')) +    return [ netns['name'] for netns in tmp ] +  def get_vrf_members(vrf: str) -> list:      """      Get list of interface VRF members @@ -78,8 +84,7 @@ def get_interface_config(interface):      """ Returns the used encapsulation protocol for given interface.          If interface does not exist, None is returned.      """ -    import os -    if not os.path.exists(f'/sys/class/net/{interface}'): +    if not interface_exists(interface):          return None      from json import loads      from vyos.utils.process import cmd @@ -90,31 +95,30 @@ def get_interface_address(interface):      """ Returns the used encapsulation protocol for given interface.          If interface does not exist, None is returned.      """ -    import os -    if not os.path.exists(f'/sys/class/net/{interface}'): +    if not interface_exists(interface):          return None      from json import loads      from vyos.utils.process import cmd      tmp = loads(cmd(f'ip --detail --json addr show dev {interface}'))[0]      return tmp -def get_interface_namespace(iface): +def get_interface_namespace(interface: str):      """         Returns wich netns the interface belongs to      """      from json import loads      from vyos.utils.process import cmd -    # Check if netns exist -    tmp = loads(cmd(f'ip --json netns ls')) -    if len(tmp) == 0: -        return None -    for ns in tmp: +    # Bail out early if netns does not exist +    tmp = cmd(f'ip --json netns ls') +    if not tmp: return None + +    for ns in loads(tmp):          netns = f'{ns["name"]}'          # Search interface in each netns          data = loads(cmd(f'ip netns exec {netns} ip --json link show'))          for tmp in data: -            if iface == tmp["ifname"]: +            if interface == tmp["ifname"]:                  return netns  def is_ipv6_tentative(iface: str, ipv6_address: str) -> bool: @@ -172,8 +176,7 @@ def is_wwan_connected(interface):  def get_bridge_fdb(interface):      """ Returns the forwarding database entries for a given interface """ -    import os -    if not os.path.exists(f'/sys/class/net/{interface}'): +    if not interface_exists(interface):          return None      from json import loads      from vyos.utils.process import cmd @@ -305,57 +308,33 @@ def is_addr_assigned(ip_address, vrf=None) -> bool:      return False -def is_intf_addr_assigned(intf, address) -> bool: +def is_intf_addr_assigned(ifname: str, addr: str, netns: str=None) -> bool:      """      Verify if the given IPv4/IPv6 address is assigned to specific interface.      It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR      address 192.0.2.1/24.      """ -    from vyos.template import is_ipv4 - -    from netifaces import ifaddresses -    from netifaces import AF_INET -    from netifaces import AF_INET6 - -    # check if the requested address type is configured at all -    # { -    # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], -    # 2:  [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}], -    # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] -    # } -    try: -        addresses = ifaddresses(intf) -    except ValueError as e: -        print(e) -        return False - -    # determine IP version (AF_INET or AF_INET6) depending on passed address -    addr_type = AF_INET if is_ipv4(address) else AF_INET6 - -    # Check every IP address on this interface for a match -    netmask = None -    if '/' in address: -        address, netmask = address.split('/') -    for ip in addresses.get(addr_type, []): -        # ip can have the interface name in the 'addr' field, we need to remove it -        # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} -        ip_addr = ip['addr'].split('%')[0] - -        if not _are_same_ip(address, ip_addr): -            continue - -        # we do not have a netmask to compare against, they are the same -        if not netmask: -            return True +    import json +    import jmespath -        prefixlen = '' -        if is_ipv4(ip_addr): -            prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')]) -        else: -            prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split('/')[0].split(':') if _]) +    from vyos.utils.process import rc_cmd +    from ipaddress import ip_interface -        if str(prefixlen) == netmask: -            return True +    netns_cmd = f'ip netns exec {netns}' if netns else '' +    rc, out = rc_cmd(f'{netns_cmd} ip --json address show dev {ifname}') +    if rc == 0: +        json_out = json.loads(out) +        addresses = jmespath.search("[].addr_info[].{family: family, address: local, prefixlen: prefixlen}", json_out) +        for address_info in addresses: +            family = address_info['family'] +            address = address_info['address'] +            prefixlen = address_info['prefixlen'] +            # Remove the interface name if present in the given address +            if '%' in addr: +                addr = addr.split('%')[0] +            interface = ip_interface(f"{address}/{prefixlen}") +            if ip_interface(addr) == interface or address == addr: +                return True      return False diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index e09c7d86d..c2ef98140 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -170,7 +170,7 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None,      (1, 'Device "eth99" does not exist.')      """      out, code = popen( -        command, flag, +        command.lstrip(), flag,          stdout=stdout, stderr=stderr,          input=input, timeout=timeout,          env=env, shell=shell, diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 820024dc9..51ccbc9e6 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -834,8 +834,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 ee6ccb710..6f9093f4d 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -529,23 +529,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/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_netns.py index b8bebb221..fd04dd520 100755 --- a/smoketest/scripts/cli/test_interfaces_netns.py +++ b/smoketest/scripts/cli/test_netns.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -16,7 +16,6 @@  import unittest -from netifaces import interfaces  from base_vyostest_shim import VyOSUnitTestSHIM  from vyos.configsession import ConfigSession @@ -24,56 +23,61 @@ from vyos.configsession import ConfigSessionError  from vyos.ifconfig import Interface  from vyos.ifconfig import Section  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'] -namespaces = ['mgmt', 'front', 'back', 'ams-ix'] +interfaces = ['dum10', 'dum12', 'dum50'] -class NETNSTest(VyOSUnitTestSHIM.TestCase): -    def setUp(self): -        self._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_create_netns(self): +    def test_netns_create(self): +        namespaces = ['mgmt', 'front', 'back']          for netns in namespaces: -            base = base_path + ['name', netns] -            self.cli_set(base) +            self.cli_set(base_path + ['name', netns])          # commit changes          self.cli_commit() -        netns_list = cmd('ip netns ls') -          # Verify NETNS configuration          for netns in namespaces: -            self.assertTrue(netns in netns_list) - +            self.assertIn(netns, get_netns_all()) -    def test_netns_assign_interface(self): +    def test_netns_interface(self):          netns = 'foo' -        self.cli_set(['netns', 'name', netns]) +        self.cli_set(base_path + ['name', netns])          # Set -        for iface in self._interfaces: +        for iface in interfaces:              self.cli_set(['interfaces', 'dummy', iface, 'netns', netns])          # commit changes          self.cli_commit() -        netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') - -        for iface in self._interfaces: -            self.assertTrue(iface in netns_iface_list) +        for interface in interfaces: +            self.assertTrue(is_netns_interface(interface, netns))          # Delete -        for iface in self._interfaces: -            self.cli_delete(['interfaces', 'dummy', iface, 'netns', netns]) +        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 iface in self._interfaces: -            self.assertNotIn(iface, netns_iface_list) +        for interface in interfaces: +            self.assertFalse(is_netns_interface(interface, netns))  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 0121df11c..70f43ab52 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -15,6 +15,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>. +import os  import time  from sys import exit @@ -24,6 +25,7 @@ from ipaddress import IPv6Interface  from vyos.base import Warning  from vyos.config import Config +from vyos.configdict import leaf_node_changed  from vyos.ifconfig.vrrp import VRRP  from vyos.template import render  from vyos.template import is_ipv4 @@ -35,6 +37,9 @@ from vyos import airbag  airbag.enable() +systemd_override = r'/run/systemd/system/keepalived.service.d/10-override.conf' + +  def get_config(config=None):      if config:          conf = config @@ -54,6 +59,9 @@ def get_config(config=None):      if conf.exists(conntrack_path):          ha['conntrack_sync_group'] = conf.return_value(conntrack_path) +    if leaf_node_changed(conf, base + ['vrrp', 'disable-snmp']): +        ha.update({'restart_required': {}}) +      return ha  def verify(ha): @@ -164,19 +172,23 @@ def verify(ha):  def generate(ha):      if not ha or 'disable' in ha: +        if os.path.isfile(systemd_override): +            os.unlink(systemd_override)          return None      render(VRRP.location['config'], 'high-availability/keepalived.conf.j2', ha) +    render(systemd_override, 'high-availability/10-override.conf.j2', ha)      return None  def apply(ha):      service_name = 'keepalived.service' +    call('systemctl daemon-reload')      if not ha or 'disable' in ha:          call(f'systemctl stop {service_name}')          return None      # Check if IPv6 address is tentative T5533 -    for group, group_config in ha['vrrp']['group'].items(): +    for group, group_config in ha.get('vrrp', {}).get('group', {}).items():          if 'hello_source_address' in group_config:              if is_ipv6(group_config['hello_source_address']):                  ipv6_address = group_config['hello_source_address'] @@ -187,7 +199,11 @@ def apply(ha):                      if is_ipv6_tentative(interface, ipv6_address):                          time.sleep(interval) -    call(f'systemctl reload-or-restart {service_name}') +    systemd_action = 'reload-or-restart' +    if 'restart_required' in ha: +        systemd_action = 'restart' + +    call(f'systemctl {systemd_action} {service_name}')      return None  if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e771581e1..db768b94d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -55,7 +55,7 @@ def generate(dummy):      return None  def apply(dummy): -    d = DummyIf(dummy['ifname']) +    d = DummyIf(**dummy)      # Remove dummy interface      if 'deleted' in dummy: diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py index 2812155e8..8af4a7916 100755 --- a/src/helpers/vyos-save-config.py +++ b/src/helpers/vyos-save-config.py @@ -44,7 +44,10 @@ ct = config.get_config_tree(effective=True)  write_file = save_file if remote_save is None else NamedTemporaryFile(delete=False).name  with open(write_file, 'w') as f: -    f.write(ct.to_string()) +    # config_tree is None before boot configuration is complete; +    # automated saves should check boot_configuration_complete +    if ct is not None: +        f.write(ct.to_string())      f.write("\n")      f.write(system_footer()) | 
