diff options
28 files changed, 586 insertions, 94 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 419d0bee1..e9422b257 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -76,7 +76,7 @@   neighbor {{ neighbor }} password {{ config.password }}  {% endif %}  {% if config.path_attribute.discard is vyos_defined %} - neighbor {{ neighbor }} path-attribute discard {{ config.path_attribute.discard }} + neighbor {{ neighbor }} path-attribute discard {{ config.path_attribute.discard | join(' ') }}  {% endif %}  {% if config.path_attribute.treat_as_withdraw is vyos_defined %}   neighbor {{ neighbor }} path-attribute treat-as-withdraw {{ config.path_attribute.treat_as_withdraw }} diff --git a/data/templates/frr/static_routes_macro.j2 b/data/templates/frr/static_routes_macro.j2 index 8afd4a68a..cf8046968 100644 --- a/data/templates/frr/static_routes_macro.j2 +++ b/data/templates/frr/static_routes_macro.j2 @@ -13,17 +13,17 @@  {% endif %}  {% if prefix_config.interface is vyos_defined %}  {%     for interface, interface_config in prefix_config.interface.items() if interface_config.disable is not defined %} -{{ ip_ipv6 }} route {{ prefix }} {{ interface }} {{ interface_config.distance if interface_config.distance is vyos_defined }} {{ 'nexthop-vrf ' ~ interface_config.vrf if interface_config.vrf is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }} +{{ ip_ipv6 }} route {{ prefix }} {{ interface }} {{ interface_config.distance if interface_config.distance is vyos_defined }} {{ 'nexthop-vrf ' ~ interface_config.vrf if interface_config.vrf is vyos_defined }} {{ 'segments ' ~ interface_config.segments if interface_config.segments is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }}  {%     endfor %}  {% endif %}  {% if prefix_config.next_hop is vyos_defined and prefix_config.next_hop is not none %}  {%     for next_hop, next_hop_config in prefix_config.next_hop.items() if next_hop_config.disable is not defined %} -{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ next_hop_config.interface if next_hop_config.interface is vyos_defined }} {{ next_hop_config.distance if next_hop_config.distance is vyos_defined }} {{ 'nexthop-vrf ' ~ next_hop_config.vrf if next_hop_config.vrf is vyos_defined }} {{ 'bfd profile ' ~ next_hop_config.bfd.profile if next_hop_config.bfd.profile is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }}  +{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ next_hop_config.interface if next_hop_config.interface is vyos_defined }} {{ next_hop_config.distance if next_hop_config.distance is vyos_defined }} {{ 'nexthop-vrf ' ~ next_hop_config.vrf if next_hop_config.vrf is vyos_defined }} {{ 'bfd profile ' ~ next_hop_config.bfd.profile if next_hop_config.bfd.profile is vyos_defined }} {{ 'segments ' ~ next_hop_config.segments if next_hop_config.segments is vyos_defined }} {{ 'table ' ~ table if table is vyos_defined }}  {%         if next_hop_config.bfd.multi_hop.source is vyos_defined %}  {%             for source, source_config in next_hop_config.bfd.multi_hop.source.items() %}  {{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} bfd multi-hop source {{ source }} profile {{ source_config.profile }}  {%             endfor %} -{%         endif %}  +{%         endif %}  {%     endfor %}  {% endif %}  {% endmacro %} diff --git a/interface-definitions/include/bgp/neighbor-path-attribute.xml.i b/interface-definitions/include/bgp/neighbor-path-attribute.xml.i index 30568d8c6..399a6bcdd 100644 --- a/interface-definitions/include/bgp/neighbor-path-attribute.xml.i +++ b/interface-definitions/include/bgp/neighbor-path-attribute.xml.i @@ -14,6 +14,7 @@          <constraint>            <validator name="numeric" argument="--range 1-255"/>          </constraint> +        <multi/>        </properties>      </leafNode>      <leafNode name="treat-as-withdraw"> 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/static/static-route-reject.xml.i b/interface-definitions/include/static/static-route-reject.xml.i index 81d4f9afd..ef713ac85 100644 --- a/interface-definitions/include/static/static-route-reject.xml.i +++ b/interface-definitions/include/static/static-route-reject.xml.i @@ -9,4 +9,3 @@    </children>  </node>  <!-- include end --> - diff --git a/interface-definitions/include/static/static-route-segments.xml.i b/interface-definitions/include/static/static-route-segments.xml.i new file mode 100644 index 000000000..2068b1ab4 --- /dev/null +++ b/interface-definitions/include/static/static-route-segments.xml.i @@ -0,0 +1,14 @@ +<!-- include start from static/static-route-segments.xml.i --> +<leafNode name="segments"> +    <properties> +      <help>SRv6 segments</help> +      <valueHelp> +        <format>txt</format> +        <description>Segs (SIDs)</description> +      </valueHelp> +      <constraint> +        <validator name="ipv6-srv6-segments"/> +      </constraint> +    </properties> +  </leafNode> +  <!-- include end --> diff --git a/interface-definitions/include/static/static-route6.xml.i b/interface-definitions/include/static/static-route6.xml.i index a83cc230b..4468c8025 100644 --- a/interface-definitions/include/static/static-route6.xml.i +++ b/interface-definitions/include/static/static-route6.xml.i @@ -31,6 +31,7 @@        <children>          #include <include/generic-disable-node.xml.i>          #include <include/static/static-route-distance.xml.i> +        #include <include/static/static-route-segments.xml.i>          #include <include/static/static-route-vrf.xml.i>        </children>      </tagNode> @@ -47,13 +48,13 @@        </properties>        <children>          #include <include/generic-disable-node.xml.i> +        #include <include/static/static-route-bfd.xml.i>          #include <include/static/static-route-distance.xml.i>          #include <include/static/static-route-interface.xml.i> +        #include <include/static/static-route-segments.xml.i>          #include <include/static/static-route-vrf.xml.i> -        #include <include/static/static-route-bfd.xml.i>        </children>      </tagNode>    </children>  </tagNode>  <!-- 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 720bebec3..7365c1f02 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -212,6 +212,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 a7dd11145..415f3436f 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -652,8 +652,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          nftables_search = [              ['ct state { established, related }', 'accept'],              ['ct state invalid', 'reject'], -            ['ct state new', 'ct status dnat', 'accept'], -            ['ct state { established, new }', 'ct status snat', 'accept'], +            ['ct state new', 'ct status == dnat', 'accept'], +            ['ct state { established, new }', 'ct status == snat', 'accept'],              ['ct state related', 'ct helper { "ftp", "pptp" }', 'accept'],              ['drop', f'comment "{name} default-action drop"'],              ['jump VYOS_STATE_POLICY'], diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index db49db98f..1d68ae08b 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -60,7 +60,7 @@ neighbor_config = {          'route_map_out'    : route_map_out,          'no_send_comm_ext' : '',          'addpath_all'      : '', -        'p_attr_discard'   : '123', +        'p_attr_discard'   : ['10', '20', '30', '40', '50'],          },      '192.0.2.2' : {          'bfd_profile'      : bfd_profile, @@ -137,7 +137,7 @@ peer_group_config = {          'cap_over'         : '',          'ttl_security'     : '5',          'disable_conn_chk' : '', -        'p_attr_discard'   : '250', +        'p_attr_discard'   : ['100', '150', '200'],          },      'bar' : {          'remote_as'        : '111', @@ -284,7 +284,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):          if 'addpath_all' in peer_config:              self.assertIn(f' neighbor {peer} addpath-tx-all-paths', frrconfig)          if 'p_attr_discard' in peer_config: -            self.assertIn(f' neighbor {peer} path-attribute discard {peer_config["p_attr_discard"]}', frrconfig) +            tmp = ' '.join(peer_config["p_attr_discard"]) +            self.assertIn(f' neighbor {peer} path-attribute discard {tmp}', frrconfig)          if 'p_attr_taw' in peer_config:              self.assertIn(f' neighbor {peer} path-attribute treat-as-withdraw {peer_config["p_attr_taw"]}', frrconfig)          if 'addpath_per_as' in peer_config: @@ -460,7 +461,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):              if 'update_src' in peer_config:                  self.cli_set(base_path + ['neighbor', peer, 'update-source', peer_config["update_src"]])              if 'p_attr_discard' in peer_config: -                self.cli_set(base_path + ['neighbor', peer, 'path-attribute', 'discard', peer_config["p_attr_discard"]]) +                for attribute in peer_config['p_attr_discard']: +                    self.cli_set(base_path + ['neighbor', peer, 'path-attribute', 'discard', attribute])              if 'p_attr_taw' in peer_config:                  self.cli_set(base_path + ['neighbor', peer, 'path-attribute', 'treat-as-withdraw', peer_config["p_attr_taw"]])              if 'route_map_in' in peer_config: @@ -584,7 +586,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):              if 'disable_conn_chk' in config:                  self.cli_set(base_path + ['peer-group', peer_group, 'disable-connected-check'])              if 'p_attr_discard' in config: -                self.cli_set(base_path + ['peer-group', peer_group, 'path-attribute', 'discard', config["p_attr_discard"]]) +                for attribute in config['p_attr_discard']: +                    self.cli_set(base_path + ['peer-group', peer_group, 'path-attribute', 'discard', attribute])              if 'p_attr_taw' in config:                  self.cli_set(base_path + ['peer-group', peer_group, 'path-attribute', 'treat-as-withdraw', config["p_attr_taw"]]) 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_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index abf1080ab..c5cf2aab6 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021-2023 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 @@ -55,13 +55,13 @@ routes = {          'blackhole' : { 'distance' : '90' },      },      '100.64.0.0/16' : { -        'blackhole' : { }, +        'blackhole' : {},      },      '100.65.0.0/16' : {          'reject'    : { 'distance' : '10', 'tag' : '200' },      },      '100.66.0.0/16' : { -        'blackhole' : { }, +        'blackhole' : {},          'reject'    : { 'distance' : '10', 'tag' : '200' },      },      '2001:db8:100::/40' : { @@ -88,8 +88,28 @@ routes = {      '2001:db8:300::/40' : {          'reject'    : { 'distance' : '250', 'tag' : '500' },      }, +    '2001:db8:400::/40' : { +        'next_hop' : { +            '2001:db8::400' : { 'segments' : '2001:db8:aaaa::400/2002::400/2003::400/2004::400' }, +        }, +    }, +    '2001:db8:500::/40' : { +        'next_hop' : { +            '2001:db8::500' : { 'segments' : '2001:db8:aaaa::500/2002::500/2003::500/2004::500' }, +        }, +    }, +    '2001:db8:600::/40' : { +        'interface' : { +            'eth0'  : { 'segments' : '2001:db8:aaaa::600/2002::600' }, +        }, +    }, +    '2001:db8:700::/40' : { +        'interface' : { +            'eth1'  : { 'segments' : '2001:db8:aaaa::700' }, +        }, +    },      '2001:db8::/32' : { -        'blackhole' : { 'distance' : '200', 'tag' : '600' }, +        'blackhole' : { 'distance' : '200', 'tag' : '600' }      },  } @@ -108,18 +128,14 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):          super(TestProtocolsStatic, cls).tearDownClass()      def tearDown(self): -        for route, route_config in routes.items(): -            route_type = 'route' -            if is_ipv6(route): -                route_type = 'route6' -            self.cli_delete(base_path + [route_type, route]) - -        for table in tables: -            self.cli_delete(base_path + ['table', table]) - -        tmp = self.getFRRconfig('', end='') +        self.cli_delete(base_path)          self.cli_commit() +        v4route = self.getFRRconfig('ip route', end='') +        self.assertFalse(v4route) +        v6route = self.getFRRconfig('ipv6 route', end='') +        self.assertFalse(v6route) +      def test_01_static(self):          bfd_profile = 'vyos-test'          for route, route_config in routes.items(): @@ -142,7 +158,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                          self.cli_set(base + ['next-hop', next_hop, 'bfd', 'profile', bfd_profile ])                      if 'bfd_source' in next_hop_config:                          self.cli_set(base + ['next-hop', next_hop, 'bfd', 'multi-hop', 'source', next_hop_config['bfd_source'], 'profile', bfd_profile]) - +                    if 'segments' in next_hop_config: +                        self.cli_set(base + ['next-hop', next_hop, 'segments', next_hop_config['segments']])              if 'interface' in route_config:                  for interface, interface_config in route_config['interface'].items(): @@ -153,6 +170,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                          self.cli_set(base + ['interface', interface, 'distance', interface_config['distance']])                      if 'vrf' in interface_config:                          self.cli_set(base + ['interface', interface, 'vrf', interface_config['vrf']]) +                    if 'segments' in interface_config: +                        self.cli_set(base + ['interface', interface, 'segments', interface_config['segments']])              if 'blackhole' in route_config:                  self.cli_set(base + ['blackhole']) @@ -200,6 +219,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                          tmp += ' bfd profile ' + bfd_profile                      if 'bfd_source' in next_hop_config:                          tmp += ' bfd multi-hop source ' + next_hop_config['bfd_source'] + ' profile ' + bfd_profile +                    if 'segments' in next_hop_config: +                        tmp += ' segments ' + next_hop_config['segments']                      if 'disable' in next_hop_config:                          self.assertNotIn(tmp, frrconfig) @@ -215,6 +236,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                          tmp += ' ' + interface_config['distance']                      if 'vrf' in interface_config:                          tmp += ' nexthop-vrf ' + interface_config['vrf'] +                    if 'segments' in interface_config: +                        tmp += ' segments ' + interface_config['segments']                      if 'disable' in interface_config:                          self.assertNotIn(tmp, frrconfig) @@ -369,7 +392,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                              self.cli_set(route_base_path + ['next-hop', next_hop, 'interface', next_hop_config['interface']])                          if 'vrf' in next_hop_config:                              self.cli_set(route_base_path + ['next-hop', next_hop, 'vrf', next_hop_config['vrf']]) - +                        if 'segments' in next_hop_config: +                            self.cli_set(route_base_path + ['next-hop', next_hop, 'segments', next_hop_config['segments']])                  if 'interface' in route_config:                      for interface, interface_config in route_config['interface'].items(): @@ -380,6 +404,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                              self.cli_set(route_base_path + ['interface', interface, 'distance', interface_config['distance']])                          if 'vrf' in interface_config:                              self.cli_set(route_base_path + ['interface', interface, 'vrf', interface_config['vrf']]) +                        if 'segments' in interface_config: +                            self.cli_set(route_base_path + ['interface', interface, 'segments', interface_config['segments']])                  if 'blackhole' in route_config:                      self.cli_set(route_base_path + ['blackhole']) @@ -417,6 +443,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                              tmp += ' ' + next_hop_config['distance']                          if 'vrf' in next_hop_config:                              tmp += ' nexthop-vrf ' + next_hop_config['vrf'] +                        if 'segments' in next_hop_config: +                            tmp += ' segments ' + next_hop_config['segments']                          if 'disable' in next_hop_config:                              self.assertNotIn(tmp, frrconfig) @@ -432,6 +460,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):                              tmp += ' ' + interface_config['distance']                          if 'vrf' in interface_config:                              tmp += ' nexthop-vrf ' + interface_config['vrf'] +                        if 'segments' in interface_config: +                            tmp += ' segments ' + interface_config['segments']                          if 'disable' in interface_config:                              self.assertNotIn(tmp, frrconfig) 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()) diff --git a/src/validators/ipv6-srv6-segments b/src/validators/ipv6-srv6-segments new file mode 100755 index 000000000..e72a4f90f --- /dev/null +++ b/src/validators/ipv6-srv6-segments @@ -0,0 +1,13 @@ +#!/bin/sh +segments="$1" +export IFS="/" + +for ipv6addr in $segments; do +    ipaddrcheck --is-ipv6-single $ipv6addr +    if [ $? -gt 0 ]; then +        echo "Error: $1 is not a valid IPv6 address" +        exit 1 +    fi +done +exit 0 +  | 
