diff options
| -rw-r--r-- | interface-definitions/container.xml.in | 1 | ||||
| -rw-r--r-- | interface-definitions/include/bgp/afi-export-import.xml.i | 1 | ||||
| -rw-r--r-- | interface-definitions/include/constraint/interface-name-with-wildcard-and-inverted.xml.i | 4 | ||||
| -rw-r--r-- | interface-definitions/include/firewall/match-interface.xml.i | 20 | ||||
| -rw-r--r-- | python/vyos/firewall.py | 22 | ||||
| -rw-r--r-- | python/vyos/utils/network.py | 22 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_firewall.py | 13 | ||||
| -rwxr-xr-x | src/conf_mode/container.py | 9 | ||||
| -rwxr-xr-x | src/conf_mode/vrf.py | 21 | ||||
| -rwxr-xr-x | src/op_mode/neighbor.py | 5 | ||||
| -rwxr-xr-x | src/op_mode/vrf.py | 21 | 
11 files changed, 102 insertions, 37 deletions
| diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 6d2eb18d0..baab6104f 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -213,6 +213,7 @@                <help>Publish port to the container</help>              </properties>              <children> +              #include <include/listen-address.xml.i>                <leafNode name="source">                  <properties>                    <help>Source host port</help> diff --git a/interface-definitions/include/bgp/afi-export-import.xml.i b/interface-definitions/include/bgp/afi-export-import.xml.i index 86817cdb3..5223af0ae 100644 --- a/interface-definitions/include/bgp/afi-export-import.xml.i +++ b/interface-definitions/include/bgp/afi-export-import.xml.i @@ -32,6 +32,7 @@          </valueHelp>          <completionHelp>            <path>vrf name</path> +          <list>default</list>          </completionHelp>          <multi/>        </properties> diff --git a/interface-definitions/include/constraint/interface-name-with-wildcard-and-inverted.xml.i b/interface-definitions/include/constraint/interface-name-with-wildcard-and-inverted.xml.i new file mode 100644 index 000000000..6a39041a3 --- /dev/null +++ b/interface-definitions/include/constraint/interface-name-with-wildcard-and-inverted.xml.i @@ -0,0 +1,4 @@ +<!-- include start from constraint/interface-name-with-wildcard-and-inverted.xml.i --> +<regex>(\!?)(bond|br|dum|en|ersp|eth|gnv|ifb|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)([0-9]?)(\*?)(.+)?|(\!?)lo</regex> +<validator name="file-path --lookup-path /sys/class/net --directory"/> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/match-interface.xml.i b/interface-definitions/include/firewall/match-interface.xml.i index a62bf8d89..7810f88ab 100644 --- a/interface-definitions/include/firewall/match-interface.xml.i +++ b/interface-definitions/include/firewall/match-interface.xml.i @@ -7,10 +7,18 @@      </completionHelp>      <valueHelp>        <format>txt</format> -      <description>Interface name, wildcard (*) supported</description> +      <description>Interface name</description> +    </valueHelp> +    <valueHelp> +      <format>txt*</format> +      <description>Interface name with wildcard</description> +    </valueHelp> +    <valueHelp> +      <format>!txt</format> +      <description>Inverted interface name to match</description>      </valueHelp>      <constraint> -      #include <include/constraint/interface-name-with-wildcard.xml.i> +      #include <include/constraint/interface-name-with-wildcard-and-inverted.xml.i>      </constraint>    </properties>  </leafNode> @@ -20,6 +28,14 @@      <completionHelp>        <path>firewall group interface-group</path>      </completionHelp> +    <valueHelp> +      <format>txt</format> +      <description>Interface-group name to match</description> +    </valueHelp> +    <valueHelp> +      <format>!txt</format> +      <description>Inverted interface-group name to match</description> +    </valueHelp>    </properties>  </leafNode>  <!-- include end -->
\ No newline at end of file diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 4aa509fe2..53ff8259e 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -272,20 +272,34 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):                  output.append(f'ip6 hoplimit {operator} {value}')      if 'inbound_interface' in rule_conf: +        operator = ''          if 'interface_name' in rule_conf['inbound_interface']:              iiface = rule_conf['inbound_interface']['interface_name'] -            output.append(f'iifname {{{iiface}}}') +            if iiface[0] == '!': +                operator = '!=' +                iiface = iiface[1:] +            output.append(f'iifname {operator} {{{iiface}}}')          else:              iiface = rule_conf['inbound_interface']['interface_group'] -            output.append(f'iifname @I_{iiface}') +            if iiface[0] == '!': +                operator = '!=' +                iiface = iiface[1:] +            output.append(f'iifname {operator} @I_{iiface}')      if 'outbound_interface' in rule_conf: +        operator = ''          if 'interface_name' in rule_conf['outbound_interface']:              oiface = rule_conf['outbound_interface']['interface_name'] -            output.append(f'oifname {{{oiface}}}') +            if oiface[0] == '!': +                operator = '!=' +                oiface = oiface[1:] +            output.append(f'oifname {operator} {{{oiface}}}')          else:              oiface = rule_conf['outbound_interface']['interface_group'] -            output.append(f'oifname @I_{oiface}') +            if oiface[0] == '!': +                operator = '!=' +                oiface = oiface[1:] +            output.append(f'oifname {operator} @I_{oiface}')      if 'ttl' in rule_conf:          operators = {'eq': '==', 'gt': '>', 'lt': '<'} diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 3f9a3ef4b..2f181d8d9 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -36,6 +36,10 @@ def get_protocol_by_name(protocol_name):      except socket.error:          return protocol_name +def interface_exists(interface) -> bool: +    import os +    return os.path.exists(f'/sys/class/net/{interface}') +  def interface_exists_in_netns(interface_name, netns):      from vyos.utils.process import rc_cmd      rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}') @@ -43,6 +47,24 @@ def interface_exists_in_netns(interface_name, netns):          return True      return False +def get_vrf_members(vrf: str) -> list: +    """ +    Get list of interface VRF members +    :param vrf: str +    :return: list +    """ +    import json +    from vyos.utils.process import cmd +    if not interface_exists(vrf): +        raise ValueError(f'VRF "{vrf}" does not exist!') +    output = cmd(f'ip --json --brief link show master {vrf}') +    answer = json.loads(output) +    interfaces = [] +    for data in answer: +        if 'ifname' in data: +            interfaces.append(data.get('ifname')) +    return interfaces +  def get_interface_vrf(interface):      """ Returns VRF of given interface """      from vyos.utils.dict import dict_search diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 7a13f396f..b2076c077 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -137,7 +137,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept'])          self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain'])          self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) -        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'interface-group', 'smoketest_interface']) +        self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'interface-group', '!smoketest_interface'])          self.cli_commit() @@ -153,7 +153,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):              ['elements = { 192.0.2.5, 192.0.2.8,'],              ['192.0.2.10, 192.0.2.11 }'],              ['ip saddr @D_smoketest_domain', 'accept'], -            ['oifname @I_smoketest_interface', 'accept'] +            ['oifname != @I_smoketest_interface', 'accept']          ]          self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -192,6 +192,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):      def test_ipv4_basic_rules(self):          name = 'smoketest'          interface = 'eth0' +        interface_inv = '!eth0'          interface_wc = 'l2tp*'          mss_range = '501-1460'          conn_mark = '555' @@ -231,7 +232,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'flags', 'syn'])          self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'mss', mss_range])          self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'packet-type', 'broadcast']) -        self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'interface-name', interface]) +        self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'interface-name', interface_wc])          self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'action', 'return'])          self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'protocol', 'gre'])          self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'connection-mark', conn_mark]) @@ -239,7 +240,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'accept'])          self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'action', 'drop'])          self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'protocol', 'gre']) -        self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'interface-name', interface_wc]) +        self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'interface-name', interface_inv])          self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'action', 'return'])          self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'protocol', 'icmp'])          self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'connection-mark', conn_mark]) @@ -255,11 +256,11 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):              ['tcp dport 22', 'add @RECENT_FWD_filter_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'meta pkttype host', 'drop'],              ['chain VYOS_INPUT_filter'],              ['type filter hook input priority filter; policy accept;'], -            ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface}"', 'meta pkttype broadcast', 'accept'], +            ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface_wc}"', 'meta pkttype broadcast', 'accept'],              ['meta l4proto gre', f'ct mark {mark_hex}', 'return'],              ['chain VYOS_OUTPUT_filter'],              ['type filter hook output priority filter; policy accept;'], -            ['meta l4proto gre', f'oifname "{interface_wc}"', 'drop'], +            ['meta l4proto gre', f'oifname != "{interface}"', 'drop'],              ['meta l4proto icmp', f'ct mark {mark_hex}', 'return'],              ['chain NAME_smoketest'],              ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index ed7cc809c..478868a9a 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -33,6 +33,7 @@ from vyos.utils.process import call  from vyos.utils.process import cmd  from vyos.utils.process import run  from vyos.utils.process import rc_cmd +from vyos.template import bracketize_ipv6  from vyos.template import inc_ip  from vyos.template import is_ipv4  from vyos.template import is_ipv6 @@ -280,6 +281,14 @@ def generate_run_arguments(name, container_config):              protocol = container_config['port'][portmap]['protocol']              sport = container_config['port'][portmap]['source']              dport = container_config['port'][portmap]['destination'] +            listen_addresses = container_config['port'][portmap].get('listen_address', []) + +        # If listen_addresses is not empty, include them in the publish command +        if listen_addresses: +            for listen_address in listen_addresses: +                port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}' +        else: +            # If listen_addresses is empty, just include the standard publish command              port += f' --publish {sport}:{dport}/{protocol}'      # Bind volume diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index be867b208..37625142c 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -28,6 +28,8 @@ from vyos.template import render  from vyos.template import render_to_string  from vyos.utils.dict import dict_search  from vyos.utils.network import get_interface_config +from vyos.utils.network import get_vrf_members +from vyos.utils.network import interface_exists  from vyos.utils.process import call  from vyos.utils.process import cmd  from vyos.utils.process import popen @@ -143,7 +145,7 @@ def verify(vrf):                  raise ConfigError(f'VRF "{name}" table id is mandatory!')              # routing table id can't be changed - OS restriction -            if os.path.isdir(f'/sys/class/net/{name}'): +            if interface_exists(name):                  tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name)))                  if tmp and tmp != vrf_config['table']:                      raise ConfigError(f'VRF "{name}" table id modification not possible!') @@ -195,12 +197,23 @@ def apply(vrf):      sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)      for tmp in (dict_search('vrf_remove', vrf) or []): -        if os.path.isdir(f'/sys/class/net/{tmp}'): -            call(f'ip link delete dev {tmp}') +        if interface_exists(tmp): +            # T5492: deleting a VRF instance may leafe processes running +            # (e.g. dhclient) as there is a depedency ordering issue in the CLI. +            # We need to ensure that we stop the dhclient processes first so +            # a proper DHCLP RELEASE message is sent +            for interface in get_vrf_members(tmp): +                vrf_iface = Interface(interface) +                vrf_iface.set_dhcp(False) +                vrf_iface.set_dhcpv6(False) +              # Remove nftables conntrack zone map item              nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'              cmd(f'nft {nft_del_element}') +            # Delete the VRF Kernel interface +            call(f'ip link delete dev {tmp}') +      if 'name' in vrf:          # Separate VRFs in conntrack table          # check if table already exists @@ -245,7 +258,7 @@ def apply(vrf):          for name, config in vrf['name'].items():              table = config['table'] -            if not os.path.isdir(f'/sys/class/net/{name}'): +            if not interface_exists(name):                  # For each VRF apart from your default context create a VRF                  # interface with a separate routing table                  call(f'ip link add {name} type vrf table {table}') diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py index 1edeb0045..8b3c45c7c 100755 --- a/src/op_mode/neighbor.py +++ b/src/op_mode/neighbor.py @@ -31,14 +31,11 @@ import sys  import typing  import vyos.opmode +from vyos.utils.network import interface_exists  ArgFamily = typing.Literal['inet', 'inet6']  ArgState = typing.Literal['reachable', 'stale', 'failed', 'permanent'] -def interface_exists(interface): -    import os -    return os.path.exists(f'/sys/class/net/{interface}') -  def get_raw_data(family, interface=None, state=None):      from json import loads      from vyos.utils.process import cmd diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py index 1f0bbbaeb..51032a4b5 100755 --- a/src/op_mode/vrf.py +++ b/src/op_mode/vrf.py @@ -20,11 +20,11 @@ import sys  import typing  from tabulate import tabulate +from vyos.utils.network import get_vrf_members  from vyos.utils.process import cmd  import vyos.opmode -  def _get_raw_data(name=None):      """      If vrf name is not set - get all VRFs @@ -45,21 +45,6 @@ def _get_raw_data(name=None):      return data -def _get_vrf_members(vrf: str) -> list: -    """ -    Get list of interface VRF members -    :param vrf: str -    :return: list -    """ -    output = cmd(f'ip --json --brief link show master {vrf}') -    answer = json.loads(output) -    interfaces = [] -    for data in answer: -        if 'ifname' in data: -            interfaces.append(data.get('ifname')) -    return interfaces if len(interfaces) > 0 else ['n/a'] - -  def _get_formatted_output(raw_data):      data_entries = []      for vrf in raw_data: @@ -67,7 +52,9 @@ def _get_formatted_output(raw_data):          state = vrf.get('operstate').lower()          hw_address = vrf.get('address')          flags = ','.join(vrf.get('flags')).lower() -        members = ','.join(_get_vrf_members(name)) +        tmp = get_vrf_members(name) +        if tmp: members = ','.join(get_vrf_members(name)) +        else: members = 'n/a'          data_entries.append([name, state, hw_address, flags, members])      headers = ["Name", "State", "MAC address", "Flags", "Interfaces"] | 
