diff options
23 files changed, 325 insertions, 92 deletions
| diff --git a/data/templates/zabbix-agent/zabbix-agent.conf.j2 b/data/templates/zabbix-agent/zabbix-agent.conf.j2 index 77f57f32f..e6dcef872 100644 --- a/data/templates/zabbix-agent/zabbix-agent.conf.j2 +++ b/data/templates/zabbix-agent/zabbix-agent.conf.j2 @@ -1,4 +1,4 @@ -# Generated by ${vyos_conf_scripts_dir}/service_zabbix_agent.py +# Generated by ${vyos_conf_scripts_dir}/service_monitoring_zabbix-agent.py  PidFile=/run/zabbix/zabbix_agent2.pid  LogFile=/var/log/zabbix/zabbix_agent2.log @@ -45,6 +45,10 @@ Server={{ server | bracketize_ipv6 | join(',') }}  ServerActive={{ servers | join(',') }}  {% endif %} +{% if host_name is vyos_defined %} +Hostname={{ host_name }} +{% endif %} +  {% if port is vyos_defined %}  ListenPort={{ port }}  {% endif %} 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/interface-definitions/include/interface/macsec-key.xml.i b/interface-definitions/include/interface/macsec-key.xml.i new file mode 100644 index 000000000..5a857a612 --- /dev/null +++ b/interface-definitions/include/interface/macsec-key.xml.i @@ -0,0 +1,15 @@ +<!-- include start from interface/macsec-key.xml.i --> +<leafNode name="key"> +  <properties> +    <help>MACsec static key</help> +    <valueHelp> +      <format>txt</format> +      <description>16-byte (128-bit) hex-string (32 hex-digits) for gcm-aes-128 or 32-byte (256-bit) hex-string (64 hex-digits) for gcm-aes-256</description> +    </valueHelp> +    <constraint> +      <regex>[A-Fa-f0-9]{32}</regex> +      <regex>[A-Fa-f0-9]{64}</regex> +    </constraint> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index 6bc28e44b..766b0bede 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -52,6 +52,28 @@                    <valueless/>                  </properties>                </leafNode> +              <node name="static"> +                <properties> +                  <help>Use static keys for MACsec [static Secure Authentication Key (SAK) mode]</help> +                </properties> +                <children> +                  #include <include/interface/macsec-key.xml.i> +                  <tagNode name="peer"> +                    <properties> +                      <help>MACsec peer name</help> +                      <constraint> +                        <regex>[^ ]{1,100}</regex> +                      </constraint> +                      <constraintErrorMessage>MACsec peer name exceeds limit of 100 characters</constraintErrorMessage> +                    </properties> +                    <children> +                      #include <include/generic-disable-node.xml.i> +                      #include <include/interface/mac.xml.i> +                      #include <include/interface/macsec-key.xml.i> +                    </children> +                  </tagNode> +                </children> +              </node>                <node name="mka">                  <properties>                    <help>MACsec Key Agreement protocol (MKA)</help> diff --git a/interface-definitions/service-monitoring-zabbix-agent.xml.in b/interface-definitions/service-monitoring-zabbix-agent.xml.in index cfeb02ce0..40f2df642 100644 --- a/interface-definitions/service-monitoring-zabbix-agent.xml.in +++ b/interface-definitions/service-monitoring-zabbix-agent.xml.in @@ -17,6 +17,15 @@                    </constraint>                  </properties>                </leafNode> +              <leafNode name="host-name"> +                <properties> +                  <help>Zabbix agent hostname</help> +                  <constraint> +                    #include <include/constraint/host-name.xml.i> +                  </constraint> +                  <constraintErrorMessage>Host-name must be alphanumeric and can contain hyphens</constraintErrorMessage> +                </properties> +              </leafNode>                <node name="limits">                  <properties>                    <help>Limit settings</help> 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/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index 1a78d18d8..9329c5ee7 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -1,4 +1,4 @@ -# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-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 @@ -41,10 +41,30 @@ class MACsecIf(Interface):          Create MACsec interface in OS kernel. Interface is administrative          down by default.          """ +          # create tunnel interface          cmd  = 'ip link add link {source_interface} {ifname} type {type}'.format(**self.config)          cmd += f' cipher {self.config["security"]["cipher"]}'          self._cmd(cmd) +        # Check if using static keys +        if 'static' in self.config["security"]: +            # Set static TX key +            cmd = 'ip macsec add {ifname} tx sa 0 pn 1 on key 00'.format(**self.config) +            cmd += f' {self.config["security"]["static"]["key"]}' +            self._cmd(cmd) + +            for peer, peer_config in self.config["security"]["static"]["peer"].items(): +                if 'disable' in peer_config: +                    continue + +                # Create the address +                cmd = 'ip macsec add {ifname} rx port 1 address'.format(**self.config) +                cmd += f' {peer_config["mac"]}' +                self._cmd(cmd) +                # Add the rx-key to the address +                cmd += f' sa 0 pn 1 on key 01 {peer_config["key"]}' +                self._cmd(cmd) +          # interface is always A/D down. It needs to be enabled explicitly          self.set_admin_state('down') diff --git a/python/vyos/nat.py b/python/vyos/nat.py index b6702f7e2..9cbc2b96e 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -56,10 +56,13 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):      elif 'translation' in rule_conf:          addr = dict_search_args(rule_conf, 'translation', 'address')          port = dict_search_args(rule_conf, 'translation', 'port') -        redirect_port = dict_search_args(rule_conf, 'translation', 'redirect', 'port') -        if redirect_port: -            translation_output = [f'redirect to {redirect_port}'] +        if 'redirect' in rule_conf['translation']: +            translation_output = [f'redirect'] +            redirect_port = dict_search_args(rule_conf, 'translation', 'redirect', 'port') +            if redirect_port: +                translation_output.append(f'to {redirect_port}')          else: +              translation_prefix = nat_type[:1]              translation_output = [f'{translation_prefix}nat'] 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/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index b32a6f524..30d1ad659 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -208,5 +208,77 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):          # Check for running process          self.assertTrue(process_named_running(PROCESS_NAME)) +    def test_macsec_static_keys(self): +        src_interface = 'eth0' +        interface = 'macsec5' +        cipher1 = 'gcm-aes-128' +        cipher2 = 'gcm-aes-256' +        tx_key_1 = '71a82a48eddfa12c08a19792ca20c4bb' +        tx_key_2 = 'dd487b2958e855ea35a5d43a5ecb3dcfbe7889ffcb877770252feb13b734478d' +        rx_key_1 = '0022d00f57e75241a230cdf7118dfcc5' +        rx_key_2 = 'b7d6d7ad075e02323fdeb845217b884d3f93ff36b2cdaf6b07eeb189b877245f' +        peer_mac = '00:11:22:33:44:55' +        self.cli_set(self._base_path + [interface]) + +         # Encrypt link +        self.cli_set(self._base_path + [interface, 'security', 'encrypt']) + +        # check validate() - source interface is mandatory +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'source-interface', src_interface]) + +        # check validate() - cipher is mandatory +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher1]) + +        # check validate() - only static or mka config is allowed +        self.cli_set(self._base_path + [interface, 'security', 'static']) +        self.cli_set(self._base_path + [interface, 'security', 'mka', 'cak', tx_key_1]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_delete(self._base_path + [interface, 'security', 'mka']) + +        # check validate() - tx-key required +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        # check validate() - tx-key length must match cipher +        self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_2]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_1]) + +        # check validate() - at least one peer must be defined +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        # check validate() - enabled peer must have both rx-key and MAC defined +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER']) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac', peer_mac]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_delete(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac']) +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'key', rx_key_1]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac', peer_mac]) + +        # check validate() - peer rx-key length must match cipher +        self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher2]) +        self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_2]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'key', rx_key_2]) + +        # final commit and verify +        self.cli_commit() +        self.assertIn(interface, interfaces()) +        self.assertEqual(cipher2, get_cipher(interface)) +        self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}')) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index e6eaedeff..31dfcef87 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -244,10 +244,17 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):          self.cli_set(dst_path + ['rule', '10', 'inbound-interface', ifname])          self.cli_set(dst_path + ['rule', '10', 'translation', 'redirect', 'port', redirected_port]) +        self.cli_set(dst_path + ['rule', '20', 'destination', 'address', dst_addr_1]) +        self.cli_set(dst_path + ['rule', '20', 'destination', 'port', dest_port]) +        self.cli_set(dst_path + ['rule', '20', 'protocol', protocol]) +        self.cli_set(dst_path + ['rule', '20', 'inbound-interface', ifname]) +        self.cli_set(dst_path + ['rule', '20', 'translation', 'redirect']) +          self.cli_commit()          nftables_search = [ -            [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'{protocol} dport {dest_port}', f'redirect to :{redirected_port}'] +            [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'{protocol} dport {dest_port}', f'redirect to :{redirected_port}'], +            [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'{protocol} dport {dest_port}', f'redirect']          ]          self.verify_nftables(nftables_search, 'ip vyos_nat') diff --git a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py index 7cc661688..cb5f84406 100755 --- a/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py +++ b/smoketest/scripts/cli/test_service_monitoring_zabbix-agent.py @@ -50,6 +50,7 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase):          port = '10050'          timeout = '5'          listen_ip = '0.0.0.0' +        hostname = 'r-vyos'          self.cli_set(base_path + ['directory', directory])          self.cli_set(base_path + ['limits', 'buffer-flush-interval', buffer_send]) @@ -61,6 +62,7 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase):          for server_active, server_config in servers_active.items():              self.cli_set(base_path + ['server-active', server_active, 'port', server_config['port']])          self.cli_set(base_path + ['timeout', timeout]) +        self.cli_set(base_path + ['host-name', hostname])          # commit changes          self.cli_commit() @@ -80,6 +82,7 @@ class TestZabbixAgent(VyOSUnitTestSHIM.TestCase):          self.assertIn(f'BufferSize={buffer_size}', config)          self.assertIn(f'Include={directory}/*.conf', config)          self.assertIn(f'Timeout={timeout}', config) +        self.assertIn(f'Hostname={hostname}', config)  if __name__ == '__main__': 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/firewall.py b/src/conf_mode/firewall.py index e946704b3..8ad3f27fc 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -351,39 +351,6 @@ def apply_sysfs(firewall):                  with open(path, 'w') as f:                      f.write(value) -def post_apply_trap(firewall): -    if 'first_install' in firewall: -        return None - -    if not process_named_running('snmpd'): -        return None - -    trap_username = os.getlogin() - -    for host, target_conf in firewall['trap_targets'].items(): -        community = target_conf['community'] if 'community' in target_conf else 'public' -        port = int(target_conf['port']) if 'port' in target_conf else 162 - -        base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} ' - -        for change_type, changes in firewall['trap_diff'].items(): -            for path_str, value in changes.items(): -                objects = [ -                    f'mgmtEventUser s "{trap_username}"', -                    f'mgmtEventSource i {snmp_event_source}', -                    f'mgmtEventType i {snmp_change_type[change_type]}' -                ] - -                if change_type == 'add': -                    objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"') -                elif change_type == 'delete': -                    objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"') -                elif change_type == 'change': -                    objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"') -                    objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"') - -                cmd(base_cmd + ' '.join(objects)) -  def apply(firewall):      install_result, output = rc_cmd(f'nft -f {nftables_conf}')      if install_result == 1: @@ -408,8 +375,6 @@ def apply(firewall):              print('Updating GeoIP. Please wait...')              geoip_update(firewall) -    post_apply_trap(firewall) -      return None  if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 3f86e2638..0a927ac88 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -43,6 +43,14 @@ airbag.enable()  # XXX: wpa_supplicant works on the source interface  wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' +# Constants +## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit +GCM_AES_128_LEN: int = 32 +GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!' +## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit +GCM_AES_256_LEN: int = 64 +GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!' +  def get_config(config=None):      """      Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -89,18 +97,54 @@ def verify(macsec):          raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))      if dict_search('security.encrypt', macsec) != None: -        if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: -            raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') +        # Check that only static or MKA config is present +        if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None): +            raise ConfigError('Only static or MKA can be used!') + +        # Logic to check static configuration +        if dict_search('security.static', macsec) != None: +            # tx-key must be defined +            if dict_search('security.static.key', macsec) == None: +                raise ConfigError('Static MACsec tx-key must be defined.') + +            tx_len = len(dict_search('security.static.key', macsec)) + +            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN: +                raise ConfigError(GCM_128_KEY_ERROR) + +            if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN: +                raise ConfigError(GCM_256_KEY_ERROR) + +            # Make sure at least one peer is defined +            if 'peer' not in macsec['security']['static']: +                raise ConfigError('Must have at least one peer defined for static MACsec') + +            # For every enabled peer, make sure a MAC and rx-key is defined +            for peer, peer_config in macsec['security']['static']['peer'].items(): +                if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config): +                    raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.') + +                # check rx-key length against cipher suite +                rx_len = len(peer_config['key']) + +                if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN: +                    raise ConfigError(GCM_128_KEY_ERROR) + +                if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN: +                    raise ConfigError(GCM_256_KEY_ERROR) + +        # Logic to check MKA configuration +        else: +            if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: +                raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') -        cak_len = len(dict_search('security.mka.cak', macsec)) +            cak_len = len(dict_search('security.mka.cak', macsec)) -        if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32: -            # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit -            raise ConfigError('gcm-aes-128 requires a 128bit long key!') +            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN: +                raise ConfigError(GCM_128_KEY_ERROR) -        elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64: -            # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit -            raise ConfigError('gcm-aes-128 requires a 256bit long key!') +            elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN: +                raise ConfigError(GCM_256_KEY_ERROR)      if 'source_interface' in macsec:          # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad @@ -115,7 +159,9 @@ def verify(macsec):  def generate(macsec): -    render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) +    # Only generate wpa_supplicant config if using MKA +    if dict_search('security.mka.cak', macsec): +        render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)      return None @@ -142,8 +188,10 @@ def apply(macsec):      i = MACsecIf(**macsec)      i.update(macsec) -    if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: -        call(f'systemctl reload-or-restart {systemd_service}') +    # Only reload/restart if using MKA +    if dict_search('security.mka.cak', macsec): +        if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: +            call(f'systemctl reload-or-restart {systemd_service}')      return None diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index f9d711b36..9da7fbe80 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -224,7 +224,7 @@ def verify(nat):              elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():                  Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') -            if not dict_search('translation.address', config) and not dict_search('translation.port', config) and not dict_search('translation.redirect.port', config): +            if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']:                  if 'exclude' not in config and 'backend' not in config['load_balance']:                      raise ConfigError(f'{err_msg} translation requires address and/or port') 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"] | 
