diff options
| -rw-r--r-- | interface-definitions/interfaces-virtual-ethernet.xml.in | 1 | ||||
| -rw-r--r-- | python/vyos/ifconfig/control.py | 14 | ||||
| -rw-r--r-- | python/vyos/ifconfig/interface.py | 83 | ||||
| -rw-r--r-- | python/vyos/util.py | 8 | ||||
| -rw-r--r-- | python/vyos/validate.py | 68 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-dummy.py | 5 | 
6 files changed, 107 insertions, 72 deletions
| 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/ifconfig/control.py b/python/vyos/ifconfig/control.py index 7a6b36e7c..915c1d2f9 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 7754488c4..85fa90653 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1,4 +1,4 @@ -# Copyright 2019-2022 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 @@ -35,6 +35,7 @@ from vyos.template import render  from vyos.util import mac2eui64  from vyos.util import dict_search  from vyos.util import read_file +from vyos.util import run  from vyos.util import get_interface_config  from vyos.util import get_interface_namespace  from vyos.util import is_systemd_service_active @@ -59,6 +60,15 @@ from netaddr import mac_unix_expanded  link_local_prefix = 'fe80::/64' + +def _interface_exists_in_netns(interface_name, netns): +    from vyos.util import rc_cmd +    rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') +    if rc == 0: +        return True +    return False + +  class Interface(Control):      # This is the class which will be used to create      # self.operational, it allows subclasses, such as @@ -137,9 +147,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}', @@ -270,8 +277,11 @@ class Interface(Control):      }      @classmethod -    def exists(cls, ifname): -        return os.path.exists(f'/sys/class/net/{ifname}') +    def exists(cls, ifname, netns=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): @@ -339,7 +349,12 @@ 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 {self.config["netns"]} {cmd}'          self._cmd(cmd)      def remove(self): @@ -374,6 +389,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.config['ifname']) +        if netns: cmd = f'ip netns exec {netns} {cmd}'          return self._cmd(cmd)      def _set_vrf_ct_zone(self, vrf): @@ -381,6 +399,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( @@ -533,13 +555,9 @@ class Interface(Control):          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 -          # 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}') +        if _interface_exists_in_netns(self.ifname, netns): +            self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}')              return      def set_netns(self, netns): @@ -551,7 +569,8 @@ class Interface(Control):          >>> Interface('dum0').set_netns('foo')          """ -        self.set_interface('netns', netns) +        cmd = f'ip link set dev {self.ifname} netns {netns}' +        self._cmd(cmd)      def set_vrf(self, vrf):          """ @@ -586,6 +605,10 @@ class Interface(Control):          return self.set_interface('arp_cache_tmo', tmo)      def _cleanup_mss_rules(self, table, ifname): +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          commands = []          results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n")          for line in results: @@ -605,6 +628,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' @@ -739,6 +766,11 @@ class Interface(Control):          As per RFC3074.          """ + +        # Don't allow for netns yet +        if 'netns' in self.config: +            return None +          if value == 'strict':              value = 1          elif value == 'loose': @@ -1101,8 +1133,9 @@ class Interface(Control):              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, self.config.get('netns')): +            exec_within_netns = f'ip netns exec {self.config.get("netns")}' if self.config.get('netns') else '' +            tmp = f'{exec_within_netns} ip addr add {addr} dev {self.ifname}'              # Add broadcast address for IPv4              if is_ipv4(addr): tmp += ' brd +' @@ -1147,8 +1180,9 @@ class Interface(Control):              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=self.config.get('netns')): +            exec_within_netns = f'ip netns exec {self.config.get("netns")}' if self.config.get('netns') else '' +            self._cmd(f'{exec_within_netns} ip addr del "{addr}" dev "{self.ifname}"')          else:              return False @@ -1168,8 +1202,12 @@ class Interface(Control):          self.set_dhcp(False)          self.set_dhcpv6(False) +        _netns = get_interface_namespace(self.config['ifname']) +        netns_exec = f'ip netns exec {_netns} ' if _netns else '' +        cmd = netns_exec +        cmd += f'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):          """ @@ -1308,6 +1346,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 @@ -1412,8 +1455,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 _interface_exists_in_netns(self.ifname, self.config['netns']): +                self.set_netns(config.get('netns', ''))          else:              self.del_netns(config.get('netns', '')) diff --git a/python/vyos/util.py b/python/vyos/util.py index 61ce59324..d83287fd2 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -906,14 +906,16 @@ def get_bridge_fdb(interface):      tmp = loads(cmd(f'bridge -j fdb show dev {interface}'))      return tmp -def get_interface_config(interface): +def get_interface_config(interface, netns=None):      """ Returns the used encapsulation protocol for given interface.          If interface does not exist, None is returned.      """ -    if not os.path.exists(f'/sys/class/net/{interface}'): +    from vyos.util import run +    netns_exec = f'ip netns exec {netns}' if netns else '' +    if run(f'{netns_exec} test -e /sys/class/net/{interface}') != 0:          return None      from json import loads -    tmp = loads(cmd(f'ip -d -j link show {interface}'))[0] +    tmp = loads(cmd(f'{netns_exec} ip -d -j link show {interface}'))[0]      return tmp  def get_interface_address(interface): diff --git a/python/vyos/validate.py b/python/vyos/validate.py index d18785aaf..d3e6e9087 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -43,57 +43,31 @@ def _are_same_ip(one, two):      s_two = AF_INET if is_ipv4(two) else AF_INET6      return inet_pton(f_one, one) == inet_pton(f_one, two) -def is_intf_addr_assigned(intf, address) -> bool: -    """ -    Verify if the given IPv4/IPv6 address is assigned to specific interface. +def is_intf_addr_assigned(ifname, addr, netns=None): +    """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 - -        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 _]) +    import json +    import jmespath +    from vyos.util import rc_cmd +    from ipaddress import ip_interface -        if str(prefixlen) == netmask: -            return True +    within_netns = f'ip netns exec {netns}' if netns else '' +    rc, out = rc_cmd(f'{within_netns} 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/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e771581e1..a02ddeb51 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -55,7 +55,10 @@ def generate(dummy):      return None  def apply(dummy): -    d = DummyIf(dummy['ifname']) +    if 'netns' in dummy: +        d = DummyIf(ifname=dummy['ifname'], netns=dummy['netns']) +    else: +        d = DummyIf(dummy['ifname'])      # Remove dummy interface      if 'deleted' in dummy: | 
