diff options
23 files changed, 531 insertions, 70 deletions
| diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index b62603e34..b0586e0bb 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -27,6 +27,7 @@          "https": ["service_https"],          "ipsec": ["vpn_ipsec"],          "openconnect": ["vpn_openconnect"], +        "rpki": ["protocols_rpki"],          "sstp": ["vpn_sstp"]      },      "vpn_l2tp": { diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index ed55db3f2..e9422b257 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -390,7 +390,7 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}  {%         endif %}  {%         if afi_config.ead_es_route_target.export is vyos_defined %}  {%             for route_target in afi_config.ead_es_route_target.export %} - ead-es-route-target export {{ route_target }} +  ead-es-route-target export {{ route_target }}  {%             endfor %}  {%         endif %}  {%         if afi_config.rt_auto_derive is vyos_defined %} @@ -402,6 +402,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}  {%         if afi_config.flooding.head_end_replication is vyos_defined %}    flooding head-end-replication  {%         endif %} +{%         if afi_config.mac_vrf.soo is vyos_defined %} +  mac-vrf soo {{ afi_config.mac_vrf.soo }} +{%         endif %}  {%         if afi_config.nexthop.vpn.export is vyos_defined %}    nexthop vpn export {{ afi_config.nexthop.vpn.export }}  {%         endif %} diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 662ba24ab..3219471b1 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -2,7 +2,7 @@  <interfaceDefinition>    <node name="firewall" owner="${vyos_conf_scripts_dir}/firewall.py">      <properties> -      <priority>199</priority> +      <priority>319</priority>        <help>Firewall</help>      </properties>      <children> @@ -94,7 +94,7 @@              <properties>                <help>Firewall domain-group</help>                <constraint> -                <regex>[a-zA-Z_][a-zA-Z0-9][\w\-\.]*</regex> +                <regex>[a-zA-Z_][a-zA-Z0-9]?[\w\-\.]*</regex>                </constraint>                <constraintErrorMessage>Name of domain-group can only contain alpha-numeric letters, hyphen, underscores and not start with numeric</constraintErrorMessage>              </properties> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index ea6e75bbd..ca67eaf3c 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -894,6 +894,30 @@              </leafNode>            </children>          </node> +        <node name="mac-vrf"> +          <properties> +            <help>EVPN MAC-VRF</help> +          </properties> +          <children> +            <leafNode name="soo"> +              <properties> +                <help>Site-of-Origin extended community</help> +                <valueHelp> +                  <format>ASN:NN</format> +                  <description>based on autonomous system number in format <0-65535:0-4294967295></description> +                </valueHelp> +                <valueHelp> +                  <format>IP:NN</format> +                  <description>Based on a router-id IP address in format <IP:0-65535></description> +                </valueHelp> +                <constraint> +                  <validator name="bgp-extended-community"/> +                </constraint> +                <constraintErrorMessage>Should be in form: ASN:NN or IPADDR:NN where ASN is autonomous system number</constraintErrorMessage> +              </properties> +            </leafNode> +          </children> +        </node>          <tagNode name="vni">            <properties>              <help>VXLAN Network Identifier</help> diff --git a/interface-definitions/include/pki/openssh-key.xml.i b/interface-definitions/include/pki/openssh-key.xml.i new file mode 100644 index 000000000..8f005d077 --- /dev/null +++ b/interface-definitions/include/pki/openssh-key.xml.i @@ -0,0 +1,14 @@ +<!-- include start from pki/openssh-key.xml.i --> +<leafNode name="key"> +  <properties> +    <help>OpenSSH key in PKI configuration</help> +    <completionHelp> +      <path>pki openssh</path> +    </completionHelp> +    <valueHelp> +      <format>txt</format> +      <description>Name of OpenSSH key in PKI configuration</description> +    </valueHelp> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i index bfef27b77..1f30368a3 100644 --- a/interface-definitions/include/version/dhcpv6-server-version.xml.i +++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i @@ -1,3 +1,3 @@  <!-- include start from include/version/dhcpv6-server-version.xml.i --> -<syntaxVersion component='dhcpv6-server' version='4'></syntaxVersion> +<syntaxVersion component='dhcpv6-server' version='5'></syntaxVersion>  <!-- include end --> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index 617bdd584..7a0b073b4 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -168,6 +168,45 @@              </properties>              <children>                #include <include/pki/cli-public-key-base64.xml.i> +              <leafNode name="type"> +                <properties> +                  <help>SSH public key type</help> +                  <completionHelp> +                    <list>ssh-rsa</list> +                  </completionHelp> +                  <valueHelp> +                    <format>ssh-rsa</format> +                    <description>Key pair based on RSA algorithm</description> +                  </valueHelp> +                  <constraint> +                    <regex>(ssh-rsa)</regex> +                  </constraint> +                </properties> +              </leafNode> +            </children> +          </node> +          <node name="private"> +            <properties> +              <help>Private key</help> +            </properties> +            <children> +              #include <include/pki/cli-private-key-base64.xml.i> +              #include <include/pki/password-protected.xml.i> +            </children> +          </node> +        </children> +      </tagNode> +      <tagNode name="openssh"> +        <properties> +          <help>OpenSSH public and private keys</help> +        </properties> +        <children> +          <node name="public"> +            <properties> +              <help>Public key</help> +            </properties> +            <children> +              #include <include/pki/cli-public-key-base64.xml.i>              </children>            </node>            <node name="private"> diff --git a/interface-definitions/protocols_rpki.xml.in b/interface-definitions/protocols_rpki.xml.in index 6c71f69f3..54d69eadb 100644 --- a/interface-definitions/protocols_rpki.xml.in +++ b/interface-definitions/protocols_rpki.xml.in @@ -47,22 +47,7 @@                    <help>RPKI SSH connection settings</help>                  </properties>                  <children> -                  <leafNode name="private-key-file"> -                    <properties> -                      <help>RPKI SSH private key file</help> -                      <constraint> -                        <validator name="file-path"/> -                      </constraint> -                    </properties> -                  </leafNode> -                  <leafNode name="public-key-file"> -                    <properties> -                      <help>RPKI SSH public key file path</help> -                      <constraint> -                        <validator name="file-path"/> -                      </constraint> -                    </properties> -                  </leafNode> +                  #include <include/pki/openssh-key.xml.i>                    #include <include/generic-username.xml.i>                  </children>                </node> diff --git a/interface-definitions/service_dhcpv6-server.xml.in b/interface-definitions/service_dhcpv6-server.xml.in index 28b97a64b..daca7b43f 100644 --- a/interface-definitions/service_dhcpv6-server.xml.in +++ b/interface-definitions/service_dhcpv6-server.xml.in @@ -97,6 +97,21 @@                  </properties>                  <children>                    #include <include/dhcp/option-v6.xml.i> +                  <leafNode name="interface"> +                    <properties> +                      <help>Optional interface for this subnet to accept requests from</help> +                      <completionHelp> +                        <script>${vyos_completion_dir}/list_interfaces</script> +                      </completionHelp> +                      <valueHelp> +                        <format>txt</format> +                        <description>Interface name</description> +                      </valueHelp> +                      <constraint> +                        #include <include/constraint/interface-name.xml.i> +                      </constraint> +                    </properties> +                  </leafNode>                    <tagNode name="range">                      <properties>                        <help>Parameters setting ranges for assigning IPv6 addresses</help> diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 0a629fcbc..894ac9e9a 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -210,6 +210,9 @@ def kea6_parse_subnet(subnet, config):      if 'option' in config:          out['option-data'] = kea6_parse_options(config['option']) +    if 'interface' in config: +        out['interface'] = config['interface'] +      if 'range' in config:          pools = []          for num, range_config in config['range'].items(): diff --git a/python/vyos/pki.py b/python/vyos/pki.py index 792e24b76..02dece471 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 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 @@ -20,7 +20,9 @@ import ipaddress  from cryptography import x509  from cryptography.exceptions import InvalidSignature  from cryptography.x509.extensions import ExtensionNotFound -from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, ExtensionOID +from cryptography.x509.oid import NameOID +from cryptography.x509.oid import ExtendedKeyUsageOID +from cryptography.x509.oid import ExtensionOID  from cryptography.hazmat.primitives import hashes  from cryptography.hazmat.primitives import serialization  from cryptography.hazmat.primitives.asymmetric import dh @@ -45,6 +47,8 @@ DH_BEGIN='-----BEGIN DH PARAMETERS-----\n'  DH_END='\n-----END DH PARAMETERS-----'  OVPN_BEGIN = '-----BEGIN OpenVPN Static key V{0}-----\n'  OVPN_END = '\n-----END OpenVPN Static key V{0}-----' +OPENSSH_KEY_BEGIN='-----BEGIN OPENSSH PRIVATE KEY-----\n' +OPENSSH_KEY_END='\n-----END OPENSSH PRIVATE KEY-----'  # Print functions @@ -229,6 +233,12 @@ def wrap_public_key(raw_data):  def wrap_private_key(raw_data, passphrase=None):      return (KEY_ENC_BEGIN if passphrase else KEY_BEGIN) + raw_data + (KEY_ENC_END if passphrase else KEY_END) +def wrap_openssh_public_key(raw_data, type): +    return f'{type} {raw_data}' + +def wrap_openssh_private_key(raw_data): +    return OPENSSH_KEY_BEGIN + raw_data +  OPENSSH_KEY_END +  def wrap_certificate_request(raw_data):      return CSR_BEGIN + raw_data + CSR_END @@ -245,7 +255,6 @@ def wrap_openvpn_key(raw_data, version='1'):      return OVPN_BEGIN.format(version) + raw_data + OVPN_END.format(version)  # Load functions -  def load_public_key(raw_data, wrap_tags=True):      if wrap_tags:          raw_data = wrap_public_key(raw_data) @@ -267,6 +276,21 @@ def load_private_key(raw_data, passphrase=None, wrap_tags=True):      except ValueError:          return False +def load_openssh_public_key(raw_data, type): +    try: +        return serialization.load_ssh_public_key(bytes(f'{type} {raw_data}', 'utf-8')) +    except ValueError: +        return False + +def load_openssh_private_key(raw_data, passphrase=None, wrap_tags=True): +    if wrap_tags: +        raw_data = wrap_openssh_private_key(raw_data) + +    try: +        return serialization.load_ssh_private_key(bytes(raw_data, 'utf-8'), password=passphrase) +    except ValueError: +        return False +  def load_certificate_request(raw_data, wrap_tags=True):      if wrap_tags:          raw_data = wrap_certificate_request(raw_data) @@ -429,4 +453,3 @@ def sort_ca_chain(ca_names, pki_node):      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/smoketest/bin/vyos-configtest-pki b/smoketest/bin/vyos-configtest-pki index 2f8af0e61..e753193e9 100755 --- a/smoketest/bin/vyos-configtest-pki +++ b/smoketest/bin/vyos-configtest-pki @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2022, VyOS maintainers and contributors +# Copyright (C) 2022-2024, 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 @@ -23,6 +23,7 @@ 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 +from vyos.utils.file import write_file  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'} @@ -41,6 +42,40 @@ 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' +rpki_ssh_priv_key = """ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAweDyflDFR4qyEwETbJkZ2ZZc+sJNiDTvYpwGsWIkju49lJSxHe1x +Kf8FhwfyMu40Snt1yDlRmmmz4CsbLgbuZGMPvXG11e34+C0pSVUvpF6aqRTeLl1pDRK7Rn +jgm3su+I8SRLQR4qbLG6VXWOFuVpwiqbExLaU0hFYTPNP+dArNpsWEEKsohk6pTXdhg3Vz +Wp3vCMjl2JTshDa3lD7p2xISSAReEY0fnfEAmQzH4Z6DIwwGdFuMWoQIg+oFBM9ARrO2/F +IjRsz6AecR/WeU72JEw4aJic1/cAJQA6PiQBHwkuo3Wll1tbpxeRZoB2NQG22ETyJLvhfT +aooNLT9HpQAAA8joU5dM6FOXTAAAAAdzc2gtcnNhAAABAQDB4PJ+UMVHirITARNsmRnZll +z6wk2INO9inAaxYiSO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV +7fj4LSlJVS+kXpqpFN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVh +M80/50Cs2mxYQQqyiGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfh +noMjDAZ0W4xahAiD6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS6j +daWXW1unF5FmgHY1AbbYRPIku+F9Nqig0tP0elAAAAAwEAAQAAAQACkDlUjzfUhtJs6uY5 +WNrdJB5NmHUS+HQzzxFNlhkapK6+wKqI1UNaRUtq6iF7J+gcFf7MK2nXS098BsXguWm8fQ +zPuemoDvHsQhiaJhyvpSqRUrvPTB/f8t/0AhQiKiJIWgfpTaIw53inAGwjujNNxNm2eafH +TThhCYxOkRT7rsT6bnSio6yeqPy5QHg7IKFztp5FXDUyiOS3aX3SvzQcDUkMXALdvzX50t +1XIk+X48Rgkq72dL4VpV2oMNDu3hM6FqBUplf9Mv3s51FNSma/cibCQoVufrIfoqYjkNTj +IpYFUcq4zZ0/KvgXgzSsy9VN/4TtbalrOuu7X/SHJbvhAAAAgGPFsXgONYQvXxCnK1dIue +ozgaZg1I/n522E2ZCOXBW4dYJVyNpppwRreDzuFzTDEe061MpNHfScjVBJCCulivFYWscL +6oaGsryDbFxO3QmB4I98UBqrds2yan9/JGc6EYe299yvaHy7Y64+NC0+fN8H2RAZ61T4w1 +0JrCaJRyvzAAAAgQDvBfuV1U7o9k/fbU+U7W2UYnWblpOZAMfi1XQP6IJJeyWs90PdTdXh ++l0eIQrCawIiRJytNfxMmbD4huwTf77fWiyCcPznmALQ7ex/yJ+W5Z0V4dPGF3h7o1uiS2 +36JhQ7mfcliCkhp/1PIklBIMPcCp0zl+s9wMv2hX7w1Pah9QAAAIEAz6YgU9Xute+J+dBw +oWxEQ+igR6KE55Um7O9AvSrqnCm9r7lSFsXC2ErYOxoDSJ3yIBEV0b4XAGn6tbbVIs3jS8 +BnLHxclAHQecOx1PGn7PKbnPW0oJRq/X9QCIEelKYvlykpayn7uZooTXqcDaPZxfPpmPdy +e8chVJvdygi7kPEAAAAMY3BvQExSMS53dWUzAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- +""" + +rpki_ssh_pub_key = """ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDB4PJ+UMVHirITARNsmRnZllz6wk2INO9inAaxYiSO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV7fj4LSlJVS+kXpqpFN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVhM80/50Cs2mxYQQqyiGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfhnoMjDAZ0W4xahAiD6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS6jdaWXW1unF5FmgHY1AbbYRPIku+F9Nqig0tP0el vyos@vyos +""" +  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) @@ -98,3 +133,7 @@ if __name__ == '__main__':      # OpenVPN Auth Key      system(f'openvpn --genkey secret {auth_key}') + +    write_file('/config/id_rsa', rpki_ssh_priv_key.strip()) +    write_file('/config/id_rsa.pub', rpki_ssh_pub_key.strip()) +    write_file('/config/known-hosts-file', '') diff --git a/smoketest/configs/rpki-only b/smoketest/configs/rpki-only new file mode 100644 index 000000000..0f89b9a1b --- /dev/null +++ b/smoketest/configs/rpki-only @@ -0,0 +1,71 @@ +interfaces { +    ethernet eth0 { +        duplex auto +        speed auto +        address 192.0.2.1/24 +    } +    loopback lo { +    } +} +protocols { +    rpki { +        cache 1.2.3.4 { +            port 3323 +            preference 10 +        } +        cache 5.6.7.8 { +            port 2222 +            preference 20 +            ssh { +                known-hosts-file "/config/known-hosts-file" +                private-key-file "/config/id_rsa" +                public-key-file "/config/id_rsa.pub" +                username vyos +            } +        } +    } +} +system { +    config-management { +        commit-revisions 200 +    } +    console { +        device ttyS0 { +            speed 115200 +        } +    } +    conntrack { +        modules { +            ftp +            h323 +            nfs +            pptp +            sip +            sqlnet +            tftp +        } +    } +    host-name vyos +    login { +        user vyos { +            authentication { +                encrypted-password $6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0 +                plaintext-password "" +            } +        } +    } +    syslog { +        global { +            facility all { +                level debug +            } +            facility protocols { +                level debug +            } +        } +    } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:container@1:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.5 diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 415f3436f..bc2848492 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -813,7 +813,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter')      def test_flow_offload(self): -        self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) +        self.cli_set(['interfaces', 'ethernet', 'eth0', 'vif', '10']) +        self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0.10'])          self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware'])          # QEMU virtual NIC does not support hw-tc-offload @@ -839,7 +840,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          nftables_search = [              ['flowtable VYOS_FLOWTABLE_smoketest'],              ['hook ingress priority filter'], -            ['devices = { eth0 }'], +            ['devices = { eth0.10 }'],              ['ct state { established, related }', 'meta l4proto { tcp, udp }', 'flow add @VYOS_FLOWTABLE_smoketest'],          ] diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index eadd8a004..1d68ae08b 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -751,7 +751,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):      def test_bgp_07_l2vpn_evpn(self):          vnis = ['10010', '10020', '10030'] -        neighbors = ['192.0.2.10', '192.0.2.20', '192.0.2.30'] +        soo = '1.2.3.4:10000'          evi_limit = '1000'          route_targets = ['1.1.1.1:100', '1.1.1.1:200', '1.1.1.1:300'] @@ -763,6 +763,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):          self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'default-originate', 'ipv6'])          self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'disable-ead-evi-rx'])          self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'disable-ead-evi-tx']) +        self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'mac-vrf', 'soo', soo])          for vni in vnis:              self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'vni', vni, 'advertise-default-gw'])              self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'vni', vni, 'advertise-svi-ip']) @@ -786,6 +787,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):          self.assertIn(f'  disable-ead-evi-rx', frrconfig)          self.assertIn(f'  disable-ead-evi-tx', frrconfig)          self.assertIn(f'  flooding disable', frrconfig) +        self.assertIn(f'  mac-vrf soo {soo}', frrconfig)          for vni in vnis:              vniconfig = self.getFRRconfig(f'  vni {vni}')              self.assertIn(f'vni {vni}', vniconfig) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index c52c0dd76..29f03a26a 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -14,20 +14,93 @@  # 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 os  import unittest  from base_vyostest_shim import VyOSUnitTestSHIM  from vyos.configsession import ConfigSessionError -from vyos.utils.process import cmd +from vyos.utils.file import read_file  from vyos.utils.process import process_named_running  base_path = ['protocols', 'rpki']  PROCESS_NAME = 'bgpd' -rpki_ssh_key = '/config/auth/id_rsa_rpki' -rpki_ssh_pub = f'{rpki_ssh_key}.pub' +rpki_key_name = 'rpki-smoketest' +rpki_key_type = 'ssh-rsa' + +rpki_ssh_key = """ +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAweDyflDFR4qyEwETbJkZ2ZZc+sJNiDTvYpwGsWIkju49lJSxHe1x +Kf8FhwfyMu40Snt1yDlRmmmz4CsbLgbuZGMPvXG11e34+C0pSVUvpF6aqRTeLl1pDRK7Rn +jgm3su+I8SRLQR4qbLG6VXWOFuVpwiqbExLaU0hFYTPNP+dArNpsWEEKsohk6pTXdhg3Vz +Wp3vCMjl2JTshDa3lD7p2xISSAReEY0fnfEAmQzH4Z6DIwwGdFuMWoQIg+oFBM9ARrO2/F +IjRsz6AecR/WeU72JEw4aJic1/cAJQA6PiQBHwkuo3Wll1tbpxeRZoB2NQG22ETyJLvhfT +aooNLT9HpQAAA8joU5dM6FOXTAAAAAdzc2gtcnNhAAABAQDB4PJ+UMVHirITARNsmRnZll +z6wk2INO9inAaxYiSO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV +7fj4LSlJVS+kXpqpFN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVh +M80/50Cs2mxYQQqyiGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfh +noMjDAZ0W4xahAiD6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS6j +daWXW1unF5FmgHY1AbbYRPIku+F9Nqig0tP0elAAAAAwEAAQAAAQACkDlUjzfUhtJs6uY5 +WNrdJB5NmHUS+HQzzxFNlhkapK6+wKqI1UNaRUtq6iF7J+gcFf7MK2nXS098BsXguWm8fQ +zPuemoDvHsQhiaJhyvpSqRUrvPTB/f8t/0AhQiKiJIWgfpTaIw53inAGwjujNNxNm2eafH +TThhCYxOkRT7rsT6bnSio6yeqPy5QHg7IKFztp5FXDUyiOS3aX3SvzQcDUkMXALdvzX50t +1XIk+X48Rgkq72dL4VpV2oMNDu3hM6FqBUplf9Mv3s51FNSma/cibCQoVufrIfoqYjkNTj +IpYFUcq4zZ0/KvgXgzSsy9VN/4TtbalrOuu7X/SHJbvhAAAAgGPFsXgONYQvXxCnK1dIue +ozgaZg1I/n522E2ZCOXBW4dYJVyNpppwRreDzuFzTDEe061MpNHfScjVBJCCulivFYWscL +6oaGsryDbFxO3QmB4I98UBqrds2yan9/JGc6EYe299yvaHy7Y64+NC0+fN8H2RAZ61T4w1 +0JrCaJRyvzAAAAgQDvBfuV1U7o9k/fbU+U7W2UYnWblpOZAMfi1XQP6IJJeyWs90PdTdXh ++l0eIQrCawIiRJytNfxMmbD4huwTf77fWiyCcPznmALQ7ex/yJ+W5Z0V4dPGF3h7o1uiS2 +36JhQ7mfcliCkhp/1PIklBIMPcCp0zl+s9wMv2hX7w1Pah9QAAAIEAz6YgU9Xute+J+dBw +oWxEQ+igR6KE55Um7O9AvSrqnCm9r7lSFsXC2ErYOxoDSJ3yIBEV0b4XAGn6tbbVIs3jS8 +BnLHxclAHQecOx1PGn7PKbnPW0oJRq/X9QCIEelKYvlykpayn7uZooTXqcDaPZxfPpmPdy +e8chVJvdygi7kPEAAAAMY3BvQExSMS53dWUzAQIDBAUGBw== +""" + +rpki_ssh_pub = """ +AAAAB3NzaC1yc2EAAAADAQABAAABAQDB4PJ+UMVHirITARNsmRnZllz6wk2INO9inAaxYi +SO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV7fj4LSlJVS+kXpqp +FN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVhM80/50Cs2mxYQQqy +iGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfhnoMjDAZ0W4xahAiD +6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS6jdaWXW1unF5FmgHY1 +AbbYRPIku+F9Nqig0tP0el +""" + +rpki_ssh_key_replacement = """ +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAtLPMwiGR3o6puPDbus9Yqoah9/7rv7i6ykykPmcEZ6ERnA0N6bl7 +LkQxnCuX270ukTTZOhROvQnvQYIZohCMz27Q16z7r+I755QXL0x8x4Gqhg/hQUY7UtX6ts +db8+pO7G1PL4r9zT6/KJAF/wv86DezJ3I6TMaA7MCikXfQWJisBvhgAXF1+7V9CWaroGgV +/hHzQJu1yd4cfsYoHyeDaZ+lwFw4egNItIy63fIGDxrnXaonJ1ODGQh7zWlpl/cwQR/KyJ +P8vvOZ9olQ6syZV+DAcAo4Fe59wW2Zj4bl8bdGcdiDn0grkafxwTcg9ynr9kwQ8b66oXY4 +hwB4vlPFPwAAA8jkGyX45Bsl+AAAAAdzc2gtcnNhAAABAQC0s8zCIZHejqm48Nu6z1iqhq +H3/uu/uLrKTKQ+ZwRnoRGcDQ3puXsuRDGcK5fbvS6RNNk6FE69Ce9BghmiEIzPbtDXrPuv +4jvnlBcvTHzHgaqGD+FBRjtS1fq2x1vz6k7sbU8viv3NPr8okAX/C/zoN7MncjpMxoDswK +KRd9BYmKwG+GABcXX7tX0JZqugaBX+EfNAm7XJ3hx+xigfJ4Npn6XAXDh6A0i0jLrd8gYP +GuddqicnU4MZCHvNaWmX9zBBH8rIk/y+85n2iVDqzJlX4MBwCjgV7n3BbZmPhuXxt0Zx2I +OfSCuRp/HBNyD3Kev2TBDxvrqhdjiHAHi+U8U/AAAAAwEAAQAAAQA99gkX5/rknXaE+9Hc +VIzKrC+NodOkgetKwszuuNRB1HD9WVyT8A3U5307V5dSuaPmFoEF8UCugWGQzNONRq+B0T +W7Po1u2dxAo/7vMQL4RfX60icjAroExWqakfFtycIWP8UPQFGWtxVFC12C/tFRrwe3Vuu2 +t7otdEBKMRM3zU0Hj88/5FIk/MDhththDCKTMe4+iwNKo30dyqSCckpTd2k5de9JYz8Aom +87jtQcyDdynaELSo9CsA8KRPlozZ4VSWTVLH+Cv2TZWPL7hy79YvvIfuF/Sd6PGkNwG1Vj +TAbq2Wx4uq+HmpNiz7W0LnbZtQJ7dzLA3FZlvQMC8fVBAAAAgQDWvImVZCyVWpoG+LnKY3 +joegjKRYKdgKRPCqGoIHiYsqCRxqSRW3jsuQCCvk4YO3/ZmqORiGktK+5r8R1QEtwg5qbi +N7GZD34m7USNuqG2G/4puEly8syMmR6VRRvEURFQrpv2wniXNSefvsDc+WDqTfXGUxr+FT +478wkzjwc/fAAAAIEA9uP0Ym3OC3cZ5FOvmu51lxo5lqPlUeE78axg2I4u/9Il8nOvSVuq +B9X5wAUyGAGcUjT3EZmRAtL2sQxc5T0Vw3bnxCjzukEbFM+DRtYy1hXSOoGTTwKoMWBpho +R3X5uRLUQL/22C4rd7tSJpjqnZXIH0B5z2fFh4vzu8/SrgCrUAAACBALtep4BcGJfjfhfF +ODzQe7Rk7tsaX8pfNv6bQu0sR5C9pDURFRf0fRC0oqgeTuzq/vHPyNLsUUgTCpKWiLFmvU +G9pelLT3XPPgzA+g0gycM0unuX8kkP3T5VQAM/7u0+h1CaJ8A6cCkzvDJxYdfio3WR60OP +ulHg7HCcyomFLaSjAAAADGNwb0BMUjEud3VlMwECAwQFBg== +""" + +rpki_ssh_pub_replacement = """ +AAAAB3NzaC1yc2EAAAADAQABAAABAQC0s8zCIZHejqm48Nu6z1iqhqH3/uu/uLrKTKQ+Zw +RnoRGcDQ3puXsuRDGcK5fbvS6RNNk6FE69Ce9BghmiEIzPbtDXrPuv4jvnlBcvTHzHgaqG +D+FBRjtS1fq2x1vz6k7sbU8viv3NPr8okAX/C/zoN7MncjpMxoDswKKRd9BYmKwG+GABcX +X7tX0JZqugaBX+EfNAm7XJ3hx+xigfJ4Npn6XAXDh6A0i0jLrd8gYPGuddqicnU4MZCHvN +aWmX9zBBH8rIk/y+85n2iVDqzJlX4MBwCjgV7n3BbZmPhuXxt0Zx2IOfSCuRp/HBNyD3Ke +v2TBDxvrqhdjiHAHi+U8U/ +"""  class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):      @classmethod @@ -44,10 +117,6 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):          self.cli_delete(base_path)          self.cli_commit() -        # Nothing RPKI specific should be left over in the config -        # frrconfig = self.getFRRconfig('rpki') -        # self.assertNotIn('rpki', frrconfig) -          # check process health and continuity          self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) @@ -107,28 +176,52 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):              },          } -        self.cli_set(base_path + ['polling-period', polling]) +        self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n','')]) +        self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n','')]) +        self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type]) -        for peer, peer_config in cache.items(): -            self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']]) -            self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']]) -            self.cli_set(base_path + ['cache', peer, 'ssh', 'username', peer_config['username']]) -            self.cli_set(base_path + ['cache', peer, 'ssh', 'public-key-file', rpki_ssh_pub]) -            self.cli_set(base_path + ['cache', peer, 'ssh', 'private-key-file', rpki_ssh_key]) +        for cache_name, cache_config in cache.items(): +            self.cli_set(base_path + ['cache', cache_name, 'port', cache_config['port']]) +            self.cli_set(base_path + ['cache', cache_name, 'preference', cache_config['preference']]) +            self.cli_set(base_path + ['cache', cache_name, 'ssh', 'username', cache_config['username']]) +            self.cli_set(base_path + ['cache', cache_name, 'ssh', 'key', rpki_key_name])          # commit changes          self.cli_commit()          # Verify FRR configuration          frrconfig = self.getFRRconfig('rpki') -        self.assertIn(f'rpki polling_period {polling}', frrconfig) +        for cache_name, cache_config in cache.items(): +            port = cache_config['port'] +            preference = cache_config['preference'] +            username = cache_config['username'] +            self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) + +            # Verify content of SSH keys +            tmp = read_file(f'/run/frr/id_rpki_{cache_name}') +            self.assertIn(rpki_ssh_key.replace('\n',''), tmp) +            tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub') +            self.assertIn(rpki_ssh_pub.replace('\n',''), tmp) + +        # Change OpenSSH key and verify it was properly written to filesystem +        self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key_replacement.replace('\n','')]) +        self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub_replacement.replace('\n','')]) +        # commit changes +        self.cli_commit() -        for peer, peer_config in cache.items(): -            port = peer_config['port'] -            preference = peer_config['preference'] -            username = peer_config['username'] -            self.assertIn(f'rpki cache {peer} {port} {username} {rpki_ssh_key} {rpki_ssh_pub} preference {preference}', frrconfig) +        for cache_name, cache_config in cache.items(): +            port = cache_config['port'] +            preference = cache_config['preference'] +            username = cache_config['username'] +            self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig) +            # Verify content of SSH keys +            tmp = read_file(f'/run/frr/id_rpki_{cache_name}') +            self.assertIn(rpki_ssh_key_replacement.replace('\n',''), tmp) +            tmp = read_file(f'/run/frr/id_rpki_{cache_name}.pub') +            self.assertIn(rpki_ssh_pub_replacement.replace('\n',''), tmp) + +        self.cli_delete(['pki', 'openssh'])      def test_rpki_verify_preference(self):          cache = { @@ -150,10 +243,5 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):          with self.assertRaises(ConfigSessionError):              self.cli_commit() -  if __name__ == '__main__': -    # Create OpenSSH keypair used in RPKI tests -    if not os.path.isfile(rpki_ssh_key): -        cmd(f'ssh-keygen -t rsa -f {rpki_ssh_key} -N ""') -      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 5a831b8a0..6ecf6c1cf 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -102,6 +102,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):          pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]          self.cli_set(base_path + ['preference', preference]) +        self.cli_set(pool + ['interface', interface])          self.cli_set(pool + ['subnet-id', '1'])          # we use the first subnet IP address as default gateway          self.cli_set(pool + ['lease-time', 'default', lease_time]) @@ -146,6 +147,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):          self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)          self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) +        self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'interface', interface)          self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1)          self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time))          self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time)) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 4be40e99e..3ab6ac5c3 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -24,11 +24,12 @@ from vyos.config import config_dict_merge  from vyos.configdep import set_dependents  from vyos.configdep import call_dependents  from vyos.configdict import node_changed -from vyos.configdiff import Diff  from vyos.defaults import directories  from vyos.pki import is_ca_certificate  from vyos.pki import load_certificate  from vyos.pki import load_public_key +from vyos.pki import load_openssh_public_key +from vyos.pki import load_openssh_private_key  from vyos.pki import load_private_key  from vyos.pki import load_crl  from vyos.pki import load_dh_parameters @@ -64,6 +65,10 @@ sync_search = [          'path': ['interfaces', 'sstpc'],      },      { +        'keys': ['key'], +        'path': ['protocols', 'rpki', 'cache'], +    }, +    {          'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'],          'path': ['vpn', 'ipsec'],      }, @@ -86,7 +91,8 @@ sync_translate = {      'remote_key': 'key_pair',      'shared_secret_key': 'openvpn',      'auth_key': 'openvpn', -    'crypt_key': 'openvpn' +    'crypt_key': 'openvpn', +    'key': 'openssh',  }  def certbot_delete(certificate): @@ -150,6 +156,11 @@ def get_config(config=None):          if 'changed' not in pki: pki.update({'changed':{}})          pki['changed'].update({'key_pair' : tmp}) +    tmp = node_changed(conf, base + ['openssh'], recursive=True) +    if tmp: +        if 'changed' not in pki: pki.update({'changed':{}}) +        pki['changed'].update({'openssh' : tmp}) +      tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True)      if tmp:          if 'changed' not in pki: pki.update({'changed':{}}) @@ -241,6 +252,17 @@ def is_valid_private_key(raw_data, protected=False):          return True      return load_private_key(raw_data, passphrase=None, wrap_tags=True) +def is_valid_openssh_public_key(raw_data, type): +    # If it loads correctly we're good, or return False +    return load_openssh_public_key(raw_data, type) + +def is_valid_openssh_private_key(raw_data, protected=False): +    # If it loads correctly we're good, or return False +    # With encrypted private keys, we always return true as we cannot ask for password to verify +    if protected: +        return True +    return load_openssh_private_key(raw_data, passphrase=None, wrap_tags=True) +  def is_valid_crl(raw_data):      # If it loads correctly we're good, or return False      return load_crl(raw_data, wrap_tags=True) @@ -322,6 +344,20 @@ def verify(pki):                  if not is_valid_private_key(private['key'], protected):                      raise ConfigError(f'Invalid private key on key-pair "{name}"') +    if 'openssh' in pki: +        for name, key_conf in pki['openssh'].items(): +            if 'public' in key_conf and 'key' in key_conf['public']: +                if 'type' not in key_conf['public']: +                    raise ConfigError(f'Must define OpenSSH public key type for "{name}"') +                if not is_valid_openssh_public_key(key_conf['public']['key'], key_conf['public']['type']): +                    raise ConfigError(f'Invalid OpenSSH public key "{name}"') + +            if 'private' in key_conf and 'key' in key_conf['private']: +                private = key_conf['private'] +                protected = 'password_protected' in private +                if not is_valid_openssh_private_key(private['key'], protected): +                    raise ConfigError(f'Invalid OpenSSH private key "{name}"') +      if 'x509' in pki:          if 'default' in pki['x509']:              default_values = pki['x509']['default'] diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 0fc14e868..a59ecf3e4 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 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 @@ -16,16 +16,22 @@  import os +from glob import glob  from sys import exit  from vyos.config import Config +from vyos.pki import wrap_openssh_public_key +from vyos.pki import wrap_openssh_private_key  from vyos.template import render_to_string -from vyos.utils.dict import dict_search +from vyos.utils.dict import dict_search_args +from vyos.utils.file import write_file  from vyos import ConfigError  from vyos import frr  from vyos import airbag  airbag.enable() +rpki_ssh_key_base = '/run/frr/id_rpki' +  def get_config(config=None):      if config:          conf = config @@ -33,7 +39,8 @@ def get_config(config=None):          conf = Config()      base = ['protocols', 'rpki'] -    rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) +    rpki = conf.get_config_dict(base, key_mangling=('-', '_'), +                                get_first_key=True, with_pki=True)      # Bail out early if configuration tree does not exist      if not conf.exists(base):          rpki.update({'deleted' : ''}) @@ -63,22 +70,40 @@ def verify(rpki):                  preferences.append(preference)              if 'ssh' in peer_config: -                files = ['private_key_file', 'public_key_file'] -                for file in files: -                    if file not in peer_config['ssh']: -                        raise ConfigError('RPKI+SSH requires username and public/private ' \ -                                          'key file to be defined!') +                if 'username' not in peer_config['ssh']: +                    raise ConfigError('RPKI+SSH requires username to be defined!') + +                if 'key' not in peer_config['ssh'] or 'openssh' not in rpki['pki']: +                    raise ConfigError('RPKI+SSH requires key to be defined!') -                    filename = peer_config['ssh'][file] -                    if not os.path.exists(filename): -                        raise ConfigError(f'RPKI SSH {file.replace("-","-")} "{filename}" does not exist!') +                if peer_config['ssh']['key'] not in rpki['pki']['openssh']: +                    raise ConfigError('RPKI+SSH key not found on PKI subsystem!')      return None  def generate(rpki): +    for key in glob(f'{rpki_ssh_key_base}*'): +        os.unlink(key) +      if not rpki:          return + +    if 'cache' in rpki: +        for cache, cache_config in rpki['cache'].items(): +            if 'ssh' in cache_config: +                key_name = cache_config['ssh']['key'] +                public_key_data = dict_search_args(rpki['pki'], 'openssh', key_name, 'public', 'key') +                public_key_type = dict_search_args(rpki['pki'], 'openssh', key_name, 'public', 'type') +                private_key_data = dict_search_args(rpki['pki'], 'openssh', key_name, 'private', 'key') + +                cache_config['ssh']['public_key_file'] = f'{rpki_ssh_key_base}_{cache}.pub' +                cache_config['ssh']['private_key_file'] = f'{rpki_ssh_key_base}_{cache}' + +                write_file(cache_config['ssh']['public_key_file'], wrap_openssh_public_key(public_key_data, public_key_type)) +                write_file(cache_config['ssh']['private_key_file'], wrap_openssh_private_key(private_key_data)) +      rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki) +      return None  def apply(rpki): diff --git a/src/init/vyos-router b/src/init/vyos-router index 2b4fac5ef..eac3e7e47 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -1,5 +1,5 @@  #!/bin/bash -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 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 @@ -449,7 +449,7 @@ start ()      run_postconfig_scripts      tmp=$(${vyos_libexec_dir}/read-saved-value.py --path "protocols rpki cache") -    if [ ! -z $tmp ]; then +    if [[ ! -z "$tmp" ]]; then          vtysh -c "rpki start"      fi  } diff --git a/src/migration-scripts/dhcpv6-server/4-to-5 b/src/migration-scripts/dhcpv6-server/4-to-5 new file mode 100755 index 000000000..e808edbe0 --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/4-to-5 @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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/>. + +# T5993: Check if subnet is locally accessible and assign interface to subnet + +import sys +from ipaddress import ip_network +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['service', 'dhcpv6-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +def find_subnet_interface(subnet): +    subnet_net = ip_network(subnet) + +    for iftype in config.list_nodes(['interfaces']): +        for ifname in config.list_nodes(['interfaces', iftype]): +            if_base = ['interfaces', iftype, ifname] + +            if config.exists(if_base + ['address']): +                for addr in config.return_values(if_base + ['address']): +                    if ip_network(addr, strict=False) == subnet_net: +                        return ifname + +    return False + +for network in config.list_nodes(base): +    if not config.exists(base + [network, 'subnet']): +        continue + +    for subnet in config.list_nodes(base + [network, 'subnet']): +        subnet_interface = find_subnet_interface(subnet) + +        if subnet_interface: +            config.set(base + [network, 'subnet', subnet, 'interface'], value=subnet_interface) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/migration-scripts/ipsec/6-to-7 b/src/migration-scripts/ipsec/6-to-7 index 71fbbe8a1..f8b6de560 100755 --- a/src/migration-scripts/ipsec/6-to-7 +++ b/src/migration-scripts/ipsec/6-to-7 @@ -63,7 +63,7 @@ if config.exists(ipsec_site_base):          changes_made = True          peer_x509_base = ipsec_site_base + [peer, 'authentication', 'x509'] -        pki_name = 'peer_' + peer.replace(".", "-") +        pki_name = 'peer_' + peer.replace(".", "-").replace("@", "")          if config.exists(peer_x509_base + ['cert-file']):              cert_file = config.return_value(peer_x509_base + ['cert-file']) diff --git a/src/migration-scripts/rpki/1-to-2 b/src/migration-scripts/rpki/1-to-2 index 559440bba..50d4a3dfc 100755 --- a/src/migration-scripts/rpki/1-to-2 +++ b/src/migration-scripts/rpki/1-to-2 @@ -19,7 +19,11 @@  from sys import exit  from sys import argv +  from vyos.configtree import ConfigTree +from vyos.pki import OPENSSH_KEY_BEGIN +from vyos.pki import OPENSSH_KEY_END +from vyos.utils.file import read_file  if len(argv) < 2:      print("Must specify file name!") @@ -43,6 +47,24 @@ if config.exists(base + ['cache']):          if config.exists(ssh_node + ['known-hosts-file']):              config.delete(ssh_node + ['known-hosts-file']) +        if config.exists(base + ['cache', cache, 'ssh']): +            private_key_node = base + ['cache', cache, 'ssh', 'private-key-file'] +            private_key_file = config.return_value(private_key_node) +            private_key = read_file(private_key_file).replace(OPENSSH_KEY_BEGIN, '').replace(OPENSSH_KEY_END, '').replace('\n','') + +            public_key_node = base + ['cache', cache, 'ssh', 'public-key-file'] +            public_key_file = config.return_value(public_key_node) +            public_key = read_file(public_key_file).split() + +            config.set(['pki', 'openssh', f'rpki-{cache}', 'private', 'key'], value=private_key) +            config.set(['pki', 'openssh', f'rpki-{cache}', 'public', 'key'], value=public_key[1]) +            config.set(['pki', 'openssh', f'rpki-{cache}', 'public', 'type'], value=public_key[0]) +            config.set_tag(['pki', 'openssh']) +            config.set(ssh_node + ['key'], value=f'rpki-{cache}') + +            config.delete(private_key_node) +            config.delete(public_key_node) +  try:      with open(file_name, 'w') as f:          f.write(config.to_string()) | 
