diff options
25 files changed, 398 insertions, 79 deletions
diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 3c0d47b27..6df12db2c 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -128,10 +128,16 @@ bind={{ radius_source_address }}  {%     if radius_dynamic_author %}  dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }}  {%     endif %} -{%     if radius_shaper_attr %} + +{%     if radius_shaper_enable %}  [shaper]  verbose=1 +{%         if radius_shaper_attr %}  attr={{ radius_shaper_attr }} +{%         endif %} +{%         if radius_shaper_multiplier %} +rate-multiplier={{ radius_shaper_multiplier }} +{%         endif %}  {%         if radius_shaper_vendor %}  vendor={{ radius_shaper_vendor }}  {%         endif %} diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install index 3739763b9..406fef4be 100644 --- a/debian/vyos-1x-smoketest.install +++ b/debian/vyos-1x-smoketest.install @@ -1,4 +1,5 @@  usr/bin/vyos-smoketest  usr/bin/vyos-configtest +usr/bin/vyos-configtest-pki  usr/libexec/vyos/tests/smoke  usr/libexec/vyos/tests/config diff --git a/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i b/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i index b9dd59bea..5057ed9ae 100644 --- a/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i +++ b/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i @@ -1,4 +1,4 @@ -<!-- include start from address-ipv4-ipv6-dhcp.xml.i --> +<!-- include start from interface/address-ipv4-ipv6-dhcp.xml.i -->  <leafNode name="address">    <properties>      <help>IP address</help> diff --git a/interface-definitions/include/interface/address-ipv4-ipv6.xml.i b/interface-definitions/include/interface/address-ipv4-ipv6.xml.i index 519622050..d689da5aa 100644 --- a/interface-definitions/include/interface/address-ipv4-ipv6.xml.i +++ b/interface-definitions/include/interface/address-ipv4-ipv6.xml.i @@ -1,4 +1,4 @@ -<!-- include start from address-ipv4-ipv6.xml.i --> +<!-- include start from interface/address-ipv4-ipv6.xml.i -->  <leafNode name="address">    <properties>      <help>IP address</help> diff --git a/interface-definitions/include/pki/ca-certificate-multi.xml.i b/interface-definitions/include/pki/ca-certificate-multi.xml.i new file mode 100644 index 000000000..646131b54 --- /dev/null +++ b/interface-definitions/include/pki/ca-certificate-multi.xml.i @@ -0,0 +1,15 @@ +<!-- include start from pki/ca-certificate-multi.xml.i --> +<leafNode name="ca-certificate"> +  <properties> +    <help>Certificate Authority chain in PKI configuration</help> +    <completionHelp> +      <path>pki ca</path> +    </completionHelp> +    <valueHelp> +      <format>txt</format> +      <description>Name of CA in PKI configuration</description> +    </valueHelp> +    <multi/> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index f1cbf8468..6cbd91ff4 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -741,7 +741,7 @@                  </properties>                </leafNode>                #include <include/pki/certificate.xml.i> -              #include <include/pki/ca-certificate.xml.i> +              #include <include/pki/ca-certificate-multi.xml.i>                <leafNode name="dh-params">                  <properties>                    <help>Diffie Hellman parameters (server only)</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 664914baa..9674cfc0e 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -4,7 +4,7 @@      <children>        <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py">          <properties> -          <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> +          <help>Point-to-Point Protocol over Ethernet (PPPoE) Interface</help>            <priority>322</priority>            <constraint>              <regex>pppoe[0-9]+</regex> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 6b62f4c61..53e6445fa 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -4,7 +4,7 @@      <children>        <tagNode name="pseudo-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-pseudo-ethernet.py">          <properties> -          <help>Pseudo Ethernet</help> +          <help>Pseudo Ethernet Interface (Macvlan)</help>            <priority>321</priority>            <constraint>              <regex>peth[0-9]+</regex> diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in index b471c3b92..aa83a04b2 100644 --- a/interface-definitions/interfaces-vti.xml.in +++ b/interface-definitions/interfaces-vti.xml.in @@ -4,7 +4,7 @@      <children>        <tagNode name="vti" owner="${vyos_conf_scripts_dir}/interfaces-vti.py">          <properties> -          <help>Virtual Tunnel interface</help> +          <help>Virtual Tunnel Interface (XFRM)</help>            <priority>381</priority>            <constraint>              <regex>vti[0-9]+</regex> @@ -16,19 +16,7 @@            </valueHelp>          </properties>          <children> -          <leafNode name="address"> -            <properties> -              <help>IP address</help> -              <valueHelp> -                <format>ipv4net</format> -                <description>IPv4 address and prefix length</description> -              </valueHelp> -              <constraint> -                <validator name="ipv4-host"/> -              </constraint> -              <multi/> -            </properties> -          </leafNode> +          #include <include/interface/address-ipv4-ipv6.xml.i>            #include <include/interface/description.xml.i>            #include <include/interface/disable.xml.i>            #include <include/interface/ipv4-options.xml.i> diff --git a/interface-definitions/service-ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index e222467b1..cd3aa3638 100644 --- a/interface-definitions/service-ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -213,6 +213,11 @@                    </tagNode>                  </children>                </tagNode> +              <node name="radius"> +                <children> +                  #include <include/accel-ppp/radius-additions-rate-limit.xml.i> +                </children> +              </node>                #include <include/radius-server-ipv4.xml.i>                #include <include/accel-ppp/radius-additions.xml.i>              </children> diff --git a/op-mode-definitions/show-ip.xml.in b/op-mode-definitions/show-ip.xml.in index d21c38ccc..d342ac192 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -7,12 +7,6 @@            <help>Show IPv4 networking information</help>          </properties>          <children> -          <node name="external"> -            <properties> -              <help>Show IPv4 external address</help> -            </properties> -            <command>${vyos_op_scripts_dir}/show_ip_external.sh</command> -          </node>            <node name="neighbors">              <properties>                <help>Show IPv4 neighbor (ARP) table</help> diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index c50cd5ce9..dc99d365a 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 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 @@ -53,3 +53,7 @@ class VTIIf(Interface):          self._cmd(cmd.format(**self.config))          self.set_interface('admin_state', 'down') + +    def get_mac(self): +        """ Get a synthetic MAC address. """ +        return self.get_mac_synthetic() diff --git a/python/vyos/pki.py b/python/vyos/pki.py index fd91fc9bf..cd15e3878 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -332,6 +332,54 @@ def verify_certificate(cert, ca_cert):      except InvalidSignature:          return False +def verify_crl(crl, ca_cert): +    # Verify CRL was signed by specified CA +    if ca_cert.subject != crl.issuer: +        return False + +    ca_public_key = ca_cert.public_key() +    try: +        if isinstance(ca_public_key, rsa.RSAPublicKeyWithSerialization): +            ca_public_key.verify( +                crl.signature, +                crl.tbs_certlist_bytes, +                padding=padding.PKCS1v15(), +                algorithm=crl.signature_hash_algorithm) +        elif isinstance(ca_public_key, dsa.DSAPublicKeyWithSerialization): +            ca_public_key.verify( +                crl.signature, +                crl.tbs_certlist_bytes, +                algorithm=crl.signature_hash_algorithm) +        elif isinstance(ca_public_key, ec.EllipticCurvePublicKeyWithSerialization): +            ca_public_key.verify( +                crl.signature, +                crl.tbs_certlist_bytes, +                signature_algorithm=ec.ECDSA(crl.signature_hash_algorithm)) +        else: +            return False # We cannot verify it +        return True +    except InvalidSignature: +        return False + +def verify_ca_chain(sorted_names, pki_node): +    if len(sorted_names) == 1: # Single cert, no chain +        return True + +    for name in sorted_names: +        cert = load_certificate(pki_node[name]['certificate']) +        verified = False +        for ca_name in sorted_names: +            if name == ca_name: +                continue +            ca_cert = load_certificate(pki_node[ca_name]['certificate']) +            if verify_certificate(cert, ca_cert): +                verified = True +                break +        if not verified and name != sorted_names[-1]: +            # Only permit top-most certificate to fail verify (e.g. signed by public CA not explicitly in chain) +            return False +    return True +  # Certificate chain  def find_parent(cert, ca_certs): @@ -357,3 +405,16 @@ def find_chain(cert, ca_certs):              chain.append(parent)      return chain + +def sort_ca_chain(ca_names, pki_node): +    def ca_cmp(ca_name1, ca_name2, pki_node): +        cert1 = load_certificate(pki_node[ca_name1]['certificate']) +        cert2 = load_certificate(pki_node[ca_name2]['certificate']) + +        if verify_certificate(cert1, cert2): # cert1 is child of cert2 +            return -1 +        return 1 + +    from functools import cmp_to_key +    return sorted(ca_names, key=cmp_to_key(lambda cert1, cert2: ca_cmp(cert1, cert2, pki_node))) + diff --git a/python/vyos/util.py b/python/vyos/util.py index 0d62fbfe9..bee5d7aec 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -197,7 +197,7 @@ def read_file(fname, defaultonfailure=None):              return defaultonfailure          raise e -def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None): +def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None, append=False):      """      Write content of data to given fname, should defaultonfailure be not None,      it is returned on failure to read. @@ -212,7 +212,7 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=N      try:          """ Write a file to string """          bytes = 0 -        with open(fname, 'w') as f: +        with open(fname, 'w' if not append else 'a') as f:              bytes = f.write(data)          chown(fname, user, group)          chmod(fname, mode) diff --git a/smoketest/bin/vyos-configtest-pki b/smoketest/bin/vyos-configtest-pki new file mode 100755 index 000000000..2f8af0e61 --- /dev/null +++ b/smoketest/bin/vyos-configtest-pki @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022, 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +from os import system +from vyos.pki import create_private_key +from vyos.pki import create_certificate_request +from vyos.pki import create_certificate +from vyos.pki import create_certificate_revocation_list +from vyos.pki import create_dh_parameters +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key + +subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos'} +ca_subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos CA'} +subca_subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos SubCA'} + +ca_cert = '/config/auth/ovpn_test_ca.pem' +ca_key = '/config/auth/ovpn_test_ca.key' +ca_cert_chain = '/config/auth/ovpn_test_chain.pem' +ca_crl = '/config/auth/ovpn_test_ca.crl' +subca_cert = '/config/auth/ovpn_test_subca.pem' +subca_csr = '/tmp/subca.csr' +subca_key = '/config/auth/ovpn_test_subca.key' +ssl_cert = '/config/auth/ovpn_test_server.pem' +ssl_key  = '/config/auth/ovpn_test_server.key' +dh_pem   = '/config/auth/ovpn_test_dh.pem' +s2s_key  = '/config/auth/ovpn_test_site2site.key' +auth_key = '/config/auth/ovpn_test_tls_auth.key' + +def create_cert(subject, cert_path, key_path, sign_by=None, sign_by_key=None, ca=False, sub_ca=False): +    priv_key = create_private_key('rsa', 2048) +    cert_req = create_certificate_request(subject, priv_key) +    cert = create_certificate( +        cert_req, +        sign_by if sign_by else cert_req, +        sign_by_key if sign_by_key else priv_key, +        is_ca=ca, is_sub_ca=sub_ca) + +    with open(cert_path, 'w') as f: +        f.write(encode_certificate(cert)) + +    with open(key_path, 'w') as f: +        f.write(encode_private_key(priv_key)) + +    return cert, priv_key + +def create_empty_crl(crl_path, sign_by, sign_by_key): +    crl = create_certificate_revocation_list(sign_by, sign_by_key, [1]) + +    with open(crl_path, 'w') as f: +        f.write(encode_certificate(crl)) + +    return crl + +if __name__ == '__main__': +    # Create Root CA +    ca_cert_obj, ca_key_obj = create_cert(ca_subject, ca_cert, ca_key, ca=True) + +    # Create Empty CRL +    create_empty_crl(ca_crl, ca_cert_obj, ca_key_obj) + +    # Create Intermediate CA +    subca_cert_obj, subca_key_obj = create_cert( +        subca_subject, subca_cert, subca_key, +        sign_by=ca_cert_obj, sign_by_key=ca_key_obj, +        ca=True, sub_ca=True) + +    # Create Chain +    with open(ca_cert_chain, 'w') as f: +        f.write(encode_certificate(subca_cert_obj) + "\n") +        f.write(encode_certificate(ca_cert_obj) + "\n") + +    # Create Server Cert +    create_cert(subject, ssl_cert, ssl_key, sign_by=subca_cert_obj, sign_by_key=subca_key_obj) + +    # Create DH params +    dh_params = create_dh_parameters() + +    with open(dh_pem, 'w') as f: +        f.write(encode_dh_parameters(dh_params)) + +    # OpenVPN S2S Key +    system(f'openvpn --genkey secret {s2s_key}') + +    # OpenVPN Auth Key +    system(f'openvpn --genkey secret {auth_key}') diff --git a/smoketest/configs/dialup-router-medium-vpn b/smoketest/configs/dialup-router-medium-vpn index 63d955738..56722d222 100644 --- a/smoketest/configs/dialup-router-medium-vpn +++ b/smoketest/configs/dialup-router-medium-vpn @@ -120,8 +120,9 @@ interfaces {          persistent-tunnel          remote-host 192.0.2.10          tls { -            ca-cert-file /config/auth/ovpn_test_ca.pem +            ca-cert-file /config/auth/ovpn_test_chain.pem              cert-file /config/auth/ovpn_test_server.pem +            crl-file /config/auth/ovpn_test_ca.crl              key-file /config/auth/ovpn_test_server.key              auth-file /config/auth/ovpn_test_tls_auth.key          } @@ -152,7 +153,7 @@ interfaces {          remote-host 01.foo.com          remote-port 1194          tls { -            ca-cert-file /config/auth/ovpn_test_ca.pem +            ca-cert-file /config/auth/ovpn_test_chain.pem              auth-file /config/auth/ovpn_test_tls_auth.key          }      } diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 816ba6dcd..8acf52243 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -232,6 +232,9 @@ class BasicInterfaceTest:              for interface in self._interfaces:                  base = self._base_path + [interface] +                # just set the interface base without any option - some interfaces +                # (VTI) do not require any option to be brought up +                self.cli_set(base)                  for option in self._options.get(interface, []):                      self.cli_set(base + option.split()) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index ca0ead9e8..26d3a23c9 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -86,9 +86,83 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):              # validate member interface configuration              for member in self._members:                  tmp = get_interface_config(member) +                # verify member is assigned to the bridge +                self.assertEqual(interface, tmp['master'])                  # Isolated must be enabled as configured above                  self.assertTrue(tmp['linkinfo']['info_slave_data']['isolated']) +    def test_igmp_querier_snooping(self): +        # Add member interfaces to bridge +        for interface in self._interfaces: +            base = self._base_path + [interface] + +            # assign members to bridge interface +            for member in self._members: +                base_member = base + ['member', 'interface', member] +                self.cli_set(base_member) + +        # commit config +        self.cli_commit() + +        for interface in self._interfaces: +            # Verify IGMP default configuration +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') +            self.assertEqual(tmp, '0') +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') +            self.assertEqual(tmp, '0') + +        # Enable IGMP snooping +        for interface in self._interfaces: +            base = self._base_path + [interface] +            self.cli_set(base + ['igmp', 'snooping']) + +        # commit config +        self.cli_commit() + +        for interface in self._interfaces: +            # Verify IGMP snooping configuration +            # Verify IGMP default configuration +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') +            self.assertEqual(tmp, '1') +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') +            self.assertEqual(tmp, '0') + +        # Enable IGMP querieer +        for interface in self._interfaces: +            base = self._base_path + [interface] +            self.cli_set(base + ['igmp', 'querier']) + +        # commit config +        self.cli_commit() + +        for interface in self._interfaces: +            # Verify IGMP snooping & querier configuration +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') +            self.assertEqual(tmp, '1') +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') +            self.assertEqual(tmp, '1') + +        # Disable IGMP +        for interface in self._interfaces: +            base = self._base_path + [interface] +            self.cli_delete(base + ['igmp']) + +        # commit config +        self.cli_commit() + +        for interface in self._interfaces: +            # Verify IGMP snooping & querier configuration +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') +            self.assertEqual(tmp, '0') +            tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') +            self.assertEqual(tmp, '0') + +            # validate member interface configuration +            for member in self._members: +                tmp = get_interface_config(member) +                # verify member is assigned to the bridge +                self.assertEqual(interface, tmp['master']) +      def test_add_remove_bridge_member(self):          # Add member interfaces to bridge and set STP cost/priority diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py new file mode 100755 index 000000000..9cbf104f0 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_interfaces_test import BasicInterfaceTest + +class VTIInterfaceTest(BasicInterfaceTest.TestCase): +    @classmethod +    def setUpClass(cls): +        cls._test_ip = True +        cls._test_ipv6 = True +        cls._test_mtu = True +        cls._base_path = ['interfaces', 'vti'] +        cls._interfaces = ['vti10', 'vti20', 'vti30'] + +        # call base-classes classmethod +        super(VTIInterfaceTest, cls).setUpClass() + +if __name__ == '__main__': +    unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 4750ca3e8..280a62b9a 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -39,6 +39,8 @@ from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import VTunIf  from vyos.pki import load_dh_parameters  from vyos.pki import load_private_key +from vyos.pki import sort_ca_chain +from vyos.pki import verify_ca_chain  from vyos.pki import wrap_certificate  from vyos.pki import wrap_crl  from vyos.pki import wrap_dh_parameters @@ -148,8 +150,14 @@ def verify_pki(openvpn):          if 'ca_certificate' not in tls:              raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') -        if tls['ca_certificate'] not in pki['ca']: -            raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') +        for ca_name in tls['ca_certificate']: +            if ca_name not in pki['ca']: +                raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + +        if len(tls['ca_certificate']) > 1: +            sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) +            if not verify_ca_chain(sorted_chain, pki['ca']): +                raise ConfigError(f'CA certificates are not a valid chain')          if mode != 'client' and 'auth_key' not in tls:              if 'certificate' not in tls: @@ -516,21 +524,28 @@ def generate_pki_files(openvpn):      if tls:          if 'ca_certificate' in tls: -            cert_name = tls['ca_certificate'] -            pki_ca = pki['ca'][cert_name] +            cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') +            crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') -            if 'certificate' in pki_ca: -                cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') -                write_file(cert_path, wrap_certificate(pki_ca['certificate']), -                           user=user, group=group, mode=0o600) +            if os.path.exists(cert_path): +                os.unlink(cert_path) + +            if os.path.exists(crl_path): +                os.unlink(crl_path) + +            for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']): +                pki_ca = pki['ca'][cert_name] + +                if 'certificate' in pki_ca: +                    write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n", +                               user=user, group=group, mode=0o600, append=True) -            if 'crl' in pki_ca: -                for crl in pki_ca['crl']: -                    crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') -                    write_file(crl_path, wrap_crl(crl), user=user, group=group, -                               mode=0o600) +                if 'crl' in pki_ca: +                    for crl in pki_ca['crl']: +                        write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group, +                                   mode=0o600, append=True) -                openvpn['tls']['crl'] = True +                    openvpn['tls']['crl'] = True          if 'certificate' in tls:              cert_name = tls['certificate'] diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index cd46cbcb4..01f14df61 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,6 +19,7 @@ import os  from sys import exit  from sys import argv +from vyos.base import Warning  from vyos.config import Config  from vyos.configdict import dict_merge  from vyos.configverify import verify_prefix_list @@ -198,6 +199,9 @@ def verify(bgp):                      if 'source_interface' in peer_config['interface']:                          raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"') +            if 'address_family' not in peer_config: +                Warning(f'BGP neighbor "{peer}" requires address-family!') +              for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',                          'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',                          'l2vpn_evpn']: diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 559d1bcd5..61f484129 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -53,6 +53,8 @@ default_config_data = {      'radius_nas_ip': '',      'radius_source_address': '',      'radius_shaper_attr': '', +    'radius_shaper_enable': False, +    'radius_shaper_multiplier': '',      'radius_shaper_vendor': '',      'radius_dynamic_author': '',      'thread_cnt': get_half_cpus() @@ -196,6 +198,18 @@ def get_config(config=None):      if conf.exists(['nas-ip-address']):          ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) +    if conf.exists(['rate-limit', 'attribute']): +        ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + +    if conf.exists(['rate-limit', 'enable']): +        ipoe['radius_shaper_enable'] = True + +    if conf.exists(['rate-limit', 'multiplier']): +        ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + +    if conf.exists(['rate-limit', 'vendor']): +        ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) +      if conf.exists(['source-address']):          ipoe['radius_source_address'] = conf.return_value(['source-address']) diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 93ce9215f..4095f2a3e 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -20,6 +20,7 @@  import os  import sys  from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN  from vyos.pki import load_certificate  from vyos.pki import load_crl  from vyos.pki import load_dh_parameters @@ -27,6 +28,7 @@ from vyos.pki import load_private_key  from vyos.pki import encode_certificate  from vyos.pki import encode_dh_parameters  from vyos.pki import encode_private_key +from vyos.pki import verify_crl  from vyos.util import run  def wrapped_pem_to_config_value(pem): @@ -129,6 +131,8 @@ if config.exists(base):              config.delete(base + [interface, 'tls', 'crypt-file']) +        ca_certs = {} +          if config.exists(x509_base + ['ca-cert-file']):              if not config.exists(pki_base + ['ca']):                  config.set(pki_base + ['ca']) @@ -136,20 +140,27 @@ if config.exists(base):              cert_file = config.return_value(x509_base + ['ca-cert-file'])              cert_path = os.path.join(AUTH_DIR, cert_file) -            cert = None              if os.path.isfile(cert_path):                  if not os.access(cert_path, os.R_OK):                      run(f'sudo chmod 644 {cert_path}')                  with open(cert_path, 'r') as f: -                    cert_data = f.read() -                    cert = load_certificate(cert_data, wrap_tags=False) - -            if cert: -                cert_pem = encode_certificate(cert) -                config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) -                config.set(x509_base + ['ca-certificate'], value=pki_name) +                    certs_str = f.read() +                    certs_data = certs_str.split(CERT_BEGIN) +                    index = 1 +                    for cert_data in certs_data[1:]: +                        cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + +                        if cert: +                            ca_certs[f'{pki_name}_{index}'] = cert +                            cert_pem = encode_certificate(cert) +                            config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) +                            config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) +                        else: +                            print(f'Failed to migrate CA certificate on openvpn interface {interface}') + +                        index += 1              else:                  print(f'Failed to migrate CA certificate on openvpn interface {interface}') @@ -163,6 +174,7 @@ if config.exists(base):              crl_file = config.return_value(x509_base + ['crl-file'])              crl_path = os.path.join(AUTH_DIR, crl_file)              crl = None +            crl_ca_name = None              if os.path.isfile(crl_path):                  if not os.access(crl_path, os.R_OK): @@ -172,9 +184,14 @@ if config.exists(base):                      crl_data = f.read()                      crl = load_crl(crl_data, wrap_tags=False) -            if crl: +                    for ca_name, ca_cert in ca_certs.items(): +                        if verify_crl(crl, ca_cert): +                            crl_ca_name = ca_name +                            break + +            if crl and crl_ca_name:                  crl_pem = encode_certificate(crl) -                config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) +                config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))              else:                  print(f'Failed to migrate CRL on openvpn interface {interface}') diff --git a/src/op_mode/show_ip_external.sh b/src/op_mode/show_ip_external.sh deleted file mode 100755 index 275d05278..000000000 --- a/src/op_mode/show_ip_external.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# Detect an external IP address -# Use random services for checking - - -array=( -    ipinfo.io/ip -    ifconfig.me -    ipecho.net/plain -    icanhazip.com -    v4.ident.me -    checkip.amazonaws.com -) - -size=${#array[@]} -index=$(($RANDOM % $size)) - -curl --silent ${array[$index]} | tr -d "[:space:]" && echo diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py index 691f674b2..1c85380bc 100755 --- a/src/system/vyos-event-handler.py +++ b/src/system/vyos-event-handler.py @@ -15,14 +15,16 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import argparse -import select -import re  import json +import re +import select +from copy import deepcopy  from os import getpid, environ  from pathlib import Path  from signal import signal, SIGTERM, SIGINT -from systemd import journal  from sys import exit +from systemd import journal +  from vyos.util import run, dict_search  # Identify this script @@ -54,7 +56,7 @@ class Analyzer:                  script_arguments = dict_search('script.arguments', event_config)                  script = f'{script} {script_arguments}'              # Prepare environment -            environment = environ +            environment = deepcopy(environ)              # Check for additional environment options              if dict_search('script.environment', event_config):                  for env_variable, env_value in dict_search( @@ -69,7 +71,7 @@ class Analyzer:                      'pattern_raw':                          pattern_raw,                      'syslog_id': -                        dict_search('filter.syslog_identifier', event_config), +                        dict_search('filter.syslog-identifier', event_config),                      'pattern_script': {                          'path': script,                          'environment': environment  | 
