diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/config_mgmt.py | 31 | ||||
| -rw-r--r-- | python/vyos/ifconfig/control.py | 14 | ||||
| -rw-r--r-- | python/vyos/ifconfig/interface.py | 167 | ||||
| -rw-r--r-- | python/vyos/utils/network.py | 97 | ||||
| -rw-r--r-- | python/vyos/utils/process.py | 2 | 
5 files changed, 167 insertions, 144 deletions
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,  | 
